compile.js 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  1. /**
  2. * While micromark is a lexer/tokenizer, the common case of going from markdown
  3. * to html is currently built in as this module, even though the parts can be
  4. * used separately to build ASTs, CSTs, or many other output formats.
  5. *
  6. * Having an HTML compiler built in is useful because it allows us to check for
  7. * compliancy to CommonMark, the de facto norm of markdown, specified in roughly
  8. * 600 input/output cases.
  9. *
  10. * This module has an interface that accepts lists of events instead of the
  11. * whole at once, however, because markdown can’t be truly streaming, we buffer
  12. * events before processing and outputting the final result.
  13. */
  14. /**
  15. * @import {
  16. * CompileContext,
  17. * CompileData,
  18. * CompileOptions,
  19. * Compile,
  20. * Definition,
  21. * Event,
  22. * Handle,
  23. * HtmlExtension,
  24. * LineEnding,
  25. * NormalizedHtmlExtension,
  26. * Token
  27. * } from 'micromark-util-types'
  28. */
  29. /**
  30. * @typedef Media
  31. * @property {boolean | undefined} [image]
  32. * @property {string | undefined} [labelId]
  33. * @property {string | undefined} [label]
  34. * @property {string | undefined} [referenceId]
  35. * @property {string | undefined} [destination]
  36. * @property {string | undefined} [title]
  37. */
  38. import { decodeNamedCharacterReference } from 'decode-named-character-reference';
  39. import { push } from 'micromark-util-chunked';
  40. import { combineHtmlExtensions } from 'micromark-util-combine-extensions';
  41. import { decodeNumericCharacterReference } from 'micromark-util-decode-numeric-character-reference';
  42. import { encode as _encode } from 'micromark-util-encode';
  43. import { normalizeIdentifier } from 'micromark-util-normalize-identifier';
  44. import { sanitizeUri } from 'micromark-util-sanitize-uri';
  45. const hasOwnProperty = {}.hasOwnProperty;
  46. /**
  47. * These two are allowlists of safe protocols for full URLs in respectively the
  48. * `href` (on `<a>`) and `src` (on `<img>`) attributes.
  49. * They are based on what is allowed on GitHub,
  50. * <https://github.com/syntax-tree/hast-util-sanitize/blob/9275b21/lib/github.json#L31>
  51. */
  52. const protocolHref = /^(https?|ircs?|mailto|xmpp)$/i;
  53. const protocolSource = /^https?$/i;
  54. /**
  55. * @param {CompileOptions | null | undefined} [options]
  56. * @returns {Compile}
  57. */
  58. export function compile(options) {
  59. const settings = options || {};
  60. /**
  61. * Tags is needed because according to markdown, links and emphasis and
  62. * whatnot can exist in images, however, as HTML doesn’t allow content in
  63. * images, the tags are ignored in the `alt` attribute, but the content
  64. * remains.
  65. *
  66. * @type {boolean | undefined}
  67. */
  68. let tags = true;
  69. /**
  70. * An object to track identifiers to media (URLs and titles) defined with
  71. * definitions.
  72. *
  73. * @type {Record<string, Definition>}
  74. */
  75. const definitions = {};
  76. /**
  77. * A lot of the handlers need to capture some of the output data, modify it
  78. * somehow, and then deal with it.
  79. * We do that by tracking a stack of buffers, that can be opened (with
  80. * `buffer`) and closed (with `resume`) to access them.
  81. *
  82. * @type {Array<Array<string>>}
  83. */
  84. const buffers = [[]];
  85. /**
  86. * As we can have links in images and the other way around, where the deepest
  87. * ones are closed first, we need to track which one we’re in.
  88. *
  89. * @type {Array<Media>}
  90. */
  91. const mediaStack = [];
  92. /**
  93. * Same as `mediaStack` for tightness, which is specific to lists.
  94. * We need to track if we’re currently in a tight or loose container.
  95. *
  96. * @type {Array<boolean>}
  97. */
  98. const tightStack = [];
  99. /** @type {HtmlExtension} */
  100. const defaultHandlers = {
  101. enter: {
  102. blockQuote: onenterblockquote,
  103. codeFenced: onentercodefenced,
  104. codeFencedFenceInfo: buffer,
  105. codeFencedFenceMeta: buffer,
  106. codeIndented: onentercodeindented,
  107. codeText: onentercodetext,
  108. content: onentercontent,
  109. definition: onenterdefinition,
  110. definitionDestinationString: onenterdefinitiondestinationstring,
  111. definitionLabelString: buffer,
  112. definitionTitleString: buffer,
  113. emphasis: onenteremphasis,
  114. htmlFlow: onenterhtmlflow,
  115. htmlText: onenterhtml,
  116. image: onenterimage,
  117. label: buffer,
  118. link: onenterlink,
  119. listItemMarker: onenterlistitemmarker,
  120. listItemValue: onenterlistitemvalue,
  121. listOrdered: onenterlistordered,
  122. listUnordered: onenterlistunordered,
  123. paragraph: onenterparagraph,
  124. reference: buffer,
  125. resource: onenterresource,
  126. resourceDestinationString: onenterresourcedestinationstring,
  127. resourceTitleString: buffer,
  128. setextHeading: onentersetextheading,
  129. strong: onenterstrong
  130. },
  131. exit: {
  132. atxHeading: onexitatxheading,
  133. atxHeadingSequence: onexitatxheadingsequence,
  134. autolinkEmail: onexitautolinkemail,
  135. autolinkProtocol: onexitautolinkprotocol,
  136. blockQuote: onexitblockquote,
  137. characterEscapeValue: onexitdata,
  138. characterReferenceMarkerHexadecimal: onexitcharacterreferencemarker,
  139. characterReferenceMarkerNumeric: onexitcharacterreferencemarker,
  140. characterReferenceValue: onexitcharacterreferencevalue,
  141. codeFenced: onexitflowcode,
  142. codeFencedFence: onexitcodefencedfence,
  143. codeFencedFenceInfo: onexitcodefencedfenceinfo,
  144. codeFencedFenceMeta: onresumedrop,
  145. codeFlowValue: onexitcodeflowvalue,
  146. codeIndented: onexitflowcode,
  147. codeText: onexitcodetext,
  148. codeTextData: onexitdata,
  149. data: onexitdata,
  150. definition: onexitdefinition,
  151. definitionDestinationString: onexitdefinitiondestinationstring,
  152. definitionLabelString: onexitdefinitionlabelstring,
  153. definitionTitleString: onexitdefinitiontitlestring,
  154. emphasis: onexitemphasis,
  155. hardBreakEscape: onexithardbreak,
  156. hardBreakTrailing: onexithardbreak,
  157. htmlFlow: onexithtml,
  158. htmlFlowData: onexitdata,
  159. htmlText: onexithtml,
  160. htmlTextData: onexitdata,
  161. image: onexitmedia,
  162. label: onexitlabel,
  163. labelText: onexitlabeltext,
  164. lineEnding: onexitlineending,
  165. link: onexitmedia,
  166. listOrdered: onexitlistordered,
  167. listUnordered: onexitlistunordered,
  168. paragraph: onexitparagraph,
  169. reference: onresumedrop,
  170. referenceString: onexitreferencestring,
  171. resource: onresumedrop,
  172. resourceDestinationString: onexitresourcedestinationstring,
  173. resourceTitleString: onexitresourcetitlestring,
  174. setextHeading: onexitsetextheading,
  175. setextHeadingLineSequence: onexitsetextheadinglinesequence,
  176. setextHeadingText: onexitsetextheadingtext,
  177. strong: onexitstrong,
  178. thematicBreak: onexitthematicbreak
  179. }
  180. };
  181. /**
  182. * Combine the HTML extensions with the default handlers.
  183. * An HTML extension is an object whose fields are either `enter` or `exit`
  184. * (reflecting whether a token is entered or exited).
  185. * The values at such objects are names of tokens mapping to handlers.
  186. * Handlers are called, respectively when a token is opener or closed, with
  187. * that token, and a context as `this`.
  188. */
  189. const handlers = /** @type {NormalizedHtmlExtension} */
  190. combineHtmlExtensions([defaultHandlers, ...(settings.htmlExtensions || [])]);
  191. /**
  192. * Handlers do often need to keep track of some state.
  193. * That state is provided here as a key-value store (an object).
  194. *
  195. * @type {CompileData}
  196. */
  197. const data = {
  198. definitions,
  199. tightStack
  200. };
  201. /**
  202. * The context for handlers references a couple of useful functions.
  203. * In handlers from extensions, those can be accessed at `this`.
  204. * For the handlers here, they can be accessed directly.
  205. *
  206. * @type {Omit<CompileContext, 'sliceSerialize'>}
  207. */
  208. const context = {
  209. buffer,
  210. encode,
  211. getData,
  212. lineEndingIfNeeded,
  213. options: settings,
  214. raw,
  215. resume,
  216. setData,
  217. tag
  218. };
  219. /**
  220. * Generally, micromark copies line endings (`'\r'`, `'\n'`, `'\r\n'`) in the
  221. * markdown document over to the compiled HTML.
  222. * In some cases, such as `> a`, CommonMark requires that extra line endings
  223. * are added: `<blockquote>\n<p>a</p>\n</blockquote>`.
  224. * This variable hold the default line ending when given (or `undefined`),
  225. * and in the latter case will be updated to the first found line ending if
  226. * there is one.
  227. */
  228. let lineEndingStyle = settings.defaultLineEnding;
  229. // Return the function that handles a slice of events.
  230. return compile;
  231. /**
  232. * Deal w/ a slice of events.
  233. * Return either the empty string if there’s nothing of note to return, or the
  234. * result when done.
  235. *
  236. * @param {ReadonlyArray<Event>} events
  237. * @returns {string}
  238. */
  239. function compile(events) {
  240. let index = -1;
  241. let start = 0;
  242. /** @type {Array<number>} */
  243. const listStack = [];
  244. // As definitions can come after references, we need to figure out the media
  245. // (urls and titles) defined by them before handling the references.
  246. // So, we do sort of what HTML does: put metadata at the start (in head), and
  247. // then put content after (`body`).
  248. /** @type {Array<Event>} */
  249. let head = [];
  250. /** @type {Array<Event>} */
  251. let body = [];
  252. while (++index < events.length) {
  253. // Figure out the line ending style used in the document.
  254. if (!lineEndingStyle && (events[index][1].type === "lineEnding" || events[index][1].type === "lineEndingBlank")) {
  255. lineEndingStyle = /** @type {LineEnding} */
  256. events[index][2].sliceSerialize(events[index][1]);
  257. }
  258. // Preprocess lists to infer whether the list is loose or not.
  259. if (events[index][1].type === "listOrdered" || events[index][1].type === "listUnordered") {
  260. if (events[index][0] === 'enter') {
  261. listStack.push(index);
  262. } else {
  263. prepareList(events.slice(listStack.pop(), index));
  264. }
  265. }
  266. // Move definitions to the front.
  267. if (events[index][1].type === "definition") {
  268. if (events[index][0] === 'enter') {
  269. body = push(body, events.slice(start, index));
  270. start = index;
  271. } else {
  272. head = push(head, events.slice(start, index + 1));
  273. start = index + 1;
  274. }
  275. }
  276. }
  277. head = push(head, body);
  278. head = push(head, events.slice(start));
  279. index = -1;
  280. const result = head;
  281. // Handle the start of the document, if defined.
  282. if (handlers.enter.null) {
  283. handlers.enter.null.call(context);
  284. }
  285. // Handle all events.
  286. while (++index < events.length) {
  287. const handles = handlers[result[index][0]];
  288. const kind = result[index][1].type;
  289. const handle = handles[kind];
  290. if (hasOwnProperty.call(handles, kind) && handle) {
  291. handle.call({
  292. sliceSerialize: result[index][2].sliceSerialize,
  293. ...context
  294. }, result[index][1]);
  295. }
  296. }
  297. // Handle the end of the document, if defined.
  298. if (handlers.exit.null) {
  299. handlers.exit.null.call(context);
  300. }
  301. return buffers[0].join('');
  302. }
  303. /**
  304. * Figure out whether lists are loose or not.
  305. *
  306. * @param {ReadonlyArray<Event>} slice
  307. * @returns {undefined}
  308. */
  309. function prepareList(slice) {
  310. const length = slice.length;
  311. let index = 0; // Skip open.
  312. let containerBalance = 0;
  313. let loose = false;
  314. /** @type {boolean | undefined} */
  315. let atMarker;
  316. while (++index < length) {
  317. const event = slice[index];
  318. if (event[1]._container) {
  319. atMarker = undefined;
  320. if (event[0] === 'enter') {
  321. containerBalance++;
  322. } else {
  323. containerBalance--;
  324. }
  325. } else switch (event[1].type) {
  326. case "listItemPrefix":
  327. {
  328. if (event[0] === 'exit') {
  329. atMarker = true;
  330. }
  331. break;
  332. }
  333. case "linePrefix":
  334. {
  335. // Ignore
  336. break;
  337. }
  338. case "lineEndingBlank":
  339. {
  340. if (event[0] === 'enter' && !containerBalance) {
  341. if (atMarker) {
  342. atMarker = undefined;
  343. } else {
  344. loose = true;
  345. }
  346. }
  347. break;
  348. }
  349. default:
  350. {
  351. atMarker = undefined;
  352. }
  353. }
  354. }
  355. slice[0][1]._loose = loose;
  356. }
  357. /**
  358. * @type {CompileContext['setData']}
  359. */
  360. function setData(key, value) {
  361. // @ts-expect-error: assume `value` is omitted (`undefined` is passed) only
  362. // if allowed.
  363. data[key] = value;
  364. }
  365. /**
  366. * @type {CompileContext['getData']}
  367. */
  368. function getData(key) {
  369. return data[key];
  370. }
  371. /** @type {CompileContext['buffer']} */
  372. function buffer() {
  373. buffers.push([]);
  374. }
  375. /** @type {CompileContext['resume']} */
  376. function resume() {
  377. const buf = buffers.pop();
  378. return buf.join('');
  379. }
  380. /** @type {CompileContext['tag']} */
  381. function tag(value) {
  382. if (!tags) return;
  383. setData('lastWasTag', true);
  384. buffers[buffers.length - 1].push(value);
  385. }
  386. /** @type {CompileContext['raw']} */
  387. function raw(value) {
  388. setData('lastWasTag');
  389. buffers[buffers.length - 1].push(value);
  390. }
  391. /**
  392. * Output an extra line ending.
  393. *
  394. * @returns {undefined}
  395. */
  396. function lineEnding() {
  397. raw(lineEndingStyle || '\n');
  398. }
  399. /** @type {CompileContext['lineEndingIfNeeded']} */
  400. function lineEndingIfNeeded() {
  401. const buffer = buffers[buffers.length - 1];
  402. const slice = buffer[buffer.length - 1];
  403. const previous = slice ? slice.charCodeAt(slice.length - 1) : null;
  404. if (previous === 10 || previous === 13 || previous === null) {
  405. return;
  406. }
  407. lineEnding();
  408. }
  409. /** @type {CompileContext['encode']} */
  410. function encode(value) {
  411. return getData('ignoreEncode') ? value : _encode(value);
  412. }
  413. //
  414. // Handlers.
  415. //
  416. /**
  417. * @returns {undefined}
  418. */
  419. function onresumedrop() {
  420. resume();
  421. }
  422. /**
  423. * @this {CompileContext}
  424. * @type {Handle}
  425. */
  426. function onenterlistordered(token) {
  427. tightStack.push(!token._loose);
  428. lineEndingIfNeeded();
  429. tag('<ol');
  430. setData('expectFirstItem', true);
  431. }
  432. /**
  433. * @this {CompileContext}
  434. * @type {Handle}
  435. */
  436. function onenterlistunordered(token) {
  437. tightStack.push(!token._loose);
  438. lineEndingIfNeeded();
  439. tag('<ul');
  440. setData('expectFirstItem', true);
  441. }
  442. /**
  443. * @this {CompileContext}
  444. * @type {Handle}
  445. */
  446. function onenterlistitemvalue(token) {
  447. if (getData('expectFirstItem')) {
  448. const value = Number.parseInt(this.sliceSerialize(token), 10);
  449. if (value !== 1) {
  450. tag(' start="' + encode(String(value)) + '"');
  451. }
  452. }
  453. }
  454. /**
  455. * @returns {undefined}
  456. */
  457. function onenterlistitemmarker() {
  458. if (getData('expectFirstItem')) {
  459. tag('>');
  460. } else {
  461. onexitlistitem();
  462. }
  463. lineEndingIfNeeded();
  464. tag('<li>');
  465. setData('expectFirstItem');
  466. // “Hack” to prevent a line ending from showing up if the item is empty.
  467. setData('lastWasTag');
  468. }
  469. /**
  470. * @returns {undefined}
  471. */
  472. function onexitlistordered() {
  473. onexitlistitem();
  474. tightStack.pop();
  475. lineEnding();
  476. tag('</ol>');
  477. }
  478. /**
  479. * @returns {undefined}
  480. */
  481. function onexitlistunordered() {
  482. onexitlistitem();
  483. tightStack.pop();
  484. lineEnding();
  485. tag('</ul>');
  486. }
  487. /**
  488. * @returns {undefined}
  489. */
  490. function onexitlistitem() {
  491. if (getData('lastWasTag') && !getData('slurpAllLineEndings')) {
  492. lineEndingIfNeeded();
  493. }
  494. tag('</li>');
  495. setData('slurpAllLineEndings');
  496. }
  497. /**
  498. * @this {CompileContext}
  499. * @type {Handle}
  500. */
  501. function onenterblockquote() {
  502. tightStack.push(false);
  503. lineEndingIfNeeded();
  504. tag('<blockquote>');
  505. }
  506. /**
  507. * @this {CompileContext}
  508. * @type {Handle}
  509. */
  510. function onexitblockquote() {
  511. tightStack.pop();
  512. lineEndingIfNeeded();
  513. tag('</blockquote>');
  514. setData('slurpAllLineEndings');
  515. }
  516. /**
  517. * @this {CompileContext}
  518. * @type {Handle}
  519. */
  520. function onenterparagraph() {
  521. if (!tightStack[tightStack.length - 1]) {
  522. lineEndingIfNeeded();
  523. tag('<p>');
  524. }
  525. setData('slurpAllLineEndings');
  526. }
  527. /**
  528. * @this {CompileContext}
  529. * @type {Handle}
  530. */
  531. function onexitparagraph() {
  532. if (tightStack[tightStack.length - 1]) {
  533. setData('slurpAllLineEndings', true);
  534. } else {
  535. tag('</p>');
  536. }
  537. }
  538. /**
  539. * @this {CompileContext}
  540. * @type {Handle}
  541. */
  542. function onentercodefenced() {
  543. lineEndingIfNeeded();
  544. tag('<pre><code');
  545. setData('fencesCount', 0);
  546. }
  547. /**
  548. * @this {CompileContext}
  549. * @type {Handle}
  550. */
  551. function onexitcodefencedfenceinfo() {
  552. const value = resume();
  553. tag(' class="language-' + value + '"');
  554. }
  555. /**
  556. * @this {CompileContext}
  557. * @type {Handle}
  558. */
  559. function onexitcodefencedfence() {
  560. const count = getData('fencesCount') || 0;
  561. if (!count) {
  562. tag('>');
  563. setData('slurpOneLineEnding', true);
  564. }
  565. setData('fencesCount', count + 1);
  566. }
  567. /**
  568. * @this {CompileContext}
  569. * @type {Handle}
  570. */
  571. function onentercodeindented() {
  572. lineEndingIfNeeded();
  573. tag('<pre><code>');
  574. }
  575. /**
  576. * @this {CompileContext}
  577. * @type {Handle}
  578. */
  579. function onexitflowcode() {
  580. const count = getData('fencesCount');
  581. // One special case is if we are inside a container, and the fenced code was
  582. // not closed (meaning it runs to the end).
  583. // In that case, the following line ending, is considered *outside* the
  584. // fenced code and block quote by micromark, but CM wants to treat that
  585. // ending as part of the code.
  586. if (count !== undefined && count < 2 && data.tightStack.length > 0 && !getData('lastWasTag')) {
  587. lineEnding();
  588. }
  589. // But in most cases, it’s simpler: when we’ve seen some data, emit an extra
  590. // line ending when needed.
  591. if (getData('flowCodeSeenData')) {
  592. lineEndingIfNeeded();
  593. }
  594. tag('</code></pre>');
  595. if (count !== undefined && count < 2) lineEndingIfNeeded();
  596. setData('flowCodeSeenData');
  597. setData('fencesCount');
  598. setData('slurpOneLineEnding');
  599. }
  600. /**
  601. * @this {CompileContext}
  602. * @type {Handle}
  603. */
  604. function onenterimage() {
  605. mediaStack.push({
  606. image: true
  607. });
  608. tags = undefined; // Disallow tags.
  609. }
  610. /**
  611. * @this {CompileContext}
  612. * @type {Handle}
  613. */
  614. function onenterlink() {
  615. mediaStack.push({});
  616. }
  617. /**
  618. * @this {CompileContext}
  619. * @type {Handle}
  620. */
  621. function onexitlabeltext(token) {
  622. mediaStack[mediaStack.length - 1].labelId = this.sliceSerialize(token);
  623. }
  624. /**
  625. * @this {CompileContext}
  626. * @type {Handle}
  627. */
  628. function onexitlabel() {
  629. mediaStack[mediaStack.length - 1].label = resume();
  630. }
  631. /**
  632. * @this {CompileContext}
  633. * @type {Handle}
  634. */
  635. function onexitreferencestring(token) {
  636. mediaStack[mediaStack.length - 1].referenceId = this.sliceSerialize(token);
  637. }
  638. /**
  639. * @this {CompileContext}
  640. * @type {Handle}
  641. */
  642. function onenterresource() {
  643. buffer(); // We can have line endings in the resource, ignore them.
  644. mediaStack[mediaStack.length - 1].destination = '';
  645. }
  646. /**
  647. * @this {CompileContext}
  648. * @type {Handle}
  649. */
  650. function onenterresourcedestinationstring() {
  651. buffer();
  652. // Ignore encoding the result, as we’ll first percent encode the url and
  653. // encode manually after.
  654. setData('ignoreEncode', true);
  655. }
  656. /**
  657. * @this {CompileContext}
  658. * @type {Handle}
  659. */
  660. function onexitresourcedestinationstring() {
  661. mediaStack[mediaStack.length - 1].destination = resume();
  662. setData('ignoreEncode');
  663. }
  664. /**
  665. * @this {CompileContext}
  666. * @type {Handle}
  667. */
  668. function onexitresourcetitlestring() {
  669. mediaStack[mediaStack.length - 1].title = resume();
  670. }
  671. /**
  672. * @this {CompileContext}
  673. * @type {Handle}
  674. */
  675. function onexitmedia() {
  676. let index = mediaStack.length - 1; // Skip current.
  677. const media = mediaStack[index];
  678. const id = media.referenceId || media.labelId;
  679. const context = media.destination === undefined ? definitions[normalizeIdentifier(id)] : media;
  680. tags = true;
  681. while (index--) {
  682. if (mediaStack[index].image) {
  683. tags = undefined;
  684. break;
  685. }
  686. }
  687. if (media.image) {
  688. tag('<img src="' + sanitizeUri(context.destination, settings.allowDangerousProtocol ? undefined : protocolSource) + '" alt="');
  689. raw(media.label);
  690. tag('"');
  691. } else {
  692. tag('<a href="' + sanitizeUri(context.destination, settings.allowDangerousProtocol ? undefined : protocolHref) + '"');
  693. }
  694. tag(context.title ? ' title="' + context.title + '"' : '');
  695. if (media.image) {
  696. tag(' />');
  697. } else {
  698. tag('>');
  699. raw(media.label);
  700. tag('</a>');
  701. }
  702. mediaStack.pop();
  703. }
  704. /**
  705. * @this {CompileContext}
  706. * @type {Handle}
  707. */
  708. function onenterdefinition() {
  709. buffer();
  710. mediaStack.push({});
  711. }
  712. /**
  713. * @this {CompileContext}
  714. * @type {Handle}
  715. */
  716. function onexitdefinitionlabelstring(token) {
  717. // Discard label, use the source content instead.
  718. resume();
  719. mediaStack[mediaStack.length - 1].labelId = this.sliceSerialize(token);
  720. }
  721. /**
  722. * @this {CompileContext}
  723. * @type {Handle}
  724. */
  725. function onenterdefinitiondestinationstring() {
  726. buffer();
  727. setData('ignoreEncode', true);
  728. }
  729. /**
  730. * @this {CompileContext}
  731. * @type {Handle}
  732. */
  733. function onexitdefinitiondestinationstring() {
  734. mediaStack[mediaStack.length - 1].destination = resume();
  735. setData('ignoreEncode');
  736. }
  737. /**
  738. * @this {CompileContext}
  739. * @type {Handle}
  740. */
  741. function onexitdefinitiontitlestring() {
  742. mediaStack[mediaStack.length - 1].title = resume();
  743. }
  744. /**
  745. * @this {CompileContext}
  746. * @type {Handle}
  747. */
  748. function onexitdefinition() {
  749. const media = mediaStack[mediaStack.length - 1];
  750. const id = normalizeIdentifier(media.labelId);
  751. resume();
  752. if (!hasOwnProperty.call(definitions, id)) {
  753. definitions[id] = mediaStack[mediaStack.length - 1];
  754. }
  755. mediaStack.pop();
  756. }
  757. /**
  758. * @this {CompileContext}
  759. * @type {Handle}
  760. */
  761. function onentercontent() {
  762. setData('slurpAllLineEndings', true);
  763. }
  764. /**
  765. * @this {CompileContext}
  766. * @type {Handle}
  767. */
  768. function onexitatxheadingsequence(token) {
  769. // Exit for further sequences.
  770. if (getData('headingRank')) return;
  771. setData('headingRank', this.sliceSerialize(token).length);
  772. lineEndingIfNeeded();
  773. tag('<h' + getData('headingRank') + '>');
  774. }
  775. /**
  776. * @this {CompileContext}
  777. * @type {Handle}
  778. */
  779. function onentersetextheading() {
  780. buffer();
  781. setData('slurpAllLineEndings');
  782. }
  783. /**
  784. * @this {CompileContext}
  785. * @type {Handle}
  786. */
  787. function onexitsetextheadingtext() {
  788. setData('slurpAllLineEndings', true);
  789. }
  790. /**
  791. * @this {CompileContext}
  792. * @type {Handle}
  793. */
  794. function onexitatxheading() {
  795. tag('</h' + getData('headingRank') + '>');
  796. setData('headingRank');
  797. }
  798. /**
  799. * @this {CompileContext}
  800. * @type {Handle}
  801. */
  802. function onexitsetextheadinglinesequence(token) {
  803. setData('headingRank', this.sliceSerialize(token).charCodeAt(0) === 61 ? 1 : 2);
  804. }
  805. /**
  806. * @this {CompileContext}
  807. * @type {Handle}
  808. */
  809. function onexitsetextheading() {
  810. const value = resume();
  811. lineEndingIfNeeded();
  812. tag('<h' + getData('headingRank') + '>');
  813. raw(value);
  814. tag('</h' + getData('headingRank') + '>');
  815. setData('slurpAllLineEndings');
  816. setData('headingRank');
  817. }
  818. /**
  819. * @this {CompileContext}
  820. * @type {Handle}
  821. */
  822. function onexitdata(token) {
  823. raw(encode(this.sliceSerialize(token)));
  824. }
  825. /**
  826. * @this {CompileContext}
  827. * @type {Handle}
  828. */
  829. function onexitlineending(token) {
  830. if (getData('slurpAllLineEndings')) {
  831. return;
  832. }
  833. if (getData('slurpOneLineEnding')) {
  834. setData('slurpOneLineEnding');
  835. return;
  836. }
  837. if (getData('inCodeText')) {
  838. raw(' ');
  839. return;
  840. }
  841. raw(encode(this.sliceSerialize(token)));
  842. }
  843. /**
  844. * @this {CompileContext}
  845. * @type {Handle}
  846. */
  847. function onexitcodeflowvalue(token) {
  848. raw(encode(this.sliceSerialize(token)));
  849. setData('flowCodeSeenData', true);
  850. }
  851. /**
  852. * @this {CompileContext}
  853. * @type {Handle}
  854. */
  855. function onexithardbreak() {
  856. tag('<br />');
  857. }
  858. /**
  859. * @returns {undefined}
  860. */
  861. function onenterhtmlflow() {
  862. lineEndingIfNeeded();
  863. onenterhtml();
  864. }
  865. /**
  866. * @returns {undefined}
  867. */
  868. function onexithtml() {
  869. setData('ignoreEncode');
  870. }
  871. /**
  872. * @returns {undefined}
  873. */
  874. function onenterhtml() {
  875. if (settings.allowDangerousHtml) {
  876. setData('ignoreEncode', true);
  877. }
  878. }
  879. /**
  880. * @returns {undefined}
  881. */
  882. function onenteremphasis() {
  883. tag('<em>');
  884. }
  885. /**
  886. * @returns {undefined}
  887. */
  888. function onenterstrong() {
  889. tag('<strong>');
  890. }
  891. /**
  892. * @returns {undefined}
  893. */
  894. function onentercodetext() {
  895. setData('inCodeText', true);
  896. tag('<code>');
  897. }
  898. /**
  899. * @returns {undefined}
  900. */
  901. function onexitcodetext() {
  902. setData('inCodeText');
  903. tag('</code>');
  904. }
  905. /**
  906. * @returns {undefined}
  907. */
  908. function onexitemphasis() {
  909. tag('</em>');
  910. }
  911. /**
  912. * @returns {undefined}
  913. */
  914. function onexitstrong() {
  915. tag('</strong>');
  916. }
  917. /**
  918. * @returns {undefined}
  919. */
  920. function onexitthematicbreak() {
  921. lineEndingIfNeeded();
  922. tag('<hr />');
  923. }
  924. /**
  925. * @this {CompileContext}
  926. * @param {Token} token
  927. * @returns {undefined}
  928. */
  929. function onexitcharacterreferencemarker(token) {
  930. setData('characterReferenceType', token.type);
  931. }
  932. /**
  933. * @this {CompileContext}
  934. * @type {Handle}
  935. */
  936. function onexitcharacterreferencevalue(token) {
  937. const value = this.sliceSerialize(token);
  938. const decoded = getData('characterReferenceType') ? decodeNumericCharacterReference(value, getData('characterReferenceType') === "characterReferenceMarkerNumeric" ? 10 : 16) : decodeNamedCharacterReference(value);
  939. // `decodeNamedCharacterReference` can return `false` for invalid named
  940. // character references,
  941. // but everything we’ve tokenized is valid.
  942. raw(encode(/** @type {string} */decoded));
  943. setData('characterReferenceType');
  944. }
  945. /**
  946. * @this {CompileContext}
  947. * @type {Handle}
  948. */
  949. function onexitautolinkprotocol(token) {
  950. const uri = this.sliceSerialize(token);
  951. tag('<a href="' + sanitizeUri(uri, settings.allowDangerousProtocol ? undefined : protocolHref) + '">');
  952. raw(encode(uri));
  953. tag('</a>');
  954. }
  955. /**
  956. * @this {CompileContext}
  957. * @type {Handle}
  958. */
  959. function onexitautolinkemail(token) {
  960. const uri = this.sliceSerialize(token);
  961. tag('<a href="' + sanitizeUri('mailto:' + uri) + '">');
  962. raw(encode(uri));
  963. tag('</a>');
  964. }
  965. }