items) {
+ var anyEmpty = false;
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].lines.length == 1) {
+ continue;
+ }
+ while (items[i].lines.isNotEmpty &&
+ _emptyPattern.hasMatch(items[i].lines.last)) {
+ if (i < items.length - 1) {
+ anyEmpty = true;
+ }
+ items[i].lines.removeLast();
+ }
+ }
+ return anyEmpty;
+ }
+
+ static int _expandedTabLength(String input) {
+ var length = 0;
+ for (final char in input.codeUnits) {
+ length += char == 0x9 ? 4 - (length % 4) : 1;
+ }
+ return length;
+ }
+}
+
+/// Parses unordered lists.
+class UnorderedListSyntax extends ListSyntax {
+ const UnorderedListSyntax();
+
+ @override
+ RegExp get pattern => _ulPattern;
+
+ @override
+ String get listTag => 'ul';
+}
+
+/// Parses ordered lists.
+class OrderedListSyntax extends ListSyntax {
+ const OrderedListSyntax();
+
+ @override
+ RegExp get pattern => _olPattern;
+
+ @override
+ String get listTag => 'ol';
+}
+
+/// Parses tables.
+class TableSyntax extends BlockSyntax {
+ const TableSyntax();
+
+ static final _pipePattern = RegExp(r'\s*\|\s*');
+ static final _openingPipe = RegExp(r'^\|\s*');
+ static final _closingPipe = RegExp(r'\s*\|$');
+
+ @override
+ bool get canEndBlock => false;
+
+ @override
+ bool canParse(BlockParser parser) {
+ // Note: matches *next* line, not the current one. We're looking for the
+ // bar separating the head row from the body rows.
+ return parser.matchesNext(_tablePattern);
+ }
+
+ /// Parses a table into its three parts:
+ ///
+ /// * a head row of head cells (`` cells)
+ /// * a divider of hyphens and pipes (not rendered)
+ /// * many body rows of body cells (` | ` cells)
+ @override
+ Node? parse(BlockParser parser) {
+ final alignments = parseAlignments(parser.next!);
+ final columnCount = alignments.length;
+ final headRow = parseRow(parser, alignments, 'th');
+ if (headRow.children!.length != columnCount) {
+ return null;
+ }
+ final head = Element('thead', [headRow]);
+
+ // Advance past the divider of hyphens.
+ parser.advance();
+
+ final rows = [];
+ while (!parser.isDone && !BlockSyntax.isAtBlockEnd(parser)) {
+ final row = parseRow(parser, alignments, 'td');
+ while (row.children!.length < columnCount) {
+ // Insert synthetic empty cells.
+ row.children!.add(Element.empty('td'));
+ }
+ while (row.children!.length > columnCount) {
+ row.children!.removeLast();
+ }
+ rows.add(row);
+ }
+ if (rows.isEmpty) {
+ return Element('table', [head]);
+ } else {
+ final body = Element('tbody', rows);
+
+ return Element('table', [head, body]);
+ }
+ }
+
+ List parseAlignments(String line) {
+ line = line.replaceFirst(_openingPipe, '').replaceFirst(_closingPipe, '');
+ return line.split('|').map((column) {
+ column = column.trim();
+ if (column.startsWith(':') && column.endsWith(':')) {
+ return 'center';
+ }
+ if (column.startsWith(':')) {
+ return 'left';
+ }
+ if (column.endsWith(':')) {
+ return 'right';
+ }
+ return null;
+ }).toList();
+ }
+
+ Element parseRow(
+ BlockParser parser, List alignments, String cellType) {
+ final line = parser.current
+ .replaceFirst(_openingPipe, '')
+ .replaceFirst(_closingPipe, '');
+ final cells = line.split(_pipePattern);
+ parser.advance();
+ final row = [];
+ String? preCell;
+
+ for (var cell in cells) {
+ if (preCell != null) {
+ cell = preCell + cell;
+ preCell = null;
+ }
+ if (cell.endsWith('\\')) {
+ preCell = '${cell.substring(0, cell.length - 1)}|';
+ continue;
+ }
+
+ final contents = UnparsedContent(cell);
+ row.add(Element(cellType, [contents]));
+ }
+
+ for (var i = 0; i < row.length && i < alignments.length; i++) {
+ if (alignments[i] == null) {
+ continue;
+ }
+ row[i].attributes['style'] = 'text-align: ${alignments[i]};';
+ }
+
+ return Element('tr', row);
+ }
+}
+
+/// Parses paragraphs of regular text.
+class ParagraphSyntax extends BlockSyntax {
+ const ParagraphSyntax();
+
+ static final _reflinkDefinitionStart = RegExp(r'[ ]{0,3}\[');
+
+ static final _whitespacePattern = RegExp(r'^\s*$');
+
+ @override
+ bool get canEndBlock => false;
+
+ @override
+ bool canParse(BlockParser parser) => true;
+
+ @override
+ Node parse(BlockParser parser) {
+ final childLines = [];
+
+ // Eat until we hit something that ends a paragraph.
+ while (!BlockSyntax.isAtBlockEnd(parser)) {
+ childLines.add(parser.current);
+ parser.advance();
+ }
+
+ final paragraphLines = _extractReflinkDefinitions(parser, childLines);
+ if (paragraphLines == null) {
+ // Paragraph consisted solely of reference link definitions.
+ return Text('');
+ } else {
+ final contents = UnparsedContent(paragraphLines.join('\n'));
+ return Element('p', [contents]);
+ }
+ }
+
+ /// Extract reference link definitions from the front of the paragraph, and
+ /// return the remaining paragraph lines.
+ List? _extractReflinkDefinitions(
+ BlockParser parser, List lines) {
+ bool lineStartsReflinkDefinition(int i) =>
+ lines[i].startsWith(_reflinkDefinitionStart);
+
+ var i = 0;
+ loopOverDefinitions:
+ while (true) {
+ // Check for reflink definitions.
+ if (!lineStartsReflinkDefinition(i)) {
+ // It's paragraph content from here on out.
+ break;
+ }
+ var contents = lines[i];
+ var j = i + 1;
+ while (j < lines.length) {
+ // Check to see if the _next_ line might start a reflink definition.
+ // Even if it turns out not to be, but it started with a '[', then it
+ // is not a part of _this_ possible reflink definition.
+ if (lineStartsReflinkDefinition(j)) {
+ // Try to parse [contents] as a reflink definition.
+ if (_parseReflinkDefinition(parser, contents)) {
+ // Loop again, starting at the next possible reflink definition.
+ i = j;
+ continue loopOverDefinitions;
+ } else {
+ // Could not parse [contents] as a reflink definition.
+ break;
+ }
+ } else {
+ contents = '$contents\n${lines[j]}';
+ j++;
+ }
+ }
+ // End of the block.
+ if (_parseReflinkDefinition(parser, contents)) {
+ i = j;
+ break;
+ }
+
+ // It may be that there is a reflink definition starting at [i], but it
+ // does not extend all the way to [j], such as:
+ //
+ // [link]: url // line i
+ // "title"
+ // garbage
+ // [link2]: url // line j
+ //
+ // In this case, [i, i+1] is a reflink definition, and the rest is
+ // paragraph content.
+ while (j >= i) {
+ // This isn't the most efficient loop, what with this big ole'
+ // Iterable allocation (`getRange`) followed by a big 'ole String
+ // allocation, but we
+ // must walk backwards, checking each range.
+ contents = lines.getRange(i, j).join('\n');
+ if (_parseReflinkDefinition(parser, contents)) {
+ // That is the last reflink definition. The rest is paragraph
+ // content.
+ i = j;
+ break;
+ }
+ j--;
+ }
+ // The ending was not a reflink definition at all. Just paragraph
+ // content.
+
+ break;
+ }
+
+ if (i == lines.length) {
+ // No paragraph content.
+ return null;
+ } else {
+ // Ends with paragraph content.
+ return lines.sublist(i);
+ }
+ }
+
+ // Parse [contents] as a reference link definition.
+ //
+ // Also adds the reference link definition to the document.
+ //
+ // Returns whether [contents] could be parsed as a reference link definition.
+ bool _parseReflinkDefinition(BlockParser parser, String contents) {
+ final pattern = RegExp(
+ // Leading indentation.
+ r'''^[ ]{0,3}'''
+ // Reference id in brackets, and URL.
+ r'''\[((?:\\\]|[^\]])+)\]:\s*(?:<(\S+)>|(\S+))\s*'''
+ // Title in double or single quotes, or parens.
+ r'''("[^"]+"|'[^']+'|\([^)]+\)|)\s*$''',
+ multiLine: true);
+ final match = pattern.firstMatch(contents);
+ if (match == null) {
+ // Not a reference link definition.
+ return false;
+ }
+ if (match[0]!.length < contents.length) {
+ // Trailing text. No good.
+ return false;
+ }
+
+ var label = match[1]!;
+ final destination = match[2] ?? match[3];
+ var title = match[4];
+
+ // The label must contain at least one non-whitespace character.
+ if (_whitespacePattern.hasMatch(label)) {
+ return false;
+ }
+
+ if (title == '') {
+ // No title.
+ title = null;
+ } else {
+ // Remove "", '', or ().
+ title = title!.substring(1, title.length - 1);
+ }
+
+ // References are case-insensitive, and internal whitespace is compressed.
+ label =
+ label.toLowerCase().trim().replaceAll(_oneOrMoreWhitespacePattern, ' ');
+
+ parser.document.linkReferences
+ .putIfAbsent(label, () => LinkReference(label, destination!, title!));
+ return true;
+ }
+}
diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown.dart
new file mode 100644
index 00000000..2221c185
--- /dev/null
+++ b/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown.dart
@@ -0,0 +1,27 @@
+library delta_markdown;
+
+import 'dart:convert';
+
+import 'delta_markdown_decoder.dart';
+import 'delta_markdown_encoder.dart';
+
+/// Codec used to convert between Markdown and Quill deltas.
+const DeltaMarkdownCodec _kCodec = DeltaMarkdownCodec();
+
+String markdownToDelta(String markdown) {
+ return _kCodec.decode(markdown);
+}
+
+String deltaToMarkdown(String delta) {
+ return _kCodec.encode(delta);
+}
+
+class DeltaMarkdownCodec extends Codec {
+ const DeltaMarkdownCodec();
+
+ @override
+ Converter get decoder => DeltaMarkdownDecoder();
+
+ @override
+ Converter get encoder => DeltaMarkdownEncoder();
+}
diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown_decoder.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown_decoder.dart
new file mode 100644
index 00000000..2333c93c
--- /dev/null
+++ b/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown_decoder.dart
@@ -0,0 +1,256 @@
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:flutter_quill/flutter_quill.dart'
+ show Attribute, AttributeScope, Delta, LinkAttribute;
+
+import 'ast.dart' as ast;
+import 'document.dart';
+
+class DeltaMarkdownDecoder extends Converter {
+ @override
+ String convert(String input) {
+ final lines = input.replaceAll('\r\n', '\n').split('\n');
+
+ final markdownDocument = Document().parseLines(lines);
+
+ return jsonEncode(_DeltaVisitor().convert(markdownDocument).toJson());
+ }
+}
+
+class _DeltaVisitor implements ast.NodeVisitor {
+ static final _blockTags =
+ RegExp('h1|h2|h3|h4|h5|h6|hr|pre|ul|ol|blockquote|p|pre');
+
+ static final _embedTags = RegExp('hr|img');
+
+ late Delta delta;
+
+ late Queue activeInlineAttributes;
+ Attribute? activeBlockAttribute;
+ late Set uniqueIds;
+
+ ast.Element? previousElement;
+ late ast.Element previousToplevelElement;
+
+ Delta convert(List nodes) {
+ delta = Delta();
+ activeInlineAttributes = Queue();
+ uniqueIds = {};
+
+ for (final node in nodes) {
+ node.accept(this);
+ }
+
+ // Ensure the delta ends with a newline.
+ if (delta.length > 0 && delta.last.value != '\n') {
+ delta.insert('\n', activeBlockAttribute?.toJson());
+ }
+
+ return delta;
+ }
+
+ @override
+ void visitText(ast.Text text) {
+ // Remove trailing newline
+ //final lines = text.text.trim().split('\n');
+
+ /*
+ final attributes = Map();
+ for (final attr in activeInlineAttributes) {
+ attributes.addAll(attr.toJson());
+ }
+
+ for (final l in lines) {
+ delta.insert(l, attributes);
+ delta.insert('\n', activeBlockAttribute.toJson());
+ }*/
+
+ final str = text.text;
+ //if (str.endsWith('\n')) str = str.substring(0, str.length - 1);
+
+ final attributes = {};
+ for (final attr in activeInlineAttributes) {
+ attributes.addAll(attr.toJson());
+ }
+
+ var newlineIndex = str.indexOf('\n');
+ var startIndex = 0;
+ while (newlineIndex != -1) {
+ final previousText = str.substring(startIndex, newlineIndex);
+ if (previousText.isNotEmpty) {
+ delta.insert(previousText, attributes.isNotEmpty ? attributes : null);
+ }
+ delta.insert('\n', activeBlockAttribute?.toJson());
+
+ startIndex = newlineIndex + 1;
+ newlineIndex = str.indexOf('\n', newlineIndex + 1);
+ }
+
+ if (startIndex < str.length) {
+ final lastStr = str.substring(startIndex);
+ delta.insert(lastStr, attributes.isNotEmpty ? attributes : null);
+ }
+ }
+
+ @override
+ bool visitElementBefore(ast.Element element) {
+ // Hackish. Separate block-level elements with newlines.
+ final attr = _tagToAttribute(element);
+
+ if (delta.isNotEmpty && _blockTags.firstMatch(element.tag) != null) {
+ if (element.isToplevel) {
+ // If the last active block attribute is not a list, we need to finish
+ // it off.
+ if (previousToplevelElement.tag != 'ul' &&
+ previousToplevelElement.tag != 'ol' &&
+ previousToplevelElement.tag != 'pre' &&
+ previousToplevelElement.tag != 'hr') {
+ delta.insert('\n', activeBlockAttribute?.toJson());
+ }
+
+ // Only separate the blocks if both are paragraphs.
+ //
+ // TODO(kolja): Determine which behavior we really want here.
+ // We can either insert an additional newline or just have the
+ // paragraphs as single lines. Zefyr will by default render two lines
+ // are different paragraphs so for now we will not add an additonal
+ // newline here.
+ //
+ // if (previousToplevelElement != null &&
+ // previousToplevelElement.tag == 'p' &&
+ // element.tag == 'p') {
+ // delta.insert('\n');
+ // }
+ } else if (element.tag == 'p' &&
+ previousElement != null &&
+ !previousElement!.isToplevel &&
+ !previousElement!.children!.contains(element)) {
+ // Here we have two children of the same toplevel element. These need
+ // to be separated by additional newlines.
+
+ delta
+ // Finish off the last lower-level block.
+ ..insert('\n', activeBlockAttribute?.toJson())
+ // Add an empty line between the lower-level blocks.
+ ..insert('\n', activeBlockAttribute?.toJson());
+ }
+ }
+
+ // Keep track of the top-level block attribute.
+ if (element.isToplevel && element.tag != 'hr') {
+ // Hacky solution for horizontal rule so that the attribute is not added
+ // to the line feed at the end of the line.
+ activeBlockAttribute = attr;
+ }
+
+ if (_embedTags.firstMatch(element.tag) != null) {
+ // We write out the element here since the embed has no children or
+ // content.
+ delta.insert(attr!.toJson());
+ } else if (_blockTags.firstMatch(element.tag) == null && attr != null) {
+ activeInlineAttributes.addLast(attr);
+ }
+
+ previousElement = element;
+ if (element.isToplevel) {
+ previousToplevelElement = element;
+ }
+
+ if (element.isEmpty) {
+ // Empty element like .
+ //buffer.write(' />');
+
+ if (element.tag == 'br') {
+ delta.insert('\n');
+ }
+
+ return false;
+ } else {
+ //buffer.write('>');
+ return true;
+ }
+ }
+
+ @override
+ void visitElementAfter(ast.Element element) {
+ if (element.tag == 'li' &&
+ (previousToplevelElement.tag == 'ol' ||
+ previousToplevelElement.tag == 'ul')) {
+ delta.insert('\n', activeBlockAttribute?.toJson());
+ }
+
+ final attr = _tagToAttribute(element);
+ if (attr == null || !attr.isInline || activeInlineAttributes.last != attr) {
+ return;
+ }
+ activeInlineAttributes.removeLast();
+
+ // Always keep track of the last element.
+ // This becomes relevant if we have something like
+ //
+ //
+ previousElement = element;
+ }
+
+ /// Uniquifies an id generated from text.
+ String uniquifyId(String id) {
+ if (!uniqueIds.contains(id)) {
+ uniqueIds.add(id);
+ return id;
+ }
+
+ var suffix = 2;
+ var suffixedId = '$id-$suffix';
+ while (uniqueIds.contains(suffixedId)) {
+ suffixedId = '$id-${suffix++}';
+ }
+ uniqueIds.add(suffixedId);
+ return suffixedId;
+ }
+
+ Attribute? _tagToAttribute(ast.Element el) {
+ switch (el.tag) {
+ case 'em':
+ return Attribute.italic;
+ case 'strong':
+ return Attribute.bold;
+ case 'ul':
+ return Attribute.ul;
+ case 'ol':
+ return Attribute.ol;
+ case 'pre':
+ return Attribute.codeBlock;
+ case 'blockquote':
+ return Attribute.blockQuote;
+ case 'h1':
+ return Attribute.h1;
+ case 'h2':
+ return Attribute.h2;
+ case 'h3':
+ return Attribute.h3;
+ case 'a':
+ final href = el.attributes['href'];
+ return LinkAttribute(href);
+ case 'img':
+ final href = el.attributes['src'];
+ return ImageAttribute(href);
+ case 'hr':
+ return const DividerAttribute();
+ }
+
+ return null;
+ }
+}
+
+class ImageAttribute extends Attribute {
+ const ImageAttribute(String? val)
+ : super('image', AttributeScope.embeds, val);
+}
+
+class DividerAttribute extends Attribute {
+ const DividerAttribute() : super('divider', AttributeScope.embeds, 'hr');
+}
diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown_encoder.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown_encoder.dart
new file mode 100644
index 00000000..2134a92e
--- /dev/null
+++ b/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown_encoder.dart
@@ -0,0 +1,270 @@
+import 'dart:convert';
+
+import 'package:collection/collection.dart' show IterableExtension;
+import 'package:flutter_quill/flutter_quill.dart'
+ show Attribute, AttributeScope, BlockEmbed, Delta, DeltaIterator, Style;
+
+class DeltaMarkdownEncoder extends Converter {
+ static const _lineFeedAsciiCode = 0x0A;
+
+ late StringBuffer markdownBuffer;
+ late StringBuffer lineBuffer;
+
+ Attribute? currentBlockStyle;
+ late Style currentInlineStyle;
+
+ late List currentBlockLines;
+
+ /// Converts the [input] delta to Markdown.
+ @override
+ String convert(String input) {
+ markdownBuffer = StringBuffer();
+ lineBuffer = StringBuffer();
+ currentInlineStyle = const Style();
+ currentBlockLines = [];
+
+ final inputJson = jsonDecode(input) as List?;
+ if (inputJson is! List) {
+ throw ArgumentError('Unexpected formatting of the input delta string.');
+ }
+ final delta = Delta.fromJson(inputJson);
+ final iterator = DeltaIterator(delta);
+
+ while (iterator.hasNext) {
+ final operation = iterator.next();
+
+ if (operation.data is String) {
+ final operationData = operation.data as String;
+
+ if (!operationData.contains('\n')) {
+ _handleInline(lineBuffer, operationData, operation.attributes);
+ } else {
+ _handleLine(operationData, operation.attributes);
+ }
+ } else if (operation.data is Map) {
+ _handleEmbed(operation.data as Map);
+ } else {
+ throw ArgumentError('Unexpected formatting of the input delta string.');
+ }
+ }
+
+ _handleBlock(currentBlockStyle); // Close the last block
+
+ return markdownBuffer.toString();
+ }
+
+ void _handleInline(
+ StringBuffer buffer,
+ String text,
+ Map? attributes,
+ ) {
+ final style = Style.fromJson(attributes);
+
+ // First close any current styles if needed
+ final markedForRemoval = [];
+ // Close the styles in reverse order, e.g. **_ for _**Test**_.
+ for (final value
+ in currentInlineStyle.attributes.values.toList().reversed) {
+ // TODO(tillf): Is block correct?
+ if (value.scope == AttributeScope.block) {
+ continue;
+ }
+ if (style.containsKey(value.key)) {
+ continue;
+ }
+
+ final padding = _trimRight(buffer);
+ _writeAttribute(buffer, value, close: true);
+ if (padding.isNotEmpty) {
+ buffer.write(padding);
+ }
+ markedForRemoval.add(value);
+ }
+
+ // Make sure to remove all attributes that are marked for removal.
+ for (final value in markedForRemoval) {
+ currentInlineStyle.attributes.removeWhere((_, v) => v == value);
+ }
+
+ // Now open any new styles.
+ for (final attribute in style.attributes.values) {
+ // TODO(tillf): Is block correct?
+ if (attribute.scope == AttributeScope.block) {
+ continue;
+ }
+ if (currentInlineStyle.containsKey(attribute.key)) {
+ continue;
+ }
+ final originalText = text;
+ text = text.trimLeft();
+ final padding = ' ' * (originalText.length - text.length);
+ if (padding.isNotEmpty) {
+ buffer.write(padding);
+ }
+ _writeAttribute(buffer, attribute);
+ }
+
+ // Write the text itself
+ buffer.write(text);
+ currentInlineStyle = style;
+ }
+
+ void _handleLine(String data, Map? attributes) {
+ final span = StringBuffer();
+
+ for (var i = 0; i < data.length; i++) {
+ if (data.codeUnitAt(i) == _lineFeedAsciiCode) {
+ if (span.isNotEmpty) {
+ // Write the span if it's not empty.
+ _handleInline(lineBuffer, span.toString(), attributes);
+ }
+ // Close any open inline styles.
+ _handleInline(lineBuffer, '', null);
+
+ final lineBlock = Style.fromJson(attributes)
+ .attributes
+ .values
+ .singleWhereOrNull((a) => a.scope == AttributeScope.block);
+
+ if (lineBlock == currentBlockStyle) {
+ currentBlockLines.add(lineBuffer.toString());
+ } else {
+ _handleBlock(currentBlockStyle);
+ currentBlockLines
+ ..clear()
+ ..add(lineBuffer.toString());
+
+ currentBlockStyle = lineBlock;
+ }
+ lineBuffer.clear();
+
+ span.clear();
+ } else {
+ span.writeCharCode(data.codeUnitAt(i));
+ }
+ }
+
+ // Remaining span
+ if (span.isNotEmpty) {
+ _handleInline(lineBuffer, span.toString(), attributes);
+ }
+ }
+
+ void _handleEmbed(Map data) {
+ final embed = BlockEmbed(data.keys.first, data.values.first as String);
+
+ if (embed.type == 'image') {
+ _writeEmbedTag(lineBuffer, embed);
+ _writeEmbedTag(lineBuffer, embed, close: true);
+ } else if (embed.type == 'divider') {
+ _writeEmbedTag(lineBuffer, embed);
+ _writeEmbedTag(lineBuffer, embed, close: true);
+ }
+ }
+
+ void _handleBlock(Attribute? blockStyle) {
+ if (currentBlockLines.isEmpty) {
+ return; // Empty block
+ }
+
+ // If there was a block before this one, add empty line between the blocks
+ if (markdownBuffer.isNotEmpty) {
+ markdownBuffer.writeln();
+ }
+
+ if (blockStyle == null) {
+ markdownBuffer
+ ..write(currentBlockLines.join('\n'))
+ ..writeln();
+ } else if (blockStyle == Attribute.codeBlock) {
+ _writeAttribute(markdownBuffer, blockStyle);
+ markdownBuffer.write(currentBlockLines.join('\n'));
+ _writeAttribute(markdownBuffer, blockStyle, close: true);
+ markdownBuffer.writeln();
+ } else {
+ // Dealing with lists or a quote.
+ for (final line in currentBlockLines) {
+ _writeBlockTag(markdownBuffer, blockStyle);
+ markdownBuffer
+ ..write(line)
+ ..writeln();
+ }
+ }
+ }
+
+ String _trimRight(StringBuffer buffer) {
+ final text = buffer.toString();
+ if (!text.endsWith(' ')) {
+ return '';
+ }
+
+ final result = text.trimRight();
+ buffer
+ ..clear()
+ ..write(result);
+ return ' ' * (text.length - result.length);
+ }
+
+ void _writeAttribute(
+ StringBuffer buffer,
+ Attribute attribute, {
+ bool close = false,
+ }) {
+ if (attribute.key == Attribute.bold.key) {
+ buffer.write('**');
+ } else if (attribute.key == Attribute.italic.key) {
+ buffer.write('_');
+ } else if (attribute.key == Attribute.link.key) {
+ buffer.write(!close ? '[' : '](${attribute.value})');
+ } else if (attribute == Attribute.codeBlock) {
+ buffer.write(!close ? '```\n' : '\n```');
+ } else {
+ throw ArgumentError('Cannot handle $attribute');
+ }
+ }
+
+ void _writeBlockTag(
+ StringBuffer buffer,
+ Attribute block, {
+ bool close = false,
+ }) {
+ if (close) {
+ return; // no close tag needed for simple blocks.
+ }
+
+ if (block == Attribute.blockQuote) {
+ buffer.write('> ');
+ } else if (block == Attribute.ul) {
+ buffer.write('* ');
+ } else if (block == Attribute.ol) {
+ buffer.write('1. ');
+ } else if (block.key == Attribute.h1.key && block.value == 1) {
+ buffer.write('# ');
+ } else if (block.key == Attribute.h2.key && block.value == 2) {
+ buffer.write('## ');
+ } else if (block.key == Attribute.h3.key && block.value == 3) {
+ buffer.write('### ');
+ } else {
+ throw ArgumentError('Cannot handle block $block');
+ }
+ }
+
+ void _writeEmbedTag(
+ StringBuffer buffer,
+ BlockEmbed embed, {
+ bool close = false,
+ }) {
+ const kImageType = 'image';
+ const kDividerType = 'divider';
+
+ if (embed.type == kImageType) {
+ if (close) {
+ buffer.write('](${embed.data})');
+ } else {
+ buffer.write('![');
+ }
+ } else if (embed.type == kDividerType && close) {
+ buffer.write('\n---\n\n');
+ }
+ }
+}
diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/document.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/document.dart
new file mode 100644
index 00000000..890b858c
--- /dev/null
+++ b/packages/quill_html_converter/lib/src/packages/delta_markdown/document.dart
@@ -0,0 +1,88 @@
+// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'ast.dart';
+import 'block_parser.dart';
+import 'extension_set.dart';
+import 'inline_parser.dart';
+
+/// Maintains the context needed to parse a Markdown document.
+class Document {
+ Document({
+ Iterable? blockSyntaxes,
+ Iterable? inlineSyntaxes,
+ ExtensionSet? extensionSet,
+ this.linkResolver,
+ this.imageLinkResolver,
+ }) : extensionSet = extensionSet ?? ExtensionSet.commonMark {
+ _blockSyntaxes
+ ..addAll(blockSyntaxes ?? [])
+ ..addAll(this.extensionSet.blockSyntaxes);
+ _inlineSyntaxes
+ ..addAll(inlineSyntaxes ?? [])
+ ..addAll(this.extensionSet.inlineSyntaxes);
+ }
+
+ final Map linkReferences = {};
+ final ExtensionSet extensionSet;
+ final Resolver? linkResolver;
+ final Resolver? imageLinkResolver;
+ final _blockSyntaxes = {};
+ final _inlineSyntaxes = {};
+
+ Iterable get blockSyntaxes => _blockSyntaxes;
+ Iterable get inlineSyntaxes => _inlineSyntaxes;
+
+ /// Parses the given [lines] of Markdown to a series of AST nodes.
+ List parseLines(List lines) {
+ final nodes = BlockParser(lines, this).parseLines();
+ // Make sure to mark the top level nodes as such.
+ for (final n in nodes) {
+ n.isToplevel = true;
+ }
+ _parseInlineContent(nodes);
+ return nodes;
+ }
+
+ /// Parses the given inline Markdown [text] to a series of AST nodes.
+ List? parseInline(String text) => InlineParser(text, this).parse();
+
+ void _parseInlineContent(List nodes) {
+ for (var i = 0; i < nodes.length; i++) {
+ final node = nodes[i];
+ if (node is UnparsedContent) {
+ final inlineNodes = parseInline(node.textContent)!;
+ nodes
+ ..removeAt(i)
+ ..insertAll(i, inlineNodes);
+ i += inlineNodes.length - 1;
+ } else if (node is Element && node.children != null) {
+ _parseInlineContent(node.children!);
+ }
+ }
+ }
+}
+
+/// A [link reference
+/// definition](http://spec.commonmark.org/0.28/#link-reference-definitions).
+class LinkReference {
+ /// Construct a [LinkReference], with all necessary fields.
+ ///
+ /// If the parsed link reference definition does not include a title, use
+ /// `null` for the [title] parameter.
+ LinkReference(this.label, this.destination, this.title);
+
+ /// The [link label](http://spec.commonmark.org/0.28/#link-label).
+ ///
+ /// Temporarily, this class is also being used to represent the link data for
+ /// an inline link (the destination and title), but this should change before
+ /// the package is released.
+ final String label;
+
+ /// The [link destination](http://spec.commonmark.org/0.28/#link-destination).
+ final String destination;
+
+ /// The [link title](http://spec.commonmark.org/0.28/#link-title).
+ final String title;
+}
diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/emojis.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/emojis.dart
new file mode 100644
index 00000000..cdb3b694
--- /dev/null
+++ b/packages/quill_html_converter/lib/src/packages/delta_markdown/emojis.dart
@@ -0,0 +1,1510 @@
+// GENERATED FILE. DO NOT EDIT.
+//
+// This file was generated from emojilib's emoji data file:
+// https://github.com/muan/emojilib/raw/master/emojis.json
+// at 2018-07-02 15:07:49.422933 by the script, tool/update_emojis.dart.
+
+const emojis = {
+ 'grinning': '๐',
+ 'grimacing': '๐ฌ',
+ 'grin': '๐',
+ 'joy': '๐',
+ 'rofl': '๐คฃ',
+ 'smiley': '๐',
+ 'smile': '๐',
+ 'sweat_smile': '๐
',
+ 'laughing': '๐',
+ 'innocent': '๐',
+ 'wink': '๐',
+ 'blush': '๐',
+ 'slightly_smiling_face': '๐',
+ 'upside_down_face': '๐',
+ 'relaxed': 'โบ๏ธ',
+ 'yum': '๐',
+ 'relieved': '๐',
+ 'heart_eyes': '๐',
+ 'kissing_heart': '๐',
+ 'kissing': '๐',
+ 'kissing_smiling_eyes': '๐',
+ 'kissing_closed_eyes': '๐',
+ 'stuck_out_tongue_winking_eye': '๐',
+ 'zany': '๐คช',
+ 'raised_eyebrow': '๐คจ',
+ 'monocle': '๐ง',
+ 'stuck_out_tongue_closed_eyes': '๐',
+ 'stuck_out_tongue': '๐',
+ 'money_mouth_face': '๐ค',
+ 'nerd_face': '๐ค',
+ 'sunglasses': '๐',
+ 'star_struck': '๐คฉ',
+ 'clown_face': '๐คก',
+ 'cowboy_hat_face': '๐ค ',
+ 'hugs': '๐ค',
+ 'smirk': '๐',
+ 'no_mouth': '๐ถ',
+ 'neutral_face': '๐',
+ 'expressionless': '๐',
+ 'unamused': '๐',
+ 'roll_eyes': '๐',
+ 'thinking': '๐ค',
+ 'lying_face': '๐คฅ',
+ 'hand_over_mouth': '๐คญ',
+ 'shushing': '๐คซ',
+ 'symbols_over_mouth': '๐คฌ',
+ 'exploding_head': '๐คฏ',
+ 'flushed': '๐ณ',
+ 'disappointed': '๐',
+ 'worried': '๐',
+ 'angry': '๐ ',
+ 'rage': '๐ก',
+ 'pensive': '๐',
+ 'confused': '๐',
+ 'slightly_frowning_face': '๐',
+ 'frowning_face': 'โน',
+ 'persevere': '๐ฃ',
+ 'confounded': '๐',
+ 'tired_face': '๐ซ',
+ 'weary': '๐ฉ',
+ 'triumph': '๐ค',
+ 'open_mouth': '๐ฎ',
+ 'scream': '๐ฑ',
+ 'fearful': '๐จ',
+ 'cold_sweat': '๐ฐ',
+ 'hushed': '๐ฏ',
+ 'frowning': '๐ฆ',
+ 'anguished': '๐ง',
+ 'cry': '๐ข',
+ 'disappointed_relieved': '๐ฅ',
+ 'drooling_face': '๐คค',
+ 'sleepy': '๐ช',
+ 'sweat': '๐',
+ 'sob': '๐ญ',
+ 'dizzy_face': '๐ต',
+ 'astonished': '๐ฒ',
+ 'zipper_mouth_face': '๐ค',
+ 'nauseated_face': '๐คข',
+ 'sneezing_face': '๐คง',
+ 'vomiting': '๐คฎ',
+ 'mask': '๐ท',
+ 'face_with_thermometer': '๐ค',
+ 'face_with_head_bandage': '๐ค',
+ 'sleeping': '๐ด',
+ 'zzz': '๐ค',
+ 'poop': '๐ฉ',
+ 'smiling_imp': '๐',
+ 'imp': '๐ฟ',
+ 'japanese_ogre': '๐น',
+ 'japanese_goblin': '๐บ',
+ 'skull': '๐',
+ 'ghost': '๐ป',
+ 'alien': '๐ฝ',
+ 'robot': '๐ค',
+ 'smiley_cat': '๐บ',
+ 'smile_cat': '๐ธ',
+ 'joy_cat': '๐น',
+ 'heart_eyes_cat': '๐ป',
+ 'smirk_cat': '๐ผ',
+ 'kissing_cat': '๐ฝ',
+ 'scream_cat': '๐',
+ 'crying_cat_face': '๐ฟ',
+ 'pouting_cat': '๐พ',
+ 'palms_up': '๐คฒ',
+ 'raised_hands': '๐',
+ 'clap': '๐',
+ 'wave': '๐',
+ 'call_me_hand': '๐ค',
+ '+1': '๐',
+ '-1': '๐',
+ 'facepunch': '๐',
+ 'fist': 'โ',
+ 'fist_left': '๐ค',
+ 'fist_right': '๐ค',
+ 'v': 'โ',
+ 'ok_hand': '๐',
+ 'raised_hand': 'โ',
+ 'raised_back_of_hand': '๐ค',
+ 'open_hands': '๐',
+ 'muscle': '๐ช',
+ 'pray': '๐',
+ 'handshake': '๐ค',
+ 'point_up': 'โ',
+ 'point_up_2': '๐',
+ 'point_down': '๐',
+ 'point_left': '๐',
+ 'point_right': '๐',
+ 'fu': '๐',
+ 'raised_hand_with_fingers_splayed': '๐',
+ 'love_you': '๐ค',
+ 'metal': '๐ค',
+ 'crossed_fingers': '๐ค',
+ 'vulcan_salute': '๐',
+ 'writing_hand': 'โ',
+ 'selfie': '๐คณ',
+ 'nail_care': '๐
',
+ 'lips': '๐',
+ 'tongue': '๐
',
+ 'ear': '๐',
+ 'nose': '๐',
+ 'eye': '๐',
+ 'eyes': '๐',
+ 'brain': '๐ง ',
+ 'bust_in_silhouette': '๐ค',
+ 'busts_in_silhouette': '๐ฅ',
+ 'speaking_head': '๐ฃ',
+ 'baby': '๐ถ',
+ 'child': '๐ง',
+ 'boy': '๐ฆ',
+ 'girl': '๐ง',
+ 'adult': '๐ง',
+ 'man': '๐จ',
+ 'woman': '๐ฉ',
+ 'blonde_woman': '๐ฑโโ๏ธ',
+ 'blonde_man': '๐ฑ',
+ 'bearded_person': '๐ง',
+ 'older_adult': '๐ง',
+ 'older_man': '๐ด',
+ 'older_woman': '๐ต',
+ 'man_with_gua_pi_mao': '๐ฒ',
+ 'woman_with_headscarf': '๐ง',
+ 'woman_with_turban': '๐ณโโ๏ธ',
+ 'man_with_turban': '๐ณ',
+ 'policewoman': '๐ฎโโ๏ธ',
+ 'policeman': '๐ฎ',
+ 'construction_worker_woman': '๐ทโโ๏ธ',
+ 'construction_worker_man': '๐ท',
+ 'guardswoman': '๐โโ๏ธ',
+ 'guardsman': '๐',
+ 'female_detective': '๐ต๏ธโโ๏ธ',
+ 'male_detective': '๐ต',
+ 'woman_health_worker': '๐ฉโโ๏ธ',
+ 'man_health_worker': '๐จโโ๏ธ',
+ 'woman_farmer': '๐ฉโ๐พ',
+ 'man_farmer': '๐จโ๐พ',
+ 'woman_cook': '๐ฉโ๐ณ',
+ 'man_cook': '๐จโ๐ณ',
+ 'woman_student': '๐ฉโ๐',
+ 'man_student': '๐จโ๐',
+ 'woman_singer': '๐ฉโ๐ค',
+ 'man_singer': '๐จโ๐ค',
+ 'woman_teacher': '๐ฉโ๐ซ',
+ 'man_teacher': '๐จโ๐ซ',
+ 'woman_factory_worker': '๐ฉโ๐ญ',
+ 'man_factory_worker': '๐จโ๐ญ',
+ 'woman_technologist': '๐ฉโ๐ป',
+ 'man_technologist': '๐จโ๐ป',
+ 'woman_office_worker': '๐ฉโ๐ผ',
+ 'man_office_worker': '๐จโ๐ผ',
+ 'woman_mechanic': '๐ฉโ๐ง',
+ 'man_mechanic': '๐จโ๐ง',
+ 'woman_scientist': '๐ฉโ๐ฌ',
+ 'man_scientist': '๐จโ๐ฌ',
+ 'woman_artist': '๐ฉโ๐จ',
+ 'man_artist': '๐จโ๐จ',
+ 'woman_firefighter': '๐ฉโ๐',
+ 'man_firefighter': '๐จโ๐',
+ 'woman_pilot': '๐ฉโโ๏ธ',
+ 'man_pilot': '๐จโโ๏ธ',
+ 'woman_astronaut': '๐ฉโ๐',
+ 'man_astronaut': '๐จโ๐',
+ 'woman_judge': '๐ฉโโ๏ธ',
+ 'man_judge': '๐จโโ๏ธ',
+ 'mrs_claus': '๐คถ',
+ 'santa': '๐
',
+ 'sorceress': '๐งโโ๏ธ',
+ 'wizard': '๐งโโ๏ธ',
+ 'woman_elf': '๐งโโ๏ธ',
+ 'man_elf': '๐งโโ๏ธ',
+ 'woman_vampire': '๐งโโ๏ธ',
+ 'man_vampire': '๐งโโ๏ธ',
+ 'woman_zombie': '๐งโโ๏ธ',
+ 'man_zombie': '๐งโโ๏ธ',
+ 'woman_genie': '๐งโโ๏ธ',
+ 'man_genie': '๐งโโ๏ธ',
+ 'mermaid': '๐งโโ๏ธ',
+ 'merman': '๐งโโ๏ธ',
+ 'woman_fairy': '๐งโโ๏ธ',
+ 'man_fairy': '๐งโโ๏ธ',
+ 'angel': '๐ผ',
+ 'pregnant_woman': '๐คฐ',
+ 'breastfeeding': '๐คฑ',
+ 'princess': '๐ธ',
+ 'prince': '๐คด',
+ 'bride_with_veil': '๐ฐ',
+ 'man_in_tuxedo': '๐คต',
+ 'running_woman': '๐โโ๏ธ',
+ 'running_man': '๐',
+ 'walking_woman': '๐ถโโ๏ธ',
+ 'walking_man': '๐ถ',
+ 'dancer': '๐',
+ 'man_dancing': '๐บ',
+ 'dancing_women': '๐ฏ',
+ 'dancing_men': '๐ฏโโ๏ธ',
+ 'couple': '๐ซ',
+ 'two_men_holding_hands': '๐ฌ',
+ 'two_women_holding_hands': '๐ญ',
+ 'bowing_woman': '๐โโ๏ธ',
+ 'bowing_man': '๐',
+ 'man_facepalming': '๐คฆ',
+ 'woman_facepalming': '๐คฆโโ๏ธ',
+ 'woman_shrugging': '๐คท',
+ 'man_shrugging': '๐คทโโ๏ธ',
+ 'tipping_hand_woman': '๐',
+ 'tipping_hand_man': '๐โโ๏ธ',
+ 'no_good_woman': '๐
',
+ 'no_good_man': '๐
โโ๏ธ',
+ 'ok_woman': '๐',
+ 'ok_man': '๐โโ๏ธ',
+ 'raising_hand_woman': '๐',
+ 'raising_hand_man': '๐โโ๏ธ',
+ 'pouting_woman': '๐',
+ 'pouting_man': '๐โโ๏ธ',
+ 'frowning_woman': '๐',
+ 'frowning_man': '๐โโ๏ธ',
+ 'haircut_woman': '๐',
+ 'haircut_man': '๐โโ๏ธ',
+ 'massage_woman': '๐',
+ 'massage_man': '๐โโ๏ธ',
+ 'woman_in_steamy_room': '๐งโโ๏ธ',
+ 'man_in_steamy_room': '๐งโโ๏ธ',
+ 'couple_with_heart_woman_man': '๐',
+ 'couple_with_heart_woman_woman': '๐ฉโโค๏ธโ๐ฉ',
+ 'couple_with_heart_man_man': '๐จโโค๏ธโ๐จ',
+ 'couplekiss_man_woman': '๐',
+ 'couplekiss_woman_woman': '๐ฉโโค๏ธโ๐โ๐ฉ',
+ 'couplekiss_man_man': '๐จโโค๏ธโ๐โ๐จ',
+ 'family_man_woman_boy': '๐ช',
+ 'family_man_woman_girl': '๐จโ๐ฉโ๐ง',
+ 'family_man_woman_girl_boy': '๐จโ๐ฉโ๐งโ๐ฆ',
+ 'family_man_woman_boy_boy': '๐จโ๐ฉโ๐ฆโ๐ฆ',
+ 'family_man_woman_girl_girl': '๐จโ๐ฉโ๐งโ๐ง',
+ 'family_woman_woman_boy': '๐ฉโ๐ฉโ๐ฆ',
+ 'family_woman_woman_girl': '๐ฉโ๐ฉโ๐ง',
+ 'family_woman_woman_girl_boy': '๐ฉโ๐ฉโ๐งโ๐ฆ',
+ 'family_woman_woman_boy_boy': '๐ฉโ๐ฉโ๐ฆโ๐ฆ',
+ 'family_woman_woman_girl_girl': '๐ฉโ๐ฉโ๐งโ๐ง',
+ 'family_man_man_boy': '๐จโ๐จโ๐ฆ',
+ 'family_man_man_girl': '๐จโ๐จโ๐ง',
+ 'family_man_man_girl_boy': '๐จโ๐จโ๐งโ๐ฆ',
+ 'family_man_man_boy_boy': '๐จโ๐จโ๐ฆโ๐ฆ',
+ 'family_man_man_girl_girl': '๐จโ๐จโ๐งโ๐ง',
+ 'family_woman_boy': '๐ฉโ๐ฆ',
+ 'family_woman_girl': '๐ฉโ๐ง',
+ 'family_woman_girl_boy': '๐ฉโ๐งโ๐ฆ',
+ 'family_woman_boy_boy': '๐ฉโ๐ฆโ๐ฆ',
+ 'family_woman_girl_girl': '๐ฉโ๐งโ๐ง',
+ 'family_man_boy': '๐จโ๐ฆ',
+ 'family_man_girl': '๐จโ๐ง',
+ 'family_man_girl_boy': '๐จโ๐งโ๐ฆ',
+ 'family_man_boy_boy': '๐จโ๐ฆโ๐ฆ',
+ 'family_man_girl_girl': '๐จโ๐งโ๐ง',
+ 'coat': '๐งฅ',
+ 'womans_clothes': '๐',
+ 'tshirt': '๐',
+ 'jeans': '๐',
+ 'necktie': '๐',
+ 'dress': '๐',
+ 'bikini': '๐',
+ 'kimono': '๐',
+ 'lipstick': '๐',
+ 'kiss': '๐',
+ 'footprints': '๐ฃ',
+ 'high_heel': '๐ ',
+ 'sandal': '๐ก',
+ 'boot': '๐ข',
+ 'mans_shoe': '๐',
+ 'athletic_shoe': '๐',
+ 'socks': '๐งฆ',
+ 'gloves': '๐งค',
+ 'scarf': '๐งฃ',
+ 'womans_hat': '๐',
+ 'tophat': '๐ฉ',
+ 'billed_hat': '๐งข',
+ 'rescue_worker_helmet': 'โ',
+ 'mortar_board': '๐',
+ 'crown': '๐',
+ 'school_satchel': '๐',
+ 'pouch': '๐',
+ 'purse': '๐',
+ 'handbag': '๐',
+ 'briefcase': '๐ผ',
+ 'eyeglasses': '๐',
+ 'dark_sunglasses': '๐ถ',
+ 'ring': '๐',
+ 'closed_umbrella': '๐',
+ 'dog': '๐ถ',
+ 'cat': '๐ฑ',
+ 'mouse': '๐ญ',
+ 'hamster': '๐น',
+ 'rabbit': '๐ฐ',
+ 'fox_face': '๐ฆ',
+ 'bear': '๐ป',
+ 'panda_face': '๐ผ',
+ 'koala': '๐จ',
+ 'tiger': '๐ฏ',
+ 'lion': '๐ฆ',
+ 'cow': '๐ฎ',
+ 'pig': '๐ท',
+ 'pig_nose': '๐ฝ',
+ 'frog': '๐ธ',
+ 'squid': '๐ฆ',
+ 'octopus': '๐',
+ 'shrimp': '๐ฆ',
+ 'monkey_face': '๐ต',
+ 'gorilla': '๐ฆ',
+ 'see_no_evil': '๐',
+ 'hear_no_evil': '๐',
+ 'speak_no_evil': '๐',
+ 'monkey': '๐',
+ 'chicken': '๐',
+ 'penguin': '๐ง',
+ 'bird': '๐ฆ',
+ 'baby_chick': '๐ค',
+ 'hatching_chick': '๐ฃ',
+ 'hatched_chick': '๐ฅ',
+ 'duck': '๐ฆ',
+ 'eagle': '๐ฆ
',
+ 'owl': '๐ฆ',
+ 'bat': '๐ฆ',
+ 'wolf': '๐บ',
+ 'boar': '๐',
+ 'horse': '๐ด',
+ 'unicorn': '๐ฆ',
+ 'honeybee': '๐',
+ 'bug': '๐',
+ 'butterfly': '๐ฆ',
+ 'snail': '๐',
+ 'beetle': '๐',
+ 'ant': '๐',
+ 'grasshopper': '๐ฆ',
+ 'spider': '๐ท',
+ 'scorpion': '๐ฆ',
+ 'crab': '๐ฆ',
+ 'snake': '๐',
+ 'lizard': '๐ฆ',
+ 't-rex': '๐ฆ',
+ 'sauropod': '๐ฆ',
+ 'turtle': '๐ข',
+ 'tropical_fish': '๐ ',
+ 'fish': '๐',
+ 'blowfish': '๐ก',
+ 'dolphin': '๐ฌ',
+ 'shark': '๐ฆ',
+ 'whale': '๐ณ',
+ 'whale2': '๐',
+ 'crocodile': '๐',
+ 'leopard': '๐',
+ 'zebra': '๐ฆ',
+ 'tiger2': '๐
',
+ 'water_buffalo': '๐',
+ 'ox': '๐',
+ 'cow2': '๐',
+ 'deer': '๐ฆ',
+ 'dromedary_camel': '๐ช',
+ 'camel': '๐ซ',
+ 'giraffe': '๐ฆ',
+ 'elephant': '๐',
+ 'rhinoceros': '๐ฆ',
+ 'goat': '๐',
+ 'ram': '๐',
+ 'sheep': '๐',
+ 'racehorse': '๐',
+ 'pig2': '๐',
+ 'rat': '๐',
+ 'mouse2': '๐',
+ 'rooster': '๐',
+ 'turkey': '๐ฆ',
+ 'dove': '๐',
+ 'dog2': '๐',
+ 'poodle': '๐ฉ',
+ 'cat2': '๐',
+ 'rabbit2': '๐',
+ 'chipmunk': '๐ฟ',
+ 'hedgehog': '๐ฆ',
+ 'paw_prints': '๐พ',
+ 'dragon': '๐',
+ 'dragon_face': '๐ฒ',
+ 'cactus': '๐ต',
+ 'christmas_tree': '๐',
+ 'evergreen_tree': '๐ฒ',
+ 'deciduous_tree': '๐ณ',
+ 'palm_tree': '๐ด',
+ 'seedling': '๐ฑ',
+ 'herb': '๐ฟ',
+ 'shamrock': 'โ',
+ 'four_leaf_clover': '๐',
+ 'bamboo': '๐',
+ 'tanabata_tree': '๐',
+ 'leaves': '๐',
+ 'fallen_leaf': '๐',
+ 'maple_leaf': '๐',
+ 'ear_of_rice': '๐พ',
+ 'hibiscus': '๐บ',
+ 'sunflower': '๐ป',
+ 'rose': '๐น',
+ 'wilted_flower': '๐ฅ',
+ 'tulip': '๐ท',
+ 'blossom': '๐ผ',
+ 'cherry_blossom': '๐ธ',
+ 'bouquet': '๐',
+ 'mushroom': '๐',
+ 'chestnut': '๐ฐ',
+ 'jack_o_lantern': '๐',
+ 'shell': '๐',
+ 'spider_web': '๐ธ',
+ 'earth_americas': '๐',
+ 'earth_africa': '๐',
+ 'earth_asia': '๐',
+ 'full_moon': '๐',
+ 'waning_gibbous_moon': '๐',
+ 'last_quarter_moon': '๐',
+ 'waning_crescent_moon': '๐',
+ 'new_moon': '๐',
+ 'waxing_crescent_moon': '๐',
+ 'first_quarter_moon': '๐',
+ 'waxing_gibbous_moon': '๐',
+ 'new_moon_with_face': '๐',
+ 'full_moon_with_face': '๐',
+ 'first_quarter_moon_with_face': '๐',
+ 'last_quarter_moon_with_face': '๐',
+ 'sun_with_face': '๐',
+ 'crescent_moon': '๐',
+ 'star': 'โญ',
+ 'star2': '๐',
+ 'dizzy': '๐ซ',
+ 'sparkles': 'โจ',
+ 'comet': 'โ',
+ 'sunny': 'โ๏ธ',
+ 'sun_behind_small_cloud': '๐ค',
+ 'partly_sunny': 'โ
',
+ 'sun_behind_large_cloud': '๐ฅ',
+ 'sun_behind_rain_cloud': '๐ฆ',
+ 'cloud': 'โ๏ธ',
+ 'cloud_with_rain': '๐ง',
+ 'cloud_with_lightning_and_rain': 'โ',
+ 'cloud_with_lightning': '๐ฉ',
+ 'zap': 'โก',
+ 'fire': '๐ฅ',
+ 'boom': '๐ฅ',
+ 'snowflake': 'โ๏ธ',
+ 'cloud_with_snow': '๐จ',
+ 'snowman': 'โ',
+ 'snowman_with_snow': 'โ',
+ 'wind_face': '๐ฌ',
+ 'dash': '๐จ',
+ 'tornado': '๐ช',
+ 'fog': '๐ซ',
+ 'open_umbrella': 'โ',
+ 'umbrella': 'โ',
+ 'droplet': '๐ง',
+ 'sweat_drops': '๐ฆ',
+ 'ocean': '๐',
+ 'green_apple': '๐',
+ 'apple': '๐',
+ 'pear': '๐',
+ 'tangerine': '๐',
+ 'lemon': '๐',
+ 'banana': '๐',
+ 'watermelon': '๐',
+ 'grapes': '๐',
+ 'strawberry': '๐',
+ 'melon': '๐',
+ 'cherries': '๐',
+ 'peach': '๐',
+ 'pineapple': '๐',
+ 'coconut': '๐ฅฅ',
+ 'kiwi_fruit': '๐ฅ',
+ 'avocado': '๐ฅ',
+ 'broccoli': '๐ฅฆ',
+ 'tomato': '๐
',
+ 'eggplant': '๐',
+ 'cucumber': '๐ฅ',
+ 'carrot': '๐ฅ',
+ 'hot_pepper': '๐ถ',
+ 'potato': '๐ฅ',
+ 'corn': '๐ฝ',
+ 'sweet_potato': '๐ ',
+ 'peanuts': '๐ฅ',
+ 'honey_pot': '๐ฏ',
+ 'croissant': '๐ฅ',
+ 'bread': '๐',
+ 'baguette_bread': '๐ฅ',
+ 'pretzel': '๐ฅจ',
+ 'cheese': '๐ง',
+ 'egg': '๐ฅ',
+ 'bacon': '๐ฅ',
+ 'steak': '๐ฅฉ',
+ 'pancakes': '๐ฅ',
+ 'poultry_leg': '๐',
+ 'meat_on_bone': '๐',
+ 'fried_shrimp': '๐ค',
+ 'fried_egg': '๐ณ',
+ 'hamburger': '๐',
+ 'fries': '๐',
+ 'stuffed_flatbread': '๐ฅ',
+ 'hotdog': '๐ญ',
+ 'pizza': '๐',
+ 'sandwich': '๐ฅช',
+ 'canned_food': '๐ฅซ',
+ 'spaghetti': '๐',
+ 'taco': '๐ฎ',
+ 'burrito': '๐ฏ',
+ 'green_salad': '๐ฅ',
+ 'shallow_pan_of_food': '๐ฅ',
+ 'ramen': '๐',
+ 'stew': '๐ฒ',
+ 'fish_cake': '๐ฅ',
+ 'fortune_cookie': '๐ฅ ',
+ 'sushi': '๐ฃ',
+ 'bento': '๐ฑ',
+ 'curry': '๐',
+ 'rice_ball': '๐',
+ 'rice': '๐',
+ 'rice_cracker': '๐',
+ 'oden': '๐ข',
+ 'dango': '๐ก',
+ 'shaved_ice': '๐ง',
+ 'ice_cream': '๐จ',
+ 'icecream': '๐ฆ',
+ 'pie': '๐ฅง',
+ 'cake': '๐ฐ',
+ 'birthday': '๐',
+ 'custard': '๐ฎ',
+ 'candy': '๐ฌ',
+ 'lollipop': '๐ญ',
+ 'chocolate_bar': '๐ซ',
+ 'popcorn': '๐ฟ',
+ 'dumpling': '๐ฅ',
+ 'doughnut': '๐ฉ',
+ 'cookie': '๐ช',
+ 'milk_glass': '๐ฅ',
+ 'beer': '๐บ',
+ 'beers': '๐ป',
+ 'clinking_glasses': '๐ฅ',
+ 'wine_glass': '๐ท',
+ 'tumbler_glass': '๐ฅ',
+ 'cocktail': '๐ธ',
+ 'tropical_drink': '๐น',
+ 'champagne': '๐พ',
+ 'sake': '๐ถ',
+ 'tea': '๐ต',
+ 'cup_with_straw': '๐ฅค',
+ 'coffee': 'โ',
+ 'baby_bottle': '๐ผ',
+ 'spoon': '๐ฅ',
+ 'fork_and_knife': '๐ด',
+ 'plate_with_cutlery': '๐ฝ',
+ 'bowl_with_spoon': '๐ฅฃ',
+ 'takeout_box': '๐ฅก',
+ 'chopsticks': '๐ฅข',
+ 'soccer': 'โฝ',
+ 'basketball': '๐',
+ 'football': '๐',
+ 'baseball': 'โพ',
+ 'tennis': '๐พ',
+ 'volleyball': '๐',
+ 'rugby_football': '๐',
+ '8ball': '๐ฑ',
+ 'golf': 'โณ',
+ 'golfing_woman': '๐๏ธโโ๏ธ',
+ 'golfing_man': '๐',
+ 'ping_pong': '๐',
+ 'badminton': '๐ธ',
+ 'goal_net': '๐ฅ
',
+ 'ice_hockey': '๐',
+ 'field_hockey': '๐',
+ 'cricket': '๐',
+ 'ski': '๐ฟ',
+ 'skier': 'โท',
+ 'snowboarder': '๐',
+ 'person_fencing': '๐คบ',
+ 'women_wrestling': '๐คผโโ๏ธ',
+ 'men_wrestling': '๐คผโโ๏ธ',
+ 'woman_cartwheeling': '๐คธโโ๏ธ',
+ 'man_cartwheeling': '๐คธโโ๏ธ',
+ 'woman_playing_handball': '๐คพโโ๏ธ',
+ 'man_playing_handball': '๐คพโโ๏ธ',
+ 'ice_skate': 'โธ',
+ 'curling_stone': '๐ฅ',
+ 'sled': '๐ท',
+ 'bow_and_arrow': '๐น',
+ 'fishing_pole_and_fish': '๐ฃ',
+ 'boxing_glove': '๐ฅ',
+ 'martial_arts_uniform': '๐ฅ',
+ 'rowing_woman': '๐ฃโโ๏ธ',
+ 'rowing_man': '๐ฃ',
+ 'climbing_woman': '๐งโโ๏ธ',
+ 'climbing_man': '๐งโโ๏ธ',
+ 'swimming_woman': '๐โโ๏ธ',
+ 'swimming_man': '๐',
+ 'woman_playing_water_polo': '๐คฝโโ๏ธ',
+ 'man_playing_water_polo': '๐คฝโโ๏ธ',
+ 'woman_in_lotus_position': '๐งโโ๏ธ',
+ 'man_in_lotus_position': '๐งโโ๏ธ',
+ 'surfing_woman': '๐โโ๏ธ',
+ 'surfing_man': '๐',
+ 'bath': '๐',
+ 'basketball_woman': 'โน๏ธโโ๏ธ',
+ 'basketball_man': 'โน',
+ 'weight_lifting_woman': '๐๏ธโโ๏ธ',
+ 'weight_lifting_man': '๐',
+ 'biking_woman': '๐ดโโ๏ธ',
+ 'biking_man': '๐ด',
+ 'mountain_biking_woman': '๐ตโโ๏ธ',
+ 'mountain_biking_man': '๐ต',
+ 'horse_racing': '๐',
+ 'business_suit_levitating': '๐ด',
+ 'trophy': '๐',
+ 'running_shirt_with_sash': '๐ฝ',
+ 'medal_sports': '๐
',
+ 'medal_military': '๐',
+ '1st_place_medal': '๐ฅ',
+ '2nd_place_medal': '๐ฅ',
+ '3rd_place_medal': '๐ฅ',
+ 'reminder_ribbon': '๐',
+ 'rosette': '๐ต',
+ 'ticket': '๐ซ',
+ 'tickets': '๐',
+ 'performing_arts': '๐ญ',
+ 'art': '๐จ',
+ 'circus_tent': '๐ช',
+ 'woman_juggling': '๐คนโโ๏ธ',
+ 'man_juggling': '๐คนโโ๏ธ',
+ 'microphone': '๐ค',
+ 'headphones': '๐ง',
+ 'musical_score': '๐ผ',
+ 'musical_keyboard': '๐น',
+ 'drum': '๐ฅ',
+ 'saxophone': '๐ท',
+ 'trumpet': '๐บ',
+ 'guitar': '๐ธ',
+ 'violin': '๐ป',
+ 'clapper': '๐ฌ',
+ 'video_game': '๐ฎ',
+ 'space_invader': '๐พ',
+ 'dart': '๐ฏ',
+ 'game_die': '๐ฒ',
+ 'slot_machine': '๐ฐ',
+ 'bowling': '๐ณ',
+ 'red_car': '๐',
+ 'taxi': '๐',
+ 'blue_car': '๐',
+ 'bus': '๐',
+ 'trolleybus': '๐',
+ 'racing_car': '๐',
+ 'police_car': '๐',
+ 'ambulance': '๐',
+ 'fire_engine': '๐',
+ 'minibus': '๐',
+ 'truck': '๐',
+ 'articulated_lorry': '๐',
+ 'tractor': '๐',
+ 'kick_scooter': '๐ด',
+ 'motorcycle': '๐',
+ 'bike': '๐ฒ',
+ 'motor_scooter': '๐ต',
+ 'rotating_light': '๐จ',
+ 'oncoming_police_car': '๐',
+ 'oncoming_bus': '๐',
+ 'oncoming_automobile': '๐',
+ 'oncoming_taxi': '๐',
+ 'aerial_tramway': '๐ก',
+ 'mountain_cableway': '๐ ',
+ 'suspension_railway': '๐',
+ 'railway_car': '๐',
+ 'train': '๐',
+ 'monorail': '๐',
+ 'bullettrain_side': '๐',
+ 'bullettrain_front': '๐
',
+ 'light_rail': '๐',
+ 'mountain_railway': '๐',
+ 'steam_locomotive': '๐',
+ 'train2': '๐',
+ 'metro': '๐',
+ 'tram': '๐',
+ 'station': '๐',
+ 'flying_saucer': '๐ธ',
+ 'helicopter': '๐',
+ 'small_airplane': '๐ฉ',
+ 'airplane': 'โ๏ธ',
+ 'flight_departure': '๐ซ',
+ 'flight_arrival': '๐ฌ',
+ 'sailboat': 'โต',
+ 'motor_boat': '๐ฅ',
+ 'speedboat': '๐ค',
+ 'ferry': 'โด',
+ 'passenger_ship': '๐ณ',
+ 'rocket': '๐',
+ 'artificial_satellite': '๐ฐ',
+ 'seat': '๐บ',
+ 'canoe': '๐ถ',
+ 'anchor': 'โ',
+ 'construction': '๐ง',
+ 'fuelpump': 'โฝ',
+ 'busstop': '๐',
+ 'vertical_traffic_light': '๐ฆ',
+ 'traffic_light': '๐ฅ',
+ 'checkered_flag': '๐',
+ 'ship': '๐ข',
+ 'ferris_wheel': '๐ก',
+ 'roller_coaster': '๐ข',
+ 'carousel_horse': '๐ ',
+ 'building_construction': '๐',
+ 'foggy': '๐',
+ 'tokyo_tower': '๐ผ',
+ 'factory': '๐ญ',
+ 'fountain': 'โฒ',
+ 'rice_scene': '๐',
+ 'mountain': 'โฐ',
+ 'mountain_snow': '๐',
+ 'mount_fuji': '๐ป',
+ 'volcano': '๐',
+ 'japan': '๐พ',
+ 'camping': '๐',
+ 'tent': 'โบ',
+ 'national_park': '๐',
+ 'motorway': '๐ฃ',
+ 'railway_track': '๐ค',
+ 'sunrise': '๐
',
+ 'sunrise_over_mountains': '๐',
+ 'desert': '๐',
+ 'beach_umbrella': '๐',
+ 'desert_island': '๐',
+ 'city_sunrise': '๐',
+ 'city_sunset': '๐',
+ 'cityscape': '๐',
+ 'night_with_stars': '๐',
+ 'bridge_at_night': '๐',
+ 'milky_way': '๐',
+ 'stars': '๐ ',
+ 'sparkler': '๐',
+ 'fireworks': '๐',
+ 'rainbow': '๐',
+ 'houses': '๐',
+ 'european_castle': '๐ฐ',
+ 'japanese_castle': '๐ฏ',
+ 'stadium': '๐',
+ 'statue_of_liberty': '๐ฝ',
+ 'house': '๐ ',
+ 'house_with_garden': '๐ก',
+ 'derelict_house': '๐',
+ 'office': '๐ข',
+ 'department_store': '๐ฌ',
+ 'post_office': '๐ฃ',
+ 'european_post_office': '๐ค',
+ 'hospital': '๐ฅ',
+ 'bank': '๐ฆ',
+ 'hotel': '๐จ',
+ 'convenience_store': '๐ช',
+ 'school': '๐ซ',
+ 'love_hotel': '๐ฉ',
+ 'wedding': '๐',
+ 'classical_building': '๐',
+ 'church': 'โช',
+ 'mosque': '๐',
+ 'synagogue': '๐',
+ 'kaaba': '๐',
+ 'shinto_shrine': 'โฉ',
+ 'watch': 'โ',
+ 'iphone': '๐ฑ',
+ 'calling': '๐ฒ',
+ 'computer': '๐ป',
+ 'keyboard': 'โจ',
+ 'desktop_computer': '๐ฅ',
+ 'printer': '๐จ',
+ 'computer_mouse': '๐ฑ',
+ 'trackball': '๐ฒ',
+ 'joystick': '๐น',
+ 'clamp': '๐',
+ 'minidisc': '๐ฝ',
+ 'floppy_disk': '๐พ',
+ 'cd': '๐ฟ',
+ 'dvd': '๐',
+ 'vhs': '๐ผ',
+ 'camera': '๐ท',
+ 'camera_flash': '๐ธ',
+ 'video_camera': '๐น',
+ 'movie_camera': '๐ฅ',
+ 'film_projector': '๐ฝ',
+ 'film_strip': '๐',
+ 'telephone_receiver': '๐',
+ 'phone': 'โ๏ธ',
+ 'pager': '๐',
+ 'fax': '๐ ',
+ 'tv': '๐บ',
+ 'radio': '๐ป',
+ 'studio_microphone': '๐',
+ 'level_slider': '๐',
+ 'control_knobs': '๐',
+ 'stopwatch': 'โฑ',
+ 'timer_clock': 'โฒ',
+ 'alarm_clock': 'โฐ',
+ 'mantelpiece_clock': '๐ฐ',
+ 'hourglass_flowing_sand': 'โณ',
+ 'hourglass': 'โ',
+ 'satellite': '๐ก',
+ 'battery': '๐',
+ 'electric_plug': '๐',
+ 'bulb': '๐ก',
+ 'flashlight': '๐ฆ',
+ 'candle': '๐ฏ',
+ 'wastebasket': '๐',
+ 'oil_drum': '๐ข',
+ 'money_with_wings': '๐ธ',
+ 'dollar': '๐ต',
+ 'yen': '๐ด',
+ 'euro': '๐ถ',
+ 'pound': '๐ท',
+ 'moneybag': '๐ฐ',
+ 'credit_card': '๐ณ',
+ 'gem': '๐',
+ 'balance_scale': 'โ',
+ 'wrench': '๐ง',
+ 'hammer': '๐จ',
+ 'hammer_and_pick': 'โ',
+ 'hammer_and_wrench': '๐ ',
+ 'pick': 'โ',
+ 'nut_and_bolt': '๐ฉ',
+ 'gear': 'โ',
+ 'chains': 'โ',
+ 'gun': '๐ซ',
+ 'bomb': '๐ฃ',
+ 'hocho': '๐ช',
+ 'dagger': '๐ก',
+ 'crossed_swords': 'โ',
+ 'shield': '๐ก',
+ 'smoking': '๐ฌ',
+ 'skull_and_crossbones': 'โ ',
+ 'coffin': 'โฐ',
+ 'funeral_urn': 'โฑ',
+ 'amphora': '๐บ',
+ 'crystal_ball': '๐ฎ',
+ 'prayer_beads': '๐ฟ',
+ 'barber': '๐',
+ 'alembic': 'โ',
+ 'telescope': '๐ญ',
+ 'microscope': '๐ฌ',
+ 'hole': '๐ณ',
+ 'pill': '๐',
+ 'syringe': '๐',
+ 'thermometer': '๐ก',
+ 'label': '๐ท',
+ 'bookmark': '๐',
+ 'toilet': '๐ฝ',
+ 'shower': '๐ฟ',
+ 'bathtub': '๐',
+ 'key': '๐',
+ 'old_key': '๐',
+ 'couch_and_lamp': '๐',
+ 'sleeping_bed': '๐',
+ 'bed': '๐',
+ 'door': '๐ช',
+ 'bellhop_bell': '๐',
+ 'framed_picture': '๐ผ',
+ 'world_map': '๐บ',
+ 'parasol_on_ground': 'โฑ',
+ 'moyai': '๐ฟ',
+ 'shopping': '๐',
+ 'shopping_cart': '๐',
+ 'balloon': '๐',
+ 'flags': '๐',
+ 'ribbon': '๐',
+ 'gift': '๐',
+ 'confetti_ball': '๐',
+ 'tada': '๐',
+ 'dolls': '๐',
+ 'wind_chime': '๐',
+ 'crossed_flags': '๐',
+ 'izakaya_lantern': '๐ฎ',
+ 'email': 'โ๏ธ',
+ 'envelope_with_arrow': '๐ฉ',
+ 'incoming_envelope': '๐จ',
+ 'e-mail': '๐ง',
+ 'love_letter': '๐',
+ 'postbox': '๐ฎ',
+ 'mailbox_closed': '๐ช',
+ 'mailbox': '๐ซ',
+ 'mailbox_with_mail': '๐ฌ',
+ 'mailbox_with_no_mail': '๐ญ',
+ 'package': '๐ฆ',
+ 'postal_horn': '๐ฏ',
+ 'inbox_tray': '๐ฅ',
+ 'outbox_tray': '๐ค',
+ 'scroll': '๐',
+ 'page_with_curl': '๐',
+ 'bookmark_tabs': '๐',
+ 'bar_chart': '๐',
+ 'chart_with_upwards_trend': '๐',
+ 'chart_with_downwards_trend': '๐',
+ 'page_facing_up': '๐',
+ 'date': '๐
',
+ 'calendar': '๐',
+ 'spiral_calendar': '๐',
+ 'card_index': '๐',
+ 'card_file_box': '๐',
+ 'ballot_box': '๐ณ',
+ 'file_cabinet': '๐',
+ 'clipboard': '๐',
+ 'spiral_notepad': '๐',
+ 'file_folder': '๐',
+ 'open_file_folder': '๐',
+ 'card_index_dividers': '๐',
+ 'newspaper_roll': '๐',
+ 'newspaper': '๐ฐ',
+ 'notebook': '๐',
+ 'closed_book': '๐',
+ 'green_book': '๐',
+ 'blue_book': '๐',
+ 'orange_book': '๐',
+ 'notebook_with_decorative_cover': '๐',
+ 'ledger': '๐',
+ 'books': '๐',
+ 'open_book': '๐',
+ 'link': '๐',
+ 'paperclip': '๐',
+ 'paperclips': '๐',
+ 'scissors': 'โ๏ธ',
+ 'triangular_ruler': '๐',
+ 'straight_ruler': '๐',
+ 'pushpin': '๐',
+ 'round_pushpin': '๐',
+ 'triangular_flag_on_post': '๐ฉ',
+ 'white_flag': '๐ณ',
+ 'black_flag': '๐ด',
+ 'rainbow_flag': '๐ณ๏ธโ๐',
+ 'closed_lock_with_key': '๐',
+ 'lock': '๐',
+ 'unlock': '๐',
+ 'lock_with_ink_pen': '๐',
+ 'pen': '๐',
+ 'fountain_pen': '๐',
+ 'black_nib': 'โ๏ธ',
+ 'memo': '๐',
+ 'pencil2': 'โ๏ธ',
+ 'crayon': '๐',
+ 'paintbrush': '๐',
+ 'mag': '๐',
+ 'mag_right': '๐',
+ 'heart': 'โค๏ธ',
+ 'orange_heart': '๐งก',
+ 'yellow_heart': '๐',
+ 'green_heart': '๐',
+ 'blue_heart': '๐',
+ 'purple_heart': '๐',
+ 'black_heart': '๐ค',
+ 'broken_heart': '๐',
+ 'heavy_heart_exclamation': 'โฃ',
+ 'two_hearts': '๐',
+ 'revolving_hearts': '๐',
+ 'heartbeat': '๐',
+ 'heartpulse': '๐',
+ 'sparkling_heart': '๐',
+ 'cupid': '๐',
+ 'gift_heart': '๐',
+ 'heart_decoration': '๐',
+ 'peace_symbol': 'โฎ',
+ 'latin_cross': 'โ',
+ 'star_and_crescent': 'โช',
+ 'om': '๐',
+ 'wheel_of_dharma': 'โธ',
+ 'star_of_david': 'โก',
+ 'six_pointed_star': '๐ฏ',
+ 'menorah': '๐',
+ 'yin_yang': 'โฏ',
+ 'orthodox_cross': 'โฆ',
+ 'place_of_worship': '๐',
+ 'ophiuchus': 'โ',
+ 'aries': 'โ',
+ 'taurus': 'โ',
+ 'gemini': 'โ',
+ 'cancer': 'โ',
+ 'leo': 'โ',
+ 'virgo': 'โ',
+ 'libra': 'โ',
+ 'scorpius': 'โ',
+ 'sagittarius': 'โ',
+ 'capricorn': 'โ',
+ 'aquarius': 'โ',
+ 'pisces': 'โ',
+ 'id': '๐',
+ 'atom_symbol': 'โ',
+ 'u7a7a': '๐ณ',
+ 'u5272': '๐น',
+ 'radioactive': 'โข',
+ 'biohazard': 'โฃ',
+ 'mobile_phone_off': '๐ด',
+ 'vibration_mode': '๐ณ',
+ 'u6709': '๐ถ',
+ 'u7121': '๐',
+ 'u7533': '๐ธ',
+ 'u55b6': '๐บ',
+ 'u6708': '๐ท๏ธ',
+ 'eight_pointed_black_star': 'โด๏ธ',
+ 'vs': '๐',
+ 'accept': '๐',
+ 'white_flower': '๐ฎ',
+ 'ideograph_advantage': '๐',
+ 'secret': 'ใ๏ธ',
+ 'congratulations': 'ใ๏ธ',
+ 'u5408': '๐ด',
+ 'u6e80': '๐ต',
+ 'u7981': '๐ฒ',
+ 'a': '๐
ฐ๏ธ',
+ 'b': '๐
ฑ๏ธ',
+ 'ab': '๐',
+ 'cl': '๐',
+ 'o2': '๐
พ๏ธ',
+ 'sos': '๐',
+ 'no_entry': 'โ',
+ 'name_badge': '๐',
+ 'no_entry_sign': '๐ซ',
+ 'x': 'โ',
+ 'o': 'โญ',
+ 'stop_sign': '๐',
+ 'anger': '๐ข',
+ 'hotsprings': 'โจ๏ธ',
+ 'no_pedestrians': '๐ท',
+ 'do_not_litter': '๐ฏ',
+ 'no_bicycles': '๐ณ',
+ 'non-potable_water': '๐ฑ',
+ 'underage': '๐',
+ 'no_mobile_phones': '๐ต',
+ 'exclamation': 'โ',
+ 'grey_exclamation': 'โ',
+ 'question': 'โ',
+ 'grey_question': 'โ',
+ 'bangbang': 'โผ๏ธ',
+ 'interrobang': 'โ๏ธ',
+ '100': '๐ฏ',
+ 'low_brightness': '๐
',
+ 'high_brightness': '๐',
+ 'trident': '๐ฑ',
+ 'fleur_de_lis': 'โ',
+ 'part_alternation_mark': 'ใฝ๏ธ',
+ 'warning': 'โ ๏ธ',
+ 'children_crossing': '๐ธ',
+ 'beginner': '๐ฐ',
+ 'recycle': 'โป๏ธ',
+ 'u6307': '๐ฏ',
+ 'chart': '๐น',
+ 'sparkle': 'โ๏ธ',
+ 'eight_spoked_asterisk': 'โณ๏ธ',
+ 'negative_squared_cross_mark': 'โ',
+ 'white_check_mark': 'โ
',
+ 'diamond_shape_with_a_dot_inside': '๐ ',
+ 'cyclone': '๐',
+ 'loop': 'โฟ',
+ 'globe_with_meridians': '๐',
+ 'm': 'โ๏ธ',
+ 'atm': '๐ง',
+ 'sa': '๐๏ธ',
+ 'passport_control': '๐',
+ 'customs': '๐',
+ 'baggage_claim': '๐',
+ 'left_luggage': '๐
',
+ 'wheelchair': 'โฟ',
+ 'no_smoking': '๐ญ',
+ 'wc': '๐พ',
+ 'parking': '๐
ฟ๏ธ',
+ 'potable_water': '๐ฐ',
+ 'mens': '๐น',
+ 'womens': '๐บ',
+ 'baby_symbol': '๐ผ',
+ 'restroom': '๐ป',
+ 'put_litter_in_its_place': '๐ฎ',
+ 'cinema': '๐ฆ',
+ 'signal_strength': '๐ถ',
+ 'koko': '๐',
+ 'ng': '๐',
+ 'ok': '๐',
+ 'up': '๐',
+ 'cool': '๐',
+ 'new': '๐',
+ 'free': '๐',
+ 'zero': '0๏ธโฃ',
+ 'one': '1๏ธโฃ',
+ 'two': '2๏ธโฃ',
+ 'three': '3๏ธโฃ',
+ 'four': '4๏ธโฃ',
+ 'five': '5๏ธโฃ',
+ 'six': '6๏ธโฃ',
+ 'seven': '7๏ธโฃ',
+ 'eight': '8๏ธโฃ',
+ 'nine': '9๏ธโฃ',
+ 'keycap_ten': '๐',
+ 'asterisk': '*โฃ',
+ '1234': '๐ข',
+ 'eject_button': 'โ๏ธ',
+ 'arrow_forward': 'โถ๏ธ',
+ 'pause_button': 'โธ',
+ 'next_track_button': 'โญ',
+ 'stop_button': 'โน',
+ 'record_button': 'โบ',
+ 'play_or_pause_button': 'โฏ',
+ 'previous_track_button': 'โฎ',
+ 'fast_forward': 'โฉ',
+ 'rewind': 'โช',
+ 'twisted_rightwards_arrows': '๐',
+ 'repeat': '๐',
+ 'repeat_one': '๐',
+ 'arrow_backward': 'โ๏ธ',
+ 'arrow_up_small': '๐ผ',
+ 'arrow_down_small': '๐ฝ',
+ 'arrow_double_up': 'โซ',
+ 'arrow_double_down': 'โฌ',
+ 'arrow_right': 'โก๏ธ',
+ 'arrow_left': 'โฌ
๏ธ',
+ 'arrow_up': 'โฌ๏ธ',
+ 'arrow_down': 'โฌ๏ธ',
+ 'arrow_upper_right': 'โ๏ธ',
+ 'arrow_lower_right': 'โ๏ธ',
+ 'arrow_lower_left': 'โ๏ธ',
+ 'arrow_upper_left': 'โ๏ธ',
+ 'arrow_up_down': 'โ๏ธ',
+ 'left_right_arrow': 'โ๏ธ',
+ 'arrows_counterclockwise': '๐',
+ 'arrow_right_hook': 'โช๏ธ',
+ 'leftwards_arrow_with_hook': 'โฉ๏ธ',
+ 'arrow_heading_up': 'โคด๏ธ',
+ 'arrow_heading_down': 'โคต๏ธ',
+ 'hash': '#๏ธโฃ',
+ 'information_source': 'โน๏ธ',
+ 'abc': '๐ค',
+ 'abcd': '๐ก',
+ 'capital_abcd': '๐ ',
+ 'symbols': '๐ฃ',
+ 'musical_note': '๐ต',
+ 'notes': '๐ถ',
+ 'wavy_dash': 'ใฐ๏ธ',
+ 'curly_loop': 'โฐ',
+ 'heavy_check_mark': 'โ๏ธ',
+ 'arrows_clockwise': '๐',
+ 'heavy_plus_sign': 'โ',
+ 'heavy_minus_sign': 'โ',
+ 'heavy_division_sign': 'โ',
+ 'heavy_multiplication_x': 'โ๏ธ',
+ 'heavy_dollar_sign': '๐ฒ',
+ 'currency_exchange': '๐ฑ',
+ 'copyright': 'ยฉ๏ธ',
+ 'registered': 'ยฎ๏ธ',
+ 'tm': 'โข๏ธ',
+ 'end': '๐',
+ 'back': '๐',
+ 'on': '๐',
+ 'top': '๐',
+ 'soon': '๐',
+ 'ballot_box_with_check': 'โ๏ธ',
+ 'radio_button': '๐',
+ 'white_circle': 'โช',
+ 'black_circle': 'โซ',
+ 'red_circle': '๐ด',
+ 'large_blue_circle': '๐ต',
+ 'small_orange_diamond': '๐ธ',
+ 'small_blue_diamond': '๐น',
+ 'large_orange_diamond': '๐ถ',
+ 'large_blue_diamond': '๐ท',
+ 'small_red_triangle': '๐บ',
+ 'black_small_square': 'โช๏ธ',
+ 'white_small_square': 'โซ๏ธ',
+ 'black_large_square': 'โฌ',
+ 'white_large_square': 'โฌ',
+ 'small_red_triangle_down': '๐ป',
+ 'black_medium_square': 'โผ๏ธ',
+ 'white_medium_square': 'โป๏ธ',
+ 'black_medium_small_square': 'โพ',
+ 'white_medium_small_square': 'โฝ',
+ 'black_square_button': '๐ฒ',
+ 'white_square_button': '๐ณ',
+ 'speaker': '๐',
+ 'sound': '๐',
+ 'loud_sound': '๐',
+ 'mute': '๐',
+ 'mega': '๐ฃ',
+ 'loudspeaker': '๐ข',
+ 'bell': '๐',
+ 'no_bell': '๐',
+ 'black_joker': '๐',
+ 'mahjong': '๐',
+ 'spades': 'โ ๏ธ',
+ 'clubs': 'โฃ๏ธ',
+ 'hearts': 'โฅ๏ธ',
+ 'diamonds': 'โฆ๏ธ',
+ 'flower_playing_cards': '๐ด',
+ 'thought_balloon': '๐ญ',
+ 'right_anger_bubble': '๐ฏ',
+ 'speech_balloon': '๐ฌ',
+ 'left_speech_bubble': '๐จ',
+ 'clock1': '๐',
+ 'clock2': '๐',
+ 'clock3': '๐',
+ 'clock4': '๐',
+ 'clock5': '๐',
+ 'clock6': '๐',
+ 'clock7': '๐',
+ 'clock8': '๐',
+ 'clock9': '๐',
+ 'clock10': '๐',
+ 'clock11': '๐',
+ 'clock12': '๐',
+ 'clock130': '๐',
+ 'clock230': '๐',
+ 'clock330': '๐',
+ 'clock430': '๐',
+ 'clock530': '๐ ',
+ 'clock630': '๐ก',
+ 'clock730': '๐ข',
+ 'clock830': '๐ฃ',
+ 'clock930': '๐ค',
+ 'clock1030': '๐ฅ',
+ 'clock1130': '๐ฆ',
+ 'clock1230': '๐ง',
+ 'afghanistan': '๐ฆ๐ซ',
+ 'aland_islands': '๐ฆ๐ฝ',
+ 'albania': '๐ฆ๐ฑ',
+ 'algeria': '๐ฉ๐ฟ',
+ 'american_samoa': '๐ฆ๐ธ',
+ 'andorra': '๐ฆ๐ฉ',
+ 'angola': '๐ฆ๐ด',
+ 'anguilla': '๐ฆ๐ฎ',
+ 'antarctica': '๐ฆ๐ถ',
+ 'antigua_barbuda': '๐ฆ๐ฌ',
+ 'argentina': '๐ฆ๐ท',
+ 'armenia': '๐ฆ๐ฒ',
+ 'aruba': '๐ฆ๐ผ',
+ 'australia': '๐ฆ๐บ',
+ 'austria': '๐ฆ๐น',
+ 'azerbaijan': '๐ฆ๐ฟ',
+ 'bahamas': '๐ง๐ธ',
+ 'bahrain': '๐ง๐ญ',
+ 'bangladesh': '๐ง๐ฉ',
+ 'barbados': '๐ง๐ง',
+ 'belarus': '๐ง๐พ',
+ 'belgium': '๐ง๐ช',
+ 'belize': '๐ง๐ฟ',
+ 'benin': '๐ง๐ฏ',
+ 'bermuda': '๐ง๐ฒ',
+ 'bhutan': '๐ง๐น',
+ 'bolivia': '๐ง๐ด',
+ 'caribbean_netherlands': '๐ง๐ถ',
+ 'bosnia_herzegovina': '๐ง๐ฆ',
+ 'botswana': '๐ง๐ผ',
+ 'brazil': '๐ง๐ท',
+ 'british_indian_ocean_territory': '๐ฎ๐ด',
+ 'british_virgin_islands': '๐ป๐ฌ',
+ 'brunei': '๐ง๐ณ',
+ 'bulgaria': '๐ง๐ฌ',
+ 'burkina_faso': '๐ง๐ซ',
+ 'burundi': '๐ง๐ฎ',
+ 'cape_verde': '๐จ๐ป',
+ 'cambodia': '๐ฐ๐ญ',
+ 'cameroon': '๐จ๐ฒ',
+ 'canada': '๐จ๐ฆ',
+ 'canary_islands': '๐ฎ๐จ',
+ 'cayman_islands': '๐ฐ๐พ',
+ 'central_african_republic': '๐จ๐ซ',
+ 'chad': '๐น๐ฉ',
+ 'chile': '๐จ๐ฑ',
+ 'cn': '๐จ๐ณ',
+ 'christmas_island': '๐จ๐ฝ',
+ 'cocos_islands': '๐จ๐จ',
+ 'colombia': '๐จ๐ด',
+ 'comoros': '๐ฐ๐ฒ',
+ 'congo_brazzaville': '๐จ๐ฌ',
+ 'congo_kinshasa': '๐จ๐ฉ',
+ 'cook_islands': '๐จ๐ฐ',
+ 'costa_rica': '๐จ๐ท',
+ 'croatia': '๐ญ๐ท',
+ 'cuba': '๐จ๐บ',
+ 'curacao': '๐จ๐ผ',
+ 'cyprus': '๐จ๐พ',
+ 'czech_republic': '๐จ๐ฟ',
+ 'denmark': '๐ฉ๐ฐ',
+ 'djibouti': '๐ฉ๐ฏ',
+ 'dominica': '๐ฉ๐ฒ',
+ 'dominican_republic': '๐ฉ๐ด',
+ 'ecuador': '๐ช๐จ',
+ 'egypt': '๐ช๐ฌ',
+ 'el_salvador': '๐ธ๐ป',
+ 'equatorial_guinea': '๐ฌ๐ถ',
+ 'eritrea': '๐ช๐ท',
+ 'estonia': '๐ช๐ช',
+ 'ethiopia': '๐ช๐น',
+ 'eu': '๐ช๐บ',
+ 'falkland_islands': '๐ซ๐ฐ',
+ 'faroe_islands': '๐ซ๐ด',
+ 'fiji': '๐ซ๐ฏ',
+ 'finland': '๐ซ๐ฎ',
+ 'fr': '๐ซ๐ท',
+ 'french_guiana': '๐ฌ๐ซ',
+ 'french_polynesia': '๐ต๐ซ',
+ 'french_southern_territories': '๐น๐ซ',
+ 'gabon': '๐ฌ๐ฆ',
+ 'gambia': '๐ฌ๐ฒ',
+ 'georgia': '๐ฌ๐ช',
+ 'de': '๐ฉ๐ช',
+ 'ghana': '๐ฌ๐ญ',
+ 'gibraltar': '๐ฌ๐ฎ',
+ 'greece': '๐ฌ๐ท',
+ 'greenland': '๐ฌ๐ฑ',
+ 'grenada': '๐ฌ๐ฉ',
+ 'guadeloupe': '๐ฌ๐ต',
+ 'guam': '๐ฌ๐บ',
+ 'guatemala': '๐ฌ๐น',
+ 'guernsey': '๐ฌ๐ฌ',
+ 'guinea': '๐ฌ๐ณ',
+ 'guinea_bissau': '๐ฌ๐ผ',
+ 'guyana': '๐ฌ๐พ',
+ 'haiti': '๐ญ๐น',
+ 'honduras': '๐ญ๐ณ',
+ 'hong_kong': '๐ญ๐ฐ',
+ 'hungary': '๐ญ๐บ',
+ 'iceland': '๐ฎ๐ธ',
+ 'india': '๐ฎ๐ณ',
+ 'indonesia': '๐ฎ๐ฉ',
+ 'iran': '๐ฎ๐ท',
+ 'iraq': '๐ฎ๐ถ',
+ 'ireland': '๐ฎ๐ช',
+ 'isle_of_man': '๐ฎ๐ฒ',
+ 'israel': '๐ฎ๐ฑ',
+ 'it': '๐ฎ๐น',
+ 'cote_divoire': '๐จ๐ฎ',
+ 'jamaica': '๐ฏ๐ฒ',
+ 'jp': '๐ฏ๐ต',
+ 'jersey': '๐ฏ๐ช',
+ 'jordan': '๐ฏ๐ด',
+ 'kazakhstan': '๐ฐ๐ฟ',
+ 'kenya': '๐ฐ๐ช',
+ 'kiribati': '๐ฐ๐ฎ',
+ 'kosovo': '๐ฝ๐ฐ',
+ 'kuwait': '๐ฐ๐ผ',
+ 'kyrgyzstan': '๐ฐ๐ฌ',
+ 'laos': '๐ฑ๐ฆ',
+ 'latvia': '๐ฑ๐ป',
+ 'lebanon': '๐ฑ๐ง',
+ 'lesotho': '๐ฑ๐ธ',
+ 'liberia': '๐ฑ๐ท',
+ 'libya': '๐ฑ๐พ',
+ 'liechtenstein': '๐ฑ๐ฎ',
+ 'lithuania': '๐ฑ๐น',
+ 'luxembourg': '๐ฑ๐บ',
+ 'macau': '๐ฒ๐ด',
+ 'macedonia': '๐ฒ๐ฐ',
+ 'madagascar': '๐ฒ๐ฌ',
+ 'malawi': '๐ฒ๐ผ',
+ 'malaysia': '๐ฒ๐พ',
+ 'maldives': '๐ฒ๐ป',
+ 'mali': '๐ฒ๐ฑ',
+ 'malta': '๐ฒ๐น',
+ 'marshall_islands': '๐ฒ๐ญ',
+ 'martinique': '๐ฒ๐ถ',
+ 'mauritania': '๐ฒ๐ท',
+ 'mauritius': '๐ฒ๐บ',
+ 'mayotte': '๐พ๐น',
+ 'mexico': '๐ฒ๐ฝ',
+ 'micronesia': '๐ซ๐ฒ',
+ 'moldova': '๐ฒ๐ฉ',
+ 'monaco': '๐ฒ๐จ',
+ 'mongolia': '๐ฒ๐ณ',
+ 'montenegro': '๐ฒ๐ช',
+ 'montserrat': '๐ฒ๐ธ',
+ 'morocco': '๐ฒ๐ฆ',
+ 'mozambique': '๐ฒ๐ฟ',
+ 'myanmar': '๐ฒ๐ฒ',
+ 'namibia': '๐ณ๐ฆ',
+ 'nauru': '๐ณ๐ท',
+ 'nepal': '๐ณ๐ต',
+ 'netherlands': '๐ณ๐ฑ',
+ 'new_caledonia': '๐ณ๐จ',
+ 'new_zealand': '๐ณ๐ฟ',
+ 'nicaragua': '๐ณ๐ฎ',
+ 'niger': '๐ณ๐ช',
+ 'nigeria': '๐ณ๐ฌ',
+ 'niue': '๐ณ๐บ',
+ 'norfolk_island': '๐ณ๐ซ',
+ 'northern_mariana_islands': '๐ฒ๐ต',
+ 'north_korea': '๐ฐ๐ต',
+ 'norway': '๐ณ๐ด',
+ 'oman': '๐ด๐ฒ',
+ 'pakistan': '๐ต๐ฐ',
+ 'palau': '๐ต๐ผ',
+ 'palestinian_territories': '๐ต๐ธ',
+ 'panama': '๐ต๐ฆ',
+ 'papua_new_guinea': '๐ต๐ฌ',
+ 'paraguay': '๐ต๐พ',
+ 'peru': '๐ต๐ช',
+ 'philippines': '๐ต๐ญ',
+ 'pitcairn_islands': '๐ต๐ณ',
+ 'poland': '๐ต๐ฑ',
+ 'portugal': '๐ต๐น',
+ 'puerto_rico': '๐ต๐ท',
+ 'qatar': '๐ถ๐ฆ',
+ 'reunion': '๐ท๐ช',
+ 'romania': '๐ท๐ด',
+ 'ru': '๐ท๐บ',
+ 'rwanda': '๐ท๐ผ',
+ 'st_barthelemy': '๐ง๐ฑ',
+ 'st_helena': '๐ธ๐ญ',
+ 'st_kitts_nevis': '๐ฐ๐ณ',
+ 'st_lucia': '๐ฑ๐จ',
+ 'st_pierre_miquelon': '๐ต๐ฒ',
+ 'st_vincent_grenadines': '๐ป๐จ',
+ 'samoa': '๐ผ๐ธ',
+ 'san_marino': '๐ธ๐ฒ',
+ 'sao_tome_principe': '๐ธ๐น',
+ 'saudi_arabia': '๐ธ๐ฆ',
+ 'senegal': '๐ธ๐ณ',
+ 'serbia': '๐ท๐ธ',
+ 'seychelles': '๐ธ๐จ',
+ 'sierra_leone': '๐ธ๐ฑ',
+ 'singapore': '๐ธ๐ฌ',
+ 'sint_maarten': '๐ธ๐ฝ',
+ 'slovakia': '๐ธ๐ฐ',
+ 'slovenia': '๐ธ๐ฎ',
+ 'solomon_islands': '๐ธ๐ง',
+ 'somalia': '๐ธ๐ด',
+ 'south_africa': '๐ฟ๐ฆ',
+ 'south_georgia_south_sandwich_islands': '๐ฌ๐ธ',
+ 'kr': '๐ฐ๐ท',
+ 'south_sudan': '๐ธ๐ธ',
+ 'es': '๐ช๐ธ',
+ 'sri_lanka': '๐ฑ๐ฐ',
+ 'sudan': '๐ธ๐ฉ',
+ 'suriname': '๐ธ๐ท',
+ 'swaziland': '๐ธ๐ฟ',
+ 'sweden': '๐ธ๐ช',
+ 'switzerland': '๐จ๐ญ',
+ 'syria': '๐ธ๐พ',
+ 'taiwan': '๐น๐ผ',
+ 'tajikistan': '๐น๐ฏ',
+ 'tanzania': '๐น๐ฟ',
+ 'thailand': '๐น๐ญ',
+ 'timor_leste': '๐น๐ฑ',
+ 'togo': '๐น๐ฌ',
+ 'tokelau': '๐น๐ฐ',
+ 'tonga': '๐น๐ด',
+ 'trinidad_tobago': '๐น๐น',
+ 'tunisia': '๐น๐ณ',
+ 'tr': '๐น๐ท',
+ 'turkmenistan': '๐น๐ฒ',
+ 'turks_caicos_islands': '๐น๐จ',
+ 'tuvalu': '๐น๐ป',
+ 'uganda': '๐บ๐ฌ',
+ 'ukraine': '๐บ๐ฆ',
+ 'united_arab_emirates': '๐ฆ๐ช',
+ 'uk': '๐ฌ๐ง',
+ 'england': '๐ด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ',
+ 'scotland': '๐ด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ',
+ 'wales': '๐ด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ',
+ 'us': '๐บ๐ธ',
+ 'us_virgin_islands': '๐ป๐ฎ',
+ 'uruguay': '๐บ๐พ',
+ 'uzbekistan': '๐บ๐ฟ',
+ 'vanuatu': '๐ป๐บ',
+ 'vatican_city': '๐ป๐ฆ',
+ 'venezuela': '๐ป๐ช',
+ 'vietnam': '๐ป๐ณ',
+ 'wallis_futuna': '๐ผ๐ซ',
+ 'western_sahara': '๐ช๐ญ',
+ 'yemen': '๐พ๐ช',
+ 'zambia': '๐ฟ๐ฒ',
+ 'zimbabwe': '๐ฟ๐ผ',
+};
diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/extension_set.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/extension_set.dart
new file mode 100644
index 00000000..eadda7bc
--- /dev/null
+++ b/packages/quill_html_converter/lib/src/packages/delta_markdown/extension_set.dart
@@ -0,0 +1,64 @@
+import 'block_parser.dart';
+import 'inline_parser.dart';
+
+/// ExtensionSets provide a simple grouping mechanism for common Markdown
+/// flavors.
+///
+/// For example, the [gitHubFlavored] set of syntax extensions allows users to
+/// output HTML from their Markdown in a similar fashion to GitHub's parsing.
+class ExtensionSet {
+ ExtensionSet(this.blockSyntaxes, this.inlineSyntaxes);
+
+ /// The [ExtensionSet.none] extension set renders Markdown similar to
+ /// [Markdown.pl].
+ ///
+ /// However, this set does not render _exactly_ the same as Markdown.pl;
+ /// rather it is more-or-less the CommonMark standard of Markdown, without
+ /// fenced code blocks, or inline HTML.
+ ///
+ /// [Markdown.pl]: http://daringfireball.net/projects/markdown/syntax
+ static final ExtensionSet none = ExtensionSet([], []);
+
+ /// The [commonMark] extension set is close to compliance with [CommonMark].
+ ///
+ /// [CommonMark]: http://commonmark.org/
+ static final ExtensionSet commonMark =
+ ExtensionSet([const FencedCodeBlockSyntax()], [InlineHtmlSyntax()]);
+
+ /// The [gitHubWeb] extension set renders Markdown similarly to GitHub.
+ ///
+ /// This is different from the [gitHubFlavored] extension set in that GitHub
+ /// actually renders HTML different from straight [GitHub flavored Markdown].
+ ///
+ /// (The only difference currently is that [gitHubWeb] renders headers with
+ /// linkable IDs.)
+ ///
+ /// [GitHub flavored Markdown]: https://github.github.com/gfm/
+ static final ExtensionSet gitHubWeb = ExtensionSet([
+ const FencedCodeBlockSyntax(),
+ const HeaderWithIdSyntax(),
+ const SetextHeaderWithIdSyntax(),
+ const TableSyntax()
+ ], [
+ InlineHtmlSyntax(),
+ StrikethroughSyntax(),
+ EmojiSyntax(),
+ AutolinkExtensionSyntax(),
+ ]);
+
+ /// The [gitHubFlavored] extension set is close to compliance with the [GitHub
+ /// flavored Markdown spec].
+ ///
+ /// [GitHub flavored Markdown]: https://github.github.com/gfm/
+ static final ExtensionSet gitHubFlavored = ExtensionSet([
+ const FencedCodeBlockSyntax(),
+ const TableSyntax()
+ ], [
+ InlineHtmlSyntax(),
+ StrikethroughSyntax(),
+ AutolinkExtensionSyntax(),
+ ]);
+
+ final List blockSyntaxes;
+ final List inlineSyntaxes;
+}
diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/html_renderer.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/html_renderer.dart
new file mode 100644
index 00000000..d6630e29
--- /dev/null
+++ b/packages/quill_html_converter/lib/src/packages/delta_markdown/html_renderer.dart
@@ -0,0 +1,121 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'ast.dart';
+import 'block_parser.dart';
+import 'document.dart';
+import 'extension_set.dart';
+import 'inline_parser.dart';
+
+/// Converts the given string of Markdown to HTML.
+String markdownToHtml(String markdown,
+ {Iterable? blockSyntaxes,
+ Iterable? inlineSyntaxes,
+ ExtensionSet? extensionSet,
+ Resolver? linkResolver,
+ Resolver? imageLinkResolver,
+ bool inlineOnly = false}) {
+ final document = Document(
+ blockSyntaxes: blockSyntaxes,
+ inlineSyntaxes: inlineSyntaxes,
+ extensionSet: extensionSet,
+ linkResolver: linkResolver,
+ imageLinkResolver: imageLinkResolver);
+
+ if (inlineOnly) {
+ return renderToHtml(document.parseInline(markdown)!);
+ }
+
+ // Replace windows line endings with unix line endings, and split.
+ final lines = markdown.replaceAll('\r\n', '\n').split('\n');
+
+ return '${renderToHtml(document.parseLines(lines))}\n';
+}
+
+/// Renders [nodes] to HTML.
+String renderToHtml(List nodes) => HtmlRenderer().render(nodes);
+
+/// Translates a parsed AST to HTML.
+class HtmlRenderer implements NodeVisitor {
+ HtmlRenderer();
+
+ static final _blockTags = RegExp('blockquote|h1|h2|h3|h4|h5|h6|hr|p|pre');
+
+ late StringBuffer buffer;
+ late Set uniqueIds;
+
+ String render(List nodes) {
+ buffer = StringBuffer();
+ uniqueIds = {};
+
+ for (final node in nodes) {
+ node.accept(this);
+ }
+
+ return buffer.toString();
+ }
+
+ @override
+ void visitText(Text text) {
+ buffer.write(text.text);
+ }
+
+ @override
+ bool visitElementBefore(Element element) {
+ // Hackish. Separate block-level elements with newlines.
+ if (buffer.isNotEmpty && _blockTags.firstMatch(element.tag) != null) {
+ buffer.write('\n');
+ }
+
+ buffer.write('<${element.tag}');
+
+ // Sort the keys so that we generate stable output.
+ final attributeNames = element.attributes.keys.toList()
+ ..sort((a, b) => a.compareTo(b));
+
+ for (final name in attributeNames) {
+ buffer.write(' $name="${element.attributes[name]}"');
+ }
+
+ // attach header anchor ids generated from text
+ if (element.generatedId != null) {
+ buffer.write(' id="${uniquifyId(element.generatedId!)}"');
+ }
+
+ if (element.isEmpty) {
+ // Empty element like .
+ buffer.write(' />');
+
+ if (element.tag == 'br') {
+ buffer.write('\n');
+ }
+
+ return false;
+ } else {
+ buffer.write('>');
+ return true;
+ }
+ }
+
+ @override
+ void visitElementAfter(Element element) {
+ buffer.write('${element.tag}>');
+ }
+
+ /// Uniquifies an id generated from text.
+ String uniquifyId(String id) {
+ if (!uniqueIds.contains(id)) {
+ uniqueIds.add(id);
+ return id;
+ }
+
+ var suffix = 2;
+ var suffixedId = '$id-$suffix';
+ while (uniqueIds.contains(suffixedId)) {
+ suffixedId = '$id-${suffix++}';
+ }
+ uniqueIds.add(suffixedId);
+ return suffixedId;
+ }
+}
diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/inline_parser.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/inline_parser.dart
new file mode 100644
index 00000000..9a7672df
--- /dev/null
+++ b/packages/quill_html_converter/lib/src/packages/delta_markdown/inline_parser.dart
@@ -0,0 +1,1267 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:charcode/charcode.dart';
+
+import 'ast.dart';
+import 'document.dart';
+import 'emojis.dart';
+import 'util.dart';
+
+/// Maintains the internal state needed to parse inline span elements in
+/// Markdown.
+class InlineParser {
+ InlineParser(this.source, this.document) : _stack = [] {
+ // User specified syntaxes are the first syntaxes to be evaluated.
+ syntaxes.addAll(document.inlineSyntaxes);
+
+ final documentHasCustomInlineSyntaxes = document.inlineSyntaxes
+ .any((s) => !document.extensionSet.inlineSyntaxes.contains(s));
+
+ // This first RegExp matches plain text to accelerate parsing. It's written
+ // so that it does not match any prefix of any following syntaxes. Most
+ // Markdown is plain text, so it's faster to match one RegExp per 'word'
+ // rather than fail to match all the following RegExps at each non-syntax
+ // character position.
+ if (documentHasCustomInlineSyntaxes) {
+ // We should be less aggressive in blowing past "words".
+ syntaxes.add(TextSyntax(r'[A-Za-z0-9]+(?=\s)'));
+ } else {
+ syntaxes.add(TextSyntax(r'[ \tA-Za-z0-9]*[A-Za-z0-9](?=\s)'));
+ }
+
+ syntaxes
+ ..addAll(_defaultSyntaxes)
+ // Custom link resolvers go after the generic text syntax.
+ ..insertAll(1, [
+ LinkSyntax(linkResolver: document.linkResolver),
+ ImageSyntax(linkResolver: document.imageLinkResolver)
+ ]);
+ }
+
+ static final List _defaultSyntaxes =
+ List.unmodifiable([
+ EmailAutolinkSyntax(),
+ AutolinkSyntax(),
+ LineBreakSyntax(),
+ LinkSyntax(),
+ ImageSyntax(),
+ // Allow any punctuation to be escaped.
+ EscapeSyntax(),
+ // "*" surrounded by spaces is left alone.
+ TextSyntax(r' \* '),
+ // "_" surrounded by spaces is left alone.
+ TextSyntax(r' _ '),
+ // Parse "**strong**" and "*emphasis*" tags.
+ TagSyntax(r'\*+', requiresDelimiterRun: true),
+ // Parse "__strong__" and "_emphasis_" tags.
+ TagSyntax(r'_+', requiresDelimiterRun: true),
+ CodeSyntax(),
+ // We will add the LinkSyntax once we know about the specific link resolver.
+ ]);
+
+ /// The string of Markdown being parsed.
+ final String source;
+
+ /// The Markdown document this parser is parsing.
+ final Document document;
+
+ final List syntaxes = [];
+
+ /// The current read position.
+ int pos = 0;
+
+ /// Starting position of the last unconsumed text.
+ int start = 0;
+
+ final List _stack;
+
+ List? parse() {
+ // Make a fake top tag to hold the results.
+ _stack.add(TagState(0, 0, null, null));
+
+ while (!isDone) {
+ // See if any of the current tags on the stack match. This takes
+ // priority over other possible matches.
+ if (_stack.reversed
+ .any((state) => state.syntax != null && state.tryMatch(this))) {
+ continue;
+ }
+
+ // See if the current text matches any defined markdown syntax.
+ if (syntaxes.any((syntax) => syntax.tryMatch(this))) {
+ continue;
+ }
+
+ // If we got here, it's just text.
+ advanceBy(1);
+ }
+
+ // Unwind any unmatched tags and get the results.
+ return _stack[0].close(this, null);
+ }
+
+ int charAt(int index) => source.codeUnitAt(index);
+
+ void writeText() {
+ writeTextRange(start, pos);
+ start = pos;
+ }
+
+ void writeTextRange(int start, int end) {
+ if (end <= start) {
+ return;
+ }
+
+ final text = source.substring(start, end);
+ final nodes = _stack.last.children;
+
+ // If the previous node is text too, just append.
+ if (nodes.isNotEmpty && nodes.last is Text) {
+ final textNode = nodes.last as Text;
+ nodes[nodes.length - 1] = Text('${textNode.text}$text');
+ } else {
+ nodes.add(Text(text));
+ }
+ }
+
+ /// Add [node] to the last [TagState] on the stack.
+ void addNode(Node node) {
+ _stack.last.children.add(node);
+ }
+
+ /// Push [state] onto the stack of [TagState]s.
+ void openTag(TagState state) => _stack.add(state);
+
+ bool get isDone => pos == source.length;
+
+ void advanceBy(int length) {
+ pos += length;
+ }
+
+ void consume(int length) {
+ pos += length;
+ start = pos;
+ }
+}
+
+/// Represents one kind of Markdown tag that can be parsed.
+abstract class InlineSyntax {
+ InlineSyntax(String pattern) : pattern = RegExp(pattern, multiLine: true);
+
+ final RegExp pattern;
+
+ /// Tries to match at the parser's current position.
+ ///
+ /// The parser's position can be overriden with [startMatchPos].
+ /// Returns whether or not the pattern successfully matched.
+ bool tryMatch(InlineParser parser, [int? startMatchPos]) {
+ startMatchPos ??= parser.pos;
+
+ final startMatch = pattern.matchAsPrefix(parser.source, startMatchPos);
+ if (startMatch == null) {
+ return false;
+ }
+
+ // Write any existing plain text up to this point.
+ parser.writeText();
+
+ if (onMatch(parser, startMatch)) {
+ parser.consume(startMatch[0]!.length);
+ }
+ return true;
+ }
+
+ /// Processes [match], adding nodes to [parser] and possibly advancing
+ /// [parser].
+ ///
+ /// Returns whether the caller should advance [parser] by `match[0].length`.
+ bool onMatch(InlineParser parser, Match match);
+}
+
+/// Represents a hard line break.
+class LineBreakSyntax extends InlineSyntax {
+ LineBreakSyntax() : super(r'(?:\\| +)\n');
+
+ /// Create a void element.
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ parser.addNode(Element.empty('br'));
+ return true;
+ }
+}
+
+/// Matches stuff that should just be passed through as straight text.
+class TextSyntax extends InlineSyntax {
+ TextSyntax(super.pattern, {String? sub}) : substitute = sub;
+
+ final String? substitute;
+
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ if (substitute == null) {
+ // Just use the original matched text.
+ parser.advanceBy(match[0]!.length);
+ return false;
+ }
+
+ // Insert the substitution.
+ parser.addNode(Text(substitute!));
+ return true;
+ }
+}
+
+/// Escape punctuation preceded by a backslash.
+class EscapeSyntax extends InlineSyntax {
+ EscapeSyntax() : super(r'''\\[!"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~]''');
+
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ // Insert the substitution.
+ parser.addNode(Text(match[0]![1]));
+ return true;
+ }
+}
+
+/// Leave inline HTML tags alone, from
+/// [CommonMark 0.28](http://spec.commonmark.org/0.28/#raw-html).
+///
+/// This is not actually a good definition (nor CommonMark's) of an HTML tag,
+/// but it is fast. It will leave text like `]*)?>');
+}
+
+/// Matches autolinks like ``.
+///
+/// See .
+class EmailAutolinkSyntax extends InlineSyntax {
+ EmailAutolinkSyntax() : super('<($_email)>');
+
+ static const _email =
+ r'''[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}'''
+ r'''[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*''';
+
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ final url = match[1]!;
+ final anchor = Element.text('a', escapeHtml(url));
+ anchor.attributes['href'] = Uri.encodeFull('mailto:$url');
+ parser.addNode(anchor);
+
+ return true;
+ }
+}
+
+/// Matches autolinks like ``.
+class AutolinkSyntax extends InlineSyntax {
+ AutolinkSyntax() : super(r'<(([a-zA-Z][a-zA-Z\-\+\.]+):(?://)?[^\s>]*)>');
+
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ final url = match[1]!;
+ final anchor = Element.text('a', escapeHtml(url));
+ anchor.attributes['href'] = Uri.encodeFull(url);
+ parser.addNode(anchor);
+
+ return true;
+ }
+}
+
+/// Matches autolinks like `http://foo.com`.
+class AutolinkExtensionSyntax extends InlineSyntax {
+ AutolinkExtensionSyntax() : super('$start(($scheme)($domain)($path))');
+
+ /// Broken up parts of the autolink regex for reusability and readability
+
+ // Autolinks can only come at the beginning of a line, after whitespace, or
+ // any of the delimiting characters *, _, ~, and (.
+ static const start = r'(?:^|[\s*_~(>])';
+ // An extended url autolink will be recognized when one of the schemes
+ // http://, https://, or ftp://, followed by a valid domain
+ static const scheme = r'(?:(?:https?|ftp):\/\/|www\.)';
+ // A valid domain consists of alphanumeric characters, underscores (_),
+ // hyphens (-) and periods (.). There must be at least one period, and no
+ // underscores may be present in the last two segments of the domain.
+ static const domainPart = r'\w\-';
+ static const domain = '[$domainPart][$domainPart.]+';
+ // A valid domain consists of alphanumeric characters, underscores (_),
+ // hyphens (-) and periods (.).
+ static const path = r'[^\s<]*';
+ // Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will not
+ // be considered part of the autolink
+ static const truncatingPunctuationPositive = r'[?!.,:*_~]';
+
+ static final regExpTrailingPunc =
+ RegExp('$truncatingPunctuationPositive*' r'$');
+ static final regExpEndsWithColon = RegExp(r'\&[a-zA-Z0-9]+;$');
+ static final regExpWhiteSpace = RegExp(r'\s');
+
+ @override
+ bool tryMatch(InlineParser parser, [int? startMatchPos]) {
+ return super.tryMatch(parser, parser.pos > 0 ? parser.pos - 1 : 0);
+ }
+
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ var url = match[1]!;
+ var href = url;
+ var matchLength = url.length;
+
+ if (url[0] == '>' || url.startsWith(regExpWhiteSpace)) {
+ url = url.substring(1, url.length - 1);
+ href = href.substring(1, href.length - 1);
+ parser.pos++;
+ matchLength--;
+ }
+
+ // Prevent accidental standard autolink matches
+ if (url.endsWith('>') && parser.source[parser.pos - 1] == '<') {
+ return false;
+ }
+
+ // When an autolink ends in ), we scan the entire autolink for the total
+ // number of parentheses. If there is a greater number of closing
+ // parentheses than opening ones, we donโt consider the last character
+ // part of the autolink, in order to facilitate including an autolink
+ // inside a parenthesis:
+ // https://github.github.com/gfm/#example-600
+ if (url.endsWith(')')) {
+ final opening = _countChars(url, '(');
+ final closing = _countChars(url, ')');
+
+ if (closing > opening) {
+ url = url.substring(0, url.length - 1);
+ href = href.substring(0, href.length - 1);
+ matchLength--;
+ }
+ }
+
+ // Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will
+ // not be considered part of the autolink, though they may be included
+ // in the interior of the link:
+ // https://github.github.com/gfm/#example-599
+ final trailingPunc = regExpTrailingPunc.firstMatch(url);
+ if (trailingPunc != null) {
+ url = url.substring(0, url.length - trailingPunc[0]!.length);
+ href = href.substring(0, href.length - trailingPunc[0]!.length);
+ matchLength -= trailingPunc[0]!.length;
+ }
+
+ // If an autolink ends in a semicolon (;), we check to see if it appears
+ // to resemble an
+ // [entity reference](https://github.github.com/gfm/#entity-references);
+ // if the preceding text is & followed by one or more alphanumeric
+ // characters. If so, it is excluded from the autolink:
+ // https://github.github.com/gfm/#example-602
+ if (url.endsWith(';')) {
+ final entityRef = regExpEndsWithColon.firstMatch(url);
+ if (entityRef != null) {
+ // Strip out HTML entity reference
+ url = url.substring(0, url.length - entityRef[0]!.length);
+ href = href.substring(0, href.length - entityRef[0]!.length);
+ matchLength -= entityRef[0]!.length;
+ }
+ }
+
+ // The scheme http will be inserted automatically
+ if (!href.startsWith('http://') &&
+ !href.startsWith('https://') &&
+ !href.startsWith('ftp://')) {
+ href = 'http://$href';
+ }
+
+ final anchor = Element.text('a', escapeHtml(url));
+ anchor.attributes['href'] = Uri.encodeFull(href);
+ parser
+ ..addNode(anchor)
+ ..consume(matchLength);
+ return false;
+ }
+
+ int _countChars(String input, String char) {
+ var count = 0;
+
+ for (var i = 0; i < input.length; i++) {
+ if (input[i] == char) {
+ count++;
+ }
+ }
+
+ return count;
+ }
+}
+
+class _DelimiterRun {
+ _DelimiterRun._(
+ {this.char,
+ this.length,
+ this.isLeftFlanking,
+ this.isRightFlanking,
+ this.isPrecededByPunctuation,
+ this.isFollowedByPunctuation});
+
+ static const String punctuation = r'''!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~''';
+ // TODO(srawlins): Unicode whitespace
+ static const String whitespace = ' \t\r\n';
+
+ final int? char;
+ final int? length;
+ final bool? isLeftFlanking;
+ final bool? isRightFlanking;
+ final bool? isPrecededByPunctuation;
+ final bool? isFollowedByPunctuation;
+
+ // ignore: prefer_constructors_over_static_methods
+ static _DelimiterRun? tryParse(
+ InlineParser parser, int runStart, int runEnd) {
+ bool leftFlanking,
+ rightFlanking,
+ precededByPunctuation,
+ followedByPunctuation;
+ String preceding, following;
+ if (runStart == 0) {
+ rightFlanking = false;
+ preceding = '\n';
+ } else {
+ preceding = parser.source.substring(runStart - 1, runStart);
+ }
+ precededByPunctuation = punctuation.contains(preceding);
+
+ if (runEnd == parser.source.length - 1) {
+ leftFlanking = false;
+ following = '\n';
+ } else {
+ following = parser.source.substring(runEnd + 1, runEnd + 2);
+ }
+ followedByPunctuation = punctuation.contains(following);
+
+ // http://spec.commonmark.org/0.28/#left-flanking-delimiter-run
+ if (whitespace.contains(following)) {
+ leftFlanking = false;
+ } else {
+ leftFlanking = !followedByPunctuation ||
+ whitespace.contains(preceding) ||
+ precededByPunctuation;
+ }
+
+ // http://spec.commonmark.org/0.28/#right-flanking-delimiter-run
+ if (whitespace.contains(preceding)) {
+ rightFlanking = false;
+ } else {
+ rightFlanking = !precededByPunctuation ||
+ whitespace.contains(following) ||
+ followedByPunctuation;
+ }
+
+ if (!leftFlanking && !rightFlanking) {
+ // Could not parse a delimiter run.
+ return null;
+ }
+
+ return _DelimiterRun._(
+ char: parser.charAt(runStart),
+ length: runEnd - runStart + 1,
+ isLeftFlanking: leftFlanking,
+ isRightFlanking: rightFlanking,
+ isPrecededByPunctuation: precededByPunctuation,
+ isFollowedByPunctuation: followedByPunctuation);
+ }
+
+ @override
+ String toString() =>
+ '';
+
+ // Whether a delimiter in this run can open emphasis or strong emphasis.
+ bool get canOpen =>
+ isLeftFlanking! &&
+ (char == $asterisk || !isRightFlanking! || isPrecededByPunctuation!);
+
+ // Whether a delimiter in this run can close emphasis or strong emphasis.
+ bool get canClose =>
+ isRightFlanking! &&
+ (char == $asterisk || !isLeftFlanking! || isFollowedByPunctuation!);
+}
+
+/// Matches syntax that has a pair of tags and becomes an element, like `*` for
+/// ``. Allows nested tags.
+class TagSyntax extends InlineSyntax {
+ TagSyntax(super.pattern, {String? end, this.requiresDelimiterRun = false})
+ : endPattern = RegExp((end != null) ? end : pattern, multiLine: true);
+
+ final RegExp endPattern;
+
+ /// Whether this is parsed according to the same nesting rules as [emphasis
+ /// delimiters][].
+ ///
+ /// [emphasis delimiters]: http://spec.commonmark.org/0.28/#can-open-emphasis
+ final bool requiresDelimiterRun;
+
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ final runLength = match.group(0)!.length;
+ final matchStart = parser.pos;
+ final matchEnd = parser.pos + runLength - 1;
+ if (!requiresDelimiterRun) {
+ parser.openTag(TagState(parser.pos, matchEnd + 1, this, null));
+ return true;
+ }
+
+ final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd);
+ if (delimiterRun != null && delimiterRun.canOpen) {
+ parser.openTag(TagState(parser.pos, matchEnd + 1, this, delimiterRun));
+ return true;
+ } else {
+ parser.advanceBy(runLength);
+ return false;
+ }
+ }
+
+ bool onMatchEnd(InlineParser parser, Match match, TagState state) {
+ final runLength = match.group(0)!.length;
+ final matchStart = parser.pos;
+ final matchEnd = parser.pos + runLength - 1;
+ final openingRunLength = state.endPos - state.startPos;
+ final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd);
+
+ if (openingRunLength == 1 && runLength == 1) {
+ parser.addNode(Element('em', state.children));
+ } else if (openingRunLength == 1 && runLength > 1) {
+ parser
+ ..addNode(Element('em', state.children))
+ ..pos = parser.pos - (runLength - 1)
+ ..start = parser.pos;
+ } else if (openingRunLength > 1 && runLength == 1) {
+ parser
+ ..openTag(
+ TagState(state.startPos, state.endPos - 1, this, delimiterRun))
+ ..addNode(Element('em', state.children));
+ } else if (openingRunLength == 2 && runLength == 2) {
+ parser.addNode(Element('strong', state.children));
+ } else if (openingRunLength == 2 && runLength > 2) {
+ parser
+ ..addNode(Element('strong', state.children))
+ ..pos = parser.pos - (runLength - 2)
+ ..start = parser.pos;
+ } else if (openingRunLength > 2 && runLength == 2) {
+ parser
+ ..openTag(
+ TagState(state.startPos, state.endPos - 2, this, delimiterRun))
+ ..addNode(Element('strong', state.children));
+ } else if (openingRunLength > 2 && runLength > 2) {
+ parser
+ ..openTag(
+ TagState(state.startPos, state.endPos - 2, this, delimiterRun))
+ ..addNode(Element('strong', state.children))
+ ..pos = parser.pos - (runLength - 2)
+ ..start = parser.pos;
+ }
+
+ return true;
+ }
+}
+
+/// Matches strikethrough syntax according to the GFM spec.
+class StrikethroughSyntax extends TagSyntax {
+ StrikethroughSyntax() : super('~+', requiresDelimiterRun: true);
+
+ @override
+ bool onMatchEnd(InlineParser parser, Match match, TagState state) {
+ final runLength = match.group(0)!.length;
+ final matchStart = parser.pos;
+ final matchEnd = parser.pos + runLength - 1;
+ final delimiterRun = _DelimiterRun.tryParse(parser, matchStart, matchEnd)!;
+ if (!delimiterRun.isRightFlanking!) {
+ return false;
+ }
+
+ parser.addNode(Element('del', state.children));
+ return true;
+ }
+}
+
+/// Matches links like `[blah][label]` and `[blah](url)`.
+class LinkSyntax extends TagSyntax {
+ LinkSyntax({Resolver? linkResolver, String pattern = r'\['})
+ : linkResolver = (linkResolver ?? (_, [__]) => null),
+ super(pattern, end: r'\]');
+
+ static final _entirelyWhitespacePattern = RegExp(r'^\s*$');
+
+ final Resolver linkResolver;
+
+ // The pending [TagState]s, all together, are "active" or "inactive" based on
+ // whether a link element has just been parsed.
+ //
+ // Links cannot be nested, so we must "deactivate" any pending ones. For
+ // example, take the following text:
+ //
+ // Text [link and [more](links)](links).
+ //
+ // Once we have parsed `Text [`, there is one (pending) link in the state
+ // stack. It is, by default, active. Once we parse the next possible link,
+ // `[more](links)`, as a real link, we must deactive the pending links (just
+ // the one, in this case).
+ var _pendingStatesAreActive = true;
+
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ final matched = super.onMatch(parser, match);
+ if (!matched) {
+ return false;
+ }
+
+ _pendingStatesAreActive = true;
+
+ return true;
+ }
+
+ @override
+ bool onMatchEnd(InlineParser parser, Match match, TagState state) {
+ if (!_pendingStatesAreActive) {
+ return false;
+ }
+
+ final text = parser.source.substring(state.endPos, parser.pos);
+ // The current character is the `]` that closed the link text. Examine the
+ // next character, to determine what type of link we might have (a '('
+ // means a possible inline link; otherwise a possible reference link).
+ if (parser.pos + 1 >= parser.source.length) {
+ // In this case, the Markdown document may have ended with a shortcut
+ // reference link.
+
+ return _tryAddReferenceLink(parser, state, text);
+ }
+ // Peek at the next character; don't advance, so as to avoid later stepping
+ // backward.
+ final char = parser.charAt(parser.pos + 1);
+
+ if (char == $lparen) {
+ // Maybe an inline link, like `[text](destination)`.
+ parser.advanceBy(1);
+ final leftParenIndex = parser.pos;
+ final inlineLink = _parseInlineLink(parser);
+ if (inlineLink != null) {
+ return _tryAddInlineLink(parser, state, inlineLink);
+ }
+
+ // Reset the parser position.
+ parser
+ ..pos = leftParenIndex
+
+ // At this point, we've matched `[...](`, but that `(` did not pan out
+ // to be an inline link. We must now check if `[...]` is simply a
+ // shortcut reference link.
+ ..advanceBy(-1);
+ return _tryAddReferenceLink(parser, state, text);
+ }
+
+ if (char == $lbracket) {
+ parser.advanceBy(1);
+ // At this point, we've matched `[...][`. Maybe a *full* reference link,
+ // like `[foo][bar]` or a *collapsed* reference link, like `[foo][]`.
+ if (parser.pos + 1 < parser.source.length &&
+ parser.charAt(parser.pos + 1) == $rbracket) {
+ // That opening `[` is not actually part of the link. Maybe a
+ // *shortcut* reference link (followed by a `[`).
+ parser.advanceBy(1);
+ return _tryAddReferenceLink(parser, state, text);
+ }
+ final label = _parseReferenceLinkLabel(parser);
+ if (label != null) {
+ return _tryAddReferenceLink(parser, state, label);
+ }
+ return false;
+ }
+
+ // The link text (inside `[...]`) was not followed with a opening `(` nor
+ // an opening `[`. Perhaps just a simple shortcut reference link (`[...]`).
+
+ return _tryAddReferenceLink(parser, state, text);
+ }
+
+ /// Resolve a possible reference link.
+ ///
+ /// Uses [linkReferences], [linkResolver], and [_createNode] to try to
+ /// resolve [label] and [state] into a [Node]. If [label] is defined in
+ /// [linkReferences] or can be resolved by [linkResolver], returns a [Node]
+ /// that links to the resolved URL.
+ ///
+ /// Otherwise, returns `null`.
+ ///
+ /// [label] does not need to be normalized.
+ Node? _resolveReferenceLink(
+ String label, TagState state, Map linkReferences) {
+ final normalizedLabel = label.toLowerCase();
+ final linkReference = linkReferences[normalizedLabel];
+ if (linkReference != null) {
+ return _createNode(state, linkReference.destination, linkReference.title);
+ } else {
+ // This link has no reference definition. But we allow users of the
+ // library to specify a custom resolver function ([linkResolver]) that
+ // may choose to handle this. Otherwise, it's just treated as plain
+ // text.
+
+ // Normally, label text does not get parsed as inline Markdown. However,
+ // for the benefit of the link resolver, we need to at least escape
+ // brackets, so that, e.g. a link resolver can receive `[\[\]]` as `[]`.
+ return linkResolver(label
+ .replaceAll(r'\\', r'\')
+ .replaceAll(r'\[', '[')
+ .replaceAll(r'\]', ']'));
+ }
+ }
+
+ /// Create the node represented by a Markdown link.
+ Node _createNode(TagState state, String destination, String? title) {
+ final element = Element('a', state.children);
+ element.attributes['href'] = escapeAttribute(destination);
+ if (title != null && title.isNotEmpty) {
+ element.attributes['title'] = escapeAttribute(title);
+ }
+ return element;
+ }
+
+ // Add a reference link node to [parser]'s AST.
+ //
+ // Returns whether the link was added successfully.
+ bool _tryAddReferenceLink(InlineParser parser, TagState state, String label) {
+ final element =
+ _resolveReferenceLink(label, state, parser.document.linkReferences);
+ if (element == null) {
+ return false;
+ }
+ parser
+ ..addNode(element)
+ ..start = parser.pos;
+ _pendingStatesAreActive = false;
+ return true;
+ }
+
+ // Add an inline link node to [parser]'s AST.
+ //
+ // Returns whether the link was added successfully.
+ bool _tryAddInlineLink(InlineParser parser, TagState state, InlineLink link) {
+ final element = _createNode(state, link.destination, link.title);
+ parser
+ ..addNode(element)
+ ..start = parser.pos;
+ _pendingStatesAreActive = false;
+ return true;
+ }
+
+ /// Parse a reference link label at the current position.
+ ///
+ /// Specifically, [parser.pos] is expected to be pointing at the `[` which
+ /// opens the link label.
+ ///
+ /// Returns the label if it could be parsed, or `null` if not.
+ String? _parseReferenceLinkLabel(InlineParser parser) {
+ // Walk past the opening `[`.
+ parser.advanceBy(1);
+ if (parser.isDone) {
+ return null;
+ }
+
+ final buffer = StringBuffer();
+ while (true) {
+ final char = parser.charAt(parser.pos);
+ if (char == $backslash) {
+ parser.advanceBy(1);
+ final next = parser.charAt(parser.pos);
+ if (next != $backslash && next != $rbracket) {
+ buffer.writeCharCode(char);
+ }
+ buffer.writeCharCode(next);
+ } else if (char == $rbracket) {
+ break;
+ } else {
+ buffer.writeCharCode(char);
+ }
+ parser.advanceBy(1);
+ if (parser.isDone) {
+ return null;
+ }
+ // TODO(srawlins): only check 999 characters, for performance reasons?
+ }
+
+ final label = buffer.toString();
+
+ // A link label must contain at least one non-whitespace character.
+ if (_entirelyWhitespacePattern.hasMatch(label)) {
+ return null;
+ }
+
+ return label;
+ }
+
+ /// Parse an inline [InlineLink] at the current position.
+ ///
+ /// At this point, we have parsed a link's (or image's) opening `[`, and then
+ /// a matching closing `]`, and [parser.pos] is pointing at an opening `(`.
+ /// This method will then attempt to parse a link destination wrapped in `<>`,
+ /// such as `()`, or a bare link destination, such as
+ /// `(http://url)`, or a link destination with a title, such as
+ /// `(http://url "title")`.
+ ///
+ /// Returns the [InlineLink] if one was parsed, or `null` if not.
+ InlineLink? _parseInlineLink(InlineParser parser) {
+ // Start walking to the character just after the opening `(`.
+ parser.advanceBy(1);
+
+ _moveThroughWhitespace(parser);
+ if (parser.isDone) {
+ return null; // EOF. Not a link.
+ }
+
+ if (parser.charAt(parser.pos) == $lt) {
+ // Maybe a `<...>`-enclosed link destination.
+ return _parseInlineBracketedLink(parser);
+ } else {
+ return _parseInlineBareDestinationLink(parser);
+ }
+ }
+
+ /// Parse an inline link with a bracketed destination (a destination wrapped
+ /// in `<...>`). The current position of the parser must be the first
+ /// character of the destination.
+ InlineLink? _parseInlineBracketedLink(InlineParser parser) {
+ parser.advanceBy(1);
+
+ final buffer = StringBuffer();
+ while (true) {
+ final char = parser.charAt(parser.pos);
+ if (char == $backslash) {
+ parser.advanceBy(1);
+ final next = parser.charAt(parser.pos);
+ if (char == $space || char == $lf || char == $cr || char == $ff) {
+ // Not a link (no whitespace allowed within `<...>`).
+ return null;
+ }
+ // TODO: Follow the backslash spec better here.
+ // http://spec.commonmark.org/0.28/#backslash-escapes
+ if (next != $backslash && next != $gt) {
+ buffer.writeCharCode(char);
+ }
+ buffer.writeCharCode(next);
+ } else if (char == $space || char == $lf || char == $cr || char == $ff) {
+ // Not a link (no whitespace allowed within `<...>`).
+ return null;
+ } else if (char == $gt) {
+ break;
+ } else {
+ buffer.writeCharCode(char);
+ }
+ parser.advanceBy(1);
+ if (parser.isDone) {
+ return null;
+ }
+ }
+ final destination = buffer.toString();
+
+ parser.advanceBy(1);
+ final char = parser.charAt(parser.pos);
+ if (char == $space || char == $lf || char == $cr || char == $ff) {
+ final title = _parseTitle(parser);
+ if (title == null && parser.charAt(parser.pos) != $rparen) {
+ // This looked like an inline link, until we found this $space
+ // followed by mystery characters; no longer a link.
+ return null;
+ }
+ return InlineLink(destination, title: title);
+ } else if (char == $rparen) {
+ return InlineLink(destination);
+ } else {
+ // We parsed something like `[foo](X`. Not a link.
+ return null;
+ }
+ }
+
+ /// Parse an inline link with a "bare" destination (a destination _not_
+ /// wrapped in `<...>`). The current position of the parser must be the first
+ /// character of the destination.
+ InlineLink? _parseInlineBareDestinationLink(InlineParser parser) {
+ // According to
+ // [CommonMark](http://spec.commonmark.org/0.28/#link-destination):
+ //
+ // > A link destination consists of [...] a nonempty sequence of
+ // > characters [...], and includes parentheses only if (a) they are
+ // > backslash-escaped or (b) they are part of a balanced pair of
+ // > unescaped parentheses.
+ //
+ // We need to count the open parens. We start with 1 for the paren that
+ // opened the destination.
+ var parenCount = 1;
+ final buffer = StringBuffer();
+
+ while (true) {
+ final char = parser.charAt(parser.pos);
+ switch (char) {
+ case $backslash:
+ parser.advanceBy(1);
+ if (parser.isDone) {
+ return null; // EOF. Not a link.
+ }
+
+ final next = parser.charAt(parser.pos);
+ // Parentheses may be escaped.
+ //
+ // http://spec.commonmark.org/0.28/#example-467
+ if (next != $backslash && next != $lparen && next != $rparen) {
+ buffer.writeCharCode(char);
+ }
+ buffer.writeCharCode(next);
+ break;
+
+ case $space:
+ case $lf:
+ case $cr:
+ case $ff:
+ final destination = buffer.toString();
+ final title = _parseTitle(parser);
+ if (title == null && parser.charAt(parser.pos) != $rparen) {
+ // This looked like an inline link, until we found this $space
+ // followed by mystery characters; no longer a link.
+ return null;
+ }
+ // [_parseTitle] made sure the title was follwed by a closing `)`
+ // (but it's up to the code here to examine the balance of
+ // parentheses).
+ parenCount--;
+ if (parenCount == 0) {
+ return InlineLink(destination, title: title);
+ }
+ break;
+
+ case $lparen:
+ parenCount++;
+ buffer.writeCharCode(char);
+ break;
+
+ case $rparen:
+ parenCount--;
+ // ignore: invariant_booleans
+ if (parenCount == 0) {
+ final destination = buffer.toString();
+ return InlineLink(destination);
+ }
+ buffer.writeCharCode(char);
+ break;
+
+ default:
+ buffer.writeCharCode(char);
+ }
+ parser.advanceBy(1);
+ if (parser.isDone) {
+ return null; // EOF. Not a link.
+ }
+ }
+ }
+
+ // Walk the parser forward through any whitespace.
+ void _moveThroughWhitespace(InlineParser parser) {
+ while (true) {
+ final char = parser.charAt(parser.pos);
+ if (char != $space &&
+ char != $tab &&
+ char != $lf &&
+ char != $vt &&
+ char != $cr &&
+ char != $ff) {
+ return;
+ }
+ parser.advanceBy(1);
+ if (parser.isDone) {
+ return;
+ }
+ }
+ }
+
+ // Parse a link title in [parser] at it's current position. The parser's
+ // current position should be a whitespace character that followed a link
+ // destination.
+ String? _parseTitle(InlineParser parser) {
+ _moveThroughWhitespace(parser);
+ if (parser.isDone) {
+ return null;
+ }
+
+ // The whitespace should be followed by a title delimiter.
+ final delimiter = parser.charAt(parser.pos);
+ if (delimiter != $apostrophe &&
+ delimiter != $quote &&
+ delimiter != $lparen) {
+ return null;
+ }
+
+ final closeDelimiter = delimiter == $lparen ? $rparen : delimiter;
+ parser.advanceBy(1);
+
+ // Now we look for an un-escaped closing delimiter.
+ final buffer = StringBuffer();
+ while (true) {
+ final char = parser.charAt(parser.pos);
+ if (char == $backslash) {
+ parser.advanceBy(1);
+ final next = parser.charAt(parser.pos);
+ if (next != $backslash && next != closeDelimiter) {
+ buffer.writeCharCode(char);
+ }
+ buffer.writeCharCode(next);
+ } else if (char == closeDelimiter) {
+ break;
+ } else {
+ buffer.writeCharCode(char);
+ }
+ parser.advanceBy(1);
+ if (parser.isDone) {
+ return null;
+ }
+ }
+ final title = buffer.toString();
+
+ // Advance past the closing delimiter.
+ parser.advanceBy(1);
+ if (parser.isDone) {
+ return null;
+ }
+ _moveThroughWhitespace(parser);
+ if (parser.isDone) {
+ return null;
+ }
+ if (parser.charAt(parser.pos) != $rparen) {
+ return null;
+ }
+ return title;
+ }
+}
+
+/// Matches images like `` and
+/// `![alternate text][label]`.
+class ImageSyntax extends LinkSyntax {
+ ImageSyntax({super.linkResolver}) : super(pattern: r'!\[');
+
+ @override
+ Node _createNode(TagState state, String destination, String? title) {
+ final element = Element.empty('img');
+ element.attributes['src'] = escapeHtml(destination);
+ element.attributes['alt'] = state.textContent;
+ if (title != null && title.isNotEmpty) {
+ element.attributes['title'] = escapeAttribute(title);
+ }
+ return element;
+ }
+
+ // Add an image node to [parser]'s AST.
+ //
+ // If [label] is present, the potential image is treated as a reference image.
+ // Otherwise, it is treated as an inline image.
+ //
+ // Returns whether the image was added successfully.
+ @override
+ bool _tryAddReferenceLink(InlineParser parser, TagState state, String label) {
+ final element =
+ _resolveReferenceLink(label, state, parser.document.linkReferences);
+ if (element == null) {
+ return false;
+ }
+ parser
+ ..addNode(element)
+ ..start = parser.pos;
+ return true;
+ }
+}
+
+/// Matches backtick-enclosed inline code blocks.
+class CodeSyntax extends InlineSyntax {
+ CodeSyntax() : super(_pattern);
+
+ // This pattern matches:
+ //
+ // * a string of backticks (not followed by any more), followed by
+ // * a non-greedy string of anything, including newlines, ending with anything
+ // except a backtick, followed by
+ // * a string of backticks the same length as the first, not followed by any
+ // more.
+ //
+ // This conforms to the delimiters of inline code, both in Markdown.pl, and
+ // CommonMark.
+ static const String _pattern = r'(`+(?!`))((?:.|\n)*?[^`])\1(?!`)';
+
+ @override
+ bool tryMatch(InlineParser parser, [int? startMatchPos]) {
+ if (parser.pos > 0 && parser.charAt(parser.pos - 1) == $backquote) {
+ // Not really a match! We can't just sneak past one backtick to try the
+ // next character. An example of this situation would be:
+ //
+ // before ``` and `` after.
+ // ^--parser.pos
+ return false;
+ }
+
+ final match = pattern.matchAsPrefix(parser.source, parser.pos);
+ if (match == null) {
+ return false;
+ }
+ parser.writeText();
+ if (onMatch(parser, match)) {
+ parser.consume(match[0]!.length);
+ }
+ return true;
+ }
+
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ parser.addNode(Element.text('code', escapeHtml(match[2]!.trim())));
+ return true;
+ }
+}
+
+/// Matches GitHub Markdown emoji syntax like `:smile:`.
+///
+/// There is no formal specification of GitHub's support for this colon-based
+/// emoji support, so this syntax is based on the results of Markdown-enabled
+/// text fields at github.com.
+class EmojiSyntax extends InlineSyntax {
+ // Emoji "aliases" are mostly limited to lower-case letters, numbers, and
+ // underscores, but GitHub also supports `:+1:` and `:-1:`.
+ EmojiSyntax() : super(':([a-z0-9_+-]+):');
+
+ @override
+ bool onMatch(InlineParser parser, Match match) {
+ final alias = match[1];
+ final emoji = emojis[alias!];
+ if (emoji == null) {
+ parser.advanceBy(1);
+ return false;
+ }
+ parser.addNode(Text(emoji));
+
+ return true;
+ }
+}
+
+/// Keeps track of a currently open tag while it is being parsed.
+///
+/// The parser maintains a stack of these so it can handle nested tags.
+class TagState {
+ TagState(this.startPos, this.endPos, this.syntax, this.openingDelimiterRun)
+ : children = [];
+
+ /// The point in the original source where this tag started.
+ final int startPos;
+
+ /// The point in the original source where open tag ended.
+ final int endPos;
+
+ /// The syntax that created this node.
+ final TagSyntax? syntax;
+
+ /// The children of this node. Will be `null` for text nodes.
+ final List children;
+
+ final _DelimiterRun? openingDelimiterRun;
+
+ /// Attempts to close this tag by matching the current text against its end
+ /// pattern.
+ bool tryMatch(InlineParser parser) {
+ final endMatch =
+ syntax!.endPattern.matchAsPrefix(parser.source, parser.pos);
+ if (endMatch == null) {
+ return false;
+ }
+
+ if (!syntax!.requiresDelimiterRun) {
+ // Close the tag.
+ close(parser, endMatch);
+ return true;
+ }
+
+ // TODO: Move this logic into TagSyntax.
+ final runLength = endMatch.group(0)!.length;
+ final openingRunLength = endPos - startPos;
+ final closingMatchStart = parser.pos;
+ final closingMatchEnd = parser.pos + runLength - 1;
+ final closingDelimiterRun =
+ _DelimiterRun.tryParse(parser, closingMatchStart, closingMatchEnd);
+ if (closingDelimiterRun != null && closingDelimiterRun.canClose) {
+ // Emphasis rules #9 and #10:
+ final oneRunOpensAndCloses =
+ (openingDelimiterRun!.canOpen && openingDelimiterRun!.canClose) ||
+ (closingDelimiterRun.canOpen && closingDelimiterRun.canClose);
+ if (oneRunOpensAndCloses &&
+ (openingRunLength + closingDelimiterRun.length!) % 3 == 0) {
+ return false;
+ }
+ // Close the tag.
+ close(parser, endMatch);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /// Pops this tag off the stack, completes it, and adds it to the output.
+ ///
+ /// Will discard any unmatched tags that happen to be above it on the stack.
+ /// If this is the last node in the stack, returns its children.
+ List? close(InlineParser parser, Match? endMatch) {
+ // If there are unclosed tags on top of this one when it's closed, that
+ // means they are mismatched. Mismatched tags are treated as plain text in
+ // markdown. So for each tag above this one, we write its start tag as text
+ // and then adds its children to this one's children.
+ final index = parser._stack.indexOf(this);
+
+ // Remove the unmatched children.
+ final unmatchedTags = parser._stack.sublist(index + 1);
+ parser._stack.removeRange(index + 1, parser._stack.length);
+
+ // Flatten them out onto this tag.
+ for (final unmatched in unmatchedTags) {
+ // Write the start tag as text.
+ parser.writeTextRange(unmatched.startPos, unmatched.endPos);
+
+ // Bequeath its children unto this tag.
+ children.addAll(unmatched.children);
+ }
+
+ // Pop this off the stack.
+ parser.writeText();
+ parser._stack.removeLast();
+
+ // If the stack is empty now, this is the special "results" node.
+ if (parser._stack.isEmpty) {
+ return children;
+ }
+ final endMatchIndex = parser.pos;
+
+ // We are still parsing, so add this to its parent's children.
+ if (syntax!.onMatchEnd(parser, endMatch!, this)) {
+ parser.consume(endMatch[0]!.length);
+ } else {
+ // Didn't close correctly so revert to text.
+ parser
+ ..writeTextRange(startPos, endPos)
+ .._stack.last.children.addAll(children)
+ ..pos = endMatchIndex
+ ..advanceBy(endMatch[0]!.length);
+ }
+
+ return null;
+ }
+
+ String get textContent => children.map((child) => child.textContent).join();
+}
+
+class InlineLink {
+ InlineLink(this.destination, {this.title});
+
+ final String destination;
+ final String? title;
+}
diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/util.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/util.dart
new file mode 100644
index 00000000..aed4c3c3
--- /dev/null
+++ b/packages/quill_html_converter/lib/src/packages/delta_markdown/util.dart
@@ -0,0 +1,71 @@
+import 'dart:convert';
+
+import 'package:charcode/charcode.dart';
+
+String escapeHtml(String html) =>
+ const HtmlEscape(HtmlEscapeMode.element).convert(html);
+
+// Escape the contents of [value], so that it may be used as an HTML attribute.
+
+// Based on http://spec.commonmark.org/0.28/#backslash-escapes.
+String escapeAttribute(String value) {
+ final result = StringBuffer();
+ int ch;
+ for (var i = 0; i < value.codeUnits.length; i++) {
+ ch = value.codeUnitAt(i);
+ if (ch == $backslash) {
+ i++;
+ if (i == value.codeUnits.length) {
+ result.writeCharCode(ch);
+ break;
+ }
+ ch = value.codeUnitAt(i);
+ switch (ch) {
+ case $quote:
+ result.write('"');
+ break;
+ case $exclamation:
+ case $hash:
+ case $dollar:
+ case $percent:
+ case $ampersand:
+ case $apostrophe:
+ case $lparen:
+ case $rparen:
+ case $asterisk:
+ case $plus:
+ case $comma:
+ case $dash:
+ case $dot:
+ case $slash:
+ case $colon:
+ case $semicolon:
+ case $lt:
+ case $equal:
+ case $gt:
+ case $question:
+ case $at:
+ case $lbracket:
+ case $backslash:
+ case $rbracket:
+ case $caret:
+ case $underscore:
+ case $backquote:
+ case $lbrace:
+ case $bar:
+ case $rbrace:
+ case $tilde:
+ result.writeCharCode(ch);
+ break;
+ default:
+ result.write('%5C');
+ result.writeCharCode(ch);
+ }
+ } else if (ch == $quote) {
+ result.write('%22');
+ } else {
+ result.writeCharCode(ch);
+ }
+ }
+ return result.toString();
+}
diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/version.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/version.dart
new file mode 100644
index 00000000..19433ffa
--- /dev/null
+++ b/packages/quill_html_converter/lib/src/packages/delta_markdown/version.dart
@@ -0,0 +1,2 @@
+// Generated code. Do not modify.
+const packageVersion = '0.0.2';
diff --git a/packages/quill_html_converter/pubspec.yaml b/packages/quill_html_converter/pubspec.yaml
index 8620008d..bd1b9555 100644
--- a/packages/quill_html_converter/pubspec.yaml
+++ b/packages/quill_html_converter/pubspec.yaml
@@ -21,8 +21,7 @@ dependencies:
flutter_quill: ^8.5.1
vsc_quill_delta_to_html: ^1.0.3
html2md: ^1.3.1
- delta_markdown:
- path: ./delta_markdown
+ charcode: ^1.3.1
dev_dependencies:
flutter_test:
|