@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.
- package/package.json +4 -1
- package/src/config/footer.ts +58 -0
- package/src/config/i18n.ts +485 -0
- package/src/config/index.ts +42 -0
- package/src/config/menu.ts +32 -0
- package/src/config/sidebar.ts +142 -0
- package/src/config/site.ts +61 -0
- package/src/config/social.ts +29 -0
- package/src/plugins/remark-containers.mjs +235 -79
- package/src/types/index.ts +80 -0
- package/src/utils/i18n.ts +418 -0
- package/src/utils/sidebar.ts +503 -0
- package/src/utils/useI18n.ts +155 -0
- package/templates/default/content/posts/blog_docs_en/01.get-started/03-create-post.md +124 -8
- package/templates/default/content/posts/blog_docs_zh/01.get-started/03-create-post.md +124 -9
- package/templates/default/package-lock.json +2 -2
|
@@ -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
|
|
5
|
+
// Pre-process: Handle containers that span across multiple sibling nodes
|
|
6
6
|
// This handles cases like:
|
|
7
|
-
// ::: tip
|
|
8
|
-
// Content
|
|
7
|
+
// ::: tip Title
|
|
8
|
+
// Content text here
|
|
9
|
+
// - list item 1
|
|
10
|
+
// - list item 2
|
|
9
11
|
// :::
|
|
10
|
-
// Where the
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
const title = customTitle || getDefaultTitle(type);
|
|
100
|
+
if (!endNodeInfo) return; // No closing found
|
|
41
101
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
108
|
+
closingHTML = `</div>
|
|
49
109
|
</details>`;
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
114
|
+
closingHTML = `</div>
|
|
55
115
|
</div>`;
|
|
56
|
-
|
|
116
|
+
}
|
|
57
117
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
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, ...
|
|
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, ...
|
|
580
|
+
const newNodes = [htmlStartNode, ...contentChildren, htmlEndNode];
|
|
425
581
|
parent.children.splice(index, 1, ...newNodes);
|
|
426
582
|
|
|
427
583
|
return index + newNodes.length;
|