@open-press/core 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { diagnose } from "./doctor.mjs";
|
|
5
5
|
import { runCommand } from "./_shared.mjs";
|
|
6
6
|
|
|
7
|
+
// Migration notes live in the framework repo, not in scaffolded workspaces.
|
|
8
|
+
// `npx open-press upgrade` fetches the notes for each pending version and
|
|
9
|
+
// caches them under `.openpress/migrations/` so agents can read locally.
|
|
10
|
+
const MIGRATION_REMOTE_BASE = "https://raw.githubusercontent.com/quan0715/open-press/main/docs/migrations";
|
|
11
|
+
const MIGRATION_CACHE_DIR = path.join(".openpress", "migrations");
|
|
12
|
+
|
|
7
13
|
export async function run({ root, options }) {
|
|
8
14
|
const dryRun = Boolean(options?.dryRun);
|
|
9
15
|
const skipSkills = Boolean(options?.noSkills);
|
|
@@ -85,7 +91,11 @@ export async function run({ root, options }) {
|
|
|
85
91
|
process.stdout.write(" (no migration docs in this version range)\n\n");
|
|
86
92
|
} else {
|
|
87
93
|
for (const m of migrationContents) {
|
|
88
|
-
|
|
94
|
+
if (m.path) {
|
|
95
|
+
process.stdout.write(` ─ ${m.path}${m.fetched ? " (fetched from github)" : ""}\n`);
|
|
96
|
+
} else {
|
|
97
|
+
process.stdout.write(` ─ ${m.version}.md (not found locally or on github — check the repo manually)\n`);
|
|
98
|
+
}
|
|
89
99
|
}
|
|
90
100
|
process.stdout.write(
|
|
91
101
|
"\nAgent: open each file, identify document-level changes, grep document/ for affected patterns, propose edits before applying.\n",
|
|
@@ -107,11 +117,43 @@ async function hasCoreDep(root) {
|
|
|
107
117
|
|
|
108
118
|
async function loadMigrations(root, versions) {
|
|
109
119
|
const results = [];
|
|
120
|
+
const cacheDir = path.join(root, MIGRATION_CACHE_DIR);
|
|
121
|
+
await mkdir(cacheDir, { recursive: true });
|
|
122
|
+
|
|
110
123
|
for (const v of versions) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
// Framework repo has docs/migrations/ at root — prefer local if present
|
|
125
|
+
// (covers the open-press framework repo itself acting as a workspace).
|
|
126
|
+
const localDocsPath = path.join(root, "docs", "migrations", `${v}.md`);
|
|
127
|
+
if (existsSync(localDocsPath)) {
|
|
128
|
+
results.push({ version: v, path: path.relative(root, localDocsPath), fetched: false });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Otherwise fetch from GitHub raw and cache to .openpress/migrations/.
|
|
133
|
+
const cachedPath = path.join(cacheDir, `${v}.md`);
|
|
134
|
+
if (existsSync(cachedPath)) {
|
|
135
|
+
results.push({ version: v, path: path.relative(root, cachedPath), fetched: false });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const body = await fetchMigration(v);
|
|
140
|
+
if (body) {
|
|
141
|
+
await writeFile(cachedPath, body, "utf8");
|
|
142
|
+
results.push({ version: v, path: path.relative(root, cachedPath), fetched: true });
|
|
143
|
+
} else {
|
|
144
|
+
results.push({ version: v, path: null, fetched: false });
|
|
114
145
|
}
|
|
115
146
|
}
|
|
116
147
|
return results;
|
|
117
148
|
}
|
|
149
|
+
|
|
150
|
+
async function fetchMigration(version) {
|
|
151
|
+
const url = `${MIGRATION_REMOTE_BASE}/${version}.md`;
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch(url, { headers: { Accept: "text/plain" } });
|
|
154
|
+
if (!res.ok) return null;
|
|
155
|
+
return await res.text();
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -92,6 +92,21 @@ export async function exportReactDocument(root = ".", { syncAssets = true } = {}
|
|
|
92
92
|
blockHeights: measurement.blockHeights,
|
|
93
93
|
sources,
|
|
94
94
|
});
|
|
95
|
+
if (process.env.OPENPRESS_DEBUG_ALLOC) {
|
|
96
|
+
const sample = measurement.mdxAreas
|
|
97
|
+
.slice(0, 5)
|
|
98
|
+
.map((a) => `${a.frameKey}#${a.indexInFrame} cap=${a.capacity.toFixed(0)} raw=${(a.rawHeight ?? 0).toFixed(0)}`);
|
|
99
|
+
const blocks = measurement.blockHeights
|
|
100
|
+
.slice(0, 8)
|
|
101
|
+
.map((b) => `${b.id} h=${b.height.toFixed(0)}`);
|
|
102
|
+
process.stderr.write(`[allocator iter ${iteration}]\n`);
|
|
103
|
+
process.stderr.write(` mdxAreas[0..4]: ${sample.join(" | ")}\n`);
|
|
104
|
+
process.stderr.write(` blocks[0..7]: ${blocks.join(" | ")}\n`);
|
|
105
|
+
process.stderr.write(` hints: ${JSON.stringify(alloc.hints.totalPagesPerChain)}\n`);
|
|
106
|
+
if (alloc.warnings.length > 0) {
|
|
107
|
+
process.stderr.write(` warnings: ${JSON.stringify(alloc.warnings)}\n`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
95
110
|
if (hintsEqual(hints, alloc.hints)) {
|
|
96
111
|
allocation = alloc.allocation;
|
|
97
112
|
warnings = alloc.warnings;
|
|
@@ -99,6 +99,16 @@ export function rehypeBlockIds(options = {}) {
|
|
|
99
99
|
includeBlockIds,
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
|
+
if (block.name === "ul" || block.name === "ol") {
|
|
103
|
+
return applyListItemBlocks({
|
|
104
|
+
node,
|
|
105
|
+
id,
|
|
106
|
+
blocks,
|
|
107
|
+
filePath,
|
|
108
|
+
chapterSlug,
|
|
109
|
+
includeBlockIds,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
102
112
|
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
103
113
|
|
|
104
114
|
setDataAttribute(node, "data-openpress-block-id", id);
|
|
@@ -314,6 +324,87 @@ function blockInfo(node) {
|
|
|
314
324
|
return null;
|
|
315
325
|
}
|
|
316
326
|
|
|
327
|
+
function applyListItemBlocks({
|
|
328
|
+
node,
|
|
329
|
+
id,
|
|
330
|
+
blocks,
|
|
331
|
+
filePath,
|
|
332
|
+
chapterSlug,
|
|
333
|
+
includeBlockIds,
|
|
334
|
+
}) {
|
|
335
|
+
const items = listItems(node);
|
|
336
|
+
if (items.length === 0) {
|
|
337
|
+
if (includeBlockIds && !includeBlockIds.has(id)) return false;
|
|
338
|
+
setDataAttribute(node, "data-openpress-block-id", id);
|
|
339
|
+
blocks.push({
|
|
340
|
+
id,
|
|
341
|
+
kind: "element",
|
|
342
|
+
name: node.tagName,
|
|
343
|
+
text: textContent(node).trim() || undefined,
|
|
344
|
+
filePath,
|
|
345
|
+
chapterSlug,
|
|
346
|
+
source: sourcePosition(node.position),
|
|
347
|
+
});
|
|
348
|
+
return "skip";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const itemRecords = items.map((item, index) => ({
|
|
352
|
+
id: `${id}-i${index}`,
|
|
353
|
+
node: item,
|
|
354
|
+
index,
|
|
355
|
+
}));
|
|
356
|
+
const selected = includeBlockIds
|
|
357
|
+
? itemRecords.filter((item) => includeBlockIds.has(item.id))
|
|
358
|
+
: itemRecords;
|
|
359
|
+
if (selected.length === 0) return false;
|
|
360
|
+
|
|
361
|
+
setDataAttribute(node, "data-openpress-list-id", id);
|
|
362
|
+
|
|
363
|
+
// For ordered lists, continuation pages must keep numbering picking up
|
|
364
|
+
// from the first surviving item. `start` is the 1-based number of the
|
|
365
|
+
// first `<li>` rendered, so if the original list had `start="5"` and we
|
|
366
|
+
// dropped the first three items, continuation starts at 5 + 3 = 8.
|
|
367
|
+
if (node.tagName === "ol" && selected[0]?.index > 0) {
|
|
368
|
+
const baseStart = Number(node.properties?.start ?? 1);
|
|
369
|
+
const continuationStart = baseStart + selected[0].index;
|
|
370
|
+
node.properties = { ...node.properties, start: continuationStart };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const selectedNodes = new Set(selected.map((item) => item.node));
|
|
374
|
+
pruneUnselectedListItems(node, new Set(itemRecords.map((item) => item.node)), selectedNodes);
|
|
375
|
+
|
|
376
|
+
for (const item of selected) {
|
|
377
|
+
setDataAttribute(item.node, "data-openpress-block-id", item.id);
|
|
378
|
+
blocks.push({
|
|
379
|
+
id: item.id,
|
|
380
|
+
kind: "list-item",
|
|
381
|
+
name: "list-item",
|
|
382
|
+
text: textContent(item.node).trim() || undefined,
|
|
383
|
+
filePath,
|
|
384
|
+
chapterSlug,
|
|
385
|
+
listId: id,
|
|
386
|
+
listTag: node.tagName,
|
|
387
|
+
itemIndex: item.index,
|
|
388
|
+
source: sourcePosition(item.node.position ?? node.position),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return "skip";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function listItems(list) {
|
|
395
|
+
if (list?.type !== "element") return [];
|
|
396
|
+
if (list.tagName !== "ul" && list.tagName !== "ol") return [];
|
|
397
|
+
return (list.children ?? []).filter((child) => child?.type === "element" && child.tagName === "li");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function pruneUnselectedListItems(node, itemNodes, selectedNodes) {
|
|
401
|
+
if (!Array.isArray(node?.children)) return;
|
|
402
|
+
node.children = node.children.filter((child) => {
|
|
403
|
+
if (!itemNodes.has(child)) return true;
|
|
404
|
+
return selectedNodes.has(child);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
317
408
|
function tableBodyRows(table) {
|
|
318
409
|
if (table?.type !== "element" || table.tagName !== "table") return [];
|
|
319
410
|
const rows = [];
|
|
@@ -135,15 +135,30 @@ async function runChromiumMeasurement(html, viewport) {
|
|
|
135
135
|
try {
|
|
136
136
|
const page = await browser.newPage({ viewport });
|
|
137
137
|
await page.setContent(html, { waitUntil: "load" });
|
|
138
|
+
// Match the print-ready settle: fonts first (font metrics affect image
|
|
139
|
+
// alt-text fallback boxes), then await every image's `complete` AND
|
|
140
|
+
// `decode()` so intrinsic sizes are committed before layout, then two
|
|
141
|
+
// animation frames so the chromium layout pass observes the final box
|
|
142
|
+
// model. Without this, `getBoundingClientRect()` on figures that hold
|
|
143
|
+
// images can race the decode and return collapsed heights, causing the
|
|
144
|
+
// allocator to pack too many blocks per page.
|
|
138
145
|
await page.evaluate(async () => {
|
|
139
|
-
await Promise.all(Array.from(document.images).map((img) => {
|
|
140
|
-
if (img.complete) return Promise.resolve();
|
|
141
|
-
return new Promise((resolve) => {
|
|
142
|
-
img.addEventListener("load", resolve, { once: true });
|
|
143
|
-
img.addEventListener("error", resolve, { once: true });
|
|
144
|
-
});
|
|
145
|
-
}));
|
|
146
146
|
if (document.fonts?.ready) await document.fonts.ready;
|
|
147
|
+
await Promise.all(Array.from(document.images).map(async (img) => {
|
|
148
|
+
if (!img.complete) {
|
|
149
|
+
await new Promise((resolve) => {
|
|
150
|
+
const settle = () => {
|
|
151
|
+
img.removeEventListener("load", settle);
|
|
152
|
+
img.removeEventListener("error", settle);
|
|
153
|
+
resolve(undefined);
|
|
154
|
+
};
|
|
155
|
+
img.addEventListener("load", settle, { once: true });
|
|
156
|
+
img.addEventListener("error", settle, { once: true });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
await img.decode?.().catch(() => undefined);
|
|
160
|
+
}));
|
|
161
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
147
162
|
});
|
|
148
163
|
|
|
149
164
|
const mdxAreas = await page.evaluate((safety) => {
|
|
@@ -232,13 +247,27 @@ async function inlineMeasurementMediaUrls(html, mediaDir) {
|
|
|
232
247
|
if (!mediaDir || !html) return html;
|
|
233
248
|
let out = String(html);
|
|
234
249
|
const matches = new Set();
|
|
235
|
-
for (const match of out.matchAll(
|
|
236
|
-
|
|
250
|
+
for (const match of out.matchAll(/\bsrc=(['"])([^\1]*?)\1/g)) {
|
|
251
|
+
const src = match[2];
|
|
252
|
+
if (!src) continue;
|
|
253
|
+
if (src.startsWith('/openpress/media/')) {
|
|
254
|
+
matches.add(src.slice('/openpress/media/'.length));
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (src.startsWith('media/')) {
|
|
258
|
+
matches.add(src.slice('media/'.length));
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (src.startsWith('./media/')) {
|
|
262
|
+
matches.add(src.slice('./media/'.length));
|
|
263
|
+
}
|
|
237
264
|
}
|
|
238
265
|
for (const rawName of matches) {
|
|
239
266
|
const dataUrl = await mediaDataUrl(mediaDir, rawName);
|
|
240
267
|
if (!dataUrl) continue;
|
|
241
268
|
out = out.replaceAll(`/openpress/media/${rawName}`, dataUrl);
|
|
269
|
+
out = out.replaceAll(`media/${rawName}`, dataUrl);
|
|
270
|
+
out = out.replaceAll(`./media/${rawName}`, dataUrl);
|
|
242
271
|
}
|
|
243
272
|
return out;
|
|
244
273
|
}
|