Skip to content

Commit 4ebf898

Browse files
[SuperEditor][Markdown] - Parse images with captions immediately above them (Resolves #3041) (#3042)
- BREAKING: Force all TextNode 'textAlign' properties to use Strings instead of randomly switching between Strings and TextAligns.
1 parent 4951da9 commit 4ebf898

12 files changed

Lines changed: 212 additions & 107 deletions

File tree

super_editor/lib/src/default_editor/tables/table_markdown.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class MarkdownTableComponentBuilder implements ComponentBuilder {
4949
nodeId: cell.id,
5050
createdAt: cell.metadata[NodeMetadata.createdAt],
5151
text: cell.text,
52-
textAlign: cell.getMetadataValue(TextNodeMetadata.textAlign) ?? TextAlign.left,
52+
textAlign: _maybeParseTextAlign(cell.getMetadataValue(TextNodeMetadata.textAlign)) ?? TextAlign.left,
5353
textStyleBuilder: noStyleBuilder,
5454
padding: const EdgeInsets.all(8.0),
5555
// ^ Default padding, can be overridden through the stylesheet.
@@ -62,6 +62,16 @@ class MarkdownTableComponentBuilder implements ComponentBuilder {
6262
);
6363
}
6464

65+
TextAlign? _maybeParseTextAlign(String? textAlign) {
66+
return switch (textAlign) {
67+
'left' => TextAlign.left,
68+
'center' => TextAlign.center,
69+
'right' => TextAlign.right,
70+
'justify' => TextAlign.justify,
71+
_ => null,
72+
};
73+
}
74+
6575
@override
6676
Widget? createComponent(
6777
SingleColumnDocumentComponentContext componentContext, SingleColumnLayoutComponentViewModel componentViewModel) {

super_editor/lib/src/default_editor/text.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import 'package:super_editor/src/core/edit_context.dart';
1515
import 'package:super_editor/src/core/editor.dart';
1616
import 'package:super_editor/src/core/styles.dart';
1717
import 'package:super_editor/src/default_editor/attributions.dart';
18-
import 'package:super_editor/src/default_editor/text_ai.dart';
1918
import 'package:super_editor/src/default_editor/text/custom_underlines.dart';
19+
import 'package:super_editor/src/default_editor/text_ai.dart';
2020
import 'package:super_editor/src/infrastructure/_logging.dart';
2121
import 'package:super_editor/src/infrastructure/attributed_text_styles.dart';
2222
import 'package:super_editor/src/infrastructure/composable_text.dart';
@@ -38,7 +38,18 @@ class TextNode extends DocumentNode {
3838
required this.id,
3939
required this.text,
4040
super.metadata,
41-
});
41+
}) {
42+
if (metadata['textAlign'] == null) {
43+
initAddToMetadata({
44+
'textAlign': 'left',
45+
});
46+
} else {
47+
assert(
48+
metadata['textAlign'] is String?,
49+
'Expected "textAlign" metadata to be of type String, but got: ${metadata['textAlign']}',
50+
);
51+
}
52+
}
4253

4354
@override
4455
final String id;

super_editor/lib/src/infrastructure/serialization/html/html_table.dart

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import 'package:flutter/material.dart';
21
import 'package:super_editor/src/core/document.dart';
32
import 'package:super_editor/src/default_editor/selection_upstream_downstream.dart';
43
import 'package:super_editor/src/default_editor/tables/table_block.dart';
@@ -111,15 +110,15 @@ extension TableBlockNodeToHtml on TableBlockNode {
111110

112111
String _getTextAlignStyle(TextNode cell) {
113112
final textAlign = cell.getMetadataValue('textAlign');
114-
if (textAlign == TextAlign.left) {
115-
// Default alignment is left, so we don't need to specify it.
116-
return '';
117-
}
118113
final textAlignString = switch (textAlign) {
119-
TextAlign.center => 'center',
120-
TextAlign.right => 'right',
114+
'center' => 'center',
115+
'right' => 'right',
121116
_ => 'left',
122117
};
118+
if (textAlign == 'left') {
119+
// Default alignment is left, so we don't need to specify it.
120+
return '';
121+
}
123122
return textAlign != null ? ' style="text-align:$textAlignString"' : '';
124123
}
125124
}

super_editor/lib/src/infrastructure/serialization/markdown/document_to_markdown_serializer.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import 'dart:ui';
22

33
import 'package:attributed_text/attributed_text.dart';
44
import 'package:flutter/foundation.dart';
5-
import 'package:markdown/markdown.dart' hide Document;
65
import 'package:super_editor/src/core/document.dart';
76
import 'package:super_editor/src/core/document_selection.dart';
87
import 'package:super_editor/src/default_editor/attributions.dart';
@@ -14,7 +13,6 @@ import 'package:super_editor/src/default_editor/selection_upstream_downstream.da
1413
import 'package:super_editor/src/default_editor/tables/table_block.dart';
1514
import 'package:super_editor/src/default_editor/tasks.dart';
1615
import 'package:super_editor/src/default_editor/text.dart';
17-
1816
import 'package:super_editor/src/infrastructure/serialization/markdown/super_editor_syntax.dart';
1917

2018
/// Serializes the given [doc] to Markdown text.

super_editor/lib/src/infrastructure/serialization/markdown/markdown_to_document_parsing.dart

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -213,13 +213,25 @@ class _MarkdownToDocument implements md.NodeVisitor {
213213
if (blockImage != null) {
214214
_addImage(blockImage);
215215
} else {
216-
final attributedText = parseInlineMarkdown(
217-
element.textContent,
218-
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
219-
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
220-
encodeHtml: encodeHtml,
221-
);
222-
_addParagraph(attributedText, element.attributes);
216+
final captionAndImage = _maybeParseImageWithCaption(element.textContent);
217+
if (captionAndImage != null) {
218+
final captionText = parseInlineMarkdown(
219+
captionAndImage.caption,
220+
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
221+
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
222+
encodeHtml: encodeHtml,
223+
);
224+
_addParagraph(captionText, element.attributes);
225+
_addImage(captionAndImage.image);
226+
} else {
227+
final attributedText = parseInlineMarkdown(
228+
element.textContent,
229+
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
230+
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
231+
encodeHtml: encodeHtml,
232+
);
233+
_addParagraph(attributedText, element.attributes);
234+
}
223235
}
224236

225237
break;
@@ -328,28 +340,25 @@ class _MarkdownToDocument implements md.NodeVisitor {
328340
break;
329341
}
330342

331-
final textAlign = element.attributes['textAlign'];
332343
_content.add(
333344
ParagraphNode(
334345
id: Editor.createNodeId(),
335346
text: _parseInlineText(element.textContent),
336347
metadata: {
337348
'blockType': headerAttribution,
338-
'textAlign': textAlign,
349+
'textAlign': element.attributes['textAlign'] ?? 'left',
339350
},
340351
),
341352
);
342353
}
343354

344355
void _addParagraph(AttributedText attributedText, Map<String, String> attributes) {
345-
final textAlign = attributes['textAlign'];
346-
347356
_content.add(
348357
ParagraphNode(
349358
id: Editor.createNodeId(),
350359
text: attributedText,
351360
metadata: {
352-
'textAlign': textAlign,
361+
'textAlign': attributes['textAlign'] ?? 'left',
353362
},
354363
),
355364
);
@@ -480,6 +489,25 @@ class _MarkdownToDocument implements md.NodeVisitor {
480489
syntax: syntax,
481490
);
482491
}
492+
493+
/// If the last line of [markdown] is a block image and there is at least one
494+
/// preceding line (the caption), returns a [_CaptionAndImage] containing the
495+
/// caption text and the parsed image. Otherwise returns `null`.
496+
_CaptionAndImage? _maybeParseImageWithCaption(String markdown) {
497+
final newlineIndex = markdown.lastIndexOf('\n');
498+
if (newlineIndex < 0) {
499+
return null;
500+
}
501+
502+
final lastLine = markdown.substring(newlineIndex + 1);
503+
final image = _maybeParseBlockImage(lastLine);
504+
if (image == null) {
505+
return null;
506+
}
507+
508+
final caption = markdown.substring(0, newlineIndex);
509+
return _CaptionAndImage(caption: caption, image: image);
510+
}
483511
}
484512

485513
/// Converts a deserialized Markdown element into a [DocumentNode].
@@ -842,6 +870,13 @@ class _HeaderWithAlignmentSyntax extends md.BlockSyntax {
842870
}
843871
}
844872

873+
class _CaptionAndImage {
874+
const _CaptionAndImage({required this.caption, required this.image});
875+
876+
final String caption;
877+
final _MarkdownImage image;
878+
}
879+
845880
class _MarkdownImage {
846881
_MarkdownImage({
847882
required this.url,

super_editor/lib/src/infrastructure/serialization/markdown/table.dart

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import 'dart:ui';
2-
31
import 'package:markdown/markdown.dart' as md;
42
import 'package:super_editor/src/core/document.dart';
53
import 'package:super_editor/src/core/editor.dart';
@@ -55,7 +53,7 @@ extension ElementTableExtension on md.Element {
5553
),
5654
metadata: const {
5755
NodeMetadata.blockType: tableHeaderAttribution,
58-
TextNodeMetadata.textAlign: TextAlign.center,
56+
TextNodeMetadata.textAlign: 'center',
5957
},
6058
),
6159
);
@@ -81,17 +79,17 @@ extension ElementTableExtension on md.Element {
8179
throw Exception('Table body cells must be <td> elements');
8280
}
8381
final textAlign = switch ((headerRow.children![i] as md.Element).attributes['align']) {
84-
'left' => TextAlign.left,
85-
'center' => TextAlign.center,
86-
'right' => TextAlign.right,
87-
_ => TextAlign.left,
82+
'left' => 'left',
83+
'center' => 'center',
84+
'right' => 'right',
85+
_ => 'left',
8886
};
8987

9088
row.add(TextNode(
9189
id: Editor.createNodeId(),
9290
text: parseInlineMarkdown(cellElement.textContent),
9391
metadata: {
94-
if (textAlign != TextAlign.left) TextNodeMetadata.textAlign: textAlign,
92+
if (textAlign != 'left') TextNodeMetadata.textAlign: textAlign,
9593
},
9694
));
9795
}

super_editor/lib/src/infrastructure/serialization/quill/serializing/serializers.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,9 @@ class TextBlockDeltaSerializer implements DeltaSerializer {
298298
final blockAttributes = <String, dynamic>{};
299299

300300
// Add all the block-level formats that aren't mutually exclusive.
301-
if (textBlock.metadata["textAlign"] != null) {
302-
blockAttributes["align"] = textBlock.metadata["textAlign"];
301+
final textAlign = textBlock.metadata["textAlign"];
302+
if (textAlign != null && textAlign != "left") {
303+
blockAttributes["align"] = textAlign;
303304
}
304305

305306
final blockType = textBlock.metadata["blockType"] as Attribution?;

super_editor/test/infrastructure/serialization/html/document_to_html_test.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import 'dart:ui';
2-
31
import 'package:flutter_test/flutter_test.dart';
42
import 'package:super_editor/super_editor.dart';
53

@@ -665,12 +663,12 @@ void main() {
665663
TextNode(
666664
id: "1.1.1",
667665
text: AttributedText("Value 1.1"),
668-
metadata: const {"textAlign": TextAlign.right},
666+
metadata: const {"textAlign": "right"},
669667
),
670668
TextNode(
671669
id: "1.1.2",
672670
text: AttributedText("Value 1.2"),
673-
metadata: const {"textAlign": TextAlign.center},
671+
metadata: const {"textAlign": "center"},
674672
),
675673
],
676674
],

0 commit comments

Comments
 (0)