feat: add custom YAML tag code block extension
This commit is contained in:
parent
a19d848456
commit
1582191655
|
|
@ -0,0 +1,62 @@
|
||||||
|
import type { MarkedExtension } from "marked";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
export default function markedCodeBlockYamlTag(): MarkedExtension {
|
||||||
|
return {
|
||||||
|
extensions: [
|
||||||
|
{
|
||||||
|
name: "code-block-yaml-tag",
|
||||||
|
level: "block",
|
||||||
|
start(src: string) {
|
||||||
|
return src.match(/^```yaml\/tag\s*\n/m)?.index;
|
||||||
|
},
|
||||||
|
tokenizer(src: string) {
|
||||||
|
const rule = /^```yaml\/tag\s*\n([\s\S]*?)\n```/;
|
||||||
|
const match = rule.exec(src);
|
||||||
|
if (match) {
|
||||||
|
const yamlContent = match[1]?.trim() || "";
|
||||||
|
let props: Record<string, unknown> = {};
|
||||||
|
try {
|
||||||
|
props =
|
||||||
|
(yaml.load(yamlContent) as Record<string, unknown>) || {};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("YAML Parse Error in code-block-yaml-tag:", e);
|
||||||
|
props = { error: "Invalid YAML content" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagName = (props.tag as string) || "tag-unknown";
|
||||||
|
const { tag, ...rest } = props;
|
||||||
|
|
||||||
|
let content = "";
|
||||||
|
if ("body" in rest) {
|
||||||
|
content = String(rest.body || "");
|
||||||
|
delete (rest as Record<string, unknown>).body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsStr = Object.entries(rest)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const strValue = String(value);
|
||||||
|
if (strValue.includes(" ") || strValue.includes('"')) {
|
||||||
|
return `${key}="${strValue.replace(/"/g, """)}"`;
|
||||||
|
}
|
||||||
|
return `${key}="${strValue}"`;
|
||||||
|
})
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "code-block-yaml-tag",
|
||||||
|
raw: match[0],
|
||||||
|
tagName,
|
||||||
|
props: propsStr,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token: any) {
|
||||||
|
const propsAttr = token.props ? ` ${token.props}` : "";
|
||||||
|
return `<${token.tagName}${propsAttr}>${token.content || ""}</${token.tagName}>\n`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,148 +1,103 @@
|
||||||
import { Marked, type MarkedExtension } from 'marked';
|
import { Marked, type MarkedExtension } from "marked";
|
||||||
import {createDirectives, presetDirectiveConfigs} from 'marked-directive';
|
import { createDirectives, presetDirectiveConfigs } from "marked-directive";
|
||||||
import yaml from 'js-yaml';
|
|
||||||
import markedAlert from "marked-alert";
|
import markedAlert from "marked-alert";
|
||||||
import markedMermaid from "./mermaid";
|
import markedMermaid from "./mermaid";
|
||||||
import markedTable from "./table";
|
import markedTable from "./table";
|
||||||
import {gfmHeadingId} from "marked-gfm-heading-id";
|
import { gfmHeadingId } from "marked-gfm-heading-id";
|
||||||
import markedColumns from "./columns";
|
import markedColumns from "./columns";
|
||||||
|
import markedCodeBlockYamlTag from "./code-block-yaml-tag";
|
||||||
|
|
||||||
let globalIconPrefix: string | undefined = undefined;
|
let globalIconPrefix: string | undefined = undefined;
|
||||||
function overrideIconPrefix(path?: string){
|
function overrideIconPrefix(path?: string) {
|
||||||
globalIconPrefix = path;
|
globalIconPrefix = path;
|
||||||
return {
|
return {
|
||||||
[Symbol.dispose](){
|
[Symbol.dispose]() {
|
||||||
globalIconPrefix = undefined;
|
globalIconPrefix = undefined;
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 marked-directive 来支持指令语法
|
// 使用 marked-directive 来支持指令语法
|
||||||
const marked = new Marked()
|
const marked = new Marked()
|
||||||
.use(gfmHeadingId())
|
.use(gfmHeadingId())
|
||||||
.use(markedAlert())
|
.use(markedAlert())
|
||||||
.use(markedMermaid())
|
.use(markedMermaid())
|
||||||
.use(markedTable())
|
.use(markedTable())
|
||||||
.use(createDirectives([
|
.use(markedCodeBlockYamlTag())
|
||||||
...presetDirectiveConfigs,
|
.use(
|
||||||
{
|
createDirectives([
|
||||||
marker: '::::',
|
...presetDirectiveConfigs,
|
||||||
level: 'container'
|
{
|
||||||
},
|
marker: "::::",
|
||||||
{
|
level: "container",
|
||||||
marker: ':::::',
|
},
|
||||||
level: 'container'
|
{
|
||||||
},
|
marker: ":::::",
|
||||||
{
|
level: "container",
|
||||||
level: 'inline',
|
},
|
||||||
marker: ':',
|
{
|
||||||
|
level: "inline",
|
||||||
|
marker: ":",
|
||||||
// :[icon] 或 :[icon.ext] 语法
|
// :[icon] 或 :[icon.ext] 语法
|
||||||
// 支持的扩展名: .svg, .png, .gif, .jpg, .jpeg, .webp
|
// 支持的扩展名: .svg, .png, .gif, .jpg, .jpeg, .webp
|
||||||
// 如果不指定扩展名,默认为 .png
|
// 如果不指定扩展名,默认为 .png
|
||||||
renderer(token) {
|
renderer(token) {
|
||||||
if (!token.meta.name) {
|
if (!token.meta.name) {
|
||||||
const iconText = token.text || '';
|
const iconText = token.text || "";
|
||||||
|
|
||||||
// 已知支持的图片扩展名
|
// 已知支持的图片扩展名
|
||||||
const supportedExtensions = ['svg', 'png', 'gif', 'jpg', 'jpeg', 'webp'];
|
const supportedExtensions = [
|
||||||
|
"svg",
|
||||||
// 检查是否包含扩展名(查找最后一个点)
|
"png",
|
||||||
let iconName = iconText;
|
"gif",
|
||||||
let extension = 'png'; // 默认扩展名
|
"jpg",
|
||||||
|
"jpeg",
|
||||||
const lastDotIndex = iconName.lastIndexOf('.');
|
"webp",
|
||||||
if (lastDotIndex > 0) {
|
];
|
||||||
const potentialExt = iconName.slice(lastDotIndex + 1).toLowerCase();
|
|
||||||
if (supportedExtensions.includes(potentialExt)) {
|
// 检查是否包含扩展名(查找最后一个点)
|
||||||
extension = potentialExt;
|
let iconName = iconText;
|
||||||
iconName = iconName.slice(0, lastDotIndex);
|
let extension = "png"; // 默认扩展名
|
||||||
}
|
|
||||||
}
|
const lastDotIndex = iconName.lastIndexOf(".");
|
||||||
|
if (lastDotIndex > 0) {
|
||||||
const label = token.attrs?.label as string | undefined;
|
const potentialExt = iconName
|
||||||
const inner = label ? `<span class="icon-label-stroke">${label}</span><span class="icon-label">${label}</span>` : "";
|
.slice(lastDotIndex + 1)
|
||||||
const style = globalIconPrefix ? `style="--icon-src: url('${globalIconPrefix}/${iconName}.${extension}')"` : '';
|
.toLowerCase();
|
||||||
const iconHtml = `<icon ${style} class="icon-${iconName} ${token.attrs?.class || ""}">${inner}</icon>`;
|
if (supportedExtensions.includes(potentialExt)) {
|
||||||
|
extension = potentialExt;
|
||||||
const repeat = parseInt(`${token.attrs?.repeat || ''}`);
|
iconName = iconName.slice(0, lastDotIndex);
|
||||||
const join = token.attrs?.join || '';
|
}
|
||||||
const separator = join ? `<${join}></${join}>` : '';
|
|
||||||
if(isNaN(repeat) || repeat < 1) return iconHtml;
|
|
||||||
|
|
||||||
return Array(repeat).fill(iconHtml).join(separator);
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
const label = token.attrs?.label as string | undefined;
|
||||||
},
|
const inner = label
|
||||||
]), {
|
? `<span class="icon-label-stroke">${label}</span><span class="icon-label">${label}</span>`
|
||||||
extensions: [
|
: "";
|
||||||
...markedColumns(),
|
const style = globalIconPrefix
|
||||||
|
? `style="--icon-src: url('${globalIconPrefix}/${iconName}.${extension}')"`
|
||||||
|
: "";
|
||||||
|
const iconHtml = `<icon ${style} class="icon-${iconName} ${token.attrs?.class || ""}">${inner}</icon>`;
|
||||||
|
|
||||||
|
const repeat = parseInt(`${token.attrs?.repeat || ""}`);
|
||||||
|
const join = token.attrs?.join || "";
|
||||||
|
const separator = join ? `<${join}></${join}>` : "";
|
||||||
|
if (isNaN(repeat) || repeat < 1) return iconHtml;
|
||||||
|
|
||||||
|
return Array(repeat).fill(iconHtml).join(separator);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
{
|
{
|
||||||
// 自定义代码块渲染器,支持 yaml/tag 格式
|
extensions: [...markedColumns()],
|
||||||
name: 'code-block-yaml-tag',
|
},
|
||||||
level: 'block',
|
);
|
||||||
start(src: string) {
|
|
||||||
// 检测 ```yaml/tag 开头的代码块
|
|
||||||
return src.match(/^```yaml\/tag\s*\n/m)?.index;
|
|
||||||
},
|
|
||||||
tokenizer(src: string) {
|
|
||||||
const rule = /^```yaml\/tag\s*\n([\s\S]*?)\n```/;
|
|
||||||
const match = rule.exec(src);
|
|
||||||
if (match) {
|
|
||||||
const yamlContent = match[1]?.trim() || '';
|
|
||||||
let props: Record<string, unknown> = {};
|
|
||||||
try {
|
|
||||||
props = (yaml.load(yamlContent) as Record<string, unknown>) || {};
|
|
||||||
} catch (e) {
|
|
||||||
console.error("YAML Parse Error in code-block-yaml-tag:", e);
|
|
||||||
props = { error: "Invalid YAML content" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取 tag 名称,默认为 tag-unknown
|
|
||||||
const tagName = (props.tag as string) || 'tag-unknown';
|
|
||||||
|
|
||||||
// 移除 tag 属性,剩下的作为 HTML 属性
|
|
||||||
const { tag, ...rest } = props;
|
|
||||||
|
|
||||||
// 提取 innerText 内容(如果有 body 字段)
|
|
||||||
let content = '';
|
|
||||||
if ('body' in rest) {
|
|
||||||
content = String(rest.body || '');
|
|
||||||
delete (rest as Record<string, unknown>).body;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建属性字符串
|
|
||||||
const propsStr = Object.entries(rest)
|
|
||||||
.map(([key, value]) => {
|
|
||||||
const strValue = String(value);
|
|
||||||
// 如果值包含空格或特殊字符,添加引号
|
|
||||||
if (strValue.includes(' ') || strValue.includes('"')) {
|
|
||||||
return `${key}="${strValue.replace(/"/g, '"')}"`;
|
|
||||||
}
|
|
||||||
return `${key}="${strValue}"`;
|
|
||||||
})
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'code-block-yaml-tag',
|
|
||||||
raw: match[0],
|
|
||||||
tagName,
|
|
||||||
props: propsStr,
|
|
||||||
content
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderer(token: any) {
|
|
||||||
// 渲染为自定义 HTML 标签
|
|
||||||
const propsAttr = token.props ? ` ${token.props}` : '';
|
|
||||||
return `<${token.tagName}${propsAttr}>${token.content || ''}</${token.tagName}>\n`;
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
export function parseMarkdown(content: string, iconPrefix?: string): string {
|
export function parseMarkdown(content: string, iconPrefix?: string): string {
|
||||||
using prefix = overrideIconPrefix(iconPrefix);
|
using prefix = overrideIconPrefix(iconPrefix);
|
||||||
return marked.parse(content.trimStart()) as string;
|
return marked.parse(content.trimStart()) as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { marked };
|
export { marked };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue