mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-30 16:23:35 +08:00
The site already ships both languages, but / always served Chinese, so English readers had to find the switcher themselves. A Chinese browser now stays on /, everything else lands on /en. The decision runs as an inline script in index.html, next to the existing theme anti-flash block: deferring it to React would show English readers a full screen of Chinese first. It only fires on the root path — /en, /start and /en/start are deliberate destinations, and redirecting those would kick readers off any cross-language link they follow. A manual switch is stored in localStorage and wins over the browser language, otherwise switching would be undone on the next visit to the home page. resolveRootRedirect carries the same rules as a testable pure function. check-docs asserts the inline copy and the module agree on the storage key, the Chinese tag pattern and the root-only guard, so the two cannot drift.
148 lines
4.9 KiB
JavaScript
148 lines
4.9 KiB
JavaScript
import { promises as fs } from 'node:fs'
|
||
import path from 'node:path'
|
||
|
||
import { generateDocsManifest, paths } from './generate-docs-manifest.mjs'
|
||
|
||
const markdownTargetPattern = /!?\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g
|
||
const htmlTargetPattern = /<(?:a|img)\b[^>]*?\b(?:href|src)=["']([^"']+)["'][^>]*>/gi
|
||
|
||
function withoutSuffix(target) {
|
||
return target.split(/[?#]/, 1)[0]
|
||
}
|
||
|
||
function isExternal(target) {
|
||
return /^(?:[a-z]+:|\/\/|#)/i.test(target)
|
||
}
|
||
|
||
function normalizeRoute(target) {
|
||
const decoded = decodeURIComponent(withoutSuffix(target))
|
||
const withoutExtension = decoded
|
||
.replace(/(?:\/index)?\.html$/i, '')
|
||
.replace(/\.md$/i, '')
|
||
const normalized = `/${withoutExtension}`.replace(/\/+/g, '/').replace(/\/$/, '')
|
||
return normalized || '/'
|
||
}
|
||
|
||
async function exists(targetPath) {
|
||
return fs.access(targetPath).then(() => true, () => false)
|
||
}
|
||
|
||
function collectTargets(markdown) {
|
||
const targets = []
|
||
const prose = markdown.replace(/```[\s\S]*?```/g, '')
|
||
|
||
for (const match of prose.matchAll(markdownTargetPattern)) {
|
||
targets.push(match[1])
|
||
}
|
||
|
||
for (const match of prose.matchAll(htmlTargetPattern)) {
|
||
targets.push(match[1])
|
||
}
|
||
|
||
return [...new Set(targets)]
|
||
}
|
||
|
||
function isImageTarget(target) {
|
||
return /\.(?:avif|gif|jpe?g|png|svg|webp)$/i.test(withoutSuffix(target))
|
||
}
|
||
|
||
/**
|
||
* 语言分流的判定规则在两处各有一份:src/lib/locale.js(可测的模块)和 index.html 里的内联
|
||
* 副本(首帧就要跳,等不到模块加载)。两处漂移不会报错,只会让首页悄悄按旧规则分流,所以
|
||
* 在这里钉死:storage key 和中文判定正则必须逐字一致,且内联脚本必须只在根路径动手。
|
||
*/
|
||
async function checkLocaleRedirect() {
|
||
const problems = []
|
||
const moduleSource = await fs.readFile(path.join(paths.siteDir, 'src/lib/locale.js'), 'utf8')
|
||
const shellSource = await fs.readFile(path.join(paths.siteDir, 'index.html'), 'utf8')
|
||
|
||
const storageKey = moduleSource.match(/LOCALE_STORAGE_KEY\s*=\s*'([^']+)'/)?.[1]
|
||
const chineseTag = moduleSource.match(/const CHINESE_TAG\s*=\s*(\/.+\/i)/)?.[1]
|
||
|
||
if (!storageKey || !chineseTag) {
|
||
problems.push('src/lib/locale.js: 读不出 LOCALE_STORAGE_KEY 或 CHINESE_TAG,防漂移校验失效')
|
||
return problems
|
||
}
|
||
|
||
if (!shellSource.includes(`localStorage.getItem('${storageKey}')`)) {
|
||
problems.push(`index.html: 内联语言脚本没有用 '${storageKey}',与 src/lib/locale.js 不一致`)
|
||
}
|
||
|
||
if (!shellSource.includes(chineseTag)) {
|
||
problems.push(`index.html: 内联语言脚本的中文判定与 src/lib/locale.js 的 ${chineseTag} 不一致`)
|
||
}
|
||
|
||
// 少了这道判断,/en/start 这类地址也会被卷进分流。
|
||
if (!shellSource.includes("window.location.pathname.replace(/\\/+$/, '') !== ''")) {
|
||
problems.push('index.html: 内联语言脚本缺少「只在根路径生效」的判断')
|
||
}
|
||
|
||
return problems
|
||
}
|
||
|
||
async function main() {
|
||
const { records } = await generateDocsManifest()
|
||
const routes = new Set([
|
||
'/',
|
||
'/docs',
|
||
'/en',
|
||
'/en/docs',
|
||
...records.map((record) => record.path),
|
||
])
|
||
const problems = [...await checkLocaleRedirect()]
|
||
let checkedTargets = 0
|
||
|
||
for (const record of records) {
|
||
const sourceAbsolutePath = path.join(paths.repoDir, record.sourcePath)
|
||
const sourceDirectory = path.dirname(sourceAbsolutePath)
|
||
|
||
for (const target of collectTargets(record.content)) {
|
||
if (!target || isExternal(target)) {
|
||
continue
|
||
}
|
||
|
||
checkedTargets += 1
|
||
const pathname = withoutSuffix(target)
|
||
const image = isImageTarget(pathname)
|
||
|
||
if (pathname.startsWith('/')) {
|
||
const publicFile = path.join(paths.docsDir, 'public', pathname.replace(/^\//, ''))
|
||
const docsFile = path.join(paths.docsDir, pathname.replace(/^\//, ''))
|
||
const valid = image
|
||
? await exists(publicFile) || await exists(docsFile)
|
||
: routes.has(normalizeRoute(pathname)) || await exists(publicFile)
|
||
|
||
if (!valid) {
|
||
problems.push(`${record.sourcePath}: unresolved ${image ? 'image' : 'link'} ${target}`)
|
||
}
|
||
continue
|
||
}
|
||
|
||
const resolvedFile = path.resolve(sourceDirectory, pathname)
|
||
const markdownRoute = normalizeRoute(path.relative(paths.docsDir, resolvedFile))
|
||
const valid = image
|
||
? await exists(resolvedFile)
|
||
: await exists(resolvedFile)
|
||
|| routes.has(markdownRoute)
|
||
|| routes.has(normalizeRoute(`${path.relative(paths.docsDir, resolvedFile)}.md`))
|
||
|
||
if (!valid) {
|
||
problems.push(`${record.sourcePath}: unresolved ${image ? 'image' : 'link'} ${target}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (problems.length > 0) {
|
||
console.error(`Documentation check found ${problems.length} problem(s):`)
|
||
for (const problem of problems) {
|
||
console.error(`- ${problem}`)
|
||
}
|
||
process.exitCode = 1
|
||
return
|
||
}
|
||
|
||
console.log(`Documentation check passed: ${records.length} pages, ${checkedTargets} local links and images.`)
|
||
}
|
||
|
||
await main()
|