Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ jobs:
# step and is worthless once it is not.
run: pnpm run check:icons
working-directory: site
- name: Check the guides index lists every guide
# The page the Start topic offers as the map of the library is written
# by hand, and it had drifted to 100 of 104 links while stating 104 in
# the prose and in its own meta description, in both languages. Cheap,
# so it runs before the build rather than after it.
run: pnpm run check:guides
working-directory: site
- name: Check EN/ES translation parity
run: node scripts/check-i18n-parity.mjs
working-directory: site
Expand Down Expand Up @@ -124,6 +131,13 @@ jobs:
# all green.
run: pnpm run check:sidebar
working-directory: site
- name: Check the landing, the contrast and the language bar
# Three audits that existed and ran for nobody: the colour tokens of
# both themes against the contrast floor, the landing page's headings
# and grid at ten viewport and font-size combinations, and the
# behaviour of the language suggestion bar. They share one preview.
run: pnpm run check:visual
working-directory: site
# Only the branch that publishes needs the tarball. A pull request has
# already got what it came for once the checks above have run.
- uses: actions/upload-pages-artifact@v5
Expand Down
2 changes: 1 addition & 1 deletion docs/materials/absorbers/porous-absorbers.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ $|\mu_a| > 40$ above 50 Hz, and $\mu_b$ falls from 1.0 at 50 Hz to 0.82 at
even though the printed sentence says "the ratio modulus": the model gives
$\mu_b(1500) = 0.811 + 0.473j$, whose real part is 1.1 % from the printed
value while its modulus, 0.939, is 14.5 % away. That is recorded in
[Errata](/phonometry/reference/errata/), along with the parameter sweep that
[Errata](../../ERRATA.md), along with the parameter sweep that
fails to bring $|\mu_b|$ anywhere near 0.82. The impedance peak of the book's
thinner sample is printed as 860 Hz and comes out at 863.5 Hz under the
$\mathrm{Im}(Z_s)$ peak rule. Everything else, including the whole
Expand Down
2 changes: 1 addition & 1 deletion scripts/api_taxonomy.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ def module_section(module: str) -> Section:


def _parent_subpackage(module: str) -> str:
"""``phonometry.signals.levels`` -> ``signal``; top level -> ``""``."""
"""``phonometry.signals.levels`` -> ``signals``; top level -> ``""``."""
parts = module.split(".")
return parts[1] if len(parts) > 2 else ""

Expand Down
21 changes: 12 additions & 9 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@
"version": "1.0.0",
"private": true,
"scripts": {
"prebuild": "node scripts/stage-media.mjs && node scripts/copy-llms.mjs",
"postbuild": "node scripts/add-sitemap-lastmod.mjs && node scripts/fix-redirect-doctype.mjs && node scripts/optimize-images.mjs && node scripts/emit-page-markdown.mjs",
"build": "astro build",
"check:guides": "node scripts/check-guides-index.mjs",
"check:i18n": "node scripts/check-i18n-parity.mjs",
"check:icons": "node scripts/icon-sheet.mjs --check",
"check:math": "node scripts/check-math-render.mjs",
"check:sidebar": "node scripts/with-preview.mjs -- node scripts/check-sidebar.mjs",
"check:visual": "node scripts/with-preview.mjs -- sh -c 'node scripts/check-contrast.mjs && node scripts/check-home-headings.mjs && node scripts/check-lang-suggest.mjs'",
"dev": "astro dev",
"build": "astro build",
"identity:sync": "node scripts/sync-identity.mjs",
"preview": "astro preview",
"html-validate": "html-validate 'dist/**/*.html'",
"pa11y": "node scripts/with-preview.mjs -- ./node_modules/.bin/pa11y-ci",
"lighthouse": "node scripts/lighthouse-audit.mjs",
"icons:sheet": "node scripts/icon-sheet.mjs",
"check:icons": "node scripts/icon-sheet.mjs --check"
"identity:sync": "node scripts/sync-identity.mjs",
"lighthouse": "node scripts/lighthouse-audit.mjs",
"pa11y": "node scripts/with-preview.mjs -- ./node_modules/.bin/pa11y-ci",
"postbuild": "node scripts/add-sitemap-lastmod.mjs && node scripts/fix-redirect-doctype.mjs && node scripts/optimize-images.mjs && node scripts/emit-page-markdown.mjs",
"prebuild": "node scripts/stage-media.mjs && node scripts/copy-llms.mjs",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/starlight": "^0.41.5",
Expand All @@ -34,6 +36,7 @@
"devDependencies": {
"html-validate": "^11.5.6",
"lighthouse": "^13.4.1",
"pa11y-ci": "^4.1.1"
"pa11y-ci": "^4.1.1",
"puppeteer": "24.43.1"
}
}
3 changes: 3 additions & 0 deletions site/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

143 changes: 143 additions & 0 deletions site/scripts/check-guides-index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// The guides index lists every guide, and says how many there are.
//
// start/guides.md is what the Start topic offers as the map of the library, and
// it is written by hand. Nothing checked it, so it drifted: it linked 100 of the
// 104 guides while stating 104 in the prose and again in the frontmatter
// description, in both languages. Two substantial features were reachable only
// from the sidebar, and the page contradicted itself in the text a search engine
// quotes.
//
// Both halves are checked here. The links, because a guide that is not on the
// map is as good as unwritten for a reader who starts there; and the count,
// because a number in prose is a claim like any other. The spelled-out form is
// checked too, since it is the half nobody remembers to update.
//
// Usage: node scripts/check-guides-index.mjs
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';

import { topics } from '../src/data/topics.mjs';

const here = dirname(fileURLToPath(import.meta.url));
const CONTENT = join(here, '..', 'src', 'content', 'docs');

/**
* The topics whose pages are guides: every one that teaches, which is every
* topic except the three carrying no teaching prose. Derived rather than
* listed, for the same reason the byline derives it (src/components/PageTitle).
*/
const NOT_A_DOMAIN = new Set(['start', 'reference', 'api']);
const DOMAINS = topics.map((topic) => topic.id).filter((id) => id && !NOT_A_DOMAIN.has(id));

/** Every markdown file under `dir`, at any depth. */
function walk(dir, out = []) {
for (const name of readdirSync(dir)) {
const full = join(dir, name);
if (statSync(full).isDirectory()) walk(full, out);
else if (/\.mdx?$/.test(name)) out.push(full);
}
return out;
}

/** The guide routes of one locale, in tree order, without the index pages. */
function guideRoutes(root) {
const routes = [];
for (const domain of DOMAINS) {
for (const file of walk(join(root, domain)).sort()) {
const route = relative(root, file).replace(/\.mdx?$/, '');
if (!/(^|\/)index$/.test(route)) routes.push(route);
}
}
return routes;
}

/** A count in words, in the two languages the site publishes. */
function spell(n, lang) {
const table = {
en: {
units: ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen',
'eighteen', 'nineteen'],
tens: ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'],
},
es: {
units: ['', 'uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis', 'siete', 'ocho', 'nueve',
'diez', 'once', 'doce', 'trece', 'catorce', 'quince', 'dieciséis', 'diecisiete',
'dieciocho', 'diecinueve'],
tens: ['', '', 'veinte', 'treinta', 'cuarenta', 'cincuenta', 'sesenta', 'setenta',
'ochenta', 'noventa'],
},
}[lang];
const below100 = (v) =>
v < 20
? table.units[v]
: table.tens[Math.floor(v / 10)] +
(v % 10 ? (lang === 'es' ? ' y ' : '-') + table.units[v % 10] : '');
if (n < 100) return below100(n);
const rest = n % 100;
const hundreds = Math.floor(n / 100);
if (lang === 'es') {
const head = hundreds === 1 ? (rest ? 'ciento' : 'cien') : `${table.units[hundreds]}cientos`;
return rest ? `${head} ${below100(rest)}` : head;
}
// "a hundred and four" and "one hundred and four" are both the number; the
// page uses the first, and a rewrite to the second should not fail a check
// about arithmetic.
const head = hundreds === 1 ? '(?:a|one) hundred' : `${table.units[hundreds]} hundred`;
return rest ? `${head} and ${below100(rest)}` : head;
}

const LOCALES = [
{ lang: 'en', root: CONTENT, word: 'guides' },
{ lang: 'es', root: join(CONTENT, 'es'), word: 'guías' },
];

let failures = 0;
const fail = (message) => {
console.error(`FAIL ${message}`);
failures += 1;
};
const ok = (message) => console.log(`ok ${message}`);

for (const { lang, root, word } of LOCALES) {
const routes = guideRoutes(root);
const index = readFileSync(join(root, 'start', 'guides.md'), 'utf8');

// The route has to appear as a link destination, not merely as text: a slug
// in a code block or a comment would satisfy a plain substring while giving
// the reader nothing to select.
const prefix = lang === 'es' ? '/phonometry/es/' : '/phonometry/';
const missing = routes.filter((route) => !index.includes(`](${prefix}${route}/)`));
if (missing.length > 0) {
fail(`${lang}: ${missing.length} guide(s) the index does not link: ${missing.join(', ')}`);
} else {
ok(`${lang}: the index links all ${routes.length} guides`);
}

// Every number that introduces the word, in the prose and in the frontmatter
// description, has to be the number of guides there are.
const digits = [...index.matchAll(new RegExp(String.raw`(\d{2,4})\s+${word}`, 'g'))].map((m) =>
Number(m[1]),
);
const wrong = digits.filter((n) => n !== routes.length);
if (digits.length === 0) fail(`${lang}: the index states no count`);
else if (wrong.length > 0) {
fail(`${lang}: the index says ${[...new Set(wrong)].join(', ')} where there are ${routes.length}`);
} else {
ok(`${lang}: every count it states in figures says ${routes.length}`);
}

// And the same number in words, which is the half nobody updates.
// `spell` returns a pattern rather than one spelling, since a number has more
// than one correct wording and the check is about the arithmetic.
const written = spell(routes.length, lang);
if (new RegExp(String.raw`\b${written}\s+${word}`, 'i').test(index)) {
ok(`${lang}: the count in words agrees with ${routes.length}`);
} else {
fail(`${lang}: the index does not say "${written} ${word}" anywhere, so its count in words has drifted from ${routes.length}`);
}
}

console.log(`\n${failures} failing check(s).`);
process.exit(failures ? 1 : 0);
9 changes: 8 additions & 1 deletion site/scripts/check-home-headings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,14 @@ for (const c of CASES) {
}
await page.setViewport({ width: c.width, height: c.height, deviceScaleFactor: SHOTS ? 2 : 1 });
await page.goto(`${BASE}${BASE_PATH}${c.path}`, { waitUntil: 'domcontentloaded', timeout: 90000 });
await page.addStyleTag({ content: 'astro-dev-toolbar{display:none !important}' });
// Only the dev server renders this toolbar, so against a preview of `dist`
// the rule matches nothing. The call itself is not free of consequence: the
// execution context can be replaced between the navigation and it, and the
// rejection failed a whole run once for a style rule that had nothing to
// hide. Losing it costs nothing, so it is allowed to fail.
await page
.addStyleTag({ content: 'astro-dev-toolbar{display:none !important}' })
.catch(() => {});
await new Promise((r) => setTimeout(r, 500));

const { problems, root, wrapped } = await page.evaluate(audit);
Expand Down
9 changes: 8 additions & 1 deletion site/scripts/check-lang-suggest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,14 @@ expect(
await new Promise((r) => setTimeout(r, 400));
// The dev server parks its toolbar in the middle of the bottom edge, and
// it hit-tests. It is not part of the site.
await page.addStyleTag({ content: 'astro-dev-toolbar{display:none !important}' });
// Only the dev server renders this toolbar, so against a preview of `dist`
// the rule matches nothing. The call itself is not free of consequence: the
// execution context can be replaced between the navigation and it, and the
// rejection failed a whole run once for a style rule that had nothing to
// hide. Losing it costs nothing, so it is allowed to fail.
await page
.addStyleTag({ content: 'astro-dev-toolbar{display:none !important}' })
.catch(() => {});
const hits = await page.evaluate(() => {
// A regression that keeps the bar off screen is exactly what this check
// is for, so report it as a failed hit test rather than throwing a raw
Expand Down
9 changes: 8 additions & 1 deletion site/scripts/check-page-chips.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,14 @@ async function landing({ path, viewport, theme, banner }) {
banner ? (path.startsWith('/es/') ? ['en-US', 'en'] : ['es-ES', 'es']) : null,
);
await page.goto(`${BASE}${BASE_PATH}${path}`, { waitUntil: 'networkidle0', timeout: 90000 });
await page.addStyleTag({ content: 'astro-dev-toolbar{display:none !important}' });
// Only the dev server renders this toolbar, so against a preview of `dist`
// the rule matches nothing. The call itself is not free of consequence: the
// execution context can be replaced between the navigation and it, and the
// rejection failed a whole run once for a style rule that had nothing to
// hide. Losing it costs nothing, so it is allowed to fail.
await page
.addStyleTag({ content: 'astro-dev-toolbar{display:none !important}' })
.catch(() => {});
const out = await page.evaluate(async (wantBanner) => {
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const bar = document.getElementById('lang-suggest');
Expand Down
9 changes: 8 additions & 1 deletion site/scripts/check-sidebar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,14 @@ async function open(path, { phone = false } = {}) {
const page = await context.newPage();
await page.setViewport(phone ? { width: 390, height: 844 } : { width: 1440, height: 900 });
await page.goto(`${BASE}${BASE_PATH}${path}`, { waitUntil: 'networkidle0', timeout: 90000 });
await page.addStyleTag({ content: 'astro-dev-toolbar{display:none !important}' });
// Only the dev server renders this toolbar, so against a preview of `dist`
// the rule matches nothing. The call itself is not free of consequence: the
// execution context can be replaced between the navigation and it, and the
// rejection failed a whole run once for a style rule that had nothing to
// hide. Losing it costs nothing, so it is allowed to fail.
await page
.addStyleTag({ content: 'astro-dev-toolbar{display:none !important}' })
.catch(() => {});
if (phone) {
await page.evaluate(() => document.querySelector('starlight-menu-button button')?.click());
await new Promise((r) => setTimeout(r, 300));
Expand Down
11 changes: 6 additions & 5 deletions site/scripts/shared/audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ export const BASE_PATH = basePath;
/**
* The puppeteer package.
*
* It is in the tree because pa11y-ci depends on it, not because we declare it,
* so under pnpm's default layout it is not linked into site/node_modules and a
* plain import does not find it. Normal resolution first (which is what works
* under `node-linker=hoisted`, npm and yarn), then the virtual store, then one
* actionable error.
* It is a declared dev dependency now. It used to be in the tree only because
* pa11y-ci depends on it, which under pnpm's default layout does not link it
* into site/node_modules at all: a plain import did not find it, and five
* checks rested on a package that a pa11y-ci upgrade could have taken away
* without a word. The store lookup below is kept because it costs nothing and
* still answers the case where the declared copy is not linked.
*/
export function loadPuppeteer() {
const require = createRequire(import.meta.url);
Expand Down
17 changes: 17 additions & 0 deletions site/src/content/docs/es/start/guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@ ANSI/ASA S12.2 y ASTM E413/E1414.
- [Transmisión por flancos en laboratorio (ISO 10848)](/phonometry/es/buildings/insulation/flanking-lab/):
el índice de reducción de vibraciones de unión y las diferencias de niveles de
flanco medidas en una instalación de ensayo.
- [Fuentes de impacto pesadas y blandas (ISO 16283-2)](/phonometry/es/buildings/insulation/heavy-impact-sources/):
la pelota de caucho y la máquina de neumático: el nivel de exposición a la
fuerza de impacto que las especifica, la comprobación de laboratorio que tienen
que pasar y el número único del anexo D de ISO 717-2.
- [Índices globales de aislamiento (ISO 717)](/phonometry/es/buildings/insulation/insulation-ratings/):
los motores de curva de referencia aéreo y de impactos con $C$, $C_{tr}$ y
$C_I$, los términos de rango ampliado y la ficha de ISO 717.
Expand All @@ -334,6 +338,10 @@ ANSI/ASA S12.2 y ASTM E413/E1414.
- [Predicción del aislamiento acústico (EN 12354)](/phonometry/es/buildings/design/insulation-prediction/):
el aislamiento in situ a ruido aéreo y de impactos entre recintos a partir de
datos de elementos, con sus caminos de flancos.
- [Predicción detallada por bandas (ISO 12354)](/phonometry/es/buildings/design/detailed-prediction/):
la misma predicción banda a banda en vez de como un solo número: datos in situ
de elemento y de unión, el índice por flancos y el nivel de impactos, y la
contribución de cada camino a R'w y L'n,w.
- [Predicción del aislamiento de paneles](/phonometry/es/buildings/design/panel-sound-insulation/):
la ley de masa y la caída de coincidencia, las paredes dobles, rendijas y
aberturas, la eficiencia de radiación de placas y las movilidades puntuales.
Expand Down Expand Up @@ -446,6 +454,10 @@ IEC 61400-11.
la fuente viaria común europea del Anexo II 2.2: potencia acústica de rodadura
y propulsión por categoría de vehículo y potencia direccional por metro de
línea fuente.
- [Emisión de la fuente ferroviaria CNOSSOS-EU](/phonometry/es/environment/sources/cnossos-rail-emission/):
la fuente ferroviaria del anexo II 2.3: rugosidad y filtro de contacto, ruido
de impacto, chirrido en curva, tracción y ruido aerodinámico, y las dos líneas
de fuente equivalentes.
- [Efecto suelo esférico y barreras avanzadas](/phonometry/es/environment/propagation/ground-barriers/):
el coeficiente de reflexión de Weyl-Van der Pol sobre suelo de impedancia
finita y la difracción en barreras desde la teoría ondulatoria.
Expand Down Expand Up @@ -491,6 +503,11 @@ ISO 17208-1/-2, ISO 18406 y JOMOPANS-ECHO.
los solvers de modos normales, trazado de rayos y ecuación parabólica de la
guía de ondas estratificada, y cómo elegir modelo de propagación.

- [Exposición a ruido de mamíferos marinos](/phonometry/es/underwater/marine-mammal-exposure/):
la cara auditiva de ese ruido: los audiogramas de grupo, las funciones de
ponderación reglamentarias con su versión de guía, y la exposición de una
campaña de hincado frente a los criterios de lesión.

## [Fuentes y dispositivos](/phonometry/es/devices/)

Potencia acústica, intensidad, declaraciones de emisión, electroacústica y
Expand Down
Loading