fix(docs): align site with bilingual source pairs
This commit is contained in:
@@ -20,6 +20,7 @@ function fixture(): { root: string; pages: DocsPage[] } {
|
||||
mkdirSync(join(root, 'packages'), { recursive: true })
|
||||
writeFileSync(join(root, 'docs/a.md'), '# A\n')
|
||||
writeFileSync(join(root, 'docs/b.md'), '# B\n')
|
||||
writeFileSync(join(root, 'docs/x(y).md'), '# Parentheses\n')
|
||||
writeFileSync(join(root, 'packages/tool.ts'), 'one\ntwo\n')
|
||||
writeFileSync(join(root, 'packages/logo.svg'), '<svg/>\n')
|
||||
return {
|
||||
@@ -88,6 +89,46 @@ describe('rewriteMarkdown', () => {
|
||||
})).toBe(source)
|
||||
})
|
||||
|
||||
it('replaces the destination token without changing repeated titles or escapes', () => {
|
||||
const { root, pages } = fixture()
|
||||
const source = '[title](b.md "b.md") [escaped](x\\(y\\).md)\n'
|
||||
expect(rewriteMarkdown(source, {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe(
|
||||
'[title](./reference/b.md "b.md") '
|
||||
+ '[escaped](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/docs/x(y).md)\n',
|
||||
)
|
||||
})
|
||||
|
||||
it('routes a pair switcher across locales while ordinary links stay in locale', () => {
|
||||
const { root, pages } = fixture()
|
||||
writeFileSync(join(root, 'docs/a.zh.md'), '# A\n')
|
||||
const paired = pages.filter(page => page.source !== 'docs/a.md')
|
||||
paired.push(
|
||||
{
|
||||
locale: 'root', contentLocale: 'zh-CN', source: 'docs/a.zh.md', sourceAliases: ['docs/a.md'],
|
||||
route: 'guide/a.md', label: 'A', sidebar: 'zh-guide', section: 'Test', order: 1,
|
||||
},
|
||||
{
|
||||
locale: 'en', contentLocale: 'en-US', source: 'docs/a.md', sourceAliases: ['docs/a.zh.md'],
|
||||
route: 'en/guide/a.md', label: 'A', sidebar: 'en-guide', section: 'Test', order: 1,
|
||||
},
|
||||
)
|
||||
expect(rewriteMarkdown('[English](a.md) [B](b.md)\n', {
|
||||
locale: 'root',
|
||||
sourcePath: 'docs/a.zh.md',
|
||||
route: 'guide/a.md',
|
||||
pages: paired,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe('[English](../en/guide/a.md) [B](../reference-root/b.md)\n')
|
||||
})
|
||||
|
||||
it('fails loud when a relative target is missing', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(() => rewriteMarkdown('[missing](missing.md)\n', {
|
||||
@@ -102,14 +143,21 @@ describe('rewriteMarkdown', () => {
|
||||
})
|
||||
|
||||
describe('docsPages locale routes', () => {
|
||||
it('publishes the same canonical source at every corresponding locale route', () => {
|
||||
it('publishes every route in both locales and selects paired user sources', () => {
|
||||
const byRoute = new Map(docsPages.map(page => [page.route, page]))
|
||||
for (const page of docsPages.filter(page => page.locale === 'root')) {
|
||||
const counterpart = byRoute.get(`en/${page.route}`)
|
||||
expect(counterpart, page.route).toBeDefined()
|
||||
expect(counterpart?.locale).toBe('en')
|
||||
expect(counterpart?.source).toBe(page.source)
|
||||
expect(counterpart?.contentLocale).toBe(page.contentLocale)
|
||||
if (page.source.startsWith('docs/user/')) {
|
||||
expect(page.source).toMatch(/\.zh\.md$/)
|
||||
expect(page.contentLocale).toBe('zh-CN')
|
||||
expect(counterpart?.source).toBe(page.source.replace(/\.zh\.md$/, '.md'))
|
||||
expect(counterpart?.contentLocale).toBe('en-US')
|
||||
} else {
|
||||
expect(counterpart?.source).toBe(page.source)
|
||||
expect(counterpart?.contentLocale).toBe(page.contentLocale)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,6 +23,13 @@ interface Replacement {
|
||||
value: string
|
||||
}
|
||||
|
||||
interface DestinationRange {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
type RewritableNode = Extract<Nodes, { type: 'link' | 'image' | 'definition' }>
|
||||
|
||||
/** Inputs for rewriting one canonical Markdown page. */
|
||||
export interface RewriteMarkdownOptions {
|
||||
locale: DocsLocale
|
||||
@@ -44,6 +51,75 @@ function isExternalOrSiteAbsolute(url: string): boolean {
|
||||
|| /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)
|
||||
}
|
||||
|
||||
function skipWhitespace(source: string, start: number): number {
|
||||
let index = start
|
||||
while (/\s/.test(source[index] ?? '')) index += 1
|
||||
return index
|
||||
}
|
||||
|
||||
function labelEnd(source: string): number {
|
||||
const first = source.indexOf('[')
|
||||
if (first === -1) return -1
|
||||
let depth = 0
|
||||
for (let index = first; index < source.length; index += 1) {
|
||||
const char = source[index]
|
||||
if (char === '\\') {
|
||||
index += 1
|
||||
} else if (char === '[') {
|
||||
depth += 1
|
||||
} else if (char === ']') {
|
||||
depth -= 1
|
||||
if (depth === 0) return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function destinationRange(rawNode: string, type: 'link' | 'image' | 'definition'): DestinationRange {
|
||||
const endOfLabel = labelEnd(rawNode)
|
||||
if (endOfLabel === -1) {
|
||||
throw new Error(`project-doc-site: cannot locate label end in ${JSON.stringify(rawNode)}.`)
|
||||
}
|
||||
|
||||
let start: number
|
||||
if (type === 'definition') {
|
||||
const colon = rawNode.indexOf(':', endOfLabel + 1)
|
||||
if (colon === -1) {
|
||||
throw new Error(`project-doc-site: cannot locate definition separator in ${JSON.stringify(rawNode)}.`)
|
||||
}
|
||||
start = skipWhitespace(rawNode, colon + 1)
|
||||
} else {
|
||||
if (rawNode[endOfLabel + 1] !== '(') {
|
||||
throw new Error(`project-doc-site: cannot locate inline destination in ${JSON.stringify(rawNode)}.`)
|
||||
}
|
||||
start = skipWhitespace(rawNode, endOfLabel + 2)
|
||||
}
|
||||
|
||||
if (rawNode[start] === '<') {
|
||||
for (let index = start + 1; index < rawNode.length; index += 1) {
|
||||
if (rawNode[index] === '\\') index += 1
|
||||
else if (rawNode[index] === '>') return { start: start + 1, end: index }
|
||||
}
|
||||
throw new Error(`project-doc-site: cannot locate angle-bracket destination end in ${JSON.stringify(rawNode)}.`)
|
||||
}
|
||||
|
||||
let depth = 0
|
||||
for (let index = start; index < rawNode.length; index += 1) {
|
||||
const char = rawNode[index]
|
||||
if (char === '\\') {
|
||||
index += 1
|
||||
} else if (char === '(') {
|
||||
depth += 1
|
||||
} else if (char === ')') {
|
||||
if (depth === 0) return { start, end: index }
|
||||
depth -= 1
|
||||
} else if (/\s/.test(char ?? '') && depth === 0) {
|
||||
return { start, end: index }
|
||||
}
|
||||
}
|
||||
return { start, end: rawNode.length }
|
||||
}
|
||||
|
||||
function splitTarget(url: string): { path: string; suffix: string } {
|
||||
const boundary = url.search(/[?#]/)
|
||||
if (boundary === -1) return { path: url, suffix: '' }
|
||||
@@ -78,6 +154,12 @@ function sourceMap(pages: DocsPage[]): Map<string, Map<DocsLocale, DocsPage>> {
|
||||
return map
|
||||
}
|
||||
|
||||
function counterpartSource(source: string): string {
|
||||
return source.endsWith('.zh.md')
|
||||
? source.replace(/\.zh\.md$/, '.md')
|
||||
: source.replace(/\.md$/, '.zh.md')
|
||||
}
|
||||
|
||||
function resolveRepositoryTarget(sourceAbs: string, rawPath: string, repoRoot: string): { absPath: string; line?: number } {
|
||||
const decoded = decodePath(rawPath)
|
||||
let absPath = resolve(dirname(sourceAbs), decoded)
|
||||
@@ -129,13 +211,17 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
|
||||
const tree = fromMarkdown(source, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })
|
||||
const replacements: Replacement[] = []
|
||||
|
||||
const rewrite = (node: Nodes & { url: string }): void => {
|
||||
const rewrite = (node: RewritableNode): void => {
|
||||
if (isExternalOrSiteAbsolute(node.url)) return
|
||||
const { path, suffix } = splitTarget(node.url)
|
||||
if (path === '') return
|
||||
const { absPath, line } = resolveRepositoryTarget(sourceAbs, path, options.repoRoot)
|
||||
const targetPath = repoPath(absPath, options.repoRoot)
|
||||
const page = published.get(targetPath)?.get(options.locale)
|
||||
const isLanguageSwitcher = targetPath === counterpartSource(options.sourcePath)
|
||||
const targetLocale: DocsLocale = isLanguageSwitcher
|
||||
? options.locale === 'root' ? 'en' : 'root'
|
||||
: options.locale
|
||||
const page = published.get(targetPath)?.get(targetLocale)
|
||||
const nextUrl = page === undefined
|
||||
? githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
|
||||
: routeTarget(options.route, page.route, suffix)
|
||||
@@ -146,13 +232,10 @@ export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions)
|
||||
throw new Error(`project-doc-site: link ${JSON.stringify(node.url)} has no source offsets.`)
|
||||
}
|
||||
const rawNode = source.slice(start, end)
|
||||
const urlOffset = rawNode.lastIndexOf(node.url)
|
||||
if (urlOffset === -1) {
|
||||
throw new Error(`project-doc-site: cannot locate raw target ${JSON.stringify(node.url)} in ${JSON.stringify(rawNode)}.`)
|
||||
}
|
||||
const rawDestination = destinationRange(rawNode, node.type)
|
||||
replacements.push({
|
||||
start: start + urlOffset,
|
||||
end: start + urlOffset + node.url.length,
|
||||
start: start + rawDestination.start,
|
||||
end: start + rawDestination.end,
|
||||
value: nextUrl,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,18 @@
|
||||
"docs/development.md",
|
||||
"docs/i18n/README.md",
|
||||
"docs/i18n/translation-rules.md",
|
||||
"docs/user/develop/basic/config.md",
|
||||
"docs/user/develop/basic/index.md",
|
||||
"docs/user/develop/basic/tool.md",
|
||||
"docs/user/develop/framework/events.md",
|
||||
"docs/user/develop/framework/index.md",
|
||||
"docs/user/develop/framework/service.md",
|
||||
"docs/user/develop/practice/index.md",
|
||||
"docs/user/develop/practice/llm-adapter.md",
|
||||
"docs/user/guide/config.md",
|
||||
"docs/user/guide/index.md",
|
||||
"docs/user/guide/quickstart.md",
|
||||
"docs/user/index.md",
|
||||
"docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md",
|
||||
"docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md",
|
||||
"python/README.md",
|
||||
|
||||
Reference in New Issue
Block a user