Преглед изворни кода

Numerous bug fixes. Unit tests working again.

main
Rocketsoup пре 1 година
родитељ
комит
02d0d1c518
2 измењених фајлова са 273 додато и 103 уклоњено
  1. 120
    57
      js/markdown.js
  2. 153
    46
      testjs.html

+ 120
- 57
js/markdown.js Прегледај датотеку

165
 		for (var s = startIndex; s < tokensToSearch.length; s++) {
165
 		for (var s = startIndex; s < tokensToSearch.length; s++) {
166
 			var startMatch = this.findFirstTokens(tokensToSearch, startPattern, s);
166
 			var startMatch = this.findFirstTokens(tokensToSearch, startPattern, s);
167
 			if (startMatch === null) return null;
167
 			if (startMatch === null) return null;
168
-			var endMatch = this.findFirstTokens(tokensToSearch, endPattern, startMatch.index + startMatch.tokens.length);
169
-			if (endMatch === null) return null;
170
-			var contents = tokensToSearch.slice(startMatch.index + startMatch.tokens.length, endMatch.index);
171
-			if (contents.length > 0 && (contentValidator === null || contentValidator(contents))) {
172
-				return {
173
-					'startTokens': startMatch.tokens,
174
-					'contentTokens': contents,
175
-					'endTokens': endMatch.tokens,
176
-					'startIndex': startMatch.index,
177
-					'contentIndex': startMatch.index + startMatch.tokens.length,
178
-					'endIndex': endMatch.index,
179
-					'totalLength': endMatch.index + endMatch.tokens.length - startMatch.index,
180
-				};
181
-			} else {
182
-				// No match. Try again right after this start index.
183
-				s = startMatch.index;
168
+			var endStart = startMatch.index + startMatch.tokens.length;
169
+			while (endStart < tokensToSearch.length) {
170
+				var endMatch = this.findFirstTokens(tokensToSearch, endPattern, endStart);
171
+				if (endMatch === null) break;
172
+				var contents = tokensToSearch.slice(startMatch.index + startMatch.tokens.length, endMatch.index);
173
+				if (contents.length > 0 && (contentValidator === null || contentValidator(contents))) {
174
+					return {
175
+						'startTokens': startMatch.tokens,
176
+						'contentTokens': contents,
177
+						'endTokens': endMatch.tokens,
178
+						'startIndex': startMatch.index,
179
+						'contentIndex': startMatch.index + startMatch.tokens.length,
180
+						'endIndex': endMatch.index,
181
+						'totalLength': endMatch.index + endMatch.tokens.length - startMatch.index,
182
+					};
183
+				} else {
184
+					// Contents rejected. Try next end match.
185
+					endStart = endMatch.index + 1;
186
+				}
184
 			}
187
 			}
188
+			// No end matches. Increment start match.
189
+			s = startMatch.index;
185
 		}
190
 		}
186
 		return null;
191
 		return null;
187
 	}
192
 	}
310
 	static tokenizeURL(line) {
315
 	static tokenizeURL(line) {
311
 		var groups;
316
 		var groups;
312
 		if (groups = this.#urlWithTitleRegex.exec(line)) {
317
 		if (groups = this.#urlWithTitleRegex.exec(line)) {
318
+			if (this.tokenizeEmail(line)) return null; // make sure it's not better described as an email address
313
 			return groups;
319
 			return groups;
314
 		}
320
 		}
315
 		if (groups = this.#urlRegex.exec(line)) {
321
 		if (groups = this.#urlRegex.exec(line)) {
322
+			if (this.tokenizeEmail(line)) return null;
316
 			return [...groups, null];
323
 			return [...groups, null];
317
 		}
324
 		}
318
 		return null;
325
 		return null;
340
 		}
347
 		}
341
 		return null;
348
 		return null;
342
 	}
349
 	}
350
+
351
+	/**
352
+	 * Describes the type of a variable for debugging.
353
+	 *
354
+	 * @param {any} value - value
355
+	 * @returns {String} description of type
356
+	 */
357
+	static typename(value) {
358
+		if (value === null) return 'null';
359
+		if (value instanceof Object) {
360
+			return value.constructor.name;
361
+		}
362
+		return typeof value;
363
+	}
343
 }
364
 }
344
 
365
 
345
 
366
 
862
 		line = line.trim();
883
 		line = line.trim();
863
 		if (line.startsWith('|')) line = line.substring(1);
884
 		if (line.startsWith('|')) line = line.substring(1);
864
 		if (line.endsWith('|')) line = line.substring(0, line.length - 1);
885
 		if (line.endsWith('|')) line = line.substring(0, line.length - 1);
865
-		return line.split('|').map(function(token) {
866
-			token = token.trim();
886
+		return line.split(/\s*\|\s*/).map(function(token) {
867
 			if (token.startsWith(':')) {
887
 			if (token.startsWith(':')) {
868
 				if (token.endsWith(':')) {
888
 				if (token.endsWith(':')) {
869
 					return MDTableCellBlock.AlignCenter;
889
 					return MDTableCellBlock.AlignCenter;
876
 		});
896
 		});
877
 	}
897
 	}
878
 
898
 
879
-	static #tableDividerRegex = /^\s*[|]?(?:\s*[:]?-+[:]?\s*\|)(?:\s*[:]?-+[:]?\s*)[|]?\s*$/;
899
+	static #tableDividerRegex = /^\s*[|]?\s*(?:[:]?-+[:]?)(?:\s*\|\s*[:]?-+[:]?)*\s*[|]?\s*$/;
880
 
900
 
881
 	readBlock(state) {
901
 	readBlock(state) {
882
 		if (!state.hasLines(2)) return null;
902
 		if (!state.hasLines(2)) return null;
883
 		let startP = state.p;
903
 		let startP = state.p;
884
 		let firstLine = state.lines[startP];
904
 		let firstLine = state.lines[startP];
885
-		var ignore, modifier;
886
-		[ignore, modifier] = MDTagModifier.fromLine(firstLine);
905
+		var modifier = MDTagModifier.fromLine(firstLine)[1];
887
 		let headerRow = this.#readTableRow(state, true);
906
 		let headerRow = this.#readTableRow(state, true);
888
 		if (headerRow === null) {
907
 		if (headerRow === null) {
889
 			state.p = startP;
908
 			state.p = startP;
1114
 		if (paragraphLines.length > 0) {
1133
 		if (paragraphLines.length > 0) {
1115
 			state.p = p;
1134
 			state.p = p;
1116
 			let content = paragraphLines.join("\n");
1135
 			let content = paragraphLines.join("\n");
1117
-			return new MDParagraphBlock(state.inlineMarkdownToSpan(content));
1136
+			return new MDParagraphBlock(new MDInlineBlock(state.inlineMarkdownToSpans(content)));
1118
 		}
1137
 		}
1119
 		return null;
1138
 		return null;
1120
 	}
1139
 	}
1232
 	 * @param {number} count - how many times the token is repeated to form the delimiter
1251
 	 * @param {number} count - how many times the token is repeated to form the delimiter
1233
 	 * @returns {boolean} `true` if substitution performed, `false` if not
1252
 	 * @returns {boolean} `true` if substitution performed, `false` if not
1234
 	 */
1253
 	 */
1235
-	attemptPair(state, priority, tokens, spanClass, delimiter, count=1) {
1254
+	attemptPair(state, priority, tokens, spanClass, delimiter, count=1, plaintext=false) {
1236
 		let delimiters = Array(count).fill(delimiter);
1255
 		let delimiters = Array(count).fill(delimiter);
1237
 		let firstPassPriority = (this.substitutePriority instanceof Array) ? this.substitutePriority[0] : null;
1256
 		let firstPassPriority = (this.substitutePriority instanceof Array) ? this.substitutePriority[0] : null;
1238
 		let match = MDToken.findPairedTokens(tokens, delimiters, delimiters, function(content) {
1257
 		let match = MDToken.findPairedTokens(tokens, delimiters, delimiters, function(content) {
1258
+			const firstType = content[0] instanceof MDToken ? content[0].type : null;
1259
+			const lastType = content[content.length - 1] instanceof MDToken ? content[content.length - 1].type : null;
1260
+			if (firstType == MDTokenType.Whitespace) return false;
1261
+			if (lastType == MDTokenType.Whitespace) return false;
1239
 			if (priority == firstPassPriority) {
1262
 			if (priority == firstPassPriority) {
1263
+				var innerCount = 0;
1240
 				for (let token of content) {
1264
 				for (let token of content) {
1241
-					if (token instanceof MDToken && token.type == delimiter) return false;
1265
+					if (token instanceof MDToken && token.type == delimiter) innerCount++;
1242
 				}
1266
 				}
1267
+				if ((innerCount % 2) != 0) return false;
1243
 			}
1268
 			}
1244
 			return true;
1269
 			return true;
1245
 		});
1270
 		});
1246
 		if (match === null) return false;
1271
 		if (match === null) return false;
1247
-		tokens.splice(match.startIndex, match.totalLength, new spanClass(state.tokensToSpans(match.contentTokens)));
1272
+		let content = (plaintext)
1273
+			? match.contentTokens.map((token) => token.original).join('')
1274
+			: state.tokensToSpans(match.contentTokens);
1275
+		tokens.splice(match.startIndex, match.totalLength, new spanClass(content));
1248
 		return true;
1276
 		return true;
1249
 	}
1277
 	}
1250
 }
1278
 }
1286
 }
1314
 }
1287
 
1315
 
1288
 class MDCodeInlineReader extends MDSimplePairInlineReader {
1316
 class MDCodeInlineReader extends MDSimplePairInlineReader {
1289
-	constructor(tokenizePriority=0.0, substitutePriority=[0.0, 50.0]) {
1317
+	constructor(tokenizePriority=0.0, substitutePriority=0.0) {
1290
 		super(tokenizePriority, substitutePriority);
1318
 		super(tokenizePriority, substitutePriority);
1291
 	}
1319
 	}
1292
 
1320
 
1296
 	}
1324
 	}
1297
 
1325
 
1298
 	substituteTokens(state, priority, tokens) {
1326
 	substituteTokens(state, priority, tokens) {
1299
-		if (this.attemptPair(state, priority, tokens, MDCodeSpan, MDTokenType.Backtick, 2)) return true;
1300
-		if (this.attemptPair(state, priority, tokens, MDCodeSpan, MDTokenType.Backtick)) return true;
1327
+		// ignore priority
1328
+		if (this.attemptPair(state, -1, tokens, MDCodeSpan, MDTokenType.Backtick, 2, true)) return true;
1329
+		if (this.attemptPair(state, -1, tokens, MDCodeSpan, MDTokenType.Backtick, 1, true)) return true;
1301
 		return false;
1330
 		return false;
1302
 	}
1331
 	}
1303
 }
1332
 }
1452
 }
1481
 }
1453
 
1482
 
1454
 class MDSimpleLinkInlineReader extends MDInlineReader {
1483
 class MDSimpleLinkInlineReader extends MDInlineReader {
1455
-	static #simpleURLRegex = new RegExp("^<(" + MDUtils.baseURLRegex.source + ")>", "i");  // 1=URL
1456
 	static #simpleEmailRegex = new RegExp("^<(" + MDUtils.baseEmailRegex.source + ")>", "i");  // 1=email
1484
 	static #simpleEmailRegex = new RegExp("^<(" + MDUtils.baseEmailRegex.source + ")>", "i");  // 1=email
1485
+	static #simpleURLRegex = new RegExp("^<(" + MDUtils.baseURLRegex.source + ")>", "i");  // 1=URL
1457
 
1486
 
1458
 	constructor(tokenizePriority=0.0, substitutePriority=0.0) {
1487
 	constructor(tokenizePriority=0.0, substitutePriority=0.0) {
1459
 		super(tokenizePriority, substitutePriority);
1488
 		super(tokenizePriority, substitutePriority);
1470
 		return null;
1499
 		return null;
1471
 	}
1500
 	}
1472
 
1501
 
1473
-	substituteTokens(state, priority, tokens) {
1502
+	#substituteEmail(state, tokens) {
1503
+		const result = MDToken.findFirstTokens(tokens, [ MDTokenType.SimpleEmail ]);
1504
+		if (result === null) return false;
1505
+		/** @type {MDToken} */
1506
+		const token = result.tokens[0];
1507
+		const link = `mailto:${token.content}`;
1508
+		const span = new MDLinkSpan(link, new MDObfuscatedTextSpan(token.content));
1509
+		tokens.splice(result.index, 1, span);
1510
+		return true;
1511
+	}
1512
+
1513
+	#substituteURL(state, tokens) {
1474
 		const result = MDToken.findFirstTokens(tokens, [ MDTokenType.SimpleLink ]);
1514
 		const result = MDToken.findFirstTokens(tokens, [ MDTokenType.SimpleLink ]);
1475
 		if (result === null) return false;
1515
 		if (result === null) return false;
1476
 		/** @type {MDToken} */
1516
 		/** @type {MDToken} */
1480
 		tokens.splice(result.index, 1, span);
1520
 		tokens.splice(result.index, 1, span);
1481
 		return true;
1521
 		return true;
1482
 	}
1522
 	}
1523
+
1524
+	substituteTokens(state, priority, tokens) {
1525
+		if (this.#substituteEmail(state, tokens)) return true;
1526
+		if (this.#substituteURL(state, tokens)) return true;
1527
+		return false;
1528
+	}
1483
 }
1529
 }
1484
 
1530
 
1485
 class MDHTMLTagInlineReader extends MDInlineReader {
1531
 class MDHTMLTagInlineReader extends MDInlineReader {
1584
 	 */
1630
 	 */
1585
 	constructor(blocks) {
1631
 	constructor(blocks) {
1586
 		super();
1632
 		super();
1587
-		this.#blocks = blocks;
1633
+		if (blocks instanceof Array) {
1634
+			this.#blocks = blocks;
1635
+		} else {
1636
+			throw new Error(`${MDUtils.typename(this)} expects MDBlock[], got ${MDUtils.typename(blocks)}`);
1637
+		}
1588
 	}
1638
 	}
1589
 
1639
 
1590
 	toHTML(state) {
1640
 	toHTML(state) {
1600
 }
1650
 }
1601
 
1651
 
1602
 class MDParagraphBlock extends MDBlock {
1652
 class MDParagraphBlock extends MDBlock {
1603
-	/** @type {MDBlock} */
1604
-	content;
1653
+	/** @type {MDBlock[]} */
1654
+	#content;
1605
 
1655
 
1606
 	/**
1656
 	/**
1607
-	 * @param {MDBlock} content
1657
+	 * @param {MDBlock|MDBlock[]} content
1608
 	 */
1658
 	 */
1609
 	constructor(content) {
1659
 	constructor(content) {
1610
 		super();
1660
 		super();
1611
-		this.content = content;
1661
+		if (content instanceof Array) {
1662
+			this.#content = content;
1663
+		} else if (content instanceof MDBlock) {
1664
+			this.#content = [ content ];
1665
+		} else {
1666
+			throw new Error(`${MDUtils.typename(this)} expects MDBlock[] or MDBlock, got ${MDUtils.typename(content)}`);
1667
+		}
1612
 	}
1668
 	}
1613
 
1669
 
1614
 	toHTML(state) {
1670
 	toHTML(state) {
1615
-		let contentHTML = this.content.toHTML(state);
1671
+		const contentHTML = MDBlock.toHTML(this.#content, state);
1616
 		return `<p${this.htmlAttributes()}>${contentHTML}</p>\n`;
1672
 		return `<p${this.htmlAttributes()}>${contentHTML}</p>\n`;
1617
 	}
1673
 	}
1618
 
1674
 
1619
 	visitChildren(fn) {
1675
 	visitChildren(fn) {
1620
-		fn(this.content);
1621
-		this.content.visitChildren(fn);
1676
+		for (const child of this.#content) {
1677
+			fn(child);
1678
+			child.visitChildren(fn);
1679
+		}
1622
 	}
1680
 	}
1623
 }
1681
 }
1624
 
1682
 
1625
 class MDHeaderBlock extends MDBlock {
1683
 class MDHeaderBlock extends MDBlock {
1626
 	/** @type {number} */
1684
 	/** @type {number} */
1627
-	level;
1685
+	#level;
1628
 	/** @type {MDBlock} */
1686
 	/** @type {MDBlock} */
1629
-	content;
1687
+	#content;
1630
 
1688
 
1631
 	/**
1689
 	/**
1632
 	 * @param {number} level
1690
 	 * @param {number} level
1634
 	 */
1692
 	 */
1635
 	constructor(level, content) {
1693
 	constructor(level, content) {
1636
 		super();
1694
 		super();
1637
-		this.level = level;
1638
-		this.content = content;
1695
+		this.#level = level;
1696
+		this.#content = content;
1639
 	}
1697
 	}
1640
 
1698
 
1641
 	toHTML(state) {
1699
 	toHTML(state) {
1642
-		let contentHTML = this.content.toHTML(state);
1643
-		return `<h${this.level}${this.htmlAttributes()}>${contentHTML}</h${this.level}>\n`;
1700
+		let contentHTML = this.#content.toHTML(state);
1701
+		return `<h${this.#level}${this.htmlAttributes()}>${contentHTML}</h${this.level}>\n`;
1644
 	}
1702
 	}
1645
 
1703
 
1646
 	visitChildren(fn) {
1704
 	visitChildren(fn) {
2318
 }
2376
 }
2319
 
2377
 
2320
 class MDCodeSpan extends MDSpan {
2378
 class MDCodeSpan extends MDSpan {
2321
-	/** @type {string} content */
2379
+	/** @type {String} content */
2322
 	#content;
2380
 	#content;
2323
 
2381
 
2324
 	/**
2382
 	/**
2325
-	 * @param {string} content
2383
+	 * @param {String} content
2326
 	 */
2384
 	 */
2327
 	constructor(content) {
2385
 	constructor(content) {
2328
 		super();
2386
 		super();
2329
-		this.#content = content;
2387
+		if (typeof content == 'string') {
2388
+			this.#content = content;
2389
+		} else {
2390
+			throw new Error(`${this.constructor.name} content must be String, got ${typeof content}`);
2391
+		}
2330
 	}
2392
 	}
2331
 
2393
 
2332
 	toHTML(state) {
2394
 	toHTML(state) {
2977
 				const changed = reader.substituteTokens(this, priority, spans);
3039
 				const changed = reader.substituteTokens(this, priority, spans);
2978
 				if (!changed) continue;
3040
 				if (!changed) continue;
2979
 				anyChanges = true;
3041
 				anyChanges = true;
3042
+				break;
2980
 			}
3043
 			}
2981
 		} while (anyChanges);
3044
 		} while (anyChanges);
2982
 
3045
 
3126
 	 * @type {MDInlineReader[]}
3189
 	 * @type {MDInlineReader[]}
3127
 	 */
3190
 	 */
3128
 	static standardInlineReaders = [
3191
 	static standardInlineReaders = [
3129
-		new MDStrongInlineReader(10.0, [ 0.0, 50.0 ]),
3130
-		new MDEmphasisInlineReader(15.0, [ 0.0, 50.0 ]),
3131
-		new MDCodeInlineReader(20.0, [ 0.0, 50.0 ]),
3132
-		new MDImageInlineReader(25.0, 10.0),
3133
-		new MDLinkInlineReader(30.0, 15.0),
3134
-		new MDSimpleLinkInlineReader(35.0, 20.0),
3135
-		new MDHTMLTagInlineReader(80.0, 25.0),
3192
+		new MDStrongInlineReader(10.0, [ 0.0, 2.0 ]),
3193
+		new MDEmphasisInlineReader(15.0, [ 5.0, 55.0 ]),
3194
+		new MDCodeInlineReader(20.0, [ 10.0, 60.0 ]),
3195
+		new MDImageInlineReader(25.0, 15.0),
3196
+		new MDLinkInlineReader(30.0, 20.0),
3197
+		new MDSimpleLinkInlineReader(35.0, 25.0),
3198
+		new MDHTMLTagInlineReader(80.0, 30.0),
3136
 	];
3199
 	];
3137
 
3200
 
3138
 	/**
3201
 	/**
3141
 	 */
3204
 	 */
3142
 	static allInlineReaders = [
3205
 	static allInlineReaders = [
3143
 		...this.standardInlineReaders,
3206
 		...this.standardInlineReaders,
3144
-		new MDStrikethroughInlineReader(21.0, [ 0.0, 50.0 ]),
3145
-		new MDFootnoteInlineReader(5.0, 30.0),
3146
-		new MDModifierInlineReader(90.0, 35.0),
3207
+		new MDStrikethroughInlineReader(21.0, [ 12.0, 50.0 ]),
3208
+		new MDFootnoteInlineReader(5.0, 40.0),
3209
+		new MDModifierInlineReader(90.0, 45.0),
3147
 	];
3210
 	];
3148
 
3211
 
3149
 	/**
3212
 	/**

+ 153
- 46
testjs.html Прегледај датотеку

50
 			}
50
 			}
51
 		</style>
51
 		</style>
52
 		<script src="js/markdown.js"></script>
52
 		<script src="js/markdown.js"></script>
53
+		<!-- Testing infrastructure -->
53
 		<script>
54
 		<script>
54
-			function onLoad() {
55
-				let testClasses = [
56
-					InlineTests,
57
-					BlockTests,
58
-				];
59
-				TestClassRunner.runAll(testClasses);
60
-			}
61
-
62
 			/**
55
 			/**
63
 			 * @param {String} text
56
 			 * @param {String} text
64
 			 * @returns {String}
57
 			 * @returns {String}
65
 			 */
58
 			 */
66
 			function escapeHTML(text) {
59
 			function escapeHTML(text) {
67
-				return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
60
+				return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br/>\n');
68
 			}
61
 			}
69
 
62
 
70
 			class ResultType {
63
 			class ResultType {
106
 					if (test) this.fail(failMessage || `expected false, got ${test}`);
99
 					if (test) this.fail(failMessage || `expected false, got ${test}`);
107
 				}
100
 				}
108
 				assertEqual(a, b, failMessage=null) {
101
 				assertEqual(a, b, failMessage=null) {
109
-					if (a != b) this.fail(failMessage || `equality failed: ${a} != ${b}`);
102
+					if (a == b) return;
103
+					const aVal = `${a}`;
104
+					const bVal = `${b}`;
105
+					if (aVal.length > 20 || bVal.length > 20) {
106
+						this.fail(failMessage || `equality failed:\n${aVal}\n!=\n${bVal}`);
107
+					} else {
108
+						this.fail(failMessage || `equality failed: ${aVal} != ${bVal}`);
109
+					}
110
 				}
110
 				}
111
 				expectError(e=true) {
111
 				expectError(e=true) {
112
 					if (this.currentRunner) this.currentRunner.expectedError = e;
112
 					if (this.currentRunner) this.currentRunner.expectedError = e;
176
 						} else {
176
 						} else {
177
 							this.result = ResultType.errored;
177
 							this.result = ResultType.errored;
178
 							this.message = e.message;
178
 							this.message = e.message;
179
+							if (e.stack !== undefined) {
180
+								this.message += "\n" + e.stack;
181
+							}
179
 						}
182
 						}
180
 					} finally {
183
 					} finally {
181
 						this.expectedError = null;
184
 						this.expectedError = null;
316
 					}, 1);
319
 					}, 1);
317
 				}
320
 				}
318
 			}
321
 			}
319
-
320
-			// ---------------------------------------------------------------
322
+		</script>
323
+		<!-- Tests -->
324
+		<script>
325
+			function onLoad() {
326
+				let testClasses = [
327
+					TokenTests,
328
+					InlineTests,
329
+					BlockTests,
330
+				];
331
+				TestClassRunner.runAll(testClasses);
332
+			}
333
+			document.addEventListener('DOMContentLoaded', onLoad);
321
 
334
 
322
 			function normalizeWhitespace(str) {
335
 			function normalizeWhitespace(str) {
323
 				return str.replace(/\s+/g, ' ').replace(/(?:^\s+|\s+$)/g, '');
336
 				return str.replace(/\s+/g, ' ').replace(/(?:^\s+|\s+$)/g, '');
324
 			}
337
 			}
325
 
338
 
339
+			class TokenTests extends BaseTest {
340
+				test_findFirstTokens() {
341
+					const tokens = [
342
+						new MDToken('Lorem', MDTokenType.Text),
343
+						new MDToken(' ', MDTokenType.Whitespace),
344
+						new MDToken('_', MDTokenType.Underscore),
345
+						new MDToken('ipsum', MDTokenType.Text),
346
+						new MDToken('_', MDTokenType.Underscore),
347
+						new MDToken(' ', MDTokenType.Whitespace),
348
+						new MDToken('dolor', MDTokenType.Text),
349
+						new MDToken('_', MDTokenType.Underscore),
350
+						new MDToken('sit', MDTokenType.Text),
351
+						new MDToken('_', MDTokenType.Underscore),
352
+					];
353
+					const pattern = [
354
+						MDTokenType.Underscore,
355
+					];
356
+					const result = MDToken.findFirstTokens(tokens, pattern);
357
+					const expected = {
358
+						tokens: [ tokens[2] ],
359
+						index: 2,
360
+					};
361
+					this.assertEqual(JSON.stringify(result), JSON.stringify(expected));
362
+				}
363
+
364
+				test_findPairedTokens() {
365
+					const tokens = [
366
+						new MDToken('Lorem', MDTokenType.Text),
367
+						new MDToken(' ', MDTokenType.Whitespace),
368
+						new MDToken('_', MDTokenType.Underscore),
369
+						new MDToken('ipsum', MDTokenType.Text),
370
+						new MDToken('_', MDTokenType.Underscore),
371
+						new MDToken(' ', MDTokenType.Whitespace),
372
+						new MDToken('dolor', MDTokenType.Text),
373
+						new MDToken('_', MDTokenType.Underscore),
374
+						new MDToken('sit', MDTokenType.Text),
375
+						new MDToken('_', MDTokenType.Underscore),
376
+					];
377
+					const pattern = [
378
+						MDTokenType.Underscore,
379
+					];
380
+					const result = MDToken.findPairedTokens(tokens, pattern, pattern);
381
+					const expected = {
382
+						startTokens: [ tokens[2] ],
383
+						contentTokens: [ tokens[3] ],
384
+						endTokens: [ tokens[4] ],
385
+						startIndex: 2,
386
+						contentIndex: 3,
387
+						endIndex: 4,
388
+						totalLength: 3,
389
+					}
390
+					this.assertEqual(JSON.stringify(result), JSON.stringify(expected));
391
+				}
392
+			}
393
+
326
 			class InlineTests extends BaseTest {
394
 			class InlineTests extends BaseTest {
395
+				/** @type {Markdown} */
396
+				parser;
397
+				md(markdown) {
398
+					return normalizeWhitespace(this.parser.toHTML(markdown));
399
+				}
400
+			
401
+				setUp() {
402
+					this.parser = Markdown.completeParser;
403
+				}
404
+
327
 				test_simpleSingleParagraph() {
405
 				test_simpleSingleParagraph() {
328
 					let markdown = 'Lorem ipsum';
406
 					let markdown = 'Lorem ipsum';
329
 					let expected = '<p>Lorem ipsum</p>';
407
 					let expected = '<p>Lorem ipsum</p>';
330
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
408
+					let actual = this.md(markdown);
331
 					this.assertEqual(actual, expected);
409
 					this.assertEqual(actual, expected);
332
 				}
410
 				}
333
 
411
 
334
 				test_strong() {
412
 				test_strong() {
335
 					let markdown = 'Lorem **ipsum** dolor **sit**';
413
 					let markdown = 'Lorem **ipsum** dolor **sit**';
336
 					let expected = '<p>Lorem <strong>ipsum</strong> dolor <strong>sit</strong></p>';
414
 					let expected = '<p>Lorem <strong>ipsum</strong> dolor <strong>sit</strong></p>';
337
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
415
+					let actual = this.md(markdown);
338
 					this.assertEqual(actual, expected);
416
 					this.assertEqual(actual, expected);
339
 				}
417
 				}
340
 
418
 
341
 				test_emphasis() {
419
 				test_emphasis() {
342
 					let markdown = 'Lorem _ipsum_ dolor _sit_';
420
 					let markdown = 'Lorem _ipsum_ dolor _sit_';
343
 					let expected = '<p>Lorem <em>ipsum</em> dolor <em>sit</em></p>';
421
 					let expected = '<p>Lorem <em>ipsum</em> dolor <em>sit</em></p>';
344
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
422
+					let actual = this.md(markdown);
345
 					this.assertEqual(actual, expected);
423
 					this.assertEqual(actual, expected);
346
 				}
424
 				}
347
 
425
 
348
-				test_strongEmphasis_easy() {
426
+				test_strongEmphasis_cleanNesting1() {
349
 					let markdown = 'Lorem **ipsum *dolor* sit** amet';
427
 					let markdown = 'Lorem **ipsum *dolor* sit** amet';
350
 					let expected = '<p>Lorem <strong>ipsum <em>dolor</em> sit</strong> amet</p>';
428
 					let expected = '<p>Lorem <strong>ipsum <em>dolor</em> sit</strong> amet</p>';
351
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
429
+					let actual = this.md(markdown);
352
 					this.assertEqual(actual, expected);
430
 					this.assertEqual(actual, expected);
353
 				}
431
 				}
354
 
432
 
355
-				test_strongEmphasis_medium() {
433
+				test_strongEmphasis_cleanNesting2() {
434
+					let markdown = 'Lorem *ipsum **dolor** sit* amet';
435
+					let expected = '<p>Lorem <em>ipsum <strong>dolor</strong> sit</em> amet</p>';
436
+					let actual = this.md(markdown);
437
+					this.assertEqual(actual, expected);
438
+				}
439
+
440
+				test_strongEmphasis_tightNesting() {
356
 					let markdown = 'Lorem ***ipsum*** dolor';
441
 					let markdown = 'Lorem ***ipsum*** dolor';
357
 					let expected1 = '<p>Lorem <strong><em>ipsum</em></strong> dolor</p>';
442
 					let expected1 = '<p>Lorem <strong><em>ipsum</em></strong> dolor</p>';
358
 					let expected2 = '<p>Lorem <em><strong>ipsum</strong></em> dolor</p>';
443
 					let expected2 = '<p>Lorem <em><strong>ipsum</strong></em> dolor</p>';
359
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
444
+					let actual = this.md(markdown);
360
 					this.assertTrue(actual == expected1 || actual == expected2);
445
 					this.assertTrue(actual == expected1 || actual == expected2);
361
 				}
446
 				}
362
 
447
 
363
-				test_strongEmphasis_hard1() {
448
+				test_strongEmphasis_lopsidedNesting1() {
364
 					let markdown = 'Lorem ***ipsum* dolor** sit';
449
 					let markdown = 'Lorem ***ipsum* dolor** sit';
365
 					let expected = '<p>Lorem <strong><em>ipsum</em> dolor</strong> sit</p>';
450
 					let expected = '<p>Lorem <strong><em>ipsum</em> dolor</strong> sit</p>';
366
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
451
+					let actual = this.md(markdown);
367
 					this.assertEqual(actual, expected);
452
 					this.assertEqual(actual, expected);
368
 				}
453
 				}
369
 
454
 
370
-				test_strongEmphasis_hard2() {
455
+				test_strongEmphasis_lopsidedNesting2() {
371
 					let markdown = 'Lorem ***ipsum** dolor* sit';
456
 					let markdown = 'Lorem ***ipsum** dolor* sit';
372
 					let expected = '<p>Lorem <em><strong>ipsum</strong> dolor</em> sit</p>';
457
 					let expected = '<p>Lorem <em><strong>ipsum</strong> dolor</em> sit</p>';
373
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
458
+					let actual = this.md(markdown);
459
+					this.assertEqual(actual, expected);
460
+				}
461
+
462
+				test_strongEmphasis_lopsidedNesting3() {
463
+					let markdown = 'Lorem **ipsum *dolor*** sit';
464
+					let expected = '<p>Lorem <strong>ipsum <em>dolor</em></strong> sit</p>';
465
+					let actual = this.md(markdown);
466
+					this.assertEqual(actual, expected);
467
+				}
468
+
469
+				test_strongEmphasis_lopsidedNesting4() {
470
+					let markdown = 'Lorem *ipsum **dolor*** sit';
471
+					let expected = '<p>Lorem <em>ipsum <strong>dolor</strong></em> sit</p>';
472
+					let actual = this.md(markdown);
374
 					this.assertEqual(actual, expected);
473
 					this.assertEqual(actual, expected);
375
 				}
474
 				}
376
 
475
 
377
 				test_inlineCode() {
476
 				test_inlineCode() {
378
 					let markdown = 'Lorem `ipsum` dolor';
477
 					let markdown = 'Lorem `ipsum` dolor';
379
 					let expected = '<p>Lorem <code>ipsum</code> dolor</p>';
478
 					let expected = '<p>Lorem <code>ipsum</code> dolor</p>';
380
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
479
+					let actual = this.md(markdown);
381
 					this.assertEqual(actual, expected);
480
 					this.assertEqual(actual, expected);
382
 				}
481
 				}
383
 
482
 
384
 				test_inlineCode_withInnerBacktick() {
483
 				test_inlineCode_withInnerBacktick() {
385
 					let markdown = 'Lorem ``ip`su`m`` dolor';
484
 					let markdown = 'Lorem ``ip`su`m`` dolor';
386
 					let expected = '<p>Lorem <code>ip`su`m</code> dolor</p>';
485
 					let expected = '<p>Lorem <code>ip`su`m</code> dolor</p>';
387
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
486
+					let actual = this.md(markdown);
388
 					this.assertEqual(actual, expected);
487
 					this.assertEqual(actual, expected);
389
 				}
488
 				}
390
 
489
 
391
 				test_strikethrough_single() {
490
 				test_strikethrough_single() {
392
 					let markdown = 'Lorem ~ipsum~ dolor';
491
 					let markdown = 'Lorem ~ipsum~ dolor';
393
 					let expected = '<p>Lorem <strike>ipsum</strike> dolor</p>';
492
 					let expected = '<p>Lorem <strike>ipsum</strike> dolor</p>';
394
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
493
+					let actual = this.md(markdown);
395
 					this.assertEqual(actual, expected);
494
 					this.assertEqual(actual, expected);
396
 				}
495
 				}
397
 
496
 
398
 				test_strikethrough_double() {
497
 				test_strikethrough_double() {
399
 					let markdown = 'Lorem ~~ipsum~~ dolor';
498
 					let markdown = 'Lorem ~~ipsum~~ dolor';
400
 					let expected = '<p>Lorem <strike>ipsum</strike> dolor</p>';
499
 					let expected = '<p>Lorem <strike>ipsum</strike> dolor</p>';
401
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
500
+					let actual = this.md(markdown);
402
 					this.assertEqual(actual, expected);
501
 					this.assertEqual(actual, expected);
403
 				}
502
 				}
404
 
503
 
405
 				test_link_fullyQualified() {
504
 				test_link_fullyQualified() {
406
 					let markdown = 'Lorem [ipsum](https://example.com/path/page.html) dolor';
505
 					let markdown = 'Lorem [ipsum](https://example.com/path/page.html) dolor';
407
 					let expected = '<p>Lorem <a href="https://example.com/path/page.html">ipsum</a> dolor</p>';
506
 					let expected = '<p>Lorem <a href="https://example.com/path/page.html">ipsum</a> dolor</p>';
408
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
507
+					let actual = this.md(markdown);
409
 					this.assertEqual(actual, expected);
508
 					this.assertEqual(actual, expected);
410
 				}
509
 				}
411
 
510
 
412
 				test_link_relative() {
511
 				test_link_relative() {
413
 					let markdown = 'Lorem [ipsum](page.html) dolor';
512
 					let markdown = 'Lorem [ipsum](page.html) dolor';
414
 					let expected = '<p>Lorem <a href="page.html">ipsum</a> dolor</p>';
513
 					let expected = '<p>Lorem <a href="page.html">ipsum</a> dolor</p>';
415
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
514
+					let actual = this.md(markdown);
416
 					this.assertEqual(actual, expected);
515
 					this.assertEqual(actual, expected);
417
 				}
516
 				}
418
 
517
 
419
 				test_link_title() {
518
 				test_link_title() {
420
 					let markdown = 'Lorem [ipsum](page.html "link title") dolor';
519
 					let markdown = 'Lorem [ipsum](page.html "link title") dolor';
421
 					let expected = '<p>Lorem <a href="page.html" title="link title">ipsum</a> dolor</p>';
520
 					let expected = '<p>Lorem <a href="page.html" title="link title">ipsum</a> dolor</p>';
422
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
521
+					let actual = this.md(markdown);
423
 					this.assertEqual(actual, expected);
522
 					this.assertEqual(actual, expected);
424
 				}
523
 				}
425
 
524
 
426
 				test_link_literal() {
525
 				test_link_literal() {
427
 					let markdown = 'Lorem <https://example.com> dolor';
526
 					let markdown = 'Lorem <https://example.com> dolor';
428
 					let expected = '<p>Lorem <a href="https://example.com">https://example.com</a> dolor</p>';
527
 					let expected = '<p>Lorem <a href="https://example.com">https://example.com</a> dolor</p>';
429
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
528
+					let actual = this.md(markdown);
430
 					this.assertEqual(actual, expected);
529
 					this.assertEqual(actual, expected);
431
 				}
530
 				}
432
 
531
 
433
 				test_link_ref() {
532
 				test_link_ref() {
434
 					let markdown = "Lorem [ipsum][ref] dolor\n\n[ref]: https://example.com";
533
 					let markdown = "Lorem [ipsum][ref] dolor\n\n[ref]: https://example.com";
435
 					let expected = '<p>Lorem <a href="https://example.com">ipsum</a> dolor</p>';
534
 					let expected = '<p>Lorem <a href="https://example.com">ipsum</a> dolor</p>';
436
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
535
+					let actual = this.md(markdown);
437
 					this.assertEqual(actual, expected);
536
 					this.assertEqual(actual, expected);
438
 				}
537
 				}
439
 
538
 
440
 				test_link_email() {
539
 				test_link_email() {
441
 					let markdown = 'Lorem [ipsum](user@example.com) dolor';
540
 					let markdown = 'Lorem [ipsum](user@example.com) dolor';
442
 					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>';
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>';
443
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
542
+					let actual = this.md(markdown);
444
 					this.assertEqual(actual, expected);
543
 					this.assertEqual(actual, expected);
445
 				}
544
 				}
446
 
545
 
447
 				test_link_email_withTitle() {
546
 				test_link_email_withTitle() {
448
 					let markdown = 'Lorem [ipsum](user@example.com "title") dolor';
547
 					let markdown = 'Lorem [ipsum](user@example.com "title") dolor';
449
 					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>';
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>';
450
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
549
+					let actual = this.md(markdown);
451
 					this.assertEqual(actual, expected);
550
 					this.assertEqual(actual, expected);
452
 				}
551
 				}
453
 
552
 
454
 				test_link_literalEmail() {
553
 				test_link_literalEmail() {
455
 					let markdown = 'Lorem <user@example.com> dolor';
554
 					let markdown = 'Lorem <user@example.com> dolor';
456
 					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>';
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>';
457
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
556
+					let actual = this.md(markdown);
458
 					this.assertEqual(actual, expected);
557
 					this.assertEqual(actual, expected);
459
 				}
558
 				}
460
 
559
 
461
 				test_image() {
560
 				test_image() {
462
 					let markdown = 'Lorem ![alt text](image.jpg) dolor';
561
 					let markdown = 'Lorem ![alt text](image.jpg) dolor';
463
 					let expected = '<p>Lorem <img src="image.jpg" alt="alt text"> dolor</p>';
562
 					let expected = '<p>Lorem <img src="image.jpg" alt="alt text"> dolor</p>';
464
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
563
+					let actual = this.md(markdown);
465
 					this.assertEqual(actual, expected);
564
 					this.assertEqual(actual, expected);
466
 				}
565
 				}
467
 
566
 
468
 				test_image_noAlt() {
567
 				test_image_noAlt() {
469
 					let markdown = 'Lorem ![](image.jpg) dolor';
568
 					let markdown = 'Lorem ![](image.jpg) dolor';
470
 					let expected = '<p>Lorem <img src="image.jpg"> dolor</p>';
569
 					let expected = '<p>Lorem <img src="image.jpg"> dolor</p>';
471
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
570
+					let actual = this.md(markdown);
472
 					this.assertEqual(actual, expected);
571
 					this.assertEqual(actual, expected);
473
 				}
572
 				}
474
 
573
 
475
 				test_image_withTitle() {
574
 				test_image_withTitle() {
476
 					let markdown = 'Lorem ![alt text](image.jpg "image title") dolor';
575
 					let markdown = 'Lorem ![alt text](image.jpg "image title") dolor';
477
 					let expected = '<p>Lorem <img src="image.jpg" alt="alt text" title="image title"> dolor</p>';
576
 					let expected = '<p>Lorem <img src="image.jpg" alt="alt text" title="image title"> dolor</p>';
478
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
577
+					let actual = this.md(markdown);
479
 					this.assertEqual(actual, expected);
578
 					this.assertEqual(actual, expected);
480
 				}
579
 				}
481
 			}
580
 			}
482
 
581
 
483
 			class BlockTests extends BaseTest {
582
 			class BlockTests extends BaseTest {
583
+				/** @type {Markdown} */
584
+				parser;
585
+				md(markdown) {
586
+					return normalizeWhitespace(this.parser.toHTML(markdown));
587
+				}
588
+			
589
+				setUp() {
590
+					this.parser = Markdown.completeParser;
591
+				}
592
+
484
 				test_paragraphs() {
593
 				test_paragraphs() {
485
 					let markdown = "Lorem ipsum\n\nDolor sit amet";
594
 					let markdown = "Lorem ipsum\n\nDolor sit amet";
486
 					let expected = "<p>Lorem ipsum</p> <p>Dolor sit amet</p>";
595
 					let expected = "<p>Lorem ipsum</p> <p>Dolor sit amet</p>";
487
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
596
+					let actual = this.md(markdown);
488
 					this.assertEqual(actual, expected);
597
 					this.assertEqual(actual, expected);
489
 				}
598
 				}
490
 
599
 
491
 				test_paragraph_lineGrouping() {
600
 				test_paragraph_lineGrouping() {
492
 					let markdown = "Lorem ipsum\ndolor sit amet";
601
 					let markdown = "Lorem ipsum\ndolor sit amet";
493
 					let expected = "<p>Lorem ipsum dolor sit amet</p>";
602
 					let expected = "<p>Lorem ipsum dolor sit amet</p>";
494
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
603
+					let actual = this.md(markdown);
495
 					this.assertEqual(actual, expected);
604
 					this.assertEqual(actual, expected);
496
 				}
605
 				}
497
 
606
 
498
 				test_unorderedList() {
607
 				test_unorderedList() {
499
 					let markdown = "* Lorem\n* Ipsum\n* Dolor";
608
 					let markdown = "* Lorem\n* Ipsum\n* Dolor";
500
 					let expected = '<ul> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ul>';
609
 					let expected = '<ul> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ul>';
501
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
610
+					let actual = this.md(markdown);
502
 					this.assertEqual(actual, expected);
611
 					this.assertEqual(actual, expected);
503
 				}
612
 				}
504
 
613
 
505
 				test_orderedList() {
614
 				test_orderedList() {
506
 					let markdown = "1. Lorem\n1. Ipsum\n5. Dolor";
615
 					let markdown = "1. Lorem\n1. Ipsum\n5. Dolor";
507
 					let expected = '<ol start="1"> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ol>';
616
 					let expected = '<ol start="1"> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ol>';
508
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
617
+					let actual = this.md(markdown);
509
 					this.assertEqual(actual, expected);
618
 					this.assertEqual(actual, expected);
510
 				}
619
 				}
511
 
620
 
512
 				test_orderedList_numbering() {
621
 				test_orderedList_numbering() {
513
 					let markdown = "4. Lorem\n1. Ipsum\n9. Dolor";
622
 					let markdown = "4. Lorem\n1. Ipsum\n9. Dolor";
514
 					let expected = '<ol start="4"> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ol>';
623
 					let expected = '<ol start="4"> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ol>';
515
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
624
+					let actual = this.md(markdown);
516
 					this.assertEqual(actual, expected);
625
 					this.assertEqual(actual, expected);
517
 				}
626
 				}
518
 
627
 
519
 				test_blockquote() {
628
 				test_blockquote() {
520
 					let markdown = '> Lorem ipsum dolor';
629
 					let markdown = '> Lorem ipsum dolor';
521
 					let expected = '<blockquote> <p>Lorem ipsum dolor</p> </blockquote>';
630
 					let expected = '<blockquote> <p>Lorem ipsum dolor</p> </blockquote>';
522
-					let actual = normalizeWhitespace(Markdown.toHTML(markdown));
631
+					let actual = this.md(markdown);
523
 					this.assertEqual(actual, expected);
632
 					this.assertEqual(actual, expected);
524
 				}
633
 				}
525
 			}
634
 			}
526
-
527
-			document.addEventListener('DOMContentLoaded', onLoad);
528
 		</script>
635
 		</script>
529
 	</head>
636
 	</head>
530
 	<body>
637
 	<body>

Loading…
Откажи
Сачувај