vitepress-plugin-toolkit 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +391 -0
- package/README.zh-CN.md +389 -0
- package/dist/client/browser/index.d.ts +174 -10
- package/dist/client/browser/index.js +209 -0
- package/dist/client/ssr/index.d.ts +174 -10
- package/dist/client/ssr/index.js +187 -0
- package/dist/node/index.d.ts +420 -45
- package/dist/node/index.js +432 -29
- package/package.json +7 -2
package/dist/node/index.js
CHANGED
|
@@ -1,14 +1,80 @@
|
|
|
1
1
|
import container from "markdown-it-container";
|
|
2
|
-
import { camelCase, deepMerge, isBoolean, isNull, isNumber, isPrimitive, isString, isUndefined, kebabCase, objectEntries, objectKeys, omit, slash } from "@pengzhanbo/utils";
|
|
2
|
+
import { LRUCache, camelCase, deepMerge, isArray, isBoolean, isNull, isNumber, isPrimitive, isString, isUndefined, kebabCase, objectEntries, objectKeys, omit, slash, toArray, uniq } from "@pengzhanbo/utils";
|
|
3
|
+
import ansis from "ansis";
|
|
3
4
|
import process from "node:process";
|
|
5
|
+
import picomatch from "picomatch";
|
|
4
6
|
import { createHash } from "node:crypto";
|
|
5
|
-
import ansis from "ansis";
|
|
6
7
|
//#region src/shared/link.ts
|
|
8
|
+
/**
|
|
9
|
+
* Regular expression that matches external URLs.
|
|
10
|
+
*
|
|
11
|
+
* Matches URLs that start with a protocol (such as `http:` or `mailto:`) or
|
|
12
|
+
* with `//` (protocol-relative URLs).
|
|
13
|
+
*
|
|
14
|
+
* 匹配外部链接的正则表达式。
|
|
15
|
+
*
|
|
16
|
+
* 匹配以协议(如 `http:` 或 `mailto:`)或 `//`(协议相对链接)开头的 URL。
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* EXTERNAL_URL_RE.test('https://example.com') // true
|
|
20
|
+
* EXTERNAL_URL_RE.test('//cdn.example.com/lib.js') // true
|
|
21
|
+
* EXTERNAL_URL_RE.test('/about') // false
|
|
22
|
+
*/
|
|
7
23
|
const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
|
|
24
|
+
/**
|
|
25
|
+
* Checks whether the given path is an external URL.
|
|
26
|
+
*
|
|
27
|
+
* 判断给定路径是否为外部链接。
|
|
28
|
+
*
|
|
29
|
+
* @param path - The path to check / 要检查的路径
|
|
30
|
+
* @returns `true` if the path is an external URL, otherwise `false` / 若为外部链接返回 `true`,否则返回 `false`
|
|
31
|
+
* @example
|
|
32
|
+
* isExternal('https://example.com') // true
|
|
33
|
+
* isExternal('//cdn.example.com/lib.js') // true
|
|
34
|
+
* isExternal('/about') // false
|
|
35
|
+
* isExternal('mailto:foo@example.com') // true
|
|
36
|
+
*/
|
|
8
37
|
function isExternal(path) {
|
|
9
38
|
return EXTERNAL_URL_RE.test(path);
|
|
10
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Regular expression that matches the protocol scheme of a URL.
|
|
42
|
+
*
|
|
43
|
+
* Matches the leading protocol portion such as `http:`, `https:`, or
|
|
44
|
+
* `mailto:`. Does not match protocol-relative URLs (`//`).
|
|
45
|
+
*
|
|
46
|
+
* 匹配 URL 协议部分的正则表达式。
|
|
47
|
+
*
|
|
48
|
+
* 匹配前导协议部分,如 `http:`、`https:` 或 `mailto:`。
|
|
49
|
+
* 不匹配协议相对链接(`//`)。
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* URL_PROTOCOL_RE.test('https://example.com') // true
|
|
53
|
+
* URL_PROTOCOL_RE.test('mailto:foo@example.com') // true
|
|
54
|
+
* URL_PROTOCOL_RE.test('//cdn.example.com/lib.js') // false
|
|
55
|
+
*/
|
|
11
56
|
const URL_PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/;
|
|
57
|
+
/**
|
|
58
|
+
* Checks whether the given link contains a URL protocol scheme or is a
|
|
59
|
+
* protocol-relative URL.
|
|
60
|
+
*
|
|
61
|
+
* Unlike {@link isExternal}, this function also matches links that start with
|
|
62
|
+
* `//` via an additional check, in addition to those matched by
|
|
63
|
+
* {@link URL_PROTOCOL_RE}.
|
|
64
|
+
*
|
|
65
|
+
* 判断给定链接是否包含 URL 协议部分或为协议相对链接。
|
|
66
|
+
*
|
|
67
|
+
* 与 {@link isExternal} 不同,此函数除了匹配 {@link URL_PROTOCOL_RE} 之外,
|
|
68
|
+
* 还会通过额外检查匹配以 `//` 开头的链接。
|
|
69
|
+
*
|
|
70
|
+
* @param link - The link to check / 要检查的链接
|
|
71
|
+
* @returns `true` if the link has a protocol or starts with `//` / 若链接包含协议或以 `//` 开头则返回 `true`
|
|
72
|
+
* @example
|
|
73
|
+
* isLinkWithProtocol('https://example.com') // true
|
|
74
|
+
* isLinkWithProtocol('mailto:foo@example.com') // true
|
|
75
|
+
* isLinkWithProtocol('//cdn.example.com/lib.js') // true
|
|
76
|
+
* isLinkWithProtocol('/about') // false
|
|
77
|
+
*/
|
|
12
78
|
function isLinkWithProtocol(link) {
|
|
13
79
|
return URL_PROTOCOL_RE.test(link) || link.startsWith("//");
|
|
14
80
|
}
|
|
@@ -28,6 +94,13 @@ const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w-]+)(?:=(?<quote>['"])(?<valueWithQuo
|
|
|
28
94
|
* @param info - Attribute string / 属性字符串
|
|
29
95
|
* @returns Object with attrs and rawAttrs / 包含 attrs 和 rawAttrs 的对象
|
|
30
96
|
* @typeParam T - Attribute type / 属性类型
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* resolveAttrs('width="100" height="50"')
|
|
100
|
+
* // { width: '100', height: '50' }
|
|
101
|
+
* resolveAttrs('disabled title="Hello"')
|
|
102
|
+
* // { disabled: true, title: 'Hello' }
|
|
103
|
+
* ```
|
|
31
104
|
*/
|
|
32
105
|
function resolveAttrs(info) {
|
|
33
106
|
info = info.trim();
|
|
@@ -54,6 +127,11 @@ function resolveAttrs(info) {
|
|
|
54
127
|
* @param info - Info string / 信息字符串
|
|
55
128
|
* @param key - Attribute key / 属性键
|
|
56
129
|
* @returns Attribute value or undefined / 属性值或 undefined
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* resolveAttr('width="100" height="50"', 'width') // '100'
|
|
133
|
+
* resolveAttr('width="100" height="50"', 'color') // undefined
|
|
134
|
+
* ```
|
|
57
135
|
*/
|
|
58
136
|
function resolveAttr(info, key) {
|
|
59
137
|
const pattern = new RegExp(`(?:^|\\s+)${key}(?:=(?<quote>['"])(?<valueWithQuote>.+?)\\k<quote>|=(?<valueWithoutQuote>\\S+))?(?:\\s+|$)`);
|
|
@@ -72,6 +150,13 @@ function resolveAttr(info, key) {
|
|
|
72
150
|
* @param options - Optional before/after render hooks / 可选的 before/after 渲染钩子
|
|
73
151
|
* @param options.before - Callback for rendering container opening tag / 渲染容器起始标签时的回调函数
|
|
74
152
|
* @param options.after - Callback for rendering container closing tag / 渲染容器结束标签时的回调函数
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* md.use(createContainerPlugin, 'tip', {
|
|
156
|
+
* before: info => `<div class="tip">${info}`,
|
|
157
|
+
* after: () => '</div>',
|
|
158
|
+
* })
|
|
159
|
+
* ```
|
|
75
160
|
*/
|
|
76
161
|
function createContainerPlugin(md, type, { before, after } = {}) {
|
|
77
162
|
const render = (tokens, index, options, env) => {
|
|
@@ -153,6 +238,7 @@ function createContainerSyntaxPlugin(md, type, render) {
|
|
|
153
238
|
}
|
|
154
239
|
//#endregion
|
|
155
240
|
//#region src/node/markdown/embed.ts
|
|
241
|
+
const EXISTS_TYPES = /* @__PURE__ */ new WeakMap();
|
|
156
242
|
/**
|
|
157
243
|
* Create embed rule block
|
|
158
244
|
*
|
|
@@ -165,48 +251,82 @@ function createContainerSyntaxPlugin(md, type, render) {
|
|
|
165
251
|
* @param md - Markdown instance / Markdown 实例
|
|
166
252
|
* @param {EmbedRuleBlockOptions} options - Embed rule block options / 嵌入规则块选项
|
|
167
253
|
* @typeParam Meta - Metadata type / 元数据类型
|
|
254
|
+
* @example
|
|
255
|
+
* ```ts
|
|
256
|
+
* createEmbedRuleBlock(md, {
|
|
257
|
+
* type: 'video',
|
|
258
|
+
* meta: (info, source) => ({ src: source, title: info }),
|
|
259
|
+
* content: meta => `<video src="${meta.src}" title="${meta.title}"></video>`,
|
|
260
|
+
* })
|
|
261
|
+
* ```
|
|
168
262
|
*/
|
|
169
|
-
function createEmbedRuleBlock(md, { type, name = type
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
263
|
+
function createEmbedRuleBlock(md, { type, name = `embed_${type}`, meta, content }) {
|
|
264
|
+
if (!type) {
|
|
265
|
+
console.warn(`${ansis.yellow("[markdown-it]")} Embed rule block type is empty`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
let exists = EXISTS_TYPES.get(md);
|
|
269
|
+
!exists && EXISTS_TYPES.set(md, exists = /* @__PURE__ */ new Set());
|
|
270
|
+
if (exists.has(type)) {
|
|
271
|
+
console.warn(`${ansis.yellow("[markdown-it]")} Embed rule block type ${ansis.green(type)} already exists`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (md.renderer.rules[name]) {
|
|
275
|
+
console.warn(`${ansis.yellow("[markdown-it]")} Embed rule block ${type} (${name}) already exists`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
exists.add(type);
|
|
175
279
|
const MIN_LENGTH = type.length + 5;
|
|
176
280
|
const START_CODES = [
|
|
177
281
|
64,
|
|
178
282
|
91,
|
|
179
283
|
...type.split("").map((c) => c.charCodeAt(0))
|
|
180
284
|
];
|
|
181
|
-
|
|
285
|
+
const escapedType = type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
286
|
+
const syntaxPattern = new RegExp(`^@\\[${escapedType}(?:\\s+([^\\]]*))?\\]\\(([^)]*)\\)$`);
|
|
287
|
+
md.block.ruler.before("code", name, (state, startLine, endLine, silent) => {
|
|
182
288
|
const pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
183
289
|
const max = state.eMarks[startLine];
|
|
184
290
|
if (pos + MIN_LENGTH > max) return false;
|
|
185
291
|
for (let i = 0; i < START_CODES.length; i += 1) if (state.src.charCodeAt(pos + i) !== START_CODES[i]) return false;
|
|
186
|
-
const content = state.src.slice(pos, max);
|
|
292
|
+
const content = state.src.slice(pos, max).trim();
|
|
187
293
|
const match = content.match(syntaxPattern);
|
|
188
294
|
if (!match) return false;
|
|
189
295
|
/* istanbul ignore if -- @preserve */
|
|
190
296
|
if (silent) return true;
|
|
191
297
|
const token = state.push(name, "", 0);
|
|
192
|
-
|
|
298
|
+
const [, info = "", source = ""] = match;
|
|
299
|
+
token.meta = meta(info.trim(), source.trim());
|
|
193
300
|
token.content = content;
|
|
194
301
|
token.map = [startLine, startLine + 1];
|
|
195
302
|
state.line = startLine + 1;
|
|
196
303
|
return true;
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
};
|
|
304
|
+
}, { alt: [
|
|
305
|
+
"paragraph",
|
|
306
|
+
"reference",
|
|
307
|
+
"blockquote",
|
|
308
|
+
"list"
|
|
309
|
+
] });
|
|
310
|
+
md.renderer.rules[name] = (tokens, index, _, env) => content?.(tokens[index].meta, env) ?? tokens[index].content;
|
|
203
311
|
}
|
|
204
312
|
//#endregion
|
|
205
313
|
//#region src/node/utils/constants.ts
|
|
314
|
+
/**
|
|
315
|
+
* Whether the current process is running in production build mode.
|
|
316
|
+
*
|
|
317
|
+
* 当前进程是否运行在生产构建模式下。
|
|
318
|
+
*/
|
|
206
319
|
const isBuild = process.env.NODE_ENV === "production";
|
|
320
|
+
/**
|
|
321
|
+
* Whether the current process is running in development mode.
|
|
322
|
+
*
|
|
323
|
+
* 当前进程是否运行在开发模式下。
|
|
324
|
+
*/
|
|
207
325
|
const isDev = process.env.NODE_ENV === "development";
|
|
208
326
|
/**
|
|
209
|
-
*
|
|
327
|
+
* Browser-supported video file name extensions.
|
|
328
|
+
*
|
|
329
|
+
* 浏览器支持的视频文件名扩展名。
|
|
210
330
|
*/
|
|
211
331
|
const EXTENSION_VIDEOS = [
|
|
212
332
|
"mp4",
|
|
@@ -224,7 +344,9 @@ const EXTENSION_VIDEOS = [
|
|
|
224
344
|
"ogv"
|
|
225
345
|
];
|
|
226
346
|
/**
|
|
227
|
-
*
|
|
347
|
+
* Browser-supported image file name extensions.
|
|
348
|
+
*
|
|
349
|
+
* 浏览器支持的图片文件名扩展名。
|
|
228
350
|
*/
|
|
229
351
|
const EXTENSION_IMAGES = [
|
|
230
352
|
"jpg",
|
|
@@ -244,7 +366,9 @@ const EXTENSION_IMAGES = [
|
|
|
244
366
|
"xbm"
|
|
245
367
|
];
|
|
246
368
|
/**
|
|
247
|
-
*
|
|
369
|
+
* Browser-supported audio file name extensions.
|
|
370
|
+
*
|
|
371
|
+
* 浏览器支持的音频文件名扩展名。
|
|
248
372
|
*/
|
|
249
373
|
const EXTENSION_AUDIOS = [
|
|
250
374
|
"mp3",
|
|
@@ -257,12 +381,81 @@ const EXTENSION_AUDIOS = [
|
|
|
257
381
|
];
|
|
258
382
|
//#endregion
|
|
259
383
|
//#region src/node/utils/hash.ts
|
|
384
|
+
/**
|
|
385
|
+
* Generate a SHA-256 hash from the given data.
|
|
386
|
+
*
|
|
387
|
+
* 根据给定数据生成 SHA-256 哈希值。
|
|
388
|
+
*
|
|
389
|
+
* Primitive values are converted to strings directly, while objects are serialized
|
|
390
|
+
* to JSON before hashing. When `length` is provided, the hash is truncated to the
|
|
391
|
+
* specified length.
|
|
392
|
+
*
|
|
393
|
+
* 基本类型直接转为字符串,对象则序列化为 JSON 后再哈希。提供 `length` 时,
|
|
394
|
+
* 哈希值会被截断到指定长度。
|
|
395
|
+
*
|
|
396
|
+
* @param data - The data to hash, can be any value / 要哈希的数据,可以是任意值
|
|
397
|
+
* @param length - Optional length to truncate the hash / 可选的哈希值截断长度
|
|
398
|
+
* @returns The SHA-256 hash string (truncated if length is given)
|
|
399
|
+
* / SHA-256 哈希字符串(指定长度时会被截断)
|
|
400
|
+
* @example
|
|
401
|
+
* ```ts
|
|
402
|
+
* genHash('hello') // full sha256 hash
|
|
403
|
+
* genHash({ a: 1 }, 8) // 8-character hash
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
260
406
|
function genHash(data, length) {
|
|
261
407
|
const str = isPrimitive(data) ? String(data) : JSON.stringify(data);
|
|
262
408
|
const hash = createHash("sha256").update(str).digest("hex");
|
|
263
409
|
return length ? hash.slice(0, length) : hash;
|
|
264
410
|
}
|
|
265
411
|
//#endregion
|
|
412
|
+
//#region src/node/utils/createMatcher.ts
|
|
413
|
+
const cache = new LRUCache({ maxSize: 100 });
|
|
414
|
+
/**
|
|
415
|
+
* Create a matcher for the given include and exclude patterns.
|
|
416
|
+
*
|
|
417
|
+
* 创建一个用于给定 include 和 exclude 模式的匹配器。
|
|
418
|
+
*
|
|
419
|
+
* @param include - Patterns to include, can be string or array / 要包含的模式,可以是字符串或数组
|
|
420
|
+
* @param exclude - Patterns to exclude, can be string or array / 要排除的模式,可以是字符串或数组
|
|
421
|
+
* @returns Matcher instance / 匹配器实例
|
|
422
|
+
*/
|
|
423
|
+
function createMatcher(include, exclude) {
|
|
424
|
+
const key = genHash([normalize(include), normalize(exclude)]);
|
|
425
|
+
if (cache.has(key)) return cache.get(key);
|
|
426
|
+
const { pattern, ignore } = resolveMatcherPattern(include, exclude);
|
|
427
|
+
const matcher = picomatch(pattern, { ignore });
|
|
428
|
+
cache.set(key, matcher);
|
|
429
|
+
return matcher;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Resolve include and exclude patterns into pattern and ignore arrays.
|
|
433
|
+
* Converts various pattern formats into a standardized format for matching.
|
|
434
|
+
*
|
|
435
|
+
* 将 include 和 exclude 模式解析为 pattern 和 ignore 数组。
|
|
436
|
+
* 将各种模式格式转换为用于匹配的标准化格式。
|
|
437
|
+
*
|
|
438
|
+
* @param include - Patterns to include, can be string or array / 要包含的模式,可以是字符串或数组
|
|
439
|
+
* @param exclude - Patterns to exclude, can be string or array / 要排除的模式,可以是字符串或数组
|
|
440
|
+
* @returns Object containing pattern and ignore arrays / 包含 pattern 和 ignore 数组的对象
|
|
441
|
+
*/
|
|
442
|
+
function resolveMatcherPattern(include, exclude) {
|
|
443
|
+
const pattern = [];
|
|
444
|
+
const ignore = uniq(toArray(exclude));
|
|
445
|
+
toArray(include).forEach((item) => {
|
|
446
|
+
if (item.startsWith("!")) ignore.push(item.slice(1));
|
|
447
|
+
else pattern.push(item);
|
|
448
|
+
});
|
|
449
|
+
if (pattern.length === 0) pattern.push("*");
|
|
450
|
+
return {
|
|
451
|
+
pattern,
|
|
452
|
+
ignore
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function normalize(arr) {
|
|
456
|
+
return isArray(arr) ? arr.sort((a, b) => a.localeCompare(b)) : arr;
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
266
459
|
//#region src/node/utils/logger.ts
|
|
267
460
|
/**
|
|
268
461
|
* Log levels mapping
|
|
@@ -284,6 +477,13 @@ const logLevels = {
|
|
|
284
477
|
* @param prefix - Log prefix / 日志前缀
|
|
285
478
|
* @param defaultLevel - Default log level / 默认日志级别
|
|
286
479
|
* @returns Logger instance / 日志实例
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* const logger = createLogger('my-plugin', 'info')
|
|
483
|
+
* logger.info('Starting build')
|
|
484
|
+
* logger.warn('Deprecated feature used')
|
|
485
|
+
* logger.debug('Verbose details', 'debug')
|
|
486
|
+
* ```
|
|
287
487
|
*/
|
|
288
488
|
function createLogger(prefix, defaultLevel = "info") {
|
|
289
489
|
prefix = `[${prefix}]`;
|
|
@@ -326,6 +526,12 @@ function createLogger(prefix, defaultLevel = "info") {
|
|
|
326
526
|
* @param str - Size string / 尺寸字符串
|
|
327
527
|
* @param unit - Unit to append (default: 'px') / 要添加的单位(默认:'px')
|
|
328
528
|
* @returns Size string with unit / 带单位的尺寸字符串
|
|
529
|
+
* @example
|
|
530
|
+
* ```ts
|
|
531
|
+
* parseRect('100') // '100px'
|
|
532
|
+
* parseRect('50%') // '50%'
|
|
533
|
+
* parseRect('200', 'rpx') // '200rpx'
|
|
534
|
+
* ```
|
|
329
535
|
*/
|
|
330
536
|
function parseRect(str, unit = "px") {
|
|
331
537
|
if (Number.parseFloat(str) === Number(str)) return `${str}${unit}`;
|
|
@@ -337,7 +543,25 @@ const rControl = /[\u0000-\u001F]/g;
|
|
|
337
543
|
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g;
|
|
338
544
|
const rCombining = /[\u0300-\u036F]/g;
|
|
339
545
|
/**
|
|
340
|
-
* Default slugification function
|
|
546
|
+
* Default slugification function that normalizes a string into a URL-safe slug.
|
|
547
|
+
*
|
|
548
|
+
* 默认的 slug 化函数,将字符串规范化为 URL 安全的 slug。
|
|
549
|
+
*
|
|
550
|
+
* The function removes accents, control characters, and special characters,
|
|
551
|
+
* replaces separators with hyphens, and ensures the result does not start
|
|
552
|
+
* with a number.
|
|
553
|
+
*
|
|
554
|
+
* 该函数会移除重音符号、控制字符和特殊字符,用连字符替换分隔符,
|
|
555
|
+
* 并确保结果不以数字开头。
|
|
556
|
+
*
|
|
557
|
+
* @param str - The string to slugify / 要 slug 化的字符串
|
|
558
|
+
* @returns The slugified string / slug 化后的字符串
|
|
559
|
+
* @example
|
|
560
|
+
* ```ts
|
|
561
|
+
* slugify('Hello World!') // 'hello-world'
|
|
562
|
+
* slugify('你好 World') // '你好-world'
|
|
563
|
+
* slugify('1st Section') // '_1st-section'
|
|
564
|
+
* ```
|
|
341
565
|
*/
|
|
342
566
|
function slugify(str) {
|
|
343
567
|
return str.normalize("NFKD").replace(rCombining, "").replace(rControl, "").replace(rSpecial, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").replace(/^(\d)/, "_$1").toLowerCase();
|
|
@@ -354,6 +578,13 @@ function slugify(str) {
|
|
|
354
578
|
* @param forceStringify - Keys to force stringify / 强制字符串化的键
|
|
355
579
|
* @returns HTML attribute string / HTML 属性字符串
|
|
356
580
|
* @typeParam T - Attribute type / 属性类型
|
|
581
|
+
* @example
|
|
582
|
+
* ```ts
|
|
583
|
+
* stringifyAttrs({ width: 100, disabled: true })
|
|
584
|
+
* // ' :width="100" disabled'
|
|
585
|
+
* stringifyAttrs({ title: 'hello', visible: false })
|
|
586
|
+
* // ' title="hello"'
|
|
587
|
+
* ```
|
|
357
588
|
*/
|
|
358
589
|
function stringifyAttrs(attrs, withUndefinedOrNull = false, forceStringify = []) {
|
|
359
590
|
const result = objectEntries(attrs).map(([key, value]) => {
|
|
@@ -375,7 +606,41 @@ function stringifyAttrs(attrs, withUndefinedOrNull = false, forceStringify = [])
|
|
|
375
606
|
}
|
|
376
607
|
//#endregion
|
|
377
608
|
//#region src/node/utils/treat-as-html.ts
|
|
609
|
+
/**
|
|
610
|
+
* Set of file extensions that should be treated as static assets rather than HTML.
|
|
611
|
+
*
|
|
612
|
+
* 应作为静态资源而非 HTML 处理的文件扩展名集合。
|
|
613
|
+
*
|
|
614
|
+
* Populated lazily on first use, combining a built-in list of known extensions
|
|
615
|
+
* with any extra extensions provided via the `VITE_EXTRA_EXTENSIONS` environment
|
|
616
|
+
* variable.
|
|
617
|
+
*
|
|
618
|
+
* 在首次使用时延迟填充,将内置已知扩展名列表与通过 `VITE_EXTRA_EXTENSIONS`
|
|
619
|
+
* 环境变量提供的额外扩展名合并。
|
|
620
|
+
*/
|
|
378
621
|
const KNOWN_EXTENSIONS = /* @__PURE__ */ new Set();
|
|
622
|
+
/**
|
|
623
|
+
* Determine whether a file should be treated as an HTML route based on its extension.
|
|
624
|
+
*
|
|
625
|
+
* 根据文件扩展名判断该文件是否应作为 HTML 路由处理。
|
|
626
|
+
*
|
|
627
|
+
* Returns `true` when the file has no extension or its extension is not in the
|
|
628
|
+
* known extensions list (meaning it should be served as an HTML page). Returns
|
|
629
|
+
* `false` for known static asset extensions.
|
|
630
|
+
*
|
|
631
|
+
* 当文件没有扩展名或其扩展名不在已知扩展名列表中时返回 `true`(表示应作为
|
|
632
|
+
* HTML 页面提供)。对于已知的静态资源扩展名返回 `false`。
|
|
633
|
+
*
|
|
634
|
+
* @param filename - The filename to check / 要检查的文件名
|
|
635
|
+
* @returns `true` if the file should be treated as HTML, `false` otherwise
|
|
636
|
+
* / 如果文件应作为 HTML 处理则返回 `true`,否则返回 `false`
|
|
637
|
+
* @example
|
|
638
|
+
* ```ts
|
|
639
|
+
* treatAsHtml('about') // true (no extension)
|
|
640
|
+
* treatAsHtml('logo.png') // false (known image extension)
|
|
641
|
+
* treatAsHtml('custom.xyz') // true (unknown extension)
|
|
642
|
+
* ```
|
|
643
|
+
*/
|
|
379
644
|
function treatAsHtml(filename) {
|
|
380
645
|
if (KNOWN_EXTENSIONS.size === 0) {
|
|
381
646
|
const extraExts = typeof process === "object" && process.env?.VITE_EXTRA_EXTENSIONS || import.meta.env?.VITE_EXTRA_EXTENSIONS || "";
|
|
@@ -386,15 +651,54 @@ function treatAsHtml(filename) {
|
|
|
386
651
|
}
|
|
387
652
|
//#endregion
|
|
388
653
|
//#region src/node/vite-plugins/iconPlugin.ts
|
|
654
|
+
/**
|
|
655
|
+
* Vite plugin name for the icons virtual module.
|
|
656
|
+
*
|
|
657
|
+
* 图标虚拟模块的 Vite 插件名称。
|
|
658
|
+
*/
|
|
389
659
|
const name = "vitepress:tuck-icons";
|
|
660
|
+
/**
|
|
661
|
+
* Global list of resolved icons accumulated across plugin invocations.
|
|
662
|
+
*
|
|
663
|
+
* 跨插件调用累积的全局已解析图标列表。
|
|
664
|
+
*/
|
|
390
665
|
const iconList = [];
|
|
666
|
+
/**
|
|
667
|
+
* Whether the virtual module is currently being processed.
|
|
668
|
+
*
|
|
669
|
+
* 虚拟模块是否正在处理中。
|
|
670
|
+
*/
|
|
391
671
|
let isProcessing = false;
|
|
672
|
+
/**
|
|
673
|
+
* Create a Vite plugin that registers SVG icons as CSS custom properties.
|
|
674
|
+
*
|
|
675
|
+
* 创建一个 Vite 插件,将 SVG 图标注册为 CSS 自定义属性。
|
|
676
|
+
*
|
|
677
|
+
* The plugin collects icons into a global list and exposes them through a
|
|
678
|
+
* virtual module `virtual:tuck-icons.css`. Each icon is rendered as a CSS
|
|
679
|
+
* rule that sets the `--icon` custom property on `.vpi-<name>` selectors.
|
|
680
|
+
*
|
|
681
|
+
* 该插件将图标收集到全局列表中,并通过虚拟模块 `virtual:tuck-icons.css`
|
|
682
|
+
* 暴露它们。每个图标被渲染为一条 CSS 规则,在 `.vpi-<name>` 选择器上
|
|
683
|
+
* 设置 `--icon` 自定义属性。
|
|
684
|
+
*
|
|
685
|
+
* @param icons - Array of icon definitions to register / 要注册的图标定义数组
|
|
686
|
+
* @returns A Vite plugin instance / Vite 插件实例
|
|
687
|
+
* @example
|
|
688
|
+
* ```ts
|
|
689
|
+
* import { iconPlugin } from 'vitepress-plugin-toolkit/node'
|
|
690
|
+
* icons: iconPlugin([
|
|
691
|
+
* { name: 'github', svg: '<svg>...</svg>' },
|
|
692
|
+
* { name: 'twitter', svg: '<svg>...</svg>' },
|
|
693
|
+
* ])
|
|
694
|
+
* ```
|
|
695
|
+
*/
|
|
392
696
|
function iconPlugin(icons) {
|
|
393
697
|
for (const icon of icons) {
|
|
394
698
|
const index = iconList.findIndex((item) => item.name === icon.name);
|
|
395
699
|
if (index === -1) iconList.push({
|
|
396
700
|
...omit(icon, ["classname"]),
|
|
397
|
-
classname: new Set([icon.classname || icon.name])
|
|
701
|
+
classname: /* @__PURE__ */ new Set([icon.classname || icon.name])
|
|
398
702
|
});
|
|
399
703
|
else icon.classname && iconList[index].classname.add(icon.classname);
|
|
400
704
|
}
|
|
@@ -414,8 +718,8 @@ function iconPlugin(icons) {
|
|
|
414
718
|
if (!isProcessing) return null;
|
|
415
719
|
let css = "";
|
|
416
720
|
for (const icon of iconList) {
|
|
417
|
-
const classname = Array.from(icon.classname).map((name) => `.vpi-${name}`).join("
|
|
418
|
-
css += `${classname} {
|
|
721
|
+
const classname = Array.from(icon.classname).map((name) => `.vpi-${name}`).join(",");
|
|
722
|
+
css += `${classname} { --icon: ${icon.svg}; }\n`;
|
|
419
723
|
}
|
|
420
724
|
isProcessing = false;
|
|
421
725
|
return css;
|
|
@@ -424,6 +728,26 @@ function iconPlugin(icons) {
|
|
|
424
728
|
}
|
|
425
729
|
//#endregion
|
|
426
730
|
//#region src/node/vitepress/get-vitepress-config.ts
|
|
731
|
+
/**
|
|
732
|
+
* Get the current VitePress site configuration from the global context.
|
|
733
|
+
*
|
|
734
|
+
* 从全局上下文获取当前的 VitePress 站点配置。
|
|
735
|
+
*
|
|
736
|
+
* VitePress stores its resolved `SiteConfig` on `globalThis.VITEPRESS_CONFIG`
|
|
737
|
+
* during the build and dev lifecycle. This helper reads that value and throws
|
|
738
|
+
* when it has not been initialized yet.
|
|
739
|
+
*
|
|
740
|
+
* VitePress 在构建和开发生命周期中将解析后的 `SiteConfig` 存储在
|
|
741
|
+
* `globalThis.VITEPRESS_CONFIG` 上。该辅助函数读取该值,未初始化时抛出错误。
|
|
742
|
+
*
|
|
743
|
+
* @returns The current VitePress site config / 当前的 VitePress 站点配置
|
|
744
|
+
* @throws {Error} When VITEPRESS_CONFIG is not initialized / VITEPRESS_CONFIG 未初始化时
|
|
745
|
+
* @example
|
|
746
|
+
* ```ts
|
|
747
|
+
* const config = getVitepressConfig()
|
|
748
|
+
* console.log(config.userConfig.title)
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
427
751
|
function getVitepressConfig() {
|
|
428
752
|
const config = globalThis.VITEPRESS_CONFIG;
|
|
429
753
|
if (!config) throw new Error("VITEPRESS_CONFIG is not initialized");
|
|
@@ -432,10 +756,30 @@ function getVitepressConfig() {
|
|
|
432
756
|
//#endregion
|
|
433
757
|
//#region src/node/vitepress/createLocales.ts
|
|
434
758
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
759
|
+
* Create a locales configuration by merging builtin locales with user locales.
|
|
760
|
+
*
|
|
761
|
+
* 通过合并内置语言环境与用户语言环境来创建语言环境配置。
|
|
762
|
+
*
|
|
763
|
+
* The function reads the VitePress locales config, matches each locale key
|
|
764
|
+
* (or its `lang` field) against the builtin locales, and falls back to the
|
|
765
|
+
* first builtin locale for the `root` entry when no match is found. User
|
|
766
|
+
* locales are deep-merged on top of the result.
|
|
767
|
+
*
|
|
768
|
+
* 该函数读取 VitePress 的语言环境配置,将每个语言环境键(或其 `lang` 字段)
|
|
769
|
+
* 与内置语言环境匹配,未匹配到时 `root` 条目回退到第一个内置语言环境。
|
|
770
|
+
* 用户语言环境会深度合并到结果之上。
|
|
771
|
+
*
|
|
772
|
+
* @param builtinLocales - Builtin locale entries / 内置语言环境条目
|
|
773
|
+
* @param userLocales - User-provided locale overrides / 用户提供的语言环境覆盖
|
|
774
|
+
* @returns Merged locales configuration / 合并后的语言环境配置
|
|
775
|
+
* @typeParam LocaleData - Locale data type / 语言环境数据类型
|
|
776
|
+
* @example
|
|
777
|
+
* ```ts
|
|
778
|
+
* const locales = createLocales(
|
|
779
|
+
* [[['en', 'en-US'], { title: 'English' }], [['zh', 'zh-CN'], { title: '中文' }]],
|
|
780
|
+
* { zh: { title: '我的站点' } },
|
|
781
|
+
* )
|
|
782
|
+
* ```
|
|
439
783
|
*/
|
|
440
784
|
function createLocales(builtinLocales, userLocales = {}) {
|
|
441
785
|
const locales = {};
|
|
@@ -450,6 +794,27 @@ function createLocales(builtinLocales, userLocales = {}) {
|
|
|
450
794
|
}
|
|
451
795
|
//#endregion
|
|
452
796
|
//#region src/node/vitepress/get-locale-with-path.ts
|
|
797
|
+
/**
|
|
798
|
+
* Resolve the locale and language code for a given path.
|
|
799
|
+
*
|
|
800
|
+
* 解析给定路径对应的语言环境和语言代码。
|
|
801
|
+
*
|
|
802
|
+
* The function inspects the VitePress locales config and finds the locale key
|
|
803
|
+
* that the provided path starts with. When the matched key is `root`, the
|
|
804
|
+
* returned locale is an empty string. Returns empty strings for both fields
|
|
805
|
+
* when no locale matches.
|
|
806
|
+
*
|
|
807
|
+
* 该函数检查 VitePress 的语言环境配置,找到所提供路径以其开头的语言环境键。
|
|
808
|
+
* 当匹配的键为 `root` 时,返回的语言环境为空字符串。无匹配时两个字段均返回空字符串。
|
|
809
|
+
*
|
|
810
|
+
* @param path - The path to resolve / 要解析的路径
|
|
811
|
+
* @returns An object with `lang` and `locale` fields / 包含 `lang` 和 `locale` 字段的对象
|
|
812
|
+
* @example
|
|
813
|
+
* ```ts
|
|
814
|
+
* getLocaleWithPath('/zh/guide/') // { lang: 'zh-CN', locale: '/zh/' }
|
|
815
|
+
* getLocaleWithPath('/guide/') // { lang: 'en', locale: '' } (root locale)
|
|
816
|
+
* ```
|
|
817
|
+
*/
|
|
453
818
|
function getLocaleWithPath(path) {
|
|
454
819
|
const locales = getVitepressConfig().userConfig?.locales || {};
|
|
455
820
|
const keys = objectKeys(locales);
|
|
@@ -465,7 +830,37 @@ function getLocaleWithPath(path) {
|
|
|
465
830
|
}
|
|
466
831
|
//#endregion
|
|
467
832
|
//#region src/node/vitepress/resolve-route-link.ts
|
|
833
|
+
/**
|
|
834
|
+
* Regular expression matching `index.md` paths with an optional hash.
|
|
835
|
+
*
|
|
836
|
+
* 匹配带可选哈希的 `index.md` 路径的正则表达式。
|
|
837
|
+
*/
|
|
468
838
|
const indexRE = /(^|.*\/)index.md(.*)$/i;
|
|
839
|
+
/**
|
|
840
|
+
* Resolve a markdown link to its final URL form based on VitePress routing rules.
|
|
841
|
+
*
|
|
842
|
+
* 根据 VitePress 路由规则将 markdown 链接解析为最终 URL 形式。
|
|
843
|
+
*
|
|
844
|
+
* External links are returned as-is. Absolute paths are prefixed with the
|
|
845
|
+
* site base. Hash links are slugified. Relative links to markdown files are
|
|
846
|
+
* transformed according to the `cleanUrls` setting (`.md` becomes `''` or
|
|
847
|
+
* `.html`), and `index.md` is collapsed to its directory.
|
|
848
|
+
*
|
|
849
|
+
* 外部链接原样返回。绝对路径添加站点 base 前缀。哈希链接进行 slug 化。
|
|
850
|
+
* 指向 markdown 文件的相对链接根据 `cleanUrls` 设置转换(`.md` 变为 `''`
|
|
851
|
+
* 或 `.html`),`index.md` 折叠为其所在目录。
|
|
852
|
+
*
|
|
853
|
+
* @param url - The raw URL from markdown content / markdown 内容中的原始 URL
|
|
854
|
+
* @param env - The markdown environment with routing context / 带路由上下文的 markdown 环境
|
|
855
|
+
* @returns The resolved URL / 解析后的 URL
|
|
856
|
+
* @example
|
|
857
|
+
* ```ts
|
|
858
|
+
* resolveRouteLink('./guide.md', { cleanUrls: false } as MarkdownEnv)
|
|
859
|
+
* // './guide.html'
|
|
860
|
+
* resolveRouteLink('/about/', {} as MarkdownEnv)
|
|
861
|
+
* // '/base/about/'
|
|
862
|
+
* ```
|
|
863
|
+
*/
|
|
469
864
|
function resolveRouteLink(url, env) {
|
|
470
865
|
if (isExternal(url)) return url;
|
|
471
866
|
const config = getVitepressConfig();
|
|
@@ -488,8 +883,16 @@ function resolveRouteLink(url, env) {
|
|
|
488
883
|
}
|
|
489
884
|
return url;
|
|
490
885
|
}
|
|
886
|
+
/**
|
|
887
|
+
* Normalize a hash fragment by slugifying its content.
|
|
888
|
+
*
|
|
889
|
+
* 通过对内容进行 slug 化来规范化哈希片段。
|
|
890
|
+
*
|
|
891
|
+
* @param str - The hash string (with leading `#`) or empty / 哈希字符串(带前导 `#`)或空字符串
|
|
892
|
+
* @returns The encoded hash string or empty / 编码后的哈希字符串或空字符串
|
|
893
|
+
*/
|
|
491
894
|
function normalizeHash(str) {
|
|
492
895
|
return str ? encodeURI(`#${slugify(decodeURI(str).slice(1))}`) : "";
|
|
493
896
|
}
|
|
494
897
|
//#endregion
|
|
495
|
-
export { EXTENSION_AUDIOS, EXTENSION_IMAGES, EXTENSION_VIDEOS, EXTERNAL_URL_RE, URL_PROTOCOL_RE, createContainerPlugin, createContainerSyntaxPlugin, createEmbedRuleBlock, createLocales, createLogger, genHash, getLocaleWithPath, getVitepressConfig, iconPlugin, isBuild, isDev, isExternal, isLinkWithProtocol, logLevels, parseRect, resolveAttr, resolveAttrs, resolveRouteLink, slugify, stringifyAttrs, treatAsHtml };
|
|
898
|
+
export { EXTENSION_AUDIOS, EXTENSION_IMAGES, EXTENSION_VIDEOS, EXTERNAL_URL_RE, URL_PROTOCOL_RE, createContainerPlugin, createContainerSyntaxPlugin, createEmbedRuleBlock, createLocales, createLogger, createMatcher, genHash, getLocaleWithPath, getVitepressConfig, iconPlugin, isBuild, isDev, isExternal, isLinkWithProtocol, logLevels, parseRect, resolveAttr, resolveAttrs, resolveMatcherPattern, resolveRouteLink, slugify, stringifyAttrs, treatAsHtml };
|
package/package.json
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitepress-plugin-toolkit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Development toolkit for vitepress plugins",
|
|
6
6
|
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
|
|
7
7
|
"license": "MIT",
|
|
8
|
+
"homepage": "https://tuck.pengzhanbo.cn/guide/toolkit",
|
|
8
9
|
"repository": {
|
|
9
10
|
"type": "git",
|
|
10
11
|
"url": "git+https://github.com/pengzhanbo/vitepress-tuck.git",
|
|
11
12
|
"directory": "packages/plugin-toolkit"
|
|
12
13
|
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/pengzhanbo/vitepress-tuck/issues"
|
|
16
|
+
},
|
|
13
17
|
"keywords": [
|
|
14
18
|
"vitepress",
|
|
15
19
|
"vitepress-plugin",
|
|
@@ -40,7 +44,8 @@
|
|
|
40
44
|
"@pengzhanbo/utils": "^3.7.3",
|
|
41
45
|
"@vueuse/core": "^14.3.0",
|
|
42
46
|
"ansis": "^4.3.1",
|
|
43
|
-
"markdown-it-container": "^4.0.0"
|
|
47
|
+
"markdown-it-container": "^4.0.0",
|
|
48
|
+
"picomatch": "^4.0.4"
|
|
44
49
|
},
|
|
45
50
|
"publishConfig": {
|
|
46
51
|
"access": "public",
|