footer.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /**
  2. * @import {ElementContent, Element} from 'hast'
  3. * @import {State} from './state.js'
  4. */
  5. /**
  6. * @callback FootnoteBackContentTemplate
  7. * Generate content for the backreference dynamically.
  8. *
  9. * For the following markdown:
  10. *
  11. * ```markdown
  12. * Alpha[^micromark], bravo[^micromark], and charlie[^remark].
  13. *
  14. * [^remark]: things about remark
  15. * [^micromark]: things about micromark
  16. * ```
  17. *
  18. * This function will be called with:
  19. *
  20. * * `0` and `0` for the backreference from `things about micromark` to
  21. * `alpha`, as it is the first used definition, and the first call to it
  22. * * `0` and `1` for the backreference from `things about micromark` to
  23. * `bravo`, as it is the first used definition, and the second call to it
  24. * * `1` and `0` for the backreference from `things about remark` to
  25. * `charlie`, as it is the second used definition
  26. * @param {number} referenceIndex
  27. * Index of the definition in the order that they are first referenced,
  28. * 0-indexed.
  29. * @param {number} rereferenceIndex
  30. * Index of calls to the same definition, 0-indexed.
  31. * @returns {Array<ElementContent> | ElementContent | string}
  32. * Content for the backreference when linking back from definitions to their
  33. * reference.
  34. *
  35. * @callback FootnoteBackLabelTemplate
  36. * Generate a back label dynamically.
  37. *
  38. * For the following markdown:
  39. *
  40. * ```markdown
  41. * Alpha[^micromark], bravo[^micromark], and charlie[^remark].
  42. *
  43. * [^remark]: things about remark
  44. * [^micromark]: things about micromark
  45. * ```
  46. *
  47. * This function will be called with:
  48. *
  49. * * `0` and `0` for the backreference from `things about micromark` to
  50. * `alpha`, as it is the first used definition, and the first call to it
  51. * * `0` and `1` for the backreference from `things about micromark` to
  52. * `bravo`, as it is the first used definition, and the second call to it
  53. * * `1` and `0` for the backreference from `things about remark` to
  54. * `charlie`, as it is the second used definition
  55. * @param {number} referenceIndex
  56. * Index of the definition in the order that they are first referenced,
  57. * 0-indexed.
  58. * @param {number} rereferenceIndex
  59. * Index of calls to the same definition, 0-indexed.
  60. * @returns {string}
  61. * Back label to use when linking back from definitions to their reference.
  62. */
  63. import structuredClone from '@ungap/structured-clone'
  64. import {normalizeUri} from 'micromark-util-sanitize-uri'
  65. /**
  66. * Generate the default content that GitHub uses on backreferences.
  67. *
  68. * @param {number} _
  69. * Index of the definition in the order that they are first referenced,
  70. * 0-indexed.
  71. * @param {number} rereferenceIndex
  72. * Index of calls to the same definition, 0-indexed.
  73. * @returns {Array<ElementContent>}
  74. * Content.
  75. */
  76. export function defaultFootnoteBackContent(_, rereferenceIndex) {
  77. /** @type {Array<ElementContent>} */
  78. const result = [{type: 'text', value: '↩'}]
  79. if (rereferenceIndex > 1) {
  80. result.push({
  81. type: 'element',
  82. tagName: 'sup',
  83. properties: {},
  84. children: [{type: 'text', value: String(rereferenceIndex)}]
  85. })
  86. }
  87. return result
  88. }
  89. /**
  90. * Generate the default label that GitHub uses on backreferences.
  91. *
  92. * @param {number} referenceIndex
  93. * Index of the definition in the order that they are first referenced,
  94. * 0-indexed.
  95. * @param {number} rereferenceIndex
  96. * Index of calls to the same definition, 0-indexed.
  97. * @returns {string}
  98. * Label.
  99. */
  100. export function defaultFootnoteBackLabel(referenceIndex, rereferenceIndex) {
  101. return (
  102. 'Back to reference ' +
  103. (referenceIndex + 1) +
  104. (rereferenceIndex > 1 ? '-' + rereferenceIndex : '')
  105. )
  106. }
  107. /**
  108. * Generate a hast footer for called footnote definitions.
  109. *
  110. * @param {State} state
  111. * Info passed around.
  112. * @returns {Element | undefined}
  113. * `section` element or `undefined`.
  114. */
  115. // eslint-disable-next-line complexity
  116. export function footer(state) {
  117. const clobberPrefix =
  118. typeof state.options.clobberPrefix === 'string'
  119. ? state.options.clobberPrefix
  120. : 'user-content-'
  121. const footnoteBackContent =
  122. state.options.footnoteBackContent || defaultFootnoteBackContent
  123. const footnoteBackLabel =
  124. state.options.footnoteBackLabel || defaultFootnoteBackLabel
  125. const footnoteLabel = state.options.footnoteLabel || 'Footnotes'
  126. const footnoteLabelTagName = state.options.footnoteLabelTagName || 'h2'
  127. const footnoteLabelProperties = state.options.footnoteLabelProperties || {
  128. className: ['sr-only']
  129. }
  130. /** @type {Array<ElementContent>} */
  131. const listItems = []
  132. let referenceIndex = -1
  133. while (++referenceIndex < state.footnoteOrder.length) {
  134. const definition = state.footnoteById.get(
  135. state.footnoteOrder[referenceIndex]
  136. )
  137. if (!definition) {
  138. continue
  139. }
  140. const content = state.all(definition)
  141. const id = String(definition.identifier).toUpperCase()
  142. const safeId = normalizeUri(id.toLowerCase())
  143. let rereferenceIndex = 0
  144. /** @type {Array<ElementContent>} */
  145. const backReferences = []
  146. const counts = state.footnoteCounts.get(id)
  147. // eslint-disable-next-line no-unmodified-loop-condition
  148. while (counts !== undefined && ++rereferenceIndex <= counts) {
  149. if (backReferences.length > 0) {
  150. backReferences.push({type: 'text', value: ' '})
  151. }
  152. let children =
  153. typeof footnoteBackContent === 'string'
  154. ? footnoteBackContent
  155. : footnoteBackContent(referenceIndex, rereferenceIndex)
  156. if (typeof children === 'string') {
  157. children = {type: 'text', value: children}
  158. }
  159. backReferences.push({
  160. type: 'element',
  161. tagName: 'a',
  162. properties: {
  163. href:
  164. '#' +
  165. clobberPrefix +
  166. 'fnref-' +
  167. safeId +
  168. (rereferenceIndex > 1 ? '-' + rereferenceIndex : ''),
  169. dataFootnoteBackref: '',
  170. ariaLabel:
  171. typeof footnoteBackLabel === 'string'
  172. ? footnoteBackLabel
  173. : footnoteBackLabel(referenceIndex, rereferenceIndex),
  174. className: ['data-footnote-backref']
  175. },
  176. children: Array.isArray(children) ? children : [children]
  177. })
  178. }
  179. const tail = content[content.length - 1]
  180. if (tail && tail.type === 'element' && tail.tagName === 'p') {
  181. const tailTail = tail.children[tail.children.length - 1]
  182. if (tailTail && tailTail.type === 'text') {
  183. tailTail.value += ' '
  184. } else {
  185. tail.children.push({type: 'text', value: ' '})
  186. }
  187. tail.children.push(...backReferences)
  188. } else {
  189. content.push(...backReferences)
  190. }
  191. /** @type {Element} */
  192. const listItem = {
  193. type: 'element',
  194. tagName: 'li',
  195. properties: {id: clobberPrefix + 'fn-' + safeId},
  196. children: state.wrap(content, true)
  197. }
  198. state.patch(definition, listItem)
  199. listItems.push(listItem)
  200. }
  201. if (listItems.length === 0) {
  202. return
  203. }
  204. return {
  205. type: 'element',
  206. tagName: 'section',
  207. properties: {dataFootnotes: true, className: ['footnotes']},
  208. children: [
  209. {
  210. type: 'element',
  211. tagName: footnoteLabelTagName,
  212. properties: {
  213. ...structuredClone(footnoteLabelProperties),
  214. id: 'footnote-label'
  215. },
  216. children: [{type: 'text', value: footnoteLabel}]
  217. },
  218. {type: 'text', value: '\n'},
  219. {
  220. type: 'element',
  221. tagName: 'ol',
  222. properties: {},
  223. children: state.wrap(listItems, true)
  224. },
  225. {type: 'text', value: '\n'}
  226. ]
  227. }
  228. }