diff --git a/lib/home.dart b/lib/home.dart index 9acced8..0245002 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -9,6 +9,7 @@ import 'package:markdown_editor/device_preference_notifier.dart'; import 'package:markdown_editor/l10n/generated/app_localizations.dart'; import 'package:markdown_editor/widgets/MarkdownBody/custom_image_config.dart'; import 'package:markdown_editor/widgets/MarkdownBody/custom_text_node.dart'; +import 'package:markdown_editor/widgets/MarkdownBody/latex_node.dart'; import 'package:markdown_editor/widgets/MarkdownTextInput/markdown_text_input.dart'; import 'package:markdown_widget/markdown_widget.dart'; @@ -228,6 +229,8 @@ class _HomeState extends State { child: MarkdownBlock( data: _inputText, generator: MarkdownGenerator( + generators: [latexGenerator], + inlineSyntaxList: [LatexSyntax()], textGenerator: (node, config, visitor) => CustomTextNode(node.textContent, config, visitor), richTextBuilder: Text.rich, diff --git a/lib/widgets/MarkdownBody/latex_node.dart b/lib/widgets/MarkdownBody/latex_node.dart new file mode 100644 index 0000000..dee48ce --- /dev/null +++ b/lib/widgets/MarkdownBody/latex_node.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_math_fork/flutter_math.dart'; +import 'package:markdown/markdown.dart' as m; +import 'package:markdown_widget/markdown_widget.dart'; + +SpanNodeGeneratorWithTag latexGenerator = SpanNodeGeneratorWithTag( + tag: _latexTag, + generator: (e, config, visitor) => + LatexNode(e.attributes, e.textContent, config), +); + +const _latexTag = 'latex'; + +class LatexSyntax extends m.InlineSyntax { + LatexSyntax() : super(r'(\$\$[\s\S]+\$\$)|(\$.+?\$)'); + + @override + bool onMatch(m.InlineParser parser, Match match) { + final input = match.input; + final matchValue = input.substring(match.start, match.end); + String content = ''; + bool isInline = true; + const blockSyntax = '\$\$'; + const inlineSyntax = '\$'; + if (matchValue.startsWith(blockSyntax) && + matchValue.endsWith(blockSyntax) && + (matchValue != blockSyntax)) { + content = matchValue.substring(2, matchValue.length - 2); + isInline = false; + } else if (matchValue.startsWith(inlineSyntax) && + matchValue.endsWith(inlineSyntax) && + matchValue != inlineSyntax) { + content = matchValue.substring(1, matchValue.length - 1); + } + final m.Element el = m.Element.text(_latexTag, matchValue); + el.attributes['content'] = content; + el.attributes['isInline'] = '$isInline'; + parser.addNode(el); + return true; + } +} + +class LatexNode extends SpanNode { + final Map attributes; + final String textContent; + final MarkdownConfig config; + + LatexNode(this.attributes, this.textContent, this.config); + + @override + InlineSpan build() { + final content = attributes['content'] ?? ''; + final isInline = attributes['isInline'] == 'true'; + final style = parentStyle ?? config.p.textStyle; + if (content.isEmpty) return TextSpan(style: style, text: textContent); + + final latex = Math.tex( + content, + mathStyle: MathStyle.text, + textStyle: style, + textScaleFactor: 1, + onErrorFallback: (error) { + return Text(textContent, style: style.copyWith(color: Colors.red)); + }, + ); + return WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: !isInline + ? Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: 16), + child: Center(child: latex), + ) + : latex, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index cffe769..996f839 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -139,6 +139,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_math_fork: + dependency: "direct main" + description: + name: flutter_math_fork + sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" + url: "https://pub.dev" + source: hosted + version: "0.7.4" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -293,6 +301,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -405,6 +421,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" scroll_to_index: dependency: transitive description: @@ -522,6 +546,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d70a5b5..c6e40e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + flutter_math_fork: ^0.7.4 flutter_svg: ^2.2.1 flutter_widget_from_html_core: ^0.17.0 html: ^0.15.6