OLD | NEW |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, 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 library markdown.inline_parser; | 5 library markdown.src.inline_parser; |
6 | 6 |
7 import 'ast.dart'; | 7 import 'ast.dart'; |
8 import 'document.dart'; | 8 import 'document.dart'; |
9 import 'util.dart'; | 9 import 'util.dart'; |
10 | 10 |
11 /// Maintains the internal state needed to parse inline span elements in | 11 /// Maintains the internal state needed to parse inline span elements in |
12 /// markdown. | 12 /// markdown. |
13 class InlineParser { | 13 class InlineParser { |
14 static final List<InlineSyntax> _defaultSyntaxes = <InlineSyntax>[ | 14 static final List<InlineSyntax> _defaultSyntaxes = <InlineSyntax>[ |
15 // This first regexp matches plain text to accelerate parsing. It must | 15 // This first regexp matches plain text to accelerate parsing. It must |
16 // be written so that it does not match any prefix of any following | 16 // be written so that it does not match any prefix of any following |
17 // syntax. Most markdown is plain text, so it is faster to match one | 17 // syntax. Most markdown is plain text, so it is faster to match one |
18 // regexp per 'word' rather than fail to match all the following regexps | 18 // regexp per 'word' rather than fail to match all the following regexps |
19 // at each non-syntax character position. It is much more important | 19 // at each non-syntax character position. It is much more important |
20 // that the regexp is fast than complete (for example, adding grouping | 20 // that the regexp is fast than complete (for example, adding grouping |
21 // is likely to slow the regexp down enough to negate its benefit). | 21 // is likely to slow the regexp down enough to negate its benefit). |
22 // Since it is purely for optimization, it can be removed for debugging. | 22 // Since it is purely for optimization, it can be removed for debugging. |
23 | 23 |
24 // TODO(amouravski): this regex will glom up any custom syntaxes unless | 24 // TODO(amouravski): this regex will glom up any custom syntaxes unless |
25 // they're at the beginning. | 25 // they're at the beginning. |
26 new TextSyntax(r'\s*[A-Za-z0-9]+'), | 26 new TextSyntax(r'\s*[A-Za-z0-9]+'), |
27 | 27 |
28 // The real syntaxes. | 28 // The real syntaxes. |
29 | |
30 new AutolinkSyntax(), | 29 new AutolinkSyntax(), |
31 new LinkSyntax(), | 30 new LinkSyntax(), |
32 new ImageLinkSyntax(), | 31 new ImageLinkSyntax(), |
33 // "*" surrounded by spaces is left alone. | 32 // "*" surrounded by spaces is left alone. |
34 new TextSyntax(r' \* '), | 33 new TextSyntax(r' \* '), |
35 // "_" surrounded by spaces is left alone. | 34 // "_" surrounded by spaces is left alone. |
36 new TextSyntax(r' _ '), | 35 new TextSyntax(r' _ '), |
37 // Leave already-encoded HTML entities alone. Ensures we don't turn | 36 // Leave already-encoded HTML entities alone. Ensures we don't turn |
38 // "&" into "&amp;" | 37 // "&" into "&amp;" |
39 new TextSyntax(r'&[#a-zA-Z0-9]*;'), | 38 new TextSyntax(r'&[#a-zA-Z0-9]*;'), |
(...skipping 28 matching lines...) Expand all Loading... |
68 | 67 |
69 /// The current read position. | 68 /// The current read position. |
70 int pos = 0; | 69 int pos = 0; |
71 | 70 |
72 /// Starting position of the last unconsumed text. | 71 /// Starting position of the last unconsumed text. |
73 int start = 0; | 72 int start = 0; |
74 | 73 |
75 final List<TagState> _stack; | 74 final List<TagState> _stack; |
76 | 75 |
77 InlineParser(this.source, this.document) : _stack = <TagState>[] { | 76 InlineParser(this.source, this.document) : _stack = <TagState>[] { |
78 /// User specified syntaxes will be the first syntaxes to be evaluated. | 77 // User specified syntaxes are the first syntaxes to be evaluated. |
79 if (document.inlineSyntaxes != null) { | 78 if (document.inlineSyntaxes != null) { |
80 syntaxes.addAll(document.inlineSyntaxes); | 79 syntaxes.addAll(document.inlineSyntaxes); |
81 } | 80 } |
| 81 |
82 syntaxes.addAll(_defaultSyntaxes); | 82 syntaxes.addAll(_defaultSyntaxes); |
| 83 |
83 // Custom link resolvers goes after the generic text syntax. | 84 // Custom link resolvers goes after the generic text syntax. |
84 syntaxes.insertAll(1, <InlineSyntax>[ | 85 syntaxes.insertAll(1, [ |
85 new LinkSyntax(linkResolver: document.linkResolver), | 86 new LinkSyntax(linkResolver: document.linkResolver), |
86 new ImageLinkSyntax(linkResolver: document.imageLinkResolver) | 87 new ImageLinkSyntax(linkResolver: document.imageLinkResolver) |
87 ]); | 88 ]); |
88 } | 89 } |
89 | 90 |
90 List<Node> parse() { | 91 List<Node> parse() { |
91 // Make a fake top tag to hold the results. | 92 // Make a fake top tag to hold the results. |
92 _stack.add(new TagState(0, 0, null)); | 93 _stack.add(new TagState(0, 0, null)); |
93 | 94 |
94 while (!isDone) { | 95 while (!isDone) { |
95 bool matched = false; | 96 var matched = false; |
96 | 97 |
97 // See if any of the current tags on the stack match. We don't allow tags | 98 // See if any of the current tags on the stack match. We don't allow tags |
98 // of the same kind to nest, so this takes priority over other possible //
matches. | 99 // of the same kind to nest, so this takes priority over other possible |
99 for (int i = _stack.length - 1; i > 0; i--) { | 100 // matches. |
| 101 for (var i = _stack.length - 1; i > 0; i--) { |
100 if (_stack[i].tryMatch(this)) { | 102 if (_stack[i].tryMatch(this)) { |
101 matched = true; | 103 matched = true; |
102 break; | 104 break; |
103 } | 105 } |
104 } | 106 } |
| 107 |
105 if (matched) continue; | 108 if (matched) continue; |
106 | 109 |
107 // See if the current text matches any defined markdown syntax. | 110 // See if the current text matches any defined markdown syntax. |
108 for (final syntax in syntaxes) { | 111 for (var syntax in syntaxes) { |
109 if (syntax.tryMatch(this)) { | 112 if (syntax.tryMatch(this)) { |
110 matched = true; | 113 matched = true; |
111 break; | 114 break; |
112 } | 115 } |
113 } | 116 } |
| 117 |
114 if (matched) continue; | 118 if (matched) continue; |
115 | 119 |
116 // If we got here, it's just text. | 120 // If we got here, it's just text. |
117 advanceBy(1); | 121 advanceBy(1); |
118 } | 122 } |
119 | 123 |
120 // Unwind any unmatched tags and get the results. | 124 // Unwind any unmatched tags and get the results. |
121 return _stack[0].close(this, null); | 125 return _stack[0].close(this, null); |
122 } | 126 } |
123 | 127 |
124 void writeText() { | 128 void writeText() { |
125 writeTextRange(start, pos); | 129 writeTextRange(start, pos); |
126 start = pos; | 130 start = pos; |
127 } | 131 } |
128 | 132 |
129 void writeTextRange(int start, int end) { | 133 void writeTextRange(int start, int end) { |
130 if (end > start) { | 134 if (end <= start) return; |
131 final text = source.substring(start, end); | |
132 final nodes = _stack.last.children; | |
133 | 135 |
134 // If the previous node is text too, just append. | 136 var text = source.substring(start, end); |
135 if ((nodes.length > 0) && (nodes.last is Text)) { | 137 var nodes = _stack.last.children; |
136 final newNode = new Text('${nodes.last.text}$text'); | 138 |
137 nodes[nodes.length - 1] = newNode; | 139 // If the previous node is text too, just append. |
138 } else { | 140 if (nodes.length > 0 && nodes.last is Text) { |
139 nodes.add(new Text(text)); | 141 nodes[nodes.length - 1] = new Text('${nodes.last.text}$text'); |
140 } | 142 } else { |
| 143 nodes.add(new Text(text)); |
141 } | 144 } |
142 } | 145 } |
143 | 146 |
144 void addNode(Node node) { | 147 void addNode(Node node) { |
145 _stack.last.children.add(node); | 148 _stack.last.children.add(node); |
146 } | 149 } |
147 | 150 |
148 // TODO(rnystrom): Only need this because RegExp doesn't let you start | 151 // TODO(rnystrom): Only need this because RegExp doesn't let you start |
149 // searching from a given offset. | 152 // searching from a given offset. |
| 153 @deprecated |
150 String get currentSource => source.substring(pos, source.length); | 154 String get currentSource => source.substring(pos, source.length); |
151 | 155 |
152 bool get isDone => pos == source.length; | 156 bool get isDone => pos == source.length; |
153 | 157 |
154 void advanceBy(int length) { | 158 void advanceBy(int length) { |
155 pos += length; | 159 pos += length; |
156 } | 160 } |
157 | 161 |
158 void consume(int length) { | 162 void consume(int length) { |
159 pos += length; | 163 pos += length; |
160 start = pos; | 164 start = pos; |
161 } | 165 } |
162 } | 166 } |
163 | 167 |
164 /// Represents one kind of markdown tag that can be parsed. | 168 /// Represents one kind of markdown tag that can be parsed. |
165 abstract class InlineSyntax { | 169 abstract class InlineSyntax { |
166 final RegExp pattern; | 170 final RegExp pattern; |
167 | 171 |
168 InlineSyntax(String pattern) : pattern = new RegExp(pattern, multiLine: true); | 172 InlineSyntax(String pattern) : pattern = new RegExp(pattern, multiLine: true); |
169 | 173 |
170 bool tryMatch(InlineParser parser) { | 174 bool tryMatch(InlineParser parser) { |
171 final startMatch = pattern.firstMatch(parser.currentSource); | 175 var startMatch = pattern.matchAsPrefix(parser.source, parser.pos); |
172 if ((startMatch != null) && (startMatch.start == 0)) { | 176 if (startMatch != null) { |
173 // Write any existing plain text up to this point. | 177 // Write any existing plain text up to this point. |
174 parser.writeText(); | 178 parser.writeText(); |
175 | 179 |
176 if (onMatch(parser, startMatch)) { | 180 if (onMatch(parser, startMatch)) parser.consume(startMatch[0].length); |
177 parser.consume(startMatch[0].length); | |
178 } | |
179 return true; | 181 return true; |
180 } | 182 } |
| 183 |
181 return false; | 184 return false; |
182 } | 185 } |
183 | 186 |
184 bool onMatch(InlineParser parser, Match match); | 187 bool onMatch(InlineParser parser, Match match); |
185 } | 188 } |
186 | 189 |
187 /// Matches stuff that should just be passed through as straight text. | 190 /// Matches stuff that should just be passed through as straight text. |
188 class TextSyntax extends InlineSyntax { | 191 class TextSyntax extends InlineSyntax { |
189 final String substitute; | 192 final String substitute; |
| 193 |
190 TextSyntax(String pattern, {String sub}) | 194 TextSyntax(String pattern, {String sub}) |
191 : super(pattern), | 195 : super(pattern), |
192 substitute = sub; | 196 substitute = sub; |
193 | 197 |
194 bool onMatch(InlineParser parser, Match match) { | 198 bool onMatch(InlineParser parser, Match match) { |
195 if (substitute == null) { | 199 if (substitute == null) { |
196 // Just use the original matched text. | 200 // Just use the original matched text. |
197 parser.advanceBy(match[0].length); | 201 parser.advanceBy(match[0].length); |
198 return false; | 202 return false; |
199 } | 203 } |
200 | 204 |
201 // Insert the substitution. | 205 // Insert the substitution. |
202 parser.addNode(new Text(substitute)); | 206 parser.addNode(new Text(substitute)); |
203 return true; | 207 return true; |
204 } | 208 } |
205 } | 209 } |
206 | 210 |
207 /// Matches autolinks like `<http://foo.com>`. | 211 /// Matches autolinks like `<http://foo.com>`. |
208 class AutolinkSyntax extends InlineSyntax { | 212 class AutolinkSyntax extends InlineSyntax { |
209 AutolinkSyntax() : super(r'<((http|https|ftp)://[^>]*)>'); | 213 AutolinkSyntax() : super(r'<((http|https|ftp)://[^>]*)>'); |
210 // TODO(rnystrom): Make case insensitive. | 214 // TODO(rnystrom): Make case insensitive. |
211 | 215 |
212 bool onMatch(InlineParser parser, Match match) { | 216 bool onMatch(InlineParser parser, Match match) { |
213 final url = match[1]; | 217 var url = match[1]; |
214 | 218 var anchor = new Element.text('a', escapeHtml(url)); |
215 final anchor = new Element.text('a', escapeHtml(url)) | 219 anchor.attributes['href'] = url; |
216 ..attributes['href'] = url; | |
217 parser.addNode(anchor); | 220 parser.addNode(anchor); |
218 | 221 |
219 return true; | 222 return true; |
220 } | 223 } |
221 } | 224 } |
222 | 225 |
223 /// Matches syntax that has a pair of tags and becomes an element, like `*` for | 226 /// Matches syntax that has a pair of tags and becomes an element, like `*` for |
224 /// `<em>`. Allows nested tags. | 227 /// `<em>`. Allows nested tags. |
225 class TagSyntax extends InlineSyntax { | 228 class TagSyntax extends InlineSyntax { |
226 final RegExp endPattern; | 229 final RegExp endPattern; |
227 final String tag; | 230 final String tag; |
228 | 231 |
229 TagSyntax(String pattern, {String tag, String end}) | 232 TagSyntax(String pattern, {this.tag, String end}) |
230 : super(pattern), | 233 : super(pattern), |
231 endPattern = new RegExp((end != null) ? end : pattern, multiLine: true), | 234 endPattern = new RegExp((end != null) ? end : pattern, multiLine: true); |
232 tag = tag; | |
233 // TODO(rnystrom): Doing this.field doesn't seem to work with named args. | |
234 | 235 |
235 bool onMatch(InlineParser parser, Match match) { | 236 bool onMatch(InlineParser parser, Match match) { |
236 parser._stack.add( | 237 parser._stack |
237 new TagState(parser.pos, parser.pos + match[0].length, this)); | 238 .add(new TagState(parser.pos, parser.pos + match[0].length, this)); |
238 return true; | 239 return true; |
239 } | 240 } |
240 | 241 |
241 bool onMatchEnd(InlineParser parser, Match match, TagState state) { | 242 bool onMatchEnd(InlineParser parser, Match match, TagState state) { |
242 parser.addNode(new Element(tag, state.children)); | 243 parser.addNode(new Element(tag, state.children)); |
243 return true; | 244 return true; |
244 } | 245 } |
245 } | 246 } |
246 | 247 |
247 /// Matches inline links like `[blah] [id]` and `[blah] (url)`. | 248 /// Matches inline links like `[blah] [id]` and `[blah] (url)`. |
248 class LinkSyntax extends TagSyntax { | 249 class LinkSyntax extends TagSyntax { |
249 final Resolver linkResolver; | 250 final Resolver linkResolver; |
250 | 251 |
251 /// Weather or not this link was resolved by a [Resolver] | 252 /// Weather or not this link was resolved by a [Resolver] |
252 bool resolved = false; | 253 bool resolved = false; |
253 | 254 |
254 /// The regex for the end of a link needs to handle both reference style and | 255 /// The regex for the end of a link needs to handle both reference style and |
255 /// inline styles as well as optional titles for inline links. To make that | 256 /// inline styles as well as optional titles for inline links. To make that |
256 /// a bit more palatable, this breaks it into pieces. | 257 /// a bit more palatable, this breaks it into pieces. |
257 static get linkPattern { | 258 static get linkPattern { |
258 final refLink = r'\s?\[([^\]]*)\]'; // "[id]" reflink id. | 259 var refLink = r'\s?\[([^\]]*)\]'; // "[id]" reflink id. |
259 final title = r'(?:[ ]*"([^"]+)"|)'; // Optional title in quotes. | 260 var title = r'(?:[ ]*"([^"]+)"|)'; // Optional title in quotes. |
260 final inlineLink = '\\s?\\(([^ )]+)$title\\)'; // "(url "title")" link. | 261 var inlineLink = '\\s?\\(([^ )]+)$title\\)'; // "(url "title")" link. |
261 return '\](?:($refLink|$inlineLink)|)'; | 262 return '\](?:($refLink|$inlineLink)|)'; |
262 | 263 |
263 // The groups matched by this are: | 264 // The groups matched by this are: |
264 // 1: Will be non-empty if it's either a ref or inline link. Will be empty | 265 // 1: Will be non-empty if it's either a ref or inline link. Will be empty |
265 // if it's just a bare pair of square brackets with nothing after them. | 266 // if it's just a bare pair of square brackets with nothing after them. |
266 // 2: Contains the id inside [] for a reference-style link. | 267 // 2: Contains the id inside [] for a reference-style link. |
267 // 3: Contains the URL for an inline link. | 268 // 3: Contains the URL for an inline link. |
268 // 4: Contains the title, if present, for an inline link. | 269 // 4: Contains the title, if present, for an inline link. |
269 } | 270 } |
270 | 271 |
271 LinkSyntax({this.linkResolver, String pattern: r'\['}) | 272 LinkSyntax({this.linkResolver, String pattern: r'\['}) |
272 : super(pattern, end: linkPattern); | 273 : super(pattern, end: linkPattern); |
273 | 274 |
274 Node createNode(InlineParser parser, Match match, TagState state) { | 275 Node createNode(InlineParser parser, Match match, TagState state) { |
275 // If we didn't match refLink or inlineLink, then it means there was | 276 // If we didn't match refLink or inlineLink, then it means there was |
276 // nothing after the first square bracket, so it isn't a normal markdown | 277 // nothing after the first square bracket, so it isn't a normal markdown |
277 // link at all. Instead, we allow users of the library to specify a special | 278 // link at all. Instead, we allow users of the library to specify a special |
278 // resolver function ([linkResolver]) that may choose to handle | 279 // resolver function ([linkResolver]) that may choose to handle |
279 // this. Otherwise, it's just treated as plain text. | 280 // this. Otherwise, it's just treated as plain text. |
280 if (isNullOrEmpty(match[1])) { | 281 if (match[1] == null) { |
281 if (linkResolver == null) return null; | 282 if (linkResolver == null) return null; |
282 | 283 |
283 // Treat the contents as unparsed text even if they happen to match. This | 284 // Treat the contents as unparsed text even if they happen to match. This |
284 // way, we can handle things like [LINK_WITH_UNDERSCORES] as a link and | 285 // way, we can handle things like [LINK_WITH_UNDERSCORES] as a link and |
285 // not get confused by the emphasis. | 286 // not get confused by the emphasis. |
286 var textToResolve = parser.source.substring(state.endPos, parser.pos); | 287 var textToResolve = parser.source.substring(state.endPos, parser.pos); |
287 | 288 |
288 // See if we have a resolver that will generate a link for us. | 289 // See if we have a resolver that will generate a link for us. |
289 resolved = true; | 290 resolved = true; |
290 return linkResolver(textToResolve); | 291 return linkResolver(textToResolve); |
291 } else { | 292 } else { |
292 Link link = getLink(parser, match, state); | 293 var link = getLink(parser, match, state); |
293 if (link == null) return null; | 294 if (link == null) return null; |
294 | 295 |
295 final Element node = new Element('a', state.children) | 296 var node = new Element('a', state.children); |
296 ..attributes["href"] = escapeHtml(link.url) | |
297 ..attributes['title'] = escapeHtml(link.title); | |
298 | 297 |
299 cleanMap(node.attributes); | 298 node.attributes["href"] = escapeHtml(link.url); |
| 299 if (link.title != null) node.attributes['title'] = escapeHtml(link.title); |
| 300 |
300 return node; | 301 return node; |
301 } | 302 } |
302 } | 303 } |
303 | 304 |
304 Link getLink(InlineParser parser, Match match, TagState state) { | 305 Link getLink(InlineParser parser, Match match, TagState state) { |
305 if ((match[3] != null) && (match[3] != '')) { | 306 if (match[3] != null && match[3] != '') { |
306 // Inline link like [foo](url). | 307 // Inline link like [foo](url). |
307 var url = match[3]; | 308 var url = match[3]; |
308 var title = match[4]; | 309 var title = match[4]; |
309 | 310 |
310 // For whatever reason, markdown allows angle-bracketed URLs here. | 311 // For whatever reason, markdown allows angle-bracketed URLs here. |
311 if (url.startsWith('<') && url.endsWith('>')) { | 312 if (url.startsWith('<') && url.endsWith('>')) { |
312 url = url.substring(1, url.length - 1); | 313 url = url.substring(1, url.length - 1); |
313 } | 314 } |
314 | 315 |
315 return new Link(null, url, title); | 316 return new Link(null, url, title); |
316 } else { | 317 } else { |
317 var id; | 318 var id; |
318 // Reference link like [foo] [bar]. | 319 // Reference link like [foo] [bar]. |
319 if (match[2] == '') | 320 if (match[2] == '') { |
320 // The id is empty ("[]") so infer it from the contents. | 321 // The id is empty ("[]") so infer it from the contents. |
321 id = parser.source.substring(state.startPos + 1, parser.pos); | 322 id = parser.source.substring(state.startPos + 1, parser.pos); |
322 else id = match[2]; | 323 } else { |
| 324 id = match[2]; |
| 325 } |
323 | 326 |
324 // References are case-insensitive. | 327 // References are case-insensitive. |
325 id = id.toLowerCase(); | 328 id = id.toLowerCase(); |
326 return parser.document.refLinks[id]; | 329 return parser.document.refLinks[id]; |
327 } | 330 } |
328 } | 331 } |
329 | 332 |
330 bool onMatchEnd(InlineParser parser, Match match, TagState state) { | 333 bool onMatchEnd(InlineParser parser, Match match, TagState state) { |
331 Node node = createNode(parser, match, state); | 334 var node = createNode(parser, match, state); |
332 if (node == null) return false; | 335 if (node == null) return false; |
| 336 |
333 parser.addNode(node); | 337 parser.addNode(node); |
334 return true; | 338 return true; |
335 } | 339 } |
336 } | 340 } |
337 | 341 |
338 /// Matches images like `![alternate text](url "optional title")` and | 342 /// Matches images like `![alternate text](url "optional title")` and |
339 /// `![alternate text][url reference]`. | 343 /// `![alternate text][url reference]`. |
340 class ImageLinkSyntax extends LinkSyntax { | 344 class ImageLinkSyntax extends LinkSyntax { |
341 final Resolver linkResolver; | 345 final Resolver linkResolver; |
| 346 |
342 ImageLinkSyntax({this.linkResolver}) : super(pattern: r'!\['); | 347 ImageLinkSyntax({this.linkResolver}) : super(pattern: r'!\['); |
343 | 348 |
344 Node createNode(InlineParser parser, Match match, TagState state) { | 349 Node createNode(InlineParser parser, Match match, TagState state) { |
345 var node = super.createNode(parser, match, state); | 350 var node = super.createNode(parser, match, state); |
| 351 |
346 if (resolved) return node; | 352 if (resolved) return node; |
347 if (node == null) return null; | 353 if (node == null) return null; |
348 | 354 |
349 final Element imageElement = new Element.withTag("img") | 355 var imageElement = new Element.withTag("img"); |
350 ..attributes["src"] = node.attributes["href"] | 356 imageElement.attributes["src"] = node.attributes["href"]; |
351 ..attributes["title"] = node.attributes["title"] | |
352 ..attributes["alt"] = node.children | |
353 .map((e) => isNullOrEmpty(e) || e is! Text ? '' : e.text) | |
354 .join(' '); | |
355 | 357 |
356 cleanMap(imageElement.attributes); | 358 if (node.attributes.containsKey("title")) { |
| 359 imageElement.attributes["title"] = node.attributes["title"]; |
| 360 } |
| 361 |
| 362 var alt = node.children.map((e) => e is! Text ? '' : e.text).join(" "); |
| 363 if (alt != "") imageElement.attributes["alt"] = alt; |
357 | 364 |
358 node.children | 365 node.children |
359 ..clear() | 366 ..clear() |
360 ..add(imageElement); | 367 ..add(imageElement); |
361 | 368 |
362 return node; | 369 return node; |
363 } | 370 } |
364 } | 371 } |
365 | 372 |
366 /// Matches backtick-enclosed inline code blocks. | 373 /// Matches backtick-enclosed inline code blocks. |
(...skipping 19 matching lines...) Expand all Loading... |
386 final TagSyntax syntax; | 393 final TagSyntax syntax; |
387 | 394 |
388 /// The children of this node. Will be `null` for text nodes. | 395 /// The children of this node. Will be `null` for text nodes. |
389 final List<Node> children; | 396 final List<Node> children; |
390 | 397 |
391 TagState(this.startPos, this.endPos, this.syntax) : children = <Node>[]; | 398 TagState(this.startPos, this.endPos, this.syntax) : children = <Node>[]; |
392 | 399 |
393 /// Attempts to close this tag by matching the current text against its end | 400 /// Attempts to close this tag by matching the current text against its end |
394 /// pattern. | 401 /// pattern. |
395 bool tryMatch(InlineParser parser) { | 402 bool tryMatch(InlineParser parser) { |
396 Match endMatch = syntax.endPattern.firstMatch(parser.currentSource); | 403 var endMatch = syntax.endPattern.matchAsPrefix(parser.source, parser.pos); |
397 if ((endMatch != null) && (endMatch.start == 0)) { | 404 if (endMatch != null) { |
398 // Close the tag. | 405 // Close the tag. |
399 close(parser, endMatch); | 406 close(parser, endMatch); |
400 return true; | 407 return true; |
401 } | 408 } |
402 | 409 |
403 return false; | 410 return false; |
404 } | 411 } |
405 | 412 |
406 /// Pops this tag off the stack, completes it, and adds it to the output. | 413 /// Pops this tag off the stack, completes it, and adds it to the output. |
407 /// Will discard any unmatched tags that happen to be above it on the stack. | 414 /// Will discard any unmatched tags that happen to be above it on the stack. |
408 /// If this is the last node in the stack, returns its children. | 415 /// If this is the last node in the stack, returns its children. |
409 List<Node> close(InlineParser parser, Match endMatch) { | 416 List<Node> close(InlineParser parser, Match endMatch) { |
410 // If there are unclosed tags on top of this one when it's closed, that | 417 // If there are unclosed tags on top of this one when it's closed, that |
411 // means they are mismatched. Mismatched tags are treated as plain text in | 418 // means they are mismatched. Mismatched tags are treated as plain text in |
412 // markdown. So for each tag above this one, we write its start tag as text | 419 // markdown. So for each tag above this one, we write its start tag as text |
413 // and then adds its children to this one's children. | 420 // and then adds its children to this one's children. |
414 int index = parser._stack.indexOf(this); | 421 var index = parser._stack.indexOf(this); |
415 | 422 |
416 // Remove the unmatched children. | 423 // Remove the unmatched children. |
417 final unmatchedTags = parser._stack.sublist(index + 1); | 424 var unmatchedTags = parser._stack.sublist(index + 1); |
418 parser._stack.removeRange(index + 1, parser._stack.length); | 425 parser._stack.removeRange(index + 1, parser._stack.length); |
419 | 426 |
420 // Flatten them out onto this tag. | 427 // Flatten them out onto this tag. |
421 for (final unmatched in unmatchedTags) { | 428 for (var unmatched in unmatchedTags) { |
422 // Write the start tag as text. | 429 // Write the start tag as text. |
423 parser.writeTextRange(unmatched.startPos, unmatched.endPos); | 430 parser.writeTextRange(unmatched.startPos, unmatched.endPos); |
424 | 431 |
425 // Bequeath its children unto this tag. | 432 // Bequeath its children unto this tag. |
426 children.addAll(unmatched.children); | 433 children.addAll(unmatched.children); |
427 } | 434 } |
428 | 435 |
429 // Pop this off the stack. | 436 // Pop this off the stack. |
430 parser.writeText(); | 437 parser.writeText(); |
431 parser._stack.removeLast(); | 438 parser._stack.removeLast(); |
432 | 439 |
433 // If the stack is empty now, this is the special "results" node. | 440 // If the stack is empty now, this is the special "results" node. |
434 if (parser._stack.length == 0) return children; | 441 if (parser._stack.length == 0) return children; |
435 | 442 |
436 // We are still parsing, so add this to its parent's children. | 443 // We are still parsing, so add this to its parent's children. |
437 if (syntax.onMatchEnd(parser, endMatch, this)) { | 444 if (syntax.onMatchEnd(parser, endMatch, this)) { |
438 parser.consume(endMatch[0].length); | 445 parser.consume(endMatch[0].length); |
439 } else { | 446 } else { |
440 // Didn't close correctly so revert to text. | 447 // Didn't close correctly so revert to text. |
441 parser.start = startPos; | 448 parser.start = startPos; |
442 parser.advanceBy(endMatch[0].length); | 449 parser.advanceBy(endMatch[0].length); |
443 } | 450 } |
444 | 451 |
445 return null; | 452 return null; |
446 } | 453 } |
447 } | 454 } |
OLD | NEW |