@jet-w/astro-blog 0.2.2 → 0.2.4

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.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * 侧边栏配置系统
3
+ *
4
+ * 支持三种配置类型:
5
+ * 1. scan - 扫描指定文件夹,自动生成树形结构
6
+ * 2. manual - 手动配置显示特定内容
7
+ * 3. mixed - 混合使用以上两种方式
8
+ */
9
+
10
+ // 路径匹配配置
11
+ export interface PathMatchConfig {
12
+ pattern: string;
13
+ exact?: boolean;
14
+ }
15
+
16
+ // 侧边栏项目类型
17
+ export interface SidebarItem {
18
+ title: string;
19
+ slug?: string;
20
+ link?: string;
21
+ icon?: string;
22
+ badge?: string;
23
+ badgeType?: 'info' | 'success' | 'warning' | 'error';
24
+ children?: SidebarItem[];
25
+ collapsed?: boolean;
26
+ }
27
+
28
+ // 扫描配置
29
+ export interface ScanConfig {
30
+ type: 'scan';
31
+ title: string;
32
+ icon?: string;
33
+ scanPath: string;
34
+ collapsed?: boolean;
35
+ maxDepth?: number;
36
+ exclude?: string[];
37
+ include?: string[];
38
+ sortBy?: 'name' | 'date' | 'title' | 'custom';
39
+ sortOrder?: 'asc' | 'desc';
40
+ showForPaths?: string[];
41
+ hideForPaths?: string[];
42
+ }
43
+
44
+ // 手动配置
45
+ export interface ManualConfig {
46
+ type: 'manual';
47
+ title: string;
48
+ icon?: string;
49
+ collapsed?: boolean;
50
+ items: SidebarItem[];
51
+ showForPaths?: string[];
52
+ hideForPaths?: string[];
53
+ }
54
+
55
+ // 混合配置
56
+ export interface MixedConfig {
57
+ type: 'mixed';
58
+ title: string;
59
+ icon?: string;
60
+ collapsed?: boolean;
61
+ sections: (ScanConfig | ManualConfig)[];
62
+ showForPaths?: string[];
63
+ hideForPaths?: string[];
64
+ }
65
+
66
+ // 分隔符
67
+ export interface DividerConfig {
68
+ type: 'divider';
69
+ title?: string;
70
+ showForPaths?: string[];
71
+ hideForPaths?: string[];
72
+ }
73
+
74
+ // 侧边栏组配置
75
+ export type SidebarGroup = ScanConfig | ManualConfig | MixedConfig | DividerConfig;
76
+
77
+ // 完整侧边栏配置
78
+ export interface SidebarConfig {
79
+ enabled: boolean;
80
+ width?: string;
81
+ position?: 'left' | 'right';
82
+ showSearch?: boolean;
83
+ showRecentPosts?: boolean;
84
+ recentPostsCount?: number;
85
+ showPopularTags?: boolean;
86
+ popularTagsCount?: number;
87
+ showArchives?: boolean;
88
+ archivesCount?: number;
89
+ showFriendLinks?: boolean;
90
+ friendLinks?: Array<{
91
+ title: string;
92
+ url: string;
93
+ icon?: string;
94
+ description?: string;
95
+ }>;
96
+ groups: SidebarGroup[];
97
+ }
98
+
99
+ /**
100
+ * 默认侧边栏配置
101
+ */
102
+ export const sidebarConfig: SidebarConfig = {
103
+ enabled: true,
104
+ width: '280px',
105
+ position: 'right',
106
+ showSearch: true,
107
+ showRecentPosts: true,
108
+ recentPostsCount: 5,
109
+ showPopularTags: true,
110
+ popularTagsCount: 8,
111
+ showArchives: true,
112
+ archivesCount: 6,
113
+ showFriendLinks: true,
114
+ friendLinks: [
115
+ { title: 'Astro 官网', url: 'https://astro.build' },
116
+ { title: 'Tailwind CSS', url: 'https://tailwindcss.com' },
117
+ { title: 'Vue.js', url: 'https://vuejs.org' },
118
+ ],
119
+ groups: [
120
+ {
121
+ type: 'scan',
122
+ title: '文档目录',
123
+ icon: 'folder',
124
+ scanPath: '',
125
+ collapsed: false,
126
+ }
127
+ ]
128
+ };
129
+
130
+ /**
131
+ * Define sidebar configuration
132
+ */
133
+ export function defineSidebarConfig(config: Partial<SidebarConfig>): SidebarConfig {
134
+ return {
135
+ ...sidebarConfig,
136
+ ...config,
137
+ groups: config.groups || sidebarConfig.groups
138
+ };
139
+ }
140
+
141
+ // 向后兼容
142
+ export const defaultSidebarConfig = sidebarConfig;
@@ -0,0 +1,61 @@
1
+ import type { SiteConfig } from '../types';
2
+
3
+ /**
4
+ * Default site configuration
5
+ * Users should override this in their own config
6
+ */
7
+ export const siteConfig: SiteConfig = {
8
+ title: 'My Astro Blog',
9
+ description: '',
10
+ author: 'Author',
11
+ email: '',
12
+ avatar: '/images/avatar.svg',
13
+ social: {
14
+ github: '',
15
+ twitter: '',
16
+ linkedin: '',
17
+ email: ''
18
+ },
19
+ menu: [
20
+ {
21
+ name: '首页',
22
+ href: '/',
23
+ icon: 'home'
24
+ },
25
+ {
26
+ name: '博客',
27
+ href: '/posts',
28
+ icon: 'posts'
29
+ },
30
+ {
31
+ name: '关于',
32
+ href: '/about',
33
+ icon: 'about'
34
+ }
35
+ ]
36
+ };
37
+
38
+ export const defaultSEO = {
39
+ title: siteConfig.title,
40
+ description: siteConfig.description,
41
+ image: '/images/og-image.jpg',
42
+ type: 'website' as const
43
+ };
44
+
45
+ /**
46
+ * Create site config with user overrides
47
+ */
48
+ export function defineSiteConfig(config: Partial<SiteConfig>): SiteConfig {
49
+ return {
50
+ ...siteConfig,
51
+ ...config,
52
+ social: {
53
+ ...siteConfig.social,
54
+ ...config.social
55
+ },
56
+ menu: config.menu || siteConfig.menu
57
+ };
58
+ }
59
+
60
+ // 向后兼容的别名
61
+ export const defaultSiteConfig = siteConfig;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 社交链接配置
3
+ */
4
+
5
+ export interface SocialLink {
6
+ type: string;
7
+ url: string;
8
+ label?: string;
9
+ icon?: string;
10
+ }
11
+
12
+ export const defaultIcons: Record<string, string> = {
13
+ github: 'M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z',
14
+ twitter: 'M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z',
15
+ linkedin: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
16
+ email: 'M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
17
+ youtube: 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z',
18
+ discord: 'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z'
19
+ };
20
+
21
+ export const socialLinks: SocialLink[] = [];
22
+ export const defaultSocialLinks = socialLinks;
23
+
24
+ /**
25
+ * Define social links
26
+ */
27
+ export function defineSocialLinks(links: SocialLink[]): SocialLink[] {
28
+ return links;
29
+ }
@@ -2,105 +2,223 @@ import { visit } from 'unist-util-visit';
2
2
 
3
3
  export function remarkContainers() {
4
4
  return (tree, file) => {
5
- // Pre-process: Handle complete containers in a single paragraph
5
+ // Pre-process: Handle containers that span across multiple sibling nodes
6
6
  // This handles cases like:
7
- // ::: tip
8
- // Content without blank lines
7
+ // ::: tip Title
8
+ // Content text here
9
+ // - list item 1
10
+ // - list item 2
9
11
  // :::
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.)
12
- visit(tree, 'paragraph', (node, index, parent) => {
13
- if (!node.children || node.children.length === 0) return;
14
-
15
- const firstChild = node.children[0];
16
- const lastChild = node.children[node.children.length - 1];
12
+ // Where the container starts in one paragraph, has sibling nodes (lists, etc),
13
+ // and the closing ::: may be in a later text node
14
+ function processMultiNodeContainers(tree) {
15
+ visit(tree, 'paragraph', (node, index, parent) => {
16
+ if (!node.children || node.children.length === 0) return;
17
+ if (!parent || !parent.children) return;
17
18
 
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;
19
+ const firstChild = node.children[0];
20
+ if (firstChild.type !== 'text') return;
22
21
 
23
- const firstText = firstChild.value;
24
- const lastText = lastChild.value;
22
+ const firstText = firstChild.value;
23
+
24
+ // Check if first text starts with ::: type [title]
25
+ // Allow content to follow on subsequent lines within this paragraph
26
+ const startMatch = firstText.match(/^(:{3,})\s+(tip|note|warning|danger|info|details)([ \t]+[^\n]*)?\n?/);
27
+ if (!startMatch) return;
28
+
29
+ const [fullMatch, openColons, type, titlePart] = startMatch;
30
+ const colonCount = openColons.length;
31
+ const customTitle = titlePart ? titlePart.trim() : '';
32
+ const title = customTitle || getDefaultTitle(type);
33
+
34
+ // Now we need to find the closing ::: which could be:
35
+ // 1. At the end of this same paragraph's text
36
+ // 2. In a sibling paragraph
37
+ // 3. Inside a text node in a list item (no blank line before :::)
38
+
39
+ const siblings = parent.children;
40
+ let endIndex = -1;
41
+ let endNodeInfo = null; // { siblingIndex, childPath, closeMatch }
42
+
43
+ // First check if closing is in the same paragraph
44
+ const lastChild = node.children[node.children.length - 1];
45
+ if (lastChild.type === 'text') {
46
+ const closeInSame = lastChild.value.match(/\n(:{3,})\s*$/);
47
+ if (closeInSame && closeInSame[1].length === colonCount) {
48
+ // Closing is in the same paragraph - handle as single paragraph container
49
+ endNodeInfo = { type: 'same-paragraph', closeMatch: closeInSame };
50
+ }
51
+ }
25
52
 
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;
53
+ if (!endNodeInfo) {
54
+ // Search through siblings for the closing :::
55
+ for (let i = index + 1; i < siblings.length; i++) {
56
+ const sibling = siblings[i];
29
57
 
30
- // Check if last text ends with :::
31
- const endMatch = lastText.match(/\n(:{3,})\s*$/);
32
- if (!endMatch) return;
58
+ // Check if sibling is a paragraph with just :::
59
+ if (sibling.type === 'paragraph' &&
60
+ sibling.children &&
61
+ sibling.children.length === 1 &&
62
+ sibling.children[0].type === 'text') {
63
+ const text = sibling.children[0].value.trim();
64
+ const closeMatch = text.match(/^(:{3,})$/);
65
+ if (closeMatch && closeMatch[1].length === colonCount) {
66
+ endIndex = i;
67
+ endNodeInfo = { type: 'sibling-paragraph', siblingIndex: i };
68
+ break;
69
+ }
70
+ }
33
71
 
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;
72
+ // Check if closing ::: is embedded in a text node (e.g., after a list item)
73
+ // This happens when there's no blank line before :::
74
+ if (sibling.type === 'list' && sibling.children) {
75
+ // Check the last item of the list
76
+ const lastItem = sibling.children[sibling.children.length - 1];
77
+ if (lastItem.children) {
78
+ const lastItemPara = lastItem.children[lastItem.children.length - 1];
79
+ if (lastItemPara.type === 'paragraph' && lastItemPara.children) {
80
+ const lastText = lastItemPara.children[lastItemPara.children.length - 1];
81
+ if (lastText.type === 'text') {
82
+ const closeMatch = lastText.value.match(/\n(:{3,})\s*$/);
83
+ if (closeMatch && closeMatch[1].length === colonCount) {
84
+ endIndex = i;
85
+ endNodeInfo = {
86
+ type: 'in-list',
87
+ siblingIndex: i,
88
+ lastText: lastText,
89
+ closeMatch: closeMatch
90
+ };
91
+ break;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
38
99
 
39
- const customTitle = titlePart ? titlePart.trim() : '';
40
- const title = customTitle || getDefaultTitle(type);
100
+ if (!endNodeInfo) return; // No closing found
41
101
 
42
- // Create HTML wrapper
43
- let openingHTML, closingHTML;
44
- if (type === 'details') {
45
- openingHTML = `<details class="container-details custom-container" data-container-type="details">
102
+ // Create HTML wrapper
103
+ let openingHTML, closingHTML;
104
+ if (type === 'details') {
105
+ openingHTML = `<details class="container-details custom-container" data-container-type="details">
46
106
  <summary class="container-title">${title}</summary>
47
107
  <div class="container-content">`;
48
- closingHTML = `</div>
108
+ closingHTML = `</div>
49
109
  </details>`;
50
- } else {
51
- openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
110
+ } else {
111
+ openingHTML = `<div class="container-${type} custom-container" data-container-type="${type}">
52
112
  <div class="container-title">${title}</div>
53
113
  <div class="container-content">`;
54
- closingHTML = `</div>
114
+ closingHTML = `</div>
55
115
  </div>`;
56
- }
116
+ }
57
117
 
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);
118
+ const htmlStartNode = { type: 'html', value: openingHTML };
119
+ const htmlEndNode = { type: 'html', value: closingHTML };
120
+
121
+ if (endNodeInfo.type === 'same-paragraph') {
122
+ // Handle single paragraph container
123
+ const lastChild = node.children[node.children.length - 1];
124
+ const newFirstText = firstText.slice(fullMatch.length);
125
+ const newLastText = lastChild.value.slice(0, endNodeInfo.closeMatch.index);
126
+
127
+ const contentChildren = [];
128
+ for (let i = 0; i < node.children.length; i++) {
129
+ const child = node.children[i];
130
+ if (i === 0) {
131
+ if (node.children.length === 1) {
132
+ // Single text node
133
+ const middleText = newFirstText.slice(0, newFirstText.length - (firstText.length - lastChild.value.length) - endNodeInfo.closeMatch[0].length);
134
+ if (middleText.trim()) {
135
+ contentChildren.push({ ...child, value: middleText.trim() });
136
+ }
137
+ } else if (newFirstText.trim()) {
138
+ contentChildren.push({ ...child, value: newFirstText });
139
+ }
140
+ } else if (i === node.children.length - 1) {
141
+ if (newLastText.trim()) {
142
+ contentChildren.push({ ...child, value: newLastText });
143
+ }
144
+ } else {
145
+ contentChildren.push({ ...child });
146
+ }
147
+ }
63
148
 
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() });
149
+ if (contentChildren.length === 0) return;
150
+
151
+ const contentParagraph = { type: 'paragraph', children: contentChildren };
152
+ parent.children.splice(index, 1, htmlStartNode, contentParagraph, htmlEndNode);
153
+ return index + 3;
154
+
155
+ } else if (endNodeInfo.type === 'sibling-paragraph') {
156
+ // Closing is in a sibling paragraph
157
+ // Extract content from opening paragraph (after the ::: line)
158
+ const newFirstText = firstText.slice(fullMatch.length);
159
+ const contentNodes = [];
160
+
161
+ // Add remaining content from opening paragraph if any
162
+ if (newFirstText.trim() || node.children.length > 1) {
163
+ const newParaChildren = [];
164
+ if (newFirstText.trim()) {
165
+ newParaChildren.push({ ...firstChild, value: newFirstText });
166
+ }
167
+ for (let i = 1; i < node.children.length; i++) {
168
+ newParaChildren.push({ ...node.children[i] });
169
+ }
170
+ if (newParaChildren.length > 0) {
171
+ contentNodes.push({ type: 'paragraph', children: newParaChildren });
75
172
  }
76
- } else if (newFirstText.trim()) {
77
- contentChildren.push({ ...child, value: newFirstText });
78
173
  }
79
- } else if (i === node.children.length - 1) {
80
- // Last child - use trimmed text
81
- if (newLastText.trim()) {
82
- contentChildren.push({ ...child, value: newLastText });
174
+
175
+ // Add all siblings between opening and closing
176
+ for (let i = index + 1; i < endIndex; i++) {
177
+ contentNodes.push(siblings[i]);
83
178
  }
84
- } else {
85
- // Middle children - keep as-is
86
- contentChildren.push({ ...child });
87
- }
88
- }
89
179
 
90
- // If no content, skip
91
- if (contentChildren.length === 0) return;
180
+ const replaceCount = endIndex - index + 1;
181
+ const newNodes = [htmlStartNode, ...contentNodes, htmlEndNode];
182
+ parent.children.splice(index, replaceCount, ...newNodes);
183
+ return index + newNodes.length;
92
184
 
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 };
185
+ } else if (endNodeInfo.type === 'in-list') {
186
+ // Closing ::: is inside the last list item
187
+ // Remove the closing from the text node
188
+ endNodeInfo.lastText.value = endNodeInfo.lastText.value.slice(0, endNodeInfo.closeMatch.index);
100
189
 
101
- parent.children.splice(index, 1, htmlStartNode, contentParagraph, htmlEndNode);
102
- return index + 3;
103
- });
190
+ // Extract content from opening paragraph
191
+ const newFirstText = firstText.slice(fullMatch.length);
192
+ const contentNodes = [];
193
+
194
+ if (newFirstText.trim() || node.children.length > 1) {
195
+ const newParaChildren = [];
196
+ if (newFirstText.trim()) {
197
+ newParaChildren.push({ ...firstChild, value: newFirstText });
198
+ }
199
+ for (let i = 1; i < node.children.length; i++) {
200
+ newParaChildren.push({ ...node.children[i] });
201
+ }
202
+ if (newParaChildren.length > 0) {
203
+ contentNodes.push({ type: 'paragraph', children: newParaChildren });
204
+ }
205
+ }
206
+
207
+ // Add all siblings including the list (which now has the ::: removed)
208
+ for (let i = index + 1; i <= endIndex; i++) {
209
+ contentNodes.push(siblings[i]);
210
+ }
211
+
212
+ const replaceCount = endIndex - index + 1;
213
+ const newNodes = [htmlStartNode, ...contentNodes, htmlEndNode];
214
+ parent.children.splice(index, replaceCount, ...newNodes);
215
+ return index + newNodes.length;
216
+ }
217
+ });
218
+ }
219
+
220
+ // Run the multi-node container processor
221
+ processMultiNodeContainers(tree);
104
222
 
105
223
  // Pre-process: Extract ::: from text nodes where it appears at the end
106
224
  // This handles cases where ::: is on a new line but without a blank line separator
@@ -355,11 +473,35 @@ export function remarkContainers() {
355
473
  console.log('DEBUG containerDirective:', type, 'children:', node.children?.length || 0, JSON.stringify(node.children?.map(c => c.type)));
356
474
  }
357
475
 
358
- // Get custom title from directive label (text after ::: type on same line)
476
+ // Get custom title from directive label
477
+ // remark-directive v4 may store label in different places:
478
+ // 1. node.data.directiveLabel (standard location)
479
+ // 2. node.children[0] as a paragraph with the label text
359
480
  let customTitle = '';
481
+ let contentChildren = node.children || [];
482
+
483
+ // Check node.data.directiveLabel first (standard remark-directive behavior)
360
484
  if (node.data && node.data.directiveLabel) {
361
485
  customTitle = node.data.directiveLabel;
362
486
  }
487
+ // Check if first child is a paragraph that contains just the title text
488
+ // This happens when label is on same line: ::: warning Title Here
489
+ else if (contentChildren.length > 0) {
490
+ const firstChild = contentChildren[0];
491
+ // If first child is paragraph with single text node, it might be the title
492
+ if (firstChild.type === 'paragraph' &&
493
+ firstChild.children &&
494
+ firstChild.children.length === 1 &&
495
+ firstChild.children[0].type === 'text') {
496
+ const text = firstChild.children[0].value.trim();
497
+ // Check if this looks like a title (single line, no markdown formatting)
498
+ // and the directive has more children (actual content)
499
+ if (!text.includes('\n') && contentChildren.length > 1) {
500
+ customTitle = text;
501
+ contentChildren = contentChildren.slice(1); // Remove title from content
502
+ }
503
+ }
504
+ }
363
505
 
364
506
  const title = customTitle || getDefaultTitle(type);
365
507
 
@@ -382,7 +524,7 @@ export function remarkContainers() {
382
524
  const htmlStartNode = { type: 'html', value: openingHTML };
383
525
  const htmlEndNode = { type: 'html', value: closingHTML };
384
526
 
385
- const newNodes = [htmlStartNode, ...node.children, htmlEndNode];
527
+ const newNodes = [htmlStartNode, ...contentChildren, htmlEndNode];
386
528
  parent.children.splice(index, 1, ...newNodes);
387
529
 
388
530
  return index + newNodes.length;
@@ -395,10 +537,24 @@ export function remarkContainers() {
395
537
  return;
396
538
  }
397
539
 
540
+ // Get custom title from directive label
398
541
  let customTitle = '';
542
+ let contentChildren = node.children || [];
543
+
399
544
  if (node.data && node.data.directiveLabel) {
400
545
  customTitle = node.data.directiveLabel;
401
546
  }
547
+ // Check if first child is a text node that could be the title
548
+ else if (contentChildren.length > 0) {
549
+ const firstChild = contentChildren[0];
550
+ if (firstChild.type === 'text') {
551
+ const text = firstChild.value.trim();
552
+ if (!text.includes('\n') && contentChildren.length > 1) {
553
+ customTitle = text;
554
+ contentChildren = contentChildren.slice(1);
555
+ }
556
+ }
557
+ }
402
558
 
403
559
  const title = customTitle || getDefaultTitle(type);
404
560
 
@@ -421,7 +577,7 @@ export function remarkContainers() {
421
577
  const htmlStartNode = { type: 'html', value: openingHTML };
422
578
  const htmlEndNode = { type: 'html', value: closingHTML };
423
579
 
424
- const newNodes = [htmlStartNode, ...node.children, htmlEndNode];
580
+ const newNodes = [htmlStartNode, ...contentChildren, htmlEndNode];
425
581
  parent.children.splice(index, 1, ...newNodes);
426
582
 
427
583
  return index + newNodes.length;