This commit is contained in:
2026-07-27 00:56:18 +03:30
commit 1c75afe0fd
244 changed files with 27710 additions and 0 deletions
@@ -0,0 +1,43 @@
{
"name": "@ohmycv/markdown-it-cross-ref",
"version": "0.1.1",
"description": "Enable cross-references in markdown-it.",
"keywords": [
"markdown",
"markdown-it-plugin",
"markdown-it"
],
"homepage": "https://github.com/Renovamen/oh-my-cv/tree/main/packages/markdown-it-cross-ref",
"bugs": {
"url": "https://github.com/Renovamen/oh-my-cv"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Renovamen/oh-my-cv.git",
"directory": "packages/markdown-it-cross-ref"
},
"license": "MIT",
"author": "Renovamen <renovamenzxh@gmail.com>",
"files": [
"dist"
],
"exports": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"scripts": {
"build-fast:pkg": "tsup src/index.ts --format cjs,esm",
"build:pkg": "pnpm run build-fast:pkg --dts"
},
"devDependencies": {
"@types/markdown-it": "^14.1.1",
"markdown-it": "^14.1.0"
},
"publishConfig": {
"access": "public"
}
}
@@ -0,0 +1,211 @@
// Based on https://github.com/markdown-it/markdown-it-footnote
import type {
Token,
PluginSimple,
ParserBlock,
ParserInline,
Core,
Renderer,
StateBlock,
StateInline
} from "markdown-it";
const _anchorId = (tokens: Token[], idx: number) =>
Number(tokens[idx].meta.id + 1).toString();
const _anchorLabel = (tokens: Token[], idx: number) => tokens[idx].meta.label;
const _isOpen = (state: StateBlock | StateInline, start: number) => {
return (
state.src.charCodeAt(start) === 0x5b /* [ */ &&
state.src.charCodeAt(start + 1) === 0x7e /* ~ */
);
};
const _isClose = (state: StateBlock | StateInline, pos: number) => {
return state.src.charCodeAt(pos) === 0x5d /* ] */;
};
const _extractLabel = (state: StateBlock | StateInline, start: number, end: number) => {
return state.src.slice(start, end);
};
const render =
(type: "defOpen" | "ref"): Renderer.RenderRule =>
(tokens, idx) => {
const id = `cross-ref-${_anchorId(tokens, idx)}`;
const label = _anchorLabel(tokens, idx);
return type === "ref"
? `<sup data-scope="cross-ref" data-part="reference"><a data-scope="cross-ref" data-part="link" href="#${id}" id="${id}">${label}</a></sup>`
: `<ul data-scope="cross-ref" data-part="definitions"><li id="${id}" data-scope="cross-ref" data-part="definition" data-label="${label}">`;
};
const _processDefToken = (
state: StateBlock,
startLine: number,
endLine: number,
pos: number,
label: string
) => {
state.env.crossRef ??= {};
state.env.crossRef.labelToId ??= {};
state.env.crossRef.labelToId[label] = -1;
// Create and push the opening token
const openToken = new state.Token("renderDefOpen", "", 1);
openToken.meta = { label };
openToken.level = state.level++;
state.tokens.push(openToken);
// Save the current state
const bMarks = state.bMarks[startLine];
const tShift = state.tShift[startLine];
const sCount = state.sCount[startLine];
// Calculate the initial offset
const posAfterColon = pos;
const initial = sCount + pos - (bMarks + tShift);
let offset = initial;
// Adjust position and offset for spaces
while (pos < state.eMarks[startLine]) {
const ch = state.src.charCodeAt(pos);
if (state.md.utils.isSpace(ch)) {
offset += ch === 0x09 ? 4 - (offset % 4) : 1;
} else {
break;
}
pos++;
}
// Update state shifts and block indent
state.tShift[startLine] = pos - posAfterColon;
state.sCount[startLine] = offset - initial;
state.bMarks[startLine] = posAfterColon;
state.blkIndent += 4;
if (state.sCount[startLine] < state.blkIndent)
state.sCount[startLine] += state.blkIndent;
// Tokenize the block
state.md.block.tokenize(state, startLine, endLine);
// Restore the previous state
state.blkIndent -= 4;
state.tShift[startLine] = tShift;
state.sCount[startLine] = sCount;
state.bMarks[startLine] = bMarks;
// Create and push the closing token
const closeToken = new state.Token("renderDefClose", "", -1);
closeToken.level = --state.level;
state.tokens.push(closeToken);
};
const processDef: ParserBlock.RuleBlock = (state, startLine, endLine, silent) => {
const start = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
// Should be at least 5 chars ("[~x]:") and start with "[~"
if (start + 4 > max || !_isOpen(state, start)) return false;
// Find closing square bracket "]"
let pos = start + 2;
while (pos < max && !_isClose(state, pos)) pos++;
// Empty label is not allowed
if (pos === start + 2) return false;
// No "]" found or no ":" after "]"
if (pos + 1 >= max || state.src.charCodeAt(pos + 1) !== 0x3a /* : */) return false;
if (silent) return true;
// Extract content between "[~" and "]"
const label = _extractLabel(state, start + 2, pos);
// Process the blocks belonging to the definition
_processDefToken(state, startLine, endLine, pos + 2, label);
return true;
};
const processRef: ParserInline.RuleInline = (state, silent) => {
if (!state.env.crossRef?.labelToId) return false;
// Should be at least 4 chars ("[~x]") and start with "[~"
if (state.pos + 3 > state.posMax || !_isOpen(state, state.pos)) return false;
// Find closing square bracket "]"
let pos = state.pos + 2;
while (pos < state.posMax && !_isClose(state, pos)) pos++;
// Empty label or no "]" found
if (pos === state.pos + 2 || pos >= state.posMax) return false;
// Extract content between "[~" and "]"
const label = _extractLabel(state, state.pos + 2, pos);
// No corresponding definition found
if (state.env.crossRef.labelToId[label] === undefined) return false;
if (!silent) {
state.env.crossRef.list ??= [];
const id =
state.env.crossRef.labelToId[label] === -1
? state.env.crossRef.list.push(label) - 1
: state.env.crossRef.labelToId[label];
state.env.crossRef.labelToId[label] = id;
const token = state.push("renderRef", "", 0);
token.meta = { id, label };
}
state.pos = pos + 1;
return true;
};
const postProcessDef: Core.RuleCore = (state) => {
// We don't know the ID of the definition when we first encounter it,
// so we need to go back and update it after we've processed all the references
for (const token of state.tokens) {
if (token.type === "renderDefOpen") {
token.meta = {
...token.meta,
id: state.env.crossRef?.labelToId?.[token.meta.label] ?? -1
};
}
}
};
/**
* A markdown-it plugin for cross-references.
*
* @example
*
* ```markdown
* [~Reference name]: This is a reference.
*
* It can span multiple lines.
*
* This is a reference to [~Reference name].
* ```
*/
export const MarkdownItCrossRef: PluginSimple = (md) => {
md.renderer.rules.renderRef = render("ref");
md.renderer.rules.renderDefOpen = render("defOpen");
md.renderer.rules.renderDefClose = () => "</li>\n</ul>\n";
md.block.ruler.before("reference", "processDef", processDef, {
alt: ["paragraph", "reference"]
});
md.inline.ruler.after("image", "processRef", processRef);
md.core.ruler.after("inline", "postProcessDef", postProcessDef);
};
export default MarkdownItCrossRef;
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"include": ["./src"]
}