Skip to content

Commit 968b4ba

Browse files
committed
✨ added html tags support
1 parent f3c125d commit 968b4ba

File tree

4 files changed

+214
-3
lines changed

4 files changed

+214
-3
lines changed

lib/home.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
77
import 'package:flutter/services.dart';
88
import 'package:markdown_editor/device_preference_notifier.dart';
99
import 'package:markdown_editor/l10n/generated/app_localizations.dart';
10-
import 'package:markdown_editor/widgets/MarkdownBody/custom_image_config.dart';
10+
import 'package:markdown_editor/widgets/MarkdownBody/custom_text_node.dart';
1111
import 'package:markdown_editor/widgets/MarkdownTextInput/markdown_text_input.dart';
1212
import 'package:markdown_widget/markdown_widget.dart';
1313

@@ -226,7 +226,12 @@ class _HomeState extends State<Home> {
226226
padding: const EdgeInsets.all(8),
227227
child: MarkdownBlock(
228228
data: _inputText,
229-
config: config.copy(configs: [CustomImgConfig()]),
229+
generator: MarkdownGenerator(
230+
textGenerator: (node, config, visitor) =>
231+
CustomTextNode(node.textContent, config, visitor),
232+
richTextBuilder: Text.rich,
233+
),
234+
config: config,
230235
),
231236
),
232237
),
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
3+
import 'package:html/dom.dart' as h;
4+
import 'package:html/dom_parsing.dart';
5+
import 'package:html/parser.dart';
6+
import 'package:markdown/markdown.dart' as m;
7+
import 'package:markdown_widget/markdown_widget.dart';
8+
9+
void htmlToMarkdown(h.Node? node, int deep, List<m.Node> mNodes) {
10+
if (node == null) return;
11+
if (node is h.Text) {
12+
mNodes.add(m.Text(node.text));
13+
} else if (node is h.Element) {
14+
final tag = node.localName;
15+
final List<m.Node> children = [];
16+
for (final e in node.children) {
17+
htmlToMarkdown(e, deep + 1, children);
18+
}
19+
m.Element element;
20+
if (tag == MarkdownTag.img.name || tag == 'video') {
21+
element = HtmlElement(tag!, children, node.text);
22+
element.attributes.addAll(node.attributes.cast());
23+
} else {
24+
element = HtmlElement(tag!, children, node.text);
25+
element.attributes.addAll(node.attributes.cast());
26+
}
27+
mNodes.add(element);
28+
}
29+
}
30+
31+
final RegExp tableRep = RegExp(r'<table[^>]*>', multiLine: true);
32+
33+
final RegExp htmlRep = RegExp(r'<[^>]*>', multiLine: true);
34+
35+
///parse [m.Node] to [h.Node]
36+
List<SpanNode> parseHtml(
37+
m.Text node, {
38+
ValueCallback<dynamic>? onError,
39+
WidgetVisitor? visitor,
40+
TextStyle? parentStyle,
41+
}) {
42+
try {
43+
final text = node.textContent.replaceAll(
44+
visitor?.splitRegExp ?? WidgetVisitor.defaultSplitRegExp,
45+
'',
46+
);
47+
if (!text.contains(htmlRep)) return [TextNode(text: node.text)];
48+
final h.DocumentFragment document = parseFragment(text);
49+
return HtmlToSpanVisitor(
50+
visitor: visitor,
51+
parentStyle: parentStyle,
52+
).toVisit(document.nodes.toList());
53+
} catch (e) {
54+
onError?.call(e);
55+
return [TextNode(text: node.text)];
56+
}
57+
}
58+
59+
class HtmlElement extends m.Element {
60+
@override
61+
final String textContent;
62+
63+
HtmlElement(super.tag, super.children, this.textContent);
64+
}
65+
66+
class HtmlToSpanVisitor extends TreeVisitor {
67+
final List<SpanNode> _spans = [];
68+
final List<SpanNode> _spansStack = [];
69+
final WidgetVisitor visitor;
70+
final TextStyle parentStyle;
71+
72+
HtmlToSpanVisitor({WidgetVisitor? visitor, TextStyle? parentStyle})
73+
: visitor = visitor ?? WidgetVisitor(),
74+
parentStyle = parentStyle ?? const TextStyle();
75+
76+
List<SpanNode> toVisit(List<h.Node> nodes) {
77+
_spans.clear();
78+
for (final node in nodes) {
79+
final emptyNode = ConcreteElementNode(style: parentStyle);
80+
_spans.add(emptyNode);
81+
_spansStack.add(emptyNode);
82+
visit(node);
83+
_spansStack.removeLast();
84+
}
85+
final result = List.of(_spans);
86+
_spans.clear();
87+
_spansStack.clear();
88+
return result;
89+
}
90+
91+
@override
92+
void visitText(h.Text node) {
93+
final last = _spansStack.last;
94+
if (last is ElementNode) {
95+
final textNode = TextNode(text: node.text);
96+
last.accept(textNode);
97+
}
98+
}
99+
100+
@override
101+
void visitElement(h.Element node) {
102+
final localName = node.localName ?? '';
103+
final mdElement = m.Element(localName, []);
104+
mdElement.attributes.addAll(node.attributes.cast());
105+
SpanNode spanNode = visitor.getNodeByElement(mdElement, visitor.config);
106+
if (spanNode is! ElementNode) {
107+
final n = ConcreteElementNode(tag: localName, style: parentStyle);
108+
n.accept(spanNode);
109+
spanNode = n;
110+
}
111+
final last = _spansStack.last;
112+
if (last is ElementNode) {
113+
last.accept(spanNode);
114+
}
115+
_spansStack.add(spanNode);
116+
for (final child in node.nodes.toList(growable: false)) {
117+
visit(child);
118+
}
119+
_spansStack.removeLast();
120+
}
121+
}
122+
123+
class CustomTextNode extends ElementNode {
124+
final String text;
125+
final MarkdownConfig config;
126+
final WidgetVisitor visitor;
127+
bool isTable = false;
128+
129+
CustomTextNode(this.text, this.config, this.visitor);
130+
131+
@override
132+
InlineSpan build() {
133+
if (isTable) {
134+
//deal complex table tag with html core widget
135+
return WidgetSpan(child: HtmlWidget(text));
136+
} else {
137+
return super.build();
138+
}
139+
}
140+
141+
@override
142+
void onAccepted(SpanNode parent) {
143+
final textStyle = config.p.textStyle.merge(parentStyle);
144+
children.clear();
145+
if (!text.contains(htmlRep)) {
146+
accept(TextNode(text: text, style: textStyle));
147+
return;
148+
}
149+
//Intercept as table tag
150+
if (text.contains(tableRep)) {
151+
isTable = true;
152+
accept(parent);
153+
return;
154+
}
155+
156+
//The remaining ones are processed by the regular HTML processing.
157+
final spans = parseHtml(
158+
m.Text(text),
159+
visitor: WidgetVisitor(
160+
config: visitor.config,
161+
generators: visitor.generators,
162+
richTextBuilder: visitor.richTextBuilder,
163+
),
164+
parentStyle: parentStyle,
165+
);
166+
for (final element in spans) {
167+
isTable = false;
168+
accept(element);
169+
}
170+
}
171+
}

pubspec.lock

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ packages:
5757
url: "https://pub.dev"
5858
source: hosted
5959
version: "0.3.4+2"
60+
csslib:
61+
dependency: transitive
62+
description:
63+
name: csslib
64+
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
65+
url: "https://pub.dev"
66+
source: hosted
67+
version: "1.0.2"
6068
dbus:
6169
dependency: transitive
6270
description:
@@ -157,6 +165,14 @@ packages:
157165
description: flutter
158166
source: sdk
159167
version: "0.0.0"
168+
flutter_widget_from_html_core:
169+
dependency: "direct main"
170+
description:
171+
name: flutter_widget_from_html_core
172+
sha256: "1120ee6ed3509ceff2d55aa6c6cbc7b6b1291434422de2411b5a59364dd6ff03"
173+
url: "https://pub.dev"
174+
source: hosted
175+
version: "0.17.0"
160176
highlight:
161177
dependency: transitive
162178
description:
@@ -165,6 +181,14 @@ packages:
165181
url: "https://pub.dev"
166182
source: hosted
167183
version: "0.7.0"
184+
html:
185+
dependency: "direct main"
186+
description:
187+
name: html
188+
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
189+
url: "https://pub.dev"
190+
source: hosted
191+
version: "0.15.6"
168192
http:
169193
dependency: transitive
170194
description:
@@ -221,8 +245,16 @@ packages:
221245
url: "https://pub.dev"
222246
source: hosted
223247
version: "6.0.0"
224-
markdown:
248+
logging:
225249
dependency: transitive
250+
description:
251+
name: logging
252+
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
253+
url: "https://pub.dev"
254+
source: hosted
255+
version: "1.3.0"
256+
markdown:
257+
dependency: "direct main"
226258
description:
227259
name: markdown
228260
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"

pubspec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ dependencies:
1717
flutter_localizations:
1818
sdk: flutter
1919
flutter_svg: ^2.2.1
20+
flutter_widget_from_html_core: ^0.17.0
21+
html: ^0.15.6
2022
intl: any
23+
markdown: ^7.3.0
2124
markdown_widget: ^2.3.2+8
2225
permission_handler: ^12.0.1
2326
shared_preferences: ^2.5.3

0 commit comments

Comments
 (0)