compile.js 27 KB

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