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 | } )() ); |
---|