diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9c1fb831..baeb70a7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -183,7 +183,7 @@ jobs: missing=0 for page in dir/overview.html dir/ru/overview.html dir/appendcolumns.html; do - for marker in 'rel="canonical"' 'asu-fab' 'asu-language-open' 'SoftwareSourceCode'; do + for marker in 'rel="canonical"' 'asu-languages' 'asu-ai-notice' 'SoftwareSourceCode'; do if ! grep -qF "$marker" "$page"; then echo "::error::${page} is missing ${marker}" missing=1 diff --git a/docs/writerside-de/search.html b/docs/writerside-de/search.html deleted file mode 100644 index 3cf9da14..00000000 --- a/docs/writerside-de/search.html +++ /dev/null @@ -1,635 +0,0 @@ - - - - - - - - - - - diff --git a/docs/writerside-fr/search.html b/docs/writerside-fr/search.html deleted file mode 100644 index 64ca7bef..00000000 --- a/docs/writerside-fr/search.html +++ /dev/null @@ -1,635 +0,0 @@ - - - - - - - - - - - diff --git a/docs/writerside-ru/search.html b/docs/writerside-ru/search.html deleted file mode 100644 index 43c0c34b..00000000 --- a/docs/writerside-ru/search.html +++ /dev/null @@ -1,635 +0,0 @@ - - - - - - - - - - - diff --git a/docs/writerside-uk/search.html b/docs/writerside-uk/search.html deleted file mode 100644 index 773bcbfe..00000000 --- a/docs/writerside-uk/search.html +++ /dev/null @@ -1,635 +0,0 @@ - - - - - - - - - - - diff --git a/docs/writerside/search.html b/docs/writerside/search.html deleted file mode 100644 index 0f87cb97..00000000 --- a/docs/writerside/search.html +++ /dev/null @@ -1,635 +0,0 @@ - - - - - - - - - - - diff --git a/scripts/docs/assets/search-widget.js b/scripts/docs/assets/search-widget.js deleted file mode 100644 index 2a24c6bc..00000000 --- a/scripts/docs/assets/search-widget.js +++ /dev/null @@ -1,365 +0,0 @@ -/** - * The search widget and language switcher injected into every published page. - * - * `scripts/docs/site.mjs` reads this file and replaces `__ASU_STRINGS__` with - * the labels of the language being built. Keeping it a real file, rather than a - * string inside the generator, is what keeps its regular expressions intact. - */ - -/* global __ASU_STRINGS__ */ - -(function () { - var strings = __ASU_STRINGS__; - - var dialog = document.getElementById("asu-search-dialog"); - - var panel = document.getElementById("asu-search-panel"); - - var input = document.getElementById("asu-search-input"); - - var list = document.getElementById("asu-search-results"); - - var status = document.getElementById("asu-search-status"); - - var open = document.getElementById("asu-search-open"); - - var languageButton = document.getElementById("asu-language-open"); - - var languageMenu = document.getElementById("asu-language-menu"); - - var index = null; - - var loading = null; - - var hits = []; - - var cursor = 0; - - // Every language publishes the same file names, so a language link keeps the - // reader on the page they were reading. - Array.prototype.forEach.call(languageMenu.querySelectorAll("a"), function (link) { - var file = window.location.pathname.split("/").pop(); - - if (file && file.indexOf(".html") !== -1) { - link.href = link.getAttribute("href") + file; - } - }); - - function base() { - var path = window.location.pathname; - - return path.slice(0, path.lastIndexOf("/") + 1); - } - - function load() { - if (loading) { - return loading; - } - - loading = fetch(base() + "search-index.json") - .then(function (response) { - if (!response.ok) { - throw new Error(String(response.status)); - } - - return response.json(); - }) - .then(function (data) { - index = data; - - return data; - }) - .catch(function () { - index = []; - status.textContent = strings.missing; - - return []; - }); - - return loading; - } - - function escapeHtml(text) { - return text.replace(/[&<>"]/g, function (character) { - return { "&": "&", "<": "<", ">": ">", '"': """ }[character]; - }); - } - - function highlight(text, terms) { - var html = escapeHtml(text); - - terms.forEach(function (term) { - var pattern = new RegExp("(" + term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "ig"); - - html = html.replace(pattern, "$1"); - }); - - return html; - } - - function snippetOf(entry, terms) { - var text = entry.text || ""; - - var at = -1; - - terms.some(function (term) { - at = text.toLowerCase().indexOf(term); - - return at !== -1; - }); - - if (at === -1) { - return entry.summary || text.slice(0, 140); - } - - var from = Math.max(0, at - 60); - - return (from > 0 ? "…" : "") + text.slice(from, from + 160) + "…"; - } - - function score(entry, terms) { - var title = (entry.title || "").toLowerCase(); - - var text = ((entry.summary || "") + " " + (entry.text || "")).toLowerCase(); - - var total = 0; - - for (var i = 0; i < terms.length; i += 1) { - var term = terms[i]; - - if (title === term) { - total += 120; - } else if (title.indexOf(term) === 0) { - total += 60; - } else if (title.indexOf(term) !== -1) { - total += 30; - } else if (text.indexOf(term) !== -1) { - total += 5; - } else { - return 0; - } - } - - return total; - } - - function render(terms) { - list.innerHTML = ""; - - hits.forEach(function (entry, position) { - var item = document.createElement("li"); - - item.setAttribute("role", "option"); - item.setAttribute("aria-selected", position === cursor ? "true" : "false"); - - var link = document.createElement("a"); - - link.href = base() + entry.url; - link.innerHTML = - '' + - highlight(entry.title, terms) + - "" + - '
' + - highlight(snippetOf(entry, terms), terms) + - "
"; - - item.appendChild(link); - list.appendChild(item); - }); - } - - function search() { - var query = input.value.trim().toLowerCase(); - - if (!query) { - hits = []; - list.innerHTML = ""; - status.textContent = strings.hint; - - return; - } - - load().then(function () { - var terms = query.split(/\s+/).filter(Boolean); - - hits = (index || []) - .map(function (entry) { - return { entry: entry, score: score(entry, terms) }; - }) - .filter(function (row) { - return row.score > 0; - }) - .sort(function (a, b) { - return b.score - a.score; - }) - .slice(0, 25) - .map(function (row) { - return row.entry; - }); - - cursor = 0; - status.textContent = hits.length - ? hits.length + " " + strings.matches - : strings.empty + " “" + query + "”"; - - render(terms); - }); - } - - function show() { - dialog.setAttribute("open", ""); - input.value = ""; - input.focus(); - status.textContent = strings.hint; - list.innerHTML = ""; - load(); - } - - function hide() { - dialog.removeAttribute("open"); - } - - function move(step) { - if (hits.length === 0) { - return; - } - - cursor = (cursor + step + hits.length) % hits.length; - - Array.prototype.forEach.call(list.children, function (item, position) { - item.setAttribute("aria-selected", position === cursor ? "true" : "false"); - }); - - var selected = list.children[cursor]; - - if (selected) { - selected.scrollIntoView({ block: "nearest" }); - } - } - - /** - * The header is drawn by the help application after the page loads, so the - * two controls wait for it and then sit beside the theme switcher, wearing - * its own classes. Until it appears — or if it never does — they stay the - * floating pair they were. - */ - function mountInHeader() { - var switchers = document.querySelector(".wh-header__switchers"); - - if (!switchers || switchers.contains(languageButton)) { - return Boolean(switchers); - } - - var theme = switchers.querySelector('[data-test="theme-switcher"]'); - - if (!theme) { - return false; - } - - [open, languageButton].forEach(function (button) { - button.className = theme.className; - button.classList.add("asu-in-header"); - }); - - switchers.insertBefore(open, theme); - switchers.insertBefore(languageButton, theme); - - return true; - } - - if (!mountInHeader()) { - var watcher = new MutationObserver(function () { - if (mountInHeader()) { - watcher.disconnect(); - } - }); - - watcher.observe(document.body, { childList: true, subtree: true }); - - // A header that has not arrived within a few seconds is not coming. - window.setTimeout(function () { - watcher.disconnect(); - }, 8000); - } - - /** - * The menu hangs under its button wherever the button ended up. - */ - function placeMenu() { - var rect = languageButton.getBoundingClientRect(); - - if (!languageButton.classList.contains("asu-in-header")) { - languageMenu.style.top = ""; - languageMenu.style.right = ""; - - return; - } - - languageMenu.style.top = rect.bottom + 8 + "px"; - languageMenu.style.right = Math.max(8, window.innerWidth - rect.right) + "px"; - } - - languageButton.addEventListener("click", function () { - var opened = languageMenu.hasAttribute("open"); - - if (opened) { - languageMenu.removeAttribute("open"); - } else { - placeMenu(); - languageMenu.setAttribute("open", ""); - } - - languageButton.setAttribute("aria-expanded", String(!opened)); - }); - - window.addEventListener("resize", function () { - if (languageMenu.hasAttribute("open")) { - placeMenu(); - } - }); - - document.addEventListener("click", function (event) { - if (!languageMenu.contains(event.target) && event.target !== languageButton) { - languageMenu.removeAttribute("open"); - languageButton.setAttribute("aria-expanded", "false"); - } - }); - - open.addEventListener("click", show); - input.addEventListener("input", search); - - dialog.addEventListener("click", function (event) { - if (!panel.contains(event.target)) { - hide(); - } - }); - - document.addEventListener("keydown", function (event) { - var typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement.tagName); - - if (!dialog.hasAttribute("open")) { - var shortcut = - (event.key === "/" && !typing) || ((event.metaKey || event.ctrlKey) && event.key === "k"); - - if (shortcut) { - event.preventDefault(); - show(); - } - - return; - } - - if (event.key === "Escape") { - hide(); - } else if (event.key === "ArrowDown") { - event.preventDefault(); - move(1); - } else if (event.key === "ArrowUp") { - event.preventDefault(); - move(-1); - } else if (event.key === "Enter" && hits[cursor]) { - event.preventDefault(); - window.location.href = base() + hits[cursor].url; - } - }); -})(); diff --git a/scripts/docs/finalize-site.mjs b/scripts/docs/finalize-site.mjs index 03101c31..41da0608 100644 --- a/scripts/docs/finalize-site.mjs +++ b/scripts/docs/finalize-site.mjs @@ -75,15 +75,28 @@ function urlOf(language, page) { * or the language switcher — so the same files are put in here instead. */ function injections(language) { - const read = (name) => { - try { - return readFileSync(join(ROOT, language.root, name), "utf8").trim(); - } catch { - return ""; - } - }; + try { + return { head: readFileSync(join(ROOT, language.root, "head.html"), "utf8").trim() }; + } catch { + return { head: "" }; + } +} - return { head: read("head.html"), body: read("search.html") }; +/** + * The same page in the other languages, as a row of links. + * + * It goes at the top of the article, which puts it directly under the + * breadcrumbs, and it is plain HTML: a reader with no JavaScript, and a crawler + * that runs none, both still see every translation. + */ +function languageRow(language, page, present) { + const links = present.map((other) => + other === language + ? `${other.name}` + : `${other.name}` + ); + + return ``; } const pages = pagesOf(root); @@ -147,22 +160,29 @@ for (const page of pages) { `$1${SITE}/images/banner-1280x640.jpg$2` ); - if (injected.body && !html.includes("asu-fab")) { - const body = html.lastIndexOf(""); + // Directly under the breadcrumbs, which sit above the article. + if (!html.includes("asu-languages")) { + const article = html.match(/]*>/); - if (body !== -1) { - html = `${html.slice(0, body)}${injected.body}\n${html.slice(body)}`; + if (article) { + const at = html.indexOf(article[0]) + article[0].length; + + html = `${html.slice(0, at)}${languageRow(language, page, present)}${html.slice(at)}`; } } - // Said once at the end of every page, in that page's language. + // Said once at the end of every page, in that page's language. It goes + // inside the article: a paragraph left at the end of `body` is laid out by + // the application's own grid and lands at the top of the page. if (!html.includes("asu-ai-notice")) { - const body = html.lastIndexOf(""); + const notice = `

${language.strings.aiNotice}

`; + + const article = html.lastIndexOf(""); - if (body !== -1) { - const notice = `

${language.strings.aiNotice}

`; + const at = article === -1 ? html.lastIndexOf("") : article; - html = `${html.slice(0, body)}${notice}\n${html.slice(body)}`; + if (at !== -1) { + html = `${html.slice(0, at)}${notice}\n${html.slice(at)}`; } } diff --git a/scripts/docs/generate-topics.mjs b/scripts/docs/generate-topics.mjs index 74c57700..4ef87e86 100644 --- a/scripts/docs/generate-topics.mjs +++ b/scripts/docs/generate-topics.mjs @@ -36,7 +36,7 @@ import prettier from "prettier"; import ts from "typescript"; import { LANGUAGES, SOURCE_LANGUAGE } from "./languages.mjs"; -import { buildProfiles, headHtml, searchHtml, writersideCfg } from "./site.mjs"; +import { buildProfiles, headHtml, writersideCfg } from "./site.mjs"; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); @@ -975,7 +975,6 @@ for (const language of LANGUAGES) { // 1. Project configuration, the search widget and the structured head. emit(join(root, "buildprofiles.xml"), buildProfiles(language)); emit(join(root, "head.html"), await pretty(headHtml(language), join(root, "head.html"))); - emit(join(root, "search.html"), await pretty(searchHtml(language), join(root, "search.html"))); // The configuration is the same everywhere but for the version, which comes // from the package, so even the English project is written rather than kept. diff --git a/scripts/docs/site.mjs b/scripts/docs/site.mjs index 0c1aeb37..9ae6d3a3 100644 --- a/scripts/docs/site.mjs +++ b/scripts/docs/site.mjs @@ -133,260 +133,3 @@ export function headHtml(language) { * page. Writerside publishes no search of its own, and has no notion of a * translated site, so both live here. */ -export function searchHtml(language) { - const strings = language.strings; - - const roots = LANGUAGES.map((other) => ({ - code: other.code, - name: other.name, - url: other.webRoot, - current: other.code === language.code - })); - - return ` - - - - - - - - - - -`; -}