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,38 @@
/**
* @import {Event} from 'micromark-util-types'
*/
/**
* @typedef {[number, number, Array<Event>]} Change
* @typedef {[number, number, number]} Jump
*/
/**
* Tracks a bunch of edits.
*/
export class EditMap {
/**
* Record of changes.
*
* @type {Array<Change>}
*/
map: Array<Change>;
/**
* Create an edit: a remove and/or add at a certain place.
*
* @param {number} index
* @param {number} remove
* @param {Array<Event>} add
* @returns {undefined}
*/
add(index: number, remove: number, add: Array<Event>): undefined;
/**
* Done, change the events.
*
* @param {Array<Event>} events
* @returns {undefined}
*/
consume(events: Array<Event>): undefined;
}
export type Change = [number, number, Array<Event>];
export type Jump = [number, number, number];
import type { Event } from 'micromark-util-types';
//# sourceMappingURL=edit-map.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"edit-map.d.ts","sourceRoot":"","sources":["edit-map.js"],"names":[],"mappings":"AAAA;;GAEG;AAeH;;;GAGG;AAEH;;GAEG;AACH;IAKI;;;;OAIG;IACH,KAFU,KAAK,CAAC,MAAM,CAAC,CAEV;IAGf;;;;;;;OAOG;IACH,WALW,MAAM,UACN,MAAM,OACN,KAAK,CAAC,KAAK,CAAC,GACV,SAAS,CAIrB;IAeD;;;;;OAKG;IACH,gBAHW,KAAK,CAAC,KAAK,CAAC,GACV,SAAS,CA2DrB;CACF;qBA7GY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;mBAC9B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;2BAlBb,sBAAsB"}

View File

@@ -0,0 +1,202 @@
/**
* @import {Event} from 'micromark-util-types'
*/
// Port of `edit_map.rs` from `markdown-rs`.
// This should move to `markdown-js` later.
// Deal with several changes in events, batching them together.
//
// Preferably, changes should be kept to a minimum.
// Sometimes, its needed to change the list of events, because parsing can be
// messy, and it helps to expose a cleaner interface of events to the compiler
// and other users.
// It can also help to merge many adjacent similar events.
// And, in other cases, its needed to parse subcontent: pass some events
// through another tokenizer and inject the result.
/**
* @typedef {[number, number, Array<Event>]} Change
* @typedef {[number, number, number]} Jump
*/
/**
* Tracks a bunch of edits.
*/
export class EditMap {
/**
* Create a new edit map.
*/
constructor() {
/**
* Record of changes.
*
* @type {Array<Change>}
*/
this.map = [];
}
/**
* Create an edit: a remove and/or add at a certain place.
*
* @param {number} index
* @param {number} remove
* @param {Array<Event>} add
* @returns {undefined}
*/
add(index, remove, add) {
addImplementation(this, index, remove, add);
}
// To do: add this when moving to `micromark`.
// /**
// * Create an edit: but insert `add` before existing additions.
// *
// * @param {number} index
// * @param {number} remove
// * @param {Array<Event>} add
// * @returns {undefined}
// */
// addBefore(index, remove, add) {
// addImplementation(this, index, remove, add, true)
// }
/**
* Done, change the events.
*
* @param {Array<Event>} events
* @returns {undefined}
*/
consume(events) {
this.map.sort(function (a, b) {
return a[0] - b[0];
});
/* c8 ignore next 3 -- `resolve` is never called without tables, so without edits. */
if (this.map.length === 0) {
return;
}
// To do: if links are added in events, like they are in `markdown-rs`,
// this is needed.
// // Calculate jumps: where items in the current list move to.
// /** @type {Array<Jump>} */
// const jumps = []
// let index = 0
// let addAcc = 0
// let removeAcc = 0
// while (index < this.map.length) {
// const [at, remove, add] = this.map[index]
// removeAcc += remove
// addAcc += add.length
// jumps.push([at, removeAcc, addAcc])
// index += 1
// }
//
// . shiftLinks(events, jumps)
let index = this.map.length;
/** @type {Array<Array<Event>>} */
const vecs = [];
while (index > 0) {
index -= 1;
vecs.push(events.slice(this.map[index][0] + this.map[index][1]), this.map[index][2]);
// Truncate rest.
events.length = this.map[index][0];
}
vecs.push(events.slice());
events.length = 0;
let slice = vecs.pop();
while (slice) {
for (const element of slice) {
events.push(element);
}
slice = vecs.pop();
}
// Truncate everything.
this.map.length = 0;
}
}
/**
* Create an edit.
*
* @param {EditMap} editMap
* @param {number} at
* @param {number} remove
* @param {Array<Event>} add
* @returns {undefined}
*/
function addImplementation(editMap, at, remove, add) {
let index = 0;
/* c8 ignore next 3 -- `resolve` is never called without tables, so without edits. */
if (remove === 0 && add.length === 0) {
return;
}
while (index < editMap.map.length) {
if (editMap.map[index][0] === at) {
editMap.map[index][1] += remove;
// To do: before not used by tables, use when moving to micromark.
// if (before) {
// add.push(...editMap.map[index][2])
// editMap.map[index][2] = add
// } else {
editMap.map[index][2].push(...add);
// }
return;
}
index += 1;
}
editMap.map.push([at, remove, add]);
}
// /**
// * Shift `previous` and `next` links according to `jumps`.
// *
// * This fixes links in case there are events removed or added between them.
// *
// * @param {Array<Event>} events
// * @param {Array<Jump>} jumps
// */
// function shiftLinks(events, jumps) {
// let jumpIndex = 0
// let index = 0
// let add = 0
// let rm = 0
// while (index < events.length) {
// const rmCurr = rm
// while (jumpIndex < jumps.length && jumps[jumpIndex][0] <= index) {
// add = jumps[jumpIndex][2]
// rm = jumps[jumpIndex][1]
// jumpIndex += 1
// }
// // Ignore items that will be removed.
// if (rm > rmCurr) {
// index += rm - rmCurr
// } else {
// // ?
// // if let Some(link) = &events[index].link {
// // if let Some(next) = link.next {
// // events[next].link.as_mut().unwrap().previous = Some(index + add - rm);
// // while jumpIndex < jumps.len() && jumps[jumpIndex].0 <= next {
// // add = jumps[jumpIndex].2;
// // rm = jumps[jumpIndex].1;
// // jumpIndex += 1;
// // }
// // events[index].link.as_mut().unwrap().next = Some(next + add - rm);
// // index = next;
// // continue;
// // }
// // }
// index += 1
// }
// }
// }

View File

@@ -0,0 +1,11 @@
/**
* Create an HTML extension for `micromark` to support GitHub tables when
* serializing to HTML.
*
* @returns {HtmlExtension}
* Extension for `micromark` that can be passed in `htmlExtensions` to
* support GitHub tables when serializing to HTML.
*/
export function gfmTableHtml(): HtmlExtension;
import type { HtmlExtension } from 'micromark-util-types';
//# sourceMappingURL=html.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["html.js"],"names":[],"mappings":"AAeA;;;;;;;GAOG;AACH,gCAJa,aAAa,CAsHzB;mCAxI+B,sBAAsB"}

130
node_modules/micromark-extension-gfm-table/lib/html.js generated vendored Normal file
View File

@@ -0,0 +1,130 @@
/**
* @import {HtmlExtension} from 'micromark-util-types'
*/
const alignment = {
none: '',
left: ' align="left"',
right: ' align="right"',
center: ' align="center"'
};
// To do: micromark@5: use `infer` here, when all events are exposed.
/**
* Create an HTML extension for `micromark` to support GitHub tables when
* serializing to HTML.
*
* @returns {HtmlExtension}
* Extension for `micromark` that can be passed in `htmlExtensions` to
* support GitHub tables when serializing to HTML.
*/
export function gfmTableHtml() {
return {
enter: {
table(token) {
const tableAlign = token._align;
this.lineEndingIfNeeded();
this.tag('<table>');
this.setData('tableAlign', tableAlign);
},
tableBody() {
this.tag('<tbody>');
},
tableData() {
const tableAlign = this.getData('tableAlign');
const tableColumn = this.getData('tableColumn');
const align = alignment[tableAlign[tableColumn]];
if (align === undefined) {
// Capture results to ignore them.
this.buffer();
} else {
this.lineEndingIfNeeded();
this.tag('<td' + align + '>');
}
},
tableHead() {
this.lineEndingIfNeeded();
this.tag('<thead>');
},
tableHeader() {
const tableAlign = this.getData('tableAlign');
const tableColumn = this.getData('tableColumn');
const align = alignment[tableAlign[tableColumn]];
this.lineEndingIfNeeded();
this.tag('<th' + align + '>');
},
tableRow() {
this.setData('tableColumn', 0);
this.lineEndingIfNeeded();
this.tag('<tr>');
}
},
exit: {
// Overwrite the default code text data handler to unescape escaped pipes when
// they are in tables.
codeTextData(token) {
let value = this.sliceSerialize(token);
if (this.getData('tableAlign')) {
value = value.replace(/\\([\\|])/g, replace);
}
this.raw(this.encode(value));
},
table() {
this.setData('tableAlign');
// Note: we dont set `slurpAllLineEndings` anymore, in delimiter rows,
// but we do need to reset it to match a funky newline GH generates for
// list items combined with tables.
this.setData('slurpAllLineEndings');
this.lineEndingIfNeeded();
this.tag('</table>');
},
tableBody() {
this.lineEndingIfNeeded();
this.tag('</tbody>');
},
tableData() {
const tableAlign = this.getData('tableAlign');
const tableColumn = this.getData('tableColumn');
if (tableColumn in tableAlign) {
this.tag('</td>');
this.setData('tableColumn', tableColumn + 1);
} else {
// Stop capturing.
this.resume();
}
},
tableHead() {
this.lineEndingIfNeeded();
this.tag('</thead>');
},
tableHeader() {
const tableColumn = this.getData('tableColumn');
this.tag('</th>');
this.setData('tableColumn', tableColumn + 1);
},
tableRow() {
const tableAlign = this.getData('tableAlign');
let tableColumn = this.getData('tableColumn');
while (tableColumn < tableAlign.length) {
this.lineEndingIfNeeded();
this.tag('<td' + alignment[tableAlign[tableColumn]] + '></td>');
tableColumn++;
}
this.setData('tableColumn', tableColumn);
this.lineEndingIfNeeded();
this.tag('</tr>');
}
}
};
}
/**
* @param {string} $0
* @param {string} $1
* @returns {string}
*/
function replace($0, $1) {
// Pipes work, backslashes dont (but cant escape pipes).
return $1 === '|' ? $1 : $0;
}

View File

@@ -0,0 +1,14 @@
/**
* Figure out the alignment of a GFM table.
*
* @param {Readonly<Array<Event>>} events
* List of events.
* @param {number} index
* Table enter event.
* @returns {Array<Align>}
* List of aligns.
*/
export function gfmTableAlign(events: Readonly<Array<Event>>, index: number): Array<Align>;
export type Align = "center" | "left" | "none" | "right";
import type { Event } from 'micromark-util-types';
//# sourceMappingURL=infer.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"infer.d.ts","sourceRoot":"","sources":["infer.js"],"names":[],"mappings":"AAUA;;;;;;;;;GASG;AACH,sCAPW,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,SAEtB,MAAM,GAEJ,KAAK,CAAC,KAAK,CAAC,CA8CxB;oBA1DY,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO;2BAJzB,sBAAsB"}

View File

@@ -0,0 +1,52 @@
/**
* @import {Event} from 'micromark-util-types'
*/
/**
* @typedef {'center' | 'left' | 'none' | 'right'} Align
*/
/**
* Figure out the alignment of a GFM table.
*
* @param {Readonly<Array<Event>>} events
* List of events.
* @param {number} index
* Table enter event.
* @returns {Array<Align>}
* List of aligns.
*/
export function gfmTableAlign(events, index) {
let inDelimiterRow = false;
/** @type {Array<Align>} */
const align = [];
while (index < events.length) {
const event = events[index];
if (inDelimiterRow) {
if (event[0] === 'enter') {
// Start of alignment value: set a new column.
// To do: `markdown-rs` uses `tableDelimiterCellValue`.
if (event[1].type === 'tableContent') {
align.push(events[index + 1][1].type === 'tableDelimiterMarker' ? 'left' : 'none');
}
}
// Exits:
// End of alignment value: change the column.
// To do: `markdown-rs` uses `tableDelimiterCellValue`.
else if (event[1].type === 'tableContent') {
if (events[index - 1][1].type === 'tableDelimiterMarker') {
const alignIndex = align.length - 1;
align[alignIndex] = align[alignIndex] === 'left' ? 'center' : 'right';
}
}
// Done!
else if (event[1].type === 'tableDelimiterRow') {
break;
}
} else if (event[0] === 'enter' && event[1].type === 'tableDelimiterRow') {
inDelimiterRow = true;
}
index += 1;
}
return align;
}

View File

@@ -0,0 +1,18 @@
/**
* Create an HTML extension for `micromark` to support GitHub tables syntax.
*
* @returns {Extension}
* Extension for `micromark` that can be passed in `extensions` to enable GFM
* table syntax.
*/
export function gfmTable(): Extension;
/**
* Cell info.
*/
export type Range = [number, number, number, number];
/**
* Where we are: `1` for head row, `2` for delimiter row, `3` for body row.
*/
export type RowKind = 0 | 1 | 2 | 3;
import type { Extension } from 'micromark-util-types';
//# sourceMappingURL=syntax.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"syntax.d.ts","sourceRoot":"","sources":["syntax.js"],"names":[],"mappings":"AAuBA;;;;;;GAMG;AACH,4BAJa,SAAS,CAUrB;;;;oBA/BY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;sBAGhC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC;+BAPoE,sBAAsB"}

View File

@@ -0,0 +1,819 @@
/**
* @import {Event, Extension, Point, Resolver, State, Token, TokenizeContext, Tokenizer} from 'micromark-util-types'
*/
/**
* @typedef {[number, number, number, number]} Range
* Cell info.
*
* @typedef {0 | 1 | 2 | 3} RowKind
* Where we are: `1` for head row, `2` for delimiter row, `3` for body row.
*/
import { factorySpace } from 'micromark-factory-space';
import { markdownLineEnding, markdownLineEndingOrSpace, markdownSpace } from 'micromark-util-character';
import { EditMap } from './edit-map.js';
import { gfmTableAlign } from './infer.js';
/**
* Create an HTML extension for `micromark` to support GitHub tables syntax.
*
* @returns {Extension}
* Extension for `micromark` that can be passed in `extensions` to enable GFM
* table syntax.
*/
export function gfmTable() {
return {
flow: {
null: {
name: 'table',
tokenize: tokenizeTable,
resolveAll: resolveTable
}
}
};
}
/**
* @this {TokenizeContext}
* @type {Tokenizer}
*/
function tokenizeTable(effects, ok, nok) {
const self = this;
let size = 0;
let sizeB = 0;
/** @type {boolean | undefined} */
let seen;
return start;
/**
* Start of a GFM table.
*
* If there is a valid table row or table head before, then we try to parse
* another row.
* Otherwise, we try to parse a head.
*
* ```markdown
* > | | a |
* ^
* | | - |
* > | | b |
* ^
* ```
* @type {State}
*/
function start(code) {
let index = self.events.length - 1;
while (index > -1) {
const type = self.events[index][1].type;
if (type === "lineEnding" ||
// Note: markdown-rs uses `whitespace` instead of `linePrefix`
type === "linePrefix") index--;else break;
}
const tail = index > -1 ? self.events[index][1].type : null;
const next = tail === 'tableHead' || tail === 'tableRow' ? bodyRowStart : headRowBefore;
// Dont allow lazy body rows.
if (next === bodyRowStart && self.parser.lazy[self.now().line]) {
return nok(code);
}
return next(code);
}
/**
* Before table head row.
*
* ```markdown
* > | | a |
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowBefore(code) {
effects.enter('tableHead');
effects.enter('tableRow');
return headRowStart(code);
}
/**
* Before table head row, after whitespace.
*
* ```markdown
* > | | a |
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowStart(code) {
if (code === 124) {
return headRowBreak(code);
}
// To do: micromark-js should let us parse our own whitespace in extensions,
// like `markdown-rs`:
//
// ```js
// // 4+ spaces.
// if (markdownSpace(code)) {
// return nok(code)
// }
// ```
seen = true;
// Count the first character, that isnt a pipe, double.
sizeB += 1;
return headRowBreak(code);
}
/**
* At break in table head row.
*
* ```markdown
* > | | a |
* ^
* ^
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowBreak(code) {
if (code === null) {
// Note: in `markdown-rs`, we need to reset, in `micromark-js` we dont.
return nok(code);
}
if (markdownLineEnding(code)) {
// If anything other than one pipe (ignoring whitespace) was used, its fine.
if (sizeB > 1) {
sizeB = 0;
// To do: check if this works.
// Feel free to interrupt:
self.interrupt = true;
effects.exit('tableRow');
effects.enter("lineEnding");
effects.consume(code);
effects.exit("lineEnding");
return headDelimiterStart;
}
// Note: in `markdown-rs`, we need to reset, in `micromark-js` we dont.
return nok(code);
}
if (markdownSpace(code)) {
// To do: check if this is fine.
// effects.attempt(State::Next(StateName::GfmTableHeadRowBreak), State::Nok)
// State::Retry(space_or_tab(tokenizer))
return factorySpace(effects, headRowBreak, "whitespace")(code);
}
sizeB += 1;
if (seen) {
seen = false;
// Header cell count.
size += 1;
}
if (code === 124) {
effects.enter('tableCellDivider');
effects.consume(code);
effects.exit('tableCellDivider');
// Whether a delimiter was seen.
seen = true;
return headRowBreak;
}
// Anything else is cell data.
effects.enter("data");
return headRowData(code);
}
/**
* In table head row data.
*
* ```markdown
* > | | a |
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowData(code) {
if (code === null || code === 124 || markdownLineEndingOrSpace(code)) {
effects.exit("data");
return headRowBreak(code);
}
effects.consume(code);
return code === 92 ? headRowEscape : headRowData;
}
/**
* In table head row escape.
*
* ```markdown
* > | | a\-b |
* ^
* | | ---- |
* | | c |
* ```
*
* @type {State}
*/
function headRowEscape(code) {
if (code === 92 || code === 124) {
effects.consume(code);
return headRowData;
}
return headRowData(code);
}
/**
* Before delimiter row.
*
* ```markdown
* | | a |
* > | | - |
* ^
* | | b |
* ```
*
* @type {State}
*/
function headDelimiterStart(code) {
// Reset `interrupt`.
self.interrupt = false;
// Note: in `markdown-rs`, we need to handle piercing here too.
if (self.parser.lazy[self.now().line]) {
return nok(code);
}
effects.enter('tableDelimiterRow');
// Track if weve seen a `:` or `|`.
seen = false;
if (markdownSpace(code)) {
return factorySpace(effects, headDelimiterBefore, "linePrefix", self.parser.constructs.disable.null.includes('codeIndented') ? undefined : 4)(code);
}
return headDelimiterBefore(code);
}
/**
* Before delimiter row, after optional whitespace.
*
* Reused when a `|` is found later, to parse another cell.
*
* ```markdown
* | | a |
* > | | - |
* ^
* | | b |
* ```
*
* @type {State}
*/
function headDelimiterBefore(code) {
if (code === 45 || code === 58) {
return headDelimiterValueBefore(code);
}
if (code === 124) {
seen = true;
// If we start with a pipe, we open a cell marker.
effects.enter('tableCellDivider');
effects.consume(code);
effects.exit('tableCellDivider');
return headDelimiterCellBefore;
}
// More whitespace / empty row not allowed at start.
return headDelimiterNok(code);
}
/**
* After `|`, before delimiter cell.
*
* ```markdown
* | | a |
* > | | - |
* ^
* ```
*
* @type {State}
*/
function headDelimiterCellBefore(code) {
if (markdownSpace(code)) {
return factorySpace(effects, headDelimiterValueBefore, "whitespace")(code);
}
return headDelimiterValueBefore(code);
}
/**
* Before delimiter cell value.
*
* ```markdown
* | | a |
* > | | - |
* ^
* ```
*
* @type {State}
*/
function headDelimiterValueBefore(code) {
// Align: left.
if (code === 58) {
sizeB += 1;
seen = true;
effects.enter('tableDelimiterMarker');
effects.consume(code);
effects.exit('tableDelimiterMarker');
return headDelimiterLeftAlignmentAfter;
}
// Align: none.
if (code === 45) {
sizeB += 1;
// To do: seems weird that this *isnt* left aligned, but that state is used?
return headDelimiterLeftAlignmentAfter(code);
}
if (code === null || markdownLineEnding(code)) {
return headDelimiterCellAfter(code);
}
return headDelimiterNok(code);
}
/**
* After delimiter cell left alignment marker.
*
* ```markdown
* | | a |
* > | | :- |
* ^
* ```
*
* @type {State}
*/
function headDelimiterLeftAlignmentAfter(code) {
if (code === 45) {
effects.enter('tableDelimiterFiller');
return headDelimiterFiller(code);
}
// Anything else is not ok after the left-align colon.
return headDelimiterNok(code);
}
/**
* In delimiter cell filler.
*
* ```markdown
* | | a |
* > | | - |
* ^
* ```
*
* @type {State}
*/
function headDelimiterFiller(code) {
if (code === 45) {
effects.consume(code);
return headDelimiterFiller;
}
// Align is `center` if it was `left`, `right` otherwise.
if (code === 58) {
seen = true;
effects.exit('tableDelimiterFiller');
effects.enter('tableDelimiterMarker');
effects.consume(code);
effects.exit('tableDelimiterMarker');
return headDelimiterRightAlignmentAfter;
}
effects.exit('tableDelimiterFiller');
return headDelimiterRightAlignmentAfter(code);
}
/**
* After delimiter cell right alignment marker.
*
* ```markdown
* | | a |
* > | | -: |
* ^
* ```
*
* @type {State}
*/
function headDelimiterRightAlignmentAfter(code) {
if (markdownSpace(code)) {
return factorySpace(effects, headDelimiterCellAfter, "whitespace")(code);
}
return headDelimiterCellAfter(code);
}
/**
* After delimiter cell.
*
* ```markdown
* | | a |
* > | | -: |
* ^
* ```
*
* @type {State}
*/
function headDelimiterCellAfter(code) {
if (code === 124) {
return headDelimiterBefore(code);
}
if (code === null || markdownLineEnding(code)) {
// Exit when:
// * there was no `:` or `|` at all (its a thematic break or setext
// underline instead)
// * the header cell count is not the delimiter cell count
if (!seen || size !== sizeB) {
return headDelimiterNok(code);
}
// Note: in markdown-rs`, a reset is needed here.
effects.exit('tableDelimiterRow');
effects.exit('tableHead');
// To do: in `markdown-rs`, resolvers need to be registered manually.
// effects.register_resolver(ResolveName::GfmTable)
return ok(code);
}
return headDelimiterNok(code);
}
/**
* In delimiter row, at a disallowed byte.
*
* ```markdown
* | | a |
* > | | x |
* ^
* ```
*
* @type {State}
*/
function headDelimiterNok(code) {
// Note: in `markdown-rs`, we need to reset, in `micromark-js` we dont.
return nok(code);
}
/**
* Before table body row.
*
* ```markdown
* | | a |
* | | - |
* > | | b |
* ^
* ```
*
* @type {State}
*/
function bodyRowStart(code) {
// Note: in `markdown-rs` we need to manually take care of a prefix,
// but in `micromark-js` that is done for us, so if were here, were
// never at whitespace.
effects.enter('tableRow');
return bodyRowBreak(code);
}
/**
* At break in table body row.
*
* ```markdown
* | | a |
* | | - |
* > | | b |
* ^
* ^
* ^
* ```
*
* @type {State}
*/
function bodyRowBreak(code) {
if (code === 124) {
effects.enter('tableCellDivider');
effects.consume(code);
effects.exit('tableCellDivider');
return bodyRowBreak;
}
if (code === null || markdownLineEnding(code)) {
effects.exit('tableRow');
return ok(code);
}
if (markdownSpace(code)) {
return factorySpace(effects, bodyRowBreak, "whitespace")(code);
}
// Anything else is cell content.
effects.enter("data");
return bodyRowData(code);
}
/**
* In table body row data.
*
* ```markdown
* | | a |
* | | - |
* > | | b |
* ^
* ```
*
* @type {State}
*/
function bodyRowData(code) {
if (code === null || code === 124 || markdownLineEndingOrSpace(code)) {
effects.exit("data");
return bodyRowBreak(code);
}
effects.consume(code);
return code === 92 ? bodyRowEscape : bodyRowData;
}
/**
* In table body row escape.
*
* ```markdown
* | | a |
* | | ---- |
* > | | b\-c |
* ^
* ```
*
* @type {State}
*/
function bodyRowEscape(code) {
if (code === 92 || code === 124) {
effects.consume(code);
return bodyRowData;
}
return bodyRowData(code);
}
}
/** @type {Resolver} */
function resolveTable(events, context) {
let index = -1;
let inFirstCellAwaitingPipe = true;
/** @type {RowKind} */
let rowKind = 0;
/** @type {Range} */
let lastCell = [0, 0, 0, 0];
/** @type {Range} */
let cell = [0, 0, 0, 0];
let afterHeadAwaitingFirstBodyRow = false;
let lastTableEnd = 0;
/** @type {Token | undefined} */
let currentTable;
/** @type {Token | undefined} */
let currentBody;
/** @type {Token | undefined} */
let currentCell;
const map = new EditMap();
while (++index < events.length) {
const event = events[index];
const token = event[1];
if (event[0] === 'enter') {
// Start of head.
if (token.type === 'tableHead') {
afterHeadAwaitingFirstBodyRow = false;
// Inject previous (body end and) table end.
if (lastTableEnd !== 0) {
flushTableEnd(map, context, lastTableEnd, currentTable, currentBody);
currentBody = undefined;
lastTableEnd = 0;
}
// Inject table start.
currentTable = {
type: 'table',
start: Object.assign({}, token.start),
// Note: correct end is set later.
end: Object.assign({}, token.end)
};
map.add(index, 0, [['enter', currentTable, context]]);
} else if (token.type === 'tableRow' || token.type === 'tableDelimiterRow') {
inFirstCellAwaitingPipe = true;
currentCell = undefined;
lastCell = [0, 0, 0, 0];
cell = [0, index + 1, 0, 0];
// Inject table body start.
if (afterHeadAwaitingFirstBodyRow) {
afterHeadAwaitingFirstBodyRow = false;
currentBody = {
type: 'tableBody',
start: Object.assign({}, token.start),
// Note: correct end is set later.
end: Object.assign({}, token.end)
};
map.add(index, 0, [['enter', currentBody, context]]);
}
rowKind = token.type === 'tableDelimiterRow' ? 2 : currentBody ? 3 : 1;
}
// Cell data.
else if (rowKind && (token.type === "data" || token.type === 'tableDelimiterMarker' || token.type === 'tableDelimiterFiller')) {
inFirstCellAwaitingPipe = false;
// First value in cell.
if (cell[2] === 0) {
if (lastCell[1] !== 0) {
cell[0] = cell[1];
currentCell = flushCell(map, context, lastCell, rowKind, undefined, currentCell);
lastCell = [0, 0, 0, 0];
}
cell[2] = index;
}
} else if (token.type === 'tableCellDivider') {
if (inFirstCellAwaitingPipe) {
inFirstCellAwaitingPipe = false;
} else {
if (lastCell[1] !== 0) {
cell[0] = cell[1];
currentCell = flushCell(map, context, lastCell, rowKind, undefined, currentCell);
}
lastCell = cell;
cell = [lastCell[1], index, 0, 0];
}
}
}
// Exit events.
else if (token.type === 'tableHead') {
afterHeadAwaitingFirstBodyRow = true;
lastTableEnd = index;
} else if (token.type === 'tableRow' || token.type === 'tableDelimiterRow') {
lastTableEnd = index;
if (lastCell[1] !== 0) {
cell[0] = cell[1];
currentCell = flushCell(map, context, lastCell, rowKind, index, currentCell);
} else if (cell[1] !== 0) {
currentCell = flushCell(map, context, cell, rowKind, index, currentCell);
}
rowKind = 0;
} else if (rowKind && (token.type === "data" || token.type === 'tableDelimiterMarker' || token.type === 'tableDelimiterFiller')) {
cell[3] = index;
}
}
if (lastTableEnd !== 0) {
flushTableEnd(map, context, lastTableEnd, currentTable, currentBody);
}
map.consume(context.events);
// To do: move this into `html`, when events are exposed there.
// Thats what `markdown-rs` does.
// That needs updates to `mdast-util-gfm-table`.
index = -1;
while (++index < context.events.length) {
const event = context.events[index];
if (event[0] === 'enter' && event[1].type === 'table') {
event[1]._align = gfmTableAlign(context.events, index);
}
}
return events;
}
/**
* Generate a cell.
*
* @param {EditMap} map
* @param {Readonly<TokenizeContext>} context
* @param {Readonly<Range>} range
* @param {RowKind} rowKind
* @param {number | undefined} rowEnd
* @param {Token | undefined} previousCell
* @returns {Token | undefined}
*/
// eslint-disable-next-line max-params
function flushCell(map, context, range, rowKind, rowEnd, previousCell) {
// `markdown-rs` uses:
// rowKind === 2 ? 'tableDelimiterCell' : 'tableCell'
const groupName = rowKind === 1 ? 'tableHeader' : rowKind === 2 ? 'tableDelimiter' : 'tableData';
// `markdown-rs` uses:
// rowKind === 2 ? 'tableDelimiterCellValue' : 'tableCellText'
const valueName = 'tableContent';
// Insert an exit for the previous cell, if there is one.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- exit
// ^^^^-- this cell
// ```
if (range[0] !== 0) {
previousCell.end = Object.assign({}, getPoint(context.events, range[0]));
map.add(range[0], 0, [['exit', previousCell, context]]);
}
// Insert enter of this cell.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- enter
// ^^^^-- this cell
// ```
const now = getPoint(context.events, range[1]);
previousCell = {
type: groupName,
start: Object.assign({}, now),
// Note: correct end is set later.
end: Object.assign({}, now)
};
map.add(range[1], 0, [['enter', previousCell, context]]);
// Insert text start at first data start and end at last data end, and
// remove events between.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- enter
// ^-- exit
// ^^^^-- this cell
// ```
if (range[2] !== 0) {
const relatedStart = getPoint(context.events, range[2]);
const relatedEnd = getPoint(context.events, range[3]);
/** @type {Token} */
const valueToken = {
type: valueName,
start: Object.assign({}, relatedStart),
end: Object.assign({}, relatedEnd)
};
map.add(range[2], 0, [['enter', valueToken, context]]);
if (rowKind !== 2) {
// Fix positional info on remaining events
const start = context.events[range[2]];
const end = context.events[range[3]];
start[1].end = Object.assign({}, end[1].end);
start[1].type = "chunkText";
start[1].contentType = "text";
// Remove if needed.
if (range[3] > range[2] + 1) {
const a = range[2] + 1;
const b = range[3] - range[2] - 1;
map.add(a, b, []);
}
}
map.add(range[3] + 1, 0, [['exit', valueToken, context]]);
}
// Insert an exit for the last cell, if at the row end.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- exit
// ^^^^^^-- this cell (the last one contains two “between” parts)
// ```
if (rowEnd !== undefined) {
previousCell.end = Object.assign({}, getPoint(context.events, rowEnd));
map.add(rowEnd, 0, [['exit', previousCell, context]]);
previousCell = undefined;
}
return previousCell;
}
/**
* Generate table end (and table body end).
*
* @param {Readonly<EditMap>} map
* @param {Readonly<TokenizeContext>} context
* @param {number} index
* @param {Token} table
* @param {Token | undefined} tableBody
*/
// eslint-disable-next-line max-params
function flushTableEnd(map, context, index, table, tableBody) {
/** @type {Array<Event>} */
const exits = [];
const related = getPoint(context.events, index);
if (tableBody) {
tableBody.end = Object.assign({}, related);
exits.push(['exit', tableBody, context]);
}
table.end = Object.assign({}, related);
exits.push(['exit', table, context]);
map.add(index + 1, 0, exits);
}
/**
* @param {Readonly<Array<Event>>} events
* @param {number} index
* @returns {Readonly<Point>}
*/
function getPoint(events, index) {
const event = events[index];
const side = event[0] === 'enter' ? 'start' : 'end';
return event[1][side];
}