@jet-w/astro-blog 0.1.6 → 0.2.1
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-6D3XRDNY.js +145 -0
- package/dist/chunk-A2E2VSAQ.js +246 -0
- package/dist/{chunk-GYLSY3OJ.js → chunk-TJTPX2WP.js} +1 -1
- package/dist/config/index.d.ts +3 -47
- package/dist/config/index.js +18 -2
- package/dist/i18n-PgMCFBw0.d.ts +222 -0
- package/dist/index.d.ts +204 -7
- package/dist/index.js +255 -3
- package/dist/integration.d.ts +9 -1
- package/dist/integration.js +2 -1
- package/dist/{sidebar-DNdiCKBw.d.ts → sidebar-Da-W_4Lr.d.ts} +1 -1
- package/dist/utils/sidebar.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/blog/FloatingToc.vue +11 -3
- package/src/components/blog/Hero.astro +17 -2
- package/src/components/blog/NavigationTabs.vue +46 -15
- package/src/components/blog/PostCard.astro +28 -10
- package/src/components/blog/RelatedPosts.astro +23 -7
- package/src/components/blog/TableOfContents.astro +10 -4
- package/src/components/blog/TagCloud.astro +4 -3
- package/src/components/home/FeaturedPostsSection.astro +22 -6
- package/src/components/home/QuickNavSection.astro +33 -4
- package/src/components/home/RecentPostsSection.astro +22 -6
- package/src/components/home/StatsSection.astro +24 -6
- package/src/components/layout/Footer.astro +36 -20
- package/src/components/layout/Header.astro +75 -17
- package/src/components/layout/Sidebar.astro +40 -25
- package/src/components/ui/LanguageSwitcher.vue +183 -0
- package/src/components/ui/SearchBox.vue +13 -5
- package/src/components/ui/SearchInterface.vue +49 -25
- package/src/layouts/BaseLayout.astro +77 -52
- package/src/layouts/PageLayout.astro +22 -27
- package/src/layouts/SlidesLayout.astro +14 -2
- package/src/pages/archives/[year]/[month].astro +36 -17
- package/src/pages/archives/index.astro +36 -20
- package/src/pages/categories/[category].astro +33 -16
- package/src/pages/categories/index.astro +37 -14
- package/src/pages/posts/[...slug].astro +125 -18
- package/src/pages/posts/index.astro +59 -37
- package/src/pages/posts/page/[page].astro +65 -27
- package/src/pages/rss.xml.ts +18 -6
- package/src/pages/search.astro +50 -14
- package/src/pages/slides/index.astro +25 -6
- package/src/pages/tags/[tag].astro +32 -15
- package/src/pages/tags/index.astro +39 -16
- package/src/plugins/remark-containers.mjs +351 -322
- package/src/plugins/remark-protect-code.mjs +69 -0
- package/src/styles/global.css +35 -1
- package/templates/default/.claude/ralph-loop.local.md +48 -0
- package/templates/default/astro.config.mjs +33 -4
- package/templates/default/content/posts/blog_docs_en/01.get-started/01-intro.md +81 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/02-install.md +137 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/03-create-post.md +176 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/04-structure.md +173 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/05-deploy.md +208 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/README.md +52 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/02-containers.md +245 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/03-code-blocks.md +207 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/03-mermaid.md +194 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/04-icons.md +229 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/06-latex.md +233 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/07-video.md +184 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/08-slides.md +359 -0
- package/templates/default/content/posts/blog_docs_en/02.guide/README.md +213 -0
- package/templates/default/content/posts/blog_docs_en/03.config/01-site.md +208 -0
- package/templates/default/content/posts/blog_docs_en/03.config/02-sidebar.md +240 -0
- package/templates/default/content/posts/blog_docs_en/03.config/03-i18n.md +349 -0
- package/templates/default/content/posts/blog_docs_en/03.config/README.md +85 -0
- package/templates/default/content/posts/blog_docs_en/README.md +79 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/01-intro.md +81 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/02-install.md +137 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/03-create-post.md +176 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/04-structure.md +173 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/05-deploy.md +208 -0
- package/templates/default/content/posts/blog_docs_zh/01.get-started/README.md +52 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/02-containers.md +245 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/03-code-blocks.md +206 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/03-mermaid.md +194 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/04-icons.md +229 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/06-latex.md +233 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/07-video.md +184 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/08-slides.md +359 -0
- package/templates/default/content/posts/blog_docs_zh/02.guide/README.md +213 -0
- package/templates/default/content/posts/blog_docs_zh/03.config/01-site.md +208 -0
- package/templates/default/content/posts/blog_docs_zh/03.config/02-sidebar.md +240 -0
- package/templates/default/content/posts/blog_docs_zh/03.config/03-i18n.md +348 -0
- package/templates/default/content/posts/blog_docs_zh/03.config/README.md +85 -0
- package/templates/default/content/posts/blog_docs_zh/README.md +78 -0
- package/templates/default/package-lock.json +9667 -0
- package/templates/default/package.json +1 -1
- package/templates/default/src/config/footer.ts +14 -11
- package/templates/default/src/config/locales/en/footer.ts +17 -0
- package/templates/default/src/config/locales/en/index.ts +20 -0
- package/templates/default/src/config/locales/en/menu.ts +14 -0
- package/templates/default/src/config/locales/en/sidebar.ts +34 -0
- package/templates/default/src/config/locales/en/site.ts +7 -0
- package/templates/default/src/config/locales/en/ui.ts +29 -0
- package/templates/default/src/config/locales/index.ts +7 -0
- package/templates/default/src/config/locales/zh-CN/footer.ts +17 -0
- package/templates/default/src/config/locales/zh-CN/index.ts +20 -0
- package/templates/default/src/config/locales/zh-CN/menu.ts +14 -0
- package/templates/default/src/config/locales/zh-CN/sidebar.ts +34 -0
- package/templates/default/src/config/locales/zh-CN/site.ts +7 -0
- package/templates/default/src/config/locales/zh-CN/ui.ts +29 -0
- package/templates/default/src/config/sidebar.ts +10 -12
- package/templates/default/src/config/site.ts +2 -2
- package/templates/default/src/content.config.ts +15 -3
- package/templates/default/src/env.d.ts +7 -0
- package/dist/chunk-MQXPSOYB.js +0 -124
- package/templates/default/content/posts/blog_docs/01-quick-start.md +0 -162
- package/templates/default/content/posts/blog_docs/02-frontmatter.md +0 -277
- package/templates/default/content/posts/blog_docs/03-markdown-basic.md +0 -350
- package/templates/default/content/posts/blog_docs/04-containers.md +0 -331
- package/templates/default/content/posts/blog_docs/05-code-blocks.md +0 -388
- package/templates/default/content/posts/blog_docs/06-mermaid.md +0 -431
- package/templates/default/content/posts/blog_docs/07-video.md +0 -243
- package/templates/default/content/posts/blog_docs/08-latex.md +0 -382
- package/templates/default/content/posts/blog_docs/09-icons.md +0 -326
- package/templates/default/content/posts/blog_docs/10-sidebar.md +0 -445
- package/templates/default/content/posts/blog_docs/11-config.md +0 -334
- package/templates/default/content/posts/blog_docs/12-slides.mdx +0 -552
- package/templates/default/content/posts/blog_docs/README.md +0 -151
|
@@ -2,406 +2,435 @@ import { visit } from 'unist-util-visit';
|
|
|
2
2
|
|
|
3
3
|
export function remarkContainers() {
|
|
4
4
|
return (tree, file) => {
|
|
5
|
-
|
|
6
|
-
//
|
|
5
|
+
// Pre-process: Handle complete containers in a single paragraph
|
|
6
|
+
// This handles cases like:
|
|
7
|
+
// ::: tip
|
|
8
|
+
// Content without blank lines
|
|
9
|
+
// :::
|
|
10
|
+
// Where the entire thing is parsed as one paragraph with text "::: tip\nContent\n:::"
|
|
11
|
+
// Also handles paragraphs with mixed content (text + inline code, etc.)
|
|
7
12
|
visit(tree, 'paragraph', (node, index, parent) => {
|
|
8
13
|
if (!node.children || node.children.length === 0) return;
|
|
9
14
|
|
|
10
15
|
const firstChild = node.children[0];
|
|
11
|
-
|
|
16
|
+
const lastChild = node.children[node.children.length - 1];
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
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]*$/));
|
|
18
|
+
// First child must be text starting with :::
|
|
19
|
+
if (firstChild.type !== 'text') return;
|
|
20
|
+
// Last child must be text ending with :::
|
|
21
|
+
if (lastChild.type !== 'text') return;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const [, type, titlePart] = containerStartMatch;
|
|
26
|
-
const customTitle = titlePart ? titlePart.trim() : '';
|
|
27
|
-
const title = customTitle || getDefaultTitle(type);
|
|
23
|
+
const firstText = firstChild.value;
|
|
24
|
+
const lastText = lastChild.value;
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
// Check if first text starts with ::: type
|
|
27
|
+
const startMatch = firstText.match(/^(:{3,})\s+(tip|note|warning|danger|info|details)([ \t]+[^\n]*)?\n?/);
|
|
28
|
+
if (!startMatch) return;
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
}
|
|
30
|
+
// Check if last text ends with :::
|
|
31
|
+
const endMatch = lastText.match(/\n(:{3,})\s*$/);
|
|
32
|
+
if (!endMatch) return;
|
|
47
33
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
// Verify the closing colons match the opening
|
|
35
|
+
const [, openColons, type, titlePart] = startMatch;
|
|
36
|
+
const [, closeColons] = endMatch;
|
|
37
|
+
if (openColons.length !== closeColons.length) return;
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (lastTextContent) {
|
|
56
|
-
contentChildren.push({ type: 'text', value: lastTextContent });
|
|
57
|
-
}
|
|
58
|
-
}
|
|
39
|
+
const customTitle = titlePart ? titlePart.trim() : '';
|
|
40
|
+
const title = customTitle || getDefaultTitle(type);
|
|
59
41
|
|
|
60
|
-
|
|
61
|
-
|
|
42
|
+
// Create HTML wrapper
|
|
43
|
+
let openingHTML, closingHTML;
|
|
44
|
+
if (type === 'details') {
|
|
45
|
+
openingHTML = `<details class="container-details custom-container" data-container-type="details">
|
|
46
|
+
<summary class="container-title">${title}</summary>
|
|
47
|
+
<div class="container-content">`;
|
|
48
|
+
closingHTML = `</div>
|
|
49
|
+
</details>`;
|
|
50
|
+
} else {
|
|
51
|
+
openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
|
|
62
52
|
<div class="container-title">${title}</div>
|
|
63
53
|
<div class="container-content">`;
|
|
64
|
-
|
|
65
|
-
const closingHTML = `</div>
|
|
54
|
+
closingHTML = `</div>
|
|
66
55
|
</div>`;
|
|
56
|
+
}
|
|
67
57
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
58
|
+
// Extract content by modifying the first and last text nodes
|
|
59
|
+
// Remove the ::: opening from first text
|
|
60
|
+
const newFirstText = firstText.slice(startMatch[0].length);
|
|
61
|
+
// Remove the ::: closing from last text
|
|
62
|
+
const newLastText = lastText.slice(0, endMatch.index);
|
|
63
|
+
|
|
64
|
+
// Build new content children
|
|
65
|
+
const contentChildren = [];
|
|
66
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
67
|
+
const child = node.children[i];
|
|
68
|
+
if (i === 0) {
|
|
69
|
+
// First child - use trimmed text
|
|
70
|
+
if (node.children.length === 1) {
|
|
71
|
+
// Single text node - extract middle content
|
|
72
|
+
const middleText = newFirstText.slice(0, newFirstText.length - (firstText.length - lastText.length) - endMatch[0].length);
|
|
73
|
+
if (middleText.trim()) {
|
|
74
|
+
contentChildren.push({ ...child, value: middleText.trim() });
|
|
75
|
+
}
|
|
76
|
+
} else if (newFirstText.trim()) {
|
|
77
|
+
contentChildren.push({ ...child, value: newFirstText });
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
} else if (i === node.children.length - 1) {
|
|
80
|
+
// Last child - use trimmed text
|
|
81
|
+
if (newLastText.trim()) {
|
|
82
|
+
contentChildren.push({ ...child, value: newLastText });
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
// Middle children - keep as-is
|
|
86
|
+
contentChildren.push({ ...child });
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
//
|
|
88
|
-
|
|
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);
|
|
90
|
+
// If no content, skip
|
|
91
|
+
if (contentChildren.length === 0) return;
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
// Replace with HTML nodes and content paragraph
|
|
94
|
+
const htmlStartNode = { type: 'html', value: openingHTML };
|
|
95
|
+
const contentParagraph = {
|
|
96
|
+
type: 'paragraph',
|
|
97
|
+
children: contentChildren
|
|
98
|
+
};
|
|
99
|
+
const htmlEndNode = { type: 'html', value: closingHTML };
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
};
|
|
101
|
+
parent.children.splice(index, 1, htmlStartNode, contentParagraph, htmlEndNode);
|
|
102
|
+
return index + 3;
|
|
103
|
+
});
|
|
110
104
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
// Pre-process: Extract ::: from text nodes where it appears at the end
|
|
106
|
+
// This handles cases where ::: is on a new line but without a blank line separator
|
|
107
|
+
// e.g., in list items: "- content\n:::" gets parsed as single text node
|
|
108
|
+
|
|
109
|
+
// Helper function to extract trailing ::: from a text node
|
|
110
|
+
function extractTrailingColons(textNode) {
|
|
111
|
+
const text = textNode.value;
|
|
112
|
+
const trailingMatch = text.match(/\n(:{3,})\s*$/);
|
|
113
|
+
if (trailingMatch) {
|
|
114
|
+
textNode.value = text.slice(0, trailingMatch.index);
|
|
115
|
+
return trailingMatch[1];
|
|
114
116
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Process list items - ::: might be attached to last list item's text
|
|
121
|
+
// We need to manually iterate because visit() doesn't handle tree modifications well
|
|
122
|
+
function processLists(node, parent, index) {
|
|
123
|
+
if (node.type === 'list' && node.children && node.children.length > 0) {
|
|
124
|
+
// Check the last list item
|
|
125
|
+
const lastItem = node.children[node.children.length - 1];
|
|
126
|
+
if (lastItem.children && lastItem.children.length > 0) {
|
|
127
|
+
// Find the last paragraph in the last list item
|
|
128
|
+
const lastParagraph = lastItem.children[lastItem.children.length - 1];
|
|
129
|
+
if (lastParagraph.type === 'paragraph' && lastParagraph.children) {
|
|
130
|
+
// Check the last text node in that paragraph
|
|
131
|
+
const lastText = lastParagraph.children[lastParagraph.children.length - 1];
|
|
132
|
+
if (lastText.type === 'text') {
|
|
133
|
+
const colons = extractTrailingColons(lastText);
|
|
134
|
+
if (colons && parent) {
|
|
135
|
+
// Create a new paragraph for the ::: after the list
|
|
136
|
+
const closingParagraph = {
|
|
137
|
+
type: 'paragraph',
|
|
138
|
+
children: [{ type: 'text', value: colons }]
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Insert after the list
|
|
142
|
+
parent.children.splice(index + 1, 0, closingParagraph);
|
|
143
|
+
return true; // Indicate we modified the tree
|
|
144
|
+
}
|
|
145
|
+
}
|
|
132
146
|
}
|
|
133
147
|
}
|
|
148
|
+
}
|
|
134
149
|
|
|
135
|
-
|
|
136
|
-
|
|
150
|
+
// Recursively process children
|
|
151
|
+
if (node.children) {
|
|
152
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
153
|
+
if (processLists(node.children[i], node, i)) {
|
|
154
|
+
// Tree was modified, need to re-check
|
|
155
|
+
i++; // Skip the newly inserted node
|
|
156
|
+
}
|
|
137
157
|
}
|
|
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
158
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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++;
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
processLists(tree, null, 0);
|
|
162
|
+
|
|
163
|
+
// Process regular paragraphs - extract trailing ::: into separate paragraphs
|
|
164
|
+
// Use a manual loop to handle tree modifications properly
|
|
165
|
+
function processAllParagraphs(node) {
|
|
166
|
+
if (!node.children) return;
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
169
|
+
const child = node.children[i];
|
|
170
|
+
|
|
171
|
+
if (child.type === 'paragraph' && child.children && child.children.length > 0) {
|
|
172
|
+
const lastChild = child.children[child.children.length - 1];
|
|
173
|
+
if (lastChild.type === 'text') {
|
|
174
|
+
const colons = extractTrailingColons(lastChild);
|
|
175
|
+
if (colons) {
|
|
176
|
+
// Create a new paragraph for the :::
|
|
177
|
+
const closingParagraph = {
|
|
178
|
+
type: 'paragraph',
|
|
179
|
+
children: [{ type: 'text', value: colons }]
|
|
180
|
+
};
|
|
181
|
+
// Insert after the current paragraph
|
|
182
|
+
node.children.splice(i + 1, 0, closingParagraph);
|
|
183
|
+
i++; // Skip the newly inserted node
|
|
184
|
+
}
|
|
197
185
|
}
|
|
198
186
|
}
|
|
199
187
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// 处理第一个文本节点,移除开始标记(只移除第一行的 ::: type title)
|
|
208
|
-
const trimmedRemaining = remainingContent.replace(/^\n/, ''); // 移除开头的换行符
|
|
209
|
-
if (trimmedRemaining !== '') {
|
|
210
|
-
contentChildren.push({ type: 'text', value: trimmedRemaining });
|
|
211
|
-
}
|
|
188
|
+
// Recursively process children (but not listItems - handled separately)
|
|
189
|
+
if (child.type !== 'listItem') {
|
|
190
|
+
processAllParagraphs(child);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
processAllParagraphs(tree);
|
|
212
195
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
196
|
+
// Process containers multiple times to handle nesting (innermost first)
|
|
197
|
+
// Each pass processes containers that don't contain other unprocessed containers
|
|
198
|
+
let maxPasses = 5; // Prevent infinite loops
|
|
199
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
200
|
+
let foundContainers = false;
|
|
217
201
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
}
|
|
202
|
+
// Handle paragraph-based container syntax
|
|
203
|
+
visit(tree, 'paragraph', (node, index, parent) => {
|
|
204
|
+
if (!node.children || node.children.length === 0) return;
|
|
235
205
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
inlineContentNodes.push({
|
|
239
|
-
type: 'paragraph',
|
|
240
|
-
children: contentChildren
|
|
241
|
-
});
|
|
242
|
-
}
|
|
206
|
+
const firstChild = node.children[0];
|
|
207
|
+
if (firstChild.type !== 'text') return;
|
|
243
208
|
|
|
244
|
-
|
|
245
|
-
// 找到了结束标记,不需要继续搜索
|
|
246
|
-
endIndex = index + 1;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
209
|
+
const fullText = firstChild.value;
|
|
249
210
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
211
|
+
// Check for container start: :::+ type [title]
|
|
212
|
+
const startMatch = fullText.match(/^(:{3,}) (tip|note|warning|danger|info|details)([ \t]+[^\n]*)?$/m);
|
|
213
|
+
if (startMatch) {
|
|
214
|
+
const [, colons, type, titlePart] = startMatch;
|
|
215
|
+
const colonCount = colons.length;
|
|
216
|
+
const customTitle = titlePart ? titlePart.trim() : '';
|
|
217
|
+
const title = customTitle || getDefaultTitle(type);
|
|
254
218
|
|
|
255
|
-
|
|
219
|
+
// Find matching closing with exact same number of colons
|
|
220
|
+
let endIndex = -1;
|
|
221
|
+
const siblings = parent.children;
|
|
222
|
+
let nestLevel = 0;
|
|
223
|
+
let hasUnprocessedInner = false;
|
|
224
|
+
|
|
225
|
+
for (let i = index + 1; i < siblings.length; i++) {
|
|
226
|
+
const sibling = siblings[i];
|
|
256
227
|
if (sibling.type === 'paragraph' &&
|
|
257
228
|
sibling.children &&
|
|
258
|
-
sibling.children.length > 0
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
229
|
+
sibling.children.length > 0 &&
|
|
230
|
+
sibling.children[0].type === 'text') {
|
|
231
|
+
const text = sibling.children[0].value.trim();
|
|
232
|
+
|
|
233
|
+
// Check for opening of inner container (fewer colons = more inner)
|
|
234
|
+
const openMatch = text.match(/^(:{3,}) (tip|note|warning|danger|info|details)/);
|
|
235
|
+
if (openMatch) {
|
|
236
|
+
if (openMatch[1].length < colonCount) {
|
|
237
|
+
// This is an inner container that should be processed first
|
|
238
|
+
hasUnprocessedInner = true;
|
|
239
|
+
} else if (openMatch[1].length === colonCount) {
|
|
240
|
+
nestLevel++;
|
|
287
241
|
}
|
|
288
242
|
}
|
|
289
243
|
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const contentBefore = closingMatch[1].trim();
|
|
296
|
-
if (contentBefore) {
|
|
297
|
-
firstChild.value = contentBefore;
|
|
298
|
-
endIndex = i + 1;
|
|
299
|
-
} else {
|
|
244
|
+
// Check for closing
|
|
245
|
+
const closeMatch = text.match(/^(:{3,})$/);
|
|
246
|
+
if (closeMatch) {
|
|
247
|
+
if (closeMatch[1].length === colonCount) {
|
|
248
|
+
if (nestLevel === 0) {
|
|
300
249
|
endIndex = i;
|
|
250
|
+
break;
|
|
251
|
+
} else {
|
|
252
|
+
nestLevel--;
|
|
301
253
|
}
|
|
302
|
-
break;
|
|
303
254
|
}
|
|
304
255
|
}
|
|
305
256
|
}
|
|
257
|
+
}
|
|
306
258
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
}
|
|
259
|
+
// Skip this container if it has unprocessed inner containers
|
|
260
|
+
if (hasUnprocessedInner) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (endIndex === -1) {
|
|
265
|
+
return; // No matching close found
|
|
341
266
|
}
|
|
267
|
+
|
|
268
|
+
foundContainers = true;
|
|
269
|
+
const contentNodes = siblings.slice(index + 1, endIndex);
|
|
270
|
+
|
|
271
|
+
let openingHTML, closingHTML;
|
|
272
|
+
if (type === 'details') {
|
|
273
|
+
openingHTML = `<details class="container-details custom-container" data-container-type="details">
|
|
274
|
+
<summary class="container-title">${title}</summary>
|
|
275
|
+
<div class="container-content">`;
|
|
276
|
+
closingHTML = `</div>
|
|
277
|
+
</details>`;
|
|
278
|
+
} else {
|
|
279
|
+
openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
|
|
280
|
+
<div class="container-title">${title}</div>
|
|
281
|
+
<div class="container-content">`;
|
|
282
|
+
closingHTML = `</div>
|
|
283
|
+
</div>`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const htmlStartNode = { type: 'html', value: openingHTML };
|
|
287
|
+
const htmlEndNode = { type: 'html', value: closingHTML };
|
|
288
|
+
|
|
289
|
+
const replaceCount = endIndex - index + 1;
|
|
290
|
+
const newNodes = [htmlStartNode, ...contentNodes, htmlEndNode];
|
|
291
|
+
siblings.splice(index, replaceCount, ...newNodes);
|
|
292
|
+
|
|
293
|
+
return index + newNodes.length;
|
|
342
294
|
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Also process tabs containers in this pass
|
|
298
|
+
visit(tree, 'paragraph', (node, index, parent) => {
|
|
299
|
+
if (!node.children || node.children.length === 0) return;
|
|
300
|
+
|
|
301
|
+
const firstChild = node.children[0];
|
|
302
|
+
if (firstChild.type !== 'text') return;
|
|
303
|
+
|
|
304
|
+
const tabsMatch = firstChild.value.match(/^:{3,}\s*tabs\s*$/m);
|
|
305
|
+
if (tabsMatch) {
|
|
306
|
+
let endIndex = -1;
|
|
307
|
+
const siblings = parent.children;
|
|
343
308
|
|
|
344
|
-
if (endIndex === -1) {
|
|
345
|
-
// 如果找不到结束标记,找到下一个容器或者文档末尾
|
|
346
309
|
for (let i = index + 1; i < siblings.length; i++) {
|
|
347
310
|
const sibling = siblings[i];
|
|
348
311
|
if (sibling.type === 'paragraph' &&
|
|
349
312
|
sibling.children &&
|
|
350
|
-
sibling.children
|
|
313
|
+
sibling.children.length > 0 &&
|
|
351
314
|
sibling.children[0].type === 'text' &&
|
|
352
|
-
sibling.children[0].value.
|
|
315
|
+
/^:{3,}$/.test(sibling.children[0].value.trim())) {
|
|
353
316
|
endIndex = i;
|
|
354
317
|
break;
|
|
355
318
|
}
|
|
356
319
|
}
|
|
357
320
|
|
|
358
|
-
// 如果还是没找到,就到文档末尾
|
|
359
321
|
if (endIndex === -1) {
|
|
360
322
|
endIndex = siblings.length;
|
|
361
323
|
}
|
|
324
|
+
|
|
325
|
+
const contentNodes = siblings.slice(index + 1, endIndex);
|
|
326
|
+
const openingHTML = '<div class="tabs-wrapper">';
|
|
327
|
+
const closingHTML = '</div>';
|
|
328
|
+
|
|
329
|
+
const replaceCount = endIndex - index + 1;
|
|
330
|
+
const newNodes = [
|
|
331
|
+
{ type: 'html', value: openingHTML },
|
|
332
|
+
...contentNodes,
|
|
333
|
+
{ type: 'html', value: closingHTML }
|
|
334
|
+
];
|
|
335
|
+
siblings.splice(index, replaceCount, ...newNodes);
|
|
336
|
+
|
|
337
|
+
return index + newNodes.length;
|
|
362
338
|
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (!foundContainers) {
|
|
342
|
+
break; // No more containers to process
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Handle containerDirective nodes created by remark-directive
|
|
347
|
+
visit(tree, 'containerDirective', (node, index, parent) => {
|
|
348
|
+
const type = node.name;
|
|
349
|
+
if (!['tip', 'note', 'warning', 'danger', 'info', 'details'].includes(type)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Debug: Log directive node
|
|
354
|
+
if (process.env.DEBUG_CONTAINERS) {
|
|
355
|
+
console.log('DEBUG containerDirective:', type, 'children:', node.children?.length || 0, JSON.stringify(node.children?.map(c => c.type)));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Get custom title from directive label (text after ::: type on same line)
|
|
359
|
+
let customTitle = '';
|
|
360
|
+
if (node.data && node.data.directiveLabel) {
|
|
361
|
+
customTitle = node.data.directiveLabel;
|
|
362
|
+
}
|
|
363
363
|
|
|
364
|
-
|
|
365
|
-
const contentNodes = [...inlineContentNodes, ...siblings.slice(searchStart, endIndex)];
|
|
364
|
+
const title = customTitle || getDefaultTitle(type);
|
|
366
365
|
|
|
367
|
-
|
|
368
|
-
|
|
366
|
+
// Create HTML wrapper - use <details>/<summary> for details type
|
|
367
|
+
let openingHTML, closingHTML;
|
|
368
|
+
if (type === 'details') {
|
|
369
|
+
openingHTML = `<details class="container-details custom-container" data-container-type="details">
|
|
370
|
+
<summary class="container-title">${title}</summary>
|
|
371
|
+
<div class="container-content">`;
|
|
372
|
+
closingHTML = `</div>
|
|
373
|
+
</details>`;
|
|
374
|
+
} else {
|
|
375
|
+
openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
|
|
369
376
|
<div class="container-title">${title}</div>
|
|
370
377
|
<div class="container-content">`;
|
|
371
|
-
|
|
372
|
-
const closingHTML = `</div>
|
|
378
|
+
closingHTML = `</div>
|
|
373
379
|
</div>`;
|
|
380
|
+
}
|
|
374
381
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
value: openingHTML
|
|
378
|
-
};
|
|
382
|
+
const htmlStartNode = { type: 'html', value: openingHTML };
|
|
383
|
+
const htmlEndNode = { type: 'html', value: closingHTML };
|
|
379
384
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
value: closingHTML
|
|
383
|
-
};
|
|
385
|
+
const newNodes = [htmlStartNode, ...node.children, htmlEndNode];
|
|
386
|
+
parent.children.splice(index, 1, ...newNodes);
|
|
384
387
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const newNodes = [htmlNode, ...contentNodes, closeNode];
|
|
388
|
-
siblings.splice(index, replaceCount, ...newNodes);
|
|
388
|
+
return index + newNodes.length;
|
|
389
|
+
});
|
|
389
390
|
|
|
390
|
-
|
|
391
|
+
// Handle leafDirective nodes (single-line directives)
|
|
392
|
+
visit(tree, 'leafDirective', (node, index, parent) => {
|
|
393
|
+
const type = node.name;
|
|
394
|
+
if (!['tip', 'note', 'warning', 'danger', 'info', 'details'].includes(type)) {
|
|
395
|
+
return;
|
|
391
396
|
}
|
|
397
|
+
|
|
398
|
+
let customTitle = '';
|
|
399
|
+
if (node.data && node.data.directiveLabel) {
|
|
400
|
+
customTitle = node.data.directiveLabel;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const title = customTitle || getDefaultTitle(type);
|
|
404
|
+
|
|
405
|
+
// Create HTML wrapper - use <details>/<summary> for details type
|
|
406
|
+
let openingHTML, closingHTML;
|
|
407
|
+
if (type === 'details') {
|
|
408
|
+
openingHTML = `<details class="container-details custom-container" data-container-type="details">
|
|
409
|
+
<summary class="container-title">${title}</summary>
|
|
410
|
+
<div class="container-content">`;
|
|
411
|
+
closingHTML = `</div>
|
|
412
|
+
</details>`;
|
|
413
|
+
} else {
|
|
414
|
+
openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
|
|
415
|
+
<div class="container-title">${title}</div>
|
|
416
|
+
<div class="container-content">`;
|
|
417
|
+
closingHTML = `</div>
|
|
418
|
+
</div>`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const htmlStartNode = { type: 'html', value: openingHTML };
|
|
422
|
+
const htmlEndNode = { type: 'html', value: closingHTML };
|
|
423
|
+
|
|
424
|
+
const newNodes = [htmlStartNode, ...node.children, htmlEndNode];
|
|
425
|
+
parent.children.splice(index, 1, ...newNodes);
|
|
426
|
+
|
|
427
|
+
return index + newNodes.length;
|
|
392
428
|
});
|
|
429
|
+
|
|
393
430
|
};
|
|
394
431
|
}
|
|
395
432
|
|
|
396
433
|
function getDefaultTitle(containerType) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
note: '📝 注意',
|
|
400
|
-
warning: '⚠️ 警告',
|
|
401
|
-
danger: '🚨 危险',
|
|
402
|
-
info: 'ℹ️ 信息',
|
|
403
|
-
details: '📋 详情'
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
return titles[containerType] || containerType.toUpperCase();
|
|
434
|
+
// Return the container type with first letter capitalized
|
|
435
|
+
return containerType.charAt(0).toUpperCase() + containerType.slice(1);
|
|
407
436
|
}
|