create-zudo-doc 5.19.1 → 5.20.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.
- package/CHANGELOG.md +15 -0
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.js +3 -3
- package/package.json +1 -1
- package/templates/base/scripts/check-links.js +84 -38
- package/templates/base/scripts/setup-doc-skill.sh +43 -14
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to `create-zudo-doc` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on Keep a Changelog, and release notes are generated from the changelog MDX pages.
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
No unreleased changes.
|
|
10
|
+
|
|
11
|
+
## [5.20.0] - 2026-09-09
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
- Made the generated documentation-skill setup script work on stock macOS Bash 3.2 and corrected its locale/config handling, including checking that the documentation symlink exists before emitting locale guidance. (`85b603e33`, `f6b699fe0`)
|
|
16
|
+
- Fixed the generated link checker to recognize IDs and links when quoted attributes contain `>`, and reduced repeated scanning by parsing each page once. (`0e52271b9`, `7c0144e51`)
|
|
17
|
+
|
|
18
|
+
### Other Changes
|
|
19
|
+
|
|
20
|
+
- Updated the Design Token Panel feature's emitted `@takazudo/zdtp` pin to `0.7.0`. Fresh scaffolds use the compatible panel integration without a migration. (`e0fc27fbd`, `9554dbbea`)
|
|
21
|
+
|
|
7
22
|
## [5.19.1] - 2026-09-08
|
|
8
23
|
|
|
9
24
|
### Other Changes
|
package/dist/scaffold.d.ts
CHANGED
|
@@ -31,5 +31,5 @@ export declare function deriveDocSkillName(projectName: string): string;
|
|
|
31
31
|
*
|
|
32
32
|
* Bumped in lockstep by scripts/release-create-zudo-doc.sh.
|
|
33
33
|
*/
|
|
34
|
-
export declare const ZUDO_DOC_PIN = "^5.
|
|
34
|
+
export declare const ZUDO_DOC_PIN = "^5.20.0";
|
|
35
35
|
export declare function scaffold(choices: UserChoices): Promise<void>;
|
package/dist/scaffold.js
CHANGED
|
@@ -47,7 +47,7 @@ export function deriveDocSkillName(projectName) {
|
|
|
47
47
|
*
|
|
48
48
|
* Bumped in lockstep by scripts/release-create-zudo-doc.sh.
|
|
49
49
|
*/
|
|
50
|
-
export const ZUDO_DOC_PIN = "^5.
|
|
50
|
+
export const ZUDO_DOC_PIN = "^5.20.0";
|
|
51
51
|
/**
|
|
52
52
|
* Files in `templates/base/**` that must not be copied by the unconditional
|
|
53
53
|
* base mirror. Each entry is matched against the path relative to
|
|
@@ -882,7 +882,7 @@ function generatePackageJson(choices, localePlan) {
|
|
|
882
882
|
// `@takazudo/zdtp/styles.css` (see features/design-token-panel.ts). Both are
|
|
883
883
|
// no-ops with the feature off, so an OFF project must not carry the dep
|
|
884
884
|
// (#4009 / #4018 — it was unconditional until then, see the `deps` block).
|
|
885
|
-
deps["@takazudo/zdtp"] = "0.
|
|
885
|
+
deps["@takazudo/zdtp"] = "0.7.0";
|
|
886
886
|
}
|
|
887
887
|
if (choices.features.includes("docHistory") ||
|
|
888
888
|
choices.features.includes("assetViewer")) {
|
|
@@ -905,7 +905,7 @@ function generatePackageJson(choices, localePlan) {
|
|
|
905
905
|
// `/exclude` at module scope from the always-bundled chrome graph; #3110
|
|
906
906
|
// moved compileExclude into @takazudo/zudo-doc, so projects with both
|
|
907
907
|
// docHistory and assetViewer off no longer need the package at all.
|
|
908
|
-
deps["@takazudo/zudo-doc-history-server"] = "^5.
|
|
908
|
+
deps["@takazudo/zudo-doc-history-server"] = "^5.20.0";
|
|
909
909
|
// tsx is no longer needed here: the relocated package plugin imports the
|
|
910
910
|
// runner directly (no `tsx -e` spawn) since the package ships compiled
|
|
911
911
|
// dist/ — package-first migration #2321 (#2337).
|
package/package.json
CHANGED
|
@@ -445,13 +445,26 @@ function decodeHtmlAttributeValue(value) {
|
|
|
445
445
|
);
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
448
|
+
// Attribute-scan grammar shared by the anchor and id scans below. A quoted
|
|
449
|
+
// attribute value may legally contain ">" (title="a > b"), so bounding an
|
|
450
|
+
// attribute scan with [^>] silently drops the whole tag: a lost id is a noisy
|
|
451
|
+
// false STRICT FAIL, while a lost href means the link is never checked at all
|
|
452
|
+
// (#4046). This run crosses ">" only inside quotes, and a decoy `href=`/`id=`
|
|
453
|
+
// written as text inside another attribute's value stays unreachable because a
|
|
454
|
+
// quoted span is consumed whole. Each alternative starts with a distinct
|
|
455
|
+
// character, so the repetition backtracks linearly.
|
|
456
|
+
const HTML_ATTRIBUTE_RUN = /(?:"[^"]*"|'[^']*'|[^>"'])/.source;
|
|
457
|
+
const HTML_ATTRIBUTE_VALUE = /(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`\\]+))/.source;
|
|
458
|
+
|
|
459
|
+
// Single shared anchor scan. Its one consumer, `classifyHtmlAnchorHrefs`,
|
|
460
|
+
// splits these matches into disjoint buckets, so the grammar and the
|
|
461
|
+
// incremental line counting live here once and every bucket sees a fix to the
|
|
462
|
+
// anchor regex.
|
|
453
463
|
function* iterateHtmlAnchorHrefs(html) {
|
|
454
|
-
const regex =
|
|
464
|
+
const regex = new RegExp(
|
|
465
|
+
`<a(?=\\s)${HTML_ATTRIBUTE_RUN}*?\\shref\\s*=\\s*${HTML_ATTRIBUTE_VALUE}${HTML_ATTRIBUTE_RUN}*>`,
|
|
466
|
+
"gi",
|
|
467
|
+
);
|
|
455
468
|
let match;
|
|
456
469
|
let lastIndex = 0;
|
|
457
470
|
let line = 1;
|
|
@@ -462,30 +475,39 @@ function* iterateHtmlAnchorHrefs(html) {
|
|
|
462
475
|
}
|
|
463
476
|
}
|
|
464
477
|
|
|
465
|
-
|
|
478
|
+
// One anchor pass, two disjoint buckets: internal links to resolve, and the
|
|
479
|
+
// protocol-relative hrefs that are external for resolution purposes but worth
|
|
480
|
+
// reporting informationally (see #3921/#3930). The dist walk calls this once
|
|
481
|
+
// per page; `extractHtmlLinks` / `extractProtocolRelativeHtmlLinks` below are
|
|
482
|
+
// single-bucket views for callers that want only one of the two.
|
|
483
|
+
export function classifyHtmlAnchorHrefs(html) {
|
|
466
484
|
const links = [];
|
|
485
|
+
const protocolRelative = [];
|
|
467
486
|
for (const { href, line } of iterateHtmlAnchorHrefs(html)) {
|
|
468
|
-
if (
|
|
487
|
+
if (/^\/\//.test(href)) {
|
|
488
|
+
protocolRelative.push({ href, line });
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (/^(?:https?:|mailto:|javascript:|data:|tel:)/i.test(href)) continue;
|
|
469
492
|
links.push({ href, line });
|
|
470
493
|
}
|
|
471
|
-
return links;
|
|
494
|
+
return { links, protocolRelative };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function extractHtmlLinks(html) {
|
|
498
|
+
return classifyHtmlAnchorHrefs(html).links;
|
|
472
499
|
}
|
|
473
500
|
|
|
474
|
-
// Informational counterpart to extractHtmlLinks: same scan, but keeps only the
|
|
475
|
-
// protocol-relative hrefs that extractHtmlLinks classifies as external and
|
|
476
|
-
// skips (see #3921/#3930).
|
|
477
501
|
export function extractProtocolRelativeHtmlLinks(html) {
|
|
478
|
-
|
|
479
|
-
for (const { href, line } of iterateHtmlAnchorHrefs(html)) {
|
|
480
|
-
if (!/^\/\//.test(href)) continue;
|
|
481
|
-
links.push({ href, line });
|
|
482
|
-
}
|
|
483
|
-
return links;
|
|
502
|
+
return classifyHtmlAnchorHrefs(html).protocolRelative;
|
|
484
503
|
}
|
|
485
504
|
|
|
486
505
|
export function extractHtmlIds(html) {
|
|
487
506
|
const ids = [];
|
|
488
|
-
const regex =
|
|
507
|
+
const regex = new RegExp(
|
|
508
|
+
`<[A-Za-z]${HTML_ATTRIBUTE_RUN}*?\\sid\\s*=\\s*${HTML_ATTRIBUTE_VALUE}${HTML_ATTRIBUTE_RUN}*>`,
|
|
509
|
+
"gi",
|
|
510
|
+
);
|
|
489
511
|
let match;
|
|
490
512
|
while ((match = regex.exec(html)) !== null) {
|
|
491
513
|
ids.push(decodeHtmlAttributeValue(match[1] ?? match[2] ?? match[3]));
|
|
@@ -636,7 +658,15 @@ function extractStaticMdxIds(body) {
|
|
|
636
658
|
visibleLines.push(codeFenceOpener === null ? stripInlineCode(line) : "");
|
|
637
659
|
}
|
|
638
660
|
const elements = visibleLines.join("\n");
|
|
639
|
-
|
|
661
|
+
// Same tokenising run as the built-HTML scans: a ">" inside a quoted MDX/JSX
|
|
662
|
+
// attribute value (title="a > b") must not truncate the tag and lose the id
|
|
663
|
+
// (#4048). The rest of this pattern keeps its own semantics — `\bid` also
|
|
664
|
+
// accepts `data-id`, and only non-empty quoted values count, unlike the
|
|
665
|
+
// built-HTML id scan.
|
|
666
|
+
const regex = new RegExp(
|
|
667
|
+
`<[A-Za-z]${HTML_ATTRIBUTE_RUN}*?\\bid\\s*=\\s*(?:"([^"]+)"|'([^']+)')${HTML_ATTRIBUTE_RUN}*>`,
|
|
668
|
+
"gs",
|
|
669
|
+
);
|
|
640
670
|
let match;
|
|
641
671
|
while ((match = regex.exec(elements)) !== null) ids.add(match[1] ?? match[2]);
|
|
642
672
|
return ids;
|
|
@@ -743,6 +773,12 @@ export async function resolveLink(href, distDir, basePath = "/", fileDir = "") {
|
|
|
743
773
|
return (await resolveDistTarget(href, distDir, basePath, fileDir)).type !== "missing";
|
|
744
774
|
}
|
|
745
775
|
|
|
776
|
+
/**
|
|
777
|
+
* Scan budget: exactly ONE anchor pass per page, and at most ONE id extraction
|
|
778
|
+
* per HTML target some fragment actually references. Ids are deliberately not
|
|
779
|
+
* extracted eagerly — most pages are never the target of a fragment link, and
|
|
780
|
+
* scanning them costs a full regex pass for a set nothing reads.
|
|
781
|
+
*/
|
|
746
782
|
export async function checkHtmlLinksAndTrailing(
|
|
747
783
|
distDir,
|
|
748
784
|
rootDir,
|
|
@@ -756,25 +792,25 @@ export async function checkHtmlLinksAndTrailing(
|
|
|
756
792
|
const protocolRelative = [];
|
|
757
793
|
const idCache = new Map();
|
|
758
794
|
const cache = new Map();
|
|
759
|
-
const pages = [];
|
|
760
795
|
const scanned = { links: 0, ids: 0 };
|
|
761
796
|
for (const file of await collectFiles(distDir, [".html"])) {
|
|
762
797
|
const content = await readFile(file, "utf-8");
|
|
763
|
-
const
|
|
764
|
-
|
|
798
|
+
const relFile = relative(rootDir, file);
|
|
799
|
+
// One anchor pass per page: internal links and the informational
|
|
800
|
+
// protocol-relative notices are disjoint buckets of the same match set.
|
|
801
|
+
const { links, protocolRelative: pageProtocolRelative } = classifyHtmlAnchorHrefs(content);
|
|
765
802
|
scanned.links += links.length;
|
|
766
|
-
scanned.ids += ids.length;
|
|
767
|
-
idCache.set(file, new Set(ids));
|
|
768
|
-
pages.push({ file, links });
|
|
769
803
|
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
804
|
+
// Filtered on the HREF, exactly like every other category below — an
|
|
805
|
+
// `excludePatterns` entry suppresses `//host/v/1.2/x` because the href
|
|
806
|
+
// matches, not because the page the href sits on is itself versioned.
|
|
807
|
+
// Without this the section had no off switch and consumers stopped
|
|
808
|
+
// reading it.
|
|
809
|
+
for (const { href, line } of pageProtocolRelative) {
|
|
810
|
+
if (excludePatterns.some((pattern) => pattern.test(href))) continue;
|
|
774
811
|
protocolRelative.push({ file: relFile, line, href });
|
|
775
812
|
}
|
|
776
|
-
|
|
777
|
-
for (const { file, links } of pages) {
|
|
813
|
+
|
|
778
814
|
for (const { href, line } of links) {
|
|
779
815
|
if (excludePatterns.some((pattern) => pattern.test(href))) continue;
|
|
780
816
|
const cacheKey = href.startsWith("/") ? href : `${file}:${href}`;
|
|
@@ -783,13 +819,16 @@ export async function checkHtmlLinksAndTrailing(
|
|
|
783
819
|
detail = await resolveDistTarget(href, distDir, basePath, dirname(file), file);
|
|
784
820
|
cache.set(cacheKey, detail);
|
|
785
821
|
}
|
|
786
|
-
if (detail.type === "missing") broken.push({ file:
|
|
822
|
+
if (detail.type === "missing") broken.push({ file: relFile, line, href });
|
|
787
823
|
if (detail.fragment !== null) {
|
|
788
824
|
let reason = detail.fragmentError;
|
|
789
825
|
if (reason === null && detail.type !== "missing" && detail.targetFile !== null && extname(detail.targetFile) === ".html") {
|
|
790
826
|
let ids = idCache.get(detail.targetFile);
|
|
791
827
|
if (ids === undefined) {
|
|
792
|
-
|
|
828
|
+
// The page being walked is already in memory; any other target is
|
|
829
|
+
// re-read here rather than retained, so the walk never holds more
|
|
830
|
+
// than one page's HTML no matter how large dist/ is.
|
|
831
|
+
const targetHtml = detail.targetFile === file ? content : await readFile(detail.targetFile, "utf-8");
|
|
793
832
|
const targetIds = extractHtmlIds(targetHtml);
|
|
794
833
|
scanned.ids += targetIds.length;
|
|
795
834
|
ids = new Set(targetIds);
|
|
@@ -797,12 +836,12 @@ export async function checkHtmlLinksAndTrailing(
|
|
|
797
836
|
}
|
|
798
837
|
if (!ids.has(detail.fragment)) reason = "missing target id";
|
|
799
838
|
}
|
|
800
|
-
if (reason !== null) anchors.push({ file:
|
|
839
|
+
if (reason !== null) anchors.push({ file: relFile, line, href, fragment: detail.fragment, reason });
|
|
801
840
|
}
|
|
802
841
|
if (checkTrailing) {
|
|
803
842
|
const pathPart = href.split("#")[0].split("?")[0];
|
|
804
843
|
if (pathPart && pathPart !== "/" && pathPart !== "." && pathPart !== "./" && !pathPart.endsWith("/") && !extname(pathPart) && detail.type === "directoryIndex") {
|
|
805
|
-
trailingSlash.push({ file:
|
|
844
|
+
trailingSlash.push({ file: relFile, line, href });
|
|
806
845
|
}
|
|
807
846
|
}
|
|
808
847
|
}
|
|
@@ -914,9 +953,16 @@ async function main() {
|
|
|
914
953
|
const realAbsolute = filter(mdxWarnings);
|
|
915
954
|
const realAnchors = filter(anchorWarnings);
|
|
916
955
|
const realTrailing = filter(trailingSlash);
|
|
917
|
-
|
|
956
|
+
// The one category filtered BEFORE printing. It has no strict gate, so the
|
|
957
|
+
// allowlist is a consumer's only way to quiet a known-good entry, and
|
|
958
|
+
// quieting has to reach the section and the count line alike or the section
|
|
959
|
+
// keeps nagging and stops being read.
|
|
960
|
+
const shownProtocolRelative = filter(protocolRelative);
|
|
961
|
+
console.log(formatReport(broken, mdxWarnings, trailingSlash, anchorWarnings, shownProtocolRelative));
|
|
918
962
|
if (hasDist) console.log(`\nBuilt HTML scan: ${scanned.links} internal link${scanned.links === 1 ? "" : "s"} and ${scanned.ids} ID attribute${scanned.ids === 1 ? "" : "s"} inspected.`);
|
|
919
|
-
if (
|
|
963
|
+
if (shownProtocolRelative.length > 0) console.log(`Protocol-relative links: ${shownProtocolRelative.length} found (informational only — see "Protocol-Relative Links" section above; not counted as issues).`);
|
|
964
|
+
// Protocol-relative suppressions are deliberately absent from this tally:
|
|
965
|
+
// the sentence is about strict-mode counts, and that category has none.
|
|
920
966
|
const skipped = broken.length - realBroken.length + mdxWarnings.length - realAbsolute.length + anchorWarnings.length - realAnchors.length + trailingSlash.length - realTrailing.length;
|
|
921
967
|
if (skipped > 0) console.log(`\nAllowlist: ${skipped} known exception${skipped === 1 ? "" : "s"} excluded from strict-mode counts (${allowlistPath}).`);
|
|
922
968
|
let failed = false;
|
|
@@ -190,7 +190,10 @@ REPO_ROOT="$(git -C "$ROOT_DIR" worktree list | head -1 | awk '{print $1}')"
|
|
|
190
190
|
PROJECT_PREFIX="$(git -C "$ROOT_DIR" rev-parse --show-prefix)"
|
|
191
191
|
MAIN_PROJECT_DIR="$REPO_ROOT/${PROJECT_PREFIX}"
|
|
192
192
|
MAIN_PROJECT_DIR="${MAIN_PROJECT_DIR%/}"
|
|
193
|
-
REPO_DOCS_DIR
|
|
193
|
+
# REPO_DOCS_DIR is set below, once the config parse resolves the real
|
|
194
|
+
# (possibly custom) docsDir -- no hardcoded fallback is defined here so a
|
|
195
|
+
# short-circuited parse fails loudly instead of silently reverting to
|
|
196
|
+
# src/content/docs (#4047).
|
|
194
197
|
|
|
195
198
|
# Read the current locale map from the project's config. The generated config
|
|
196
199
|
# puts `defaultLocale`, `docsDir`, and `locales` directly in `zudoDoc({...})`.
|
|
@@ -200,7 +203,16 @@ REPO_DOCS_DIR="$REPO_ROOT/${PROJECT_PREFIX}src/content/docs"
|
|
|
200
203
|
# This deliberately does NOT walk `src/content/docs-*`: that naming convention
|
|
201
204
|
# also matches version snapshots such as `docs-v1-ja`, which are not current
|
|
202
205
|
# locale roots. The config map is the only source of truth.
|
|
203
|
-
|
|
206
|
+
#
|
|
207
|
+
# The node call is wrapped in a function rather than inlined into the
|
|
208
|
+
# assignment's `$( ... )`: bash 3.2 (stock macOS /bin/bash) does not honour a
|
|
209
|
+
# heredoc opened inside a command substitution -- it scans the heredoc body as
|
|
210
|
+
# shell text, and the JavaScript below contains an odd number of backticks
|
|
211
|
+
# (template literals), so the whole FILE fails to parse. Keeping the heredoc at
|
|
212
|
+
# statement level sidesteps that parser bug while preserving stdin execution,
|
|
213
|
+
# argument indexing, and node's exit status through the assignment.
|
|
214
|
+
read_config_locale_data() {
|
|
215
|
+
node - "$ROOT_DIR" <<'NODE'
|
|
204
216
|
const fs = require("node:fs");
|
|
205
217
|
const path = require("node:path");
|
|
206
218
|
|
|
@@ -445,7 +457,8 @@ if (localeObject) {
|
|
|
445
457
|
}
|
|
446
458
|
}
|
|
447
459
|
NODE
|
|
448
|
-
|
|
460
|
+
}
|
|
461
|
+
CONFIG_LOCALE_DATA="$(read_config_locale_data)"
|
|
449
462
|
|
|
450
463
|
DEFAULT_LOCALE="en"
|
|
451
464
|
DOCS_DIR_REL="src/content/docs"
|
|
@@ -461,9 +474,13 @@ while IFS=$'\t' read -r record_type record_value record_extra; do
|
|
|
461
474
|
[ -n "$record_value" ] || continue
|
|
462
475
|
[ "$record_value" = "$DEFAULT_LOCALE" ] && continue
|
|
463
476
|
duplicate="false"
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
477
|
+
# bash 3.2 under `set -u` treats "${arr[@]}" on an EMPTY array as an
|
|
478
|
+
# unbound variable, so the length guard is required, not cosmetic.
|
|
479
|
+
if [ "${#LOCALE_CODES[@]}" -gt 0 ]; then
|
|
480
|
+
for existing_code in "${LOCALE_CODES[@]}"; do
|
|
481
|
+
[ "$existing_code" = "$record_value" ] && duplicate="true"
|
|
482
|
+
done
|
|
483
|
+
fi
|
|
467
484
|
[ "$duplicate" = "true" ] && continue
|
|
468
485
|
LOCALE_CODES+=("$record_value")
|
|
469
486
|
LOCALE_DIR_RELS+=("$record_extra")
|
|
@@ -578,12 +595,16 @@ locale_link_name() {
|
|
|
578
595
|
suffix=2
|
|
579
596
|
while :; do
|
|
580
597
|
collision="false"
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
598
|
+
# Length guard: see the LOCALE_CODES loop above -- bash 3.2 under `set -u`
|
|
599
|
+
# errors on "${arr[@]}" when the array is empty.
|
|
600
|
+
if [ "${#LOCALE_LINK_NAMES[@]}" -gt 0 ]; then
|
|
601
|
+
for existing in "${LOCALE_LINK_NAMES[@]}"; do
|
|
602
|
+
if [ "$existing" = "$name" ]; then
|
|
603
|
+
collision="true"
|
|
604
|
+
break
|
|
605
|
+
fi
|
|
606
|
+
done
|
|
607
|
+
fi
|
|
587
608
|
[ "$collision" = "false" ] && break
|
|
588
609
|
name="docs-$code-$suffix"
|
|
589
610
|
suffix=$((suffix + 1))
|
|
@@ -606,13 +627,17 @@ locale_starter_note() {
|
|
|
606
627
|
esac
|
|
607
628
|
}
|
|
608
629
|
|
|
609
|
-
# Build exact locale-map guidance for the generated skill.
|
|
610
|
-
#
|
|
630
|
+
# Build exact locale-map guidance for the generated skill. Entries come from
|
|
631
|
+
# configured `locales`, but only when the directory exists on disk -- gated
|
|
632
|
+
# by the SAME existence check generate_skill uses below before symlinking,
|
|
633
|
+
# so the guidance can never point at a locale link that was never created
|
|
634
|
+
# (#4047).
|
|
611
635
|
LOCALE_GUIDANCE="- \`$DEFAULT_LOCALE\` (default): \`${DOCS_DIR_REL%/}/\` — \`/docs/...\` ($(locale_starter_note "$DEFAULT_LOCALE"))"
|
|
612
636
|
LOCALE_GUIDANCE+=$'\n'
|
|
613
637
|
for locale_index in "${!LOCALE_CODES[@]}"; do
|
|
614
638
|
locale_code="${LOCALE_CODES[$locale_index]}"
|
|
615
639
|
locale_dir="${LOCALE_DIR_RELS[$locale_index]}"
|
|
640
|
+
[ -d "$ROOT_DIR/$locale_dir" ] || continue
|
|
616
641
|
locale_link="${LOCALE_LINK_NAMES[$locale_index]}"
|
|
617
642
|
locale_note="$(locale_starter_note "$locale_code")"
|
|
618
643
|
LOCALE_GUIDANCE+="- \`$locale_code\`: \`${locale_dir%/}/\` — \`/$locale_code/docs/...\` (${locale_note}); lookup link: \`$locale_link/\`"
|
|
@@ -627,6 +652,10 @@ if [ "$DEFAULT_LOCALE" = "ja" ]; then
|
|
|
627
652
|
else
|
|
628
653
|
for locale_index in "${!LOCALE_CODES[@]}"; do
|
|
629
654
|
if [ "${LOCALE_CODES[$locale_index]}" = "ja" ]; then
|
|
655
|
+
locale_dir="${LOCALE_DIR_RELS[$locale_index]}"
|
|
656
|
+
# Same existence check as the LOCALE_GUIDANCE loop above and
|
|
657
|
+
# generate_skill's symlink step below (#4047).
|
|
658
|
+
[ -d "$ROOT_DIR/$locale_dir" ] || break
|
|
630
659
|
JA_DOCS_LINK="${LOCALE_LINK_NAMES[$locale_index]}"
|
|
631
660
|
JA_DOCS_PATH="${LOCALE_DIR_RELS[$locale_index]}"
|
|
632
661
|
break
|