2025-10-04 13:37:47 +05:30
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:markdown/markdown.dart' as m;
|
|
|
|
|
import 'package:markdown_widget/markdown_widget.dart';
|
|
|
|
|
|
2025-10-04 16:04:49 +05:30
|
|
|
import 'latex_block_widget.dart';
|
2025-10-04 13:37:47 +05:30
|
|
|
|
|
|
|
|
const String _latexTag = 'latex';
|
|
|
|
|
|
|
|
|
|
/// Provides LaTeX parsing support for markdown_widget.
|
|
|
|
|
class ConduitLatex {
|
|
|
|
|
const ConduitLatex();
|
|
|
|
|
|
|
|
|
|
/// Returns the inline syntax used to identify LaTeX segments.
|
2025-10-04 16:04:49 +05:30
|
|
|
List<m.InlineSyntax> syntaxes() => [
|
|
|
|
|
_LatexDollarSyntax(),
|
|
|
|
|
_LatexEscapedSyntax(),
|
|
|
|
|
];
|
2025-10-04 13:37:47 +05:30
|
|
|
|
|
|
|
|
/// Returns the span generator that renders LaTeX expressions.
|
|
|
|
|
SpanNodeGeneratorWithTag generator({required bool isDark}) {
|
|
|
|
|
return SpanNodeGeneratorWithTag(
|
|
|
|
|
tag: _latexTag,
|
|
|
|
|
generator: (element, config, visitor) {
|
|
|
|
|
return _LatexNode(
|
|
|
|
|
attributes: element.attributes,
|
|
|
|
|
rawText: element.textContent,
|
|
|
|
|
config: config,
|
|
|
|
|
isDark: isDark,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 16:04:49 +05:30
|
|
|
class _LatexDollarSyntax extends m.InlineSyntax {
|
|
|
|
|
_LatexDollarSyntax()
|
|
|
|
|
: super(
|
|
|
|
|
r'(\$\$[\s\S]+?\$\$)|(\$[^\n]+?\$)',
|
|
|
|
|
startCharacter: r'$'.codeUnitAt(0),
|
|
|
|
|
);
|
2025-10-04 13:37:47 +05:30
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool onMatch(m.InlineParser parser, Match match) {
|
2025-10-04 16:04:49 +05:30
|
|
|
return _handleMatch(parser, match.input.substring(match.start, match.end));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _LatexEscapedSyntax extends m.InlineSyntax {
|
|
|
|
|
_LatexEscapedSyntax()
|
|
|
|
|
: super(
|
|
|
|
|
r'(\\\\\([\s\S]+?\\\\\))|(\\\\\[[\s\S]+?\\\\\])',
|
|
|
|
|
startCharacter: r'\'.codeUnitAt(0),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool onMatch(m.InlineParser parser, Match match) {
|
|
|
|
|
return _handleMatch(parser, match.input.substring(match.start, match.end));
|
2025-10-04 13:37:47 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 16:04:49 +05:30
|
|
|
bool _handleMatch(m.InlineParser parser, String raw) {
|
|
|
|
|
final element = m.Element.text(_latexTag, raw);
|
|
|
|
|
String content = raw;
|
|
|
|
|
var isInline = true;
|
|
|
|
|
|
|
|
|
|
if (raw.startsWith(r'$$') && raw.endsWith(r'$$') && raw.length > 4) {
|
|
|
|
|
content = raw.substring(2, raw.length - 2);
|
|
|
|
|
isInline = false;
|
|
|
|
|
} else if (raw.startsWith(r'$') && raw.endsWith(r'$') && raw.length > 2) {
|
|
|
|
|
content = raw.substring(1, raw.length - 1);
|
|
|
|
|
isInline = true;
|
|
|
|
|
} else if (raw.startsWith(r'\\(') && raw.endsWith(r'\\)') && raw.length > 4) {
|
|
|
|
|
content = raw.substring(2, raw.length - 2);
|
|
|
|
|
isInline = true;
|
|
|
|
|
} else if (raw.startsWith(r'\\[') && raw.endsWith(r'\\]') && raw.length > 4) {
|
|
|
|
|
content = raw.substring(2, raw.length - 2);
|
|
|
|
|
isInline = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
element.attributes['content'] = content;
|
|
|
|
|
element.attributes['isInline'] = '$isInline';
|
|
|
|
|
parser.addNode(element);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 13:37:47 +05:30
|
|
|
class _LatexNode extends SpanNode {
|
|
|
|
|
_LatexNode({
|
|
|
|
|
required this.attributes,
|
|
|
|
|
required this.rawText,
|
|
|
|
|
required this.config,
|
|
|
|
|
required this.isDark,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final Map<String, String> attributes;
|
|
|
|
|
final String rawText;
|
|
|
|
|
final MarkdownConfig config;
|
|
|
|
|
final bool isDark;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
InlineSpan build() {
|
|
|
|
|
final content = attributes['content']?.trim();
|
|
|
|
|
final isInline = attributes['isInline'] == 'true';
|
|
|
|
|
final baseStyle = (parentStyle ?? config.p.textStyle).copyWith(
|
|
|
|
|
color:
|
|
|
|
|
(parentStyle ?? config.p.textStyle).color ??
|
|
|
|
|
(isDark ? Colors.white : Colors.black),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (content == null || content.isEmpty) {
|
|
|
|
|
return TextSpan(text: rawText, style: baseStyle);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 16:04:49 +05:30
|
|
|
return WidgetSpan(
|
|
|
|
|
alignment: PlaceholderAlignment.middle,
|
|
|
|
|
child: LatexBlockWidget(
|
|
|
|
|
content: content,
|
|
|
|
|
isInline: isInline,
|
|
|
|
|
style: baseStyle,
|
|
|
|
|
isDark: isDark,
|
|
|
|
|
),
|
2025-10-04 13:37:47 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|