@ansidev-oss/vitepress-theme-ansidev 1.0.12 → 1.2.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/dist/client/Donation-BBPb8v5p.js +66 -0
- package/dist/client/GoogleAnalytics-34XEM7eO.js +25 -0
- package/dist/client/Sharing-CpagX9oy.js +185 -0
- package/dist/client/Swetrix-GKEyGNrV.js +647 -0
- package/dist/client/components/PostDate.test.d.ts +1 -0
- package/dist/client/composables/index.d.ts +1 -0
- package/dist/client/composables/lang.d.ts +27 -0
- package/dist/client/composables/lang.test.d.ts +1 -0
- package/dist/client/index.js +3769 -9
- package/dist/client/medium-zoom-DPl0T7qn.js +266 -0
- package/dist/client/record-Cgv_fXDH.js +2739 -0
- package/dist/client/styles.css +2 -1
- package/dist/client/types/index.d.ts +8 -0
- package/dist/node/composables.d.mts +16 -17
- package/dist/node/composables.mjs +39 -15
- package/dist/node/config.d.mts +2 -17
- package/dist/node/config.mjs +9 -6
- package/package.json +27 -21
- package/src/client/components/DonationWidget.vue +9 -3
- package/src/client/components/Footer.vue +11 -4
- package/src/client/components/Link.vue +11 -5
- package/src/client/components/LocaleSwitcher.vue +5 -5
- package/src/client/components/PostDate.test.ts +82 -0
- package/src/client/components/PostDate.vue +32 -13
- package/src/client/components/PostItem.vue +17 -7
- package/src/client/components/PostList.vue +7 -4
- package/src/client/components/ProjectCard.vue +42 -14
- package/src/client/components/ProjectList.vue +12 -5
- package/src/client/components/RouterLink.vue +3 -3
- package/src/client/components/SharingWidget.vue +3 -1
- package/src/client/components/StatusBadge.vue +10 -10
- package/src/client/components/TermBadge.vue +11 -7
- package/src/client/components/TermLink.vue +4 -4
- package/src/client/composables/date.ts +1 -1
- package/src/client/composables/index.ts +1 -0
- package/src/client/composables/lang.test.ts +56 -0
- package/src/client/composables/lang.ts +65 -0
- package/src/client/composables/locale.ts +4 -1
- package/src/client/composables/project.ts +3 -3
- package/src/client/composables/rank.ts +5 -2
- package/src/client/index.ts +5 -0
- package/src/client/layouts/Layout.vue +2 -2
- package/src/client/pages/ArchivesPage.vue +10 -5
- package/src/client/pages/CategoriesPage.vue +13 -5
- package/src/client/pages/HomePage.vue +3 -3
- package/src/client/pages/Page.vue +13 -11
- package/src/client/pages/PostsPage.vue +4 -4
- package/src/client/pages/ProjectsPage.vue +10 -7
- package/src/client/pages/TagsPage.vue +10 -4
- package/src/client/plugins/donation/components/Donation.vue +5 -3
- package/src/client/plugins/donation/components/DonationButton.vue +3 -1
- package/src/client/plugins/donation/index.ts +11 -8
- package/src/client/plugins/i18n/index.ts +2 -2
- package/src/client/plugins/sharing/components/Sharing.vue +39 -15
- package/src/client/plugins/sharing/components/SharingButton.vue +34 -14
- package/src/client/plugins/sharing/index.ts +6 -2
- package/src/client/styles/base.css +30 -0
- package/src/client/styles/blog.css +376 -0
- package/src/client/styles/index.css +2 -30
- package/src/client/types/index.ts +8 -0
- package/src/node/composables/excerpt.test.ts +88 -0
- package/src/node/composables/excerpt.ts +57 -0
- package/src/node/composables/index.ts +1 -0
- package/src/node/composables/markdown.ts +8 -2
- package/src/node/composables/route.ts +15 -7
- package/src/node/composables/slug.ts +12 -4
- package/src/node/config.ts +20 -14
- package/src/shared/composables/useSlug.ts +2 -1
- package/src/vite-env.d.ts +1 -0
- package/dist/client/Donation-D9Xn0MPt.js +0 -76
- package/dist/client/GoogleAnalytics-Dp3EGn1x.js +0 -29
- package/dist/client/Sharing-ZrU7YF-x.js +0 -190
- package/dist/client/Swetrix-CM1mbD6X.js +0 -450
- package/dist/client/VPAlgoliaSearchBox-C18GEhLm.js +0 -126
- package/dist/client/VPCarbonAds-DwoamMao.js +0 -31
- package/dist/client/VPLocalSearchBox-DyteUinQ.js +0 -4086
- package/dist/client/index-BtT3qA6T.js +0 -12663
- package/dist/client/index-CCR5sUM8.js +0 -10688
- package/dist/client/index-CkmxezC0.js +0 -7616
- package/dist/client/index-DuYWs0vj.js +0 -265
- package/dist/node/composables/index.d.ts +0 -3
- package/dist/node/composables/markdown.d.ts +0 -2
- package/dist/node/composables/route.d.ts +0 -15
- package/dist/node/composables/slug.d.ts +0 -7
- package/dist/node/composables/types.d.ts +0 -7
- package/dist/node/config.d.ts +0 -45
|
@@ -34,6 +34,10 @@ export interface Post {
|
|
|
34
34
|
twitter: string;
|
|
35
35
|
url?: string;
|
|
36
36
|
date: Date;
|
|
37
|
+
/**
|
|
38
|
+
* Excerpt rendered as HTML and normalized by the theme's `useExcerptHtml` helper.
|
|
39
|
+
* Undefined when the post contains no excerpt separator.
|
|
40
|
+
*/
|
|
37
41
|
excerpt: string | undefined;
|
|
38
42
|
categories: string[];
|
|
39
43
|
tags: string[];
|
|
@@ -49,6 +53,10 @@ export interface Project {
|
|
|
49
53
|
url: string;
|
|
50
54
|
showcaseUrl: string;
|
|
51
55
|
repositoryUrl: string;
|
|
56
|
+
/**
|
|
57
|
+
* Excerpt rendered as HTML and normalized by the theme's `useExcerptHtml` helper.
|
|
58
|
+
* Undefined when the project contains no excerpt separator.
|
|
59
|
+
*/
|
|
52
60
|
excerpt: string | undefined;
|
|
53
61
|
license: string;
|
|
54
62
|
licenseUrl: string;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a post/project excerpt into safe HTML suitable for v-html.
|
|
3
|
+
*
|
|
4
|
+
* Excerpts may originate raw (gray-matter) or pre-rendered (createContentLoader).
|
|
5
|
+
* Raw input is rendered with markdown-it with html disabled, so inline markdown
|
|
6
|
+
* (links, emphasis, inline code) works and source HTML is escaped (no injection).
|
|
7
|
+
* Excess `undefined`/empty input yields undefined.
|
|
8
|
+
*/
|
|
9
|
+
export declare const useExcerptHtml: (excerpt?: string) => string | undefined;
|
|
2
10
|
type Pattern = string | string[];
|
|
3
11
|
type Frontmatter = Record<string, unknown> & {
|
|
4
12
|
date?: string;
|
|
@@ -6,27 +14,18 @@ type Frontmatter = Record<string, unknown> & {
|
|
|
6
14
|
};
|
|
7
15
|
type FrontmatterToString = (frontmatter: Frontmatter) => string;
|
|
8
16
|
type StringOrFrontmatterToString = string | FrontmatterToString;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
declare const useMarkdownFrontmatter: (pattern: Pattern, excerptSeparator?: string) => Array<Frontmatter>;
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region src/node/composables/route.d.ts
|
|
14
|
-
declare const useMarkdownFrontmatterRoute: (pattern: Pattern, frontmatterKey: string, excerptSeparator?: string) => {
|
|
17
|
+
export declare const useMarkdownFrontmatter: (pattern: Pattern, excerptSeparator?: string) => Array<Frontmatter>;
|
|
18
|
+
export declare const useMarkdownFrontmatterRoute: (pattern: Pattern, frontmatterKey: string, excerptSeparator?: string) => {
|
|
15
19
|
paths(): any;
|
|
16
20
|
};
|
|
17
|
-
declare const useArchiveRoute: (pattern: Pattern, excerptSeparator?: string) => {
|
|
21
|
+
export declare const useArchiveRoute: (pattern: Pattern, excerptSeparator?: string) => {
|
|
18
22
|
paths(): any;
|
|
19
23
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
declare const
|
|
23
|
-
declare const useSlugFilter: (slug: string) => (str: string) => boolean;
|
|
24
|
-
//#endregion
|
|
25
|
-
//#region src/node/composables/slug.d.ts
|
|
26
|
-
declare const useSlugFromMarkdownFrontMatter: (pattern: Pattern, frontmatterKeyOrMappingFn: StringOrFrontmatterToString, excerptSeparator?: string) => {
|
|
24
|
+
export declare const useSlug: (str: string) => string;
|
|
25
|
+
export declare const useSlugFilter: (slug: string) => (str: string) => boolean;
|
|
26
|
+
export declare const useSlugFromMarkdownFrontMatter: (pattern: Pattern, frontmatterKeyOrMappingFn: StringOrFrontmatterToString, excerptSeparator?: string) => {
|
|
27
27
|
params: {
|
|
28
28
|
slug: any;
|
|
29
29
|
};
|
|
30
30
|
}[];
|
|
31
|
-
|
|
32
|
-
export { useArchiveRoute, useMarkdownFrontmatter, useMarkdownFrontmatterRoute, useSlug, useSlugFilter, useSlugFromMarkdownFrontMatter };
|
|
31
|
+
export {};
|
|
@@ -1,12 +1,44 @@
|
|
|
1
|
+
import MarkdownItLib from "markdown-it";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import { globSync } from "glob";
|
|
4
5
|
import matter from "gray-matter";
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
// src/node/composables/excerpt.ts
|
|
7
|
+
const markdown = new MarkdownItLib({
|
|
8
|
+
html: false,
|
|
9
|
+
linkify: true,
|
|
10
|
+
breaks: false
|
|
11
|
+
});
|
|
12
|
+
const BLOCK_HTML_RE = /^<(?:p|div|ul|ol|dl|h[1-6]|blockquote|pre|table)(?:\s[^>]*)?>/;
|
|
13
|
+
const DANGEROUS_ELEMENT_RE = /<(?:script|style|iframe|object|embed)\b[^>]*>[\s\S]*?<\/(?:script|style|iframe|object|embed)>|<(?:script|style|iframe|object|embed)\b[^>]*\/?>(?:<\/(?:script|style|iframe|object|embed)>)?/gi;
|
|
14
|
+
const EVENT_ATTR_RE = /\s+on[a-z]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?/gi;
|
|
15
|
+
const JS_URL_RE = /\s(?:href|src)\s*=\s*("|')\s*javascript:[^"']*\1/gi;
|
|
16
|
+
const sanitizeHtml = (html) => html.replace(DANGEROUS_ELEMENT_RE, "").replace(EVENT_ATTR_RE, "").replace(JS_URL_RE, "");
|
|
17
|
+
const stripSingleParagraphWrapper = (html) => {
|
|
18
|
+
if (!html.startsWith("<p>") || !html.endsWith("</p>")) return html;
|
|
19
|
+
const inner = html.slice(3, -4);
|
|
20
|
+
if (inner.includes("</p>")) return html;
|
|
21
|
+
return inner;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Normalizes a post/project excerpt into safe HTML suitable for v-html.
|
|
25
|
+
*
|
|
26
|
+
* Excerpts may originate raw (gray-matter) or pre-rendered (createContentLoader).
|
|
27
|
+
* Raw input is rendered with markdown-it with html disabled, so inline markdown
|
|
28
|
+
* (links, emphasis, inline code) works and source HTML is escaped (no injection).
|
|
29
|
+
* Excess `undefined`/empty input yields undefined.
|
|
30
|
+
*/
|
|
31
|
+
const useExcerptHtml = (excerpt) => {
|
|
32
|
+
const trimmed = excerpt?.trim();
|
|
33
|
+
if (!trimmed) return void 0;
|
|
34
|
+
const html = BLOCK_HTML_RE.test(trimmed) ? sanitizeHtml(trimmed) : markdown.render(trimmed).trim();
|
|
35
|
+
return stripSingleParagraphWrapper(html).trim() || void 0;
|
|
36
|
+
};
|
|
37
|
+
// src/node/composables/markdown.ts
|
|
7
38
|
const useMarkdownFrontmatter = (pattern, excerptSeparator = "---") => {
|
|
8
39
|
return globSync(pattern, { ignore: ["node_modules/**", ".git/**"] }).map((file) => {
|
|
9
|
-
const
|
|
40
|
+
const src = fs.readFileSync(resolve(file), "utf-8");
|
|
41
|
+
const { data, excerpt } = matter(src, {
|
|
10
42
|
excerpt: true,
|
|
11
43
|
excerpt_separator: excerptSeparator
|
|
12
44
|
});
|
|
@@ -16,16 +48,12 @@ const useMarkdownFrontmatter = (pattern, excerptSeparator = "---") => {
|
|
|
16
48
|
};
|
|
17
49
|
});
|
|
18
50
|
};
|
|
19
|
-
|
|
20
|
-
//#endregion
|
|
21
|
-
//#region src/shared/composables/useSlug.ts
|
|
51
|
+
// src/shared/composables/useSlug.ts
|
|
22
52
|
const useSlug = (str) => {
|
|
23
53
|
return str.replace(/#/, "-sharp-").replace(/\./, "-dot-").replace(/-$/, "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/đ/g, "d").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
24
54
|
};
|
|
25
55
|
const useSlugFilter = (slug) => (str) => useSlug(str) === slug;
|
|
26
|
-
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region src/node/composables/slug.ts
|
|
56
|
+
// src/node/composables/slug.ts
|
|
29
57
|
const useSlugFromMarkdownFrontMatter = (pattern, frontmatterKeyOrMappingFn, excerptSeparator = "---") => {
|
|
30
58
|
const frontMatters = useMarkdownFrontmatter(pattern, excerptSeparator);
|
|
31
59
|
const _frontmatterMappingFn = typeof frontmatterKeyOrMappingFn === "string" ? (frontMatter) => frontMatter[frontmatterKeyOrMappingFn] : frontmatterKeyOrMappingFn;
|
|
@@ -36,9 +64,7 @@ const useSlugFromMarkdownFrontMatter = (pattern, frontmatterKeyOrMappingFn, exce
|
|
|
36
64
|
return { params: { slug } };
|
|
37
65
|
});
|
|
38
66
|
};
|
|
39
|
-
|
|
40
|
-
//#endregion
|
|
41
|
-
//#region src/node/composables/route.ts
|
|
67
|
+
// src/node/composables/route.ts
|
|
42
68
|
const useMarkdownFrontmatterRoute = (pattern, frontmatterKey, excerptSeparator = "---") => {
|
|
43
69
|
return { paths() {
|
|
44
70
|
return useSlugFromMarkdownFrontMatter(pattern, frontmatterKey, excerptSeparator);
|
|
@@ -49,6 +75,4 @@ const useArchiveRoute = (pattern, excerptSeparator = "---") => {
|
|
|
49
75
|
return useSlugFromMarkdownFrontMatter(pattern, (frontmatter) => new Date(frontmatter.date).getFullYear().toString(), excerptSeparator);
|
|
50
76
|
} };
|
|
51
77
|
};
|
|
52
|
-
|
|
53
|
-
//#endregion
|
|
54
|
-
export { useArchiveRoute, useMarkdownFrontmatter, useMarkdownFrontmatterRoute, useSlug, useSlugFilter, useSlugFromMarkdownFrontMatter };
|
|
78
|
+
export { useArchiveRoute, useExcerptHtml, useMarkdownFrontmatter, useMarkdownFrontmatterRoute, useSlug, useSlugFilter, useSlugFromMarkdownFrontMatter };
|
package/dist/node/config.d.mts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { UserConfig } from "vitepress";
|
|
2
2
|
import { DefaultTheme } from "vitepress/theme";
|
|
3
3
|
import * as Swetrix from "swetrix";
|
|
4
|
-
|
|
5
|
-
//#region src/client/plugins/donation/index.d.ts
|
|
6
4
|
declare interface DonationPluginConfig {
|
|
7
5
|
buymeacoffee?: string;
|
|
8
6
|
kofi?: string;
|
|
@@ -19,8 +17,6 @@ declare interface CustomDonationBaseConfig {
|
|
|
19
17
|
declare interface CustomDonationConfig extends CustomDonationBaseConfig {
|
|
20
18
|
donationId: string;
|
|
21
19
|
}
|
|
22
|
-
//#endregion
|
|
23
|
-
//#region src/client/plugins/google-analytics/index.d.ts
|
|
24
20
|
interface GoogleAnalyticsOptions {
|
|
25
21
|
/**
|
|
26
22
|
* The Google Analytics ID.
|
|
@@ -29,8 +25,6 @@ interface GoogleAnalyticsOptions {
|
|
|
29
25
|
*/
|
|
30
26
|
id: string;
|
|
31
27
|
}
|
|
32
|
-
//#endregion
|
|
33
|
-
//#region src/client/plugins/medium-zoom/index.d.ts
|
|
34
28
|
declare module 'medium-zoom' {
|
|
35
29
|
interface Zoom {
|
|
36
30
|
refresh: (selector?: string) => void;
|
|
@@ -41,8 +35,6 @@ type MediumZoomPluginConfig = {
|
|
|
41
35
|
zoomOptions?: string;
|
|
42
36
|
delay?: number;
|
|
43
37
|
};
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region src/client/plugins/sharing/index.d.ts
|
|
46
38
|
declare interface SharingPluginConfig {
|
|
47
39
|
wrapperCssClass?: string;
|
|
48
40
|
titleCssClass?: string;
|
|
@@ -54,8 +46,6 @@ declare interface SharingPluginConfig {
|
|
|
54
46
|
iconSize?: number;
|
|
55
47
|
iconStyle?: string;
|
|
56
48
|
}
|
|
57
|
-
//#endregion
|
|
58
|
-
//#region src/client/plugins/swetrix/index.d.ts
|
|
59
49
|
interface SwetrixOptions {
|
|
60
50
|
/**
|
|
61
51
|
* The Swetrix ID.
|
|
@@ -65,8 +55,6 @@ interface SwetrixOptions {
|
|
|
65
55
|
id: string;
|
|
66
56
|
options?: Swetrix.LibOptions;
|
|
67
57
|
}
|
|
68
|
-
//#endregion
|
|
69
|
-
//#region src/client/types/index.d.ts
|
|
70
58
|
interface ThemeConfig extends DefaultTheme.Config {
|
|
71
59
|
/**
|
|
72
60
|
* The site URL.
|
|
@@ -84,8 +72,6 @@ interface ThemeConfig extends DefaultTheme.Config {
|
|
|
84
72
|
*/
|
|
85
73
|
themeFooter?: DefaultTheme.Footer;
|
|
86
74
|
}
|
|
87
|
-
//#endregion
|
|
88
|
-
//#region src/node/config.d.ts
|
|
89
75
|
/**
|
|
90
76
|
* @type {import('vitepress').UserConfig}
|
|
91
77
|
*/
|
|
@@ -127,6 +113,5 @@ declare const baseConfig: {
|
|
|
127
113
|
* Helper function to define theme configuration with proper typing
|
|
128
114
|
* Note: TailwindCSS plugin should be added in vite.config.ts, not here
|
|
129
115
|
*/
|
|
130
|
-
declare function defineWithDefaultThemeConfig(config?: UserConfig<ThemeConfig>): UserConfig<ThemeConfig>;
|
|
131
|
-
|
|
132
|
-
export { baseConfig as default, defineWithDefaultThemeConfig };
|
|
116
|
+
export declare function defineWithDefaultThemeConfig(config?: UserConfig<ThemeConfig>): UserConfig<ThemeConfig>;
|
|
117
|
+
export { baseConfig as default };
|
package/dist/node/config.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { loadEnv, mergeConfig } from "vitepress";
|
|
2
|
-
|
|
3
|
-
//#region src/node/config.ts
|
|
2
|
+
// src/node/config.ts
|
|
4
3
|
process.env.VITE_EXTRA_EXTENSIONS = "rss";
|
|
5
4
|
globalThis.__VUE_PROD_DEVTOOLS__ = process.env.NODE_ENV === "development";
|
|
6
5
|
const env = loadEnv(process.env.NODE_ENV || "development", process.cwd(), "VITE_");
|
|
@@ -21,7 +20,13 @@ const baseConfig = {
|
|
|
21
20
|
googleAnalytics: { id: env.VITE_GA_ID },
|
|
22
21
|
search: {
|
|
23
22
|
provider: "local",
|
|
24
|
-
options: {
|
|
23
|
+
options: {
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} src
|
|
26
|
+
* @param {import('vitepress').MarkdownEnv} env
|
|
27
|
+
* @param {import('markdown-it')} md
|
|
28
|
+
*/
|
|
29
|
+
async _render(src, env, md) {
|
|
25
30
|
const html = await md.renderAsync(src, env);
|
|
26
31
|
if (env.frontmatter?.search === false) return "";
|
|
27
32
|
let renderedHtml = "";
|
|
@@ -51,6 +56,4 @@ function defineWithDefaultThemeConfig(config = {}) {
|
|
|
51
56
|
}
|
|
52
57
|
return mergeConfig(baseConfig, config);
|
|
53
58
|
}
|
|
54
|
-
|
|
55
|
-
//#endregion
|
|
56
|
-
export { baseConfig as default, defineWithDefaultThemeConfig };
|
|
59
|
+
export { baseConfig as default, defineWithDefaultThemeConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ansidev-oss/vitepress-theme-ansidev",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "The VitePress theme for ansidev's blog",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
"import": "./dist/client/index.js",
|
|
28
28
|
"types": "./dist/client/index.d.ts"
|
|
29
29
|
},
|
|
30
|
-
"./styles.css":
|
|
30
|
+
"./styles.css": {
|
|
31
|
+
"import": "./dist/client/styles.css",
|
|
32
|
+
"types": "./styles.d.ts"
|
|
33
|
+
},
|
|
31
34
|
"./config": {
|
|
32
35
|
"import": "./dist/node/config.mjs",
|
|
33
36
|
"types": "./dist/node/config.d.ts"
|
|
@@ -39,42 +42,44 @@
|
|
|
39
42
|
},
|
|
40
43
|
"files": [
|
|
41
44
|
"dist",
|
|
45
|
+
"!dist/.pack",
|
|
42
46
|
"src",
|
|
43
47
|
"README.md",
|
|
44
48
|
"LICENSE"
|
|
45
49
|
],
|
|
46
50
|
"dependencies": {
|
|
47
51
|
"@tailwindcss/vite": "^4.2.0",
|
|
48
|
-
"@vueuse/core": "^
|
|
52
|
+
"@vueuse/core": "^15.0.0",
|
|
53
|
+
"markdown-it": "^15.0.3",
|
|
49
54
|
"vue-i18n": "^11.2.8"
|
|
50
55
|
},
|
|
51
56
|
"devDependencies": {
|
|
52
|
-
"@biomejs/biome": "2.4.4",
|
|
53
57
|
"@iconify/vue": "^5.0.0",
|
|
54
58
|
"@tailwindcss/postcss": "^4.2.0",
|
|
55
|
-
"@types/
|
|
59
|
+
"@types/markdown-it": "^14.2.0",
|
|
60
|
+
"@types/node": "^26.0.0",
|
|
56
61
|
"@vitejs/plugin-vue": "^6.0.4",
|
|
62
|
+
"@vue/test-utils": "^2.5.1",
|
|
57
63
|
"autoprefixer": "^10.4.24",
|
|
58
|
-
"
|
|
59
|
-
"glob": "^13.0.6",
|
|
64
|
+
"glob": "^14.0.0",
|
|
60
65
|
"gray-matter": "^4.0.3",
|
|
66
|
+
"happy-dom": "^21.0.4",
|
|
61
67
|
"medium-zoom": "^1.1.0",
|
|
62
68
|
"postcss": "^8.5.6",
|
|
63
|
-
"prettier": "^3.8.1",
|
|
64
69
|
"rimraf": "^6.1.3",
|
|
65
70
|
"swetrix": "^4.1.0",
|
|
66
71
|
"tailwindcss": "^4.2.0",
|
|
67
72
|
"ts-node": "^10.9.2",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"vite": "^7.3.1",
|
|
73
|
+
"typescript": "^6.0.0",
|
|
74
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
|
|
71
75
|
"vitepress": "^2.0.0-alpha.16",
|
|
72
76
|
"vue": "^3.5.28",
|
|
73
|
-
"vue-tsc": "^3.2.5"
|
|
77
|
+
"vue-tsc": "^3.2.5",
|
|
78
|
+
"vite-plus": "1.4.0"
|
|
74
79
|
},
|
|
75
80
|
"peerDependencies": {
|
|
76
81
|
"@iconify/vue": "^5.0.0",
|
|
77
|
-
"glob": "^13.0.6",
|
|
82
|
+
"glob": "^13.0.6 || ^14.0.0",
|
|
78
83
|
"gray-matter": "^4.0.3",
|
|
79
84
|
"medium-zoom": "^1.1.0",
|
|
80
85
|
"swetrix": "^4.1.0",
|
|
@@ -83,16 +88,17 @@
|
|
|
83
88
|
"vitepress": "^2.0.0-alpha.16",
|
|
84
89
|
"vue": "^3.5.28"
|
|
85
90
|
},
|
|
91
|
+
"engines": {
|
|
92
|
+
"node": ">=26.2.0",
|
|
93
|
+
"pnpm": ">=11.3.0"
|
|
94
|
+
},
|
|
86
95
|
"scripts": {
|
|
87
96
|
"typecheck": "tsc --noEmit",
|
|
88
|
-
"
|
|
89
|
-
"lint": "
|
|
90
|
-
"
|
|
91
|
-
"dev:node": "tsdown --watch",
|
|
92
|
-
"dev": "concurrently --kill-others \"pnpm dev:client\" \"pnpm dev:node\"",
|
|
97
|
+
"test": "vp test run",
|
|
98
|
+
"lint:check": "vp check",
|
|
99
|
+
"lint": "vp check --fix",
|
|
93
100
|
"clean": "rimraf dist",
|
|
94
|
-
"build
|
|
95
|
-
"build
|
|
96
|
-
"build": "pnpm clean && concurrently \"pnpm build:client\" \"pnpm build:node\""
|
|
101
|
+
"build": "vp run build:all",
|
|
102
|
+
"dev": "vp build --watch & vp pack --watch --on-success \"node ../../scripts/publish-node.mjs && node ../../scripts/touch-demo.mjs\" & wait"
|
|
97
103
|
}
|
|
98
104
|
}
|
|
@@ -8,15 +8,21 @@ const props = defineProps<{
|
|
|
8
8
|
direction?: 'horizontal' | 'vertical'
|
|
9
9
|
}>()
|
|
10
10
|
|
|
11
|
-
const directionCssClass =
|
|
11
|
+
const directionCssClass =
|
|
12
|
+
props.direction === 'vertical' ? 'space-y-4' : 'space-x-4'
|
|
12
13
|
|
|
13
14
|
const Donation = theme.value.donation
|
|
14
|
-
? defineAsyncComponent(
|
|
15
|
+
? defineAsyncComponent(
|
|
16
|
+
() => import('../plugins/donation/components/Donation.vue')
|
|
17
|
+
)
|
|
15
18
|
: () => null
|
|
16
19
|
</script>
|
|
17
20
|
|
|
18
21
|
<template>
|
|
19
|
-
<div
|
|
22
|
+
<div
|
|
23
|
+
class="flex flex-wrap py-4 items-center justify-center"
|
|
24
|
+
:class="directionCssClass"
|
|
25
|
+
>
|
|
20
26
|
<Donation :donation="theme.donation" />
|
|
21
27
|
</div>
|
|
22
28
|
</template>
|
|
@@ -9,16 +9,23 @@ const { hasSidebar, hasAside } = useLayout()
|
|
|
9
9
|
|
|
10
10
|
const copyright = computed(() => {
|
|
11
11
|
return typeof theme.value.themeFooter.copyright === 'string'
|
|
12
|
-
? theme.value.themeFooter.copyright.replace(
|
|
12
|
+
? theme.value.themeFooter.copyright.replace(
|
|
13
|
+
/#\{present\}/g,
|
|
14
|
+
new Date().getFullYear().toString()
|
|
15
|
+
)
|
|
13
16
|
: theme.value.themeFooter.copyright
|
|
14
17
|
})
|
|
15
18
|
|
|
16
19
|
const GoogleAnalytics = theme.value.googleAnalytics
|
|
17
|
-
? defineAsyncComponent(
|
|
20
|
+
? defineAsyncComponent(
|
|
21
|
+
() => import('../plugins/google-analytics/components/GoogleAnalytics.vue')
|
|
22
|
+
)
|
|
18
23
|
: () => null
|
|
19
24
|
|
|
20
25
|
const Swetrix = theme.value.swetrix
|
|
21
|
-
? defineAsyncComponent(
|
|
26
|
+
? defineAsyncComponent(
|
|
27
|
+
() => import('../plugins/swetrix/components/Swetrix.vue')
|
|
28
|
+
)
|
|
22
29
|
: () => null
|
|
23
30
|
</script>
|
|
24
31
|
|
|
@@ -58,7 +65,7 @@ const Swetrix = theme.value.swetrix
|
|
|
58
65
|
.VPFooter :deep(a) {
|
|
59
66
|
text-decoration-line: underline;
|
|
60
67
|
text-underline-offset: 2px;
|
|
61
|
-
transition:
|
|
68
|
+
transition: none;
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
.VPFooter :deep(a:hover) {
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
<script setup lang=
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import RouterLink from './RouterLink.vue'
|
|
3
3
|
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
href: {
|
|
6
6
|
type: String,
|
|
7
7
|
required: true,
|
|
8
|
-
default: '#'
|
|
8
|
+
default: '#'
|
|
9
9
|
},
|
|
10
10
|
disableRouterLink: {
|
|
11
11
|
type: Boolean,
|
|
12
12
|
required: false,
|
|
13
|
-
default: false
|
|
14
|
-
}
|
|
13
|
+
default: false
|
|
14
|
+
}
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
const isInternalLink = props.href?.startsWith('/') && !props.disableRouterLink
|
|
@@ -25,7 +25,13 @@ const isAnchorLink = props.href?.startsWith('#')
|
|
|
25
25
|
<a v-else-if="disableRouterLink" target="_self" :href="href" v-bind="$attrs">
|
|
26
26
|
<slot />
|
|
27
27
|
</a>
|
|
28
|
-
<a
|
|
28
|
+
<a
|
|
29
|
+
v-else
|
|
30
|
+
target="_blank"
|
|
31
|
+
rel="noopener noreferrer"
|
|
32
|
+
:href="href"
|
|
33
|
+
v-bind="$attrs"
|
|
34
|
+
>
|
|
29
35
|
<slot />
|
|
30
36
|
</a>
|
|
31
37
|
</template>
|
|
@@ -6,8 +6,8 @@ defineProps({
|
|
|
6
6
|
size: {
|
|
7
7
|
type: Number,
|
|
8
8
|
required: false,
|
|
9
|
-
default: 20
|
|
10
|
-
}
|
|
9
|
+
default: 20
|
|
10
|
+
}
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
const { t, availableLocales, locale } = useI18n()
|
|
@@ -33,7 +33,7 @@ const toggleLocale = () => switchLocale(availableLocales, locale)
|
|
|
33
33
|
width: 1px;
|
|
34
34
|
height: 24px;
|
|
35
35
|
background-color: var(--vp-c-divider);
|
|
36
|
-
content:
|
|
36
|
+
content: '';
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
.VPLocaleSwitcher {
|
|
@@ -43,12 +43,12 @@ const toggleLocale = () => switchLocale(availableLocales, locale)
|
|
|
43
43
|
width: 36px;
|
|
44
44
|
height: 36px;
|
|
45
45
|
color: var(--vp-c-text-2);
|
|
46
|
-
transition:
|
|
46
|
+
transition: none;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
.VPLocaleSwitcher:hover {
|
|
50
50
|
color: var(--vp-c-text-1);
|
|
51
|
-
transition:
|
|
51
|
+
transition: none;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
.VPLocaleSwitcher > :deep(svg) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
3
|
+
import { createI18n } from 'vue-i18n'
|
|
4
|
+
import PostDate from './PostDate.vue'
|
|
5
|
+
|
|
6
|
+
const data = vi.hoisted(() => ({
|
|
7
|
+
frontmatter: { value: {} as Record<string, unknown> },
|
|
8
|
+
lang: { value: 'en-US' }
|
|
9
|
+
}))
|
|
10
|
+
vi.mock('vitepress', () => ({ useData: () => data }))
|
|
11
|
+
|
|
12
|
+
const i18n = createI18n({
|
|
13
|
+
legacy: false,
|
|
14
|
+
locale: 'en',
|
|
15
|
+
messages: { en: { posted_on: 'Posted on' } }
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const render = (date?: Date | string) =>
|
|
19
|
+
mount(PostDate, { props: { date }, global: { plugins: [i18n] } })
|
|
20
|
+
const datetimeOf = (date?: Date | string) =>
|
|
21
|
+
render(date).get('time').attributes('datetime')
|
|
22
|
+
|
|
23
|
+
describe('PostDate locale', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
data.frontmatter.value = {}
|
|
26
|
+
data.lang.value = 'en-US'
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const textOf = () => render('2023-04-05T06:07:08.000Z').get('time').text()
|
|
30
|
+
|
|
31
|
+
it('formats in English for en-US', () => {
|
|
32
|
+
expect(textOf()).toBe('April 5, 2023')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('formats in Vietnamese when frontmatter lang is vi', () => {
|
|
36
|
+
data.frontmatter.value = { lang: 'vi' }
|
|
37
|
+
expect(textOf()).toBe('5 tháng 4, 2023')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('falls back to the site lang without frontmatter lang', () => {
|
|
41
|
+
data.lang.value = 'vi'
|
|
42
|
+
expect(textOf()).toBe('5 tháng 4, 2023')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('falls back to English for an invalid lang tag', () => {
|
|
46
|
+
data.frontmatter.value = { lang: 'not_a_lang!' }
|
|
47
|
+
expect(textOf()).toBe('April 5, 2023')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('PostDate datetime', () => {
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.useRealTimers()
|
|
54
|
+
data.frontmatter.value = {}
|
|
55
|
+
data.lang.value = 'en-US'
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('matches the date prop when given a Date', () => {
|
|
59
|
+
const date = new Date('2023-04-05T06:07:08.000Z')
|
|
60
|
+
expect(datetimeOf(date)).toBe(date.toISOString())
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('matches the date prop when given a string', () => {
|
|
64
|
+
expect(datetimeOf('2023-04-05T06:07:08.000Z')).toBe(
|
|
65
|
+
'2023-04-05T06:07:08.000Z'
|
|
66
|
+
)
|
|
67
|
+
expect(datetimeOf('2023-04-05')).toBe(new Date('2023-04-05').toISOString())
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('does not depend on render time', () => {
|
|
71
|
+
vi.useFakeTimers()
|
|
72
|
+
vi.setSystemTime(new Date('2030-01-01T00:00:00.000Z'))
|
|
73
|
+
expect(datetimeOf('2023-04-05T06:07:08.000Z')).toBe(
|
|
74
|
+
'2023-04-05T06:07:08.000Z'
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('omits datetime and does not throw for a missing or invalid date', () => {
|
|
79
|
+
expect(datetimeOf(undefined)).toBeUndefined()
|
|
80
|
+
expect(datetimeOf('not a date')).toBeUndefined()
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
<script setup lang=
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import { useI18n } from 'vue-i18n'
|
|
4
|
+
import { usePageLang } from '../composables/lang'
|
|
4
5
|
|
|
5
6
|
const props = defineProps({
|
|
6
7
|
/**
|
|
@@ -9,25 +10,40 @@ const props = defineProps({
|
|
|
9
10
|
date: [Date, String],
|
|
10
11
|
textSize: {
|
|
11
12
|
type: String,
|
|
12
|
-
default: null
|
|
13
|
-
}
|
|
13
|
+
default: null
|
|
14
|
+
}
|
|
14
15
|
})
|
|
15
16
|
|
|
16
17
|
const { t } = useI18n()
|
|
18
|
+
const pageLang = usePageLang()
|
|
19
|
+
|
|
20
|
+
const formatDate = (date: Date, locale: string) => {
|
|
21
|
+
const options: Intl.DateTimeFormatOptions = {
|
|
22
|
+
year: 'numeric',
|
|
23
|
+
month: 'long',
|
|
24
|
+
day: 'numeric',
|
|
25
|
+
timeZone: 'UTC'
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return date.toLocaleDateString(locale, options)
|
|
29
|
+
} catch {
|
|
30
|
+
// Invalid lang tag: fall back to English.
|
|
31
|
+
return date.toLocaleDateString('en-US', options)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
const dateObject = computed(() => {
|
|
18
|
-
const date =
|
|
36
|
+
const date =
|
|
37
|
+
props.date instanceof Date ? props.date : new Date(props.date as string)
|
|
38
|
+
|
|
39
|
+
const isValid = !Number.isNaN(+date)
|
|
19
40
|
|
|
20
41
|
return {
|
|
21
42
|
time: +date,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
month: 'long',
|
|
25
|
-
day: 'numeric',
|
|
26
|
-
}),
|
|
43
|
+
iso: isValid ? date.toISOString() : undefined,
|
|
44
|
+
string: formatDate(date, pageLang.value)
|
|
27
45
|
}
|
|
28
46
|
})
|
|
29
|
-
|
|
30
|
-
const getDateTime = () => new Date().toISOString()
|
|
31
47
|
</script>
|
|
32
48
|
|
|
33
49
|
<template>
|
|
@@ -35,8 +51,11 @@ const getDateTime = () => new Date().toISOString()
|
|
|
35
51
|
<dt class="sr-only">
|
|
36
52
|
{{ t('posted_on') }}
|
|
37
53
|
</dt>
|
|
38
|
-
<dd
|
|
39
|
-
|
|
54
|
+
<dd
|
|
55
|
+
class="font-medium leading-6"
|
|
56
|
+
:class="[textSize ? `text-${textSize}` : 'text-base']"
|
|
57
|
+
>
|
|
58
|
+
<time :datetime="dateObject.iso">{{ dateObject.string }}</time>
|
|
40
59
|
</dd>
|
|
41
60
|
</dl>
|
|
42
61
|
</template>
|