Refactor routing in App component to enhance navigation and improve error handling by integrating dynamic routes and updating the NotFound route.

This commit is contained in:
becarta
2025-05-23 12:43:00 +02:00
parent f40db0f5c9
commit a544759a3b
11127 changed files with 1647032 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
/**
* 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 (optional).
* @returns {HtmlExtension}
* Extension for `micromark` that can be passed in `htmlExtensions` to
* support GFM footnotes when serializing to HTML.
*/
export function gfmFootnoteHtml(options?: Options | null | undefined): HtmlExtension;
import type { HtmlOptions as Options } from 'micromark-extension-gfm-footnote';
import type { HtmlExtension } from 'micromark-util-types';

View File

@@ -0,0 +1,154 @@
/**
* @import {HtmlOptions as Options} from 'micromark-extension-gfm-footnote'
* @import {HtmlExtension} from 'micromark-util-types'
*/
import { normalizeIdentifier } from 'micromark-util-normalize-identifier';
import { sanitizeUri } from 'micromark-util-sanitize-uri';
const own = {}.hasOwnProperty;
/** @type {Options} */
const emptyOptions = {};
/**
* 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 (optional).
* @returns {HtmlExtension}
* Extension for `micromark` that can be passed in `htmlExtensions` to
* support GFM footnotes when serializing to HTML.
*/
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 = config.clobberPrefix === null || config.clobberPrefix === undefined ? 'user-content-' : config.clobberPrefix;
return {
enter: {
gfmFootnoteDefinition() {
const stack = this.getData('tightStack');
stack.push(false);
},
gfmFootnoteDefinitionLabelString() {
this.buffer();
},
gfmFootnoteCallString() {
this.buffer();
}
},
exit: {
gfmFootnoteDefinition() {
let definitions = this.getData('gfmFootnoteDefinitions');
const footnoteStack = this.getData('gfmFootnoteDefinitionStack');
const tightStack = this.getData('tightStack');
const current = footnoteStack.pop();
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 were in a definition in
// an empty list item.
this.setData('lastWasTag');
},
gfmFootnoteDefinitionLabelString(token) {
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.
},
gfmFootnoteCallString(token) {
let calls = this.getData('gfmFootnoteCallOrder');
let counts = this.getData('gfmFootnoteCallCounts');
const id = normalizeIdentifier(this.sliceSerialize(token));
/** @type {number} */
let counter;
this.resume();
if (!calls) this.setData('gfmFootnoteCallOrder', calls = []);
if (!counts) this.setData('gfmFootnoteCallCounts', counts = {});
const index = calls.indexOf(id);
const safeId = sanitizeUri(id.toLowerCase());
if (index === -1) {
calls.push(id);
counts[id] = 1;
counter = calls.length;
} else {
counts[id]++;
counter = index + 1;
}
const reuseCounter = counts[id];
this.tag('<sup><a href="#' + clobberPrefix + 'fn-' + safeId + '" id="' + clobberPrefix + 'fnref-' + safeId + (reuseCounter > 1 ? '-' + reuseCounter : '') + '" data-footnote-ref="" aria-describedby="footnote-label">' + String(counter) + '</a></sup>');
},
null() {
const calls = this.getData('gfmFootnoteCallOrder') || [];
const counts = this.getData('gfmFootnoteCallCounts') || {};
const definitions = this.getData('gfmFootnoteDefinitions') || {};
let index = -1;
if (calls.length > 0) {
this.lineEndingIfNeeded();
this.tag('<section data-footnotes="" class="footnotes"><' + labelTagName + ' id="footnote-label"' + (labelAttributes ? ' ' + labelAttributes : '') + '>');
this.raw(this.encode(label));
this.tag('</' + labelTagName + '>');
this.lineEndingIfNeeded();
this.tag('<ol>');
}
while (++index < calls.length) {
// Called definitions are always defined.
const id = calls[index];
const safeId = sanitizeUri(id.toLowerCase());
let referenceIndex = 0;
/** @type {Array<string>} */
const references = [];
while (++referenceIndex <= counts[id]) {
references.push('<a href="#' + clobberPrefix + 'fnref-' + safeId + (referenceIndex > 1 ? '-' + referenceIndex : '') + '" data-footnote-backref="" aria-label="' + this.encode(typeof backLabel === 'string' ? backLabel : backLabel(index, referenceIndex)) + '" class="data-footnote-backref">↩' + (referenceIndex > 1 ? '<sup>' + referenceIndex + '</sup>' : '') + '</a>');
}
const reference = references.join(' ');
let injected = false;
this.lineEndingIfNeeded();
this.tag('<li id="' + clobberPrefix + 'fn-' + safeId + '">');
this.lineEndingIfNeeded();
this.tag(definitions[id].replace(/<\/p>(?:\r?\n|\r)?$/, function ($0) {
injected = true;
return ' ' + reference + $0;
}));
if (!injected) {
this.lineEndingIfNeeded();
this.tag(reference);
}
this.lineEndingIfNeeded();
this.tag('</li>');
}
if (calls.length > 0) {
this.lineEndingIfNeeded();
this.tag('</ol>');
this.lineEndingIfNeeded();
this.tag('</section>');
}
}
}
};
}

View File

@@ -0,0 +1,9 @@
/**
* 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;
import type { Extension } from 'micromark-util-types';

View File

@@ -0,0 +1,485 @@
/**
* @import {Event, Exiter, Extension, Resolver, State, Token, TokenizeContext, Tokenizer} from 'micromark-util-types'
*/
import { blankLine } from 'micromark-core-commonmark';
import { factorySpace } from 'micromark-factory-space';
import { markdownLineEndingOrSpace } from 'micromark-util-character';
import { normalizeIdentifier } from 'micromark-util-normalize-identifier';
const indent = {
tokenize: tokenizeIndent,
partial: true
};
// 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() {
/** @type {Extension} */
return {
document: {
[91]: {
name: 'gfmFootnoteDefinition',
tokenize: tokenizeDefinitionStart,
continuation: {
tokenize: tokenizeDefinitionContinuation
},
exit: gfmFootnoteDefinitionEnd
}
},
text: {
[91]: {
name: 'gfmFootnoteCall',
tokenize: tokenizeGfmFootnoteCall
},
[93]: {
name: 'gfmPotentialFootnoteCall',
add: 'after',
tokenize: tokenizePotentialGfmFootnoteCall,
resolveTo: resolveToPotentialGfmFootnoteCall
}
}
};
}
// To do: remove after micromark update.
/**
* @this {TokenizeContext}
* @type {Tokenizer}
*/
function tokenizePotentialGfmFootnoteCall(effects, ok, nok) {
const self = this;
let index = self.events.length;
const defined = self.parser.gfmFootnotes || (self.parser.gfmFootnotes = []);
/** @type {Token} */
let labelStart;
// Find an opening.
while (index--) {
const token = self.events[index][1];
if (token.type === "labelImage") {
labelStart = token;
break;
}
// Exit if weve walked far enough.
if (token.type === 'gfmFootnoteCall' || token.type === "labelLink" || token.type === "label" || token.type === "image" || token.type === "link") {
break;
}
}
return start;
/**
* @type {State}
*/
function start(code) {
if (!labelStart || !labelStart._balanced) {
return nok(code);
}
const id = normalizeIdentifier(self.sliceSerialize({
start: labelStart.end,
end: self.now()
}));
if (id.codePointAt(0) !== 94 || !defined.includes(id.slice(1))) {
return nok(code);
}
effects.enter('gfmFootnoteCallLabelMarker');
effects.consume(code);
effects.exit('gfmFootnoteCallLabelMarker');
return ok(code);
}
}
// To do: remove after micromark update.
/** @type {Resolver} */
function resolveToPotentialGfmFootnoteCall(events, context) {
let index = events.length;
/** @type {Token | undefined} */
let labelStart;
// Find an opening.
while (index--) {
if (events[index][1].type === "labelImage" && events[index][0] === 'enter') {
labelStart = events[index][1];
break;
}
}
// Change the `labelImageMarker` to a `data`.
events[index + 1][1].type = "data";
events[index + 3][1].type = 'gfmFootnoteCallLabelMarker';
// The whole (without `!`):
/** @type {Token} */
const call = {
type: 'gfmFootnoteCall',
start: Object.assign({}, events[index + 3][1].start),
end: Object.assign({}, events[events.length - 1][1].end)
};
// The `^` marker
/** @type {Token} */
const marker = {
type: 'gfmFootnoteCallMarker',
start: Object.assign({}, events[index + 3][1].end),
end: Object.assign({}, events[index + 3][1].end)
};
// Increment the end 1 character.
marker.end.column++;
marker.end.offset++;
marker.end._bufferIndex++;
/** @type {Token} */
const string = {
type: 'gfmFootnoteCallString',
start: Object.assign({}, marker.end),
end: Object.assign({}, events[events.length - 1][1].start)
};
/** @type {Token} */
const chunk = {
type: "chunkString",
contentType: 'string',
start: Object.assign({}, string.start),
end: Object.assign({}, string.end)
};
/** @type {Array<Event>} */
const replacement = [
// Take the `labelImageMarker` (now `data`, the `!`)
events[index + 1], events[index + 2], ['enter', call, context],
// The `[`
events[index + 3], events[index + 4],
// The `^`.
['enter', marker, context], ['exit', marker, context],
// Everything in between.
['enter', string, context], ['enter', chunk, context], ['exit', chunk, context], ['exit', string, context],
// The ending (`]`, properly parsed and labelled).
events[events.length - 2], events[events.length - 1], ['exit', call, context]];
events.splice(index, events.length - index + 1, ...replacement);
return events;
}
/**
* @this {TokenizeContext}
* @type {Tokenizer}
*/
function tokenizeGfmFootnoteCall(effects, ok, nok) {
const self = this;
const defined = self.parser.gfmFootnotes || (self.parser.gfmFootnotes = []);
let size = 0;
/** @type {boolean} */
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 cant do that, but luckily, we can parse footnotes in a simpler way than
// needed for labels.
return start;
/**
* Start of footnote label.
*
* ```markdown
* > | a [^b] c
* ^
* ```
*
* @type {State}
*/
function start(code) {
effects.enter('gfmFootnoteCall');
effects.enter('gfmFootnoteCallLabelMarker');
effects.consume(code);
effects.exit('gfmFootnoteCallLabelMarker');
return callStart;
}
/**
* After `[`, at `^`.
*
* ```markdown
* > | a [^b] c
* ^
* ```
*
* @type {State}
*/
function callStart(code) {
if (code !== 94) return nok(code);
effects.enter('gfmFootnoteCallMarker');
effects.consume(code);
effects.exit('gfmFootnoteCallMarker');
effects.enter('gfmFootnoteCallString');
effects.enter('chunkString').contentType = 'string';
return callData;
}
/**
* In label.
*
* ```markdown
* > | a [^b] c
* ^
* ```
*
* @type {State}
*/
function callData(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) {
effects.exit('chunkString');
const token = effects.exit('gfmFootnoteCallString');
if (!defined.includes(normalizeIdentifier(self.sliceSerialize(token)))) {
return nok(code);
}
effects.enter('gfmFootnoteCallLabelMarker');
effects.consume(code);
effects.exit('gfmFootnoteCallLabelMarker');
effects.exit('gfmFootnoteCall');
return ok;
}
if (!markdownLineEndingOrSpace(code)) {
data = true;
}
size++;
effects.consume(code);
return code === 92 ? callEscape : callData;
}
/**
* On character after escape.
*
* ```markdown
* > | a [^b\c] d
* ^
* ```
*
* @type {State}
*/
function callEscape(code) {
if (code === 91 || code === 92 || code === 93) {
effects.consume(code);
size++;
return callData;
}
return callData(code);
}
}
/**
* @this {TokenizeContext}
* @type {Tokenizer}
*/
function tokenizeDefinitionStart(effects, ok, nok) {
const self = this;
const defined = self.parser.gfmFootnotes || (self.parser.gfmFootnotes = []);
/** @type {string} */
let identifier;
let size = 0;
/** @type {boolean | undefined} */
let data;
return start;
/**
* Start of GFM footnote definition.
*
* ```markdown
* > | [^a]: b
* ^
* ```
*
* @type {State}
*/
function start(code) {
effects.enter('gfmFootnoteDefinition')._container = true;
effects.enter('gfmFootnoteDefinitionLabel');
effects.enter('gfmFootnoteDefinitionLabelMarker');
effects.consume(code);
effects.exit('gfmFootnoteDefinitionLabelMarker');
return labelAtMarker;
}
/**
* In label, at caret.
*
* ```markdown
* > | [^a]: b
* ^
* ```
*
* @type {State}
*/
function labelAtMarker(code) {
if (code === 94) {
effects.enter('gfmFootnoteDefinitionMarker');
effects.consume(code);
effects.exit('gfmFootnoteDefinitionMarker');
effects.enter('gfmFootnoteDefinitionLabelString');
effects.enter('chunkString').contentType = 'string';
return labelInside;
}
return nok(code);
}
/**
* 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) {
effects.exit('chunkString');
const token = effects.exit('gfmFootnoteDefinitionLabelString');
identifier = normalizeIdentifier(self.sliceSerialize(token));
effects.enter('gfmFootnoteDefinitionLabelMarker');
effects.consume(code);
effects.exit('gfmFootnoteDefinitionLabelMarker');
effects.exit('gfmFootnoteDefinitionLabel');
return labelAfter;
}
if (!markdownLineEndingOrSpace(code)) {
data = true;
}
size++;
effects.consume(code);
return code === 92 ? labelEscape : labelInside;
}
/**
* 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) {
if (code === 91 || code === 92 || code === 93) {
effects.consume(code);
size++;
return labelInside;
}
return labelInside(code);
}
/**
* After definition label.
*
* ```markdown
* > | [^a]: b
* ^
* ```
*
* @type {State}
*/
function labelAfter(code) {
if (code === 58) {
effects.enter('definitionMarker');
effects.consume(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, whitespaceAfter, 'gfmFootnoteDefinitionWhitespace');
}
return nok(code);
}
/**
* 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);
}
}
/**
* @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');
}
/**
* @this {TokenizeContext}
* @type {Tokenizer}
*/
function tokenizeIndent(effects, ok, nok) {
const self = this;
return factorySpace(effects, afterPrefix, 'gfmFootnoteDefinitionIndent', 4 + 1);
/**
* @type {State}
*/
function afterPrefix(code) {
const tail = self.events[self.events.length - 1];
return tail && tail[1].type === 'gfmFootnoteDefinitionIndent' && tail[2].sliceSerialize(tail[1], true).length === 4 ? ok(code) : nok(code);
}
}