diff --git a/README.md b/README.md index df92312e..78363b35 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ SelectableMath( ## [Line Breaking](doc/line_breaking.md) ## Credits -This project is possible thanks to the inspirations and resources from [the KaTeX Project](https://katex.org/), [MathJax](www.mathjax.org), [Zefyr](https://github.com/memspace/zefyr), and [CaTeX](https://github.com/simpleclub/CaTeX). +This project is possible thanks to the inspirations and resources from [the KaTeX Project](https://katex.org/), [MathJax](www.mathjax.org), [Zefyr](https://github.com/memspace/zefyr), and [CaTeX](https://github.com/simpleclub/CaTeX), and [flutter_html_math](https://github.com/Sub6Resources/flutter_html). ## Goals - [x] : TeX math parsing (See [design doc](doc/design.md)) @@ -117,4 +117,4 @@ This project is possible thanks to the inspirations and resources from [the KaTe - [ ] : UnicodeMath parsing and encoding - [ ] : [UnicodeMath](https://www.unicode.org/notes/tn28/UTN28-PlainTextMath-v3.1.pdf)-style editing - [ ] : Breakable equations -- [ ] : MathML parsing and encoding +- [X] : MathML parsing and encoding diff --git a/example/lib/equations.dart b/example/lib/equations.dart index 11f81a2b..9f3a0cf3 100644 --- a/example/lib/equations.dart +++ b/example/lib/equations.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_math_fork/flutter_math.dart'; -import 'package:google_fonts/google_fonts.dart'; const equations = [ ['Solution of quadratic equation', r'x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}'], @@ -37,10 +36,7 @@ class EquationsPage extends StatelessWidget { children: [ ListTile( title: Text(entry[0]), - subtitle: SelectableText( - entry[1], - style: GoogleFonts.robotoMono(), - ), + subtitle: SelectableText(entry[1]), ), Container( padding: const EdgeInsets.fromLTRB(1, 5, 1, 5), diff --git a/example/pubspec.lock b/example/pubspec.lock index 33a3595d..a35ecff2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.10.0" boolean_selector: dependency: transitive description: @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" + source: hosted + version: "0.17.2" fake_async: dependency: transitive description: @@ -122,14 +130,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.3+4" - google_fonts: - dependency: "direct main" + html: + dependency: transitive description: - name: google_fonts - sha256: "6b6f10f0ce3c42f6552d1c70d2c28d764cf22bb487f50f66cca31dcd5194f4d6" + name: html + sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "0.15.2" http: dependency: transitive description: @@ -166,10 +174,10 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: c94db23593b89766cda57aab9ac311e3616cf87c6fa4e9749df032f66f30dcb8 url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.14" material_color_utilities: dependency: transitive description: @@ -182,10 +190,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: "12307e7f0605ce3da64cf0db90e5fcab0869f3ca03f76be6bb2991ce0a55e82b" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.9.0" mime: dependency: transitive description: @@ -218,7 +226,7 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - path_provider: + path_provider: dependency: transitive description: name: path_provider @@ -355,10 +363,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "6182294da5abf431177fccc1ee02401f6df30f766bc6130a0852c6b6d7ee6b2d" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.4.18" tuple: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 62ad6730..848c540d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -17,7 +17,6 @@ dependencies: path: ../ provider: any - flutter_tex: ^4.0.3+4 google_fonts: ^4.0.4 dev_dependencies: diff --git a/lib/src/render/svg/draw_svg_root.dart b/lib/src/render/svg/draw_svg_root.dart index fa244164..62d5e8c9 100644 --- a/lib/src/render/svg/draw_svg_root.dart +++ b/lib/src/render/svg/draw_svg_root.dart @@ -2,6 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_svg/flutter_svg.dart'; void drawSvgRoot(PictureInfo svgRoot, PaintingContext context, Offset offset) { + if (svgRoot.picture == null) return; + final canvas = context.canvas; canvas.save(); canvas.translate(offset.dx, offset.dy); @@ -15,6 +17,6 @@ void drawSvgRoot(PictureInfo svgRoot, PaintingContext context, Offset offset) { svgRoot.size.width, svgRoot.size.height, )); - canvas.drawPicture(svgRoot.picture); + canvas.drawPicture(svgRoot.picture!); canvas.restore(); } diff --git a/lib/src/utils/mathml_to_tex.dart b/lib/src/utils/mathml_to_tex.dart new file mode 100644 index 00000000..2dfa9d73 --- /dev/null +++ b/lib/src/utils/mathml_to_tex.dart @@ -0,0 +1,99 @@ +import 'package:html/dom.dart' as dom; + +String parseMathMLRecursive(dom.Node node, String parsed) { + if (node is dom.Element) { + List nodeList = node.nodes.whereType().toList(); + if (node.localName == "math" || + node.localName == "mrow" || + node.localName == "mtr") { + for (var element in nodeList) { + parsed = parseMathMLRecursive(element, parsed); + } + } + // note: munder, mover, and munderover do not support placing braces and other + // markings above/below elements, instead they are treated as super/subscripts for now. + if ((node.localName == "msup" || + node.localName == "msub" || + node.localName == "munder" || + node.localName == "mover") && + nodeList.length == 2) { + parsed = parseMathMLRecursive(nodeList[0], parsed); + parsed = + "${parseMathMLRecursive(nodeList[1], "$parsed${node.localName == "msup" || node.localName == "mover" ? "^" : "_"}{")}}"; + } + if ((node.localName == "msubsup" || node.localName == "munderover") && + nodeList.length == 3) { + parsed = parseMathMLRecursive(nodeList[0], parsed); + parsed = "${parseMathMLRecursive(nodeList[1], "${parsed}_{")}}"; + parsed = "${parseMathMLRecursive(nodeList[2], "$parsed^{")}}"; + } + if (node.localName == "mfrac" && nodeList.length == 2) { + parsed = "${parseMathMLRecursive(nodeList[0], parsed + r"\frac{")}}"; + parsed = "${parseMathMLRecursive(nodeList[1], "$parsed{")}}"; + } + // note: doesn't support answer & intermediate steps + if (node.localName == "mlongdiv" && nodeList.length == 4) { + parsed = parseMathMLRecursive(nodeList[0], parsed); + parsed = "${parseMathMLRecursive(nodeList[2], parsed + r"\overline{)")}}"; + } + if (node.localName == "msqrt") { + parsed = parsed + r"\sqrt{"; + for (var element in nodeList) { + parsed = parseMathMLRecursive(element, parsed); + } + parsed = "$parsed}"; + } + if (node.localName == "mroot" && nodeList.length == 2) { + parsed = "${parseMathMLRecursive(nodeList[1], parsed + r"\sqrt[")}]"; + parsed = "${parseMathMLRecursive(nodeList[0], "$parsed{")}}"; + } + if (node.localName == "mi" || + node.localName == "mn" || + node.localName == "mo") { + if (_mathML2Tex.keys.contains(node.text.trim())) { + parsed = parsed + + _mathML2Tex[ + _mathML2Tex.keys.firstWhere((e) => e == node.text.trim())]!; + } else if (node.text.startsWith("&") && node.text.endsWith(";")) { + parsed = parsed + + node.text + .trim() + .replaceFirst("&", r"\") + .substring(0, node.text.trim().length - 1); + } else { + parsed = parsed + node.text.trim(); + } + } + if (node.localName == 'mtable') { + String inner = + nodeList.map((e) => parseMathMLRecursive(e, '')).join(' \\\\'); + parsed = '$parsed\\begin{matrix}$inner\\end{matrix}'; + } + if (node.localName == "mtd") { + for (var element in nodeList) { + parsed = parseMathMLRecursive(element, parsed); + } + parsed = '$parsed & '; + } + } + return parsed; +} + +Map _mathML2Tex = { + "sin": r"\sin", + "sinh": r"\sinh", + "csc": r"\csc", + "csch": r"csch", + "cos": r"\cos", + "cosh": r"\cosh", + "sec": r"\sec", + "sech": r"\sech", + "tan": r"\tan", + "tanh": r"\tanh", + "cot": r"\cot", + "coth": r"\coth", + "log": r"\log", + "ln": r"\ln", + "{": r"\{", + "}": r"\}", +}; \ No newline at end of file diff --git a/lib/src/widgets/math.dart b/lib/src/widgets/math.dart index 7400a288..8c29138b 100644 --- a/lib/src/widgets/math.dart +++ b/lib/src/widgets/math.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:html/parser.dart'; import 'package:provider/provider.dart'; +import 'package:html/dom.dart' as dom; import '../ast/options.dart'; import '../ast/style.dart'; @@ -8,6 +10,7 @@ import '../ast/tex_break.dart'; import '../parser/tex/parse_error.dart'; import '../parser/tex/parser.dart'; import '../parser/tex/settings.dart'; +import '../utils/mathml_to_tex.dart'; import 'exception.dart'; import 'mode.dart'; import 'selectable.dart'; @@ -163,6 +166,61 @@ class Math extends StatelessWidget { ); } + /// Math builder using a MathML string + /// + /// {@template flutter_math_fork.widgets.math.tex_builder} + /// [expression] will first be parsed under [settings]. Then the acquired + /// [SyntaxTree] will be built under a specific options. If [ParseException] + /// is thrown or a build error occurs, [onErrorFallback] will be displayed. + /// + /// You can control the options via [mathStyle] and [textStyle]. + /// {@endtemplate} + /// + /// See alse: + /// + /// * [Math.mathStyle] + /// * [Math.textStyle] + factory Math.mathML( + String expression, { + Key? key, + MathStyle mathStyle = MathStyle.display, + TextStyle? textStyle, + OnErrorFallback onErrorFallback = defaultOnErrorFallback, + TexParserSettings settings = const TexParserSettings(), + double? textScaleFactor, + MathOptions? options, + }) { + return Math.mathMLFromDom(parse(expression)); + } + + /// Math builder using a Math Element + /// + /// {@template flutter_math_fork.widgets.math.tex_builder} + /// [expression] will first be parsed under [settings]. Then the acquired + /// [SyntaxTree] will be built under a specific options. If [ParseException] + /// is thrown or a build error occurs, [onErrorFallback] will be displayed. + /// + /// You can control the options via [mathStyle] and [textStyle]. + /// {@endtemplate} + /// + /// See alse: + /// + /// * [Math.mathStyle] + /// * [Math.textStyle] + factory Math.mathMLFromDom( + dom.Node node, { + Key? key, + MathStyle mathStyle = MathStyle.display, + TextStyle? textStyle, + OnErrorFallback onErrorFallback = defaultOnErrorFallback, + TexParserSettings settings = const TexParserSettings(), + double? textScaleFactor, + MathOptions? options, + }) { + final expression = parseMathMLRecursive(node, r''); + return Math.tex(expression); + } + @override Widget build(BuildContext context) { if (parseError != null) { diff --git a/lib/src/widgets/selection/gesture_detector_builder.dart b/lib/src/widgets/selection/gesture_detector_builder.dart index f44d1c3a..a0b8c34c 100644 --- a/lib/src/widgets/selection/gesture_detector_builder.dart +++ b/lib/src/widgets/selection/gesture_detector_builder.dart @@ -153,8 +153,11 @@ class MathSelectionGestureDetectorBuilder { } } + late TapDragStartDetails startDetails; + @protected void onDragSelectionStart(TapDragStartDetails details) { + startDetails = details; delegate.selectPositionAt( from: details.globalPosition, cause: SelectionChangedCause.drag, @@ -167,10 +170,10 @@ class MathSelectionGestureDetectorBuilder { } @protected - void onDragSelectionUpdate(TapDragUpdateDetails details) { + void onDragSelectionUpdate(TapDragUpdateDetails updateDetails) { delegate.selectPositionAt( - from: details.globalPosition - details.offsetFromOrigin, - to: details.globalPosition, + from: startDetails.globalPosition, + to: updateDetails.globalPosition, cause: SelectionChangedCause.drag, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 5b06dcc1..46117852 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,12 +10,14 @@ environment: dependencies: flutter: sdk: flutter - + flutter_svg: ">=2.0.0+1 <3.0.0" provider: ^6.0.5 meta: ^1.8.0 collection: ^1.17.0 tuple: ^2.0.1 + # Plugin for parsing html + html: ^0.15.0 dev_dependencies: flutter_test: