Explorar el Código

Adding footnotes, definitions, abbreviations

main
Rocketsoup hace 1 año
padre
commit
70ef0919d0
Se han modificado 2 ficheros con 242 adiciones y 151 borrados
  1. 226
    136
      js/markdown.js
  2. 16
    15
      markdownjs.html

+ 226
- 136
js/markdown.js Ver fichero

1
 // TODO: Linked image not parsed correctly.  [![](image.jpg)](link.html)
1
 // TODO: Linked image not parsed correctly.  [![](image.jpg)](link.html)
2
-
3
-// Blocks
4
-// - Paragraph
5
-// - Header 1-6   # ## ### #### ##### ######  or   ===  ---
6
-// - Blockquote (nestable)   >
7
-// - Unordered list (nestable)   *_
8
-// - Ordered list (nestable)    1._
9
-// - Code block   ```\ncode\n```  or  4 spaces/tab indent
10
-// - Horizontal rule   ---    - - -    * * *  etc
11
-// - Table   -|-
12
-// - Definition list     term\n: definition\n: alternate definition
13
-// - Footnote (bottom)   citation[^1]
14
-// - Abbreviation (definition)   *[ABC]: Abbrev Blah Cat
15
-// Inline
16
-// - Link   [text](https://url)
17
-// - Emphasis   *emphasized*
18
-// - Strong   **bold**
19
-// - Inline code   `code`
20
-// - Strikethrough   ~strike~
21
-// - Image   ![alt text](https://image){.cssclass}
22
-// - Footnote (inline)   [^1]: footnote text
23
-// - Abbreviation (inline)
2
+// TODO: {.class #cssid lang=fr}
3
+//     # Header {.class}
4
+//     Header {.class}
5
+//     ---
6
+//     [link](url){.class}
7
+//     ``` {.class}
8
+// FIXME: Nested lists not working right
9
+// FIXME: Nested blockquotes require blank line
10
+// FIXME: Ordered list should start with first number
24
 
11
 
25
 class _MDHAlign {
12
 class _MDHAlign {
26
 	static Left = new _MDHAlign('Left');
13
 	static Left = new _MDHAlign('Left');
111
 
98
 
112
 
99
 
113
 class _MDSpan {
100
 class _MDSpan {
114
-	toHTML(config) {
101
+	/**
102
+	 * @param {_MDState} state
103
+	 * @returns {String} HTML
104
+	 */
105
+	toHTML(state) {
115
 		throw new Error(self.constructor.name + ".toHTML not implemented");
106
 		throw new Error(self.constructor.name + ".toHTML not implemented");
116
 	}
107
 	}
117
 
108
 
118
 	/**
109
 	/**
119
 	 * @param {_MDSpan[]} spans
110
 	 * @param {_MDSpan[]} spans
120
-	 * @param {_MDConfig} config
111
+	 * @param {_MDState} state
121
 	 */
112
 	 */
122
-	static toHTML(spans, config) {
123
-		return spans.map((span) => span.toHTML(config)).join("");
113
+	static toHTML(spans, state) {
114
+		return spans.map((span) => span.toHTML(state)).join("");
124
 	}
115
 	}
125
 }
116
 }
126
 class _MDMultiSpan extends _MDSpan {
117
 class _MDMultiSpan extends _MDSpan {
133
 		super();
124
 		super();
134
 		this.content = content;
125
 		this.content = content;
135
 	}
126
 	}
136
-	toHTML() {
137
-		return _MDSpan.toHTML(this.content);
127
+	toHTML(state) {
128
+		return _MDSpan.toHTML(this.content, state);
138
 	}
129
 	}
139
 }
130
 }
140
 class _MDTextSpan extends _MDSpan {
131
 class _MDTextSpan extends _MDSpan {
147
 		super();
138
 		super();
148
 		this.text = text;
139
 		this.text = text;
149
 	}
140
 	}
150
-	toHTML(config) {
151
-		return this.text.replace('<', '&lt;');
141
+	toHTML(state) {
142
+		let html = this.text.replace('<', '&lt;');
143
+		let abbrevs = state.abbreviations;
144
+		let regexes = state.abbreviationRegexes;
145
+		for (const abbrev in abbrevs) {
146
+			let def = abbrevs[abbrev];
147
+			let regex = regexes[abbrev];
148
+			let escapedDef = def.replace('"', '&quot;');
149
+			html = html.replace(regex, `<abbr title="${escapedDef}">$1</abbr>`);
150
+		}
151
+		return html;
152
 	}
152
 	}
153
 }
153
 }
154
 class _MDHTMLSpan extends _MDSpan {
154
 class _MDHTMLSpan extends _MDSpan {
161
 		super();
161
 		super();
162
 		this.html = html;
162
 		this.html = html;
163
 	}
163
 	}
164
-	toHTML(config) {
164
+	toHTML(state) {
165
 		return this.html;
165
 		return this.html;
166
 	}
166
 	}
167
 }
167
 }
183
 		this.content = content;
183
 		this.content = content;
184
 	}
184
 	}
185
 
185
 
186
-	toHTML(config) {
186
+	toHTML(state) {
187
 		let escapedLink = this.link.replace('"', '&quot;');
187
 		let escapedLink = this.link.replace('"', '&quot;');
188
 		var html = `<a href="${escapedLink}"`;
188
 		var html = `<a href="${escapedLink}"`;
189
 		if (target) {
189
 		if (target) {
190
 			let escapedTarget = this.target.replace('"', '&quot;');
190
 			let escapedTarget = this.target.replace('"', '&quot;');
191
 			html += ` target="${escapedTarget}"`;
191
 			html += ` target="${escapedTarget}"`;
192
 		}
192
 		}
193
-		html += '>' + this.content.toHTML(config) + '</a>';
193
+		html += '>' + this.content.toHTML(state) + '</a>';
194
 		return html;
194
 		return html;
195
 	}
195
 	}
196
 }
196
 }
201
 		super(null, content);
201
 		super(null, content);
202
 		this.id = id;
202
 		this.id = id;
203
 	}
203
 	}
204
-	toHTML(config) {
204
+	toHTML(state) {
205
 		if (this.link) {
205
 		if (this.link) {
206
-			return super.toHTML(config);
206
+			return super.toHTML(state);
207
 		} else {
207
 		} else {
208
-			let contentHTML = this.content.toHTML(config);
208
+			let contentHTML = this.content.toHTML(state);
209
 			return `[${contentHTML}][${this.id}]`;
209
 			return `[${contentHTML}][${this.id}]`;
210
 		}
210
 		}
211
 	}
211
 	}
220
 		super();
220
 		super();
221
 		this.#content = content;
221
 		this.#content = content;
222
 	}
222
 	}
223
-	toHTML(config) {
224
-		let contentHTML = this.#content.toHTML(config);
223
+	toHTML(state) {
224
+		let contentHTML = this.#content.toHTML(state);
225
 		return `<em>${contentHTML}</em>`;
225
 		return `<em>${contentHTML}</em>`;
226
 	}
226
 	}
227
 }
227
 }
235
 		super();
235
 		super();
236
 		this.#content = content;
236
 		this.#content = content;
237
 	}
237
 	}
238
-	toHTML(config) {
239
-		let contentHTML = this.#content.toHTML(config);
238
+	toHTML(state) {
239
+		let contentHTML = this.#content.toHTML(state);
240
 		return `<strong>${contentHTML}</strong>`;
240
 		return `<strong>${contentHTML}</strong>`;
241
 	}
241
 	}
242
 }
242
 }
250
 		super();
250
 		super();
251
 		this.#content = content;
251
 		this.#content = content;
252
 	}
252
 	}
253
-	toHTML(config) {
254
-		let contentHTML = this.#content.toHTML(config);
253
+	toHTML(state) {
254
+		let contentHTML = this.#content.toHTML(state);
255
 		return `<strike>${contentHTML}</strike>`;
255
 		return `<strike>${contentHTML}</strike>`;
256
 	}
256
 	}
257
 }
257
 }
265
 		super();
265
 		super();
266
 		this.#content = content;
266
 		this.#content = content;
267
 	}
267
 	}
268
-	toHTML(config) {
269
-		let contentHTML = this.#content.toHTML(config);
268
+	toHTML(state) {
269
+		let contentHTML = this.#content.toHTML(state);
270
 		return `<code>${contentHTML}</code>`;
270
 		return `<code>${contentHTML}</code>`;
271
 	}
271
 	}
272
 }
272
 }
275
 	source;
275
 	source;
276
 	/** @var {String|null} */
276
 	/** @var {String|null} */
277
 	alt;
277
 	alt;
278
+	/** @var {String|null} */
279
+	title;
278
 	/**
280
 	/**
279
 	 * @param {String} source
281
 	 * @param {String} source
280
 	 */
282
 	 */
281
-	constructor(source, alt) {
283
+	constructor(source, alt, title=null) {
282
 		super();
284
 		super();
283
 		this.source = source;
285
 		this.source = source;
284
 		this.alt = alt;
286
 		this.alt = alt;
287
+		this.title = title;
285
 	}
288
 	}
286
-	toHTML(config) {
289
+	toHTML(state) {
287
 		let escapedSource = this.source.replace('"', '&quot;');
290
 		let escapedSource = this.source.replace('"', '&quot;');
288
 		let html = `<img src="${escapedSource}"`;
291
 		let html = `<img src="${escapedSource}"`;
289
 		if (this.alt) {
292
 		if (this.alt) {
290
 			let altEscaped = this.alt.replace('"', '&quot');
293
 			let altEscaped = this.alt.replace('"', '&quot');
291
 			html += ` alt="${altEscaped}"`;
294
 			html += ` alt="${altEscaped}"`;
292
 		}
295
 		}
296
+		if (this.title) {
297
+			let titleEscaped = this.title.replace('"', '&quot;');
298
+			html += ` title="${titleEscaped}"`;
299
+		}
293
 		html += '>';
300
 		html += '>';
294
 		return html;
301
 		return html;
295
 	}
302
 	}
304
 		super(null, alt);
311
 		super(null, alt);
305
 		this.id = id;
312
 		this.id = id;
306
 	}
313
 	}
307
-	toHTML(config) {
314
+	toHTML(state) {
308
 		if (this.source) {
315
 		if (this.source) {
309
-			return super.toHTML(config);
316
+			return super.toHTML(state);
310
 		} else {
317
 		} else {
311
 			let altEscaped = this.alt.replace('"', '&quot;');
318
 			let altEscaped = this.alt.replace('"', '&quot;');
312
 			let idEscaped = this.id.replace('"', '&quot;');
319
 			let idEscaped = this.id.replace('"', '&quot;');
317
 class _MDFootnoteReferenceSpan extends _MDSpan {
324
 class _MDFootnoteReferenceSpan extends _MDSpan {
318
 	/** @var {String} */
325
 	/** @var {String} */
319
 	symbol;
326
 	symbol;
320
-	/** @var {Number} */
321
-	differentiator = 0;
322
 	/**
327
 	/**
323
 	 * @param {String} symbol
328
 	 * @param {String} symbol
324
 	 */
329
 	 */
326
 		super();
331
 		super();
327
 		this.symbol = symbol;
332
 		this.symbol = symbol;
328
 	}
333
 	}
329
-	toHTML(config) {
330
-		return `<sup id="fnref-${this.symbol}-${this.differentiator}"><a href="#fndef-${this.symbol}">${this.symbol}</a></sup>`;
334
+	toHTML(state) {
335
+		return `<!--FNREF:{${this.symbol}}-->`
331
 	}
336
 	}
332
 }
337
 }
333
 class _MDAbbreviationSpan extends _MDSpan {
338
 class _MDAbbreviationSpan extends _MDSpan {
344
 		this.abbreviation = abbreviation;
349
 		this.abbreviation = abbreviation;
345
 		this.definition = definition;
350
 		this.definition = definition;
346
 	}
351
 	}
347
-	toHTML(config) {
352
+	toHTML(state) {
348
 		let definitionEscaped = this.definition.replace('"', '&quot;');
353
 		let definitionEscaped = this.definition.replace('"', '&quot;');
349
 		return `<abbr title="${definitionEscaped}">${this.abbreviation}</em>`;
354
 		return `<abbr title="${definitionEscaped}">${this.abbreviation}</em>`;
350
 	}
355
 	}
355
 
360
 
356
 
361
 
357
 class _MDBlock {
362
 class _MDBlock {
358
-	toHTML(config) {
363
+	/**
364
+	 * @param {_MDState} state
365
+	 */
366
+	toHTML(state) {
359
 		throw new Error(self.constructor.name + ".toHTML not implemented");
367
 		throw new Error(self.constructor.name + ".toHTML not implemented");
360
 	}
368
 	}
361
 
369
 
362
 	/**
370
 	/**
363
 	 * @param {_MDBlock[]} blocks
371
 	 * @param {_MDBlock[]} blocks
372
+	 * @param {_MDState} state
364
 	 * @returns {String}
373
 	 * @returns {String}
365
 	 */
374
 	 */
366
-	static toHTML(blocks, config) {
367
-		return blocks.map((block) => block.toHTML(config)).join("\n");
375
+	static toHTML(blocks, state) {
376
+		return blocks.map((block) => block.toHTML(state)).join("\n");
368
 	}
377
 	}
369
 }
378
 }
370
 
379
 
378
 		super();
387
 		super();
379
 		this.#blocks = blocks;
388
 		this.#blocks = blocks;
380
 	}
389
 	}
381
-	toHTML(config) {
382
-		return _MDBlock.toHTML(this.#blocks, config);
390
+	toHTML(state) {
391
+		return _MDBlock.toHTML(this.#blocks, state);
383
 	}
392
 	}
384
 }
393
 }
385
 
394
 
395
 		this.content = content;
404
 		this.content = content;
396
 	}
405
 	}
397
 
406
 
398
-	toHTML(config) {
399
-		let contentHTML = this.content.toHTML(config);
407
+	toHTML(state) {
408
+		let contentHTML = this.content.toHTML(state);
400
 		return `<p>${contentHTML}</p>\n`;
409
 		return `<p>${contentHTML}</p>\n`;
401
 	}
410
 	}
402
 }
411
 }
417
 		this.content = content;
426
 		this.content = content;
418
 	}
427
 	}
419
 
428
 
420
-	toHTML(config) {
421
-		let contentHTML = this.content.toHTML(config);
429
+	toHTML(state) {
430
+		let contentHTML = this.content.toHTML(state);
422
 		return `<h${this.level}>${contentHTML}</h${this.level}>\n`;
431
 		return `<h${this.level}>${contentHTML}</h${this.level}>\n`;
423
 	}
432
 	}
424
 }
433
 }
433
 		super();
442
 		super();
434
 		this.content = content;
443
 		this.content = content;
435
 	}
444
 	}
436
-	toHTML(config) {
437
-		let contentHTML = _MDBlock.toHTML(this.content, config);
445
+	toHTML(state) {
446
+		let contentHTML = _MDBlock.toHTML(this.content, state);
438
 		return `<blockquote>\n${contentHTML}\n</blockquote>`;
447
 		return `<blockquote>\n${contentHTML}\n</blockquote>`;
439
 	}
448
 	}
440
 }
449
 }
449
 		super();
458
 		super();
450
 		this.items = items;
459
 		this.items = items;
451
 	}
460
 	}
452
-	toHTML(config) {
453
-		let contentHTML = _MDBlock.toHTML(this.items);
461
+	toHTML(state) {
462
+		let contentHTML = _MDBlock.toHTML(this.items, state);
454
 		return `<ul>\n${contentHTML}\n</ul>`;
463
 		return `<ul>\n${contentHTML}\n</ul>`;
455
 	}
464
 	}
456
 }
465
 }
465
 		super();
474
 		super();
466
 		this.items = items;
475
 		this.items = items;
467
 	}
476
 	}
468
-	toHTML(config) {
469
-		let contentHTML = _MDBlock.toHTML(this.items);
477
+	toHTML(state) {
478
+		let contentHTML = _MDBlock.toHTML(this.items, state);
470
 		return `<ol>\n${contentHTML}\n</ol>`;
479
 		return `<ol>\n${contentHTML}\n</ol>`;
471
 	}
480
 	}
472
 }
481
 }
483
 		this.content = content;
492
 		this.content = content;
484
 	}
493
 	}
485
 
494
 
486
-	toHTML(config) {
487
-		let contentHTML = this.content.toHTML(config);
495
+	toHTML(state) {
496
+		let contentHTML = this.content.toHTML(state);
488
 		return `<li>${contentHTML}</li>`;
497
 		return `<li>${contentHTML}</li>`;
489
 	}
498
 	}
490
 }
499
 }
499
 		super();
508
 		super();
500
 		this.#code = code;
509
 		this.#code = code;
501
 	}
510
 	}
502
-	toHTML(config) {
511
+	toHTML(state) {
503
 		return `<pre><code>${this.#code}</code></pre>`;
512
 		return `<pre><code>${this.#code}</code></pre>`;
504
 	}
513
 	}
505
 }
514
 }
506
 
515
 
507
 class _MDHorizontalRuleBlock extends _MDBlock {
516
 class _MDHorizontalRuleBlock extends _MDBlock {
508
-	toHTML(config) {
517
+	toHTML(state) {
509
 		return "<hr>\n";
518
 		return "<hr>\n";
510
 	}
519
 	}
511
 }
520
 }
522
 		super();
531
 		super();
523
 		this.#content = content;
532
 		this.#content = content;
524
 	}
533
 	}
525
-	toHTML(config) {
526
-		let contentHTML = this.#content.toHTML(config);
534
+	toHTML(state) {
535
+		let contentHTML = this.#content.toHTML(state);
527
 		let alignAttribute = _MDHAlign.toHTMLAttribute(this.align);
536
 		let alignAttribute = _MDHAlign.toHTMLAttribute(this.align);
528
 		return `<td${alignAttribute}>${contentHTML}</td>`;
537
 		return `<td${alignAttribute}>${contentHTML}</td>`;
529
 	}
538
 	}
530
 }
539
 }
531
 
540
 
532
 class _MDTableHeaderCellBlock extends _MDTableCellBlock {
541
 class _MDTableHeaderCellBlock extends _MDTableCellBlock {
533
-	toHTML(config) {
534
-		let html = super.toHTML(config);
542
+	toHTML(state) {
543
+		let html = super.toHTML(state);
535
 		let groups = /^<td(.*)td>$/.exec(html);
544
 		let groups = /^<td(.*)td>$/.exec(html);
536
 		return `<th${groups[1]}th>`;
545
 		return `<th${groups[1]}th>`;
537
 	}
546
 	}
557
 			cell.align = align;
566
 			cell.align = align;
558
 		}
567
 		}
559
 	}
568
 	}
560
-	toHTML(config) {
561
-		let cellsHTML = _MDBlock.toHTML(this.#cells, config);
569
+	toHTML(state) {
570
+		let cellsHTML = _MDBlock.toHTML(this.#cells, state);
562
 		return `<tr>\n${cellsHTML}\n</tr>`;
571
 		return `<tr>\n${cellsHTML}\n</tr>`;
563
 	}
572
 	}
564
 }
573
 }
577
 		this.#headerRow = headerRow;
586
 		this.#headerRow = headerRow;
578
 		this.#bodyRows = bodyRows;
587
 		this.#bodyRows = bodyRows;
579
 	}
588
 	}
580
-	toHTML(config) {
581
-		let headerRowHTML = this.#headerRow.toHTML(config);
582
-		let bodyRowsHTML = _MDBlock.toHTML(this.#bodyRows);
589
+	toHTML(state) {
590
+		let headerRowHTML = this.#headerRow.toHTML(state);
591
+		let bodyRowsHTML = _MDBlock.toHTML(this.#bodyRows, state);
583
 		return `<table>\n<thead>\n${headerRowHTML}\n</thead>\n<tbody>\n${bodyRowsHTML}\n</tbody>\n</table>`;
592
 		return `<table>\n<thead>\n${headerRowHTML}\n</thead>\n<tbody>\n${bodyRowsHTML}\n</tbody>\n</table>`;
584
 	}
593
 	}
585
 }
594
 }
594
 		super();
603
 		super();
595
 		this.#content = content;
604
 		this.#content = content;
596
 	}
605
 	}
597
-	toHTML(config) {
598
-		let contentHTML = _MDBlock.toHTML(this.#content);
606
+	toHTML(state) {
607
+		let contentHTML = _MDBlock.toHTML(this.#content, state);
599
 		return `<dl>\n${contentHTML}\n</dl>`;
608
 		return `<dl>\n${contentHTML}\n</dl>`;
600
 	}
609
 	}
601
 }
610
 }
610
 		super();
619
 		super();
611
 		this.#content = content;
620
 		this.#content = content;
612
 	}
621
 	}
613
-	toHTML(config) {
614
-		let contentHTML = this.#content.toHTML(config);
622
+	toHTML(state) {
623
+		let contentHTML = this.#content.toHTML(state);
615
 		return `<dt>${contentHTML}</dt>`;
624
 		return `<dt>${contentHTML}</dt>`;
616
 	}
625
 	}
617
 }
626
 }
626
 		super();
635
 		super();
627
 		this.#content = content;
636
 		this.#content = content;
628
 	}
637
 	}
629
-	toHTML(config) {
630
-		let contentHTML = this.#content.toHTML(config);
638
+	toHTML(state) {
639
+		let contentHTML = this.#content.toHTML(state);
631
 		return `<dd>${contentHTML}</dd>`;
640
 		return `<dd>${contentHTML}</dd>`;
632
 	}
641
 	}
633
 }
642
 }
634
 
643
 
635
-class _MDFootnoteBlock extends _MDBlock {
636
-	/** @var {String} */
637
-	#id;
638
-	/** @var {_MDBlock} */
639
-	#content;
640
-	/**
641
-	 * @param {String} id
642
-	 * @param {_MDBlock} content
643
-	 */
644
-	constructor(id, content) {
645
-		super();
646
-		this.#id = id;
647
-		this.#content = content;
648
-	}
649
-	toHTML(config) {
650
-		// TODO: Forward and back links
651
-		// TODO: Deferring footnotes to end of document
652
-		//<ol>
653
-		//<li id="fn:1" role="doc-endnote">
654
-		//<p>Footnote&nbsp;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
655
-		//</li>
656
-		//</ol>
657
-		return '';
658
-	}
659
-}
660
-
661
 class _MDInlineBlock extends _MDBlock {
644
 class _MDInlineBlock extends _MDBlock {
662
 	/** @var {_MDSpan[]} */
645
 	/** @var {_MDSpan[]} */
663
 	#content;
646
 	#content;
669
 		this.#content = content;
652
 		this.#content = content;
670
 	}
653
 	}
671
 
654
 
672
-	toHTML(config) {
673
-		return _MDSpan.toHTML(this.#content);
655
+	toHTML(state) {
656
+		return _MDSpan.toHTML(this.#content, state);
674
 	}
657
 	}
675
 }
658
 }
676
 
659
 
706
 	#abbreviations = {};
689
 	#abbreviations = {};
707
 
690
 
708
 	/** @var {Object} */
691
 	/** @var {Object} */
692
+	#abbreviationRegexes = {};
693
+
694
+	/** @var {Object} */
709
 	#footnotes = {};
695
 	#footnotes = {};
710
 
696
 
711
 	/** @var {number} */
697
 	/** @var {number} */
715
 	#parent = null;
701
 	#parent = null;
716
 
702
 
717
 	/** @var {Object} */
703
 	/** @var {Object} */
718
-	get abbreviations() { (this.#parent) ? this.#parent.abbreviations : this.#abbreviations; }
704
+	get abbreviations() {
705
+		return (this.#parent) ? this.#parent.abbreviations : this.#abbreviations;
706
+	}
719
 
707
 
720
 	/** @var {Object} */
708
 	/** @var {Object} */
721
-	get footnotes() { (this.#parent) ? this.#parent.footnotes : this.#footnotes; }
709
+	get abbreviationRegexes() {
710
+		return (this.#parent) ? this.#parent.abbreviationRegexes : this.#abbreviationRegexes;
711
+	}
712
+
713
+	/** @var {Object} */
714
+	get footnotes() {
715
+		return (this.#parent) ? this.#parent.footnotes : this.#footnotes;
716
+	}
722
 
717
 
723
 	/**
718
 	/**
724
 	 * @param {String[]} lines
719
 	 * @param {String[]} lines
738
 	defineAbbreviation(abbreviation, definition) {
733
 	defineAbbreviation(abbreviation, definition) {
739
 		if (this.#parent) {
734
 		if (this.#parent) {
740
 			this.#parent.defineAbbreviation(abbreviation, definition);
735
 			this.#parent.defineAbbreviation(abbreviation, definition);
741
-		} else {
742
-			this.#abbreviations[abbreviation] = definition;
736
+			return;
743
 		}
737
 		}
738
+		this.#abbreviations[abbreviation] = definition;
739
+		let regex = new RegExp("\\b(" + abbreviation + ")\\b", "ig");
740
+		this.#abbreviationRegexes[abbreviation] = regex;
744
 	}
741
 	}
745
 
742
 
746
 	/**
743
 	/**
819
 	 */
816
 	 */
820
 	static #readNextBlock(state) {
817
 	static #readNextBlock(state) {
821
 		while (state.hasLines(1) && state.lines[state.p].trim().length == 0) {
818
 		while (state.hasLines(1) && state.lines[state.p].trim().length == 0) {
822
-			console.info("Skipping blank line " + state.p);
823
 			state.p++;
819
 			state.p++;
824
 		}
820
 		}
825
 		var block;
821
 		var block;
832
 		block = this.#readIndentedCodeBlock(state); if (block) return block;
828
 		block = this.#readIndentedCodeBlock(state); if (block) return block;
833
 		block = this.#readHorizontalRule(state); if (block) return block;
829
 		block = this.#readHorizontalRule(state); if (block) return block;
834
 		block = this.#readTable(state); if (block) return block;
830
 		block = this.#readTable(state); if (block) return block;
835
-		block = this.#readDefinitionList(state); if (block) return block;
836
 		block = this.#readFootnoteDef(state); if (block) return block;
831
 		block = this.#readFootnoteDef(state); if (block) return block;
837
 		block = this.#readAbbreviationDef(state); if (block) return block;
832
 		block = this.#readAbbreviationDef(state); if (block) return block;
833
+		block = this.#readDefinitionList(state); if (block) return block;
838
 		block = this.#readParagraph(state); if (block) return block;
834
 		block = this.#readParagraph(state); if (block) return block;
839
 		return null;
835
 		return null;
840
 	}
836
 	}
1011
 	// Modified from https://emailregex.com/ to remove capture groups.
1007
 	// Modified from https://emailregex.com/ to remove capture groups.
1012
 	static #baseEmailRegex = /(?:(?:[^<>()\[\]\\.,;:\s@"]+(?:\.[^<>()\[\]\\.,;:\s@"]+)*)|(?:".+"))@(?:(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(?:(?:[a-z\-0-9]+\.)+[a-z]{2,}))/i;
1008
 	static #baseEmailRegex = /(?:(?:[^<>()\[\]\\.,;:\s@"]+(?:\.[^<>()\[\]\\.,;:\s@"]+)*)|(?:".+"))@(?:(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(?:(?:[a-z\-0-9]+\.)+[a-z]{2,}))/i;
1013
 
1009
 
1014
-	static #footnoteWithTitleRegex = /^\[\^\s*([^\]"]+?)\s+"(.*?)"\s*\]/;  // 1=symbol, 2=title
1015
-	static #footnoteRegex = /^\[\^\s*([^\]]+?)\s*\]/;  // 1=symbol
1010
+	static #footnoteWithTitleRegex = /^\[\^(\d+?)\s+"(.*?)"\]/;  // 1=symbol, 2=title
1011
+	static #footnoteRegex = /^\[\^(\d+?)\]/;  // 1=symbol
1016
 	static #labelRegex = /^\[(.*?)\]/;  // 1=content
1012
 	static #labelRegex = /^\[(.*?)\]/;  // 1=content
1017
-	static #urlWithTitleRegex = /^\((\S+?)\s+"(.*?)"\)/i;
1013
+	static #urlWithTitleRegex = /^\((\S+?)\s+"(.*?)"\)/i;  // 1=URL, 2=title
1018
 	static #urlRegex = /^\((\S+?)\)/i;  // 1=URL
1014
 	static #urlRegex = /^\((\S+?)\)/i;  // 1=URL
1019
 	static #emailWithTitleRegex = new RegExp("^\\(\\s*(" + this.#baseEmailRegex.source + ")\\s+\"(.*?)\"\\s*\\)", "i");  // 1=email, 2=title
1015
 	static #emailWithTitleRegex = new RegExp("^\\(\\s*(" + this.#baseEmailRegex.source + ")\\s+\"(.*?)\"\\s*\\)", "i");  // 1=email, 2=title
1020
 	static #emailRegex = new RegExp("^\\(\\s*(" + this.#baseEmailRegex.source + ")\\s*\\)", "i");  // 1=email
1016
 	static #emailRegex = new RegExp("^\\(\\s*(" + this.#baseEmailRegex.source + ")\\s*\\)", "i");  // 1=email
1098
 			} else if (groups = this.#emailWithTitleRegex.exec(remainder)) {
1094
 			} else if (groups = this.#emailWithTitleRegex.exec(remainder)) {
1099
 				// Email address with title   (user@example.com  "Foo")
1095
 				// Email address with title   (user@example.com  "Foo")
1100
 				endText();
1096
 				endText();
1101
-				tokens.push(new _MDToken(groups[0], _MDTokenType.Email, groups[1]));
1097
+				tokens.push(new _MDToken(groups[0], _MDTokenType.Email, groups[1], groups[2]));
1102
 				p += groups[0].length - 1;
1098
 				p += groups[0].length - 1;
1103
 			} else if (groups = this.#urlRegex.exec(remainder)) {
1099
 			} else if (groups = this.#urlRegex.exec(remainder)) {
1104
 				// URL   (https://example.com)
1100
 				// URL   (https://example.com)
1188
 			])) !== null) {
1184
 			])) !== null) {
1189
 				let alt = spans[index + 1];
1185
 				let alt = spans[index + 1];
1190
 				let url = spans[index + 2];
1186
 				let url = spans[index + 2];
1191
-				spans.splice(index, 3, new _MDImageSpan(url.content, alt.content));
1187
+				spans.splice(index, 3, new _MDImageSpan(url.content, alt.content, url.extra));
1192
 				anyChanges = true;
1188
 				anyChanges = true;
1193
 			}
1189
 			}
1194
 			
1190
 			
1341
 	 * @returns {_MDBlock}
1337
 	 * @returns {_MDBlock}
1342
 	 */
1338
 	 */
1343
 	static #readInteriorContent(state, firstLineStartPos, stopRegex, inList=false) {
1339
 	static #readInteriorContent(state, firstLineStartPos, stopRegex, inList=false) {
1344
-		// FIXME: When reading <li> content need to detect nested list without
1345
-		// a blank line
1346
 		var p = state.p;
1340
 		var p = state.p;
1347
 		var seenBlankLine = false;
1341
 		var seenBlankLine = false;
1348
 		var needsBlocks = false;
1342
 		var needsBlocks = false;
1408
 		return null;
1402
 		return null;
1409
 	}
1403
 	}
1410
 
1404
 
1411
-	static #hashHeaderRegex = /^(#{1,6})\s*([^#].*)\s*$/;  // 1=hashes, 2=content
1405
+	static #hashHeaderRegex = /^(#{1,6})\s*([^#].*?)\s*\#*\s*$/;  // 1=hashes, 2=content
1412
 
1406
 
1413
 	/**
1407
 	/**
1414
 	 * @param {_MDState} state
1408
 	 * @param {_MDState} state
1514
 	 * @returns {_MDBlock|null}
1508
 	 * @returns {_MDBlock|null}
1515
 	 */
1509
 	 */
1516
 	static #readFencedCodeBlock(state) {
1510
 	static #readFencedCodeBlock(state) {
1511
+		if (!state.hasLines(2)) return null;
1517
 		var p = state.p;
1512
 		var p = state.p;
1518
 		if (state.lines[p++].trim() != '```') return null;
1513
 		if (state.lines[p++].trim() != '```') return null;
1519
 		var codeLines = [];
1514
 		var codeLines = [];
1644
 	 * @returns {_MDBlock|null}
1639
 	 * @returns {_MDBlock|null}
1645
 	 */
1640
 	 */
1646
 	static #readDefinitionList(state) {
1641
 	static #readDefinitionList(state) {
1647
-		// TODO: Definition list
1648
-		return null;
1642
+		var p = state.p;
1643
+		var groups;
1644
+		var termCount = 0;
1645
+		var definitionCount = 0;
1646
+		var defLines = [];
1647
+		while (state.hasLines(1, p)) {
1648
+			let line = state.lines[p++];
1649
+			if (line.trim().length == 0) {
1650
+				p--;
1651
+				break;
1652
+			}
1653
+			if (/^\s+/.exec(line)) {
1654
+				if (defLines.length == 0) return null;
1655
+				defLines[defLines.length - 1] += "\n" + line;
1656
+			} else if (/^:\s+/.exec(line)) {
1657
+				defLines.push(line);
1658
+				definitionCount++;
1659
+			} else {
1660
+				defLines.push(line);
1661
+				termCount++;
1662
+			}
1663
+		}
1664
+		if (termCount == 0 || definitionCount == 0) return null;
1665
+		let blocks = defLines.map(function(line) {
1666
+			if (groups = /^:\s+(.*)$/.exec(line)) {
1667
+				return new _MDDefinitionDefinitionBlock(Markdown.#readInline(state, groups[1]));
1668
+			} else {
1669
+				return new _MDDefinitionTermBlock(Markdown.#readInline(state, line));
1670
+			}
1671
+		});
1672
+		state.p = p;
1673
+		return new _MDDefinitionListBlock(blocks);
1649
 	}
1674
 	}
1650
 
1675
 
1651
 	/**
1676
 	/**
1653
 	 * @returns {_MDBlock|null}
1678
 	 * @returns {_MDBlock|null}
1654
 	 */
1679
 	 */
1655
 	static #readFootnoteDef(state) {
1680
 	static #readFootnoteDef(state) {
1656
-		// TODO: Footnote definition
1657
-		return null;
1681
+		var p = state.p;
1682
+		let groups = /^\s*\[\^\s*([^\]]+)\s*\]:\s+(.*)\s*$/.exec(state.lines[p++]);
1683
+		if (groups === null) return null;
1684
+		let symbol = groups[1];
1685
+		let def = groups[2];
1686
+		while (state.hasLines(1, p)) {
1687
+			let line = state.lines[p++];
1688
+			if (/^\s+/.exec(line)) {
1689
+				def += "\n" + line;
1690
+			} else {
1691
+				p--;
1692
+				break;
1693
+			}
1694
+		}
1695
+		state.p = p;
1696
+		let content = this.#readInline(state, def);
1697
+		state.defineFootnote(symbol, content);
1698
+		state.p = p;
1699
+		return new _MDMultiBlock([]);
1658
 	}
1700
 	}
1659
 
1701
 
1660
 	/**
1702
 	/**
1662
 	 * @returns {_MDBlock|null}
1704
 	 * @returns {_MDBlock|null}
1663
 	 */
1705
 	 */
1664
 	static #readAbbreviationDef(state) {
1706
 	static #readAbbreviationDef(state) {
1665
-		// TODO: Abbreviation definition
1666
-		return null;
1707
+		var p = state.p;
1708
+		let line = state.lines[p++];
1709
+		let groups = /^\s*\*\[([^\]]+?)\]:\s+(.*?)\s*$/.exec(line);
1710
+		if (groups === null) return null;
1711
+		let abbrev = groups[1];
1712
+		let def = groups[2];
1713
+		state.defineAbbreviation(abbrev, def);
1714
+		state.p = p;
1715
+		return new _MDMultiBlock([]);
1667
 	}
1716
 	}
1668
 
1717
 
1669
 	/**
1718
 	/**
1689
 	}
1738
 	}
1690
 
1739
 
1691
 	/**
1740
 	/**
1741
+	 * @param {String} html
1742
+	 * @param {_MDState} state
1743
+	 * @returns {String}
1744
+	 */
1745
+	static #postProcessFootnotes(html, state) {
1746
+		let footnotes = state.footnotes;
1747
+		if (Object.keys(footnotes).length == 0) return html;
1748
+		var symbolOrder = [];
1749
+		var footnoteOccurrences = {};
1750
+		var footnoteIndex = 0;
1751
+		html = html.replace(/<!--FNREF:{(\d+)}-->/g, function(match, symbol) {
1752
+			footnoteIndex++;
1753
+			symbol = symbol.toLowerCase();
1754
+			if (!symbolOrder.includes(symbol)) {
1755
+				symbolOrder.push(symbol);
1756
+			}
1757
+			var occurrences = footnoteOccurrences[symbol] || [];
1758
+			occurrences.push(footnoteIndex);
1759
+			footnoteOccurrences[symbol] = occurrences;
1760
+			return `<sup id="footnoteref_${footnoteIndex}"><a href="#footnote_${symbol}">${symbol}</a></sup>`;
1761
+		});
1762
+		if (footnoteIndex == 0) return html;
1763
+		html += '<div class="footnotes"><hr/>';
1764
+		html += '<ol>';
1765
+		for (const symbol of symbolOrder) {
1766
+			let content = state.footnotes[symbol];
1767
+			if (!content) continue;
1768
+			html += `<li value="${symbol}" id="footnote_${symbol}">${content.toHTML(state)}`;
1769
+			for (const ref of footnoteOccurrences[symbol]) {
1770
+				html += ` <a href="#footnoteref_${ref}" class="footnote-backref" role="doc-backlink">↩︎</a>`;
1771
+			}
1772
+			html += `</li>\n`;
1773
+		}
1774
+		html += '</ol>';
1775
+		html += '</div>';
1776
+		// <!--FNREF:{symbol}-->
1777
+		return html;
1778
+	}
1779
+
1780
+	/**
1692
 	 * @param  {String} markdown
1781
 	 * @param  {String} markdown
1693
 	 * @returns {String} HTML
1782
 	 * @returns {String} HTML
1694
 	 */
1783
 	 */
1697
 		let lines = markdown.split(/(?:\n|\r|\r\n)/);
1786
 		let lines = markdown.split(/(?:\n|\r|\r\n)/);
1698
 		state.lines = lines;
1787
 		state.lines = lines;
1699
 		let blocks = this.#readBlocks(state);
1788
 		let blocks = this.#readBlocks(state);
1700
-		let html = _MDBlock.toHTML(blocks);
1789
+		let html = _MDBlock.toHTML(blocks, state);
1790
+		html = this.#postProcessFootnotes(html, state);
1701
 		return html;
1791
 		return html;
1702
 	}
1792
 	}
1703
 }
1793
 }

+ 16
- 15
markdownjs.html Ver fichero

3
 	<head>
3
 	<head>
4
 		<meta charset="utf-8">
4
 		<meta charset="utf-8">
5
 		<title>Markdown Test</title>
5
 		<title>Markdown Test</title>
6
-		<script src="js/markdown.js"></script>
7
-		<script>
8
-			function onDocumentLoad() {
9
-				document.getElementById('markdowninput').addEventListener('input', onMarkdownChange);
10
-				setTimeout(onMarkdownChange, 0);
11
-			}
12
-			function onMarkdownChange() {
13
-				let markdown = document.getElementById('markdowninput').value;
14
-				let html = Markdown.toHTML(markdown);
15
-				document.getElementById('preview').innerHTML = html;
16
-			}
17
-
18
-			document.addEventListener('DOMContentLoaded', onDocumentLoad);
19
-		</script>
20
-		<style>
6
+		<link rel="icon" href="data:;base64,iVBORw0KGgo=">
7
+		<style type="text/css">
21
 			:root, body {
8
 			:root, body {
22
 				width: 100%;
9
 				width: 100%;
23
 				height: 100%;
10
 				height: 100%;
68
 				padding: 0.2em 0.5em;
55
 				padding: 0.2em 0.5em;
69
 			}
56
 			}
70
 		</style>
57
 		</style>
58
+		<script src="js/markdown.js"></script>
59
+		<script>
60
+			function onDocumentLoad() {
61
+				document.getElementById('markdowninput').addEventListener('input', onMarkdownChange);
62
+				setTimeout(onMarkdownChange, 0);
63
+			}
64
+			function onMarkdownChange() {
65
+				let markdown = document.getElementById('markdowninput').value;
66
+				let html = Markdown.toHTML(markdown);
67
+				document.getElementById('preview').innerHTML = html;
68
+			}
69
+
70
+			document.addEventListener('DOMContentLoaded', onDocumentLoad);
71
+		</script>
71
 	</head>
72
 	</head>
72
 
73
 
73
 	<body>
74
 	<body>

Loading…
Cancelar
Guardar