mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
142 lines
4.2 KiB
JavaScript
142 lines
4.2 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
|
||
|
|
import fs from 'fs';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
const rootDir = process.cwd();
|
||
|
|
const docsDir = path.join(rootDir, 'docs');
|
||
|
|
const mkdocsPath = path.join(rootDir, 'mkdocs.yml');
|
||
|
|
const readmePath = path.join(rootDir, 'README.md');
|
||
|
|
const manifestPath = path.join(rootDir, 'manifest.json');
|
||
|
|
const releaseIndexPath = path.join(docsDir, 'releases.md');
|
||
|
|
const privacyPath = path.join(docsDir, 'privacy.md');
|
||
|
|
const rootPrivacyPath = path.join(rootDir, 'PRIVACY.md');
|
||
|
|
|
||
|
|
const errors = [];
|
||
|
|
|
||
|
|
function normalizeLink(link) {
|
||
|
|
return decodeURIComponent(link.split('#')[0].split('?')[0]).trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
function stripMarkdownCodeBlocks(content) {
|
||
|
|
return content
|
||
|
|
.replace(/```[\s\S]*?```/g, '')
|
||
|
|
.replace(/`[^`\n]+`/g, '');
|
||
|
|
}
|
||
|
|
|
||
|
|
function markdownFiles() {
|
||
|
|
const files = [readmePath];
|
||
|
|
|
||
|
|
function walk(dir) {
|
||
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||
|
|
const fullPath = path.join(dir, entry.name);
|
||
|
|
if (entry.isDirectory()) {
|
||
|
|
walk(fullPath);
|
||
|
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||
|
|
files.push(fullPath);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
walk(docsDir);
|
||
|
|
return files;
|
||
|
|
}
|
||
|
|
|
||
|
|
function verifyMkdocsNavTargetsExist() {
|
||
|
|
const config = fs.readFileSync(mkdocsPath, 'utf8');
|
||
|
|
const matches = [...config.matchAll(/:\s*([A-Za-z0-9_./-]+\.md)\s*$/gm)];
|
||
|
|
|
||
|
|
for (const match of matches) {
|
||
|
|
const relative = match[1];
|
||
|
|
const full = path.join(docsDir, relative);
|
||
|
|
if (!fs.existsSync(full)) {
|
||
|
|
errors.push(`mkdocs nav references missing file: docs/${relative}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!config.includes('calendar-setup.md')) {
|
||
|
|
errors.push('mkdocs nav is missing docs/calendar-setup.md');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function verifyCanonicalDocsUrl() {
|
||
|
|
const readme = fs.readFileSync(readmePath, 'utf8');
|
||
|
|
if (!readme.includes('https://tasknotes.dev/')) {
|
||
|
|
errors.push('README.md is missing canonical docs URL: https://tasknotes.dev/');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function verifyReleaseIndexMatchesManifest() {
|
||
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||
|
|
const releaseIndex = fs.readFileSync(releaseIndexPath, 'utf8');
|
||
|
|
const releaseFile = path.join(docsDir, 'releases', `${manifest.version}.md`);
|
||
|
|
|
||
|
|
if (!fs.existsSync(releaseFile)) {
|
||
|
|
errors.push(`Missing release file for manifest version: docs/releases/${manifest.version}.md`);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!releaseIndex.includes(`releases/${manifest.version}.md`)) {
|
||
|
|
errors.push(`docs/releases.md does not include current manifest version ${manifest.version}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function verifyPrivacyMirror() {
|
||
|
|
const docsPrivacy = fs.readFileSync(privacyPath, 'utf8').trimEnd();
|
||
|
|
const rootPrivacy = fs.readFileSync(rootPrivacyPath, 'utf8').trimEnd();
|
||
|
|
|
||
|
|
if (docsPrivacy !== rootPrivacy) {
|
||
|
|
errors.push('PRIVACY.md is out of sync with docs/privacy.md');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function verifyLocalMarkdownLinks() {
|
||
|
|
const files = markdownFiles();
|
||
|
|
const mdLinkPattern = /!?\[[^\]]*\]\(([^)]+)\)/g;
|
||
|
|
|
||
|
|
for (const file of files) {
|
||
|
|
if (file.includes(`${path.sep}docs${path.sep}releases${path.sep}`)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const directory = path.dirname(file);
|
||
|
|
const content = stripMarkdownCodeBlocks(fs.readFileSync(file, 'utf8'));
|
||
|
|
|
||
|
|
for (const match of content.matchAll(mdLinkPattern)) {
|
||
|
|
const raw = match[1].replace(/^<|>$/g, '').trim();
|
||
|
|
if (!raw) continue;
|
||
|
|
if (raw.startsWith('#')) continue;
|
||
|
|
if (raw.startsWith('http://') || raw.startsWith('https://')) continue;
|
||
|
|
if (raw.startsWith('mailto:')) continue;
|
||
|
|
if (raw.startsWith('data:')) continue;
|
||
|
|
|
||
|
|
const normalized = normalizeLink(raw);
|
||
|
|
if (!normalized) continue;
|
||
|
|
|
||
|
|
const resolved = normalized.startsWith('/')
|
||
|
|
? path.join(rootDir, normalized.slice(1))
|
||
|
|
: path.resolve(directory, normalized);
|
||
|
|
|
||
|
|
if (!fs.existsSync(resolved)) {
|
||
|
|
const relativeFile = path.relative(rootDir, file);
|
||
|
|
errors.push(`Broken local link in ${relativeFile}: ${raw}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
verifyMkdocsNavTargetsExist();
|
||
|
|
verifyCanonicalDocsUrl();
|
||
|
|
verifyReleaseIndexMatchesManifest();
|
||
|
|
verifyPrivacyMirror();
|
||
|
|
verifyLocalMarkdownLinks();
|
||
|
|
|
||
|
|
if (errors.length > 0) {
|
||
|
|
console.error('Documentation checks failed:');
|
||
|
|
for (const error of errors) {
|
||
|
|
console.error(`- ${error}`);
|
||
|
|
}
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('Documentation checks passed.');
|