diff --git a/packages/quill_html_converter/lib/quill_html_converter.dart b/packages/quill_html_converter/lib/quill_html_converter.dart index 00f5fed6..72645e99 100644 --- a/packages/quill_html_converter/lib/quill_html_converter.dart +++ b/packages/quill_html_converter/lib/quill_html_converter.dart @@ -2,14 +2,13 @@ library quill_html_converter; import 'dart:convert' show jsonDecode; +import 'package:delta_markdown_converter/delta_markdown_converter.dart' + as delta_markdown show markdownToDelta; import 'package:flutter_quill/flutter_quill.dart' show Delta; import 'package:html2md/html2md.dart' as html2md; import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart' as conventer show ConverterOptions, QuillDeltaToHtmlConverter; -import 'src/packages/delta_markdown/delta_markdown.dart' as delta_markdown - show markdownToDelta; - typedef ConverterOptions = conventer.ConverterOptions; /// A extension for [Delta] which comes from `flutter_quill` to extends diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/ast.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/ast.dart deleted file mode 100644 index 5356f1d0..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/ast.dart +++ /dev/null @@ -1,113 +0,0 @@ -// 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. - -typedef Resolver = Node? Function(String name, [String? title]); - -/// Base class for any AST item. -/// -/// Roughly corresponds to Node in the DOM. Will be either an Element or Text. -class Node { - void accept(NodeVisitor visitor) {} - - bool isToplevel = false; - - String? get textContent { - return null; - } -} - -/// A named tag that can contain other nodes. -class Element extends Node { - /// Instantiates a [tag] Element with [children]. - Element(this.tag, this.children) : attributes = {}; - - /// Instantiates an empty, self-closing [tag] Element. - Element.empty(this.tag) - : children = null, - attributes = {}; - - /// Instantiates a [tag] Element with no [children]. - Element.withTag(this.tag) - : children = [], - attributes = {}; - - /// Instantiates a [tag] Element with a single Text child. - Element.text(this.tag, String text) - : children = [Text(text)], - attributes = {}; - - final String tag; - final List? children; - final Map attributes; - String? generatedId; - - /// Whether this element is self-closing. - bool get isEmpty => children == null; - - @override - void accept(NodeVisitor visitor) { - if (visitor.visitElementBefore(this)) { - if (children != null) { - for (final child in children!) { - child.accept(visitor); - } - } - visitor.visitElementAfter(this); - } - } - - @override - String get textContent => children == null - ? '' - : children!.map((child) => child.textContent).join(); -} - -/// A plain text element. -class Text extends Node { - Text(this.text); - - final String text; - - @override - void accept(NodeVisitor visitor) => visitor.visitText(this); - - @override - String get textContent => text; -} - -/// Inline content that has not been parsed into inline nodes (strong, links, -/// etc). -/// -/// These placeholder nodes should only remain in place while the block nodes -/// of a document are still being parsed, in order to gather all reference link -/// definitions. -class UnparsedContent extends Node { - UnparsedContent(this.textContent); - - @override - final String textContent; - - @override - void accept(NodeVisitor visitor); -} - -/// Visitor pattern for the AST. -/// -/// Renderers or other AST transformers should implement this. -abstract class NodeVisitor { - /// Called when a Text node has been reached. - void visitText(Text text); - - /// Called when an Element has been reached, before its children have been - /// visited. - /// - /// Returns `false` to skip its children. - bool visitElementBefore(Element element); - - /// Called when an Element has been reached, after its children have been - /// visited. - /// - /// Will not be called if [visitElementBefore] returns `false`. - void visitElementAfter(Element element); -} diff --git a/packages/quill_html_converter/lib/src/packages/delta_markdown/block_parser.dart b/packages/quill_html_converter/lib/src/packages/delta_markdown/block_parser.dart deleted file mode 100644 index d222c7b6..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/block_parser.dart +++ /dev/null @@ -1,1099 +0,0 @@ -// 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 'document.dart'; -import 'util.dart'; - -/// The line contains only whitespace or is empty. -final _emptyPattern = RegExp(r'^(?:[ \t]*)$'); - -/// A series of `=` or `-` (on the next line) define setext-style headers. -final _setextPattern = RegExp(r'^[ ]{0,3}(=+|-+)\s*$'); - -/// Leading (and trailing) `#` define atx-style headers. -/// -/// Starts with 1-6 unescaped `#` characters which must not be followed by a -/// non-space character. Line may end with any number of `#` characters,. -final _headerPattern = RegExp(r'^ {0,3}(#{1,6})[ \x09\x0b\x0c](.*?)#*$'); - -/// The line starts with `>` with one optional space after. -final _blockquotePattern = RegExp(r'^[ ]{0,3}>[ ]?(.*)$'); - -/// A line indented four spaces. Used for code blocks and lists. -final _indentPattern = RegExp(r'^(?: | {0,3}\t)(.*)$'); - -/// Fenced code block. -final _codePattern = RegExp(r'^[ ]{0,3}(`{3,}|~{3,})(.*)$'); - -/// Three or more hyphens, asterisks or underscores by themselves. Note that -/// a line like `----` is valid as both HR and SETEXT. In case of a tie, -/// SETEXT should win. -final _hrPattern = RegExp(r'^ {0,3}([-*_])[ \t]*\1[ \t]*\1(?:\1|[ \t])*$'); - -/// One or more whitespace, for compressing. -final _oneOrMoreWhitespacePattern = RegExp('[ \n\r\t]+'); - -/// A line starting with one of these markers: `-`, `*`, `+`. May have up to -/// three leading spaces before the marker and any number of spaces or tabs -/// after. -/// -/// Contains a dummy group at [2], so that the groups in [_ulPattern] and -/// [_olPattern] match up; in both, [2] is the length of the number that begins -/// the list marker. -final _ulPattern = RegExp(r'^([ ]{0,3})()([*+-])(([ \t])([ \t]*)(.*))?$'); - -/// A line starting with a number like `123.`. May have up to three leading -/// spaces before the marker and any number of spaces or tabs after. -final _olPattern = - RegExp(r'^([ ]{0,3})(\d{1,9})([\.)])(([ \t])([ \t]*)(.*))?$'); - -/// A line of hyphens separated by at least one pipe. -final _tablePattern = RegExp(r'^[ ]{0,3}\|?( *:?\-+:? *\|)+( *:?\-+:? *)?$'); - -/// Maintains the internal state needed to parse a series of lines into blocks -/// of Markdown suitable for further inline parsing. -class BlockParser { - BlockParser(this.lines, this.document) { - blockSyntaxes - ..addAll(document.blockSyntaxes) - ..addAll(standardBlockSyntaxes); - } - - final List lines; - - /// The Markdown document this parser is parsing. - final Document document; - - /// The enabled block syntaxes. - /// - /// To turn a series of lines into blocks, each of these will be tried in - /// turn. Order matters here. - final List blockSyntaxes = []; - - /// Index of the current line. - int _pos = 0; - - /// Whether the parser has encountered a blank line between two block-level - /// elements. - bool encounteredBlankLine = false; - - /// The collection of built-in block parsers. - final List standardBlockSyntaxes = [ - const EmptyBlockSyntax(), - const BlockTagBlockHtmlSyntax(), - LongBlockHtmlSyntax(r'^ {0,3}|$)', ''), - LongBlockHtmlSyntax(r'^ {0,3}|$)', ''), - LongBlockHtmlSyntax(r'^ {0,3}|$)', ''), - LongBlockHtmlSyntax('^ {0,3}'), - LongBlockHtmlSyntax('^ {0,3}<\\?', '\\?>'), - LongBlockHtmlSyntax('^ {0,3}'), - LongBlockHtmlSyntax('^ {0,3}'), - const OtherTagBlockHtmlSyntax(), - const SetextHeaderSyntax(), - const HeaderSyntax(), - const CodeBlockSyntax(), - const BlockquoteSyntax(), - const HorizontalRuleSyntax(), - const UnorderedListSyntax(), - const OrderedListSyntax(), - const ParagraphSyntax() - ]; - - /// Gets the current line. - String get current => lines[_pos]; - - /// Gets the line after the current one or `null` if there is none. - String? get next { - // Don't read past the end. - if (_pos >= lines.length - 1) { - return null; - } - return lines[_pos + 1]; - } - - /// Gets the line that is [linesAhead] lines ahead of the current one, or - /// `null` if there is none. - /// - /// `peek(0)` is equivalent to [current]. - /// - /// `peek(1)` is equivalent to [next]. - String? peek(int linesAhead) { - if (linesAhead < 0) { - throw ArgumentError('Invalid linesAhead: $linesAhead; must be >= 0.'); - } - // Don't read past the end. - if (_pos >= lines.length - linesAhead) { - return null; - } - return lines[_pos + linesAhead]; - } - - void advance() { - _pos++; - } - - bool get isDone => _pos >= lines.length; - - /// Gets whether or not the current line matches the given pattern. - bool matches(RegExp regex) { - if (isDone) { - return false; - } - return regex.firstMatch(current) != null; - } - - /// Gets whether or not the next line matches the given pattern. - bool matchesNext(RegExp regex) { - if (next == null) { - return false; - } - return regex.firstMatch(next!) != null; - } - - List parseLines() { - final blocks = []; - while (!isDone) { - for (final syntax in blockSyntaxes) { - if (syntax.canParse(this)) { - final block = syntax.parse(this); - if (block != null) { - blocks.add(block); - } - break; - } - } - } - - return blocks; - } -} - -abstract class BlockSyntax { - const BlockSyntax(); - - /// Gets the regex used to identify the beginning of this block, if any. - RegExp? get pattern => null; - - bool get canEndBlock => true; - - bool canParse(BlockParser parser) { - return pattern!.firstMatch(parser.current) != null; - } - - Node? parse(BlockParser parser); - - List parseChildLines(BlockParser parser) { - // Grab all of the lines that form the block element. - final childLines = []; - - while (!parser.isDone) { - final match = pattern!.firstMatch(parser.current); - if (match == null) { - break; - } - childLines.add(match[1]); - parser.advance(); - } - - return childLines; - } - - /// Gets whether or not [parser]'s current line should end the previous block. - static bool isAtBlockEnd(BlockParser parser) { - if (parser.isDone) { - return true; - } - return parser.blockSyntaxes.any((s) => s.canParse(parser) && s.canEndBlock); - } - - /// Generates a valid HTML anchor from the inner text of [element]. - static String generateAnchorHash(Element element) => - element.children!.first.textContent! - .toLowerCase() - .trim() - .replaceAll(RegExp(r'[^a-z0-9 _-]'), '') - .replaceAll(RegExp(r'\s'), '-'); -} - -class EmptyBlockSyntax extends BlockSyntax { - const EmptyBlockSyntax(); - - @override - RegExp get pattern => _emptyPattern; - - @override - Node? parse(BlockParser parser) { - parser - ..encounteredBlankLine = true - ..advance(); - - // Don't actually emit anything. - return null; - } -} - -/// Parses setext-style headers. -class SetextHeaderSyntax extends BlockSyntax { - const SetextHeaderSyntax(); - - @override - bool canParse(BlockParser parser) { - if (!_interperableAsParagraph(parser.current)) { - return false; - } - - var i = 1; - while (true) { - final nextLine = parser.peek(i); - if (nextLine == null) { - // We never reached an underline. - return false; - } - if (_setextPattern.hasMatch(nextLine)) { - return true; - } - // Ensure that we're still in something like paragraph text. - if (!_interperableAsParagraph(nextLine)) { - return false; - } - i++; - } - } - - @override - Node parse(BlockParser parser) { - final lines = []; - late String tag; - while (!parser.isDone) { - final match = _setextPattern.firstMatch(parser.current); - if (match == null) { - // More text. - lines.add(parser.current); - parser.advance(); - continue; - } else { - // The underline. - tag = (match[1]![0] == '=') ? 'h1' : 'h2'; - parser.advance(); - break; - } - } - - final contents = UnparsedContent(lines.join('\n')); - - return Element(tag, [contents]); - } - - bool _interperableAsParagraph(String line) => - !(_indentPattern.hasMatch(line) || - _codePattern.hasMatch(line) || - _headerPattern.hasMatch(line) || - _blockquotePattern.hasMatch(line) || - _hrPattern.hasMatch(line) || - _ulPattern.hasMatch(line) || - _olPattern.hasMatch(line) || - _emptyPattern.hasMatch(line)); -} - -/// Parses setext-style headers, and adds generated IDs to the generated -/// elements. -class SetextHeaderWithIdSyntax extends SetextHeaderSyntax { - const SetextHeaderWithIdSyntax(); - - @override - Node parse(BlockParser parser) { - final element = super.parse(parser) as Element; - element.generatedId = BlockSyntax.generateAnchorHash(element); - return element; - } -} - -/// Parses atx-style headers: `## Header ##`. -class HeaderSyntax extends BlockSyntax { - const HeaderSyntax(); - - @override - RegExp get pattern => _headerPattern; - - @override - Node parse(BlockParser parser) { - final match = pattern.firstMatch(parser.current)!; - parser.advance(); - final level = match[1]!.length; - final contents = UnparsedContent(match[2]!.trim()); - return Element('h$level', [contents]); - } -} - -/// Parses atx-style headers, and adds generated IDs to the generated elements. -class HeaderWithIdSyntax extends HeaderSyntax { - const HeaderWithIdSyntax(); - - @override - Node parse(BlockParser parser) { - final element = super.parse(parser) as Element; - element.generatedId = BlockSyntax.generateAnchorHash(element); - return element; - } -} - -/// Parses email-style blockquotes: `> quote`. -class BlockquoteSyntax extends BlockSyntax { - const BlockquoteSyntax(); - - @override - RegExp get pattern => _blockquotePattern; - - @override - List parseChildLines(BlockParser parser) { - // Grab all of the lines that form the blockquote, stripping off the ">". - final childLines = []; - - while (!parser.isDone) { - final match = pattern.firstMatch(parser.current); - if (match != null) { - childLines.add(match[1]!); - parser.advance(); - continue; - } - - // A paragraph continuation is OK. This is content that cannot be parsed - // as any other syntax except Paragraph, and it doesn't match the bar in - // a Setext header. - if (parser.blockSyntaxes.firstWhere((s) => s.canParse(parser)) - is ParagraphSyntax) { - childLines.add(parser.current); - parser.advance(); - } else { - break; - } - } - - return childLines; - } - - @override - Node parse(BlockParser parser) { - final childLines = parseChildLines(parser); - - // Recursively parse the contents of the blockquote. - final children = BlockParser(childLines, parser.document).parseLines(); - return Element('blockquote', children); - } -} - -/// Parses preformatted code blocks that are indented four spaces. -class CodeBlockSyntax extends BlockSyntax { - const CodeBlockSyntax(); - - @override - RegExp get pattern => _indentPattern; - - @override - bool get canEndBlock => false; - - @override - List parseChildLines(BlockParser parser) { - final childLines = []; - - while (!parser.isDone) { - final match = pattern.firstMatch(parser.current); - if (match != null) { - childLines.add(match[1]); - parser.advance(); - } else { - // If there's a codeblock, then a newline, then a codeblock, keep the - // code blocks together. - final nextMatch = - parser.next != null ? pattern.firstMatch(parser.next!) : null; - if (parser.current.trim() == '' && nextMatch != null) { - childLines - ..add('') - ..add(nextMatch[1]); - parser - ..advance() - ..advance(); - } else { - break; - } - } - } - return childLines; - } - - @override - Node parse(BlockParser parser) { - final childLines = parseChildLines(parser) - // The Markdown tests expect a trailing newline. - ..add(''); - - // Escape the code. - final escaped = escapeHtml(childLines.join('\n')); - - return Element('pre', [Element.text('code', escaped)]); - } -} - -/// Parses preformatted code blocks between two ~~~ or ``` sequences. -/// -/// See [Pandoc's documentation](http://pandoc.org/README.html#fenced-code-blocks). -class FencedCodeBlockSyntax extends BlockSyntax { - const FencedCodeBlockSyntax(); - - @override - RegExp get pattern => _codePattern; - - @override - List parseChildLines(BlockParser parser, [String? endBlock]) { - endBlock ??= ''; - - final childLines = []; - parser.advance(); - - while (!parser.isDone) { - final match = pattern.firstMatch(parser.current); - if (match == null || !match[1]!.startsWith(endBlock)) { - childLines.add(parser.current); - parser.advance(); - } else { - parser.advance(); - break; - } - } - - return childLines; - } - - @override - Node parse(BlockParser parser) { - // Get the syntax identifier, if there is one. - final match = pattern.firstMatch(parser.current)!; - final endBlock = match.group(1); - var infoString = match.group(2)!; - - final childLines = parseChildLines(parser, endBlock) - // The Markdown tests expect a trailing newline. - ..add(''); - - final code = Element.text('code', childLines.join('\n')); - - // the info-string should be trimmed - // http://spec.commonmark.org/0.22/#example-100 - infoString = infoString.trim(); - if (infoString.isNotEmpty) { - // only use the first word in the syntax - // http://spec.commonmark.org/0.22/#example-100 - infoString = infoString.split(' ').first; - code.attributes['class'] = 'language-$infoString'; - } - - final element = Element('pre', [code]); - return element; - } -} - -/// Parses horizontal rules like `---`, `_ _ _`, `* * *`, etc. -class HorizontalRuleSyntax extends BlockSyntax { - const HorizontalRuleSyntax(); - - @override - RegExp get pattern => _hrPattern; - - @override - Node parse(BlockParser parser) { - parser.advance(); - return Element.empty('hr'); - } -} - -/// Parses inline HTML at the block level. This differs from other Markdown -/// implementations in several ways: -/// -/// 1. This one is way way WAY simpler. -/// 2. Essentially no HTML parsing or validation is done. We're a Markdown -/// parser, not an HTML parser! -abstract class BlockHtmlSyntax extends BlockSyntax { - const BlockHtmlSyntax(); - - @override - bool get canEndBlock => true; -} - -class BlockTagBlockHtmlSyntax extends BlockHtmlSyntax { - const BlockTagBlockHtmlSyntax(); - - static final _pattern = RegExp( - r'^ {0,3}|/>|$)'); - - @override - RegExp get pattern => _pattern; - - @override - Node parse(BlockParser parser) { - final childLines = []; - - // Eat until we hit a blank line. - while (!parser.isDone && !parser.matches(_emptyPattern)) { - childLines.add(parser.current); - parser.advance(); - } - - return Text(childLines.join('\n')); - } -} - -class OtherTagBlockHtmlSyntax extends BlockTagBlockHtmlSyntax { - const OtherTagBlockHtmlSyntax(); - - @override - bool get canEndBlock => false; - - // Really hacky way to detect "other" HTML. This matches: - // - // * any opening spaces - // * open bracket and maybe a slash ("<" or " RegExp(r'^ {0,3}|\s+[^>]*>)\s*$'); -} - -/// A BlockHtmlSyntax that has a specific `endPattern`. -/// -/// In practice this means that the syntax dominates; it is allowed to eat -/// many lines, including blank lines, before matching its `endPattern`. -class LongBlockHtmlSyntax extends BlockHtmlSyntax { - LongBlockHtmlSyntax(String pattern, String endPattern) - : pattern = RegExp(pattern), - _endPattern = RegExp(endPattern); - - @override - final RegExp pattern; - final RegExp _endPattern; - - @override - Node parse(BlockParser parser) { - final childLines = []; - // Eat until we hit [endPattern]. - while (!parser.isDone) { - childLines.add(parser.current); - if (parser.matches(_endPattern)) { - break; - } - parser.advance(); - } - - parser.advance(); - return Text(childLines.join('\n')); - } -} - -class ListItem { - ListItem(this.lines); - - bool forceBlock = false; - final List lines; -} - -/// Base class for both ordered and unordered lists. -abstract class ListSyntax extends BlockSyntax { - const ListSyntax(); - - @override - bool get canEndBlock => true; - - String get listTag; - - /// A list of patterns that can start a valid block within a list item. - static final blocksInList = [ - _blockquotePattern, - _headerPattern, - _hrPattern, - _indentPattern, - _ulPattern, - _olPattern - ]; - - static final _whitespaceRe = RegExp('[ \t]*'); - - @override - Node parse(BlockParser parser) { - final items = []; - var childLines = []; - - void endItem() { - if (childLines.isNotEmpty) { - items.add(ListItem(childLines)); - childLines = []; - } - } - - Match? match; - bool tryMatch(RegExp pattern) { - match = pattern.firstMatch(parser.current); - return match != null; - } - - String? listMarker; - String? indent; - // In case the first number in an ordered list is not 1, use it as the - // "start". - int? startNumber; - - while (!parser.isDone) { - final leadingSpace = - _whitespaceRe.matchAsPrefix(parser.current)!.group(0)!; - final leadingExpandedTabLength = _expandedTabLength(leadingSpace); - if (tryMatch(_emptyPattern)) { - if (_emptyPattern.firstMatch(parser.next ?? '') != null) { - // Two blank lines ends a list. - break; - } - // Add a blank line to the current list item. - childLines.add(''); - } else if (indent != null && indent.length <= leadingExpandedTabLength) { - // Strip off indent and add to current item. - final line = parser.current - .replaceFirst(leadingSpace, ' ' * leadingExpandedTabLength) - .replaceFirst(indent, ''); - childLines.add(line); - } else if (tryMatch(_hrPattern)) { - // Horizontal rule takes precedence to a list item. - break; - } else if (tryMatch(_ulPattern) || tryMatch(_olPattern)) { - final precedingWhitespace = match![1]; - final digits = match![2] ?? ''; - if (startNumber == null && digits.isNotEmpty) { - startNumber = int.parse(digits); - } - final marker = match![3]; - final firstWhitespace = match![5] ?? ''; - final restWhitespace = match![6] ?? ''; - final content = match![7] ?? ''; - final isBlank = content.isEmpty; - if (listMarker != null && listMarker != marker) { - // Changing the bullet or ordered list delimiter starts a list. - break; - } - listMarker = marker; - final markerAsSpaces = ' ' * (digits.length + marker!.length); - if (isBlank) { - // See http://spec.commonmark.org/0.28/#list-items under "3. Item - // starting with a blank line." - // - // If the list item starts with a blank line, the final piece of the - // indentation is just a single space. - indent = '$precedingWhitespace$markerAsSpaces '; - } else if (restWhitespace.length >= 4) { - // See http://spec.commonmark.org/0.28/#list-items under "2. Item - // starting with indented code." - // - // If the list item starts with indented code, we need to _not_ count - // any indentation past the required whitespace character. - indent = precedingWhitespace! + markerAsSpaces + firstWhitespace; - } else { - indent = precedingWhitespace! + - markerAsSpaces + - firstWhitespace + - restWhitespace; - } - // End the current list item and start a one. - endItem(); - childLines.add(restWhitespace + content); - } else if (BlockSyntax.isAtBlockEnd(parser)) { - // Done with the list. - break; - } else { - // If the previous item is a blank line, this means we're done with the - // list and are starting a top-level paragraph. - if ((childLines.isNotEmpty) && (childLines.last == '')) { - parser.encounteredBlankLine = true; - break; - } - - // Anything else is paragraph continuation text. - childLines.add(parser.current); - } - parser.advance(); - } - - endItem(); - final itemNodes = []; - - items.forEach(removeLeadingEmptyLine); - final anyEmptyLines = removeTrailingEmptyLines(items); - var anyEmptyLinesBetweenBlocks = false; - - for (final item in items) { - final itemParser = BlockParser(item.lines, parser.document); - final children = itemParser.parseLines(); - itemNodes.add(Element('li', children)); - anyEmptyLinesBetweenBlocks = - anyEmptyLinesBetweenBlocks || itemParser.encounteredBlankLine; - } - - // Must strip paragraph tags if the list is "tight". - // http://spec.commonmark.org/0.28/#lists - final listIsTight = !anyEmptyLines && !anyEmptyLinesBetweenBlocks; - - if (listIsTight) { - // We must post-process the list items, converting any top-level paragraph - // elements to just text elements. - for (final item in itemNodes) { - for (var i = 0; i < item.children!.length; i++) { - final child = item.children![i]; - if (child is Element && child.tag == 'p') { - item.children!.removeAt(i); - item.children!.insertAll(i, child.children!); - } - } - } - } - - if (listTag == 'ol' && startNumber != 1) { - return Element(listTag, itemNodes)..attributes['start'] = '$startNumber'; - } else { - return Element(listTag, itemNodes); - } - } - - void removeLeadingEmptyLine(ListItem item) { - if (item.lines.isNotEmpty && _emptyPattern.hasMatch(item.lines.first)) { - item.lines.removeAt(0); - } - } - - /// Removes any trailing empty lines and notes whether any items are separated - /// by such lines. - bool removeTrailingEmptyLines(List 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 deleted file mode 100644 index 2221c185..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown.dart +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 2333c93c..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown_decoder.dart +++ /dev/null @@ -1,256 +0,0 @@ -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 deleted file mode 100644 index 2134a92e..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/delta_markdown_encoder.dart +++ /dev/null @@ -1,270 +0,0 @@ -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 deleted file mode 100644 index 890b858c..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/document.dart +++ /dev/null @@ -1,88 +0,0 @@ -// 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 deleted file mode 100644 index cdb3b694..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/emojis.dart +++ /dev/null @@ -1,1510 +0,0 @@ -// 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 deleted file mode 100644 index eadda7bc..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/extension_set.dart +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index d6630e29..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/html_renderer.dart +++ /dev/null @@ -1,121 +0,0 @@ -// 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(''); - } - - /// 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 deleted file mode 100644 index ecbc9a16..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/inline_parser.dart +++ /dev/null @@ -1,1266 +0,0 @@ -// 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 `![alternate text](url "optional title")` 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 deleted file mode 100644 index aed4c3c3..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/util.dart +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index 19433ffa..00000000 --- a/packages/quill_html_converter/lib/src/packages/delta_markdown/version.dart +++ /dev/null @@ -1,2 +0,0 @@ -// 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 236ba3d3..c75c0ed1 100644 --- a/packages/quill_html_converter/pubspec.yaml +++ b/packages/quill_html_converter/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: # markdown: ^7.1.1 charcode: ^1.3.1 collection: ^1.17.2 + delta_markdown_converter: ^0.0.2 dev_dependencies: flutter_test: