@jet-w/astro-blog 0.2.0 → 0.2.2

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.
Files changed (121) hide show
  1. package/dist/{chunk-HVQKQN6B.js → chunk-6D3XRDNY.js} +1 -1
  2. package/dist/{chunk-ATRISB7B.js → chunk-A2E2VSAQ.js} +43 -3
  3. package/dist/chunk-DAH2XP4W.js +154 -0
  4. package/dist/{chunk-AZHCNNAC.js → chunk-PG43JO4O.js} +1 -153
  5. package/dist/chunk-PZICDGJG.js +69 -0
  6. package/dist/chunk-Z3O3JK56.js +186 -0
  7. package/dist/config/index.d.ts +2 -2
  8. package/dist/config/index.js +9 -7
  9. package/dist/{i18n-5H4W145i.d.ts → i18n-DYYPTq4o.d.ts} +21 -1
  10. package/dist/index.d.ts +10 -184
  11. package/dist/index.js +37 -210
  12. package/dist/integration.d.ts +2 -2
  13. package/dist/integration.js +2 -2
  14. package/dist/{sidebar-Da-W_4Lr.d.ts → sidebar-DNdiCKBw.d.ts} +1 -1
  15. package/dist/utils/i18n.d.ts +133 -0
  16. package/dist/utils/i18n.js +49 -0
  17. package/dist/utils/sidebar.d.ts +1 -1
  18. package/dist/utils/useI18n.d.ts +74 -0
  19. package/dist/utils/useI18n.js +15 -0
  20. package/package.json +9 -1
  21. package/src/components/blog/FloatingToc.vue +11 -3
  22. package/src/components/blog/Hero.astro +17 -2
  23. package/src/components/blog/NavigationTabs.vue +46 -15
  24. package/src/components/blog/PostCard.astro +28 -10
  25. package/src/components/blog/RelatedPosts.astro +23 -7
  26. package/src/components/blog/TableOfContents.astro +10 -4
  27. package/src/components/blog/TagCloud.astro +4 -3
  28. package/src/components/home/FeaturedPostsSection.astro +22 -6
  29. package/src/components/home/QuickNavSection.astro +33 -4
  30. package/src/components/home/RecentPostsSection.astro +22 -6
  31. package/src/components/home/StatsSection.astro +24 -6
  32. package/src/components/layout/Header.astro +9 -5
  33. package/src/components/layout/Sidebar.astro +14 -11
  34. package/src/components/ui/SearchBox.vue +13 -5
  35. package/src/components/ui/SearchInterface.vue +49 -25
  36. package/src/pages/archives/[year]/[month].astro +36 -17
  37. package/src/pages/archives/index.astro +36 -20
  38. package/src/pages/categories/[category].astro +33 -16
  39. package/src/pages/categories/index.astro +37 -14
  40. package/src/pages/posts/[...slug].astro +125 -18
  41. package/src/pages/posts/index.astro +59 -37
  42. package/src/pages/posts/page/[page].astro +65 -27
  43. package/src/pages/search.astro +50 -14
  44. package/src/pages/slides/index.astro +25 -6
  45. package/src/pages/tags/[tag].astro +32 -15
  46. package/src/pages/tags/index.astro +39 -16
  47. package/src/plugins/remark-containers.mjs +351 -322
  48. package/src/plugins/remark-protect-code.mjs +69 -0
  49. package/src/styles/global.css +35 -1
  50. package/templates/default/.claude/ralph-loop.local.md +48 -0
  51. package/templates/default/astro.config.mjs +13 -4
  52. package/templates/default/content/posts/blog_docs_en/{get-started → 01.get-started}/01-intro.md +1 -1
  53. package/templates/default/content/posts/blog_docs_en/{get-started → 01.get-started}/02-install.md +1 -1
  54. package/templates/default/content/posts/blog_docs_en/{get-started → 01.get-started}/03-create-post.md +1 -1
  55. package/templates/default/content/posts/blog_docs_en/{get-started → 01.get-started}/04-structure.md +1 -1
  56. package/templates/default/content/posts/blog_docs_en/01.get-started/05-deploy.md +208 -0
  57. package/templates/default/content/posts/blog_docs_en/{get-started → 01.get-started}/README.md +1 -1
  58. package/templates/default/content/posts/blog_docs_en/02.guide/02-containers.md +245 -0
  59. package/templates/default/content/posts/blog_docs_en/{guide/markdown → 02.guide}/03-code-blocks.md +2 -1
  60. package/templates/default/content/posts/blog_docs_en/{guide/features/01-mermaid.md → 02.guide/03-mermaid.md} +1 -1
  61. package/templates/default/content/posts/blog_docs_en/{guide/features → 02.guide}/04-icons.md +4 -2
  62. package/templates/default/content/posts/blog_docs_en/{guide/features/02-latex.md → 02.guide/06-latex.md} +1 -1
  63. package/templates/default/content/posts/blog_docs_en/{guide/features/03-video.md → 02.guide/07-video.md} +1 -1
  64. package/templates/default/content/posts/blog_docs_en/02.guide/08-slides.md +359 -0
  65. package/templates/default/content/posts/blog_docs_en/{guide/markdown → 02.guide}/README.md +22 -3
  66. package/templates/default/content/posts/blog_docs_en/{config → 03.config}/01-site.md +1 -1
  67. package/templates/default/content/posts/blog_docs_en/{config → 03.config}/02-sidebar.md +1 -1
  68. package/templates/default/content/posts/blog_docs_en/{config → 03.config}/03-i18n.md +88 -24
  69. package/templates/default/content/posts/blog_docs_en/{config → 03.config}/README.md +1 -1
  70. package/templates/default/content/posts/blog_docs_en/README.md +2 -1
  71. package/templates/default/content/posts/blog_docs_zh/01.get-started/01-intro.md +81 -0
  72. package/templates/default/content/posts/blog_docs_zh/01.get-started/02-install.md +137 -0
  73. package/templates/default/content/posts/blog_docs_zh/01.get-started/03-create-post.md +176 -0
  74. package/templates/default/content/posts/blog_docs_zh/01.get-started/04-structure.md +173 -0
  75. package/templates/default/content/posts/blog_docs_zh/01.get-started/05-deploy.md +208 -0
  76. package/templates/default/content/posts/blog_docs_zh/01.get-started/README.md +52 -0
  77. package/templates/default/content/posts/blog_docs_zh/02.guide/02-containers.md +245 -0
  78. package/templates/default/content/posts/blog_docs_zh/02.guide/03-code-blocks.md +206 -0
  79. package/templates/default/content/posts/blog_docs_zh/02.guide/03-mermaid.md +194 -0
  80. package/templates/default/content/posts/blog_docs_zh/02.guide/04-icons.md +229 -0
  81. package/templates/default/content/posts/blog_docs_zh/02.guide/06-latex.md +233 -0
  82. package/templates/default/content/posts/blog_docs_zh/02.guide/07-video.md +184 -0
  83. package/templates/default/content/posts/blog_docs_zh/02.guide/08-slides.md +359 -0
  84. package/templates/default/content/posts/blog_docs_zh/02.guide/README.md +213 -0
  85. package/templates/default/content/posts/blog_docs_zh/03.config/01-site.md +208 -0
  86. package/templates/default/content/posts/blog_docs_zh/03.config/02-sidebar.md +240 -0
  87. package/templates/default/content/posts/blog_docs_zh/03.config/03-i18n.md +348 -0
  88. package/templates/default/content/posts/blog_docs_zh/03.config/README.md +85 -0
  89. package/templates/default/content/posts/blog_docs_zh/README.md +78 -0
  90. package/templates/default/package.dev.json +31 -0
  91. package/templates/default/package.json +1 -1
  92. package/templates/default/src/config/locales/en/index.ts +5 -1
  93. package/templates/default/src/config/locales/en/menu.ts +3 -1
  94. package/templates/default/src/config/locales/en/sidebar.ts +18 -2
  95. package/templates/default/src/config/locales/en/site.ts +1 -1
  96. package/templates/default/src/config/locales/en/ui.ts +29 -0
  97. package/templates/default/src/config/locales/zh-CN/index.ts +5 -1
  98. package/templates/default/src/config/locales/zh-CN/menu.ts +7 -5
  99. package/templates/default/src/config/locales/zh-CN/sidebar.ts +22 -6
  100. package/templates/default/src/config/locales/zh-CN/site.ts +2 -2
  101. package/templates/default/src/config/locales/zh-CN/ui.ts +29 -0
  102. package/templates/default/src/config/site.ts +2 -2
  103. package/templates/default/src/content.config.ts +15 -3
  104. package/templates/default/content/posts/blog_docs/01-quick-start.md +0 -162
  105. package/templates/default/content/posts/blog_docs/02-frontmatter.md +0 -277
  106. package/templates/default/content/posts/blog_docs/03-markdown-basic.md +0 -350
  107. package/templates/default/content/posts/blog_docs/04-containers.md +0 -331
  108. package/templates/default/content/posts/blog_docs/05-code-blocks.md +0 -388
  109. package/templates/default/content/posts/blog_docs/06-mermaid.md +0 -431
  110. package/templates/default/content/posts/blog_docs/07-video.md +0 -243
  111. package/templates/default/content/posts/blog_docs/08-latex.md +0 -382
  112. package/templates/default/content/posts/blog_docs/09-icons.md +0 -326
  113. package/templates/default/content/posts/blog_docs/10-sidebar.md +0 -445
  114. package/templates/default/content/posts/blog_docs/11-config.md +0 -334
  115. package/templates/default/content/posts/blog_docs/12-i18n.md +0 -355
  116. package/templates/default/content/posts/blog_docs/12-slides.mdx +0 -552
  117. package/templates/default/content/posts/blog_docs/README.md +0 -152
  118. package/templates/default/content/posts/blog_docs_en/get-started/05-deploy.md +0 -197
  119. package/templates/default/content/posts/blog_docs_en/guide/README.md +0 -59
  120. package/templates/default/content/posts/blog_docs_en/guide/features/README.md +0 -51
  121. package/templates/default/content/posts/blog_docs_en/guide/markdown/02-containers.md +0 -226
@@ -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
- if (firstChild.type !== 'text') return;
16
+ const lastChild = node.children[node.children.length - 1];
12
17
 
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]*$/));
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
- if (hasClosingInSameParagraph) {
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
- let contentChildren = [];
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
- 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
- }
30
+ // Check if last text ends with :::
31
+ const endMatch = lastText.match(/\n(:{3,})\s*$/);
32
+ if (!endMatch) return;
47
33
 
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
- }
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
- // 移除最后一个 text 节点中的结束标记
54
- let lastTextContent = lastChild.value.replace(/\n?:::[\s]*$/, '');
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
- // 创建 HTML 开始标签
61
- const openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
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
- 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
- });
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
- newNodes.push(htmlEndNode);
80
-
81
- // 替换当前段落
82
- parent.children.splice(index, 1, ...newNodes);
83
- return index + newNodes.length;
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
- 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);
90
+ // If no content, skip
91
+ if (contentChildren.length === 0) return;
94
92
 
95
- // 内容是第一行之后的所有内容
96
- const contentText = lines.slice(1).join('\n').trim();
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
- // 创建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
- };
101
+ parent.children.splice(index, 1, htmlStartNode, contentParagraph, htmlEndNode);
102
+ return index + 3;
103
+ });
110
104
 
111
- // 替换当前段落
112
- parent.children[index] = htmlNode;
113
- return;
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
- // 检查是否是 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;
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
- if (endIndex === -1) {
136
- endIndex = siblings.length;
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
- 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++;
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
- 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
- }
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
- // 复制其他子节点(strong、emphasis 等)
214
- for (let i = 1; i < node.children.length; i++) {
215
- contentChildren.push(JSON.parse(JSON.stringify(node.children[i])));
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
- 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
- }
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
- if (contentChildren.length > 0) {
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
- if (hasClosingTag) {
245
- // 找到了结束标记,不需要继续搜索
246
- endIndex = index + 1;
247
- }
248
- }
209
+ const fullText = firstChild.value;
249
210
 
250
- // 如果还没找到结束标记,继续搜索后续节点
251
- if (endIndex === -1) {
252
- for (let i = searchStart; i < siblings.length; i++) {
253
- const sibling = siblings[i];
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
- 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;
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
- 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 {
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
- 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
- }
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[0] &&
313
+ sibling.children.length > 0 &&
351
314
  sibling.children[0].type === 'text' &&
352
- sibling.children[0].value.match(/^::: (tip|note|warning|danger|info|details)/)) {
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
- // 创建HTML容器
368
- const openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
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
- const htmlNode = {
376
- type: 'html',
377
- value: openingHTML
378
- };
382
+ const htmlStartNode = { type: 'html', value: openingHTML };
383
+ const htmlEndNode = { type: 'html', value: closingHTML };
379
384
 
380
- const closeNode = {
381
- type: 'html',
382
- value: closingHTML
383
- };
385
+ const newNodes = [htmlStartNode, ...node.children, htmlEndNode];
386
+ parent.children.splice(index, 1, ...newNodes);
384
387
 
385
- // 替换节点 - 需要考虑可能跳过的空段落
386
- const replaceCount = endIndex - index;
387
- const newNodes = [htmlNode, ...contentNodes, closeNode];
388
- siblings.splice(index, replaceCount, ...newNodes);
388
+ return index + newNodes.length;
389
+ });
389
390
 
390
- return index + newNodes.length;
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
- const titles = {
398
- tip: '💡 提示',
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
  }