Skip to content

Commit 56c88e4

Browse files
committedJul 17, 2023
Fix to match GH for HTML generated for backreferences
* GH previously generated HTML for backreferences to repeated references that was not accessible, as it failed [WCAG 2.1 SC 2.4.4 — Link Purpose (In Context)](https://www.w3.org/TR/WCAG21/#link-purpose-in-context) * GH changed the text content they use in their backreferences from `Back to content` to `Back to reference i`, where `i` is either `x` or `x-y`, of which `x` is the reference index, and `y` the rereference index This commit changes all HTML output for users that relied on the defaults, so that it matches GH again, exactly. The default handling is exposed as `defaultFootnoteBackLabel`. Users who set `footnoteBackLabel` are not affected. But these users can now provide a function instead of a `string`, to also solve the WCAG issue. The type for this function is exposed as `FootnoteBackLabelTemplate`. Additionally, you can now pass `footnoteBackContent` to set the *content* of the backreference. Related-to: github/cmark-gfm#307. Closes remarkjs/remark-rehype#32.
1 parent 3e300ea commit 56c88e4

8 files changed

+548
-94
lines changed
 

‎index.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import type {Data, ElementContent, Literal, Properties} from 'hast'
22

33
// Expose types.
4+
export type {
5+
FootnoteBackContentTemplate,
6+
FootnoteBackLabelTemplate
7+
} from './lib/footer.js'
48
export type {Handler, Handlers, Options, State} from './lib/state.js'
59

610
// Expose JS API.
711
export {handlers as defaultHandlers} from './lib/handlers/index.js'
12+
export {
13+
defaultFootnoteBackContent,
14+
defaultFootnoteBackLabel
15+
} from './lib/footer.js'
816
export {toHast} from './lib/index.js'
917

1018
/**

‎index.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
// Note: types exposed from `index.d.ts`.
22
export {handlers as defaultHandlers} from './lib/handlers/index.js'
33
export {toHast} from './lib/index.js'
4+
export {
5+
defaultFootnoteBackContent,
6+
defaultFootnoteBackLabel
7+
} from './lib/footer.js'

‎lib/footer.js

+138-29
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,114 @@
55
* @typedef {import('./state.js').State} State
66
*/
77

8+
/**
9+
* @callback FootnoteBackContentTemplate
10+
* Generate content for the backreference dynamically.
11+
*
12+
* For the following markdown:
13+
*
14+
* ```markdown
15+
* Alpha[^micromark], bravo[^micromark], and charlie[^remark].
16+
*
17+
* [^remark]: things about remark
18+
* [^micromark]: things about micromark
19+
* ```
20+
*
21+
* This function will be called with:
22+
*
23+
* * `0` and `0` for the backreference from `things about micromark` to
24+
* `alpha`, as it is the first used definition, and the first call to it
25+
* * `0` and `1` for the backreference from `things about micromark` to
26+
* `bravo`, as it is the first used definition, and the second call to it
27+
* * `1` and `0` for the backreference from `things about remark` to
28+
* `charlie`, as it is the second used definition
29+
* @param {number} referenceIndex
30+
* Index of the definition in the order that they are first referenced,
31+
* 0-indexed.
32+
* @param {number} rereferenceIndex
33+
* Index of calls to the same definition, 0-indexed.
34+
* @returns {Array<ElementContent> | ElementContent | string}
35+
* Content for the backreference when linking back from definitions to their
36+
* reference.
37+
*
38+
* @callback FootnoteBackLabelTemplate
39+
* Generate a back label dynamically.
40+
*
41+
* For the following markdown:
42+
*
43+
* ```markdown
44+
* Alpha[^micromark], bravo[^micromark], and charlie[^remark].
45+
*
46+
* [^remark]: things about remark
47+
* [^micromark]: things about micromark
48+
* ```
49+
*
50+
* This function will be called with:
51+
*
52+
* * `0` and `0` for the backreference from `things about micromark` to
53+
* `alpha`, as it is the first used definition, and the first call to it
54+
* * `0` and `1` for the backreference from `things about micromark` to
55+
* `bravo`, as it is the first used definition, and the second call to it
56+
* * `1` and `0` for the backreference from `things about remark` to
57+
* `charlie`, as it is the second used definition
58+
* @param {number} referenceIndex
59+
* Index of the definition in the order that they are first referenced,
60+
* 0-indexed.
61+
* @param {number} rereferenceIndex
62+
* Index of calls to the same definition, 0-indexed.
63+
* @returns {string}
64+
* Back label to use when linking back from definitions to their reference.
65+
*/
66+
867
import structuredClone from '@ungap/structured-clone'
968
import {normalizeUri} from 'micromark-util-sanitize-uri'
1069

70+
/**
71+
* Generate the default content that GitHub uses on backreferences.
72+
*
73+
* @param {number} _
74+
* Index of the definition in the order that they are first referenced,
75+
* 0-indexed.
76+
* @param {number} rereferenceIndex
77+
* Index of calls to the same definition, 0-indexed.
78+
* @returns {Array<ElementContent>}
79+
* Content.
80+
*/
81+
export function defaultFootnoteBackContent(_, rereferenceIndex) {
82+
/** @type {Array<ElementContent>} */
83+
const result = [{type: 'text', value: '↩'}]
84+
85+
if (rereferenceIndex > 1) {
86+
result.push({
87+
type: 'element',
88+
tagName: 'sup',
89+
properties: {},
90+
children: [{type: 'text', value: String(rereferenceIndex)}]
91+
})
92+
}
93+
94+
return result
95+
}
96+
97+
/**
98+
* Generate the default label that GitHub uses on backreferences.
99+
*
100+
* @param {number} referenceIndex
101+
* Index of the definition in the order that they are first referenced,
102+
* 0-indexed.
103+
* @param {number} rereferenceIndex
104+
* Index of calls to the same definition, 0-indexed.
105+
* @returns {string}
106+
* Label.
107+
*/
108+
export function defaultFootnoteBackLabel(referenceIndex, rereferenceIndex) {
109+
return (
110+
'Back to reference ' +
111+
(referenceIndex + 1) +
112+
(rereferenceIndex > 1 ? '-' + rereferenceIndex : '')
113+
)
114+
}
115+
11116
/**
12117
* Generate a hast footer for called footnote definitions.
13118
*
@@ -16,23 +121,27 @@ import {normalizeUri} from 'micromark-util-sanitize-uri'
16121
* @returns {Element | undefined}
17122
* `section` element or `undefined`.
18123
*/
124+
// eslint-disable-next-line complexity
19125
export function footer(state) {
20126
const clobberPrefix =
21127
typeof state.options.clobberPrefix === 'string'
22128
? state.options.clobberPrefix
23129
: 'user-content-'
24-
const footnoteBackLabel = state.options.footnoteBackLabel || 'Back to content'
130+
const footnoteBackContent =
131+
state.options.footnoteBackContent || defaultFootnoteBackContent
132+
const footnoteBackLabel =
133+
state.options.footnoteBackLabel || defaultFootnoteBackLabel
25134
const footnoteLabel = state.options.footnoteLabel || 'Footnotes'
26135
const footnoteLabelTagName = state.options.footnoteLabelTagName || 'h2'
27136
const footnoteLabelProperties = state.options.footnoteLabelProperties || {
28137
className: ['sr-only']
29138
}
30139
/** @type {Array<ElementContent>} */
31140
const listItems = []
32-
let index = -1
141+
let referenceIndex = -1
33142

34-
while (++index < state.footnoteOrder.length) {
35-
const def = state.footnoteById.get(state.footnoteOrder[index])
143+
while (++referenceIndex < state.footnoteOrder.length) {
144+
const def = state.footnoteById.get(state.footnoteOrder[referenceIndex])
36145

37146
if (!def) {
38147
continue
@@ -41,15 +150,27 @@ export function footer(state) {
41150
const content = state.all(def)
42151
const id = String(def.identifier).toUpperCase()
43152
const safeId = normalizeUri(id.toLowerCase())
44-
let referenceIndex = 0
153+
let rereferenceIndex = 0
45154
/** @type {Array<ElementContent>} */
46155
const backReferences = []
47156
const counts = state.footnoteCounts.get(id)
48157

49158
// eslint-disable-next-line no-unmodified-loop-condition
50-
while (counts !== undefined && ++referenceIndex <= counts) {
51-
/** @type {Element} */
52-
const backReference = {
159+
while (counts !== undefined && ++rereferenceIndex <= counts) {
160+
if (backReferences.length > 0) {
161+
backReferences.push({type: 'text', value: ' '})
162+
}
163+
164+
let children =
165+
typeof footnoteBackContent === 'string'
166+
? footnoteBackContent
167+
: footnoteBackContent(referenceIndex, rereferenceIndex)
168+
169+
if (typeof children === 'string') {
170+
children = {type: 'text', value: children}
171+
}
172+
173+
backReferences.push({
53174
type: 'element',
54175
tagName: 'a',
55176
properties: {
@@ -58,28 +179,16 @@ export function footer(state) {
58179
clobberPrefix +
59180
'fnref-' +
60181
safeId +
61-
(referenceIndex > 1 ? '-' + referenceIndex : ''),
62-
dataFootnoteBackref: true,
63-
className: ['data-footnote-backref'],
64-
ariaLabel: footnoteBackLabel
182+
(rereferenceIndex > 1 ? '-' + rereferenceIndex : ''),
183+
dataFootnoteBackref: '',
184+
ariaLabel:
185+
typeof footnoteBackLabel === 'string'
186+
? footnoteBackLabel
187+
: footnoteBackLabel(referenceIndex, rereferenceIndex),
188+
className: ['data-footnote-backref']
65189
},
66-
children: [{type: 'text', value: '↩'}]
67-
}
68-
69-
if (referenceIndex > 1) {
70-
backReference.children.push({
71-
type: 'element',
72-
tagName: 'sup',
73-
properties: {},
74-
children: [{type: 'text', value: String(referenceIndex)}]
75-
})
76-
}
77-
78-
if (backReferences.length > 0) {
79-
backReferences.push({type: 'text', value: ' '})
80-
}
81-
82-
backReferences.push(backReference)
190+
children: Array.isArray(children) ? children : [children]
191+
})
83192
}
84193

85194
const tail = content[content.length - 1]

‎lib/state.js

+122-33
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
* @typedef {import('mdast').FootnoteDefinition} MdastFootnoteDefinition
1111
* @typedef {import('mdast').Nodes} MdastNodes
1212
* @typedef {import('mdast').Parents} MdastParents
13+
*
14+
* @typedef {import('./footer.js').FootnoteBackContentTemplate} FootnoteBackContentTemplate
15+
* @typedef {import('./footer.js').FootnoteBackLabelTemplate} FootnoteBackLabelTemplate
1316
*/
1417

1518
/**
@@ -24,6 +27,121 @@
2427
* @returns {Array<HastElementContent> | HastElementContent | undefined}
2528
* hast node.
2629
*
30+
* @typedef {Record<string, Handler>} Handlers
31+
* Handle nodes.
32+
*
33+
* @typedef Options
34+
* Configuration (optional).
35+
* @property {boolean | null | undefined} [allowDangerousHtml=false]
36+
* Whether to persist raw HTML in markdown in the hast tree (default:
37+
* `false`).
38+
* @property {string | null | undefined} [clobberPrefix='user-content-']
39+
* Prefix to use before the `id` property on footnotes to prevent them from
40+
* *clobbering* (default: `'user-content-'`).
41+
*
42+
* Pass `''` for trusted markdown and when you are careful with
43+
* polyfilling.
44+
* You could pass a different prefix.
45+
*
46+
* DOM clobbering is this:
47+
*
48+
* ```html
49+
* <p id="x"></p>
50+
* <script>alert(x) // `x` now refers to the `p#x` DOM element</script>
51+
* ```
52+
*
53+
* The above example shows that elements are made available by browsers, by
54+
* their ID, on the `window` object.
55+
* This is a security risk because you might be expecting some other variable
56+
* at that place.
57+
* It can also break polyfills.
58+
* Using a prefix solves these problems.
59+
* @property {FootnoteBackContentTemplate | string | null | undefined} [footnoteBackContent]
60+
* Content of the backreference back to references (default: `defaultFootnoteBackContent`).
61+
*
62+
* The default value is:
63+
*
64+
* ```js
65+
* function defaultFootnoteBackContent(_, rereferenceIndex) {
66+
* const result = [{type: 'text', value: '↩'}]
67+
*
68+
* if (rereferenceIndex > 1) {
69+
* result.push({
70+
* type: 'element',
71+
* tagName: 'sup',
72+
* properties: {},
73+
* children: [{type: 'text', value: String(rereferenceIndex)}]
74+
* })
75+
* }
76+
*
77+
* return result
78+
* }
79+
* ```
80+
*
81+
* This content is used in the `a` element of each backreference (the `↩`
82+
* links).
83+
* @property {FootnoteBackLabelTemplate | string | null | undefined} [footnoteBackLabel]
84+
* Label to describe the backreference back to references (default:
85+
* `defaultFootnoteBackLabel`).
86+
*
87+
* The default value is:
88+
*
89+
* ```js
90+
* function defaultFootnoteBackLabel(referenceIndex, rereferenceIndex) {
91+
* return (
92+
* 'Back to reference ' +
93+
* (referenceIndex + 1) +
94+
* (rereferenceIndex > 1 ? '-' + rereferenceIndex : '')
95+
* )
96+
* }
97+
* ```
98+
*
99+
* Change it when the markdown is not in English.
100+
*
101+
* This label is used in the `ariaLabel` property on each backreference
102+
* (the `↩` links).
103+
* It affects users of assistive technology.
104+
* @property {string | null | undefined} [footnoteLabel='Footnotes']
105+
* Textual label to use for the footnotes section (default: `'Footnotes'`).
106+
*
107+
* Change it when the markdown is not in English.
108+
*
109+
* This label is typically hidden visually (assuming a `sr-only` CSS class
110+
* is defined that does that) and so affects screen readers only.
111+
* If you do have such a class, but want to show this section to everyone,
112+
* pass different properties with the `footnoteLabelProperties` option.
113+
* @property {HastProperties | null | undefined} [footnoteLabelProperties={className: ['sr-only']}]
114+
* Properties to use on the footnote label (default: `{className:
115+
* ['sr-only']}`).
116+
*
117+
* Change it to show the label and add other properties.
118+
*
119+
* This label is typically hidden visually (assuming an `sr-only` CSS class
120+
* is defined that does that) and so affects screen readers only.
121+
* If you do have such a class, but want to show this section to everyone,
122+
* pass an empty string.
123+
* You can also add different properties.
124+
*
125+
* > 👉 **Note**: `id: 'footnote-label'` is always added, because footnote
126+
* > calls use it with `aria-describedby` to provide an accessible label.
127+
* @property {string | null | undefined} [footnoteLabelTagName='h2']
128+
* HTML tag name to use for the footnote label element (default: `'h2'`).
129+
*
130+
* Change it to match your document structure.
131+
*
132+
* This label is typically hidden visually (assuming a `sr-only` CSS class
133+
* is defined that does that) and so affects screen readers only.
134+
* If you do have such a class, but want to show this section to everyone,
135+
* pass different properties with the `footnoteLabelProperties` option.
136+
* @property {Handlers | null | undefined} [handlers]
137+
* Extra handlers for nodes (optional).
138+
* @property {Array<string> | null | undefined} [passThrough]
139+
* List of custom mdast node types to pass through (keep) in hast (note that
140+
* the node itself is passed, but eventual children are transformed)
141+
* (optional).
142+
* @property {Handler | null | undefined} [unknownHandler]
143+
* Handler for all unknown nodes (optional).
144+
*
27145
* @typedef State
28146
* Info passed around.
29147
* @property {(node: MdastNodes) => Array<HastElementContent>} all
@@ -48,38 +166,6 @@
48166
* Copy a node’s positional info.
49167
* @property {<Type extends HastRootContent>(nodes: Array<Type>, loose?: boolean | undefined) => Array<HastText | Type>} wrap
50168
* Wrap `nodes` with line endings between each node, adds initial/final line endings when `loose`.
51-
*
52-
* @typedef Options
53-
* Configuration (optional).
54-
* @property {boolean | null | undefined} [allowDangerousHtml=false]
55-
* Whether to persist raw HTML in markdown in the hast tree (default:
56-
* `false`).
57-
* @property {string | null | undefined} [clobberPrefix='user-content-']
58-
* Prefix to use before the `id` attribute on footnotes to prevent it from
59-
* *clobbering*(default: `'user-content-'`).
60-
* @property {string | null | undefined} [footnoteBackLabel='Back to content']
61-
* Label to use from backreferences back to their footnote call (affects
62-
* screen readers) (default: `'Back to content'`).
63-
* @property {string | null | undefined} [footnoteLabel='Footnotes']
64-
* Label to use for the footnotes section (affects screen readers) (default:
65-
* `'Footnotes'`).
66-
* @property {HastProperties | null | undefined} [footnoteLabelProperties={className: ['sr-only']}]
67-
* Properties to use on the footnote label (note that `id: 'footnote-label'`
68-
* is always added as footnote calls use it with `aria-describedby` to
69-
* provide an accessible label) (default: `{className: ['sr-only']}`).
70-
* @property {string | null | undefined} [footnoteLabelTagName='h2']
71-
* Tag name to use for the footnote label (default: `'h2'`).
72-
* @property {Handlers | null | undefined} [handlers]
73-
* Extra handlers for nodes (optional).
74-
* @property {Array<string> | null | undefined} [passThrough]
75-
* List of custom mdast node types to pass through (keep) in hast (note that
76-
* the node itself is passed, but eventual children are transformed)
77-
* (optional).
78-
* @property {Handler | null | undefined} [unknownHandler]
79-
* Handler for all unknown nodes (optional).
80-
*
81-
* @typedef {Record<string, Handler>} Handlers
82-
* Handle nodes.
83169
*/
84170

85171
import {visit} from 'unist-util-visit'
@@ -88,6 +174,9 @@ import {handlers as defaultHandlers} from './handlers/index.js'
88174

89175
const own = {}.hasOwnProperty
90176

177+
/** @type {Options} */
178+
const emptyOptions = {}
179+
91180
/**
92181
* Create `state` from an mdast tree.
93182
*
@@ -99,7 +188,7 @@ const own = {}.hasOwnProperty
99188
* `state` function.
100189
*/
101190
export function createState(tree, options) {
102-
const settings = options || {}
191+
const settings = options || emptyOptions
103192
/** @type {Map<string, MdastDefinition>} */
104193
const definitionById = new Map()
105194
/** @type {Map<string, MdastFootnoteDefinition>} */

‎readme.md

+150-14
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
* [Install](#install)
1818
* [Use](#use)
1919
* [API](#api)
20+
* [`defaultFootnoteBackContent(referenceIndex, rereferenceIndex)`](#defaultfootnotebackcontentreferenceindex-rereferenceindex)
21+
* [`defaultFootnoteBackLabel(referenceIndex, rereferenceIndex)`](#defaultfootnotebacklabelreferenceindex-rereferenceindex)
2022
* [`defaultHandlers`](#defaulthandlers)
2123
* [`toHast(tree[, options])`](#tohasttree-options)
24+
* [`FootnoteBackContentTemplate`](#footnotebackcontenttemplate)
25+
* [`FootnoteBackLabelTemplate`](#footnotebacklabeltemplate)
2226
* [`Handler`](#handler)
2327
* [`Handlers`](#handlers)
2428
* [`Options`](#options)
@@ -114,10 +118,45 @@ console.log(html)
114118

115119
## API
116120

117-
This package exports the identifiers [`defaultHandlers`][api-default-handlers]
118-
and [`toHast`][api-to-hast].
121+
This package exports the identifiers
122+
[`defaultFootnoteBackContent`][api-default-footnote-back-content],
123+
[`defaultFootnoteBackLabel`][api-default-footnote-back-label],
124+
[`defaultHandlers`][api-default-handlers], and
125+
[`toHast`][api-to-hast].
119126
There is no default export.
120127

128+
### `defaultFootnoteBackContent(referenceIndex, rereferenceIndex)`
129+
130+
Generate the default content that GitHub uses on backreferences.
131+
132+
###### Parameters
133+
134+
* `referenceIndex` (`number`)
135+
— index of the definition in the order that they are first referenced,
136+
0-indexed
137+
* `rereferenceIndex` (`number`)
138+
— index of calls to the same definition, 0-indexed
139+
140+
###### Returns
141+
142+
Content (`Array<ElementContent>`).
143+
144+
### `defaultFootnoteBackLabel(referenceIndex, rereferenceIndex)`
145+
146+
Generate the default label that GitHub uses on backreferences.
147+
148+
###### Parameters
149+
150+
* `referenceIndex` (`number`)
151+
— index of the definition in the order that they are first referenced,
152+
0-indexed
153+
* `rereferenceIndex` (`number`)
154+
— index of calls to the same definition, 0-indexed
155+
156+
###### Returns
157+
158+
Label (`string`).
159+
121160
### `defaultHandlers`
122161

123162
Default handlers for nodes ([`Handlers`][api-handlers]).
@@ -204,6 +243,76 @@ The default behavior for unknown nodes is:
204243

205244
This behavior can be changed by passing an `unknownHandler`.
206245

246+
### `FootnoteBackContentTemplate`
247+
248+
Generate content for the backreference dynamically.
249+
250+
For the following markdown:
251+
252+
```markdown
253+
Alpha[^micromark], bravo[^micromark], and charlie[^remark].
254+
255+
[^remark]: things about remark
256+
[^micromark]: things about micromark
257+
```
258+
259+
This function will be called with:
260+
261+
* `0` and `0` for the backreference from `things about micromark` to
262+
`alpha`, as it is the first used definition, and the first call to it
263+
* `0` and `1` for the backreference from `things about micromark` to
264+
`bravo`, as it is the first used definition, and the second call to it
265+
* `1` and `0` for the backreference from `things about remark` to
266+
`charlie`, as it is the second used definition
267+
268+
###### Parameters
269+
270+
* `referenceIndex` (`number`)
271+
— index of the definition in the order that they are first referenced,
272+
0-indexed
273+
* `rereferenceIndex` (`number`)
274+
— index of calls to the same definition, 0-indexed
275+
276+
###### Returns
277+
278+
Content for the backreference when linking back from definitions to their
279+
reference (`Array<ElementContent>`, `ElementContent`, or `string`).
280+
281+
### `FootnoteBackLabelTemplate`
282+
283+
Generate a back label dynamically.
284+
285+
For the following markdown:
286+
287+
```markdown
288+
Alpha[^micromark], bravo[^micromark], and charlie[^remark].
289+
290+
[^remark]: things about remark
291+
[^micromark]: things about micromark
292+
```
293+
294+
This function will be called with:
295+
296+
* `0` and `0` for the backreference from `things about micromark` to
297+
`alpha`, as it is the first used definition, and the first call to it
298+
* `0` and `1` for the backreference from `things about micromark` to
299+
`bravo`, as it is the first used definition, and the second call to it
300+
* `1` and `0` for the backreference from `things about remark` to
301+
`charlie`, as it is the second used definition
302+
303+
###### Parameters
304+
305+
* `referenceIndex` (`number`)
306+
— index of the definition in the order that they are first referenced,
307+
0-indexed
308+
* `rereferenceIndex` (`number`)
309+
— index of calls to the same definition, 0-indexed
310+
311+
###### Returns
312+
313+
Back label to use when linking back from definitions to their reference
314+
(`string`).
315+
207316
### `Handler`
208317

209318
Handle a node (TypeScript).
@@ -240,11 +349,18 @@ Configuration (TypeScript).
240349
* `allowDangerousHtml` (`boolean`, default: `false`)
241350
— whether to persist raw HTML in markdown in the hast tree
242351
* `clobberPrefix` (`string`, default: `'user-content-'`)
243-
— prefix to use before the `id` attribute on footnotes to prevent it from
352+
— prefix to use before the `id` property on footnotes to prevent them from
244353
*clobbering*
245-
* `footnoteBackLabel` (`string`, default: `'Back to content'`)
246-
— label to use from backreferences back to their footnote call (affects
247-
screen readers)
354+
* `footnoteBackContent`
355+
([`FootnoteBackContentTemplate`][api-footnote-back-content-template]
356+
or `string`, default:
357+
[`defaultFootnoteBackContent`][api-default-footnote-back-content])
358+
— content of the backreference back to references
359+
* `footnoteBackLabel`
360+
([`FootnoteBackLabelTemplate`][api-footnote-back-label-template]
361+
or `string`, default:
362+
[`defaultFootnoteBackLabel`][api-default-footnote-back-label])
363+
— label to describe the backreference back to references
248364
* `footnoteLabel` (`string`, default: `'Footnotes'`)
249365
— label to use for the footnotes section (affects screen readers)
250366
* `footnoteLabelProperties`
@@ -408,7 +524,7 @@ console.log(html)
408524
<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
409525
<ol>
410526
<li id="user-content-fn-1">
411-
<p>Monde! <a href="#user-content-fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a></p>
527+
<p>Monde! <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
412528
</li>
413529
</ol>
414530
</section>
@@ -420,14 +536,20 @@ In that case, it’s important to translate and define the labels relating to
420536
footnotes so that screen reader users can properly pronounce the page:
421537

422538
```diff
423-
@@ -9,7 +9,10 @@ const mdast = fromMarkdown(markdown, {
539+
@@ -9,7 +9,16 @@ const mdast = fromMarkdown(markdown, {
424540
extensions: [gfm()],
425541
mdastExtensions: [gfmFromMarkdown()]
426542
})
427543
-const hast = toHast(mdast)
428544
+const hast = toHast(mdast, {
429545
+ footnoteLabel: 'Notes de bas de page',
430-
+ footnoteBackLabel: 'Arrière'
546+
+ footnoteBackLabel(referenceIndex, rereferenceIndex) {
547+
+ return (
548+
+ 'Retour à la référence ' +
549+
+ (referenceIndex + 1) +
550+
+ (rereferenceIndex > 1 ? '-' + rereferenceIndex : '')
551+
+ )
552+
+ }
431553
+})
432554
const html = toHtml(hast)
433555

@@ -443,8 +565,8 @@ footnotes so that screen reader users can properly pronounce the page:
443565
+<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Notes de bas de page</h2>
444566
<ol>
445567
<li id="user-content-fn-1">
446-
-<p>Monde! <a href="#user-content-fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a></p>
447-
+<p>Monde! <a href="#user-content-fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Arrière">↩</a></p>
568+
-<p>Monde! <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
569+
+<p>Monde! <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Retour à la référence 1" class="data-footnote-backref">↩</a></p>
448570
</li>
449571
</ol>
450572
</section>
@@ -1344,8 +1466,14 @@ Raw nodes are typically ignored but are handled by
13441466
## Types
13451467

13461468
This package is fully typed with [TypeScript][].
1347-
It exports the [`Handler`][api-handler], [`Handlers`][api-handlers],
1348-
[`Options`][api-options], [`Raw`][api-raw], and [`State`][api-state] types.
1469+
It exports the
1470+
[`FootnoteBackContentTemplate`][api-footnote-back-content-template],
1471+
[`FootnoteBackLabelTemplate`][api-footnote-back-label-template],
1472+
[`Handler`][api-handler],
1473+
[`Handlers`][api-handlers],
1474+
[`Options`][api-options],
1475+
[`Raw`][api-raw], and
1476+
[`State`][api-state] types.
13491477

13501478
It also registers the `Raw` node type with `@types/hast`.
13511479
If you’re working with the syntax tree (and you pass
@@ -1567,9 +1695,15 @@ abide by its terms.
15671695
15681696
[dfn-literal]: https://github.com/syntax-tree/hast#literal
15691697
1698+
[api-default-footnote-back-content]: #defaultfootnotebackcontentreferenceindex-rereferenceindex
1699+
1700+
[api-default-footnote-back-label]: #defaultfootnotebacklabelreferenceindex-rereferenceindex
1701+
15701702
[api-default-handlers]: #defaulthandlers
15711703
1572-
[api-to-hast]: #tohasttree-options
1704+
[api-footnote-back-content-template]: #footnotebackcontenttemplate
1705+
1706+
[api-footnote-back-label-template]: #footnotebacklabeltemplate
15731707
15741708
[api-handler]: #handler
15751709
@@ -1580,3 +1714,5 @@ abide by its terms.
15801714
[api-raw]: #raw
15811715
15821716
[api-state]: #state
1717+
1718+
[api-to-hast]: #tohasttree-options

‎test/core.js

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {toHast} from '../index.js'
1212
test('toHast', async function (t) {
1313
await t.test('should expose the public api', async function () {
1414
assert.deepEqual(Object.keys(await import('../index.js')).sort(), [
15+
'defaultFootnoteBackContent',
16+
'defaultFootnoteBackLabel',
1517
'defaultHandlers',
1618
'toHast'
1719
])

‎test/footnote-definition.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ test('footnoteDefinition', async function (t) {
6363
'a',
6464
{
6565
href: '#user-content-fnref-charlie',
66-
dataFootnoteBackref: true,
67-
className: ['data-footnote-backref'],
68-
ariaLabel: 'Back to content'
66+
dataFootnoteBackref: '',
67+
ariaLabel: 'Back to reference 1',
68+
className: ['data-footnote-backref']
6969
},
7070
'↩'
7171
)
@@ -138,9 +138,9 @@ test('footnoteDefinition', async function (t) {
138138
'a',
139139
{
140140
href: '#user-content-fnref-echo',
141-
dataFootnoteBackref: true,
142-
className: ['data-footnote-backref'],
143-
ariaLabel: 'Back to content'
141+
dataFootnoteBackref: '',
142+
ariaLabel: 'Back to reference 1',
143+
className: ['data-footnote-backref']
144144
},
145145
'↩'
146146
)

‎test/footnote.js

+118-12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
* @typedef {import('hast').ElementContent} ElementContent
3+
*/
4+
15
import assert from 'node:assert/strict'
26
import test from 'node:test'
37
import {toHtml} from 'hast-util-to-html'
@@ -40,7 +44,7 @@ test('footnote', async function (t) {
4044
<blockquote>
4145
<p>delta</p>
4246
</blockquote>
43-
<a href="#user-content-fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a>
47+
<a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a>
4448
</li>
4549
</ol>
4650
</section>`
@@ -82,10 +86,10 @@ test('footnote', async function (t) {
8286
<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
8387
<ol>
8488
<li id="user-content-fn-1">
85-
<p>a <a href="#user-content-fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a></p>
89+
<p>a <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
8690
</li>
8791
<li id="user-content-fn-2">
88-
<p>b <a href="#user-content-fnref-2" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a></p>
92+
<p>b <a href="#user-content-fnref-2" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
8993
</li>
9094
</ol>
9195
</section>`
@@ -118,10 +122,10 @@ test('footnote', async function (t) {
118122
<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
119123
<ol>
120124
<li id="user-content-fn-1">
121-
<p>a <a href="#user-content-fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a></p>
125+
<p>a <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
122126
</li>
123127
<li id="user-content-fn-2">
124-
<p>b <a href="#user-content-fnref-2" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a></p>
128+
<p>b <a href="#user-content-fnref-2" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
125129
</li>
126130
</ol>
127131
</section>`
@@ -145,7 +149,7 @@ test('footnote', async function (t) {
145149
<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
146150
<ol>
147151
<li id="user-content-fn-1">
148-
<p>Recursion<sup><a href="#user-content-fn-1" id="user-content-fnref-1-3" data-footnote-ref aria-describedby="footnote-label">1</a></sup><sup><a href="#user-content-fn-1" id="user-content-fnref-1-4" data-footnote-ref aria-describedby="footnote-label">1</a></sup> <a href="#user-content-fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a> <a href="#user-content-fnref-1-2" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩<sup>2</sup></a> <a href="#user-content-fnref-1-3" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩<sup>3</sup></a> <a href="#user-content-fnref-1-4" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩<sup>4</sup></a></p>
152+
<p>Recursion<sup><a href="#user-content-fn-1" id="user-content-fnref-1-3" data-footnote-ref aria-describedby="footnote-label">1</a></sup><sup><a href="#user-content-fn-1" id="user-content-fnref-1-4" data-footnote-ref aria-describedby="footnote-label">1</a></sup> <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a> <a href="#user-content-fnref-1-2" data-footnote-backref="" aria-label="Back to reference 1-2" class="data-footnote-backref">↩<sup>2</sup></a> <a href="#user-content-fnref-1-3" data-footnote-backref="" aria-label="Back to reference 1-3" class="data-footnote-backref">↩<sup>3</sup></a> <a href="#user-content-fnref-1-4" data-footnote-backref="" aria-label="Back to reference 1-4" class="data-footnote-backref">↩<sup>4</sup></a></p>
149153
</li>
150154
</ol>
151155
</section>`
@@ -171,7 +175,109 @@ test('footnote', async function (t) {
171175
<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Voetnoten</h2>
172176
<ol>
173177
<li id="user-content-fn-1">
174-
<p>a <a href="#user-content-fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Terug naar de inhoud">↩</a></p>
178+
<p>a <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Terug naar de inhoud" class="data-footnote-backref">↩</a></p>
179+
</li>
180+
</ol>
181+
</section>`
182+
)
183+
}
184+
)
185+
186+
await t.test(
187+
'should support `footnoteBackLabel` as a function',
188+
async function () {
189+
assert.equal(
190+
toHtml(
191+
// @ts-expect-error: to do: remove when `to-html` is released.
192+
toHast(
193+
fromMarkdown('[^1]\n[^1]: a', {
194+
extensions: [gfm()],
195+
mdastExtensions: [gfmFromMarkdown()]
196+
}),
197+
{
198+
footnoteBackLabel(referenceIndex, rereferenceIndex) {
199+
return (
200+
'Terug naar referentie ' +
201+
(referenceIndex + 1) +
202+
(rereferenceIndex > 1 ? '-' + rereferenceIndex : '')
203+
)
204+
}
205+
}
206+
)
207+
),
208+
`<p><sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
209+
<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
210+
<ol>
211+
<li id="user-content-fn-1">
212+
<p>a <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Terug naar referentie 1" class="data-footnote-backref">↩</a></p>
213+
</li>
214+
</ol>
215+
</section>`
216+
)
217+
}
218+
)
219+
220+
await t.test(
221+
'should support `footnoteBackContent` as `string`',
222+
async function () {
223+
assert.equal(
224+
toHtml(
225+
// @ts-expect-error: to do: remove when `to-html` is released.
226+
toHast(
227+
fromMarkdown('[^1]\n[^1]: a', {
228+
extensions: [gfm()],
229+
mdastExtensions: [gfmFromMarkdown()]
230+
}),
231+
{footnoteBackContent: '⬆️'}
232+
)
233+
),
234+
`<p><sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
235+
<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
236+
<ol>
237+
<li id="user-content-fn-1">
238+
<p>a <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">⬆️</a></p>
239+
</li>
240+
</ol>
241+
</section>`
242+
)
243+
}
244+
)
245+
246+
await t.test(
247+
'should support `footnoteBackContent` as a function`',
248+
async function () {
249+
assert.equal(
250+
toHtml(
251+
// @ts-expect-error: to do: remove when `to-html` is released.
252+
toHast(
253+
fromMarkdown('[^1]\n[^1]: a', {
254+
extensions: [gfm()],
255+
mdastExtensions: [gfmFromMarkdown()]
256+
}),
257+
{
258+
footnoteBackContent(_, rereferenceIndex) {
259+
/** @type {Array<ElementContent>} */
260+
const result = [{type: 'text', value: '⬆️'}]
261+
262+
if (rereferenceIndex > 1) {
263+
result.push({
264+
type: 'element',
265+
tagName: 'sup',
266+
properties: {},
267+
children: [{type: 'text', value: String(rereferenceIndex)}]
268+
})
269+
}
270+
271+
return result
272+
}
273+
}
274+
)
275+
),
276+
`<p><sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref aria-describedby="footnote-label">1</a></sup></p>
277+
<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
278+
<ol>
279+
<li id="user-content-fn-1">
280+
<p>a <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">⬆️</a></p>
175281
</li>
176282
</ol>
177283
</section>`
@@ -195,7 +301,7 @@ test('footnote', async function (t) {
195301
<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
196302
<ol>
197303
<li id="fn-1">
198-
<p>a <a href="#fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a></p>
304+
<p>a <a href="#fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
199305
</li>
200306
</ol>
201307
</section>`
@@ -218,7 +324,7 @@ test('footnote', async function (t) {
218324
<section data-footnotes class="footnotes"><h1 class="sr-only" id="footnote-label">Footnotes</h1>
219325
<ol>
220326
<li id="user-content-fn-1">
221-
<p>a <a href="#user-content-fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a></p>
327+
<p>a <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
222328
</li>
223329
</ol>
224330
</section>`
@@ -241,7 +347,7 @@ test('footnote', async function (t) {
241347
<section data-footnotes class="footnotes"><h2 id="footnote-label">Footnotes</h2>
242348
<ol>
243349
<li id="user-content-fn-1">
244-
<p>a <a href="#user-content-fnref-1" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a></p>
350+
<p>a <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
245351
</li>
246352
</ol>
247353
</section>`
@@ -266,10 +372,10 @@ test('footnote', async function (t) {
266372
<section data-footnotes class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
267373
<ol>
268374
<li id="user-content-fn-__proto__">
269-
<p>d <a href="#user-content-fnref-__proto__" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a> <a href="#user-content-fnref-__proto__-2" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩<sup>2</sup></a></p>
375+
<p>d <a href="#user-content-fnref-__proto__" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a> <a href="#user-content-fnref-__proto__-2" data-footnote-backref="" aria-label="Back to reference 1-2" class="data-footnote-backref">↩<sup>2</sup></a></p>
270376
</li>
271377
<li id="user-content-fn-constructor">
272-
<p>e <a href="#user-content-fnref-constructor" data-footnote-backref class="data-footnote-backref" aria-label="Back to content">↩</a></p>
378+
<p>e <a href="#user-content-fnref-constructor" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
273379
</li>
274380
</ol>
275381
</section>`

0 commit comments

Comments
 (0)
Please sign in to comment.