| 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 `` and | 342 /// Matches images like `` 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 |