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