@ansidev-oss/vitepress-theme-ansidev 1.0.11 → 1.1.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-k7SUudua.js +624 -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/plugins/sharing/index.d.ts +4 -0
- package/dist/client/record-Cgv_fXDH.js +2739 -0
- package/dist/client/styles.css +2 -1
- package/dist/node/composables.d.mts +7 -17
- package/dist/node/composables.mjs +7 -15
- package/dist/node/config.d.mts +6 -17
- package/dist/node/config.mjs +9 -6
- package/package.json +23 -12
- package/src/client/components/PostDate.test.ts +73 -0
- package/src/client/components/PostDate.vue +18 -8
- package/src/client/components/PostItem.vue +1 -1
- package/src/client/composables/index.ts +1 -0
- package/src/client/composables/lang.test.ts +52 -0
- package/src/client/composables/lang.ts +58 -0
- package/src/client/index.ts +5 -0
- package/src/client/plugins/sharing/index.ts +7 -1
- package/src/client/styles/base.css +30 -0
- package/src/client/styles/blog.css +352 -0
- package/src/client/styles/index.css +3 -30
- 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-BAhNuo7n.js +0 -126
- package/dist/client/VPCarbonAds-CSK7KnIu.js +0 -31
- package/dist/client/VPLocalSearchBox-TYbANUkJ.js +0 -4086
- package/dist/client/index-BtT3qA6T.js +0 -12663
- package/dist/client/index-C43AnX3f.js +0 -7616
- package/dist/client/index-CCR5sUM8.js +0 -10688
- package/dist/client/index-DuYWs0vj.js +0 -265
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/node/composables/types.d.ts
|
|
2
1
|
type Pattern = string | string[];
|
|
3
2
|
type Frontmatter = Record<string, unknown> & {
|
|
4
3
|
date?: string;
|
|
@@ -6,27 +5,18 @@ type Frontmatter = Record<string, unknown> & {
|
|
|
6
5
|
};
|
|
7
6
|
type FrontmatterToString = (frontmatter: Frontmatter) => string;
|
|
8
7
|
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) => {
|
|
8
|
+
export declare const useMarkdownFrontmatter: (pattern: Pattern, excerptSeparator?: string) => Array<Frontmatter>;
|
|
9
|
+
export declare const useMarkdownFrontmatterRoute: (pattern: Pattern, frontmatterKey: string, excerptSeparator?: string) => {
|
|
15
10
|
paths(): any;
|
|
16
11
|
};
|
|
17
|
-
declare const useArchiveRoute: (pattern: Pattern, excerptSeparator?: string) => {
|
|
12
|
+
export declare const useArchiveRoute: (pattern: Pattern, excerptSeparator?: string) => {
|
|
18
13
|
paths(): any;
|
|
19
14
|
};
|
|
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) => {
|
|
15
|
+
export declare const useSlug: (str: string) => string;
|
|
16
|
+
export declare const useSlugFilter: (slug: string) => (str: string) => boolean;
|
|
17
|
+
export declare const useSlugFromMarkdownFrontMatter: (pattern: Pattern, frontmatterKeyOrMappingFn: StringOrFrontmatterToString, excerptSeparator?: string) => {
|
|
27
18
|
params: {
|
|
28
19
|
slug: any;
|
|
29
20
|
};
|
|
30
21
|
}[];
|
|
31
|
-
|
|
32
|
-
export { useArchiveRoute, useMarkdownFrontmatter, useMarkdownFrontmatterRoute, useSlug, useSlugFilter, useSlugFromMarkdownFrontMatter };
|
|
22
|
+
export {};
|
|
@@ -2,11 +2,11 @@ import fs from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { globSync } from "glob";
|
|
4
4
|
import matter from "gray-matter";
|
|
5
|
-
|
|
6
|
-
//#region src/node/composables/markdown.ts
|
|
5
|
+
// src/node/composables/markdown.ts
|
|
7
6
|
const useMarkdownFrontmatter = (pattern, excerptSeparator = "---") => {
|
|
8
7
|
return globSync(pattern, { ignore: ["node_modules/**", ".git/**"] }).map((file) => {
|
|
9
|
-
const
|
|
8
|
+
const src = fs.readFileSync(resolve(file), "utf-8");
|
|
9
|
+
const { data, excerpt } = matter(src, {
|
|
10
10
|
excerpt: true,
|
|
11
11
|
excerpt_separator: excerptSeparator
|
|
12
12
|
});
|
|
@@ -16,16 +16,12 @@ const useMarkdownFrontmatter = (pattern, excerptSeparator = "---") => {
|
|
|
16
16
|
};
|
|
17
17
|
});
|
|
18
18
|
};
|
|
19
|
-
|
|
20
|
-
//#endregion
|
|
21
|
-
//#region src/shared/composables/useSlug.ts
|
|
19
|
+
// src/shared/composables/useSlug.ts
|
|
22
20
|
const useSlug = (str) => {
|
|
23
21
|
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
22
|
};
|
|
25
23
|
const useSlugFilter = (slug) => (str) => useSlug(str) === slug;
|
|
26
|
-
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region src/node/composables/slug.ts
|
|
24
|
+
// src/node/composables/slug.ts
|
|
29
25
|
const useSlugFromMarkdownFrontMatter = (pattern, frontmatterKeyOrMappingFn, excerptSeparator = "---") => {
|
|
30
26
|
const frontMatters = useMarkdownFrontmatter(pattern, excerptSeparator);
|
|
31
27
|
const _frontmatterMappingFn = typeof frontmatterKeyOrMappingFn === "string" ? (frontMatter) => frontMatter[frontmatterKeyOrMappingFn] : frontmatterKeyOrMappingFn;
|
|
@@ -36,9 +32,7 @@ const useSlugFromMarkdownFrontMatter = (pattern, frontmatterKeyOrMappingFn, exce
|
|
|
36
32
|
return { params: { slug } };
|
|
37
33
|
});
|
|
38
34
|
};
|
|
39
|
-
|
|
40
|
-
//#endregion
|
|
41
|
-
//#region src/node/composables/route.ts
|
|
35
|
+
// src/node/composables/route.ts
|
|
42
36
|
const useMarkdownFrontmatterRoute = (pattern, frontmatterKey, excerptSeparator = "---") => {
|
|
43
37
|
return { paths() {
|
|
44
38
|
return useSlugFromMarkdownFrontMatter(pattern, frontmatterKey, excerptSeparator);
|
|
@@ -49,6 +43,4 @@ const useArchiveRoute = (pattern, excerptSeparator = "---") => {
|
|
|
49
43
|
return useSlugFromMarkdownFrontMatter(pattern, (frontmatter) => new Date(frontmatter.date).getFullYear().toString(), excerptSeparator);
|
|
50
44
|
} };
|
|
51
45
|
};
|
|
52
|
-
|
|
53
|
-
//#endregion
|
|
54
|
-
export { useArchiveRoute, useMarkdownFrontmatter, useMarkdownFrontmatterRoute, useSlug, useSlugFilter, useSlugFromMarkdownFrontMatter };
|
|
46
|
+
export { useArchiveRoute, 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,17 +35,17 @@ 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 {
|
|
39
|
+
wrapperCssClass?: string;
|
|
40
|
+
titleCssClass?: string;
|
|
41
|
+
buttonCssClass?: string;
|
|
42
|
+
title?: string;
|
|
47
43
|
networks?: string[];
|
|
48
44
|
displayMode?: string;
|
|
49
45
|
buttonSize?: number;
|
|
50
46
|
iconSize?: number;
|
|
51
47
|
iconStyle?: string;
|
|
52
48
|
}
|
|
53
|
-
//#endregion
|
|
54
|
-
//#region src/client/plugins/swetrix/index.d.ts
|
|
55
49
|
interface SwetrixOptions {
|
|
56
50
|
/**
|
|
57
51
|
* The Swetrix ID.
|
|
@@ -61,8 +55,6 @@ interface SwetrixOptions {
|
|
|
61
55
|
id: string;
|
|
62
56
|
options?: Swetrix.LibOptions;
|
|
63
57
|
}
|
|
64
|
-
//#endregion
|
|
65
|
-
//#region src/client/types/index.d.ts
|
|
66
58
|
interface ThemeConfig extends DefaultTheme.Config {
|
|
67
59
|
/**
|
|
68
60
|
* The site URL.
|
|
@@ -80,8 +72,6 @@ interface ThemeConfig extends DefaultTheme.Config {
|
|
|
80
72
|
*/
|
|
81
73
|
themeFooter?: DefaultTheme.Footer;
|
|
82
74
|
}
|
|
83
|
-
//#endregion
|
|
84
|
-
//#region src/node/config.d.ts
|
|
85
75
|
/**
|
|
86
76
|
* @type {import('vitepress').UserConfig}
|
|
87
77
|
*/
|
|
@@ -123,6 +113,5 @@ declare const baseConfig: {
|
|
|
123
113
|
* Helper function to define theme configuration with proper typing
|
|
124
114
|
* Note: TailwindCSS plugin should be added in vite.config.ts, not here
|
|
125
115
|
*/
|
|
126
|
-
declare function defineWithDefaultThemeConfig(config?: UserConfig<ThemeConfig>): UserConfig<ThemeConfig>;
|
|
127
|
-
|
|
128
|
-
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.1.0",
|
|
4
4
|
"description": "The VitePress theme for ansidev's blog",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -49,39 +49,50 @@
|
|
|
49
49
|
"vue-i18n": "^11.2.8"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@biomejs/biome": "2.
|
|
52
|
+
"@biomejs/biome": "2.6.3",
|
|
53
53
|
"@iconify/vue": "^5.0.0",
|
|
54
|
-
"@tailwindcss/postcss": "^4.
|
|
55
|
-
"@types/node": "^25.
|
|
54
|
+
"@tailwindcss/postcss": "^4.2.0",
|
|
55
|
+
"@types/node": "^25.3.0",
|
|
56
56
|
"@vitejs/plugin-vue": "^6.0.4",
|
|
57
|
+
"@vue/test-utils": "^2.5.1",
|
|
57
58
|
"autoprefixer": "^10.4.24",
|
|
58
|
-
"concurrently": "^
|
|
59
|
-
"glob": "^13.0.
|
|
59
|
+
"concurrently": "^10.0.0",
|
|
60
|
+
"glob": "^13.0.6",
|
|
60
61
|
"gray-matter": "^4.0.3",
|
|
62
|
+
"happy-dom": "^21.0.4",
|
|
61
63
|
"medium-zoom": "^1.1.0",
|
|
62
64
|
"postcss": "^8.5.6",
|
|
63
65
|
"prettier": "^3.8.1",
|
|
64
66
|
"rimraf": "^6.1.3",
|
|
65
|
-
"rollup-plugin-copy": "^3.5.0",
|
|
66
67
|
"swetrix": "^4.1.0",
|
|
67
|
-
"tailwindcss": "^4.
|
|
68
|
+
"tailwindcss": "^4.2.0",
|
|
68
69
|
"ts-node": "^10.9.2",
|
|
69
|
-
"tsdown": "^0.
|
|
70
|
-
"typescript": "^
|
|
71
|
-
"vite": "^
|
|
70
|
+
"tsdown": "^0.23.0",
|
|
71
|
+
"typescript": "^6.0.0",
|
|
72
|
+
"vite": "^8.0.0",
|
|
72
73
|
"vitepress": "^2.0.0-alpha.16",
|
|
74
|
+
"vitest": "^5.0.5",
|
|
73
75
|
"vue": "^3.5.28",
|
|
74
|
-
"vue-tsc": "^3.2.
|
|
76
|
+
"vue-tsc": "^3.2.5"
|
|
75
77
|
},
|
|
76
78
|
"peerDependencies": {
|
|
77
79
|
"@iconify/vue": "^5.0.0",
|
|
80
|
+
"glob": "^13.0.6",
|
|
81
|
+
"gray-matter": "^4.0.3",
|
|
78
82
|
"medium-zoom": "^1.1.0",
|
|
79
83
|
"swetrix": "^4.1.0",
|
|
84
|
+
"ts-node": "^10.9.2",
|
|
85
|
+
"typescript": "^5.9.3",
|
|
80
86
|
"vitepress": "^2.0.0-alpha.16",
|
|
81
87
|
"vue": "^3.5.28"
|
|
82
88
|
},
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=26.2.0",
|
|
91
|
+
"pnpm": ">=11.3.0"
|
|
92
|
+
},
|
|
83
93
|
"scripts": {
|
|
84
94
|
"typecheck": "tsc --noEmit",
|
|
95
|
+
"test": "vitest run",
|
|
85
96
|
"lint:check": "biome check",
|
|
86
97
|
"lint": "biome check --write",
|
|
87
98
|
"dev:client": "vite build --watch",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { createI18n } from 'vue-i18n'
|
|
4
|
+
import PostDate from './PostDate.vue'
|
|
5
|
+
|
|
6
|
+
const data = vi.hoisted(() => ({ frontmatter: { value: {} as Record<string, unknown> }, lang: { value: 'en-US' } }))
|
|
7
|
+
vi.mock('vitepress', () => ({ useData: () => data }))
|
|
8
|
+
|
|
9
|
+
const i18n = createI18n({
|
|
10
|
+
legacy: false,
|
|
11
|
+
locale: 'en',
|
|
12
|
+
messages: { en: { posted_on: 'Posted on' } },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const render = (date?: Date | string) => mount(PostDate, { props: { date }, global: { plugins: [i18n] } })
|
|
16
|
+
const datetimeOf = (date?: Date | string) => render(date).get('time').attributes('datetime')
|
|
17
|
+
|
|
18
|
+
describe('PostDate locale', () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
data.frontmatter.value = {}
|
|
21
|
+
data.lang.value = 'en-US'
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const textOf = () => render('2023-04-05T06:07:08.000Z').get('time').text()
|
|
25
|
+
|
|
26
|
+
it('formats in English for en-US', () => {
|
|
27
|
+
expect(textOf()).toBe('April 5, 2023')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('formats in Vietnamese when frontmatter lang is vi', () => {
|
|
31
|
+
data.frontmatter.value = { lang: 'vi' }
|
|
32
|
+
expect(textOf()).toBe('5 tháng 4, 2023')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('falls back to the site lang without frontmatter lang', () => {
|
|
36
|
+
data.lang.value = 'vi'
|
|
37
|
+
expect(textOf()).toBe('5 tháng 4, 2023')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('falls back to English for an invalid lang tag', () => {
|
|
41
|
+
data.frontmatter.value = { lang: 'not_a_lang!' }
|
|
42
|
+
expect(textOf()).toBe('April 5, 2023')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('PostDate datetime', () => {
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
vi.useRealTimers()
|
|
49
|
+
data.frontmatter.value = {}
|
|
50
|
+
data.lang.value = 'en-US'
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('matches the date prop when given a Date', () => {
|
|
54
|
+
const date = new Date('2023-04-05T06:07:08.000Z')
|
|
55
|
+
expect(datetimeOf(date)).toBe(date.toISOString())
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('matches the date prop when given a string', () => {
|
|
59
|
+
expect(datetimeOf('2023-04-05T06:07:08.000Z')).toBe('2023-04-05T06:07:08.000Z')
|
|
60
|
+
expect(datetimeOf('2023-04-05')).toBe(new Date('2023-04-05').toISOString())
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('does not depend on render time', () => {
|
|
64
|
+
vi.useFakeTimers()
|
|
65
|
+
vi.setSystemTime(new Date('2030-01-01T00:00:00.000Z'))
|
|
66
|
+
expect(datetimeOf('2023-04-05T06:07:08.000Z')).toBe('2023-04-05T06:07:08.000Z')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('omits datetime and does not throw for a missing or invalid date', () => {
|
|
70
|
+
expect(datetimeOf(undefined)).toBeUndefined()
|
|
71
|
+
expect(datetimeOf('not a date')).toBeUndefined()
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
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
|
/**
|
|
@@ -14,20 +15,29 @@ const props = defineProps({
|
|
|
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 = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }
|
|
22
|
+
try {
|
|
23
|
+
return date.toLocaleDateString(locale, options)
|
|
24
|
+
} catch {
|
|
25
|
+
// Invalid lang tag: fall back to English.
|
|
26
|
+
return date.toLocaleDateString('en-US', options)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
const dateObject = computed(() => {
|
|
18
31
|
const date = props.date instanceof Date ? props.date : new Date(props.date as string)
|
|
19
32
|
|
|
33
|
+
const isValid = !Number.isNaN(+date)
|
|
34
|
+
|
|
20
35
|
return {
|
|
21
36
|
time: +date,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
month: 'long',
|
|
25
|
-
day: 'numeric',
|
|
26
|
-
}),
|
|
37
|
+
iso: isValid ? date.toISOString() : undefined,
|
|
38
|
+
string: formatDate(date, pageLang.value),
|
|
27
39
|
}
|
|
28
40
|
})
|
|
29
|
-
|
|
30
|
-
const getDateTime = () => new Date().toISOString()
|
|
31
41
|
</script>
|
|
32
42
|
|
|
33
43
|
<template>
|
|
@@ -36,7 +46,7 @@ const getDateTime = () => new Date().toISOString()
|
|
|
36
46
|
{{ t('posted_on') }}
|
|
37
47
|
</dt>
|
|
38
48
|
<dd class="font-medium leading-6" :class="[textSize ? `text-${textSize}` : 'text-base']">
|
|
39
|
-
<time :datetime="
|
|
49
|
+
<time :datetime="dateObject.iso">{{ dateObject.string }}</time>
|
|
40
50
|
</dd>
|
|
41
51
|
</dl>
|
|
42
52
|
</template>
|
|
@@ -27,7 +27,7 @@ const postDate = computed(() => usePostDate(props.post.date))
|
|
|
27
27
|
<div class="flex flex-wrap mb-3">
|
|
28
28
|
<TermLink v-for="category in post.categories" :key="category" :text="category" basePath="/categories/" />
|
|
29
29
|
</div>
|
|
30
|
-
<p
|
|
30
|
+
<p v-html="post?.excerpt" />
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
33
|
</template>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { nextTick, ref } from 'vue'
|
|
3
|
+
import { createI18n } from 'vue-i18n'
|
|
4
|
+
import { matchLocale, usePageLocale } from './lang'
|
|
5
|
+
|
|
6
|
+
const data = { frontmatter: ref<Record<string, unknown>>({}), lang: ref('en') }
|
|
7
|
+
vi.mock('vitepress', () => ({ useData: () => data }))
|
|
8
|
+
|
|
9
|
+
describe('matchLocale', () => {
|
|
10
|
+
const available = ['en', 'vi']
|
|
11
|
+
|
|
12
|
+
it('matches exactly', () => {
|
|
13
|
+
expect(matchLocale('vi', available)).toBe('vi')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('matches by base language and is case-insensitive', () => {
|
|
17
|
+
expect(matchLocale('vi-VN', available)).toBe('vi')
|
|
18
|
+
expect(matchLocale('EN-us', available)).toBe('en')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns undefined when unsupported', () => {
|
|
22
|
+
expect(matchLocale('fr', available)).toBeUndefined()
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('usePageLocale', () => {
|
|
27
|
+
const setup = () => {
|
|
28
|
+
const i18n = createI18n({ legacy: false, locale: 'en', messages: { en: {}, vi: {} } })
|
|
29
|
+
usePageLocale(i18n)
|
|
30
|
+
return i18n
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
it('sets the locale from front matter lang immediately', () => {
|
|
34
|
+
data.frontmatter.value = { lang: 'vi' }
|
|
35
|
+
expect(setup().global.locale.value).toBe('vi')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('keeps a manual choice until the page lang changes', async () => {
|
|
39
|
+
data.frontmatter.value = { lang: 'vi' }
|
|
40
|
+
const i18n = setup()
|
|
41
|
+
i18n.global.locale.value = 'en'
|
|
42
|
+
data.frontmatter.value = { lang: 'vi' }
|
|
43
|
+
await nextTick()
|
|
44
|
+
expect(i18n.global.locale.value).toBe('en')
|
|
45
|
+
data.frontmatter.value = {}
|
|
46
|
+
await nextTick()
|
|
47
|
+
expect(i18n.global.locale.value).toBe('en')
|
|
48
|
+
data.frontmatter.value = { lang: 'vi' }
|
|
49
|
+
await nextTick()
|
|
50
|
+
expect(i18n.global.locale.value).toBe('vi')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useData } from 'vitepress'
|
|
2
|
+
import { type ComputedRef, computed, onMounted, watch, watchEffect } from 'vue'
|
|
3
|
+
import type { Locale } from 'vue-i18n'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolved language of the current page: `frontmatter.lang`, falling back to the site lang.
|
|
7
|
+
*/
|
|
8
|
+
export const usePageLang = (): ComputedRef<string> => {
|
|
9
|
+
const { frontmatter, lang } = useData()
|
|
10
|
+
return computed(() => {
|
|
11
|
+
const pageLang = frontmatter.value.lang
|
|
12
|
+
return typeof pageLang === 'string' && pageLang.trim() ? pageLang.trim() : lang.value
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Keeps `<html lang>` in sync with the resolved page lang.
|
|
18
|
+
* VitePress resets it to the site lang on mount, so re-apply it after mount and on navigation.
|
|
19
|
+
*/
|
|
20
|
+
export const useHtmlLang = (): void => {
|
|
21
|
+
const pageLang = usePageLang()
|
|
22
|
+
// Registered inside onMounted so it runs after VitePress's own mount-time reset.
|
|
23
|
+
onMounted(() => {
|
|
24
|
+
watchEffect(
|
|
25
|
+
() => {
|
|
26
|
+
document.documentElement.lang = pageLang.value
|
|
27
|
+
},
|
|
28
|
+
{ flush: 'post' },
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Picks the available locale matching `lang` exactly or by base language (`vi-VN` -> `vi`).
|
|
35
|
+
*/
|
|
36
|
+
export const matchLocale = (lang: string, availableLocales: readonly Locale[]): Locale | undefined => {
|
|
37
|
+
const normalized = lang.toLowerCase()
|
|
38
|
+
return (
|
|
39
|
+
availableLocales.find((l) => l.toLowerCase() === normalized) ??
|
|
40
|
+
availableLocales.find((l) => l.toLowerCase() === normalized.split('-')[0])
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Syncs the vue-i18n locale with the resolved page lang, synchronously so SSR renders the right strings.
|
|
46
|
+
* A manual `LocaleSwitcher` choice lasts until the page lang changes.
|
|
47
|
+
*/
|
|
48
|
+
export const usePageLocale = (i18n: { global: { locale: { value: Locale }; availableLocales: Locale[] } }): void => {
|
|
49
|
+
const pageLang = usePageLang()
|
|
50
|
+
watch(
|
|
51
|
+
pageLang,
|
|
52
|
+
(lang) => {
|
|
53
|
+
const matched = matchLocale(lang, i18n.global.availableLocales)
|
|
54
|
+
if (matched) i18n.global.locale.value = matched
|
|
55
|
+
},
|
|
56
|
+
{ immediate: true },
|
|
57
|
+
)
|
|
58
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Icon } from '@iconify/vue'
|
|
2
2
|
import type { Theme } from 'vitepress'
|
|
3
3
|
import DefaultTheme from 'vitepress/theme'
|
|
4
|
+
import { useHtmlLang, usePageLocale } from './composables/lang'
|
|
4
5
|
import Layout from './layouts/Layout.vue'
|
|
5
6
|
import ArchivesPage from './pages/ArchivesPage.vue'
|
|
6
7
|
import CategoriesPage from './pages/CategoriesPage.vue'
|
|
@@ -29,6 +30,10 @@ const theme: Theme = {
|
|
|
29
30
|
app.component('ArchivesPage', ArchivesPage)
|
|
30
31
|
app.use(i18n)
|
|
31
32
|
},
|
|
33
|
+
setup() {
|
|
34
|
+
useHtmlLang()
|
|
35
|
+
usePageLocale(i18n)
|
|
36
|
+
},
|
|
32
37
|
} satisfies Theme
|
|
33
38
|
|
|
34
39
|
export * from './composables'
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare interface SharingPluginConfig {
|
|
2
|
+
wrapperCssClass?: string
|
|
3
|
+
titleCssClass?: string
|
|
4
|
+
buttonCssClass?: string
|
|
5
|
+
title?: string
|
|
2
6
|
networks?: string[]
|
|
3
7
|
displayMode?: string
|
|
4
8
|
buttonSize?: number
|
|
@@ -65,7 +69,9 @@ export const useUrlQueryParamsBuilder = (params: Record<string, string | undefin
|
|
|
65
69
|
}
|
|
66
70
|
const esc = encodeURIComponent
|
|
67
71
|
const paramKeys = Object.keys(params)
|
|
68
|
-
if (paramKeys.length < 1)
|
|
72
|
+
if (paramKeys.length < 1) {
|
|
73
|
+
return ''
|
|
74
|
+
}
|
|
69
75
|
|
|
70
76
|
const s = paramKeys
|
|
71
77
|
.filter((key) => params[key] !== undefined)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@variant dark (&:where(.dark, .dark *));
|
|
3
|
+
|
|
4
|
+
@theme {
|
|
5
|
+
--color-primary: rgb(59, 130, 246);
|
|
6
|
+
--color-primary-alt: rgb(29, 78, 216);
|
|
7
|
+
--color-primary-alt-dark: rgb(147, 197, 253);
|
|
8
|
+
--color-secondary: rgb(239, 68, 68);
|
|
9
|
+
--color-secondary-dark: rgb(234, 179, 8);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@utility component-border {
|
|
13
|
+
@apply border rounded-md border-gray-500 hover:border-secondary dark:hover:border-secondary-dark;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@utility link {
|
|
17
|
+
@apply no-underline hover:text-secondary dark:hover:text-secondary-dark;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@utility link-alt {
|
|
21
|
+
@apply text-primary-alt! dark:text-primary-alt-dark! link!;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:root {
|
|
25
|
+
--vp-c-brand-1: var(--color-primary);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.vp-doc a {
|
|
29
|
+
@apply link;
|
|
30
|
+
}
|