| OLD | NEW |
| 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /// Maintains the internal state needed to parse inline span elements in | 5 /// Maintains the internal state needed to parse inline span elements in |
| 6 /// markdown. | 6 /// markdown. |
| 7 class InlineParser { | 7 class InlineParser { |
| 8 static List<InlineSyntax> get syntaxes() { | 8 static List<InlineSyntax> get syntaxes() { |
| 9 // Lazy initialize. | 9 // Lazy initialize. |
| 10 if (_syntaxes == null) { | 10 if (_syntaxes == null) { |
| 11 _syntaxes = <InlineSyntax>[ | 11 _syntaxes = <InlineSyntax>[ |
| 12 new AutolinkSyntax(), | 12 new AutolinkSyntax(), |
| 13 new LinkSyntax(), | 13 new LinkSyntax(), |
| 14 // "*" surrounded by spaces is left alone. | 14 // "*" surrounded by spaces is left alone. |
| 15 new TextSyntax(@' \* '), | 15 new TextSyntax(@' \* '), |
| 16 // "_" surrounded by spaces is left alone. | 16 // "_" surrounded by spaces is left alone. |
| 17 new TextSyntax(@' _ '), | 17 new TextSyntax(@' _ '), |
| 18 // Leave already-encoded HTML entities alone. Ensures we don't turn | 18 // Leave already-encoded HTML entities alone. Ensures we don't turn |
| 19 // "&" into "&amp;" | 19 // "&" into "&amp;" |
| 20 new TextSyntax(@'&[#a-zA-Z0-9]*;'), | 20 new TextSyntax(@'&[#a-zA-Z0-9]*;'), |
| 21 // Encode "&". | 21 // Encode "&". |
| 22 new TextSyntax(@'&', sub: '&'), | 22 new TextSyntax(@'&', sub: '&'), |
| 23 // Encode "<". (Why not encode ">" too? Gruber is toying with us.) | 23 // Encode "<". (Why not encode ">" too? Gruber is toying with us.) |
| 24 new TextSyntax(@'<', sub: '<'), | 24 new TextSyntax(@'<', sub: '<'), |
| 25 // Parse "**strong**" tags. | 25 // Parse "**strong**" tags. |
| 26 new TagSyntax(@'\*\*', tag: 'strong'), | 26 new TagSyntax(@'\*\*', tag: 'strong'), |
| 27 // Parse "__strong__" tags. | 27 // Parse "__strong__" tags. |
| 28 new TagSyntax(@'__', tag: 'strong'), | 28 new TagSyntax(@'__', tag: 'strong'), |
| 29 // Parse "*emphasis*" tags. | 29 // Parse "*emphasis*" tags. |
| 30 new TagSyntax(@'\*', tag: 'em'), | 30 new TagSyntax(@'\*', tag: 'em'), |
| 31 // Parse "_emphasis_" tags. | 31 // Parse "_emphasis_" tags. |
| 32 // TODO(rnystrom): Underscores in the middle of a word should not be | 32 // TODO(rnystrom): Underscores in the middle of a word should not be |
| 33 // parsed as emphasis like_in_this. | 33 // parsed as emphasis like_in_this. |
| 34 new TagSyntax(@'_', tag: 'em'), | 34 new TagSyntax(@'_', tag: 'em'), |
| 35 // Parse inline code within double backticks: "``code``". | 35 // Parse inline code within double backticks: "``code``". |
| 36 new CodeSyntax(@'``[ ]?(.*?)[ ]?``'), | 36 new CodeSyntax(@'``\s?((?:.|\n)*?)\s?``'), |
| 37 // Parse inline code within backticks: "`code`". | 37 // Parse inline code within backticks: "`code`". |
| 38 new CodeSyntax(@'`([^`]*)`') | 38 new CodeSyntax(@'`([^`]*)`') |
| 39 ]; | 39 ]; |
| 40 } | 40 } |
| 41 | 41 |
| 42 return _syntaxes; | 42 return _syntaxes; |
| 43 } | 43 } |
| 44 | 44 |
| 45 static List<InlineSyntax> _syntaxes; | 45 static List<InlineSyntax> _syntaxes; |
| 46 | 46 |
| 47 /// The string of markdown being parsed. | 47 /// The string of markdown being parsed. |
| 48 final String source; | 48 final String source; |
| 49 | 49 |
| 50 /// The markdown document this parser is parsing. | 50 /// The markdown document this parser is parsing. |
| 51 final Document document; | 51 final Document document; |
| 52 | 52 |
| 53 /// The current read position. | 53 /// The current read position. |
| 54 int pos = 0; | 54 int pos = 0; |
| 55 | 55 |
| 56 /// Starting position of the last unconsumed text. | 56 /// Starting position of the last unconsumed text. |
| 57 int start = 0; | 57 int start = 0; |
| 58 | 58 |
| 59 final List<TagState> _stack; | 59 final List<TagState> _stack; |
| 60 | 60 |
| 61 InlineParser(this.source, this.document) | 61 InlineParser(this.source, this.document) |
| 62 : _stack = <TagState>[]; | 62 : _stack = <TagState>[]; |
| 63 | 63 |
| 64 List<Node> parse() { | 64 List<Node> parse() { |
| 65 // Make a fake top tag to hold the results. | 65 // Make a fake top tag to hold the results. |
| 66 _stack.add(new TagState(0, null)); | 66 _stack.add(new TagState(0, 0, null)); |
| 67 | 67 |
| 68 while (!isDone) { | 68 while (!isDone) { |
| 69 bool matched = false; | 69 bool matched = false; |
| 70 | 70 |
| 71 // See if any of the current tags on the stack match. We don't allow tags | 71 // See if any of the current tags on the stack match. We don't allow tags |
| 72 // of the same kind to nest, so this takes priority over other possible //
matches. | 72 // of the same kind to nest, so this takes priority over other possible //
matches. |
| 73 for (int i = _stack.length - 1; i > 0; i--) { | 73 for (int i = _stack.length - 1; i > 0; i--) { |
| 74 if (_stack[i].tryMatch(this)) { | 74 if (_stack[i].tryMatch(this)) { |
| 75 matched = true; | 75 matched = true; |
| 76 break; | 76 break; |
| (...skipping 12 matching lines...) Expand all Loading... |
| 89 | 89 |
| 90 // If we got here, it's just text. | 90 // If we got here, it's just text. |
| 91 advanceBy(1); | 91 advanceBy(1); |
| 92 } | 92 } |
| 93 | 93 |
| 94 // Unwind any unmatched tags and get the results. | 94 // Unwind any unmatched tags and get the results. |
| 95 return _stack[0].close(this, null); | 95 return _stack[0].close(this, null); |
| 96 } | 96 } |
| 97 | 97 |
| 98 writeText() { | 98 writeText() { |
| 99 if (pos > start) { | 99 writeTextRange(start, pos); |
| 100 final text = source.substring(start, pos); | 100 start = pos; |
| 101 } |
| 102 |
| 103 writeTextRange(int start, int end) { |
| 104 if (end > start) { |
| 105 final text = source.substring(start, end); |
| 101 final nodes = _stack.last().children; | 106 final nodes = _stack.last().children; |
| 102 | 107 |
| 103 // If the previous node is text too, just append. | 108 // If the previous node is text too, just append. |
| 104 if ((nodes.length > 0) && (nodes.last() is Text)) { | 109 if ((nodes.length > 0) && (nodes.last() is Text)) { |
| 105 final newNode = new Text('${nodes.last().text}$text'); | 110 final newNode = new Text('${nodes.last().text}$text'); |
| 106 nodes[nodes.length - 1] = newNode; | 111 nodes[nodes.length - 1] = newNode; |
| 107 } else { | 112 } else { |
| 108 nodes.add(new Text(text)); | 113 nodes.add(new Text(text)); |
| 109 } | 114 } |
| 110 | |
| 111 start = pos; | |
| 112 } | 115 } |
| 113 } | 116 } |
| 114 | 117 |
| 115 /// Removes the top tag from the stack, reverts it to plain text and adds it | |
| 116 /// to the output. | |
| 117 discardUnmatchedTag() { | |
| 118 final unfinished = _stack.removeLast(); | |
| 119 start = unfinished.startPos; | |
| 120 } | |
| 121 | |
| 122 addNode(Node node) { | 118 addNode(Node node) { |
| 123 _stack.last().children.add(node); | 119 _stack.last().children.add(node); |
| 124 } | 120 } |
| 125 | 121 |
| 126 // TODO(rnystrom): Only need this because RegExp doesn't let you start | 122 // TODO(rnystrom): Only need this because RegExp doesn't let you start |
| 127 // searching from a given offset. | 123 // searching from a given offset. |
| 128 String get currentSource() => source.substring(pos, source.length); | 124 String get currentSource() => source.substring(pos, source.length); |
| 129 | 125 |
| 130 bool get isDone() => pos == source.length; | 126 bool get isDone() => pos == source.length; |
| 131 | 127 |
| 132 void advanceBy(int length) => pos += length; | 128 void advanceBy(int length) { |
| 129 pos += length; |
| 130 } |
| 131 |
| 133 void consume(int length) { | 132 void consume(int length) { |
| 134 pos += length; | 133 pos += length; |
| 135 start = pos; | 134 start = pos; |
| 136 } | 135 } |
| 137 } | 136 } |
| 138 | 137 |
| 139 /// Represents one kind of markdown tag that can be parsed. | 138 /// Represents one kind of markdown tag that can be parsed. |
| 140 class InlineSyntax { | 139 class InlineSyntax { |
| 141 final RegExp pattern; | 140 final RegExp pattern; |
| 142 | 141 |
| 143 InlineSyntax(String pattern) | 142 InlineSyntax(String pattern) |
| 144 : pattern = new RegExp(pattern, true); | 143 : pattern = new RegExp(pattern, true); |
| 145 // TODO(rnystrom): Should use named arg for RegExp multiLine. | 144 // TODO(rnystrom): Should use named arg for RegExp multiLine. |
| 146 | 145 |
| 147 bool tryMatch(InlineParser parser) { | 146 bool tryMatch(InlineParser parser) { |
| 148 final startMatch = pattern.firstMatch(parser.currentSource); | 147 final startMatch = pattern.firstMatch(parser.currentSource); |
| 149 if ((startMatch != null) && (startMatch.start() == 0)) { | 148 if ((startMatch != null) && (startMatch.start() == 0)) { |
| 150 // Write any existing plain text up to this point. | 149 // Write any existing plain text up to this point. |
| 151 parser.writeText(); | 150 parser.writeText(); |
| 152 | 151 |
| 153 if (onMatch(parser, startMatch)) { | 152 if (onMatch(parser, startMatch)) { |
| 154 parser.consume(startMatch.group(0).length); | 153 parser.consume(startMatch.group(0).length); |
| 155 } | 154 } |
| 156 return true; | 155 return true; |
| 157 } | 156 } |
| 158 return false; | 157 return false; |
| 159 } | 158 } |
| 160 | 159 |
| 161 abstract bool match(InlineParser parser, Match match); | 160 abstract bool onMatch(InlineParser parser, Match match); |
| 162 } | 161 } |
| 163 | 162 |
| 164 /// Matches stuff that should just be passed through as straight text. | 163 /// Matches stuff that should just be passed through as straight text. |
| 165 class TextSyntax extends InlineSyntax { | 164 class TextSyntax extends InlineSyntax { |
| 166 String substitute; | 165 String substitute; |
| 167 TextSyntax(String pattern, [String sub]) | 166 TextSyntax(String pattern, [String sub]) |
| 168 : super(pattern), | 167 : super(pattern), |
| 169 substitute = sub; | 168 substitute = sub; |
| 170 | 169 |
| 171 bool onMatch(InlineParser parser, Match match) { | 170 bool onMatch(InlineParser parser, Match match) { |
| 172 if (substitute == null) { | 171 if (substitute == null) { |
| 173 // Just use the original matched text. | 172 // Just use the original matched text. |
| 174 parser.advanceBy(match.group(0).length); | 173 parser.advanceBy(match.group(0).length); |
| 175 return false; | 174 return false; |
| 176 } | 175 } |
| 177 | 176 |
| 178 // Insert the substitution. | 177 // Insert the substitution. |
| 179 parser.addNode(new Text(substitute)); | 178 parser.addNode(new Text(substitute)); |
| 180 return true; | 179 return true; |
| 181 } | 180 } |
| 182 } | 181 } |
| 183 | 182 |
| 184 /// Matches autolinks like <http://foo.com>. | 183 /// Matches autolinks like `<http://foo.com>`. |
| 185 class AutolinkSyntax extends InlineSyntax { | 184 class AutolinkSyntax extends InlineSyntax { |
| 186 AutolinkSyntax() | 185 AutolinkSyntax() |
| 187 : super(@'<((http|https|ftp)://[^>]*)>'); | 186 : super(@'<((http|https|ftp)://[^>]*)>'); |
| 188 // TODO(rnystrom): Make case insensitive. | 187 // TODO(rnystrom): Make case insensitive. |
| 189 | 188 |
| 190 bool onMatch(InlineParser parser, Match match) { | 189 bool onMatch(InlineParser parser, Match match) { |
| 191 final url = match.group(1); | 190 final url = match.group(1); |
| 192 | 191 |
| 193 final anchor = new Element.text('a', escapeHtml(url)); | 192 final anchor = new Element.text('a', escapeHtml(url)); |
| 194 anchor.attributes['href'] = url; | 193 anchor.attributes['href'] = url; |
| 195 parser.addNode(anchor); | 194 parser.addNode(anchor); |
| 196 | 195 |
| 197 return true; | 196 return true; |
| 198 } | 197 } |
| 199 } | 198 } |
| 200 | 199 |
| 201 /// Matches syntax that has a pair of tags and becomes an element, like '*' for | 200 /// Matches syntax that has a pair of tags and becomes an element, like `*` for |
| 202 /// `<em>`. Allows nested tags. | 201 /// `<em>`. Allows nested tags. |
| 203 class TagSyntax extends InlineSyntax { | 202 class TagSyntax extends InlineSyntax { |
| 204 final RegExp endPattern; | 203 final RegExp endPattern; |
| 205 final String tag; | 204 final String tag; |
| 206 | 205 |
| 207 TagSyntax(String pattern, [String tag, String end = null]) | 206 TagSyntax(String pattern, [String tag, String end = null]) |
| 208 : super(pattern), | 207 : super(pattern), |
| 209 endPattern = new RegExp((end != null) ? end : pattern, true), | 208 endPattern = new RegExp((end != null) ? end : pattern, true), |
| 210 tag = tag; | 209 tag = tag; |
| 211 // TODO(rnystrom): Doing this.field doesn't seem to work with named args. | 210 // TODO(rnystrom): Doing this.field doesn't seem to work with named args. |
| 212 // TODO(rnystrom): Should use named arg for RegExp multiLine. | 211 // TODO(rnystrom): Should use named arg for RegExp multiLine. |
| 213 | 212 |
| 214 bool onMatch(InlineParser parser, Match match) { | 213 bool onMatch(InlineParser parser, Match match) { |
| 215 parser._stack.add(new TagState(parser.pos, this)); | 214 parser._stack.add(new TagState(parser.pos, |
| 215 parser.pos + match.group(0).length, this)); |
| 216 return true; | 216 return true; |
| 217 } | 217 } |
| 218 | 218 |
| 219 bool onMatchEnd(InlineParser parser, Match match, TagState state) { | 219 bool onMatchEnd(InlineParser parser, Match match, TagState state) { |
| 220 parser.addNode(new Element(tag, state.children)); | 220 parser.addNode(new Element(tag, state.children)); |
| 221 return true; | 221 return true; |
| 222 } | 222 } |
| 223 } | 223 } |
| 224 | 224 |
| 225 /// Matches inline links like [blah] [id] and [blah] (url). | 225 /// Matches inline links like `[blah] [id]` and `[blah] (url)`. |
| 226 class LinkSyntax extends TagSyntax { | 226 class LinkSyntax extends TagSyntax { |
| 227 /// The regex for the end of a link needs to handle both reference style and | 227 /// The regex for the end of a link needs to handle both reference style and |
| 228 /// inline styles as well as optional titles for inline links. To make that | 228 /// inline styles as well as optional titles for inline links. To make that |
| 229 /// a bit more palatable, this breaks it into pieces. | 229 /// a bit more palatable, this breaks it into pieces. |
| 230 static get linkPattern() { | 230 static get linkPattern() { |
| 231 final bracket = @'\][ \n\t]?'; // "]" with optional space after. | 231 final refLink = @'\s?\[([^\]]*)\]'; // "[id]" reflink id. |
| 232 final refLink = @'\[([^\]]*)\]'; // "[id]" reflink id. | 232 final title = @'(?:[ ]*"([^"]+)"|)'; // Optional title in quotes. |
| 233 final title = @'(?:[ ]*"([^"]+)"|)'; // Optional title in quotes. | 233 final inlineLink = '\\s?\\(([^ )]+)$title\\)'; // "(url "title")" link. |
| 234 final inlineLink = '\\(([^ )]+)$title\\)'; // "(url "title")" inline link. | 234 return '\](?:($refLink|$inlineLink)|)'; |
| 235 return '$bracket(?:$refLink|$inlineLink)'; | 235 |
| 236 // The groups matched by this are: |
| 237 // 1: Will be non-empty if it's either a ref or inline link. Will be empty |
| 238 // if it's just a bare pair of square brackets with nothing after them. |
| 239 // 2: Contains the id inside [] for a reference-style link. |
| 240 // 3: Contains the URL for an inline link. |
| 241 // 4: Contains the title, if present, for an inline link. |
| 236 } | 242 } |
| 237 | 243 |
| 238 LinkSyntax() | 244 LinkSyntax() |
| 239 : super(@'\[', end: linkPattern); | 245 : super(@'\[', end: linkPattern); |
| 240 | 246 |
| 241 bool onMatchEnd(InlineParser parser, Match match, TagState state) { | 247 bool onMatchEnd(InlineParser parser, Match match, TagState state) { |
| 242 var url; | 248 var url; |
| 243 var title; | 249 var title; |
| 244 | 250 |
| 245 if (match.group(2) != '') { | 251 // If we didn't match refLink or inlineLink, then it means there was |
| 252 // nothing after the first square bracket, so it isn't a normal markdown |
| 253 // link at all. Instead, we allow users of the library to specify a special |
| 254 // resolver function ([setImplicitLinkResolver]) that may choose to handle |
| 255 // this. Otherwise, it's just treated as plain text. |
| 256 if ((match.group(1) == null) || (match.group(1) == '')) { |
| 257 if (_implicitLinkResolver == null) return false; |
| 258 |
| 259 // Only allow implicit links if the content is just text. |
| 260 // TODO(rnystrom): Do we want to relax this? |
| 261 if (state.children.length != 1) return false; |
| 262 if (state.children[0] is! Text) return false; |
| 263 |
| 264 Text link = state.children[0]; |
| 265 |
| 266 // See if we have a resolver that will generate a link for us. |
| 267 final node = _implicitLinkResolver(link.text); |
| 268 if (node == null) return false; |
| 269 |
| 270 parser.addNode(node); |
| 271 return true; |
| 272 } |
| 273 |
| 274 if ((match.group(3) != null) && (match.group(3) != '')) { |
| 246 // Inline link like [foo](url). | 275 // Inline link like [foo](url). |
| 247 url = match.group(2); | 276 url = match.group(3); |
| 248 title = match.group(3); | 277 title = match.group(4); |
| 249 | 278 |
| 250 // For whatever reason, markdown allows angle-bracketed URLs here. | 279 // For whatever reason, markdown allows angle-bracketed URLs here. |
| 251 if (url.startsWith('<') && url.endsWith('>')) { | 280 if (url.startsWith('<') && url.endsWith('>')) { |
| 252 url = url.substring(1, url.length - 1); | 281 url = url.substring(1, url.length - 1); |
| 253 } | 282 } |
| 254 } else { | 283 } else { |
| 255 // Reference link like [foo] [bar]. | 284 // Reference link like [foo] [bar]. |
| 256 var id = match.group(1); | 285 var id = match.group(2); |
| 257 if (id == '') { | 286 if (id == '') { |
| 258 // The id is empty ("[]") so infer it from the contents. | 287 // The id is empty ("[]") so infer it from the contents. |
| 259 id = parser.source.substring(state.startPos + 1, parser.pos); | 288 id = parser.source.substring(state.startPos + 1, parser.pos); |
| 260 } | 289 } |
| 261 | 290 |
| 262 // Look up the link. | 291 // Look up the link. |
| 263 final link = parser.document.refLinks[id]; | 292 final link = parser.document.refLinks[id]; |
| 264 // If it's an unknown link just emit plaintext. | 293 // If it's an unknown link just emit plaintext. |
| 265 if (link == null) return false; | 294 if (link == null) return false; |
| 266 | 295 |
| (...skipping 22 matching lines...) Expand all Loading... |
| 289 return true; | 318 return true; |
| 290 } | 319 } |
| 291 } | 320 } |
| 292 | 321 |
| 293 /// Keeps track of a currently open tag while it is being parsed. The parser | 322 /// Keeps track of a currently open tag while it is being parsed. The parser |
| 294 /// maintains a stack of these so it can handle nested tags. | 323 /// maintains a stack of these so it can handle nested tags. |
| 295 class TagState { | 324 class TagState { |
| 296 /// The point in the original source where this tag started. | 325 /// The point in the original source where this tag started. |
| 297 int startPos; | 326 int startPos; |
| 298 | 327 |
| 328 /// The point in the original source where open tag ended. |
| 329 int endPos; |
| 330 |
| 299 /// The syntax that created this node. | 331 /// The syntax that created this node. |
| 300 final TagSyntax syntax; | 332 final TagSyntax syntax; |
| 301 | 333 |
| 302 /// The children of this node. Will be `null` for text nodes. | 334 /// The children of this node. Will be `null` for text nodes. |
| 303 final List<Node> children; | 335 final List<Node> children; |
| 304 | 336 |
| 305 TagState(this.startPos, this.syntax) | 337 TagState(this.startPos, this.endPos, this.syntax) |
| 306 : children = <Node>[]; | 338 : children = <Node>[]; |
| 307 | 339 |
| 308 /// Attempts to close this tag by matching the current text against its end | 340 /// Attempts to close this tag by matching the current text against its end |
| 309 /// pattern. | 341 /// pattern. |
| 310 bool tryMatch(InlineParser parser) { | 342 bool tryMatch(InlineParser parser) { |
| 311 Match endMatch = syntax.endPattern.firstMatch(parser.currentSource); | 343 Match endMatch = syntax.endPattern.firstMatch(parser.currentSource); |
| 312 if ((endMatch != null) && (endMatch.start() == 0)) { | 344 if ((endMatch != null) && (endMatch.start() == 0)) { |
| 313 // Close the tag. | 345 // Close the tag. |
| 314 close(parser, endMatch); | 346 close(parser, endMatch); |
| 315 return true; | 347 return true; |
| 316 } | 348 } |
| 317 | 349 |
| 318 return false; | 350 return false; |
| 319 } | 351 } |
| 320 | 352 |
| 321 /// Pops this tag off the stack, completes it, and adds it to the output. | 353 /// Pops this tag off the stack, completes it, and adds it to the output. |
| 322 /// Will discard any unmatched tags that happen to be above it on the stack. | 354 /// Will discard any unmatched tags that happen to be above it on the stack. |
| 323 /// If this is the last node in the stack, returns its children. | 355 /// If this is the last node in the stack, returns its children. |
| 324 List<Node> close(InlineParser parser, Match endMatch) { | 356 List<Node> close(InlineParser parser, Match endMatch) { |
| 325 // Found a match. If there is anything above this tag on the stack, | 357 // If there are unclosed tags on top of this one when it's closed, that |
| 326 // discard it. For example, given '*a _b*...' when we reach the second | 358 // means they are mismatched. Mismatched tags are treated as plain text in |
| 327 // '*', '_' will be on the top of the stack. It's mismatched, so we | 359 // markdown. So for each tag above this one, we write its start tag as text |
| 328 // just treat it as text. | 360 // and then adds its children to this one's children. |
| 329 while (parser._stack.last() != this) parser.discardUnmatchedTag(); | 361 int index = parser._stack.indexOf(this); |
| 362 |
| 363 // Remove the unmatched children. |
| 364 final unmatchedTags = parser._stack.getRange(index + 1, |
| 365 parser._stack.length - index - 1); |
| 366 parser._stack.removeRange(index + 1, parser._stack.length - index - 1); |
| 367 |
| 368 // Flatten them out onto this tag. |
| 369 for (final unmatched in unmatchedTags) { |
| 370 // Write the start tag as text. |
| 371 parser.writeTextRange(unmatched.startPos, unmatched.endPos); |
| 372 |
| 373 // Bequeath its children unto this tag. |
| 374 children.addAll(unmatched.children); |
| 375 } |
| 330 | 376 |
| 331 // Pop this off the stack. | 377 // Pop this off the stack. |
| 332 parser.writeText(); | 378 parser.writeText(); |
| 333 parser._stack.removeLast(); | 379 parser._stack.removeLast(); |
| 334 | 380 |
| 335 // If the stack is empty now, this is the special "results" node. | 381 // If the stack is empty now, this is the special "results" node. |
| 336 if (parser._stack.length == 0) return children; | 382 if (parser._stack.length == 0) return children; |
| 337 | 383 |
| 338 // We are still parsing, so add this to its parent's children. | 384 // We are still parsing, so add this to its parent's children. |
| 339 if (syntax.onMatchEnd(parser, endMatch, this)) { | 385 if (syntax.onMatchEnd(parser, endMatch, this)) { |
| 340 parser.consume(endMatch.group(0).length); | 386 parser.consume(endMatch.group(0).length); |
| 341 } else { | 387 } else { |
| 342 // Didn't close correctly so revert to text. | 388 // Didn't close correctly so revert to text. |
| 343 parser.start = startPos; | 389 parser.start = startPos; |
| 344 parser.advanceBy(endMatch.group(0).length); | 390 parser.advanceBy(endMatch.group(0).length); |
| 345 } | 391 } |
| 346 | 392 |
| 347 return null; | 393 return null; |
| 348 } | 394 } |
| 349 } | 395 } |
| OLD | NEW |