-
Notifications
You must be signed in to change notification settings - Fork 293
Expand file tree
/
Copy pathdocument_markdown_decoder.dart
More file actions
110 lines (93 loc) · 2.97 KB
/
document_markdown_decoder.dart
File metadata and controls
110 lines (93 loc) · 2.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/plugins/markdown/decoder/custom_syntaxes/underline_syntax.dart';
import 'package:collection/collection.dart';
import 'package:markdown/markdown.dart' as md;
class DocumentMarkdownDecoder extends Converter<String, Document> {
DocumentMarkdownDecoder({
this.markdownElementParsers = const [],
this.inlineSyntaxes = const [],
});
final List<CustomMarkdownParser> markdownElementParsers;
final List<md.InlineSyntax> inlineSyntaxes;
@override
Document convert(String input) {
final formattedMarkdown = _formatMarkdown(input);
final List<md.Node> mdNodes = md.Document(
extensionSet: md.ExtensionSet.gitHubFlavored,
inlineSyntaxes: [
...inlineSyntaxes,
UnderlineInlineSyntax(),
],
encodeHtml: false,
).parse(formattedMarkdown);
final document = Document.blank();
final nodes = mdNodes
.map((e) => _parseNode(e))
.nonNulls
.flattened
.toList(growable: false); // avoid lazy evaluation
if (nodes.isNotEmpty) {
document.insert([0], nodes);
}
return document;
}
// handle node itself and its children
List<Node> _parseNode(md.Node mdNode) {
List<Node> nodes = [];
for (final parser in markdownElementParsers) {
nodes = parser.transform(
mdNode,
markdownElementParsers,
);
if (nodes.isNotEmpty) {
break;
}
}
if (nodes.isEmpty) {
AppFlowyEditorLog.editor.debug(
'empty result from node: $mdNode, text: ${mdNode.textContent}',
);
}
return nodes;
}
String _formatMarkdown(String markdown) {
String result = markdown;
// 1. Ensure every image is *preceded* by two newlines
// Handles:
// - Inline images after text (e.g., "text ")
// - 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 before = match[1];
final alt = match[2];
final url = match[3];
return '$before\n\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';
},
);
// 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\n)'),
(match) => '${match[0]}\n\n',
);
// 3. Clean up excessive newlines (e.g., \n\n\n)
result = result.replaceAll(RegExp(r'\n{3,}'), '\n\n');
return result.trim();
}
}