Skip to content

Commit 56c88e4

Browse files
committed
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

File tree

8 files changed

+548
-94
lines changed

8 files changed

+548
-94
lines changed

index.d.ts

Lines changed: 8 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 138 additions & 29 deletions
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]

0 commit comments

Comments
 (0)