Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions lib/src/plugins/markdown/decoder/document_markdown_decoder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,48 @@ class DocumentMarkdownDecoder extends Converter<String, Document> {
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'(?<!\n)\s*!\[([^\]]*)\]\(([^)]+)\)'),
(match) {
final alt = match[1];
final url = match[2];
return '\n\n![$alt]($url)';
},
);

// 2. Ensure every image is *followed* by two newlines
// So that next content is not inline with the image
result = result.replaceAllMapped(
RegExp(r'([^\n])!\[([^\]]*)\]\(([^)]+)\)'),
(match) => '${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();
}
}
72 changes: 44 additions & 28 deletions test/plugins/markdown/document_markdown_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '''{
Expand Down
Loading