[290] | 1 | // Released under MIT license |
---|
| 2 | // Copyright (c) 2009-2010 Dominic Baggott |
---|
| 3 | // Copyright (c) 2009-2010 Ash Berlin |
---|
| 4 | // Copyright (c) 2011 Christoph Dorn <christoph@christophdorn.com> (http://www.christophdorn.com) |
---|
| 5 | |
---|
| 6 | (function( expose ) { |
---|
| 7 | |
---|
| 8 | /** |
---|
| 9 | * class Markdown |
---|
| 10 | * |
---|
| 11 | * Markdown processing in Javascript done right. We have very particular views |
---|
| 12 | * on what constitutes 'right' which include: |
---|
| 13 | * |
---|
| 14 | * - produces well-formed HTML (this means that em and strong nesting is |
---|
| 15 | * important) |
---|
| 16 | * |
---|
| 17 | * - has an intermediate representation to allow processing of parsed data (We |
---|
| 18 | * in fact have two, both as [JsonML]: a markdown tree and an HTML tree). |
---|
| 19 | * |
---|
| 20 | * - is easily extensible to add new dialects without having to rewrite the |
---|
| 21 | * entire parsing mechanics |
---|
| 22 | * |
---|
| 23 | * - has a good test suite |
---|
| 24 | * |
---|
| 25 | * This implementation fulfills all of these (except that the test suite could |
---|
| 26 | * do with expanding to automatically run all the fixtures from other Markdown |
---|
| 27 | * implementations.) |
---|
| 28 | * |
---|
| 29 | * ##### Intermediate Representation |
---|
| 30 | * |
---|
| 31 | * *TODO* Talk about this :) Its JsonML, but document the node names we use. |
---|
| 32 | * |
---|
| 33 | * [JsonML]: http://jsonml.org/ "JSON Markup Language" |
---|
| 34 | **/ |
---|
| 35 | var Markdown = expose.Markdown = function Markdown(dialect) { |
---|
| 36 | switch (typeof dialect) { |
---|
| 37 | case "undefined": |
---|
| 38 | this.dialect = Markdown.dialects.Gruber; |
---|
| 39 | break; |
---|
| 40 | case "object": |
---|
| 41 | this.dialect = dialect; |
---|
| 42 | break; |
---|
| 43 | default: |
---|
| 44 | if (dialect in Markdown.dialects) { |
---|
| 45 | this.dialect = Markdown.dialects[dialect]; |
---|
| 46 | } |
---|
| 47 | else { |
---|
| 48 | throw new Error("Unknown Markdown dialect '" + String(dialect) + "'"); |
---|
| 49 | } |
---|
| 50 | break; |
---|
| 51 | } |
---|
| 52 | this.em_state = []; |
---|
| 53 | this.strong_state = []; |
---|
| 54 | this.debug_indent = ""; |
---|
| 55 | }; |
---|
| 56 | |
---|
| 57 | /** |
---|
| 58 | * parse( markdown, [dialect] ) -> JsonML |
---|
| 59 | * - markdown (String): markdown string to parse |
---|
| 60 | * - dialect (String | Dialect): the dialect to use, defaults to gruber |
---|
| 61 | * |
---|
| 62 | * Parse `markdown` and return a markdown document as a Markdown.JsonML tree. |
---|
| 63 | **/ |
---|
| 64 | expose.parse = function( source, dialect ) { |
---|
| 65 | // dialect will default if undefined |
---|
| 66 | var md = new Markdown( dialect ); |
---|
| 67 | return md.toTree( source ); |
---|
| 68 | }; |
---|
| 69 | |
---|
| 70 | /** |
---|
| 71 | * toHTML( markdown, [dialect] ) -> String |
---|
| 72 | * toHTML( md_tree ) -> String |
---|
| 73 | * - markdown (String): markdown string to parse |
---|
| 74 | * - md_tree (Markdown.JsonML): parsed markdown tree |
---|
| 75 | * |
---|
| 76 | * Take markdown (either as a string or as a JsonML tree) and run it through |
---|
| 77 | * [[toHTMLTree]] then turn it into a well-formated HTML fragment. |
---|
| 78 | **/ |
---|
| 79 | expose.toHTML = function toHTML( source , dialect , options ) { |
---|
| 80 | var input = expose.toHTMLTree( source , dialect , options ); |
---|
| 81 | |
---|
| 82 | return expose.renderJsonML( input ); |
---|
| 83 | }; |
---|
| 84 | |
---|
| 85 | /** |
---|
| 86 | * toHTMLTree( markdown, [dialect] ) -> JsonML |
---|
| 87 | * toHTMLTree( md_tree ) -> JsonML |
---|
| 88 | * - markdown (String): markdown string to parse |
---|
| 89 | * - dialect (String | Dialect): the dialect to use, defaults to gruber |
---|
| 90 | * - md_tree (Markdown.JsonML): parsed markdown tree |
---|
| 91 | * |
---|
| 92 | * Turn markdown into HTML, represented as a JsonML tree. If a string is given |
---|
| 93 | * to this function, it is first parsed into a markdown tree by calling |
---|
| 94 | * [[parse]]. |
---|
| 95 | **/ |
---|
| 96 | expose.toHTMLTree = function toHTMLTree( input, dialect , options ) { |
---|
| 97 | // convert string input to an MD tree |
---|
| 98 | if ( typeof input ==="string" ) input = this.parse( input, dialect ); |
---|
| 99 | |
---|
| 100 | // Now convert the MD tree to an HTML tree |
---|
| 101 | |
---|
| 102 | // remove references from the tree |
---|
| 103 | var attrs = extract_attr( input ), |
---|
| 104 | refs = {}; |
---|
| 105 | |
---|
| 106 | if ( attrs && attrs.references ) { |
---|
| 107 | refs = attrs.references; |
---|
| 108 | } |
---|
| 109 | |
---|
| 110 | var html = convert_tree_to_html( input, refs , options ); |
---|
| 111 | merge_text_nodes( html ); |
---|
| 112 | return html; |
---|
| 113 | }; |
---|
| 114 | |
---|
| 115 | // For Spidermonkey based engines |
---|
| 116 | function mk_block_toSource() { |
---|
| 117 | return "Markdown.mk_block( " + |
---|
| 118 | uneval(this.toString()) + |
---|
| 119 | ", " + |
---|
| 120 | uneval(this.trailing) + |
---|
| 121 | ", " + |
---|
| 122 | uneval(this.lineNumber) + |
---|
| 123 | " )"; |
---|
| 124 | } |
---|
| 125 | |
---|
| 126 | // node |
---|
| 127 | function mk_block_inspect() { |
---|
| 128 | var util = require('util'); |
---|
| 129 | return "Markdown.mk_block( " + |
---|
| 130 | util.inspect(this.toString()) + |
---|
| 131 | ", " + |
---|
| 132 | util.inspect(this.trailing) + |
---|
| 133 | ", " + |
---|
| 134 | util.inspect(this.lineNumber) + |
---|
| 135 | " )"; |
---|
| 136 | |
---|
| 137 | } |
---|
| 138 | |
---|
| 139 | var mk_block = Markdown.mk_block = function(block, trail, line) { |
---|
| 140 | // Be helpful for default case in tests. |
---|
| 141 | if ( arguments.length == 1 ) trail = "\n\n"; |
---|
| 142 | |
---|
| 143 | var s = new String(block); |
---|
| 144 | s.trailing = trail; |
---|
| 145 | // To make it clear its not just a string |
---|
| 146 | s.inspect = mk_block_inspect; |
---|
| 147 | s.toSource = mk_block_toSource; |
---|
| 148 | |
---|
| 149 | if (line != undefined) |
---|
| 150 | s.lineNumber = line; |
---|
| 151 | |
---|
| 152 | return s; |
---|
| 153 | }; |
---|
| 154 | |
---|
| 155 | function count_lines( str ) { |
---|
| 156 | var n = 0, i = -1; |
---|
| 157 | while ( ( i = str.indexOf('\n', i+1) ) !== -1) n++; |
---|
| 158 | return n; |
---|
| 159 | } |
---|
| 160 | |
---|
| 161 | // Internal - split source into rough blocks |
---|
| 162 | Markdown.prototype.split_blocks = function splitBlocks( input, startLine ) { |
---|
| 163 | // [\s\S] matches _anything_ (newline or space) |
---|
| 164 | var re = /([\s\S]+?)($|\n(?:\s*\n|$)+)/g, |
---|
| 165 | blocks = [], |
---|
| 166 | m; |
---|
| 167 | |
---|
| 168 | var line_no = 1; |
---|
| 169 | |
---|
| 170 | if ( ( m = /^(\s*\n)/.exec(input) ) != null ) { |
---|
| 171 | // skip (but count) leading blank lines |
---|
| 172 | line_no += count_lines( m[0] ); |
---|
| 173 | re.lastIndex = m[0].length; |
---|
| 174 | } |
---|
| 175 | |
---|
| 176 | while ( ( m = re.exec(input) ) !== null ) { |
---|
| 177 | blocks.push( mk_block( m[1], m[2], line_no ) ); |
---|
| 178 | line_no += count_lines( m[0] ); |
---|
| 179 | } |
---|
| 180 | |
---|
| 181 | return blocks; |
---|
| 182 | }; |
---|
| 183 | |
---|
| 184 | /** |
---|
| 185 | * Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ] |
---|
| 186 | * - block (String): the block to process |
---|
| 187 | * - next (Array): the following blocks |
---|
| 188 | * |
---|
| 189 | * Process `block` and return an array of JsonML nodes representing `block`. |
---|
| 190 | * |
---|
| 191 | * It does this by asking each block level function in the dialect to process |
---|
| 192 | * the block until one can. Succesful handling is indicated by returning an |
---|
| 193 | * array (with zero or more JsonML nodes), failure by a false value. |
---|
| 194 | * |
---|
| 195 | * Blocks handlers are responsible for calling [[Markdown#processInline]] |
---|
| 196 | * themselves as appropriate. |
---|
| 197 | * |
---|
| 198 | * If the blocks were split incorrectly or adjacent blocks need collapsing you |
---|
| 199 | * can adjust `next` in place using shift/splice etc. |
---|
| 200 | * |
---|
| 201 | * If any of this default behaviour is not right for the dialect, you can |
---|
| 202 | * define a `__call__` method on the dialect that will get invoked to handle |
---|
| 203 | * the block processing. |
---|
| 204 | */ |
---|
| 205 | Markdown.prototype.processBlock = function processBlock( block, next ) { |
---|
| 206 | var cbs = this.dialect.block, |
---|
| 207 | ord = cbs.__order__; |
---|
| 208 | |
---|
| 209 | if ( "__call__" in cbs ) { |
---|
| 210 | return cbs.__call__.call(this, block, next); |
---|
| 211 | } |
---|
| 212 | |
---|
| 213 | for ( var i = 0; i < ord.length; i++ ) { |
---|
| 214 | //D:this.debug( "Testing", ord[i] ); |
---|
| 215 | var res = cbs[ ord[i] ].call( this, block, next ); |
---|
| 216 | if ( res ) { |
---|
| 217 | //D:this.debug(" matched"); |
---|
| 218 | if ( !isArray(res) || ( res.length > 0 && !( isArray(res[0]) ) ) ) |
---|
| 219 | this.debug(ord[i], "didn't return a proper array"); |
---|
| 220 | //D:this.debug( "" ); |
---|
| 221 | return res; |
---|
| 222 | } |
---|
| 223 | } |
---|
| 224 | |
---|
| 225 | // Uhoh! no match! Should we throw an error? |
---|
| 226 | return []; |
---|
| 227 | }; |
---|
| 228 | |
---|
| 229 | Markdown.prototype.processInline = function processInline( block ) { |
---|
| 230 | return this.dialect.inline.__call__.call( this, String( block ) ); |
---|
| 231 | }; |
---|
| 232 | |
---|
| 233 | /** |
---|
| 234 | * Markdown#toTree( source ) -> JsonML |
---|
| 235 | * - source (String): markdown source to parse |
---|
| 236 | * |
---|
| 237 | * Parse `source` into a JsonML tree representing the markdown document. |
---|
| 238 | **/ |
---|
| 239 | // custom_tree means set this.tree to `custom_tree` and restore old value on return |
---|
| 240 | Markdown.prototype.toTree = function toTree( source, custom_root ) { |
---|
| 241 | var blocks = source instanceof Array ? source : this.split_blocks( source ); |
---|
| 242 | |
---|
| 243 | // Make tree a member variable so its easier to mess with in extensions |
---|
| 244 | var old_tree = this.tree; |
---|
| 245 | try { |
---|
| 246 | this.tree = custom_root || this.tree || [ "markdown" ]; |
---|
| 247 | |
---|
| 248 | blocks: |
---|
| 249 | while ( blocks.length ) { |
---|
| 250 | var b = this.processBlock( blocks.shift(), blocks ); |
---|
| 251 | |
---|
| 252 | // Reference blocks and the like won't return any content |
---|
| 253 | if ( !b.length ) continue blocks; |
---|
| 254 | |
---|
| 255 | this.tree.push.apply( this.tree, b ); |
---|
| 256 | } |
---|
| 257 | return this.tree; |
---|
| 258 | } |
---|
| 259 | finally { |
---|
| 260 | if ( custom_root ) { |
---|
| 261 | this.tree = old_tree; |
---|
| 262 | } |
---|
| 263 | } |
---|
| 264 | }; |
---|
| 265 | |
---|
| 266 | // Noop by default |
---|
| 267 | Markdown.prototype.debug = function () { |
---|
| 268 | var args = Array.prototype.slice.call( arguments); |
---|
| 269 | args.unshift(this.debug_indent); |
---|
| 270 | if (typeof print !== "undefined") |
---|
| 271 | print.apply( print, args ); |
---|
| 272 | if (typeof console !== "undefined" && typeof console.log !== "undefined") |
---|
| 273 | console.log.apply( null, args ); |
---|
| 274 | } |
---|
| 275 | |
---|
| 276 | Markdown.prototype.loop_re_over_block = function( re, block, cb ) { |
---|
| 277 | // Dont use /g regexps with this |
---|
| 278 | var m, |
---|
| 279 | b = block.valueOf(); |
---|
| 280 | |
---|
| 281 | while ( b.length && (m = re.exec(b) ) != null) { |
---|
| 282 | b = b.substr( m[0].length ); |
---|
| 283 | cb.call(this, m); |
---|
| 284 | } |
---|
| 285 | return b; |
---|
| 286 | }; |
---|
| 287 | |
---|
| 288 | /** |
---|
| 289 | * Markdown.dialects |
---|
| 290 | * |
---|
| 291 | * Namespace of built-in dialects. |
---|
| 292 | **/ |
---|
| 293 | Markdown.dialects = {}; |
---|
| 294 | |
---|
| 295 | /** |
---|
| 296 | * Markdown.dialects.Gruber |
---|
| 297 | * |
---|
| 298 | * The default dialect that follows the rules set out by John Gruber's |
---|
| 299 | * markdown.pl as closely as possible. Well actually we follow the behaviour of |
---|
| 300 | * that script which in some places is not exactly what the syntax web page |
---|
| 301 | * says. |
---|
| 302 | **/ |
---|
| 303 | Markdown.dialects.Gruber = { |
---|
| 304 | block: { |
---|
| 305 | atxHeader: function atxHeader( block, next ) { |
---|
| 306 | var m = block.match( /^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/ ); |
---|
| 307 | |
---|
| 308 | if ( !m ) return undefined; |
---|
| 309 | |
---|
| 310 | var header = [ "header", { level: m[ 1 ].length } ]; |
---|
| 311 | Array.prototype.push.apply(header, this.processInline(m[ 2 ])); |
---|
| 312 | |
---|
| 313 | if ( m[0].length < block.length ) |
---|
| 314 | next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) ); |
---|
| 315 | |
---|
| 316 | return [ header ]; |
---|
| 317 | }, |
---|
| 318 | |
---|
| 319 | setextHeader: function setextHeader( block, next ) { |
---|
| 320 | var m = block.match( /^(.*)\n([-=])\2\2+(?:\n|$)/ ); |
---|
| 321 | |
---|
| 322 | if ( !m ) return undefined; |
---|
| 323 | |
---|
| 324 | var level = ( m[ 2 ] === "=" ) ? 1 : 2; |
---|
| 325 | var header = [ "header", { level : level }, m[ 1 ] ]; |
---|
| 326 | |
---|
| 327 | if ( m[0].length < block.length ) |
---|
| 328 | next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) ); |
---|
| 329 | |
---|
| 330 | return [ header ]; |
---|
| 331 | }, |
---|
| 332 | |
---|
| 333 | code: function code( block, next ) { |
---|
| 334 | // | Foo |
---|
| 335 | // |bar |
---|
| 336 | // should be a code block followed by a paragraph. Fun |
---|
| 337 | // |
---|
| 338 | // There might also be adjacent code block to merge. |
---|
| 339 | |
---|
| 340 | var ret = [], |
---|
| 341 | re = /^(?: {0,3}\t| {4})(.*)\n?/, |
---|
| 342 | lines; |
---|
| 343 | |
---|
| 344 | // 4 spaces + content |
---|
| 345 | if ( !block.match( re ) ) return undefined; |
---|
| 346 | |
---|
| 347 | block_search: |
---|
| 348 | do { |
---|
| 349 | // Now pull out the rest of the lines |
---|
| 350 | var b = this.loop_re_over_block( |
---|
| 351 | re, block.valueOf(), function( m ) { ret.push( m[1] ); } ); |
---|
| 352 | |
---|
| 353 | if (b.length) { |
---|
| 354 | // Case alluded to in first comment. push it back on as a new block |
---|
| 355 | next.unshift( mk_block(b, block.trailing) ); |
---|
| 356 | break block_search; |
---|
| 357 | } |
---|
| 358 | else if (next.length) { |
---|
| 359 | // Check the next block - it might be code too |
---|
| 360 | if ( !next[0].match( re ) ) break block_search; |
---|
| 361 | |
---|
| 362 | // Pull how how many blanks lines follow - minus two to account for .join |
---|
| 363 | ret.push ( block.trailing.replace(/[^\n]/g, '').substring(2) ); |
---|
| 364 | |
---|
| 365 | block = next.shift(); |
---|
| 366 | } |
---|
| 367 | else { |
---|
| 368 | break block_search; |
---|
| 369 | } |
---|
| 370 | } while (true); |
---|
| 371 | |
---|
| 372 | return [ [ "code_block", ret.join("\n") ] ]; |
---|
| 373 | }, |
---|
| 374 | |
---|
| 375 | horizRule: function horizRule( block, next ) { |
---|
| 376 | // this needs to find any hr in the block to handle abutting blocks |
---|
| 377 | var m = block.match( /^(?:([\s\S]*?)\n)?[ \t]*([-_*])(?:[ \t]*\2){2,}[ \t]*(?:\n([\s\S]*))?$/ ); |
---|
| 378 | |
---|
| 379 | if ( !m ) { |
---|
| 380 | return undefined; |
---|
| 381 | } |
---|
| 382 | |
---|
| 383 | var jsonml = [ [ "hr" ] ]; |
---|
| 384 | |
---|
| 385 | // if there's a leading abutting block, process it |
---|
| 386 | if ( m[ 1 ] ) { |
---|
| 387 | jsonml.unshift.apply( jsonml, this.processBlock( m[ 1 ], [] ) ); |
---|
| 388 | } |
---|
| 389 | |
---|
| 390 | // if there's a trailing abutting block, stick it into next |
---|
| 391 | if ( m[ 3 ] ) { |
---|
| 392 | next.unshift( mk_block( m[ 3 ] ) ); |
---|
| 393 | } |
---|
| 394 | |
---|
| 395 | return jsonml; |
---|
| 396 | }, |
---|
| 397 | |
---|
| 398 | // There are two types of lists. Tight and loose. Tight lists have no whitespace |
---|
| 399 | // between the items (and result in text just in the <li>) and loose lists, |
---|
| 400 | // which have an empty line between list items, resulting in (one or more) |
---|
| 401 | // paragraphs inside the <li>. |
---|
| 402 | // |
---|
| 403 | // There are all sorts weird edge cases about the original markdown.pl's |
---|
| 404 | // handling of lists: |
---|
| 405 | // |
---|
| 406 | // * Nested lists are supposed to be indented by four chars per level. But |
---|
| 407 | // if they aren't, you can get a nested list by indenting by less than |
---|
| 408 | // four so long as the indent doesn't match an indent of an existing list |
---|
| 409 | // item in the 'nest stack'. |
---|
| 410 | // |
---|
| 411 | // * The type of the list (bullet or number) is controlled just by the |
---|
| 412 | // first item at the indent. Subsequent changes are ignored unless they |
---|
| 413 | // are for nested lists |
---|
| 414 | // |
---|
| 415 | lists: (function( ) { |
---|
| 416 | // Use a closure to hide a few variables. |
---|
| 417 | var any_list = "[*+-]|\\d+\\.", |
---|
| 418 | bullet_list = /[*+-]/, |
---|
| 419 | number_list = /\d+\./, |
---|
| 420 | // Capture leading indent as it matters for determining nested lists. |
---|
| 421 | is_list_re = new RegExp( "^( {0,3})(" + any_list + ")[ \t]+" ), |
---|
| 422 | indent_re = "(?: {0,3}\\t| {4})"; |
---|
| 423 | |
---|
| 424 | // TODO: Cache this regexp for certain depths. |
---|
| 425 | // Create a regexp suitable for matching an li for a given stack depth |
---|
| 426 | function regex_for_depth( depth ) { |
---|
| 427 | |
---|
| 428 | return new RegExp( |
---|
| 429 | // m[1] = indent, m[2] = list_type |
---|
| 430 | "(?:^(" + indent_re + "{0," + depth + "} {0,3})(" + any_list + ")\\s+)|" + |
---|
| 431 | // m[3] = cont |
---|
| 432 | "(^" + indent_re + "{0," + (depth-1) + "}[ ]{0,4})" |
---|
| 433 | ); |
---|
| 434 | } |
---|
| 435 | function expand_tab( input ) { |
---|
| 436 | return input.replace( / {0,3}\t/g, " " ); |
---|
| 437 | } |
---|
| 438 | |
---|
| 439 | // Add inline content `inline` to `li`. inline comes from processInline |
---|
| 440 | // so is an array of content |
---|
| 441 | function add(li, loose, inline, nl) { |
---|
| 442 | if (loose) { |
---|
| 443 | li.push( [ "para" ].concat(inline) ); |
---|
| 444 | return; |
---|
| 445 | } |
---|
| 446 | // Hmmm, should this be any block level element or just paras? |
---|
| 447 | var add_to = li[li.length -1] instanceof Array && li[li.length - 1][0] == "para" |
---|
| 448 | ? li[li.length -1] |
---|
| 449 | : li; |
---|
| 450 | |
---|
| 451 | // If there is already some content in this list, add the new line in |
---|
| 452 | if (nl && li.length > 1) inline.unshift(nl); |
---|
| 453 | |
---|
| 454 | for (var i=0; i < inline.length; i++) { |
---|
| 455 | var what = inline[i], |
---|
| 456 | is_str = typeof what == "string"; |
---|
| 457 | if (is_str && add_to.length > 1 && typeof add_to[add_to.length-1] == "string" ) { |
---|
| 458 | add_to[ add_to.length-1 ] += what; |
---|
| 459 | } |
---|
| 460 | else { |
---|
| 461 | add_to.push( what ); |
---|
| 462 | } |
---|
| 463 | } |
---|
| 464 | } |
---|
| 465 | |
---|
| 466 | // contained means have an indent greater than the current one. On |
---|
| 467 | // *every* line in the block |
---|
| 468 | function get_contained_blocks( depth, blocks ) { |
---|
| 469 | |
---|
| 470 | var re = new RegExp( "^(" + indent_re + "{" + depth + "}.*?\\n?)*$" ), |
---|
| 471 | replace = new RegExp("^" + indent_re + "{" + depth + "}", "gm"), |
---|
| 472 | ret = []; |
---|
| 473 | |
---|
| 474 | while ( blocks.length > 0 ) { |
---|
| 475 | if ( re.exec( blocks[0] ) ) { |
---|
| 476 | var b = blocks.shift(), |
---|
| 477 | // Now remove that indent |
---|
| 478 | x = b.replace( replace, ""); |
---|
| 479 | |
---|
| 480 | ret.push( mk_block( x, b.trailing, b.lineNumber ) ); |
---|
| 481 | } |
---|
| 482 | break; |
---|
| 483 | } |
---|
| 484 | return ret; |
---|
| 485 | } |
---|
| 486 | |
---|
| 487 | // passed to stack.forEach to turn list items up the stack into paras |
---|
| 488 | function paragraphify(s, i, stack) { |
---|
| 489 | var list = s.list; |
---|
| 490 | var last_li = list[list.length-1]; |
---|
| 491 | |
---|
| 492 | if (last_li[1] instanceof Array && last_li[1][0] == "para") { |
---|
| 493 | return; |
---|
| 494 | } |
---|
| 495 | if (i+1 == stack.length) { |
---|
| 496 | // Last stack frame |
---|
| 497 | // Keep the same array, but replace the contents |
---|
| 498 | last_li.push( ["para"].concat( last_li.splice(1) ) ); |
---|
| 499 | } |
---|
| 500 | else { |
---|
| 501 | var sublist = last_li.pop(); |
---|
| 502 | last_li.push( ["para"].concat( last_li.splice(1) ), sublist ); |
---|
| 503 | } |
---|
| 504 | } |
---|
| 505 | |
---|
| 506 | // The matcher function |
---|
| 507 | return function( block, next ) { |
---|
| 508 | var m = block.match( is_list_re ); |
---|
| 509 | if ( !m ) return undefined; |
---|
| 510 | |
---|
| 511 | function make_list( m ) { |
---|
| 512 | var list = bullet_list.exec( m[2] ) |
---|
| 513 | ? ["bulletlist"] |
---|
| 514 | : ["numberlist"]; |
---|
| 515 | |
---|
| 516 | stack.push( { list: list, indent: m[1] } ); |
---|
| 517 | return list; |
---|
| 518 | } |
---|
| 519 | |
---|
| 520 | |
---|
| 521 | var stack = [], // Stack of lists for nesting. |
---|
| 522 | list = make_list( m ), |
---|
| 523 | last_li, |
---|
| 524 | loose = false, |
---|
| 525 | ret = [ stack[0].list ], |
---|
| 526 | i; |
---|
| 527 | |
---|
| 528 | // Loop to search over block looking for inner block elements and loose lists |
---|
| 529 | loose_search: |
---|
| 530 | while( true ) { |
---|
| 531 | // Split into lines preserving new lines at end of line |
---|
| 532 | var lines = block.split( /(?=\n)/ ); |
---|
| 533 | |
---|
| 534 | // We have to grab all lines for a li and call processInline on them |
---|
| 535 | // once as there are some inline things that can span lines. |
---|
| 536 | var li_accumulate = ""; |
---|
| 537 | |
---|
| 538 | // Loop over the lines in this block looking for tight lists. |
---|
| 539 | tight_search: |
---|
| 540 | for (var line_no=0; line_no < lines.length; line_no++) { |
---|
| 541 | var nl = "", |
---|
| 542 | l = lines[line_no].replace(/^\n/, function(n) { nl = n; return ""; }); |
---|
| 543 | |
---|
| 544 | // TODO: really should cache this |
---|
| 545 | var line_re = regex_for_depth( stack.length ); |
---|
| 546 | |
---|
| 547 | m = l.match( line_re ); |
---|
| 548 | //print( "line:", uneval(l), "\nline match:", uneval(m) ); |
---|
| 549 | |
---|
| 550 | // We have a list item |
---|
| 551 | if ( m[1] !== undefined ) { |
---|
| 552 | // Process the previous list item, if any |
---|
| 553 | if ( li_accumulate.length ) { |
---|
| 554 | add( last_li, loose, this.processInline( li_accumulate ), nl ); |
---|
| 555 | // Loose mode will have been dealt with. Reset it |
---|
| 556 | loose = false; |
---|
| 557 | li_accumulate = ""; |
---|
| 558 | } |
---|
| 559 | |
---|
| 560 | m[1] = expand_tab( m[1] ); |
---|
| 561 | var wanted_depth = Math.floor(m[1].length/4)+1; |
---|
| 562 | //print( "want:", wanted_depth, "stack:", stack.length); |
---|
| 563 | if ( wanted_depth > stack.length ) { |
---|
| 564 | // Deep enough for a nested list outright |
---|
| 565 | //print ( "new nested list" ); |
---|
| 566 | list = make_list( m ); |
---|
| 567 | last_li.push( list ); |
---|
| 568 | last_li = list[1] = [ "listitem" ]; |
---|
| 569 | } |
---|
| 570 | else { |
---|
| 571 | // We aren't deep enough to be strictly a new level. This is |
---|
| 572 | // where Md.pl goes nuts. If the indent matches a level in the |
---|
| 573 | // stack, put it there, else put it one deeper then the |
---|
| 574 | // wanted_depth deserves. |
---|
| 575 | var found = false; |
---|
| 576 | for (i = 0; i < stack.length; i++) { |
---|
| 577 | if ( stack[ i ].indent != m[1] ) continue; |
---|
| 578 | list = stack[ i ].list; |
---|
| 579 | stack.splice( i+1 ); |
---|
| 580 | found = true; |
---|
| 581 | break; |
---|
| 582 | } |
---|
| 583 | |
---|
| 584 | if (!found) { |
---|
| 585 | //print("not found. l:", uneval(l)); |
---|
| 586 | wanted_depth++; |
---|
| 587 | if (wanted_depth <= stack.length) { |
---|
| 588 | stack.splice(wanted_depth); |
---|
| 589 | //print("Desired depth now", wanted_depth, "stack:", stack.length); |
---|
| 590 | list = stack[wanted_depth-1].list; |
---|
| 591 | //print("list:", uneval(list) ); |
---|
| 592 | } |
---|
| 593 | else { |
---|
| 594 | //print ("made new stack for messy indent"); |
---|
| 595 | list = make_list(m); |
---|
| 596 | last_li.push(list); |
---|
| 597 | } |
---|
| 598 | } |
---|
| 599 | |
---|
| 600 | //print( uneval(list), "last", list === stack[stack.length-1].list ); |
---|
| 601 | last_li = [ "listitem" ]; |
---|
| 602 | list.push(last_li); |
---|
| 603 | } // end depth of shenegains |
---|
| 604 | nl = ""; |
---|
| 605 | } |
---|
| 606 | |
---|
| 607 | // Add content |
---|
| 608 | if (l.length > m[0].length) { |
---|
| 609 | li_accumulate += nl + l.substr( m[0].length ); |
---|
| 610 | } |
---|
| 611 | } // tight_search |
---|
| 612 | |
---|
| 613 | if ( li_accumulate.length ) { |
---|
| 614 | add( last_li, loose, this.processInline( li_accumulate ), nl ); |
---|
| 615 | // Loose mode will have been dealt with. Reset it |
---|
| 616 | loose = false; |
---|
| 617 | li_accumulate = ""; |
---|
| 618 | } |
---|
| 619 | |
---|
| 620 | // Look at the next block - we might have a loose list. Or an extra |
---|
| 621 | // paragraph for the current li |
---|
| 622 | var contained = get_contained_blocks( stack.length, next ); |
---|
| 623 | |
---|
| 624 | // Deal with code blocks or properly nested lists |
---|
| 625 | if (contained.length > 0) { |
---|
| 626 | // Make sure all listitems up the stack are paragraphs |
---|
| 627 | forEach( stack, paragraphify, this); |
---|
| 628 | |
---|
| 629 | last_li.push.apply( last_li, this.toTree( contained, [] ) ); |
---|
| 630 | } |
---|
| 631 | |
---|
| 632 | var next_block = next[0] && next[0].valueOf() || ""; |
---|
| 633 | |
---|
| 634 | if ( next_block.match(is_list_re) || next_block.match( /^ / ) ) { |
---|
| 635 | block = next.shift(); |
---|
| 636 | |
---|
| 637 | // Check for an HR following a list: features/lists/hr_abutting |
---|
| 638 | var hr = this.dialect.block.horizRule( block, next ); |
---|
| 639 | |
---|
| 640 | if (hr) { |
---|
| 641 | ret.push.apply(ret, hr); |
---|
| 642 | break; |
---|
| 643 | } |
---|
| 644 | |
---|
| 645 | // Make sure all listitems up the stack are paragraphs |
---|
| 646 | forEach( stack, paragraphify, this); |
---|
| 647 | |
---|
| 648 | loose = true; |
---|
| 649 | continue loose_search; |
---|
| 650 | } |
---|
| 651 | break; |
---|
| 652 | } // loose_search |
---|
| 653 | |
---|
| 654 | return ret; |
---|
| 655 | }; |
---|
| 656 | })(), |
---|
| 657 | |
---|
| 658 | blockquote: function blockquote( block, next ) { |
---|
| 659 | if ( !block.match( /^>/m ) ) |
---|
| 660 | return undefined; |
---|
| 661 | |
---|
| 662 | var jsonml = []; |
---|
| 663 | |
---|
| 664 | // separate out the leading abutting block, if any |
---|
| 665 | if ( block[ 0 ] != ">" ) { |
---|
| 666 | var lines = block.split( /\n/ ), |
---|
| 667 | prev = []; |
---|
| 668 | |
---|
| 669 | // keep shifting lines until you find a crotchet |
---|
| 670 | while ( lines.length && lines[ 0 ][ 0 ] != ">" ) { |
---|
| 671 | prev.push( lines.shift() ); |
---|
| 672 | } |
---|
| 673 | |
---|
| 674 | // reassemble! |
---|
| 675 | block = lines.join( "\n" ); |
---|
| 676 | jsonml.push.apply( jsonml, this.processBlock( prev.join( "\n" ), [] ) ); |
---|
| 677 | } |
---|
| 678 | |
---|
| 679 | // if the next block is also a blockquote merge it in |
---|
| 680 | while ( next.length && next[ 0 ][ 0 ] == ">" ) { |
---|
| 681 | var b = next.shift(); |
---|
| 682 | block = new String(block + block.trailing + b); |
---|
| 683 | block.trailing = b.trailing; |
---|
| 684 | } |
---|
| 685 | |
---|
| 686 | // Strip off the leading "> " and re-process as a block. |
---|
| 687 | var input = block.replace( /^> ?/gm, '' ), |
---|
| 688 | old_tree = this.tree; |
---|
| 689 | jsonml.push( this.toTree( input, [ "blockquote" ] ) ); |
---|
| 690 | |
---|
| 691 | return jsonml; |
---|
| 692 | }, |
---|
| 693 | |
---|
| 694 | referenceDefn: function referenceDefn( block, next) { |
---|
| 695 | var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/; |
---|
| 696 | // interesting matches are [ , ref_id, url, , title, title ] |
---|
| 697 | |
---|
| 698 | if ( !block.match(re) ) |
---|
| 699 | return undefined; |
---|
| 700 | |
---|
| 701 | // make an attribute node if it doesn't exist |
---|
| 702 | if ( !extract_attr( this.tree ) ) { |
---|
| 703 | this.tree.splice( 1, 0, {} ); |
---|
| 704 | } |
---|
| 705 | |
---|
| 706 | var attrs = extract_attr( this.tree ); |
---|
| 707 | |
---|
| 708 | // make a references hash if it doesn't exist |
---|
| 709 | if ( attrs.references === undefined ) { |
---|
| 710 | attrs.references = {}; |
---|
| 711 | } |
---|
| 712 | |
---|
| 713 | var b = this.loop_re_over_block(re, block, function( m ) { |
---|
| 714 | |
---|
| 715 | if ( m[2] && m[2][0] == '<' && m[2][m[2].length-1] == '>' ) |
---|
| 716 | m[2] = m[2].substring( 1, m[2].length - 1 ); |
---|
| 717 | |
---|
| 718 | var ref = attrs.references[ m[1].toLowerCase() ] = { |
---|
| 719 | href: m[2] |
---|
| 720 | }; |
---|
| 721 | |
---|
| 722 | if (m[4] !== undefined) |
---|
| 723 | ref.title = m[4]; |
---|
| 724 | else if (m[5] !== undefined) |
---|
| 725 | ref.title = m[5]; |
---|
| 726 | |
---|
| 727 | } ); |
---|
| 728 | |
---|
| 729 | if (b.length) |
---|
| 730 | next.unshift( mk_block( b, block.trailing ) ); |
---|
| 731 | |
---|
| 732 | return []; |
---|
| 733 | }, |
---|
| 734 | |
---|
| 735 | para: function para( block, next ) { |
---|
| 736 | // everything's a para! |
---|
| 737 | return [ ["para"].concat( this.processInline( block ) ) ]; |
---|
| 738 | } |
---|
| 739 | } |
---|
| 740 | }; |
---|
| 741 | |
---|
| 742 | Markdown.dialects.Gruber.inline = { |
---|
| 743 | |
---|
| 744 | __oneElement__: function oneElement( text, patterns_or_re, previous_nodes ) { |
---|
| 745 | var m, |
---|
| 746 | res, |
---|
| 747 | lastIndex = 0; |
---|
| 748 | |
---|
| 749 | patterns_or_re = patterns_or_re || this.dialect.inline.__patterns__; |
---|
| 750 | var re = new RegExp( "([\\s\\S]*?)(" + (patterns_or_re.source || patterns_or_re) + ")" ); |
---|
| 751 | |
---|
| 752 | m = re.exec( text ); |
---|
| 753 | if (!m) { |
---|
| 754 | // Just boring text |
---|
| 755 | return [ text.length, text ]; |
---|
| 756 | } |
---|
| 757 | else if ( m[1] ) { |
---|
| 758 | // Some un-interesting text matched. Return that first |
---|
| 759 | return [ m[1].length, m[1] ]; |
---|
| 760 | } |
---|
| 761 | |
---|
| 762 | var res; |
---|
| 763 | if ( m[2] in this.dialect.inline ) { |
---|
| 764 | res = this.dialect.inline[ m[2] ].call( |
---|
| 765 | this, |
---|
| 766 | text.substr( m.index ), m, previous_nodes || [] ); |
---|
| 767 | } |
---|
| 768 | // Default for now to make dev easier. just slurp special and output it. |
---|
| 769 | res = res || [ m[2].length, m[2] ]; |
---|
| 770 | return res; |
---|
| 771 | }, |
---|
| 772 | |
---|
| 773 | __call__: function inline( text, patterns ) { |
---|
| 774 | |
---|
| 775 | var out = [], |
---|
| 776 | res; |
---|
| 777 | |
---|
| 778 | function add(x) { |
---|
| 779 | //D:self.debug(" adding output", uneval(x)); |
---|
| 780 | if (typeof x == "string" && typeof out[out.length-1] == "string") |
---|
| 781 | out[ out.length-1 ] += x; |
---|
| 782 | else |
---|
| 783 | out.push(x); |
---|
| 784 | } |
---|
| 785 | |
---|
| 786 | while ( text.length > 0 ) { |
---|
| 787 | res = this.dialect.inline.__oneElement__.call(this, text, patterns, out ); |
---|
| 788 | text = text.substr( res.shift() ); |
---|
| 789 | forEach(res, add ) |
---|
| 790 | } |
---|
| 791 | |
---|
| 792 | return out; |
---|
| 793 | }, |
---|
| 794 | |
---|
| 795 | // These characters are intersting elsewhere, so have rules for them so that |
---|
| 796 | // chunks of plain text blocks don't include them |
---|
| 797 | "]": function () {}, |
---|
| 798 | "}": function () {}, |
---|
| 799 | |
---|
| 800 | "\\": function escaped( text ) { |
---|
| 801 | // [ length of input processed, node/children to add... ] |
---|
| 802 | // Only esacape: \ ` * _ { } [ ] ( ) # * + - . ! |
---|
| 803 | if ( text.match( /^\\[\\`\*_{}\[\]()#\+.!\-]/ ) ) |
---|
| 804 | return [ 2, text[1] ]; |
---|
| 805 | else |
---|
| 806 | // Not an esacpe |
---|
| 807 | return [ 1, "\\" ]; |
---|
| 808 | }, |
---|
| 809 | |
---|
| 810 | " |
---|
| 816 | // 1 2 3 4 <--- captures |
---|
| 817 | var m = text.match( /^!\[(.*?)\][ \t]*\([ \t]*(\S*)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/ ); |
---|
| 818 | |
---|
| 819 | if ( m ) { |
---|
| 820 | if ( m[2] && m[2][0] == '<' && m[2][m[2].length-1] == '>' ) |
---|
| 821 | m[2] = m[2].substring( 1, m[2].length - 1 ); |
---|
| 822 | |
---|
| 823 | m[2] = this.dialect.inline.__call__.call( this, m[2], /\\/ )[0]; |
---|
| 824 | |
---|
| 825 | var attrs = { alt: m[1], href: m[2] || "" }; |
---|
| 826 | if ( m[4] !== undefined) |
---|
| 827 | attrs.title = m[4]; |
---|
| 828 | |
---|
| 829 | return [ m[0].length, [ "img", attrs ] ]; |
---|
| 830 | } |
---|
| 831 | |
---|
| 832 | // ![Alt text][id] |
---|
| 833 | m = text.match( /^!\[(.*?)\][ \t]*\[(.*?)\]/ ); |
---|
| 834 | |
---|
| 835 | if ( m ) { |
---|
| 836 | // We can't check if the reference is known here as it likely wont be |
---|
| 837 | // found till after. Check it in md tree->hmtl tree conversion |
---|
| 838 | return [ m[0].length, [ "img_ref", { alt: m[1], ref: m[2].toLowerCase(), original: m[0] } ] ]; |
---|
| 839 | } |
---|
| 840 | |
---|
| 841 | // Just consume the '![' |
---|
| 842 | return [ 2, "![" ]; |
---|
| 843 | }, |
---|
| 844 | |
---|
| 845 | "[": function link( text ) { |
---|
| 846 | |
---|
| 847 | var orig = String(text); |
---|
| 848 | // Inline content is possible inside `link text` |
---|
| 849 | var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), ']' ); |
---|
| 850 | |
---|
| 851 | // No closing ']' found. Just consume the [ |
---|
| 852 | if ( !res ) return [ 1, '[' ]; |
---|
| 853 | |
---|
| 854 | var consumed = 1 + res[ 0 ], |
---|
| 855 | children = res[ 1 ], |
---|
| 856 | link, |
---|
| 857 | attrs; |
---|
| 858 | |
---|
| 859 | // At this point the first [...] has been parsed. See what follows to find |
---|
| 860 | // out which kind of link we are (reference or direct url) |
---|
| 861 | text = text.substr( consumed ); |
---|
| 862 | |
---|
| 863 | // [link text](/path/to/img.jpg "Optional title") |
---|
| 864 | // 1 2 3 <--- captures |
---|
| 865 | // This will capture up to the last paren in the block. We then pull |
---|
| 866 | // back based on if there a matching ones in the url |
---|
| 867 | // ([here](/url/(test)) |
---|
| 868 | // The parens have to be balanced |
---|
| 869 | var m = text.match( /^\s*\([ \t]*(\S+)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/ ); |
---|
| 870 | if ( m ) { |
---|
| 871 | var url = m[1]; |
---|
| 872 | consumed += m[0].length; |
---|
| 873 | |
---|
| 874 | if ( url && url[0] == '<' && url[url.length-1] == '>' ) |
---|
| 875 | url = url.substring( 1, url.length - 1 ); |
---|
| 876 | |
---|
| 877 | // If there is a title we don't have to worry about parens in the url |
---|
| 878 | if ( !m[3] ) { |
---|
| 879 | var open_parens = 1; // One open that isn't in the capture |
---|
| 880 | for (var len = 0; len < url.length; len++) { |
---|
| 881 | switch ( url[len] ) { |
---|
| 882 | case '(': |
---|
| 883 | open_parens++; |
---|
| 884 | break; |
---|
| 885 | case ')': |
---|
| 886 | if ( --open_parens == 0) { |
---|
| 887 | consumed -= url.length - len; |
---|
| 888 | url = url.substring(0, len); |
---|
| 889 | } |
---|
| 890 | break; |
---|
| 891 | } |
---|
| 892 | } |
---|
| 893 | } |
---|
| 894 | |
---|
| 895 | // Process escapes only |
---|
| 896 | url = this.dialect.inline.__call__.call( this, url, /\\/ )[0]; |
---|
| 897 | |
---|
| 898 | attrs = { href: url || "" }; |
---|
| 899 | if ( m[3] !== undefined) |
---|
| 900 | attrs.title = m[3]; |
---|
| 901 | |
---|
| 902 | link = [ "link", attrs ].concat( children ); |
---|
| 903 | return [ consumed, link ]; |
---|
| 904 | } |
---|
| 905 | |
---|
| 906 | // [Alt text][id] |
---|
| 907 | // [Alt text] [id] |
---|
| 908 | m = text.match( /^\s*\[(.*?)\]/ ); |
---|
| 909 | |
---|
| 910 | if ( m ) { |
---|
| 911 | |
---|
| 912 | consumed += m[ 0 ].length; |
---|
| 913 | |
---|
| 914 | // [links][] uses links as its reference |
---|
| 915 | attrs = { ref: ( m[ 1 ] || String(children) ).toLowerCase(), original: orig.substr( 0, consumed ) }; |
---|
| 916 | |
---|
| 917 | link = [ "link_ref", attrs ].concat( children ); |
---|
| 918 | |
---|
| 919 | // We can't check if the reference is known here as it likely wont be |
---|
| 920 | // found till after. Check it in md tree->hmtl tree conversion. |
---|
| 921 | // Store the original so that conversion can revert if the ref isn't found. |
---|
| 922 | return [ consumed, link ]; |
---|
| 923 | } |
---|
| 924 | |
---|
| 925 | // [id] |
---|
| 926 | // Only if id is plain (no formatting.) |
---|
| 927 | if ( children.length == 1 && typeof children[0] == "string" ) { |
---|
| 928 | |
---|
| 929 | attrs = { ref: children[0].toLowerCase(), original: orig.substr( 0, consumed ) }; |
---|
| 930 | link = [ "link_ref", attrs, children[0] ]; |
---|
| 931 | return [ consumed, link ]; |
---|
| 932 | } |
---|
| 933 | |
---|
| 934 | // Just consume the '[' |
---|
| 935 | return [ 1, "[" ]; |
---|
| 936 | }, |
---|
| 937 | |
---|
| 938 | |
---|
| 939 | "<": function autoLink( text ) { |
---|
| 940 | var m; |
---|
| 941 | |
---|
| 942 | if ( ( m = text.match( /^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/ ) ) != null ) { |
---|
| 943 | if ( m[3] ) { |
---|
| 944 | return [ m[0].length, [ "link", { href: "mailto:" + m[3] }, m[3] ] ]; |
---|
| 945 | |
---|
| 946 | } |
---|
| 947 | else if ( m[2] == "mailto" ) { |
---|
| 948 | return [ m[0].length, [ "link", { href: m[1] }, m[1].substr("mailto:".length ) ] ]; |
---|
| 949 | } |
---|
| 950 | else |
---|
| 951 | return [ m[0].length, [ "link", { href: m[1] }, m[1] ] ]; |
---|
| 952 | } |
---|
| 953 | |
---|
| 954 | return [ 1, "<" ]; |
---|
| 955 | }, |
---|
| 956 | |
---|
| 957 | "`": function inlineCode( text ) { |
---|
| 958 | // Inline code block. as many backticks as you like to start it |
---|
| 959 | // Always skip over the opening ticks. |
---|
| 960 | var m = text.match( /(`+)(([\s\S]*?)\1)/ ); |
---|
| 961 | |
---|
| 962 | if ( m && m[2] ) |
---|
| 963 | return [ m[1].length + m[2].length, [ "inlinecode", m[3] ] ]; |
---|
| 964 | else { |
---|
| 965 | // TODO: No matching end code found - warn! |
---|
| 966 | return [ 1, "`" ]; |
---|
| 967 | } |
---|
| 968 | }, |
---|
| 969 | |
---|
| 970 | " \n": function lineBreak( text ) { |
---|
| 971 | return [ 3, [ "linebreak" ] ]; |
---|
| 972 | } |
---|
| 973 | |
---|
| 974 | }; |
---|
| 975 | |
---|
| 976 | // Meta Helper/generator method for em and strong handling |
---|
| 977 | function strong_em( tag, md ) { |
---|
| 978 | |
---|
| 979 | var state_slot = tag + "_state", |
---|
| 980 | other_slot = tag == "strong" ? "em_state" : "strong_state"; |
---|
| 981 | |
---|
| 982 | function CloseTag(len) { |
---|
| 983 | this.len_after = len; |
---|
| 984 | this.name = "close_" + md; |
---|
| 985 | } |
---|
| 986 | |
---|
| 987 | return function ( text, orig_match ) { |
---|
| 988 | |
---|
| 989 | if (this[state_slot][0] == md) { |
---|
| 990 | // Most recent em is of this type |
---|
| 991 | //D:this.debug("closing", md); |
---|
| 992 | this[state_slot].shift(); |
---|
| 993 | |
---|
| 994 | // "Consume" everything to go back to the recrusion in the else-block below |
---|
| 995 | return[ text.length, new CloseTag(text.length-md.length) ]; |
---|
| 996 | } |
---|
| 997 | else { |
---|
| 998 | // Store a clone of the em/strong states |
---|
| 999 | var other = this[other_slot].slice(), |
---|
| 1000 | state = this[state_slot].slice(); |
---|
| 1001 | |
---|
| 1002 | this[state_slot].unshift(md); |
---|
| 1003 | |
---|
| 1004 | //D:this.debug_indent += " "; |
---|
| 1005 | |
---|
| 1006 | // Recurse |
---|
| 1007 | var res = this.processInline( text.substr( md.length ) ); |
---|
| 1008 | //D:this.debug_indent = this.debug_indent.substr(2); |
---|
| 1009 | |
---|
| 1010 | var last = res[res.length - 1]; |
---|
| 1011 | |
---|
| 1012 | //D:this.debug("processInline from", tag + ": ", uneval( res ) ); |
---|
| 1013 | |
---|
| 1014 | var check = this[state_slot].shift(); |
---|
| 1015 | if (last instanceof CloseTag) { |
---|
| 1016 | res.pop(); |
---|
| 1017 | // We matched! Huzzah. |
---|
| 1018 | var consumed = text.length - last.len_after; |
---|
| 1019 | return [ consumed, [ tag ].concat(res) ]; |
---|
| 1020 | } |
---|
| 1021 | else { |
---|
| 1022 | // Restore the state of the other kind. We might have mistakenly closed it. |
---|
| 1023 | this[other_slot] = other; |
---|
| 1024 | this[state_slot] = state; |
---|
| 1025 | |
---|
| 1026 | // We can't reuse the processed result as it could have wrong parsing contexts in it. |
---|
| 1027 | return [ md.length, md ]; |
---|
| 1028 | } |
---|
| 1029 | } |
---|
| 1030 | }; // End returned function |
---|
| 1031 | } |
---|
| 1032 | |
---|
| 1033 | Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**"); |
---|
| 1034 | Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__"); |
---|
| 1035 | Markdown.dialects.Gruber.inline["*"] = strong_em("em", "*"); |
---|
| 1036 | Markdown.dialects.Gruber.inline["_"] = strong_em("em", "_"); |
---|
| 1037 | |
---|
| 1038 | |
---|
| 1039 | // Build default order from insertion order. |
---|
| 1040 | Markdown.buildBlockOrder = function(d) { |
---|
| 1041 | var ord = []; |
---|
| 1042 | for ( var i in d ) { |
---|
| 1043 | if ( i == "__order__" || i == "__call__" ) continue; |
---|
| 1044 | ord.push( i ); |
---|
| 1045 | } |
---|
| 1046 | d.__order__ = ord; |
---|
| 1047 | }; |
---|
| 1048 | |
---|
| 1049 | // Build patterns for inline matcher |
---|
| 1050 | Markdown.buildInlinePatterns = function(d) { |
---|
| 1051 | var patterns = []; |
---|
| 1052 | |
---|
| 1053 | for ( var i in d ) { |
---|
| 1054 | // __foo__ is reserved and not a pattern |
---|
| 1055 | if ( i.match( /^__.*__$/) ) continue; |
---|
| 1056 | var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" ) |
---|
| 1057 | .replace( /\n/, "\\n" ); |
---|
| 1058 | patterns.push( i.length == 1 ? l : "(?:" + l + ")" ); |
---|
| 1059 | } |
---|
| 1060 | |
---|
| 1061 | patterns = patterns.join("|"); |
---|
| 1062 | d.__patterns__ = patterns; |
---|
| 1063 | //print("patterns:", uneval( patterns ) ); |
---|
| 1064 | |
---|
| 1065 | var fn = d.__call__; |
---|
| 1066 | d.__call__ = function(text, pattern) { |
---|
| 1067 | if (pattern != undefined) { |
---|
| 1068 | return fn.call(this, text, pattern); |
---|
| 1069 | } |
---|
| 1070 | else |
---|
| 1071 | { |
---|
| 1072 | return fn.call(this, text, patterns); |
---|
| 1073 | } |
---|
| 1074 | }; |
---|
| 1075 | }; |
---|
| 1076 | |
---|
| 1077 | Markdown.DialectHelpers = {}; |
---|
| 1078 | Markdown.DialectHelpers.inline_until_char = function( text, want ) { |
---|
| 1079 | var consumed = 0, |
---|
| 1080 | nodes = []; |
---|
| 1081 | |
---|
| 1082 | while ( true ) { |
---|
| 1083 | if ( text[ consumed ] == want ) { |
---|
| 1084 | // Found the character we were looking for |
---|
| 1085 | consumed++; |
---|
| 1086 | return [ consumed, nodes ]; |
---|
| 1087 | } |
---|
| 1088 | |
---|
| 1089 | if ( consumed >= text.length ) { |
---|
| 1090 | // No closing char found. Abort. |
---|
| 1091 | return null; |
---|
| 1092 | } |
---|
| 1093 | |
---|
| 1094 | var res = this.dialect.inline.__oneElement__.call(this, text.substr( consumed ) ); |
---|
| 1095 | consumed += res[ 0 ]; |
---|
| 1096 | // Add any returned nodes. |
---|
| 1097 | nodes.push.apply( nodes, res.slice( 1 ) ); |
---|
| 1098 | } |
---|
| 1099 | } |
---|
| 1100 | |
---|
| 1101 | // Helper function to make sub-classing a dialect easier |
---|
| 1102 | Markdown.subclassDialect = function( d ) { |
---|
| 1103 | function Block() {} |
---|
| 1104 | Block.prototype = d.block; |
---|
| 1105 | function Inline() {} |
---|
| 1106 | Inline.prototype = d.inline; |
---|
| 1107 | |
---|
| 1108 | return { block: new Block(), inline: new Inline() }; |
---|
| 1109 | }; |
---|
| 1110 | |
---|
| 1111 | Markdown.buildBlockOrder ( Markdown.dialects.Gruber.block ); |
---|
| 1112 | Markdown.buildInlinePatterns( Markdown.dialects.Gruber.inline ); |
---|
| 1113 | |
---|
| 1114 | Markdown.dialects.Maruku = Markdown.subclassDialect( Markdown.dialects.Gruber ); |
---|
| 1115 | |
---|
| 1116 | Markdown.dialects.Maruku.processMetaHash = function processMetaHash( meta_string ) { |
---|
| 1117 | var meta = split_meta_hash( meta_string ), |
---|
| 1118 | attr = {}; |
---|
| 1119 | |
---|
| 1120 | for ( var i = 0; i < meta.length; ++i ) { |
---|
| 1121 | // id: #foo |
---|
| 1122 | if ( /^#/.test( meta[ i ] ) ) { |
---|
| 1123 | attr.id = meta[ i ].substring( 1 ); |
---|
| 1124 | } |
---|
| 1125 | // class: .foo |
---|
| 1126 | else if ( /^\./.test( meta[ i ] ) ) { |
---|
| 1127 | // if class already exists, append the new one |
---|
| 1128 | if ( attr['class'] ) { |
---|
| 1129 | attr['class'] = attr['class'] + meta[ i ].replace( /./, " " ); |
---|
| 1130 | } |
---|
| 1131 | else { |
---|
| 1132 | attr['class'] = meta[ i ].substring( 1 ); |
---|
| 1133 | } |
---|
| 1134 | } |
---|
| 1135 | // attribute: foo=bar |
---|
| 1136 | else if ( /\=/.test( meta[ i ] ) ) { |
---|
| 1137 | var s = meta[ i ].split( /\=/ ); |
---|
| 1138 | attr[ s[ 0 ] ] = s[ 1 ]; |
---|
| 1139 | } |
---|
| 1140 | } |
---|
| 1141 | |
---|
| 1142 | return attr; |
---|
| 1143 | } |
---|
| 1144 | |
---|
| 1145 | function split_meta_hash( meta_string ) { |
---|
| 1146 | var meta = meta_string.split( "" ), |
---|
| 1147 | parts = [ "" ], |
---|
| 1148 | in_quotes = false; |
---|
| 1149 | |
---|
| 1150 | while ( meta.length ) { |
---|
| 1151 | var letter = meta.shift(); |
---|
| 1152 | switch ( letter ) { |
---|
| 1153 | case " " : |
---|
| 1154 | // if we're in a quoted section, keep it |
---|
| 1155 | if ( in_quotes ) { |
---|
| 1156 | parts[ parts.length - 1 ] += letter; |
---|
| 1157 | } |
---|
| 1158 | // otherwise make a new part |
---|
| 1159 | else { |
---|
| 1160 | parts.push( "" ); |
---|
| 1161 | } |
---|
| 1162 | break; |
---|
| 1163 | case "'" : |
---|
| 1164 | case '"' : |
---|
| 1165 | // reverse the quotes and move straight on |
---|
| 1166 | in_quotes = !in_quotes; |
---|
| 1167 | break; |
---|
| 1168 | case "\\" : |
---|
| 1169 | // shift off the next letter to be used straight away. |
---|
| 1170 | // it was escaped so we'll keep it whatever it is |
---|
| 1171 | letter = meta.shift(); |
---|
| 1172 | default : |
---|
| 1173 | parts[ parts.length - 1 ] += letter; |
---|
| 1174 | break; |
---|
| 1175 | } |
---|
| 1176 | } |
---|
| 1177 | |
---|
| 1178 | return parts; |
---|
| 1179 | } |
---|
| 1180 | |
---|
| 1181 | Markdown.dialects.Maruku.block.document_meta = function document_meta( block, next ) { |
---|
| 1182 | // we're only interested in the first block |
---|
| 1183 | if ( block.lineNumber > 1 ) return undefined; |
---|
| 1184 | |
---|
| 1185 | // document_meta blocks consist of one or more lines of `Key: Value\n` |
---|
| 1186 | if ( ! block.match( /^(?:\w+:.*\n)*\w+:.*$/ ) ) return undefined; |
---|
| 1187 | |
---|
| 1188 | // make an attribute node if it doesn't exist |
---|
| 1189 | if ( !extract_attr( this.tree ) ) { |
---|
| 1190 | this.tree.splice( 1, 0, {} ); |
---|
| 1191 | } |
---|
| 1192 | |
---|
| 1193 | var pairs = block.split( /\n/ ); |
---|
| 1194 | for ( p in pairs ) { |
---|
| 1195 | var m = pairs[ p ].match( /(\w+):\s*(.*)$/ ), |
---|
| 1196 | key = m[ 1 ].toLowerCase(), |
---|
| 1197 | value = m[ 2 ]; |
---|
| 1198 | |
---|
| 1199 | this.tree[ 1 ][ key ] = value; |
---|
| 1200 | } |
---|
| 1201 | |
---|
| 1202 | // document_meta produces no content! |
---|
| 1203 | return []; |
---|
| 1204 | }; |
---|
| 1205 | |
---|
| 1206 | Markdown.dialects.Maruku.block.block_meta = function block_meta( block, next ) { |
---|
| 1207 | // check if the last line of the block is an meta hash |
---|
| 1208 | var m = block.match( /(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/ ); |
---|
| 1209 | if ( !m ) return undefined; |
---|
| 1210 | |
---|
| 1211 | // process the meta hash |
---|
| 1212 | var attr = this.dialect.processMetaHash( m[ 2 ] ); |
---|
| 1213 | |
---|
| 1214 | var hash; |
---|
| 1215 | |
---|
| 1216 | // if we matched ^ then we need to apply meta to the previous block |
---|
| 1217 | if ( m[ 1 ] === "" ) { |
---|
| 1218 | var node = this.tree[ this.tree.length - 1 ]; |
---|
| 1219 | hash = extract_attr( node ); |
---|
| 1220 | |
---|
| 1221 | // if the node is a string (rather than JsonML), bail |
---|
| 1222 | if ( typeof node === "string" ) return undefined; |
---|
| 1223 | |
---|
| 1224 | // create the attribute hash if it doesn't exist |
---|
| 1225 | if ( !hash ) { |
---|
| 1226 | hash = {}; |
---|
| 1227 | node.splice( 1, 0, hash ); |
---|
| 1228 | } |
---|
| 1229 | |
---|
| 1230 | // add the attributes in |
---|
| 1231 | for ( a in attr ) { |
---|
| 1232 | hash[ a ] = attr[ a ]; |
---|
| 1233 | } |
---|
| 1234 | |
---|
| 1235 | // return nothing so the meta hash is removed |
---|
| 1236 | return []; |
---|
| 1237 | } |
---|
| 1238 | |
---|
| 1239 | // pull the meta hash off the block and process what's left |
---|
| 1240 | var b = block.replace( /\n.*$/, "" ), |
---|
| 1241 | result = this.processBlock( b, [] ); |
---|
| 1242 | |
---|
| 1243 | // get or make the attributes hash |
---|
| 1244 | hash = extract_attr( result[ 0 ] ); |
---|
| 1245 | if ( !hash ) { |
---|
| 1246 | hash = {}; |
---|
| 1247 | result[ 0 ].splice( 1, 0, hash ); |
---|
| 1248 | } |
---|
| 1249 | |
---|
| 1250 | // attach the attributes to the block |
---|
| 1251 | for ( a in attr ) { |
---|
| 1252 | hash[ a ] = attr[ a ]; |
---|
| 1253 | } |
---|
| 1254 | |
---|
| 1255 | return result; |
---|
| 1256 | }; |
---|
| 1257 | |
---|
| 1258 | Markdown.dialects.Maruku.block.definition_list = function definition_list( block, next ) { |
---|
| 1259 | // one or more terms followed by one or more definitions, in a single block |
---|
| 1260 | var tight = /^((?:[^\s:].*\n)+):\s+([\s\S]+)$/, |
---|
| 1261 | list = [ "dl" ], |
---|
| 1262 | i; |
---|
| 1263 | |
---|
| 1264 | // see if we're dealing with a tight or loose block |
---|
| 1265 | if ( ( m = block.match( tight ) ) ) { |
---|
| 1266 | // pull subsequent tight DL blocks out of `next` |
---|
| 1267 | var blocks = [ block ]; |
---|
| 1268 | while ( next.length && tight.exec( next[ 0 ] ) ) { |
---|
| 1269 | blocks.push( next.shift() ); |
---|
| 1270 | } |
---|
| 1271 | |
---|
| 1272 | for ( var b = 0; b < blocks.length; ++b ) { |
---|
| 1273 | var m = blocks[ b ].match( tight ), |
---|
| 1274 | terms = m[ 1 ].replace( /\n$/, "" ).split( /\n/ ), |
---|
| 1275 | defns = m[ 2 ].split( /\n:\s+/ ); |
---|
| 1276 | |
---|
| 1277 | // print( uneval( m ) ); |
---|
| 1278 | |
---|
| 1279 | for ( i = 0; i < terms.length; ++i ) { |
---|
| 1280 | list.push( [ "dt", terms[ i ] ] ); |
---|
| 1281 | } |
---|
| 1282 | |
---|
| 1283 | for ( i = 0; i < defns.length; ++i ) { |
---|
| 1284 | // run inline processing over the definition |
---|
| 1285 | list.push( [ "dd" ].concat( this.processInline( defns[ i ].replace( /(\n)\s+/, "$1" ) ) ) ); |
---|
| 1286 | } |
---|
| 1287 | } |
---|
| 1288 | } |
---|
| 1289 | else { |
---|
| 1290 | return undefined; |
---|
| 1291 | } |
---|
| 1292 | |
---|
| 1293 | return [ list ]; |
---|
| 1294 | }; |
---|
| 1295 | |
---|
| 1296 | Markdown.dialects.Maruku.inline[ "{:" ] = function inline_meta( text, matches, out ) { |
---|
| 1297 | if ( !out.length ) { |
---|
| 1298 | return [ 2, "{:" ]; |
---|
| 1299 | } |
---|
| 1300 | |
---|
| 1301 | // get the preceeding element |
---|
| 1302 | var before = out[ out.length - 1 ]; |
---|
| 1303 | |
---|
| 1304 | if ( typeof before === "string" ) { |
---|
| 1305 | return [ 2, "{:" ]; |
---|
| 1306 | } |
---|
| 1307 | |
---|
| 1308 | // match a meta hash |
---|
| 1309 | var m = text.match( /^\{:\s*((?:\\\}|[^\}])*)\s*\}/ ); |
---|
| 1310 | |
---|
| 1311 | // no match, false alarm |
---|
| 1312 | if ( !m ) { |
---|
| 1313 | return [ 2, "{:" ]; |
---|
| 1314 | } |
---|
| 1315 | |
---|
| 1316 | // attach the attributes to the preceeding element |
---|
| 1317 | var meta = this.dialect.processMetaHash( m[ 1 ] ), |
---|
| 1318 | attr = extract_attr( before ); |
---|
| 1319 | |
---|
| 1320 | if ( !attr ) { |
---|
| 1321 | attr = {}; |
---|
| 1322 | before.splice( 1, 0, attr ); |
---|
| 1323 | } |
---|
| 1324 | |
---|
| 1325 | for ( var k in meta ) { |
---|
| 1326 | attr[ k ] = meta[ k ]; |
---|
| 1327 | } |
---|
| 1328 | |
---|
| 1329 | // cut out the string and replace it with nothing |
---|
| 1330 | return [ m[ 0 ].length, "" ]; |
---|
| 1331 | }; |
---|
| 1332 | |
---|
| 1333 | Markdown.buildBlockOrder ( Markdown.dialects.Maruku.block ); |
---|
| 1334 | Markdown.buildInlinePatterns( Markdown.dialects.Maruku.inline ); |
---|
| 1335 | |
---|
| 1336 | var isArray = Array.isArray || function(obj) { |
---|
| 1337 | return Object.prototype.toString.call(obj) == '[object Array]'; |
---|
| 1338 | }; |
---|
| 1339 | |
---|
| 1340 | var forEach; |
---|
| 1341 | // Don't mess with Array.prototype. Its not friendly |
---|
| 1342 | if ( Array.prototype.forEach ) { |
---|
| 1343 | forEach = function( arr, cb, thisp ) { |
---|
| 1344 | return arr.forEach( cb, thisp ); |
---|
| 1345 | }; |
---|
| 1346 | } |
---|
| 1347 | else { |
---|
| 1348 | forEach = function(arr, cb, thisp) { |
---|
| 1349 | for (var i = 0; i < arr.length; i++) { |
---|
| 1350 | cb.call(thisp || arr, arr[i], i, arr); |
---|
| 1351 | } |
---|
| 1352 | } |
---|
| 1353 | } |
---|
| 1354 | |
---|
| 1355 | function extract_attr( jsonml ) { |
---|
| 1356 | return isArray(jsonml) |
---|
| 1357 | && jsonml.length > 1 |
---|
| 1358 | && typeof jsonml[ 1 ] === "object" |
---|
| 1359 | && !( isArray(jsonml[ 1 ]) ) |
---|
| 1360 | ? jsonml[ 1 ] |
---|
| 1361 | : undefined; |
---|
| 1362 | } |
---|
| 1363 | |
---|
| 1364 | |
---|
| 1365 | |
---|
| 1366 | /** |
---|
| 1367 | * renderJsonML( jsonml[, options] ) -> String |
---|
| 1368 | * - jsonml (Array): JsonML array to render to XML |
---|
| 1369 | * - options (Object): options |
---|
| 1370 | * |
---|
| 1371 | * Converts the given JsonML into well-formed XML. |
---|
| 1372 | * |
---|
| 1373 | * The options currently understood are: |
---|
| 1374 | * |
---|
| 1375 | * - root (Boolean): wether or not the root node should be included in the |
---|
| 1376 | * output, or just its children. The default `false` is to not include the |
---|
| 1377 | * root itself. |
---|
| 1378 | */ |
---|
| 1379 | expose.renderJsonML = function( jsonml, options ) { |
---|
| 1380 | options = options || {}; |
---|
| 1381 | // include the root element in the rendered output? |
---|
| 1382 | options.root = options.root || false; |
---|
| 1383 | |
---|
| 1384 | var content = []; |
---|
| 1385 | |
---|
| 1386 | if ( options.root ) { |
---|
| 1387 | content.push( render_tree( jsonml ) ); |
---|
| 1388 | } |
---|
| 1389 | else { |
---|
| 1390 | jsonml.shift(); // get rid of the tag |
---|
| 1391 | if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) { |
---|
| 1392 | jsonml.shift(); // get rid of the attributes |
---|
| 1393 | } |
---|
| 1394 | |
---|
| 1395 | while ( jsonml.length ) { |
---|
| 1396 | content.push( render_tree( jsonml.shift() ) ); |
---|
| 1397 | } |
---|
| 1398 | } |
---|
| 1399 | |
---|
| 1400 | return content.join( "\n\n" ); |
---|
| 1401 | }; |
---|
| 1402 | |
---|
| 1403 | function escapeHTML( text ) { |
---|
| 1404 | return text.replace( /&/g, "&" ) |
---|
| 1405 | .replace( /</g, "<" ) |
---|
| 1406 | .replace( />/g, ">" ) |
---|
| 1407 | .replace( /"/g, """ ) |
---|
| 1408 | .replace( /'/g, "'" ); |
---|
| 1409 | } |
---|
| 1410 | |
---|
| 1411 | function render_tree( jsonml ) { |
---|
| 1412 | // basic case |
---|
| 1413 | if ( typeof jsonml === "string" ) { |
---|
| 1414 | return escapeHTML( jsonml ); |
---|
| 1415 | } |
---|
| 1416 | |
---|
| 1417 | var tag = jsonml.shift(), |
---|
| 1418 | attributes = {}, |
---|
| 1419 | content = []; |
---|
| 1420 | |
---|
| 1421 | if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) { |
---|
| 1422 | attributes = jsonml.shift(); |
---|
| 1423 | } |
---|
| 1424 | |
---|
| 1425 | while ( jsonml.length ) { |
---|
| 1426 | content.push( arguments.callee( jsonml.shift() ) ); |
---|
| 1427 | } |
---|
| 1428 | |
---|
| 1429 | var tag_attrs = ""; |
---|
| 1430 | for ( var a in attributes ) { |
---|
| 1431 | tag_attrs += " " + a + '="' + escapeHTML( attributes[ a ] ) + '"'; |
---|
| 1432 | } |
---|
| 1433 | |
---|
| 1434 | // be careful about adding whitespace here for inline elements |
---|
| 1435 | if ( tag == "img" || tag == "br" || tag == "hr" ) { |
---|
| 1436 | return "<"+ tag + tag_attrs + "/>"; |
---|
| 1437 | } |
---|
| 1438 | else { |
---|
| 1439 | return "<"+ tag + tag_attrs + ">" + content.join( "" ) + "</" + tag + ">"; |
---|
| 1440 | } |
---|
| 1441 | } |
---|
| 1442 | |
---|
| 1443 | function convert_tree_to_html( tree, references, options ) { |
---|
| 1444 | var i; |
---|
| 1445 | options = options || {}; |
---|
| 1446 | |
---|
| 1447 | // shallow clone |
---|
| 1448 | var jsonml = tree.slice( 0 ); |
---|
| 1449 | |
---|
| 1450 | if (typeof options.preprocessTreeNode === "function") { |
---|
| 1451 | jsonml = options.preprocessTreeNode(jsonml, references); |
---|
| 1452 | } |
---|
| 1453 | |
---|
| 1454 | // Clone attributes if they exist |
---|
| 1455 | var attrs = extract_attr( jsonml ); |
---|
| 1456 | if ( attrs ) { |
---|
| 1457 | jsonml[ 1 ] = {}; |
---|
| 1458 | for ( i in attrs ) { |
---|
| 1459 | jsonml[ 1 ][ i ] = attrs[ i ]; |
---|
| 1460 | } |
---|
| 1461 | attrs = jsonml[ 1 ]; |
---|
| 1462 | } |
---|
| 1463 | |
---|
| 1464 | // basic case |
---|
| 1465 | if ( typeof jsonml === "string" ) { |
---|
| 1466 | return jsonml; |
---|
| 1467 | } |
---|
| 1468 | |
---|
| 1469 | // convert this node |
---|
| 1470 | switch ( jsonml[ 0 ] ) { |
---|
| 1471 | case "header": |
---|
| 1472 | jsonml[ 0 ] = "h" + jsonml[ 1 ].level; |
---|
| 1473 | delete jsonml[ 1 ].level; |
---|
| 1474 | break; |
---|
| 1475 | case "bulletlist": |
---|
| 1476 | jsonml[ 0 ] = "ul"; |
---|
| 1477 | break; |
---|
| 1478 | case "numberlist": |
---|
| 1479 | jsonml[ 0 ] = "ol"; |
---|
| 1480 | break; |
---|
| 1481 | case "listitem": |
---|
| 1482 | jsonml[ 0 ] = "li"; |
---|
| 1483 | break; |
---|
| 1484 | case "para": |
---|
| 1485 | jsonml[ 0 ] = "p"; |
---|
| 1486 | break; |
---|
| 1487 | case "markdown": |
---|
| 1488 | jsonml[ 0 ] = "html"; |
---|
| 1489 | if ( attrs ) delete attrs.references; |
---|
| 1490 | break; |
---|
| 1491 | case "code_block": |
---|
| 1492 | jsonml[ 0 ] = "pre"; |
---|
| 1493 | i = attrs ? 2 : 1; |
---|
| 1494 | var code = [ "code" ]; |
---|
| 1495 | code.push.apply( code, jsonml.splice( i ) ); |
---|
| 1496 | jsonml[ i ] = code; |
---|
| 1497 | break; |
---|
| 1498 | case "inlinecode": |
---|
| 1499 | jsonml[ 0 ] = "code"; |
---|
| 1500 | break; |
---|
| 1501 | case "img": |
---|
| 1502 | jsonml[ 1 ].src = jsonml[ 1 ].href; |
---|
| 1503 | delete jsonml[ 1 ].href; |
---|
| 1504 | break; |
---|
| 1505 | case "linebreak": |
---|
| 1506 | jsonml[ 0 ] = "br"; |
---|
| 1507 | break; |
---|
| 1508 | case "link": |
---|
| 1509 | jsonml[ 0 ] = "a"; |
---|
| 1510 | break; |
---|
| 1511 | case "link_ref": |
---|
| 1512 | jsonml[ 0 ] = "a"; |
---|
| 1513 | |
---|
| 1514 | // grab this ref and clean up the attribute node |
---|
| 1515 | var ref = references[ attrs.ref ]; |
---|
| 1516 | |
---|
| 1517 | // if the reference exists, make the link |
---|
| 1518 | if ( ref ) { |
---|
| 1519 | delete attrs.ref; |
---|
| 1520 | |
---|
| 1521 | // add in the href and title, if present |
---|
| 1522 | attrs.href = ref.href; |
---|
| 1523 | if ( ref.title ) { |
---|
| 1524 | attrs.title = ref.title; |
---|
| 1525 | } |
---|
| 1526 | |
---|
| 1527 | // get rid of the unneeded original text |
---|
| 1528 | delete attrs.original; |
---|
| 1529 | } |
---|
| 1530 | // the reference doesn't exist, so revert to plain text |
---|
| 1531 | else { |
---|
| 1532 | return attrs.original; |
---|
| 1533 | } |
---|
| 1534 | break; |
---|
| 1535 | case "img_ref": |
---|
| 1536 | jsonml[ 0 ] = "img"; |
---|
| 1537 | |
---|
| 1538 | // grab this ref and clean up the attribute node |
---|
| 1539 | var ref = references[ attrs.ref ]; |
---|
| 1540 | |
---|
| 1541 | // if the reference exists, make the link |
---|
| 1542 | if ( ref ) { |
---|
| 1543 | delete attrs.ref; |
---|
| 1544 | |
---|
| 1545 | // add in the href and title, if present |
---|
| 1546 | attrs.src = ref.href; |
---|
| 1547 | if ( ref.title ) { |
---|
| 1548 | attrs.title = ref.title; |
---|
| 1549 | } |
---|
| 1550 | |
---|
| 1551 | // get rid of the unneeded original text |
---|
| 1552 | delete attrs.original; |
---|
| 1553 | } |
---|
| 1554 | // the reference doesn't exist, so revert to plain text |
---|
| 1555 | else { |
---|
| 1556 | return attrs.original; |
---|
| 1557 | } |
---|
| 1558 | break; |
---|
| 1559 | } |
---|
| 1560 | |
---|
| 1561 | // convert all the children |
---|
| 1562 | i = 1; |
---|
| 1563 | |
---|
| 1564 | // deal with the attribute node, if it exists |
---|
| 1565 | if ( attrs ) { |
---|
| 1566 | // if there are keys, skip over it |
---|
| 1567 | for ( var key in jsonml[ 1 ] ) { |
---|
| 1568 | i = 2; |
---|
| 1569 | } |
---|
| 1570 | // if there aren't, remove it |
---|
| 1571 | if ( i === 1 ) { |
---|
| 1572 | jsonml.splice( i, 1 ); |
---|
| 1573 | } |
---|
| 1574 | } |
---|
| 1575 | |
---|
| 1576 | for ( ; i < jsonml.length; ++i ) { |
---|
| 1577 | jsonml[ i ] = arguments.callee( jsonml[ i ], references, options ); |
---|
| 1578 | } |
---|
| 1579 | |
---|
| 1580 | return jsonml; |
---|
| 1581 | } |
---|
| 1582 | |
---|
| 1583 | |
---|
| 1584 | // merges adjacent text nodes into a single node |
---|
| 1585 | function merge_text_nodes( jsonml ) { |
---|
| 1586 | // skip the tag name and attribute hash |
---|
| 1587 | var i = extract_attr( jsonml ) ? 2 : 1; |
---|
| 1588 | |
---|
| 1589 | while ( i < jsonml.length ) { |
---|
| 1590 | // if it's a string check the next item too |
---|
| 1591 | if ( typeof jsonml[ i ] === "string" ) { |
---|
| 1592 | if ( i + 1 < jsonml.length && typeof jsonml[ i + 1 ] === "string" ) { |
---|
| 1593 | // merge the second string into the first and remove it |
---|
| 1594 | jsonml[ i ] += jsonml.splice( i + 1, 1 )[ 0 ]; |
---|
| 1595 | } |
---|
| 1596 | else { |
---|
| 1597 | ++i; |
---|
| 1598 | } |
---|
| 1599 | } |
---|
| 1600 | // if it's not a string recurse |
---|
| 1601 | else { |
---|
| 1602 | arguments.callee( jsonml[ i ] ); |
---|
| 1603 | ++i; |
---|
| 1604 | } |
---|
| 1605 | } |
---|
| 1606 | } |
---|
| 1607 | |
---|
| 1608 | } )( (function() { |
---|
| 1609 | if ( typeof exports === "undefined" ) { |
---|
| 1610 | window.markdown = {}; |
---|
| 1611 | return window.markdown; |
---|
| 1612 | } |
---|
| 1613 | else { |
---|
| 1614 | return exports; |
---|
| 1615 | } |
---|
| 1616 | } )() ); |
---|