@master/internal-site 0.1.3 → 0.2.1
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.
- package/README.md +37 -0
- package/dist/astro.mjs +92 -41
- package/dist/config.d.mts +6 -0
- package/dist/localization-LfIbIysE.mjs +356 -0
- package/dist/localization.d.mts +69 -0
- package/dist/localization.mjs +1 -0
- package/package.json +12 -9
- package/src/astro/content-pages.ts +30 -11
- package/src/astro/index.ts +9 -5
- package/src/astro/localized-mdx.ts +45 -0
- package/src/astro/routes.template.ts +3 -0
- package/src/astro/site-context.ts +10 -2
- package/src/benchmarks/BenchmarkChart.astro +4 -1
- package/src/benchmarks/BenchmarkMetricTable.astro +1 -1
- package/src/benchmarks/BenchmarkStackedBars.astro +2 -2
- package/src/benchmarks/BenchmarkStatus.astro +3 -1
- package/src/benchmarks/localization.ts +22 -0
- package/src/blog/article/BlogArticleEnd.astro +6 -9
- package/src/blog/article/BlogArticleMasthead.astro +5 -8
- package/src/config.ts +3 -0
- package/src/css/foundation-data/specimen-content.ts +2 -2
- package/src/css/markdown.ts +52 -11
- package/src/demo/Demo.svelte +6 -4
- package/src/demo/DemoAsset.astro +5 -3
- package/src/demo/DemoAxes.svelte +4 -2
- package/src/demo/DemoCatalog.astro +8 -6
- package/src/demo/DemoContainer.svelte +5 -3
- package/src/demo/DemoControls.svelte +3 -1
- package/src/demo/DemoCopyButton.svelte +6 -4
- package/src/demo/DemoCopyGroup.svelte +4 -2
- package/src/demo/DemoExample.astro +3 -1
- package/src/demo/DemoFeatureSupport.svelte +3 -1
- package/src/demo/DemoGeneratedCSS.svelte +3 -1
- package/src/demo/DemoIndex.astro +5 -3
- package/src/demo/DemoLegend.svelte +3 -1
- package/src/demo/DemoMeasure.svelte +4 -2
- package/src/demo/DemoMedia.svelte +3 -1
- package/src/demo/DemoMotion.svelte +5 -3
- package/src/demo/DemoPaletteGroups.svelte +4 -2
- package/src/demo/DemoStyleComparison.astro +5 -3
- package/src/demo/DemoThemeComparison.astro +4 -2
- package/src/demo/DemoTokenTable.astro +4 -2
- package/src/demo/DemoViewTransition.astro +3 -1
- package/src/demo/DemoViewport.svelte +13 -11
- package/src/demo/DemoWaterfall.astro +3 -1
- package/src/demo/ProjectStyleExample.astro +4 -1
- package/src/demo/foundations/FoundationDialog.astro +5 -2
- package/src/demo/foundations/FoundationTokensView.astro +16 -5
- package/src/demo/foundations/Palette.svelte +9 -7
- package/src/demo/foundations/dialog-copy.d.ts +1 -0
- package/src/demo/foundations/dialog-copy.js +5 -0
- package/src/docs-shell/common/i18n.config.d.ts +1 -1
- package/src/docs-shell/common/i18n.config.js +4 -12
- package/src/docs-shell/common/locales.d.ts +8 -0
- package/src/docs-shell/common/locales.js +18 -0
- package/src/docs-shell/common/terminology.json +32 -0
- package/src/docs-shell/common/translations.d.ts +3 -0
- package/src/docs-shell/common/translations.js +294 -0
- package/src/docs-shell/components/ArticleHeader.astro +3 -3
- package/src/docs-shell/components/CodeCopyButton.astro +3 -1
- package/src/docs-shell/components/CodeTabBar.astro +5 -3
- package/src/docs-shell/components/DocHeader.svelte +3 -4
- package/src/docs-shell/components/DocHeading.astro +3 -1
- package/src/docs-shell/components/DocMenuButton.svelte +4 -5
- package/src/docs-shell/components/DocSidebar.svelte +1 -2
- package/src/docs-shell/components/DocumentCodeTable.astro +3 -1
- package/src/docs-shell/components/DocumentIllustration.astro +3 -1
- package/src/docs-shell/components/DocumentKeyList.astro +5 -3
- package/src/docs-shell/components/DocumentNamespaceTable.astro +4 -2
- package/src/docs-shell/components/DocumentOptions.astro +3 -1
- package/src/docs-shell/components/DocumentationIndex.svelte +6 -7
- package/src/docs-shell/components/GeneratedCSS.astro +3 -1
- package/src/docs-shell/components/Link.svelte +3 -2
- package/src/docs-shell/components/NotFound.astro +3 -4
- package/src/docs-shell/components/PageNavs.astro +2 -3
- package/src/docs-shell/components/SearchDialog.svelte +13 -16
- package/src/docs-shell/components/TimeAgo.svelte +6 -6
- package/src/docs-shell/contexts/search.ts +1 -1
- package/src/docs-shell/factories/Page.astro +10 -5
- package/src/docs-shell/factories/create-page.ts +33 -6
- package/src/docs-shell/layouts/BlogArticle.astro +3 -11
- package/src/docs-shell/remark/slug-and-toc.js +5 -7
- package/src/docs-shell/scripts/code.ts +5 -2
- package/src/docs-shell/site.ts +9 -1
- package/src/docs-shell/utils/document-copy.d.ts +2 -0
- package/src/docs-shell/utils/document-copy.js +18 -0
- package/src/docs-shell/utils/generate-metadata.ts +2 -0
- package/src/docs-shell/utils/i18n-pathname.ts +13 -2
- package/src/docs-shell/utils/i18n.ts +4 -1
- package/src/docs-shell/utils/localized-metadata.ts +22 -0
- package/src/docs-shell/utils/markdown-links.ts +61 -0
- package/src/docs-shell/utils/markdown-tree.ts +19 -6
- package/src/docs-shell/utils/resolve-heading.d.ts +1 -0
- package/src/docs-shell/utils/resolve-heading.js +21 -0
- package/src/docs-shell/utils/search-pages.ts +75 -22
- package/src/home/hero.css +5 -0
- package/src/layouts/Document.astro +1 -1
- package/src/scripts/localization.ts +239 -0
- package/src/scripts/mdx-sample.ts +12 -0
- package/src/scripts/static-social.ts +17 -3
- package/src/scripts/style-delivery.mjs +14 -4
- package/src/sections/guide.astro +3 -1
- package/src/styles/docs-shell/prose.css +1 -1
- package/src/virtual.d.ts +5 -1
- package/src/blog/article/language.ts +0 -29
package/README.md
CHANGED
|
@@ -68,6 +68,43 @@ Import individual component subpaths, including `.astro` and `.svelte` extension
|
|
|
68
68
|
|
|
69
69
|
The CSS-specific subpaths require the matching optional compiler, tooling, schema, preset and language-service peers. Keep them out of browser-only imports. Shared public assets are served in development and copied into build output; consumer public files take precedence. No install hook writes consumer source files.
|
|
70
70
|
|
|
71
|
+
## Local MDX translations
|
|
72
|
+
|
|
73
|
+
English uses the root URL. The shared locale definitions map `ja`, `tw`, `ko`
|
|
74
|
+
and `cn` to matching URL prefixes and the HTML language tags `ja`, `zh-TW`,
|
|
75
|
+
`ko` and `zh-CN`. Keep complete sibling editions in `content.<locale>.mdx`;
|
|
76
|
+
`metadata.ts` retains English identity, dates, authors and ordering. Translated
|
|
77
|
+
frontmatter supplies titles, summaries, image descriptions and article-specific
|
|
78
|
+
demonstration copy. Keep explicit heading IDs identical across editions.
|
|
79
|
+
|
|
80
|
+
`build/localization.ts` supplies filesystem discovery, localized document reads,
|
|
81
|
+
dependency fingerprints, explicit review operations and completeness checks.
|
|
82
|
+
Configure `i18n.validation` with the consumer's resource-loader and review-index
|
|
83
|
+
URLs. The integration validates them on every production build, including direct
|
|
84
|
+
`astro build` calls. Source fingerprints include metadata and imported local or
|
|
85
|
+
shared source files. A missing edition or changed source/translation fails the
|
|
86
|
+
gate until independently reviewed. Builds never refresh review records.
|
|
87
|
+
|
|
88
|
+
The resource loader also exposes local and shared UI dictionaries to the same
|
|
89
|
+
gate. Common controls use `common/translations.js`; product copy belongs to each
|
|
90
|
+
consumer. Both sites follow `common/terminology.json`. Use `sharedInterfaceKeys()`
|
|
91
|
+
to include newly introduced literal interface requests in the UI inventory.
|
|
92
|
+
|
|
93
|
+
The route registry imports each selected MDX edition through Astro's propagated
|
|
94
|
+
asset boundary. Nested MDX imports inherit their parent's language through Vite;
|
|
95
|
+
missing prose editions and draft includes throw. Prose-free fenced companion
|
|
96
|
+
examples stay shared. Do not import every edition eagerly or reimplement the
|
|
97
|
+
resolver in a consumer. Per-request `documentCopyOf(Astro)` and `demoCopy` let
|
|
98
|
+
shared demonstrations read localized prose while retaining one executable model.
|
|
99
|
+
|
|
100
|
+
Consumers derive navigation, search, Markdown, feeds and llms exports from their
|
|
101
|
+
public inventory and the selected local edition. Use `extractSearchNodesFromMdx`
|
|
102
|
+
for authored MDX and `extractSearchNodesFromMarkdown` for portable output; the
|
|
103
|
+
latter retains explicit anchor targets and GFM tables. `transformMarkdownLinks`
|
|
104
|
+
edits reader links without changing inline or fenced examples. Withdraw stale
|
|
105
|
+
owned exports when a source becomes a draft or is removed. Required Reference
|
|
106
|
+
inputs reject draft status instead of publishing a partial catalog.
|
|
107
|
+
|
|
71
108
|
## Development and releases
|
|
72
109
|
|
|
73
110
|
From the Master repository, run `bun run build:packages`, `bun run --cwd packages/internal-site type-check` and `bun run --cwd packages/internal-site test`. Astro checks `.astro` implementations and Svelte checks interactive components separately. The root `build` builds the Node entrypoints before the site.
|
package/dist/astro.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
+
import { d as validateSiteTranslations, f as isSharedMdxSample, h as createHeadingResolver, m as locales } from "./localization-LfIbIysE.mjs";
|
|
1
2
|
import { svgoOptimizer } from "astro/config";
|
|
2
3
|
import svelte from "@astrojs/svelte";
|
|
3
4
|
import mdx from "@astrojs/mdx";
|
|
4
|
-
import { unified } from "@astrojs/markdown-remark";
|
|
5
|
+
import { parseFrontmatter, unified } from "@astrojs/markdown-remark";
|
|
5
6
|
import masterCSS from "@master/css-astro";
|
|
6
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
8
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
9
|
import { cp, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
9
10
|
import { createReadStream, existsSync } from "node:fs";
|
|
10
11
|
import { isCSSRequest, searchForWorkspaceRoot, transformWithOxc } from "vite";
|
|
11
|
-
import { optimize } from "svgo";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
-
import Slugger from "github-slugger";
|
|
14
|
-
import { SKIP, visit } from "unist-util-visit";
|
|
15
13
|
import { parse, parseExpressionAt } from "acorn";
|
|
14
|
+
import { optimize } from "svgo";
|
|
15
|
+
import { SKIP, visit } from "unist-util-visit";
|
|
16
16
|
// src/astro/module-id.ts
|
|
17
17
|
/** A Vite module id without its query or hash. */
|
|
18
18
|
function cleanId(id) {
|
|
@@ -57,6 +57,33 @@ async function prepareContentPages(site, root, contentPrefix) {
|
|
|
57
57
|
const destination = join(root, ".astro/master-site/pages");
|
|
58
58
|
const pages = {};
|
|
59
59
|
const wanted = /* @__PURE__ */ new Set();
|
|
60
|
+
const localizedDocuments = {};
|
|
61
|
+
const localizedMetadata = {};
|
|
62
|
+
const availableLocales = {};
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
if (!file.endsWith("/content.mdx")) continue;
|
|
65
|
+
const directory = dirname(file);
|
|
66
|
+
const route = relative(content, directory).replaceAll("\\", "/");
|
|
67
|
+
const pathname = "/" + route.split("/").filter((part) => !/^\(.+\)$/.test(part)).join("/");
|
|
68
|
+
const key = `${contentPrefix}/${route ? route + "/" : ""}page.astro`;
|
|
69
|
+
const section = route.split("/").find((part) => !/^\(.+\)$/.test(part)) ?? "home";
|
|
70
|
+
const locales = site.i18n.sectionLocales?.[section] ?? site.i18n.locales;
|
|
71
|
+
const metadataSource = await readFile(join(directory, "metadata.ts"), "utf8").catch(() => "");
|
|
72
|
+
const draft = parseFrontmatter(await readFile(file, "utf8")).frontmatter.draft === true || /\bdraft\s*:\s*true\b/.test(metadataSource);
|
|
73
|
+
const documents = {};
|
|
74
|
+
const metadata = {};
|
|
75
|
+
for (const locale of draft ? [] : locales) {
|
|
76
|
+
const variant = locale === site.i18n.defaultLocale ? file : join(directory, `content.${locale}.mdx`);
|
|
77
|
+
if (!files.has(variant)) continue;
|
|
78
|
+
const { frontmatter } = parseFrontmatter(await readFile(variant, "utf8"));
|
|
79
|
+
if (frontmatter.draft === true) continue;
|
|
80
|
+
documents[locale] = pathToFileURL(variant);
|
|
81
|
+
metadata[locale] = frontmatter;
|
|
82
|
+
}
|
|
83
|
+
localizedDocuments[pathname] = documents;
|
|
84
|
+
localizedMetadata[pathname] = metadata;
|
|
85
|
+
availableLocales[key] = Object.keys(documents);
|
|
86
|
+
}
|
|
60
87
|
for (const file of files) {
|
|
61
88
|
if (!file.endsWith("/content.mdx")) continue;
|
|
62
89
|
const directory = dirname(file);
|
|
@@ -87,15 +114,6 @@ export const generateMetadata = createPageMetadata(metadata, dictionaries)
|
|
|
87
114
|
pages[`${contentPrefix}/${route ? route + "/" : ""}page.astro`] = pathToFileURL(output);
|
|
88
115
|
}
|
|
89
116
|
for (const stale of await contentFiles(destination).catch(() => [])) if (stale.endsWith("/page.astro") && !wanted.has(stale)) await unlink(stale);
|
|
90
|
-
const availableLocales = {};
|
|
91
|
-
for (const file of files) {
|
|
92
|
-
if (!file.endsWith("/content.mdx")) continue;
|
|
93
|
-
const directory = dirname(file);
|
|
94
|
-
const route = relative(content, directory).replaceAll("\\", "/");
|
|
95
|
-
const section = route.split("/").find((segment) => !/^\(.+\)$/.test(segment)) ?? "home";
|
|
96
|
-
if (!site.i18n.translatedOnly?.includes(section)) continue;
|
|
97
|
-
availableLocales[`${contentPrefix}/${route ? route + "/" : ""}page.astro`] = site.i18n.locales.filter((locale) => locale === site.i18n.defaultLocale || files.has(join(directory, `content.${locale}.mdx`)));
|
|
98
|
-
}
|
|
99
117
|
for (const section of site.i18n.translatedOnly ?? []) {
|
|
100
118
|
const key = `${contentPrefix}/${section}/page.astro`;
|
|
101
119
|
if (!files.has(join(content, section, "page.astro"))) continue;
|
|
@@ -104,7 +122,9 @@ export const generateMetadata = createPageMetadata(metadata, dictionaries)
|
|
|
104
122
|
}
|
|
105
123
|
return {
|
|
106
124
|
pages,
|
|
107
|
-
availableLocales
|
|
125
|
+
availableLocales,
|
|
126
|
+
localizedDocuments,
|
|
127
|
+
localizedMetadata
|
|
108
128
|
};
|
|
109
129
|
}
|
|
110
130
|
// src/astro/server-styles.ts
|
|
@@ -157,6 +177,52 @@ function svgMarkup() {
|
|
|
157
177
|
}
|
|
158
178
|
};
|
|
159
179
|
}
|
|
180
|
+
// src/astro/localized-mdx.ts
|
|
181
|
+
/** Nested article MDX follows its importer; executable examples keep one source. */
|
|
182
|
+
function localizedMdx({ locales: locales$1 = locales, defaultLocale = "en" } = {}) {
|
|
183
|
+
return {
|
|
184
|
+
name: "@master/internal-site:localized-mdx",
|
|
185
|
+
enforce: "pre",
|
|
186
|
+
async resolveId(source, importer, options) {
|
|
187
|
+
if (!importer || !cleanId(importer).endsWith(".mdx")) return;
|
|
188
|
+
const language = cleanId(importer).match(/\.([a-z]{2})\.mdx$/)?.[1];
|
|
189
|
+
const locale = language && locales$1.includes(language) ? language : defaultLocale;
|
|
190
|
+
const resolved = await this.resolve(source, importer, {
|
|
191
|
+
...options,
|
|
192
|
+
skipSelf: true
|
|
193
|
+
});
|
|
194
|
+
if (!resolved || resolved.external) return;
|
|
195
|
+
const id = cleanId(resolved.id);
|
|
196
|
+
if (!id.endsWith(".mdx") || id.startsWith("\0")) return;
|
|
197
|
+
const suffix = resolved.id.slice(id.length) || source.slice(cleanId(source).length);
|
|
198
|
+
const file = id.startsWith("file:") ? fileURLToPath(id) : id.startsWith("/@fs/") ? id.slice(5) : id;
|
|
199
|
+
if (!isAbsolute(file)) return;
|
|
200
|
+
const original = file.replace(/\.([a-z]{2})\.mdx$/, (match, language) => locales$1.includes(language) ? ".mdx" : match);
|
|
201
|
+
this.addWatchFile(original);
|
|
202
|
+
const originalSource = await readFile(original, "utf8");
|
|
203
|
+
const originalMetadata = parseFrontmatter(originalSource).frontmatter;
|
|
204
|
+
if (originalMetadata.draft === true || originalMetadata.disabled === true) throw new Error(`Unpublished source MDX include: ${original}`);
|
|
205
|
+
if (locale === defaultLocale || !originalSource.trim() || isSharedMdxSample(original, originalSource)) return {
|
|
206
|
+
...resolved,
|
|
207
|
+
id: original + suffix
|
|
208
|
+
};
|
|
209
|
+
const translated = original.replace(/\.mdx$/, `.${locale}.mdx`);
|
|
210
|
+
this.addWatchFile(translated);
|
|
211
|
+
let translatedSource;
|
|
212
|
+
try {
|
|
213
|
+
translatedSource = await readFile(translated, "utf8");
|
|
214
|
+
} catch (cause) {
|
|
215
|
+
throw new Error(`Missing translated MDX include: ${translated}`, { cause });
|
|
216
|
+
}
|
|
217
|
+
const metadata = parseFrontmatter(translatedSource).frontmatter;
|
|
218
|
+
if (metadata.draft === true || metadata.disabled === true) throw new Error(`Unpublished translated MDX include: ${translated}`);
|
|
219
|
+
return {
|
|
220
|
+
...resolved,
|
|
221
|
+
id: translated + suffix
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
160
226
|
// src/astro/style-assets.ts
|
|
161
227
|
/** Collapse only the compiler's unconditional, same-directory asset links.
|
|
162
228
|
* This is concatenation of already compiled CSS, not a second CSS compiler.
|
|
@@ -259,41 +325,23 @@ async function compactStyleAssets(output) {
|
|
|
259
325
|
await writeFile(file, html);
|
|
260
326
|
}
|
|
261
327
|
}
|
|
262
|
-
// src/docs-shell/utils/resolve-heading.js
|
|
263
|
-
function resolveHeading(title, id) {
|
|
264
|
-
const explicit = typeof title === "string" && title.match(/\s+\{#([\w-]+)\}$/);
|
|
265
|
-
if (explicit) {
|
|
266
|
-
title = title.slice(0, explicit.index);
|
|
267
|
-
id = explicit[1];
|
|
268
|
-
}
|
|
269
|
-
const words = title?.split?.(" ");
|
|
270
|
-
if (words?.pop() === "[sr-only]") return {
|
|
271
|
-
title: words.join(" "),
|
|
272
|
-
id: id?.replace("-sr-only", "")?.replace("-[sr-only]", ""),
|
|
273
|
-
className: "sr-only"
|
|
274
|
-
};
|
|
275
|
-
return {
|
|
276
|
-
title,
|
|
277
|
-
id
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
328
|
// src/docs-shell/remark/slug-and-toc.js
|
|
281
329
|
function remarkSlugAndTOC() {
|
|
282
330
|
return (tree) => {
|
|
283
|
-
const
|
|
331
|
+
const resolveHeading = createHeadingResolver();
|
|
284
332
|
const toc = [];
|
|
285
333
|
visit(tree, "heading", (node) => {
|
|
286
334
|
const depth = node.depth;
|
|
287
|
-
if (depth
|
|
335
|
+
if (depth < 2 || depth > 6) return;
|
|
288
336
|
const title = node.children.filter((n) => n.type === "text" || n.type === "inlineCode").map((n) => n.value).join(" ").trim();
|
|
289
|
-
const resolved = resolveHeading(title,
|
|
337
|
+
const resolved = resolveHeading(title, depth);
|
|
290
338
|
const lastText = node.children.findLast((child) => child.type === "text");
|
|
291
339
|
if (lastText) lastText.value = lastText.value.replace(/\s+\{#[\w-]+\}$/, "");
|
|
292
340
|
node.data ??= {};
|
|
293
341
|
node.data.hProperties ??= {};
|
|
294
342
|
node.data.id = resolved.id;
|
|
295
343
|
node.data.hProperties.id = resolved.id;
|
|
296
|
-
toc.push({
|
|
344
|
+
if (depth === 2 || depth === 3) toc.push({
|
|
297
345
|
id: resolved.id,
|
|
298
346
|
title: resolved.title,
|
|
299
347
|
level: depth
|
|
@@ -498,7 +546,8 @@ function masterSite(site) {
|
|
|
498
546
|
return {
|
|
499
547
|
name: "@master/internal-site",
|
|
500
548
|
hooks: {
|
|
501
|
-
"astro:config:setup": async ({ config, updateConfig, addRenderer, injectRoute }) => {
|
|
549
|
+
"astro:config:setup": async ({ config, command, updateConfig, addRenderer, injectRoute }) => {
|
|
550
|
+
if (command === "build") await validateSiteTranslations(site.i18n);
|
|
502
551
|
const root = fileURLToPath(config.root);
|
|
503
552
|
const contentRelative = relative(root, fileURLToPath(site.content)).split(sep).join("/");
|
|
504
553
|
if (contentRelative.startsWith("..")) throw new Error("Site content must be inside the Astro project root");
|
|
@@ -516,7 +565,8 @@ function masterSite(site) {
|
|
|
516
565
|
"virtual:master-site/config": `export default ${JSON.stringify(site)}`,
|
|
517
566
|
"virtual:master-site/app": moduleImport(site.app),
|
|
518
567
|
"virtual:master-site/highlight": moduleImport(site.highlighter ?? new URL("../docs-shell/utils/highlight-code-core.ts", new URL("../src/astro/index.ts", import.meta.url).href)),
|
|
519
|
-
"virtual:master-site/translations": `export default
|
|
568
|
+
"virtual:master-site/translations": `export default {${Object.entries(prepared.localizedDocuments).map(([pathname, documents]) => `${JSON.stringify(pathname)}: {${Object.entries(documents).map(([locale, url]) => `${JSON.stringify(locale)}: ${propagated(url)}`).join(",")}}`).join(",")}}`,
|
|
569
|
+
"virtual:master-site/localized-metadata": `export default ${JSON.stringify(prepared.localizedMetadata)}`
|
|
520
570
|
};
|
|
521
571
|
const plugin = {
|
|
522
572
|
name: "@master/internal-site:modules",
|
|
@@ -554,10 +604,10 @@ function masterSite(site) {
|
|
|
554
604
|
server.restart();
|
|
555
605
|
}, 100);
|
|
556
606
|
};
|
|
557
|
-
server.watcher.on("add", refresh).on("unlink", refresh);
|
|
607
|
+
server.watcher.on("add", refresh).on("unlink", refresh).on("change", refresh);
|
|
558
608
|
server.httpServer?.once("close", () => {
|
|
559
609
|
clearTimeout(restart);
|
|
560
|
-
server.watcher.off("add", refresh).off("unlink", refresh);
|
|
610
|
+
server.watcher.off("add", refresh).off("unlink", refresh).off("change", refresh);
|
|
561
611
|
});
|
|
562
612
|
server.middlewares.use((request, response, next) => {
|
|
563
613
|
let pathname;
|
|
@@ -613,6 +663,7 @@ function masterSite(site) {
|
|
|
613
663
|
vite: {
|
|
614
664
|
plugins: [
|
|
615
665
|
plugin,
|
|
666
|
+
localizedMdx(site.i18n),
|
|
616
667
|
sourceURL(),
|
|
617
668
|
serverStyles([root, packageSource]),
|
|
618
669
|
svgMarkup()
|
package/dist/config.d.mts
CHANGED
|
@@ -31,10 +31,16 @@ export interface SiteI18nConfig {
|
|
|
31
31
|
hreflangOfLocale: Record<string, string>;
|
|
32
32
|
nameOfLocale: Record<string, string>;
|
|
33
33
|
localizablePathnameRoots: string[];
|
|
34
|
+
localizableOrigins?: Record<string, readonly string[]>;
|
|
34
35
|
translations: Record<string, Record<string, string>>;
|
|
35
36
|
sectionLocales?: Record<string, readonly string[]>;
|
|
36
37
|
/** These sections emit translated URLs only when translated MDX exists. */
|
|
37
38
|
translatedOnly?: readonly string[];
|
|
39
|
+
/** Build-only resource loader and version-controlled explicit translation reviews. */
|
|
40
|
+
validation?: {
|
|
41
|
+
reviews: URL;
|
|
42
|
+
resources: URL;
|
|
43
|
+
};
|
|
38
44
|
}
|
|
39
45
|
export interface DocumentConfig {
|
|
40
46
|
layout: URL;
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { parseFrontmatter } from "@astrojs/markdown-remark";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
7
|
+
import { mdxjsEsm } from "micromark-extension-mdxjs-esm";
|
|
8
|
+
import { mdxjsEsmFromMarkdown } from "mdast-util-mdxjs-esm";
|
|
9
|
+
import { mdxJsx } from "micromark-extension-mdx-jsx";
|
|
10
|
+
import { mdxJsxFromMarkdown } from "mdast-util-mdx-jsx";
|
|
11
|
+
import * as acorn from "acorn";
|
|
12
|
+
import Slugger from "github-slugger";
|
|
13
|
+
// src/docs-shell/utils/resolve-heading.js
|
|
14
|
+
function resolveHeading(title, id) {
|
|
15
|
+
const explicit = typeof title === "string" && title.match(/\s+\{#([\w-]+)\}$/);
|
|
16
|
+
if (explicit) {
|
|
17
|
+
title = title.slice(0, explicit.index);
|
|
18
|
+
id = explicit[1];
|
|
19
|
+
}
|
|
20
|
+
const words = title?.split?.(" ");
|
|
21
|
+
if (words?.pop() === "[sr-only]") return {
|
|
22
|
+
title: words.join(" "),
|
|
23
|
+
id: id?.replace("-sr-only", "")?.replace("-[sr-only]", ""),
|
|
24
|
+
className: "sr-only"
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
title,
|
|
28
|
+
id
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Section counters stay stable when a detail heading is inserted between them. */
|
|
32
|
+
function createHeadingResolver() {
|
|
33
|
+
const sections = new Slugger();
|
|
34
|
+
const details = new Slugger();
|
|
35
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
36
|
+
return (title, depth) => {
|
|
37
|
+
const explicit = /\s+\{#[\w-]+\}$/.test(title);
|
|
38
|
+
let heading;
|
|
39
|
+
if (explicit) heading = resolveHeading(title);
|
|
40
|
+
else {
|
|
41
|
+
const slugs = depth === 2 || depth === 3 ? sections : details;
|
|
42
|
+
do
|
|
43
|
+
heading = resolveHeading(title, slugs.slug(title));
|
|
44
|
+
while (heading.id && assigned.has(heading.id));
|
|
45
|
+
}
|
|
46
|
+
if (heading.id) assigned.add(heading.id);
|
|
47
|
+
return {
|
|
48
|
+
...heading,
|
|
49
|
+
explicit
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// src/docs-shell/common/locales.js
|
|
54
|
+
/** URL prefixes stay stable; document language tags use standard BCP 47 values. */
|
|
55
|
+
const locales = [
|
|
56
|
+
"en",
|
|
57
|
+
"tw",
|
|
58
|
+
"ja",
|
|
59
|
+
"ko",
|
|
60
|
+
"cn"
|
|
61
|
+
];
|
|
62
|
+
// src/docs-shell/utils/localized-metadata.ts
|
|
63
|
+
/** Prose can vary by locale; publication identity and technical configuration stay with the source. */
|
|
64
|
+
function localizedMetadata(metadata, frontmatter, locale) {
|
|
65
|
+
const localized = {
|
|
66
|
+
...metadata,
|
|
67
|
+
locale
|
|
68
|
+
};
|
|
69
|
+
for (const key of ["description", "subtitle"]) if (typeof frontmatter[key] === "string") localized[key] = frontmatter[key];
|
|
70
|
+
if (typeof frontmatter.title === "string") localized.title = metadata.title && typeof metadata.title === "object" ? {
|
|
71
|
+
...metadata.title,
|
|
72
|
+
..."absolute" in metadata.title ? { absolute: frontmatter.title } : { default: frontmatter.title }
|
|
73
|
+
} : frontmatter.title;
|
|
74
|
+
if (metadata.cover && typeof frontmatter.cover?.alt === "string") localized.cover = {
|
|
75
|
+
...metadata.cover,
|
|
76
|
+
alt: frontmatter.cover.alt
|
|
77
|
+
};
|
|
78
|
+
for (const channel of ["openGraph", "twitter"]) {
|
|
79
|
+
const authored = frontmatter[channel];
|
|
80
|
+
if (metadata[channel] || authored) {
|
|
81
|
+
const prose = Object.fromEntries(["title", "description"].flatMap((key) => {
|
|
82
|
+
const text = authored?.[key] ?? (locale !== "en" ? frontmatter[key] : void 0);
|
|
83
|
+
return typeof text === "string" ? [[key, text]] : [];
|
|
84
|
+
}));
|
|
85
|
+
localized[channel] = {
|
|
86
|
+
...metadata[channel],
|
|
87
|
+
...prose
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return localized;
|
|
92
|
+
}
|
|
93
|
+
// src/scripts/mdx-sample.ts
|
|
94
|
+
/** Only prose-free companion examples share one source across editions. */
|
|
95
|
+
function isSharedMdxSample(file, source) {
|
|
96
|
+
if (["content.mdx", "contract.mdx"].includes(basename(file))) return false;
|
|
97
|
+
const { content, frontmatter } = parseFrontmatter(source);
|
|
98
|
+
if (Object.keys(frontmatter).length) return false;
|
|
99
|
+
const nodes = fromMarkdown(content).children;
|
|
100
|
+
return nodes.length > 0 && nodes.every((node) => node.type === "code");
|
|
101
|
+
}
|
|
102
|
+
// src/scripts/localization.ts
|
|
103
|
+
const emptyReviews = () => ({
|
|
104
|
+
version: 1,
|
|
105
|
+
resources: {}
|
|
106
|
+
});
|
|
107
|
+
const hash = (value) => createHash("sha256").update(value.replaceAll("\r\n", "\n")).digest("hex");
|
|
108
|
+
const sourceHash = (resource) => hash(JSON.stringify([resource.source, Object.entries(resource.dependencies ?? {}).sort(([a], [b]) => a.localeCompare(b))]));
|
|
109
|
+
/** Explicit review operation. Preparation and production builds never call this function. */
|
|
110
|
+
function reviewTranslation(resource, locale) {
|
|
111
|
+
const translation = resource.translations[locale];
|
|
112
|
+
if (!translation?.trim()) throw new Error(`Missing translation: ${resource.id} (${locale})`);
|
|
113
|
+
return {
|
|
114
|
+
sourceHash: sourceHash(resource),
|
|
115
|
+
translationHash: hash(translation)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function auditTranslations(resources, reviews, { locales: locales$1 = locales, defaultLocale = "en" } = {}) {
|
|
119
|
+
if (reviews.version !== 1 || !reviews.resources) throw new Error("Unsupported translation review index; expected version 1");
|
|
120
|
+
const diagnostics = [];
|
|
121
|
+
const ids = /* @__PURE__ */ new Set();
|
|
122
|
+
for (const resource of resources) {
|
|
123
|
+
if (ids.has(resource.id)) throw new Error(`Duplicate translation resource: ${resource.id}`);
|
|
124
|
+
ids.add(resource.id);
|
|
125
|
+
if (resource.draft) continue;
|
|
126
|
+
for (const locale of resource.locales ?? locales$1) {
|
|
127
|
+
if (locale === defaultLocale) continue;
|
|
128
|
+
const report = (reason, message) => diagnostics.push({
|
|
129
|
+
id: resource.id,
|
|
130
|
+
locale,
|
|
131
|
+
reason,
|
|
132
|
+
message
|
|
133
|
+
});
|
|
134
|
+
const translation = resource.translations[locale];
|
|
135
|
+
if (!translation?.trim()) {
|
|
136
|
+
report("missing", "Translation is missing");
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (parseFrontmatter(translation).frontmatter.draft === true) {
|
|
140
|
+
report("missing", "Translation is a draft and is not available for publication");
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const review = reviews.resources[resource.id]?.[locale];
|
|
144
|
+
if (!review) report("unreviewed", "Translation has not been reviewed");
|
|
145
|
+
else {
|
|
146
|
+
if (review.sourceHash !== sourceHash(resource)) report("source-changed", "English source changed after translation review");
|
|
147
|
+
if (review.translationHash !== hash(translation)) report("translation-changed", "Translation changed after review");
|
|
148
|
+
}
|
|
149
|
+
if (resource.checkAnchors) {
|
|
150
|
+
const expected = headingAnchors(resource.source);
|
|
151
|
+
const actual = headingAnchors(translation);
|
|
152
|
+
if (new Set(actual).size !== actual.length || JSON.stringify(expected) !== JSON.stringify(actual)) report("anchors", "Translated heading IDs must preserve source IDs and order");
|
|
153
|
+
}
|
|
154
|
+
if (resource.requireMetadata) {
|
|
155
|
+
const { frontmatter } = parseFrontmatter(translation);
|
|
156
|
+
if (typeof frontmatter.title !== "string" || !frontmatter.title.trim() || typeof frontmatter.description !== "string" || !frontmatter.description.trim()) report("metadata", "Localized MDX requires title and description frontmatter");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return diagnostics;
|
|
161
|
+
}
|
|
162
|
+
function assertTranslations(resources, reviews, options = {}) {
|
|
163
|
+
const diagnostics = auditTranslations(resources, reviews, options);
|
|
164
|
+
if (diagnostics.length) throw new Error(`Translation completeness check failed (${diagnostics.length}):\n${diagnostics.map((issue) => `${issue.id} [${issue.locale}] ${issue.reason}: ${issue.message}`).join("\n")}`);
|
|
165
|
+
}
|
|
166
|
+
/** Shared Astro build gate, also usable by a consumer's explicit audit command. */
|
|
167
|
+
async function validateSiteTranslations({ validation, ...options }) {
|
|
168
|
+
const locales$2 = options.locales ?? locales;
|
|
169
|
+
if (!validation) {
|
|
170
|
+
if (locales$2.some((locale) => locale !== (options.defaultLocale ?? "en"))) throw new Error("Multilingual production builds require i18n.validation resources and reviews");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const [{ default: load }, raw] = await Promise.all([import(validation.resources.href), readFile(validation.reviews, "utf8")]);
|
|
174
|
+
const resources = typeof load === "function" ? await load() : load;
|
|
175
|
+
if (!Array.isArray(resources) || resources.length === 0) throw new Error("Translation resource loader must return a nonempty resource inventory");
|
|
176
|
+
assertTranslations(resources, JSON.parse(raw), options);
|
|
177
|
+
}
|
|
178
|
+
async function readLocalizedDocument({ sourceFile, locale, defaultLocale = "en", metadata = {} }) {
|
|
179
|
+
const original = sourceFile.replace(/\.([a-z]{2})\.mdx$/, ".mdx");
|
|
180
|
+
const file = locale === defaultLocale ? original : original.replace(/\.mdx$/, `.${locale}.mdx`);
|
|
181
|
+
let source;
|
|
182
|
+
try {
|
|
183
|
+
source = await readFile(file, "utf8");
|
|
184
|
+
} catch (cause) {
|
|
185
|
+
throw new Error(`Missing localized document: ${file} (${locale})`, { cause });
|
|
186
|
+
}
|
|
187
|
+
const { content, frontmatter } = parseFrontmatter(source);
|
|
188
|
+
return {
|
|
189
|
+
file,
|
|
190
|
+
content,
|
|
191
|
+
frontmatter,
|
|
192
|
+
metadata: localizedMetadata(metadata, frontmatter, locale)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function filesOf(directory) {
|
|
196
|
+
const files = [];
|
|
197
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
198
|
+
if (entry.name.startsWith(".")) continue;
|
|
199
|
+
const file = join(directory, entry.name);
|
|
200
|
+
if (entry.isDirectory()) {
|
|
201
|
+
if (entry.name !== "drafts") files.push(...await filesOf(file));
|
|
202
|
+
} else files.push(file);
|
|
203
|
+
}
|
|
204
|
+
return files.sort();
|
|
205
|
+
}
|
|
206
|
+
/** Filesystem inventory, with no page imports, component execution or generated source writes. */
|
|
207
|
+
async function discoverDocumentResources({ contentRoot, locales: locales$3 = locales, defaultLocale = "en", exclude = [], siteRoot = dirname(contentRoot) }) {
|
|
208
|
+
const files = new Set(await filesOf(contentRoot));
|
|
209
|
+
const resources = [];
|
|
210
|
+
for (const file of files) {
|
|
211
|
+
if (!file.endsWith(".mdx") || /\.[a-z]{2}\.mdx$/.test(file)) continue;
|
|
212
|
+
const source = await readFile(file, "utf8");
|
|
213
|
+
const id = relative(contentRoot, file).replaceAll("\\", "/");
|
|
214
|
+
if (!source.trim() || exclude.includes(id)) continue;
|
|
215
|
+
if (isSharedMdxSample(file, source)) continue;
|
|
216
|
+
const metadata = join(dirname(file), "metadata.ts");
|
|
217
|
+
const sourceMetadata = files.has(metadata) ? await readFile(metadata, "utf8") : void 0;
|
|
218
|
+
const dependencies = await collectSourceDependencies([file, ...sourceMetadata ? [metadata] : []], { siteRoot });
|
|
219
|
+
delete dependencies[relative(siteRoot, file).replaceAll("\\", "/")];
|
|
220
|
+
const translations = {};
|
|
221
|
+
for (const locale of locales$3) {
|
|
222
|
+
if (locale === defaultLocale) continue;
|
|
223
|
+
const translatedFile = file.replace(/\.mdx$/, `.${locale}.mdx`);
|
|
224
|
+
if (files.has(translatedFile)) translations[locale] = await readFile(translatedFile, "utf8");
|
|
225
|
+
}
|
|
226
|
+
resources.push({
|
|
227
|
+
id,
|
|
228
|
+
source,
|
|
229
|
+
translations,
|
|
230
|
+
dependencies,
|
|
231
|
+
checkAnchors: true,
|
|
232
|
+
requireMetadata: basename(file) === "content.mdx",
|
|
233
|
+
draft: parseFrontmatter(source).frontmatter.draft === true || /\bdraft\s*:\s*true\b/.test(sourceMetadata ?? "")
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return resources;
|
|
237
|
+
}
|
|
238
|
+
/** Literal interface requests must exist even when a newly added control has no dictionary entry yet. */
|
|
239
|
+
async function sharedInterfaceKeys(sourceRoot = join(dirname(fileURLToPath(import.meta.resolve("@master/internal-site/package.json"))), "src")) {
|
|
240
|
+
const keys = /* @__PURE__ */ new Set();
|
|
241
|
+
const visit = async (directory) => {
|
|
242
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
243
|
+
const file = join(directory, entry.name);
|
|
244
|
+
if (entry.isDirectory()) await visit(file);
|
|
245
|
+
else if (/\.(?:astro|svelte|ts|js)$/.test(entry.name) && !/\.(?:test|spec)\./.test(entry.name)) {
|
|
246
|
+
const source = await readFile(file, "utf8");
|
|
247
|
+
for (const match of source.matchAll(/\bt\s*\(\s*(?=['"`])/g)) {
|
|
248
|
+
const expression = acorn.parseExpressionAt(source, match.index + match[0].length, { ecmaVersion: "latest" });
|
|
249
|
+
const value = expression.type === "Literal" ? expression.value : expression.type === "TemplateLiteral" && !expression.expressions.length ? expression.quasis[0].value.cooked : void 0;
|
|
250
|
+
if (typeof value === "string" && value.trim()) keys.add(value);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
await visit(sourceRoot);
|
|
256
|
+
return [...keys].sort();
|
|
257
|
+
}
|
|
258
|
+
/** Follow local prose/data modules without executing them or importing product compiler code. */
|
|
259
|
+
async function collectSourceDependencies(files, { siteRoot }) {
|
|
260
|
+
const root = resolve(siteRoot);
|
|
261
|
+
const sharedRoot = dirname(fileURLToPath(import.meta.resolve("@master/internal-site/package.json")));
|
|
262
|
+
const collected = {};
|
|
263
|
+
const seen = /* @__PURE__ */ new Set();
|
|
264
|
+
const visit = async (file) => {
|
|
265
|
+
file = resolve(file).replace(/\.(tw|ja|ko|cn)\.mdx$/, ".mdx");
|
|
266
|
+
if (!file.startsWith(root + sep) && !file.startsWith(sharedRoot + sep) || seen.has(file)) return;
|
|
267
|
+
if (file.startsWith(sharedRoot + sep) && /\/common\/(?:translations|locales)\.[jt]s$/.test(file.replaceAll("\\", "/"))) return;
|
|
268
|
+
seen.add(file);
|
|
269
|
+
const source = await readFile(file, "utf8").catch(() => void 0);
|
|
270
|
+
if (source === void 0) return;
|
|
271
|
+
const key = file.startsWith(sharedRoot + sep) ? `@master/internal-site/${relative(sharedRoot, file)}` : relative(root, file);
|
|
272
|
+
collected[key.replaceAll("\\", "/")] = source;
|
|
273
|
+
const imports = source.matchAll(/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?)(['"])([^'"\n]+)\1/g);
|
|
274
|
+
for (const [, , specifier] of imports) {
|
|
275
|
+
if (!specifier.startsWith(".") && !specifier.startsWith("~/site/") && !specifier.startsWith("@master/internal-site/")) continue;
|
|
276
|
+
let target;
|
|
277
|
+
try {
|
|
278
|
+
target = specifier.startsWith("@master/internal-site/") ? fileURLToPath(import.meta.resolve(specifier.split("?")[0])) : specifier.startsWith("~/site/") ? resolve(root, specifier.slice(7)) : resolve(dirname(file), specifier.split("?")[0]);
|
|
279
|
+
} catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (extname(target) && !/\.(?:[cm]?[jt]s|[jt]sx|mdx|astro|svelte)$/.test(target)) continue;
|
|
283
|
+
const candidates = extname(target) ? [target, .../\.js$/.test(target) ? [target.replace(/\.js$/, ".ts")] : []] : [
|
|
284
|
+
"",
|
|
285
|
+
".ts",
|
|
286
|
+
".js",
|
|
287
|
+
".mdx",
|
|
288
|
+
".astro",
|
|
289
|
+
".svelte",
|
|
290
|
+
"/index.ts",
|
|
291
|
+
"/index.js"
|
|
292
|
+
].map((suffix) => target + suffix);
|
|
293
|
+
for (const candidate of candidates) {
|
|
294
|
+
const before = Object.keys(collected).length;
|
|
295
|
+
await visit(candidate);
|
|
296
|
+
if (Object.keys(collected).length > before) break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
for (const file of files) await visit(file);
|
|
301
|
+
return Object.fromEntries(Object.entries(collected).sort(([a], [b]) => a.localeCompare(b)));
|
|
302
|
+
}
|
|
303
|
+
function headings(source) {
|
|
304
|
+
const parsed = parseFrontmatter(source);
|
|
305
|
+
const offset = source.indexOf(parsed.content, parsed.rawFrontmatter ? parsed.rawFrontmatter.length : 0);
|
|
306
|
+
const tree = fromMarkdown(parsed.content, {
|
|
307
|
+
extensions: [
|
|
308
|
+
{ disable: { null: [
|
|
309
|
+
"htmlFlow",
|
|
310
|
+
"htmlText",
|
|
311
|
+
"codeIndented"
|
|
312
|
+
] } },
|
|
313
|
+
mdxjsEsm({
|
|
314
|
+
acorn,
|
|
315
|
+
addResult: true
|
|
316
|
+
}),
|
|
317
|
+
mdxJsx({
|
|
318
|
+
acorn,
|
|
319
|
+
addResult: true
|
|
320
|
+
})
|
|
321
|
+
],
|
|
322
|
+
mdastExtensions: [mdxjsEsmFromMarkdown(), mdxJsxFromMarkdown()]
|
|
323
|
+
});
|
|
324
|
+
const resolveHeading = createHeadingResolver();
|
|
325
|
+
const found = [];
|
|
326
|
+
const visit = (node) => {
|
|
327
|
+
if (node.type === "heading" && node.depth >= 2 && node.depth <= 6) {
|
|
328
|
+
const title = node.children.filter((child) => child.type === "text" || child.type === "inlineCode").map((child) => child.value).join(" ").trim();
|
|
329
|
+
const resolved = resolveHeading(title, node.depth);
|
|
330
|
+
const raw = parsed.content.slice(node.position.start.offset, node.position.end.offset);
|
|
331
|
+
const end = raw.includes("\n") ? node.position.start.offset + raw.indexOf("\n") : node.position.end.offset;
|
|
332
|
+
found.push({
|
|
333
|
+
id: resolved.id ?? "",
|
|
334
|
+
explicit: resolved.explicit,
|
|
335
|
+
offset: Math.max(0, offset) + end
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
node.children?.forEach(visit);
|
|
339
|
+
};
|
|
340
|
+
visit(tree);
|
|
341
|
+
return found;
|
|
342
|
+
}
|
|
343
|
+
function headingAnchors(source) {
|
|
344
|
+
return headings(source).map((heading) => heading.id);
|
|
345
|
+
}
|
|
346
|
+
/** Pin the existing automatic English IDs before translation, without modifying fenced examples. */
|
|
347
|
+
function stabilizeHeadingAnchors(source) {
|
|
348
|
+
for (const heading of headings(source).reverse()) {
|
|
349
|
+
if (heading.explicit || !heading.id) continue;
|
|
350
|
+
const closing = source.slice(0, heading.offset).match(/[ \t]+#+[ \t]*$/)?.[0] ?? "";
|
|
351
|
+
const at = heading.offset - closing.length;
|
|
352
|
+
source = source.slice(0, at) + ` \\{#${heading.id}\\}` + source.slice(at);
|
|
353
|
+
}
|
|
354
|
+
return source;
|
|
355
|
+
}
|
|
356
|
+
export { emptyReviews as a, reviewTranslation as c, validateSiteTranslations as d, isSharedMdxSample as f, createHeadingResolver as h, discoverDocumentResources as i, sharedInterfaceKeys as l, locales as m, auditTranslations as n, headingAnchors as o, localizedMetadata as p, collectSourceDependencies as r, readLocalizedDocument as s, assertTranslations as t, stabilizeHeadingAnchors as u };
|