micromark-extension-gfm-footnote
Advanced tools
Comparing version 1.0.4 to 1.1.0
export {gfmFootnote} from './lib/syntax.js' | ||
export {gfmFootnoteHtml} from './lib/html.js' | ||
export type BackLabelTemplate = import('./lib/html.js').BackLabelTemplate | ||
export type HtmlOptions = import('./lib/html.js').Options | ||
export {gfmFootnoteHtml, defaultBackLabel} from './lib/html.js' |
/** | ||
* @typedef {import('./lib/html.js').BackLabelTemplate} BackLabelTemplate | ||
* @typedef {import('./lib/html.js').Options} HtmlOptions | ||
@@ -6,2 +7,2 @@ */ | ||
export {gfmFootnote} from './lib/syntax.js' | ||
export {gfmFootnoteHtml} from './lib/html.js' | ||
export {gfmFootnoteHtml, defaultBackLabel} from './lib/html.js' |
/** | ||
* @param {Options} [options={}] | ||
* Generate the default label that GitHub uses on backreferences. | ||
* | ||
* @param {number} referenceIndex | ||
* Index of the definition in the order that they are first referenced, | ||
* 0-indexed. | ||
* @param {number} rereferenceIndex | ||
* Index of calls to the same definition, 0-indexed. | ||
* @returns {string} | ||
* Default label. | ||
*/ | ||
export function defaultBackLabel( | ||
referenceIndex: number, | ||
rereferenceIndex: number | ||
): string | ||
/** | ||
* Create an extension for `micromark` to support GFM footnotes when | ||
* serializing to HTML. | ||
* | ||
* @param {Options | null | undefined} [options] | ||
* Configuration. | ||
* @returns {HtmlExtension} | ||
* Extension for `micromark` that can be passed in `htmlExtensions` to | ||
* support GFM footnotes when serializing to HTML. | ||
*/ | ||
export function gfmFootnoteHtml(options?: Options | undefined): HtmlExtension | ||
export function gfmFootnoteHtml( | ||
options?: Options | null | undefined | ||
): HtmlExtension | ||
export type HtmlExtension = import('micromark-util-types').HtmlExtension | ||
export type CompileContext = import('micromark-util-types').CompileContext | ||
/** | ||
* Generate a back label dynamically. | ||
* | ||
* For the following markdown: | ||
* | ||
* ```markdown | ||
* Alpha[^micromark], bravo[^micromark], and charlie[^remark]. | ||
* | ||
* [^remark]: things about remark | ||
* [^micromark]: things about micromark | ||
* ``` | ||
* | ||
* This function will be called with: | ||
* | ||
* * `0` and `0` for the backreference from `things about micromark` to | ||
* `alpha`, as it is the first used definition, and the first call to it | ||
* * `0` and `1` for the backreference from `things about micromark` to | ||
* `bravo`, as it is the first used definition, and the second call to it | ||
* * `1` and `0` for the backreference from `things about remark` to | ||
* `charlie`, as it is the second used definition | ||
*/ | ||
export type BackLabelTemplate = ( | ||
referenceIndex: number, | ||
rereferenceIndex: number | ||
) => string | ||
/** | ||
* Configuration. | ||
*/ | ||
export type Options = { | ||
/** | ||
* Prefix to use before the `id` attribute to prevent it from *clobbering*. | ||
* attributes. | ||
* Prefix to use before the `id` attribute on footnotes to prevent them from | ||
* *clobbering*. | ||
* | ||
* The default is `'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)</script> | ||
* <p id="x"></p> | ||
* <script>alert(x) // `x` now refers to the `p#x` DOM element</script> | ||
* ``` | ||
* | ||
* Elements by their ID are made available in browsers on the `window` object. | ||
* Using a prefix prevents this from being a problem. | ||
* 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. | ||
*/ | ||
clobberPrefix?: string | undefined | ||
clobberPrefix?: string | ||
/** | ||
* Label to use for the footnotes section. | ||
* Affects screen reader users. | ||
* Change it if you’re authoring in a different language. | ||
* Textual label to use for the footnotes section. | ||
* | ||
* The default value is `'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 attributes with the `labelAttributes` option. | ||
*/ | ||
label?: string | undefined | ||
label?: string | ||
/** | ||
* Label to use from backreferences back to their footnote call. | ||
* Affects screen reader users. | ||
* Change it if you’re authoring in a different language. | ||
* Attributes to use on the footnote label. | ||
* | ||
* Change it to show the label and add other attributes. | ||
* | ||
* 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 attributes. | ||
* | ||
* > 👉 **Note**: `id="footnote-label"` is always added, because footnote | ||
* > calls use it with `aria-describedby` to provide an accessible label. | ||
*/ | ||
backLabel?: string | undefined | ||
labelAttributes?: string | ||
/** | ||
* HTML tag name to use for the footnote label element. | ||
* | ||
* 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 attributes with the `labelAttributes` option. | ||
*/ | ||
labelTagName?: string | ||
/** | ||
* Textual label to describe the backreference back to references. | ||
* | ||
* The default value is: | ||
* | ||
* ```js | ||
* function defaultBackLabel(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 `aria-label` attribute on each backreference | ||
* (the `↩` links). | ||
* It affects users of assistive technology. | ||
*/ | ||
backLabel?: BackLabelTemplate | string | ||
} |
/** | ||
* @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension | ||
* @typedef {import('micromark-util-types').CompileContext} CompileContext | ||
*/ | ||
/** | ||
* @callback BackLabelTemplate | ||
* Generate a back label dynamically. | ||
* | ||
* For the following markdown: | ||
* | ||
* ```markdown | ||
* Alpha[^micromark], bravo[^micromark], and charlie[^remark]. | ||
* | ||
* [^remark]: things about remark | ||
* [^micromark]: things about micromark | ||
* ``` | ||
* | ||
* This function will be called with: | ||
* | ||
* * `0` and `0` for the backreference from `things about micromark` to | ||
* `alpha`, as it is the first used definition, and the first call to it | ||
* * `0` and `1` for the backreference from `things about micromark` to | ||
* `bravo`, as it is the first used definition, and the second call to it | ||
* * `1` and `0` for the backreference from `things about remark` to | ||
* `charlie`, as it is the second used definition | ||
* @param {number} referenceIndex | ||
* Index of the definition in the order that they are first referenced, | ||
* 0-indexed. | ||
* @param {number} rereferenceIndex | ||
* Index of calls to the same definition, 0-indexed. | ||
* @returns {string} | ||
* Back label to use when linking back from definitions to their reference. | ||
*/ | ||
/** | ||
* @typedef Options | ||
* Configuration. | ||
* @property {string} [clobberPrefix='user-content-'] | ||
* Prefix to use before the `id` attribute to prevent it from *clobbering*. | ||
* attributes. | ||
* Prefix to use before the `id` attribute on footnotes to prevent them from | ||
* *clobbering*. | ||
* | ||
* The default is `'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)</script> | ||
* <p id="x"></p> | ||
* <script>alert(x) // `x` now refers to the `p#x` DOM element</script> | ||
* ``` | ||
* | ||
* Elements by their ID are made available in browsers on the `window` object. | ||
* Using a prefix prevents this from being a problem. | ||
* 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 {string} [label='Footnotes'] | ||
* Label to use for the footnotes section. | ||
* Affects screen reader users. | ||
* Change it if you’re authoring in a different language. | ||
* @property {string} [backLabel='Back to content'] | ||
* Label to use from backreferences back to their footnote call. | ||
* Affects screen reader users. | ||
* Change it if you’re authoring in a different language. | ||
* Textual label to use for the footnotes section. | ||
* | ||
* The default value is `'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 attributes with the `labelAttributes` option. | ||
* @property {string} [labelAttributes='class="sr-only"'] | ||
* Attributes to use on the footnote label. | ||
* | ||
* Change it to show the label and add other attributes. | ||
* | ||
* 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 attributes. | ||
* | ||
* > 👉 **Note**: `id="footnote-label"` is always added, because footnote | ||
* > calls use it with `aria-describedby` to provide an accessible label. | ||
* @property {string} [labelTagName='h2'] | ||
* HTML tag name to use for the footnote label element. | ||
* | ||
* 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 attributes with the `labelAttributes` option. | ||
* @property {BackLabelTemplate | string} [backLabel] | ||
* Textual label to describe the backreference back to references. | ||
* | ||
* The default value is: | ||
* | ||
* ```js | ||
* function defaultBackLabel(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 `aria-label` attribute on each backreference | ||
* (the `↩` links). | ||
* It affects users of assistive technology. | ||
*/ | ||
@@ -34,13 +120,47 @@ | ||
/** @type {Options} */ | ||
const emptyOptions = {} | ||
/** | ||
* @param {Options} [options={}] | ||
* Generate the default label that GitHub uses on backreferences. | ||
* | ||
* @param {number} referenceIndex | ||
* Index of the definition in the order that they are first referenced, | ||
* 0-indexed. | ||
* @param {number} rereferenceIndex | ||
* Index of calls to the same definition, 0-indexed. | ||
* @returns {string} | ||
* Default label. | ||
*/ | ||
export function defaultBackLabel(referenceIndex, rereferenceIndex) { | ||
return ( | ||
'Back to reference ' + | ||
(referenceIndex + 1) + | ||
(rereferenceIndex > 1 ? '-' + rereferenceIndex : '') | ||
) | ||
} | ||
/** | ||
* Create an extension for `micromark` to support GFM footnotes when | ||
* serializing to HTML. | ||
* | ||
* @param {Options | null | undefined} [options] | ||
* Configuration. | ||
* @returns {HtmlExtension} | ||
* Extension for `micromark` that can be passed in `htmlExtensions` to | ||
* support GFM footnotes when serializing to HTML. | ||
*/ | ||
export function gfmFootnoteHtml(options = {}) { | ||
const label = options.label || 'Footnotes' | ||
const backLabel = options.backLabel || 'Back to content' | ||
export function gfmFootnoteHtml(options) { | ||
const config = options || emptyOptions | ||
const label = config.label || 'Footnotes' | ||
const labelTagName = config.labelTagName || 'h2' | ||
const labelAttributes = | ||
config.labelAttributes === null || config.labelAttributes === undefined | ||
? 'class="sr-only"' | ||
: config.labelAttributes | ||
const backLabel = config.backLabel || defaultBackLabel | ||
const clobberPrefix = | ||
options.clobberPrefix === undefined || options.clobberPrefix === null | ||
config.clobberPrefix === null || config.clobberPrefix === undefined | ||
? 'user-content-' | ||
: options.clobberPrefix | ||
: config.clobberPrefix | ||
return { | ||
@@ -160,6 +280,10 @@ enter: { | ||
this.tag( | ||
'<section data-footnotes="" class="footnotes"><h2 id="footnote-label" class="sr-only">' | ||
'<section data-footnotes="" class="footnotes"><' + | ||
labelTagName + | ||
' id="footnote-label"' + | ||
(labelAttributes ? ' ' + labelAttributes : '') + | ||
'>' | ||
) | ||
this.raw(this.encode(label)) | ||
this.tag('</h2>') | ||
this.tag('</' + labelTagName + '>') | ||
this.lineEndingIfNeeded() | ||
@@ -184,5 +308,9 @@ this.tag('<ol>') | ||
(referenceIndex > 1 ? '-' + referenceIndex : '') + | ||
'" data-footnote-backref="" class="data-footnote-backref" aria-label="' + | ||
this.encode(backLabel) + | ||
'">↩' + | ||
'" data-footnote-backref="" aria-label="' + | ||
this.encode( | ||
typeof backLabel === 'string' | ||
? backLabel | ||
: backLabel(index, referenceIndex) | ||
) + | ||
'" class="data-footnote-backref">↩' + | ||
(referenceIndex > 1 | ||
@@ -189,0 +317,0 @@ ? '<sup>' + referenceIndex + '</sup>' |
/** | ||
* Create an extension for `micromark` to enable GFM footnote syntax. | ||
* | ||
* @returns {Extension} | ||
* Extension for `micromark` that can be passed in `extensions` to | ||
* enable GFM footnote syntax. | ||
*/ | ||
export function gfmFootnote(): Extension | ||
export type Event = import('micromark-util-types').Event | ||
export type Exiter = import('micromark-util-types').Exiter | ||
export type Extension = import('micromark-util-types').Extension | ||
export type Resolver = import('micromark-util-types').Resolver | ||
export type State = import('micromark-util-types').State | ||
export type Token = import('micromark-util-types').Token | ||
export type TokenizeContext = import('micromark-util-types').TokenizeContext | ||
export type Tokenizer = import('micromark-util-types').Tokenizer | ||
export type Exiter = import('micromark-util-types').Exiter | ||
export type State = import('micromark-util-types').State | ||
export type Event = import('micromark-util-types').Event |
/** | ||
* @typedef {import('micromark-util-types').Event} Event | ||
* @typedef {import('micromark-util-types').Exiter} Exiter | ||
* @typedef {import('micromark-util-types').Extension} Extension | ||
* @typedef {import('micromark-util-types').Resolver} Resolver | ||
* @typedef {import('micromark-util-types').State} State | ||
* @typedef {import('micromark-util-types').Token} Token | ||
* @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext | ||
* @typedef {import('micromark-util-types').Tokenizer} Tokenizer | ||
* @typedef {import('micromark-util-types').Exiter} Exiter | ||
* @typedef {import('micromark-util-types').State} State | ||
* @typedef {import('micromark-util-types').Event} Event | ||
*/ | ||
@@ -14,6 +15,3 @@ | ||
import {factorySpace} from 'micromark-factory-space' | ||
import { | ||
markdownLineEnding, | ||
markdownLineEndingOrSpace | ||
} from 'micromark-util-character' | ||
import {markdownLineEndingOrSpace} from 'micromark-util-character' | ||
import {codes} from 'micromark-util-symbol/codes.js' | ||
@@ -26,4 +24,14 @@ import {constants} from 'micromark-util-symbol/constants.js' | ||
// To do: micromark should support a `_hiddenGfmFootnoteSupport`, which only | ||
// affects label start (image). | ||
// That will let us drop `tokenizePotentialGfmFootnote*`. | ||
// It currently has a `_hiddenFootnoteSupport`, which affects that and more. | ||
// That can be removed when `micromark-extension-footnote` is archived. | ||
/** | ||
* Create an extension for `micromark` to enable GFM footnote syntax. | ||
* | ||
* @returns {Extension} | ||
* Extension for `micromark` that can be passed in `extensions` to | ||
* enable GFM footnote syntax. | ||
*/ | ||
@@ -51,3 +59,7 @@ export function gfmFootnote() { | ||
/** @type {Tokenizer} */ | ||
// To do: remove after micromark update. | ||
/** | ||
* @this {TokenizeContext} | ||
* @type {Tokenizer} | ||
*/ | ||
function tokenizePotentialGfmFootnoteCall(effects, ok, nok) { | ||
@@ -85,3 +97,5 @@ const self = this | ||
/** @type {State} */ | ||
/** | ||
* @type {State} | ||
*/ | ||
function start(code) { | ||
@@ -98,3 +112,3 @@ assert(code === codes.rightSquareBracket, 'expected `]`') | ||
if (id.charCodeAt(0) !== codes.caret || !defined.includes(id.slice(1))) { | ||
if (id.codePointAt(0) !== codes.caret || !defined.includes(id.slice(1))) { | ||
return nok(code) | ||
@@ -110,6 +124,7 @@ } | ||
// To do: remove after micromark update. | ||
/** @type {Resolver} */ | ||
function resolveToPotentialGfmFootnoteCall(events, context) { | ||
let index = events.length | ||
/** @type {Token|undefined} */ | ||
/** @type {Token | undefined} */ | ||
let labelStart | ||
@@ -190,3 +205,6 @@ | ||
/** @type {Tokenizer} */ | ||
/** | ||
* @this {TokenizeContext} | ||
* @type {Tokenizer} | ||
*/ | ||
function tokenizeGfmFootnoteCall(effects, ok, nok) { | ||
@@ -201,5 +219,19 @@ const self = this | ||
// Note: the implementation of `markdown-rs` is different, because it houses | ||
// core *and* extensions in one project. | ||
// Therefore, it can include footnote logic inside `label-end`. | ||
// We can’t do that, but luckily, we can parse footnotes in a simpler way than | ||
// needed for labels. | ||
return start | ||
/** @type {State} */ | ||
/** | ||
* Start of footnote label. | ||
* | ||
* ```markdown | ||
* > | a [^b] c | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function start(code) { | ||
@@ -214,3 +246,12 @@ assert(code === codes.leftSquareBracket, 'expected `[`') | ||
/** @type {State} */ | ||
/** | ||
* After `[`, at `^`. | ||
* | ||
* ```markdown | ||
* > | a [^b] c | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function callStart(code) { | ||
@@ -227,11 +268,23 @@ if (code !== codes.caret) return nok(code) | ||
/** @type {State} */ | ||
/** | ||
* In label. | ||
* | ||
* ```markdown | ||
* > | a [^b] c | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function callData(code) { | ||
/** @type {Token} */ | ||
let token | ||
if ( | ||
// Too long. | ||
size > constants.linkReferenceSizeMax || | ||
// Closing brace with nothing. | ||
(code === codes.rightSquareBracket && !data) || | ||
// Space or tab is not supported by GFM for some reason. | ||
// `\n` and `[` not being supported makes sense. | ||
code === codes.eof || | ||
code === codes.leftSquareBracket || | ||
size++ > constants.linkReferenceSizeMax | ||
markdownLineEndingOrSpace(code) | ||
) { | ||
@@ -242,15 +295,16 @@ return nok(code) | ||
if (code === codes.rightSquareBracket) { | ||
if (!data) { | ||
effects.exit('chunkString') | ||
const token = effects.exit('gfmFootnoteCallString') | ||
if (!defined.includes(normalizeIdentifier(self.sliceSerialize(token)))) { | ||
return nok(code) | ||
} | ||
effects.exit('chunkString') | ||
token = effects.exit('gfmFootnoteCallString') | ||
return defined.includes(normalizeIdentifier(self.sliceSerialize(token))) | ||
? end(code) | ||
: nok(code) | ||
effects.enter('gfmFootnoteCallLabelMarker') | ||
effects.consume(code) | ||
effects.exit('gfmFootnoteCallLabelMarker') | ||
effects.exit('gfmFootnoteCall') | ||
return ok | ||
} | ||
effects.consume(code) | ||
if (!markdownLineEndingOrSpace(code)) { | ||
@@ -260,6 +314,17 @@ data = true | ||
size++ | ||
effects.consume(code) | ||
return code === codes.backslash ? callEscape : callData | ||
} | ||
/** @type {State} */ | ||
/** | ||
* On character after escape. | ||
* | ||
* ```markdown | ||
* > | a [^b\c] d | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function callEscape(code) { | ||
@@ -278,15 +343,8 @@ if ( | ||
} | ||
/** @type {State} */ | ||
function end(code) { | ||
assert(code === codes.rightSquareBracket, 'expected `]`') | ||
effects.enter('gfmFootnoteCallLabelMarker') | ||
effects.consume(code) | ||
effects.exit('gfmFootnoteCallLabelMarker') | ||
effects.exit('gfmFootnoteCall') | ||
return ok | ||
} | ||
} | ||
/** @type {Tokenizer} */ | ||
/** | ||
* @this {TokenizeContext} | ||
* @type {Tokenizer} | ||
*/ | ||
function tokenizeDefinitionStart(effects, ok, nok) { | ||
@@ -300,3 +358,3 @@ const self = this | ||
let size = 0 | ||
/** @type {boolean|undefined} */ | ||
/** @type {boolean | undefined} */ | ||
let data | ||
@@ -306,3 +364,12 @@ | ||
/** @type {State} */ | ||
/** | ||
* Start of GFM footnote definition. | ||
* | ||
* ```markdown | ||
* > | [^a]: b | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function start(code) { | ||
@@ -315,7 +382,16 @@ assert(code === codes.leftSquareBracket, 'expected `[`') | ||
effects.exit('gfmFootnoteDefinitionLabelMarker') | ||
return labelStart | ||
return labelAtMarker | ||
} | ||
/** @type {State} */ | ||
function labelStart(code) { | ||
/** | ||
* In label, at caret. | ||
* | ||
* ```markdown | ||
* > | [^a]: b | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function labelAtMarker(code) { | ||
if (code === codes.caret) { | ||
@@ -326,3 +402,4 @@ effects.enter('gfmFootnoteDefinitionMarker') | ||
effects.enter('gfmFootnoteDefinitionLabelString') | ||
return atBreak | ||
effects.enter('chunkString').contentType = 'string' | ||
return labelInside | ||
} | ||
@@ -333,11 +410,26 @@ | ||
/** @type {State} */ | ||
function atBreak(code) { | ||
/** @type {Token} */ | ||
let token | ||
/** | ||
* In label. | ||
* | ||
* > 👉 **Note**: `cmark-gfm` prevents whitespace from occurring in footnote | ||
* > definition labels. | ||
* | ||
* ```markdown | ||
* > | [^a]: b | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function labelInside(code) { | ||
if ( | ||
// Too long. | ||
size > constants.linkReferenceSizeMax || | ||
// Closing brace with nothing. | ||
(code === codes.rightSquareBracket && !data) || | ||
// Space or tab is not supported by GFM for some reason. | ||
// `\n` and `[` not being supported makes sense. | ||
code === codes.eof || | ||
code === codes.leftSquareBracket || | ||
size > constants.linkReferenceSizeMax | ||
markdownLineEndingOrSpace(code) | ||
) { | ||
@@ -348,7 +440,4 @@ return nok(code) | ||
if (code === codes.rightSquareBracket) { | ||
if (!data) { | ||
return nok(code) | ||
} | ||
token = effects.exit('gfmFootnoteDefinitionLabelString') | ||
effects.exit('chunkString') | ||
const token = effects.exit('gfmFootnoteDefinitionLabelString') | ||
identifier = normalizeIdentifier(self.sliceSerialize(token)) | ||
@@ -362,27 +451,2 @@ effects.enter('gfmFootnoteDefinitionLabelMarker') | ||
if (markdownLineEnding(code)) { | ||
effects.enter('lineEnding') | ||
effects.consume(code) | ||
effects.exit('lineEnding') | ||
size++ | ||
return atBreak | ||
} | ||
effects.enter('chunkString').contentType = 'string' | ||
return label(code) | ||
} | ||
/** @type {State} */ | ||
function label(code) { | ||
if ( | ||
code === codes.eof || | ||
markdownLineEnding(code) || | ||
code === codes.leftSquareBracket || | ||
code === codes.rightSquareBracket || | ||
size > constants.linkReferenceSizeMax | ||
) { | ||
effects.exit('chunkString') | ||
return atBreak(code) | ||
} | ||
if (!markdownLineEndingOrSpace(code)) { | ||
@@ -394,6 +458,18 @@ data = true | ||
effects.consume(code) | ||
return code === codes.backslash ? labelEscape : label | ||
return code === codes.backslash ? labelEscape : labelInside | ||
} | ||
/** @type {State} */ | ||
/** | ||
* After `\`, at a special character. | ||
* | ||
* > 👉 **Note**: `cmark-gfm` currently does not support escaped brackets: | ||
* > <https://github.com/github/cmark-gfm/issues/240> | ||
* | ||
* ```markdown | ||
* > | [^a\*b]: c | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function labelEscape(code) { | ||
@@ -407,9 +483,18 @@ if ( | ||
size++ | ||
return label | ||
return labelInside | ||
} | ||
return label(code) | ||
return labelInside(code) | ||
} | ||
/** @type {State} */ | ||
/** | ||
* After definition label. | ||
* | ||
* ```markdown | ||
* > | [^a]: b | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function labelAfter(code) { | ||
@@ -420,6 +505,15 @@ if (code === codes.colon) { | ||
effects.exit('definitionMarker') | ||
if (!defined.includes(identifier)) { | ||
defined.push(identifier) | ||
} | ||
// Any whitespace after the marker is eaten, forming indented code | ||
// is not possible. | ||
// No space is also fine, just like a block quote marker. | ||
return factorySpace(effects, done, 'gfmFootnoteDefinitionWhitespace') | ||
return factorySpace( | ||
effects, | ||
whitespaceAfter, | ||
'gfmFootnoteDefinitionWhitespace' | ||
) | ||
} | ||
@@ -430,8 +524,14 @@ | ||
/** @type {State} */ | ||
function done(code) { | ||
if (!defined.includes(identifier)) { | ||
defined.push(identifier) | ||
} | ||
/** | ||
* After definition prefix. | ||
* | ||
* ```markdown | ||
* > | [^a]: b | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function whitespaceAfter(code) { | ||
// `markdown-rs` has a wrapping token for the prefix that is closed here. | ||
return ok(code) | ||
@@ -441,4 +541,15 @@ } | ||
/** @type {Tokenizer} */ | ||
/** | ||
* @this {TokenizeContext} | ||
* @type {Tokenizer} | ||
*/ | ||
function tokenizeDefinitionContinuation(effects, ok, nok) { | ||
/// Start of footnote definition continuation. | ||
/// | ||
/// ```markdown | ||
/// | [^a]: b | ||
/// > | c | ||
/// ^ | ||
/// ``` | ||
// | ||
// Either a blank line, which is okay, or an indented thing. | ||
@@ -453,3 +564,6 @@ return effects.check(blankLine, ok, effects.attempt(indent, ok, nok)) | ||
/** @type {Tokenizer} */ | ||
/** | ||
* @this {TokenizeContext} | ||
* @type {Tokenizer} | ||
*/ | ||
function tokenizeIndent(effects, ok, nok) { | ||
@@ -465,3 +579,5 @@ const self = this | ||
/** @type {State} */ | ||
/** | ||
* @type {State} | ||
*/ | ||
function afterPrefix(code) { | ||
@@ -468,0 +584,0 @@ const tail = self.events[self.events.length - 1] |
export {gfmFootnote} from './lib/syntax.js' | ||
export {gfmFootnoteHtml} from './lib/html.js' | ||
export type BackLabelTemplate = import('./lib/html.js').BackLabelTemplate | ||
export type HtmlOptions = import('./lib/html.js').Options | ||
export {gfmFootnoteHtml, defaultBackLabel} from './lib/html.js' |
/** | ||
* @typedef {import('./lib/html.js').BackLabelTemplate} BackLabelTemplate | ||
* @typedef {import('./lib/html.js').Options} HtmlOptions | ||
*/ | ||
export {gfmFootnote} from './lib/syntax.js' | ||
export {gfmFootnoteHtml} from './lib/html.js' | ||
export {gfmFootnoteHtml, defaultBackLabel} from './lib/html.js' |
/** | ||
* @param {Options} [options={}] | ||
* Generate the default label that GitHub uses on backreferences. | ||
* | ||
* @param {number} referenceIndex | ||
* Index of the definition in the order that they are first referenced, | ||
* 0-indexed. | ||
* @param {number} rereferenceIndex | ||
* Index of calls to the same definition, 0-indexed. | ||
* @returns {string} | ||
* Default label. | ||
*/ | ||
export function defaultBackLabel( | ||
referenceIndex: number, | ||
rereferenceIndex: number | ||
): string | ||
/** | ||
* Create an extension for `micromark` to support GFM footnotes when | ||
* serializing to HTML. | ||
* | ||
* @param {Options | null | undefined} [options] | ||
* Configuration. | ||
* @returns {HtmlExtension} | ||
* Extension for `micromark` that can be passed in `htmlExtensions` to | ||
* support GFM footnotes when serializing to HTML. | ||
*/ | ||
export function gfmFootnoteHtml(options?: Options | undefined): HtmlExtension | ||
export function gfmFootnoteHtml( | ||
options?: Options | null | undefined | ||
): HtmlExtension | ||
export type HtmlExtension = import('micromark-util-types').HtmlExtension | ||
export type CompileContext = import('micromark-util-types').CompileContext | ||
/** | ||
* Generate a back label dynamically. | ||
* | ||
* For the following markdown: | ||
* | ||
* ```markdown | ||
* Alpha[^micromark], bravo[^micromark], and charlie[^remark]. | ||
* | ||
* [^remark]: things about remark | ||
* [^micromark]: things about micromark | ||
* ``` | ||
* | ||
* This function will be called with: | ||
* | ||
* * `0` and `0` for the backreference from `things about micromark` to | ||
* `alpha`, as it is the first used definition, and the first call to it | ||
* * `0` and `1` for the backreference from `things about micromark` to | ||
* `bravo`, as it is the first used definition, and the second call to it | ||
* * `1` and `0` for the backreference from `things about remark` to | ||
* `charlie`, as it is the second used definition | ||
*/ | ||
export type BackLabelTemplate = ( | ||
referenceIndex: number, | ||
rereferenceIndex: number | ||
) => string | ||
/** | ||
* Configuration. | ||
*/ | ||
export type Options = { | ||
/** | ||
* Prefix to use before the `id` attribute to prevent it from *clobbering*. | ||
* attributes. | ||
* Prefix to use before the `id` attribute on footnotes to prevent them from | ||
* *clobbering*. | ||
* | ||
* The default is `'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)</script> | ||
* <p id="x"></p> | ||
* <script>alert(x) // `x` now refers to the `p#x` DOM element</script> | ||
* ``` | ||
* | ||
* Elements by their ID are made available in browsers on the `window` object. | ||
* Using a prefix prevents this from being a problem. | ||
* 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. | ||
*/ | ||
clobberPrefix?: string | undefined | ||
clobberPrefix?: string | ||
/** | ||
* Label to use for the footnotes section. | ||
* Affects screen reader users. | ||
* Change it if you’re authoring in a different language. | ||
* Textual label to use for the footnotes section. | ||
* | ||
* The default value is `'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 attributes with the `labelAttributes` option. | ||
*/ | ||
label?: string | undefined | ||
label?: string | ||
/** | ||
* Label to use from backreferences back to their footnote call. | ||
* Affects screen reader users. | ||
* Change it if you’re authoring in a different language. | ||
* Attributes to use on the footnote label. | ||
* | ||
* Change it to show the label and add other attributes. | ||
* | ||
* 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 attributes. | ||
* | ||
* > 👉 **Note**: `id="footnote-label"` is always added, because footnote | ||
* > calls use it with `aria-describedby` to provide an accessible label. | ||
*/ | ||
backLabel?: string | undefined | ||
labelAttributes?: string | ||
/** | ||
* HTML tag name to use for the footnote label element. | ||
* | ||
* 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 attributes with the `labelAttributes` option. | ||
*/ | ||
labelTagName?: string | ||
/** | ||
* Textual label to describe the backreference back to references. | ||
* | ||
* The default value is: | ||
* | ||
* ```js | ||
* function defaultBackLabel(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 `aria-label` attribute on each backreference | ||
* (the `↩` links). | ||
* It affects users of assistive technology. | ||
*/ | ||
backLabel?: BackLabelTemplate | string | ||
} |
214
lib/html.js
/** | ||
* @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension | ||
* @typedef {import('micromark-util-types').CompileContext} CompileContext | ||
*/ | ||
/** | ||
* @callback BackLabelTemplate | ||
* Generate a back label dynamically. | ||
* | ||
* For the following markdown: | ||
* | ||
* ```markdown | ||
* Alpha[^micromark], bravo[^micromark], and charlie[^remark]. | ||
* | ||
* [^remark]: things about remark | ||
* [^micromark]: things about micromark | ||
* ``` | ||
* | ||
* This function will be called with: | ||
* | ||
* * `0` and `0` for the backreference from `things about micromark` to | ||
* `alpha`, as it is the first used definition, and the first call to it | ||
* * `0` and `1` for the backreference from `things about micromark` to | ||
* `bravo`, as it is the first used definition, and the second call to it | ||
* * `1` and `0` for the backreference from `things about remark` to | ||
* `charlie`, as it is the second used definition | ||
* @param {number} referenceIndex | ||
* Index of the definition in the order that they are first referenced, | ||
* 0-indexed. | ||
* @param {number} rereferenceIndex | ||
* Index of calls to the same definition, 0-indexed. | ||
* @returns {string} | ||
* Back label to use when linking back from definitions to their reference. | ||
*/ | ||
/** | ||
* @typedef Options | ||
* Configuration. | ||
* @property {string} [clobberPrefix='user-content-'] | ||
* Prefix to use before the `id` attribute to prevent it from *clobbering*. | ||
* attributes. | ||
* Prefix to use before the `id` attribute on footnotes to prevent them from | ||
* *clobbering*. | ||
* | ||
* The default is `'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)</script> | ||
* <p id="x"></p> | ||
* <script>alert(x) // `x` now refers to the `p#x` DOM element</script> | ||
* ``` | ||
* | ||
* Elements by their ID are made available in browsers on the `window` object. | ||
* Using a prefix prevents this from being a problem. | ||
* 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 {string} [label='Footnotes'] | ||
* Label to use for the footnotes section. | ||
* Affects screen reader users. | ||
* Change it if you’re authoring in a different language. | ||
* @property {string} [backLabel='Back to content'] | ||
* Label to use from backreferences back to their footnote call. | ||
* Affects screen reader users. | ||
* Change it if you’re authoring in a different language. | ||
* Textual label to use for the footnotes section. | ||
* | ||
* The default value is `'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 attributes with the `labelAttributes` option. | ||
* @property {string} [labelAttributes='class="sr-only"'] | ||
* Attributes to use on the footnote label. | ||
* | ||
* Change it to show the label and add other attributes. | ||
* | ||
* 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 attributes. | ||
* | ||
* > 👉 **Note**: `id="footnote-label"` is always added, because footnote | ||
* > calls use it with `aria-describedby` to provide an accessible label. | ||
* @property {string} [labelTagName='h2'] | ||
* HTML tag name to use for the footnote label element. | ||
* | ||
* 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 attributes with the `labelAttributes` option. | ||
* @property {BackLabelTemplate | string} [backLabel] | ||
* Textual label to describe the backreference back to references. | ||
* | ||
* The default value is: | ||
* | ||
* ```js | ||
* function defaultBackLabel(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 `aria-label` attribute on each backreference | ||
* (the `↩` links). | ||
* It affects users of assistive technology. | ||
*/ | ||
import {normalizeIdentifier} from 'micromark-util-normalize-identifier' | ||
import {sanitizeUri} from 'micromark-util-sanitize-uri' | ||
const own = {}.hasOwnProperty | ||
/** @type {Options} */ | ||
const emptyOptions = {} | ||
/** | ||
* @param {Options} [options={}] | ||
* Generate the default label that GitHub uses on backreferences. | ||
* | ||
* @param {number} referenceIndex | ||
* Index of the definition in the order that they are first referenced, | ||
* 0-indexed. | ||
* @param {number} rereferenceIndex | ||
* Index of calls to the same definition, 0-indexed. | ||
* @returns {string} | ||
* Default label. | ||
*/ | ||
export function defaultBackLabel(referenceIndex, rereferenceIndex) { | ||
return ( | ||
'Back to reference ' + | ||
(referenceIndex + 1) + | ||
(rereferenceIndex > 1 ? '-' + rereferenceIndex : '') | ||
) | ||
} | ||
/** | ||
* Create an extension for `micromark` to support GFM footnotes when | ||
* serializing to HTML. | ||
* | ||
* @param {Options | null | undefined} [options] | ||
* Configuration. | ||
* @returns {HtmlExtension} | ||
* Extension for `micromark` that can be passed in `htmlExtensions` to | ||
* support GFM footnotes when serializing to HTML. | ||
*/ | ||
export function gfmFootnoteHtml(options = {}) { | ||
const label = options.label || 'Footnotes' | ||
const backLabel = options.backLabel || 'Back to content' | ||
export function gfmFootnoteHtml(options) { | ||
const config = options || emptyOptions | ||
const label = config.label || 'Footnotes' | ||
const labelTagName = config.labelTagName || 'h2' | ||
const labelAttributes = | ||
config.labelAttributes === null || config.labelAttributes === undefined | ||
? 'class="sr-only"' | ||
: config.labelAttributes | ||
const backLabel = config.backLabel || defaultBackLabel | ||
const clobberPrefix = | ||
options.clobberPrefix === undefined || options.clobberPrefix === null | ||
config.clobberPrefix === null || config.clobberPrefix === undefined | ||
? 'user-content-' | ||
: options.clobberPrefix | ||
: config.clobberPrefix | ||
return { | ||
enter: { | ||
gfmFootnoteDefinition() { | ||
const stack = | ||
/** @type {Array<boolean>} */ | ||
this.getData('tightStack') | ||
const stack = /** @type {Array<boolean>} */ this.getData('tightStack') | ||
stack.push(false) | ||
}, | ||
gfmFootnoteDefinitionLabelString() { | ||
this.buffer() | ||
}, | ||
gfmFootnoteCallString() { | ||
@@ -72,15 +189,12 @@ this.buffer() | ||
const value = this.resume() | ||
if (!definitions) { | ||
this.setData('gfmFootnoteDefinitions', (definitions = {})) | ||
} | ||
if (!own.call(definitions, current)) definitions[current] = value | ||
tightStack.pop() | ||
this.setData('slurpOneLineEnding', true) // “Hack” to prevent a line ending from showing up if we’re in a definition in | ||
this.setData('slurpOneLineEnding', true) | ||
// “Hack” to prevent a line ending from showing up if we’re in a definition in | ||
// an empty list item. | ||
this.setData('lastWasTag') | ||
}, | ||
gfmFootnoteDefinitionLabelString(token) { | ||
@@ -90,10 +204,7 @@ let footnoteStack = | ||
this.getData('gfmFootnoteDefinitionStack') | ||
if (!footnoteStack) { | ||
this.setData('gfmFootnoteDefinitionStack', (footnoteStack = [])) | ||
} | ||
footnoteStack.push(normalizeIdentifier(this.sliceSerialize(token))) | ||
this.resume() // Drop the label. | ||
this.buffer() // Get ready for a value. | ||
@@ -111,3 +222,2 @@ }, | ||
/** @type {number} */ | ||
let counter | ||
@@ -119,3 +229,2 @@ this.resume() | ||
const safeId = sanitizeUri(id.toLowerCase()) | ||
if (index === -1) { | ||
@@ -129,3 +238,2 @@ calls.push(id) | ||
} | ||
const reuseCounter = counts[id] | ||
@@ -147,3 +255,2 @@ this.tag( | ||
}, | ||
null() { | ||
@@ -160,14 +267,16 @@ const calls = | ||
let index = -1 | ||
if (calls.length > 0) { | ||
this.lineEndingIfNeeded() | ||
this.tag( | ||
'<section data-footnotes="" class="footnotes"><h2 id="footnote-label" class="sr-only">' | ||
'<section data-footnotes="" class="footnotes"><' + | ||
labelTagName + | ||
' id="footnote-label"' + | ||
(labelAttributes ? ' ' + labelAttributes : '') + | ||
'>' | ||
) | ||
this.raw(this.encode(label)) | ||
this.tag('</h2>') | ||
this.tag('</' + labelTagName + '>') | ||
this.lineEndingIfNeeded() | ||
this.tag('<ol>') | ||
} | ||
while (++index < calls.length) { | ||
@@ -179,5 +288,3 @@ // Called definitions are always defined. | ||
/** @type {Array<string>} */ | ||
const references = [] | ||
while (++referenceIndex <= counts[id]) { | ||
@@ -190,5 +297,9 @@ references.push( | ||
(referenceIndex > 1 ? '-' + referenceIndex : '') + | ||
'" data-footnote-backref="" class="data-footnote-backref" aria-label="' + | ||
this.encode(backLabel) + | ||
'">↩' + | ||
'" data-footnote-backref="" aria-label="' + | ||
this.encode( | ||
typeof backLabel === 'string' | ||
? backLabel | ||
: backLabel(index, referenceIndex) | ||
) + | ||
'" class="data-footnote-backref">↩' + | ||
(referenceIndex > 1 | ||
@@ -200,3 +311,2 @@ ? '<sup>' + referenceIndex + '</sup>' | ||
} | ||
const reference = references.join(' ') | ||
@@ -210,6 +320,3 @@ let injected = false | ||
/<\/p>(?:\r?\n|\r)?$/, | ||
( | ||
/** @type {string} */ | ||
$0 | ||
) => { | ||
(/** @type {string} */ $0) => { | ||
injected = true | ||
@@ -220,3 +327,2 @@ return ' ' + reference + $0 | ||
) | ||
if (!injected) { | ||
@@ -226,7 +332,5 @@ this.lineEndingIfNeeded() | ||
} | ||
this.lineEndingIfNeeded() | ||
this.tag('</li>') | ||
} | ||
if (calls.length > 0) { | ||
@@ -233,0 +337,0 @@ this.lineEndingIfNeeded() |
/** | ||
* Create an extension for `micromark` to enable GFM footnote syntax. | ||
* | ||
* @returns {Extension} | ||
* Extension for `micromark` that can be passed in `extensions` to | ||
* enable GFM footnote syntax. | ||
*/ | ||
export function gfmFootnote(): Extension | ||
export type Event = import('micromark-util-types').Event | ||
export type Exiter = import('micromark-util-types').Exiter | ||
export type Extension = import('micromark-util-types').Extension | ||
export type Resolver = import('micromark-util-types').Resolver | ||
export type State = import('micromark-util-types').State | ||
export type Token = import('micromark-util-types').Token | ||
export type TokenizeContext = import('micromark-util-types').TokenizeContext | ||
export type Tokenizer = import('micromark-util-types').Tokenizer | ||
export type Exiter = import('micromark-util-types').Exiter | ||
export type State = import('micromark-util-types').State | ||
export type Event = import('micromark-util-types').Event |
/** | ||
* @typedef {import('micromark-util-types').Event} Event | ||
* @typedef {import('micromark-util-types').Exiter} Exiter | ||
* @typedef {import('micromark-util-types').Extension} Extension | ||
* @typedef {import('micromark-util-types').Resolver} Resolver | ||
* @typedef {import('micromark-util-types').State} State | ||
* @typedef {import('micromark-util-types').Token} Token | ||
* @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext | ||
* @typedef {import('micromark-util-types').Tokenizer} Tokenizer | ||
* @typedef {import('micromark-util-types').Exiter} Exiter | ||
* @typedef {import('micromark-util-types').State} State | ||
* @typedef {import('micromark-util-types').Event} Event | ||
*/ | ||
import {blankLine} from 'micromark-core-commonmark' | ||
import {factorySpace} from 'micromark-factory-space' | ||
import { | ||
markdownLineEnding, | ||
markdownLineEndingOrSpace | ||
} from 'micromark-util-character' | ||
import {markdownLineEndingOrSpace} from 'micromark-util-character' | ||
import {normalizeIdentifier} from 'micromark-util-normalize-identifier' | ||
@@ -21,6 +20,16 @@ const indent = { | ||
} | ||
// To do: micromark should support a `_hiddenGfmFootnoteSupport`, which only | ||
// affects label start (image). | ||
// That will let us drop `tokenizePotentialGfmFootnote*`. | ||
// It currently has a `_hiddenFootnoteSupport`, which affects that and more. | ||
// That can be removed when `micromark-extension-footnote` is archived. | ||
/** | ||
* Create an extension for `micromark` to enable GFM footnote syntax. | ||
* | ||
* @returns {Extension} | ||
* Extension for `micromark` that can be passed in `extensions` to | ||
* enable GFM footnote syntax. | ||
*/ | ||
export function gfmFootnote() { | ||
@@ -50,4 +59,8 @@ /** @type {Extension} */ | ||
} | ||
/** @type {Tokenizer} */ | ||
// To do: remove after micromark update. | ||
/** | ||
* @this {TokenizeContext} | ||
* @type {Tokenizer} | ||
*/ | ||
function tokenizePotentialGfmFootnoteCall(effects, ok, nok) { | ||
@@ -58,16 +71,15 @@ const self = this | ||
// @ts-expect-error It’s fine! | ||
const defined = self.parser.gfmFootnotes || (self.parser.gfmFootnotes = []) | ||
/** @type {Token} */ | ||
let labelStart | ||
let labelStart // Find an opening. | ||
// Find an opening. | ||
while (index--) { | ||
const token = self.events[index][1] | ||
if (token.type === 'labelImage') { | ||
labelStart = token | ||
break | ||
} // Exit if we’ve walked far enough. | ||
} | ||
// Exit if we’ve walked far enough. | ||
if ( | ||
@@ -83,6 +95,7 @@ token.type === 'gfmFootnoteCall' || | ||
} | ||
return start | ||
/** @type {State} */ | ||
/** | ||
* @type {State} | ||
*/ | ||
function start(code) { | ||
@@ -92,3 +105,2 @@ if (!labelStart || !labelStart._balanced) { | ||
} | ||
const id = normalizeIdentifier( | ||
@@ -100,7 +112,5 @@ self.sliceSerialize({ | ||
) | ||
if (id.charCodeAt(0) !== 94 || !defined.includes(id.slice(1))) { | ||
if (id.codePointAt(0) !== 94 || !defined.includes(id.slice(1))) { | ||
return nok(code) | ||
} | ||
effects.enter('gfmFootnoteCallLabelMarker') | ||
@@ -112,10 +122,11 @@ effects.consume(code) | ||
} | ||
// To do: remove after micromark update. | ||
/** @type {Resolver} */ | ||
function resolveToPotentialGfmFootnoteCall(events, context) { | ||
let index = events.length | ||
/** @type {Token|undefined} */ | ||
/** @type {Token | undefined} */ | ||
let labelStart | ||
let labelStart // Find an opening. | ||
// Find an opening. | ||
while (index--) { | ||
@@ -130,7 +141,7 @@ if ( | ||
} | ||
// Change the `labelImageMarker` to a `data`. | ||
events[index + 1][1].type = 'data' | ||
events[index + 3][1].type = 'gfmFootnoteCallLabelMarker' // The whole (without `!`): | ||
events[index + 3][1].type = 'gfmFootnoteCallLabelMarker' | ||
// The whole (without `!`): | ||
const call = { | ||
@@ -140,4 +151,4 @@ type: 'gfmFootnoteCall', | ||
end: Object.assign({}, events[events.length - 1][1].end) | ||
} // The `^` marker | ||
} | ||
// The `^` marker | ||
const marker = { | ||
@@ -147,4 +158,4 @@ type: 'gfmFootnoteCallMarker', | ||
end: Object.assign({}, events[index + 3][1].end) | ||
} // Increment the end 1 character. | ||
} | ||
// Increment the end 1 character. | ||
marker.end.column++ | ||
@@ -164,4 +175,4 @@ marker.end.offset++ | ||
} | ||
/** @type {Array<Event>} */ | ||
const replacement = [ | ||
@@ -171,11 +182,15 @@ // Take the `labelImageMarker` (now `data`, the `!`) | ||
events[index + 2], | ||
['enter', call, context], // The `[` | ||
['enter', call, context], | ||
// The `[` | ||
events[index + 3], | ||
events[index + 4], // The `^`. | ||
events[index + 4], | ||
// The `^`. | ||
['enter', marker, context], | ||
['exit', marker, context], // Everything in between. | ||
['exit', marker, context], | ||
// Everything in between. | ||
['enter', string, context], | ||
['enter', chunk, context], | ||
['exit', chunk, context], | ||
['exit', string, context], // The ending (`]`, properly parsed and labelled). | ||
['exit', string, context], | ||
// The ending (`]`, properly parsed and labelled). | ||
events[events.length - 2], | ||
@@ -188,4 +203,7 @@ events[events.length - 1], | ||
} | ||
/** @type {Tokenizer} */ | ||
/** | ||
* @this {TokenizeContext} | ||
* @type {Tokenizer} | ||
*/ | ||
function tokenizeGfmFootnoteCall(effects, ok, nok) { | ||
@@ -195,11 +213,24 @@ const self = this | ||
// @ts-expect-error It’s fine! | ||
const defined = self.parser.gfmFootnotes || (self.parser.gfmFootnotes = []) | ||
let size = 0 | ||
/** @type {boolean} */ | ||
let data | ||
let data | ||
// Note: the implementation of `markdown-rs` is different, because it houses | ||
// core *and* extensions in one project. | ||
// Therefore, it can include footnote logic inside `label-end`. | ||
// We can’t do that, but luckily, we can parse footnotes in a simpler way than | ||
// needed for labels. | ||
return start | ||
/** @type {State} */ | ||
/** | ||
* Start of footnote label. | ||
* | ||
* ```markdown | ||
* > | a [^b] c | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function start(code) { | ||
@@ -212,4 +243,13 @@ effects.enter('gfmFootnoteCall') | ||
} | ||
/** @type {State} */ | ||
/** | ||
* After `[`, at `^`. | ||
* | ||
* ```markdown | ||
* > | a [^b] c | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function callStart(code) { | ||
@@ -224,34 +264,57 @@ if (code !== 94) return nok(code) | ||
} | ||
/** @type {State} */ | ||
/** | ||
* In label. | ||
* | ||
* ```markdown | ||
* > | a [^b] c | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function callData(code) { | ||
/** @type {Token} */ | ||
let token | ||
if (code === null || code === 91 || size++ > 999) { | ||
if ( | ||
// Too long. | ||
size > 999 || | ||
// Closing brace with nothing. | ||
(code === 93 && !data) || | ||
// Space or tab is not supported by GFM for some reason. | ||
// `\n` and `[` not being supported makes sense. | ||
code === null || | ||
code === 91 || | ||
markdownLineEndingOrSpace(code) | ||
) { | ||
return nok(code) | ||
} | ||
if (code === 93) { | ||
if (!data) { | ||
effects.exit('chunkString') | ||
const token = effects.exit('gfmFootnoteCallString') | ||
if (!defined.includes(normalizeIdentifier(self.sliceSerialize(token)))) { | ||
return nok(code) | ||
} | ||
effects.exit('chunkString') | ||
token = effects.exit('gfmFootnoteCallString') | ||
return defined.includes(normalizeIdentifier(self.sliceSerialize(token))) | ||
? end(code) | ||
: nok(code) | ||
effects.enter('gfmFootnoteCallLabelMarker') | ||
effects.consume(code) | ||
effects.exit('gfmFootnoteCallLabelMarker') | ||
effects.exit('gfmFootnoteCall') | ||
return ok | ||
} | ||
effects.consume(code) | ||
if (!markdownLineEndingOrSpace(code)) { | ||
data = true | ||
} | ||
size++ | ||
effects.consume(code) | ||
return code === 92 ? callEscape : callData | ||
} | ||
/** @type {State} */ | ||
/** | ||
* On character after escape. | ||
* | ||
* ```markdown | ||
* > | a [^b\c] d | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function callEscape(code) { | ||
@@ -263,17 +326,10 @@ if (code === 91 || code === 92 || code === 93) { | ||
} | ||
return callData(code) | ||
} | ||
/** @type {State} */ | ||
function end(code) { | ||
effects.enter('gfmFootnoteCallLabelMarker') | ||
effects.consume(code) | ||
effects.exit('gfmFootnoteCallLabelMarker') | ||
effects.exit('gfmFootnoteCall') | ||
return ok | ||
} | ||
} | ||
/** @type {Tokenizer} */ | ||
/** | ||
* @this {TokenizeContext} | ||
* @type {Tokenizer} | ||
*/ | ||
function tokenizeDefinitionStart(effects, ok, nok) { | ||
@@ -283,14 +339,20 @@ const self = this | ||
// @ts-expect-error It’s fine! | ||
const defined = self.parser.gfmFootnotes || (self.parser.gfmFootnotes = []) | ||
/** @type {string} */ | ||
let identifier | ||
let size = 0 | ||
/** @type {boolean|undefined} */ | ||
/** @type {boolean | undefined} */ | ||
let data | ||
return start | ||
/** @type {State} */ | ||
/** | ||
* Start of GFM footnote definition. | ||
* | ||
* ```markdown | ||
* > | [^a]: b | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function start(code) { | ||
@@ -302,7 +364,16 @@ effects.enter('gfmFootnoteDefinition')._container = true | ||
effects.exit('gfmFootnoteDefinitionLabelMarker') | ||
return labelStart | ||
return labelAtMarker | ||
} | ||
/** @type {State} */ | ||
function labelStart(code) { | ||
/** | ||
* In label, at caret. | ||
* | ||
* ```markdown | ||
* > | [^a]: b | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function labelAtMarker(code) { | ||
if (code === 94) { | ||
@@ -313,23 +384,38 @@ effects.enter('gfmFootnoteDefinitionMarker') | ||
effects.enter('gfmFootnoteDefinitionLabelString') | ||
return atBreak | ||
effects.enter('chunkString').contentType = 'string' | ||
return labelInside | ||
} | ||
return nok(code) | ||
} | ||
/** @type {State} */ | ||
function atBreak(code) { | ||
/** @type {Token} */ | ||
let token | ||
if (code === null || code === 91 || size > 999) { | ||
/** | ||
* In label. | ||
* | ||
* > 👉 **Note**: `cmark-gfm` prevents whitespace from occurring in footnote | ||
* > definition labels. | ||
* | ||
* ```markdown | ||
* > | [^a]: b | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function labelInside(code) { | ||
if ( | ||
// Too long. | ||
size > 999 || | ||
// Closing brace with nothing. | ||
(code === 93 && !data) || | ||
// Space or tab is not supported by GFM for some reason. | ||
// `\n` and `[` not being supported makes sense. | ||
code === null || | ||
code === 91 || | ||
markdownLineEndingOrSpace(code) | ||
) { | ||
return nok(code) | ||
} | ||
if (code === 93) { | ||
if (!data) { | ||
return nok(code) | ||
} | ||
token = effects.exit('gfmFootnoteDefinitionLabelString') | ||
effects.exit('chunkString') | ||
const token = effects.exit('gfmFootnoteDefinitionLabelString') | ||
identifier = normalizeIdentifier(self.sliceSerialize(token)) | ||
@@ -342,38 +428,23 @@ effects.enter('gfmFootnoteDefinitionLabelMarker') | ||
} | ||
if (markdownLineEnding(code)) { | ||
effects.enter('lineEnding') | ||
effects.consume(code) | ||
effects.exit('lineEnding') | ||
size++ | ||
return atBreak | ||
} | ||
effects.enter('chunkString').contentType = 'string' | ||
return label(code) | ||
} | ||
/** @type {State} */ | ||
function label(code) { | ||
if ( | ||
code === null || | ||
markdownLineEnding(code) || | ||
code === 91 || | ||
code === 93 || | ||
size > 999 | ||
) { | ||
effects.exit('chunkString') | ||
return atBreak(code) | ||
} | ||
if (!markdownLineEndingOrSpace(code)) { | ||
data = true | ||
} | ||
size++ | ||
effects.consume(code) | ||
return code === 92 ? labelEscape : label | ||
return code === 92 ? labelEscape : labelInside | ||
} | ||
/** @type {State} */ | ||
/** | ||
* After `\`, at a special character. | ||
* | ||
* > 👉 **Note**: `cmark-gfm` currently does not support escaped brackets: | ||
* > <https://github.com/github/cmark-gfm/issues/240> | ||
* | ||
* ```markdown | ||
* > | [^a\*b]: c | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function labelEscape(code) { | ||
@@ -383,9 +454,17 @@ if (code === 91 || code === 92 || code === 93) { | ||
size++ | ||
return label | ||
return labelInside | ||
} | ||
return label(code) | ||
return labelInside(code) | ||
} | ||
/** @type {State} */ | ||
/** | ||
* After definition label. | ||
* | ||
* ```markdown | ||
* > | [^a]: b | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function labelAfter(code) { | ||
@@ -395,34 +474,61 @@ if (code === 58) { | ||
effects.consume(code) | ||
effects.exit('definitionMarker') // Any whitespace after the marker is eaten, forming indented code | ||
effects.exit('definitionMarker') | ||
if (!defined.includes(identifier)) { | ||
defined.push(identifier) | ||
} | ||
// Any whitespace after the marker is eaten, forming indented code | ||
// is not possible. | ||
// No space is also fine, just like a block quote marker. | ||
return factorySpace(effects, done, 'gfmFootnoteDefinitionWhitespace') | ||
return factorySpace( | ||
effects, | ||
whitespaceAfter, | ||
'gfmFootnoteDefinitionWhitespace' | ||
) | ||
} | ||
return nok(code) | ||
} | ||
/** @type {State} */ | ||
function done(code) { | ||
if (!defined.includes(identifier)) { | ||
defined.push(identifier) | ||
} | ||
/** | ||
* After definition prefix. | ||
* | ||
* ```markdown | ||
* > | [^a]: b | ||
* ^ | ||
* ``` | ||
* | ||
* @type {State} | ||
*/ | ||
function whitespaceAfter(code) { | ||
// `markdown-rs` has a wrapping token for the prefix that is closed here. | ||
return ok(code) | ||
} | ||
} | ||
/** @type {Tokenizer} */ | ||
/** | ||
* @this {TokenizeContext} | ||
* @type {Tokenizer} | ||
*/ | ||
function tokenizeDefinitionContinuation(effects, ok, nok) { | ||
/// Start of footnote definition continuation. | ||
/// | ||
/// ```markdown | ||
/// | [^a]: b | ||
/// > | c | ||
/// ^ | ||
/// ``` | ||
// | ||
// Either a blank line, which is okay, or an indented thing. | ||
return effects.check(blankLine, ok, effects.attempt(indent, ok, nok)) | ||
} | ||
/** @type {Exiter} */ | ||
function gfmFootnoteDefinitionEnd(effects) { | ||
effects.exit('gfmFootnoteDefinition') | ||
} | ||
/** @type {Tokenizer} */ | ||
/** | ||
* @this {TokenizeContext} | ||
* @type {Tokenizer} | ||
*/ | ||
function tokenizeIndent(effects, ok, nok) { | ||
@@ -436,4 +542,6 @@ const self = this | ||
) | ||
/** @type {State} */ | ||
/** | ||
* @type {State} | ||
*/ | ||
function afterPrefix(code) { | ||
@@ -440,0 +548,0 @@ const tail = self.events[self.events.length - 1] |
{ | ||
"name": "micromark-extension-gfm-footnote", | ||
"version": "1.0.4", | ||
"version": "1.1.0", | ||
"description": "micromark extension to support GFM footnotes", | ||
@@ -51,3 +51,3 @@ "license": "MIT", | ||
"devDependencies": { | ||
"@types/tape": "^4.0.0", | ||
"@types/node": "^18.0.0", | ||
"c8": "^7.0.0", | ||
@@ -58,15 +58,16 @@ "create-gfm-fixtures": "^1.0.0", | ||
"prettier": "^2.0.0", | ||
"remark-cli": "^10.0.0", | ||
"remark-cli": "^11.0.0", | ||
"remark-preset-wooorm": "^9.0.0", | ||
"rimraf": "^3.0.0", | ||
"tape": "^5.0.0", | ||
"type-coverage": "^2.0.0", | ||
"typescript": "^4.0.0", | ||
"xo": "^0.48.0" | ||
"typescript": "^5.0.0", | ||
"xo": "^0.53.0" | ||
}, | ||
"scripts": { | ||
"build": "rimraf \"dev/**/*.d.ts\" \"test/**/*.d.ts\" && tsc && type-coverage && micromark-build", | ||
"prepack": "npm run build && npm run format", | ||
"build": "tsc --build --clean && tsc --build && type-coverage && micromark-build", | ||
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", | ||
"test-api": "node --conditions development test/index.js", | ||
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node --conditions development test/index.js", | ||
"test-api-prod": "node --conditions production test/index.js", | ||
"test-api-dev": "node --conditions development test/index.js", | ||
"test-api": "npm run test-api-dev && npm run test-api-prod", | ||
"test-coverage": "c8 --100 --reporter lcov npm run test-api", | ||
"test": "npm run build && npm run format && npm run test-coverage" | ||
@@ -85,11 +86,19 @@ }, | ||
"rules": { | ||
"unicorn/prefer-code-point": "off", | ||
"node/file-extension-in-import": "off", | ||
"unicorn/no-this-assignment": "off", | ||
"unicorn/prefer-node-protocol": "off" | ||
} | ||
"n/file-extension-in-import": "off", | ||
"unicorn/no-this-assignment": "off" | ||
}, | ||
"overrides": [ | ||
{ | ||
"files": [ | ||
"test/**/*.js" | ||
], | ||
"rules": { | ||
"no-await-in-loop": 0 | ||
} | ||
} | ||
] | ||
}, | ||
"remarkConfig": { | ||
"plugins": [ | ||
"preset-wooorm" | ||
"remark-preset-wooorm" | ||
] | ||
@@ -96,0 +105,0 @@ }, |
477
readme.md
@@ -11,4 +11,3 @@ # micromark-extension-gfm-footnote | ||
**[micromark][]** extension to support GitHub flavored markdown (GFM) | ||
[footnotes][post]. | ||
[micromark][] extensions to support GFM [footnotes][post]. | ||
@@ -22,5 +21,12 @@ ## Contents | ||
* [API](#api) | ||
* [`defaultBackLabel(referenceIndex, rereferenceIndex)`](#defaultbacklabelreferenceindex-rereferenceindex) | ||
* [`gfmFootnote()`](#gfmfootnote) | ||
* [`gfmFootnoteHtml(htmlOptions)`](#gfmfootnotehtmlhtmloptions) | ||
* [Recommended CSS](#recommended-css) | ||
* [`gfmFootnoteHtml(options?)`](#gfmfootnotehtmloptions) | ||
* [`BackLabelTemplate`](#backlabeltemplate) | ||
* [`HtmlOptions`](#htmloptions) | ||
* [Bugs](#bugs) | ||
* [Authoring](#authoring) | ||
* [HTML](#html) | ||
* [CSS](#css) | ||
* [Syntax](#syntax) | ||
* [Types](#types) | ||
@@ -35,8 +41,15 @@ * [Compatibility](#compatibility) | ||
This package is a micromark extension to add support for GFM footnotes. | ||
GFM footnotes were [announced September 30, 2021][post] but are not yet | ||
specified. | ||
Their implementation on github.com is currently quite buggy. | ||
The bugs have been reported on | ||
[`cmark-gfm`][cmark-gfm]. | ||
This package contains extensions that add support for footnotes as enabled by | ||
GFM to [`micromark`][micromark]. | ||
GitHub announced footnotes [on September 30, 2021][post] but did not specify | ||
them in their GFM spec. | ||
As they are implemented in their parser and supported in all places where | ||
other GFM features work, they can be considered part of GFM. | ||
GitHub employs several other features (such as mentions or frontmatter) that | ||
are either not in their parser, or not in all places where GFM features work, | ||
which should not be considered GFM. | ||
The implementation of footnotes on github.com is currently buggy. | ||
The bugs have been reported on [`cmark-gfm`][cmark-gfm]. | ||
This micromark extension matches github.com except for its bugs. | ||
@@ -46,18 +59,18 @@ | ||
In many cases, when working with micromark, you’d want to use | ||
[`micromark-extension-gfm`][micromark-extension-gfm] instead, which combines | ||
this package with other GFM features. | ||
This project is useful when you want to support footnotes in markdown. | ||
When working with syntax trees, you’d want to combine this package with | ||
[`mdast-util-gfm-footnote`][mdast-util-gfm-footnote] (or | ||
[`mdast-util-gfm`][mdast-util-gfm] when using `micromark-extension-gfm`). | ||
You can use these extensions when you are working with [`micromark`][micromark]. | ||
To support all GFM features, use | ||
[`micromark-extension-gfm`][micromark-extension-gfm] instead. | ||
These tools are all rather low-level. | ||
In most cases, you’d instead want to use [`remark-gfm`][remark-gfm] with | ||
[remark][]. | ||
When you need a syntax tree, combine this package with | ||
[`mdast-util-gfm-footnote`][mdast-util-gfm-footnote]. | ||
All these packages are used in [`remark-gfm`][remark-gfm], which focusses on | ||
making it easier to transform content by abstracting these internals away. | ||
## Install | ||
This package is [ESM only][esm]. | ||
In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]: | ||
In Node.js (version 14.14+), install with [npm][]: | ||
@@ -68,13 +81,13 @@ ```sh | ||
In Deno with [Skypack][]: | ||
In Deno with [`esm.sh`][esmsh]: | ||
```js | ||
import {gfmFootnote, gfmFootnoteHtml} from 'https://cdn.skypack.dev/micromark-extension-gfm-footnote@1?dts' | ||
import {gfmFootnote, gfmFootnoteHtml} from 'https://esm.sh/micromark-extension-gfm-footnote@1' | ||
``` | ||
In browsers with [Skypack][]: | ||
In browsers with [`esm.sh`][esmsh]: | ||
```html | ||
<script type="module"> | ||
import {gfmFootnote, gfmFootnoteHtml} from 'https://cdn.skypack.dev/micromark-extension-gfm-footnote@1?min' | ||
import {gfmFootnote, gfmFootnoteHtml} from 'https://esm.sh/micromark-extension-gfm-footnote@1?bundle' | ||
</script> | ||
@@ -85,3 +98,3 @@ ``` | ||
Say we have the following file `example.md`: | ||
Say our document `example.md` contains: | ||
@@ -106,10 +119,10 @@ ````markdown | ||
And our module `example.js` looks as follows: | ||
…and our module `example.js` looks as follows: | ||
```js | ||
import fs from 'node:fs' | ||
import fs from 'node:fs/promises' | ||
import {micromark} from 'micromark' | ||
import {gfmFootnote, gfmFootnoteHtml} from 'micromark-extension-gfm-footnote' | ||
const output = micromark(fs.readFileSync('example.md'), { | ||
const output = micromark(await fs.readFile('example.md'), { | ||
extensions: [gfmFootnote()], | ||
@@ -122,3 +135,3 @@ htmlExtensions: [gfmFootnoteHtml()] | ||
Now running `node example.js` yields: | ||
…now running `node example.js` yields: | ||
@@ -147,51 +160,257 @@ ```html | ||
This package exports the following identifiers: `gfmFootnote`, | ||
`gfmFootnoteHtml`. | ||
This package exports the identifiers | ||
[`defaultBackLabel`][api-default-back-label], | ||
[`gfmFootnote`][api-gfm-footnote], and | ||
[`gfmFootnoteHtml`][api-gfm-footnote-html]. | ||
There is no default export. | ||
The export map supports the endorsed | ||
[`development` condition](https://nodejs.org/api/packages.html#packages_resolving_user_conditions). | ||
The export map supports the [`development` condition][development]. | ||
Run `node --conditions development module.js` to get instrumented dev code. | ||
Without this condition, production code is loaded. | ||
### `defaultBackLabel(referenceIndex, rereferenceIndex)` | ||
Generate the default label that GitHub uses on backreferences | ||
([`BackLabelTemplate`][api-back-label-template]). | ||
### `gfmFootnote()` | ||
A function that can be called to get an extension for micromark to parse | ||
GFM footnotes (can be passed in `extensions`). | ||
Create an extension for `micromark` to enable GFM footnote syntax. | ||
### `gfmFootnoteHtml(htmlOptions)` | ||
###### Returns | ||
A function that can be called to get an extension to compile them to HTML (can | ||
be passed in `htmlExtensions`). | ||
Extension for `micromark` that can be passed in `extensions` to enable GFM | ||
footnote syntax ([`Extension`][micromark-extension]). | ||
###### `htmlOptions.clobberPrefix` | ||
### `gfmFootnoteHtml(options?)` | ||
Prefix to use before the `id` attribute to prevent it from *clobbering* | ||
attributes (`string`, default: `'user-content-'`). | ||
Create an extension for `micromark` to support GFM footnotes when serializing | ||
to HTML. | ||
###### Parameters | ||
* `options` ([`HtmlOptions`][api-html-options], optional) | ||
— configuration | ||
###### Returns | ||
Extension for `micromark` that can be passed in `htmlExtensions` to support GFM | ||
footnotes when serializing to HTML | ||
([`HtmlExtension`][micromark-html-extension]). | ||
### `BackLabelTemplate` | ||
Generate a back label dynamically (TypeScript type). | ||
For the following markdown: | ||
```markdown | ||
Alpha[^micromark], bravo[^micromark], and charlie[^remark]. | ||
[^remark]: things about remark | ||
[^micromark]: things about micromark | ||
``` | ||
This function will be called with: | ||
* `0` and `0` for the backreference from `things about micromark` to | ||
`alpha`, as it is the first used definition, and the first call to it | ||
* `0` and `1` for the backreference from `things about micromark` to | ||
`bravo`, as it is the first used definition, and the second call to it | ||
* `1` and `0` for the backreference from `things about remark` to | ||
`charlie`, as it is the second used definition | ||
###### Parameters | ||
* `referenceIndex` (`number`) | ||
— index of the definition in the order that they are first referenced, | ||
0-indexed | ||
* `rereferenceIndex` (`number`) | ||
— index of calls to the same definition, 0-indexed | ||
###### Returns | ||
Back label to use when linking back from definitions to their reference | ||
(`string`). | ||
### `HtmlOptions` | ||
Configuration (TypeScript type). | ||
##### Fields | ||
###### `clobberPrefix` | ||
Prefix to use before the `id` attribute on footnotes to prevent them from | ||
*clobbering* (`string`, 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)</script> | ||
<p id="x"></p> | ||
<script>alert(x) // `x` now refers to the `p#x` DOM element</script> | ||
``` | ||
Elements by their ID are made available in browsers on the `window` object. | ||
Using a prefix this that from being a problem. | ||
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. | ||
###### `htmlOptions.label` | ||
###### `label` | ||
Label to use for the footnotes section (`string`, default: `'Footnotes'`). | ||
Affects screen reader users. | ||
Change it if you’re authoring in a different language. | ||
Textual label to use for the footnotes section (`string`, default: | ||
`'Footnotes'`). | ||
###### `htmlOptions.backLabel` | ||
Change it when the markdown is not in English. | ||
Label to use from backreferences back to their footnote call (`string`, default: | ||
`'Back to content'`). | ||
Affects screen reader users. | ||
Change it if you’re authoring in a different language. | ||
This label is typically hidden visually (assuming a `sr-only` CSS class | ||
is defined that does that) and so affects screen readers only. | ||
## Recommended CSS | ||
###### `labelAttributes` | ||
The following CSS is needed to make footnotes look a bit like GitHub. | ||
Attributes to use on the footnote label (`string`, default: | ||
`'class="sr-only"'`). | ||
Change it to show the label and add other attributes. | ||
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 attributes. | ||
> 👉 **Note**: `id="footnote-label"` is always added, because footnote | ||
> calls use it with `aria-describedby` to provide an accessible label. | ||
###### `labelTagName` | ||
HTML tag name to use for the footnote label element (`string`, 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. | ||
###### `backLabel` | ||
Textual label to describe the backreference back to footnote calls | ||
([`BackLabelTemplate`][api-back-label-template] or `string`, | ||
default: [`defaultBackLabel`][api-default-back-label]). | ||
Change it when the markdown is not in English. | ||
This label is used in the [`aria-label`][aria-label] attribute on each | ||
backreference (the `↩` links). | ||
It affects users of assistive technology. | ||
## Bugs | ||
GitHub’s own algorithm to parse footnote definitions contains several bugs. | ||
These are not present in this project. | ||
The issues relating to footnote definitions are: | ||
* [Footnote reference call identifiers are trimmed, but definition | ||
identifiers aren’t](https://github.com/github/cmark-gfm/issues/237)\ | ||
— initial and final whitespace in labels causes them not to match | ||
* [Footnotes are matched case-insensitive, but links keep their casing, | ||
breaking them](https://github.com/github/cmark-gfm/issues/239)\ | ||
— using uppercase (or any character that will be percent encoded) in | ||
identifiers breaks links | ||
* [Colons in footnotes generate links w/o | ||
`href`](https://github.com/github/cmark-gfm/issues/250)\ | ||
— colons in identifiers generate broken links | ||
* [Character escape of `]` does not work in footnote | ||
identifiers](https://github.com/github/cmark-gfm/issues/240)\ | ||
— some character escapes don’t work | ||
* [Footnotes in links are | ||
broken](https://github.com/github/cmark-gfm/issues/249)\ | ||
— while `CommonMark` prevents links in links, GitHub does not prevent | ||
footnotes (which turn into links) in links | ||
* [Footnote-like brackets around image, break that | ||
image](https://github.com/github/cmark-gfm/issues/275)\ | ||
— images can’t be used in what looks like a footnote call | ||
* [GFM footnotes: line ending in footnote definition label causes text to | ||
disappear](https://github.com/github/cmark-gfm/issues/282)\ | ||
— line endings in footnote definitions cause text to disappear | ||
## Authoring | ||
When authoring markdown with footnotes it’s recommended to use words instead | ||
of numbers (or letters or anything with an order) as identifiers. | ||
That makes it easier to reuse and reorder footnotes. | ||
It’s recommended to place footnotes definitions at the bottom of the document. | ||
## HTML | ||
GFM footnotes do not, on their own, relate to anything in HTML. | ||
When a footnote reference matches with a definition, they each relate to several | ||
elements in HTML. | ||
The reference relates to `<sup>` and `<a>` elements in HTML: | ||
```html | ||
<sup><a href="#user-content-fn-x" id="user-content-fnref-x" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup></p> | ||
``` | ||
…where `x` is the identifier used in the markdown source and `1` the number of | ||
corresponding, listed, definition. | ||
See [*§ 4.5.19 The `sub` and `sup` elements*][html-sup], | ||
[*§ 4.5.1 The `a` element*][html-a], and | ||
[*§ 3.2.6.6 Embedding custom non-visible data with the `data-*` | ||
attributes*][html-data] | ||
in the HTML spec, and | ||
[*§ 6.8 `aria-describedby` property*][aria-describedby] | ||
in WAI-ARIA, for more info. | ||
When one or more definitions are referenced, a footnote section is generated at | ||
the end of the document, using `<section>`, `<h2>`, and `<ol>` elements: | ||
```html | ||
<section data-footnotes="" class="footnotes"><h2 id="footnote-label" class="sr-only">Footnotes</h2> | ||
<ol>…</ol> | ||
</section> | ||
``` | ||
Each definition is generated as a `<li>` in the `<ol>` in the order they were | ||
first referenced: | ||
```html | ||
<li id="user-content-fn-1">…</li> | ||
``` | ||
Backreferences are injected at the end of the first paragraph, or, when there | ||
is no paragraph, at the end of the definition. | ||
When a definition is referenced multiple times, multiple backreferences are | ||
generated. | ||
Further backreferences use an extra counter in the `href` attribute and | ||
visually in a `<span>` after `↩`. | ||
```html | ||
<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> | ||
``` | ||
See | ||
[*§ 4.5.1 The `a` element*][html-a], | ||
[*§ 4.3.6 The `h1`, `h2`, `h3`, `h4`, `h5`, and `h6` elements*][html-h], | ||
[*§ 4.4.8 The `li` element*][html-li], | ||
[*§ 4.4.5 The `ol` element*][html-ol], | ||
[*§ 4.4.1 The `p` element*][html-p], | ||
[*§ 4.3.3 The `section` element*][html-section], and | ||
[*§ 4.5.19 The `sub` and `sup` elements*][html-sup] | ||
in the HTML spec, and | ||
[*§ 6.8 `aria-label` property*][aria-label] | ||
in WAI-ARIA, for more info. | ||
## CSS | ||
The following CSS is needed to make footnotes look a bit like GitHub (and fixes | ||
a bug). | ||
For the complete actual CSS see | ||
@@ -220,3 +439,3 @@ [`sindresorhus/github-markdown-css`](https://github.com/sindresorhus/github-markdown-css). | ||
/* Place `[` and `]` around footnote calls. */ | ||
/* Place `[` and `]` around footnote references. */ | ||
[data-footnote-ref]::before { | ||
@@ -231,25 +450,105 @@ content: '['; | ||
## Syntax | ||
Footnotes form with, roughly, the following BNF: | ||
```bnf | ||
gfm_footnote_reference ::= gfm_footnote_label | ||
gfm_footnote_definition_start ::= gfm_footnote_label ':' *space_or_tab | ||
; Restriction: blank line allowed. | ||
gfm_footnote_definition_cont ::= 4(space_or_tab) | ||
; Restriction: maximum `999` codes between `^` and `]`. | ||
gfm_footnote_label ::= '[' '^' 1*(gfm_footnote_label_byte | gfm_footnote_label_escape) ']' | ||
gfm_footnote_label_byte ::= text - '[' - '\\' - ']' | ||
gfm_footnote_label_escape ::= '\\' ['[' | '\\' | ']'] | ||
; Any byte (u8) | ||
byte ::= 0x00..=0xFFFF | ||
space_or_tab ::= '\t' | ' ' | ||
eol ::= '\n' | '\r' | '\r\n' | ||
line ::= byte - eol | ||
text ::= line - space_or_tab | ||
``` | ||
Further lines after `gfm_footnote_definition_start` that are not prefixed with | ||
`gfm_footnote_definition_cont` cause the footnote definition to be exited, | ||
except when those lines are lazy continuation or blank. | ||
Like so many things in markdown, footnote definition too are complex. | ||
See [*§ Phase 1: block structure* in `CommonMark`][commonmark-block] for more | ||
on parsing details. | ||
<!-- | ||
To do: update link when `string` is documented on its own. | ||
Also, add links to character escape/reference constructs. | ||
--> | ||
The identifiers in the `label` parts are interpreted as the | ||
[string][micromark-content-types] content type. | ||
That means that character escapes and character references are allowed. | ||
Definitions match to references through identifiers. | ||
To match, both labels must be equal after normalizing with | ||
[`normalizeIdentifier`][micromark-normalize-identifier]. | ||
One definition can match to multiple calls. | ||
Multiple definitions with the same, normalized, identifier are ignored: the | ||
first definition is preferred. | ||
To illustrate, the definition with the content of `x` wins: | ||
```markdown | ||
[^a]: x | ||
[^a]: y | ||
[^a] | ||
``` | ||
Importantly, while labels *can* include [string][micromark-content-types] | ||
content (character escapes and character references), these are not considered | ||
when matching. | ||
To illustrate, neither definition matches the reference: | ||
```markdown | ||
[^a&b]: x | ||
[^a\&b]: y | ||
[^a&b] | ||
``` | ||
Because footnote definitions are containers (like block quotes and list items), | ||
they can contain more footnote definitions. | ||
They can even include references to themselves. | ||
## Types | ||
This package is fully typed with [TypeScript][]. | ||
It exports additional `HtmlOptions` type that models its respective interface. | ||
It exports the additional types [`BackLabelTemplate`][api-back-label-template] | ||
and [`HtmlOptions`][api-html-options]. | ||
## Compatibility | ||
This package is at least compatible with all maintained versions of Node.js. | ||
As of now, that is Node.js 12.20+, 14.14+, and 16.0+. | ||
It also works in Deno and modern browsers. | ||
Projects maintained by the unified collective are compatible with all maintained | ||
versions of Node.js. | ||
As of now, that is Node.js 14.14+. | ||
Our projects sometimes work with older versions, but this is not guaranteed. | ||
These extensions work with `micromark` version 3+. | ||
## Security | ||
This package is safe. | ||
Setting `clobberPrefix = ''` is dangerous, it opens you up to DOM clobbering. | ||
The `labelTagName` and `labelAttributes` options are unsafe when used with user | ||
content, they allow defining arbitrary HTML. | ||
## Related | ||
* [`syntax-tree/mdast-util-gfm-footnote`][mdast-util-gfm-footnote] | ||
— support GFM footnotes in mdast | ||
* [`syntax-tree/mdast-util-gfm`][mdast-util-gfm] | ||
— support GFM in mdast | ||
* [`remarkjs/remark-gfm`][remark-gfm] | ||
— support GFM in remark | ||
* [`micromark-extension-gfm`][micromark-extension-gfm] | ||
— support all of GFM | ||
* [`mdast-util-gfm-footnote`][mdast-util-gfm-footnote] | ||
— support all of GFM in mdast | ||
* [`mdast-util-gfm`][mdast-util-gfm] | ||
— support all of GFM in mdast | ||
* [`remark-gfm`][remark-gfm] | ||
— support all of GFM in remark | ||
@@ -300,3 +599,3 @@ ## Contribute | ||
[skypack]: https://www.skypack.dev | ||
[esmsh]: https://esm.sh | ||
@@ -317,6 +616,14 @@ [license]: license | ||
[development]: https://nodejs.org/api/packages.html#packages_resolving_user_conditions | ||
[micromark]: https://github.com/micromark/micromark | ||
[remark]: https://github.com/remarkjs/remark | ||
[micromark-content-types]: https://github.com/micromark/micromark#content-types | ||
[micromark-extension]: https://github.com/micromark/micromark#syntaxextension | ||
[micromark-html-extension]: https://github.com/micromark/micromark#htmlextension | ||
[micromark-normalize-identifier]: https://github.com/micromark/micromark/tree/main/packages/micromark-util-normalize-identifier | ||
[micromark-extension-gfm]: https://github.com/micromark/micromark-extension-gfm | ||
@@ -333,1 +640,33 @@ | ||
[cmark-gfm]: https://github.com/github/cmark-gfm | ||
[commonmark-block]: https://spec.commonmark.org/0.30/#phase-1-block-structure | ||
[html-a]: https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element | ||
[html-data]: https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes | ||
[html-h]: https://html.spec.whatwg.org/multipage/sections.html#the-h1,-h2,-h3,-h4,-h5,-and-h6-elements | ||
[html-li]: https://html.spec.whatwg.org/multipage/grouping-content.html#the-li-element | ||
[html-ol]: https://html.spec.whatwg.org/multipage/grouping-content.html#the-ol-element | ||
[html-p]: https://html.spec.whatwg.org/multipage/grouping-content.html#the-p-element | ||
[html-section]: https://html.spec.whatwg.org/multipage/sections.html#the-section-element | ||
[html-sup]: https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-sub-and-sup-elements | ||
[aria-describedby]: https://w3c.github.io/aria/#aria-describedby | ||
[aria-label]: https://w3c.github.io/aria/#aria-label | ||
[api-gfm-footnote]: #gfmfootnote | ||
[api-gfm-footnote-html]: #gfmfootnotehtmloptions | ||
[api-html-options]: #htmloptions | ||
[api-default-back-label]: #defaultbacklabelreferenceindex-rereferenceindex | ||
[api-back-label-template]: #backlabeltemplate |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
87601
11
2000
659