| Index: utils/markdown/inline_parser.dart
|
| diff --git a/utils/markdown/inline_parser.dart b/utils/markdown/inline_parser.dart
|
| index 8bf7693d81d8050abac5cec43a8cb8aecafc1010..201f7ef0d6de3ee3b4379e6c4b76a04a23f7c9ab 100644
|
| --- a/utils/markdown/inline_parser.dart
|
| +++ b/utils/markdown/inline_parser.dart
|
| @@ -23,17 +23,17 @@ class InlineParser {
|
| // Encode "<". (Why not encode ">" too? Gruber is toying with us.)
|
| new TextSyntax(@'<', sub: '<'),
|
| // Parse "**strong**" tags.
|
| - new TagSyntax(@'\*\*', tag: 'strong'),
|
| + new TagSyntax(@'\*\*', tag: 'strong'),
|
| // Parse "__strong__" tags.
|
| - new TagSyntax(@'__', tag: 'strong'),
|
| + new TagSyntax(@'__', tag: 'strong'),
|
| // Parse "*emphasis*" tags.
|
| - new TagSyntax(@'\*', tag: 'em'),
|
| + new TagSyntax(@'\*', tag: 'em'),
|
| // Parse "_emphasis_" tags.
|
| // TODO(rnystrom): Underscores in the middle of a word should not be
|
| // parsed as emphasis like_in_this.
|
| - new TagSyntax(@'_', tag: 'em'),
|
| + new TagSyntax(@'_', tag: 'em'),
|
| // Parse inline code within double backticks: "``code``".
|
| - new CodeSyntax(@'``[ ]?(.*?)[ ]?``'),
|
| + new CodeSyntax(@'``\s?((?:.|\n)*?)\s?``'),
|
| // Parse inline code within backticks: "`code`".
|
| new CodeSyntax(@'`([^`]*)`')
|
| ];
|
| @@ -63,7 +63,7 @@ class InlineParser {
|
|
|
| List<Node> parse() {
|
| // Make a fake top tag to hold the results.
|
| - _stack.add(new TagState(0, null));
|
| + _stack.add(new TagState(0, 0, null));
|
|
|
| while (!isDone) {
|
| bool matched = false;
|
| @@ -96,8 +96,13 @@ class InlineParser {
|
| }
|
|
|
| writeText() {
|
| - if (pos > start) {
|
| - final text = source.substring(start, pos);
|
| + writeTextRange(start, pos);
|
| + start = pos;
|
| + }
|
| +
|
| + writeTextRange(int start, int end) {
|
| + if (end > start) {
|
| + final text = source.substring(start, end);
|
| final nodes = _stack.last().children;
|
|
|
| // If the previous node is text too, just append.
|
| @@ -107,18 +112,9 @@ class InlineParser {
|
| } else {
|
| nodes.add(new Text(text));
|
| }
|
| -
|
| - start = pos;
|
| }
|
| }
|
|
|
| - /// Removes the top tag from the stack, reverts it to plain text and adds it
|
| - /// to the output.
|
| - discardUnmatchedTag() {
|
| - final unfinished = _stack.removeLast();
|
| - start = unfinished.startPos;
|
| - }
|
| -
|
| addNode(Node node) {
|
| _stack.last().children.add(node);
|
| }
|
| @@ -129,7 +125,10 @@ class InlineParser {
|
|
|
| bool get isDone() => pos == source.length;
|
|
|
| - void advanceBy(int length) => pos += length;
|
| + void advanceBy(int length) {
|
| + pos += length;
|
| + }
|
| +
|
| void consume(int length) {
|
| pos += length;
|
| start = pos;
|
| @@ -158,7 +157,7 @@ class InlineSyntax {
|
| return false;
|
| }
|
|
|
| - abstract bool match(InlineParser parser, Match match);
|
| + abstract bool onMatch(InlineParser parser, Match match);
|
| }
|
|
|
| /// Matches stuff that should just be passed through as straight text.
|
| @@ -181,7 +180,7 @@ class TextSyntax extends InlineSyntax {
|
| }
|
| }
|
|
|
| -/// Matches autolinks like <http://foo.com>.
|
| +/// Matches autolinks like `<http://foo.com>`.
|
| class AutolinkSyntax extends InlineSyntax {
|
| AutolinkSyntax()
|
| : super(@'<((http|https|ftp)://[^>]*)>');
|
| @@ -198,7 +197,7 @@ class AutolinkSyntax extends InlineSyntax {
|
| }
|
| }
|
|
|
| -/// Matches syntax that has a pair of tags and becomes an element, like '*' for
|
| +/// Matches syntax that has a pair of tags and becomes an element, like `*` for
|
| /// `<em>`. Allows nested tags.
|
| class TagSyntax extends InlineSyntax {
|
| final RegExp endPattern;
|
| @@ -212,7 +211,8 @@ class TagSyntax extends InlineSyntax {
|
| // TODO(rnystrom): Should use named arg for RegExp multiLine.
|
|
|
| bool onMatch(InlineParser parser, Match match) {
|
| - parser._stack.add(new TagState(parser.pos, this));
|
| + parser._stack.add(new TagState(parser.pos,
|
| + parser.pos + match.group(0).length, this));
|
| return true;
|
| }
|
|
|
| @@ -222,17 +222,23 @@ class TagSyntax extends InlineSyntax {
|
| }
|
| }
|
|
|
| -/// Matches inline links like [blah] [id] and [blah] (url).
|
| +/// Matches inline links like `[blah] [id]` and `[blah] (url)`.
|
| class LinkSyntax extends TagSyntax {
|
| /// The regex for the end of a link needs to handle both reference style and
|
| /// inline styles as well as optional titles for inline links. To make that
|
| /// a bit more palatable, this breaks it into pieces.
|
| static get linkPattern() {
|
| - final bracket = @'\][ \n\t]?'; // "]" with optional space after.
|
| - final refLink = @'\[([^\]]*)\]'; // "[id]" reflink id.
|
| - final title = @'(?:[ ]*"([^"]+)"|)'; // Optional title in quotes.
|
| - final inlineLink = '\\(([^ )]+)$title\\)'; // "(url "title")" inline link.
|
| - return '$bracket(?:$refLink|$inlineLink)';
|
| + final refLink = @'\s?\[([^\]]*)\]'; // "[id]" reflink id.
|
| + final title = @'(?:[ ]*"([^"]+)"|)'; // Optional title in quotes.
|
| + final inlineLink = '\\s?\\(([^ )]+)$title\\)'; // "(url "title")" link.
|
| + return '\](?:($refLink|$inlineLink)|)';
|
| +
|
| + // The groups matched by this are:
|
| + // 1: Will be non-empty if it's either a ref or inline link. Will be empty
|
| + // if it's just a bare pair of square brackets with nothing after them.
|
| + // 2: Contains the id inside [] for a reference-style link.
|
| + // 3: Contains the URL for an inline link.
|
| + // 4: Contains the title, if present, for an inline link.
|
| }
|
|
|
| LinkSyntax()
|
| @@ -242,10 +248,33 @@ class LinkSyntax extends TagSyntax {
|
| var url;
|
| var title;
|
|
|
| - if (match.group(2) != '') {
|
| + // If we didn't match refLink or inlineLink, then it means there was
|
| + // nothing after the first square bracket, so it isn't a normal markdown
|
| + // link at all. Instead, we allow users of the library to specify a special
|
| + // resolver function ([setImplicitLinkResolver]) that may choose to handle
|
| + // this. Otherwise, it's just treated as plain text.
|
| + if ((match.group(1) == null) || (match.group(1) == '')) {
|
| + if (_implicitLinkResolver == null) return false;
|
| +
|
| + // Only allow implicit links if the content is just text.
|
| + // TODO(rnystrom): Do we want to relax this?
|
| + if (state.children.length != 1) return false;
|
| + if (state.children[0] is! Text) return false;
|
| +
|
| + Text link = state.children[0];
|
| +
|
| + // See if we have a resolver that will generate a link for us.
|
| + final node = _implicitLinkResolver(link.text);
|
| + if (node == null) return false;
|
| +
|
| + parser.addNode(node);
|
| + return true;
|
| + }
|
| +
|
| + if ((match.group(3) != null) && (match.group(3) != '')) {
|
| // Inline link like [foo](url).
|
| - url = match.group(2);
|
| - title = match.group(3);
|
| + url = match.group(3);
|
| + title = match.group(4);
|
|
|
| // For whatever reason, markdown allows angle-bracketed URLs here.
|
| if (url.startsWith('<') && url.endsWith('>')) {
|
| @@ -253,7 +282,7 @@ class LinkSyntax extends TagSyntax {
|
| }
|
| } else {
|
| // Reference link like [foo] [bar].
|
| - var id = match.group(1);
|
| + var id = match.group(2);
|
| if (id == '') {
|
| // The id is empty ("[]") so infer it from the contents.
|
| id = parser.source.substring(state.startPos + 1, parser.pos);
|
| @@ -296,13 +325,16 @@ class TagState {
|
| /// The point in the original source where this tag started.
|
| int startPos;
|
|
|
| + /// The point in the original source where open tag ended.
|
| + int endPos;
|
| +
|
| /// The syntax that created this node.
|
| final TagSyntax syntax;
|
|
|
| /// The children of this node. Will be `null` for text nodes.
|
| final List<Node> children;
|
|
|
| - TagState(this.startPos, this.syntax)
|
| + TagState(this.startPos, this.endPos, this.syntax)
|
| : children = <Node>[];
|
|
|
| /// Attempts to close this tag by matching the current text against its end
|
| @@ -322,11 +354,25 @@ class TagState {
|
| /// 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<Node> close(InlineParser parser, Match endMatch) {
|
| - // Found a match. If there is anything above this tag on the stack,
|
| - // discard it. For example, given '*a _b*...' when we reach the second
|
| - // '*', '_' will be on the top of the stack. It's mismatched, so we
|
| - // just treat it as text.
|
| - while (parser._stack.last() != this) parser.discardUnmatchedTag();
|
| + // 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.
|
| + int index = parser._stack.indexOf(this);
|
| +
|
| + // Remove the unmatched children.
|
| + final unmatchedTags = parser._stack.getRange(index + 1,
|
| + parser._stack.length - index - 1);
|
| + parser._stack.removeRange(index + 1, parser._stack.length - index - 1);
|
| +
|
| + // 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();
|
|
|