diff --git a/docs/site/.gitignore b/docs/site/.gitignore index 8336c7f1d7d..869cbb82391 100644 --- a/docs/site/.gitignore +++ b/docs/site/.gitignore @@ -3,3 +3,5 @@ /assets/vendor /sitemap.xml /sitemap.md + + diff --git a/docs/site/assets/css/page.css b/docs/site/assets/css/page.css index 684f7bc8e85..6f5b50aa896 100644 --- a/docs/site/assets/css/page.css +++ b/docs/site/assets/css/page.css @@ -27,7 +27,7 @@ header .navparent > div { header .title { display: table-cell; - color: yellow; + color: white; font-size: 1.5em; } @@ -41,22 +41,22 @@ header .nav a.uplink { } header .nav div.subpages { - box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); - z-index: 1; - background-color: white; - position: absolute; - color: black; - padding: 0.5em; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; + background-color: white; + position: absolute; + color: black; + padding: 0.5em; } div.subpages .hamburger { - left: 1em; - top: 1em; - color: darkslateblue; + left: 1em; + top: 1em; + color: darkslateblue; } .subpages .hamburger:hover { - color: gray; + color: gray; } header .nav ul b { @@ -80,7 +80,7 @@ header .nav ul li { } .subpages .li a { - color: black !important; + color: black !important; } header .message { diff --git a/docs/site/assets/js/build.mjs b/docs/site/assets/js/build.mjs index c185a19c817..9eaf376d7b8 100644 --- a/docs/site/assets/js/build.mjs +++ b/docs/site/assets/js/build.mjs @@ -1,4 +1,4 @@ -// extract site frontmatter, save to json +// extract site frontmatter and read from /sitemap.tsv, save to json import * as fs from "node:fs/promises"; import * as path from "node:path"; @@ -6,9 +6,28 @@ import { default as process } from "node:process"; import { default as matter } from "gray-matter"; import { SitemapStream, streamToPromise } from "sitemap"; import { Readable } from "node:stream"; +import { Dirent } from "node:fs"; +// utilities and constants + +// files to skip const SKIP_THESE = /(node_modules|\.jekyll-cache|^sitemap.*)/; +// final URL of site +const SITE = "https://cldr.unicode.org"; + +// input file +const SITEMAPFILE = "sitemap.tsv"; + +// utility collator +const coll = new Intl.Collator(["und"]); + +/** + * Directory Crawler: process one directory + * @param {string} d directory paren + * @param {string} fullPath path to this file + * @param {object} out output object + */ async function processFile(d, fullPath, out) { const f = await fs.readFile(fullPath, "utf-8"); const m = matter(f); @@ -20,7 +39,13 @@ async function processFile(d, fullPath, out) { } } -/** process one dirent */ +/** + * Directory Crawler: process one dirent + * @param {string} d directory paren + * @param {object} out output object + * @param {Dirent} e directory entry + * @returns + */ async function processEntry(d, out, e) { const fullpath = path.join(d, e.name); if (SKIP_THESE.test(e.name)) return; @@ -33,6 +58,7 @@ async function processEntry(d, out, e) { } /** + * Directory Crawler: kick off the crawl (or subcrawl) of a directory * @param {string} d path to directory * @param {object} out output struct */ @@ -42,12 +68,46 @@ async function traverse(d, out) { return Promise.all(promises); } +/** replace a/b/c.md with a/b */ +function path2dir(p) { + const dir = p.split("/").slice(0, -1).join("/"); + return dir; +} + +/** replace a/b/c.md with a/b/c.html */ +function md2html(p) { + return p.replace(/\.md$/, ".html"); +} + +/** replace a/b/c.html with a/b/c.md */ +function html2md(p) { + return p.replace(/\.html$/, ".md"); +} + /** replace a/b/c.md with a/b/c */ function dropmd(p) { return p.replace(/\.md$/, ""); } -async function writeSiteMaps(out) { +/** + * + * @param {number} n + * @returns string with n tabs + */ +function tabs(n) { + let s = []; + for (let i = 0; i < n; i++) { + s.push("\t"); + } + return s.join(""); +} + +/** convert a markdown path to a final URL */ +function mkurl(p) { + return `${SITE}/${md2html(p)}`; +} + +async function writeXmlSiteMap(out) { // simple list of links const links = await Promise.all( out.all.map(async ({ fullPath, title }) => { @@ -58,43 +118,132 @@ async function writeSiteMaps(out) { }; }) ); - const stream = new SitemapStream({ hostname: "https://cldr.unicode.org" }); + const stream = new SitemapStream({ hostname: SITE }); const data = ( await streamToPromise(Readable.from(links).pipe(stream)) ).toString(); await fs.writeFile("./sitemap.xml", data, "utf-8"); - console.log("Wrote sitemap.xml"); + console.log(`Wrote sitemap.xml with ${links.length} entries`); +} - /* - const coll = new Intl.Collator(["und"]); - const allSorted = [...out.all].sort((a, b) => - coll.compare(a.fullPath, b.fullPath) - ); - await fs.writeFile( - "./sitemap.md", - `---\ntitle: Site Map\n---\n\n` + - allSorted - .map( - ({ fullPath, title }) => - `- [/${fullPath}](/${dropmd(fullPath)}) - ${title}` - ) - .join("\n"), - "utf-8" - ); - console.log("Wrote sitemap.md"); +async function readTsvSiteMap(out) { + console.log(`Reading ${SITEMAPFILE}`); + const lines = (await fs.readFile(SITEMAPFILE, "utf-8")).split("\n"); // don't skip comment lines here so we can get line numbers. + const errors = []; + + // user's specified map + const usermap = { + /* + index: { + parent: null, + title: 'CLDR Site', + children: [ + 'cldr-spec', + 'downloads', + … + ], + }, + 'cldr-spec': { + parent: 'index', + title: …, + children: [ + 'cldr-spec/collation-guidelines', + … + ], + }, + 'cldr-spec/collation-guidelines': { + parent: 'cldr-spec', + title: …, + children: null, + }, */ + }; + // stack of parents, in order + let parents = []; + let n = 0; + for (let line of lines) { + n++; + const location = `${SITEMAPFILE}:${n}: `; // for errors + // skip comment or blank lines + if (/^[ \t]*#/.test(line) || !line.trim()) continue; + + // # of leading + const tabs = /^[\t]*/.exec(line)[0].length; + // rest of line: the actual path + const path = line.slice(tabs).trim(); + if (usermap[path]) { + errors.push(`${location} duplicate path: ${path}`); + continue; + } + const foundItem = out.all.find(({ fullPath }) => fullPath === `${path}.md`); + if (!foundItem) { + errors.push(`${location} could not find file: ${path}.md`); + continue; + } + if (!foundItem.title) { + errors.push(`${location} missing title in ${path}.md`); + // let this continue on + } + usermap[path] = { + title: foundItem.title ?? path, + }; + const parentCount = parents.length; + if (tabs < parentCount) { + /** + * index [1] + * foo [2] + * + */ + // outdent + if (tabs == 0) { + errors.push(`${location} can't have more than one root page!`); + break; + } + // drop 'n' parents + parents = parents.slice(0, tabs); + } else if (tabs > parentCount) { + // Error - wrong indent + errors.push( + `${location} indent too deep (expected ${parentCount} tabs at most)` + ); + continue; + } + const parent = parents.slice(-1)[0] || null; // calculate parent (null for index page) + usermap[path].parent = parent; + if (parent) { + // not for index + usermap[parent].children = usermap[parent].children ?? []; + usermap[parent].children.push(path); + } + parents.push(path); // for next time + } + out.usermap = usermap; + out.all.forEach(({ fullPath }) => { + if (!usermap[dropmd(fullPath)]) { + errors.push(`${SITEMAPFILE}: missing: ${dropmd(fullPath)}`); + } + }); + if (errors.length) { + errors.forEach((l) => console.error(l)); + throw Error(`${errors.length} errors reading tsv`); + } else { + console.log(`${SITEMAPFILE} Valid.`); + } } +/** top level async */ async function main() { const out = { all: [], - dirs: {}, }; await fs.mkdir("assets/json/", { recursive: true }); await traverse(".", out); + await writeXmlSiteMap(out); + await readTsvSiteMap(out); + // write final json asset + delete out.all; //not needed at this phase, so trim out of the deploy await fs.writeFile("assets/json/tree.json", JSON.stringify(out, null, " ")); console.log("Wrote assets/json/tree.json"); - await writeSiteMaps(out); } main().then( diff --git a/docs/site/assets/js/cldrsite.js b/docs/site/assets/js/cldrsite.js index a93e610782c..57342cf5768 100644 --- a/docs/site/assets/js/cldrsite.js +++ b/docs/site/assets/js/cldrsite.js @@ -18,55 +18,36 @@ function md2html(p) { return p.replace(/\.md$/, ".html"); } +/** replace a/b/c with to /a/b/c, also '' => '/' */ +function path2url(p) { + if (p === "index") { + return "/"; + } + return `/${p}`; +} + /** replace a/b/c.html with a/b/c.md */ function html2md(p) { return p.replace(/\.html$/, ".md"); } +/** replace a/b/c.md with a/b/c */ +function dropmd(p) { + return p.replace(/\.md$/, ""); +} + +/** replace a/b/c.html with a/b/c */ +function drophtml(p) { + return p.replace(/\.html$/, "").replace(/\/$/, ""); +} + /** load and cook the site data */ async function siteData() { // load the json const d = await fetch("/assets/json/tree.json"); const j = await d.json(); - const { all } = j; + const { usermap } = j; - // 'all' is an array of { title, fullPath } entries. - // Flat list of paths - const allPaths = all.map(({ fullPath }) => fullPath); - // Find all 'directories' (ending with /) - const allDirs = new Set(); - allPaths.forEach((p) => { - const segs = p.split("/").slice(0, -1); // ['', 'dir1'] - for (let n = 0; n <= segs.length; n++) { - // add all parent paths, so: '', dir1, dir1/dir2 etc. - const subpath = segs.slice(0, n).join("/"); - allDirs.add(subpath); - } - }); - j.allDirs = {}; - j.allIndexes = []; - // allDirs: '', index, downloads, etc… - allDirs.forEach((dir) => { - // presumed index page: /downloads -> /downloads.md - // also / -> /index.md - const dirIndex = `${dir || "index"}.md`; - // console.dir({dir, dirIndex}); - if (allPaths.indexOf(dirIndex) !== -1) { - j.allDirs[dir] = { index: dirIndex }; - j.allIndexes.push(dirIndex); - } else { - console.error(`No index page: ${dirIndex}`); - j.allDirs[dir] = {}; - } - j.allDirs[dir].pages = []; - }); - allPaths.forEach((p) => { - const dir = path2dir(p); - j.allDirs[dir].pages.push(p); - }); - // map md -> title - j.title = {}; - all.forEach(({ title, fullPath }) => (j.title[fullPath] = title)); return j; } @@ -97,109 +78,73 @@ const app = Vue.createApp( path: String, }, computed: { - mdPath() { - if (this.path) { - return html2md(this.path); - } - return null; - }, - ourDir() { + /** base path: 'index' or 'downloads/cldr-33' */ + base() { if (this.path) { - return path2dir(this.path); - } - return ""; - }, - ourIndex() { - if (this.tree?.value) { - // first ARE we an index page? - if (this.tree.value.allIndexes.indexOf(this.mdPath) != -1) { - return this.mdPath; // we are an index - } - return this.tree.value.allDirs[this.ourDir].index; - } - return null; - }, - ourIndexHtml() { - if (this.ourIndex) { - return md2html(this.ourIndex); - } else { - return null; - } - }, - ourIndexTitle() { - if (this.ourIndex && this.tree?.value) { - return this.tree.value.title[this.ourIndex] || this.ourIndex; + return drophtml(this.path); } else { - return null; + return "index"; // '' => 'index' } + return null; }, ourTitle() { if (this.tree?.value) { if (this.path === "") return this.rootTitle; - return this.tree.value.title[html2md(this.path)]; + return this?.tree?.value?.usermap[this.base]?.title; } }, // title of root rootTitle() { - return this.tree?.value?.title["index.md"]; + const usermap = this?.tree?.value?.usermap ?? {}; + return usermap?.index?.title ?? "CLDR"; }, - // list of pages for siblings of this dir - siblingPages() { - if (!this.tree?.value) return []; - let dirForPage = this.ourDir; - if (this.tree.value.allIndexes.indexOf(this.mdPath) != -1) { - const dirPages = Object.entries(this.tree?.value?.allDirs).filter( - ([k, { index }]) => index == this.mdPath - )[0]; - if (dirPages) { - // our page is an index -so, show the subpages instead of the siblings. - dirForPage = dirPages[0]; // the adjusted index - } else { - return []; // no sibling pages; - } - } else { - return []; // no sibling pages - } - let thePages = this.tree?.value?.allDirs[dirForPage].pages ?? []; - if (dirForPage === "") { - thePages = [...thePages, ...this.tree?.value?.allDirs["index"].pages]; - } - const c = new Intl.Collator([]); - const t = this; - return thePages - .map((path) => ({ - path, - html: md2html(path), - title: this.tree.value.title[path] ?? path, - })) - .sort((a, b) => c.compare(a.title, b.title)) - .filter(({ html }) => html != t.path); // skip showing the index page in the subpage list + children() { + const usermap = this?.tree?.value?.usermap; + if (!usermap) return []; // no children + const entry = usermap[this.base]; + const children = entry?.children; + if (!children || !children.length) return []; + return children.map((path) => ({ + path, + href: path2url(path), + title: usermap[path]?.title || path, + children: (usermap[path].children ?? []).length > 0, + })); }, ancestorPages() { const pages = []; // if we are not loaded, or if we're at the root, then exit - if (!this.tree?.value || !this.path || this.path == "index.html") - return pages; + const usermap = this?.tree?.value?.usermap; + if ( + !usermap || + !this.path || + this.path == "index.html" || + this.map == "index" + ) { + return []; + } // traverse - let path = this.path; + let path = drophtml(this.path); // can't be null, empty, or index (see above). Map a/b/c.html to a/b/c do { // calculate the immediate ancestor - const pathMd = html2md(path); - const dir = path2dir(path); - const nextIndex = this.tree.value.allDirs[dir].index || "index.md"; // falls back to top - const nextIndexHtml = md2html(nextIndex); - const nextIndexTitle = this.tree.value.title[nextIndex]; + const nextParentPath = usermap[path]?.parent; + if (!nextParentPath) break; + if (nextParentPath == path) { + console.error("Loop detected!"); + break; + } + const nextParent = usermap[nextParentPath]; + if (!nextParent) break; + const href = path2url(nextParentPath); + const { title } = nextParent || nextParentPath; // prepend pages.push({ - href: "/" + nextIndexHtml, - title: nextIndexTitle, + href, + title, + path: nextParentPath, }); - if (nextIndexHtml == path) { - console.error("Loop detected from " + this.path); - path = "index.html"; // exit - } - path = nextIndexHtml; - } while (path && path != "index.html"); // we iterate over 'path', so html + path = nextParentPath; + } while (path); // we iterate over 'path' until it returns null pages.reverse(); return pages; }, @@ -215,19 +160,17 @@ const app = Vue.createApp( {{ ancestor.title }} -
{{ ourTitle }}
+
{{ ourTitle }}
{{ ourTitle }}
@@ -244,19 +187,6 @@ const app = Vue.createApp( } ); -// app.component("CldrPage", { -// setup() {}, -// template: `

Hello

-// `, -// }); - -// app.component("CldrList", { -// setup() {}, -// template: ` -//

Hullo

-// `, -// }); - app.mount("#nav"); // load anchor.js diff --git a/docs/site/sitemap.tsv b/docs/site/sitemap.tsv new file mode 100644 index 00000000000..f1c4851542e --- /dev/null +++ b/docs/site/sitemap.tsv @@ -0,0 +1,201 @@ +# CLDR Site Map. +# This is a comment. +# The file is a TSV, tab separated value. +# There must be a single 'index' entry here at the root. +# Every page must be listed. index/charts means index/charts.md for example. +# If an item has 'sub items' it becomes a directory parent. +# You can comment out lines, but an error will be given at build time if a page is missing. +index + index/acknowledgments + index/charts + ddl + downloads + downloads/cldr-31 + downloads/cldr-32 + downloads/cldr-33 + downloads/cldr-33-1 + downloads/cldr-34 + downloads/cldr-35 + downloads/cldr-36 + downloads/cldr-37 + downloads/cldr-38 + downloads/cldr-39 + downloads/cldr-40 + downloads/cldr-41 + downloads/cldr-42 + downloads/cldr-43 + downloads/cldr-44 + downloads/cldr-45 + downloads/cldr-46 + downloads/brs-copy-en_gb-to-en_001 + index/keyboard-workgroup + index/process + index/process/cldr-data-retention-policy + index/downloads + index/downloads/cldr-43 + index/downloads/cldr-44 + index/cldr-spec + index/cldr-spec/collation-guidelines + index/cldr-spec/core-data-for-new-locales + index/cldr-spec/coverage-levels + index/cldr-spec/currency-process + index/cldr-spec/definitions + index/cldr-spec/picking-the-right-language-code + index/cldr-spec/plural-rules + index/cldr-spec/transliteration-guidelines + index/survey-tool + index/survey-tool/bulk-data-upload + index/survey-tool/faq-and-known-bugs + index/survey-tool/managing-users + index/survey-tool/coverage + index/survey-tool/survey-tool-accounts + index/corrigenda + covered-by-other-projects + index/draft-schedule + translation + translation/characters + translation/characters/character-labels + translation/characters/short-names-and-keywords + translation/characters/typographic-names + translation/core-data + translation/core-data/characters + translation/core-data/numbering-systems + translation/core-data/exemplars + translation/currency-names-and-symbols + translation/currency-names-and-symbols/currency-names + translation/currency-names-and-symbols/special-cases + translation/date-time + translation/date-time/date-times-terminology + translation/date-time/date-time-names + translation/date-time/date-time-patterns + translation/date-time/date-time-symbols + translation/displaynames + translation/displaynames/countryregion-territory-names + translation/displaynames/languagelocale-name-patterns + translation/displaynames/languagelocale-names + translation/displaynames/locale-option-names-key + translation/displaynames/script-names + translation/error-codes + translation/translation-guide-general + translation/translation-guide-general/capitalization + translation/translation-guide-general/default-content + translation/translation-guide-general/references + translation/getting-started + translation/getting-started/data-stability + translation/getting-started/empty-cache + translation/getting-started/errors-and-warnings + translation/getting-started/resolving-errors + translation/getting-started/plurals + translation/getting-started/review-formats + translation/getting-started/guide + translation/getting-started/survey-tool-phases + translation/getting-started/vetting-view + translation/grammatical-inflection + translation/miscellaneous-displaying-lists + translation/number-currency-formats + translation/number-currency-formats/number-and-currency-patterns + translation/number-currency-formats/number-symbols + translation/number-currency-formats/other-patterns + translation/miscellaneous-person-name-formats + translation/time-zones-and-city-names + translation/transforms + translation/unique-translations + translation/units + translation/units/measurement-systems + translation/units/unit-names-and-patterns + translation/language-specific + translation/language-specific/lakota + translation/language-specific/odia + translation/language-specific/persian + development + development/adding-locales + development/creating-the-archive + development/cldr-development-site + development/cldr-development-site/running-cldr-tools + development/cldr-development-site/updating-englishroot + development/cldr-big-red-switch + development/cldr-big-red-switch/generating-charts + development/coding-cldr-tools + development/coding-cldr-tools/documenting-cldr-tools + development/guidance-on-direct-modifications-to-cldr-data + development/development-process + development/development-process/design-proposals + development/development-process/design-proposals/alternate-time-formats + development/development-process/design-proposals/bcp-47-changes-draft + development/development-process/design-proposals/bcp47-syntax-mapping + development/development-process/design-proposals/bcp47-validation-and-canonicalization + development/development-process/design-proposals/bidi-handling-of-structured-text + development/development-process/design-proposals/change-to-sites + development/development-process/design-proposals/chinese-and-other-calendar-support-intercalary-months-year-cycles + development/development-process/design-proposals/consistent-casing + development/development-process/design-proposals/coverage-revision + development/development-process/design-proposals/currency-code-fallback + development/development-process/design-proposals/day-period-design + development/development-process/design-proposals/delimiter-quotation-mark-proposal + development/development-process/design-proposals/english-inheritance + development/development-process/design-proposals/european-ordering-rules-issues + development/development-process/design-proposals/extended-windows-olson-zid-mapping + development/development-process/design-proposals/fractional-plurals + development/development-process/design-proposals/generic-calendar-data + development/development-process/design-proposals/grammar-capitalization-forms-for-datetime-elements-and-others + development/development-process/design-proposals/grapheme-usage + development/development-process/design-proposals/hebrew-months + development/development-process/design-proposals/index-characters + development/development-process/design-proposals/islamic-calendar-types + development/development-process/design-proposals/iso-636-deprecation-requests-draft + development/development-process/design-proposals/json-packaging-approved-by-the-cldr-tc-on-2015-03-25 + development/development-process/design-proposals/language-data-consistency + development/development-process/design-proposals/language-distance-data + development/development-process/design-proposals/list-formatting + development/development-process/design-proposals/locale-format + development/development-process/design-proposals/localized-gmt-format + development/development-process/design-proposals/math-formula-preferences + development/development-process/design-proposals/new-bcp47-extension-t-fields + development/development-process/design-proposals/new-time-zone-patterns + development/development-process/design-proposals/path-filtering + development/development-process/design-proposals/pattern-character-for-related-year + development/development-process/design-proposals/pinyin-fixes + development/development-process/design-proposals/post-mortem + development/development-process/design-proposals/proposed-collation-additions + development/development-process/design-proposals/resolution-of-cldr-files + development/development-process/design-proposals/script-metadata + development/development-process/design-proposals/search-collators + development/development-process/design-proposals/secularneutral-eras + development/development-process/design-proposals/specifying-text-break-variants-in-locale-ids + development/development-process/design-proposals/suggested-exemplar-revisions + development/development-process/design-proposals/supported-numberingsystems + development/development-process/design-proposals/thoughts-on-survey-tool-backend + development/development-process/design-proposals/time-zone-data-reorganization + development/development-process/design-proposals/transform-fallback + development/development-process/design-proposals/transform-keywords + development/development-process/design-proposals/unihan-data + development/development-process/design-proposals/units-pixels-ems-display-resolution + development/development-process/design-proposals/uts-35-splitting + development/development-process/design-proposals/voting + development/development-process/design-proposals/xmb + development/maven + development/new-cldr-developers + development/running-tests + development/running-tools + development/updating-codes + development/updating-codes/likelysubtags-and-default-content + development/updating-codes/update-currency-codes + development/updating-codes/update-language-script-info + development/updating-codes/update-language-script-info/language-script-description + development/updating-codes/update-languagescriptregion-subtags + development/updating-codes/update-time-zone-data-for-zoneparser + development/updating-codes/update-validity-xml + development/updating-codes/external-version-metadata + development/updating-codes/updating-population-gdp-literacy + development/updating-codes/updating-script-metadata + development/updating-codes/updating-subdivision-codes + development/updating-codes/updating-subdivision-translations + development/updating-codes/updating-un-codes + development/updating-dtds + index/json-format-data + index/language-support-levels + index/locale-coverage + index/requesting-additionsupdates-to-cldr-languagepopulation-data + stable-links-info + index/cldr-presentations + index/bcp47-extension