@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
@@ -1,29 +1,37 @@
1
1
  import type { HTMLAttributes, ReactNode } from "react";
2
2
 
3
- export type PageKind = "cover" | "toc" | "content" | "back-cover";
3
+ // ---------------------------------------------------------------------------
4
+ // Frame / MdxArea / Press primitives
5
+ // ---------------------------------------------------------------------------
4
6
 
5
- export interface PageProps {
6
- pageIndex: number;
7
- totalPages: number;
8
- chapterSlug?: string;
9
- chapterTone?: string;
10
- children: ReactNode;
11
- }
7
+ // `role` is opaque to core. Helpers and themes interpret it; the engine
8
+ // never branches on its value. Format convention is dotted, e.g.
9
+ // "manuscript.cover", "manuscript.content", "folio.page".
10
+ export type FrameRole = string;
12
11
 
13
- export type BasePageProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
14
- kind: PageKind;
15
- footer?: boolean;
12
+ export type FrameProps = Omit<HTMLAttributes<HTMLElement>, "role" | "children"> & {
13
+ frameKey: string;
14
+ role?: FrameRole;
15
+ chrome?: boolean;
16
+ className?: string;
16
17
  children?: ReactNode;
17
18
  };
18
19
 
19
- export type BaseShellPageProps = Omit<BasePageProps, "kind" | "footer">;
20
+ export type MdxAreaOverflow = "extend" | "truncate" | "error";
20
21
 
21
- export type BaseContentPageProps = Omit<BasePageProps, "kind" | "footer" | "children"> &
22
- PageProps & {
23
- runningHeader?: ReactNode;
24
- footerLeft?: ReactNode;
25
- footerRight?: ReactNode;
26
- };
22
+ export type MdxAreaProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
23
+ chainId: string;
24
+ overflow?: MdxAreaOverflow;
25
+ className?: string;
26
+ };
27
+
28
+ export interface PressProps {
29
+ children: ReactNode;
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Content primitives (figure, callout)
34
+ // ---------------------------------------------------------------------------
27
35
 
28
36
  export type BaseFigureProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
29
37
  caption?: ReactNode;
@@ -37,6 +45,105 @@ export type BaseCalloutProps = Omit<HTMLAttributes<HTMLElement>, "children"> & {
37
45
  children: ReactNode;
38
46
  };
39
47
 
48
+ // ---------------------------------------------------------------------------
49
+ // Source descriptors and resolved sources
50
+ // ---------------------------------------------------------------------------
51
+
52
+ export interface MdxSourceDescriptorSectionFolders {
53
+ type: "mdx";
54
+ preset: "section-folders";
55
+ root?: string;
56
+ }
57
+
58
+ export interface MdxSourceDescriptorSectionFiles {
59
+ type: "mdx";
60
+ preset: "section-files";
61
+ root?: string;
62
+ }
63
+
64
+ export interface MdxSourceDescriptorFileList {
65
+ type: "mdx";
66
+ preset: "file-list";
67
+ files: string[];
68
+ }
69
+
70
+ export type MdxSourceDescriptor =
71
+ | MdxSourceDescriptorSectionFolders
72
+ | MdxSourceDescriptorSectionFiles
73
+ | MdxSourceDescriptorFileList;
74
+
75
+ export type SourceDescriptor = MdxSourceDescriptor;
76
+
77
+ export interface SourceNode {
78
+ id: string;
79
+ slug: string;
80
+ title: string;
81
+ meta?: Record<string, unknown>;
82
+ children?: SourceNode[];
83
+ }
84
+
85
+ export interface OutlineItem {
86
+ id: string;
87
+ depth: number;
88
+ title: string;
89
+ sectionSlug: string;
90
+ pageNumber?: number;
91
+ }
92
+
93
+ export interface TocEntry {
94
+ id: string;
95
+ blockId: string;
96
+ sourceId: string;
97
+ sectionSlug: string;
98
+ title: string;
99
+ href: string;
100
+ level: 2 | 3;
101
+ label: string;
102
+ pageNumber?: number;
103
+ }
104
+
105
+ export interface SourceFileRecord {
106
+ path: string;
107
+ absolutePath: string;
108
+ sectionSlug: string;
109
+ }
110
+
111
+ export interface SourceBlock {
112
+ id: string;
113
+ kind: string;
114
+ name?: string;
115
+ chainId: string;
116
+ sectionSlug: string;
117
+ path: string;
118
+ source: {
119
+ file: string;
120
+ line?: number;
121
+ column?: number;
122
+ offset?: number;
123
+ };
124
+ }
125
+
126
+ export interface ResolvedSource {
127
+ id: string;
128
+ type: "mdx";
129
+ tree: SourceNode[];
130
+ outline: OutlineItem[];
131
+ chains: Record<string, SourceBlock[]>;
132
+ files: SourceFileRecord[];
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Allocation context shape (engine -> Frame -> MdxArea)
137
+ // ---------------------------------------------------------------------------
138
+
139
+ // Per-frame, per-chain, ordered list of React nodes assigned to each
140
+ // MdxArea by area index.
141
+ export type FrameAllocation = Record<string, Record<string, ReactNode[]>>;
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Manifest
145
+ // ---------------------------------------------------------------------------
146
+
40
147
  export interface Manifest {
41
148
  title: string;
42
149
  subtitle?: string;
@@ -50,13 +157,18 @@ export interface Manifest {
50
157
  designDoc?: string;
51
158
  publicDir?: string;
52
159
  outputDir?: string;
160
+ captionNumbering?: {
161
+ figure?: string;
162
+ table?: string;
163
+ separator?: string;
164
+ };
53
165
  pdf?: {
54
166
  filename?: string;
55
167
  };
56
168
  deploy?: {
57
169
  adapter?: string;
58
170
  source?: string;
59
- projectName?: string;
171
+ projectName?: string | null;
60
172
  commitDirty?: boolean;
61
173
  requiresConfirmation?: boolean;
62
174
  };
@@ -0,0 +1,28 @@
1
+ import { useContext } from "react";
2
+ import { PressContext } from "./Press";
3
+ import type { ResolvedSource } from "./types";
4
+
5
+ // Read a resolved source by its registered key. The engine populates the
6
+ // PressContext before rendering, so this is synchronous and safe to call
7
+ // from any component inside <Press>.
8
+ //
9
+ // Throws if the source is missing, with a hint listing known sources.
10
+ export function useSource<T extends ResolvedSource = ResolvedSource>(id: string): T {
11
+ const ctx = useContext(PressContext);
12
+ if (!ctx) {
13
+ throw new Error(
14
+ `useSource("${id}") called outside <Press> tree. ` +
15
+ `Source hooks only work inside the default-exported <Press> component.`,
16
+ );
17
+ }
18
+ const source = ctx.sources[id];
19
+ if (!source) {
20
+ const known = Object.keys(ctx.sources).sort();
21
+ const knownText = known.length > 0 ? known.join(", ") : "(none)";
22
+ throw new Error(
23
+ `Unknown source "${id}". Available sources: ${knownText}. ` +
24
+ `Register it under \`export const sources\` in document/index.tsx.`,
25
+ );
26
+ }
27
+ return source as T;
28
+ }
@@ -0,0 +1,196 @@
1
+ // @open-press/core/manuscript — long-form section-flow helpers.
2
+ //
3
+ // Manuscript helpers cover the "paper / report / book / monograph" pattern:
4
+ // a source resolves into a sequence of sections, each section becomes one
5
+ // content frame (with overflow auto-cloning across pages), and a TOC frame
6
+ // summarizes the outline.
7
+ //
8
+ // These helpers are conventions only. Core never knows about them. Any
9
+ // document type that wants section flow imports from here; documents that
10
+ // do not (slides, folios, calendars) skip this module entirely.
11
+
12
+ import { Fragment, useContext, type ReactNode } from "react";
13
+ import { Frame, FrameContext, PressContext, useSource } from "../core";
14
+ import type { MdxAreaOverflow, ResolvedSource } from "../core";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // <Sections>
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export interface SectionsPageProps {
21
+ frameKey: string;
22
+ chainId: string;
23
+ pageIndex: number;
24
+ totalPages: number;
25
+ sectionSlug: string;
26
+ sectionTitle: string;
27
+ sectionTone?: string;
28
+ sectionMeta: Record<string, unknown>;
29
+ }
30
+
31
+ export interface SectionsOpenerProps {
32
+ frameKey: string;
33
+ sectionSlug: string;
34
+ sectionTitle: string;
35
+ sectionIndex: number;
36
+ sectionMeta: Record<string, unknown>;
37
+ }
38
+
39
+ export interface SectionsProps {
40
+ source: string;
41
+ page: React.ComponentType<SectionsPageProps>;
42
+ opener?: React.ComponentType<SectionsOpenerProps>;
43
+ }
44
+
45
+ export function Sections({ source: sourceId, page: Page, opener: Opener }: SectionsProps) {
46
+ const source = useSource(sourceId);
47
+ const press = useContext(PressContext);
48
+ const hints = press?.hints ?? null;
49
+ return (
50
+ <Fragment>
51
+ {source.tree.map((section, index) => {
52
+ const chainId = `${sourceId}:${section.slug}`;
53
+ const meta = (section.meta ?? {}) as Record<string, unknown>;
54
+ const tone = typeof meta.tone === "string" ? meta.tone : undefined;
55
+ const totalPages = Math.max(1, hints?.totalPagesPerChain?.[chainId] ?? 1);
56
+ const pages: ReactNode[] = [];
57
+ for (let i = 0; i < totalPages; i++) {
58
+ pages.push(
59
+ <Page
60
+ key={i}
61
+ frameKey={`${sourceId}:${section.slug}:content:${i}`}
62
+ chainId={chainId}
63
+ pageIndex={i}
64
+ totalPages={totalPages}
65
+ sectionSlug={section.slug}
66
+ sectionTitle={section.title}
67
+ sectionTone={tone}
68
+ sectionMeta={meta}
69
+ />,
70
+ );
71
+ }
72
+ return (
73
+ <Fragment key={section.slug}>
74
+ {Opener ? (
75
+ <Opener
76
+ frameKey={`${sourceId}:${section.slug}:opener`}
77
+ sectionSlug={section.slug}
78
+ sectionTitle={section.title}
79
+ sectionIndex={index}
80
+ sectionMeta={meta}
81
+ />
82
+ ) : null}
83
+ {pages}
84
+ </Fragment>
85
+ );
86
+ })}
87
+ </Fragment>
88
+ );
89
+ }
90
+
91
+ // Compatibility alias for chapter vocabulary.
92
+ export const Chapters = Sections;
93
+ export type ChaptersProps = SectionsProps;
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // <Toc>
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export interface TocProps {
100
+ source: string;
101
+ className?: string;
102
+ heading?: ReactNode;
103
+ frameKey?: string;
104
+ maxLevel?: 2 | 3;
105
+ overflow?: MdxAreaOverflow;
106
+ page?: React.ComponentType<TocPageProps>;
107
+ }
108
+
109
+ export interface TocPageProps {
110
+ frameKey: string;
111
+ chainId: string;
112
+ pageIndex: number;
113
+ totalPages: number;
114
+ sourceId: string;
115
+ heading?: ReactNode;
116
+ className?: string;
117
+ maxLevel?: 2 | 3;
118
+ overflow?: MdxAreaOverflow;
119
+ }
120
+
121
+ export interface TocAreaProps {
122
+ chainId: string;
123
+ maxLevel?: 2 | 3;
124
+ overflow?: MdxAreaOverflow;
125
+ className?: string;
126
+ }
127
+
128
+ export function Toc({ source: sourceId, className, heading, frameKey = "toc", maxLevel = 3, overflow = "extend", page: Page = DefaultTocPage }: TocProps) {
129
+ useSource(sourceId) as ResolvedSource;
130
+ const press = useContext(PressContext);
131
+ const chainId = maxLevel <= 2 ? `toc:${sourceId}:h2` : `toc:${sourceId}`;
132
+ const totalPages = Math.max(1, press?.hints?.totalPagesPerChain?.[chainId] ?? 1);
133
+ const pages: ReactNode[] = [];
134
+ for (let i = 0; i < totalPages; i++) {
135
+ pages.push(
136
+ <Page
137
+ key={i}
138
+ frameKey={i === 0 ? frameKey : `${frameKey}:page:${i}`}
139
+ chainId={chainId}
140
+ pageIndex={i}
141
+ totalPages={totalPages}
142
+ sourceId={sourceId}
143
+ heading={heading}
144
+ className={className}
145
+ maxLevel={maxLevel}
146
+ overflow={overflow}
147
+ />,
148
+ );
149
+ }
150
+ return <Fragment>{pages}</Fragment>;
151
+ }
152
+
153
+ function DefaultTocPage({ frameKey, chainId, pageIndex, totalPages, heading, className, maxLevel, overflow }: TocPageProps) {
154
+ const isContinuation = pageIndex > 0;
155
+ const tocClassName = ["reader-page--toc", isContinuation ? "toc-continuation" : null, className].filter(Boolean).join(" ") || undefined;
156
+ return (
157
+ <Frame
158
+ frameKey={frameKey}
159
+ role="manuscript.toc"
160
+ chrome={false}
161
+ className={tocClassName}
162
+ >
163
+ <div className="page-frame">
164
+ <header className="page-header toc-header">
165
+ {heading ?? (
166
+ <h2 className={isContinuation ? "toc-heading toc-heading--continuation" : "toc-heading"} id={isContinuation ? `${frameKey}-title` : "toc-title"}>
167
+ {isContinuation ? "目錄續" : "目錄"}
168
+ </h2>
169
+ )}
170
+ </header>
171
+ <main className="page-body">
172
+ <TocArea chainId={chainId} maxLevel={maxLevel} overflow={overflow} />
173
+ </main>
174
+ </div>
175
+ </Frame>
176
+ );
177
+ }
178
+
179
+ export function TocArea({ chainId, maxLevel, overflow = "extend", className }: TocAreaProps) {
180
+ const frame = useContext(FrameContext);
181
+ const blocks = frame?.consumeArea(chainId) ?? null;
182
+ return (
183
+ <div
184
+ className="openpress-mdx-area openpress-toc-area"
185
+ data-openpress-mdx-area="true"
186
+ data-openpress-mdx-area-chain={chainId}
187
+ data-openpress-toc-max-level={maxLevel}
188
+ data-openpress-mdx-area-overflow={overflow}
189
+ data-openpress-mdx-area-empty={blocks == null ? "true" : undefined}
190
+ >
191
+ <ol className={["toc-list", className].filter(Boolean).join(" ") || undefined}>
192
+ {blocks}
193
+ </ol>
194
+ </div>
195
+ );
196
+ }
@@ -0,0 +1,88 @@
1
+ // @open-press/core/mdx — pure source descriptor factories.
2
+ //
3
+ // These factories MUST stay pure: they construct descriptor objects only.
4
+ // They must not touch the filesystem, fetch the network, or execute
5
+ // workspace logic at module load. Resolution happens in Layer 1 inside the
6
+ // engine, where IO is allowed.
7
+
8
+ import type {
9
+ MdxSourceDescriptor,
10
+ MdxSourceDescriptorFileList,
11
+ MdxSourceDescriptorSectionFiles,
12
+ MdxSourceDescriptorSectionFolders,
13
+ } from "../core/types";
14
+
15
+ export type {
16
+ MdxSourceDescriptor,
17
+ MdxSourceDescriptorFileList,
18
+ MdxSourceDescriptorSectionFiles,
19
+ MdxSourceDescriptorSectionFolders,
20
+ } from "../core/types";
21
+
22
+ export type {
23
+ OutlineItem,
24
+ ResolvedSource,
25
+ SourceBlock,
26
+ SourceFileRecord,
27
+ SourceNode,
28
+ } from "../core/types";
29
+
30
+ type MdxSourceOptions =
31
+ | { preset: "section-folders"; root?: string }
32
+ | { preset: "section-files"; root?: string }
33
+ | { preset: "file-list"; files: string[] };
34
+
35
+ const VALID_PRESETS = new Set(["section-folders", "section-files", "file-list"]);
36
+
37
+ export function mdxSource(options: MdxSourceOptions): MdxSourceDescriptor {
38
+ if (!options || typeof options !== "object") {
39
+ throw new Error("mdxSource() requires an options object.");
40
+ }
41
+ if (!VALID_PRESETS.has(options.preset)) {
42
+ throw new Error(
43
+ `mdxSource() preset must be one of: section-folders, section-files, file-list. Got "${
44
+ (options as { preset?: unknown }).preset
45
+ }".`,
46
+ );
47
+ }
48
+
49
+ if (options.preset === "section-folders") {
50
+ return normalizeRooted("section-folders", options.root, "chapters") as MdxSourceDescriptorSectionFolders;
51
+ }
52
+ if (options.preset === "section-files") {
53
+ return normalizeRooted("section-files", options.root, "content") as MdxSourceDescriptorSectionFiles;
54
+ }
55
+
56
+ // file-list
57
+ if (!Array.isArray(options.files)) {
58
+ throw new Error('mdxSource({ preset: "file-list" }) requires `files: string[]`.');
59
+ }
60
+ const files: string[] = [];
61
+ for (const raw of options.files) {
62
+ if (typeof raw !== "string") {
63
+ throw new Error('mdxSource({ preset: "file-list" }) `files` entries must be strings.');
64
+ }
65
+ const trimmed = raw.trim();
66
+ if (!trimmed) continue;
67
+ files.push(trimmed);
68
+ }
69
+ if (files.length === 0) {
70
+ throw new Error('mdxSource({ preset: "file-list" }) requires at least one file.');
71
+ }
72
+ return { type: "mdx", preset: "file-list", files };
73
+ }
74
+
75
+ function normalizeRooted(
76
+ preset: "section-folders" | "section-files",
77
+ root: string | undefined,
78
+ defaultRoot: string,
79
+ ): MdxSourceDescriptor {
80
+ if (root !== undefined && typeof root !== "string") {
81
+ throw new Error(`mdxSource() \`root\` must be a string if provided. Got ${typeof root}.`);
82
+ }
83
+ return {
84
+ type: "mdx",
85
+ preset,
86
+ root: (root ?? defaultRoot).trim() || defaultRoot,
87
+ };
88
+ }