diff --git a/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart b/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart index eb7291332..c45527c8e 100644 --- a/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart +++ b/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart @@ -63,26 +63,48 @@ class DocumentMarkdownDecoder extends Converter { return nodes; } - String _formatMarkdown(String markdown) { - // Rule 1: single '\n' between text and image, add double '\n' - String result = markdown.replaceAllMapped( - RegExp(r'([^\n])\n!\[([^\]]*)\]\(([^)]+)\)', multiLine: true), +String _formatMarkdown(String markdown) { + String result = markdown; + + // 1. Ensure every image is *preceded* by two newlines + // Handles: + // - Inline images after text (e.g., "text ![img](url)") + // - List items before images + // - Consecutive images + // - Images directly at line start + // + // We apply two separate rules: + // a) Images directly after non-newline characters + result = result.replaceAllMapped( + RegExp(r'([^\n])\s*!\[([^\]]*)\]\(([^)]+)\)'), (match) { - final text = match[1] ?? ''; - final altText = match[2] ?? ''; - final url = match[3] ?? ''; - return '$text\n\n![$altText]($url)'; + final before = match[1]; + final alt = match[2]; + final url = match[3]; + return '$before\n\n![$alt]($url)'; }, ); - // Rule 2: without '\n' between text and image, add double '\n' + // b) Images not preceded by a blank line + result = result.replaceAllMapped( + RegExp(r'(? '${match[1]}\n\n![${match[2]}](${match[3]})', + RegExp(r'!\[[^\]]*\]\([^)]+\)(?!\n\n)'), + (match) => '${match[0]}\n\n', ); - // Add another rules here. + // 3. Clean up excessive newlines (e.g., \n\n\n) + result = result.replaceAll(RegExp(r'\n{3,}'), '\n\n'); - return result; + return result.trim(); } } diff --git a/test/plugins/markdown/document_markdown_test.dart b/test/plugins/markdown/document_markdown_test.dart index 08cb20df1..c02b69fb4 100644 --- a/test/plugins/markdown/document_markdown_test.dart +++ b/test/plugins/markdown/document_markdown_test.dart @@ -27,37 +27,53 @@ void main() { expect(markdown, markdownDocumentEncoded); }); - test('paragraph + image with single \n', () { - const markdown = '''This is the first line -![image](https://example.com/image.png)'''; - final document = markdownToDocument(markdown); - final nodes = document.root.children; - expect(nodes.length, 2); - expect(nodes[0].delta?.toPlainText(), 'This is the first line'); - expect(nodes[1].attributes['url'], 'https://example.com/image.png'); - }); +test('image inside paragraph (no spacing)', () { + const markdown = 'Text before![img](https://example.com/image.png)text after.'; + final document = markdownToDocument(markdown); + final nodes = document.root.children; + expect(nodes.length, 3); + expect(nodes[0].delta?.toPlainText(), 'Text before'); + expect(nodes[1].attributes['url'], 'https://example.com/image.png'); + expect(nodes[2].delta?.toPlainText(), 'text after.'); +}); - test('paragraph + image with double \n', () { - const markdown = '''This is the first line +test('image between two paragraphs (no blank lines)', () { + const markdown = 'First paragraph.\n![img](https://example.com/image.png)\nSecond paragraph.'; + final document = markdownToDocument(markdown); + final nodes = document.root.children; + expect(nodes.length, 3); + expect(nodes[0].delta?.toPlainText(), 'First paragraph.'); + expect(nodes[1].attributes['url'], 'https://example.com/image.png'); + expect(nodes[2].delta?.toPlainText(), 'Second paragraph.'); +}); -![image](https://example.com/image.png)'''; - final document = markdownToDocument(markdown); - final nodes = document.root.children; - expect(nodes.length, 2); - expect(nodes[0].delta?.toPlainText(), 'This is the first line'); - expect(nodes[1].attributes['url'], 'https://example.com/image.png'); - }); +test('multiple images on same line (inline)', () { + const markdown = '![img1](https://example.com/image.png) ![img2](https://example.com/image.png)![img3](https://example.com/image.png)'; + final document = markdownToDocument(markdown); + final nodes = document.root.children; + expect(nodes.length, 3); + expect(nodes[0].attributes['url'], 'https://example.com/image.png'); + expect(nodes[1].attributes['url'], 'https://example.com/image.png'); + expect(nodes[2].attributes['url'], 'https://example.com/image.png'); +}); - test('paragraph + image without \n', () { - const markdown = - '''This is the first line![image](https://example.com/image.png)'''; - final document = markdownToDocument(markdown); - final nodes = document.root.children; - expect(nodes.length, 2); - expect(nodes[0].delta?.toPlainText(), 'This is the first line'); - expect(nodes[1].attributes['url'], 'https://example.com/image.png'); - }); - }); +test('image attached directly to previous content (no newline or space)', () { + const markdown = 'Paragraph![img](https://example.com/image.png)'; + final document = markdownToDocument(markdown); + final nodes = document.root.children; + expect(nodes.length, 2); + expect(nodes[0].delta?.toPlainText(), 'Paragraph'); + expect(nodes[1].attributes['url'], 'https://example.com/image.png'); +}); + +test('image attached directly to next content (no newline)', () { + const markdown = '![img](https://example.com/image.png)This is a sentence.'; + final document = markdownToDocument(markdown); + final nodes = document.root.children; + expect(nodes.length, 2); + expect(nodes[0].attributes['url'], 'https://example.com/image.png'); + expect(nodes[1].delta?.toPlainText(), 'This is a sentence.'); +}); } const testDocument = '''{