@open-press/core 0.6.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.
Files changed (71) hide show
  1. package/README.md +9 -5
  2. package/engine/cli.mjs +2 -5
  3. package/engine/commands/_shared.mjs +4 -4
  4. package/engine/commands/deploy.mjs +1 -1
  5. package/engine/commands/inspect.mjs +3 -3
  6. package/engine/commands/replace.mjs +1 -1
  7. package/engine/commands/search.mjs +1 -1
  8. package/engine/commands/upgrade.mjs +47 -5
  9. package/engine/commands/validate.mjs +2 -2
  10. package/engine/document-export.mjs +1 -1
  11. package/engine/{chrome-pdf.mjs → output/chrome-pdf.mjs} +1 -2
  12. package/engine/{deploy-sync.mjs → output/deploy-sync.mjs} +2 -2
  13. package/engine/{fonts.mjs → output/fonts.mjs} +1 -1
  14. package/engine/{public-assets.mjs → output/public-assets.mjs} +2 -2
  15. package/engine/{static-server.mjs → output/static-server.mjs} +2 -2
  16. package/engine/react/caption-numbering.mjs +73 -0
  17. package/engine/react/comment-marker.mjs +54 -10
  18. package/engine/react/document-entry.mjs +124 -64
  19. package/engine/react/document-export.mjs +266 -310
  20. package/engine/react/mdx-compile.mjs +214 -3
  21. package/engine/react/measurement-css.mjs +3 -3
  22. package/engine/react/pagination/allocator.mjs +122 -0
  23. package/engine/react/pagination/regions.mjs +81 -0
  24. package/engine/react/pagination.mjs +9 -121
  25. package/engine/react/pipeline/allocate.mjs +248 -0
  26. package/engine/react/pipeline/final-render.mjs +94 -0
  27. package/engine/react/pipeline/frame-measurement.mjs +300 -0
  28. package/engine/react/pipeline/press-tree.mjs +135 -0
  29. package/engine/react/project-asset-endpoint.mjs +2 -2
  30. package/engine/react/{chapter-css.mjs → section-css.mjs} +12 -9
  31. package/engine/react/sources/heading-numbering.mjs +132 -0
  32. package/engine/react/sources/mdx-resolver.mjs +441 -0
  33. package/engine/react/{workspace-discovery.mjs → style-discovery.mjs} +29 -40
  34. package/engine/{config.mjs → runtime/config.mjs} +15 -0
  35. package/engine/{file-utils.mjs → runtime/file-utils.mjs} +1 -1
  36. package/engine/{inspection.mjs → runtime/inspection.mjs} +3 -4
  37. package/engine/{source-text-tools.mjs → runtime/source-text-tools.mjs} +24 -7
  38. package/engine/runtime/source-workspace.mjs +186 -0
  39. package/engine/{validation.mjs → runtime/validation.mjs} +19 -17
  40. package/package.json +5 -2
  41. package/src/openpress/anchorMap.ts +27 -0
  42. package/src/openpress/core/Frame.tsx +80 -0
  43. package/src/openpress/core/FrameContext.tsx +19 -0
  44. package/src/openpress/core/MdxArea.tsx +35 -0
  45. package/src/openpress/core/Press.tsx +34 -0
  46. package/src/openpress/core/index.tsx +34 -15
  47. package/src/openpress/core/primitives.tsx +23 -0
  48. package/src/openpress/core/types.ts +131 -19
  49. package/src/openpress/core/useSource.ts +28 -0
  50. package/src/openpress/manuscript/index.tsx +196 -0
  51. package/src/openpress/mdx/index.ts +88 -0
  52. package/src/openpress/numbering/index.ts +294 -0
  53. package/src/openpress/publicPage.tsx +4 -186
  54. package/src/openpress/reactDocumentMetadata.ts +2 -16
  55. package/src/openpress/types.ts +0 -16
  56. package/src/openpress/workbench.tsx +2 -36
  57. package/src/styles/openpress/responsive.css +0 -14
  58. package/tsconfig.json +4 -1
  59. package/vite.config.ts +10 -3
  60. package/engine/commands/migrate-to-react.mjs +0 -27
  61. package/engine/page-renderer.mjs +0 -217
  62. package/engine/react/migrate-to-react.mjs +0 -355
  63. package/engine/source-workspace.mjs +0 -76
  64. package/src/openpress/core/basePages.tsx +0 -87
  65. package/src/openpress/pagination.ts +0 -845
  66. /package/engine/{chrome-pdf.d.mts → output/chrome-pdf.d.mts} +0 -0
  67. /package/engine/{katex-assets.mjs → output/katex-assets.mjs} +0 -0
  68. /package/engine/{page-block.mjs → output/page-block.mjs} +0 -0
  69. /package/engine/{pdf-media.mjs → output/pdf-media.mjs} +0 -0
  70. /package/engine/{config.d.mts → runtime/config.d.mts} +0 -0
  71. /package/engine/{issue-report.mjs → runtime/issue-report.mjs} +0 -0
@@ -0,0 +1,186 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { loadReactDocumentEntry } from "../react/document-entry.mjs";
4
+
5
+ export const REACT_MDX_CONTENT_EXTENSIONS = new Set([".mdx"]);
6
+
7
+ export async function resolveActiveSourceWorkspace(config) {
8
+ const reactEntry = await loadReactDocumentEntry(config.root);
9
+ if (!reactEntry) {
10
+ throw new Error(
11
+ "React/MDX document entry not found. Expected document/index.tsx with a Press default export before using workspace source tools.",
12
+ );
13
+ }
14
+ const contentRoots = contentRootsFromSources(reactEntry.sources, reactEntry.config);
15
+ const sourceDir = firstDirectoryRoot(contentRoots) ?? reactEntry.config.paths.documentRoot;
16
+
17
+ return {
18
+ kind: "react-mdx",
19
+ checkedName: "react-source",
20
+ config: reactEntry.config,
21
+ entryPath: reactEntry.entryPath,
22
+ sourceDir,
23
+ contentRoots,
24
+ contentExtensions: REACT_MDX_CONTENT_EXTENSIONS,
25
+ contentLabel: "React MDX chapter source",
26
+ missingCode: "react-source.missing",
27
+ emptyCode: "react-source.empty",
28
+ missingMessage: "Registered React MDX sources do not exist yet; create the files or roots declared in document/index.tsx `sources` before running export.",
29
+ emptyMessage: "Registered React MDX sources contain no `*.mdx` files; the document will export with zero source blocks.",
30
+ };
31
+ }
32
+
33
+ export async function collectActiveContentFiles(sourceWorkspace, { skipUnderscoreFiles = false } = {}) {
34
+ const files = [];
35
+ const seen = new Set();
36
+ for (const root of sourceWorkspace.contentRoots ?? [{ kind: "dir", absolutePath: sourceWorkspace.sourceDir }]) {
37
+ const visit = async (absolutePath) => {
38
+ if (seen.has(absolutePath)) return;
39
+ if (!sourceWorkspace.contentExtensions.has(path.extname(absolutePath))) return;
40
+ const name = path.basename(absolutePath);
41
+ if (skipUnderscoreFiles && name.startsWith("_")) return;
42
+ seen.add(absolutePath);
43
+ files.push({
44
+ absolutePath,
45
+ name,
46
+ relativePath: rootRelativePath(sourceWorkspace.config, absolutePath),
47
+ sourceRelativePath: path.relative(root.basePath ?? path.dirname(absolutePath), absolutePath).replaceAll("\\", "/"),
48
+ text: await fs.readFile(absolutePath, "utf8"),
49
+ });
50
+ };
51
+ if (root.kind === "file") {
52
+ try {
53
+ const stat = await fs.stat(root.absolutePath);
54
+ if (stat.isFile()) await visit(root.absolutePath);
55
+ } catch (error) {
56
+ if (error?.code !== "ENOENT") throw error;
57
+ }
58
+ continue;
59
+ }
60
+ await walkFiles(root.absolutePath, visit);
61
+ }
62
+ files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
63
+ return files;
64
+ }
65
+
66
+ export async function sourceDirectoryExists(sourceWorkspace) {
67
+ const roots = sourceWorkspace.contentRoots ?? [{ kind: "dir", absolutePath: sourceWorkspace.sourceDir }];
68
+ for (const root of roots) {
69
+ try {
70
+ const stat = await fs.stat(root.absolutePath);
71
+ if (root.kind === "file" ? stat.isFile() : stat.isDirectory()) return true;
72
+ } catch (error) {
73
+ if (error?.code === "ENOENT") continue;
74
+ throw error;
75
+ }
76
+ }
77
+ return false;
78
+ }
79
+
80
+ export function rootRelativePath(config, absolutePath) {
81
+ return path.relative(config.root, absolutePath).replaceAll("\\", "/");
82
+ }
83
+
84
+ async function walkFiles(directory, visit) {
85
+ let entries;
86
+ try {
87
+ entries = await fs.readdir(directory, { withFileTypes: true });
88
+ } catch (error) {
89
+ if (error?.code === "ENOENT") return;
90
+ throw error;
91
+ }
92
+ for (const entry of entries) {
93
+ if (entry.name.startsWith(".")) continue;
94
+ const absolutePath = path.join(directory, entry.name);
95
+ if (entry.isDirectory()) await walkFiles(absolutePath, visit);
96
+ else if (entry.isFile()) await visit(absolutePath);
97
+ }
98
+ }
99
+
100
+ function contentRootsFromSources(sources, config) {
101
+ const entries = Object.entries(sources ?? {});
102
+ if (entries.length === 0) {
103
+ return [{
104
+ kind: "dir",
105
+ absolutePath: config.paths.sourceDir,
106
+ basePath: config.paths.sourceDir,
107
+ sourceId: "default",
108
+ preset: "section-folders",
109
+ }];
110
+ }
111
+
112
+ const roots = [];
113
+ for (const [sourceId, descriptor] of entries) {
114
+ if (!descriptor || descriptor.type !== "mdx") continue;
115
+ if (descriptor.preset === "section-folders") {
116
+ roots.push(directoryRoot(config, descriptor.root ?? "chapters", sourceId, descriptor.preset));
117
+ continue;
118
+ }
119
+ if (descriptor.preset === "section-files") {
120
+ roots.push(directoryRoot(config, descriptor.root ?? "content", sourceId, descriptor.preset));
121
+ continue;
122
+ }
123
+ if (descriptor.preset === "file-list") {
124
+ for (const file of descriptor.files ?? []) {
125
+ roots.push(fileRoot(config, file, sourceId, descriptor.preset));
126
+ }
127
+ continue;
128
+ }
129
+ }
130
+ return dedupeRoots(roots);
131
+ }
132
+
133
+ function directoryRoot(config, rel, sourceId, preset) {
134
+ const absolutePath = resolveDocumentRelativePath(config.paths.documentRoot, rel, `Source "${sourceId}" ${preset} root`);
135
+ return {
136
+ kind: "dir",
137
+ absolutePath,
138
+ basePath: absolutePath,
139
+ sourceId,
140
+ preset,
141
+ };
142
+ }
143
+
144
+ function fileRoot(config, rel, sourceId, preset) {
145
+ if (typeof rel !== "string" || !rel.trim()) {
146
+ throw new Error(`Source "${sourceId}" file-list contains an empty or invalid entry.`);
147
+ }
148
+ if (!rel.endsWith(".mdx")) {
149
+ throw new Error(`Source "${sourceId}" file-list path "${rel}" must end with .mdx.`);
150
+ }
151
+ const absolutePath = resolveDocumentRelativePath(config.paths.documentRoot, rel, `Source "${sourceId}" file-list path "${rel}"`);
152
+ return {
153
+ kind: "file",
154
+ absolutePath,
155
+ basePath: path.dirname(absolutePath),
156
+ sourceId,
157
+ preset,
158
+ };
159
+ }
160
+
161
+ function resolveDocumentRelativePath(documentRoot, rel, label) {
162
+ if (typeof rel !== "string" || !rel.trim()) throw new Error(`${label} must be a non-empty document-relative path.`);
163
+ if (rel.includes("..")) throw new Error(`${label} contains "..", rejected.`);
164
+ const absolutePath = path.resolve(documentRoot, rel);
165
+ const relCheck = path.relative(documentRoot, absolutePath);
166
+ if (relCheck.startsWith("..") || path.isAbsolute(relCheck)) {
167
+ throw new Error(`${label} escapes the document root.`);
168
+ }
169
+ return absolutePath;
170
+ }
171
+
172
+ function dedupeRoots(roots) {
173
+ const seen = new Set();
174
+ const out = [];
175
+ for (const root of roots) {
176
+ const key = `${root.kind}:${root.absolutePath}`;
177
+ if (seen.has(key)) continue;
178
+ seen.add(key);
179
+ out.push(root);
180
+ }
181
+ return out;
182
+ }
183
+
184
+ function firstDirectoryRoot(roots) {
185
+ return roots.find((root) => root.kind === "dir")?.absolutePath ?? null;
186
+ }
@@ -109,27 +109,19 @@ export async function validateWorkspace(root) {
109
109
  }
110
110
  }
111
111
 
112
- mark("react-pagination");
112
+ mark("react-source");
113
113
  const documentJsonPath = path.join(activeConfig.paths.publicDir, "document.json");
114
114
  const exportedDocument = await readJsonIfExists(documentJsonPath);
115
- const paginationWarnings = exportedDocument?.source?.pagination?.warnings;
116
- if (Array.isArray(paginationWarnings)) {
117
- for (const warning of paginationWarnings) {
118
- if (warning?.code !== "block-overflows-page") continue;
119
- const warningPath = typeof warning.path === "string" && warning.path
120
- ? path.resolve(activeConfig.root, warning.path)
121
- : documentJsonPath;
115
+ const pressWarnings = exportedDocument?.source?.warnings;
116
+ if (Array.isArray(pressWarnings)) {
117
+ for (const warning of pressWarnings) {
118
+ const code = typeof warning?.code === "string" && warning.code ? warning.code : "warning";
122
119
  add(
123
120
  "warning",
124
- "react-pagination.block-overflows-page",
125
- `Block \`${warning.blockId ?? "(unknown)"}\` exceeds the configured page safe area during React pagination.`,
126
- warningPath,
127
- {
128
- blockId: warning.blockId,
129
- height: warning.height,
130
- pageSafeHeightPx: warning.pageSafeHeightPx,
131
- source: warning.source,
132
- },
121
+ `react-source.${code}`,
122
+ pressWarningMessage(warning),
123
+ documentJsonPath,
124
+ warning,
133
125
  );
134
126
  }
135
127
  }
@@ -153,6 +145,16 @@ function findCommentMarkers(text) {
153
145
  return markers;
154
146
  }
155
147
 
148
+ function pressWarningMessage(warning) {
149
+ if (warning?.code === "chain-overflowed") {
150
+ return `Content chain \`${warning.chainId ?? "(unknown)"}\` overflowed during Press Tree allocation.`;
151
+ }
152
+ if (warning?.code === "chain-has-no-area") {
153
+ return `Content chain \`${warning.chainId ?? "(unknown)"}\` has blocks but no matching MdxArea.`;
154
+ }
155
+ return `Press Tree export warning: ${warning?.code ?? "warning"}.`;
156
+ }
157
+
156
158
  async function readJsonIfExists(filePath) {
157
159
  try {
158
160
  return JSON.parse(await fs.readFile(filePath, "utf8"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-press/core",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "description": "open-press core — runtime primitives, CLI, and render pipeline for AI-first fixed-layout documents.",
6
6
  "license": "MIT",
@@ -26,7 +26,10 @@
26
26
  "main": "./src/openpress/core/index.tsx",
27
27
  "types": "./src/openpress/core/index.tsx",
28
28
  "exports": {
29
- ".": "./src/openpress/core/index.tsx"
29
+ ".": "./src/openpress/core/index.tsx",
30
+ "./mdx": "./src/openpress/mdx/index.ts",
31
+ "./manuscript": "./src/openpress/manuscript/index.tsx",
32
+ "./numbering": "./src/openpress/numbering/index.ts"
30
33
  },
31
34
  "bin": {
32
35
  "open-press": "engine/cli.mjs"
@@ -0,0 +1,27 @@
1
+ // Anchor → page-index resolution shared between the public viewer and
2
+ // the workbench. Lives in its own module so React Fast Refresh can keep
3
+ // `PublicPage` / `HtmlWorkbench` HMR-clean (Fast Refresh expects component
4
+ // files to export only components).
5
+
6
+ import type { DisplayPage } from "./workbenchTypes";
7
+
8
+ export function createAnchorPageMap(pages: DisplayPage[]) {
9
+ const map = new Map<string, number>();
10
+ pages.forEach((page, index) => {
11
+ page.anchors?.forEach((anchor) => {
12
+ if (anchor && !map.has(anchor)) map.set(anchor, index);
13
+ });
14
+ });
15
+ return map;
16
+ }
17
+
18
+ export function resolveAnchorPageIndex(
19
+ anchorPageMap: Map<string, number>,
20
+ pageCount: number,
21
+ anchorId: string,
22
+ pageIndex?: number,
23
+ ): number | null {
24
+ if (typeof pageIndex === "number" && Number.isInteger(pageIndex) && pageIndex >= 0 && pageIndex < pageCount) return pageIndex;
25
+ const mapped = anchorPageMap.get(anchorId);
26
+ return mapped === undefined ? null : mapped;
27
+ }
@@ -0,0 +1,80 @@
1
+ import { useContext, type ReactNode } from "react";
2
+ import { FrameContext, type FrameContextValue } from "./FrameContext";
3
+ import { PressContext } from "./Press";
4
+ import type { FrameProps } from "./types";
5
+
6
+ // Substring reserved for the overflow extension pipeline.
7
+ const RESERVED_EXTENDED = ":extended:";
8
+
9
+ export const FRAME_MARKER: unique symbol = Symbol.for("@open-press/core:Frame");
10
+
11
+ function classNames(...values: Array<string | undefined>) {
12
+ const joined = values.filter(Boolean).join(" ");
13
+ return joined.length > 0 ? joined : undefined;
14
+ }
15
+
16
+ export function Frame({
17
+ frameKey,
18
+ role,
19
+ chrome = true,
20
+ className,
21
+ children,
22
+ ...rest
23
+ }: FrameProps) {
24
+ if (!frameKey || !String(frameKey).trim()) {
25
+ throw new Error("Frame requires a non-empty frameKey.");
26
+ }
27
+ if (frameKey && frameKey.includes(RESERVED_EXTENDED)) {
28
+ throw new Error(
29
+ `Frame frameKey="${frameKey}" contains reserved substring ":extended:". ` +
30
+ `This pattern is reserved for the overflow-extension pipeline.`,
31
+ );
32
+ }
33
+
34
+ const press = useContext(PressContext);
35
+ const allocation = press?.allocation ?? null;
36
+ const frameAllocation = frameKey && allocation ? allocation[frameKey] : undefined;
37
+
38
+ // Mutable per-render counter. SSR renders a Frame exactly once, so a plain
39
+ // object is fine — no useRef needed.
40
+ const areaCounts: Record<string, number> = {};
41
+ const frameContextValue: FrameContextValue = {
42
+ frameKey: frameKey ?? "",
43
+ consumeArea(chainId: string): ReactNode | null {
44
+ const index = areaCounts[chainId] ?? 0;
45
+ areaCounts[chainId] = index + 1;
46
+ if (!frameAllocation) return null;
47
+ const chainSlots = frameAllocation[chainId];
48
+ if (!chainSlots) return null;
49
+ return chainSlots[index] ?? null;
50
+ },
51
+ };
52
+
53
+ const pageKind = derivePageKind(role);
54
+
55
+ return (
56
+ <FrameContext.Provider value={frameContextValue}>
57
+ <section
58
+ {...(rest as Record<string, unknown>)}
59
+ className={classNames("reader-page", className)}
60
+ data-openpress-frame-key={frameKey}
61
+ data-frame-role={role}
62
+ data-page-kind={pageKind}
63
+ data-frame-chrome={chrome ? "true" : "false"}
64
+ data-page-footer={chrome ? "true" : "false"}
65
+ >
66
+ {children}
67
+ </section>
68
+ </FrameContext.Provider>
69
+ );
70
+ }
71
+
72
+ (Frame as unknown as { openpressMarker: typeof FRAME_MARKER }).openpressMarker = FRAME_MARKER;
73
+
74
+ function derivePageKind(role: string | undefined): string | undefined {
75
+ if (!role) return undefined;
76
+ const trimmed = role.trim();
77
+ if (!trimmed) return undefined;
78
+ const lastDot = trimmed.lastIndexOf(".");
79
+ return lastDot === -1 ? trimmed : trimmed.slice(lastDot + 1);
80
+ }
@@ -0,0 +1,19 @@
1
+ import { createContext, type ReactNode } from "react";
2
+
3
+ // FrameContext is the runtime channel between Frame and its descendant
4
+ // MdxArea instances. Frame creates one of these on each render; MdxArea
5
+ // reads it to claim its slot of allocated content.
6
+ //
7
+ // "Claiming" is order-sensitive: the first <MdxArea chainId="X"> rendered
8
+ // inside a Frame takes areaIndex 0 for chain X, the next takes index 1,
9
+ // and so on. Empty Frames (no allocation) return null, which renders the
10
+ // MdxArea as a measurement placeholder.
11
+
12
+ export interface FrameContextValue {
13
+ frameKey: string;
14
+ // Consume the next allocation slot for this chainId. Returns null if the
15
+ // frame has no allocation (measurement pass) or no blocks for this chain.
16
+ consumeArea(chainId: string): ReactNode | null;
17
+ }
18
+
19
+ export const FrameContext = createContext<FrameContextValue | null>(null);
@@ -0,0 +1,35 @@
1
+ import { useContext, type ReactNode } from "react";
2
+ import { FrameContext } from "./FrameContext";
3
+ import type { MdxAreaProps } from "./types";
4
+
5
+ function classNames(...values: Array<string | undefined>) {
6
+ const joined = values.filter(Boolean).join(" ");
7
+ return joined.length > 0 ? joined : undefined;
8
+ }
9
+
10
+ export function MdxArea({
11
+ chainId,
12
+ overflow = "extend",
13
+ className,
14
+ ...rest
15
+ }: MdxAreaProps) {
16
+ const frame = useContext(FrameContext);
17
+
18
+ let blocks: ReactNode | null = null;
19
+ if (frame) {
20
+ blocks = frame.consumeArea(chainId);
21
+ }
22
+
23
+ return (
24
+ <div
25
+ {...(rest as Record<string, unknown>)}
26
+ className={classNames("openpress-mdx-area", className)}
27
+ data-openpress-mdx-area="true"
28
+ data-openpress-mdx-area-chain={chainId}
29
+ data-openpress-mdx-area-overflow={overflow}
30
+ data-openpress-mdx-area-empty={blocks == null ? "true" : undefined}
31
+ >
32
+ {blocks}
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,34 @@
1
+ import { createContext, Fragment, type ReactNode } from "react";
2
+ import type { FrameAllocation, ResolvedSource, TocEntry } from "./types";
3
+
4
+ // Marker the engine uses to distinguish a Press default export from any other
5
+ // React component. Workspaces register a default export whose `type` is this
6
+ // `Press` component; the engine identity-checks against it.
7
+ export const PRESS_MARKER: unique symbol = Symbol.for("@open-press/core:Press");
8
+
9
+ // Allocation hints feed Layer 4 results back to Layer 2 so helpers like
10
+ // <Sections> can emit the correct number of pages per chain. Null during
11
+ // the first pass; populated after the allocator has decided how many
12
+ // frames each chain needs.
13
+ export interface AllocationHints {
14
+ totalPagesPerChain: Record<string, number>;
15
+ }
16
+
17
+ export interface PressContextValue {
18
+ sources: Record<string, ResolvedSource>;
19
+ // Allocation map keyed by frameKey -> chainId -> areaIndex -> blocks.
20
+ // null during measurement passes; populated during final render.
21
+ allocation: FrameAllocation | null;
22
+ // Allocation hints fed back from Layer 4 to Layer 2 helpers. null on
23
+ // the first measurement pass.
24
+ hints: AllocationHints | null;
25
+ toc: Record<string, TocEntry[]> | null;
26
+ }
27
+
28
+ export const PressContext = createContext<PressContextValue | null>(null);
29
+
30
+ export function Press({ children }: { children: ReactNode }) {
31
+ return <Fragment>{children}</Fragment>;
32
+ }
33
+
34
+ (Press as unknown as { openpressMarker: typeof PRESS_MARKER }).openpressMarker = PRESS_MARKER;
@@ -1,20 +1,39 @@
1
- export {
2
- BaseBackCoverPage,
3
- BaseCallout,
4
- BaseCoverPage,
5
- BaseFigure,
6
- BasePage,
7
- BaseContentPage,
8
- BaseTocPage,
9
- } from "./basePages";
1
+ // @open-press/core — Press tree primitives.
2
+ //
3
+ // Layout: this entry exports only the kernel. Source descriptors live in
4
+ // `@open-press/core/mdx`; manuscript helpers live in
5
+ // `@open-press/core/manuscript`. Keeping the surface small is intentional —
6
+ // the engine is not allowed to know about higher-level conventions.
7
+
8
+ export { Press, PressContext, PRESS_MARKER } from "./Press";
9
+ export { Frame, FRAME_MARKER } from "./Frame";
10
+ export { FrameContext } from "./FrameContext";
11
+ export { MdxArea } from "./MdxArea";
12
+ export { useSource } from "./useSource";
13
+ export { BaseFigure, BaseCallout } from "./primitives";
14
+
10
15
  export type {
16
+ FrameProps,
17
+ FrameRole,
18
+ MdxAreaProps,
19
+ MdxAreaOverflow,
20
+ PressProps,
21
+ BaseFigureProps,
11
22
  BaseCalloutKind,
12
23
  BaseCalloutProps,
13
- BaseFigureProps,
14
- BasePageProps,
15
- BaseContentPageProps,
16
- BaseShellPageProps,
17
- PageKind,
18
- PageProps,
19
24
  Manifest,
25
+ // Source-side types are re-exported here for convenience so authors can
26
+ // import `ResolvedSource` from the same place they import primitives.
27
+ ResolvedSource,
28
+ SourceNode,
29
+ OutlineItem,
30
+ SourceFileRecord,
31
+ SourceBlock,
32
+ TocEntry,
33
+ MdxSourceDescriptor,
34
+ SourceDescriptor,
35
+ FrameAllocation,
20
36
  } from "./types";
37
+
38
+ export type { PressContextValue, AllocationHints } from "./Press";
39
+ export type { FrameContextValue } from "./FrameContext";
@@ -0,0 +1,23 @@
1
+ import type { BaseCalloutProps, BaseFigureProps } from "./types";
2
+
3
+ function classNames(...values: Array<string | undefined>) {
4
+ const joined = values.filter(Boolean).join(" ");
5
+ return joined.length > 0 ? joined : undefined;
6
+ }
7
+
8
+ export function BaseFigure({ caption, className, children, ...figureProps }: BaseFigureProps) {
9
+ return (
10
+ <figure {...figureProps} className={classNames("openpress-figure", className)}>
11
+ <div data-figure-body>{children}</div>
12
+ {caption === undefined ? null : <figcaption>{caption}</figcaption>}
13
+ </figure>
14
+ );
15
+ }
16
+
17
+ export function BaseCallout({ kind = "info", className, children, ...calloutProps }: BaseCalloutProps) {
18
+ return (
19
+ <aside {...calloutProps} className={classNames("openpress-callout", className)} data-callout-kind={kind}>
20
+ {children}
21
+ </aside>
22
+ );
23
+ }