2026-05-31 13:54:31 +02:00

726 lines
21 KiB
JavaScript

import { parse } from 'yaml';
import TokenClass from 'markdown-it/lib/token.mjs';
const NUMBER_CHAR_RE = /\d/;
const STR_SPLITTERS = ["-", "_", "/", "."];
function isUppercase(char = "") {
if (NUMBER_CHAR_RE.test(char)) {
return void 0;
}
return char !== char.toLowerCase();
}
function splitByCase(str, separators) {
const splitters = STR_SPLITTERS;
const parts = [];
if (!str || typeof str !== "string") {
return parts;
}
let buff = "";
let previousUpper;
let previousSplitter;
for (const char of str) {
const isSplitter = splitters.includes(char);
if (isSplitter === true) {
parts.push(buff);
buff = "";
previousUpper = void 0;
continue;
}
const isUpper = isUppercase(char);
if (previousSplitter === false) {
if (previousUpper === false && isUpper === true) {
parts.push(buff);
buff = char;
previousUpper = isUpper;
continue;
}
if (previousUpper === true && isUpper === false && buff.length > 1) {
const lastChar = buff.at(-1);
parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
buff = lastChar + char;
previousUpper = isUpper;
continue;
}
}
buff += char;
previousUpper = isUpper;
previousSplitter = isSplitter;
}
parts.push(buff);
return parts;
}
function kebabCase(str, joiner) {
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join("-") : "";
}
function parseBracketContent(str, startIndex) {
if (str[startIndex] !== "[")
return null;
let index = startIndex + 1;
while (index < str.length) {
if (str[index] === "\\" && index + 1 < str.length) {
index += 2;
continue;
}
if (str[index] === "]") {
const content = str.slice(startIndex + 1, index);
return { content, endIndex: index + 1 };
}
index += 1;
}
return null;
}
const bracketPairs = {
"[": "]",
"{": "}",
"(": ")"
};
const quotePairs = {
"'": "'",
'"': '"',
"`": "`"
};
function searchProps(content, index = 0) {
if (content[index] !== "{")
throw new Error(`Invalid props, expected \`{\` but got '${content[index]}'`);
const props = [];
if (content[index + 1] === "{")
return void 0;
index += 1;
while (index < content.length) {
if (content[index] === "\\") {
index += 2;
} else if (content[index] === "}") {
index += 1;
break;
} else if (content[index] === " ") {
index += 1;
} else if (content[index] === ".") {
index += 1;
props.push([
"class",
searchUntil(" #.}")
]);
} else if (content[index] === "#") {
index += 1;
props.push([
"id",
searchUntil(" #.}")
]);
} else {
const start = index;
while (index < content.length) {
index += 1;
if (" }=".includes(content[index]))
break;
}
const char = content[index];
if (start !== index) {
const key = content.slice(start, index).trim();
if (char === "=") {
index += 1;
props.push([
key,
searchValue()
]);
} else {
props.push([
key,
"true"
]);
}
}
}
}
function searchUntil(str) {
const start = index;
while (index < content.length) {
index += 1;
if (content[index] === "\\")
index += 2;
if (str.includes(content[index]))
break;
}
return content.slice(start, index);
}
function searchValue() {
const start = index;
if (content[index] in bracketPairs) {
searchBracket(bracketPairs[content[index]]);
index += 1;
return content.slice(start, index);
} else if (content[index] in quotePairs) {
searchString(quotePairs[content[index]]);
index += 1;
return content.slice(start, index);
} else {
return searchUntil(" }");
}
}
function searchBracket(end) {
while (index < content.length) {
index++;
if (content[index] in quotePairs)
searchString(quotePairs[content[index]]);
else if (content[index] in bracketPairs)
searchBracket(bracketPairs[content[index]]);
else if (content[index] === end)
return;
}
}
function searchString(end) {
return searchUntil(end);
}
props.forEach((v) => {
if (v[1].match(/^(['"`]).*\1$/))
v[1] = v[1].slice(1, -1);
});
return {
props,
index
};
}
const RE_BLOCK_NAME = /^[a-z$][$\w.-]*/i;
function parseBlockParams(str) {
str = str.trim();
if (!str)
return { name: "" };
const name = str.match(RE_BLOCK_NAME)?.[0];
if (!name)
throw new Error(`Invalid block params: ${str}`);
let remaining = str.slice(name.length).trim();
let content;
let props;
let unparsedRemaining;
if (remaining.startsWith("[")) {
const result2 = parseBracketContent(remaining, 0);
if (result2) {
content = result2.content;
remaining = remaining.slice(result2.endIndex).trim();
}
}
if (remaining.startsWith("{")) {
const propsResult = searchProps(remaining, 0);
if (propsResult) {
props = propsResult.props;
const afterProps = remaining.slice(propsResult.index).trim();
if (afterProps) {
unparsedRemaining = afterProps;
}
}
} else if (remaining) {
unparsedRemaining = remaining;
}
const result = {
name: kebabCase(name)
};
if (content !== void 0) {
result.content = content;
}
if (props !== void 0) {
result.props = props;
}
if (unparsedRemaining) {
result.remaining = unparsedRemaining;
}
return result;
}
const MarkdownItMdcBlock = (md) => {
const min_markers = 2;
const marker_str = ":";
const marker_char = marker_str.charCodeAt(0);
md.block.ruler.before(
"fence",
"mdc_block_shorthand",
// eslint-disable-next-line prefer-arrow-callback
function mdc_block_shorthand(state, startLine, endLine, silent) {
const line = state.src.slice(state.bMarks[startLine] + state.tShift[startLine], state.eMarks[startLine]);
if (!line.match(/^:\w/))
return false;
const parsed = parseBlockParams(line.slice(1));
const {
name,
content,
props,
remaining
} = parsed;
if (remaining) {
return false;
}
state.lineMax = startLine + 1;
if (!silent) {
if (content !== void 0) {
const tokenOpen = state.push("mdc_block_shorthand", name, 1);
props?.forEach(([key, value]) => {
if (key === "class")
tokenOpen.attrJoin(key, value);
else
tokenOpen.attrSet(key, value);
});
const inline = state.push("inline", "", 0);
inline.content = "";
const text = new state.Token("text", "", 0);
text.content = content;
inline.children = [text];
state.push("mdc_block_shorthand", name, -1);
} else {
const token = state.push("mdc_block_shorthand", name, 0);
props?.forEach(([key, value]) => {
if (key === "class")
token.attrJoin(key, value);
else
token.attrSet(key, value);
});
}
}
state.line = startLine + 1;
return true;
}
);
md.block.ruler.before(
"fence",
"mdc_block",
// eslint-disable-next-line prefer-arrow-callback
function mdc_block(state, startLine, endLine, silent) {
let pos;
let nextLine;
let auto_closed = false;
let start = state.bMarks[startLine] + state.tShift[startLine];
let max = state.eMarks[startLine];
const indent = state.sCount[startLine];
let inCodeFence = false;
let codeFenceCharCode = 0;
let codeFenceCount = 0;
let nestingDepth = 0;
if (state.src[start] !== ":")
return false;
for (pos = start + 1; pos <= max; pos++) {
if (marker_str !== state.src[pos])
break;
}
const marker_count = Math.floor(pos - start);
if (marker_count < min_markers)
return false;
const markup = state.src.slice(start, pos);
const params = parseBlockParams(state.src.slice(pos, max));
if (!params.name)
return false;
if (silent)
return true;
nextLine = startLine;
for (; ; ) {
nextLine++;
if (nextLine >= endLine) {
break;
}
start = state.bMarks[nextLine] + state.tShift[nextLine];
max = state.eMarks[nextLine];
if (start < max && state.sCount[nextLine] < state.blkIndent) {
break;
}
const lineCharCode = state.src.charCodeAt(start);
if (inCodeFence) {
if (lineCharCode === codeFenceCharCode) {
let fencePos = start + 1;
while (fencePos < max && state.src.charCodeAt(fencePos) === codeFenceCharCode)
fencePos++;
if (fencePos - start >= codeFenceCount) {
const afterFence = state.skipSpaces(fencePos);
if (afterFence >= max)
inCodeFence = false;
}
}
continue;
}
if (lineCharCode === 96 || lineCharCode === 126) {
let fencePos = start + 1;
while (fencePos < max && state.src.charCodeAt(fencePos) === lineCharCode)
fencePos++;
if (fencePos - start >= 3) {
inCodeFence = true;
codeFenceCharCode = lineCharCode;
codeFenceCount = fencePos - start;
continue;
}
}
if (marker_char !== lineCharCode)
continue;
for (pos = start + 1; pos <= max; pos++) {
if (marker_str !== state.src[pos])
break;
}
if (pos - start !== marker_count)
continue;
pos = state.skipSpaces(pos);
if (pos < max) {
nestingDepth++;
continue;
}
if (nestingDepth > 0) {
nestingDepth--;
continue;
}
auto_closed = true;
break;
}
const old_parent = state.parentType;
const old_line_max = state.lineMax;
state.parentType = "mdc_block";
state.lineMax = nextLine;
const tokenOpen = state.push("mdc_block_open", params.name, 1);
tokenOpen.markup = markup;
tokenOpen.block = true;
tokenOpen.info = params.name;
tokenOpen.map = [startLine, nextLine];
if (params.props) {
params.props?.forEach(([key, value]) => {
if (key === "class")
tokenOpen.attrJoin(key, value);
else
tokenOpen.attrSet(key, value);
});
}
const blkIndent = state.blkIndent;
state.blkIndent = indent;
state.env.mdcBlockTokens ||= [];
state.env.mdcBlockTokens.unshift(tokenOpen);
state.md.block.tokenize(state, startLine + 1, nextLine);
state.blkIndent = blkIndent;
state.env.mdcBlockTokens.shift(tokenOpen);
const tokenClose = state.push("mdc_block_close", params.name, -1);
tokenClose.map = [startLine, nextLine];
tokenClose.markup = state.src.slice(start, pos);
tokenClose.block = true;
state.tokens.slice(
state.tokens.indexOf(tokenOpen) + 1,
state.tokens.indexOf(tokenClose)
).filter((i) => i.level === tokenOpen.level + 1).forEach((i, _, arr) => {
if (arr.length <= 2 && i.tag === "p")
i.hidden = true;
});
state.parentType = old_parent;
state.lineMax = old_line_max;
state.line = nextLine + (auto_closed ? 1 : 0);
return true;
},
{
alt: ["paragraph", "reference", "blockquote", "list"]
}
);
md.block.ruler.after(
"code",
"mdc_block_yaml",
// eslint-disable-next-line prefer-arrow-callback
function mdc_block_yaml(state, startLine, endLine, silent) {
if (!state.env.mdcBlockTokens?.length)
return false;
const start = state.bMarks[startLine] + state.tShift[startLine];
const end = state.eMarks[startLine];
if (state.src.slice(start, end) !== "---")
return false;
let lineEnd = startLine + 1;
let found = false;
while (lineEnd < endLine) {
const line = state.src.slice(state.bMarks[lineEnd] + state.tShift[startLine], state.eMarks[lineEnd]);
if (line === "---") {
found = true;
break;
}
lineEnd += 1;
}
if (!found)
return false;
if (!silent) {
const yaml = state.src.slice(state.bMarks[startLine + 1], state.eMarks[lineEnd - 1]);
const data = parse(yaml);
const token = state.env.mdcBlockTokens[0];
Object.entries(data || {}).forEach(([key, value]) => {
if (key === "class")
token.attrJoin(key, value);
else
token.attrSet(key, typeof value === "string" ? value : JSON.stringify(value));
});
}
state.line = lineEnd + 1;
state.lineMax = lineEnd + 1;
return true;
}
);
md.block.ruler.after(
"code",
"mdc_block_slots",
// eslint-disable-next-line prefer-arrow-callback
function mdc_block(state, startLine, endLine, silent) {
if (!state.env.mdcBlockTokens?.length)
return false;
const start = state.bMarks[startLine] + state.tShift[startLine];
if (!(state.src[start] === "#" && state.src[start + 1] !== " "))
return false;
const line = state.src.slice(start, state.eMarks[startLine]);
const {
name,
props
} = parseBlockParams(line.slice(1));
let lineEnd = startLine + 1;
while (lineEnd < endLine) {
const line2 = state.src.slice(state.bMarks[lineEnd] + state.tShift[startLine], state.eMarks[lineEnd]);
if (line2.match(/^#(\w)+/) || line2.startsWith("::"))
break;
lineEnd += 1;
}
if (silent) {
state.line = lineEnd;
state.lineMax = lineEnd;
return true;
}
state.lineMax = startLine + 1;
const slot = state.push("mdc_block_slot", "template", 1);
slot.attrSet(`#${name}`, "");
props?.forEach(([key, value]) => {
if (key === "class")
slot.attrJoin(key, value);
else
slot.attrSet(key, value);
});
state.line = startLine + 1;
state.lineMax = lineEnd;
state.md.block.tokenize(state, startLine + 1, lineEnd);
state.push("mdc_block_slot", "template", -1);
state.line = lineEnd;
state.lineMax = lineEnd;
return true;
}
);
};
const ALLOWED_PREV_CHARS = /* @__PURE__ */ new Set([" ", " ", "\n", "*", "_", "["]);
const MarkdownItInlineComponent = (md) => {
md.inline.ruler.after("entity", "mdc_inline_component", (state, silent) => {
const start = state.pos;
const char = state.src[start];
if (char !== ":")
return false;
const prevChar = state.src[start - 1];
if (start > 0 && !ALLOWED_PREV_CHARS.has(prevChar))
return false;
let index = start + 1;
let nameEnd = -1;
let content;
while (index < state.src.length) {
const char2 = state.src[index];
if (char2 === "[") {
nameEnd = index;
const result = parseBracketContent(state.src, index);
if (result) {
content = result.content;
index = result.endIndex;
}
break;
}
if (!/[\w$\-]/.test(char2))
break;
index += 1;
}
if (nameEnd === -1)
nameEnd = index;
if (nameEnd <= start + 1)
return false;
state.pos = index;
if (silent)
return true;
const name = state.src.slice(start + 1, nameEnd);
if (content !== void 0) {
state.push("mdc_inline_component", name, 1);
const text = state.push("text", "", 0);
text.content = content;
state.push("mdc_inline_component", name, -1);
} else {
state.push("mdc_inline_component", name, 0);
}
return true;
});
};
const MarkdownItInlineProps = (md) => {
md.inline.ruler.after("entity", "mdc_inline_props", (state, silent) => {
const start = state.pos;
const char = state.src[start];
if (char !== "{")
return false;
if (state.src[start + 1] === "{" || state.src[start - 1] === "{" || state.src[start - 1] === "$")
return false;
const search = searchProps(state.src, start);
if (!search)
return false;
const {
props,
index: end
} = search;
if (end === start)
return false;
state.pos = end;
if (silent)
return true;
const token = state.push("mdc_inline_props", "span", 0);
token.attrs = props;
token.hidden = true;
return true;
});
md.renderer.rules.mdc_inline_props = () => {
return "";
};
const _parse = md.parse;
md.parse = function(src, env) {
const tokens = _parse.call(this, src, env);
tokens.forEach((token, index) => {
const prev = tokens[index - 1];
const next = tokens[index + 1];
if (!["heading_open", "paragraph_open", "list_item_open"].includes(prev?.type) || prev.hidden)
return;
if (token.hidden && next.type === "inline")
token = next;
if (token.type === "inline" && token.children?.length === 2 && token.children[0].type === "text" && token.children[1].type === "mdc_inline_props") {
const props = token.children[1].attrs;
token.children.splice(1, 1);
props?.forEach(([key, value]) => {
if (key === "class")
prev.attrJoin("class", value);
else
prev.attrSet(key, value);
});
}
});
tokens.forEach((tokenOpen, index) => {
if (tokenOpen.type !== "bullet_list_open")
return;
const prev = tokens[index - 1];
if (!prev || prev.type !== "mdc_block_open" || prev.tag !== "ul")
return;
let closeIndex = index + 1;
while (closeIndex < tokens.length) {
const close = tokens[closeIndex];
if (close.type === "bullet_list_close" && close.level === tokenOpen.level)
break;
closeIndex += 1;
}
const tokenClose = tokens[closeIndex];
if (tokenClose.type !== "bullet_list_close")
return;
const next = tokens[closeIndex + 1];
if (next.type === "mdc_block_close" && next.tag === "ul") {
tokenOpen.hidden = true;
tokenClose.hidden = true;
}
});
return tokens;
};
const _renderInline = md.renderer.renderInline;
md.renderer.renderInline = function(tokens, options, env) {
tokens = [...tokens];
tokens.forEach((token, index) => {
if (token.type === "mdc_inline_props") {
let prevIndex = index - 1;
let prev = tokens[prevIndex];
while (prevIndex >= 0) {
if (prev.type === "text" && !prev.content.trim()) {
prevIndex--;
prev = tokens[prevIndex];
} else {
break;
}
}
if (!prev.tag && prev.type === "text") {
prev = new TokenClass("mdc_inline_span", "span", 1);
tokens.splice(index - 1, 0, prev);
const close = new TokenClass("mdc_inline_span", "span", -1);
tokens.splice(index + 2, 0, close);
} else if (prev.nesting === -1) {
let searchIndex = index - 1;
while (searchIndex >= 0) {
const searchToken = tokens[searchIndex];
if (searchToken.nesting === 1 && searchToken.tag === prev.tag && searchToken.level === prev.level) {
prev = searchToken;
break;
}
searchIndex--;
}
}
if (prev.nesting === -1)
throw new Error(`No matching opening tag found for ${JSON.stringify(prev)}`);
token.attrs?.forEach(([key, value]) => {
if (key === "class")
prev.attrJoin("class", value);
else
prev.attrSet(key, value);
});
}
});
return _renderInline.call(this, tokens, options, env);
};
};
const MarkdownItInlineSpan = (md) => {
md.inline.ruler.before("link", "mdc_inline_span", (state, silent) => {
const start = state.pos;
const char = state.src[start];
if (char !== "[")
return false;
let index = start + 1;
let depth = 0;
while (index < state.src.length) {
if (state.src[index] === "\\") {
index += 2;
continue;
}
if (state.src[index] === "[") {
depth++;
} else if (state.src[index] === "]") {
if (depth === 0)
break;
depth--;
}
index += 1;
}
if (index === start)
return false;
const nextChar = state.src[index + 1];
if (nextChar === "(" || nextChar === "[")
return false;
if (silent)
return true;
state.push("mdc_inline_span", "span", 1);
const oldPos = state.pos;
const oldPosMax = state.posMax;
state.pos = start + 1;
state.posMax = index;
state.md.inline.tokenize(state);
state.pos = oldPos;
state.posMax = oldPosMax;
state.push("mdc_inline_span", "span", -1);
state.pos = index + 1;
return true;
});
};
const MarkdownItMdc = (md, options = {}) => {
const {
blockComponent = true,
inlineProps = true,
inlineSpan = true,
inlineComponent = true
} = options.syntax || {};
if (blockComponent)
md.use(MarkdownItMdcBlock);
if (inlineProps)
md.use(MarkdownItInlineProps);
if (inlineSpan)
md.use(MarkdownItInlineSpan);
if (inlineComponent)
md.use(MarkdownItInlineComponent);
};
export { MarkdownItMdc as default };