@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.
Files changed (86) hide show
  1. package/dist/client/Donation-BBPb8v5p.js +66 -0
  2. package/dist/client/GoogleAnalytics-34XEM7eO.js +25 -0
  3. package/dist/client/Sharing-CpagX9oy.js +185 -0
  4. package/dist/client/Swetrix-GKEyGNrV.js +647 -0
  5. package/dist/client/components/PostDate.test.d.ts +1 -0
  6. package/dist/client/composables/index.d.ts +1 -0
  7. package/dist/client/composables/lang.d.ts +27 -0
  8. package/dist/client/composables/lang.test.d.ts +1 -0
  9. package/dist/client/index.js +3769 -9
  10. package/dist/client/medium-zoom-DPl0T7qn.js +266 -0
  11. package/dist/client/record-Cgv_fXDH.js +2739 -0
  12. package/dist/client/styles.css +2 -1
  13. package/dist/client/types/index.d.ts +8 -0
  14. package/dist/node/composables.d.mts +16 -17
  15. package/dist/node/composables.mjs +39 -15
  16. package/dist/node/config.d.mts +2 -17
  17. package/dist/node/config.mjs +9 -6
  18. package/package.json +27 -21
  19. package/src/client/components/DonationWidget.vue +9 -3
  20. package/src/client/components/Footer.vue +11 -4
  21. package/src/client/components/Link.vue +11 -5
  22. package/src/client/components/LocaleSwitcher.vue +5 -5
  23. package/src/client/components/PostDate.test.ts +82 -0
  24. package/src/client/components/PostDate.vue +32 -13
  25. package/src/client/components/PostItem.vue +17 -7
  26. package/src/client/components/PostList.vue +7 -4
  27. package/src/client/components/ProjectCard.vue +42 -14
  28. package/src/client/components/ProjectList.vue +12 -5
  29. package/src/client/components/RouterLink.vue +3 -3
  30. package/src/client/components/SharingWidget.vue +3 -1
  31. package/src/client/components/StatusBadge.vue +10 -10
  32. package/src/client/components/TermBadge.vue +11 -7
  33. package/src/client/components/TermLink.vue +4 -4
  34. package/src/client/composables/date.ts +1 -1
  35. package/src/client/composables/index.ts +1 -0
  36. package/src/client/composables/lang.test.ts +56 -0
  37. package/src/client/composables/lang.ts +65 -0
  38. package/src/client/composables/locale.ts +4 -1
  39. package/src/client/composables/project.ts +3 -3
  40. package/src/client/composables/rank.ts +5 -2
  41. package/src/client/index.ts +5 -0
  42. package/src/client/layouts/Layout.vue +2 -2
  43. package/src/client/pages/ArchivesPage.vue +10 -5
  44. package/src/client/pages/CategoriesPage.vue +13 -5
  45. package/src/client/pages/HomePage.vue +3 -3
  46. package/src/client/pages/Page.vue +13 -11
  47. package/src/client/pages/PostsPage.vue +4 -4
  48. package/src/client/pages/ProjectsPage.vue +10 -7
  49. package/src/client/pages/TagsPage.vue +10 -4
  50. package/src/client/plugins/donation/components/Donation.vue +5 -3
  51. package/src/client/plugins/donation/components/DonationButton.vue +3 -1
  52. package/src/client/plugins/donation/index.ts +11 -8
  53. package/src/client/plugins/i18n/index.ts +2 -2
  54. package/src/client/plugins/sharing/components/Sharing.vue +39 -15
  55. package/src/client/plugins/sharing/components/SharingButton.vue +34 -14
  56. package/src/client/plugins/sharing/index.ts +6 -2
  57. package/src/client/styles/base.css +30 -0
  58. package/src/client/styles/blog.css +376 -0
  59. package/src/client/styles/index.css +2 -30
  60. package/src/client/types/index.ts +8 -0
  61. package/src/node/composables/excerpt.test.ts +88 -0
  62. package/src/node/composables/excerpt.ts +57 -0
  63. package/src/node/composables/index.ts +1 -0
  64. package/src/node/composables/markdown.ts +8 -2
  65. package/src/node/composables/route.ts +15 -7
  66. package/src/node/composables/slug.ts +12 -4
  67. package/src/node/config.ts +20 -14
  68. package/src/shared/composables/useSlug.ts +2 -1
  69. package/src/vite-env.d.ts +1 -0
  70. package/dist/client/Donation-D9Xn0MPt.js +0 -76
  71. package/dist/client/GoogleAnalytics-Dp3EGn1x.js +0 -29
  72. package/dist/client/Sharing-ZrU7YF-x.js +0 -190
  73. package/dist/client/Swetrix-CM1mbD6X.js +0 -450
  74. package/dist/client/VPAlgoliaSearchBox-C18GEhLm.js +0 -126
  75. package/dist/client/VPCarbonAds-DwoamMao.js +0 -31
  76. package/dist/client/VPLocalSearchBox-DyteUinQ.js +0 -4086
  77. package/dist/client/index-BtT3qA6T.js +0 -12663
  78. package/dist/client/index-CCR5sUM8.js +0 -10688
  79. package/dist/client/index-CkmxezC0.js +0 -7616
  80. package/dist/client/index-DuYWs0vj.js +0 -265
  81. package/dist/node/composables/index.d.ts +0 -3
  82. package/dist/node/composables/markdown.d.ts +0 -2
  83. package/dist/node/composables/route.d.ts +0 -15
  84. package/dist/node/composables/slug.d.ts +0 -7
  85. package/dist/node/composables/types.d.ts +0 -7
  86. 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
- //#region src/node/composables/types.d.ts
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
- //#endregion
10
- //#region src/node/composables/markdown.d.ts
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
- //#endregion
21
- //#region src/shared/composables/useSlug.d.ts
22
- declare const useSlug: (str: string) => string;
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
- //#endregion
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
- //#region src/node/composables/markdown.ts
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 { data, excerpt } = matter(fs.readFileSync(resolve(file), "utf-8"), {
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 };
@@ -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
- //#endregion
132
- export { baseConfig as default, defineWithDefaultThemeConfig };
116
+ export declare function defineWithDefaultThemeConfig(config?: UserConfig<ThemeConfig>): UserConfig<ThemeConfig>;
117
+ export { baseConfig as default };
@@ -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: { async _render(src, env, md) {
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.12",
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": "./dist/client/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": "^14.2.1",
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/node": "^25.3.0",
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
- "concurrently": "^9.2.1",
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
- "tsdown": "^0.20.3",
69
- "typescript": "^5.9.3",
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
- "lint:check": "biome check",
89
- "lint": "biome check --write",
90
- "dev:client": "vite build --watch",
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:client": "vite build && tsc --emitDeclarationOnly",
95
- "build:node": "tsdown",
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 = props.direction === 'vertical' ? 'space-y-4' : 'space-x-4'
11
+ const directionCssClass =
12
+ props.direction === 'vertical' ? 'space-y-4' : 'space-x-4'
12
13
 
13
14
  const Donation = theme.value.donation
14
- ? defineAsyncComponent(() => import('../plugins/donation/components/Donation.vue'))
15
+ ? defineAsyncComponent(
16
+ () => import('../plugins/donation/components/Donation.vue')
17
+ )
15
18
  : () => null
16
19
  </script>
17
20
 
18
21
  <template>
19
- <div class="flex flex-wrap py-4 items-center justify-center" :class="directionCssClass">
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(/#\{present\}/g, new Date().getFullYear().toString())
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(() => import('../plugins/google-analytics/components/GoogleAnalytics.vue'))
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(() => import('../plugins/swetrix/components/Swetrix.vue'))
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: color 0.25s;
68
+ transition: none;
62
69
  }
63
70
 
64
71
  .VPFooter :deep(a:hover) {
@@ -1,17 +1,17 @@
1
- <script setup lang='ts'>
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 v-else target="_blank" rel="noopener noreferrer" :href="href" v-bind="$attrs">
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: color 0.5s;
46
+ transition: none;
47
47
  }
48
48
 
49
49
  .VPLocaleSwitcher:hover {
50
50
  color: var(--vp-c-text-1);
51
- transition: color 0.25s;
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='ts'>
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 = props.date instanceof Date ? props.date : new Date(props.date as string)
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
- string: date.toLocaleDateString('en-US', {
23
- year: 'numeric',
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 class="font-medium leading-6" :class="[textSize ? `text-${textSize}` : 'text-base']">
39
- <time :datetime="getDateTime()">{{ dateObject.string }}</time>
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>