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