Skip to content

Commit 17a4766

Browse files
committed
docs: migration from VuePress to VitePress
1 parent a85b616 commit 17a4766

47 files changed

Lines changed: 2657 additions & 2183 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docs-deploy.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- name: Setup Node.js
2626
uses: actions/setup-node@v4
2727
with:
28-
node-version: 18
28+
node-version: 22
2929
- name: Prepare Java 21
3030
uses: actions/setup-java@v4
3131
with:
@@ -43,7 +43,7 @@ jobs:
4343
key: gradle-deps-core-${{ hashFiles('**/build.gradle.kts') }}
4444
restore-keys: |
4545
gradle-deps
46-
- name: Build VuePress site
46+
- name: Build VitePress site
4747
run: |
4848
cd docs-source
4949
yarn -i

docs-source/.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
/node_modules
2-
/src/.vuepress/.cache
3-
/src/.vuepress/.temp
2+
/src/.vitepress/cache
43
/dist

docs-source/build-dokka.sh

100644100755
File mode changed.

docs-source/package.json

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
{
22
"name": "pangutext_docs",
33
"license": "Apache-2.0",
4+
"type": "module",
45
"devDependencies": {
5-
"@mr-hope/vuepress-plugin-copy-code": "^1.30.0",
6-
"@vuepress/plugin-prismjs": "2.0.0-rc.0",
7-
"@vuepress/plugin-search": "2.0.0-rc.0",
8-
"@vuepress/plugin-shiki": "2.0.0-rc.0",
9-
"vuepress": "2.0.0-rc.0"
6+
"@mdit-vue/shared": "^2.1.3",
7+
"@nolebase/vitepress-plugin-enhanced-readabilities": "^2.18.2",
8+
"@nolebase/vitepress-plugin-git-changelog": "^2.18.2",
9+
"@types/markdown-it": "14.1.2",
10+
"@types/node": "^22.0.0",
11+
"markdown-it": "^14.1.0",
12+
"sass": "^1.89.2",
13+
"vitepress": "^1.6.4",
14+
"vitepress-plugin-comment-with-giscus": "^1.1.15",
15+
"vue": "^3.5.33"
1016
},
1117
"scripts": {
12-
"docs:dev": "vuepress dev src",
13-
"docs:build": "vuepress build src",
14-
"docs:build-gh-pages": "vuepress build src && touch dist/.nojekyll && sh build-dokka.sh"
15-
},
16-
"dependencies": {}
18+
"docs:dev": "vitepress dev src --host 0.0.0.0",
19+
"docs:build": "vitepress build src",
20+
"docs:preview": "vitepress preview src --host 0.0.0.0",
21+
"docs:build-gh-pages": "vitepress build src && touch dist/.nojekyll && sh build-dokka.sh"
22+
}
1723
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {
2+
GitChangelog,
3+
GitChangelogMarkdownSection
4+
} from '@nolebase/vitepress-plugin-git-changelog/vite';
5+
import { defineConfig } from 'vitepress';
6+
import { alignI18nAnchors } from './configs/anchors';
7+
import { createHomepageAlternates, createRootLocaleRedirect } from './configs/i18n';
8+
import { configs, pageLinkRefs } from './configs/template';
9+
import { env, markdown } from './configs/utils';
10+
import locales from './locales';
11+
12+
export default defineConfig({
13+
base: configs.website.base,
14+
title: configs.website.title,
15+
description: configs.website.locales.en.description,
16+
outDir: configs.dev.dest,
17+
cacheDir: '.vitepress/cache',
18+
vite: {
19+
css: {
20+
preprocessorOptions: {
21+
scss: {
22+
// Vite 5 defaults to Sass's legacy JS API, which Sass 2 will remove.
23+
api: 'modern-compiler'
24+
}
25+
}
26+
},
27+
optimizeDeps: {
28+
exclude: [
29+
'@nolebase/vitepress-plugin-enhanced-readabilities/client',
30+
'@nolebase/ui',
31+
'vitepress'
32+
]
33+
},
34+
server: {
35+
port: configs.dev.port
36+
},
37+
plugins: [
38+
GitChangelog({
39+
repoURL: () => configs.github.repo
40+
}),
41+
GitChangelogMarkdownSection({
42+
excludes: [
43+
'index.md',
44+
'en/index.md',
45+
'zh-cn/index.md'
46+
],
47+
sections: {
48+
disableContributors: true
49+
}
50+
})
51+
],
52+
ssr: {
53+
noExternal: [
54+
'@nolebase/vitepress-plugin-enhanced-readabilities',
55+
'@nolebase/ui'
56+
]
57+
}
58+
},
59+
head: [
60+
['meta', { name: 'color-scheme', content: 'light dark' }],
61+
['link', { rel: 'icon', href: configs.website.icon }]
62+
],
63+
transformHead: ({ page }) => [
64+
...createHomepageAlternates(page),
65+
...createRootLocaleRedirect(page)
66+
],
67+
locales: locales.locales,
68+
markdown: {
69+
image: {
70+
lazyLoading: true
71+
},
72+
config: (md) => {
73+
md.use(alignI18nAnchors);
74+
markdown.localizeContainerTitles(md);
75+
markdown.injectLinks(md, env.dev ? pageLinkRefs.dev : pageLinkRefs.prod, configs.website.base);
76+
}
77+
},
78+
themeConfig: {
79+
logo: configs.website.logo,
80+
socialLinks: [{
81+
icon: 'github',
82+
link: configs.github.repo
83+
}],
84+
search: {
85+
provider: 'local',
86+
options: {
87+
// VitePress has no isSearchable callback; empty rendered HTML excludes the root redirect page.
88+
_render: (src, renderEnv, md) => renderEnv.relativePath === 'index.md' ? '' : md.render(src, renderEnv),
89+
locales: {
90+
'zh-cn': {
91+
translations: {
92+
button: {
93+
buttonText: '搜索',
94+
buttonAriaLabel: '搜索'
95+
},
96+
modal: {
97+
noResultsText: '无法找到相关结果',
98+
resetButtonTitle: '清除查询条件',
99+
footer: {
100+
selectText: '选择',
101+
navigateText: '切换',
102+
closeText: '关闭'
103+
}
104+
}
105+
}
106+
}
107+
}
108+
}
109+
},
110+
footer: {
111+
message: 'Released under the Apache-2.0 License',
112+
copyright: 'Copyright © 2019 HighCapable'
113+
}
114+
}
115+
});

docs-source/src/.vuepress/configs/anchors.ts renamed to docs-source/src/.vitepress/configs/anchors.ts

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { existsSync, readFileSync, statSync } from 'node:fs';
22
import path from 'node:path';
33
import MarkdownIt from 'markdown-it';
4-
import type StateCore from 'markdown-it/lib/rules_core/state_core';
5-
import type Token from 'markdown-it/lib/token';
64
import { slugify } from '@mdit-vue/shared';
5+
import type { VitePressMarkdownIt } from './types';
76

87
interface HeadingAnchor {
98
alignedId: string;
@@ -21,10 +20,19 @@ interface CachedHeadingLookup {
2120
mtimeMs: number;
2221
}
2322

23+
interface MarkdownToken {
24+
children: MarkdownToken[] | null;
25+
content: string;
26+
tag: string;
27+
type: string;
28+
attrGet: (name: string) => string | null;
29+
attrSet: (name: string, value: string) => void;
30+
}
31+
2432
interface HeadingToken {
25-
inlineToken: Token;
33+
inlineToken: MarkdownToken;
2634
pathKey: string;
27-
token: Token;
35+
token: MarkdownToken;
2836
}
2937

3038
interface LinkResolveContext {
@@ -52,9 +60,9 @@ const createHeadingPathTracker = (): ((level: number) => string) => {
5260
};
5361
};
5462

55-
const getHeadingLevel = (token: Token): number => Number.parseInt(token.tag.slice(1), 10);
63+
const getHeadingLevel = (token: MarkdownToken): number => Number.parseInt(token.tag.slice(1), 10);
5664

57-
const getHeadingText = (inlineToken: Token): string =>
65+
const getHeadingText = (inlineToken: MarkdownToken): string =>
5866
(inlineToken.children ?? [])
5967
.filter((child) => child.type === 'text' || child.type === 'code_inline')
6068
.map((child) => child.content)
@@ -72,7 +80,7 @@ const createUniqueSlug = (candidate: string, usedSlugs: Set<string>): string =>
7280
return resolvedSlug;
7381
};
7482

75-
const collectHeadings = (tokens: Token[]): HeadingAnchor[] => {
83+
const collectHeadings = (tokens: MarkdownToken[]): HeadingAnchor[] => {
7684
const nextPathKey = createHeadingPathTracker();
7785
const usedSlugs = new Set<string>();
7886
const headings: HeadingAnchor[] = [];
@@ -165,7 +173,7 @@ const resolveHeadingLookup = (filePathRelative: string | null | undefined): Head
165173
return lookup;
166174
};
167175

168-
const collectHeadingTokens = (tokens: Token[]): HeadingToken[] => {
176+
const collectHeadingTokens = (tokens: MarkdownToken[]): HeadingToken[] => {
169177
const nextPathKey = createHeadingPathTracker();
170178
const headings: HeadingToken[] = [];
171179
for (let index = 0; index < tokens.length; index += 1) {
@@ -186,7 +194,7 @@ const collectHeadingTokens = (tokens: Token[]): HeadingToken[] => {
186194
return headings;
187195
};
188196

189-
const syncPermalinkHref = (inlineToken: Token, id: string): void => {
197+
const syncPermalinkHref = (inlineToken: MarkdownToken, id: string): void => {
190198
for (const child of inlineToken.children ?? []) {
191199
if (child.type !== 'link_open') {
192200
continue;
@@ -218,22 +226,6 @@ const hasUriScheme = (value: string): boolean => /^[a-z][a-z\d+.-]*:/i.test(valu
218226
const normalizeFilePathRelative = (filePathRelative: string): string =>
219227
filePathRelative.replace(/\\/g, '/');
220228

221-
const normalizeBase = (base = '/'): string => {
222-
const trimmed = base.trim();
223-
if (trimmed === '' || trimmed === '/') {
224-
return '/';
225-
}
226-
return `/${trimmed.replace(/^\/+|\/+$/g, '')}/`;
227-
};
228-
229-
const toSiteRoutePath = (filePathRelative: string, base = '/'): string => {
230-
const normalizedFilePath = normalizeFilePathRelative(filePathRelative);
231-
const routePath = normalizedFilePath.endsWith('/index.md')
232-
? normalizedFilePath.slice(0, -'index.md'.length)
233-
: normalizedFilePath.replace(/\.md$/, '.html');
234-
return `${normalizeBase(base)}${routePath}`.replace(/\/{2,}/g, '/');
235-
};
236-
237229
const resolveTargetFilePathRelative = (currentFilePathRelative: string, rawPath: string, base = '/'): string | null => {
238230
const normalizedCurrentPath = normalizeFilePathRelative(currentFilePathRelative);
239231
if (rawPath.length === 0) {
@@ -264,9 +256,10 @@ const resolveTargetFilePathRelative = (currentFilePathRelative: string, rawPath:
264256
return `${path.posix.join(path.posix.dirname(normalizedCurrentPath), rawPath)}.md`;
265257
};
266258

267-
export const alignI18nAnchors = (md: MarkdownIt): void => {
268-
md.core.ruler.after('anchor', 'align-i18n-anchors', (state: StateCore) => {
269-
const lookup = resolveHeadingLookup(state.env.filePathRelative);
259+
/** Aligns localized heading IDs with the matching English document structure. */
260+
export const alignI18nAnchors = (md: VitePressMarkdownIt): void => {
261+
md.core.ruler.after('anchor', 'align-i18n-anchors', (state) => {
262+
const lookup = resolveHeadingLookup(state.env.relativePath);
270263
if (!lookup) {
271264
return;
272265
}
@@ -285,6 +278,7 @@ export const alignI18nAnchors = (md: MarkdownIt): void => {
285278
});
286279
};
287280

281+
/** Rewrites localized heading fragments while preserving VitePress source-relative paths. */
288282
export const resolveI18nLink = (context: LinkResolveContext, rawHref: string): string => {
289283
if (!context.filePathRelative || !rawHref.includes('#')) {
290284
return rawHref;
@@ -308,5 +302,5 @@ export const resolveI18nLink = (context: LinkResolveContext, rawHref: string): s
308302
if (normalizeFilePathRelative(targetFilePathRelative) === currentFilePathRelative) {
309303
return `#${resolvedHash}`;
310304
}
311-
return `${toSiteRoutePath(targetFilePathRelative, context.base)}#${resolvedHash}`;
305+
return `${rawPath}#${resolvedHash}`;
312306
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type giscusTalk from 'vitepress-plugin-comment-with-giscus';
2+
3+
type GiscusOptions = Parameters<typeof giscusTalk>[0];
4+
5+
/** Lists source pages that must not mount the Giscus comment section. */
6+
export const giscusExcludedPages = [
7+
'en/about/about.md',
8+
'zh-cn/about/about.md'
9+
];
10+
11+
/** Defines the GitHub Discussions repository, category, and localized Giscus behavior. */
12+
export const giscusOptions = {
13+
repo: 'BetterAndroid/PanguText',
14+
repoId: 'R_kgDOMRzxbg',
15+
category: 'General',
16+
categoryId: 'DIC_kwDOMRzxbs4DB_ZV',
17+
inputPosition: 'bottom',
18+
locales: {
19+
'en-US': 'en',
20+
'zh-CN': 'zh-CN'
21+
},
22+
homePageShowComment: false
23+
} satisfies GiscusOptions;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { HeadConfig } from 'vitepress';
2+
import { configs } from './template';
3+
4+
export type DocsLocale = keyof typeof configs.website.locales;
5+
6+
const supportedLocales = Object.keys(configs.website.locales) as DocsLocale[];
7+
const homepagePaths = new Set(['index.md', 'en/index.md', 'zh-cn/index.md']);
8+
9+
/** Locale used when the visitor has not selected a documentation language. */
10+
export const defaultLocale: DocsLocale = 'en';
11+
12+
/** Storage key used to remember the last documentation locale visited by the user. */
13+
export const localeStorageKey = 'pangutext-docs-locale';
14+
15+
/** Returns a supported locale when the route belongs to a localized documentation tree. */
16+
export const resolveRouteLocale = (path: string) => path
17+
.split('/')
18+
.find((segment): segment is DocsLocale => supportedLocales.includes(segment as DocsLocale));
19+
20+
/** Returns a stored locale when valid, otherwise falling back to English. */
21+
export const resolveStoredLocale = (locale: string | null) => supportedLocales
22+
.find((supportedLocale) => supportedLocale === locale) ?? defaultLocale;
23+
24+
/** Creates reciprocal hreflang links for every localized homepage and the x-default root. */
25+
export const createHomepageAlternates = (page: string): HeadConfig[] => {
26+
if (!homepagePaths.has(page)) return [];
27+
const siteRoot = `${configs.github.page}/`;
28+
const localeLinks = Object.entries(configs.website.locales).map(([locale, options]) => [
29+
'link',
30+
{
31+
rel: 'alternate',
32+
hreflang: options.lang,
33+
href: `${siteRoot}${locale}/`
34+
}
35+
] satisfies HeadConfig);
36+
return [
37+
...localeLinks,
38+
['link', { rel: 'alternate', hreflang: 'x-default', href: siteRoot }]
39+
];
40+
};
41+
42+
/** Creates the root-page fallback redirect used when static hosting cannot return HTTP 302. */
43+
export const createRootLocaleRedirect = (page: string): HeadConfig[] => {
44+
if (page !== 'index.md') return [];
45+
const base = configs.website.base;
46+
const script = `(() => { let locale = '${defaultLocale}'; try { const saved = localStorage.getItem('${localeStorageKey}'); if (saved === 'en' || saved === 'zh-cn') locale = saved; } catch {} location.replace('${base}' + locale + '/'); })();`;
47+
return [['script', {}, script]];
48+
};

0 commit comments

Comments
 (0)