valaxy 0.23.6 → 0.24.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.
@@ -1,26 +1,32 @@
1
1
  <script setup lang="ts">
2
2
  // https://router.vuejs.org/guide/advanced/extending-router-link.html#extending-routerlink
3
3
  import { computed } from 'vue'
4
+ import { RouterLink } from 'vue-router'
5
+ import { EXTERNAL_URL_RE } from '../../node/constants'
4
6
 
5
7
  const props = defineProps<{
6
8
  showExternalIcon?: boolean
7
9
  to?: string
8
10
  href?: string
11
+ target?: string
9
12
  }>()
10
13
 
11
14
  const link = computed(() => props.href || props.to || '#')
12
15
 
13
16
  const isExternalLink = computed(() => {
14
- return typeof link.value === 'string' && link.value.startsWith('http')
17
+ return (link.value && EXTERNAL_URL_RE.test(link.value)) || props.target === '_blank'
15
18
  })
16
19
  </script>
17
20
 
18
21
  <template>
19
- <a v-if="isExternalLink" v-bind="$attrs" :href="link" target="_blank">
22
+ <a
23
+ v-if="isExternalLink" class="va-link" v-bind="$attrs" :href="link"
24
+ :target="target ?? (isExternalLink ? '_blank' : undefined)"
25
+ >
20
26
  <slot />
21
27
  <div v-if="showExternalIcon" class="icon-link inline-block" i-ri-arrow-right-up-line />
22
28
  </a>
23
- <RouterLink v-else v-bind="$attrs" :to="link">
29
+ <RouterLink v-else class="va-link" v-bind="$attrs" :to="link">
24
30
  <slot />
25
31
  </RouterLink>
26
32
  </template>
@@ -31,7 +31,7 @@ const licenseHtml = computed(() => {
31
31
  <strong>
32
32
  {{ t('post.copyright.author') + t('symbol.colon') }}
33
33
  </strong>
34
- <span>{{ siteConfig.author.name }}</span>
34
+ <span>{{ t(siteConfig.author.name) }}</span>
35
35
  </li>
36
36
  <li v-if="url" class="post-copyright-link">
37
37
  <strong>
@@ -5,9 +5,10 @@ import { useSeoMeta } from '@unhead/vue'
5
5
  import { computed } from 'vue'
6
6
  import { useI18n } from 'vue-i18n'
7
7
 
8
+ import { tObject } from '../../../shared/utils/i18n'
8
9
  import { useFrontmatter, useValaxyHead } from '../../composables'
9
- import { useTimezone } from '../../composables/global'
10
10
 
11
+ import { useTimezone } from '../../composables/global'
11
12
  // https://github.com/vueuse/head
12
13
  // you can use this to manipulate the document head in any components,
13
14
  // they will be rendered correctly in the html results with vite-ssg
@@ -20,7 +21,7 @@ export function useValaxyApp() {
20
21
 
21
22
  const { locale } = useI18n()
22
23
 
23
- const title = computed(() => fm.value[`title_${locale.value}`] || fm.value.title)
24
+ const title = computed(() => tObject(fm.value.title || '', locale.value))
24
25
 
25
26
  // seo
26
27
  // todo: get first image url from markdown
@@ -33,7 +34,7 @@ export function useValaxyApp() {
33
34
  ogLocale: computed(() => locale.value || fm.value.lang || siteConfig.value.lang || 'en'),
34
35
  ogLocaleAlternate: computed(() => siteConfig.value.languages.filter(l => l !== locale.value)),
35
36
  ogSiteName: computed(() => siteConfig.value.title),
36
- ogTitle: computed(() => fm.value.title || siteConfig.value.title),
37
+ ogTitle: computed(() => tObject(fm.value.title || siteConfig.value.title, locale.value)),
37
38
  ogImage: computed(() => fm.value.ogImage || fm.value.cover || siteConfig.value.favicon),
38
39
  ogType: 'website',
39
40
  ogUrl: siteUrl,
@@ -5,15 +5,16 @@ import { useI18n } from 'vue-i18n'
5
5
 
6
6
  import { useFrontmatter } from '../../composables'
7
7
  import { useSiteConfig } from '../../config'
8
+ import { tObject } from '../../utils'
8
9
 
9
10
  export function useValaxyHead() {
10
11
  const { locale } = useI18n()
11
12
 
12
13
  const fm = useFrontmatter()
13
14
  const siteConfig = useSiteConfig()
14
- const title = computed<string>(() => fm.value[`title_${locale.value}`] || fm.value.title)
15
+ const $title = computed(() => tObject(fm.value.title || '', locale.value))
15
16
  useHead({
16
- title,
17
+ title: $title,
17
18
  titleTemplate: (title) => {
18
19
  return fm.value.titleTemplate || (title ? `${title} - ${siteConfig.value.title}` : siteConfig.value.title)
19
20
  },
@@ -1,8 +1,11 @@
1
+ import type { Ref } from 'vue'
1
2
  import { isClient, useStorage } from '@vueuse/core'
2
3
  import dayjs from 'dayjs'
4
+ import { computed } from 'vue'
5
+
3
6
  // not optimize deps all locales
4
7
  import { useI18n } from 'vue-i18n'
5
-
8
+ import { tObject } from '../../shared/utils/i18n'
6
9
  import 'dayjs/locale/en'
7
10
  import 'dayjs/locale/zh-cn'
8
11
 
@@ -34,3 +37,29 @@ export function useLocale() {
34
37
  toggleLocales,
35
38
  }
36
39
  }
40
+
41
+ /**
42
+ * get locale title
43
+ *
44
+ * ```md
45
+ * ---
46
+ * title:
47
+ * zh-CN: Valaxy - Vue 驱动的静态站点生成器
48
+ * en: Valaxy - Vue Powered Static Site Generator
49
+ * ---
50
+ * ```
51
+ *
52
+ * @param fm
53
+ */
54
+ export function useLocaleTitle(fm: Ref<{
55
+ title?: string | Record<string, string>
56
+ } | null>) {
57
+ const { locale } = useI18n()
58
+ return computed(() => {
59
+ if (!fm.value)
60
+ return ''
61
+
62
+ const lang = locale.value
63
+ return tObject(fm.value.title || '', lang) || ''
64
+ })
65
+ }
@@ -3,7 +3,7 @@ import type { ComputedRef } from 'vue'
3
3
  import { computed } from 'vue'
4
4
  import { useI18n } from 'vue-i18n'
5
5
  import { useRouterStore } from '../../stores'
6
- import { sortByDate } from '../../utils'
6
+ import { sortByDate, tObject } from '../../utils'
7
7
 
8
8
  export * from './usePagination'
9
9
  export * from './usePrevNext'
@@ -11,8 +11,7 @@ export * from './usePrevNext'
11
11
  export function usePostTitle(post: ComputedRef<Post>) {
12
12
  const { locale } = useI18n()
13
13
  return computed(() => {
14
- const lang = locale.value === 'zh-CN' ? 'zh' : locale.value
15
- return post.value[`title_${lang}`] || post.value.title
14
+ return tObject(post.value.title || '', locale.value)
16
15
  })
17
16
  }
18
17
 
package/client/main.ts CHANGED
@@ -20,6 +20,7 @@ import { setupValaxyDevTools } from './utils/dev'
20
20
  */
21
21
  import '#valaxy/styles'
22
22
  import 'uno.css'
23
+ import 'virtual:group-icons.css'
23
24
 
24
25
  const valaxyConfig = initValaxyConfig()
25
26
 
@@ -48,6 +48,8 @@ export const i18n = createI18n({
48
48
  legacy: false,
49
49
  locale: '',
50
50
  messages: valaxyMessages,
51
+ // use key
52
+ missingWarn: false,
51
53
  })
52
54
 
53
55
  export async function install({ app, router }: ViteSSGContext, config: ComputedRef<ValaxyConfig<DefaultTheme.Config>>) {
@@ -1,3 +1,4 @@
1
+ export * from '../../shared'
1
2
  export * from './cdn'
2
3
  export * from './code'
3
4
  export * from './content'
@@ -5,4 +6,5 @@ export * from './helper'
5
6
  export * from './router'
6
7
  export * from './time'
7
8
  export * from './types'
9
+
8
10
  export * from './wrap'
@@ -4,7 +4,7 @@ import yargs from "yargs";
4
4
  import { hideBin } from "yargs/helpers";
5
5
 
6
6
  // package.json
7
- var version = "0.23.6";
7
+ var version = "0.24.0";
8
8
 
9
9
  // node/modules/fuse.ts
10
10
  import path4 from "path";
@@ -514,7 +514,7 @@ function getGitTimestamp(file, type = "updated") {
514
514
 
515
515
  // node/constants/index.ts
516
516
  var EXCERPT_SEPARATOR = "<!-- more -->";
517
- var EXTERNAL_URL_RE = /^https?:/i;
517
+ var EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
518
518
  var PATHNAME_PROTOCOL_RE = /^pathname:\/\//;
519
519
  var ALL_ROUTE = "/:all(.*)*";
520
520
  var customElements = /* @__PURE__ */ new Set([
@@ -973,12 +973,20 @@ import { consola as consola10 } from "consola";
973
973
  import { colors as colors8 } from "consola/utils";
974
974
  import dayjs from "dayjs";
975
975
  import fg3 from "fast-glob";
976
+ import { Feed } from "feed";
976
977
  import fs9 from "fs-extra";
977
978
  import matter2 from "gray-matter";
978
979
  import MarkdownIt from "markdown-it";
979
980
  import ora3 from "ora";
980
981
  import { getBorderCharacters, table } from "table";
981
- import { createFeed, generateAtom1, generateJson1, generateRss2 } from "zfeed";
982
+
983
+ // shared/utils/i18n.ts
984
+ function tObject(data, lang) {
985
+ if (typeof data === "object") {
986
+ return data[lang] || Object.values(data)[0] || "";
987
+ }
988
+ return data;
989
+ }
982
990
 
983
991
  // node/utils/date.ts
984
992
  import fs8 from "fs-extra";
@@ -999,6 +1007,7 @@ async function build(options) {
999
1007
  const s = ora3("RSS Generating ...").start();
1000
1008
  const { config } = options;
1001
1009
  const siteConfig = config.siteConfig;
1010
+ const lang = siteConfig.lang || "en";
1002
1011
  if (!siteConfig.url || siteConfig.url === "/") {
1003
1012
  consola10.error("You must set `url` (like `https://example.com`) in `site.config.ts` to generate rss.");
1004
1013
  return;
@@ -1015,15 +1024,19 @@ async function build(options) {
1015
1024
  json: `${siteConfig.feed?.name || "feed"}.json`,
1016
1025
  rss: `${siteConfig.feed?.name || "feed"}.xml`
1017
1026
  };
1027
+ const feedLinks = {};
1028
+ Object.keys(feedNameMap).forEach((key) => {
1029
+ feedLinks[key] = `${siteUrl}${feedNameMap[key]}`;
1030
+ });
1031
+ const title = tObject(siteConfig.title, lang);
1032
+ const description = tObject(siteConfig.description, lang);
1018
1033
  const feedOptions = {
1019
- title: siteConfig.title || "Valaxy Blog",
1020
- description: siteConfig.description,
1034
+ title: title || "Valaxy Blog",
1035
+ description,
1021
1036
  id: siteUrl || "valaxy",
1022
1037
  link: siteUrl,
1023
1038
  copyright: `CC ${siteConfig.license?.type?.toUpperCase()} ${ccVersion} ${(/* @__PURE__ */ new Date()).getFullYear()} \xA9 ${siteConfig.author?.name}`,
1024
- feed: Object.fromEntries(
1025
- Object.entries(feedNameMap).map(([key, value]) => [key, `${siteUrl}${value}`])
1026
- )
1039
+ feedLinks
1027
1040
  };
1028
1041
  const DOMAIN = siteConfig.url.slice(0, -1);
1029
1042
  const files = await fg3(`${options.userRoot}/pages/posts/**/*.md`);
@@ -1082,23 +1095,23 @@ async function getPosts(params, options) {
1082
1095
  const link = DOMAIN + path17.replace(`${options.userRoot}/pages`, "").replace(/\.md$/, "");
1083
1096
  const tip = `<br/><p>${lang === "zh-CN" ? `\u8BBF\u95EE <a href="${link}" target="_blank">${link}</a> ${fullText ? "\u67E5\u770B\u539F\u6587" : "\u9605\u8BFB\u5168\u6587"}\u3002` : `Visit <a href="${link}" target="_blank">${link}</a> to ${fullText ? "view original article" : "read more"}.`}</p>`;
1084
1097
  posts.push({
1085
- title: data.title,
1086
- updatedAt: new Date(data.date),
1087
- publishedAt: new Date(data.updated || data.date),
1098
+ ...data,
1099
+ title: tObject(data.title, lang),
1100
+ description: tObject(data.description, lang),
1101
+ date: new Date(data.date),
1102
+ published: new Date(data.updated || data.date),
1088
1103
  content: html + tip,
1089
- author,
1104
+ author: [author],
1090
1105
  id: data.id || link,
1091
1106
  link
1092
1107
  });
1093
1108
  }
1094
- posts.sort((a, b) => +(b.publishedAt || b.updatedAt) - +(a.publishedAt || a.updatedAt));
1109
+ posts.sort((a, b) => +(b.published || b.date) - +(a.published || a.date));
1095
1110
  return posts;
1096
1111
  }
1097
1112
  async function writeFeed(feedOptions, posts, options, feedNameMap) {
1098
- const feed = createFeed({
1099
- ...feedOptions,
1100
- items: posts
1101
- });
1113
+ const feed = new Feed(feedOptions);
1114
+ posts.forEach((item) => feed.addItem(item));
1102
1115
  await fs9.ensureDir(dirname3(`./dist/${feedNameMap.atom}`));
1103
1116
  const path17 = resolve5(options.userRoot, "./dist");
1104
1117
  const publicFolder = resolve5(options.userRoot, "public");
@@ -1115,11 +1128,11 @@ async function writeFeed(feedOptions, posts, options, feedNameMap) {
1115
1128
  let data = "";
1116
1129
  const distFeedPath = `${path17}/${feedNameMap[type]}`;
1117
1130
  if (type === "rss")
1118
- data = generateRss2(feed);
1131
+ data = feed.rss2();
1119
1132
  else if (type === "atom")
1120
- data = generateAtom1(feed);
1133
+ data = feed.atom1();
1121
1134
  else if (type === "json")
1122
- data = generateJson1(feed);
1135
+ data = feed.json1();
1123
1136
  await fs9.writeFile(distFeedPath, data, "utf-8");
1124
1137
  consola10.debug(`[${colors8.cyan(type)}] dist: ${colors8.dim(distFeedPath)}`);
1125
1138
  tableData.push([colors8.cyan(type), colors8.yellow("dist"), colors8.dim(distFeedPath)]);
@@ -1459,11 +1472,6 @@ import { colors as colors9 } from "consola/utils";
1459
1472
 
1460
1473
  // ../../node_modules/.pnpm/nanoid@5.1.5/node_modules/nanoid/index.js
1461
1474
  import { webcrypto as crypto } from "crypto";
1462
-
1463
- // ../../node_modules/.pnpm/nanoid@5.1.5/node_modules/nanoid/url-alphabet/index.js
1464
- var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
1465
-
1466
- // ../../node_modules/.pnpm/nanoid@5.1.5/node_modules/nanoid/index.js
1467
1475
  var POOL_SIZE_MULTIPLIER = 128;
1468
1476
  var pool;
1469
1477
  var poolOffset;
@@ -1500,14 +1508,6 @@ function customRandom(alphabet, defaultSize, getRandom) {
1500
1508
  function customAlphabet(alphabet, size = 21) {
1501
1509
  return customRandom(alphabet, size, random);
1502
1510
  }
1503
- function nanoid(size = 21) {
1504
- fillPool(size |= 0);
1505
- let id = "";
1506
- for (let i = poolOffset - size; i < poolOffset; i++) {
1507
- id += urlAlphabet[pool[i] & 63];
1508
- }
1509
- return id;
1510
- }
1511
1511
 
1512
1512
  // node/plugins/markdown/plugins/highlight.ts
1513
1513
  import {
@@ -1515,7 +1515,7 @@ import {
1515
1515
  createHighlighter,
1516
1516
  isSpecialLang
1517
1517
  } from "shiki";
1518
- var nanoid2 = customAlphabet("abcdefghijklmnopqrstuvwxyz", 10);
1518
+ var nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz", 10);
1519
1519
  function attrsToLines(attrs) {
1520
1520
  attrs = attrs.replace(/^(?:\[.*?\])?.*?([\d,-]+).*/, "$1").trim();
1521
1521
  const result = [];
@@ -1602,7 +1602,7 @@ The language '${lang}' is not loaded, falling back to '${defaultLang}' for synta
1602
1602
  return s.replace(mustacheRE, (match) => {
1603
1603
  let marker = mustaches.get(match);
1604
1604
  if (!marker) {
1605
- marker = nanoid2();
1605
+ marker = nanoid();
1606
1606
  mustaches.set(match, marker);
1607
1607
  }
1608
1608
  return marker;
@@ -2014,9 +2014,6 @@ import container from "markdown-it-container";
2014
2014
  function extractLang(info) {
2015
2015
  return info.trim().replace(/=(\d*)/, "").replace(/:(no-)?line-numbers(\{| |$|=\d*).*/, "").replace(/(-vue|\{| ).*$/, "").replace(/^vue-html$/, "template").replace(/^ansi$/, "");
2016
2016
  }
2017
- function getAdaptiveThemeMarker(options) {
2018
- return options.hasSingleTheme ? "" : " vp-adaptive-theme";
2019
- }
2020
2017
  function extractTitle(info, html = false) {
2021
2018
  if (html) {
2022
2019
  return info.replace(/<!--[\s\S]*?-->/g, "").match(/data-title="(.*?)"/)?.[1] || "";
@@ -2027,7 +2024,7 @@ function getCodeHeightLimitStyle(options, env) {
2027
2024
  const codeHeightLimit = env.frontmatter?.codeHeightLimit || options?.siteConfig?.codeHeightLimit;
2028
2025
  if (codeHeightLimit === void 0 || codeHeightLimit <= 0)
2029
2026
  return "";
2030
- return `style="max-height: ${codeHeightLimit}px;"`;
2027
+ return ` max-h-${codeHeightLimit}px`;
2031
2028
  }
2032
2029
  function preWrapperPlugin(md3, options) {
2033
2030
  const fence = md3.renderer.rules.fence;
@@ -2035,40 +2032,42 @@ function preWrapperPlugin(md3, options) {
2035
2032
  const [tokens, idx, _, env] = args;
2036
2033
  const token = tokens[idx];
2037
2034
  token.info = token.info.replace(/\[.*\]/, "");
2035
+ const active = / active( |$)/.test(token.info) ? " active" : "";
2036
+ token.info = token.info.replace(/ active$/, "").replace(/ active /, " ");
2038
2037
  const lang = extractLang(token.info);
2039
2038
  const rawCode = fence(...args);
2040
- return `
2041
- <div ${getCodeHeightLimitStyle(options, env)} class="language-${lang}${getAdaptiveThemeMarker(options)}${// eslint-disable-next-line regexp/no-unused-capturing-group
2042
- / active( |$)/.test(token.info) ? " active" : ""}">
2043
- <button title="Copy Code" class="copy"></button><span class="lang">${lang}</span>${rawCode}<button class="collapse"></button>
2044
- </div>`;
2039
+ const codeHeightLimitClass = getCodeHeightLimitStyle(options, env);
2040
+ return `<div class="language-${lang}${active}${codeHeightLimitClass}"><button title="${options.codeCopyButtonTitle}" class="copy"></button><span class="lang">${lang}</span>${rawCode}<button class="collapse"></button></div>`;
2045
2041
  };
2046
2042
  }
2047
2043
 
2048
2044
  // node/plugins/markdown/plugins/markdown-it/container.ts
2049
- function createContainer(classes, { icon, color, text: defaultTitle, langs } = {}) {
2045
+ function createContainer(classes, { icon, color, text: defaultTitle, langs } = {}, md3) {
2050
2046
  return [
2051
2047
  container,
2052
2048
  classes,
2053
2049
  {
2054
2050
  render(tokens, idx) {
2055
2051
  const token = tokens[idx];
2056
- const info = token.info.trim().slice(classes.length).trim();
2057
2052
  if (token.nesting === 1) {
2058
- if (classes === "details") {
2059
- return `<details class="${classes} custom-block">${`<summary>${info}</summary>`}
2060
- `;
2061
- }
2053
+ token.attrJoin("class", `${classes} custom-block`);
2054
+ const attrs = md3.renderer.renderAttrs(token);
2055
+ const info = token.info.trim().slice(classes.length).trim();
2062
2056
  let iconTag = "";
2063
2057
  if (icon)
2064
2058
  iconTag = `<i class="icon ${icon}" ${color ? `style="color: ${color}"` : ""}></i>`;
2065
- let title = `<span lang="en">${info || defaultTitle}</span>`;
2059
+ let titleWithLang = `<span lang="en">${info || defaultTitle}</span>`;
2066
2060
  if (langs) {
2067
2061
  Object.keys(langs).forEach((lang) => {
2068
- title += `<span lang="${lang}">${info || langs[lang]}</span>`;
2062
+ titleWithLang += `<span lang="${lang}">${info || langs[lang]}</span>`;
2069
2063
  });
2070
2064
  }
2071
- return `<div class="${classes} custom-block"><p class="custom-block-title">${iconTag}${title}</p>
2065
+ const title = md3.renderInline(titleWithLang, {});
2066
+ const titleClass = `custom-block-title${info ? "" : " custom-block-title-default"}`;
2067
+ if (classes === "details")
2068
+ return `<details ${attrs}><summary>${title}</summary>
2069
+ `;
2070
+ return `<div ${attrs}><p class="${titleClass}">${iconTag}${title}</p>
2072
2071
  `;
2073
2072
  } else {
2074
2073
  return classes === "details" ? "</details>\n" : "</div>\n";
@@ -2109,16 +2108,25 @@ var defaultBlocksOptions = {
2109
2108
  }
2110
2109
  }
2111
2110
  };
2112
- function containerPlugin(md3, options, containerOptions = {}) {
2113
- const blockKeys = Object.keys(Object.assign(defaultBlocksOptions, containerOptions));
2111
+ function containerPlugin(md3, containerOptions = {}) {
2112
+ const blockKeys = new Set(Object.keys(Object.assign(defaultBlocksOptions, containerOptions)));
2114
2113
  blockKeys.forEach((optionKey) => {
2115
2114
  const option = {
2116
2115
  ...defaultBlocksOptions[optionKey],
2117
2116
  ...containerOptions[optionKey] || {}
2118
2117
  };
2119
- md3.use(...createContainer(optionKey, option));
2118
+ md3.use(...createContainer(optionKey, option, md3));
2119
+ });
2120
+ md3.use(container, "v-pre", {
2121
+ render: (tokens, idx) => tokens[idx].nesting === 1 ? `<div v-pre>
2122
+ ` : `</div>
2123
+ `
2124
+ });
2125
+ md3.use(container, "raw", {
2126
+ render: (tokens, idx) => tokens[idx].nesting === 1 ? `<div class="vp-raw">
2127
+ ` : `</div>
2128
+ `
2120
2129
  });
2121
- md3.use(...createCodeGroup(options));
2122
2130
  const languages = ["zh-CN", "en"];
2123
2131
  languages.forEach((lang) => {
2124
2132
  md3.use(container, lang, {
@@ -2126,17 +2134,17 @@ function containerPlugin(md3, options, containerOptions = {}) {
2126
2134
  ` : "</div>\n"
2127
2135
  });
2128
2136
  });
2137
+ md3.use(...createCodeGroup(md3));
2129
2138
  }
2130
- function createCodeGroup(options) {
2139
+ function createCodeGroup(md3) {
2131
2140
  return [
2132
2141
  container,
2133
2142
  "code-group",
2134
2143
  {
2135
2144
  render(tokens, idx) {
2136
2145
  if (tokens[idx].nesting === 1) {
2137
- const name = nanoid(5);
2138
2146
  let tabs = "";
2139
- let checked = 'checked="checked"';
2147
+ let checked = "checked";
2140
2148
  for (let i = idx + 1; !(tokens[i].nesting === -1 && tokens[i].type === "container_code-group_close"); ++i) {
2141
2149
  const isHtml = tokens[i].type === "html_block";
2142
2150
  if (tokens[i].type === "fence" && tokens[i].tag === "code" || isHtml) {
@@ -2145,17 +2153,15 @@ function createCodeGroup(options) {
2145
2153
  isHtml
2146
2154
  );
2147
2155
  if (title) {
2148
- const id = nanoid(7);
2149
- tabs += `<input type="radio" name="group-${name}" id="tab-${id}" ${checked}><label for="tab-${id}">${title}</label>`;
2156
+ tabs += `<input type="radio" name="group-${idx}" id="tab-${i}" ${checked}/>`;
2157
+ tabs += `<label data-title="${md3.utils.escapeHtml(title)}" for="tab-${i}">${title}</label>`;
2150
2158
  if (checked && !isHtml)
2151
2159
  tokens[i].info += " active";
2152
2160
  checked = "";
2153
2161
  }
2154
2162
  }
2155
2163
  }
2156
- return `<div class="vp-code-group${getAdaptiveThemeMarker(
2157
- options
2158
- )}"><div class="tabs">${tabs}</div><div class="blocks">
2164
+ return `<div class="vp-code-group"><div class="tabs">${tabs}</div><div class="blocks">
2159
2165
  `;
2160
2166
  }
2161
2167
  return `</div></div>
@@ -2533,13 +2539,10 @@ var defaultCodeTheme = { light: "github-light", dark: "github-dark" };
2533
2539
  async function setupMarkdownPlugins(md3, options, base = "/") {
2534
2540
  const mdOptions = options?.config.markdown || {};
2535
2541
  const theme = mdOptions.theme ?? defaultCodeTheme;
2536
- const hasSingleTheme = typeof theme === "string" || "name" in theme;
2537
2542
  const siteConfig = options?.config.siteConfig || {};
2538
2543
  if (mdOptions.preConfig)
2539
2544
  mdOptions.preConfig(md3);
2540
2545
  md3.use(highlightLinePlugin).use(preWrapperPlugin, { theme, siteConfig }).use(snippetPlugin, options?.userRoot).use(containerPlugin, {
2541
- hasSingleTheme
2542
- }, {
2543
2546
  ...mdOptions.blocks,
2544
2547
  ...mdOptions?.container
2545
2548
  }).use(cssI18nContainer, {
@@ -2559,6 +2562,9 @@ async function setupMarkdownPlugins(md3, options, base = "/") {
2559
2562
  md3.use(emojiPlugin).use(footnotePlugin).use(footnoteTooltipPlugin);
2560
2563
  md3.use(anchorPlugin, {
2561
2564
  slugify,
2565
+ getTokensText: (tokens) => {
2566
+ return tokens.filter((t) => !["html_inline", "emoji"].includes(t.type)).map((t) => t.content).join("");
2567
+ },
2562
2568
  permalink: anchorPlugin.permalink.linkInsideHeader({
2563
2569
  symbol: "&ZeroWidthSpace;",
2564
2570
  renderAttrs: (slug, state) => {
@@ -2576,11 +2582,13 @@ async function setupMarkdownPlugins(md3, options, base = "/") {
2576
2582
  ...mdOptions.anchor
2577
2583
  });
2578
2584
  md3.use(headersPlugin, {
2585
+ level: [2, 3, 4, 5, 6],
2579
2586
  slugify,
2580
2587
  ...typeof mdOptions.headers === "boolean" ? void 0 : mdOptions.headers
2581
2588
  }).use(sfcPlugin, {
2582
2589
  ...mdOptions.sfc
2583
2590
  }).use(titlePlugin).use(tocPlugin, {
2591
+ slugify,
2584
2592
  ...mdOptions.toc
2585
2593
  });
2586
2594
  md3.use(math_plugin, mdOptions.katex);
@@ -2602,12 +2610,10 @@ async function setupMarkdownPlugins(md3, options, base = "/") {
2602
2610
  ...mdOptions.imageFigures
2603
2611
  });
2604
2612
  md3.use(TaskLists);
2605
- if (options?.config.groupIcons) {
2606
- const { groupIconMdPlugin } = await import("vitepress-plugin-group-icons");
2607
- md3.use(groupIconMdPlugin, {
2608
- titleBar: { includeSnippet: true }
2609
- });
2610
- }
2613
+ const { groupIconMdPlugin } = await import("vitepress-plugin-group-icons");
2614
+ md3.use(groupIconMdPlugin, {
2615
+ titleBar: { includeSnippet: true }
2616
+ });
2611
2617
  if (mdOptions.config)
2612
2618
  mdOptions.config(md3);
2613
2619
  return md3;
@@ -4027,38 +4033,18 @@ async function ViteValaxyPlugins(valaxyApp, serverOptions = {}) {
4027
4033
  }),
4028
4034
  createFixPlugins(options)
4029
4035
  ];
4030
- if (valaxyConfig.groupIcons) {
4031
- const { groupIconVitePlugin } = await import("vitepress-plugin-group-icons");
4032
- plugins.push(
4033
- groupIconVitePlugin({
4034
- customIcon: {
4035
- nodejs: "vscode-icons:file-type-node",
4036
- playwright: "vscode-icons:file-type-playwright",
4037
- typedoc: "vscode-icons:file-type-typedoc",
4038
- eslint: "vscode-icons:file-type-eslint"
4039
- },
4040
- ...valaxyConfig.groupIcons
4041
- })
4042
- );
4043
- } else {
4044
- const virtualCssId = "virtual:group-icons.css";
4045
- const resolvedVirtualCssId = `\0${virtualCssId}`;
4046
- plugins.push({
4047
- name: "valaxy:virtual:group-icons.css",
4048
- resolveId(id) {
4049
- if (id === virtualCssId) {
4050
- return resolvedVirtualCssId;
4051
- }
4052
- return void 0;
4036
+ const { groupIconVitePlugin } = await import("vitepress-plugin-group-icons");
4037
+ plugins.push(
4038
+ groupIconVitePlugin({
4039
+ customIcon: {
4040
+ nodejs: "vscode-icons:file-type-node",
4041
+ playwright: "vscode-icons:file-type-playwright",
4042
+ typedoc: "vscode-icons:file-type-typedoc",
4043
+ eslint: "vscode-icons:file-type-eslint"
4053
4044
  },
4054
- async load(id) {
4055
- if (id === resolvedVirtualCssId) {
4056
- return "";
4057
- }
4058
- return void 0;
4059
- }
4060
- });
4061
- }
4045
+ ...valaxyConfig.groupIcons
4046
+ })
4047
+ );
4062
4048
  if (valaxyConfig.visualizer) {
4063
4049
  try {
4064
4050
  const visualizer = (await import("rollup-plugin-visualizer")).visualizer;
@@ -4383,24 +4369,11 @@ import path14 from "path";
4383
4369
  import process10 from "process";
4384
4370
  import { consola as consola17 } from "consola";
4385
4371
  import fs22 from "fs-extra";
4386
-
4387
- // node/cli/utils/fs.ts
4388
- import { access } from "fs/promises";
4389
- async function exists(path17) {
4390
- try {
4391
- await access(path17);
4392
- return true;
4393
- } catch (e) {
4394
- return false;
4395
- }
4396
- }
4397
-
4398
- // node/cli/clean.ts
4399
4372
  async function cleanDist() {
4400
4373
  const distDir = path14.join(process10.cwd(), "dist");
4401
4374
  const cacheDir = path14.join(process10.cwd(), ".valaxy");
4402
4375
  consola17.box("\u{1F9F9} Starting clean...");
4403
- if (await exists(distDir)) {
4376
+ if (await fs22.exists(distDir)) {
4404
4377
  consola17.info("dist directory exists, removing...");
4405
4378
  try {
4406
4379
  await fs22.rm(distDir, { recursive: true, force: true });
@@ -4412,7 +4385,7 @@ async function cleanDist() {
4412
4385
  } else {
4413
4386
  consola17.info("No dist directory found, nothing to clean.");
4414
4387
  }
4415
- if (await exists(cacheDir)) {
4388
+ if (await fs22.exists(cacheDir)) {
4416
4389
  consola17.info(".valaxy cache directory exists, removing...");
4417
4390
  try {
4418
4391
  await fs22.rm(cacheDir, { recursive: true, force: true });
@@ -4681,13 +4654,12 @@ function registerDevCommand(cli2) {
4681
4654
  }
4682
4655
 
4683
4656
  // node/cli/utils/post.ts
4684
- import { writeFile as writeFile2 } from "fs/promises";
4685
- import { join as join9, resolve as resolve14 } from "path";
4686
- import { ensureSuffix as ensureSuffix2 } from "@antfu/utils";
4657
+ import { dirname as dirname5, join as join9, resolve as resolve14 } from "path";
4687
4658
  import { consola as consola19 } from "consola";
4688
4659
  import { colors as colors17 } from "consola/utils";
4689
4660
  import dayjs2 from "dayjs";
4690
4661
  import { render } from "ejs";
4662
+ import fs24 from "fs-extra";
4691
4663
 
4692
4664
  // node/cli/utils/constants.ts
4693
4665
  import process14 from "process";
@@ -4702,42 +4674,38 @@ date: <%=date%>
4702
4674
  // node/cli/utils/scaffold.ts
4703
4675
  import { readFile as readFile2 } from "fs/promises";
4704
4676
  import path16 from "path";
4677
+ import fs23 from "fs-extra";
4705
4678
  async function getTemplate(layout) {
4706
4679
  const { clientRoot, themeRoot } = await resolveOptions({ userRoot });
4707
4680
  const roots = [userRoot, themeRoot, clientRoot];
4708
4681
  for (const root of roots) {
4709
4682
  const scaffoldPath = path16.resolve(root, "scaffolds", `${layout}.md`);
4710
- if (await exists(scaffoldPath))
4683
+ if (await fs23.exists(scaffoldPath))
4711
4684
  return readFile2(scaffoldPath, "utf-8");
4712
4685
  }
4713
4686
  return false;
4714
4687
  }
4715
4688
 
4716
4689
  // node/cli/utils/post.ts
4717
- async function create(data) {
4718
- const pagesPath = resolve14(userRoot, "pages");
4719
- const {
4720
- path: path17,
4721
- title
4722
- } = data;
4723
- const postPath = path17 || join9("posts", title);
4690
+ async function create(params) {
4691
+ const pagesPath = resolve14(userRoot, params.path || "pages");
4724
4692
  let counter = 0;
4725
4693
  while (true) {
4726
- let destinationPath = resolve14(pagesPath, postPath);
4727
- if (counter)
4728
- destinationPath = `${destinationPath}-${counter}`;
4729
- destinationPath = ensureSuffix2(".md", destinationPath);
4730
- if (!await exists(destinationPath)) {
4731
- const content = await genLayoutTemplate(data);
4694
+ const postFileName = `${params.title}${counter ? `-${counter}` : ""}`;
4695
+ const postFilePath = params.folder ? join9(postFileName, "index.md") : `${postFileName}.md`;
4696
+ const targetPath = resolve14(pagesPath, "posts", postFilePath);
4697
+ if (!await fs24.exists(targetPath)) {
4698
+ await fs24.ensureDir(dirname5(targetPath));
4699
+ const content = await genLayoutTemplate(params);
4732
4700
  try {
4733
- await writeFile2(destinationPath, content, "utf-8");
4734
- consola19.success(`[valaxy new]: successfully generated file ${colors17.magenta(destinationPath)}`);
4701
+ await fs24.writeFile(targetPath, content, "utf-8");
4702
+ consola19.success(`[valaxy new]: successfully generated file ${colors17.magenta(targetPath)}`);
4735
4703
  } catch (e) {
4736
4704
  console.log(e);
4737
- consola19.error(`[valaxy new]: failed to write file ${destinationPath}`);
4705
+ consola19.error(`[valaxy new]: failed to write file ${targetPath}`);
4738
4706
  consola19.warn(`You should run ${colors17.green("valaxy new")} in your valaxy project root directory.`);
4739
4707
  }
4740
- return destinationPath;
4708
+ return targetPath;
4741
4709
  }
4742
4710
  counter++;
4743
4711
  }
@@ -4763,6 +4731,11 @@ function registerNewCommand(cli2) {
4763
4731
  args.usage("$0 <title> -p [path] -l [layout]").positional("title", {
4764
4732
  describe: "The title of the new post",
4765
4733
  required: true
4734
+ }).option("folder", {
4735
+ alias: "f",
4736
+ type: "boolean",
4737
+ default: false,
4738
+ describe: "Generate a new post in a folder"
4766
4739
  }).option("path", {
4767
4740
  alias: "p",
4768
4741
  type: "string",
@@ -4778,12 +4751,13 @@ function registerNewCommand(cli2) {
4778
4751
  describe: "Generate post with the current date"
4779
4752
  }).strict().help();
4780
4753
  },
4781
- async ({ title, path: path17, date, layout }) => {
4754
+ async ({ title, folder, path: path17, date, layout }) => {
4782
4755
  await create({
4783
4756
  title,
4784
4757
  date,
4785
4758
  layout,
4786
- path: path17
4759
+ path: path17,
4760
+ folder
4787
4761
  });
4788
4762
  }
4789
4763
  );
@@ -103,8 +103,22 @@ interface PageFrontMatter extends Record<string, any> {
103
103
  /**
104
104
  * Title
105
105
  * @description 文章标题
106
- */
107
- title: string;
106
+ *
107
+ * ```md
108
+ * ---
109
+ * title: Post Title
110
+ * ---
111
+ * ```
112
+ *
113
+ * ```md
114
+ * ---
115
+ * title:
116
+ * en: Post Title
117
+ * zh-CN: 文章标题
118
+ * ---
119
+ * ```
120
+ */
121
+ title: string | Record<string, string>;
108
122
  date: string | number | Date;
109
123
  /**
110
124
  * Updated Time
@@ -338,7 +352,7 @@ interface PostFrontMatter extends PageFrontMatter {
338
352
  }
339
353
 
340
354
  interface FuseListItem extends Record<string, any> {
341
- title: string;
355
+ title: string | Record<string, string>;
342
356
  excerpt?: string;
343
357
  author: string;
344
358
  tags: string[];
@@ -3,7 +3,7 @@ import {
3
3
  registerDevCommand,
4
4
  run,
5
5
  startValaxyDev
6
- } from "../../chunk-SBITNE3A.js";
6
+ } from "../../chunk-UBPYRQAO.js";
7
7
  export {
8
8
  cli,
9
9
  registerDevCommand,
@@ -2,7 +2,7 @@ import { ViteSSGOptions } from 'vite-ssg';
2
2
  import * as vite from 'vite';
3
3
  import { UserConfig, InlineConfig, PluginOption, Plugin } from 'vite';
4
4
  import { MarkdownEnv } from 'unplugin-vue-markdown/types';
5
- import { S as SiteConfig, D as DefaultTheme, V as ValaxyConfig, a as ValaxyAddon, P as PartialDeep, R as RuntimeConfig, b as RedirectItem, U as UserSiteConfig } from '../config-D40juN_m.js';
5
+ import { S as SiteConfig, D as DefaultTheme, V as ValaxyConfig, a as ValaxyAddon, P as PartialDeep, R as RuntimeConfig, b as RedirectItem, U as UserSiteConfig } from '../config-DHcACRUt.js';
6
6
  import Vue from '@vitejs/plugin-vue';
7
7
  import { Options as Options$3 } from 'beasties';
8
8
  import { Hookable } from 'hookable';
@@ -47,7 +47,7 @@ declare module 'vite' {
47
47
  declare function createValaxyNode(options: ResolvedValaxyOptions): ValaxyNode;
48
48
 
49
49
  interface Options {
50
- hasSingleTheme: boolean;
50
+ codeCopyButtonTitle: string;
51
51
  theme: ThemeOptions;
52
52
  siteConfig?: SiteConfig;
53
53
  }
@@ -310,6 +310,9 @@ interface ValaxyExtendConfig {
310
310
  * @see https://github.com/btd/rollup-plugin-visualizer
311
311
  */
312
312
  visualizer?: PluginVisualizerOptions;
313
+ /**
314
+ * @see https://github.com/yuyinws/vitepress-plugin-group-icons
315
+ */
313
316
  groupIcons?: Partial<Options$2>;
314
317
  /**
315
318
  * unocss presets
@@ -50,7 +50,7 @@ import {
50
50
  startValaxyDev,
51
51
  toAtFS,
52
52
  transformObject
53
- } from "../chunk-SBITNE3A.js";
53
+ } from "../chunk-UBPYRQAO.js";
54
54
  export {
55
55
  ALL_ROUTE,
56
56
  EXCERPT_SEPARATOR,
@@ -1,5 +1,5 @@
1
- import { c as PostFrontMatter, d as PageFrontMatter } from '../config-D40juN_m.js';
2
- export { A as Album, D as DefaultTheme, E as ExcerptType, F as FuseListItem, P as PartialDeep, i as Photo, g as Pkg, b as RedirectItem, f as RedirectRule, R as RuntimeConfig, S as SiteConfig, e as SocialLink, U as UserSiteConfig, h as UserValaxyConfig, a as ValaxyAddon, V as ValaxyConfig } from '../config-D40juN_m.js';
1
+ import { c as PostFrontMatter, d as PageFrontMatter } from '../config-DHcACRUt.js';
2
+ export { A as Album, D as DefaultTheme, E as ExcerptType, F as FuseListItem, P as PartialDeep, i as Photo, g as Pkg, b as RedirectItem, f as RedirectRule, R as RuntimeConfig, S as SiteConfig, e as SocialLink, U as UserSiteConfig, h as UserValaxyConfig, a as ValaxyAddon, V as ValaxyConfig } from '../config-DHcACRUt.js';
3
3
  import { Header } from '@valaxyjs/utils';
4
4
  import '@vueuse/integrations/useFuse';
5
5
  import 'medium-zoom';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "valaxy",
3
3
  "type": "module",
4
- "version": "0.23.6",
4
+ "version": "0.24.0",
5
5
  "description": "📄 Vite & Vue powered static blog generator.",
6
6
  "author": {
7
7
  "email": "me@yunyoujun.cn",
@@ -45,6 +45,7 @@
45
45
  "client",
46
46
  "dist",
47
47
  "index.d.ts",
48
+ "shared",
48
49
  "shims.d.ts",
49
50
  "types"
50
51
  ],
@@ -59,14 +60,14 @@
59
60
  "@intlify/unplugin-vue-i18n": "^6.0.8",
60
61
  "@shikijs/transformers": "^3.7.0",
61
62
  "@types/katex": "^0.16.7",
62
- "@unhead/addons": "^2.0.11",
63
- "@unhead/schema-org": "^2.0.11",
64
- "@unhead/vue": "^2.0.11",
63
+ "@unhead/addons": "^2.0.12",
64
+ "@unhead/schema-org": "^2.0.12",
65
+ "@unhead/vue": "^2.0.12",
65
66
  "@vitejs/plugin-vue": "^6.0.0",
66
67
  "@vue/devtools-api": "7.7.2",
67
- "@vueuse/core": "^13.4.0",
68
- "@vueuse/integrations": "^13.4.0",
69
- "beasties": "^0.3.4",
68
+ "@vueuse/core": "^13.5.0",
69
+ "@vueuse/integrations": "^13.5.0",
70
+ "beasties": "^0.3.5",
70
71
  "birpc": "^2.4.0",
71
72
  "consola": "^3.4.2",
72
73
  "cross-spawn": "^7.0.6",
@@ -77,6 +78,7 @@
77
78
  "ejs": "^3.1.10",
78
79
  "escape-html": "^1.0.3",
79
80
  "fast-glob": "^3.3.3",
81
+ "feed": "^5.1.0",
80
82
  "floating-vue": "^5.2.2",
81
83
  "fs-extra": "^11.3.0",
82
84
  "fuse.js": "^7.1.0",
@@ -99,7 +101,7 @@
99
101
  "markdown-it-table-of-contents": "^0.9.0",
100
102
  "markdown-it-task-lists": "^2.1.1",
101
103
  "medium-zoom": "^1.1.0",
102
- "mermaid": "^11.7.0",
104
+ "mermaid": "^11.8.1",
103
105
  "mlly": "^1.7.4",
104
106
  "nprogress": "^0.2.0",
105
107
  "open": "10.1.0",
@@ -113,26 +115,25 @@
113
115
  "shiki": "^3.7.0",
114
116
  "star-markdown-css": "^0.5.3",
115
117
  "table": "^6.9.0",
116
- "unhead": "^2.0.11",
117
- "unocss": "^66.3.2",
118
+ "unhead": "^2.0.12",
119
+ "unocss": "^66.3.3",
118
120
  "unplugin-vue-components": "28.0.0",
119
- "unplugin-vue-markdown": "^29.0.0",
120
- "unplugin-vue-router": "^0.13.0",
121
+ "unplugin-vue-markdown": "^29.1.0",
122
+ "unplugin-vue-router": "^0.14.0",
121
123
  "vanilla-lazyload": "^19.1.3",
122
- "vite": "^7.0.0",
124
+ "vite": "^7.0.4",
123
125
  "vite-dev-rpc": "^1.1.0",
124
126
  "vite-plugin-vue-devtools": "^7.7.7",
125
127
  "vite-plugin-vue-layouts": "^0.11.0",
126
128
  "vite-ssg": "^28.0.0",
127
129
  "vite-ssg-sitemap": "^0.9.0",
128
- "vitepress-plugin-group-icons": "^1.6.0",
130
+ "vitepress-plugin-group-icons": "^1.6.1",
129
131
  "vue": "^3.5.17",
130
- "vue-i18n": "^11.1.7",
132
+ "vue-i18n": "^11.1.9",
131
133
  "vue-router": "^4.5.1",
132
134
  "yargs": "^18.0.0",
133
- "zfeed": "^0.2.3",
134
- "@valaxyjs/devtools": "0.23.6",
135
- "@valaxyjs/utils": "0.23.6"
135
+ "@valaxyjs/devtools": "0.24.0",
136
+ "@valaxyjs/utils": "0.24.0"
136
137
  },
137
138
  "devDependencies": {
138
139
  "@mdit-vue/plugin-component": "^2.1.4",
@@ -160,7 +161,7 @@
160
161
  "rollup-plugin-visualizer": "^6.0.3"
161
162
  },
162
163
  "resolutions": {
163
- "vite": "catalog:"
164
+ "vite": "catalog:build"
164
165
  },
165
166
  "scripts": {
166
167
  "clean": "rimraf dist",
@@ -0,0 +1 @@
1
+ export * from './utils'
@@ -0,0 +1,17 @@
1
+ /**
2
+ * translate object
3
+ *
4
+ * { 'en': 'English', 'zh-CN': '中文' }
5
+ *
6
+ * ```ts
7
+ * tObject({ 'en': 'English', 'zh-CN': '中文' }, 'zh-CN') // 中文
8
+ * tObject({ 'en': 'English', 'zh-CN': '中文' }, 'en') // English
9
+ * tObject({ 'en': 'English', 'zh-CN': '中文' }, 'fr') // English
10
+ * ```
11
+ */
12
+ export function tObject(data: string | Record<string, string>, lang: string): string {
13
+ if (typeof data === 'object') {
14
+ return data[lang] || Object.values(data)[0] || ''
15
+ }
16
+ return data
17
+ }
@@ -0,0 +1 @@
1
+ export * from './i18n'
@@ -44,8 +44,22 @@ export interface PageFrontMatter extends Record<string, any> {
44
44
  /**
45
45
  * Title
46
46
  * @description 文章标题
47
- */
48
- title: string
47
+ *
48
+ * ```md
49
+ * ---
50
+ * title: Post Title
51
+ * ---
52
+ * ```
53
+ *
54
+ * ```md
55
+ * ---
56
+ * title:
57
+ * en: Post Title
58
+ * zh-CN: 文章标题
59
+ * ---
60
+ * ```
61
+ */
62
+ title: string | Record<string, string>
49
63
  date: string | number | Date
50
64
  /**
51
65
  * Updated Time
package/types/node.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export interface FuseListItem extends Record<string, any> {
2
- title: string
2
+ title: string | Record<string, string>
3
3
  excerpt?: string
4
4
  author: string
5
5
  tags: string[]