Added markdown rendering

This commit is contained in:
Magnus Åhall 2026-05-15 08:22:43 +02:00
parent 26ca510785
commit 5a0340c226
172 changed files with 12198 additions and 8338 deletions

View file

@ -0,0 +1,192 @@
/* node:coverage ignore next */
import type { MarkedExtension, Token, Tokens } from 'marked';
export interface TokenWithPosition extends Tokens.Generic {
position: Position;
}
interface Position {
/**
* Positions for each line of the token. LinePositions will not include the newline character for the line.
*/
lines: LinePosition[]
/**
* Position at the beginning of token
*/
start: PositionFields;
/**
* Position at the end of token
*/
end: PositionFields;
}
interface LinePosition {
/**
* Position at the beginning of line
*/
start: PositionFields;
/**
* Position at the end of line. Will not include the newline character.
*/
end: PositionFields;
}
interface PositionFields {
/**
* Number of characters from the beginning of the markdown string
*/
offset: number;
/**
* Line number of the token. Starts at line 0.
*/
line: number;
/**
* Column number of the token. Starts at column 0.
*/
column: number;
}
/**
* Add position field to tokens
*/
export function addTokenPositions(tokens: Token[]) {
const markdown = tokens.map(token => token.raw).join('');
return addPosition(tokens, 0, 0, 0, markdown).tokens;
}
/**
* Marked extension to add position field to tokens
*/
export default function(options = {}): MarkedExtension {
return {
hooks: {
processAllTokens(tokens) {
return addTokenPositions(tokens);
},
},
};
}
function addPosition(tokens: Token[], offset: number, line: number, column: number, markdown: string) {
for (const token of tokens) {
const genericToken = token as Tokens.Generic;
const position = getPosition(offset, line, column, markdown, genericToken.raw);
genericToken.position = position;
if (genericToken.tokens) {
addPosition(genericToken.tokens, offset, line, column, markdown);
}
if (genericToken.childTokens) {
let nextOffset = offset;
let nextLine = line;
let nextColumn = column;
let nextMarkdown = markdown;
for (const childToken of genericToken.childTokens) {
const nextPosition = addPosition(genericToken[childToken], nextOffset, nextLine, nextColumn, nextMarkdown);
nextOffset = nextPosition.offset;
nextLine = nextPosition.line;
nextColumn = nextPosition.column;
nextMarkdown = nextPosition.markdown;
}
}
if (genericToken.type === 'list') {
addPosition(genericToken.items, offset, line, column, markdown);
}
if (genericToken.type === 'table') {
let nextOffset = offset;
let nextLine = line;
let nextColumn = column;
let nextMarkdown = markdown;
for (const headerCell of genericToken.header) {
const nextPosition = addPosition(headerCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);
nextOffset = nextPosition.offset;
nextLine = nextPosition.line;
nextColumn = nextPosition.column;
nextMarkdown = nextPosition.markdown;
}
for (const row of genericToken.rows) {
for (const rowCell of row) {
const nextPosition = addPosition(rowCell.tokens, nextOffset, nextLine, nextColumn, nextMarkdown);
nextOffset = nextPosition.offset;
nextLine = nextPosition.line;
nextColumn = nextPosition.column;
nextMarkdown = nextPosition.markdown;
}
}
}
const deltaOffset = position.end.offset - offset;
offset = position.end.offset;
line = position.end.line;
column = position.end.column;
markdown = markdown.slice(deltaOffset);
}
return {
tokens: tokens as TokenWithPosition[],
offset,
line,
column,
markdown,
};
}
function getPosition(offset: number, line: number, column: number, markdown: string, raw: string): Position {
let lines: LinePosition[] = [];
const rawLines = raw.split('\n');
const markdownLines = markdown.split('\n');
// eslint-disable-next-line no-labels
md: for (let i = 0; i <= markdownLines.length - rawLines.length; i++) {
lines = [];
for (let j = 0; j < rawLines.length; j++) {
const markdownLine = markdownLines[i + j];
const rawLine = rawLines[j];
const lineStartOffset = markdownLine.indexOf(rawLine);
if (lineStartOffset === -1) {
// eslint-disable-next-line no-labels
continue md;
}
const beforeMarkdownLines = markdownLines.slice(0, i + j).join('\n') + (i + j > 0 ? '\n' : '');
const start = {
offset: offset + beforeMarkdownLines.length + lineStartOffset,
line: line + i + j,
column: (i + j === 0 ? column : 0) + lineStartOffset,
};
const end = {
offset: start.offset + rawLine.length,
line: start.line,
column: start.column + rawLine.length,
};
lines.push({
start,
end,
});
}
break;
}
/* node:coverage ignore next 4 */
if (lines.length === 0) {
// This shouldn't ever happen but if it does it would be nice to have a good error message
throw new Error(`Cannot find ${JSON.stringify(raw)} in ${JSON.stringify(markdown)}`);
}
const start = lines[0].start;
const end = lines.at(-1)!.end;
if (lines.length > 1 && lines.at(-1)!.start.offset === end.offset) {
lines = lines.slice(0, -1);
}
return {
lines,
start,
end,
};
}