mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-23 06:10:02 +08:00
docs/content/docs/limitations.mdx now holds the frontmatter, the headings, the anchor spans and one <include> line per partial. The text moves unchanged to docs/content/limitations/: one file per labeled bullet (<section>/<label>.mdx) and one per section's other text (<section>.mdx). Edits to different bullets no longer touch adjacent lines of one file. remarkDocsConventions splices the included partials into the page and joins the lists that consecutive bullet partials contribute, so the page, its Markdown twin and the search index render as before. The route tests read the page with its partials inlined. A page that includes partials takes its last-modified date, used by the sitemap, from the newest commit to the page or any of its partials. docs/scripts/limitations-partials.mjs joins the partials back into one file, splits a single-file page, and merges a branch's edits to the single-file page into the partials.
169 lines
6.8 KiB
JavaScript
169 lines
6.8 KiB
JavaScript
/* Splits the Limitations page into partials and joins them back.
|
|
*
|
|
* content/docs/limitations.mdx keeps the frontmatter, the headings and the
|
|
* anchor spans. Everything else lives in content/limitations/:
|
|
*
|
|
* introduction.mdx prose before the first heading
|
|
* <section>.mdx a section's prose (paragraphs, code
|
|
* blocks, unlabeled lists)
|
|
* <section>/<label>.mdx one labeled bullet (`- **Label:** ...`)
|
|
*
|
|
* <section> is the slug of the nearest heading and <label> the slug of the
|
|
* bullet's bold label. The page includes each partial with
|
|
* `<include>../limitations/<path>.mdx</include>` in page order; the remark
|
|
* step in src/lib/remark-docs.ts joins consecutive bullet partials back into
|
|
* one list.
|
|
*
|
|
* Usage (from docs/):
|
|
* node scripts/limitations-partials.mjs join [page] print the page with partials inlined
|
|
* node scripts/limitations-partials.mjs split <file> [page] write the page and partials from one file
|
|
* node scripts/limitations-partials.mjs merge <base> <theirs>
|
|
* three-way merge a branch's single-file page (<theirs>, forked from
|
|
* <base>) into the current partials, then split the result */
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, relative, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const docsRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
const defaultPage = join(docsRoot, "content/docs/limitations.mdx");
|
|
const partialsDir = join(docsRoot, "content/limitations");
|
|
const includeLine = /^<include>(\.\.\/limitations\/[\w/-]+\.mdx)<\/include>$/;
|
|
const labeledBullet = /^- \*\*([^*]+?):\*\*/;
|
|
|
|
export function slug(text) {
|
|
return text
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
}
|
|
|
|
/** The page with every partial include replaced by the partial's text. */
|
|
export function joinPage(page = defaultPage) {
|
|
const out = [];
|
|
for (const line of readFileSync(page, "utf8").split("\n")) {
|
|
const include = line.match(includeLine);
|
|
out.push(include ? readFileSync(resolve(dirname(page), include[1]), "utf8").replace(/\n$/, "") : line);
|
|
}
|
|
// Consecutive bullet partials sit between blank lines in the page; the
|
|
// single file has them as one list.
|
|
let text = out.join("\n");
|
|
for (let prev; prev !== text; ) {
|
|
prev = text;
|
|
text = text.replace(/^(- \*\*[^\n]*)\n\n(?=- \*\*)/gm, "$1\n");
|
|
}
|
|
return text.replace(/\n*$/, "\n");
|
|
}
|
|
|
|
/** Page text and partials (path relative to content/limitations → text). */
|
|
export function splitPage(text) {
|
|
const lines = text.replace(/\n$/, "").split("\n");
|
|
if (lines[0] !== "---") throw new Error("the page has no frontmatter");
|
|
const fmEnd = lines.indexOf("---", 1);
|
|
const page = lines.slice(0, fmEnd + 1);
|
|
const partials = new Map();
|
|
let section = "introduction";
|
|
let prose = [];
|
|
let proseCount = 0;
|
|
const flushProse = () => {
|
|
while (prose.length && prose[prose.length - 1] === "") prose.pop();
|
|
while (prose.length && prose[0] === "") prose.shift();
|
|
if (!prose.length) return;
|
|
const name = `${section}${proseCount ? `-${proseCount + 1}` : ""}.mdx`;
|
|
proseCount++;
|
|
add(name, prose.join("\n"));
|
|
prose = [];
|
|
};
|
|
const add = (name, body) => {
|
|
if (partials.has(name)) throw new Error(`two partials named ${name}`);
|
|
partials.set(name, body + "\n");
|
|
page.push("", `<include>../limitations/${name}</include>`);
|
|
};
|
|
let inFence = false;
|
|
let afterBullet = false;
|
|
for (const line of lines.slice(fmEnd + 1)) {
|
|
if (afterBullet && line !== "" && !labeledBullet.test(line) && !/^#{2,6} /.test(line))
|
|
throw new Error(`a labeled bullet is followed by a line that is not a blank line, bullet or heading: ${line.slice(0, 60)}`);
|
|
if (/^(```|~~~)/.test(line)) inFence = !inFence;
|
|
const heading = !inFence && line.match(/^#{2,6} (.*)$/);
|
|
const anchor = !inFence && /^<span id="[\w-]+" \/>$/.test(line);
|
|
const bullet = !inFence && line.match(labeledBullet);
|
|
afterBullet = Boolean(bullet);
|
|
if (heading || anchor) {
|
|
flushProse();
|
|
page.push("", line);
|
|
if (heading) {
|
|
section = slug(heading[1]);
|
|
proseCount = 0;
|
|
}
|
|
} else if (bullet) {
|
|
flushProse();
|
|
add(`${section}/${slug(bullet[1])}.mdx`, line);
|
|
} else {
|
|
prose.push(line);
|
|
}
|
|
}
|
|
flushProse();
|
|
// Collapse the blank-line runs the pushes above produce.
|
|
const pageText = page.join("\n").replace(/\n{3,}/g, "\n\n") + "\n";
|
|
return { page: pageText, partials };
|
|
}
|
|
|
|
function write(text, page = defaultPage) {
|
|
const { page: pageText, partials } = splitPage(text);
|
|
rmSync(partialsDir, { recursive: true, force: true });
|
|
for (const [name, body] of partials) {
|
|
const file = join(partialsDir, name);
|
|
mkdirSync(dirname(file), { recursive: true });
|
|
writeFileSync(file, body);
|
|
}
|
|
writeFileSync(page, pageText);
|
|
const roundTrip = joinPage(page);
|
|
if (roundTrip !== text) throw new Error("joining the partials does not reproduce the input");
|
|
console.error(`${relative(docsRoot, page)}: ${partials.size} partials in ${relative(docsRoot, partialsDir)}/`);
|
|
}
|
|
|
|
function listPartials(dir = partialsDir) {
|
|
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>
|
|
entry.isDirectory() ? listPartials(join(dir, entry.name)) : [join(dir, entry.name)],
|
|
);
|
|
}
|
|
|
|
const [command, ...args] = process.argv.slice(2);
|
|
if (command === "join") {
|
|
process.stdout.write(joinPage(args[0] ?? defaultPage));
|
|
} else if (command === "split") {
|
|
write(readFileSync(args[0], "utf8"), args[1] ?? defaultPage);
|
|
} else if (command === "merge") {
|
|
const [base, theirs] = args;
|
|
const work = mkdtempSync(join(tmpdir(), "scriptc-limitations-"));
|
|
try {
|
|
const ours = join(work, "ours.mdx");
|
|
writeFileSync(ours, joinPage());
|
|
let conflicted = false;
|
|
try {
|
|
execFileSync("git", ["merge-file", "-L", "main", "-L", "base", "-L", "branch", ours, base, theirs]);
|
|
} catch (error) {
|
|
if (error.status > 0 && error.status < 128) conflicted = true;
|
|
else throw error;
|
|
}
|
|
const merged = readFileSync(ours, "utf8");
|
|
if (conflicted) {
|
|
const out = join(docsRoot, "limitations.merged.mdx");
|
|
writeFileSync(out, merged);
|
|
console.error(`conflicts: resolve them in ${relative(process.cwd(), out)}, then run: node scripts/limitations-partials.mjs split ${relative(docsRoot, out)}`);
|
|
process.exitCode = 1;
|
|
} else {
|
|
write(merged);
|
|
}
|
|
} finally {
|
|
rmSync(work, { recursive: true, force: true });
|
|
}
|
|
} else if (command === "list") {
|
|
for (const file of listPartials().sort()) console.log(relative(docsRoot, file));
|
|
} else {
|
|
console.error("usage: limitations-partials.mjs join [page] | split <file> [page] | merge <base> <theirs> | list");
|
|
process.exitCode = 2;
|
|
}
|