Files
notesnook/docs/help/scripts/build-versions.mjs
Ammar Ahmed 52d6044e73 docs(help): rebuild the help site with VitePress (#10169)
* docs: new help built with vitepress.

* docs: improve docs home ui

Added a Go to Docs button on homepage
Added a searchbar for directly searching what you are looking for in the docs

* docs(help): wire up with build system & setup proper ci

* Clean up new documentation (#10174)

* Docs: Cleanup pass 1

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: cleanup pass 2

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Fix vscode's manglement that I missed.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Update docs/help/contents/plans-and-limits.md

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Update docs/help/contents/rich-text-editor/outline-lists.md

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Remove self-hosting guide from sidebar, and comments for reviewer.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

---------

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>

* docs: some fixes

* ci: do not publish help on push

* docs(help): improve regional pricing & free trials

* docs: improve sidebar

* docs: some more fixes after re-review

* docs: improve search and nav

* docs: a few more fixes

* docs: fix tabs formatting compat with prettier

* docs: fix tabs formatting in various places

* docs: show correct plus button image for mobile

* docs: disable notesnook self hosting docs

* docs: update help docs with fixes

---------

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
Co-authored-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2026-08-14 08:57:51 +05:00

206 lines
6.8 KiB
JavaScript

/**
* Composes the `/v<version>/` doc trees before VitePress runs.
*
* Only *differences* are stored in the repo. `contents/_versions/<version>/`
* holds the pages whose content differs from the current docs, plus an optional
* `_excluded.txt` listing pages that did not exist in that version. Everything
* else is shared with the latest docs.
*
* For each archived version this writes a complete `contents/v<version>/` tree
* (gitignored, regenerated on every build) by layering:
*
* current docs ← newest version's overrides ← … ← this version's
*
* so a page forked once keeps applying to every older version until an older
* fork supersedes it. It also generates `.vitepress/sidebars/generated.mjs`,
* which the config imports.
*
* Run automatically by `predev` / `prebuild`.
*/
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
statSync,
writeFileSync
} from "fs";
import { dirname, join, relative } from "path";
import { fileURLToPath, pathToFileURL } from "url";
const HELP = dirname(dirname(fileURLToPath(import.meta.url)));
const CONTENTS = join(HELP, "contents");
const OVERRIDES = join(CONTENTS, "_versions");
const SIDEBARS = join(HELP, ".vitepress", "sidebars");
const { LATEST, ARCHIVED } = await import(
pathToFileURL(join(HELP, ".vitepress", "versions.mjs")).href
);
const { sidebar } = await import(
pathToFileURL(join(HELP, ".vitepress", "sidebar.mjs")).href
);
const isGeneratedVersionDir = (name) => /^v\d+\.\d+$/.test(name);
/** Every page in the current docs, as paths relative to `contents/`. */
function currentPages(dir = CONTENTS, out = []) {
for (const entry of readdirSync(dir)) {
if (dir === CONTENTS && (entry === "public" || entry === "_versions")) continue;
if (dir === CONTENTS && isGeneratedVersionDir(entry)) continue;
if (entry === ".vitepress" || entry === "node_modules") continue;
const p = join(dir, entry);
if (statSync(p).isDirectory()) currentPages(p, out);
else if (p.endsWith(".md")) out.push(relative(CONTENTS, p));
}
return out;
}
/** The overrides recorded for one version. */
function overridesFor(version) {
const dir = join(OVERRIDES, version);
const pages = new Map();
const excluded = new Set();
if (!existsSync(dir)) return { pages, excluded };
const excludeFile = join(dir, "_excluded.txt");
if (existsSync(excludeFile)) {
for (const line of readFileSync(excludeFile, "utf8").split("\n")) {
const page = line.trim();
if (page && !page.startsWith("#")) excluded.add(page);
}
}
(function walk(d) {
for (const entry of readdirSync(d)) {
const p = join(d, entry);
if (statSync(p).isDirectory()) walk(p);
else if (p.endsWith(".md")) pages.set(relative(dir, p), p);
}
})(dir);
return { pages, excluded };
}
/* Internal page links must stay inside the version; asset links must not —
images and fonts are shared across versions. */
const ASSET = /\.(png|jpe?g|gif|svg|webp|ico|css|js|woff2?|ttf|pdf|json|txt)$/i;
const scope = (href, version) =>
ASSET.test(href) ? href : `/v${version}${href}`;
function scopeLinks(markdown, version) {
const fm = markdown.match(/^---\n[\s\S]*?\n---\n/);
const head = fm
? // Frontmatter carries links too — the home page's hero actions and
// feature cards are `link:` values, not markdown links.
fm[0].replace(
/^(\s*(?:-\s+)?link:\s*)(["']?)(\/[^\s"']*)\2\s*$/gm,
(_, prefix, quote, href) => `${prefix}${quote}${scope(href, version)}${quote}`
)
: "";
const body = fm ? markdown.slice(fm[0].length) : markdown;
return (
head +
body.replace(/\]\((\/[^)\s]*)\)/g, (match, href) =>
ASSET.test(href) ? match : `](${scope(href, version)})`
)
);
}
/* ------------------------------------------------------------------ compose */
// Wipe previously generated trees so a removed version leaves nothing behind.
for (const entry of readdirSync(CONTENTS)) {
if (isGeneratedVersionDir(entry)) rmSync(join(CONTENTS, entry), { recursive: true });
}
const shared = currentPages();
const summary = [];
for (const version of ARCHIVED) {
// Layer overrides from the newest archived version down to this one.
const layers = ARCHIVED.slice(0, ARCHIVED.indexOf(version) + 1).map(overridesFor);
const resolved = new Map(); // page -> source file (or null = use current docs)
for (const page of shared) resolved.set(page, null);
for (const layer of layers) {
for (const [page, file] of layer.pages) resolved.set(page, file);
for (const page of layer.excluded) resolved.delete(page);
}
const outDir = join(CONTENTS, `v${version}`);
let forked = 0;
for (const [page, override] of resolved) {
const dest = join(outDir, page);
mkdirSync(dirname(dest), { recursive: true });
const src = override ?? join(CONTENTS, page);
writeFileSync(dest, scopeLinks(readFileSync(src, "utf8"), version));
if (override) forked++;
}
summary.push(
` v${version}: ${resolved.size} pages, ${forked} version-specific, ${
resolved.size - forked
} shared`
);
}
/* ----------------------------------------------------------------- sidebars */
/** The current sidebar, re-pointed at a version and stripped of missing pages. */
function sidebarFor(version) {
const present = new Set(
existsSync(join(CONTENTS, `v${version}`))
? (function walk(d, out = []) {
for (const entry of readdirSync(d)) {
const p = join(d, entry);
if (statSync(p).isDirectory()) walk(p, out);
else if (p.endsWith(".md"))
out.push("/" + relative(join(CONTENTS, `v${version}`), p).replace(/(index)?\.md$/, "").replace(/\/$/, "/"));
}
return out;
})(join(CONTENTS, `v${version}`))
: []
);
const exists = (link) => present.has(link) || present.has(link.replace(/\/$/, "/"));
const rewrite = (items) =>
items
.map((item) => {
const next = { ...item };
if (next.items) next.items = rewrite(next.items);
if (next.link) {
if (!exists(next.link)) return null;
next.link = `/v${version}${next.link}`;
}
return next.items && !next.items.length && !next.link ? null : next;
})
.filter(Boolean);
return rewrite(sidebar);
}
mkdirSync(SIDEBARS, { recursive: true });
writeFileSync(
join(SIDEBARS, "generated.mjs"),
[
"// Generated by scripts/build-versions.mjs — do not edit, do not commit.",
"export const archivedSidebars = {",
...ARCHIVED.map(
(v) => ` "/v${v}/": ${JSON.stringify(sidebarFor(v), null, 2).replace(/\n/g, "\n ")},`
),
"};",
""
].join("\n")
);
console.log(
ARCHIVED.length
? [`Composed ${ARCHIVED.length} archived version(s) (latest is v${LATEST}):`, ...summary].join(
"\n"
)
: `No archived versions yet — the site serves v${LATEST} from the root.`
);