haperone_local-image-compress/scripts/validate-readme-locales.js

68 lines
3.9 KiB
JavaScript

"use strict";
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const { resolveRepositoryLayout } = require("./repository-layout");
const { repositoryRoot } = resolveRepositoryLayout(__dirname);
const localeNames = [
"ar", "de", "es", "fa", "fr", "id", "it", "ja", "ko", "nl",
"pl", "pt-br", "pt", "ru", "th", "tr", "uk", "vi", "zh-cn", "zh-tw"
];
const localeFiles = localeNames.map((locale) => path.join(repositoryRoot, "assets", `README.${locale}.md`));
const allReadmes = [path.join(repositoryRoot, "README.md"), ...localeFiles];
const requiredTokens = [
"PNG", "JPEG", "WebP", "GIF", "BMP", "HEIC/HEIF", "AVIF",
"65-80", "85", "Compressed", "10-1000", "1-60", "1-365", "30", "50", "true", "false",
"Vault/.local-image-compress/backups/cache/",
"Vault/.local-image-compress/backups/originals/",
"obsidian-paste-image-rename", "app.plugins", "main.js", "GPL-3.0-or-later", "THIRD_PARTY_NOTICES.md"
];
function slugHeading(heading) {
return heading
.trim()
.toLowerCase()
.replace(/[^\p{L}\p{M}\p{N}\s-]/gu, "")
.replace(/[\u200c\u200d]/g, "")
.replace(/\s+/g, "-");
}
for (const filePath of allReadmes) {
assert(fs.existsSync(filePath), `Missing README: ${path.relative(repositoryRoot, filePath)}`);
const source = fs.readFileSync(filePath, "utf8");
const expectedFeatureImage = filePath === path.join(repositoryRoot, "README.md")
? "![Local Image Compress features](assets/Features.gif)"
: "![Local Image Compress features](Features.gif)";
assert(source.includes(expectedFeatureImage), `Missing feature GIF in ${path.relative(repositoryRoot, filePath)}`);
assert(fs.existsSync(path.resolve(path.dirname(filePath), expectedFeatureImage.match(/\(([^)]+)\)/)[1])), `Broken feature GIF link in ${path.relative(repositoryRoot, filePath)}`);
const languageLine = source.split(/\r?\n/).find((line) => line.startsWith("Read in your language:"));
assert(languageLine, `Missing language selector: ${path.relative(repositoryRoot, filePath)}`);
const languageTargets = [...languageLine.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)].map((match) => match[1]);
assert.strictEqual(languageTargets.length, 21, `Language selector must contain 21 links: ${path.relative(repositoryRoot, filePath)}`);
for (const target of languageTargets) {
assert(fs.existsSync(path.resolve(path.dirname(filePath), target)), `Broken language link in ${path.relative(repositoryRoot, filePath)}: ${target}`);
}
const headings = new Set([...source.matchAll(/^#{1,6}\s+(.+)$/gm)].map((match) => slugHeading(match[1])));
for (const anchor of [...source.matchAll(/\]\(#([^)]+)\)/g)].map((match) => match[1])) {
assert(headings.has(anchor), `Broken heading link in ${path.relative(repositoryRoot, filePath)}: #${anchor}`);
}
}
for (const filePath of allReadmes) {
const source = fs.readFileSync(filePath, "utf8");
const relativePath = path.relative(repositoryRoot, filePath);
assert.strictEqual([...source.matchAll(/^### .+$/gm)].length, 12, `${relativePath} must contain the 12 canonical sections`);
assert.strictEqual([...source.matchAll(/^- \[[^\]]+\]\(#[^)]+\)$/gm)].length, 11, `${relativePath} must contain the 11 canonical table-of-contents links`);
assert.strictEqual([...source.matchAll(/^<details>$/gm)].length, 1, `${relativePath} must contain one collapsible details block`);
assert.strictEqual([...source.matchAll(/^<summary>.+<\/summary>$/gm)].length, 1, `${relativePath} must contain one details summary`);
assert.strictEqual([...source.matchAll(/^\|.+\|$/gm)].length, 14, `${relativePath} must contain the complete 12-setting table`);
for (const token of requiredTokens) {
assert(source.includes(token), `${relativePath} is missing canonical token: ${token}`);
}
assert(source.includes("100 MB") || source.includes("100 МБ"), `${relativePath} is missing the 100 MB safety limit`);
}
process.stdout.write(`README locale validation passed: ${localeFiles.length} locales, ${allReadmes.length * 21} language links.\n`);