@jet-w/astro-blog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-FXPGR372.js +0 -0
- package/dist/chunk-GYLSY3OJ.js +173 -0
- package/dist/config/index.d.ts +166 -0
- package/dist/config/index.js +38 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +59 -0
- package/dist/types/index.d.ts +75 -0
- package/dist/types/index.js +1 -0
- package/package.json +84 -0
- package/src/components/EChartsCard.vue +118 -0
- package/src/components/Mermaid.vue +73 -0
- package/src/components/about/ContentCard.astro +27 -0
- package/src/components/about/IconCard.astro +77 -0
- package/src/components/about/SocialLinks.astro +54 -0
- package/src/components/about/TagCard.astro +65 -0
- package/src/components/about/TagGroup.astro +33 -0
- package/src/components/about/TimelineCard.astro +52 -0
- package/src/components/blog/FloatingToc.vue +198 -0
- package/src/components/blog/Hero.astro +147 -0
- package/src/components/blog/NavigationTabs.vue +245 -0
- package/src/components/blog/PostCard.astro +161 -0
- package/src/components/blog/PostNavigation.astro +106 -0
- package/src/components/blog/RelatedPosts.astro +175 -0
- package/src/components/blog/TableOfContents.astro +153 -0
- package/src/components/blog/TagCloud.astro +91 -0
- package/src/components/home/FeaturedPostsSection.astro +54 -0
- package/src/components/home/QuickNavSection.astro +81 -0
- package/src/components/home/RecentPostsSection.astro +52 -0
- package/src/components/home/StatsSection.astro +44 -0
- package/src/components/layout/Footer.astro +103 -0
- package/src/components/layout/Header.astro +68 -0
- package/src/components/layout/Sidebar.astro +594 -0
- package/src/components/media/Bilibili.astro +114 -0
- package/src/components/media/Slides.astro +313 -0
- package/src/components/media/Video.astro +111 -0
- package/src/components/media/VideoPlayer.astro +89 -0
- package/src/components/media/YouTube.astro +92 -0
- package/src/components/pte/StudyCalendar.vue +1348 -0
- package/src/components/ui/Icon.astro +187 -0
- package/src/components/ui/MobileMenu.vue +201 -0
- package/src/components/ui/Pagination.astro +143 -0
- package/src/components/ui/SearchBox.vue +179 -0
- package/src/components/ui/SearchInterface.vue +409 -0
- package/src/components/ui/SidebarToggle.vue +57 -0
- package/src/components/ui/ThemeToggle.vue +90 -0
- package/src/layouts/AboutLayout.astro +18 -0
- package/src/layouts/BaseLayout.astro +362 -0
- package/src/layouts/PageLayout.astro +217 -0
- package/src/layouts/SlidesLayout.astro +320 -0
- package/src/plugins/rehype-clean-containers.mjs +24 -0
- package/src/plugins/rehype-relative-links.mjs +43 -0
- package/src/plugins/rehype-tabs.mjs +116 -0
- package/src/plugins/remark-containers.mjs +407 -0
- package/src/plugins/remark-mermaid.mjs +46 -0
- package/src/styles/global.css +870 -0
- package/src/styles/slides.css +220 -0
- package/src/utils/sidebar.ts +492 -0
- package/templates/default/astro.config.mjs +51 -0
- package/templates/default/content/pages/about.mdx +93 -0
- package/templates/default/content/pages/index.mdx +20 -0
- package/templates/default/content/posts/blog_docs/01-quick-start.md +162 -0
- package/templates/default/content/posts/blog_docs/02-frontmatter.md +277 -0
- package/templates/default/content/posts/blog_docs/03-markdown-basic.md +350 -0
- package/templates/default/content/posts/blog_docs/04-containers.md +331 -0
- package/templates/default/content/posts/blog_docs/05-code-blocks.md +388 -0
- package/templates/default/content/posts/blog_docs/06-mermaid.md +431 -0
- package/templates/default/content/posts/blog_docs/07-video.md +243 -0
- package/templates/default/content/posts/blog_docs/08-latex.md +382 -0
- package/templates/default/content/posts/blog_docs/09-icons.md +326 -0
- package/templates/default/content/posts/blog_docs/10-sidebar.md +445 -0
- package/templates/default/content/posts/blog_docs/11-config.md +334 -0
- package/templates/default/content/posts/blog_docs/12-slides.mdx +552 -0
- package/templates/default/content/posts/blog_docs/README.md +151 -0
- package/templates/default/content/slides/demo.md +146 -0
- package/templates/default/content/slides/docs/basic-demo.md +35 -0
- package/templates/default/content/slides/docs/code-demo.md +62 -0
- package/templates/default/content/slides/docs/echarts-demo.md +139 -0
- package/templates/default/content/slides/docs/fragment-demo.md +35 -0
- package/templates/default/content/slides/docs/math-demo.md +48 -0
- package/templates/default/content/slides/docs/mermaid-demo.md +105 -0
- package/templates/default/content/slides/docs/theme-demo.md +38 -0
- package/templates/default/content/slides/docs/vertical-demo.md +50 -0
- package/templates/default/package.json +31 -0
- package/templates/default/public/favicon-bak.svg +4 -0
- package/templates/default/public/images/avatar.jpg +0 -0
- package/templates/default/public/images/avatar.svg +142 -0
- package/templates/default/public/js/mermaid-container.js +402 -0
- package/templates/default/public/js/mermaid-init.js +131 -0
- package/templates/default/public/js/mermaid-render.js +98 -0
- package/templates/default/public/js/mermaid-simple.js +95 -0
- package/templates/default/public/js/tabs-init.js +86 -0
- package/templates/default/public/media/individual_portfolio/INDIVIDUAL PORTFOLIO.png +0 -0
- package/templates/default/public/slides/plugin/highlight/highlight.js +5 -0
- package/templates/default/public/slides/plugin/highlight/monokai.css +71 -0
- package/templates/default/public/slides/plugin/markdown/markdown.js +7 -0
- package/templates/default/public/slides/plugin/math/math.js +1 -0
- package/templates/default/public/slides/plugin/notes/notes.js +1 -0
- package/templates/default/public/slides/reveal.css +9 -0
- package/templates/default/public/slides/reveal.js +9 -0
- package/templates/default/public/slides/theme/beige.css +366 -0
- package/templates/default/public/slides/theme/black-contrast.css +362 -0
- package/templates/default/public/slides/theme/black.css +359 -0
- package/templates/default/public/slides/theme/blood.css +392 -0
- package/templates/default/public/slides/theme/dracula.css +385 -0
- package/templates/default/public/slides/theme/league.css +368 -0
- package/templates/default/public/slides/theme/moon.css +362 -0
- package/templates/default/public/slides/theme/night.css +360 -0
- package/templates/default/public/slides/theme/serif.css +363 -0
- package/templates/default/public/slides/theme/simple.css +362 -0
- package/templates/default/public/slides/theme/sky.css +370 -0
- package/templates/default/public/slides/theme/solarized.css +363 -0
- package/templates/default/public/slides/theme/white-contrast.css +362 -0
- package/templates/default/public/slides/theme/white.css +359 -0
- package/templates/default/public/slides/theme/white_contrast_compact_verbatim_headers.css +360 -0
- package/templates/default/public/test-complete.html +43 -0
- package/templates/default/public/test-mermaid.html +124 -0
- package/templates/default/src/config/index.ts +114 -0
- package/templates/default/src/content.config.ts +96 -0
- package/templates/default/src/pages/[...slug].astro +27 -0
- package/templates/default/src/pages/archives/[year]/[month]/page/[page].astro +176 -0
- package/templates/default/src/pages/archives/[year]/[month].astro +158 -0
- package/templates/default/src/pages/archives/index.astro +210 -0
- package/templates/default/src/pages/categories/[category]/page/[page].astro +218 -0
- package/templates/default/src/pages/categories/[category].astro +198 -0
- package/templates/default/src/pages/categories/index.astro +190 -0
- package/templates/default/src/pages/container-test.astro +79 -0
- package/templates/default/src/pages/mermaid-direct.html +78 -0
- package/templates/default/src/pages/posts/[...slug].astro +335 -0
- package/templates/default/src/pages/posts/index.astro +541 -0
- package/templates/default/src/pages/posts/page/[page].astro +146 -0
- package/templates/default/src/pages/rss.xml.ts +28 -0
- package/templates/default/src/pages/search-index.json.ts +21 -0
- package/templates/default/src/pages/search.astro +50 -0
- package/templates/default/src/pages/slides/[...slug].astro +54 -0
- package/templates/default/src/pages/slides/index.astro +135 -0
- package/templates/default/src/pages/tags/[tag]/page/[page].astro +211 -0
- package/templates/default/src/pages/tags/[tag].astro +191 -0
- package/templates/default/src/pages/tags/index.astro +167 -0
- package/templates/default/tailwind.config.mjs +78 -0
- package/templates/default/tsconfig.json +9 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
|
|
3
|
+
export function remarkContainers() {
|
|
4
|
+
return (tree, file) => {
|
|
5
|
+
|
|
6
|
+
// 首先检查是否有多行容器语法(开始和结束在同一段落)
|
|
7
|
+
visit(tree, 'paragraph', (node, index, parent) => {
|
|
8
|
+
if (!node.children || node.children.length === 0) return;
|
|
9
|
+
|
|
10
|
+
const firstChild = node.children[0];
|
|
11
|
+
if (firstChild.type !== 'text') return;
|
|
12
|
+
|
|
13
|
+
const fullText = firstChild.value;
|
|
14
|
+
|
|
15
|
+
// 检查是否是完整的容器语法在同一段落中(包括可能有格式化内容的情况)
|
|
16
|
+
const containerStartMatch = fullText.match(/^::: (tip|note|warning|danger|info|details)\s*(.*?)(?:\n|$)/);
|
|
17
|
+
if (containerStartMatch) {
|
|
18
|
+
// 检查最后一个子节点是否包含结束标记
|
|
19
|
+
const lastChild = node.children[node.children.length - 1];
|
|
20
|
+
const hasClosingInSameParagraph = lastChild.type === 'text' &&
|
|
21
|
+
(lastChild.value.endsWith(':::') || lastChild.value.match(/\n:::[\s]*$/));
|
|
22
|
+
|
|
23
|
+
if (hasClosingInSameParagraph) {
|
|
24
|
+
// 整个容器在同一个段落中,包含格式化内容
|
|
25
|
+
const [, type, titlePart] = containerStartMatch;
|
|
26
|
+
const customTitle = titlePart ? titlePart.trim() : '';
|
|
27
|
+
const title = customTitle || getDefaultTitle(type);
|
|
28
|
+
|
|
29
|
+
// 构建内容节点数组
|
|
30
|
+
let contentChildren = [];
|
|
31
|
+
|
|
32
|
+
if (node.children.length === 1) {
|
|
33
|
+
// 只有一个子节点:移除开始标记和结束标记,保留中间内容
|
|
34
|
+
let content = fullText
|
|
35
|
+
.replace(/^::: (tip|note|warning|danger|info|details)\s*(.*?)(?:\n|$)/, '') // 移除开始标记
|
|
36
|
+
.replace(/\n?:::[\s]*$/, ''); // 移除结束标记
|
|
37
|
+
if (content) {
|
|
38
|
+
contentChildren.push({ type: 'text', value: content });
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
// 多个子节点:第一个和最后一个是 text,中间可能有 strong/emphasis 等
|
|
42
|
+
// 提取第一个 text 节点中开始标记之后的内容
|
|
43
|
+
const firstTextContent = fullText.replace(/^::: (tip|note|warning|danger|info|details)\s*(.*?)(?:\n|$)/, '');
|
|
44
|
+
if (firstTextContent) {
|
|
45
|
+
contentChildren.push({ type: 'text', value: firstTextContent });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 添加中间的所有节点(strong、emphasis 等)
|
|
49
|
+
for (let i = 1; i < node.children.length - 1; i++) {
|
|
50
|
+
contentChildren.push(JSON.parse(JSON.stringify(node.children[i])));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 移除最后一个 text 节点中的结束标记
|
|
54
|
+
let lastTextContent = lastChild.value.replace(/\n?:::[\s]*$/, '');
|
|
55
|
+
if (lastTextContent) {
|
|
56
|
+
contentChildren.push({ type: 'text', value: lastTextContent });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 创建 HTML 开始标签
|
|
61
|
+
const openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
|
|
62
|
+
<div class="container-title">${title}</div>
|
|
63
|
+
<div class="container-content">`;
|
|
64
|
+
|
|
65
|
+
const closingHTML = `</div>
|
|
66
|
+
</div>`;
|
|
67
|
+
|
|
68
|
+
const htmlStartNode = { type: 'html', value: openingHTML };
|
|
69
|
+
const htmlEndNode = { type: 'html', value: closingHTML };
|
|
70
|
+
|
|
71
|
+
// 如果有内容,创建段落节点
|
|
72
|
+
let newNodes = [htmlStartNode];
|
|
73
|
+
if (contentChildren.length > 0) {
|
|
74
|
+
newNodes.push({
|
|
75
|
+
type: 'paragraph',
|
|
76
|
+
children: contentChildren
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
newNodes.push(htmlEndNode);
|
|
80
|
+
|
|
81
|
+
// 替换当前段落
|
|
82
|
+
parent.children.splice(index, 1, ...newNodes);
|
|
83
|
+
return index + newNodes.length;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 旧的简单情况:纯文本容器(无格式化)在同一段落
|
|
88
|
+
const completeContainerMatch = fullText.match(/^::: (tip|note|warning|danger|info|details)([^]*?):::$/s);
|
|
89
|
+
if (completeContainerMatch) {
|
|
90
|
+
const [, type, content] = completeContainerMatch;
|
|
91
|
+
const lines = content.trim().split('\n');
|
|
92
|
+
const customTitle = lines.length > 0 ? lines[0].trim() : '';
|
|
93
|
+
const title = customTitle || getDefaultTitle(type);
|
|
94
|
+
|
|
95
|
+
// 内容是第一行之后的所有内容
|
|
96
|
+
const contentText = lines.slice(1).join('\n').trim();
|
|
97
|
+
|
|
98
|
+
// 创建HTML容器
|
|
99
|
+
const htmlContent = `<div class="container-${type} custom-container" data-container-type="${type}">
|
|
100
|
+
<div class="container-title">${title}</div>
|
|
101
|
+
<div class="container-content">
|
|
102
|
+
<p>${contentText.replace(/\n/g, '</p>\n<p>')}</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>`;
|
|
105
|
+
|
|
106
|
+
const htmlNode = {
|
|
107
|
+
type: 'html',
|
|
108
|
+
value: htmlContent
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// 替换当前段落
|
|
112
|
+
parent.children[index] = htmlNode;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 检查是否是 tabs 容器开始语法(支持 :::tabs 和 ::: tabs)
|
|
117
|
+
const tabsMatch = firstChild.value.match(/^:::\s*tabs\s*$/m);
|
|
118
|
+
if (tabsMatch) {
|
|
119
|
+
// 寻找 tabs 结束标记
|
|
120
|
+
let endIndex = -1;
|
|
121
|
+
const siblings = parent.children;
|
|
122
|
+
|
|
123
|
+
for (let i = index + 1; i < siblings.length; i++) {
|
|
124
|
+
const sibling = siblings[i];
|
|
125
|
+
if (sibling.type === 'paragraph' &&
|
|
126
|
+
sibling.children &&
|
|
127
|
+
sibling.children.length > 0 &&
|
|
128
|
+
sibling.children[0].type === 'text' &&
|
|
129
|
+
sibling.children[0].value.trim() === ':::') {
|
|
130
|
+
endIndex = i;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (endIndex === -1) {
|
|
136
|
+
endIndex = siblings.length;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 收集中间的内容
|
|
140
|
+
const contentNodes = siblings.slice(index + 1, endIndex);
|
|
141
|
+
|
|
142
|
+
// 创建 tabs 包装器
|
|
143
|
+
const openingHTML = '<div class="tabs-wrapper">';
|
|
144
|
+
const closingHTML = '</div>';
|
|
145
|
+
|
|
146
|
+
const htmlNode = {
|
|
147
|
+
type: 'html',
|
|
148
|
+
value: openingHTML
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const closeNode = {
|
|
152
|
+
type: 'html',
|
|
153
|
+
value: closingHTML
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// 替换节点
|
|
157
|
+
const replaceCount = endIndex - index + 1; // +1 包含结束标记
|
|
158
|
+
const newNodes = [htmlNode, ...contentNodes, closeNode];
|
|
159
|
+
siblings.splice(index, replaceCount, ...newNodes);
|
|
160
|
+
|
|
161
|
+
return index + newNodes.length;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 检查是否匹配容器开始语法(支持标题后直接跟内容,无需空行)
|
|
165
|
+
const containerMatch = firstChild.value.match(/^::: (tip|note|warning|danger|info|details)(.*)$/m);
|
|
166
|
+
if (containerMatch) {
|
|
167
|
+
const [matchedLine, type, titlePart] = containerMatch;
|
|
168
|
+
const customTitle = titlePart ? titlePart.trim() : '';
|
|
169
|
+
const title = customTitle || getDefaultTitle(type);
|
|
170
|
+
|
|
171
|
+
// 检查是否标题行后面还有内容(无空行的情况)
|
|
172
|
+
const fullValue = firstChild.value;
|
|
173
|
+
const matchEnd = fullValue.indexOf(matchedLine) + matchedLine.length;
|
|
174
|
+
const remainingContent = fullValue.slice(matchEnd).replace(/^\n/, ''); // 移除开头的换行符
|
|
175
|
+
|
|
176
|
+
// 检查是否这个段落只包含开始标签
|
|
177
|
+
const isOnlyStartTag = remainingContent.trim() === '' &&
|
|
178
|
+
(fullValue.trim() === `:::${type}${titlePart}`.trim() ||
|
|
179
|
+
fullValue.trim() === `::: ${type}${titlePart}`.trim() ||
|
|
180
|
+
fullValue.trim() === `::: ${type} ${titlePart}`.trim());
|
|
181
|
+
|
|
182
|
+
// 寻找结束标记
|
|
183
|
+
let endIndex = -1;
|
|
184
|
+
const siblings = parent.children;
|
|
185
|
+
|
|
186
|
+
// 如果是独立的开始标签,跳过紧接着的空段落
|
|
187
|
+
let searchStart = index + 1;
|
|
188
|
+
if (isOnlyStartTag && searchStart < siblings.length) {
|
|
189
|
+
const nextNode = siblings[searchStart];
|
|
190
|
+
// 如果下一个节点是空段落,跳过它
|
|
191
|
+
if (nextNode.type === 'paragraph' &&
|
|
192
|
+
(!nextNode.children || nextNode.children.length === 0 ||
|
|
193
|
+
(nextNode.children.length === 1 &&
|
|
194
|
+
nextNode.children[0].type === 'text' &&
|
|
195
|
+
nextNode.children[0].value.trim() === ''))) {
|
|
196
|
+
searchStart++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 用于存储开始段落中的剩余内容(无空行情况)
|
|
201
|
+
let inlineContentNodes = [];
|
|
202
|
+
if (!isOnlyStartTag) {
|
|
203
|
+
// 标题行后面直接有内容,需要处理这部分内容
|
|
204
|
+
// 创建内容节点的副本,避免修改原始节点
|
|
205
|
+
let contentChildren = [];
|
|
206
|
+
|
|
207
|
+
// 处理第一个文本节点,移除开始标记(只移除第一行的 ::: type title)
|
|
208
|
+
const trimmedRemaining = remainingContent.replace(/^\n/, ''); // 移除开头的换行符
|
|
209
|
+
if (trimmedRemaining !== '') {
|
|
210
|
+
contentChildren.push({ type: 'text', value: trimmedRemaining });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 复制其他子节点(strong、emphasis 等)
|
|
214
|
+
for (let i = 1; i < node.children.length; i++) {
|
|
215
|
+
contentChildren.push(JSON.parse(JSON.stringify(node.children[i])));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 检查最后一个子节点是否包含结束标记
|
|
219
|
+
let hasClosingTag = false;
|
|
220
|
+
if (contentChildren.length > 0) {
|
|
221
|
+
const lastChild = contentChildren[contentChildren.length - 1];
|
|
222
|
+
if (lastChild.type === 'text') {
|
|
223
|
+
const closingMatch = lastChild.value.match(/([\s\S]*?)\n:::(\s*)$/) ||
|
|
224
|
+
lastChild.value.match(/([\s\S]*?):::(\s*)$/);
|
|
225
|
+
if (closingMatch) {
|
|
226
|
+
lastChild.value = closingMatch[1].trimEnd();
|
|
227
|
+
hasClosingTag = true;
|
|
228
|
+
// 如果最后一个文本节点变空了,移除它
|
|
229
|
+
if (!lastChild.value) {
|
|
230
|
+
contentChildren.pop();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 如果有内容,创建段落节点
|
|
237
|
+
if (contentChildren.length > 0) {
|
|
238
|
+
inlineContentNodes.push({
|
|
239
|
+
type: 'paragraph',
|
|
240
|
+
children: contentChildren
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (hasClosingTag) {
|
|
245
|
+
// 找到了结束标记,不需要继续搜索
|
|
246
|
+
endIndex = index + 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 如果还没找到结束标记,继续搜索后续节点
|
|
251
|
+
if (endIndex === -1) {
|
|
252
|
+
for (let i = searchStart; i < siblings.length; i++) {
|
|
253
|
+
const sibling = siblings[i];
|
|
254
|
+
|
|
255
|
+
// 检查段落类型中是否有结束标记
|
|
256
|
+
if (sibling.type === 'paragraph' &&
|
|
257
|
+
sibling.children &&
|
|
258
|
+
sibling.children.length > 0) {
|
|
259
|
+
|
|
260
|
+
// 检查第一个子节点是否是独立的结束标记
|
|
261
|
+
const firstChild = sibling.children[0];
|
|
262
|
+
if (firstChild.type === 'text' && firstChild.value.trim() === ':::') {
|
|
263
|
+
endIndex = i;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 检查最后一个子节点是否包含结束标记
|
|
268
|
+
const lastChild = sibling.children[sibling.children.length - 1];
|
|
269
|
+
if (lastChild.type === 'text') {
|
|
270
|
+
const textValue = lastChild.value;
|
|
271
|
+
|
|
272
|
+
// 检查是否包含结束标记(可能在行末,如 "内容\n:::" 或直接 ":::")
|
|
273
|
+
const closingMatch = textValue.match(/([\s\S]*?)\n:::(\s*)$/) ||
|
|
274
|
+
textValue.match(/([\s\S]*?):::(\s*)$/);
|
|
275
|
+
if (closingMatch) {
|
|
276
|
+
const contentBefore = closingMatch[1].trimEnd();
|
|
277
|
+
|
|
278
|
+
if (contentBefore || sibling.children.length > 1) {
|
|
279
|
+
// 保留结束标记前的内容
|
|
280
|
+
lastChild.value = contentBefore;
|
|
281
|
+
endIndex = i + 1; // 包含这个段落(作为内容的一部分)
|
|
282
|
+
} else {
|
|
283
|
+
// 没有内容在结束标记前,这是一个独立的结束标记
|
|
284
|
+
endIndex = i;
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 也检查第一个子节点是否以容器开始语法开头(但不是结束标记)
|
|
291
|
+
if (firstChild.type === 'text') {
|
|
292
|
+
const closingMatch = firstChild.value.match(/^([\s\S]*?)\n:::(\s*)$/) ||
|
|
293
|
+
firstChild.value.match(/^([\s\S]+?):::(\s*)$/);
|
|
294
|
+
if (closingMatch) {
|
|
295
|
+
const contentBefore = closingMatch[1].trim();
|
|
296
|
+
if (contentBefore) {
|
|
297
|
+
firstChild.value = contentBefore;
|
|
298
|
+
endIndex = i + 1;
|
|
299
|
+
} else {
|
|
300
|
+
endIndex = i;
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 检查列表中是否包含结束标记
|
|
308
|
+
if (sibling.type === 'list') {
|
|
309
|
+
let foundClosing = false;
|
|
310
|
+
|
|
311
|
+
// 遍历列表项查找结束标记
|
|
312
|
+
for (let itemIdx = 0; itemIdx < sibling.children.length; itemIdx++) {
|
|
313
|
+
const listItem = sibling.children[itemIdx];
|
|
314
|
+
if (!listItem.children) continue;
|
|
315
|
+
|
|
316
|
+
for (let paraIdx = 0; paraIdx < listItem.children.length; paraIdx++) {
|
|
317
|
+
const para = listItem.children[paraIdx];
|
|
318
|
+
if (para.type === 'paragraph' && para.children) {
|
|
319
|
+
for (let textIdx = 0; textIdx < para.children.length; textIdx++) {
|
|
320
|
+
const textNode = para.children[textIdx];
|
|
321
|
+
if (textNode.type === 'text' && textNode.value) {
|
|
322
|
+
// 检查文本是否包含结束标记(支持 \n::: 或直接 :::)
|
|
323
|
+
const closingMatch = textNode.value.match(/^([\s\S]*?)\n:::(\s*)$/) ||
|
|
324
|
+
textNode.value.match(/^([\s\S]*?):::(\s*)$/);
|
|
325
|
+
if (closingMatch) {
|
|
326
|
+
const contentBefore = closingMatch[1].trimEnd();
|
|
327
|
+
textNode.value = contentBefore;
|
|
328
|
+
endIndex = i + 1; // 包含这个列表
|
|
329
|
+
foundClosing = true;
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (foundClosing) break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (foundClosing) break;
|
|
338
|
+
}
|
|
339
|
+
if (foundClosing) break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (endIndex === -1) {
|
|
345
|
+
// 如果找不到结束标记,找到下一个容器或者文档末尾
|
|
346
|
+
for (let i = index + 1; i < siblings.length; i++) {
|
|
347
|
+
const sibling = siblings[i];
|
|
348
|
+
if (sibling.type === 'paragraph' &&
|
|
349
|
+
sibling.children &&
|
|
350
|
+
sibling.children[0] &&
|
|
351
|
+
sibling.children[0].type === 'text' &&
|
|
352
|
+
sibling.children[0].value.match(/^::: (tip|note|warning|danger|info|details)/)) {
|
|
353
|
+
endIndex = i;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 如果还是没找到,就到文档末尾
|
|
359
|
+
if (endIndex === -1) {
|
|
360
|
+
endIndex = siblings.length;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 收集中间的内容,从正确的起始位置开始
|
|
365
|
+
const contentNodes = [...inlineContentNodes, ...siblings.slice(searchStart, endIndex)];
|
|
366
|
+
|
|
367
|
+
// 创建HTML容器
|
|
368
|
+
const openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
|
|
369
|
+
<div class="container-title">${title}</div>
|
|
370
|
+
<div class="container-content">`;
|
|
371
|
+
|
|
372
|
+
const closingHTML = `</div>
|
|
373
|
+
</div>`;
|
|
374
|
+
|
|
375
|
+
const htmlNode = {
|
|
376
|
+
type: 'html',
|
|
377
|
+
value: openingHTML
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const closeNode = {
|
|
381
|
+
type: 'html',
|
|
382
|
+
value: closingHTML
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// 替换节点 - 需要考虑可能跳过的空段落
|
|
386
|
+
const replaceCount = endIndex - index;
|
|
387
|
+
const newNodes = [htmlNode, ...contentNodes, closeNode];
|
|
388
|
+
siblings.splice(index, replaceCount, ...newNodes);
|
|
389
|
+
|
|
390
|
+
return index + newNodes.length;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function getDefaultTitle(containerType) {
|
|
397
|
+
const titles = {
|
|
398
|
+
tip: '💡 提示',
|
|
399
|
+
note: '📝 注意',
|
|
400
|
+
warning: '⚠️ 警告',
|
|
401
|
+
danger: '🚨 危险',
|
|
402
|
+
info: 'ℹ️ 信息',
|
|
403
|
+
details: '📋 详情'
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
return titles[containerType] || containerType.toUpperCase();
|
|
407
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
|
|
3
|
+
export function remarkMermaid() {
|
|
4
|
+
return (tree) => {
|
|
5
|
+
visit(tree, 'code', (node, index, parent) => {
|
|
6
|
+
const cleanLang = node.lang ? node.lang.trim().toLowerCase() : '';
|
|
7
|
+
if (cleanLang === 'mermaid') {
|
|
8
|
+
const mermaidCode = node.value.trim();
|
|
9
|
+
const id = `mermaid-${Math.random().toString(36).slice(2, 11)}`;
|
|
10
|
+
|
|
11
|
+
const htmlContent = `<div class="mermaid-container" data-mermaid-source="${escapeBase64(mermaidCode)}" data-id="${id}">
|
|
12
|
+
<div class="mermaid-loading">🔄 正在渲染 Mermaid 图表...</div>
|
|
13
|
+
<pre class="mermaid-fallback" style="display: none;"><code data-language="mermaid">${escapeHtml(mermaidCode)}</code></pre>
|
|
14
|
+
</div>`;
|
|
15
|
+
|
|
16
|
+
parent.children[index] = {
|
|
17
|
+
type: 'html',
|
|
18
|
+
value: htmlContent
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 辅助函数:HTML 转义
|
|
26
|
+
function escapeHtml(text) {
|
|
27
|
+
const map = {
|
|
28
|
+
'&': '&',
|
|
29
|
+
'<': '<',
|
|
30
|
+
'>': '>',
|
|
31
|
+
'"': '"',
|
|
32
|
+
"'": '''
|
|
33
|
+
};
|
|
34
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 辅助函数:Base64 编码(用于存储 mermaid 代码)
|
|
38
|
+
function escapeBase64(text) {
|
|
39
|
+
try {
|
|
40
|
+
// 在 Node.js 环境中
|
|
41
|
+
return Buffer.from(text).toString('base64');
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// 在浏览器环境中(虽然这个插件在服务端运行)
|
|
44
|
+
return btoa(text);
|
|
45
|
+
}
|
|
46
|
+
}
|