PHP and Javascript implementations of a simple markdown parser
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

testjs.html 38KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>Markdown Unit Tests</title>
  6. <link rel="icon" href="data:;base64,iVBORw0KGgo=">
  7. <style type="text/css">
  8. :root {
  9. font-family: sans-serif;
  10. --color-passed: #090;
  11. --color-failed: #a00;
  12. --color-errored: #a80;
  13. --color-untested: #888;
  14. }
  15. .testclass {
  16. border: 1px solid black;
  17. padding: 0.5em 1em;
  18. margin-bottom: 1em;
  19. max-width: 50em;
  20. }
  21. .testclassname {
  22. font-weight: bold;
  23. font-size: 1.25rem;
  24. padding-bottom: 0.25em;
  25. }
  26. .testclassstatus {
  27. }
  28. .testclassstatus.passed { color: var(--color-passed); }
  29. .testclassstatus.failed { color: var(--color-failed); }
  30. .testclassstatus.errored { color: var(--color-errored); }
  31. .testclassstatus.untested { color: var(--color-untested); }
  32. .testcase {
  33. clear: both;
  34. padding: 0.2em 0;
  35. margin-left: 2em;
  36. }
  37. .testcase {
  38. border-top: 1px solid #888;
  39. }
  40. .testcasename {
  41. font-family: monospace;
  42. }
  43. .testcasestatus {
  44. font-weight: bold;
  45. font-size: 80%;
  46. float: left;
  47. }
  48. .testcasetiming {
  49. float: right;
  50. color: #888;
  51. font-size: 80%;
  52. }
  53. .testcaseresult {
  54. height: 1em;
  55. }
  56. .testcasemessage {
  57. clear: both;
  58. }
  59. .result-untested {
  60. color: #888;
  61. }
  62. .result-testing {
  63. color: black;
  64. }
  65. .result-passed {
  66. color: #090;
  67. }
  68. .result-failed {
  69. color: #a00;
  70. }
  71. .result-errored {
  72. color: #a80;
  73. }
  74. </style>
  75. <script src="js/markdown.js"></script>
  76. <!-- Testing infrastructure -->
  77. <script>
  78. /**
  79. * @param {String} text
  80. * @returns {String}
  81. */
  82. function escapeHTML(text) {
  83. return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br/>\n');
  84. }
  85. class ResultType {
  86. static untested = new ResultType('untested');
  87. static testing = new ResultType('testing');
  88. static passed = new ResultType('passed');
  89. static failed = new ResultType('failed');
  90. static errored = new ResultType('errored');
  91. #name;
  92. constructor(name) {
  93. this.#name = name;
  94. }
  95. toString() {
  96. return `${this.constructor.name}.${this.#name}`;
  97. }
  98. }
  99. class FailureError extends Error {
  100. constructor(message=null) {
  101. super(message);
  102. }
  103. }
  104. class BaseTest {
  105. setUp() {}
  106. tearDown() {}
  107. /** @var {TestCaseRunner|null} */
  108. currentRunner = null;
  109. fail(failMessage=null) {
  110. throw new FailureError(failMessage || 'failed');
  111. }
  112. assertTrue(test, failMessage=null) {
  113. if (!test) this.fail(failMessage || `expected true, got ${test}`);
  114. }
  115. assertFalse(test, failMessage=null) {
  116. if (test) this.fail(failMessage || `expected false, got ${test}`);
  117. }
  118. assertEqual(a, b, failMessage=null) {
  119. if (MDUtils.equal(a, b)) return;
  120. const aVal = `${a}`;
  121. const bVal = `${b}`;
  122. if (aVal.length > 20 || bVal.length > 20) {
  123. this.fail(failMessage || `equality failed:\n${aVal}\n!=\n${bVal}`);
  124. } else {
  125. this.fail(failMessage || `equality failed: ${aVal} != ${bVal}`);
  126. }
  127. }
  128. expectError(e=true) {
  129. if (this.currentRunner) this.currentRunner.expectedError = e;
  130. }
  131. /**
  132. * @param {number} maxTime maximum time in seconds
  133. * @param {function} timedCode code to run and time
  134. */
  135. profile(maxTime, timedCode) {
  136. const startTime = performance.now();
  137. const result = timedCode();
  138. const endTime = performance.now();
  139. const seconds = (endTime - startTime) / 1000.0;
  140. if (seconds > maxTime) {
  141. this.fail(`Expected <= ${maxTime}s execution time, actual ${seconds}s.`);
  142. }
  143. return result;
  144. }
  145. }
  146. /**
  147. * Manages the running and results of a single test method on a test
  148. * class.
  149. */
  150. class TestCaseRunner {
  151. /** @var {number} */
  152. static #nextUniqueId = 1;
  153. /** @var {number} */
  154. uniqueId = 0;
  155. /** @var {BaseTest} */
  156. #objectUnderTest;
  157. /** @var {method} */
  158. #method;
  159. /** @var {ResultType} */
  160. result = ResultType.untested;
  161. /** @var {String|null} */
  162. message = null;
  163. expectedError = null;
  164. duration = null;
  165. /** @var {String} */
  166. get className() { return this.#objectUnderTest.constructor.name; }
  167. /** @var {String} */
  168. get methodName() { return this.#method.name; }
  169. /**
  170. * @param {BaseTest} objectUnderTest
  171. * @param {method} method
  172. */
  173. constructor(objectUnderTest, method) {
  174. this.#objectUnderTest = objectUnderTest;
  175. this.#method = method;
  176. this.uniqueId = TestCaseRunner.#nextUniqueId++;
  177. }
  178. run() {
  179. var start;
  180. this.expectedError = null;
  181. this.#objectUnderTest.currentRunner = this;
  182. try {
  183. this.#objectUnderTest.setUp();
  184. } catch (e) {
  185. console.error(`Failed to run ${this.className}.setUp() - ${e.message}`);
  186. this.result = ResultType.errored;
  187. this.message = e.message;
  188. return;
  189. }
  190. try {
  191. start = performance.now();
  192. this.#method.bind(this.#objectUnderTest)();
  193. this.duration = performance.now() - start;
  194. this.result = ResultType.passed;
  195. this.message = null;
  196. } catch (e) {
  197. this.duration = performance.now() - start;
  198. if (e instanceof FailureError) {
  199. this.result = ResultType.failed;
  200. this.message = e.message;
  201. } else if (this.#isExpectedError(e)) {
  202. this.result = ResultType.passed;
  203. this.message = null;
  204. } else {
  205. this.result = ResultType.errored;
  206. this.message = e.message;
  207. if (e.stack !== undefined) {
  208. this.message += "\n" + e.stack;
  209. }
  210. }
  211. } finally {
  212. this.expectedError = null;
  213. try {
  214. this.#objectUnderTest.tearDown();
  215. this.#objectUnderTest.currentRunner = null;
  216. } catch (e0) {
  217. console.error(`Failed to run ${this.className}.tearDown() - ${e0.message}`);
  218. this.result = ResultType.errored;
  219. this.message = e0.message;
  220. }
  221. }
  222. }
  223. get #cssId() { return `testcase${this.uniqueId}`; }
  224. #isExpectedError(e) {
  225. if (this.expectedError === null) return false;
  226. if (this.expectedError === true) return true;
  227. // TODO: Have a way to specify details about what kind of error is expected. Maybe a prototype instance and/or a testing lambda.
  228. return false;
  229. }
  230. toHTML() {
  231. var html = `<div class="testcase" id="${this.#cssId}">`;
  232. html += `<div class="testcasename"><span class="testcasemethod">${this.methodName}</span></div>`;
  233. html += '<div class="testcaseresult">';
  234. switch (this.result) {
  235. case ResultType.untested:
  236. html += '<div class="testcasestatus result-untested">Waiting to test</div>';
  237. break;
  238. case ResultType.testing:
  239. html += '<div class="testcasestatus result-testing">Testing...</div>';
  240. break;
  241. case ResultType.passed:
  242. html += '<div class="testcasestatus result-passed">Passed</div>';
  243. break;
  244. case ResultType.failed:
  245. html += '<div class="testcasestatus result-failed">Failed</div>';
  246. break;
  247. case ResultType.errored:
  248. html += '<div class="testcasestatus result-errored">Errored</div>';
  249. break;
  250. }
  251. if (this.duration !== null) {
  252. html += `<div class="testcasetiming">${Number(this.duration / 1000.0).toFixed(3)}s</div>`;
  253. }
  254. html += '</div>';
  255. if (this.message !== null) {
  256. html += `<div class="testcasemessage">${escapeHTML(this.message)}</div>`;
  257. }
  258. html += `</div>`;
  259. return html;
  260. }
  261. /**
  262. * Updates the HTML node in-place with the current status, or
  263. * adds it if it does not exist yet.
  264. */
  265. updateHTML() {
  266. let existing = document.getElementById(this.#cssId);
  267. if (existing) {
  268. existing.outerHTML = this.toHTML();
  269. } else {
  270. document.getElementById('results').innerHTML += this.toHTML();
  271. }
  272. }
  273. /**
  274. * @param {object} objectUnderTest
  275. * @returns {TestCaseRunner[]}
  276. */
  277. static findTestCases(objectUnderTest) {
  278. if (!(objectUnderTest instanceof BaseTest)) return [];
  279. var members = [];
  280. var obj = objectUnderTest;
  281. do {
  282. members.push(...Object.getOwnPropertyNames(obj));
  283. } while (obj = Object.getPrototypeOf(obj));
  284. return members.sort().filter((e, i, arr) => {
  285. if (e != arr[i + 1] && typeof objectUnderTest[e] == 'function' && e.startsWith('test')) return true;
  286. }).map((name) => {
  287. return new TestCaseRunner(objectUnderTest, objectUnderTest[name]);
  288. });
  289. }
  290. }
  291. class TestClassRunner {
  292. static #nextUniqueId = 1;
  293. #uniqueId = 0;
  294. #theClass;
  295. #instance;
  296. #testCases;
  297. get testCases() { return this.#testCases; }
  298. constructor(theClass) {
  299. this.#theClass = theClass;
  300. this.#instance = new theClass();
  301. this.#testCases = TestCaseRunner.findTestCases(this.#instance);
  302. this.#uniqueId = TestClassRunner.#nextUniqueId++;
  303. }
  304. get #cssId() { return `testclass${this.#uniqueId}`; }
  305. #summaryHTML() {
  306. var anyTesting = false;
  307. var anyFailed = false;
  308. var anyErrored = false;
  309. var anyUntested = false;
  310. var anyPassed = false;
  311. var allPassed = true;
  312. for (const test of this.testCases) {
  313. switch (test.result) {
  314. case ResultType.untested:
  315. anyUntested = true;
  316. allPassed = false;
  317. break;
  318. case ResultType.testing:
  319. anyTesting = true;
  320. allPassed = false;
  321. break;
  322. case ResultType.passed:
  323. anyPassed = true;
  324. break;
  325. case ResultType.failed:
  326. anyFailed = true;
  327. allPassed = false;
  328. break;
  329. case ResultType.errored:
  330. anyErrored = true;
  331. allPassed = false;
  332. break;
  333. }
  334. }
  335. var html = '';
  336. html += `<summary class="testclasssummary" id="${this.#cssId}summary">`;
  337. html += `<span class="testclassname">${this.#theClass.name}</span> `;
  338. if (anyTesting || (anyUntested && (anyPassed || anyFailed || anyErrored))) {
  339. html += '<span class="testclassstatus testing">Testing...</span>';
  340. } else if (anyErrored) {
  341. html += '<span class="testclassstatus errored">Errored</span>';
  342. } else if (anyFailed) {
  343. html += '<span class="testclassstatus failed">Failed</span>';
  344. } else if (allPassed) {
  345. html += '<span class="testclassstatus passed">Passed</span>';
  346. }
  347. html += '</summary>';
  348. return html;
  349. }
  350. toHTML() {
  351. var html = '';
  352. html += `<div class="testclass" id="${this.#cssId}">`;
  353. html += `<details id="${this.#cssId}details">`;
  354. html += this.#summaryHTML();
  355. for (const testCase of this.#testCases) {
  356. html += testCase.toHTML();
  357. }
  358. html += '</details>';
  359. html += '</div>';
  360. return html;
  361. }
  362. updateHTML() {
  363. var existing = document.getElementById(`${this.#cssId}summary`);
  364. if (!existing) {
  365. document.getElementById('results').innerHTML += this.toHTML();
  366. } else {
  367. existing.outerHTML = this.#summaryHTML();
  368. var allPassed = true;
  369. for (const test of this.testCases) {
  370. if (test.result != ResultType.passed) {
  371. allPassed = false;
  372. break;
  373. }
  374. }
  375. document.getElementById(`${this.#cssId}details`).open = !allPassed;
  376. }
  377. }
  378. static runAll(testClasses) {
  379. var tests = []; // tuples of TestClassRunner and TestCaseRunner
  380. for (const testClass of testClasses) {
  381. const classRunner = new TestClassRunner(testClass);
  382. classRunner.updateHTML();
  383. tests.push(...classRunner.testCases.map(function(test) { return [ classRunner, test ] }));
  384. }
  385. var testInterval = setInterval(function() {
  386. if (tests.length == 0) {
  387. clearInterval(testInterval);
  388. testInterval = null;
  389. return;
  390. }
  391. var classRunner;
  392. var testCase;
  393. [ classRunner, testCase ] = tests[0];
  394. if (testCase.result == ResultType.untested) {
  395. testCase.result = ResultType.testing;
  396. testCase.updateHTML();
  397. classRunner.updateHTML();
  398. } else if (testCase.result == ResultType.testing) {
  399. tests.splice(0, 1);
  400. testCase.run();
  401. testCase.updateHTML();
  402. classRunner.updateHTML();
  403. }
  404. }, 1);
  405. }
  406. }
  407. </script>
  408. <!-- Tests -->
  409. <script>
  410. function onLoad() {
  411. let testClasses = [
  412. TokenTests,
  413. UtilsTests,
  414. InlineTests,
  415. BlockTests,
  416. ];
  417. TestClassRunner.runAll(testClasses);
  418. }
  419. document.addEventListener('DOMContentLoaded', onLoad);
  420. function normalizeWhitespace(str) {
  421. return str.replace(/\s+/g, ' ').replace(/(?:^\s+|\s+$)/g, '');
  422. }
  423. class TokenTests extends BaseTest {
  424. test_findFirstTokens() {
  425. const tokens = [
  426. new MDToken('Lorem', MDTokenType.Text),
  427. new MDToken(' ', MDTokenType.Whitespace),
  428. new MDToken('_', MDTokenType.Underscore),
  429. new MDToken('ipsum', MDTokenType.Text),
  430. new MDToken('_', MDTokenType.Underscore),
  431. new MDToken(' ', MDTokenType.Whitespace),
  432. new MDToken('dolor', MDTokenType.Text),
  433. new MDToken('_', MDTokenType.Underscore),
  434. new MDToken('sit', MDTokenType.Text),
  435. new MDToken('_', MDTokenType.Underscore),
  436. ];
  437. const pattern = [
  438. MDTokenType.Underscore,
  439. ];
  440. const result = MDToken.findFirstTokens(tokens, pattern);
  441. const expected = {
  442. tokens: [ tokens[2] ],
  443. index: 2,
  444. };
  445. this.assertEqual(result, expected);
  446. }
  447. test_findFirstTokens_optionalWhitespace1() {
  448. const tokens = [
  449. new MDToken('Lorem', MDTokenType.Text),
  450. new MDToken(' ', MDTokenType.Whitespace),
  451. new MDToken('[ipsum]', MDTokenType.Label, 'ipsum'),
  452. new MDToken('(link.html)', MDTokenType.URL, 'link.html'),
  453. new MDToken(' ', MDTokenType.Whitespace),
  454. new MDToken('dolor', MDTokenType.Text),
  455. ];
  456. const pattern = [
  457. MDTokenType.Label,
  458. MDTokenType.META_OptionalWhitespace,
  459. MDTokenType.URL,
  460. ];
  461. const result = MDToken.findFirstTokens(tokens, pattern);
  462. const expected = {
  463. tokens: [ tokens[2], tokens[3] ],
  464. index: 2,
  465. };
  466. this.assertEqual(result, expected);
  467. }
  468. test_findFirstTokens_optionalWhitespace2() {
  469. const tokens = [
  470. new MDToken('Lorem', MDTokenType.Text),
  471. new MDToken(' ', MDTokenType.Whitespace),
  472. new MDToken('[ipsum]', MDTokenType.Label, 'ipsum'),
  473. new MDToken(' ', MDTokenType.Whitespace),
  474. new MDToken('(link.html)', MDTokenType.URL, 'link.html'),
  475. new MDToken(' ', MDTokenType.Whitespace),
  476. new MDToken('dolor', MDTokenType.Text),
  477. ];
  478. const pattern = [
  479. MDTokenType.Label,
  480. MDTokenType.META_OptionalWhitespace,
  481. MDTokenType.URL,
  482. ];
  483. const result = MDToken.findFirstTokens(tokens, pattern);
  484. const expected = {
  485. tokens: [ tokens[2], tokens[3], tokens[4] ],
  486. index: 2,
  487. };
  488. this.assertEqual(result, expected);
  489. }
  490. test_findPairedTokens() {
  491. const tokens = [
  492. new MDToken('Lorem', MDTokenType.Text),
  493. new MDToken(' ', MDTokenType.Whitespace),
  494. new MDToken('_', MDTokenType.Underscore),
  495. new MDToken('ipsum', MDTokenType.Text),
  496. new MDToken('_', MDTokenType.Underscore),
  497. new MDToken(' ', MDTokenType.Whitespace),
  498. new MDToken('dolor', MDTokenType.Text),
  499. new MDToken('_', MDTokenType.Underscore),
  500. new MDToken('sit', MDTokenType.Text),
  501. new MDToken('_', MDTokenType.Underscore),
  502. ];
  503. const pattern = [
  504. MDTokenType.Underscore,
  505. ];
  506. const result = MDToken.findPairedTokens(tokens, pattern, pattern);
  507. const expected = {
  508. startTokens: [ tokens[2] ],
  509. contentTokens: [ tokens[3] ],
  510. endTokens: [ tokens[4] ],
  511. startIndex: 2,
  512. contentIndex: 3,
  513. endIndex: 4,
  514. totalLength: 3,
  515. }
  516. this.assertEqual(result, expected);
  517. }
  518. }
  519. class UtilsTests extends BaseTest {
  520. test_stripIndent() {
  521. this.assertEqual(MDUtils.stripIndent(''), '');
  522. this.assertEqual(MDUtils.stripIndent(' '), '');
  523. this.assertEqual(MDUtils.stripIndent('foo'), 'foo');
  524. this.assertEqual(MDUtils.stripIndent(' foo'), 'foo');
  525. this.assertEqual(MDUtils.stripIndent(' foo'), 'foo');
  526. this.assertEqual(MDUtils.stripIndent(' foo'), 'foo');
  527. this.assertEqual(MDUtils.stripIndent(' foo'), 'foo');
  528. this.assertEqual(MDUtils.stripIndent(' foo'), ' foo');
  529. this.assertEqual(MDUtils.stripIndent('\tfoo'), 'foo');
  530. this.assertEqual(MDUtils.stripIndent('\t\tfoo'), '\tfoo');
  531. this.assertEqual(MDUtils.stripIndent('\t\tfoo', 2), 'foo');
  532. this.assertEqual(MDUtils.stripIndent(' foo', 2), 'foo');
  533. }
  534. test_countIndents() {
  535. this.assertEqual(MDUtils.countIndents(''), 0);
  536. this.assertEqual(MDUtils.countIndents(' '), 1);
  537. this.assertEqual(MDUtils.countIndents(' '), 1);
  538. this.assertEqual(MDUtils.countIndents('foo'), 0);
  539. this.assertEqual(MDUtils.countIndents('foo'), 0);
  540. this.assertEqual(MDUtils.countIndents(' foo'), 1);
  541. this.assertEqual(MDUtils.countIndents(' foo'), 1);
  542. this.assertEqual(MDUtils.countIndents(' foo'), 1);
  543. this.assertEqual(MDUtils.countIndents(' foo'), 1);
  544. this.assertEqual(MDUtils.countIndents(' foo'), 2);
  545. this.assertEqual(MDUtils.countIndents('\tfoo'), 1);
  546. this.assertEqual(MDUtils.countIndents('\t\tfoo'), 2);
  547. this.assertEqual(MDUtils.countIndents('', true), 0);
  548. this.assertEqual(MDUtils.countIndents(' ', true), 0);
  549. this.assertEqual(MDUtils.countIndents(' ', true), 1);
  550. this.assertEqual(MDUtils.countIndents('foo', true), 0);
  551. this.assertEqual(MDUtils.countIndents(' foo', true), 0);
  552. this.assertEqual(MDUtils.countIndents(' foo', true), 0);
  553. this.assertEqual(MDUtils.countIndents(' foo', true), 0);
  554. this.assertEqual(MDUtils.countIndents(' foo', true), 1);
  555. this.assertEqual(MDUtils.countIndents(' foo', true), 1);
  556. this.assertEqual(MDUtils.countIndents('\tfoo', true), 1);
  557. this.assertEqual(MDUtils.countIndents('\t\tfoo', true), 2);
  558. }
  559. test_tokenizeLabel() {
  560. // Escapes are preserved
  561. this.assertEqual(MDUtils.tokenizeLabel('[foo] bar'), [ '[foo]', 'foo' ]);
  562. this.assertEqual(MDUtils.tokenizeLabel('[foo\\[] bar'), [ '[foo\\[]', 'foo\\[' ]);
  563. this.assertEqual(MDUtils.tokenizeLabel('[foo\\]] bar'), [ '[foo\\]]', 'foo\\]' ]);
  564. this.assertEqual(MDUtils.tokenizeLabel('[foo[]] bar'), [ '[foo[]]', 'foo[]' ]);
  565. this.assertEqual(MDUtils.tokenizeLabel('[foo\\(] bar'), [ '[foo\\(]', 'foo\\(' ]);
  566. this.assertEqual(MDUtils.tokenizeLabel('[foo\\)] bar'), [ '[foo\\)]', 'foo\\)' ]);
  567. this.assertEqual(MDUtils.tokenizeLabel('[foo()] bar'), [ '[foo()]', 'foo()' ]);
  568. this.assertEqual(MDUtils.tokenizeLabel('foo bar'), null);
  569. this.assertEqual(MDUtils.tokenizeLabel('[foo\\] bar'), null);
  570. this.assertEqual(MDUtils.tokenizeLabel('[foo bar'), null);
  571. this.assertEqual(MDUtils.tokenizeLabel('[foo[] bar'), null);
  572. }
  573. test_tokenizeURL() {
  574. this.assertEqual(MDUtils.tokenizeURL('(page.html) foo'), [ '(page.html)', 'page.html', null ]);
  575. this.assertEqual(MDUtils.tokenizeURL('(page.html "link title") foo'), [ '(page.html "link title")', 'page.html', 'link title' ]);
  576. 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 ]);
  577. this.assertEqual(MDUtils.tokenizeURL('page.html foo'), null);
  578. this.assertEqual(MDUtils.tokenizeURL('(page.html foo'), null);
  579. this.assertEqual(MDUtils.tokenizeURL('page.html) foo'), null);
  580. this.assertEqual(MDUtils.tokenizeURL('(page.html "title) foo'), null);
  581. this.assertEqual(MDUtils.tokenizeURL('(page .html) foo'), null);
  582. this.assertEqual(MDUtils.tokenizeURL('(user@example.com) foo'), null);
  583. this.assertEqual(MDUtils.tokenizeURL('(user@example.com "title") foo'), null);
  584. }
  585. test_tokenizeEmail() {
  586. this.assertEqual(MDUtils.tokenizeEmail('(user@example.com)'), [ '(user@example.com)', 'user@example.com', null ]);
  587. this.assertEqual(MDUtils.tokenizeEmail('(user@example.com "link title")'), [ '(user@example.com "link title")', 'user@example.com', 'link title' ]);
  588. this.assertEqual(MDUtils.tokenizeEmail('(https://example.com) foo'), null);
  589. this.assertEqual(MDUtils.tokenizeEmail('(https://example.com "link title") foo'), null);
  590. this.assertEqual(MDUtils.tokenizeEmail('(user@example.com "link title) foo'), null);
  591. this.assertEqual(MDUtils.tokenizeEmail('(user@example.com foo'), null);
  592. this.assertEqual(MDUtils.tokenizeEmail('user@example.com) foo'), null);
  593. }
  594. }
  595. class InlineTests extends BaseTest {
  596. /** @type {Markdown} */
  597. parser;
  598. md(markdown) {
  599. return normalizeWhitespace(this.parser.toHTML(markdown));
  600. }
  601. setUp() {
  602. this.parser = Markdown.completeParser;
  603. }
  604. test_simpleText() {
  605. let markdown = 'Lorem ipsum';
  606. let expected = 'Lorem ipsum';
  607. let actual = this.md(markdown);
  608. this.assertEqual(actual, expected);
  609. }
  610. test_strong() {
  611. let markdown = 'Lorem **ipsum** dolor **sit**';
  612. let expected = 'Lorem <strong>ipsum</strong> dolor <strong>sit</strong>';
  613. let actual = this.md(markdown);
  614. this.assertEqual(actual, expected);
  615. }
  616. test_emphasis() {
  617. let markdown = 'Lorem _ipsum_ dolor _sit_';
  618. let expected = 'Lorem <em>ipsum</em> dolor <em>sit</em>';
  619. let actual = this.md(markdown);
  620. this.assertEqual(actual, expected);
  621. }
  622. test_strongEmphasis_cleanNesting1() {
  623. let markdown = 'Lorem **ipsum *dolor* sit** amet';
  624. let expected = 'Lorem <strong>ipsum <em>dolor</em> sit</strong> amet';
  625. let actual = this.md(markdown);
  626. this.assertEqual(actual, expected);
  627. }
  628. test_strongEmphasis_cleanNesting2() {
  629. let markdown = 'Lorem *ipsum **dolor** sit* amet';
  630. let expected = 'Lorem <em>ipsum <strong>dolor</strong> sit</em> amet';
  631. let actual = this.md(markdown);
  632. this.assertEqual(actual, expected);
  633. }
  634. test_strongEmphasis_tightNesting() {
  635. let markdown = 'Lorem ***ipsum*** dolor';
  636. let expected1 = 'Lorem <strong><em>ipsum</em></strong> dolor';
  637. let expected2 = 'Lorem <em><strong>ipsum</strong></em> dolor';
  638. let actual = this.md(markdown);
  639. this.assertTrue(actual == expected1 || actual == expected2);
  640. }
  641. test_strongEmphasis_lopsidedNesting1() {
  642. let markdown = 'Lorem ***ipsum* dolor** sit';
  643. let expected = 'Lorem <strong><em>ipsum</em> dolor</strong> sit';
  644. let actual = this.md(markdown);
  645. this.assertEqual(actual, expected);
  646. }
  647. test_strongEmphasis_lopsidedNesting2() {
  648. let markdown = 'Lorem ***ipsum** dolor* sit';
  649. let expected = 'Lorem <em><strong>ipsum</strong> dolor</em> sit';
  650. let actual = this.md(markdown);
  651. this.assertEqual(actual, expected);
  652. }
  653. test_strongEmphasis_lopsidedNesting3() {
  654. let markdown = 'Lorem **ipsum *dolor*** sit';
  655. let expected = 'Lorem <strong>ipsum <em>dolor</em></strong> sit';
  656. let actual = this.md(markdown);
  657. this.assertEqual(actual, expected);
  658. }
  659. test_strongEmphasis_lopsidedNesting4() {
  660. let markdown = 'Lorem *ipsum **dolor*** sit';
  661. let expected = 'Lorem <em>ipsum <strong>dolor</strong></em> sit';
  662. let actual = this.md(markdown);
  663. this.assertEqual(actual, expected);
  664. }
  665. test_inlineCode() {
  666. let markdown = 'Lorem `ipsum` dolor';
  667. let expected = 'Lorem <code>ipsum</code> dolor';
  668. let actual = this.md(markdown);
  669. this.assertEqual(actual, expected);
  670. }
  671. test_inlineCode_withInnerBacktick() {
  672. let markdown = 'Lorem ``ip`su`m`` dolor';
  673. let expected = 'Lorem <code>ip`su`m</code> dolor';
  674. let actual = this.md(markdown);
  675. this.assertEqual(actual, expected);
  676. }
  677. test_strikethrough_single() {
  678. let markdown = 'Lorem ~ipsum~ dolor';
  679. let expected = 'Lorem <strike>ipsum</strike> dolor';
  680. let actual = this.md(markdown);
  681. this.assertEqual(actual, expected);
  682. }
  683. test_strikethrough_double() {
  684. let markdown = 'Lorem ~~ipsum~~ dolor';
  685. let expected = 'Lorem <strike>ipsum</strike> dolor';
  686. let actual = this.md(markdown);
  687. this.assertEqual(actual, expected);
  688. }
  689. test_link_fullyQualified() {
  690. let markdown = 'Lorem [ipsum](https://example.com/path/page.html) dolor';
  691. let expected = 'Lorem <a href="https://example.com/path/page.html">ipsum</a> dolor';
  692. let actual = this.md(markdown);
  693. this.assertEqual(actual, expected);
  694. }
  695. test_link_relative() {
  696. let markdown = 'Lorem [ipsum](page.html) dolor';
  697. let expected = 'Lorem <a href="page.html">ipsum</a> dolor';
  698. let actual = this.md(markdown);
  699. this.assertEqual(actual, expected);
  700. }
  701. test_link_title() {
  702. let markdown = 'Lorem [ipsum](page.html "link title") dolor';
  703. let expected = 'Lorem <a href="page.html" title="link title">ipsum</a> dolor';
  704. let actual = this.md(markdown);
  705. this.assertEqual(actual, expected);
  706. }
  707. test_link_literal() {
  708. let markdown = 'Lorem <https://example.com> dolor';
  709. let expected = 'Lorem <a href="https://example.com">https://example.com</a> dolor';
  710. let actual = this.md(markdown);
  711. this.assertEqual(actual, expected);
  712. }
  713. test_link_ref() {
  714. let markdown = "Lorem [ipsum][ref] dolor\n\n[ref]: https://example.com";
  715. let expected = '<p>Lorem <a href="https://example.com">ipsum</a> dolor</p>';
  716. let actual = this.md(markdown);
  717. this.assertEqual(actual, expected);
  718. }
  719. test_link_email() {
  720. let markdown = 'Lorem [ipsum](user@example.com) dolor';
  721. 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';
  722. let actual = this.md(markdown);
  723. this.assertEqual(actual, expected);
  724. }
  725. test_link_email_withTitle() {
  726. let markdown = 'Lorem [ipsum](user@example.com "title") dolor';
  727. 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';
  728. let actual = this.md(markdown);
  729. this.assertEqual(actual, expected);
  730. }
  731. test_link_literalEmail() {
  732. let markdown = 'Lorem <user@example.com> dolor';
  733. 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';
  734. let actual = this.md(markdown);
  735. this.assertEqual(actual, expected);
  736. }
  737. test_link_image() {
  738. let markdown = 'Lorem [![alt](image.jpg)](page.html) ipsum';
  739. let expected = 'Lorem <a href="page.html"><img src="image.jpg" alt="alt"></a> ipsum';
  740. let actual = this.md(markdown);
  741. this.assertEqual(actual, expected);
  742. }
  743. test_link_image_complex() {
  744. let markdown = 'Lorem [![alt] (image.jpg "image title")] (page.html "link title") ipsum';
  745. let expected = 'Lorem <a href="page.html" title="link title"><img src="image.jpg" alt="alt" title="image title"></a> ipsum';
  746. let actual = this.md(markdown);
  747. this.assertEqual(actual, expected);
  748. }
  749. test_image() {
  750. let markdown = 'Lorem ![alt text](image.jpg) dolor';
  751. let expected = 'Lorem <img src="image.jpg" alt="alt text"> dolor';
  752. let actual = this.md(markdown);
  753. this.assertEqual(actual, expected);
  754. }
  755. test_image_noAlt() {
  756. let markdown = 'Lorem ![](image.jpg) dolor';
  757. let expected = 'Lorem <img src="image.jpg"> dolor';
  758. let actual = this.md(markdown);
  759. this.assertEqual(actual, expected);
  760. }
  761. test_image_withTitle() {
  762. let markdown = 'Lorem ![alt text](image.jpg "image title") dolor';
  763. let expected = 'Lorem <img src="image.jpg" alt="alt text" title="image title"> dolor';
  764. let actual = this.md(markdown);
  765. this.assertEqual(actual, expected);
  766. }
  767. test_image_ref() {
  768. let markdown = 'Lorem ![alt text][ref] dolor\n\n' +
  769. '[ref]: image.jpg "image title"';
  770. let expected = '<p>Lorem <img src="image.jpg" alt="alt text" title="image title"> dolor</p>';
  771. let actual = this.md(markdown);
  772. this.assertEqual(actual, expected);
  773. }
  774. test_htmlTags() {
  775. let markdown = 'Lorem <strong title="value" foo=\'with " quote\' bar="with \' apostrophe" attr=unquoted checked>ipsum</strong> dolor';
  776. let expected = markdown;
  777. let actual = this.md(markdown);
  778. this.assertEqual(actual, expected);
  779. }
  780. }
  781. class BlockTests extends BaseTest {
  782. /** @type {Markdown} */
  783. parser;
  784. md(markdown) {
  785. return normalizeWhitespace(this.parser.toHTML(markdown));
  786. }
  787. setUp() {
  788. this.parser = Markdown.completeParser;
  789. }
  790. test_paragraphs() {
  791. let markdown = "Lorem ipsum\n\nDolor sit amet";
  792. let expected = "<p>Lorem ipsum</p> <p>Dolor sit amet</p>";
  793. let actual = this.md(markdown);
  794. this.assertEqual(actual, expected);
  795. }
  796. test_paragraph_lineGrouping() {
  797. let markdown = "Lorem ipsum\ndolor sit amet";
  798. let expected = "Lorem ipsum dolor sit amet";
  799. let actual = this.md(markdown);
  800. this.assertEqual(actual, expected);
  801. }
  802. test_header_underlineH1() {
  803. let markdown = "Header 1\n===\n\nLorem ipsum";
  804. let expected = "<h1>Header 1</h1> <p>Lorem ipsum</p>";
  805. let actual = this.md(markdown);
  806. this.assertEqual(actual, expected);
  807. }
  808. test_header_underlineH2() {
  809. let markdown = "Header 2\n---\n\nLorem ipsum";
  810. let expected = "<h2>Header 2</h2> <p>Lorem ipsum</p>";
  811. let actual = this.md(markdown);
  812. this.assertEqual(actual, expected);
  813. }
  814. test_header_hash() {
  815. let markdown = "# Header 1\n## Header 2\n### Header 3\n#### Header 4\n##### Header 5\n###### Header 6\n";
  816. 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>';
  817. let actual = this.md(markdown);
  818. this.assertEqual(actual, expected);
  819. }
  820. test_header_hash_trailing() {
  821. let markdown = "# Header 1 #\n## Header 2 ##\n### Header 3 ######";
  822. let expected = '<h1>Header 1</h1> <h2>Header 2</h2> <h3>Header 3</h3>';
  823. let actual = this.md(markdown);
  824. this.assertEqual(actual, expected);
  825. }
  826. test_unorderedList() {
  827. let markdown = "* Lorem\n* Ipsum\n* Dolor";
  828. let expected = '<ul> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ul>';
  829. let actual = this.md(markdown);
  830. this.assertEqual(actual, expected);
  831. }
  832. test_unorderedList_nested() {
  833. let markdown = "* Lorem\n + Ipsum\n* Dolor";
  834. let expected = '<ul> <li>Lorem <ul> <li>Ipsum</li> </ul></li> <li>Dolor</li> </ul>';
  835. let actual = this.md(markdown);
  836. this.assertEqual(actual, expected);
  837. }
  838. test_unorderedList_hitch() {
  839. // This incomplete bulleted list locked up the browser at one
  840. // point, not forever but a REALLY long time
  841. this.profile(1.0, () => {
  842. let markdown = "Testing\n\n* ";
  843. let expected = '<p>Testing</p> <ul> <li></li> </ul>';
  844. let actual = this.md(markdown);
  845. this.assertEqual(actual, expected);
  846. });
  847. }
  848. test_orderedList() {
  849. let markdown = "1. Lorem\n1. Ipsum\n5. Dolor";
  850. let expected = '<ol> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ol>';
  851. let actual = this.md(markdown);
  852. this.assertEqual(actual, expected);
  853. }
  854. test_orderedList_numbering() {
  855. let markdown = "4. Lorem\n1. Ipsum\n9. Dolor";
  856. let expected = '<ol start="4"> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ol>';
  857. let actual = this.md(markdown);
  858. this.assertEqual(actual, expected);
  859. }
  860. test_orderedList_nested1() {
  861. let markdown = "1. Lorem\n 1. Ipsum\n1. Dolor";
  862. let expected = '<ol> <li>Lorem <ol> <li>Ipsum</li> </ol></li> <li>Dolor</li> </ol>';
  863. let actual = this.md(markdown);
  864. this.assertEqual(actual, expected);
  865. }
  866. test_orderedList_nested2() {
  867. let markdown = "1. Lorem\n 1. Ipsum\n 1. Dolor\n 1. Sit\n1. Amet";
  868. let expected = '<ol> <li>Lorem <ol> <li>Ipsum <ol> <li>Dolor</li> </ol></li> <li>Sit</li> </ol></li> <li>Amet</li> </ol>';
  869. let actual = this.md(markdown);
  870. this.assertEqual(actual, expected);
  871. }
  872. test_blockquote() {
  873. let markdown = '> Lorem ipsum dolor';
  874. let expected = '<blockquote> Lorem ipsum dolor </blockquote>';
  875. let actual = this.md(markdown);
  876. this.assertEqual(actual, expected);
  877. }
  878. test_blockquote_paragraphs() {
  879. let markdown = '> Lorem ipsum dolor\n>\n>Sit amet';
  880. let expected = '<blockquote> <p>Lorem ipsum dolor</p> <p>Sit amet</p> </blockquote>';
  881. let actual = this.md(markdown);
  882. this.assertEqual(actual, expected);
  883. }
  884. test_blockquote_list() {
  885. let markdown = '> 1. Lorem\n> 2. Ipsum';
  886. let expected = '<blockquote> <ol> <li>Lorem</li> <li>Ipsum</li> </ol> </blockquote>';
  887. let actual = this.md(markdown);
  888. this.assertEqual(actual, expected);
  889. }
  890. test_codeBlock_indented() {
  891. let markdown = "Code\n\n function foo() {\n return 'bar';\n }\n\nend";
  892. let expected = "<p>Code</p>\n\n<pre><code>function foo() {\n return 'bar';\n}</code></pre>\n<p>end</p>\n";
  893. let actual = this.parser.toHTML(markdown); // don't normalize whitespace
  894. this.assertEqual(actual.replace(/ /g, '⎵'), expected.replace(/ /g, '⎵'));
  895. }
  896. test_codeBlock_fenced() {
  897. let markdown = "Code\n\n```\nfunction foo() {\n return 'bar';\n}\n```\n\nend";
  898. let expected = "<p>Code</p>\n\n<pre><code>function foo() {\n return 'bar';\n}</code></pre>\n<p>end</p>\n";
  899. let actual = this.parser.toHTML(markdown); // don't normalize whitespace
  900. this.assertEqual(actual.replace(/ /g, '⎵'), expected.replace(/ /g, '⎵'));
  901. }
  902. test_horizontalRule() {
  903. let markdown = "Before\n\n---\n\n- - -\n\n***\n\n* * * * * * *\n\nafter";
  904. let expected = "<p>Before</p> <hr> <hr> <hr> <hr> <p>after</p>";
  905. let actual = this.md(markdown);
  906. this.assertEqual(actual, expected);
  907. }
  908. test_table_unfenced() {
  909. let markdown = "Column A | Column B | Column C\n--- | --- | ---\n1 | 2 | 3\n4 | 5 | 6";
  910. 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>";
  911. let actual = this.md(markdown);
  912. this.assertEqual(actual, expected);
  913. }
  914. test_table_fenced() {
  915. let markdown = "| Column A | Column B | Column C |\n| --- | --- | --- |\n| 1 | 2 | 3\n4 | 5 | 6 |";
  916. 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>";
  917. let actual = this.md(markdown);
  918. this.assertEqual(actual, expected);
  919. }
  920. test_table_alignment() {
  921. let markdown = 'Column A | Column B | Column C\n' +
  922. ':--- | :---: | ---:\n' +
  923. '1 | 2 | 3\n' +
  924. '4 | 5 | 6';
  925. let expected = '<table> ' +
  926. '<thead> ' +
  927. '<tr> ' +
  928. '<th align="left">Column A</th> ' +
  929. '<th align="center">Column B</th> ' +
  930. '<th align="right">Column C</th> ' +
  931. '</tr> ' +
  932. '</thead> ' +
  933. '<tbody> ' +
  934. '<tr> ' +
  935. '<td align="left">1</td> ' +
  936. '<td align="center">2</td> ' +
  937. '<td align="right">3</td> ' +
  938. '</tr> ' +
  939. '<tr> ' +
  940. '<td align="left">4</td> ' +
  941. '<td align="center">5</td> ' +
  942. '<td align="right">6</td> ' +
  943. '</tr> ' +
  944. '</tbody> ' +
  945. '</table>';
  946. let actual = this.md(markdown);
  947. this.assertEqual(actual, expected);
  948. }
  949. test_table_holes() {
  950. let markdown = 'Column A||Column C\n' +
  951. '---|---|---\n' +
  952. '|1|2||\n' +
  953. '|4||6|\n' +
  954. '||8|9|';
  955. let expected = '<table> ' +
  956. '<thead> ' +
  957. '<tr> ' +
  958. '<th>Column A</th> ' +
  959. '<th></th> ' +
  960. '<th>Column C</th> ' +
  961. '</tr> ' +
  962. '</thead> ' +
  963. '<tbody> ' +
  964. '<tr> ' +
  965. '<td>1</td> ' +
  966. '<td>2</td> ' +
  967. '<td></td> ' +
  968. '</tr> ' +
  969. '<tr> ' +
  970. '<td>4</td> ' +
  971. '<td></td> ' +
  972. '<td>6</td> ' +
  973. '</tr> ' +
  974. '<tr> ' +
  975. '<td></td> ' +
  976. '<td>8</td> ' +
  977. '<td>9</td> ' +
  978. '</tr> ' +
  979. '</tbody> ' +
  980. '</table>';
  981. let actual = this.md(markdown);
  982. this.assertEqual(actual, expected);
  983. }
  984. test_definitionList() {
  985. let markdown = 'term\n' +
  986. ': definition\n' +
  987. 'another' +
  988. ' term\n' +
  989. ': def 1\n' +
  990. ' broken on next line\n' +
  991. ': def 2';
  992. let expected = '<dl> ' +
  993. '<dt>term</dt> ' +
  994. '<dd>definition</dd> ' +
  995. '<dt>another term</dt> ' +
  996. '<dd>def 1 broken on next line</dd> ' +
  997. '<dd>def 2</dd> ' +
  998. '</dl>';
  999. let actual = this.md(markdown);
  1000. this.assertEqual(actual, expected);
  1001. }
  1002. test_footnotes() {
  1003. let markdown = 'Lorem ipsum[^1] dolor[^2] sit[^1] amet\n\n[^1]: A footnote\n[^2]: Another footnote';
  1004. let expected = '<p>Lorem ipsum<sup id="footnoteref_1"><a href="#footnote_1">1</a></sup> ' +
  1005. 'dolor<sup id="footnoteref_2"><a href="#footnote_2">2</a></sup> ' +
  1006. 'sit<sup id="footnoteref_3"><a href="#footnote_1">1</a></sup> amet</p> ' +
  1007. '<div class="footnotes">' +
  1008. '<hr/>' +
  1009. '<ol>' +
  1010. '<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> ' +
  1011. '<li value="2" id="footnote_2">Another footnote <a href="#footnoteref_2" class="footnote-backref">↩︎</a></li> ' +
  1012. '</ol>' +
  1013. '</div>';
  1014. let actual = this.md(markdown);
  1015. this.assertEqual(actual, expected);
  1016. }
  1017. test_abbreviations() {
  1018. let markdown = 'Lorem ipsum HTML dolor HTML sit\n' +
  1019. '\n' +
  1020. '*[HTML]: Hypertext Markup Language';
  1021. let expected = '<p>Lorem ipsum <abbr title="Hypertext Markup Language">HTML</abbr> dolor <abbr title="Hypertext Markup Language">HTML</abbr> sit</p>';
  1022. let actual = this.md(markdown);
  1023. this.assertEqual(actual, expected);
  1024. }
  1025. }
  1026. </script>
  1027. </head>
  1028. <body>
  1029. <div id="results"></div>
  1030. </body>
  1031. </html>