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 <[email protected]> (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 | "![": function image( text ) { |
||
811 | |||
812 | // Unlike images, alt text is plain text only. no other elements are |
||
813 | // allowed in there |
||
814 | |||
815 | // ![Alt text](/path/to/img.jpg "Optional title") |
||
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() ) ); |
||
0 ignored issues
–
show
Compatibility
introduced
by
Loading history...
|
|||
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 ); |
||
0 ignored issues
–
show
|
|||
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 ] ); |
||
0 ignored issues
–
show
|
|||
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 | } )() ); |