| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- /**
- * @import {
- * ElementContent as HastElementContent,
- * Element as HastElement,
- * Nodes as HastNodes,
- * Properties as HastProperties,
- * RootContent as HastRootContent,
- * Text as HastText
- * } from 'hast'
- * @import {
- * Definition as MdastDefinition,
- * FootnoteDefinition as MdastFootnoteDefinition,
- * Nodes as MdastNodes,
- * Parents as MdastParents
- * } from 'mdast'
- * @import {VFile} from 'vfile'
- * @import {
- * FootnoteBackContentTemplate,
- * FootnoteBackLabelTemplate
- * } from './footer.js'
- */
- /**
- * @callback Handler
- * Handle a node.
- * @param {State} state
- * Info passed around.
- * @param {any} node
- * mdast node to handle.
- * @param {MdastParents | undefined} parent
- * Parent of `node`.
- * @returns {Array<HastElementContent> | HastElementContent | undefined}
- * hast node.
- *
- * @typedef {Partial<Record<MdastNodes['type'], Handler>>} Handlers
- * Handle nodes.
- *
- * @typedef Options
- * Configuration (optional).
- * @property {boolean | null | undefined} [allowDangerousHtml=false]
- * Whether to persist raw HTML in markdown in the hast tree (default:
- * `false`).
- * @property {string | null | undefined} [clobberPrefix='user-content-']
- * Prefix to use before the `id` property on footnotes to prevent them from
- * *clobbering* (default: `'user-content-'`).
- *
- * Pass `''` for trusted markdown and when you are careful with
- * polyfilling.
- * You could pass a different prefix.
- *
- * DOM clobbering is this:
- *
- * ```html
- * <p id="x"></p>
- * <script>alert(x) // `x` now refers to the `p#x` DOM element</script>
- * ```
- *
- * The above example shows that elements are made available by browsers, by
- * their ID, on the `window` object.
- * This is a security risk because you might be expecting some other variable
- * at that place.
- * It can also break polyfills.
- * Using a prefix solves these problems.
- * @property {VFile | null | undefined} [file]
- * Corresponding virtual file representing the input document (optional).
- * @property {FootnoteBackContentTemplate | string | null | undefined} [footnoteBackContent]
- * Content of the backreference back to references (default: `defaultFootnoteBackContent`).
- *
- * The default value is:
- *
- * ```js
- * function defaultFootnoteBackContent(_, rereferenceIndex) {
- * const result = [{type: 'text', value: '↩'}]
- *
- * if (rereferenceIndex > 1) {
- * result.push({
- * type: 'element',
- * tagName: 'sup',
- * properties: {},
- * children: [{type: 'text', value: String(rereferenceIndex)}]
- * })
- * }
- *
- * return result
- * }
- * ```
- *
- * This content is used in the `a` element of each backreference (the `↩`
- * links).
- * @property {FootnoteBackLabelTemplate | string | null | undefined} [footnoteBackLabel]
- * Label to describe the backreference back to references (default:
- * `defaultFootnoteBackLabel`).
- *
- * The default value is:
- *
- * ```js
- * function defaultFootnoteBackLabel(referenceIndex, rereferenceIndex) {
- * return (
- * 'Back to reference ' +
- * (referenceIndex + 1) +
- * (rereferenceIndex > 1 ? '-' + rereferenceIndex : '')
- * )
- * }
- * ```
- *
- * Change it when the markdown is not in English.
- *
- * This label is used in the `ariaLabel` property on each backreference
- * (the `↩` links).
- * It affects users of assistive technology.
- * @property {string | null | undefined} [footnoteLabel='Footnotes']
- * Textual label to use for the footnotes section (default: `'Footnotes'`).
- *
- * Change it when the markdown is not in English.
- *
- * This label is typically hidden visually (assuming a `sr-only` CSS class
- * is defined that does that) and so affects screen readers only.
- * If you do have such a class, but want to show this section to everyone,
- * pass different properties with the `footnoteLabelProperties` option.
- * @property {HastProperties | null | undefined} [footnoteLabelProperties={className: ['sr-only']}]
- * Properties to use on the footnote label (default: `{className:
- * ['sr-only']}`).
- *
- * Change it to show the label and add other properties.
- *
- * This label is typically hidden visually (assuming an `sr-only` CSS class
- * is defined that does that) and so affects screen readers only.
- * If you do have such a class, but want to show this section to everyone,
- * pass an empty string.
- * You can also add different properties.
- *
- * > **Note**: `id: 'footnote-label'` is always added, because footnote
- * > calls use it with `aria-describedby` to provide an accessible label.
- * @property {string | null | undefined} [footnoteLabelTagName='h2']
- * HTML tag name to use for the footnote label element (default: `'h2'`).
- *
- * Change it to match your document structure.
- *
- * This label is typically hidden visually (assuming a `sr-only` CSS class
- * is defined that does that) and so affects screen readers only.
- * If you do have such a class, but want to show this section to everyone,
- * pass different properties with the `footnoteLabelProperties` option.
- * @property {Handlers | null | undefined} [handlers]
- * Extra handlers for nodes (optional).
- * @property {Array<MdastNodes['type']> | null | undefined} [passThrough]
- * List of custom mdast node types to pass through (keep) in hast (note that
- * the node itself is passed, but eventual children are transformed)
- * (optional).
- * @property {Handler | null | undefined} [unknownHandler]
- * Handler for all unknown nodes (optional).
- *
- * @typedef State
- * Info passed around.
- * @property {(node: MdastNodes) => Array<HastElementContent>} all
- * Transform the children of an mdast parent to hast.
- * @property {<Type extends HastNodes>(from: MdastNodes, to: Type) => HastElement | Type} applyData
- * Honor the `data` of `from`, and generate an element instead of `node`.
- * @property {Map<string, MdastDefinition>} definitionById
- * Definitions by their identifier.
- * @property {Map<string, MdastFootnoteDefinition>} footnoteById
- * Footnote definitions by their identifier.
- * @property {Map<string, number>} footnoteCounts
- * Counts for how often the same footnote was called.
- * @property {Array<string>} footnoteOrder
- * Identifiers of order when footnote calls first appear in tree order.
- * @property {Handlers} handlers
- * Applied handlers.
- * @property {(node: MdastNodes, parent: MdastParents | undefined) => Array<HastElementContent> | HastElementContent | undefined} one
- * Transform an mdast node to hast.
- * @property {Options} options
- * Configuration.
- * @property {(from: MdastNodes, node: HastNodes) => undefined} patch
- * Copy a node’s positional info.
- * @property {<Type extends HastRootContent>(nodes: Array<Type>, loose?: boolean | undefined) => Array<HastText | Type>} wrap
- * Wrap `nodes` with line endings between each node, adds initial/final line endings when `loose`.
- */
- import structuredClone from '@ungap/structured-clone'
- import {visit} from 'unist-util-visit'
- import {position} from 'unist-util-position'
- import {handlers as defaultHandlers} from './handlers/index.js'
- const own = {}.hasOwnProperty
- /** @type {Options} */
- const emptyOptions = {}
- /**
- * Create `state` from an mdast tree.
- *
- * @param {MdastNodes} tree
- * mdast node to transform.
- * @param {Options | null | undefined} [options]
- * Configuration (optional).
- * @returns {State}
- * `state` function.
- */
- export function createState(tree, options) {
- const settings = options || emptyOptions
- /** @type {Map<string, MdastDefinition>} */
- const definitionById = new Map()
- /** @type {Map<string, MdastFootnoteDefinition>} */
- const footnoteById = new Map()
- /** @type {Map<string, number>} */
- const footnoteCounts = new Map()
- /** @type {Handlers} */
- // @ts-expect-error: the root handler returns a root.
- // Hard to type.
- const handlers = {...defaultHandlers, ...settings.handlers}
- /** @type {State} */
- const state = {
- all,
- applyData,
- definitionById,
- footnoteById,
- footnoteCounts,
- footnoteOrder: [],
- handlers,
- one,
- options: settings,
- patch,
- wrap
- }
- visit(tree, function (node) {
- if (node.type === 'definition' || node.type === 'footnoteDefinition') {
- const map = node.type === 'definition' ? definitionById : footnoteById
- const id = String(node.identifier).toUpperCase()
- // Mimick CM behavior of link definitions.
- // See: <https://github.com/syntax-tree/mdast-util-definitions/blob/9032189/lib/index.js#L20-L21>.
- if (!map.has(id)) {
- // @ts-expect-error: node type matches map.
- map.set(id, node)
- }
- }
- })
- return state
- /**
- * Transform an mdast node into a hast node.
- *
- * @param {MdastNodes} node
- * mdast node.
- * @param {MdastParents | undefined} [parent]
- * Parent of `node`.
- * @returns {Array<HastElementContent> | HastElementContent | undefined}
- * Resulting hast node.
- */
- function one(node, parent) {
- const type = node.type
- const handle = state.handlers[type]
- if (own.call(state.handlers, type) && handle) {
- return handle(state, node, parent)
- }
- if (state.options.passThrough && state.options.passThrough.includes(type)) {
- if ('children' in node) {
- const {children, ...shallow} = node
- const result = structuredClone(shallow)
- // @ts-expect-error: TS doesn’t understand…
- result.children = state.all(node)
- // @ts-expect-error: TS doesn’t understand…
- return result
- }
- // @ts-expect-error: it’s custom.
- return structuredClone(node)
- }
- const unknown = state.options.unknownHandler || defaultUnknownHandler
- return unknown(state, node, parent)
- }
- /**
- * Transform the children of an mdast node into hast nodes.
- *
- * @param {MdastNodes} parent
- * mdast node to compile
- * @returns {Array<HastElementContent>}
- * Resulting hast nodes.
- */
- function all(parent) {
- /** @type {Array<HastElementContent>} */
- const values = []
- if ('children' in parent) {
- const nodes = parent.children
- let index = -1
- while (++index < nodes.length) {
- const result = state.one(nodes[index], parent)
- // To do: see if we van clean this? Can we merge texts?
- if (result) {
- if (index && nodes[index - 1].type === 'break') {
- if (!Array.isArray(result) && result.type === 'text') {
- result.value = trimMarkdownSpaceStart(result.value)
- }
- if (!Array.isArray(result) && result.type === 'element') {
- const head = result.children[0]
- if (head && head.type === 'text') {
- head.value = trimMarkdownSpaceStart(head.value)
- }
- }
- }
- if (Array.isArray(result)) {
- values.push(...result)
- } else {
- values.push(result)
- }
- }
- }
- }
- return values
- }
- }
- /**
- * Copy a node’s positional info.
- *
- * @param {MdastNodes} from
- * mdast node to copy from.
- * @param {HastNodes} to
- * hast node to copy into.
- * @returns {undefined}
- * Nothing.
- */
- function patch(from, to) {
- if (from.position) to.position = position(from)
- }
- /**
- * Honor the `data` of `from` and maybe generate an element instead of `to`.
- *
- * @template {HastNodes} Type
- * Node type.
- * @param {MdastNodes} from
- * mdast node to use data from.
- * @param {Type} to
- * hast node to change.
- * @returns {HastElement | Type}
- * Nothing.
- */
- function applyData(from, to) {
- /** @type {HastElement | Type} */
- let result = to
- // Handle `data.hName`, `data.hProperties, `data.hChildren`.
- if (from && from.data) {
- const hName = from.data.hName
- const hChildren = from.data.hChildren
- const hProperties = from.data.hProperties
- if (typeof hName === 'string') {
- // Transforming the node resulted in an element with a different name
- // than wanted:
- if (result.type === 'element') {
- result.tagName = hName
- }
- // Transforming the node resulted in a non-element, which happens for
- // raw, text, and root nodes (unless custom handlers are passed).
- // The intent of `hName` is to create an element, but likely also to keep
- // the content around (otherwise: pass `hChildren`).
- else {
- /** @type {Array<HastElementContent>} */
- // @ts-expect-error: assume no doctypes in `root`.
- const children = 'children' in result ? result.children : [result]
- result = {type: 'element', tagName: hName, properties: {}, children}
- }
- }
- if (result.type === 'element' && hProperties) {
- Object.assign(result.properties, structuredClone(hProperties))
- }
- if (
- 'children' in result &&
- result.children &&
- hChildren !== null &&
- hChildren !== undefined
- ) {
- result.children = hChildren
- }
- }
- return result
- }
- /**
- * Transform an unknown node.
- *
- * @param {State} state
- * Info passed around.
- * @param {MdastNodes} node
- * Unknown mdast node.
- * @returns {HastElement | HastText}
- * Resulting hast node.
- */
- function defaultUnknownHandler(state, node) {
- const data = node.data || {}
- /** @type {HastElement | HastText} */
- const result =
- 'value' in node &&
- !(own.call(data, 'hProperties') || own.call(data, 'hChildren'))
- ? {type: 'text', value: node.value}
- : {
- type: 'element',
- tagName: 'div',
- properties: {},
- children: state.all(node)
- }
- state.patch(node, result)
- return state.applyData(node, result)
- }
- /**
- * Wrap `nodes` with line endings between each node.
- *
- * @template {HastRootContent} Type
- * Node type.
- * @param {Array<Type>} nodes
- * List of nodes to wrap.
- * @param {boolean | undefined} [loose=false]
- * Whether to add line endings at start and end (default: `false`).
- * @returns {Array<HastText | Type>}
- * Wrapped nodes.
- */
- export function wrap(nodes, loose) {
- /** @type {Array<HastText | Type>} */
- const result = []
- let index = -1
- if (loose) {
- result.push({type: 'text', value: '\n'})
- }
- while (++index < nodes.length) {
- if (index) result.push({type: 'text', value: '\n'})
- result.push(nodes[index])
- }
- if (loose && nodes.length > 0) {
- result.push({type: 'text', value: '\n'})
- }
- return result
- }
- /**
- * Trim spaces and tabs at the start of `value`.
- *
- * @param {string} value
- * Value to trim.
- * @returns {string}
- * Result.
- */
- function trimMarkdownSpaceStart(value) {
- let index = 0
- let code = value.charCodeAt(index)
- while (code === 9 || code === 32) {
- index++
- code = value.charCodeAt(index)
- }
- return value.slice(index)
- }
|