Ver código fonte

All blocks and spans covered by unit tests

main
Rocketsoup 1 ano atrás
pai
commit
ee94689498
2 arquivos alterados com 786 adições e 163 exclusões
  1. 279
    123
      js/markdown.js
  2. 507
    40
      testjs.html

+ 279
- 123
js/markdown.js Ver arquivo

@@ -1,10 +1,11 @@
1 1
 // FIXME: Nested blockquotes require blank line
2 2
 // TODO: HTML tags probably need better handling. Consider whether interior of matched tags should be interpreted as markdown.
3 3
 // TODO: Test broken/incomplete syntax thoroughly
4
-// TODO: Sanity checks on loops/recursion?
5
-// TODO: Tolerate whitespace between tokens (e.g. [click here] [urlref])
4
+// TODO: Sanity checks on loops/recursion
6 5
 // TODO: Spreadsheet functions in tables
7
-// TODO: Support document differentiators for CSS identifiers
6
+// TODO: Support document differentiators for CSS identifiers (using markdown to render 2+ documents in the same page, need ids to be unique within the page)
7
+// TODO: Support language marker in ``` to at least add a CSS class to the <pre>
8
+// TODO: Better way to detect start of new block inside of list item without line break
8 9
 
9 10
 class MDTokenType {
10 11
 	static Text = new MDTokenType('Text');
@@ -43,6 +44,10 @@ class MDTokenType {
43 44
 	toString() {
44 45
 		return `${this.constructor.name}.${this.name}`;
45 46
 	}
47
+
48
+	equals(other) {
49
+		return (other instanceof MDTokenType) && other.name == this.name;
50
+	}
46 51
 }
47 52
 
48 53
 class MDToken {
@@ -83,6 +88,10 @@ class MDToken {
83 88
 		this.tag = tag;
84 89
 	}
85 90
 
91
+	toString() {
92
+		return `(${this.constructor.name} type=${this.type.toString()} content=${this.content})`;
93
+	}
94
+
86 95
 	/**
87 96
 	 * Searches an array of MDToken for the given pattern of MDTokenTypes.
88 97
 	 * If found, returns an object with the given keys.
@@ -190,6 +199,17 @@ class MDToken {
190 199
 		}
191 200
 		return null;
192 201
 	}
202
+
203
+	equals(other) {
204
+		if (!(other instanceof MDToken)) return false;
205
+		if (other.original !== this.original) return false;
206
+		if (!other.type.equals(this.type)) return false;
207
+		if (other.content !== this.content) return false;
208
+		if (other.extra !== this.extra) return false;
209
+		if (!MDUtils.equal(other.tag, this.tag)) return false;
210
+		if (!MDUtils.equals(other.modifier, this.modifier)) return false;
211
+		return true
212
+	}
193 213
 }
194 214
 
195 215
 class MDUtils {
@@ -199,15 +219,26 @@ class MDUtils {
199 219
 	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;
200 220
 
201 221
 	/**
202
-	 * @param {string} str
203
-	 * @returns {string}
222
+	 * Escapes special HTML characters.
223
+	 *
224
+	 * @param {string} str - string to escape
225
+	 * @param {boolean} encodeNewlinesAsBreaks - whether to convert newline characters to `<br>` tags
226
+	 * @returns {string} escaped HTML
204 227
 	 */
205
-	static escapeHTML(str) {
206
-		return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
228
+	static escapeHTML(str, encodeNewlinesAsBreaks=false) {
229
+		var html = str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
230
+		if (encodeNewlinesAsBreaks) {
231
+			html = html.replace(/\n/g, "<br>\n");
232
+		}
233
+		return html;
207 234
 	}
208 235
 
209 236
 	/**
210
-	 * @param {string} email
237
+	 * Encodes characters as HTML numeric entities to make it marginally more
238
+	 * difficult for web scrapers to grab sensitive info.
239
+	 *
240
+	 * @param {string} text - text to escape
241
+	 * @returns {string} escaped HTML
211 242
 	 */
212 243
 	static escapeObfuscated(text) {
213 244
 		var html = '';
@@ -247,6 +278,20 @@ class MDUtils {
247 278
 	}
248 279
 
249 280
 	/**
281
+	 * Tests if an array of lines contains at least one blank. A blank line
282
+	 * can contain whitespace.
283
+	 * 
284
+	 * @param {String[]} lines
285
+	 * @returns {boolean} whether `lines` contains any whitespace-only lines
286
+	 */
287
+	static containsBlankLine(lines) {
288
+		for (const line of lines) {
289
+			if (line.trim().length == 0) return true;
290
+		}
291
+		return false;
292
+	}
293
+
294
+	/**
250 295
 	 * Counts the number of indent levels in a line of text. Partial indents
251 296
 	 * (1 to 3 spaces) are counted as one indent level unless `fullIndentsOnly`
252 297
 	 * is `true`.
@@ -361,6 +406,51 @@ class MDUtils {
361 406
 		}
362 407
 		return typeof value;
363 408
 	}
409
+
410
+	static #equalArrays(a, b) {
411
+		if (a === b) return true;
412
+		if (!(a instanceof Array) || !(b instanceof Array)) return false;
413
+		if (a == null || b == null) return false;
414
+		if (a.length != b.length) return false;
415
+		for (var i = 0; i < a.length; i++) {
416
+			if (!this.equal(a[i], b[i])) return false;
417
+		}
418
+		return true;
419
+	}
420
+
421
+	static #equalObjects(a, b) {
422
+		if (a === b) return true;
423
+		if (!(a instanceof Object) || !(b instanceof Object)) return false;
424
+		if (a == null || b == null) return false;
425
+		if (a.equals !== undefined) {
426
+			return a.equals(b);
427
+		}
428
+		for (const key of Object.keys(a)) {
429
+			if (!this.equal(a[key], b[key])) return false;
430
+		}
431
+		for (const key of Object.keys(b)) {
432
+			if (!this.equal(a[key], b[key])) return false;
433
+		}
434
+		return true;
435
+	}
436
+
437
+	/**
438
+	 * Tests for equality on lots of different kinds of values including objects
439
+	 * and arrays. Will use `.equals` on objects that implement it.
440
+	 * 
441
+	 * @param {any} a
442
+	 * @param {any} b
443
+	 * @returns {boolean}
444
+	 */
445
+	static equal(a, b) {
446
+		if (a instanceof Array && b instanceof Array) {
447
+			return this.#equalArrays(a, b);
448
+		}
449
+		if (a instanceof Object && b instanceof Object) {
450
+			return this.#equalObjects(a, b);
451
+		}
452
+		return a == b;
453
+	}
364 454
 }
365 455
 
366 456
 
@@ -499,7 +589,7 @@ class MDHashHeaderBlockReader extends MDBlockReader {
499 589
 		state.p = p;
500 590
 		const level = groups[1].length;
501 591
 		const content = groups[2];
502
-		let block = new MDHeaderBlock(level, state.inlineMarkdownToSpan(content));
592
+		let block = new MDHeaderBlock(level, new MDInlineBlock(state.inlineMarkdownToSpan(content)));
503 593
 		if (modifier) modifier.applyTo(block);
504 594
 		return block;
505 595
 	}
@@ -546,6 +636,9 @@ class MDBlockQuoteBlockReader extends MDBlockReader {
546 636
 	}
547 637
 }
548 638
 
639
+/**
640
+ * Abstract base class for ordered and unordered lists.
641
+ */
549 642
 class MDBaseListBlockReader extends MDBlockReader {
550 643
 	constructor(priority) {
551 644
 		super(priority);
@@ -868,7 +961,7 @@ class MDTableBlockReader extends MDBlockReader {
868 961
 		if (line.endsWith('|')) line = line.substring(0, line.length - 1);
869 962
 		let cellTokens = line.split('|');
870 963
 		let cells = cellTokens.map(function(token) {
871
-			let content = state.inlineMarkdownToSpan(token);
964
+			let content = state.inlineMarkdownToSpan(token.trim());
872 965
 			return isHeader ? new MDTableHeaderCellBlock(content) : new MDTableCellBlock(content);
873 966
 		});
874 967
 		state.p = p;
@@ -956,7 +1049,6 @@ class MDDefinitionListBlockReader extends MDBlockReader {
956 1049
 		while (state.hasLines(1, p)) {
957 1050
 			let line = state.lines[p++];
958 1051
 			if (line.trim().length == 0) {
959
-				p--;
960 1052
 				break;
961 1053
 			}
962 1054
 			if (/^\s+/.exec(line)) {
@@ -972,7 +1064,7 @@ class MDDefinitionListBlockReader extends MDBlockReader {
972 1064
 		}
973 1065
 		if (termCount == 0 || definitionCount == 0) return null;
974 1066
 		let blocks = defLines.map(function(line) {
975
-			if (groups = /^:\s+(.*)$/.exec(line)) {
1067
+			if (groups = /^:\s+(.*?)$/s.exec(line)) {
976 1068
 				return new MDDefinitionDefinitionBlock(state.inlineMarkdownToSpans(groups[1]));
977 1069
 			} else {
978 1070
 				return new MDDefinitionTermBlock(state.inlineMarkdownToSpans(line));
@@ -1123,13 +1215,19 @@ class MDParagraphBlockReader extends MDBlockReader {
1123 1215
 	readBlock(state) {
1124 1216
 		var paragraphLines = [];
1125 1217
 		var p = state.p;
1218
+		var foundBlankLine = false;
1126 1219
 		while (p < state.lines.length) {
1127 1220
 			let line = state.lines[p++];
1128 1221
 			if (line.trim().length == 0) {
1222
+				foundBlankLine = true;
1129 1223
 				break;
1130 1224
 			}
1131 1225
 			paragraphLines.push(line);
1132 1226
 		}
1227
+		if (state.p == 0 && p >= state.lines.length) {
1228
+			// If it's the entire document don't wrap it in a paragraph
1229
+			return null;
1230
+		}
1133 1231
 		if (paragraphLines.length > 0) {
1134 1232
 			state.p = p;
1135 1233
 			let content = paragraphLines.join("\n");
@@ -1683,27 +1781,29 @@ class MDParagraphBlock extends MDBlock {
1683 1781
 class MDHeaderBlock extends MDBlock {
1684 1782
 	/** @type {number} */
1685 1783
 	#level;
1686
-	/** @type {MDBlock} */
1784
+	/** @type {MDBlock[]} */
1687 1785
 	#content;
1688 1786
 
1689 1787
 	/**
1690 1788
 	 * @param {number} level
1691
-	 * @param {MDBlock} content
1789
+	 * @param {MDBlock|MDBlock[]} content
1692 1790
 	 */
1693 1791
 	constructor(level, content) {
1694 1792
 		super();
1695 1793
 		this.#level = level;
1696
-		this.#content = content;
1794
+		this.#content = (content instanceof Array) ? content : [ content ];
1697 1795
 	}
1698 1796
 
1699 1797
 	toHTML(state) {
1700
-		let contentHTML = this.#content.toHTML(state);
1701
-		return `<h${this.#level}${this.htmlAttributes()}>${contentHTML}</h${this.level}>\n`;
1798
+		let contentHTML = MDBlock.toHTML(this.#content, state);
1799
+		return `<h${this.#level}${this.htmlAttributes()}>${contentHTML}</h${this.#level}>\n`;
1702 1800
 	}
1703 1801
 
1704 1802
 	visitChildren(fn) {
1705
-		fn(this.content);
1706
-		this.content.visitChildren(fn);
1803
+		for (const child of this.#content) {
1804
+			fn(child);
1805
+			child.visitChildren(fn);
1806
+		}
1707 1807
 	}
1708 1808
 }
1709 1809
 
@@ -1775,7 +1875,7 @@ class MDOrderedListBlock extends MDBlock {
1775 1875
 
1776 1876
 	htmlAttributes() {
1777 1877
 		var html = super.htmlAttributes();
1778
-		if (this.startOrdinal !== null) {
1878
+		if (this.startOrdinal !== null && this.startOrdinal != 1) {
1779 1879
 			html += ` start="${this.startOrdinal}"`;
1780 1880
 		}
1781 1881
 		return html;
@@ -1989,48 +2089,64 @@ class MDDefinitionListBlock extends MDBlock {
1989 2089
 }
1990 2090
 
1991 2091
 class MDDefinitionTermBlock extends MDBlock {
1992
-	/** @type {MDBlock} */
2092
+	/** @type {MDBlock[]} */
1993 2093
 	#content;
1994 2094
 
1995 2095
 	/**
1996
-	 * @param {MDBlock} content
2096
+	 * @param {MDBlock|MDBlock[]} content
1997 2097
 	 */
1998 2098
 	constructor(content) {
1999 2099
 		super();
2000
-		this.#content = content;
2100
+		if (content instanceof Array) {
2101
+			this.#content = content;
2102
+		} else if (content instanceof MDBlock) {
2103
+			this.#content = [ content ];
2104
+		} else {
2105
+			throw new Error(`${this.constructor.name} expects MDBlock or MDBlock[], got ${typeof content}`);
2106
+		}
2001 2107
 	}
2002 2108
 
2003 2109
 	toHTML(state) {
2004
-		let contentHTML = this.#content.toHTML(state);
2110
+		let contentHTML = MDBlock.toHTML(this.#content, state);
2005 2111
 		return `<dt${this.htmlAttributes()}>${contentHTML}</dt>`;
2006 2112
 	}
2007 2113
 
2008 2114
 	visitChildren(fn) {
2009
-		fn(this.#content);
2010
-		this.#content.visitChildren(fn);
2115
+		for (const child of this.#content) {
2116
+			fn(child);
2117
+			child.visitChildren(fn);
2118
+		}
2011 2119
 	}
2012 2120
 }
2013 2121
 
2014 2122
 class MDDefinitionDefinitionBlock extends MDBlock {
2015
-	/** @type {MDBlock} */
2123
+	/** @type {MDBlock[]} */
2016 2124
 	#content;
2017 2125
 
2018 2126
 	/**
2019
-	 * @param {MDBlock} content
2127
+	 * @param {MDBlock|MDBlock[]} content
2020 2128
 	 */
2021 2129
 	constructor(content) {
2022 2130
 		super();
2023
-		this.#content = content;
2131
+		if (content instanceof Array) {
2132
+			this.#content = content;
2133
+		} else if (content instanceof MDBlock) {
2134
+			this.#content = [ content ];
2135
+		} else {
2136
+			throw new Error(`${this.constructor.name} expects MDBlock or MDBlock[], got ${typeof content}`);
2137
+		}
2024 2138
 	}
2025 2139
 
2026 2140
 	toHTML(state) {
2027
-		let contentHTML = this.#content.toHTML(state);
2141
+		let contentHTML = MDBlock.toHTML(this.#content, state);
2028 2142
 		return `<dd${this.htmlAttributes()}>${contentHTML}</dd>`;
2029 2143
 	}
2030 2144
 
2031 2145
 	visitChildren(fn) {
2032
-		fn(this.#content);
2033
-		this.#content.visitChildren(fn);
2146
+		for (const child of this.#content) {
2147
+			fn(child);
2148
+			child.visitChildren(fn);
2149
+		}
2034 2150
 	}
2035 2151
 }
2036 2152
 
@@ -2071,11 +2187,16 @@ class MDInlineBlock extends MDBlock {
2071 2187
 	#content;
2072 2188
 
2073 2189
 	/**
2074
-	 * @param {MDSpan[]} content
2190
+	 * @param {MDSpan|MDSpan[]} content
2075 2191
 	 */
2076 2192
 	constructor(content) {
2077 2193
 		super();
2078
-		this.#content = content;
2194
+		this.#content = (content instanceof Array) ? content : [ content ];
2195
+		for (const span of this.#content) {
2196
+			if (!(span instanceof MDSpan)) {
2197
+				throw new Error(`${this.constructor.name} expects MDSpan or MDSpan[], got ${MDUtils.typename(span)}`);
2198
+			}
2199
+		}
2079 2200
 	}
2080 2201
 
2081 2202
 	toHTML(state) {
@@ -2508,6 +2629,15 @@ class MDHTMLTag {
2508 2629
 		this.attributes = attributes;
2509 2630
 	}
2510 2631
 
2632
+	toString() {
2633
+		return this.fullTag;
2634
+	}
2635
+
2636
+	equals(other) {
2637
+		if (!(other instanceof MDHTMLTag)) return false;
2638
+		return other.fullTag == this.fullTag;
2639
+	}
2640
+
2511 2641
 	static #htmlTagNameFirstRegex = /[a-z]/i;
2512 2642
 	static #htmlTagNameMedialRegex = /[a-z0-9]/i;
2513 2643
 	static #htmlAttributeNameFirstRegex = /[a-z]/i;
@@ -2674,6 +2804,106 @@ class MDHTMLTag {
2674 2804
 	}
2675 2805
 }
2676 2806
 
2807
+class MDTagModifier {
2808
+	/** @type {string} */
2809
+	original;
2810
+	/** @type {string[]} */
2811
+	cssClasses = [];
2812
+	/** @type {string|null} */
2813
+	cssId = null;
2814
+	/** @type {object} */
2815
+	attributes = {};
2816
+
2817
+	static #baseClassRegex = /\.([a-z_\-][a-z0-9_\-]*?)/i;
2818
+	static #baseIdRegex = /#([a-z_\-][a-z0-9_\-]*?)/i;
2819
+	static #baseAttributeRegex = /([a-z0-9]+?)=([^\s\}]+?)/i;
2820
+	static #baseRegex = /\{([^}]+?)}/i;
2821
+	static #leadingClassRegex = new RegExp('^' + this.#baseRegex.source, 'i');
2822
+	static #trailingClassRegex = new RegExp('^(.*?)\\s*' + this.#baseRegex.source + '\\s*$', 'i');
2823
+	static #classRegex = new RegExp('^' + this.#baseClassRegex.source + '$', 'i');  // 1=classname
2824
+	static #idRegex = new RegExp('^' + this.#baseIdRegex.source + '$', 'i');  // 1=id
2825
+	static #attributeRegex = new RegExp('^' + this.#baseAttributeRegex.source + '$', 'i');  // 1=attribute name, 2=attribute value
2826
+
2827
+	/**
2828
+	 * @param {MDBlock|MDSpan} elem
2829
+	 */
2830
+	applyTo(elem) {
2831
+		if (elem instanceof MDBlock || elem instanceof MDSpan) {
2832
+			elem.cssClasses = elem.cssClasses.concat(this.cssClasses);
2833
+			if (this.cssId) elem.cssId = this.cssId;
2834
+			for (const name in this.attributes) {
2835
+				elem.attributes[name] = this.attributes[name];
2836
+			}
2837
+		}
2838
+	}
2839
+
2840
+	equals(other) {
2841
+		if (!(other instanceof MDTagModifier)) return false;
2842
+		if (!MDUtils.equal(other.cssClasses, this.cssClasses)) return false;
2843
+		if (other.cssId !== this.cssId) return false;
2844
+		if (!MDUtils.equal(other.attributes, this.attributes)) return false;
2845
+		return true;
2846
+	}
2847
+
2848
+	toString() {
2849
+		return this.original;
2850
+	}
2851
+
2852
+	static #fromContents(contents) {
2853
+		let modifierTokens = contents.split(/\s+/);
2854
+		let mod = new MDTagModifier();
2855
+		mod.original = `{${contents}}`;
2856
+		var groups;
2857
+		for (const token of modifierTokens) {
2858
+			if (token.trim() == '') continue;
2859
+			if (groups = this.#classRegex.exec(token)) {
2860
+				mod.cssClasses.push(groups[1]);
2861
+			} else if (groups = this.#idRegex.exec(token)) {
2862
+				mod.cssId = groups[1];
2863
+			} else if (groups = this.#attributeRegex.exec(token)) {
2864
+				mod.attributes[groups[1]] = groups[2];
2865
+			} else {
2866
+				return null;
2867
+			}
2868
+		}
2869
+		return mod;
2870
+	}
2871
+
2872
+	/**
2873
+	 * Extracts modifier from line.
2874
+	 * @param {string} line
2875
+	 * @returns {Array} Tuple with remaining line and MDTagModifier.
2876
+	 */
2877
+	static fromLine(line) {
2878
+		let groups = this.#trailingClassRegex.exec(line);
2879
+		if (groups === null) return [ line, null ];
2880
+		let bareLine = groups[1];
2881
+		let mod = this.#fromContents(groups[2]);
2882
+		return [ bareLine, mod ];
2883
+	}
2884
+
2885
+	/**
2886
+	 * Extracts modifier from head of string.
2887
+	 * @param {string} line
2888
+	 * @returns {MDTagModifier}
2889
+	 */
2890
+	static fromStart(line) {
2891
+		let groups = this.#leadingClassRegex.exec(line);
2892
+		if (groups === null) return null;
2893
+		return this.#fromContents(groups[1]);
2894
+	}
2895
+
2896
+	/**
2897
+	 * @param {string} line
2898
+	 * @returns {string}
2899
+	 */
2900
+	static strip(line) {
2901
+		let groups = this.#trailingClassRegex.exec(line);
2902
+		if (groups === null) return line;
2903
+		return groups[1];
2904
+	}
2905
+}
2906
+
2677 2907
 class MDState {
2678 2908
 	/** @type {string[]} */
2679 2909
 	#lines = [];
@@ -2907,6 +3137,19 @@ class MDState {
2907 3137
 	}
2908 3138
 
2909 3139
 	/**
3140
+	 * Creates a simple `MDInlineBlock` if no other registered blocks match.
3141
+	 * 
3142
+	 * @returns {MDInlineBlock|null} fallback block
3143
+	 */
3144
+	#readFallbackBlock() {
3145
+		if (this.p >= this.lines.length) return null;
3146
+		const lines = MDUtils.withoutTrailingBlankLines(this.lines.slice(this.p));
3147
+		if (lines.length == 0) return null;
3148
+		this.p = this.lines.length;
3149
+		return new MDInlineBlock(this.inlineMarkdownToSpans(lines.join("\n")));
3150
+	}
3151
+
3152
+	/**
2910 3153
 	 * Attempts to read one block from the current line pointer. The pointer
2911 3154
 	 * will be positioned just after the end of the block.
2912 3155
 	 *
@@ -2922,7 +3165,8 @@ class MDState {
2922 3165
 			const block = reader.readBlock(this);
2923 3166
 			if (block) return block;
2924 3167
 		}
2925
-		return null;
3168
+		const fallback = this.#readFallbackBlock();
3169
+		return fallback;
2926 3170
 	}
2927 3171
 
2928 3172
 	static #textWhitespaceRegex = /^(\s*)(?:(\S|\S.*\S)(\s*?))?$/; // 1=leading WS, 2=text, 3=trailing WS
@@ -3066,94 +3310,6 @@ class MDState {
3066 3310
 	}
3067 3311
 }
3068 3312
 
3069
-class MDTagModifier {
3070
-	/** @type {string} */
3071
-	original;
3072
-	/** @type {string[]} */
3073
-	cssClasses = [];
3074
-	/** @type {string|null} */
3075
-	cssId = null;
3076
-	/** @type {object} */
3077
-	attributes = {};
3078
-
3079
-	static #baseClassRegex = /\.([a-z_\-][a-z0-9_\-]*?)/i;
3080
-	static #baseIdRegex = /#([a-z_\-][a-z0-9_\-]*?)/i;
3081
-	static #baseAttributeRegex = /([a-z0-9]+?)=([^\s\}]+?)/i;
3082
-	static #baseRegex = /\{([^}]+?)}/i;
3083
-	static #leadingClassRegex = new RegExp('^' + this.#baseRegex.source, 'i');
3084
-	static #trailingClassRegex = new RegExp('^(.*?)\\s*' + this.#baseRegex.source + '\\s*$', 'i');
3085
-	static #classRegex = new RegExp('^' + this.#baseClassRegex.source + '$', 'i');  // 1=classname
3086
-	static #idRegex = new RegExp('^' + this.#baseIdRegex.source + '$', 'i');  // 1=id
3087
-	static #attributeRegex = new RegExp('^' + this.#baseAttributeRegex.source + '$', 'i');  // 1=attribute name, 2=attribute value
3088
-
3089
-	/**
3090
-	 * @param {MDBlock|MDSpan} elem
3091
-	 */
3092
-	applyTo(elem) {
3093
-		if (elem instanceof MDBlock || elem instanceof MDSpan) {
3094
-			elem.cssClasses = elem.cssClasses.concat(this.cssClasses);
3095
-			if (this.cssId) elem.cssId = this.cssId;
3096
-			for (const name in this.attributes) {
3097
-				elem.attributes[name] = this.attributes[name];
3098
-			}
3099
-		}
3100
-	}
3101
-
3102
-	static #fromContents(contents) {
3103
-		let modifierTokens = contents.split(/\s+/);
3104
-		let mod = new MDTagModifier();
3105
-		mod.original = `{${contents}}`;
3106
-		var groups;
3107
-		for (const token of modifierTokens) {
3108
-			if (token.trim() == '') continue;
3109
-			if (groups = this.#classRegex.exec(token)) {
3110
-				mod.cssClasses.push(groups[1]);
3111
-			} else if (groups = this.#idRegex.exec(token)) {
3112
-				mod.cssId = groups[1];
3113
-			} else if (groups = this.#attributeRegex.exec(token)) {
3114
-				mod.attributes[groups[1]] = groups[2];
3115
-			} else {
3116
-				return null;
3117
-			}
3118
-		}
3119
-		return mod;
3120
-	}
3121
-
3122
-	/**
3123
-	 * Extracts modifier from line.
3124
-	 * @param {string} line
3125
-	 * @returns {Array} Tuple with remaining line and MDTagModifier.
3126
-	 */
3127
-	static fromLine(line) {
3128
-		let groups = this.#trailingClassRegex.exec(line);
3129
-		if (groups === null) return [ line, null ];
3130
-		let bareLine = groups[1];
3131
-		let mod = this.#fromContents(groups[2]);
3132
-		return [ bareLine, mod ];
3133
-	}
3134
-
3135
-	/**
3136
-	 * Extracts modifier from head of string.
3137
-	 * @param {string} line
3138
-	 * @returns {MDTagModifier}
3139
-	 */
3140
-	static fromStart(line) {
3141
-		let groups = this.#leadingClassRegex.exec(line);
3142
-		if (groups === null) return null;
3143
-		return this.#fromContents(groups[1]);
3144
-	}
3145
-
3146
-	/**
3147
-	 * @param {string} line
3148
-	 * @returns {string}
3149
-	 */
3150
-	static strip(line) {
3151
-		let groups = this.#trailingClassRegex.exec(line);
3152
-		if (groups === null) return line;
3153
-		return groups[1];
3154
-	}
3155
-}
3156
-
3157 3313
 class Markdown {
3158 3314
 	/**
3159 3315
 	 * Set of standard block readers.

+ 507
- 40
testjs.html Ver arquivo

@@ -7,6 +7,10 @@
7 7
 		<style type="text/css">
8 8
 			:root {
9 9
 				font-family: sans-serif;
10
+				--color-passed: #090;
11
+				--color-failed: #a00;
12
+				--color-errored: #a80;
13
+				--color-untested: #888;
10 14
 			}
11 15
 			.testclass {
12 16
 				border: 1px solid black;
@@ -19,7 +23,15 @@
19 23
 				font-size: 1.25rem;
20 24
 				padding-bottom: 0.25em;
21 25
 			}
26
+			.testclassstatus {
27
+
28
+			}
29
+			.testclassstatus.passed { color: var(--color-passed); }
30
+			.testclassstatus.failed { color: var(--color-failed); }
31
+			.testclassstatus.errored { color: var(--color-errored); }
32
+			.testclassstatus.untested { color: var(--color-untested); }
22 33
 			.testcase {
34
+				clear: both;
23 35
 				padding: 0.2em 0;
24 36
 				margin-left: 2em;
25 37
 			}
@@ -27,11 +39,23 @@
27 39
 				border-top: 1px solid #888;
28 40
 			}
29 41
 			.testcasename {
30
-				font-size: 115%;
31
-				font-weight: bold;
42
+				font-family: monospace;
32 43
 			}
33 44
 			.testcasestatus {
34 45
 				font-weight: bold;
46
+				font-size: 80%;
47
+				float: left;
48
+			}
49
+			.testcasetiming {
50
+				float: right;
51
+				color: #888;
52
+				font-size: 80%;
53
+			}
54
+			.testcaseresult {
55
+				height: 1em;
56
+			}
57
+			.testcasemessage {
58
+				clear: both;
35 59
 			}
36 60
 			.result-untested {
37 61
 				color: #888;
@@ -99,7 +123,7 @@
99 123
 					if (test) this.fail(failMessage || `expected false, got ${test}`);
100 124
 				}
101 125
 				assertEqual(a, b, failMessage=null) {
102
-					if (a == b) return;
126
+					if (MDUtils.equal(a, b)) return;
103 127
 					const aVal = `${a}`;
104 128
 					const bVal = `${b}`;
105 129
 					if (aVal.length > 20 || bVal.length > 20) {
@@ -145,6 +169,7 @@
145 169
 				/** @var {String|null} */
146 170
 				message = null;
147 171
 				expectedError = null;
172
+				duration = null;
148 173
 				/** @var {String} */
149 174
 				get className() { return this.#objectUnderTest.constructor.name; }
150 175
 				/** @var {String} */
@@ -159,14 +184,25 @@
159 184
 					this.uniqueId = TestCaseRunner.#nextUniqueId++;
160 185
 				}
161 186
 				run() {
187
+					var start;
188
+					this.expectedError = null;
189
+					this.#objectUnderTest.currentRunner = this;
162 190
 					try {
163
-						this.expectedError = null;
164
-						this.#objectUnderTest.currentRunner = this;
165 191
 						this.#objectUnderTest.setUp();
192
+					} catch (e) {
193
+						console.error(`Failed to run ${this.className}.setUp() - ${e.message}`);
194
+						this.result = ResultType.errored;
195
+						this.message = e.message;
196
+						return;
197
+					}
198
+					try {
199
+						start = performance.now();
166 200
 						this.#method.bind(this.#objectUnderTest)();
201
+						this.duration = performance.now() - start;
167 202
 						this.result = ResultType.passed;
168 203
 						this.message = null;
169 204
 					} catch (e) {
205
+						this.duration = performance.now() - start;
170 206
 						if (e instanceof FailureError) {
171 207
 							this.result = ResultType.failed;
172 208
 							this.message = e.message;
@@ -202,25 +238,31 @@
202 238
 				toHTML() {
203 239
 					var html = `<div class="testcase" id="${this.#cssId}">`;
204 240
 					html += `<div class="testcasename"><span class="testcasemethod">${this.methodName}</span></div>`;
241
+					html += '<div class="testcaseresult">';
205 242
 					switch (this.result) {
206 243
 						case ResultType.untested:
207 244
 							html += '<div class="testcasestatus result-untested">Waiting to test</div>';
208 245
 							break;
209 246
 						case ResultType.testing:
210
-							html += '<div class="testcasestatus result-tesitng">Testing...</div>';
247
+							html += '<div class="testcasestatus result-testing">Testing...</div>';
211 248
 							break;
212 249
 						case ResultType.passed:
213 250
 							html += '<div class="testcasestatus result-passed">Passed</div>';
214 251
 							break;
215 252
 						case ResultType.failed:
216 253
 							html += '<div class="testcasestatus result-failed">Failed</div>';
217
-							html += `<div class="testcasemessage">${escapeHTML(this.message)}</div>`;
218 254
 							break;
219 255
 						case ResultType.errored:
220 256
 							html += '<div class="testcasestatus result-errored">Errored</div>';
221
-							html += `<div class="testcasemessage">${escapeHTML(this.message)}</div>`;
222 257
 							break;
223 258
 					}
259
+					if (this.duration !== null) {
260
+						html += `<div class="testcasetiming">${Number(this.duration / 1000.0).toFixed(3)}s</div>`;
261
+					}
262
+					html += '</div>';
263
+					if (this.message !== null) {
264
+						html += `<div class="testcasemessage">${escapeHTML(this.message)}</div>`;
265
+					}
224 266
 					html += `</div>`;
225 267
 					return html;
226 268
 				}
@@ -273,20 +315,77 @@
273 315
 					this.#uniqueId = TestClassRunner.#nextUniqueId++;
274 316
 				}
275 317
 				get #cssId() { return `testclass${this.#uniqueId}`; }
318
+				#summaryHTML() {
319
+					var anyTesting = false;
320
+					var anyFailed = false;
321
+					var anyErrored = false;
322
+					var anyUntested = false;
323
+					var anyPassed = false;
324
+					var allPassed = true;
325
+					for (const test of this.testCases) {
326
+						switch (test.result) {
327
+							case ResultType.untested:
328
+								anyUntested = true;
329
+								allPassed = false;
330
+								break;
331
+							case ResultType.testing:
332
+								anyTesting = true;
333
+								allPassed = false;
334
+								break;
335
+							case ResultType.passed:
336
+								anyPassed = true;
337
+								break;
338
+							case ResultType.failed:
339
+								anyFailed = true;
340
+								allPassed = false;
341
+								break;
342
+							case ResultType.errored:
343
+								anyErrored = true;
344
+								allPassed = false;
345
+								break;
346
+						}
347
+					}
348
+					var html = '';
349
+					html += `<summary class="testclasssummary" id="${this.#cssId}summary">`;
350
+					html += `<span class="testclassname">${this.#theClass.name}</span> `;
351
+					if (anyTesting || (anyUntested && (anyPassed || anyFailed || anyErrored))) {
352
+						html += '<span class="testclassstatus testing">Testing...</span>';
353
+					} else if (anyErrored) {
354
+						html += '<span class="testclassstatus errored">Errored</span>';
355
+					} else if (anyFailed) {
356
+						html += '<span class="testclassstatus failed">Failed</span>';
357
+					} else if (allPassed) {
358
+						html += '<span class="testclassstatus passed">Passed</span>';
359
+					}
360
+					html += '</summary>';
361
+					return html;
362
+				}
276 363
 				toHTML() {
277 364
 					var html = '';
278 365
 					html += `<div class="testclass" id="${this.#cssId}">`;
279
-					html += `<div class="testclassname">${this.#theClass.name}</div>`;
366
+					html += `<details id="${this.#cssId}details">`;
367
+					html += this.#summaryHTML();
280 368
 					for (const testCase of this.#testCases) {
281 369
 						html += testCase.toHTML();
282 370
 					}
371
+					html += '</details>';
283 372
 					html += '</div>';
284 373
 					return html;
285 374
 				}
286 375
 				updateHTML() {
287
-					var existing = document.getElementById(this.#cssId);
376
+					var existing = document.getElementById(`${this.#cssId}summary`);
288 377
 					if (!existing) {
289 378
 						document.getElementById('results').innerHTML += this.toHTML();
379
+					} else {
380
+						existing.outerHTML = this.#summaryHTML();
381
+						var allPassed = true;
382
+						for (const test of this.testCases) {
383
+							if (test.result != ResultType.passed) {
384
+								allPassed = false;
385
+								break;
386
+							}
387
+						}
388
+						document.getElementById(`${this.#cssId}details`).open = !allPassed;
290 389
 					}
291 390
 				}
292 391
 
@@ -325,6 +424,7 @@
325 424
 			function onLoad() {
326 425
 				let testClasses = [
327 426
 					TokenTests,
427
+					UtilsTests,
328 428
 					InlineTests,
329 429
 					BlockTests,
330 430
 				];
@@ -358,7 +458,52 @@
358 458
 						tokens: [ tokens[2] ],
359 459
 						index: 2,
360 460
 					};
361
-					this.assertEqual(JSON.stringify(result), JSON.stringify(expected));
461
+					this.assertEqual(result, expected);
462
+				}
463
+
464
+				test_findFirstTokens_optionalWhitespace1() {
465
+					const tokens = [
466
+						new MDToken('Lorem', MDTokenType.Text),
467
+						new MDToken(' ', MDTokenType.Whitespace),
468
+						new MDToken('[ipsum]', MDTokenType.Label, 'ipsum'),
469
+						new MDToken('(link.html)', MDTokenType.URL, 'link.html'),
470
+						new MDToken(' ', MDTokenType.Whitespace),
471
+						new MDToken('dolor', MDTokenType.Text),
472
+					];
473
+					const pattern = [
474
+						MDTokenType.Label,
475
+						MDTokenType.META_OptionalWhitespace,
476
+						MDTokenType.URL,
477
+					];
478
+					const result = MDToken.findFirstTokens(tokens, pattern);
479
+					const expected = {
480
+						tokens: [ tokens[2], tokens[3] ],
481
+						index: 2,
482
+					};
483
+					this.assertEqual(result, expected);
484
+				}
485
+
486
+				test_findFirstTokens_optionalWhitespace2() {
487
+					const tokens = [
488
+						new MDToken('Lorem', MDTokenType.Text),
489
+						new MDToken(' ', MDTokenType.Whitespace),
490
+						new MDToken('[ipsum]', MDTokenType.Label, 'ipsum'),
491
+						new MDToken(' ', MDTokenType.Whitespace),
492
+						new MDToken('(link.html)', MDTokenType.URL, 'link.html'),
493
+						new MDToken(' ', MDTokenType.Whitespace),
494
+						new MDToken('dolor', MDTokenType.Text),
495
+					];
496
+					const pattern = [
497
+						MDTokenType.Label,
498
+						MDTokenType.META_OptionalWhitespace,
499
+						MDTokenType.URL,
500
+					];
501
+					const result = MDToken.findFirstTokens(tokens, pattern);
502
+					const expected = {
503
+						tokens: [ tokens[2], tokens[3], tokens[4] ],
504
+						index: 2,
505
+					};
506
+					this.assertEqual(result, expected);
362 507
 				}
363 508
 
364 509
 				test_findPairedTokens() {
@@ -387,7 +532,92 @@
387 532
 						endIndex: 4,
388 533
 						totalLength: 3,
389 534
 					}
390
-					this.assertEqual(JSON.stringify(result), JSON.stringify(expected));
535
+					this.assertEqual(result, expected);
536
+				}
537
+			}
538
+
539
+			class UtilsTests extends BaseTest {
540
+				test_stripIndent() {
541
+					this.assertEqual(MDUtils.stripIndent(''), '');
542
+					this.assertEqual(MDUtils.stripIndent('  '), '');
543
+					this.assertEqual(MDUtils.stripIndent('foo'), 'foo');
544
+					this.assertEqual(MDUtils.stripIndent(' foo'), 'foo');
545
+					this.assertEqual(MDUtils.stripIndent('  foo'), 'foo');
546
+					this.assertEqual(MDUtils.stripIndent('   foo'), 'foo');
547
+					this.assertEqual(MDUtils.stripIndent('    foo'), 'foo');
548
+					this.assertEqual(MDUtils.stripIndent('     foo'), ' foo');
549
+					this.assertEqual(MDUtils.stripIndent('\tfoo'), 'foo');
550
+					this.assertEqual(MDUtils.stripIndent('\t\tfoo'), '\tfoo');
551
+					this.assertEqual(MDUtils.stripIndent('\t\tfoo', 2), 'foo');
552
+					this.assertEqual(MDUtils.stripIndent('      foo', 2), 'foo');
553
+				}
554
+
555
+				test_countIndents() {
556
+					this.assertEqual(MDUtils.countIndents(''), 0);
557
+					this.assertEqual(MDUtils.countIndents('  '), 1);
558
+					this.assertEqual(MDUtils.countIndents('    '), 1);
559
+					this.assertEqual(MDUtils.countIndents('foo'), 0);
560
+					this.assertEqual(MDUtils.countIndents('foo'), 0);
561
+					this.assertEqual(MDUtils.countIndents(' foo'), 1);
562
+					this.assertEqual(MDUtils.countIndents('  foo'), 1);
563
+					this.assertEqual(MDUtils.countIndents('   foo'), 1);
564
+					this.assertEqual(MDUtils.countIndents('    foo'), 1);
565
+					this.assertEqual(MDUtils.countIndents('     foo'), 2);
566
+					this.assertEqual(MDUtils.countIndents('\tfoo'), 1);
567
+					this.assertEqual(MDUtils.countIndents('\t\tfoo'), 2);
568
+
569
+					this.assertEqual(MDUtils.countIndents('', true), 0);
570
+					this.assertEqual(MDUtils.countIndents('  ', true), 0);
571
+					this.assertEqual(MDUtils.countIndents('    ', true), 1);
572
+					this.assertEqual(MDUtils.countIndents('foo', true), 0);
573
+					this.assertEqual(MDUtils.countIndents(' foo', true), 0);
574
+					this.assertEqual(MDUtils.countIndents('  foo', true), 0);
575
+					this.assertEqual(MDUtils.countIndents('   foo', true), 0);
576
+					this.assertEqual(MDUtils.countIndents('    foo', true), 1);
577
+					this.assertEqual(MDUtils.countIndents('     foo', true), 1);
578
+					this.assertEqual(MDUtils.countIndents('\tfoo', true), 1);
579
+					this.assertEqual(MDUtils.countIndents('\t\tfoo', true), 2);
580
+				}
581
+
582
+				test_tokenizeLabel() {
583
+					// Escapes are preserved
584
+					this.assertEqual(MDUtils.tokenizeLabel('[foo] bar'), [ '[foo]', 'foo' ]);
585
+					this.assertEqual(MDUtils.tokenizeLabel('[foo\\[] bar'), [ '[foo\\[]', 'foo\\[' ]);
586
+					this.assertEqual(MDUtils.tokenizeLabel('[foo\\]] bar'), [ '[foo\\]]', 'foo\\]' ]);
587
+					this.assertEqual(MDUtils.tokenizeLabel('[foo[]] bar'), [ '[foo[]]', 'foo[]' ]);
588
+					this.assertEqual(MDUtils.tokenizeLabel('[foo\\(] bar'), [ '[foo\\(]', 'foo\\(' ]);
589
+					this.assertEqual(MDUtils.tokenizeLabel('[foo\\)] bar'), [ '[foo\\)]', 'foo\\)' ]);
590
+					this.assertEqual(MDUtils.tokenizeLabel('[foo()] bar'), [ '[foo()]', 'foo()' ]);
591
+
592
+					this.assertEqual(MDUtils.tokenizeLabel('foo bar'), null);
593
+					this.assertEqual(MDUtils.tokenizeLabel('[foo\\] bar'), null);
594
+					this.assertEqual(MDUtils.tokenizeLabel('[foo bar'), null);
595
+					this.assertEqual(MDUtils.tokenizeLabel('[foo[] bar'), null);
596
+				}
597
+
598
+				test_tokenizeURL() {
599
+					this.assertEqual(MDUtils.tokenizeURL('(page.html) foo'), [ '(page.html)', 'page.html', null ]);
600
+					this.assertEqual(MDUtils.tokenizeURL('(page.html "link title") foo'), [ '(page.html "link title")', 'page.html', 'link title' ]);
601
+					this.assertEqual(MDUtils.tokenizeURL('(https://example.com/path/page.html?query=foo&bar=baz#fragment) foo'), [ '(https://example.com/path/page.html?query=foo&bar=baz#fragment)', 'https://example.com/path/page.html?query=foo&bar=baz#fragment', null ]);
602
+
603
+					this.assertEqual(MDUtils.tokenizeURL('page.html foo'), null);
604
+					this.assertEqual(MDUtils.tokenizeURL('(page.html foo'), null);
605
+					this.assertEqual(MDUtils.tokenizeURL('page.html) foo'), null);
606
+					this.assertEqual(MDUtils.tokenizeURL('(page.html "title) foo'), null);
607
+					this.assertEqual(MDUtils.tokenizeURL('(page .html) foo'), null);
608
+					this.assertEqual(MDUtils.tokenizeURL('(user@example.com) foo'), null);
609
+					this.assertEqual(MDUtils.tokenizeURL('(user@example.com "title") foo'), null);
610
+				}
611
+
612
+				test_tokenizeEmail() {
613
+					this.assertEqual(MDUtils.tokenizeEmail('(user@example.com)'), [ '(user@example.com)', 'user@example.com', null ]);
614
+					this.assertEqual(MDUtils.tokenizeEmail('(user@example.com "link title")'), [ '(user@example.com "link title")', 'user@example.com', 'link title' ]);
615
+
616
+					this.assertEqual(MDUtils.tokenizeEmail('(https://example.com) foo'), null);
617
+					this.assertEqual(MDUtils.tokenizeEmail('(https://example.com "link title") foo'), null);
618
+					this.assertEqual(MDUtils.tokenizeEmail('(user@example.com "link title) foo'), null);
619
+					this.assertEqual(MDUtils.tokenizeEmail('(user@example.com foo'), null);
620
+					this.assertEqual(MDUtils.tokenizeEmail('user@example.com) foo'), null);
391 621
 				}
392 622
 			}
393 623
 
@@ -402,129 +632,129 @@
402 632
 					this.parser = Markdown.completeParser;
403 633
 				}
404 634
 
405
-				test_simpleSingleParagraph() {
635
+				test_simpleText() {
406 636
 					let markdown = 'Lorem ipsum';
407
-					let expected = '<p>Lorem ipsum</p>';
637
+					let expected = 'Lorem ipsum';
408 638
 					let actual = this.md(markdown);
409 639
 					this.assertEqual(actual, expected);
410 640
 				}
411 641
 
412 642
 				test_strong() {
413 643
 					let markdown = 'Lorem **ipsum** dolor **sit**';
414
-					let expected = '<p>Lorem <strong>ipsum</strong> dolor <strong>sit</strong></p>';
644
+					let expected = 'Lorem <strong>ipsum</strong> dolor <strong>sit</strong>';
415 645
 					let actual = this.md(markdown);
416 646
 					this.assertEqual(actual, expected);
417 647
 				}
418 648
 
419 649
 				test_emphasis() {
420 650
 					let markdown = 'Lorem _ipsum_ dolor _sit_';
421
-					let expected = '<p>Lorem <em>ipsum</em> dolor <em>sit</em></p>';
651
+					let expected = 'Lorem <em>ipsum</em> dolor <em>sit</em>';
422 652
 					let actual = this.md(markdown);
423 653
 					this.assertEqual(actual, expected);
424 654
 				}
425 655
 
426 656
 				test_strongEmphasis_cleanNesting1() {
427 657
 					let markdown = 'Lorem **ipsum *dolor* sit** amet';
428
-					let expected = '<p>Lorem <strong>ipsum <em>dolor</em> sit</strong> amet</p>';
658
+					let expected = 'Lorem <strong>ipsum <em>dolor</em> sit</strong> amet';
429 659
 					let actual = this.md(markdown);
430 660
 					this.assertEqual(actual, expected);
431 661
 				}
432 662
 
433 663
 				test_strongEmphasis_cleanNesting2() {
434 664
 					let markdown = 'Lorem *ipsum **dolor** sit* amet';
435
-					let expected = '<p>Lorem <em>ipsum <strong>dolor</strong> sit</em> amet</p>';
665
+					let expected = 'Lorem <em>ipsum <strong>dolor</strong> sit</em> amet';
436 666
 					let actual = this.md(markdown);
437 667
 					this.assertEqual(actual, expected);
438 668
 				}
439 669
 
440 670
 				test_strongEmphasis_tightNesting() {
441 671
 					let markdown = 'Lorem ***ipsum*** dolor';
442
-					let expected1 = '<p>Lorem <strong><em>ipsum</em></strong> dolor</p>';
443
-					let expected2 = '<p>Lorem <em><strong>ipsum</strong></em> dolor</p>';
672
+					let expected1 = 'Lorem <strong><em>ipsum</em></strong> dolor';
673
+					let expected2 = 'Lorem <em><strong>ipsum</strong></em> dolor';
444 674
 					let actual = this.md(markdown);
445 675
 					this.assertTrue(actual == expected1 || actual == expected2);
446 676
 				}
447 677
 
448 678
 				test_strongEmphasis_lopsidedNesting1() {
449 679
 					let markdown = 'Lorem ***ipsum* dolor** sit';
450
-					let expected = '<p>Lorem <strong><em>ipsum</em> dolor</strong> sit</p>';
680
+					let expected = 'Lorem <strong><em>ipsum</em> dolor</strong> sit';
451 681
 					let actual = this.md(markdown);
452 682
 					this.assertEqual(actual, expected);
453 683
 				}
454 684
 
455 685
 				test_strongEmphasis_lopsidedNesting2() {
456 686
 					let markdown = 'Lorem ***ipsum** dolor* sit';
457
-					let expected = '<p>Lorem <em><strong>ipsum</strong> dolor</em> sit</p>';
687
+					let expected = 'Lorem <em><strong>ipsum</strong> dolor</em> sit';
458 688
 					let actual = this.md(markdown);
459 689
 					this.assertEqual(actual, expected);
460 690
 				}
461 691
 
462 692
 				test_strongEmphasis_lopsidedNesting3() {
463 693
 					let markdown = 'Lorem **ipsum *dolor*** sit';
464
-					let expected = '<p>Lorem <strong>ipsum <em>dolor</em></strong> sit</p>';
694
+					let expected = 'Lorem <strong>ipsum <em>dolor</em></strong> sit';
465 695
 					let actual = this.md(markdown);
466 696
 					this.assertEqual(actual, expected);
467 697
 				}
468 698
 
469 699
 				test_strongEmphasis_lopsidedNesting4() {
470 700
 					let markdown = 'Lorem *ipsum **dolor*** sit';
471
-					let expected = '<p>Lorem <em>ipsum <strong>dolor</strong></em> sit</p>';
701
+					let expected = 'Lorem <em>ipsum <strong>dolor</strong></em> sit';
472 702
 					let actual = this.md(markdown);
473 703
 					this.assertEqual(actual, expected);
474 704
 				}
475 705
 
476 706
 				test_inlineCode() {
477 707
 					let markdown = 'Lorem `ipsum` dolor';
478
-					let expected = '<p>Lorem <code>ipsum</code> dolor</p>';
708
+					let expected = 'Lorem <code>ipsum</code> dolor';
479 709
 					let actual = this.md(markdown);
480 710
 					this.assertEqual(actual, expected);
481 711
 				}
482 712
 
483 713
 				test_inlineCode_withInnerBacktick() {
484 714
 					let markdown = 'Lorem ``ip`su`m`` dolor';
485
-					let expected = '<p>Lorem <code>ip`su`m</code> dolor</p>';
715
+					let expected = 'Lorem <code>ip`su`m</code> dolor';
486 716
 					let actual = this.md(markdown);
487 717
 					this.assertEqual(actual, expected);
488 718
 				}
489 719
 
490 720
 				test_strikethrough_single() {
491 721
 					let markdown = 'Lorem ~ipsum~ dolor';
492
-					let expected = '<p>Lorem <strike>ipsum</strike> dolor</p>';
722
+					let expected = 'Lorem <strike>ipsum</strike> dolor';
493 723
 					let actual = this.md(markdown);
494 724
 					this.assertEqual(actual, expected);
495 725
 				}
496 726
 
497 727
 				test_strikethrough_double() {
498 728
 					let markdown = 'Lorem ~~ipsum~~ dolor';
499
-					let expected = '<p>Lorem <strike>ipsum</strike> dolor</p>';
729
+					let expected = 'Lorem <strike>ipsum</strike> dolor';
500 730
 					let actual = this.md(markdown);
501 731
 					this.assertEqual(actual, expected);
502 732
 				}
503 733
 
504 734
 				test_link_fullyQualified() {
505 735
 					let markdown = 'Lorem [ipsum](https://example.com/path/page.html) dolor';
506
-					let expected = '<p>Lorem <a href="https://example.com/path/page.html">ipsum</a> dolor</p>';
736
+					let expected = 'Lorem <a href="https://example.com/path/page.html">ipsum</a> dolor';
507 737
 					let actual = this.md(markdown);
508 738
 					this.assertEqual(actual, expected);
509 739
 				}
510 740
 
511 741
 				test_link_relative() {
512 742
 					let markdown = 'Lorem [ipsum](page.html) dolor';
513
-					let expected = '<p>Lorem <a href="page.html">ipsum</a> dolor</p>';
743
+					let expected = 'Lorem <a href="page.html">ipsum</a> dolor';
514 744
 					let actual = this.md(markdown);
515 745
 					this.assertEqual(actual, expected);
516 746
 				}
517 747
 
518 748
 				test_link_title() {
519 749
 					let markdown = 'Lorem [ipsum](page.html "link title") dolor';
520
-					let expected = '<p>Lorem <a href="page.html" title="link title">ipsum</a> dolor</p>';
750
+					let expected = 'Lorem <a href="page.html" title="link title">ipsum</a> dolor';
521 751
 					let actual = this.md(markdown);
522 752
 					this.assertEqual(actual, expected);
523 753
 				}
524 754
 
525 755
 				test_link_literal() {
526 756
 					let markdown = 'Lorem <https://example.com> dolor';
527
-					let expected = '<p>Lorem <a href="https://example.com">https://example.com</a> dolor</p>';
757
+					let expected = 'Lorem <a href="https://example.com">https://example.com</a> dolor';
528 758
 					let actual = this.md(markdown);
529 759
 					this.assertEqual(actual, expected);
530 760
 				}
@@ -538,45 +768,74 @@
538 768
 
539 769
 				test_link_email() {
540 770
 					let markdown = 'Lorem [ipsum](user@example.com) dolor';
541
-					let expected = '<p>Lorem <a href="mailto:&#117;&#115;&#101;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;">ipsum</a> dolor</p>';
771
+					let expected = 'Lorem <a href="mailto:&#117;&#115;&#101;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;">ipsum</a> dolor';
542 772
 					let actual = this.md(markdown);
543 773
 					this.assertEqual(actual, expected);
544 774
 				}
545 775
 
546 776
 				test_link_email_withTitle() {
547 777
 					let markdown = 'Lorem [ipsum](user@example.com "title") dolor';
548
-					let expected = '<p>Lorem <a href="mailto:&#117;&#115;&#101;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;" title="title">ipsum</a> dolor</p>';
778
+					let expected = 'Lorem <a href="mailto:&#117;&#115;&#101;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;" title="title">ipsum</a> dolor';
549 779
 					let actual = this.md(markdown);
550 780
 					this.assertEqual(actual, expected);
551 781
 				}
552 782
 
553 783
 				test_link_literalEmail() {
554 784
 					let markdown = 'Lorem <user@example.com> dolor';
555
-					let expected = '<p>Lorem <a href="mailto:&#117;&#115;&#101;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;">&#117;&#115;&#101;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;</a> dolor</p>';
785
+					let expected = 'Lorem <a href="mailto:&#117;&#115;&#101;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;">&#117;&#115;&#101;&#114;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;</a> dolor';
786
+					let actual = this.md(markdown);
787
+					this.assertEqual(actual, expected);
788
+				}
789
+
790
+				test_link_image() {
791
+					let markdown = 'Lorem [![alt](image.jpg)](page.html) ipsum';
792
+					let expected = 'Lorem <a href="page.html"><img src="image.jpg" alt="alt"></a> ipsum';
793
+					let actual = this.md(markdown);
794
+					this.assertEqual(actual, expected);
795
+				}
796
+
797
+				test_link_image_complex() {
798
+					let markdown = 'Lorem [![alt] (image.jpg "image title")] (page.html "link title") ipsum';
799
+					let expected = 'Lorem <a href="page.html" title="link title"><img src="image.jpg" alt="alt" title="image title"></a> ipsum';
556 800
 					let actual = this.md(markdown);
557 801
 					this.assertEqual(actual, expected);
558 802
 				}
559 803
 
560 804
 				test_image() {
561 805
 					let markdown = 'Lorem ![alt text](image.jpg) dolor';
562
-					let expected = '<p>Lorem <img src="image.jpg" alt="alt text"> dolor</p>';
806
+					let expected = 'Lorem <img src="image.jpg" alt="alt text"> dolor';
563 807
 					let actual = this.md(markdown);
564 808
 					this.assertEqual(actual, expected);
565 809
 				}
566 810
 
567 811
 				test_image_noAlt() {
568 812
 					let markdown = 'Lorem ![](image.jpg) dolor';
569
-					let expected = '<p>Lorem <img src="image.jpg"> dolor</p>';
813
+					let expected = 'Lorem <img src="image.jpg"> dolor';
570 814
 					let actual = this.md(markdown);
571 815
 					this.assertEqual(actual, expected);
572 816
 				}
573 817
 
574 818
 				test_image_withTitle() {
575 819
 					let markdown = 'Lorem ![alt text](image.jpg "image title") dolor';
820
+					let expected = 'Lorem <img src="image.jpg" alt="alt text" title="image title"> dolor';
821
+					let actual = this.md(markdown);
822
+					this.assertEqual(actual, expected);
823
+				}
824
+
825
+				test_image_ref() {
826
+					let markdown = 'Lorem ![alt text][ref] dolor\n\n' +
827
+						'[ref]: image.jpg "image title"';
576 828
 					let expected = '<p>Lorem <img src="image.jpg" alt="alt text" title="image title"> dolor</p>';
577 829
 					let actual = this.md(markdown);
578 830
 					this.assertEqual(actual, expected);
579 831
 				}
832
+
833
+				test_htmlTags() {
834
+					let markdown = 'Lorem <strong title="value" foo=\'with " quote\' bar="with \' apostrophe" attr=unquoted checked>ipsum</strong> dolor';
835
+					let expected = markdown;
836
+					let actual = this.md(markdown);
837
+					this.assertEqual(actual, expected);
838
+				}
580 839
 			}
581 840
 
582 841
 			class BlockTests extends BaseTest {
@@ -599,7 +858,35 @@
599 858
 
600 859
 				test_paragraph_lineGrouping() {
601 860
 					let markdown = "Lorem ipsum\ndolor sit amet";
602
-					let expected = "<p>Lorem ipsum dolor sit amet</p>";
861
+					let expected = "Lorem ipsum dolor sit amet";
862
+					let actual = this.md(markdown);
863
+					this.assertEqual(actual, expected);
864
+				}
865
+
866
+				test_header_underlineH1() {
867
+					let markdown = "Header 1\n===\n\nLorem ipsum";
868
+					let expected = "<h1>Header 1</h1> <p>Lorem ipsum</p>";
869
+					let actual = this.md(markdown);
870
+					this.assertEqual(actual, expected);
871
+				}
872
+
873
+				test_header_underlineH2() {
874
+					let markdown = "Header 2\n---\n\nLorem ipsum";
875
+					let expected = "<h2>Header 2</h2> <p>Lorem ipsum</p>";
876
+					let actual = this.md(markdown);
877
+					this.assertEqual(actual, expected);
878
+				}
879
+
880
+				test_header_hash() {
881
+					let markdown = "# Header 1\n## Header 2\n### Header 3\n#### Header 4\n##### Header 5\n###### Header 6\n";
882
+					let expected = '<h1>Header 1</h1> <h2>Header 2</h2> <h3>Header 3</h3> <h4>Header 4</h4> <h5>Header 5</h5> <h6>Header 6</h6>';
883
+					let actual = this.md(markdown);
884
+					this.assertEqual(actual, expected);
885
+				}
886
+
887
+				test_header_hash_trailing() {
888
+					let markdown = "# Header 1 #\n## Header 2 ##\n### Header 3 ######";
889
+					let expected = '<h1>Header 1</h1> <h2>Header 2</h2> <h3>Header 3</h3>';
603 890
 					let actual = this.md(markdown);
604 891
 					this.assertEqual(actual, expected);
605 892
 				}
@@ -611,9 +898,16 @@
611 898
 					this.assertEqual(actual, expected);
612 899
 				}
613 900
 
901
+				test_unorderedList_nested() {
902
+					let markdown = "* Lorem\n + Ipsum\n* Dolor";
903
+					let expected = '<ul> <li>Lorem <ul> <li>Ipsum</li> </ul></li> <li>Dolor</li> </ul>';
904
+					let actual = this.md(markdown);
905
+					this.assertEqual(actual, expected);
906
+				}
907
+
614 908
 				test_orderedList() {
615 909
 					let markdown = "1. Lorem\n1. Ipsum\n5. Dolor";
616
-					let expected = '<ol start="1"> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ol>';
910
+					let expected = '<ol> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ol>';
617 911
 					let actual = this.md(markdown);
618 912
 					this.assertEqual(actual, expected);
619 913
 				}
@@ -625,9 +919,182 @@
625 919
 					this.assertEqual(actual, expected);
626 920
 				}
627 921
 
922
+				test_orderedList_nested1() {
923
+					let markdown = "1. Lorem\n 1. Ipsum\n1. Dolor";
924
+					let expected = '<ol> <li>Lorem <ol> <li>Ipsum</li> </ol></li> <li>Dolor</li> </ol>';
925
+					let actual = this.md(markdown);
926
+					this.assertEqual(actual, expected);
927
+				}
928
+
929
+				test_orderedList_nested2() {
930
+					let markdown = "1. Lorem\n 1. Ipsum\n      1. Dolor\n 1. Sit\n1. Amet";
931
+					let expected = '<ol> <li>Lorem <ol> <li>Ipsum <ol> <li>Dolor</li> </ol></li> <li>Sit</li> </ol></li> <li>Amet</li> </ol>';
932
+					let actual = this.md(markdown);
933
+					this.assertEqual(actual, expected);
934
+				}
935
+
628 936
 				test_blockquote() {
629 937
 					let markdown = '> Lorem ipsum dolor';
630
-					let expected = '<blockquote> <p>Lorem ipsum dolor</p> </blockquote>';
938
+					let expected = '<blockquote> Lorem ipsum dolor </blockquote>';
939
+					let actual = this.md(markdown);
940
+					this.assertEqual(actual, expected);
941
+				}
942
+
943
+				test_blockquote_paragraphs() {
944
+					let markdown = '> Lorem ipsum dolor\n>\n>Sit amet';
945
+					let expected = '<blockquote> <p>Lorem ipsum dolor</p> <p>Sit amet</p> </blockquote>';
946
+					let actual = this.md(markdown);
947
+					this.assertEqual(actual, expected);
948
+				}
949
+
950
+				test_blockquote_list() {
951
+					let markdown = '> 1. Lorem\n> 2. Ipsum';
952
+					let expected = '<blockquote> <ol> <li>Lorem</li> <li>Ipsum</li> </ol> </blockquote>';
953
+					let actual = this.md(markdown);
954
+					this.assertEqual(actual, expected);
955
+				}
956
+
957
+				test_codeBlock_indented() {
958
+					let markdown = "Code\n\n    function foo() {\n        return 'bar';\n    }\n\nend";
959
+					let expected = "<p>Code</p>\n\n<pre><code>function foo() {\n    return 'bar';\n}</code></pre>\n<p>end</p>\n";
960
+					let actual = this.parser.toHTML(markdown); // don't normalize whitespace
961
+					this.assertEqual(actual.replace(/ /g, '⎵'), expected.replace(/ /g, '⎵'));
962
+				}
963
+
964
+				test_codeBlock_fenced() {
965
+					let markdown = "Code\n\n```\nfunction foo() {\n    return 'bar';\n}\n```\n\nend";
966
+					let expected = "<p>Code</p>\n\n<pre><code>function foo() {\n    return 'bar';\n}</code></pre>\n<p>end</p>\n";
967
+					let actual = this.parser.toHTML(markdown); // don't normalize whitespace
968
+					this.assertEqual(actual.replace(/ /g, '⎵'), expected.replace(/ /g, '⎵'));
969
+				}
970
+
971
+				test_horizontalRule() {
972
+					let markdown = "Before\n\n---\n\n- - -\n\n***\n\n* * * * * * *\n\nafter";
973
+					let expected = "<p>Before</p> <hr> <hr> <hr> <hr> <p>after</p>";
974
+					let actual = this.md(markdown);
975
+					this.assertEqual(actual, expected);
976
+				}
977
+
978
+				test_table_unfenced() {
979
+					let markdown = "Column A | Column B | Column C\n--- | --- | ---\n1 | 2 | 3\n4 | 5 | 6";
980
+					let expected = "<table> <thead> <tr> <th>Column A</th> <th>Column B</th> <th>Column C</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>2</td> <td>3</td> </tr> <tr> <td>4</td> <td>5</td> <td>6</td> </tr> </tbody> </table>";
981
+					let actual = this.md(markdown);
982
+					this.assertEqual(actual, expected);
983
+				}
984
+
985
+				test_table_fenced() {
986
+					let markdown = "| Column A | Column B | Column C |\n| --- | --- | --- |\n| 1 | 2 | 3\n4 | 5 | 6 |";
987
+					let expected = "<table> <thead> <tr> <th>Column A</th> <th>Column B</th> <th>Column C</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>2</td> <td>3</td> </tr> <tr> <td>4</td> <td>5</td> <td>6</td> </tr> </tbody> </table>";
988
+					let actual = this.md(markdown);
989
+					this.assertEqual(actual, expected);
990
+				}
991
+
992
+				test_table_alignment() {
993
+					let markdown = 'Column A | Column B | Column C\n' +
994
+						':--- | :---: | ---:\n' +
995
+						'1 | 2 | 3\n' +
996
+						'4 | 5 | 6';
997
+					let expected = '<table> ' +
998
+						'<thead> ' +
999
+						'<tr> ' +
1000
+						'<th align="left">Column A</th> ' +
1001
+						'<th align="center">Column B</th> ' +
1002
+						'<th align="right">Column C</th> ' +
1003
+						'</tr> ' +
1004
+						'</thead> ' +
1005
+						'<tbody> ' +
1006
+						'<tr> ' +
1007
+						'<td align="left">1</td> ' +
1008
+						'<td align="center">2</td> ' +
1009
+						'<td align="right">3</td> ' +
1010
+						'</tr> ' +
1011
+						'<tr> ' +
1012
+						'<td align="left">4</td> ' +
1013
+						'<td align="center">5</td> ' +
1014
+						'<td align="right">6</td> ' +
1015
+						'</tr> ' +
1016
+						'</tbody> ' +
1017
+						'</table>';
1018
+					let actual = this.md(markdown);
1019
+					this.assertEqual(actual, expected);
1020
+				}
1021
+
1022
+				test_table_holes() {
1023
+					let markdown = 'Column A||Column C\n' +
1024
+						'---|---|---\n' +
1025
+						'|1|2||\n' +
1026
+						'|4||6|\n' +
1027
+						'||8|9|';
1028
+					let expected = '<table> ' +
1029
+						'<thead> ' +
1030
+						'<tr> ' +
1031
+						'<th>Column A</th> ' +
1032
+						'<th></th> ' +
1033
+						'<th>Column C</th> ' +
1034
+						'</tr> ' +
1035
+						'</thead> ' +
1036
+						'<tbody> ' +
1037
+						'<tr> ' +
1038
+						'<td>1</td> ' +
1039
+						'<td>2</td> ' +
1040
+						'<td></td> ' +
1041
+						'</tr> ' +
1042
+						'<tr> ' +
1043
+						'<td>4</td> ' +
1044
+						'<td></td> ' +
1045
+						'<td>6</td> ' +
1046
+						'</tr> ' +
1047
+						'<tr> ' +
1048
+						'<td></td> ' +
1049
+						'<td>8</td> ' +
1050
+						'<td>9</td> ' +
1051
+						'</tr> ' +
1052
+						'</tbody> ' +
1053
+						'</table>';
1054
+					let actual = this.md(markdown);
1055
+					this.assertEqual(actual, expected);
1056
+				}
1057
+
1058
+				test_definitionList() {
1059
+					let markdown = 'term\n' +
1060
+						': definition\n' +
1061
+						'another' +
1062
+						' term\n' +
1063
+						': def 1\n' +
1064
+						' broken on next line\n' +
1065
+						': def 2';
1066
+					let expected = '<dl> ' +
1067
+						'<dt>term</dt> ' +
1068
+						'<dd>definition</dd> ' +
1069
+						'<dt>another term</dt> ' +
1070
+						'<dd>def 1 broken on next line</dd> ' +
1071
+						'<dd>def 2</dd> ' +
1072
+						'</dl>';
1073
+					let actual = this.md(markdown);
1074
+					this.assertEqual(actual, expected);
1075
+				}
1076
+
1077
+				test_footnotes() {
1078
+					let markdown = 'Lorem ipsum[^1] dolor[^2] sit[^1] amet\n\n[^1]: A footnote\n[^2]: Another footnote';
1079
+					let expected = '<p>Lorem ipsum<sup id="footnoteref_1"><a href="#footnote_1">1</a></sup> ' +
1080
+						'dolor<sup id="footnoteref_2"><a href="#footnote_2">2</a></sup> ' +
1081
+						'sit<sup id="footnoteref_3"><a href="#footnote_1">1</a></sup> amet</p> ' +
1082
+						'<div class="footnotes">' +
1083
+						'<hr/>' +
1084
+						'<ol>' +
1085
+						'<li value="1" id="footnote_1">A footnote <a href="#footnoteref_1" class="footnote-backref">↩︎</a> <a href="#footnoteref_3" class="footnote-backref">↩︎</a></li> ' +
1086
+						'<li value="2" id="footnote_2">Another footnote <a href="#footnoteref_2" class="footnote-backref">↩︎</a></li> ' +
1087
+						'</ol>' +
1088
+						'</div>';
1089
+					let actual = this.md(markdown);
1090
+					this.assertEqual(actual, expected);
1091
+				}
1092
+
1093
+				test_abbreviations() {
1094
+					let markdown = 'Lorem ipsum HTML dolor HTML sit\n' +
1095
+						'\n' +
1096
+						'*[HTML]: Hypertext Markup Language';
1097
+					let expected = '<p>Lorem ipsum <abbr title="Hypertext Markup Language">HTML</abbr> dolor <abbr title="Hypertext Markup Language">HTML</abbr> sit</p>';
631 1098
 					let actual = this.md(markdown);
632 1099
 					this.assertEqual(actual, expected);
633 1100
 				}

Carregando…
Cancelar
Salvar