Skip to content

Commit 2315204

Browse files
matthew-carrollweb-flow
authored andcommitted
[Super Editor] - Markdown: Parse back to back images with captions (#3059)
1 parent 98ad3a5 commit 2315204

2 files changed

Lines changed: 103 additions & 41 deletions

File tree

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

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -211,29 +211,31 @@ class _MarkdownToDocument implements md.NodeVisitor {
211211
_addHeader(element, level: 6);
212212
break;
213213
case 'p':
214-
final blockImage = _maybeParseBlockImage(element.textContent);
215-
if (blockImage != null) {
216-
_addImage(blockImage);
217-
} else {
218-
final captionAndImage = _maybeParseImageWithCaption(element.textContent);
219-
if (captionAndImage != null) {
220-
final captionText = parseInlineMarkdown(
221-
captionAndImage.caption,
222-
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
223-
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
224-
encodeHtml: encodeHtml,
225-
);
226-
_addParagraph(captionText, element.attributes);
227-
_addImage(captionAndImage.image);
228-
} else {
229-
final attributedText = parseInlineMarkdown(
230-
element.textContent,
231-
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
232-
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
233-
encodeHtml: encodeHtml,
234-
);
235-
_addParagraph(attributedText, element.attributes);
214+
final mixedContent = syntax == MarkdownSyntax.superEditor
215+
? _parseMixedParagraphContent(element.textContent)
216+
: null;
217+
if (mixedContent != null) {
218+
for (final segment in mixedContent) {
219+
if (segment.image != null) {
220+
_addImage(segment.image!);
221+
} else {
222+
final attributedText = parseInlineMarkdown(
223+
segment.text!,
224+
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
225+
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
226+
encodeHtml: encodeHtml,
227+
);
228+
_addParagraph(attributedText, element.attributes);
229+
}
236230
}
231+
} else {
232+
final attributedText = parseInlineMarkdown(
233+
element.textContent,
234+
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
235+
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
236+
encodeHtml: encodeHtml,
237+
);
238+
_addParagraph(attributedText, element.attributes);
237239
}
238240

239241
break;
@@ -494,23 +496,49 @@ class _MarkdownToDocument implements md.NodeVisitor {
494496
);
495497
}
496498

497-
/// If the last line of [markdown] is a block image and there is at least one
498-
/// preceding line (the caption), returns a [_CaptionAndImage] containing the
499-
/// caption text and the parsed image. Otherwise returns `null`.
500-
_CaptionAndImage? _maybeParseImageWithCaption(String markdown) {
501-
final newlineIndex = markdown.lastIndexOf('\n');
502-
if (newlineIndex < 0) {
503-
return null;
499+
/// Splits [markdown] (a multi-line paragraph) into an ordered list of
500+
/// paragraph-text segments and block images.
501+
///
502+
/// Returns `null` if the content contains no block images, in which case the
503+
/// caller should treat the whole text as a single paragraph.
504+
///
505+
/// When images are present, consecutive non-image lines are grouped into a
506+
/// single paragraph segment. For example:
507+
///
508+
/// ```
509+
/// A caption:
510+
/// ![Image 1](url1)
511+
/// B caption:
512+
/// ![Image 2](url2)
513+
/// ```
514+
///
515+
/// produces: paragraph("A caption:"), image(url1), paragraph("B caption:"), image(url2).
516+
List<_ParagraphOrImage>? _parseMixedParagraphContent(String markdown) {
517+
final lines = markdown.split('\n');
518+
final segments = <_ParagraphOrImage>[];
519+
final textBuffer = StringBuffer();
520+
bool foundImage = false;
521+
522+
for (final line in lines) {
523+
final image = _maybeParseBlockImage(line);
524+
if (image != null) {
525+
foundImage = true;
526+
if (textBuffer.isNotEmpty) {
527+
segments.add(_ParagraphOrImage.paragraph(textBuffer.toString()));
528+
textBuffer.clear();
529+
}
530+
segments.add(_ParagraphOrImage.image(image));
531+
} else {
532+
if (textBuffer.isNotEmpty) textBuffer.write('\n');
533+
textBuffer.write(line);
534+
}
504535
}
505536

506-
final lastLine = markdown.substring(newlineIndex + 1);
507-
final image = _maybeParseBlockImage(lastLine);
508-
if (image == null) {
509-
return null;
537+
if (textBuffer.isNotEmpty) {
538+
segments.add(_ParagraphOrImage.paragraph(textBuffer.toString()));
510539
}
511540

512-
final caption = markdown.substring(0, newlineIndex);
513-
return _CaptionAndImage(caption: caption, image: image);
541+
return foundImage ? segments : null;
514542
}
515543
}
516544

@@ -874,11 +902,13 @@ class _HeaderWithAlignmentSyntax extends md.BlockSyntax {
874902
}
875903
}
876904

877-
class _CaptionAndImage {
878-
const _CaptionAndImage({required this.caption, required this.image});
905+
/// A segment of a mixed paragraph: either a run of plain text or a block image.
906+
class _ParagraphOrImage {
907+
_ParagraphOrImage.paragraph(String this.text) : image = null;
908+
_ParagraphOrImage.image(_MarkdownImage this.image) : text = null;
879909

880-
final String caption;
881-
final _MarkdownImage image;
910+
final String? text;
911+
final _MarkdownImage? image;
882912
}
883913

884914
class _MarkdownImage {

super_editor/test/infrastructure/serialization/markdown/super_editor_markdown_test.dart

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,39 @@ B caption:
10741074
C caption:
10751075
![Image 3](https://images.com/some/image3.png)
10761076
1077-
Paragraph after the captioned imagea.''',
1077+
Paragraph after the captioned image.''',
1078+
);
1079+
1080+
expect(
1081+
doc,
1082+
documentEquivalentTo(
1083+
MutableDocument(nodes: [
1084+
ParagraphNode(id: "1", text: AttributedText("Document with image with caption.")),
1085+
ParagraphNode(id: "2", text: AttributedText("A caption:")),
1086+
ImageNode(id: "3", imageUrl: "https://images.com/some/image1.png", altText: "Image 1"),
1087+
ParagraphNode(id: "4", text: AttributedText("B caption:")),
1088+
ImageNode(id: "5", imageUrl: "https://images.com/some/image2.png", altText: "Image 2"),
1089+
ParagraphNode(id: "6", text: AttributedText("C caption:")),
1090+
ImageNode(id: "7", imageUrl: "https://images.com/some/image3.png", altText: "Image 3"),
1091+
ParagraphNode(id: "8", text: AttributedText("Paragraph after the captioned image.")),
1092+
]),
1093+
),
1094+
);
1095+
});
1096+
1097+
test('multiple images with captions above without empty lines between', () {
1098+
final doc = deserializeMarkdownToDocument(
1099+
'''
1100+
Document with image with caption.
1101+
1102+
A caption:
1103+
![Image 1](https://images.com/some/image1.png)
1104+
B caption:
1105+
![Image 2](https://images.com/some/image2.png)
1106+
C caption:
1107+
![Image 3](https://images.com/some/image3.png)
1108+
1109+
Paragraph after the captioned image.''',
10781110
);
10791111

10801112
expect(
@@ -1088,7 +1120,7 @@ Paragraph after the captioned imagea.''',
10881120
ImageNode(id: "5", imageUrl: "https://images.com/some/image2.png", altText: "Image 2"),
10891121
ParagraphNode(id: "6", text: AttributedText("C caption:")),
10901122
ImageNode(id: "7", imageUrl: "https://images.com/some/image3.png", altText: "Image 3"),
1091-
ParagraphNode(id: "8", text: AttributedText("Paragraph after the captioned imagea.")),
1123+
ParagraphNode(id: "8", text: AttributedText("Paragraph after the captioned image.")),
10921124
]),
10931125
),
10941126
);

0 commit comments

Comments
 (0)