compile.js 27 KB

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