From f2cc9022fab5568c12d6a275fdce1174c491b18b Mon Sep 17 00:00:00 2001 From: Tomic-Riedel <64801984+Tomic-Riedel@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:23:03 +0000 Subject: [PATCH 1/2] fix: add newline rules to separate multiple images in markdown --- .../decoder/document_markdown_decoder.dart | 15 ++++++++++++ .../markdown/document_markdown_test.dart | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart b/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart index eb7291332..2a5ebc359 100644 --- a/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart +++ b/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart @@ -81,6 +81,21 @@ class DocumentMarkdownDecoder extends Converter { (match) => '${match[1]}\n\n![${match[2]}](${match[3]})', ); + // Rule 3: single '\n' between two images, add double '\n' + result = result.replaceAllMapped( + RegExp( + r'(!\[[^\]]*\]\([^)]+\))\n(?=!?\[[^\]]*\]\([^)]+\))', + multiLine: true, + ), + (match) => '${match[1]}\n\n', + ); + + // Rule 4:without '\n' between two images, add double '\n' + result = result.replaceAllMapped( + RegExp(r'(!\[[^\]]*\]\([^)]+\))(?=!?\[[^\]]*\]\([^)]+\))'), + (match) => '${match[1]}\n\n', + ); + // Add another rules here. return result; diff --git a/test/plugins/markdown/document_markdown_test.dart b/test/plugins/markdown/document_markdown_test.dart index 08cb20df1..93d4306f6 100644 --- a/test/plugins/markdown/document_markdown_test.dart +++ b/test/plugins/markdown/document_markdown_test.dart @@ -57,6 +57,30 @@ void main() { expect(nodes[0].delta?.toPlainText(), 'This is the first line'); expect(nodes[1].attributes['url'], 'https://example.com/image.png'); }); + test('multiple images on separate lines without blank lines', () { + const markdown = '''![image1](https://example.com/image.png) +![image2](https://example.com/image.png) +![image3](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('multiple images on same line (inline)', () { + const markdown = ''' +![inline1](https://example.com/image.png) ![inline2](https://example.com/image.png) ![inline3](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'); + }); }); } From c976d8283d0b582a1b84c0d1b21c9224b0b0804e Mon Sep 17 00:00:00 2001 From: Tomic-Riedel <64801984+Tomic-Riedel@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:39:02 +0000 Subject: [PATCH 2/2] fix: improve image formatting rules in markdown decoder to work with any type of block before & after images --- .../decoder/document_markdown_decoder.dart | 59 +++++++----- .../markdown/document_markdown_test.dart | 94 +++++++++---------- 2 files changed, 76 insertions(+), 77 deletions(-) diff --git a/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart b/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart index 2a5ebc359..c45527c8e 100644 --- a/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart +++ b/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart @@ -63,41 +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' - result = result.replaceAllMapped( - RegExp(r'([^\n])!\[([^\]]*)\]\(([^)]+)\)'), - (match) => '${match[1]}\n\n![${match[2]}](${match[3]})', - ); - - // Rule 3: single '\n' between two images, add double '\n' + // b) Images not preceded by a blank line result = result.replaceAllMapped( - RegExp( - r'(!\[[^\]]*\]\([^)]+\))\n(?=!?\[[^\]]*\]\([^)]+\))', - multiLine: true, - ), - (match) => '${match[1]}\n\n', + RegExp(r'(? '${match[1]}\n\n', + 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 93d4306f6..c02b69fb4 100644 --- a/test/plugins/markdown/document_markdown_test.dart +++ b/test/plugins/markdown/document_markdown_test.dart @@ -27,61 +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('multiple images on separate lines without blank lines', () { - const markdown = '''![image1](https://example.com/image.png) -![image2](https://example.com/image.png) -![image3](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('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('multiple images on same line (inline)', () { - const markdown = ''' -![inline1](https://example.com/image.png) ![inline2](https://example.com/image.png) ![inline3](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('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 = '''{