| 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.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 List<InlineSyntax> defaultSyntaxes = <InlineSyntax>[ | 14 static 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 |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 57 new CodeSyntax(r'`([^`]*)`') | 57 new CodeSyntax(r'`([^`]*)`') |
| 58 // We will add the LinkSyntax once we know about the specific link resolver. | 58 // We will add the LinkSyntax once we know about the specific link resolver. |
| 59 ]; | 59 ]; |
| 60 | 60 |
| 61 /// The string of markdown being parsed. | 61 /// The string of markdown being parsed. |
| 62 final String source; | 62 final String source; |
| 63 | 63 |
| 64 /// The markdown document this parser is parsing. | 64 /// The markdown document this parser is parsing. |
| 65 final Document document; | 65 final Document document; |
| 66 | 66 |
| 67 List<InlineSyntax> syntaxes; | 67 final List<InlineSyntax> syntaxes = <InlineSyntax>[]; |
| 68 | 68 |
| 69 /// The current read position. | 69 /// The current read position. |
| 70 int pos = 0; | 70 int pos = 0; |
| 71 | 71 |
| 72 /// Starting position of the last unconsumed text. | 72 /// Starting position of the last unconsumed text. |
| 73 int start = 0; | 73 int start = 0; |
| 74 | 74 |
| 75 final List<TagState> _stack; | 75 final List<TagState> _stack; |
| 76 | 76 |
| 77 InlineParser(this.source, this.document) | 77 InlineParser(this.source, this.document) |
| 78 : _stack = <TagState>[] { | 78 : _stack = <TagState>[] { |
| 79 /// User specified syntaxes will be the first syntaxes to be evaluated. | 79 /// User specified syntaxes will be the first syntaxes to be evaluated. |
| 80 if (document.inlineSyntaxes != null) { | 80 if (document.inlineSyntaxes != null) { |
| 81 syntaxes = []; | |
| 82 syntaxes.addAll(document.inlineSyntaxes); | 81 syntaxes.addAll(document.inlineSyntaxes); |
| 83 syntaxes.addAll(defaultSyntaxes); | |
| 84 } else { | |
| 85 syntaxes = defaultSyntaxes; | |
| 86 } | 82 } |
| 83 syntaxes.addAll(_defaultSyntaxes); |
| 87 // Custom link resolvers goes after the generic text syntax. | 84 // Custom link resolvers goes after the generic text syntax. |
| 88 syntaxes.insertAll(1, [ | 85 syntaxes.insertAll(1, [ |
| 89 new LinkSyntax(linkResolver: document.linkResolver), | 86 new LinkSyntax(linkResolver: document.linkResolver), |
| 90 new ImageLinkSyntax(linkResolver: document.linkResolver) | 87 new ImageLinkSyntax(linkResolver: document.linkResolver) |
| 91 ]); | 88 ]); |
| 92 } | 89 } |
| 93 | 90 |
| 94 List<Node> parse() { | 91 List<Node> parse() { |
| 95 // Make a fake top tag to hold the results. | 92 // Make a fake top tag to hold the results. |
| 96 _stack.add(new TagState(0, 0, null)); | 93 _stack.add(new TagState(0, 0, null)); |
| (...skipping 21 matching lines...) Expand all Loading... |
| 118 if (matched) continue; | 115 if (matched) continue; |
| 119 | 116 |
| 120 // If we got here, it's just text. | 117 // If we got here, it's just text. |
| 121 advanceBy(1); | 118 advanceBy(1); |
| 122 } | 119 } |
| 123 | 120 |
| 124 // Unwind any unmatched tags and get the results. | 121 // Unwind any unmatched tags and get the results. |
| 125 return _stack[0].close(this, null); | 122 return _stack[0].close(this, null); |
| 126 } | 123 } |
| 127 | 124 |
| 128 writeText() { | 125 void writeText() { |
| 129 writeTextRange(start, pos); | 126 writeTextRange(start, pos); |
| 130 start = pos; | 127 start = pos; |
| 131 } | 128 } |
| 132 | 129 |
| 133 writeTextRange(int start, int end) { | 130 void writeTextRange(int start, int end) { |
| 134 if (end > start) { | 131 if (end > start) { |
| 135 final text = source.substring(start, end); | 132 final text = source.substring(start, end); |
| 136 final nodes = _stack.last.children; | 133 final nodes = _stack.last.children; |
| 137 | 134 |
| 138 // If the previous node is text too, just append. | 135 // If the previous node is text too, just append. |
| 139 if ((nodes.length > 0) && (nodes.last is Text)) { | 136 if ((nodes.length > 0) && (nodes.last is Text)) { |
| 140 final newNode = new Text('${nodes.last.text}$text'); | 137 final newNode = new Text('${nodes.last.text}$text'); |
| 141 nodes[nodes.length - 1] = newNode; | 138 nodes[nodes.length - 1] = newNode; |
| 142 } else { | 139 } else { |
| 143 nodes.add(new Text(text)); | 140 nodes.add(new Text(text)); |
| 144 } | 141 } |
| 145 } | 142 } |
| 146 } | 143 } |
| 147 | 144 |
| 148 addNode(Node node) { | 145 void addNode(Node node) { |
| 149 _stack.last.children.add(node); | 146 _stack.last.children.add(node); |
| 150 } | 147 } |
| 151 | 148 |
| 152 // TODO(rnystrom): Only need this because RegExp doesn't let you start | 149 // TODO(rnystrom): Only need this because RegExp doesn't let you start |
| 153 // searching from a given offset. | 150 // searching from a given offset. |
| 154 String get currentSource => source.substring(pos, source.length); | 151 String get currentSource => source.substring(pos, source.length); |
| 155 | 152 |
| 156 bool get isDone => pos == source.length; | 153 bool get isDone => pos == source.length; |
| 157 | 154 |
| 158 void advanceBy(int length) { | 155 void advanceBy(int length) { |
| (...skipping 25 matching lines...) Expand all Loading... |
| 184 return true; | 181 return true; |
| 185 } | 182 } |
| 186 return false; | 183 return false; |
| 187 } | 184 } |
| 188 | 185 |
| 189 bool onMatch(InlineParser parser, Match match); | 186 bool onMatch(InlineParser parser, Match match); |
| 190 } | 187 } |
| 191 | 188 |
| 192 /// Matches stuff that should just be passed through as straight text. | 189 /// Matches stuff that should just be passed through as straight text. |
| 193 class TextSyntax extends InlineSyntax { | 190 class TextSyntax extends InlineSyntax { |
| 194 String substitute; | 191 final String substitute; |
| 195 TextSyntax(String pattern, {String sub}) | 192 TextSyntax(String pattern, {String sub}) |
| 196 : super(pattern), | 193 : super(pattern), |
| 197 substitute = sub; | 194 substitute = sub; |
| 198 | 195 |
| 199 bool onMatch(InlineParser parser, Match match) { | 196 bool onMatch(InlineParser parser, Match match) { |
| 200 if (substitute == null) { | 197 if (substitute == null) { |
| 201 // Just use the original matched text. | 198 // Just use the original matched text. |
| 202 parser.advanceBy(match[0].length); | 199 parser.advanceBy(match[0].length); |
| 203 return false; | 200 return false; |
| 204 } | 201 } |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 245 } | 242 } |
| 246 | 243 |
| 247 bool onMatchEnd(InlineParser parser, Match match, TagState state) { | 244 bool onMatchEnd(InlineParser parser, Match match, TagState state) { |
| 248 parser.addNode(new Element(tag, state.children)); | 245 parser.addNode(new Element(tag, state.children)); |
| 249 return true; | 246 return true; |
| 250 } | 247 } |
| 251 } | 248 } |
| 252 | 249 |
| 253 /// Matches inline links like `[blah] [id]` and `[blah] (url)`. | 250 /// Matches inline links like `[blah] [id]` and `[blah] (url)`. |
| 254 class LinkSyntax extends TagSyntax { | 251 class LinkSyntax extends TagSyntax { |
| 255 Resolver linkResolver; | 252 final Resolver linkResolver; |
| 256 | 253 |
| 257 /// The regex for the end of a link needs to handle both reference style and | 254 /// The regex for the end of a link needs to handle both reference style and |
| 258 /// inline styles as well as optional titles for inline links. To make that | 255 /// inline styles as well as optional titles for inline links. To make that |
| 259 /// a bit more palatable, this breaks it into pieces. | 256 /// a bit more palatable, this breaks it into pieces. |
| 260 static get linkPattern { | 257 static get linkPattern { |
| 261 final refLink = r'\s?\[([^\]]*)\]'; // "[id]" reflink id. | 258 final refLink = r'\s?\[([^\]]*)\]'; // "[id]" reflink id. |
| 262 final title = r'(?:[ ]*"([^"]+)"|)'; // Optional title in quotes. | 259 final title = r'(?:[ ]*"([^"]+)"|)'; // Optional title in quotes. |
| 263 final inlineLink = '\\s?\\(([^ )]+)$title\\)'; // "(url "title")" link. | 260 final inlineLink = '\\s?\\(([^ )]+)$title\\)'; // "(url "title")" link. |
| 264 return '\](?:($refLink|$inlineLink)|)'; | 261 return '\](?:($refLink|$inlineLink)|)'; |
| 265 | 262 |
| (...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 337 Node node = createNode(parser, match, state); | 334 Node node = createNode(parser, match, state); |
| 338 if (node == null) return false; | 335 if (node == null) return false; |
| 339 parser.addNode(node); | 336 parser.addNode(node); |
| 340 return true; | 337 return true; |
| 341 } | 338 } |
| 342 } | 339 } |
| 343 | 340 |
| 344 /// Matches images like `` and | 341 /// Matches images like `` and |
| 345 /// `![alternate text][url reference]`. | 342 /// `![alternate text][url reference]`. |
| 346 class ImageLinkSyntax extends LinkSyntax { | 343 class ImageLinkSyntax extends LinkSyntax { |
| 347 Resolver linkResolver; | 344 final Resolver linkResolver; |
| 348 ImageLinkSyntax({this.linkResolver}) | 345 ImageLinkSyntax({this.linkResolver}) |
| 349 : super(pattern: r'!\['); | 346 : super(pattern: r'!\['); |
| 350 | 347 |
| 351 Node createNode(InlineParser parser, Match match, TagState state) { | 348 Element createNode(InlineParser parser, Match match, TagState state) { |
| 352 Node node = super.createNode(parser, match, state); | 349 Element node = super.createNode(parser, match, state); |
| 353 if (node == null) return null; | 350 if (node == null) return null; |
| 354 | 351 |
| 355 final Element imageElement = new Element.withTag("img") | 352 final Element imageElement = new Element.withTag("img") |
| 356 ..attributes["src"] = node.attributes["href"] | 353 ..attributes["src"] = node.attributes["href"] |
| 357 ..attributes["title"] = node.attributes["title"] | 354 ..attributes["title"] = node.attributes["title"] |
| 358 ..attributes["alt"] = node.children | 355 ..attributes["alt"] = node.children |
| 359 .map((e) => isNullOrEmpty(e) || e is! Text ? '' : e.text) | 356 .map((e) => isNullOrEmpty(e) || e is! Text ? '' : e.text) |
| 360 .join(' '); | 357 .join(' '); |
| 361 | 358 |
| 362 cleanMap(imageElement.attributes); | 359 cleanMap(imageElement.attributes); |
| (...skipping 15 matching lines...) Expand all Loading... |
| 378 bool onMatch(InlineParser parser, Match match) { | 375 bool onMatch(InlineParser parser, Match match) { |
| 379 parser.addNode(new Element.text('code', escapeHtml(match[1]))); | 376 parser.addNode(new Element.text('code', escapeHtml(match[1]))); |
| 380 return true; | 377 return true; |
| 381 } | 378 } |
| 382 } | 379 } |
| 383 | 380 |
| 384 /// Keeps track of a currently open tag while it is being parsed. The parser | 381 /// Keeps track of a currently open tag while it is being parsed. The parser |
| 385 /// maintains a stack of these so it can handle nested tags. | 382 /// maintains a stack of these so it can handle nested tags. |
| 386 class TagState { | 383 class TagState { |
| 387 /// The point in the original source where this tag started. | 384 /// The point in the original source where this tag started. |
| 388 int startPos; | 385 final int startPos; |
| 389 | 386 |
| 390 /// The point in the original source where open tag ended. | 387 /// The point in the original source where open tag ended. |
| 391 int endPos; | 388 final int endPos; |
| 392 | 389 |
| 393 /// The syntax that created this node. | 390 /// The syntax that created this node. |
| 394 final TagSyntax syntax; | 391 final TagSyntax syntax; |
| 395 | 392 |
| 396 /// The children of this node. Will be `null` for text nodes. | 393 /// The children of this node. Will be `null` for text nodes. |
| 397 final List<Node> children; | 394 final List<Node> children; |
| 398 | 395 |
| 399 TagState(this.startPos, this.endPos, this.syntax) | 396 TagState(this.startPos, this.endPos, this.syntax) |
| 400 : children = <Node>[]; | 397 : children = <Node>[]; |
| 401 | 398 |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 447 parser.consume(endMatch[0].length); | 444 parser.consume(endMatch[0].length); |
| 448 } else { | 445 } else { |
| 449 // Didn't close correctly so revert to text. | 446 // Didn't close correctly so revert to text. |
| 450 parser.start = startPos; | 447 parser.start = startPos; |
| 451 parser.advanceBy(endMatch[0].length); | 448 parser.advanceBy(endMatch[0].length); |
| 452 } | 449 } |
| 453 | 450 |
| 454 return null; | 451 return null; |
| 455 } | 452 } |
| 456 } | 453 } |
| OLD | NEW |