1
YUI.add('gallery-markdown', function(Y) {
3
// Released under MIT license
4
// Copyright (c) 2009-2010 Dominic Baggott
5
// Copyright (c) 2009-2010 Ash Berlin
6
// Copyright (c) 2011 Christoph Dorn <christoph@christophdorn.com> (http://www.christophdorn.com)
9
A wrapper around the fantastic Markdown library at https://github.com/evilstreak/markdown-js
11
@module gallery-markdown
16
A YUI wrapper around the fantastic Markdown library at https://github.com/evilstreak/markdown-js.
17
This supports several dialects and works very well. I (jshirley) am not the author, merely the one maintaining the YUI wrapper.
19
Below is the original documentation:
21
Markdown processing in Javascript done right. We have very particular views
22
on what constitutes 'right' which include:
24
- produces well-formed HTML (this means that em and strong nesting is
27
- has an intermediate representation to allow processing of parsed data (We
28
in fact have two, both as [JsonML]: a markdown tree and an HTML tree).
30
- is easily extensible to add new dialects without having to rewrite the
31
entire parsing mechanics
33
- has a good test suite
35
This implementation fulfills all of these (except that the test suite could
36
do with expanding to automatically run all the fixtures from other Markdown
39
##### Intermediate Representation
41
*TODO* Talk about this :) Its JsonML, but document the node names we use.
43
[JsonML]: http://jsonml.org/ "JSON Markup Language"
49
var Markdown = expose.Markdown = function Markdown(dialect) {
50
switch (typeof dialect) {
52
this.dialect = Markdown.dialects.Gruber;
55
this.dialect = dialect;
58
if (dialect in Markdown.dialects) {
59
this.dialect = Markdown.dialects[dialect];
62
throw new Error("Unknown Markdown dialect '" + String(dialect) + "'");
67
this.strong_state = [];
68
this.debug_indent = "";
72
Parse `markdown` and return a markdown document as a Markdown.JsonML tree.
75
@param markdown {String} The Markdown string to parse
76
@param dialect {String} The Markdown dialect to use, defaults to gruber
80
expose.parse = function( source, dialect ) {
81
// dialect will default if undefined
82
var md = new Markdown( dialect );
83
return md.toTree( source );
87
Take markdown (either as a string or as a JsonML tree) and run it through
88
[[toHTMLTree]] then turn it into a well-formated HTML fragment.
92
@param source {String} markdown string to parse
93
@param dialect {String} dialect to use
96
expose.toHTML = function toHTML( source , dialect , options ) {
97
var input = expose.toHTMLTree( source , dialect , options );
99
return expose.renderJsonML( input );
103
Turn markdown into HTML, represented as a JsonML tree. If a string is given
104
to this function, it is first parsed into a markdown tree by calling
109
@param markdown {String | Object } markdown string to parse or already parsed tree
110
@param dialect {String} the dialect to use, defaults to gruber
112
expose.toHTMLTree = function toHTMLTree( input, dialect , options ) {
113
// convert string input to an MD tree
114
if ( typeof input ==="string" ) input = this.parse( input, dialect );
116
// Now convert the MD tree to an HTML tree
118
// remove references from the tree
119
var attrs = extract_attr( input ),
122
if ( attrs && attrs.references ) {
123
refs = attrs.references;
126
var html = convert_tree_to_html( input, refs , options );
127
merge_text_nodes( html );
131
// For Spidermonkey based engines
132
function mk_block_toSource() {
133
return "Markdown.mk_block( " +
134
uneval(this.toString()) +
136
uneval(this.trailing) +
138
uneval(this.lineNumber) +
143
function mk_block_inspect() {
144
var util = require('util');
145
return "Markdown.mk_block( " +
146
util.inspect(this.toString()) +
148
util.inspect(this.trailing) +
150
util.inspect(this.lineNumber) +
155
var mk_block = Markdown.mk_block = function(block, trail, line) {
156
// Be helpful for default case in tests.
157
if ( arguments.length == 1 ) trail = "\n\n";
159
var s = new String(block);
161
// To make it clear its not just a string
162
s.inspect = mk_block_inspect;
163
s.toSource = mk_block_toSource;
165
if (line != undefined)
171
function count_lines( str ) {
173
while ( ( i = str.indexOf('\n', i+1) ) !== -1) n++;
177
// Internal - split source into rough blocks
178
Markdown.prototype.split_blocks = function splitBlocks( input, startLine ) {
179
// [\s\S] matches _anything_ (newline or space)
180
var re = /([\s\S]+?)($|\n(?:\s*\n|$)+)/g,
186
if ( ( m = /^(\s*\n)/.exec(input) ) != null ) {
187
// skip (but count) leading blank lines
188
line_no += count_lines( m[0] );
189
re.lastIndex = m[0].length;
192
while ( ( m = re.exec(input) ) !== null ) {
193
blocks.push( mk_block( m[1], m[2], line_no ) );
194
line_no += count_lines( m[0] );
201
Process `block` and return an array of JsonML nodes representing `block`.
203
It does this by asking each block level function in the dialect to process
204
the block until one can. Succesful handling is indicated by returning an
205
array (with zero or more JsonML nodes), failure by a false value.
207
Blocks handlers are responsible for calling [[Markdown#processInline]]
208
themselves as appropriate.
210
If the blocks were split incorrectly or adjacent blocks need collapsing you
211
can adjust `next` in place using shift/splice etc.
213
If any of this default behaviour is not right for the dialect, you can
214
define a `__call__` method on the dialect that will get invoked to handle
215
the block processing.
220
@param block {String} the block to process
221
@param next {Array} following blocks
223
Markdown.prototype.processBlock = function processBlock( block, next ) {
224
var cbs = this.dialect.block,
227
if ( "__call__" in cbs ) {
228
return cbs.__call__.call(this, block, next);
231
for ( var i = 0; i < ord.length; i++ ) {
232
//D:this.debug( "Testing", ord[i] );
233
var res = cbs[ ord[i] ].call( this, block, next );
235
//D:this.debug(" matched");
236
if ( !isArray(res) || ( res.length > 0 && !( isArray(res[0]) ) ) )
237
this.debug(ord[i], "didn't return a proper array");
238
//D:this.debug( "" );
243
// Uhoh! no match! Should we throw an error?
247
Markdown.prototype.processInline = function processInline( block ) {
248
return this.dialect.inline.__call__.call( this, String( block ) );
252
Parse `source` into a JsonML tree representing the markdown document.
257
@param source {String} markdown source to parse.
258
@param custom_root {Object} A previous tree to append to
260
// custom_tree means set this.tree to `custom_tree` and restore old value on return
261
Markdown.prototype.toTree = function toTree( source, custom_root ) {
262
var blocks = source instanceof Array ? source : this.split_blocks( source );
264
// Make tree a member variable so its easier to mess with in extensions
265
var old_tree = this.tree;
267
this.tree = custom_root || this.tree || [ "markdown" ];
270
while ( blocks.length ) {
271
var b = this.processBlock( blocks.shift(), blocks );
273
// Reference blocks and the like won't return any content
274
if ( !b.length ) continue blocks;
276
this.tree.push.apply( this.tree, b );
282
this.tree = old_tree;
288
Markdown.prototype.debug = function () {
289
var args = Array.prototype.slice.call( arguments);
290
args.unshift(this.debug_indent);
291
if (typeof print !== "undefined")
292
print.apply( print, args );
293
if (typeof console !== "undefined" && typeof console.log !== "undefined")
294
console.log.apply( null, args );
297
Markdown.prototype.loop_re_over_block = function( re, block, cb ) {
298
// Dont use /g regexps with this
302
while ( b.length && (m = re.exec(b) ) != null) {
303
b = b.substr( m[0].length );
312
* Namespace of built-in dialects.
314
Markdown.dialects = {};
317
* Markdown.dialects.Gruber
319
* The default dialect that follows the rules set out by John Gruber's
320
* markdown.pl as closely as possible. Well actually we follow the behaviour of
321
* that script which in some places is not exactly what the syntax web page
324
Markdown.dialects.Gruber = {
326
atxHeader: function atxHeader( block, next ) {
327
var m = block.match( /^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/ );
329
if ( !m ) return undefined;
331
var header = [ "header", { level: m[ 1 ].length } ];
332
Array.prototype.push.apply(header, this.processInline(m[ 2 ]));
334
if ( m[0].length < block.length )
335
next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) );
340
setextHeader: function setextHeader( block, next ) {
341
var m = block.match( /^(.*)\n([-=])\2\2+(?:\n|$)/ );
343
if ( !m ) return undefined;
345
var level = ( m[ 2 ] === "=" ) ? 1 : 2;
346
var header = [ "header", { level : level }, m[ 1 ] ];
348
if ( m[0].length < block.length )
349
next.unshift( mk_block( block.substr( m[0].length ), block.trailing, block.lineNumber + 2 ) );
354
code: function code( block, next ) {
357
// should be a code block followed by a paragraph. Fun
359
// There might also be adjacent code block to merge.
362
re = /^(?: {0,3}\t| {4})(.*)\n?/,
365
// 4 spaces + content
366
if ( !block.match( re ) ) return undefined;
370
// Now pull out the rest of the lines
371
var b = this.loop_re_over_block(
372
re, block.valueOf(), function( m ) { ret.push( m[1] ); } );
375
// Case alluded to in first comment. push it back on as a new block
376
next.unshift( mk_block(b, block.trailing) );
379
else if (next.length) {
380
// Check the next block - it might be code too
381
if ( !next[0].match( re ) ) break block_search;
383
// Pull how how many blanks lines follow - minus two to account for .join
384
ret.push ( block.trailing.replace(/[^\n]/g, '').substring(2) );
386
block = next.shift();
393
return [ [ "code_block", ret.join("\n") ] ];
396
horizRule: function horizRule( block, next ) {
397
// this needs to find any hr in the block to handle abutting blocks
398
var m = block.match( /^(?:([\s\S]*?)\n)?[ \t]*([-_*])(?:[ \t]*\2){2,}[ \t]*(?:\n([\s\S]*))?$/ );
404
var jsonml = [ [ "hr" ] ];
406
// if there's a leading abutting block, process it
408
jsonml.unshift.apply( jsonml, this.processBlock( m[ 1 ], [] ) );
411
// if there's a trailing abutting block, stick it into next
413
next.unshift( mk_block( m[ 3 ] ) );
419
// There are two types of lists. Tight and loose. Tight lists have no whitespace
420
// between the items (and result in text just in the <li>) and loose lists,
421
// which have an empty line between list items, resulting in (one or more)
422
// paragraphs inside the <li>.
424
// There are all sorts weird edge cases about the original markdown.pl's
425
// handling of lists:
427
// * Nested lists are supposed to be indented by four chars per level. But
428
// if they aren't, you can get a nested list by indenting by less than
429
// four so long as the indent doesn't match an indent of an existing list
430
// item in the 'nest stack'.
432
// * The type of the list (bullet or number) is controlled just by the
433
// first item at the indent. Subsequent changes are ignored unless they
434
// are for nested lists
436
lists: (function( ) {
437
// Use a closure to hide a few variables.
438
var any_list = "[*+-]|\\d+\\.",
439
bullet_list = /[*+-]/,
440
number_list = /\d+\./,
441
// Capture leading indent as it matters for determining nested lists.
442
is_list_re = new RegExp( "^( {0,3})(" + any_list + ")[ \t]+" ),
443
indent_re = "(?: {0,3}\\t| {4})";
445
// TODO: Cache this regexp for certain depths.
446
// Create a regexp suitable for matching an li for a given stack depth
447
function regex_for_depth( depth ) {
450
// m[1] = indent, m[2] = list_type
451
"(?:^(" + indent_re + "{0," + depth + "} {0,3})(" + any_list + ")\\s+)|" +
453
"(^" + indent_re + "{0," + (depth-1) + "}[ ]{0,4})"
456
function expand_tab( input ) {
457
return input.replace( / {0,3}\t/g, " " );
460
// Add inline content `inline` to `li`. inline comes from processInline
461
// so is an array of content
462
function add(li, loose, inline, nl) {
464
li.push( [ "para" ].concat(inline) );
467
// Hmmm, should this be any block level element or just paras?
468
var add_to = li[li.length -1] instanceof Array && li[li.length - 1][0] == "para"
472
// If there is already some content in this list, add the new line in
473
if (nl && li.length > 1) inline.unshift(nl);
475
for (var i=0; i < inline.length; i++) {
476
var what = inline[i],
477
is_str = typeof what == "string";
478
if (is_str && add_to.length > 1 && typeof add_to[add_to.length-1] == "string" ) {
479
add_to[ add_to.length-1 ] += what;
487
// contained means have an indent greater than the current one. On
488
// *every* line in the block
489
function get_contained_blocks( depth, blocks ) {
491
var re = new RegExp( "^(" + indent_re + "{" + depth + "}.*?\\n?)*$" ),
492
replace = new RegExp("^" + indent_re + "{" + depth + "}", "gm"),
495
while ( blocks.length > 0 ) {
496
if ( re.exec( blocks[0] ) ) {
497
var b = blocks.shift(),
498
// Now remove that indent
499
x = b.replace( replace, "");
501
ret.push( mk_block( x, b.trailing, b.lineNumber ) );
508
// passed to stack.forEach to turn list items up the stack into paras
509
function paragraphify(s, i, stack) {
511
var last_li = list[list.length-1];
513
if (last_li[1] instanceof Array && last_li[1][0] == "para") {
516
if (i+1 == stack.length) {
518
// Keep the same array, but replace the contents
519
last_li.push( ["para"].concat( last_li.splice(1) ) );
522
var sublist = last_li.pop();
523
last_li.push( ["para"].concat( last_li.splice(1) ), sublist );
527
// The matcher function
528
return function( block, next ) {
529
var m = block.match( is_list_re );
530
if ( !m ) return undefined;
532
function make_list( m ) {
533
var list = bullet_list.exec( m[2] )
537
stack.push( { list: list, indent: m[1] } );
542
var stack = [], // Stack of lists for nesting.
543
list = make_list( m ),
546
ret = [ stack[0].list ],
549
// Loop to search over block looking for inner block elements and loose lists
552
// Split into lines preserving new lines at end of line
553
var lines = block.split( /(?=\n)/ );
555
// We have to grab all lines for a li and call processInline on them
556
// once as there are some inline things that can span lines.
557
var li_accumulate = "";
559
// Loop over the lines in this block looking for tight lists.
561
for (var line_no=0; line_no < lines.length; line_no++) {
563
l = lines[line_no].replace(/^\n/, function(n) { nl = n; return ""; });
565
// TODO: really should cache this
566
var line_re = regex_for_depth( stack.length );
568
m = l.match( line_re );
569
//print( "line:", uneval(l), "\nline match:", uneval(m) );
571
// We have a list item
572
if ( m[1] !== undefined ) {
573
// Process the previous list item, if any
574
if ( li_accumulate.length ) {
575
add( last_li, loose, this.processInline( li_accumulate ), nl );
576
// Loose mode will have been dealt with. Reset it
581
m[1] = expand_tab( m[1] );
582
var wanted_depth = Math.floor(m[1].length/4)+1;
583
//print( "want:", wanted_depth, "stack:", stack.length);
584
if ( wanted_depth > stack.length ) {
585
// Deep enough for a nested list outright
586
//print ( "new nested list" );
587
list = make_list( m );
588
last_li.push( list );
589
last_li = list[1] = [ "listitem" ];
592
// We aren't deep enough to be strictly a new level. This is
593
// where Md.pl goes nuts. If the indent matches a level in the
594
// stack, put it there, else put it one deeper then the
595
// wanted_depth deserves.
597
for (i = 0; i < stack.length; i++) {
598
if ( stack[ i ].indent != m[1] ) continue;
599
list = stack[ i ].list;
606
//print("not found. l:", uneval(l));
608
if (wanted_depth <= stack.length) {
609
stack.splice(wanted_depth);
610
//print("Desired depth now", wanted_depth, "stack:", stack.length);
611
list = stack[wanted_depth-1].list;
612
//print("list:", uneval(list) );
615
//print ("made new stack for messy indent");
621
//print( uneval(list), "last", list === stack[stack.length-1].list );
622
last_li = [ "listitem" ];
624
} // end depth of shenegains
629
if (l.length > m[0].length) {
630
li_accumulate += nl + l.substr( m[0].length );
634
if ( li_accumulate.length ) {
635
add( last_li, loose, this.processInline( li_accumulate ), nl );
636
// Loose mode will have been dealt with. Reset it
641
// Look at the next block - we might have a loose list. Or an extra
642
// paragraph for the current li
643
var contained = get_contained_blocks( stack.length, next );
645
// Deal with code blocks or properly nested lists
646
if (contained.length > 0) {
647
// Make sure all listitems up the stack are paragraphs
648
forEach( stack, paragraphify, this);
650
last_li.push.apply( last_li, this.toTree( contained, [] ) );
653
var next_block = next[0] && next[0].valueOf() || "";
655
if ( next_block.match(is_list_re) || next_block.match( /^ / ) ) {
656
block = next.shift();
658
// Check for an HR following a list: features/lists/hr_abutting
659
var hr = this.dialect.block.horizRule( block, next );
662
ret.push.apply(ret, hr);
666
// Make sure all listitems up the stack are paragraphs
667
forEach( stack, paragraphify, this);
670
continue loose_search;
679
blockquote: function blockquote( block, next ) {
680
if ( !block.match( /^>/m ) )
685
// separate out the leading abutting block, if any
686
if ( block[ 0 ] != ">" ) {
687
var lines = block.split( /\n/ ),
690
// keep shifting lines until you find a crotchet
691
while ( lines.length && lines[ 0 ][ 0 ] != ">" ) {
692
prev.push( lines.shift() );
696
block = lines.join( "\n" );
697
jsonml.push.apply( jsonml, this.processBlock( prev.join( "\n" ), [] ) );
700
// if the next block is also a blockquote merge it in
701
while ( next.length && next[ 0 ][ 0 ] == ">" ) {
702
var b = next.shift();
703
block = new String(block + block.trailing + b);
704
block.trailing = b.trailing;
707
// Strip off the leading "> " and re-process as a block.
708
var input = block.replace( /^> ?/gm, '' ),
709
old_tree = this.tree;
710
jsonml.push( this.toTree( input, [ "blockquote" ] ) );
715
referenceDefn: function referenceDefn( block, next) {
716
var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/;
717
// interesting matches are [ , ref_id, url, , title, title ]
719
if ( !block.match(re) )
722
// make an attribute node if it doesn't exist
723
if ( !extract_attr( this.tree ) ) {
724
this.tree.splice( 1, 0, {} );
727
var attrs = extract_attr( this.tree );
729
// make a references hash if it doesn't exist
730
if ( attrs.references === undefined ) {
731
attrs.references = {};
734
var b = this.loop_re_over_block(re, block, function( m ) {
736
if ( m[2] && m[2][0] == '<' && m[2][m[2].length-1] == '>' )
737
m[2] = m[2].substring( 1, m[2].length - 1 );
739
var ref = attrs.references[ m[1].toLowerCase() ] = {
743
if (m[4] !== undefined)
745
else if (m[5] !== undefined)
751
next.unshift( mk_block( b, block.trailing ) );
756
para: function para( block, next ) {
757
// everything's a para!
758
return [ ["para"].concat( this.processInline( block ) ) ];
763
Markdown.dialects.Gruber.inline = {
765
__oneElement__: function oneElement( text, patterns_or_re, previous_nodes ) {
770
patterns_or_re = patterns_or_re || this.dialect.inline.__patterns__;
771
var re = new RegExp( "([\\s\\S]*?)(" + (patterns_or_re.source || patterns_or_re) + ")" );
776
return [ text.length, text ];
779
// Some un-interesting text matched. Return that first
780
return [ m[1].length, m[1] ];
784
if ( m[2] in this.dialect.inline ) {
785
res = this.dialect.inline[ m[2] ].call(
787
text.substr( m.index ), m, previous_nodes || [] );
789
// Default for now to make dev easier. just slurp special and output it.
790
res = res || [ m[2].length, m[2] ];
794
__call__: function inline( text, patterns ) {
800
//D:self.debug(" adding output", uneval(x));
801
if (typeof x == "string" && typeof out[out.length-1] == "string")
802
out[ out.length-1 ] += x;
807
while ( text.length > 0 ) {
808
res = this.dialect.inline.__oneElement__.call(this, text, patterns, out );
809
text = text.substr( res.shift() );
816
// These characters are intersting elsewhere, so have rules for them so that
817
// chunks of plain text blocks don't include them
821
"\\": function escaped( text ) {
822
// [ length of input processed, node/children to add... ]
823
// Only esacape: \ ` * _ { } [ ] ( ) # * + - . !
824
if ( text.match( /^\\[\\`\*_{}\[\]()#\+.!\-]/ ) )
825
return [ 2, text[1] ];
831
"![": function image( text ) {
833
// Unlike images, alt text is plain text only. no other elements are
836
// ![Alt text](/path/to/img.jpg "Optional title")
837
// 1 2 3 4 <--- captures
838
var m = text.match( /^!\[(.*?)\][ \t]*\([ \t]*(\S*)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/ );
841
if ( m[2] && m[2][0] == '<' && m[2][m[2].length-1] == '>' )
842
m[2] = m[2].substring( 1, m[2].length - 1 );
844
m[2] = this.dialect.inline.__call__.call( this, m[2], /\\/ )[0];
846
var attrs = { alt: m[1], href: m[2] || "" };
847
if ( m[4] !== undefined)
850
return [ m[0].length, [ "img", attrs ] ];
854
m = text.match( /^!\[(.*?)\][ \t]*\[(.*?)\]/ );
857
// We can't check if the reference is known here as it likely wont be
858
// found till after. Check it in md tree->hmtl tree conversion
859
return [ m[0].length, [ "img_ref", { alt: m[1], ref: m[2].toLowerCase(), original: m[0] } ] ];
862
// Just consume the '!['
866
"[": function link( text ) {
868
var orig = String(text);
869
// Inline content is possible inside `link text`
870
var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), ']' );
872
// No closing ']' found. Just consume the [
873
if ( !res ) return [ 1, '[' ];
875
var consumed = 1 + res[ 0 ],
880
// At this point the first [...] has been parsed. See what follows to find
881
// out which kind of link we are (reference or direct url)
882
text = text.substr( consumed );
884
// [link text](/path/to/img.jpg "Optional title")
885
// 1 2 3 <--- captures
886
// This will capture up to the last paren in the block. We then pull
887
// back based on if there a matching ones in the url
888
// ([here](/url/(test))
889
// The parens have to be balanced
890
var m = text.match( /^\s*\([ \t]*(\S+)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/ );
893
consumed += m[0].length;
895
if ( url && url[0] == '<' && url[url.length-1] == '>' )
896
url = url.substring( 1, url.length - 1 );
898
// If there is a title we don't have to worry about parens in the url
900
var open_parens = 1; // One open that isn't in the capture
901
for (var len = 0; len < url.length; len++) {
902
switch ( url[len] ) {
907
if ( --open_parens == 0) {
908
consumed -= url.length - len;
909
url = url.substring(0, len);
916
// Process escapes only
917
url = this.dialect.inline.__call__.call( this, url, /\\/ )[0];
919
attrs = { href: url || "" };
920
if ( m[3] !== undefined)
923
link = [ "link", attrs ].concat( children );
924
return [ consumed, link ];
929
m = text.match( /^\s*\[(.*?)\]/ );
933
consumed += m[ 0 ].length;
935
// [links][] uses links as its reference
936
attrs = { ref: ( m[ 1 ] || String(children) ).toLowerCase(), original: orig.substr( 0, consumed ) };
938
link = [ "link_ref", attrs ].concat( children );
940
// We can't check if the reference is known here as it likely wont be
941
// found till after. Check it in md tree->hmtl tree conversion.
942
// Store the original so that conversion can revert if the ref isn't found.
943
return [ consumed, link ];
947
// Only if id is plain (no formatting.)
948
if ( children.length == 1 && typeof children[0] == "string" ) {
950
attrs = { ref: children[0].toLowerCase(), original: orig.substr( 0, consumed ) };
951
link = [ "link_ref", attrs, children[0] ];
952
return [ consumed, link ];
955
// Just consume the '['
960
"<": function autoLink( text ) {
963
if ( ( m = text.match( /^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/ ) ) != null ) {
965
return [ m[0].length, [ "link", { href: "mailto:" + m[3] }, m[3] ] ];
968
else if ( m[2] == "mailto" ) {
969
return [ m[0].length, [ "link", { href: m[1] }, m[1].substr("mailto:".length ) ] ];
972
return [ m[0].length, [ "link", { href: m[1] }, m[1] ] ];
978
"`": function inlineCode( text ) {
979
// Inline code block. as many backticks as you like to start it
980
// Always skip over the opening ticks.
981
var m = text.match( /(`+)(([\s\S]*?)\1)/ );
984
return [ m[1].length + m[2].length, [ "inlinecode", m[3] ] ];
986
// TODO: No matching end code found - warn!
991
" \n": function lineBreak( text ) {
992
return [ 3, [ "linebreak" ] ];
997
// Meta Helper/generator method for em and strong handling
998
function strong_em( tag, md ) {
1000
var state_slot = tag + "_state",
1001
other_slot = tag == "strong" ? "em_state" : "strong_state";
1003
function CloseTag(len) {
1004
this.len_after = len;
1005
this.name = "close_" + md;
1008
return function ( text, orig_match ) {
1010
if (this[state_slot][0] == md) {
1011
// Most recent em is of this type
1012
//D:this.debug("closing", md);
1013
this[state_slot].shift();
1015
// "Consume" everything to go back to the recrusion in the else-block below
1016
return[ text.length, new CloseTag(text.length-md.length) ];
1019
// Store a clone of the em/strong states
1020
var other = this[other_slot].slice(),
1021
state = this[state_slot].slice();
1023
this[state_slot].unshift(md);
1025
//D:this.debug_indent += " ";
1028
var res = this.processInline( text.substr( md.length ) );
1029
//D:this.debug_indent = this.debug_indent.substr(2);
1031
var last = res[res.length - 1];
1033
//D:this.debug("processInline from", tag + ": ", uneval( res ) );
1035
var check = this[state_slot].shift();
1036
if (last instanceof CloseTag) {
1038
// We matched! Huzzah.
1039
var consumed = text.length - last.len_after;
1040
return [ consumed, [ tag ].concat(res) ];
1043
// Restore the state of the other kind. We might have mistakenly closed it.
1044
this[other_slot] = other;
1045
this[state_slot] = state;
1047
// We can't reuse the processed result as it could have wrong parsing contexts in it.
1048
return [ md.length, md ];
1051
}; // End returned function
1054
Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**");
1055
Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__");
1056
Markdown.dialects.Gruber.inline["*"] = strong_em("em", "*");
1057
Markdown.dialects.Gruber.inline["_"] = strong_em("em", "_");
1060
// Build default order from insertion order.
1061
Markdown.buildBlockOrder = function(d) {
1063
for ( var i in d ) {
1064
if ( i == "__order__" || i == "__call__" ) continue;
1070
// Build patterns for inline matcher
1071
Markdown.buildInlinePatterns = function(d) {
1074
for ( var i in d ) {
1075
// __foo__ is reserved and not a pattern
1076
if ( i.match( /^__.*__$/) ) continue;
1077
var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" )
1078
.replace( /\n/, "\\n" );
1079
patterns.push( i.length == 1 ? l : "(?:" + l + ")" );
1082
patterns = patterns.join("|");
1083
d.__patterns__ = patterns;
1084
//print("patterns:", uneval( patterns ) );
1086
var fn = d.__call__;
1087
d.__call__ = function(text, pattern) {
1088
if (pattern != undefined) {
1089
return fn.call(this, text, pattern);
1093
return fn.call(this, text, patterns);
1098
Markdown.DialectHelpers = {};
1099
Markdown.DialectHelpers.inline_until_char = function( text, want ) {
1104
if ( text[ consumed ] == want ) {
1105
// Found the character we were looking for
1107
return [ consumed, nodes ];
1110
if ( consumed >= text.length ) {
1111
// No closing char found. Abort.
1115
res = this.dialect.inline.__oneElement__.call(this, text.substr( consumed ) );
1116
consumed += res[ 0 ];
1117
// Add any returned nodes.
1118
nodes.push.apply( nodes, res.slice( 1 ) );
1122
// Helper function to make sub-classing a dialect easier
1123
Markdown.subclassDialect = function( d ) {
1125
Block.prototype = d.block;
1126
function Inline() {}
1127
Inline.prototype = d.inline;
1129
return { block: new Block(), inline: new Inline() };
1132
Markdown.buildBlockOrder ( Markdown.dialects.Gruber.block );
1133
Markdown.buildInlinePatterns( Markdown.dialects.Gruber.inline );
1135
Markdown.dialects.Maruku = Markdown.subclassDialect( Markdown.dialects.Gruber );
1137
Markdown.dialects.Maruku.processMetaHash = function processMetaHash( meta_string ) {
1138
var meta = split_meta_hash( meta_string ),
1141
for ( var i = 0; i < meta.length; ++i ) {
1143
if ( /^#/.test( meta[ i ] ) ) {
1144
attr.id = meta[ i ].substring( 1 );
1147
else if ( /^\./.test( meta[ i ] ) ) {
1148
// if class already exists, append the new one
1149
if ( attr['class'] ) {
1150
attr['class'] = attr['class'] + meta[ i ].replace( /./, " " );
1153
attr['class'] = meta[ i ].substring( 1 );
1156
// attribute: foo=bar
1157
else if ( /\=/.test( meta[ i ] ) ) {
1158
var s = meta[ i ].split( /\=/ );
1159
attr[ s[ 0 ] ] = s[ 1 ];
1166
function split_meta_hash( meta_string ) {
1167
var meta = meta_string.split( "" ),
1171
while ( meta.length ) {
1172
var letter = meta.shift();
1175
// if we're in a quoted section, keep it
1177
parts[ parts.length - 1 ] += letter;
1179
// otherwise make a new part
1186
// reverse the quotes and move straight on
1187
in_quotes = !in_quotes;
1190
// shift off the next letter to be used straight away.
1191
// it was escaped so we'll keep it whatever it is
1192
letter = meta.shift();
1194
parts[ parts.length - 1 ] += letter;
1202
Markdown.dialects.Maruku.block.document_meta = function document_meta( block, next ) {
1203
// we're only interested in the first block
1204
if ( block.lineNumber > 1 ) return undefined;
1206
// document_meta blocks consist of one or more lines of `Key: Value\n`
1207
if ( ! block.match( /^(?:\w+:.*\n)*\w+:.*$/ ) ) return undefined;
1209
// make an attribute node if it doesn't exist
1210
if ( !extract_attr( this.tree ) ) {
1211
this.tree.splice( 1, 0, {} );
1214
var pairs = block.split( /\n/ );
1215
for ( p in pairs ) {
1216
var m = pairs[ p ].match( /(\w+):\s*(.*)$/ ),
1217
key = m[ 1 ].toLowerCase(),
1220
this.tree[ 1 ][ key ] = value;
1223
// document_meta produces no content!
1227
Markdown.dialects.Maruku.block.block_meta = function block_meta( block, next ) {
1228
// check if the last line of the block is an meta hash
1229
var m = block.match( /(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/ );
1230
if ( !m ) return undefined;
1232
// process the meta hash
1233
var attr = this.dialect.processMetaHash( m[ 2 ] );
1237
// if we matched ^ then we need to apply meta to the previous block
1238
if ( m[ 1 ] === "" ) {
1239
var node = this.tree[ this.tree.length - 1 ];
1240
hash = extract_attr( node );
1242
// if the node is a string (rather than JsonML), bail
1243
if ( typeof node === "string" ) return undefined;
1245
// create the attribute hash if it doesn't exist
1248
node.splice( 1, 0, hash );
1251
// add the attributes in
1253
hash[ a ] = attr[ a ];
1256
// return nothing so the meta hash is removed
1260
// pull the meta hash off the block and process what's left
1261
var b = block.replace( /\n.*$/, "" ),
1262
result = this.processBlock( b, [] );
1264
// get or make the attributes hash
1265
hash = extract_attr( result[ 0 ] );
1268
result[ 0 ].splice( 1, 0, hash );
1271
// attach the attributes to the block
1273
hash[ a ] = attr[ a ];
1279
Markdown.dialects.Maruku.block.definition_list = function definition_list( block, next ) {
1280
// one or more terms followed by one or more definitions, in a single block
1281
var tight = /^((?:[^\s:].*\n)+):\s+([\s\S]+)$/,
1285
// see if we're dealing with a tight or loose block
1286
if ( ( m = block.match( tight ) ) ) {
1287
// pull subsequent tight DL blocks out of `next`
1288
var blocks = [ block ];
1289
while ( next.length && tight.exec( next[ 0 ] ) ) {
1290
blocks.push( next.shift() );
1293
for ( var b = 0; b < blocks.length; ++b ) {
1294
var m = blocks[ b ].match( tight ),
1295
terms = m[ 1 ].replace( /\n$/, "" ).split( /\n/ ),
1296
defns = m[ 2 ].split( /\n:\s+/ );
1298
// print( uneval( m ) );
1300
for ( i = 0; i < terms.length; ++i ) {
1301
list.push( [ "dt", terms[ i ] ] );
1304
for ( i = 0; i < defns.length; ++i ) {
1305
// run inline processing over the definition
1306
list.push( [ "dd" ].concat( this.processInline( defns[ i ].replace( /(\n)\s+/, "$1" ) ) ) );
1317
Markdown.dialects.Maruku.inline[ "{:" ] = function inline_meta( text, matches, out ) {
1318
if ( !out.length ) {
1322
// get the preceeding element
1323
var before = out[ out.length - 1 ];
1325
if ( typeof before === "string" ) {
1329
// match a meta hash
1330
var m = text.match( /^\{:\s*((?:\\\}|[^\}])*)\s*\}/ );
1332
// no match, false alarm
1337
// attach the attributes to the preceeding element
1338
var meta = this.dialect.processMetaHash( m[ 1 ] ),
1339
attr = extract_attr( before );
1343
before.splice( 1, 0, attr );
1346
for ( var k in meta ) {
1347
attr[ k ] = meta[ k ];
1350
// cut out the string and replace it with nothing
1351
return [ m[ 0 ].length, "" ];
1354
Markdown.buildBlockOrder ( Markdown.dialects.Maruku.block );
1355
Markdown.buildInlinePatterns( Markdown.dialects.Maruku.inline );
1357
var isArray = Array.isArray || function(obj) {
1358
return Object.prototype.toString.call(obj) == '[object Array]';
1362
// Don't mess with Array.prototype. Its not friendly
1363
if ( Array.prototype.forEach ) {
1364
forEach = function( arr, cb, thisp ) {
1365
return arr.forEach( cb, thisp );
1369
forEach = function(arr, cb, thisp) {
1370
for (var i = 0; i < arr.length; i++) {
1371
cb.call(thisp || arr, arr[i], i, arr);
1376
function extract_attr( jsonml ) {
1377
return isArray(jsonml)
1378
&& jsonml.length > 1
1379
&& typeof jsonml[ 1 ] === "object"
1380
&& !( isArray(jsonml[ 1 ]) )
1388
* renderJsonML( jsonml[, options] ) -> String
1389
* - jsonml (Array): JsonML array to render to XML
1390
* - options (Object): options
1392
* Converts the given JsonML into well-formed XML.
1394
* The options currently understood are:
1396
* - root (Boolean): wether or not the root node should be included in the
1397
* output, or just its children. The default `false` is to not include the
1400
expose.renderJsonML = function( jsonml, options ) {
1401
options = options || {};
1402
// include the root element in the rendered output?
1403
options.root = options.root || false;
1407
if ( options.root ) {
1408
content.push( render_tree( jsonml ) );
1411
jsonml.shift(); // get rid of the tag
1412
if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) {
1413
jsonml.shift(); // get rid of the attributes
1416
while ( jsonml.length ) {
1417
content.push( render_tree( jsonml.shift() ) );
1421
return content.join( "\n\n" );
1424
function escapeHTML( text ) {
1425
return text.replace( /&/g, "&" )
1426
.replace( /</g, "<" )
1427
.replace( />/g, ">" )
1428
.replace( /"/g, """ )
1429
.replace( /'/g, "'" );
1432
function render_tree( jsonml ) {
1434
if ( typeof jsonml === "string" ) {
1435
return escapeHTML( jsonml );
1438
var tag = jsonml.shift(),
1442
if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) {
1443
attributes = jsonml.shift();
1446
while ( jsonml.length ) {
1447
content.push( arguments.callee( jsonml.shift() ) );
1451
for ( var a in attributes ) {
1452
tag_attrs += " " + a + '="' + escapeHTML( attributes[ a ] ) + '"';
1455
// be careful about adding whitespace here for inline elements
1456
if ( tag == "img" || tag == "br" || tag == "hr" ) {
1457
return "<"+ tag + tag_attrs + "/>";
1460
return "<"+ tag + tag_attrs + ">" + content.join( "" ) + "</" + tag + ">";
1464
function convert_tree_to_html( tree, references, options ) {
1466
options = options || {};
1469
var jsonml = tree.slice( 0 );
1471
if (typeof options.preprocessTreeNode === "function") {
1472
jsonml = options.preprocessTreeNode(jsonml, references);
1475
// Clone attributes if they exist
1476
var attrs = extract_attr( jsonml );
1479
for ( i in attrs ) {
1480
jsonml[ 1 ][ i ] = attrs[ i ];
1482
attrs = jsonml[ 1 ];
1486
if ( typeof jsonml === "string" ) {
1490
// convert this node
1491
switch ( jsonml[ 0 ] ) {
1493
jsonml[ 0 ] = "h" + jsonml[ 1 ].level;
1494
delete jsonml[ 1 ].level;
1509
jsonml[ 0 ] = "html";
1510
if ( attrs ) delete attrs.references;
1513
jsonml[ 0 ] = "pre";
1515
var code = [ "code" ];
1516
code.push.apply( code, jsonml.splice( i ) );
1520
jsonml[ 0 ] = "code";
1523
jsonml[ 1 ].src = jsonml[ 1 ].href;
1524
delete jsonml[ 1 ].href;
1535
// grab this ref and clean up the attribute node
1536
var ref = references[ attrs.ref ];
1538
// if the reference exists, make the link
1542
// add in the href and title, if present
1543
attrs.href = ref.href;
1545
attrs.title = ref.title;
1548
// get rid of the unneeded original text
1549
delete attrs.original;
1551
// the reference doesn't exist, so revert to plain text
1553
return attrs.original;
1557
jsonml[ 0 ] = "img";
1559
// grab this ref and clean up the attribute node
1560
var ref = references[ attrs.ref ];
1562
// if the reference exists, make the link
1566
// add in the href and title, if present
1567
attrs.src = ref.href;
1569
attrs.title = ref.title;
1572
// get rid of the unneeded original text
1573
delete attrs.original;
1575
// the reference doesn't exist, so revert to plain text
1577
return attrs.original;
1582
// convert all the children
1585
// deal with the attribute node, if it exists
1587
// if there are keys, skip over it
1588
for ( var key in jsonml[ 1 ] ) {
1591
// if there aren't, remove it
1593
jsonml.splice( i, 1 );
1597
for ( ; i < jsonml.length; ++i ) {
1598
jsonml[ i ] = arguments.callee( jsonml[ i ], references, options );
1605
// merges adjacent text nodes into a single node
1606
function merge_text_nodes( jsonml ) {
1607
// skip the tag name and attribute hash
1608
var i = extract_attr( jsonml ) ? 2 : 1;
1610
while ( i < jsonml.length ) {
1611
// if it's a string check the next item too
1612
if ( typeof jsonml[ i ] === "string" ) {
1613
if ( i + 1 < jsonml.length && typeof jsonml[ i + 1 ] === "string" ) {
1614
// merge the second string into the first and remove it
1615
jsonml[ i ] += jsonml.splice( i + 1, 1 )[ 0 ];
1621
// if it's not a string recurse
1623
arguments.callee( jsonml[ i ] );
1630
if ( typeof Y !== "undefined" ) {
1631
return Y.namespace('Markdown');
1633
else if ( typeof exports === "undefined" ) {
1634
window.markdown = {};
1635
return window.markdown;
1643
}, 'gallery-2012.07.18-13-22' );