顾海波

Merge remote-tracking branch 'github/main'

# Conflicts:
#	example/lib/main.dart
## 1.0.16
* `IndentMd` and `BlockQuote` fixed.
* Baseline of bloc type component is fixed.
* block quote support improved.
* custom components support added.
* `Table` syntax improved.
## 1.0.15
... ...
... ... @@ -21,15 +21,16 @@ A comprehensive Flutter package for rendering rich Markdown and LaTeX content in
| ➖ Horizontal Line | ✅ | |
| 🔢 Latex Math | ✅ | |
| ↩️ Indent | ✅ |
| ↩️ BlockQuote | ✅ |
| 🖼️ Image | ✅ |
| ✨ Highlighted Text | ✅ |
| ✂️ Striked Text | ✅ |
| ✂️ Strike Text | ✅ |
| 🔵 Bold Text | ✅ |
| 📜 Italic Text | ✅ |
| 🔗 Links | ✅ |
| 📱 Selectable | ✅ |
| 🧩 Custom components | ✅ | |
| 📎 Underline | | 🔜 |
| 🧩 Custom components | | 🔜 |
## ✨ Key Features
... ...
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
... ...
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
... ...
... ... @@ -76,19 +76,216 @@ class _MyHomePageState extends State<MyHomePage> {
TextDirection _direction = TextDirection.ltr;
final TextEditingController _controller = TextEditingController(
text: r'''
```markdown
# Complex Markdown Document for Testing
一袋大米25千克,已经吃了它的$\frac{2}{5}$,吃了( )千克,还剩( )千克。
1. 计算已经吃的大米重量:
$$
已经吃的大米重量 = 25 \text{千克} \times \frac{2}{5} = 25 \times 0.4 = 1
$$
2. 计算剩余的大米重量:
$$
剩余的大米重量 = 25 \text{千克} - 10 \text{千克} = 15 \text{千克}
$$
【答案】
吃了10千克,还剩15千克。
This document is designed to **challenge** your `gpt_markdown` package by incorporating a wide variety of Markdown components including headers, lists, tables, code blocks, blockquotes, footnotes, and LaTeX math expressions.
---
## Table of Contents
1. [Headers and Emphasis](#headers-and-emphasis)
2. [Lists](#lists)
3. [Code Blocks and Inline Code](#code-blocks-and-inline-code)
4. [Tables](#tables)
5. [Blockquotes and Nested Elements](#blockquotes-and-nested-elements)
6. [Mathematical Expressions](#mathematical-expressions)
7. [Links and Images](#links-and-images)
8. [Footnotes](#footnotes)
9. [Horizontal Rules and Miscellaneous](#horizontal-rules-and-miscellaneous)
---
## Headers and Emphasis
### Header Levels
Markdown supports multiple header levels:
- `# Header 1`
- `## Header 2`
- `### Header 3`
- `#### Header 4`
- `##### Header 5`
- `###### Header 6`
### Emphasis Examples
- *Italicized text* using single asterisks or underscores.
- **Bold text** using double asterisks or underscores.
- ***Bold and italic*** by combining them.
- ~~Strikethrough~~ text using two tildes.
---
## Lists
### Unordered List
- Item 1
- Nested Item 1.1
- Nested Item 1.2
- Deeply Nested Item 1.2.1
- Item 2
- [ ] Task not completed
- [x] Task completed
### Ordered List
1. First item
2. Second item with nested list:
1. Subitem 2.1
2. Subitem 2.2
3. Third item
### Mixed List Example
- **Fruits**
1. Apple
2. Banana
3. Cherry
- **Vegetables**
- Carrot
- Lettuce
- Spinach
---
## Code Blocks and Inline Code
### Inline Code
Here is an example of inline code: `print("Hello, world!")`.
### Fenced Code Block (Python)
```python
def greet(name):
"""
Greets a person with the provided name.
"""
print(f"Hello, {name}!")
greet("Alice")
```
### Fenced Code Block (JavaScript)
```javascript
function greet(name) {
console.log(`Hello, ${name}!`);
}
greet("Bob");
```
---
## Tables
Here is a table demonstrating various elements:
| Syntax | Description | Example |
| ----------- | ---------------------------------------- | --------------------------------- |
| Header | Title | **Bold Header** |
| Paragraph | Text with *italic* and **bold** elements | This is a sample paragraph. |
| Inline Code | `code snippet` | `let x = 10;` |
Additional table with alignment:
| Left Align | Center Align | Right Align |
| :--------- |:------------:| ----------:|
| Row 1 | Row 1 | Row 1 |
| Row 2 | Row 2 | Row 2 |
---
## Blockquotes and Nested Elements
> **Blockquote Header**
>
> This is a blockquote. You can include **bold** and *italic* text, as well as `inline code` within blockquotes.
>
> > ### Nested Blockquote
> > - Nested list item 1
> > - Nested list item 2
> > 1. Numbered subitem 1
> > 2. Numbered subitem 2
> >
> > ```python
> > # Code snippet inside nested blockquote
> > for i in range(3):
> > print(i)
> > ```
>
> Back to the outer blockquote.
---
## Mathematical Expressions
### Inline Math
You can write inline math using the `\( ... \)` syntax. For example, the quadratic formula is given by:
\( x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} \).
### Display Math
Display math can be rendered using the `\[ ... \]` syntax. For example, consider the integral:
\[
\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}
\]
More complex display equations:
\[
E = mc^2 \quad \text{and} \quad F = ma
\]
---
## Links and Images
### Links
Here are examples of links:
- [OpenAI](https://www.openai.com)
- [GitHub](https://github.com)
### Images
Inline images can be embedded as follows:
![Alt Text for Image](https://via.placeholder.com/150 "Image Title")
Images can also be referenced with links:
[![Linked Image](https://via.placeholder.com/100 "Thumbnail")](https://via.placeholder.com/500 "Full Image")
---
## Footnotes
Here is a statement with a footnote.[^1] Another reference can be added here.[^long]
[^1]: This is a simple footnote.
[^long]: This footnote contains a longer explanation to showcase how multiple lines can be formatted in a footnote. It supports Markdown formatting such as **bold** and *italic* text.
---
## Horizontal Rules and Miscellaneous
Horizontal rules can be used to separate sections:
---
### Task List Example
- [x] Write complex Markdown document
- [x] Include LaTeX math expressions
- [ ] Add more Markdown components if needed
### Nested Quotes with Code and Math
> **Example of Nested Components**
>
> - Inline code: `sum = a + b`
> - Math expression: \( \sum_{i=1}^n i = \frac{n(n+1)}{2} \)
> - More text with **bold** formatting.
>
> ```javascript
> // JavaScript code example inside a nested blockquote
> const sum = (n) => (n * (n + 1)) / 2;
> console.log(sum(10));
> ```
---
## Conclusion
This document was created to test the robustness of Markdown parsers and to ensure that all components, including advanced LaTeX expressions and nested structures, are rendered correctly. Enjoy testing and feel free to extend it further!
```
''',
);
File? file;
... ... @@ -143,7 +340,8 @@ $$
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context)
.colorScheme
.onSurface,
.onSurface
.withValues(alpha: 0.38),
),
),
IconButton(
... ... @@ -241,7 +439,8 @@ $$
border: Border.all(
color: Theme.of(context)
.colorScheme
.secondary,
.secondary
.withValues(alpha: 0.5),
width: 1,
),
),
... ... @@ -387,6 +586,40 @@ $$
),
);
},
components: [
CodeBlockMd(),
NewLines(),
BlockQuote(),
ImageMd(),
ATagMd(),
TableMd(),
HTag(),
UnOrderedList(),
OrderedList(),
RadioButtonMd(),
CheckBoxMd(),
HrLine(),
StrikeMd(),
BoldMd(),
ItalicMd(),
LatexMath(),
LatexMathMultiLine(),
HighlightedText(),
SourceTag(),
IndentMd(),
],
inlineComponents: [
ImageMd(),
ATagMd(),
TableMd(),
StrikeMd(),
BoldMd(),
ItalicMd(),
LatexMath(),
LatexMathMultiLine(),
HighlightedText(),
SourceTag(),
],
// codeBuilder: (context, name, code, closed) {
// return Padding(
// padding: const EdgeInsets.symmetric(
... ...
... ... @@ -12,6 +12,7 @@ class BlockQuoteWidget extends StatelessWidget {
required this.child,
required this.direction,
required this.color,
this.width = 3,
});
/// The child widget to be indented.
... ... @@ -23,29 +24,39 @@ class BlockQuoteWidget extends StatelessWidget {
/// The color of the indent.
final Color color;
/// The width of the indent.
final double width;
@override
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: IndentPainter(color, direction),
child: child,
return Row(
children: [
Flexible(
child: CustomPaint(
foregroundPainter: BlockQuotePainter(color, direction, width),
child: child,
),
),
],
);
}
}
/// A custom painter that draws an indent on a canvas.
///
/// The [IndentPainter] class extends CustomPainter and is responsible for
/// The [BlockQuotePainter] class extends CustomPainter and is responsible for
/// painting the indent on a canvas. It takes a [color] and [direction] parameter
/// and uses them to draw an indent in the UI.
class IndentPainter extends CustomPainter {
IndentPainter(this.color, this.direction);
class BlockQuotePainter extends CustomPainter {
BlockQuotePainter(this.color, this.direction, this.width);
final Color color;
final TextDirection direction;
final double width;
@override
void paint(Canvas canvas, Size size) {
var left = direction == TextDirection.ltr;
var start = left ? 0.0 : size.width - 4;
var rect = Rect.fromLTWH(start, 0, 4, size.height);
var start = left ? 0.0 : size.width - width;
var rect = Rect.fromLTWH(start, 0, width, size.height);
var paint = Paint()..color = color;
canvas.drawRect(rect, paint);
}
... ...
import 'package:flutter/material.dart';
import 'package:gpt_markdown/gpt_markdown.dart';
/// A builder function for the ordered list.
typedef OrderedListBuilder =
... ... @@ -80,6 +81,8 @@ class GptMarkdownConfig {
this.imageBuilder,
this.maxLines,
this.overflow,
this.components,
this.inlineComponents,
});
/// The direction of the text.
... ... @@ -133,6 +136,12 @@ class GptMarkdownConfig {
/// The image builder.
final ImageBuilder? imageBuilder;
/// The list of components.
final List<MarkdownComponent>? components;
/// The list of inline components.
final List<MarkdownComponent>? inlineComponents;
/// A copy of the configuration with the specified parameters.
GptMarkdownConfig copyWith({
TextStyle? style,
... ... @@ -152,6 +161,8 @@ class GptMarkdownConfig {
final ImageBuilder? imageBuilder,
final OrderedListBuilder? orderedListBuilder,
final UnOrderedListBuilder? unOrderedListBuilder,
final List<MarkdownComponent>? components,
final List<MarkdownComponent>? inlineComponents,
}) {
return GptMarkdownConfig(
style: style ?? this.style,
... ... @@ -171,6 +182,8 @@ class GptMarkdownConfig {
imageBuilder: imageBuilder ?? this.imageBuilder,
orderedListBuilder: orderedListBuilder ?? this.orderedListBuilder,
unOrderedListBuilder: unOrderedListBuilder ?? this.unOrderedListBuilder,
components: components ?? this.components,
inlineComponents: inlineComponents ?? this.inlineComponents,
);
}
... ...
... ... @@ -40,6 +40,8 @@ class GptMarkdown extends StatelessWidget {
this.overflow,
this.orderedListBuilder,
this.unOrderedListBuilder,
this.components,
this.inlineComponents,
});
/// The direction of the text.
... ... @@ -94,16 +96,60 @@ class GptMarkdown extends StatelessWidget {
/// The unordered list builder.
final UnOrderedListBuilder? unOrderedListBuilder;
/// The list of components.
/// ```dart
/// List<MarkdownComponent> components = [
/// CodeBlockMd(),
/// NewLines(),
/// BlockQuote(),
/// ImageMd(),
/// ATagMd(),
/// TableMd(),
/// HTag(),
/// UnOrderedList(),
/// OrderedList(),
/// RadioButtonMd(),
/// CheckBoxMd(),
/// HrLine(),
/// StrikeMd(),
/// BoldMd(),
/// ItalicMd(),
/// LatexMath(),
/// LatexMathMultiLine(),
/// HighlightedText(),
/// SourceTag(),
/// IndentMd(),
/// ];
/// ```
final List<MarkdownComponent>? components;
/// The list of inline components.
/// ```dart
/// List<MarkdownComponent> inlineComponents = [
/// ImageMd(),
/// ATagMd(),
/// TableMd(),
/// StrikeMd(),
/// BoldMd(),
/// ItalicMd(),
/// LatexMath(),
/// LatexMathMultiLine(),
/// HighlightedText(),
/// SourceTag(),
/// ];
/// ```
final List<MarkdownComponent>? inlineComponents;
/// A method to remove extra lines inside block LaTeX.
String _removeExtraLinesInsideBlockLatex(String text) {
return text.replaceAllMapped(
RegExp(r"\\\[(.*?)\\\]", multiLine: true, dotAll: true),
(match) {
String content = match[0] ?? "";
return content.replaceAllMapped(RegExp(r"\n[\n\ ]+"), (match) => "\n");
},
);
}
// String _removeExtraLinesInsideBlockLatex(String text) {
// return text.replaceAllMapped(
// RegExp(r"\\\[(.*?)\\\]", multiLine: true, dotAll: true),
// (match) {
// String content = match[0] ?? "";
// return content.replaceAllMapped(RegExp(r"\n[\n\ ]+"), (match) => "\n");
// },
// );
// }
@override
Widget build(BuildContext context) {
... ... @@ -130,7 +176,7 @@ class GptMarkdown extends StatelessWidget {
},
);
}
tex = _removeExtraLinesInsideBlockLatex(tex);
// tex = _removeExtraLinesInsideBlockLatex(tex);
return ClipRRect(
child: MdWidget(
tex,
... ... @@ -153,6 +199,8 @@ class GptMarkdown extends StatelessWidget {
imageBuilder: imageBuilder,
orderedListBuilder: orderedListBuilder,
unOrderedListBuilder: unOrderedListBuilder,
components: components,
inlineComponents: inlineComponents,
),
),
);
... ...
... ... @@ -47,8 +47,8 @@ abstract class MarkdownComponent {
) {
var components =
includeGlobalComponents
? MarkdownComponent.components
: MarkdownComponent.inlineComponents;
? config.components ?? MarkdownComponent.components
: config.inlineComponents ?? MarkdownComponent.inlineComponents;
List<InlineSpan> spans = [];
Iterable<String> regexes = components.map<String>((e) => e.exp.pattern);
final combinedRegex = RegExp(
... ... @@ -68,29 +68,7 @@ abstract class MarkdownComponent {
dotAll: each.exp.isDotAll,
);
if (exp.hasMatch(element)) {
if (each.inline) {
spans.add(each.span(context, element, config));
} else {
spans.addAll([
TextSpan(
text: "\n ",
style: TextStyle(
fontSize: 0,
height: 0,
color: config.style?.color,
),
),
each.span(context, element, config),
TextSpan(
text: "\n ",
style: TextStyle(
fontSize: 0,
height: 0,
color: config.style?.color,
),
),
]);
}
spans.add(each.span(context, element, config));
return "";
}
}
... ... @@ -156,7 +134,12 @@ abstract class BlockMd extends MarkdownComponent {
child: child,
);
}
return WidgetSpan(child: child, alignment: PlaceholderAlignment.middle);
child = Row(children: [Flexible(child: child)]);
return WidgetSpan(
child: child,
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
);
}
Widget build(
... ... @@ -182,7 +165,7 @@ class IndentMd extends BlockMd {
textDirection: config.textDirection,
child: Row(
children: [
Expanded(
Flexible(
child: config.getRich(
TextSpan(
children: MarkdownComponent.generate(
... ... @@ -293,7 +276,7 @@ class HrLine extends BlockMd {
/// Checkbox component
class CheckBoxMd extends BlockMd {
@override
String get expString => (r"\[(\x?)\]\ (\S[^\n]*?)$");
String get expString => (r"\[((?:\x|\ ))\]\ (\S[^\n]*?)$");
get onLinkTab => null;
@override
... ... @@ -314,7 +297,7 @@ class CheckBoxMd extends BlockMd {
/// Radio Button component
class RadioButtonMd extends BlockMd {
@override
String get expString => (r"\((\x?)\)\ (\S[^\n]*)$");
String get expString => (r"\(((?:\x|\ ))\)\ (\S[^\n]*)$");
get onLinkTab => null;
@override
... ... @@ -339,7 +322,11 @@ class BlockQuote extends InlineMd {
@override
RegExp get exp =>
// RegExp(r"(?<=\n\n)(\ +)(.+?)(?=\n\n)", dotAll: true, multiLine: true);
RegExp(r"^>([^\n]+)$", dotAll: true, multiLine: true);
RegExp(
r"(?:(?:^)\ *>[^\n]+)(?:(?:\n)\ *>[^\n]+)*",
dotAll: true,
multiLine: true,
);
@override
InlineSpan span(
... ... @@ -348,9 +335,20 @@ class BlockQuote extends InlineMd {
final GptMarkdownConfig config,
) {
var match = exp.firstMatch(text);
var data = "${match?[1]}".trim();
// data = data.replaceAll(RegExp(r'\n\ {' '$spaces' '}'), '\n').trim();
data = data.trim();
var dataBuilder = StringBuffer();
var m = match?[0] ?? '';
for (var each in m.split('\n')) {
if (each.startsWith(RegExp(r'\ *>'))) {
var subString = each.trimLeft().substring(1);
if (subString.startsWith(' ')) {
subString = subString.substring(1);
}
dataBuilder.writeln(subString);
} else {
dataBuilder.writeln(each);
}
}
var data = dataBuilder.toString().trim();
var child = TextSpan(
children: MarkdownComponent.generate(context, data, config, true),
);
... ... @@ -364,8 +362,9 @@ class BlockQuote extends InlineMd {
child: BlockQuoteWidget(
color: Theme.of(context).colorScheme.onSurfaceVariant,
direction: config.textDirection,
width: 3,
child: Padding(
padding: const EdgeInsetsDirectional.only(start: 10.0),
padding: const EdgeInsetsDirectional.only(start: 8.0),
child: config.getRich(child),
),
),
... ... @@ -390,7 +389,7 @@ class UnOrderedList extends BlockMd {
) {
var match = this.exp.firstMatch(text);
var child = MdWidget("${match?[1]?.trim()}", false, config: config);
var child = MdWidget("${match?[1]?.trim()}", true, config: config);
return config.unOrderedListBuilder?.call(
context,
... ... @@ -428,7 +427,7 @@ class OrderedList extends BlockMd {
var no = "${match?[1]}";
var child = MdWidget("${match?[2]?.trim()}", false, config: config);
var child = MdWidget("${match?[2]?.trim()}", true, config: config);
return config.orderedListBuilder?.call(
context,
no,
... ... @@ -582,7 +581,8 @@ class ItalicMd extends InlineMd {
class LatexMathMultiLine extends BlockMd {
@override
String get expString => (r"\\\[(((?!\n\n).)*?)\\\]|(\\begin.*?\\end{.*?})");
String get expString => (r"\ *\\\[((?:.)*?)\\\]|(\ *\\begin.*?\\end{.*?})");
// (r"\ *\\\[((?:(?!\n\n\n).)*?)\\\]|(\\begin.*?\\end{.*?})");
@override
RegExp get exp => RegExp(expString, dotAll: true, multiLine: true);
... ... @@ -593,7 +593,7 @@ class LatexMathMultiLine extends BlockMd {
final GptMarkdownConfig config,
) {
var p0 = exp.firstMatch(text.trim());
String mathText = p0?[1] ?? p0?[2] ?? "";
String mathText = p0?[1] ?? p0?[2] ?? '';
var workaround = config.latexWorkaround ?? (String tex) => tex;
var builder =
... ... @@ -889,7 +889,7 @@ class ImageMd extends InlineMd {
class TableMd extends BlockMd {
@override
String get expString =>
(r"(((\|[^\n\|]+\|)((([^\n\|]+\|)+)?))(\n(((\|[^\n\|]+\|)(([^\n\|]+\|)+)?)))+)$");
(r"(((\|[^\n\|]+\|)((([^\n\|]+\|)+)?)\ *)(\n\ *(((\|[^\n\|]+\|)(([^\n\|]+\|)+)?))\ *)+)$");
@override
Widget build(
BuildContext context,
... ... @@ -902,6 +902,7 @@ class TableMd extends BlockMd {
.map<Map<int, String>>(
(e) =>
e
.trim()
.split('|')
.where((element) => element.isNotEmpty)
.toList()
... ... @@ -955,7 +956,7 @@ class TableMd extends BlockMd {
children: List.generate(maxCol, (index) {
var e = entry.value;
String data = e[index] ?? "";
if (RegExp(r"^--+$").hasMatch(data.trim()) ||
if (RegExp(r"^:?--+:?$").hasMatch(data.trim()) ||
data.trim().isEmpty) {
return const SizedBox();
}
... ...
... ... @@ -20,24 +20,21 @@ class MdWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<InlineSpan> list = [];
list.addAll(
MarkdownComponent.generate(
context,
exp,
// .replaceAllMapped(
// RegExp(
// r"\\\[(.*?)\\\]|(\\begin.*?\\end{.*?})",
// multiLine: true,
// dotAll: true,
// ), (match) {
// //
// String body = (match[1] ?? match[2])?.replaceAll("\n", " ") ?? "";
// return "\\[$body\\]";
// }),
config,
includeGlobalComponents,
),
List<InlineSpan> list = MarkdownComponent.generate(
context,
exp,
// .replaceAllMapped(
// RegExp(
// r"\\\[(.*?)\\\]|(\\begin.*?\\end{.*?})",
// multiLine: true,
// dotAll: true,
// ), (match) {
// //
// String body = (match[1] ?? match[2])?.replaceAll("\n", " ") ?? "";
// return "\\[$body\\]";
// }),
config,
includeGlobalComponents,
);
return config.getRich(
TextSpan(children: list, style: config.style?.copyWith()),
... ...