| // Copyright (c) 2022, 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 '../inline_parser.dart'; | |
| import '../patterns.dart'; | |
| import 'inline_syntax.dart'; | |
| /// Matches syntax that has a pair of tags and becomes an element, like `*` for | |
| /// `<em>`. Allows nested tags. | |
| class DelimiterSyntax extends InlineSyntax { | |
| /// Whether this is parsed according to the same nesting rules as [emphasis | |
| /// delimiters][]. | |
| /// | |
| /// [emphasis delimiters]: https://spec.commonmark.org/0.30/#can-open-emphasis | |
| final bool requiresDelimiterRun; | |
| /// Whether to allow intra-word delimiter runs. CommonMark emphasis and | |
| /// strong emphasis does not allow this, but GitHub-Flavored Markdown allows | |
| /// it on strikethrough. | |
| final bool allowIntraWord; | |
| final List<DelimiterTag>? tags; | |
| /// Creates a new [DelimiterSyntax] which matches text on [pattern]. | |
| /// | |
| /// The [pattern] is used to find the matching text. If [requiresDelimiterRun] | |
| /// is passed, this syntax parses according to the same nesting rules as | |
| /// emphasis delimiters. If [startCharacter] is passed, it is used as a | |
| /// pre-matching check which is faster than matching against [pattern]. | |
| DelimiterSyntax( | |
| super.pattern, { | |
| this.requiresDelimiterRun = false, | |
| super.startCharacter, | |
| this.allowIntraWord = false, | |
| this.tags, | |
| }); | |
| @override | |
| bool onMatch(InlineParser parser, Match match) { | |
| final runLength = match.group(0)!.length; | |
| final matchStart = parser.pos; | |
| final matchEnd = parser.pos + runLength; | |
| final text = Text(parser.source.substring(matchStart, matchEnd)); | |
| if (!requiresDelimiterRun) { | |
| parser.pushDelimiter( | |
| SimpleDelimiter( | |
| node: text, | |
| length: runLength, | |
| char: parser.source.codeUnitAt(matchStart), | |
| canOpen: true, | |
| canClose: false, | |
| syntax: this, | |
| endPos: matchEnd, | |
| ), | |
| ); | |
| parser.addNode(text); | |
| return true; | |
| } | |
| final delimiterRun = DelimiterRun.tryParse( | |
| parser, | |
| matchStart, | |
| matchEnd, | |
| syntax: this, | |
| node: text, | |
| allowIntraWord: allowIntraWord, | |
| tags: tags ?? const [], | |
| ); | |
| if (delimiterRun != null) { | |
| parser.pushDelimiter(delimiterRun); | |
| parser.addNode(text); | |
| return true; | |
| } else { | |
| parser.advanceBy(runLength); | |
| return false; | |
| } | |
| } | |
| /// Attempts to close this tag at the current position. | |
| /// | |
| /// If a tag cannot be closed at the current position (for example, if a link | |
| /// reference cannot be found for a link tag's label), then `null` is | |
| /// returned. | |
| /// | |
| /// If a tag can be closed at the current position, then this method calls | |
| /// [getChildren], in which [parser] parses any nested text into child nodes. | |
| /// The returned [Iterable] includes these children nodes. | |
| Iterable<Node>? close( | |
| InlineParser parser, | |
| Delimiter opener, | |
| Delimiter closer, { | |
| required String tag, | |
| required List<Node> Function() getChildren, | |
| }) { | |
| return [Element(tag, getChildren())]; | |
| } | |
| } | |
| class DelimiterTag { | |
| DelimiterTag(this.tag, this.indicatorLength); | |
| // Tag name of the HTML element. | |
| final String tag; | |
| final int indicatorLength; | |
| } | |
| /// A delimiter indicating the possible "open" or possible "close" of a tag for | |
| /// a [DelimiterSyntax]. | |
| abstract class Delimiter { | |
| /// The [Text] node representing the plain text representing this delimiter. | |
| abstract Text node; | |
| /// The type of delimiter. | |
| /// | |
| /// For the two-character image delimiter, `](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). | |
| abstract bool isActive; | |
| /// Whether this delimiter can open emphasis or strong emphasis. | |
| bool get canOpen; | |
| /// Whether this delimiter can close emphasis or strong emphasis. | |
| bool get canClose; | |
| /// The syntax which uses this delimiter to parse a tag. | |
| DelimiterSyntax get syntax; | |
| } | |
| /// A simple delimiter implements the [Delimiter] interface with basic fields, | |
| /// and does not have the concept of "left-flanking" or "right-flanking". | |
| class SimpleDelimiter implements Delimiter { | |
| @override | |
| Text node; | |
| @override | |
| final int char; | |
| @override | |
| final int length; | |
| @override | |
| bool isActive; | |
| @override | |
| final bool canOpen; | |
| @override | |
| final bool canClose; | |
| @override | |
| final DelimiterSyntax syntax; | |
| final int endPos; | |
| SimpleDelimiter({ | |
| required this.node, | |
| required this.char, | |
| required this.length, | |
| required this.canOpen, | |
| required this.canClose, | |
| required this.syntax, | |
| required this.endPos, | |
| }) : isActive = true; | |
| } | |
| /// An implementation of [Delimiter] which uses concepts of "left-flanking" and | |
| /// "right-flanking" to determine the values of [canOpen] and [canClose]. | |
| /// | |
| /// This is primarily used when parsing emphasis and strong emphasis, but can | |
| /// also be used by other extensions of [DelimiterSyntax]. | |
| class DelimiterRun implements Delimiter { | |
| /// According to | |
| /// [CommonMark](https://spec.commonmark.org/0.30/#unicode-punctuation-character): | |
| /// | |
| /// > A punctuation character is an ASCII punctuation character or anything in | |
| /// > the general Unicode categories `Pc`, `Pd`, `Pe`, `Pf`, `Pi`, `Po`, or | |
| /// > `Ps`. | |
| // This RegExp is inspired by | |
| // https://github.com/commonmark/commonmark.js/blob/1f7d09099c20d7861a674674a5a88733f55ff729/lib/inlines.js#L39. | |
| // I don't know if there is any way to simplify it or maintain it. | |
| static final unicodePunctuationPattern = RegExp( | |
| '[' | |
| '$asciiPunctuationEscaped' | |
| r'\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE' | |
| r'\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E' | |
| r'\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E' | |
| r'\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14' | |
| r'\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB' | |
| r'\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736' | |
| r'\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F' | |
| r'\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E' | |
| r'\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051' | |
| r'\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A' | |
| r'\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC' | |
| r'\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42' | |
| r'\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE' | |
| r'\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF' | |
| r'\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF' | |
| r'\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19' | |
| r'\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03' | |
| r'\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F' | |
| r'\uFF5B\uFF5D\uFF5F-\uFF65' | |
| ']', | |
| ); | |
| /// Unicode whitespace. | |
| // See https://spec.commonmark.org/0.30/#unicode-whitespace-character. | |
| // Unicode Zs: https://www.compart.com/en/unicode/category. | |
| static const unicodeWhitespace = | |
| '\u0020\u0009\u000A\u000C\u000D' | |
| '\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008' | |
| '\u2009\u200A\u202F\u205F\u3000'; | |
| @override | |
| Text node; | |
| @override | |
| final int char; | |
| @override | |
| int get length => node.text.length; | |
| @override | |
| bool isActive; | |
| @override | |
| final DelimiterSyntax syntax; | |
| final bool allowIntraWord; | |
| @override | |
| final bool canOpen; | |
| @override | |
| final bool canClose; | |
| final List<DelimiterTag> tags; | |
| DelimiterRun._({ | |
| required this.node, | |
| required this.char, | |
| required this.syntax, | |
| required this.tags, | |
| required bool isLeftFlanking, | |
| required bool isRightFlanking, | |
| required bool isPrecededByPunctuation, | |
| required bool isFollowedByPunctuation, | |
| required this.allowIntraWord, | |
| }) : canOpen = | |
| isLeftFlanking && | |
| (!isRightFlanking || allowIntraWord || isPrecededByPunctuation), | |
| canClose = | |
| isRightFlanking && | |
| (!isLeftFlanking || allowIntraWord || isFollowedByPunctuation), | |
| isActive = true; | |
| /// Tries to parse a delimiter run from [runStart] (inclusive) to [runEnd] | |
| /// (exclusive). | |
| static DelimiterRun? tryParse( | |
| InlineParser parser, | |
| int runStart, | |
| int runEnd, { | |
| required DelimiterSyntax syntax, | |
| required List<DelimiterTag> tags, | |
| required Text node, | |
| bool allowIntraWord = false, | |
| }) { | |
| bool precededByWhitespace; | |
| bool followedByWhitespace; | |
| bool precededByPunctuation; | |
| bool followedByPunctuation; | |
| if (runStart == 0) { | |
| precededByWhitespace = true; | |
| precededByPunctuation = false; | |
| } else { | |
| final preceding = parser.source.substring(runStart - 1, runStart); | |
| precededByWhitespace = unicodeWhitespace.contains(preceding); | |
| precededByPunctuation = | |
| !precededByWhitespace && | |
| unicodePunctuationPattern.hasMatch(preceding); | |
| } | |
| if (runEnd == parser.source.length) { | |
| followedByWhitespace = true; | |
| followedByPunctuation = false; | |
| } else { | |
| final following = parser.source.substring(runEnd, runEnd + 1); | |
| followedByWhitespace = unicodeWhitespace.contains(following); | |
| followedByPunctuation = | |
| !followedByWhitespace && | |
| unicodePunctuationPattern.hasMatch(following); | |
| } | |
| // If it is a left-flanking delimiter run, see | |
| // https://spec.commonmark.org/0.30/#left-flanking-delimiter-run. | |
| final isLeftFlanking = | |
| !followedByWhitespace && | |
| (!followedByPunctuation || | |
| precededByWhitespace || | |
| precededByPunctuation); | |
| // If it is a right-flanking delimiter run, see | |
| // https://spec.commonmark.org/0.30/#right-flanking-delimiter-run. | |
| final isRightFlanking = | |
| !precededByWhitespace && | |
| (!precededByPunctuation || | |
| followedByWhitespace || | |
| followedByPunctuation); | |
| // Make sure the shorter delimiter takes precedence. | |
| tags.sort((a, b) => a.indicatorLength.compareTo(b.indicatorLength)); | |
| return DelimiterRun._( | |
| node: node, | |
| char: parser.charAt(runStart), | |
| syntax: syntax, | |
| tags: tags, | |
| isLeftFlanking: isLeftFlanking, | |
| isRightFlanking: isRightFlanking, | |
| isPrecededByPunctuation: precededByPunctuation, | |
| isFollowedByPunctuation: followedByPunctuation, | |
| allowIntraWord: allowIntraWord, | |
| ); | |
| } | |
| @override | |
| String toString() => | |
| '<char: $char, length: $length, canOpen: $canOpen, ' | |
| 'canClose: $canClose>'; | |
| } |