PHP and Javascript implementations of a simple markdown parser
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107
  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_orderedList() {
  839. let markdown = "1. Lorem\n1. Ipsum\n5. Dolor";
  840. let expected = '<ol> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ol>';
  841. let actual = this.md(markdown);
  842. this.assertEqual(actual, expected);
  843. }
  844. test_orderedList_numbering() {
  845. let markdown = "4. Lorem\n1. Ipsum\n9. Dolor";
  846. let expected = '<ol start="4"> <li>Lorem</li> <li>Ipsum</li> <li>Dolor</li> </ol>';
  847. let actual = this.md(markdown);
  848. this.assertEqual(actual, expected);
  849. }
  850. test_orderedList_nested1() {
  851. let markdown = "1. Lorem\n 1. Ipsum\n1. Dolor";
  852. let expected = '<ol> <li>Lorem <ol> <li>Ipsum</li> </ol></li> <li>Dolor</li> </ol>';
  853. let actual = this.md(markdown);
  854. this.assertEqual(actual, expected);
  855. }
  856. test_orderedList_nested2() {
  857. let markdown = "1. Lorem\n 1. Ipsum\n 1. Dolor\n 1. Sit\n1. Amet";
  858. let expected = '<ol> <li>Lorem <ol> <li>Ipsum <ol> <li>Dolor</li> </ol></li> <li>Sit</li> </ol></li> <li>Amet</li> </ol>';
  859. let actual = this.md(markdown);
  860. this.assertEqual(actual, expected);
  861. }
  862. test_blockquote() {
  863. let markdown = '> Lorem ipsum dolor';
  864. let expected = '<blockquote> Lorem ipsum dolor </blockquote>';
  865. let actual = this.md(markdown);
  866. this.assertEqual(actual, expected);
  867. }
  868. test_blockquote_paragraphs() {
  869. let markdown = '> Lorem ipsum dolor\n>\n>Sit amet';
  870. let expected = '<blockquote> <p>Lorem ipsum dolor</p> <p>Sit amet</p> </blockquote>';
  871. let actual = this.md(markdown);
  872. this.assertEqual(actual, expected);
  873. }
  874. test_blockquote_list() {
  875. let markdown = '> 1. Lorem\n> 2. Ipsum';
  876. let expected = '<blockquote> <ol> <li>Lorem</li> <li>Ipsum</li> </ol> </blockquote>';
  877. let actual = this.md(markdown);
  878. this.assertEqual(actual, expected);
  879. }
  880. test_codeBlock_indented() {
  881. let markdown = "Code\n\n function foo() {\n return 'bar';\n }\n\nend";
  882. let expected = "<p>Code</p>\n\n<pre><code>function foo() {\n return 'bar';\n}</code></pre>\n<p>end</p>\n";
  883. let actual = this.parser.toHTML(markdown); // don't normalize whitespace
  884. this.assertEqual(actual.replace(/ /g, '⎵'), expected.replace(/ /g, '⎵'));
  885. }
  886. test_codeBlock_fenced() {
  887. let markdown = "Code\n\n```\nfunction foo() {\n return 'bar';\n}\n```\n\nend";
  888. let expected = "<p>Code</p>\n\n<pre><code>function foo() {\n return 'bar';\n}</code></pre>\n<p>end</p>\n";
  889. let actual = this.parser.toHTML(markdown); // don't normalize whitespace
  890. this.assertEqual(actual.replace(/ /g, '⎵'), expected.replace(/ /g, '⎵'));
  891. }
  892. test_horizontalRule() {
  893. let markdown = "Before\n\n---\n\n- - -\n\n***\n\n* * * * * * *\n\nafter";
  894. let expected = "<p>Before</p> <hr> <hr> <hr> <hr> <p>after</p>";
  895. let actual = this.md(markdown);
  896. this.assertEqual(actual, expected);
  897. }
  898. test_table_unfenced() {
  899. let markdown = "Column A | Column B | Column C\n--- | --- | ---\n1 | 2 | 3\n4 | 5 | 6";
  900. 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>";
  901. let actual = this.md(markdown);
  902. this.assertEqual(actual, expected);
  903. }
  904. test_table_fenced() {
  905. let markdown = "| Column A | Column B | Column C |\n| --- | --- | --- |\n| 1 | 2 | 3\n4 | 5 | 6 |";
  906. 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>";
  907. let actual = this.md(markdown);
  908. this.assertEqual(actual, expected);
  909. }
  910. test_table_alignment() {
  911. let markdown = 'Column A | Column B | Column C\n' +
  912. ':--- | :---: | ---:\n' +
  913. '1 | 2 | 3\n' +
  914. '4 | 5 | 6';
  915. let expected = '<table> ' +
  916. '<thead> ' +
  917. '<tr> ' +
  918. '<th align="left">Column A</th> ' +
  919. '<th align="center">Column B</th> ' +
  920. '<th align="right">Column C</th> ' +
  921. '</tr> ' +
  922. '</thead> ' +
  923. '<tbody> ' +
  924. '<tr> ' +
  925. '<td align="left">1</td> ' +
  926. '<td align="center">2</td> ' +
  927. '<td align="right">3</td> ' +
  928. '</tr> ' +
  929. '<tr> ' +
  930. '<td align="left">4</td> ' +
  931. '<td align="center">5</td> ' +
  932. '<td align="right">6</td> ' +
  933. '</tr> ' +
  934. '</tbody> ' +
  935. '</table>';
  936. let actual = this.md(markdown);
  937. this.assertEqual(actual, expected);
  938. }
  939. test_table_holes() {
  940. let markdown = 'Column A||Column C\n' +
  941. '---|---|---\n' +
  942. '|1|2||\n' +
  943. '|4||6|\n' +
  944. '||8|9|';
  945. let expected = '<table> ' +
  946. '<thead> ' +
  947. '<tr> ' +
  948. '<th>Column A</th> ' +
  949. '<th></th> ' +
  950. '<th>Column C</th> ' +
  951. '</tr> ' +
  952. '</thead> ' +
  953. '<tbody> ' +
  954. '<tr> ' +
  955. '<td>1</td> ' +
  956. '<td>2</td> ' +
  957. '<td></td> ' +
  958. '</tr> ' +
  959. '<tr> ' +
  960. '<td>4</td> ' +
  961. '<td></td> ' +
  962. '<td>6</td> ' +
  963. '</tr> ' +
  964. '<tr> ' +
  965. '<td></td> ' +
  966. '<td>8</td> ' +
  967. '<td>9</td> ' +
  968. '</tr> ' +
  969. '</tbody> ' +
  970. '</table>';
  971. let actual = this.md(markdown);
  972. this.assertEqual(actual, expected);
  973. }
  974. test_definitionList() {
  975. let markdown = 'term\n' +
  976. ': definition\n' +
  977. 'another' +
  978. ' term\n' +
  979. ': def 1\n' +
  980. ' broken on next line\n' +
  981. ': def 2';
  982. let expected = '<dl> ' +
  983. '<dt>term</dt> ' +
  984. '<dd>definition</dd> ' +
  985. '<dt>another term</dt> ' +
  986. '<dd>def 1 broken on next line</dd> ' +
  987. '<dd>def 2</dd> ' +
  988. '</dl>';
  989. let actual = this.md(markdown);
  990. this.assertEqual(actual, expected);
  991. }
  992. test_footnotes() {
  993. let markdown = 'Lorem ipsum[^1] dolor[^2] sit[^1] amet\n\n[^1]: A footnote\n[^2]: Another footnote';
  994. let expected = '<p>Lorem ipsum<sup id="footnoteref_1"><a href="#footnote_1">1</a></sup> ' +
  995. 'dolor<sup id="footnoteref_2"><a href="#footnote_2">2</a></sup> ' +
  996. 'sit<sup id="footnoteref_3"><a href="#footnote_1">1</a></sup> amet</p> ' +
  997. '<div class="footnotes">' +
  998. '<hr/>' +
  999. '<ol>' +
  1000. '<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> ' +
  1001. '<li value="2" id="footnote_2">Another footnote <a href="#footnoteref_2" class="footnote-backref">↩︎</a></li> ' +
  1002. '</ol>' +
  1003. '</div>';
  1004. let actual = this.md(markdown);
  1005. this.assertEqual(actual, expected);
  1006. }
  1007. test_abbreviations() {
  1008. let markdown = 'Lorem ipsum HTML dolor HTML sit\n' +
  1009. '\n' +
  1010. '*[HTML]: Hypertext Markup Language';
  1011. let expected = '<p>Lorem ipsum <abbr title="Hypertext Markup Language">HTML</abbr> dolor <abbr title="Hypertext Markup Language">HTML</abbr> sit</p>';
  1012. let actual = this.md(markdown);
  1013. this.assertEqual(actual, expected);
  1014. }
  1015. }
  1016. </script>
  1017. </head>
  1018. <body>
  1019. <div id="results"></div>
  1020. </body>
  1021. </html>