Parcourir la source

Adding footnotes, definitions, abbreviations

main
Rocketsoup il y a 1 an
Parent
révision
70ef0919d0
2 fichiers modifiés avec 242 ajouts et 151 suppressions
  1. 226
    136
      js/markdown.js
  2. 16
    15
      markdownjs.html

+ 226
- 136
js/markdown.js Voir le fichier

@@ -1,26 +1,13 @@
1 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 12
 class _MDHAlign {
26 13
 	static Left = new _MDHAlign('Left');
@@ -111,16 +98,20 @@ class _MDToken {
111 98
 
112 99
 
113 100
 class _MDSpan {
114
-	toHTML(config) {
101
+	/**
102
+	 * @param {_MDState} state
103
+	 * @returns {String} HTML
104
+	 */
105
+	toHTML(state) {
115 106
 		throw new Error(self.constructor.name + ".toHTML not implemented");
116 107
 	}
117 108
 
118 109
 	/**
119 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 117
 class _MDMultiSpan extends _MDSpan {
@@ -133,8 +124,8 @@ class _MDMultiSpan extends _MDSpan {
133 124
 		super();
134 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 131
 class _MDTextSpan extends _MDSpan {
@@ -147,8 +138,17 @@ class _MDTextSpan extends _MDSpan {
147 138
 		super();
148 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 154
 class _MDHTMLSpan extends _MDSpan {
@@ -161,7 +161,7 @@ class _MDHTMLSpan extends _MDSpan {
161 161
 		super();
162 162
 		this.html = html;
163 163
 	}
164
-	toHTML(config) {
164
+	toHTML(state) {
165 165
 		return this.html;
166 166
 	}
167 167
 }
@@ -183,14 +183,14 @@ class _MDLinkSpan extends _MDSpan {
183 183
 		this.content = content;
184 184
 	}
185 185
 
186
-	toHTML(config) {
186
+	toHTML(state) {
187 187
 		let escapedLink = this.link.replace('"', '&quot;');
188 188
 		var html = `<a href="${escapedLink}"`;
189 189
 		if (target) {
190 190
 			let escapedTarget = this.target.replace('"', '&quot;');
191 191
 			html += ` target="${escapedTarget}"`;
192 192
 		}
193
-		html += '>' + this.content.toHTML(config) + '</a>';
193
+		html += '>' + this.content.toHTML(state) + '</a>';
194 194
 		return html;
195 195
 	}
196 196
 }
@@ -201,11 +201,11 @@ class _MDReferencedLinkSpan extends _MDLinkSpan {
201 201
 		super(null, content);
202 202
 		this.id = id;
203 203
 	}
204
-	toHTML(config) {
204
+	toHTML(state) {
205 205
 		if (this.link) {
206
-			return super.toHTML(config);
206
+			return super.toHTML(state);
207 207
 		} else {
208
-			let contentHTML = this.content.toHTML(config);
208
+			let contentHTML = this.content.toHTML(state);
209 209
 			return `[${contentHTML}][${this.id}]`;
210 210
 		}
211 211
 	}
@@ -220,8 +220,8 @@ class _MDEmphasisSpan extends _MDSpan {
220 220
 		super();
221 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 225
 		return `<em>${contentHTML}</em>`;
226 226
 	}
227 227
 }
@@ -235,8 +235,8 @@ class _MDStrongSpan extends _MDSpan {
235 235
 		super();
236 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 240
 		return `<strong>${contentHTML}</strong>`;
241 241
 	}
242 242
 }
@@ -250,8 +250,8 @@ class _MDStrikethroughSpan extends _MDSpan {
250 250
 		super();
251 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 255
 		return `<strike>${contentHTML}</strike>`;
256 256
 	}
257 257
 }
@@ -265,8 +265,8 @@ class _MDCodeSpan extends _MDSpan {
265 265
 		super();
266 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 270
 		return `<code>${contentHTML}</code>`;
271 271
 	}
272 272
 }
@@ -275,21 +275,28 @@ class _MDImageSpan extends _MDSpan {
275 275
 	source;
276 276
 	/** @var {String|null} */
277 277
 	alt;
278
+	/** @var {String|null} */
279
+	title;
278 280
 	/**
279 281
 	 * @param {String} source
280 282
 	 */
281
-	constructor(source, alt) {
283
+	constructor(source, alt, title=null) {
282 284
 		super();
283 285
 		this.source = source;
284 286
 		this.alt = alt;
287
+		this.title = title;
285 288
 	}
286
-	toHTML(config) {
289
+	toHTML(state) {
287 290
 		let escapedSource = this.source.replace('"', '&quot;');
288 291
 		let html = `<img src="${escapedSource}"`;
289 292
 		if (this.alt) {
290 293
 			let altEscaped = this.alt.replace('"', '&quot');
291 294
 			html += ` alt="${altEscaped}"`;
292 295
 		}
296
+		if (this.title) {
297
+			let titleEscaped = this.title.replace('"', '&quot;');
298
+			html += ` title="${titleEscaped}"`;
299
+		}
293 300
 		html += '>';
294 301
 		return html;
295 302
 	}
@@ -304,9 +311,9 @@ class _MDReferencedImageSpan extends _MDImageSpan {
304 311
 		super(null, alt);
305 312
 		this.id = id;
306 313
 	}
307
-	toHTML(config) {
314
+	toHTML(state) {
308 315
 		if (this.source) {
309
-			return super.toHTML(config);
316
+			return super.toHTML(state);
310 317
 		} else {
311 318
 			let altEscaped = this.alt.replace('"', '&quot;');
312 319
 			let idEscaped = this.id.replace('"', '&quot;');
@@ -317,8 +324,6 @@ class _MDReferencedImageSpan extends _MDImageSpan {
317 324
 class _MDFootnoteReferenceSpan extends _MDSpan {
318 325
 	/** @var {String} */
319 326
 	symbol;
320
-	/** @var {Number} */
321
-	differentiator = 0;
322 327
 	/**
323 328
 	 * @param {String} symbol
324 329
 	 */
@@ -326,8 +331,8 @@ class _MDFootnoteReferenceSpan extends _MDSpan {
326 331
 		super();
327 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 338
 class _MDAbbreviationSpan extends _MDSpan {
@@ -344,7 +349,7 @@ class _MDAbbreviationSpan extends _MDSpan {
344 349
 		this.abbreviation = abbreviation;
345 350
 		this.definition = definition;
346 351
 	}
347
-	toHTML(config) {
352
+	toHTML(state) {
348 353
 		let definitionEscaped = this.definition.replace('"', '&quot;');
349 354
 		return `<abbr title="${definitionEscaped}">${this.abbreviation}</em>`;
350 355
 	}
@@ -355,16 +360,20 @@ class _MDAbbreviationSpan extends _MDSpan {
355 360
 
356 361
 
357 362
 class _MDBlock {
358
-	toHTML(config) {
363
+	/**
364
+	 * @param {_MDState} state
365
+	 */
366
+	toHTML(state) {
359 367
 		throw new Error(self.constructor.name + ".toHTML not implemented");
360 368
 	}
361 369
 
362 370
 	/**
363 371
 	 * @param {_MDBlock[]} blocks
372
+	 * @param {_MDState} state
364 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,8 +387,8 @@ class _MDMultiBlock extends _MDBlock {
378 387
 		super();
379 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,8 +404,8 @@ class _MDParagraphBlock extends _MDBlock {
395 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 409
 		return `<p>${contentHTML}</p>\n`;
401 410
 	}
402 411
 }
@@ -417,8 +426,8 @@ class _MDHeaderBlock extends _MDBlock {
417 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 431
 		return `<h${this.level}>${contentHTML}</h${this.level}>\n`;
423 432
 	}
424 433
 }
@@ -433,8 +442,8 @@ class _MDBlockquoteBlock extends _MDBlock {
433 442
 		super();
434 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 447
 		return `<blockquote>\n${contentHTML}\n</blockquote>`;
439 448
 	}
440 449
 }
@@ -449,8 +458,8 @@ class _MDUnorderedListBlock extends _MDBlock {
449 458
 		super();
450 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 463
 		return `<ul>\n${contentHTML}\n</ul>`;
455 464
 	}
456 465
 }
@@ -465,8 +474,8 @@ class _MDOrderedListBlock extends _MDBlock {
465 474
 		super();
466 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 479
 		return `<ol>\n${contentHTML}\n</ol>`;
471 480
 	}
472 481
 }
@@ -483,8 +492,8 @@ class _MDListItemBlock extends _MDBlock {
483 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 497
 		return `<li>${contentHTML}</li>`;
489 498
 	}
490 499
 }
@@ -499,13 +508,13 @@ class _MDCodeBlock extends _MDBlock {
499 508
 		super();
500 509
 		this.#code = code;
501 510
 	}
502
-	toHTML(config) {
511
+	toHTML(state) {
503 512
 		return `<pre><code>${this.#code}</code></pre>`;
504 513
 	}
505 514
 }
506 515
 
507 516
 class _MDHorizontalRuleBlock extends _MDBlock {
508
-	toHTML(config) {
517
+	toHTML(state) {
509 518
 		return "<hr>\n";
510 519
 	}
511 520
 }
@@ -522,16 +531,16 @@ class _MDTableCellBlock extends _MDBlock {
522 531
 		super();
523 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 536
 		let alignAttribute = _MDHAlign.toHTMLAttribute(this.align);
528 537
 		return `<td${alignAttribute}>${contentHTML}</td>`;
529 538
 	}
530 539
 }
531 540
 
532 541
 class _MDTableHeaderCellBlock extends _MDTableCellBlock {
533
-	toHTML(config) {
534
-		let html = super.toHTML(config);
542
+	toHTML(state) {
543
+		let html = super.toHTML(state);
535 544
 		let groups = /^<td(.*)td>$/.exec(html);
536 545
 		return `<th${groups[1]}th>`;
537 546
 	}
@@ -557,8 +566,8 @@ class _MDTableRowBlock extends _MDBlock {
557 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 571
 		return `<tr>\n${cellsHTML}\n</tr>`;
563 572
 	}
564 573
 }
@@ -577,9 +586,9 @@ class _MDTableBlock extends _MDBlock {
577 586
 		this.#headerRow = headerRow;
578 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 592
 		return `<table>\n<thead>\n${headerRowHTML}\n</thead>\n<tbody>\n${bodyRowsHTML}\n</tbody>\n</table>`;
584 593
 	}
585 594
 }
@@ -594,8 +603,8 @@ class _MDDefinitionListBlock extends _MDBlock {
594 603
 		super();
595 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 608
 		return `<dl>\n${contentHTML}\n</dl>`;
600 609
 	}
601 610
 }
@@ -610,8 +619,8 @@ class _MDDefinitionTermBlock extends _MDBlock {
610 619
 		super();
611 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 624
 		return `<dt>${contentHTML}</dt>`;
616 625
 	}
617 626
 }
@@ -626,38 +635,12 @@ class _MDDefinitionDefinitionBlock extends _MDBlock {
626 635
 		super();
627 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 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 644
 class _MDInlineBlock extends _MDBlock {
662 645
 	/** @var {_MDSpan[]} */
663 646
 	#content;
@@ -669,8 +652,8 @@ class _MDInlineBlock extends _MDBlock {
669 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,6 +689,9 @@ class _MDState {
706 689
 	#abbreviations = {};
707 690
 
708 691
 	/** @var {Object} */
692
+	#abbreviationRegexes = {};
693
+
694
+	/** @var {Object} */
709 695
 	#footnotes = {};
710 696
 
711 697
 	/** @var {number} */
@@ -715,10 +701,19 @@ class _MDState {
715 701
 	#parent = null;
716 702
 
717 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 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 719
 	 * @param {String[]} lines
@@ -738,9 +733,11 @@ class _MDState {
738 733
 	defineAbbreviation(abbreviation, definition) {
739 734
 		if (this.#parent) {
740 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,7 +816,6 @@ class Markdown {
819 816
 	 */
820 817
 	static #readNextBlock(state) {
821 818
 		while (state.hasLines(1) && state.lines[state.p].trim().length == 0) {
822
-			console.info("Skipping blank line " + state.p);
823 819
 			state.p++;
824 820
 		}
825 821
 		var block;
@@ -832,9 +828,9 @@ class Markdown {
832 828
 		block = this.#readIndentedCodeBlock(state); if (block) return block;
833 829
 		block = this.#readHorizontalRule(state); if (block) return block;
834 830
 		block = this.#readTable(state); if (block) return block;
835
-		block = this.#readDefinitionList(state); if (block) return block;
836 831
 		block = this.#readFootnoteDef(state); if (block) return block;
837 832
 		block = this.#readAbbreviationDef(state); if (block) return block;
833
+		block = this.#readDefinitionList(state); if (block) return block;
838 834
 		block = this.#readParagraph(state); if (block) return block;
839 835
 		return null;
840 836
 	}
@@ -1011,10 +1007,10 @@ class Markdown {
1011 1007
 	// Modified from https://emailregex.com/ to remove capture groups.
1012 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 1012
 	static #labelRegex = /^\[(.*?)\]/;  // 1=content
1017
-	static #urlWithTitleRegex = /^\((\S+?)\s+"(.*?)"\)/i;
1013
+	static #urlWithTitleRegex = /^\((\S+?)\s+"(.*?)"\)/i;  // 1=URL, 2=title
1018 1014
 	static #urlRegex = /^\((\S+?)\)/i;  // 1=URL
1019 1015
 	static #emailWithTitleRegex = new RegExp("^\\(\\s*(" + this.#baseEmailRegex.source + ")\\s+\"(.*?)\"\\s*\\)", "i");  // 1=email, 2=title
1020 1016
 	static #emailRegex = new RegExp("^\\(\\s*(" + this.#baseEmailRegex.source + ")\\s*\\)", "i");  // 1=email
@@ -1098,7 +1094,7 @@ class Markdown {
1098 1094
 			} else if (groups = this.#emailWithTitleRegex.exec(remainder)) {
1099 1095
 				// Email address with title   (user@example.com  "Foo")
1100 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 1098
 				p += groups[0].length - 1;
1103 1099
 			} else if (groups = this.#urlRegex.exec(remainder)) {
1104 1100
 				// URL   (https://example.com)
@@ -1188,7 +1184,7 @@ class Markdown {
1188 1184
 			])) !== null) {
1189 1185
 				let alt = spans[index + 1];
1190 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 1188
 				anyChanges = true;
1193 1189
 			}
1194 1190
 			
@@ -1341,8 +1337,6 @@ class Markdown {
1341 1337
 	 * @returns {_MDBlock}
1342 1338
 	 */
1343 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 1340
 		var p = state.p;
1347 1341
 		var seenBlankLine = false;
1348 1342
 		var needsBlocks = false;
@@ -1408,7 +1402,7 @@ class Markdown {
1408 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 1408
 	 * @param {_MDState} state
@@ -1514,6 +1508,7 @@ class Markdown {
1514 1508
 	 * @returns {_MDBlock|null}
1515 1509
 	 */
1516 1510
 	static #readFencedCodeBlock(state) {
1511
+		if (!state.hasLines(2)) return null;
1517 1512
 		var p = state.p;
1518 1513
 		if (state.lines[p++].trim() != '```') return null;
1519 1514
 		var codeLines = [];
@@ -1644,8 +1639,38 @@ class Markdown {
1644 1639
 	 * @returns {_MDBlock|null}
1645 1640
 	 */
1646 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,8 +1678,25 @@ class Markdown {
1653 1678
 	 * @returns {_MDBlock|null}
1654 1679
 	 */
1655 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,8 +1704,15 @@ class Markdown {
1662 1704
 	 * @returns {_MDBlock|null}
1663 1705
 	 */
1664 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,6 +1738,46 @@ class Markdown {
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 1781
 	 * @param  {String} markdown
1693 1782
 	 * @returns {String} HTML
1694 1783
 	 */
@@ -1697,7 +1786,8 @@ class Markdown {
1697 1786
 		let lines = markdown.split(/(?:\n|\r|\r\n)/);
1698 1787
 		state.lines = lines;
1699 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 1791
 		return html;
1702 1792
 	}
1703 1793
 }

+ 16
- 15
markdownjs.html Voir le fichier

@@ -3,21 +3,8 @@
3 3
 	<head>
4 4
 		<meta charset="utf-8">
5 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 8
 			:root, body {
22 9
 				width: 100%;
23 10
 				height: 100%;
@@ -68,6 +55,20 @@
68 55
 				padding: 0.2em 0.5em;
69 56
 			}
70 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 72
 	</head>
72 73
 
73 74
 	<body>

Chargement…
Annuler
Enregistrer