write 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,7 +1,40 @@
1
- # Write
1
+ # WRITE
2
2
 
3
3
  gem "write"
4
4
 
5
- Write is a mountable Rails engine that gives the ability to use Github gists as a backend store for customizable blog tool.
5
+ WRITE is a mountable Rails engine that gives the ability to use Github gists as a backend store for a customizable blog tool.
6
6
 
7
- Install the gem, mount it somewhere under your site, configure the account to use for gists, and you're done. No database needed.
7
+ Install the gem, mount it somewhere under your Rails site, configure it with the account to use for gists, and you're done. **No database needed**.
8
+
9
+ ## Configuring
10
+
11
+ You need to tell WRITE which github account to pull posts from. This is what I used on [my own blog](http://blog.samer.ps)
12
+
13
+ Write.config = {
14
+ :accounts => ["samerbuna"], # You can use multiple accounts
15
+ :layout => "profile", # WRITE will render all views using this main-app layout
16
+ :title => "Samer Buna", # This will appear in each page title
17
+ :tagline => "Hot & Sour Ideations" # The main page title and header text
18
+ }
19
+
20
+ The :layout is where you can control the look and feel of a WRITE-blog
21
+
22
+ You then need to mount the WRITE engine, for example, under /blog
23
+
24
+ mount Write::Engine => "/"
25
+
26
+ I mounted it under "/" with a domain constraint, so that I can have my blog at blog.samer.ps
27
+
28
+ That's all you need.
29
+
30
+ Any github gist under a configured account, with the special file "write.md", will be included in the list of posts.
31
+
32
+ WRITE serves everything from Rails.cache, it hits github only once and then caches everything. If you need to refresh the cached content (without rebooting), you need to enable a login system.
33
+
34
+ On my blog, I am using [this login engine](http://github.com/samerbuna/login), which gives a quick and easy way to activate a ready devise-omniauth configurations. If you login with the configured github account, WRITE will give you a link to refresh all posts.
35
+
36
+ You should be able to override the "write_admin?" helper to instruct WRITE about which user is considered admin (I haven't tested that though)
37
+
38
+ ### Read about the features I plan to add to WRITE in [this blog post](http://blog.samer.ps/2012/write-a-rails-engine-blog-using-github-gists-as-the-database)
39
+
40
+ Feedback and help are welcome.
@@ -2,8 +2,8 @@ jQuery ($)->
2
2
  commentNode = (comment) ->
3
3
  container = $("<div>").addClass("comment-container")
4
4
  container.append $("<div>").addClass("comment-author").text(comment.user.login)
5
- container.append $("<div>").addClass("comment-time").text(comment.updated_at)
6
- container.append $("<div>").addClass("comment-content").text(comment.body)
5
+ container.append $("<abbr>").addClass("comment-time").prop("title", comment.updated_at).timeago()
6
+ container.append $("<div>").addClass("comment-content").html(markdown.toHTML(comment.body))
7
7
  $.ajax
8
8
  dataType: 'jsonp',
9
9
  url: $('#post-view').data('comments-url')
@@ -0,0 +1,1616 @@
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
+ "![": function image( text ) {
811
+
812
+ // Unlike images, alt text is plain text only. no other elements are
813
+ // allowed in there
814
+
815
+ // ![Alt text](/path/to/img.jpg "Optional title")
816
+ // 1 2 3 4 <--- captures
817
+ var m = text.match( /^!\[(.*?)\][ \t]*\([ \t]*(\S*)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/ );
818
+
819
+ if ( m ) {
820
+ if ( m[2] && m[2][0] == '<' && m[2][m[2].length-1] == '>' )
821
+ m[2] = m[2].substring( 1, m[2].length - 1 );
822
+
823
+ m[2] = this.dialect.inline.__call__.call( this, m[2], /\\/ )[0];
824
+
825
+ var attrs = { alt: m[1], href: m[2] || "" };
826
+ if ( m[4] !== undefined)
827
+ attrs.title = m[4];
828
+
829
+ return [ m[0].length, [ "img", attrs ] ];
830
+ }
831
+
832
+ // ![Alt text][id]
833
+ m = text.match( /^!\[(.*?)\][ \t]*\[(.*?)\]/ );
834
+
835
+ if ( m ) {
836
+ // We can't check if the reference is known here as it likely wont be
837
+ // found till after. Check it in md tree->hmtl tree conversion
838
+ return [ m[0].length, [ "img_ref", { alt: m[1], ref: m[2].toLowerCase(), original: m[0] } ] ];
839
+ }
840
+
841
+ // Just consume the '!['
842
+ return [ 2, "![" ];
843
+ },
844
+
845
+ "[": function link( text ) {
846
+
847
+ var orig = String(text);
848
+ // Inline content is possible inside `link text`
849
+ var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), ']' );
850
+
851
+ // No closing ']' found. Just consume the [
852
+ if ( !res ) return [ 1, '[' ];
853
+
854
+ var consumed = 1 + res[ 0 ],
855
+ children = res[ 1 ],
856
+ link,
857
+ attrs;
858
+
859
+ // At this point the first [...] has been parsed. See what follows to find
860
+ // out which kind of link we are (reference or direct url)
861
+ text = text.substr( consumed );
862
+
863
+ // [link text](/path/to/img.jpg "Optional title")
864
+ // 1 2 3 <--- captures
865
+ // This will capture up to the last paren in the block. We then pull
866
+ // back based on if there a matching ones in the url
867
+ // ([here](/url/(test))
868
+ // The parens have to be balanced
869
+ var m = text.match( /^\s*\([ \t]*(\S+)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/ );
870
+ if ( m ) {
871
+ var url = m[1];
872
+ consumed += m[0].length;
873
+
874
+ if ( url && url[0] == '<' && url[url.length-1] == '>' )
875
+ url = url.substring( 1, url.length - 1 );
876
+
877
+ // If there is a title we don't have to worry about parens in the url
878
+ if ( !m[3] ) {
879
+ var open_parens = 1; // One open that isn't in the capture
880
+ for (var len = 0; len < url.length; len++) {
881
+ switch ( url[len] ) {
882
+ case '(':
883
+ open_parens++;
884
+ break;
885
+ case ')':
886
+ if ( --open_parens == 0) {
887
+ consumed -= url.length - len;
888
+ url = url.substring(0, len);
889
+ }
890
+ break;
891
+ }
892
+ }
893
+ }
894
+
895
+ // Process escapes only
896
+ url = this.dialect.inline.__call__.call( this, url, /\\/ )[0];
897
+
898
+ attrs = { href: url || "" };
899
+ if ( m[3] !== undefined)
900
+ attrs.title = m[3];
901
+
902
+ link = [ "link", attrs ].concat( children );
903
+ return [ consumed, link ];
904
+ }
905
+
906
+ // [Alt text][id]
907
+ // [Alt text] [id]
908
+ m = text.match( /^\s*\[(.*?)\]/ );
909
+
910
+ if ( m ) {
911
+
912
+ consumed += m[ 0 ].length;
913
+
914
+ // [links][] uses links as its reference
915
+ attrs = { ref: ( m[ 1 ] || String(children) ).toLowerCase(), original: orig.substr( 0, consumed ) };
916
+
917
+ link = [ "link_ref", attrs ].concat( children );
918
+
919
+ // We can't check if the reference is known here as it likely wont be
920
+ // found till after. Check it in md tree->hmtl tree conversion.
921
+ // Store the original so that conversion can revert if the ref isn't found.
922
+ return [ consumed, link ];
923
+ }
924
+
925
+ // [id]
926
+ // Only if id is plain (no formatting.)
927
+ if ( children.length == 1 && typeof children[0] == "string" ) {
928
+
929
+ attrs = { ref: children[0].toLowerCase(), original: orig.substr( 0, consumed ) };
930
+ link = [ "link_ref", attrs, children[0] ];
931
+ return [ consumed, link ];
932
+ }
933
+
934
+ // Just consume the '['
935
+ return [ 1, "[" ];
936
+ },
937
+
938
+
939
+ "<": function autoLink( text ) {
940
+ var m;
941
+
942
+ if ( ( m = text.match( /^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/ ) ) != null ) {
943
+ if ( m[3] ) {
944
+ return [ m[0].length, [ "link", { href: "mailto:" + m[3] }, m[3] ] ];
945
+
946
+ }
947
+ else if ( m[2] == "mailto" ) {
948
+ return [ m[0].length, [ "link", { href: m[1] }, m[1].substr("mailto:".length ) ] ];
949
+ }
950
+ else
951
+ return [ m[0].length, [ "link", { href: m[1] }, m[1] ] ];
952
+ }
953
+
954
+ return [ 1, "<" ];
955
+ },
956
+
957
+ "`": function inlineCode( text ) {
958
+ // Inline code block. as many backticks as you like to start it
959
+ // Always skip over the opening ticks.
960
+ var m = text.match( /(`+)(([\s\S]*?)\1)/ );
961
+
962
+ if ( m && m[2] )
963
+ return [ m[1].length + m[2].length, [ "inlinecode", m[3] ] ];
964
+ else {
965
+ // TODO: No matching end code found - warn!
966
+ return [ 1, "`" ];
967
+ }
968
+ },
969
+
970
+ " \n": function lineBreak( text ) {
971
+ return [ 3, [ "linebreak" ] ];
972
+ }
973
+
974
+ };
975
+
976
+ // Meta Helper/generator method for em and strong handling
977
+ function strong_em( tag, md ) {
978
+
979
+ var state_slot = tag + "_state",
980
+ other_slot = tag == "strong" ? "em_state" : "strong_state";
981
+
982
+ function CloseTag(len) {
983
+ this.len_after = len;
984
+ this.name = "close_" + md;
985
+ }
986
+
987
+ return function ( text, orig_match ) {
988
+
989
+ if (this[state_slot][0] == md) {
990
+ // Most recent em is of this type
991
+ //D:this.debug("closing", md);
992
+ this[state_slot].shift();
993
+
994
+ // "Consume" everything to go back to the recrusion in the else-block below
995
+ return[ text.length, new CloseTag(text.length-md.length) ];
996
+ }
997
+ else {
998
+ // Store a clone of the em/strong states
999
+ var other = this[other_slot].slice(),
1000
+ state = this[state_slot].slice();
1001
+
1002
+ this[state_slot].unshift(md);
1003
+
1004
+ //D:this.debug_indent += " ";
1005
+
1006
+ // Recurse
1007
+ var res = this.processInline( text.substr( md.length ) );
1008
+ //D:this.debug_indent = this.debug_indent.substr(2);
1009
+
1010
+ var last = res[res.length - 1];
1011
+
1012
+ //D:this.debug("processInline from", tag + ": ", uneval( res ) );
1013
+
1014
+ var check = this[state_slot].shift();
1015
+ if (last instanceof CloseTag) {
1016
+ res.pop();
1017
+ // We matched! Huzzah.
1018
+ var consumed = text.length - last.len_after;
1019
+ return [ consumed, [ tag ].concat(res) ];
1020
+ }
1021
+ else {
1022
+ // Restore the state of the other kind. We might have mistakenly closed it.
1023
+ this[other_slot] = other;
1024
+ this[state_slot] = state;
1025
+
1026
+ // We can't reuse the processed result as it could have wrong parsing contexts in it.
1027
+ return [ md.length, md ];
1028
+ }
1029
+ }
1030
+ }; // End returned function
1031
+ }
1032
+
1033
+ Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**");
1034
+ Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__");
1035
+ Markdown.dialects.Gruber.inline["*"] = strong_em("em", "*");
1036
+ Markdown.dialects.Gruber.inline["_"] = strong_em("em", "_");
1037
+
1038
+
1039
+ // Build default order from insertion order.
1040
+ Markdown.buildBlockOrder = function(d) {
1041
+ var ord = [];
1042
+ for ( var i in d ) {
1043
+ if ( i == "__order__" || i == "__call__" ) continue;
1044
+ ord.push( i );
1045
+ }
1046
+ d.__order__ = ord;
1047
+ };
1048
+
1049
+ // Build patterns for inline matcher
1050
+ Markdown.buildInlinePatterns = function(d) {
1051
+ var patterns = [];
1052
+
1053
+ for ( var i in d ) {
1054
+ // __foo__ is reserved and not a pattern
1055
+ if ( i.match( /^__.*__$/) ) continue;
1056
+ var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" )
1057
+ .replace( /\n/, "\\n" );
1058
+ patterns.push( i.length == 1 ? l : "(?:" + l + ")" );
1059
+ }
1060
+
1061
+ patterns = patterns.join("|");
1062
+ d.__patterns__ = patterns;
1063
+ //print("patterns:", uneval( patterns ) );
1064
+
1065
+ var fn = d.__call__;
1066
+ d.__call__ = function(text, pattern) {
1067
+ if (pattern != undefined) {
1068
+ return fn.call(this, text, pattern);
1069
+ }
1070
+ else
1071
+ {
1072
+ return fn.call(this, text, patterns);
1073
+ }
1074
+ };
1075
+ };
1076
+
1077
+ Markdown.DialectHelpers = {};
1078
+ Markdown.DialectHelpers.inline_until_char = function( text, want ) {
1079
+ var consumed = 0,
1080
+ nodes = [];
1081
+
1082
+ while ( true ) {
1083
+ if ( text[ consumed ] == want ) {
1084
+ // Found the character we were looking for
1085
+ consumed++;
1086
+ return [ consumed, nodes ];
1087
+ }
1088
+
1089
+ if ( consumed >= text.length ) {
1090
+ // No closing char found. Abort.
1091
+ return null;
1092
+ }
1093
+
1094
+ var res = this.dialect.inline.__oneElement__.call(this, text.substr( consumed ) );
1095
+ consumed += res[ 0 ];
1096
+ // Add any returned nodes.
1097
+ nodes.push.apply( nodes, res.slice( 1 ) );
1098
+ }
1099
+ }
1100
+
1101
+ // Helper function to make sub-classing a dialect easier
1102
+ Markdown.subclassDialect = function( d ) {
1103
+ function Block() {}
1104
+ Block.prototype = d.block;
1105
+ function Inline() {}
1106
+ Inline.prototype = d.inline;
1107
+
1108
+ return { block: new Block(), inline: new Inline() };
1109
+ };
1110
+
1111
+ Markdown.buildBlockOrder ( Markdown.dialects.Gruber.block );
1112
+ Markdown.buildInlinePatterns( Markdown.dialects.Gruber.inline );
1113
+
1114
+ Markdown.dialects.Maruku = Markdown.subclassDialect( Markdown.dialects.Gruber );
1115
+
1116
+ Markdown.dialects.Maruku.processMetaHash = function processMetaHash( meta_string ) {
1117
+ var meta = split_meta_hash( meta_string ),
1118
+ attr = {};
1119
+
1120
+ for ( var i = 0; i < meta.length; ++i ) {
1121
+ // id: #foo
1122
+ if ( /^#/.test( meta[ i ] ) ) {
1123
+ attr.id = meta[ i ].substring( 1 );
1124
+ }
1125
+ // class: .foo
1126
+ else if ( /^\./.test( meta[ i ] ) ) {
1127
+ // if class already exists, append the new one
1128
+ if ( attr['class'] ) {
1129
+ attr['class'] = attr['class'] + meta[ i ].replace( /./, " " );
1130
+ }
1131
+ else {
1132
+ attr['class'] = meta[ i ].substring( 1 );
1133
+ }
1134
+ }
1135
+ // attribute: foo=bar
1136
+ else if ( /\=/.test( meta[ i ] ) ) {
1137
+ var s = meta[ i ].split( /\=/ );
1138
+ attr[ s[ 0 ] ] = s[ 1 ];
1139
+ }
1140
+ }
1141
+
1142
+ return attr;
1143
+ }
1144
+
1145
+ function split_meta_hash( meta_string ) {
1146
+ var meta = meta_string.split( "" ),
1147
+ parts = [ "" ],
1148
+ in_quotes = false;
1149
+
1150
+ while ( meta.length ) {
1151
+ var letter = meta.shift();
1152
+ switch ( letter ) {
1153
+ case " " :
1154
+ // if we're in a quoted section, keep it
1155
+ if ( in_quotes ) {
1156
+ parts[ parts.length - 1 ] += letter;
1157
+ }
1158
+ // otherwise make a new part
1159
+ else {
1160
+ parts.push( "" );
1161
+ }
1162
+ break;
1163
+ case "'" :
1164
+ case '"' :
1165
+ // reverse the quotes and move straight on
1166
+ in_quotes = !in_quotes;
1167
+ break;
1168
+ case "\\" :
1169
+ // shift off the next letter to be used straight away.
1170
+ // it was escaped so we'll keep it whatever it is
1171
+ letter = meta.shift();
1172
+ default :
1173
+ parts[ parts.length - 1 ] += letter;
1174
+ break;
1175
+ }
1176
+ }
1177
+
1178
+ return parts;
1179
+ }
1180
+
1181
+ Markdown.dialects.Maruku.block.document_meta = function document_meta( block, next ) {
1182
+ // we're only interested in the first block
1183
+ if ( block.lineNumber > 1 ) return undefined;
1184
+
1185
+ // document_meta blocks consist of one or more lines of `Key: Value\n`
1186
+ if ( ! block.match( /^(?:\w+:.*\n)*\w+:.*$/ ) ) return undefined;
1187
+
1188
+ // make an attribute node if it doesn't exist
1189
+ if ( !extract_attr( this.tree ) ) {
1190
+ this.tree.splice( 1, 0, {} );
1191
+ }
1192
+
1193
+ var pairs = block.split( /\n/ );
1194
+ for ( p in pairs ) {
1195
+ var m = pairs[ p ].match( /(\w+):\s*(.*)$/ ),
1196
+ key = m[ 1 ].toLowerCase(),
1197
+ value = m[ 2 ];
1198
+
1199
+ this.tree[ 1 ][ key ] = value;
1200
+ }
1201
+
1202
+ // document_meta produces no content!
1203
+ return [];
1204
+ };
1205
+
1206
+ Markdown.dialects.Maruku.block.block_meta = function block_meta( block, next ) {
1207
+ // check if the last line of the block is an meta hash
1208
+ var m = block.match( /(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/ );
1209
+ if ( !m ) return undefined;
1210
+
1211
+ // process the meta hash
1212
+ var attr = this.dialect.processMetaHash( m[ 2 ] );
1213
+
1214
+ var hash;
1215
+
1216
+ // if we matched ^ then we need to apply meta to the previous block
1217
+ if ( m[ 1 ] === "" ) {
1218
+ var node = this.tree[ this.tree.length - 1 ];
1219
+ hash = extract_attr( node );
1220
+
1221
+ // if the node is a string (rather than JsonML), bail
1222
+ if ( typeof node === "string" ) return undefined;
1223
+
1224
+ // create the attribute hash if it doesn't exist
1225
+ if ( !hash ) {
1226
+ hash = {};
1227
+ node.splice( 1, 0, hash );
1228
+ }
1229
+
1230
+ // add the attributes in
1231
+ for ( a in attr ) {
1232
+ hash[ a ] = attr[ a ];
1233
+ }
1234
+
1235
+ // return nothing so the meta hash is removed
1236
+ return [];
1237
+ }
1238
+
1239
+ // pull the meta hash off the block and process what's left
1240
+ var b = block.replace( /\n.*$/, "" ),
1241
+ result = this.processBlock( b, [] );
1242
+
1243
+ // get or make the attributes hash
1244
+ hash = extract_attr( result[ 0 ] );
1245
+ if ( !hash ) {
1246
+ hash = {};
1247
+ result[ 0 ].splice( 1, 0, hash );
1248
+ }
1249
+
1250
+ // attach the attributes to the block
1251
+ for ( a in attr ) {
1252
+ hash[ a ] = attr[ a ];
1253
+ }
1254
+
1255
+ return result;
1256
+ };
1257
+
1258
+ Markdown.dialects.Maruku.block.definition_list = function definition_list( block, next ) {
1259
+ // one or more terms followed by one or more definitions, in a single block
1260
+ var tight = /^((?:[^\s:].*\n)+):\s+([\s\S]+)$/,
1261
+ list = [ "dl" ],
1262
+ i;
1263
+
1264
+ // see if we're dealing with a tight or loose block
1265
+ if ( ( m = block.match( tight ) ) ) {
1266
+ // pull subsequent tight DL blocks out of `next`
1267
+ var blocks = [ block ];
1268
+ while ( next.length && tight.exec( next[ 0 ] ) ) {
1269
+ blocks.push( next.shift() );
1270
+ }
1271
+
1272
+ for ( var b = 0; b < blocks.length; ++b ) {
1273
+ var m = blocks[ b ].match( tight ),
1274
+ terms = m[ 1 ].replace( /\n$/, "" ).split( /\n/ ),
1275
+ defns = m[ 2 ].split( /\n:\s+/ );
1276
+
1277
+ // print( uneval( m ) );
1278
+
1279
+ for ( i = 0; i < terms.length; ++i ) {
1280
+ list.push( [ "dt", terms[ i ] ] );
1281
+ }
1282
+
1283
+ for ( i = 0; i < defns.length; ++i ) {
1284
+ // run inline processing over the definition
1285
+ list.push( [ "dd" ].concat( this.processInline( defns[ i ].replace( /(\n)\s+/, "$1" ) ) ) );
1286
+ }
1287
+ }
1288
+ }
1289
+ else {
1290
+ return undefined;
1291
+ }
1292
+
1293
+ return [ list ];
1294
+ };
1295
+
1296
+ Markdown.dialects.Maruku.inline[ "{:" ] = function inline_meta( text, matches, out ) {
1297
+ if ( !out.length ) {
1298
+ return [ 2, "{:" ];
1299
+ }
1300
+
1301
+ // get the preceeding element
1302
+ var before = out[ out.length - 1 ];
1303
+
1304
+ if ( typeof before === "string" ) {
1305
+ return [ 2, "{:" ];
1306
+ }
1307
+
1308
+ // match a meta hash
1309
+ var m = text.match( /^\{:\s*((?:\\\}|[^\}])*)\s*\}/ );
1310
+
1311
+ // no match, false alarm
1312
+ if ( !m ) {
1313
+ return [ 2, "{:" ];
1314
+ }
1315
+
1316
+ // attach the attributes to the preceeding element
1317
+ var meta = this.dialect.processMetaHash( m[ 1 ] ),
1318
+ attr = extract_attr( before );
1319
+
1320
+ if ( !attr ) {
1321
+ attr = {};
1322
+ before.splice( 1, 0, attr );
1323
+ }
1324
+
1325
+ for ( var k in meta ) {
1326
+ attr[ k ] = meta[ k ];
1327
+ }
1328
+
1329
+ // cut out the string and replace it with nothing
1330
+ return [ m[ 0 ].length, "" ];
1331
+ };
1332
+
1333
+ Markdown.buildBlockOrder ( Markdown.dialects.Maruku.block );
1334
+ Markdown.buildInlinePatterns( Markdown.dialects.Maruku.inline );
1335
+
1336
+ var isArray = Array.isArray || function(obj) {
1337
+ return Object.prototype.toString.call(obj) == '[object Array]';
1338
+ };
1339
+
1340
+ var forEach;
1341
+ // Don't mess with Array.prototype. Its not friendly
1342
+ if ( Array.prototype.forEach ) {
1343
+ forEach = function( arr, cb, thisp ) {
1344
+ return arr.forEach( cb, thisp );
1345
+ };
1346
+ }
1347
+ else {
1348
+ forEach = function(arr, cb, thisp) {
1349
+ for (var i = 0; i < arr.length; i++) {
1350
+ cb.call(thisp || arr, arr[i], i, arr);
1351
+ }
1352
+ }
1353
+ }
1354
+
1355
+ function extract_attr( jsonml ) {
1356
+ return isArray(jsonml)
1357
+ && jsonml.length > 1
1358
+ && typeof jsonml[ 1 ] === "object"
1359
+ && !( isArray(jsonml[ 1 ]) )
1360
+ ? jsonml[ 1 ]
1361
+ : undefined;
1362
+ }
1363
+
1364
+
1365
+
1366
+ /**
1367
+ * renderJsonML( jsonml[, options] ) -> String
1368
+ * - jsonml (Array): JsonML array to render to XML
1369
+ * - options (Object): options
1370
+ *
1371
+ * Converts the given JsonML into well-formed XML.
1372
+ *
1373
+ * The options currently understood are:
1374
+ *
1375
+ * - root (Boolean): wether or not the root node should be included in the
1376
+ * output, or just its children. The default `false` is to not include the
1377
+ * root itself.
1378
+ */
1379
+ expose.renderJsonML = function( jsonml, options ) {
1380
+ options = options || {};
1381
+ // include the root element in the rendered output?
1382
+ options.root = options.root || false;
1383
+
1384
+ var content = [];
1385
+
1386
+ if ( options.root ) {
1387
+ content.push( render_tree( jsonml ) );
1388
+ }
1389
+ else {
1390
+ jsonml.shift(); // get rid of the tag
1391
+ if ( jsonml.length && typeof jsonml[ 0 ] === "object" && !( jsonml[ 0 ] instanceof Array ) ) {
1392
+ jsonml.shift(); // get rid of the attributes
1393
+ }
1394
+
1395
+ while ( jsonml.length ) {
1396
+ content.push( render_tree( jsonml.shift() ) );
1397
+ }
1398
+ }
1399
+
1400
+ return content.join( "\n\n" );
1401
+ };
1402
+
1403
+ function escapeHTML( text ) {
1404
+ return text.replace( /&/g, "&amp;" )
1405
+ .replace( /</g, "&lt;" )
1406
+ .replace( />/g, "&gt;" )
1407
+ .replace( /"/g, "&quot;" )
1408
+ .replace( /'/g, "&#39;" );
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
+ } )() );