compile.js 27 KB

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