Skip to content

Commit 652361e

Browse files
[Markdown] - Make inline parsing customizable (Resolves #2780) (#2781)
1 parent 5b21d3b commit 652361e

File tree

4 files changed

+184
-62
lines changed

4 files changed

+184
-62
lines changed

super_editor_markdown/lib/src/markdown_inline_parser.dart

Lines changed: 117 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,48 @@ import 'package:super_editor_markdown/super_editor_markdown.dart';
55

66
/// Parses inline markdown content.
77
///
8-
/// Supports strikethrough, underline, bold, italics, code and links.
8+
/// {@macro markdown_two_phase}
99
///
10-
/// The given [syntax] controls how the [text] is parsed, e.g., [MarkdownSyntax.normal]
11-
/// for strict Markdown parsing, or [MarkdownSyntax.superEditor] to use Super Editor's
12-
/// extended syntax.
10+
/// {@macro inline_markdown_syntaxes}
1311
///
1412
/// If [encodeHtml] is `true`, it escapes HTML symbols like &, <, and >. For example,
1513
/// `&` becomes `&amp;`, `<` becomes `&lt;`, and `>` becomes `&gt;`.
1614
AttributedText parseInlineMarkdown(
1715
String text, {
18-
MarkdownSyntax syntax = MarkdownSyntax.superEditor,
16+
Iterable<md.InlineSyntax>? inlineMarkdownSyntaxes,
17+
Iterable<InlineHtmlSyntax>? inlineHtmlSyntaxes,
1918
bool encodeHtml = false,
2019
}) {
2120
final inlineParser = md.InlineParser(
2221
text,
2322
md.Document(
24-
inlineSyntaxes: [
25-
SingleStrikethroughSyntax(), // this needs to be before md.StrikethroughSyntax to be recognized
26-
md.StrikethroughSyntax(),
27-
UnderlineSyntax(),
28-
if (syntax == MarkdownSyntax.superEditor) //
29-
SuperEditorImageSyntax(),
30-
],
23+
inlineSyntaxes: inlineMarkdownSyntaxes ?? defaultSuperEditorInlineSyntaxes,
3124
encodeHtml: encodeHtml,
3225
),
3326
);
34-
final inlineVisitor = _InlineMarkdownToDocument();
27+
final inlineVisitor = _InlineMarkdownToDocument(
28+
inlineHtmlSyntaxes: inlineHtmlSyntaxes ?? defaultInlineHtmlSyntaxes,
29+
);
3530
final inlineNodes = inlineParser.parse();
3631
for (final inlineNode in inlineNodes) {
3732
inlineNode.accept(inlineVisitor);
3833
}
3934
return inlineVisitor.attributedText;
4035
}
4136

37+
final defaultSuperEditorInlineSyntaxes = [
38+
SingleStrikethroughSyntax(), // this needs to be before md.StrikethroughSyntax to be recognized
39+
md.StrikethroughSyntax(),
40+
UnderlineSyntax(),
41+
SuperEditorImageSyntax(),
42+
];
43+
44+
final defaultNonSuperEditorInlineSyntaxes = [
45+
SingleStrikethroughSyntax(), // this needs to be before md.StrikethroughSyntax to be recognized
46+
md.StrikethroughSyntax(),
47+
UnderlineSyntax(),
48+
];
49+
4250
/// Parses inline markdown content.
4351
///
4452
/// Apply [_InlineMarkdownToDocument] to a text [md.Element] to
@@ -49,7 +57,11 @@ AttributedText parseInlineMarkdown(
4957
/// that contains image tags. If any non-image text is found,
5058
/// the content is treated as styled text.
5159
class _InlineMarkdownToDocument implements md.NodeVisitor {
52-
_InlineMarkdownToDocument();
60+
_InlineMarkdownToDocument({
61+
required this.inlineHtmlSyntaxes,
62+
});
63+
64+
final Iterable<InlineHtmlSyntax> inlineHtmlSyntaxes;
5365

5466
AttributedText get attributedText => _textStack.first;
5567

@@ -72,38 +84,14 @@ class _InlineMarkdownToDocument implements md.NodeVisitor {
7284
void visitElementAfter(md.Element element) {
7385
// Reset to normal text style because a plain text element does
7486
// not receive a call to visitElementBefore().
75-
final styledText = _textStack.removeLast();
76-
77-
if (element.tag == 'strong') {
78-
styledText.addAttribution(
79-
boldAttribution,
80-
SpanRange(0, styledText.length - 1),
81-
);
82-
} else if (element.tag == 'em') {
83-
styledText.addAttribution(
84-
italicsAttribution,
85-
SpanRange(0, styledText.length - 1),
86-
);
87-
} else if (element.tag == "del") {
88-
styledText.addAttribution(
89-
strikethroughAttribution,
90-
SpanRange(0, styledText.length - 1),
91-
);
92-
} else if (element.tag == "code") {
93-
styledText.addAttribution(
94-
codeAttribution,
95-
SpanRange(0, styledText.length - 1),
96-
);
97-
} else if (element.tag == "u") {
98-
styledText.addAttribution(
99-
underlineAttribution,
100-
SpanRange(0, styledText.length - 1),
101-
);
102-
} else if (element.tag == 'a') {
103-
styledText.addAttribution(
104-
LinkAttribution.fromUri(Uri.parse(element.attributes['href']!)),
105-
SpanRange(0, styledText.length - 1),
106-
);
87+
var styledText = _textStack.removeLast();
88+
89+
for (final inlineHtmlSyntax in inlineHtmlSyntaxes) {
90+
final finalText = inlineHtmlSyntax(element, styledText);
91+
if (finalText != null) {
92+
styledText = finalText;
93+
break;
94+
}
10795
}
10896

10997
if (_textStack.isNotEmpty) {
@@ -114,3 +102,86 @@ class _InlineMarkdownToDocument implements md.NodeVisitor {
114102
}
115103
}
116104
}
105+
106+
const defaultInlineHtmlSyntaxes = [
107+
boldHtmlSyntax,
108+
italicHtmlSyntax,
109+
underlineHtmlSyntax,
110+
strikethroughHtmlSyntax,
111+
anchorHtmlSyntax,
112+
codeInlineHtmlSyntax,
113+
];
114+
115+
typedef InlineHtmlSyntax = AttributedText? Function(md.Element element, AttributedText text);
116+
117+
AttributedText? boldHtmlSyntax(md.Element element, AttributedText text) {
118+
if (element.tag != 'strong') {
119+
return null;
120+
}
121+
122+
return text
123+
..addAttribution(
124+
boldAttribution,
125+
SpanRange(0, text.length - 1),
126+
);
127+
}
128+
129+
AttributedText? italicHtmlSyntax(md.Element element, AttributedText text) {
130+
if (element.tag != 'em') {
131+
return null;
132+
}
133+
134+
return text
135+
..addAttribution(
136+
italicsAttribution,
137+
SpanRange(0, text.length - 1),
138+
);
139+
}
140+
141+
AttributedText? underlineHtmlSyntax(md.Element element, AttributedText text) {
142+
if (element.tag != 'u') {
143+
return null;
144+
}
145+
146+
return text
147+
..addAttribution(
148+
underlineAttribution,
149+
SpanRange(0, text.length - 1),
150+
);
151+
}
152+
153+
AttributedText? strikethroughHtmlSyntax(md.Element element, AttributedText text) {
154+
if (element.tag != 'del') {
155+
return null;
156+
}
157+
158+
return text
159+
..addAttribution(
160+
strikethroughAttribution,
161+
SpanRange(0, text.length - 1),
162+
);
163+
}
164+
165+
AttributedText? anchorHtmlSyntax(md.Element element, AttributedText text) {
166+
if (element.tag != 'a') {
167+
return null;
168+
}
169+
170+
return text
171+
..addAttribution(
172+
LinkAttribution.fromUri(Uri.parse(element.attributes['href']!)),
173+
SpanRange(0, text.length - 1),
174+
);
175+
}
176+
177+
AttributedText? codeInlineHtmlSyntax(md.Element element, AttributedText text) {
178+
if (element.tag != 'code') {
179+
return null;
180+
}
181+
182+
return text
183+
..addAttribution(
184+
codeAttribution,
185+
SpanRange(0, text.length - 1),
186+
);
187+
}

super_editor_markdown/lib/src/markdown_to_document_parsing.dart

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,44 @@ import 'super_editor_syntax.dart';
1212

1313
/// Parses the given [markdown] and deserializes it into a [MutableDocument].
1414
///
15-
/// The given [syntax] controls how the [markdown] is parsed, e.g., [MarkdownSyntax.normal]
16-
/// for strict Markdown parsing, or [MarkdownSyntax.superEditor] to use Super Editor's
17-
/// extended syntax.
15+
/// ## Parsing
16+
/// {@template markdown_two_phase}
17+
/// Markdown parsing is a two-phase process:
18+
/// 1. Parse Markdown syntax to HTML
19+
/// 2. Convert HTML to [AttributedText]
20+
/// {@endtemplate}
1821
///
22+
/// This two-phase process is true for both block-level parsing, e.g., blockquotes,
23+
/// code blocks, and also for inline parsing, e.g., bold, italics, links.
24+
///
25+
/// ### Custom Block Parsing
1926
/// To add support for parsing non-standard Markdown blocks, provide [customBlockSyntax]s
2027
/// that parse Markdown text into [md.Element]s, and provide [customElementToNodeConverters] that
2128
/// turn those [md.Element]s into [DocumentNode]s.
29+
///
30+
/// ### Custom Inline Parsing
31+
/// {@template inline_markdown_syntaxes}
32+
/// By default, when no syntaxes are provided, this method parses Markdown to
33+
/// HTML with [defaultSuperEditorInlineSyntaxes]. Then, this method configures
34+
/// the [AttributedText] based on the HTML, using [defaultInlineHtmlSyntaxes].
35+
///
36+
/// To customize the supported Markdown syntaxes, provide a custom chain of
37+
/// responsibility for [inlineMarkdownSyntaxes].
38+
///
39+
/// To customize the supported HTML, which configures the final [AttributedText],
40+
/// provide a custom chain of responsibility for [inlineHtmlSyntaxes].
41+
/// {@endtemplate}
42+
///
43+
/// The given [syntax] further adjusts how the Markdown is interpreted, e.g., [MarkdownSyntax.normal]
44+
/// for strict Markdown parsing, or [MarkdownSyntax.superEditor] to use Super Editor's
45+
/// extended syntax.
2246
MutableDocument deserializeMarkdownToDocument(
2347
String markdown, {
2448
MarkdownSyntax syntax = MarkdownSyntax.superEditor,
2549
List<md.BlockSyntax> customBlockSyntax = const [],
2650
List<ElementToNodeConverter> customElementToNodeConverters = const [],
51+
Iterable<md.InlineSyntax>? inlineMarkdownSyntaxes,
52+
Iterable<InlineHtmlSyntax>? inlineHtmlSyntaxes,
2753
bool encodeHtml = false,
2854
}) {
2955
final markdownLines = const LineSplitter().convert(markdown).map<md.Line>(
@@ -50,7 +76,13 @@ MutableDocument deserializeMarkdownToDocument(
5076
final markdownNodes = blockParser.parseLines();
5177

5278
// Convert structured markdown to a Document.
53-
final nodeVisitor = _MarkdownToDocument(customElementToNodeConverters, encodeHtml, syntax);
79+
final nodeVisitor = _MarkdownToDocument(
80+
elementToNodeConverters: customElementToNodeConverters,
81+
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
82+
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
83+
encodeHtml: encodeHtml,
84+
syntax: syntax,
85+
);
5486
for (final node in markdownNodes) {
5587
node.accept(nodeVisitor);
5688
}
@@ -86,15 +118,20 @@ MutableDocument deserializeMarkdownToDocument(
86118
/// contains [DocumentNode]s that correspond to the visited
87119
/// markdown content.
88120
class _MarkdownToDocument implements md.NodeVisitor {
89-
_MarkdownToDocument([
90-
this._elementToNodeConverters = const [],
91-
this._encodeHtml = false,
121+
_MarkdownToDocument({
122+
this.elementToNodeConverters = const [],
123+
this.inlineMarkdownSyntaxes,
124+
this.inlineHtmlSyntaxes,
125+
this.encodeHtml = false,
92126
this.syntax = MarkdownSyntax.normal,
93-
]);
127+
});
94128

95129
final MarkdownSyntax syntax;
96130

97-
final List<ElementToNodeConverter> _elementToNodeConverters;
131+
final List<ElementToNodeConverter> elementToNodeConverters;
132+
133+
final Iterable<md.InlineSyntax>? inlineMarkdownSyntaxes;
134+
final Iterable<InlineHtmlSyntax>? inlineHtmlSyntaxes;
98135

99136
final _content = <DocumentNode>[];
100137
List<DocumentNode> get content => _content;
@@ -119,11 +156,11 @@ class _MarkdownToDocument implements md.NodeVisitor {
119156
/// symbols are left as-is.
120157
///
121158
/// Example: "&" -> "&amp;", "<" -> "&lt;", ">" -> "&gt;"
122-
final bool _encodeHtml;
159+
final bool encodeHtml;
123160

124161
@override
125162
bool visitElementBefore(md.Element element) {
126-
for (final converter in _elementToNodeConverters) {
163+
for (final converter in elementToNodeConverters) {
127164
final node = converter.handleElement(element);
128165
if (node != null) {
129166
_content.add(node);
@@ -170,7 +207,12 @@ class _MarkdownToDocument implements md.NodeVisitor {
170207
if (blockImage != null) {
171208
_addImage(blockImage);
172209
} else {
173-
final attributedText = parseInlineMarkdown(element.textContent, syntax: syntax, encodeHtml: _encodeHtml);
210+
final attributedText = parseInlineMarkdown(
211+
element.textContent,
212+
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
213+
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
214+
encodeHtml: encodeHtml,
215+
);
174216
_addParagraph(attributedText, element.attributes);
175217
}
176218

@@ -415,8 +457,9 @@ class _MarkdownToDocument implements md.NodeVisitor {
415457
AttributedText _parseInlineText(String text) {
416458
return parseInlineMarkdown(
417459
text,
418-
syntax: syntax,
419-
encodeHtml: _encodeHtml,
460+
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
461+
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
462+
encodeHtml: encodeHtml,
420463
);
421464
}
422465

super_editor_markdown/lib/src/table.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ extension ElementTableExtension on md.Element {
1010
/// The element must have a `table` tag.
1111
///
1212
/// Throws an exception if the element is not a valid table structure.
13-
TableBlockNode asTable() {
13+
TableBlockNode asTable({
14+
Iterable<md.InlineSyntax>? inlineMarkdownSyntaxes,
15+
Iterable<InlineHtmlSyntax>? inlineHtmlSyntaxes,
16+
}) {
1417
if (tag != 'table') {
1518
throw Exception('Cannot parse a table from an element with tag "$tag"');
1619
}
@@ -42,7 +45,11 @@ extension ElementTableExtension on md.Element {
4245
headerNodes.add(
4346
TextNode(
4447
id: Editor.createNodeId(),
45-
text: parseInlineMarkdown(headerCell.textContent),
48+
text: parseInlineMarkdown(
49+
headerCell.textContent,
50+
inlineMarkdownSyntaxes: inlineMarkdownSyntaxes,
51+
inlineHtmlSyntaxes: inlineHtmlSyntaxes,
52+
),
4653
metadata: const {
4754
NodeMetadata.blockType: tableHeaderAttribution,
4855
TextNodeMetadata.textAlign: TextAlign.center,

super_editor_markdown/lib/super_editor_markdown.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export 'src/document_to_markdown_serializer.dart';
2+
export 'src/markdown_inline_parser.dart';
23
export 'src/markdown_inline_upstream_plugin.dart';
34
export 'src/markdown_to_attributed_text_parsing.dart';
45
export 'src/markdown_to_document_parsing.dart';

0 commit comments

Comments
 (0)