Showing
8 changed files
with
655 additions
and
278 deletions
tex_markdown/.vscode/settings.json
0 → 100644
1 | import 'dart:developer'; | 1 | import 'dart:developer'; |
2 | 2 | ||
3 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
4 | +import 'package:flutter/services.dart'; | ||
4 | import 'package:tex_markdown/tex_markdown.dart'; | 5 | import 'package:tex_markdown/tex_markdown.dart'; |
5 | 6 | ||
6 | void main() { | 7 | void main() { |
@@ -105,8 +106,8 @@ class _MyHomePageState extends State<MyHomePage> { | @@ -105,8 +106,8 @@ class _MyHomePageState extends State<MyHomePage> { | ||
105 | log(url, name: "url"); | 106 | log(url, name: "url"); |
106 | }, | 107 | }, |
107 | style: const TextStyle( | 108 | style: const TextStyle( |
108 | - // color: Colors.red, | ||
109 | - ), | 109 | + color: Colors.green, |
110 | + ), | ||
110 | ); | 111 | ); |
111 | }), | 112 | }), |
112 | ], | 113 | ], |
@@ -114,14 +115,37 @@ class _MyHomePageState extends State<MyHomePage> { | @@ -114,14 +115,37 @@ class _MyHomePageState extends State<MyHomePage> { | ||
114 | ), | 115 | ), |
115 | ConstrainedBox( | 116 | ConstrainedBox( |
116 | constraints: const BoxConstraints(maxHeight: 200), | 117 | constraints: const BoxConstraints(maxHeight: 200), |
117 | - child: Padding( | ||
118 | - padding: const EdgeInsets.all(8.0), | ||
119 | - child: TextField( | ||
120 | - decoration: const InputDecoration( | ||
121 | - border: OutlineInputBorder(), label: Text("Type here:")), | ||
122 | - maxLines: null, | ||
123 | - controller: _controller, | ||
124 | - ), | 118 | + child: Stack( |
119 | + children: [ | ||
120 | + Padding( | ||
121 | + padding: const EdgeInsets.all(8.0), | ||
122 | + child: TextField( | ||
123 | + decoration: const InputDecoration( | ||
124 | + border: OutlineInputBorder(), | ||
125 | + label: Text("Type here:")), | ||
126 | + maxLines: null, | ||
127 | + controller: _controller, | ||
128 | + ), | ||
129 | + ), | ||
130 | + IconButton( | ||
131 | + onPressed: () { | ||
132 | + String html = '''<!DOCTYPE html> | ||
133 | +<html lang="en"> | ||
134 | +<head> | ||
135 | +<meta charset="UTF-8"><title>markdown</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css" integrity="sha384-zB1R0rpPzHqg7Kpt0Aljp8JPLqbXI3bhnPWROx27a9N0Ll6ZP/+DiW/UqRcLbRjq" crossorigin="anonymous"> | ||
136 | +<script defer src="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.js" integrity="sha384-y23I5Q6l+B6vatafAwxRu/0oK/79VlbSz7Q9aiSZUvyWYIYsd+qj+o24G5ZU2zJz" crossorigin="anonymous"></script> | ||
137 | +<script defer src="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/contrib/auto-render.min.js" integrity="sha384-kWPLUVMOks5AQFrykwIup5lo0m3iMkkHrD0uJ4H5cjeGihAutqP0yW0J6dpFiVkI" crossorigin="anonymous" onload="renderMathInElement(document.body);"></script> | ||
138 | +</head> | ||
139 | +<body> | ||
140 | +${TexMarkdown.toHtml(_controller.text)} | ||
141 | +</body> | ||
142 | +</html> | ||
143 | +'''; | ||
144 | + Clipboard.setData(ClipboardData(text: html)); | ||
145 | + }, | ||
146 | + icon: const Icon(Icons.html), | ||
147 | + ), | ||
148 | + ], | ||
125 | ), | 149 | ), |
126 | ), | 150 | ), |
127 | ], | 151 | ], |
@@ -238,15 +238,15 @@ packages: | @@ -238,15 +238,15 @@ packages: | ||
238 | path: ".." | 238 | path: ".." |
239 | relative: true | 239 | relative: true |
240 | source: path | 240 | source: path |
241 | - version: "0.0.6" | 241 | + version: "0.0.9" |
242 | tex_text: | 242 | tex_text: |
243 | dependency: transitive | 243 | dependency: transitive |
244 | description: | 244 | description: |
245 | name: tex_text | 245 | name: tex_text |
246 | - sha256: "562415e16b9c46816d8c2ed128fc46b299bb27bca9fac65db0c07535be0d0c60" | 246 | + sha256: "58c556fb09dff7034d6f29b77b3be6b90e7f1257817daa0000c6f1609094490a" |
247 | url: "https://pub.dev" | 247 | url: "https://pub.dev" |
248 | source: hosted | 248 | source: hosted |
249 | - version: "0.0.8" | 249 | + version: "0.1.2" |
250 | tuple: | 250 | tuple: |
251 | dependency: transitive | 251 | dependency: transitive |
252 | description: | 252 | description: |
tex_markdown/lib/markdown_component.dart
0 → 100644
1 | +import 'package:flutter/material.dart'; | ||
2 | +import 'package:tex_text/tex_text.dart'; | ||
3 | +import 'md_widget.dart'; | ||
4 | + | ||
5 | +/// Markdown components | ||
6 | +abstract class MarkdownComponent { | ||
7 | + static final List<MarkdownComponent> components = [ | ||
8 | + HTag(), | ||
9 | + BoldMd(), | ||
10 | + ItalicMd(), | ||
11 | + ATagMd(), | ||
12 | + ImageMd(), | ||
13 | + UnOrderedList(), | ||
14 | + OrderedList(), | ||
15 | + RadioButtonMd(), | ||
16 | + CheckBoxMd(), | ||
17 | + HrLine(), | ||
18 | + TextMd(), | ||
19 | + ]; | ||
20 | + static String toHtml(String text) { | ||
21 | + String html = ""; | ||
22 | + text.split(RegExp(r"\n+")).forEach((element) { | ||
23 | + for (var each in components) { | ||
24 | + if (each.exp.hasMatch(element.trim())) { | ||
25 | + if (each is InlineMd) { | ||
26 | + html += "${each.toHtml(element)}\n"; | ||
27 | + break; | ||
28 | + } else if (each is BlockMd) { | ||
29 | + html += "${each.toHtml(element)}\n"; | ||
30 | + break; | ||
31 | + } | ||
32 | + } | ||
33 | + } | ||
34 | + }); | ||
35 | + return html; | ||
36 | + } | ||
37 | + | ||
38 | + static Widget generate( | ||
39 | + BuildContext context, | ||
40 | + String text, | ||
41 | + TextStyle? style, | ||
42 | + final void Function(String url, String title)? onLinkTab, | ||
43 | + ) { | ||
44 | + List<Widget> children = []; | ||
45 | + List<InlineSpan> spans = []; | ||
46 | + text.split(RegExp(r"\n+")).forEach( | ||
47 | + (element) { | ||
48 | + for (var each in components) { | ||
49 | + if (each.exp.hasMatch(element.trim())) { | ||
50 | + if (each is InlineMd) { | ||
51 | + if (spans.isNotEmpty) { | ||
52 | + spans.add( | ||
53 | + TextSpan(text: " ", style: style), | ||
54 | + ); | ||
55 | + } | ||
56 | + spans.add(each.inlineSpan( | ||
57 | + context, | ||
58 | + element, | ||
59 | + style, | ||
60 | + onLinkTab, | ||
61 | + )); | ||
62 | + } else { | ||
63 | + if (spans.isNotEmpty) { | ||
64 | + children.add( | ||
65 | + Text.rich( | ||
66 | + TextSpan(children: List.from(spans)), | ||
67 | + textAlign: TextAlign.left, | ||
68 | + ), | ||
69 | + ); | ||
70 | + spans.clear(); | ||
71 | + } | ||
72 | + if (each is BlockMd) { | ||
73 | + children.add( | ||
74 | + each.build(context, element, style, onLinkTab), | ||
75 | + ); | ||
76 | + } | ||
77 | + } | ||
78 | + return; | ||
79 | + } | ||
80 | + } | ||
81 | + }, | ||
82 | + ); | ||
83 | + if (spans.isNotEmpty) { | ||
84 | + children.add( | ||
85 | + Text.rich( | ||
86 | + TextSpan(children: List.from(spans)), | ||
87 | + textAlign: TextAlign.left, | ||
88 | + ), | ||
89 | + ); | ||
90 | + } | ||
91 | + return Column( | ||
92 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
93 | + children: children, | ||
94 | + ); | ||
95 | + } | ||
96 | + | ||
97 | + RegExp get exp; | ||
98 | + bool get inline; | ||
99 | +} | ||
100 | + | ||
101 | +abstract class InlineMd extends MarkdownComponent { | ||
102 | + @override | ||
103 | + bool get inline => true; | ||
104 | + InlineSpan inlineSpan( | ||
105 | + BuildContext context, | ||
106 | + String text, | ||
107 | + TextStyle? style, | ||
108 | + final void Function(String url, String title)? onLinkTab, | ||
109 | + ); | ||
110 | + String toHtml(String text); | ||
111 | +} | ||
112 | + | ||
113 | +abstract class BlockMd extends MarkdownComponent { | ||
114 | + @override | ||
115 | + bool get inline => false; | ||
116 | + Widget build( | ||
117 | + BuildContext context, | ||
118 | + String text, | ||
119 | + TextStyle? style, | ||
120 | + final void Function(String url, String title)? onLinkTab, | ||
121 | + ); | ||
122 | + String toHtml(String text); | ||
123 | +} | ||
124 | + | ||
125 | +class HTag extends BlockMd { | ||
126 | + @override | ||
127 | + final RegExp exp = RegExp(r"^(#{1,6})\s([^\n]+)$"); | ||
128 | + @override | ||
129 | + Widget build( | ||
130 | + BuildContext context, | ||
131 | + String text, | ||
132 | + TextStyle? style, | ||
133 | + final void Function(String url, String title)? onLinkTab, | ||
134 | + ) { | ||
135 | + var match = exp.firstMatch(text.trim()); | ||
136 | + return Column( | ||
137 | + children: [ | ||
138 | + Row( | ||
139 | + children: [ | ||
140 | + Expanded( | ||
141 | + child: TexText( | ||
142 | + "${match?[2]}", | ||
143 | + style: [ | ||
144 | + Theme.of(context) | ||
145 | + .textTheme | ||
146 | + .headlineLarge | ||
147 | + ?.copyWith(color: style?.color), | ||
148 | + Theme.of(context) | ||
149 | + .textTheme | ||
150 | + .headlineMedium | ||
151 | + ?.copyWith(color: style?.color), | ||
152 | + Theme.of(context) | ||
153 | + .textTheme | ||
154 | + .headlineSmall | ||
155 | + ?.copyWith(color: style?.color), | ||
156 | + Theme.of(context) | ||
157 | + .textTheme | ||
158 | + .titleLarge | ||
159 | + ?.copyWith(color: style?.color), | ||
160 | + Theme.of(context) | ||
161 | + .textTheme | ||
162 | + .titleMedium | ||
163 | + ?.copyWith(color: style?.color), | ||
164 | + Theme.of(context) | ||
165 | + .textTheme | ||
166 | + .titleSmall | ||
167 | + ?.copyWith(color: style?.color), | ||
168 | + ][match![1]!.length - 1], | ||
169 | + ), | ||
170 | + ), | ||
171 | + ], | ||
172 | + ), | ||
173 | + if (match[1]!.length == 1) const Divider(height: 6), | ||
174 | + ], | ||
175 | + ); | ||
176 | + } | ||
177 | + | ||
178 | + @override | ||
179 | + String toHtml(String text) { | ||
180 | + var match = exp.firstMatch(text.trim()); | ||
181 | + return "<h${match![1]!.length}>${TexText.toHtmlData(match[2].toString().trim())}<h${match[1]!.length}>"; | ||
182 | + } | ||
183 | +} | ||
184 | + | ||
185 | +class HrLine extends BlockMd { | ||
186 | + @override | ||
187 | + final RegExp exp = RegExp(r"^(--)[-]+$"); | ||
188 | + @override | ||
189 | + Widget build( | ||
190 | + BuildContext context, | ||
191 | + String text, | ||
192 | + TextStyle? style, | ||
193 | + final void Function(String url, String title)? onLinkTab, | ||
194 | + ) { | ||
195 | + return const Divider( | ||
196 | + height: 5, | ||
197 | + ); | ||
198 | + } | ||
199 | + | ||
200 | + @override | ||
201 | + String toHtml(String text) { | ||
202 | + return "<hr/>"; | ||
203 | + } | ||
204 | +} | ||
205 | + | ||
206 | +class CheckBoxMd extends BlockMd { | ||
207 | + get onLinkTab => null; | ||
208 | + | ||
209 | + @override | ||
210 | + Widget build( | ||
211 | + BuildContext context, | ||
212 | + String text, | ||
213 | + TextStyle? style, | ||
214 | + final void Function(String url, String title)? onLinkTab, | ||
215 | + ) { | ||
216 | + var match = exp.firstMatch(text.trim()); | ||
217 | + return Row( | ||
218 | + crossAxisAlignment: CrossAxisAlignment.center, | ||
219 | + mainAxisSize: MainAxisSize.min, | ||
220 | + children: [ | ||
221 | + Padding( | ||
222 | + padding: const EdgeInsets.symmetric(horizontal: 10), | ||
223 | + child: Checkbox( | ||
224 | + // value: true, | ||
225 | + value: ("${match?[1]}" == "x"), | ||
226 | + onChanged: (value) {}, | ||
227 | + fillColor: ButtonStyleButton.allOrNull(style?.color), | ||
228 | + ), | ||
229 | + ), | ||
230 | + Expanded( | ||
231 | + child: MdWidget( | ||
232 | + "${match?[2]}", | ||
233 | + onLinkTab: onLinkTab, | ||
234 | + style: style, | ||
235 | + ), | ||
236 | + ), | ||
237 | + ], | ||
238 | + ); | ||
239 | + } | ||
240 | + | ||
241 | + @override | ||
242 | + final RegExp exp = RegExp(r"^\[(\x?)\]\s(\S.*)$"); | ||
243 | + | ||
244 | + @override | ||
245 | + String toHtml(String text) { | ||
246 | + var match = exp.firstMatch(text.trim()); | ||
247 | + return '<p><input type="checkbox"${("${match?[1]}" == "x") ? "checked" : ""}>${MdWidget.toHtml((match?[2]).toString()).trim()}</p>'; | ||
248 | + } | ||
249 | +} | ||
250 | + | ||
251 | +class RadioButtonMd extends BlockMd { | ||
252 | + get onLinkTab => null; | ||
253 | + | ||
254 | + @override | ||
255 | + Widget build( | ||
256 | + BuildContext context, | ||
257 | + String text, | ||
258 | + TextStyle? style, | ||
259 | + final void Function(String url, String title)? onLinkTab, | ||
260 | + ) { | ||
261 | + var match = exp.firstMatch(text.trim()); | ||
262 | + return Row( | ||
263 | + crossAxisAlignment: CrossAxisAlignment.center, | ||
264 | + mainAxisSize: MainAxisSize.min, | ||
265 | + children: [ | ||
266 | + Padding( | ||
267 | + padding: const EdgeInsets.symmetric(horizontal: 10), | ||
268 | + child: Radio( | ||
269 | + value: true, | ||
270 | + groupValue: ("${match?[1]}" == "x"), | ||
271 | + onChanged: (value) {}, | ||
272 | + fillColor: ButtonStyleButton.allOrNull(style?.color), | ||
273 | + ), | ||
274 | + ), | ||
275 | + Expanded( | ||
276 | + child: MdWidget( | ||
277 | + "${match?[2]}", | ||
278 | + onLinkTab: onLinkTab, | ||
279 | + style: style, | ||
280 | + ), | ||
281 | + ), | ||
282 | + ], | ||
283 | + ); | ||
284 | + } | ||
285 | + | ||
286 | + @override | ||
287 | + final RegExp exp = RegExp(r"^\((\x?)\)\s(\S.*)$"); | ||
288 | + | ||
289 | + @override | ||
290 | + String toHtml(String text) { | ||
291 | + var match = exp.firstMatch(text.trim()); | ||
292 | + return '<p><input type="radio"${("${match?[1]}" == "x") ? "checked" : ""}>${MdWidget.toHtml((match?[2]).toString().trim())}</p>'; | ||
293 | + } | ||
294 | +} | ||
295 | + | ||
296 | +class UnOrderedList extends BlockMd { | ||
297 | + get onLinkTab => null; | ||
298 | + | ||
299 | + @override | ||
300 | + Widget build( | ||
301 | + BuildContext context, | ||
302 | + String text, | ||
303 | + TextStyle? style, | ||
304 | + final void Function(String url, String title)? onLinkTab, | ||
305 | + ) { | ||
306 | + var match = exp.firstMatch(text.trim()); | ||
307 | + return Row( | ||
308 | + crossAxisAlignment: CrossAxisAlignment.center, | ||
309 | + mainAxisSize: MainAxisSize.max, | ||
310 | + children: [ | ||
311 | + Padding( | ||
312 | + padding: const EdgeInsets.symmetric(horizontal: 10), | ||
313 | + child: Icon( | ||
314 | + Icons.circle, | ||
315 | + color: style?.color, | ||
316 | + size: 8, | ||
317 | + ), | ||
318 | + ), | ||
319 | + Expanded( | ||
320 | + child: MdWidget( | ||
321 | + "${match?[2]}", | ||
322 | + onLinkTab: onLinkTab, | ||
323 | + style: style, | ||
324 | + ), | ||
325 | + ), | ||
326 | + ], | ||
327 | + ); | ||
328 | + } | ||
329 | + | ||
330 | + @override | ||
331 | + final RegExp exp = RegExp(r"^(\-|\*)\s([^\n]+)$"); | ||
332 | + | ||
333 | + @override | ||
334 | + String toHtml(String text) { | ||
335 | + var match = exp.firstMatch(text.trim()); | ||
336 | + return "<ul><li>${MdWidget.toHtml((match?[2]).toString())}</li></ul>"; | ||
337 | + } | ||
338 | +} | ||
339 | + | ||
340 | +class OrderedList extends BlockMd { | ||
341 | + @override | ||
342 | + final RegExp exp = RegExp(r"^([0-9]+\.)\s([^\n]+)$"); | ||
343 | + | ||
344 | + get onLinkTab => null; | ||
345 | + | ||
346 | + @override | ||
347 | + Widget build( | ||
348 | + BuildContext context, | ||
349 | + String text, | ||
350 | + TextStyle? style, | ||
351 | + final void Function(String url, String title)? onLinkTab, | ||
352 | + ) { | ||
353 | + var match = exp.firstMatch(text.trim()); | ||
354 | + return Row( | ||
355 | + crossAxisAlignment: CrossAxisAlignment.center, | ||
356 | + mainAxisSize: MainAxisSize.max, | ||
357 | + children: [ | ||
358 | + Padding( | ||
359 | + padding: const EdgeInsets.symmetric(horizontal: 11), | ||
360 | + child: Text( | ||
361 | + "${match?[1]}", | ||
362 | + style: (style ?? const TextStyle()) | ||
363 | + .copyWith(fontWeight: FontWeight.bold), | ||
364 | + ), | ||
365 | + ), | ||
366 | + Expanded( | ||
367 | + child: MdWidget( | ||
368 | + "${match?[2]}", | ||
369 | + onLinkTab: onLinkTab, | ||
370 | + style: style, | ||
371 | + ), | ||
372 | + ), | ||
373 | + ], | ||
374 | + ); | ||
375 | + } | ||
376 | + | ||
377 | + @override | ||
378 | + String toHtml(String text) { | ||
379 | + var match = exp.firstMatch(text.trim()); | ||
380 | + return '<ol start="${match?[1]}"><li>${MdWidget.toHtml((match?[2]).toString())}</li></ol>'; | ||
381 | + } | ||
382 | +} | ||
383 | + | ||
384 | +class BoldMd extends InlineMd { | ||
385 | + @override | ||
386 | + final RegExp exp = RegExp(r"^\*{2}(([\S^\*].*)?[\S^\*])\*{2}$"); | ||
387 | + | ||
388 | + @override | ||
389 | + InlineSpan inlineSpan( | ||
390 | + BuildContext context, | ||
391 | + String text, | ||
392 | + TextStyle? style, | ||
393 | + final void Function(String url, String title)? onLinkTab, | ||
394 | + ) { | ||
395 | + var match = exp.firstMatch(text.trim()); | ||
396 | + return WidgetSpan( | ||
397 | + // text: "${match?[1]}", | ||
398 | + child: TexText( | ||
399 | + "${match?[1]}", | ||
400 | + style: style?.copyWith(fontWeight: FontWeight.bold) ?? | ||
401 | + const TextStyle(fontWeight: FontWeight.bold), | ||
402 | + ), | ||
403 | + style: style?.copyWith(fontWeight: FontWeight.bold) ?? | ||
404 | + const TextStyle(fontWeight: FontWeight.bold), | ||
405 | + ); | ||
406 | + } | ||
407 | + | ||
408 | + @override | ||
409 | + String toHtml(String text) { | ||
410 | + var match = exp.firstMatch(text.trim()); | ||
411 | + return '<b>${TexText.toHtmlData((match?[1]).toString())}</b>'; | ||
412 | + } | ||
413 | +} | ||
414 | + | ||
415 | +class ItalicMd extends InlineMd { | ||
416 | + @override | ||
417 | + final RegExp exp = RegExp(r"^\*{1}(([\S^\*].*)?[\S^\*])\*{1}$"); | ||
418 | + | ||
419 | + @override | ||
420 | + InlineSpan inlineSpan( | ||
421 | + BuildContext context, | ||
422 | + String text, | ||
423 | + TextStyle? style, | ||
424 | + final void Function(String url, String title)? onLinkTab, | ||
425 | + ) { | ||
426 | + var match = exp.firstMatch(text.trim()); | ||
427 | + return WidgetSpan( | ||
428 | + child: TexText( | ||
429 | + "${match?[1]}", | ||
430 | + style: | ||
431 | + (style ?? const TextStyle()).copyWith(fontStyle: FontStyle.italic), | ||
432 | + ), | ||
433 | + style: (style ?? const TextStyle()).copyWith(fontStyle: FontStyle.italic), | ||
434 | + ); | ||
435 | + } | ||
436 | + | ||
437 | + @override | ||
438 | + String toHtml(String text) { | ||
439 | + var match = exp.firstMatch(text.trim()); | ||
440 | + return '<i>${TexText.toHtmlData((match?[1]).toString())}</i>'; | ||
441 | + } | ||
442 | +} | ||
443 | + | ||
444 | +class ATagMd extends InlineMd { | ||
445 | + @override | ||
446 | + final RegExp exp = RegExp(r"^\[([^\s\*].*[^\s]?)?\]\(([^\s\*]+)?\)$"); | ||
447 | + | ||
448 | + @override | ||
449 | + InlineSpan inlineSpan( | ||
450 | + BuildContext context, | ||
451 | + String text, | ||
452 | + TextStyle? style, | ||
453 | + final void Function(String url, String title)? onLinkTab, | ||
454 | + ) { | ||
455 | + var match = exp.firstMatch(text.trim()); | ||
456 | + if (match?[1] == null && match?[2] == null) { | ||
457 | + return const TextSpan(); | ||
458 | + } | ||
459 | + return WidgetSpan( | ||
460 | + child: GestureDetector( | ||
461 | + onTap: () { | ||
462 | + if (onLinkTab == null) { | ||
463 | + return; | ||
464 | + } | ||
465 | + onLinkTab("${match?[2]}", "${match?[1]}"); | ||
466 | + }, | ||
467 | + child: TexText( | ||
468 | + "${match?[1]}", | ||
469 | + style: (style ?? const TextStyle()).copyWith( | ||
470 | + color: Colors.blueAccent, | ||
471 | + decorationColor: Colors.blue, | ||
472 | + decoration: TextDecoration.underline, | ||
473 | + ), | ||
474 | + ), | ||
475 | + ), | ||
476 | + ); | ||
477 | + } | ||
478 | + | ||
479 | + @override | ||
480 | + String toHtml(String text) { | ||
481 | + var match = exp.firstMatch(text.trim()); | ||
482 | + return '<a href="${match?[2]}">${TexText.toHtmlData((match?[1]).toString())}</a>'; | ||
483 | + } | ||
484 | +} | ||
485 | + | ||
486 | +class ImageMd extends InlineMd { | ||
487 | + @override | ||
488 | + final RegExp exp = RegExp(r"^\!\[([^\s].*[^\s]?)?\]\(([^\s]+)\)$"); | ||
489 | + | ||
490 | + @override | ||
491 | + InlineSpan inlineSpan( | ||
492 | + BuildContext context, | ||
493 | + String text, | ||
494 | + TextStyle? style, | ||
495 | + final void Function(String url, String title)? onLinkTab, | ||
496 | + ) { | ||
497 | + var match = exp.firstMatch(text.trim()); | ||
498 | + double? height; | ||
499 | + double? width; | ||
500 | + if (match?[1] != null) { | ||
501 | + var size = RegExp(r"^([0-9]+)?x?([0-9]+)?") | ||
502 | + .firstMatch(match![1].toString().trim()); | ||
503 | + width = double.tryParse(size?[1]?.toString() ?? 'a'); | ||
504 | + height = double.tryParse(size?[2]?.toString() ?? 'a'); | ||
505 | + } | ||
506 | + return WidgetSpan( | ||
507 | + // alignment: PlaceholderAlignment.middle, | ||
508 | + child: SizedBox( | ||
509 | + width: width, | ||
510 | + height: height, | ||
511 | + child: Image.network( | ||
512 | + "${match?[2]}", | ||
513 | + fit: BoxFit.fill, | ||
514 | + errorBuilder: (context, error, stackTrace) { | ||
515 | + return Placeholder( | ||
516 | + color: Theme.of(context).colorScheme.error, | ||
517 | + child: Text( | ||
518 | + "${match?[2]}\n$error", | ||
519 | + style: style, | ||
520 | + ), | ||
521 | + ); | ||
522 | + }, | ||
523 | + ), | ||
524 | + ), | ||
525 | + ); | ||
526 | + } | ||
527 | + | ||
528 | + @override | ||
529 | + String toHtml(String text) { | ||
530 | + var match = exp.firstMatch(text.trim()); | ||
531 | + return '<img src="${match?[2]}">'; | ||
532 | + } | ||
533 | +} | ||
534 | + | ||
535 | +class TextMd extends InlineMd { | ||
536 | + @override | ||
537 | + final RegExp exp = RegExp(".*"); | ||
538 | + | ||
539 | + @override | ||
540 | + InlineSpan inlineSpan(BuildContext context, String text, TextStyle? style, | ||
541 | + void Function(String url, String title)? onLinkTab) { | ||
542 | + return WidgetSpan( | ||
543 | + child: TexText( | ||
544 | + text, | ||
545 | + style: style, | ||
546 | + )); | ||
547 | + } | ||
548 | + | ||
549 | + @override | ||
550 | + String toHtml(String text) { | ||
551 | + return TexText.toHtmlData(text); | ||
552 | + } | ||
553 | +} |
1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
2 | -import 'package:tex_text/tex_text.dart'; | 2 | +import 'package:tex_markdown/markdown_component.dart'; |
3 | 3 | ||
4 | /// It creates a markdown widget closed to each other. | 4 | /// It creates a markdown widget closed to each other. |
5 | class MdWidget extends StatelessWidget { | 5 | class MdWidget extends StatelessWidget { |
@@ -9,18 +9,43 @@ class MdWidget extends StatelessWidget { | @@ -9,18 +9,43 @@ class MdWidget extends StatelessWidget { | ||
9 | final TextStyle? style; | 9 | final TextStyle? style; |
10 | final void Function(String url, String title)? onLinkTab; | 10 | final void Function(String url, String title)? onLinkTab; |
11 | final bool followLinkColor; | 11 | final bool followLinkColor; |
12 | + | ||
13 | + /// To convert markdown text to html text. | ||
14 | + static String toHtml(String text) { | ||
15 | + final RegExp table = RegExp( | ||
16 | + r"^(((\|[^\n\|]+\|)((([^\n\|]+\|)+)?))(\n(((\|[^\n\|]+\|)(([^\n\|]+\|)+)?)))+)?$", | ||
17 | + ); | ||
18 | + if (table.hasMatch(text)) { | ||
19 | + final String value = text.trim().splitMapJoin( | ||
20 | + RegExp(r'^\||\|\n\||\|$'), | ||
21 | + onMatch: (p0) => "\n", | ||
22 | + onNonMatch: (p0) { | ||
23 | + if (p0.trim().isEmpty) { | ||
24 | + return ""; | ||
25 | + } | ||
26 | + // return p0; | ||
27 | + return '<tr>${p0.trim().splitMapJoin( | ||
28 | + '|', | ||
29 | + onMatch: (p0) { | ||
30 | + return ""; | ||
31 | + }, | ||
32 | + onNonMatch: (p0) { | ||
33 | + return '<td>$p0</td>'; | ||
34 | + }, | ||
35 | + )}</tr>'; | ||
36 | + }, | ||
37 | + ); | ||
38 | + return ''' | ||
39 | +<table border="1" cellspacing="0"> | ||
40 | +$value | ||
41 | +</table> | ||
42 | +'''; | ||
43 | + } | ||
44 | + return MarkdownComponent.toHtml(text); | ||
45 | + } | ||
46 | + | ||
12 | @override | 47 | @override |
13 | Widget build(BuildContext context) { | 48 | Widget build(BuildContext context) { |
14 | - final RegExp h = RegExp(r"^(#{1,6})\s([^\n]+)$"); | ||
15 | - final RegExp b = RegExp(r"^\*{2}(([\S^\*].*)?[\S^\*])\*{2}$"); | ||
16 | - final RegExp i = RegExp(r"^\*{1}(([\S^\*].*)?[\S^\*])\*{1}$"); | ||
17 | - final RegExp a = RegExp(r"^\[([^\s\*].*[^\s]?)?\]\(([^\s\*]+)?\)$"); | ||
18 | - final RegExp img = RegExp(r"^\!\[([^\s].*[^\s]?)?\]\(([^\s]+)\)$"); | ||
19 | - final RegExp ul = RegExp(r"^(\-|\*)\s([^\n]+)$"); | ||
20 | - final RegExp ol = RegExp(r"^([0-9]+\.)\s([^\n]+)$"); | ||
21 | - final RegExp rb = RegExp(r"^\((\x?)\)\s(\S.*)$"); | ||
22 | - final RegExp cb = RegExp(r"^\[(\x?)\]\s(\S.*)$"); | ||
23 | - final RegExp hr = RegExp(r"^(--)[-]+$"); | ||
24 | final RegExp table = RegExp( | 49 | final RegExp table = RegExp( |
25 | r"^(((\|[^\n\|]+\|)((([^\n\|]+\|)+)?))(\n(((\|[^\n\|]+\|)(([^\n\|]+\|)+)?)))+)?$", | 50 | r"^(((\|[^\n\|]+\|)((([^\n\|]+\|)+)?))(\n(((\|[^\n\|]+\|)(([^\n\|]+\|)+)?)))+)?$", |
26 | ); | 51 | ); |
@@ -42,7 +67,6 @@ class MdWidget extends StatelessWidget { | @@ -42,7 +67,6 @@ class MdWidget extends StatelessWidget { | ||
42 | } | 67 | } |
43 | } | 68 | } |
44 | if (maxCol == 0) { | 69 | if (maxCol == 0) { |
45 | - // return const SizedBox(); | ||
46 | return Text("", style: style); | 70 | return Text("", style: style); |
47 | } | 71 | } |
48 | return Table( | 72 | return Table( |
@@ -69,256 +93,6 @@ class MdWidget extends StatelessWidget { | @@ -69,256 +93,6 @@ class MdWidget extends StatelessWidget { | ||
69 | .toList(), | 93 | .toList(), |
70 | ); | 94 | ); |
71 | } | 95 | } |
72 | - if (h.hasMatch(exp)) { | ||
73 | - var match = h.firstMatch(exp.trim()); | ||
74 | - return Column( | ||
75 | - children: [ | ||
76 | - Row( | ||
77 | - children: [ | ||
78 | - Expanded( | ||
79 | - child: TexText( | ||
80 | - "${match?[2]}", | ||
81 | - style: [ | ||
82 | - Theme.of(context) | ||
83 | - .textTheme | ||
84 | - .headlineLarge | ||
85 | - ?.copyWith(color: style?.color), | ||
86 | - Theme.of(context) | ||
87 | - .textTheme | ||
88 | - .headlineMedium | ||
89 | - ?.copyWith(color: style?.color), | ||
90 | - Theme.of(context) | ||
91 | - .textTheme | ||
92 | - .headlineSmall | ||
93 | - ?.copyWith(color: style?.color), | ||
94 | - Theme.of(context) | ||
95 | - .textTheme | ||
96 | - .titleLarge | ||
97 | - ?.copyWith(color: style?.color), | ||
98 | - Theme.of(context) | ||
99 | - .textTheme | ||
100 | - .titleMedium | ||
101 | - ?.copyWith(color: style?.color), | ||
102 | - Theme.of(context) | ||
103 | - .textTheme | ||
104 | - .titleSmall | ||
105 | - ?.copyWith(color: style?.color), | ||
106 | - ][match![1]!.length - 1], | ||
107 | - ), | ||
108 | - ), | ||
109 | - ], | ||
110 | - ), | ||
111 | - if (match[1]!.length == 1) const Divider(height: 6), | ||
112 | - ], | ||
113 | - ); | ||
114 | - } | ||
115 | - if (hr.hasMatch(exp)) { | ||
116 | - return const Divider( | ||
117 | - height: 5, | ||
118 | - ); | ||
119 | - } | ||
120 | - if (cb.hasMatch(exp)) { | ||
121 | - var match = cb.firstMatch(exp.trim()); | ||
122 | - return Row( | ||
123 | - crossAxisAlignment: CrossAxisAlignment.center, | ||
124 | - mainAxisSize: MainAxisSize.min, | ||
125 | - children: [ | ||
126 | - Padding( | ||
127 | - padding: const EdgeInsets.symmetric(horizontal: 10), | ||
128 | - child: Checkbox( | ||
129 | - // value: true, | ||
130 | - value: ("${match?[1]}" == "x"), | ||
131 | - onChanged: (value) {}, | ||
132 | - fillColor: ButtonStyleButton.allOrNull(style?.color), | ||
133 | - ), | ||
134 | - ), | ||
135 | - Expanded( | ||
136 | - child: MdWidget( | ||
137 | - "${match?[2]}", | ||
138 | - onLinkTab: onLinkTab, | ||
139 | - style: style, | ||
140 | - ), | ||
141 | - ), | ||
142 | - ], | ||
143 | - ); | ||
144 | - } | ||
145 | - if (rb.hasMatch(exp)) { | ||
146 | - var match = rb.firstMatch(exp.trim()); | ||
147 | - return Row( | ||
148 | - crossAxisAlignment: CrossAxisAlignment.center, | ||
149 | - mainAxisSize: MainAxisSize.min, | ||
150 | - children: [ | ||
151 | - Padding( | ||
152 | - padding: const EdgeInsets.symmetric(horizontal: 10), | ||
153 | - child: Radio( | ||
154 | - value: true, | ||
155 | - groupValue: ("${match?[1]}" == "x"), | ||
156 | - onChanged: (value) {}, | ||
157 | - fillColor: ButtonStyleButton.allOrNull(style?.color), | ||
158 | - ), | ||
159 | - ), | ||
160 | - Expanded( | ||
161 | - child: MdWidget( | ||
162 | - "${match?[2]}", | ||
163 | - onLinkTab: onLinkTab, | ||
164 | - style: style, | ||
165 | - ), | ||
166 | - ), | ||
167 | - ], | ||
168 | - ); | ||
169 | - } | ||
170 | - if (ul.hasMatch(exp)) { | ||
171 | - var match = ul.firstMatch(exp.trim()); | ||
172 | - return Row( | ||
173 | - crossAxisAlignment: CrossAxisAlignment.center, | ||
174 | - mainAxisSize: MainAxisSize.max, | ||
175 | - children: [ | ||
176 | - Padding( | ||
177 | - padding: const EdgeInsets.symmetric(horizontal: 10), | ||
178 | - child: Icon( | ||
179 | - Icons.circle, | ||
180 | - color: style?.color, | ||
181 | - size: 8, | ||
182 | - ), | ||
183 | - ), | ||
184 | - Expanded( | ||
185 | - child: MdWidget( | ||
186 | - "${match?[2]}", | ||
187 | - onLinkTab: onLinkTab, | ||
188 | - style: style, | ||
189 | - ), | ||
190 | - ), | ||
191 | - ], | ||
192 | - ); | ||
193 | - } | ||
194 | - if (ol.hasMatch(exp)) { | ||
195 | - var match = ol.firstMatch(exp.trim()); | ||
196 | - return Row( | ||
197 | - crossAxisAlignment: CrossAxisAlignment.center, | ||
198 | - mainAxisSize: MainAxisSize.max, | ||
199 | - children: [ | ||
200 | - Padding( | ||
201 | - padding: const EdgeInsets.symmetric(horizontal: 11), | ||
202 | - child: Text( | ||
203 | - "${match?[1]}", | ||
204 | - style: (style ?? const TextStyle()) | ||
205 | - .copyWith(fontWeight: FontWeight.bold), | ||
206 | - ), | ||
207 | - ), | ||
208 | - Expanded( | ||
209 | - child: MdWidget( | ||
210 | - "${match?[2]}", | ||
211 | - onLinkTab: onLinkTab, | ||
212 | - style: style, | ||
213 | - ), | ||
214 | - ), | ||
215 | - ], | ||
216 | - ); | ||
217 | - } | ||
218 | - if (b.hasMatch(exp)) { | ||
219 | - var match = b.firstMatch(exp.trim()); | ||
220 | - return TexText( | ||
221 | - "${match?[1]}", | ||
222 | - style: style?.copyWith(fontWeight: FontWeight.bold) ?? | ||
223 | - const TextStyle(fontWeight: FontWeight.bold), | ||
224 | - ); | ||
225 | - } | ||
226 | - if (i.hasMatch(exp)) { | ||
227 | - var match = i.firstMatch(exp.trim()); | ||
228 | - return TexText( | ||
229 | - "${match?[1]}", | ||
230 | - style: | ||
231 | - (style ?? const TextStyle()).copyWith(fontStyle: FontStyle.italic), | ||
232 | - ); | ||
233 | - } | ||
234 | - if (a.hasMatch(exp)) { | ||
235 | - var match = a.firstMatch(exp.trim()); | ||
236 | - if (match?[1] == null && match?[2] == null) { | ||
237 | - return const SizedBox(); | ||
238 | - } | ||
239 | - return GestureDetector( | ||
240 | - onTap: () { | ||
241 | - if (onLinkTab == null) { | ||
242 | - return; | ||
243 | - } | ||
244 | - onLinkTab!("${match?[2]}", "${match?[1]}"); | ||
245 | - }, | ||
246 | - child: MdWidget( | ||
247 | - "${match?[1]}", | ||
248 | - onLinkTab: onLinkTab, | ||
249 | - style: ((followLinkColor && style != null) | ||
250 | - ? style | ||
251 | - : const TextStyle(color: Colors.blueAccent)) | ||
252 | - ?.copyWith(decoration: TextDecoration.underline), | ||
253 | - ), | ||
254 | - ); | ||
255 | - } | ||
256 | - if (img.hasMatch(exp)) { | ||
257 | - var match = img.firstMatch(exp.trim()); | ||
258 | - double? height; | ||
259 | - double? width; | ||
260 | - if (match?[1] != null) { | ||
261 | - var size = RegExp(r"^([0-9]+)?x?([0-9]+)?") | ||
262 | - .firstMatch(match![1].toString().trim()); | ||
263 | - width = double.tryParse(size?[1]?.toString() ?? 'a'); | ||
264 | - height = double.tryParse(size?[2]?.toString() ?? 'a'); | ||
265 | - } | ||
266 | - return SizedBox( | ||
267 | - width: width, | ||
268 | - height: height, | ||
269 | - child: Image.network( | ||
270 | - "${match?[2]}", | ||
271 | - fit: BoxFit.fill, | ||
272 | - errorBuilder: (context, error, stackTrace) { | ||
273 | - return Placeholder( | ||
274 | - color: Theme.of(context).colorScheme.error, | ||
275 | - child: Text( | ||
276 | - "${match?[2]}\n$error", | ||
277 | - style: style, | ||
278 | - ), | ||
279 | - ); | ||
280 | - }, | ||
281 | - ), | ||
282 | - ); | ||
283 | - } | ||
284 | - List<String> expL = exp.split('\n'); | ||
285 | - // .map( | ||
286 | - // (e) => e.trim(), | ||
287 | - // ) | ||
288 | - // .toList(); | ||
289 | - bool hasMatch = false; | ||
290 | - | ||
291 | - for (final each in expL) { | ||
292 | - if (a.hasMatch(each) || | ||
293 | - b.hasMatch(each) || | ||
294 | - i.hasMatch(each) || | ||
295 | - h.hasMatch(each) || | ||
296 | - hr.hasMatch(each) || | ||
297 | - ol.hasMatch(each) || | ||
298 | - rb.hasMatch(each) || | ||
299 | - cb.hasMatch(each) || | ||
300 | - img.hasMatch(each) || | ||
301 | - ul.hasMatch(each)) { | ||
302 | - hasMatch = true; | ||
303 | - } | ||
304 | - } | ||
305 | - if (hasMatch) { | ||
306 | - return Wrap( | ||
307 | - crossAxisAlignment: WrapCrossAlignment.center, | ||
308 | - spacing: 10, | ||
309 | - children: exp | ||
310 | - .split("\n") | ||
311 | - .map<Widget>((e) => MdWidget( | ||
312 | - e, | ||
313 | - onLinkTab: onLinkTab, | ||
314 | - style: style, | ||
315 | - )) | ||
316 | - .toList(), | ||
317 | - ); | ||
318 | - } | ||
319 | - return TexText( | ||
320 | - exp, | ||
321 | - style: style, | ||
322 | - ); | 96 | + return MarkdownComponent.generate(context, exp, style, onLinkTab); |
323 | } | 97 | } |
324 | } | 98 | } |
@@ -17,6 +17,13 @@ class TexMarkdown extends StatelessWidget { | @@ -17,6 +17,13 @@ class TexMarkdown extends StatelessWidget { | ||
17 | final TextStyle? style; | 17 | final TextStyle? style; |
18 | final void Function(String url, String title)? onLinkTab; | 18 | final void Function(String url, String title)? onLinkTab; |
19 | final bool followLinkColor; | 19 | final bool followLinkColor; |
20 | + static String toHtml(String text) { | ||
21 | + String html = ""; | ||
22 | + text.trim().split(RegExp(r"\n\n+")).forEach((element) { | ||
23 | + html += MdWidget.toHtml(element); | ||
24 | + }); | ||
25 | + return html; | ||
26 | + } | ||
20 | 27 | ||
21 | @override | 28 | @override |
22 | Widget build(BuildContext context) { | 29 | Widget build(BuildContext context) { |
1 | name: tex_markdown | 1 | name: tex_markdown |
2 | description: This package is used to create flutter widget that can render markdown and latex formulas. It is very simple to use and uses native flutter components. | 2 | description: This package is used to create flutter widget that can render markdown and latex formulas. It is very simple to use and uses native flutter components. |
3 | -version: 0.0.6 | 3 | +version: 0.0.9 |
4 | homepage: https://github.com/saminsohag/flutter_packages/tree/main/tex_markdown | 4 | homepage: https://github.com/saminsohag/flutter_packages/tree/main/tex_markdown |
5 | 5 | ||
6 | environment: | 6 | environment: |
@@ -10,7 +10,7 @@ environment: | @@ -10,7 +10,7 @@ environment: | ||
10 | dependencies: | 10 | dependencies: |
11 | flutter: | 11 | flutter: |
12 | sdk: flutter | 12 | sdk: flutter |
13 | - tex_text: ^0.0.8 | 13 | + tex_text: ^0.1.2 |
14 | 14 | ||
15 | dev_dependencies: | 15 | dev_dependencies: |
16 | flutter_test: | 16 | flutter_test: |
-
Please register or login to post a comment