@open-press/cli 0.6.0 → 0.7.0

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.
Files changed (86) hide show
  1. package/package.json +1 -1
  2. package/template/core/CHANGELOG.md +67 -1
  3. package/template/core/README.md +9 -5
  4. package/template/core/engine/cli.mjs +2 -5
  5. package/template/core/engine/commands/_shared.mjs +4 -4
  6. package/template/core/engine/commands/deploy.mjs +1 -1
  7. package/template/core/engine/commands/inspect.mjs +3 -3
  8. package/template/core/engine/commands/replace.mjs +1 -1
  9. package/template/core/engine/commands/search.mjs +1 -1
  10. package/template/core/engine/commands/validate.mjs +2 -2
  11. package/template/core/engine/document-export.mjs +1 -1
  12. package/template/core/engine/{chrome-pdf.mjs → output/chrome-pdf.mjs} +1 -2
  13. package/template/core/engine/{deploy-sync.mjs → output/deploy-sync.mjs} +2 -2
  14. package/template/core/engine/{fonts.mjs → output/fonts.mjs} +1 -1
  15. package/template/core/engine/{public-assets.mjs → output/public-assets.mjs} +2 -2
  16. package/template/core/engine/{static-server.mjs → output/static-server.mjs} +2 -2
  17. package/template/core/engine/react/caption-numbering.mjs +73 -0
  18. package/template/core/engine/react/comment-marker.mjs +54 -10
  19. package/template/core/engine/react/document-entry.mjs +124 -64
  20. package/template/core/engine/react/document-export.mjs +252 -311
  21. package/template/core/engine/react/mdx-compile.mjs +123 -3
  22. package/template/core/engine/react/measurement-css.mjs +3 -3
  23. package/template/core/engine/react/pagination/allocator.mjs +122 -0
  24. package/template/core/engine/react/pagination/regions.mjs +81 -0
  25. package/template/core/engine/react/pagination.mjs +9 -121
  26. package/template/core/engine/react/pipeline/allocate.mjs +248 -0
  27. package/template/core/engine/react/pipeline/final-render.mjs +94 -0
  28. package/template/core/engine/react/pipeline/frame-measurement.mjs +271 -0
  29. package/template/core/engine/react/pipeline/press-tree.mjs +135 -0
  30. package/template/core/engine/react/project-asset-endpoint.mjs +2 -2
  31. package/template/core/engine/react/{chapter-css.mjs → section-css.mjs} +12 -9
  32. package/template/core/engine/react/sources/heading-numbering.mjs +132 -0
  33. package/template/core/engine/react/sources/mdx-resolver.mjs +441 -0
  34. package/template/core/engine/react/{workspace-discovery.mjs → style-discovery.mjs} +29 -40
  35. package/template/core/engine/{config.mjs → runtime/config.mjs} +15 -0
  36. package/template/core/engine/{file-utils.mjs → runtime/file-utils.mjs} +1 -1
  37. package/template/core/engine/{inspection.mjs → runtime/inspection.mjs} +3 -4
  38. package/template/core/engine/{source-text-tools.mjs → runtime/source-text-tools.mjs} +24 -7
  39. package/template/core/engine/runtime/source-workspace.mjs +186 -0
  40. package/template/core/engine/{validation.mjs → runtime/validation.mjs} +19 -17
  41. package/template/core/package.json +5 -2
  42. package/template/core/src/openpress/anchorMap.ts +27 -0
  43. package/template/core/src/openpress/core/Frame.tsx +80 -0
  44. package/template/core/src/openpress/core/FrameContext.tsx +19 -0
  45. package/template/core/src/openpress/core/MdxArea.tsx +35 -0
  46. package/template/core/src/openpress/core/Press.tsx +34 -0
  47. package/template/core/src/openpress/core/index.tsx +34 -15
  48. package/template/core/src/openpress/core/primitives.tsx +23 -0
  49. package/template/core/src/openpress/core/types.ts +131 -19
  50. package/template/core/src/openpress/core/useSource.ts +28 -0
  51. package/template/core/src/openpress/manuscript/index.tsx +196 -0
  52. package/template/core/src/openpress/mdx/index.ts +88 -0
  53. package/template/core/src/openpress/numbering/index.ts +294 -0
  54. package/template/core/src/openpress/publicPage.tsx +4 -186
  55. package/template/core/src/openpress/reactDocumentMetadata.ts +2 -16
  56. package/template/core/src/openpress/types.ts +0 -16
  57. package/template/core/src/openpress/workbench.tsx +2 -36
  58. package/template/core/src/styles/openpress/responsive.css +0 -14
  59. package/template/core/tsconfig.json +4 -1
  60. package/template/core/vite.config.ts +10 -3
  61. package/template/packs/academic-paper/document/components/Page.tsx +24 -14
  62. package/template/packs/academic-paper/document/design.md +2 -2
  63. package/template/packs/academic-paper/document/index.tsx +98 -74
  64. package/template/packs/academic-paper/document/theme/page-surfaces/toc.css +19 -9
  65. package/template/packs/claude-document/document/components/Page.tsx +24 -14
  66. package/template/packs/claude-document/document/design.md +2 -2
  67. package/template/packs/claude-document/document/index.tsx +67 -62
  68. package/template/packs/claude-document/document/theme/page-surfaces/toc.css +19 -7
  69. package/template/packs/editorial-monograph/document/components/Page.tsx +24 -14
  70. package/template/packs/editorial-monograph/document/design.md +2 -2
  71. package/template/packs/editorial-monograph/document/index.tsx +71 -47
  72. package/template/packs/editorial-monograph/document/theme/page-surfaces/toc.css +19 -9
  73. package/template/core/engine/commands/migrate-to-react.mjs +0 -27
  74. package/template/core/engine/page-renderer.mjs +0 -217
  75. package/template/core/engine/react/migrate-to-react.mjs +0 -355
  76. package/template/core/engine/source-workspace.mjs +0 -76
  77. package/template/core/src/openpress/core/basePages.tsx +0 -87
  78. package/template/core/src/openpress/pagination.ts +0 -845
  79. package/template/packs/claude-document/document/chapters/01-document-shape/chapter.tsx +0 -30
  80. package/template/packs/claude-document/document/chapters/02-review-loop/chapter.tsx +0 -30
  81. /package/template/core/engine/{chrome-pdf.d.mts → output/chrome-pdf.d.mts} +0 -0
  82. /package/template/core/engine/{katex-assets.mjs → output/katex-assets.mjs} +0 -0
  83. /package/template/core/engine/{page-block.mjs → output/page-block.mjs} +0 -0
  84. /package/template/core/engine/{pdf-media.mjs → output/pdf-media.mjs} +0 -0
  85. /package/template/core/engine/{config.d.mts → runtime/config.d.mts} +0 -0
  86. /package/template/core/engine/{issue-report.mjs → runtime/issue-report.mjs} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-press/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Scaffolder for open-press — AI-first fixed-layout document workspaces.",
6
6
  "license": "MIT",
@@ -1,5 +1,68 @@
1
1
  # @open-press/core
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 718d2d1: **Press Tree render architecture** — full refresh of the React export pipeline (clean break, no v0.5 compatibility).
8
+
9
+ The render kernel no longer knows about `cover`, `toc`, `chapter`, or `back-cover` as built-in concepts. Workspaces describe their document as a React tree using three primitives:
10
+
11
+ - `Press` — root composition boundary.
12
+ - `Frame` — a single fixed-layout surface (replaces `BasePage` and friends).
13
+ - `MdxArea` — a measurable slot consuming a content chain, with `overflow="extend|truncate|error"` control.
14
+
15
+ Sources are now declarative descriptors:
16
+
17
+ ```tsx
18
+ export const sources = {
19
+ story: mdxSource({ preset: "section-folders", root: "chapters" }),
20
+ };
21
+
22
+ export default function MyPress() {
23
+ return (
24
+ <Press>
25
+ <Cover />
26
+ <Toc source="story" />
27
+ <Sections source="story" page={Page} />
28
+ <BackCover />
29
+ </Press>
30
+ );
31
+ }
32
+ ```
33
+
34
+ Three `mdxSource()` presets: `section-folders` (existing convention), `section-files` (flat file-per-section), `file-list` (explicit ordering).
35
+
36
+ Manuscript helpers (`Toc`, `Sections`, `Chapters` alias) ship in `@open-press/core/manuscript`. `mdxSource()` lives in `@open-press/core/mdx`. Subpath exports keep the public surface tight without committing to separate npm packages.
37
+
38
+ `Toc` is implemented as a manuscript helper, not a core kernel special case. Registered sources generate a synthetic `toc:<sourceId>` chain; `TocArea` consumes it with the same allocation path as `MdxArea`.
39
+
40
+ Reader-side pagination is removed. The export pipeline writes final frame HTML into `document.json`; the reader displays that HTML and no longer mutates headings/captions, injects footers, or reflows pages at runtime. Page shell belongs to workspace components.
41
+
42
+ MDX source resolution now derives manuscript TOC entries from actual `##` / `###` headings, not folder slugs. Heading numbering is written during export as `data-chapter="01"` / `data-section="1.1"` attributes so themes can render numbering with CSS without reader-side mutation.
43
+
44
+ **Removed (no compatibility layer):**
45
+
46
+ - `BasePage`, `BaseCoverPage`, `BaseTocPage`, `BaseContentPage`, `BaseBackCoverPage`.
47
+ - Legacy named exports (`cover`, `toc`, `backCover`) from `document/index.tsx`.
48
+ - The `migrate-to-react` CLI command.
49
+ - Implicit chapter discovery as the only source mechanism.
50
+ - Legacy `chapter.tsx` meta/opener auto-discovery. Section openers are explicit workspace components in the Press tree.
51
+
52
+ The top-level purity gate remains: `config` must be data, `sources` must be pure `mdxSource()` descriptors, and filesystem/network/process side effects are rejected before module execution. Default-exported function bodies can contain normal React component logic, including hooks and `.map()`.
53
+
54
+ All `<Frame>` instances require a stable `frameKey`; source roots and file-list entries must stay inside `document/`.
55
+
56
+ **Workspace data attributes:**
57
+
58
+ - `data-frame-role` (new, opaque role like `"manuscript.content"`).
59
+ - `data-page-kind` (derived from role's last segment — reader CSS keeps using this).
60
+ - `data-section-id` replaces `data-chapter-slug` for section-scoped CSS.
61
+
62
+ **Migration:** Workspaces written for v0.5.x must rewrite `document/index.tsx` to default-export a Press component. Pre-1.0 minor bump is acceptable per repo policy; no production deployments exist to break.
63
+
64
+ See `docs/superpowers/specs/2026-05-23-press-tree-render-architecture-design.md` for full design rationale.
65
+
3
66
  ## 0.6.0
4
67
 
5
68
  ### Minor Changes
@@ -40,6 +103,7 @@
40
103
  - 0169cba: Agent-driven upgrade flow.
41
104
 
42
105
  **New commands:**
106
+
43
107
  - `npx open-press doctor` — diagnose workspace against latest framework state. Reports `@open-press/core` version vs npm latest, installed skill count, and any pending `docs/migrations/<version>.md` notes between current and latest. `--json` for machine-readable output, `--no-cache` to bypass the 24h cache. Always exits 0 (informational only).
44
108
 
45
109
  - `npx open-press upgrade` — orchestrate the upgrade. Runs `npm update @open-press/core` (when the workspace declares the dep) and `npx skills upgrade`, then surfaces the list of migration notes for the agent to read. **Does not auto-edit `document/` content** — the agent reads the surfaced `docs/migrations/<version>.md` notes and proposes edits to the user with confirmation. Use `--dry-run` to preview, `--no-deps` / `--no-skills` to target one layer.
@@ -49,6 +113,7 @@
49
113
  `open-press dev` now runs `doctor` before starting Vite. When the workspace is behind, a single line prints: `○ open-press: @open-press/core 0.4.0 → 0.5.0 · 1 migration note(s) — run npx open-press doctor for details.` Cached for 24h, network failure is silent, never blocks dev.
50
114
 
51
115
  **Migration docs:**
116
+
52
117
  - New `docs/migrations/_template.md` — each release with breaking changes ships a `docs/migrations/<version>.md` file with sections the agent reads.
53
118
  - New `docs/migrations/0.4.0.md` — backfilled. Documents the SKILL fold (no document or CLI changes).
54
119
 
@@ -65,6 +130,7 @@
65
130
  ### Minor Changes
66
131
 
67
132
  - 3cb4939: Consolidate internal skills (13 → 11).
133
+
68
134
  - `openpress-update` folded into `openpress` as an "Updating An Existing Workspace" section. The release-upgrade flow, pre-flight checks, breaking-change reference, and do-not list are now part of the system-operation skill where they naturally belong.
69
135
  - `openpress-document-hierarchy` folded into `openpress-writing` as a "Hierarchy" section. Hierarchy decisions (H2/H3/H4 model, TOC depth, appendix placement, H4 granularity) and prose decisions happen in the same workflow; one skill, one routing decision.
70
136
  - `references/data-structures-outline.md` moved from the hierarchy skill into `openpress-writing/references/`.
@@ -81,4 +147,4 @@
81
147
 
82
148
  **@open-press/cli** (new): scaffolder for open-press workspaces. Run `npx @open-press/cli init <target> --pack <pack>` to create a fixed-layout document workspace from a bundled template. Supports `editorial-monograph` and `claude-document` style packs, metadata flags, and AI-agent skill installation under `.claude/skills/` and `.agents/skills/`.
83
149
 
84
- **@open-press/core** (new): framework runtime, CLI engine, render pipeline, and base page primitives (BasePage, BaseCoverPage, BaseTocPage, BaseBackCoverPage, BaseFigure, BaseCallout). Consumed transitively by workspaces scaffolded via `@open-press/cli`. Exposes the `open-press` bin (dev / build / preview / validate / pdf / deploy / export).
150
+ **@open-press/core** (new): framework runtime, CLI engine, render pipeline, and document primitives. Consumed transitively by workspaces scaffolded via `@open-press/cli`. Exposes the `open-press` bin (dev / build / preview / validate / pdf / deploy / export).
@@ -1,6 +1,6 @@
1
1
  # @open-press/core
2
2
 
3
- Framework runtime, CLI engine, and page primitives for [open-press](https://github.com/quan0715/open-press) — an AI-first fixed-layout document workspace.
3
+ Framework runtime, CLI engine, and Press Tree primitives for [open-press](https://github.com/quan0715/open-press) — an AI-first fixed-layout document workspace.
4
4
 
5
5
  Most users do **not** install this package directly. Instead, scaffold a workspace with the CLI:
6
6
 
@@ -20,15 +20,19 @@ npm install @open-press/core
20
20
 
21
21
  ```tsx
22
22
  import {
23
- BasePage,
24
- BaseCoverPage,
25
- BaseTocPage,
26
- BaseBackCoverPage,
23
+ Press,
24
+ Frame,
25
+ MdxArea,
27
26
  BaseFigure,
28
27
  BaseCallout,
29
28
  } from "@open-press/core";
29
+
30
+ import { mdxSource } from "@open-press/core/mdx";
31
+ import { Sections, Toc } from "@open-press/core/manuscript";
30
32
  ```
31
33
 
34
+ `document/index.tsx` default-exports a `<Press>` tree. `Frame` marks fixed-layout pages, `MdxArea` receives measured MDX blocks, and `mdxSource()` declares which MDX files participate in the render pipeline.
35
+
32
36
  The CLI bin (`open-press`) supports dev / build / preview / validate / pdf / deploy / export commands. It requires a workspace with `openpress.config.mjs` and the surrounding framework files (which the scaffolder installs).
33
37
 
34
38
  ## License
@@ -6,7 +6,6 @@ import * as doctorCmd from "./commands/doctor.mjs";
6
6
  import * as exportCmd from "./commands/export.mjs";
7
7
  import * as initCmd from "./commands/init.mjs";
8
8
  import * as inspectCmd from "./commands/inspect.mjs";
9
- import * as migrateToReactCmd from "./commands/migrate-to-react.mjs";
10
9
  import * as pdfCmd from "./commands/pdf.mjs";
11
10
  import * as previewCmd from "./commands/preview.mjs";
12
11
  import * as replaceCmd from "./commands/replace.mjs";
@@ -16,13 +15,12 @@ import * as typecheckCmd from "./commands/typecheck.mjs";
16
15
  import * as upgradeCmd from "./commands/upgrade.mjs";
17
16
  import * as validateCmd from "./commands/validate.mjs";
18
17
  import { parseOptions } from "./commands/_shared.mjs";
19
- import { loadConfig } from "./config.mjs";
18
+ import { loadConfig } from "./runtime/config.mjs";
20
19
  import { listStylePackSkills } from "./init.mjs";
21
- import { discoverWorkspace } from "./validation.mjs";
20
+ import { discoverWorkspace } from "./runtime/validation.mjs";
22
21
 
23
22
  const COMMANDS = {
24
23
  init: initCmd,
25
- "migrate-to-react": migrateToReactCmd,
26
24
  validate: validateCmd,
27
25
  inspect: inspectCmd,
28
26
  search: searchCmd,
@@ -79,7 +77,6 @@ async function printHelp() {
79
77
 
80
78
  Commands:
81
79
  init <target> [--skill <name>] [--force]
82
- migrate-to-react [path] [--dry-run] [--force] [--json]
83
80
  validate
84
81
  inspect [--json] [--no-build] [--dry-run]
85
82
  search [path] <query> [--json] [--scope content|all]
@@ -2,14 +2,14 @@ import { spawn, spawnSync } from "node:child_process";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { printUrlToPdf, stopChildProcess, waitForPrintReady } from "../chrome-pdf.mjs";
6
- import { loadConfig, publicPdfHref } from "../config.mjs";
5
+ import { printUrlToPdf, stopChildProcess, waitForPrintReady } from "../output/chrome-pdf.mjs";
6
+ import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
7
7
  import { exportDocument } from "../document-export.mjs";
8
- import { optimizePdfMediaForStaticRoot } from "../pdf-media.mjs";
8
+ import { optimizePdfMediaForStaticRoot } from "../output/pdf-media.mjs";
9
9
 
10
10
  export const ENGINE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
11
  export const CLI_ENTRY = path.join(ENGINE_DIR, "cli.mjs");
12
- export const STATIC_SERVER = path.join(ENGINE_DIR, "static-server.mjs");
12
+ export const STATIC_SERVER = path.join(ENGINE_DIR, "output", "static-server.mjs");
13
13
 
14
14
  export function parseOptions(argv) {
15
15
  const options = {};
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { deploySync } from "../deploy-sync.mjs";
2
+ import { deploySync } from "../output/deploy-sync.mjs";
3
3
  import { CLI_ENTRY, buildReactPdf, formatNodeScriptCommand, runCommand, writePdfStageDeployConfig } from "./_shared.mjs";
4
4
 
5
5
  export async function run({ root, config, options, recurse }) {
@@ -1,5 +1,5 @@
1
- import { inspectWorkspace } from "../inspection.mjs";
2
- import { exitCodeForIssueReport } from "../issue-report.mjs";
1
+ import { inspectWorkspace } from "../runtime/inspection.mjs";
2
+ import { exitCodeForIssueReport } from "../runtime/issue-report.mjs";
3
3
 
4
4
  export async function run({ root, config, options, recurse }) {
5
5
  const host = options.host ?? "127.0.0.1";
@@ -10,7 +10,7 @@ export async function run({ root, config, options, recurse }) {
10
10
  if (!options.noBuild) {
11
11
  console.log("Command: node engine/cli.mjs render . --renderer react");
12
12
  }
13
- console.log(`Command: node engine/static-server.mjs ${config.outputDir} --host ${host} --port ${port} --workspace .`);
13
+ console.log(`Command: node engine/output/static-server.mjs ${config.outputDir} --host ${host} --port ${port} --workspace .`);
14
14
  console.log(`Chrome inspection URL: ${url}`);
15
15
  return 0;
16
16
  }
@@ -1,4 +1,4 @@
1
- import { replaceSourceText } from "../source-text-tools.mjs";
1
+ import { replaceSourceText } from "../runtime/source-text-tools.mjs";
2
2
 
3
3
  export async function run({ config, options }) {
4
4
  const args = replaceArgsFromOptions(options);
@@ -1,4 +1,4 @@
1
- import { searchSourceText } from "../source-text-tools.mjs";
1
+ import { searchSourceText } from "../runtime/source-text-tools.mjs";
2
2
 
3
3
  export async function run({ config, options }) {
4
4
  const query = searchQueryFromOptions(options);
@@ -1,5 +1,5 @@
1
- import { validateWorkspace } from "../validation.mjs";
2
- import { exitCodeForIssueReport } from "../issue-report.mjs";
1
+ import { validateWorkspace } from "../runtime/validation.mjs";
2
+ import { exitCodeForIssueReport } from "../runtime/issue-report.mjs";
3
3
 
4
4
  export async function run({ root, options }) {
5
5
  const report = await validateWorkspace(root);
@@ -10,6 +10,6 @@ export async function exportDocument(root = ROOT) {
10
10
  if (reactResult) return reactResult;
11
11
 
12
12
  throw new Error(
13
- "React/MDX document entry not found. Expected document/index.tsx; run `node engine/cli.mjs migrate-to-react .` before exporting.",
13
+ "React/MDX document entry not found. Expected document/index.tsx with a Press default export before exporting.",
14
14
  );
15
15
  }
@@ -203,8 +203,7 @@ export async function waitForPrintReady(client) {
203
203
  awaitPromise: true,
204
204
  expression: `Promise.resolve().then(async () => {
205
205
  const root = document.querySelector('[data-openpress-print-document="true"]');
206
- const ready = root?.getAttribute('data-openpress-pagination') === 'ready';
207
- if (!ready) return 0;
206
+ if (!root || root.querySelectorAll('.openpress-html-page').length === 0) return 0;
208
207
 
209
208
  await document.fonts?.ready;
210
209
  await Promise.all(Array.from(document.images).map(async (img) => {
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { loadConfig } from "./config.mjs";
4
- import { copyDirectory } from "./file-utils.mjs";
3
+ import { loadConfig } from "../runtime/config.mjs";
4
+ import { copyDirectory } from "../runtime/file-utils.mjs";
5
5
 
6
6
  export async function deploySync(root, sourceDir, deployDir) {
7
7
  const config = await loadConfig(root);
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { copyDirectory } from "./file-utils.mjs";
3
+ import { copyDirectory } from "../runtime/file-utils.mjs";
4
4
 
5
5
  export async function copyThemeFonts(root, publicOutputDir, config) {
6
6
  const themeDir = config?.paths?.themeDir ?? path.join(path.resolve(root), "theme");
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { loadConfig } from "./config.mjs";
4
- import { copyDirectory, writeComponentsCss, writeContentCss } from "./file-utils.mjs";
3
+ import { loadConfig } from "../runtime/config.mjs";
4
+ import { copyDirectory, writeComponentsCss, writeContentCss } from "../runtime/file-utils.mjs";
5
5
  import { copyThemeFonts } from "./fonts.mjs";
6
6
  import { copyKatexFonts } from "./katex-assets.mjs";
7
7
 
@@ -2,8 +2,8 @@ import fs from "node:fs/promises";
2
2
  import http from "node:http";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
- import { loadConfig, publicPdfHref } from "./config.mjs";
6
- import { handleProjectAssetRequest } from "./react/project-asset-endpoint.mjs";
5
+ import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
6
+ import { handleProjectAssetRequest } from "../react/project-asset-endpoint.mjs";
7
7
 
8
8
  const [rootArg = "dist", ...rest] = process.argv.slice(2);
9
9
  const host = valueAfter(rest, "--host") ?? "127.0.0.1";
@@ -0,0 +1,73 @@
1
+ const DEFAULT_CAPTION_NUMBERING = {
2
+ figure: "Figure",
3
+ table: "Table",
4
+ separator: " ",
5
+ };
6
+
7
+ export function normalizeCaptionNumbering(value = {}) {
8
+ const input = value && typeof value === "object" && !Array.isArray(value) ? value : {};
9
+ return {
10
+ figure: stringOption(input.figure, DEFAULT_CAPTION_NUMBERING.figure),
11
+ table: stringOption(input.table, DEFAULT_CAPTION_NUMBERING.table),
12
+ separator: typeof input.separator === "string" ? input.separator : DEFAULT_CAPTION_NUMBERING.separator,
13
+ };
14
+ }
15
+
16
+ export function createCaptionNumberingState() {
17
+ return {
18
+ figure: 0,
19
+ table: 0,
20
+ seenTables: new Set(),
21
+ };
22
+ }
23
+
24
+ export function numberCaptionsInHtml(html, numbering, state = createCaptionNumberingState()) {
25
+ if (!html) return html;
26
+ const options = normalizeCaptionNumbering(numbering);
27
+ let out = String(html);
28
+ out = numberTableCaptions(out, options, state);
29
+ out = numberFigureCaptions(out, options, state);
30
+ return out;
31
+ }
32
+
33
+ function numberTableCaptions(html, options, state) {
34
+ return html.replace(/<table\b([^>]*)>([\s\S]*?<caption\b([^>]*)>)([\s\S]*?)(<\/caption>[\s\S]*?<\/table>)/g, (match, tableAttrs, beforeCaptionText, captionAttrs, captionText, afterCaptionText) => {
35
+ if (captionText.includes("data-openpress-caption-label=")) return match;
36
+ const tableId = attrValue(tableAttrs, "data-openpress-table-id");
37
+ if (tableId && state.seenTables.has(tableId)) return match;
38
+ if (tableId) state.seenTables.add(tableId);
39
+ state.table += 1;
40
+ const label = captionLabel(options.table, state.table, options.separator);
41
+ return `<table${tableAttrs}>${beforeCaptionText}${captionLabelSpan("table", state.table, label)} ${captionText}${afterCaptionText}`;
42
+ });
43
+ }
44
+
45
+ function numberFigureCaptions(html, options, state) {
46
+ return html.replace(/<figure\b([^>]*)>([\s\S]*?<figcaption\b([^>]*)>)([\s\S]*?)(<\/figcaption>[\s\S]*?<\/figure>)/g, (match, figureAttrs, beforeCaptionText, captionAttrs, captionText, afterCaptionText) => {
47
+ if (captionText.includes("data-openpress-caption-label=")) return match;
48
+ state.figure += 1;
49
+ const label = captionLabel(options.figure, state.figure, options.separator);
50
+ return `<figure${figureAttrs}>${beforeCaptionText}${captionLabelSpan("figure", state.figure, label)} ${captionText}${afterCaptionText}`;
51
+ });
52
+ }
53
+
54
+ function captionLabel(noun, number, separator) {
55
+ return `${noun}${separator}${number}`;
56
+ }
57
+
58
+ function captionLabelSpan(kind, number, label) {
59
+ return `<span class="openpress-caption-label" data-openpress-caption-label="${kind}" data-openpress-caption-number="${number}">${escapeHtml(label)}</span>`;
60
+ }
61
+
62
+ function attrValue(attrs, name) {
63
+ const pattern = new RegExp(`${name}=(["'])(.*?)\\1`);
64
+ return attrs.match(pattern)?.[2] ?? "";
65
+ }
66
+
67
+ function stringOption(value, fallback) {
68
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
69
+ }
70
+
71
+ function escapeHtml(value) {
72
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
73
+ }
@@ -1,15 +1,18 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { loadConfig } from "../config.mjs";
5
- import { collectSourceTextFiles } from "../source-text-tools.mjs";
6
-
4
+ import { loadConfig } from "../runtime/config.mjs";
5
+ import { collectSourceTextFiles } from "../runtime/source-text-tools.mjs";
6
+
7
+ // Any `.mdx` or `.tsx` file under `document/` is a legal comment target.
8
+ // The Press Tree allows arbitrary source layouts — `section-folders`,
9
+ // `section-files`, `file-list`, custom `root` paths, etc. — so we no
10
+ // longer hardcode `document/chapters/<slug>/content/*.mdx`. The boundary
11
+ // is "inside the workspace's authored `document/` directory" and "looks
12
+ // like an editable React/MDX source" by extension.
7
13
  const EDITABLE_COMMENT_SOURCE_PATTERNS = [
8
- /^document\/index\.tsx$/,
9
- /^document\/chapters\/[^/]+\/content\/[^/]+\.mdx$/,
10
- /^document\/chapters\/[^/]+\/chapter\.tsx$/,
11
- /^document\/chapters\/[^/]+\/components\/.+\.tsx$/,
12
- /^document\/components\/.+\.tsx$/,
14
+ /^document\/.+\.mdx$/,
15
+ /^document\/.+\.tsx$/,
13
16
  ];
14
17
  const COMMENT_MARKER_RE = /\{\/\*\s*@openpress-comment\b(?<attrs>[^*]*)\*\/\}/g;
15
18
  const COMMENT_LINE_RE = /^\s*\{\/\*\s*@openpress-comment\b[^*]*\*\/\}\s*$/;
@@ -138,8 +141,14 @@ export function assertEditableCommentPath(relativePath) {
138
141
  }
139
142
  }
140
143
 
144
+ // Strict check against workspace-relative paths. Callers walking the
145
+ // workspace (`applyCommentMarker` via `collectSourceTextFiles`) already
146
+ // receive paths with the `document/` prefix and must not have system
147
+ // paths silently mapped into the editable set.
141
148
  export function isEditableCommentPath(relativePath) {
142
- return EDITABLE_COMMENT_SOURCE_PATTERNS.some((pattern) => pattern.test(relativePath));
149
+ if (typeof relativePath !== "string" || !relativePath) return false;
150
+ const trimmed = relativePath.trim().replaceAll("\\", "/").replace(/^\.\//, "");
151
+ return EDITABLE_COMMENT_SOURCE_PATTERNS.some((pattern) => pattern.test(trimmed));
143
152
  }
144
153
 
145
154
  function normalizeEditableSourcePath(value) {
@@ -150,7 +159,42 @@ function normalizeEditableSourcePath(value) {
150
159
  if (path.posix.isAbsolute(normalized) || normalized.includes("\0") || normalized === "." || normalized.startsWith("../")) {
151
160
  throw new Error(`OpenPress comment target path is invalid: ${value}`);
152
161
  }
153
- return path.posix.normalize(normalized);
162
+ const posix = path.posix.normalize(normalized);
163
+ // The Press Tree source resolver emits paths relative to `document/`
164
+ // (e.g. "chapters/01-start/content/01-start.mdx"). The comment marker
165
+ // works in workspace-relative paths (with the `document/` prefix). If
166
+ // the incoming path is documentRoot-relative, prepend `document/`.
167
+ if (!posix.startsWith("document/") && looksDocumentRelative(posix)) {
168
+ return `document/${posix}`;
169
+ }
170
+ return posix;
171
+ }
172
+
173
+ // Identify paths the Press Tree source resolver emits — those are relative
174
+ // to `document/`. Match `.mdx` / `.tsx` files that don't already have the
175
+ // `document/` prefix and don't look like system / engine paths. The check
176
+ // is intentionally tight so we never silently rewrite engine internals
177
+ // (e.g. `src/openpress/...`) into "editable" workspace paths.
178
+ const SYSTEM_PATH_PREFIXES = [
179
+ "document/",
180
+ "src/",
181
+ "engine/",
182
+ "dist/",
183
+ "dist-react/",
184
+ "node_modules/",
185
+ "tests/",
186
+ "public/",
187
+ "packages/",
188
+ ".openpress/",
189
+ ".deploy/",
190
+ ".changeset/",
191
+ ".github/",
192
+ ];
193
+
194
+ function looksDocumentRelative(posixPath) {
195
+ if (!/\.(mdx|tsx)$/.test(posixPath)) return false;
196
+ if (SYSTEM_PATH_PREFIXES.some((prefix) => posixPath.startsWith(prefix))) return false;
197
+ return true;
154
198
  }
155
199
 
156
200
  function normalizeLineNumber(value) {