feat: support custom markdown parsers (#835)
* feat: support custom markdown parsers * fix: markdown test * feat: add markdown quote parser * chore: use markdown parser * chore: parse unordered list, to_do list and quote * feat: add a markdown editor example * feat: add bulleted list parser * feat: support nested list and img * feat: support code block * feat: support table parser * chore: refacotr code * fix: flutter analyze * chore: add more tests and remove unused files * fix: test
This commit is contained in:
parent
e8ee051719
commit
5bf6c7acd0
36 changed files with 1413 additions and 1099 deletions
|
|
@ -12,6 +12,7 @@ import 'package:example/pages/editor.dart';
|
||||||
import 'package:example/pages/editor_list.dart';
|
import 'package:example/pages/editor_list.dart';
|
||||||
import 'package:example/pages/fixed_toolbar_editor.dart';
|
import 'package:example/pages/fixed_toolbar_editor.dart';
|
||||||
import 'package:example/pages/focus_example_for_editor.dart';
|
import 'package:example/pages/focus_example_for_editor.dart';
|
||||||
|
import 'package:example/pages/markdown_editor.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
@ -165,6 +166,14 @@ class _HomePageState extends State<HomePage> {
|
||||||
|
|
||||||
// Theme Demo
|
// Theme Demo
|
||||||
_buildSeparator(context, 'Showcases'),
|
_buildSeparator(context, 'Showcases'),
|
||||||
|
_buildListTile(context, 'Markdown Editor', () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const MarkdownEditor(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
_buildListTile(context, 'Auto complete Editor', () {
|
_buildListTile(context, 'Auto complete Editor', () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
51
example/lib/pages/markdown/markdown_code_block_parser.dart
Normal file
51
example/lib/pages/markdown/markdown_code_block_parser.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownCodeBlockParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownCodeBlockParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'pre') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final ec = element.children;
|
||||||
|
if (ec == null || ec.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final code = ec.first;
|
||||||
|
if (code is! md.Element || code.tag != 'code') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
String? language;
|
||||||
|
if (code.attributes.containsKey('class')) {
|
||||||
|
final classes = code.attributes['class']!.split(' ');
|
||||||
|
final languageClass = classes.firstWhere(
|
||||||
|
(c) => c.startsWith('language-'),
|
||||||
|
orElse: () => '',
|
||||||
|
);
|
||||||
|
language = languageClass.substring('language-'.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
final deltaDecoder = DeltaMarkdownDecoder();
|
||||||
|
|
||||||
|
return [
|
||||||
|
codeBlockNode(
|
||||||
|
language: language,
|
||||||
|
delta: deltaDecoder.convertNodes(code.children),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
100
example/lib/pages/markdown_editor.dart
Normal file
100
example/lib/pages/markdown_editor.dart
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||||
|
import 'package:example/pages/markdown/markdown_code_block_parser.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
class MarkdownEditor extends StatefulWidget {
|
||||||
|
const MarkdownEditor({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MarkdownEditor> createState() => _MarkdownEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MarkdownEditorState extends State<MarkdownEditor> {
|
||||||
|
EditorState editorState = EditorState.blank();
|
||||||
|
final controller = TextEditingController();
|
||||||
|
final editorStyle = EditorStyle.desktop(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
textStyleConfiguration: TextStyleConfiguration(
|
||||||
|
lineHeight: 1.2,
|
||||||
|
applyHeightToFirstAscent: true,
|
||||||
|
applyHeightToLastDescent: true,
|
||||||
|
text: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
code: GoogleFonts.architectsDaughter(),
|
||||||
|
bold: GoogleFonts.poppins(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
controller.addListener(_onTextChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
title: const Text('Markdown Editor'),
|
||||||
|
titleTextStyle: const TextStyle(color: Colors.white),
|
||||||
|
iconTheme: const IconThemeData(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: AppFlowyEditor(
|
||||||
|
editorState: editorState,
|
||||||
|
editorStyle: editorStyle,
|
||||||
|
editable: true,
|
||||||
|
blockComponentBuilders: {
|
||||||
|
...standardBlockComponentBuilderMap,
|
||||||
|
CodeBlockKeys.type: CodeBlockComponentBuilder(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const VerticalDivider(),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
maxLines: null,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Type markdown here ...',
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTextChanged() {
|
||||||
|
final document = markdownToDocument(
|
||||||
|
controller.text,
|
||||||
|
markdownParsers: [
|
||||||
|
const MarkdownCodeBlockParserV2(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
editorState = EditorState(document: document);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,7 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
path: ../
|
|
||||||
provider: ^6.0.3
|
provider: ^6.0.3
|
||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.1.5
|
||||||
path_provider: ^2.0.15
|
path_provider: ^2.0.15
|
||||||
|
|
@ -43,6 +43,12 @@ dependencies:
|
||||||
highlight: ^0.7.0
|
highlight: ^0.7.0
|
||||||
http: ^1.1.0
|
http: ^1.1.0
|
||||||
show_fps: ^1.0.6
|
show_fps: ^1.0.6
|
||||||
|
appflowy_editor_plugins: ^0.0.6
|
||||||
|
markdown:
|
||||||
|
|
||||||
|
dependency_overrides:
|
||||||
|
appflowy_editor:
|
||||||
|
path: ../
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,17 @@ class DeltaMarkdownDecoder extends Converter<String, Delta>
|
||||||
return _delta;
|
return _delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Delta convertNodes(List<md.Node>? nodes) {
|
||||||
|
if (nodes == null) {
|
||||||
|
return Delta();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final node in nodes) {
|
||||||
|
node.accept(this);
|
||||||
|
}
|
||||||
|
return _delta;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void visitElementAfter(md.Element element) {
|
void visitElementAfter(md.Element element) {
|
||||||
_removeAttributeKey(element);
|
_removeAttributeKey(element);
|
||||||
|
|
@ -64,7 +75,11 @@ class DeltaMarkdownDecoder extends Converter<String, Delta>
|
||||||
_attributes[BuiltInAttributeKey.underline] = true;
|
_attributes[BuiltInAttributeKey.underline] = true;
|
||||||
} else {
|
} else {
|
||||||
element.attributes.forEach((key, value) {
|
element.attributes.forEach((key, value) {
|
||||||
_attributes[key] = jsonDecode(value);
|
try {
|
||||||
|
_attributes[key] = jsonDecode(value);
|
||||||
|
} catch (_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,307 +1,66 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/plugins/markdown/decoder/parser/custom_node_parser.dart';
|
import 'package:appflowy_editor/src/plugins/markdown/decoder/custom_syntaxes/underline_syntax.dart';
|
||||||
import 'package:appflowy_editor/src/plugins/markdown/decoder/table_markdown_decoder.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
class DocumentMarkdownDecoder extends Converter<String, Document> {
|
class DocumentMarkdownDecoder extends Converter<String, Document> {
|
||||||
DocumentMarkdownDecoder({
|
DocumentMarkdownDecoder({
|
||||||
this.customNodeParsers = const [],
|
this.markdownElementParsers = const [],
|
||||||
this.customInlineSyntaxes = const [],
|
this.inlineSyntaxes = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<CustomNodeParser> customNodeParsers;
|
final List<CustomMarkdownParser> markdownElementParsers;
|
||||||
final List<md.InlineSyntax> customInlineSyntaxes;
|
final List<md.InlineSyntax> inlineSyntaxes;
|
||||||
final imageRegex = RegExp(r'^!\[[^\]]*\]\((.*?)\)');
|
|
||||||
final assetRegex = RegExp(r'^\[[^\]]*\]\((.*?)\)');
|
|
||||||
final htmlRegex = RegExp('^(http|https)://');
|
|
||||||
final numberedListRegex = RegExp(r'^\d+\. ');
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Document convert(String input) {
|
Document convert(String input) {
|
||||||
final lines = input.split('\n');
|
final List<md.Node> mdNodes = md.Document(
|
||||||
|
extensionSet: md.ExtensionSet.gitHubWeb,
|
||||||
|
inlineSyntaxes: [
|
||||||
|
...inlineSyntaxes,
|
||||||
|
UnderlineInlineSyntax(),
|
||||||
|
],
|
||||||
|
).parse(input);
|
||||||
final document = Document.blank();
|
final document = Document.blank();
|
||||||
// In the below while loop we iterate through each line of the input markdown
|
|
||||||
// if the line starts with ``` it means there are chances it may be a code block
|
|
||||||
// so it iterates through all the lines(using another while loop) storing them in a
|
|
||||||
// string variable codeblock until it finds another ``` which marks the end of code
|
|
||||||
// block it then sends this to convert _convertLineToNode function .
|
|
||||||
// If we reach the end of lines and we still haven't found ``` then we will treat the
|
|
||||||
// line starting with ``` as a regular paragraph and continue the main while loop from
|
|
||||||
// a temporary pointer which stored the line number of the line with starting ```
|
|
||||||
int i = 0;
|
|
||||||
while (i < lines.length) {
|
|
||||||
if (lines[i].startsWith('```') &&
|
|
||||||
lines[i].endsWith('```') &&
|
|
||||||
lines[i].length > 3) {
|
|
||||||
document.insert(
|
|
||||||
[i],
|
|
||||||
[convertLineToNode(lines[i], customInlineSyntaxes)],
|
|
||||||
);
|
|
||||||
i++;
|
|
||||||
} else if (lines[i].startsWith('```')) {
|
|
||||||
String codeBlock = '';
|
|
||||||
codeBlock += '${lines[i]}\n';
|
|
||||||
int tempLinePointer = i;
|
|
||||||
i++;
|
|
||||||
while (i < lines.length && !lines[i].endsWith('```')) {
|
|
||||||
codeBlock += '${lines[i]}\n';
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i == lines.length) {
|
final nodes = mdNodes
|
||||||
i = tempLinePointer;
|
.map((mdNode) => _parseNode(mdNode))
|
||||||
document.insert(
|
.toList()
|
||||||
[i],
|
.whereNotNull()
|
||||||
[convertLineToNode(lines[i], customInlineSyntaxes)],
|
.toList()
|
||||||
);
|
.flattened;
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
codeBlock += lines[i];
|
|
||||||
document.insert(
|
|
||||||
[tempLinePointer],
|
|
||||||
[convertLineToNode(codeBlock, customInlineSyntaxes)],
|
|
||||||
);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
} else if (i + 1 < lines.length &&
|
|
||||||
TableMarkdownDecoder.isTable(lines[i], lines[i + 1])) {
|
|
||||||
final node = TableMarkdownDecoder().convert(lines.sublist(i));
|
|
||||||
i += node.attributes['rowsLen'] as int;
|
|
||||||
|
|
||||||
document.insert([i], [node]);
|
if (nodes.isNotEmpty) {
|
||||||
i++;
|
document.insert([0], nodes);
|
||||||
} else if (i + 1 < lines.length && isNestedList(lines[i], lines[i + 1])) {
|
|
||||||
String text = lines[i];
|
|
||||||
int tempLinePointer = i;
|
|
||||||
int currentIndent = getIndentLevel(lines[i]);
|
|
||||||
i++;
|
|
||||||
|
|
||||||
while (i < lines.length &&
|
|
||||||
(lines[i].trimLeft().startsWith('- ') ||
|
|
||||||
lines[i].trimLeft().startsWith('* ') ||
|
|
||||||
numberedListRegex.hasMatch(lines[i].trimLeft())) &&
|
|
||||||
getIndentLevel(lines[i]) > currentIndent) {
|
|
||||||
text += '\n${lines[i]}';
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
List<String> listLines = text.split('\n');
|
|
||||||
Node? rootList;
|
|
||||||
Node? currentParent;
|
|
||||||
int indentLevel = 0;
|
|
||||||
|
|
||||||
for (String line in listLines) {
|
|
||||||
currentIndent = getIndentLevel(line);
|
|
||||||
Node newNode =
|
|
||||||
convertLineToNode(line.trimLeft(), customInlineSyntaxes);
|
|
||||||
|
|
||||||
if (rootList == null) {
|
|
||||||
rootList = newNode;
|
|
||||||
currentParent = newNode;
|
|
||||||
} else if (currentIndent > indentLevel) {
|
|
||||||
currentParent!.insert(newNode);
|
|
||||||
currentParent = newNode;
|
|
||||||
} else {
|
|
||||||
while (
|
|
||||||
currentIndent <= indentLevel && currentParent!.parent != null) {
|
|
||||||
currentParent = currentParent.parent;
|
|
||||||
indentLevel--;
|
|
||||||
}
|
|
||||||
if (currentParent != rootList) {
|
|
||||||
currentParent!.parent!.insert(newNode);
|
|
||||||
} else {
|
|
||||||
rootList.insert(newNode);
|
|
||||||
}
|
|
||||||
currentParent = newNode;
|
|
||||||
}
|
|
||||||
indentLevel = currentIndent;
|
|
||||||
}
|
|
||||||
if (rootList != null) {
|
|
||||||
document.insert([tempLinePointer], [rootList]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.insert(
|
|
||||||
[i],
|
|
||||||
[convertLineToNode(lines[i], customInlineSyntaxes)],
|
|
||||||
);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isNestedList(String line1, String line2) {
|
// handle node itself and its children
|
||||||
if ((line1.trimLeft().startsWith('- ') ||
|
List<Node> _parseNode(md.Node mdNode) {
|
||||||
line1.trimLeft().startsWith('* ') ||
|
List<Node> nodes = [];
|
||||||
numberedListRegex.hasMatch(line1.trimLeft())) &&
|
|
||||||
(line2.trimLeft().startsWith('- ') ||
|
|
||||||
line2.trimLeft().startsWith('* ') ||
|
|
||||||
numberedListRegex.hasMatch(line2.trimLeft()))) {
|
|
||||||
return getIndentLevel(line2) > getIndentLevel(line1);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getIndentLevel(String line) {
|
for (final parser in markdownElementParsers) {
|
||||||
int indent = 0;
|
nodes = parser.transform(
|
||||||
while (indent < line.length && line[indent] == ' ') {
|
mdNode,
|
||||||
indent++;
|
markdownElementParsers,
|
||||||
}
|
MarkdownListType.unknown,
|
||||||
return (indent / 2).floor();
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Node convertLineToNode(
|
if (nodes.isNotEmpty) {
|
||||||
String line,
|
break;
|
||||||
List<md.InlineSyntax> customInlineSyntaxes,
|
|
||||||
) {
|
|
||||||
final decoder = DeltaMarkdownDecoder(
|
|
||||||
customInlineSyntaxes: customInlineSyntaxes,
|
|
||||||
);
|
|
||||||
for (final parser in customNodeParsers) {
|
|
||||||
final node = parser.transform(line);
|
|
||||||
if (node != null) {
|
|
||||||
return node;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heading Style
|
if (nodes.isEmpty) {
|
||||||
if (line.startsWith('### ')) {
|
Log.editor.debug(
|
||||||
return headingNode(
|
'empty result from node: $mdNode, text: ${mdNode.textContent}',
|
||||||
level: 3,
|
|
||||||
attributes: {'delta': decoder.convert(line.substring(4)).toJson()},
|
|
||||||
);
|
|
||||||
} else if (line.startsWith('## ')) {
|
|
||||||
return headingNode(
|
|
||||||
level: 2,
|
|
||||||
attributes: {'delta': decoder.convert(line.substring(3)).toJson()},
|
|
||||||
);
|
|
||||||
} else if (line.startsWith('# ')) {
|
|
||||||
return headingNode(
|
|
||||||
level: 1,
|
|
||||||
attributes: {'delta': decoder.convert(line.substring(2)).toJson()},
|
|
||||||
);
|
|
||||||
} else if (line.startsWith('- [ ] ')) {
|
|
||||||
return todoListNode(
|
|
||||||
checked: false,
|
|
||||||
attributes: {'delta': decoder.convert(line.substring(6)).toJson()},
|
|
||||||
);
|
|
||||||
} else if (line.startsWith('- [x] ')) {
|
|
||||||
return todoListNode(
|
|
||||||
checked: true,
|
|
||||||
attributes: {'delta': decoder.convert(line.substring(6)).toJson()},
|
|
||||||
);
|
|
||||||
} else if (line.startsWith('> ')) {
|
|
||||||
return quoteNode(
|
|
||||||
attributes: {'delta': decoder.convert(line.substring(2)).toJson()},
|
|
||||||
);
|
|
||||||
} else if (line.startsWith('- ') || line.startsWith('* ')) {
|
|
||||||
return bulletedListNode(
|
|
||||||
attributes: {'delta': decoder.convert(line.substring(2)).toJson()},
|
|
||||||
);
|
|
||||||
} else if (line.isNotEmpty && RegExp('^-*').stringMatch(line) == line) {
|
|
||||||
return Node(type: 'divider');
|
|
||||||
} else if (line.startsWith('```') && line.endsWith('```')) {
|
|
||||||
return codeBlockNodeFromMarkdown(line, decoder);
|
|
||||||
} else if (imageRegex.hasMatch(line.trim())) {
|
|
||||||
final filePath = extractImagePath(line.trim());
|
|
||||||
final regex = RegExp(
|
|
||||||
r'\.(png|jpg|jpeg|gif|bmp|svg|webp|tiff|ico)(\?.*)?$',
|
|
||||||
caseSensitive: false,
|
|
||||||
);
|
|
||||||
if (filePath == null || !regex.hasMatch(filePath)) {
|
|
||||||
return paragraphNode(text: line.trim());
|
|
||||||
}
|
|
||||||
return imageNode(url: filePath);
|
|
||||||
} else if (assetRegex.hasMatch(line.trim())) {
|
|
||||||
// this might be a url or a file like pdf, videos, etc
|
|
||||||
final filepath = extractFilePath(line.trim());
|
|
||||||
if (filepath != null && !htmlRegex.hasMatch(filepath)) {
|
|
||||||
return paragraphNode(text: line);
|
|
||||||
}
|
|
||||||
} else if (numberedListRegex.hasMatch(line)) {
|
|
||||||
return numberedListNode(
|
|
||||||
attributes: {
|
|
||||||
'delta':
|
|
||||||
decoder.convert(line.substring(line.indexOf('.') + 2)).toJson(),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.isNotEmpty) {
|
return nodes;
|
||||||
return paragraphNode(
|
|
||||||
attributes: {'delta': decoder.convert(line).toJson()},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return paragraphNode(
|
|
||||||
attributes: {'delta': Delta().toJson()},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String? extractImagePath(String text) {
|
|
||||||
final match = imageRegex.firstMatch(text);
|
|
||||||
return match?.group(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
String? extractFilePath(String text) {
|
|
||||||
final match = assetRegex.firstMatch(text);
|
|
||||||
return match?.group(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Node codeBlockNodeFromMarkdown(
|
|
||||||
String markdown,
|
|
||||||
DeltaMarkdownDecoder decoder,
|
|
||||||
) {
|
|
||||||
// This if conditions checks for cases like - ````` where we can see
|
|
||||||
// it starts with ``` as well as end with ``` but is not a valid code block
|
|
||||||
// so this is treated like a normal paragraph
|
|
||||||
if (!markdown.contains('\n') &&
|
|
||||||
markdown.split('`').length - 1 == markdown.length) {
|
|
||||||
return paragraphNode(
|
|
||||||
attributes: {'delta': decoder.convert(markdown).toJson()},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const codeMarker = '```';
|
|
||||||
int codeStartIndex = markdown.indexOf(codeMarker);
|
|
||||||
int codeEndIndex = markdown.indexOf(
|
|
||||||
codeMarker,
|
|
||||||
codeStartIndex + codeMarker.length,
|
|
||||||
);
|
|
||||||
// This if condition is for handling cases like ```\n`
|
|
||||||
// In this case codeStartIndex = 0 and codeEndIndex = -1
|
|
||||||
if (codeEndIndex < codeStartIndex) {
|
|
||||||
return paragraphNode(
|
|
||||||
attributes: {'delta': decoder.convert(markdown).toJson()},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
String codeBlock = markdown.substring(
|
|
||||||
codeStartIndex + codeMarker.length,
|
|
||||||
codeEndIndex,
|
|
||||||
);
|
|
||||||
List<String> codeLines = codeBlock.trim().split('\n');
|
|
||||||
|
|
||||||
// This if condition will handle cases like ```hello world```
|
|
||||||
// this would be treated as a code block with hello world as its content
|
|
||||||
if (codeLines[0].split(' ').length > 1) {
|
|
||||||
return Node(
|
|
||||||
type: 'code',
|
|
||||||
attributes: {
|
|
||||||
'delta': decoder.convert(codeBlock).toJson(),
|
|
||||||
'language': null,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final language = codeLines[0].trim();
|
|
||||||
final codeContent = codeLines.sublist(1).join('\n');
|
|
||||||
|
|
||||||
return Node(
|
|
||||||
type: 'code',
|
|
||||||
attributes: {
|
|
||||||
'delta': decoder.convert(codeContent).toJson(),
|
|
||||||
'language': language.isEmpty ? null : language,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
abstract class CustomMarkdownParser {
|
||||||
|
const CustomMarkdownParser();
|
||||||
|
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import '../../../../../appflowy_editor.dart';
|
|
||||||
|
|
||||||
abstract class CustomNodeParser {
|
|
||||||
Node? transform(String input);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownBlockQuoteParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownBlockQuoteParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'blockquote') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final deltaDecoder = DeltaMarkdownDecoder();
|
||||||
|
return [
|
||||||
|
quoteNode(
|
||||||
|
delta: deltaDecoder.convertNodes(element.children),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownDividerParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownDividerParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'hr') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
dividerNode(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
final _headingTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||||
|
|
||||||
|
class MarkdownHeadingParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownHeadingParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_headingTags.contains(element.tag)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final level = _headingTags.indexOf(element.tag) + 1;
|
||||||
|
|
||||||
|
final deltaDecoder = DeltaMarkdownDecoder();
|
||||||
|
return [
|
||||||
|
headingNode(
|
||||||
|
level: level,
|
||||||
|
delta: deltaDecoder.convertNodes(element.children),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownImageParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownImageParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.children?.length != 1 ||
|
||||||
|
element.children?.first is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final ec = element.children?.first as md.Element;
|
||||||
|
if (ec.tag != 'img' || ec.attributes['src'] == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
imageNode(url: ec.attributes['src']!),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownOrderedListItemParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownOrderedListItemParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'li' ||
|
||||||
|
element.attributes.isNotEmpty ||
|
||||||
|
listType != MarkdownListType.ordered) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<md.Node> ec = [];
|
||||||
|
int sliceIndex = -1;
|
||||||
|
if (element.children != null) {
|
||||||
|
for (final child in element.children!.reversed) {
|
||||||
|
if (child is md.Element) {
|
||||||
|
ec.add(child);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceIndex = element.children!.length - ec.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
final deltaDecoder = DeltaMarkdownDecoder();
|
||||||
|
final deltaNodes = sliceIndex == -1
|
||||||
|
? element.children
|
||||||
|
: element.children!.slice(0, sliceIndex);
|
||||||
|
|
||||||
|
return [
|
||||||
|
numberedListNode(
|
||||||
|
delta: deltaDecoder.convertNodes(
|
||||||
|
deltaNodes,
|
||||||
|
),
|
||||||
|
children: parseElementChildren(
|
||||||
|
ec.reversed.toList(),
|
||||||
|
parsers,
|
||||||
|
listType: MarkdownListType.ordered,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownOrderedListParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownOrderedListParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'ol') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// flatten the list
|
||||||
|
return parseElementChildren(
|
||||||
|
element.children,
|
||||||
|
parsers,
|
||||||
|
listType: MarkdownListType.ordered,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownParagraphParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownParagraphParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'p') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// exclude the img tag
|
||||||
|
final ec = element.children;
|
||||||
|
if (ec != null && ec.length == 1 && ec.first is md.Element) {
|
||||||
|
final e = ec.first as md.Element;
|
||||||
|
if (e.tag == 'img') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final deltaDecoder = DeltaMarkdownDecoder();
|
||||||
|
return [
|
||||||
|
paragraphNode(
|
||||||
|
delta: deltaDecoder.convertNodes(element.children),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
enum MarkdownListType {
|
||||||
|
ordered,
|
||||||
|
unordered,
|
||||||
|
unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Node> parseElementChildren(
|
||||||
|
List<md.Node>? elementChildren,
|
||||||
|
List<CustomMarkdownParser> parsers, {
|
||||||
|
MarkdownListType listType = MarkdownListType.unknown,
|
||||||
|
}) {
|
||||||
|
final List<Node> children = [];
|
||||||
|
|
||||||
|
if (elementChildren == null || elementChildren.isEmpty) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final child in elementChildren) {
|
||||||
|
for (final parser in parsers) {
|
||||||
|
final nodes = parser.transform(
|
||||||
|
child,
|
||||||
|
parsers,
|
||||||
|
listType,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nodes.isNotEmpty) {
|
||||||
|
children.addAll(nodes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/block_component/table_block_component/table_node.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownTableListParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownTableListParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'table') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final ec = element.children;
|
||||||
|
if (ec == null || ec.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<List<Node>> cells = [];
|
||||||
|
|
||||||
|
final th = ec
|
||||||
|
.whereType<md.Element>()
|
||||||
|
.where((e) => e.tag == 'thead')
|
||||||
|
.firstOrNull
|
||||||
|
?.children
|
||||||
|
?.whereType<md.Element>()
|
||||||
|
.where((e) => e.tag == 'tr')
|
||||||
|
.expand((e) => e.children?.whereType<md.Element>().toList() ?? [])
|
||||||
|
.where((e) => e.tag == 'th')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final td = ec
|
||||||
|
.whereType<md.Element>()
|
||||||
|
.where((e) => e.tag == 'tbody')
|
||||||
|
.firstOrNull
|
||||||
|
?.children
|
||||||
|
?.whereType<md.Element>()
|
||||||
|
.where((e) => e.tag == 'tr')
|
||||||
|
.expand((e) => e.children?.whereType<md.Element>().toList() ?? [])
|
||||||
|
.where((e) => e.tag == 'td')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (th == null || td == null || th.isEmpty || td.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < th.length; i++) {
|
||||||
|
final List<Node> row = [];
|
||||||
|
|
||||||
|
row.add(
|
||||||
|
paragraphNode(
|
||||||
|
delta: DeltaMarkdownDecoder().convertNodes(th[i].children),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (var j = i; j < td.length; j += th.length) {
|
||||||
|
row.add(
|
||||||
|
paragraphNode(
|
||||||
|
delta: DeltaMarkdownDecoder().convertNodes(td[j].children),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cells.add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
final tableNode = TableNode.fromList(cells);
|
||||||
|
|
||||||
|
return [
|
||||||
|
tableNode.node,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownTodoListParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownTodoListParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'li' ||
|
||||||
|
element.attributes['class'] != 'task-list-item') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final ec = element.children;
|
||||||
|
if (ec == null || ec.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// for the task list item, the first two children are the input and the label
|
||||||
|
// the rest are its children
|
||||||
|
final checked = (ec[0] as md.Element).attributes['checked'] == 'true';
|
||||||
|
md.Element? last;
|
||||||
|
// if the last child is not a list or paragraph, ignore it
|
||||||
|
if (ec.last is md.Element) {
|
||||||
|
final lastElement = ec.last as md.Element;
|
||||||
|
if (lastElement.tag == 'ul' || lastElement.tag == 'ol') {
|
||||||
|
last = lastElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final deltaDecoder = DeltaMarkdownDecoder();
|
||||||
|
return [
|
||||||
|
todoListNode(
|
||||||
|
checked: checked,
|
||||||
|
delta: deltaDecoder.convertNodes(
|
||||||
|
element.children?.sublist(
|
||||||
|
1,
|
||||||
|
ec.length - (last != null ? 1 : 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
children: last == null
|
||||||
|
? null
|
||||||
|
: parseElementChildren(
|
||||||
|
[last],
|
||||||
|
parsers,
|
||||||
|
listType: MarkdownListType.unknown,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownUnorderedListItemParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownUnorderedListItemParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'li' ||
|
||||||
|
element.attributes.isNotEmpty ||
|
||||||
|
listType != MarkdownListType.unordered) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<md.Node> ec = [];
|
||||||
|
int sliceIndex = -1;
|
||||||
|
if (element.children != null) {
|
||||||
|
for (final child in element.children!.reversed) {
|
||||||
|
if (child is md.Element) {
|
||||||
|
ec.add(child);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceIndex = element.children!.length - ec.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
final deltaDecoder = DeltaMarkdownDecoder();
|
||||||
|
final deltaNodes = sliceIndex == -1
|
||||||
|
? element.children
|
||||||
|
: element.children!.slice(0, sliceIndex);
|
||||||
|
|
||||||
|
return [
|
||||||
|
bulletedListNode(
|
||||||
|
delta: deltaDecoder.convertNodes(
|
||||||
|
deltaNodes,
|
||||||
|
),
|
||||||
|
children: parseElementChildren(
|
||||||
|
ec.reversed.toList(),
|
||||||
|
parsers,
|
||||||
|
listType: MarkdownListType.unknown,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class MarkdownUnorderedListParserV2 extends CustomMarkdownParser {
|
||||||
|
const MarkdownUnorderedListParserV2();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> transform(
|
||||||
|
md.Node element,
|
||||||
|
List<CustomMarkdownParser> parsers,
|
||||||
|
MarkdownListType listType,
|
||||||
|
) {
|
||||||
|
if (element is! md.Element) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.tag != 'ul') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// flatten the list
|
||||||
|
return parseElementChildren(
|
||||||
|
element.children,
|
||||||
|
parsers,
|
||||||
|
listType: MarkdownListType.unordered,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue