= {
flex: "flex_layout",
col: "flex_column",
head: "head_layout",
main: "main_layout",
generic: "container",
}
/** 从行文本取整行内容(跳过缩进),供标记分类使用 */
function lineAt(state: any, i: number): string {
return state.src.slice(state.bMarks[i] + state.tShift[i], state.eMarks[i])
}
/** 取 [from, to) 区间原始文本 */
function rawRange(state: any, from: number, to: number): string {
if (to <= from) return ""
return state.src.slice(state.bMarks[from], state.eMarks[to - 1]) + "\n"
}
interface Frame {
kind: ContainerKind
style: "legacy" | "named"
colons: number
}
/**
* 扫描容器闭合位置,并收集顶层列分隔符。
* 嵌套规则:任意子容器开启时压栈,与之匹配的关闭出栈(允许隐式跨层闭合,
* 即 :::: 可以同时关闭未闭合的内层 ::: col);裸 ::: 只有在栈空且当前
* 容器为 flex 时才是列分隔符。
*/
function scanRange(state: any, startLine: number, endLine: number, open: Marker) {
const stack: Frame[] = []
const separators: number[] = []
// 从栈顶向下找匹配的 frame,弹出它及其之上的所有 frame(隐式闭合)
const popThrough = (match: (f: Frame) => boolean) => {
for (let d = stack.length - 1; d >= 0; d--) {
if (match(stack[d])) {
stack.length = d
return true
}
}
return false
}
for (let i = startLine + 1; i < endLine; i++) {
const info = parseMarker(lineAt(state, i))
if (!info) continue
const c = classify(info)
if (c.t === "open") {
stack.push({ kind: c.kind, style: c.style, colons: c.colons })
} else if (c.t === "close") {
if (popThrough(f => f.kind === c.kind)) continue
if (
!stack.length &&
open.t === "open" &&
open.kind === c.kind &&
open.style === "legacy"
) {
return { closeLine: i, separators }
}
} else {
// 裸 ::: 标记
const top = stack[stack.length - 1]
if (top) {
// 匹配栈内任一 named frame 的同数量井号(隐式闭合其上的层)
popThrough(f => f.style === "named" && f.colons === c.colons)
} else if (open.t === "open" && open.kind === "flex") {
if (open.style === "legacy" && c.colons === 3) separators.push(i)
else if (open.style === "named") {
if (c.colons === open.colons) return { closeLine: i, separators }
if (c.colons < open.colons) separators.push(i)
}
} else if (
open.t === "open" &&
open.style === "named" &&
c.colons === open.colons
) {
// 非.flex 的具名容器(warning/col/head/main 等):同数量裸井号即闭合
return { closeLine: i, separators }
}
}
}
// 未闭合:与 v1 行为一致,消费到文档结尾
return { closeLine: endLine, separators }
}
function containerRule(state: any, startLine: number, endLine: number, silent: boolean): boolean {
const startInfo = parseMarker(lineAt(state, startLine))
if (!startInfo) return false
const open = classify(startInfo)
if (open.t !== "open") return false
if (silent) return true
const { closeLine, separators } = scanRange(state, startLine, endLine, open)
const prefix = TOKEN_PREFIX[open.kind]
const openToken = state.push(`${prefix}_open`, "div", 1)
openToken.block = true
openToken.map = [startLine, closeLine]
if (open.style === "named") openToken.meta = { name: open.name }
if (open.kind === "flex" && !(open.style === "named" && separators.length === 0)) {
// 按 separators 切分为多个列,每列内容独立走一遍 block 解析。
// 新语法下若内容里已用 ::: col 显式分列且没有裸分隔符,则不再包列。
let segStart = startLine + 1
for (const sep of [...separators, closeLine]) {
const col = state.push("flex_column_open", "div", 1)
col.block = true
col.map = [segStart, sep]
state.md.block.parse(rawRange(state, segStart, sep), state.md, state.env, state.tokens)
const colClose = state.push("flex_column_close", "div", -1)
colClose.block = true
segStart = sep + 1
}
} else {
state.md.block.parse(rawRange(state, startLine + 1, closeLine), state.md, state.env, state.tokens)
}
const closeToken = state.push(`${prefix}_close`, "div", -1)
closeToken.block = true
state.line = closeLine + 1
return true
}
const escapeAttr = (s: string) => s.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">")
export function containersPlugin(md: any) {
md.block.ruler.before("fence", "resume_container", containerRule, {
alt: ["paragraph", "reference", "blockquote", "list"],
})
md.renderer.rules.flex_layout_open = () => '\n'
md.renderer.rules.flex_layout_close = () => "
\n"
md.renderer.rules.flex_column_open = () => ''
md.renderer.rules.flex_column_close = () => "
\n"
md.renderer.rules.head_layout_open = () => ''
md.renderer.rules.head_layout_close = () => "
\n"
md.renderer.rules.main_layout_open = () => ''
md.renderer.rules.main_layout_close = () => "
\n"
// 未知名称的通用容器:,为后续扩展(callout 等)留口
md.renderer.rules.container_open = (tokens: any[], idx: number) => {
const name = tokens[idx].meta?.name || ""
const cls = /^[\w-]+$/.test(name) ? ` class="${escapeAttr(name)}"` : ""
return `
\n`
}
md.renderer.rules.container_close = () => "
\n"
}