diff --git a/src/lib/components/MarkdownRenderer.svelte b/src/lib/components/MarkdownRenderer.svelte
index b40b616..95ec592 100644
--- a/src/lib/components/MarkdownRenderer.svelte
+++ b/src/lib/components/MarkdownRenderer.svelte
@@ -7,6 +7,8 @@
import {
createUniqueHeadingId,
replaceGithubAdmonitions,
+ replaceHugoAdmonitions,
+ resolveAdmonitionTitle,
sanitizeMarkdownHtml
} from '$lib/markdown/rendering';
@@ -337,25 +339,15 @@
''
};
- let html = replaceGithubAdmonitions(markdown, (type, title, content) => {
+ function renderAdmonition(type: string, rawTitle: string, content: string) {
const typeLower = type.toLowerCase();
- const displayTitle = escapeHtml(
- title ? title : type.charAt(0).toUpperCase() + type.slice(1).toLowerCase()
- );
+ const displayTitle = escapeHtml(resolveAdmonitionTitle(type, rawTitle));
const icon = icons[typeLower as keyof typeof icons] || icons.note;
return `\n
\n
${icon}${displayTitle}
\n
\n
\n`;
- });
+ }
- const hugoRegex =
- /\{\{<\s*admonition\s+([a-zA-Z]+)(?:\s+"(.*?)")?\s*>\}\}\r?\n([\s\S]*?)\{\{<\s*\/admonition\s*>\}\}/gi;
- html = html.replace(hugoRegex, (match, type, title, content) => {
- const typeLower = type.toLowerCase();
- const displayTitle = escapeHtml(
- title ? title : type.charAt(0).toUpperCase() + type.slice(1).toLowerCase()
- );
- const icon = icons[typeLower as keyof typeof icons] || icons.note;
- return `\n\n
${icon}${displayTitle}
\n
\n
\n`;
- });
+ let html = replaceGithubAdmonitions(markdown, renderAdmonition);
+ html = replaceHugoAdmonitions(html, renderAdmonition);
return html;
}
diff --git a/src/lib/markdown/rendering.test.ts b/src/lib/markdown/rendering.test.ts
index b147fb2..6c8e703 100644
--- a/src/lib/markdown/rendering.test.ts
+++ b/src/lib/markdown/rendering.test.ts
@@ -3,6 +3,8 @@ import {
createHeadingId,
createUniqueHeadingId,
replaceGithubAdmonitions,
+ replaceHugoAdmonitions,
+ resolveAdmonitionTitle,
sanitizeMarkdownHtml
} from './rendering';
@@ -21,6 +23,52 @@ describe('replaceGithubAdmonitions', () => {
});
});
+describe('resolveAdmonitionTitle', () => {
+ it('strips matching quotes and trims bare titles', () => {
+ expect(resolveAdmonitionTitle('note', '"自定义标题"')).toBe('自定义标题');
+ expect(resolveAdmonitionTitle('note', "'自定义标题'")).toBe('自定义标题');
+ expect(resolveAdmonitionTitle('note', ' 裸标题 ')).toBe('裸标题');
+ });
+
+ it('falls back to the localized default title, then the capitalized type', () => {
+ expect(resolveAdmonitionTitle('note', '')).toBe('注意');
+ expect(resolveAdmonitionTitle('ABSTRACT', ' ')).toBe('摘要');
+ expect(resolveAdmonitionTitle('unknown', '')).toBe('Unknown');
+ });
+});
+
+describe('replaceHugoAdmonitions', () => {
+ const render = (type: string, title: string, content: string) =>
+ ``;
+
+ it('accepts quoted, unquoted and missing titles', () => {
+ const quoted = replaceHugoAdmonitions(
+ '{{< admonition abstract "摘要" >}}\nbody\n{{< /admonition >}}',
+ render
+ );
+ const unquoted = replaceHugoAdmonitions(
+ '{{< admonition abstract 摘要>}}\nbody\n{{< /admonition >}}',
+ render
+ );
+ const missing = replaceHugoAdmonitions(
+ '{{< admonition abstract >}}\nbody\n{{< /admonition >}}',
+ render
+ );
+
+ expect(quoted).toContain('');
+ expect(unquoted).toContain('');
+ expect(missing).toContain('');
+ });
+
+ it('supports single quotes and multi-word bare titles', () => {
+ const singleQuoted = replaceHugoAdmonitions(
+ "{{< admonition tip 'Best practice' >}}\nbody\n{{< /admonition >}}",
+ render
+ );
+ expect(singleQuoted).toContain('');
+ });
+});
+
describe('heading IDs', () => {
it('keeps Unicode letters and provides a deterministic fallback', () => {
expect(createHeadingId('中文 标题')).toBe('中文-标题');
diff --git a/src/lib/markdown/rendering.ts b/src/lib/markdown/rendering.ts
index d9f086d..ddd6d0a 100644
--- a/src/lib/markdown/rendering.ts
+++ b/src/lib/markdown/rendering.ts
@@ -2,6 +2,46 @@ import sanitizeHtml from 'sanitize-html';
type AdmonitionRenderer = (type: string, title: string, content: string) => string;
+/** 无标题时的默认标题,与编辑器菜单中的中文名称保持一致 */
+export const DEFAULT_ADMONITION_TITLES: Record = {
+ note: '注意',
+ abstract: '摘要',
+ info: '信息',
+ tip: '技巧',
+ success: '成功',
+ question: '问题',
+ warning: '警告',
+ failure: '失败',
+ danger: '危险',
+ bug: 'Bug',
+ example: '示例',
+ quote: '引用',
+ important: '重要',
+ caution: '谨慎'
+};
+
+/**
+ * 解析 admonition 标题:
+ * - 兼容 `"标题"`、`'标题'` 以及不带引号的裸标题(如 `abstract 摘要`)
+ * - 未提供标题时回退到该类型的默认标题,再回退到首字母大写的类型名
+ */
+export function resolveAdmonitionTitle(type: string, rawTitle: string): string {
+ const title = (rawTitle ?? '')
+ .trim()
+ .replace(/^(["'])([\s\S]*)\1$/, '$2')
+ .trim();
+
+ if (title) {
+ return title;
+ }
+
+ const typeLower = type.toLowerCase();
+ return (
+ DEFAULT_ADMONITION_TITLES[typeLower] ||
+ type.charAt(0).toUpperCase() + type.slice(1).toLowerCase()
+ );
+}
+
export function replaceGithubAdmonitions(markdown: string, render: AdmonitionRenderer): string {
const pattern =
/(?:^|\r?\n)> ?\[!(NOTE|ABSTRACT|INFO|TIP|SUCCESS|QUESTION|WARNING|FAILURE|DANGER|BUG|EXAMPLE|QUOTE|IMPORTANT|CAUTION)\][ \t]*(.*?)\r?\n((?:>.*(?:\r?\n|$))*)/gi;
@@ -11,6 +51,20 @@ export function replaceGithubAdmonitions(markdown: string, render: AdmonitionRen
);
}
+/**
+ * 替换 Hugo/Docsy 风格的 admonition shortcode:
+ * `{{< admonition [标题] >}} ... {{< /admonition >}}`
+ * 标题可选,且可以是 `"标题"`、`'标题'` 或裸文本。
+ */
+export function replaceHugoAdmonitions(html: string, render: AdmonitionRenderer): string {
+ const pattern =
+ /\{\{<\s*admonition\s+([a-zA-Z][\w-]*)([^>]*?)>\}\}\r?\n([\s\S]*?)\{\{<\s*\/admonition\s*>\}\}/gi;
+
+ return html.replace(pattern, (_match, type: string, rawTitle: string, content: string) =>
+ render(type, rawTitle.trim(), content)
+ );
+}
+
export function createHeadingId(rawText: string): string {
const slug = rawText
.replace(/<[^>]*>?/g, '')