@forevka/wordcanvas 0.1.0 → 0.2.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.
@@ -320,7 +320,7 @@ class c {
320
320
  this.handle = t, o(t);
321
321
  },
322
322
  onEvent: (t) => this.emit(t)
323
- }), import("./editorApp-Bl8rfr3S.js");
323
+ }), import("./editorApp-Dl_Glngl.js");
324
324
  });
325
325
  }
326
326
  /** Subscribe to a collaboration event. Returns an unsubscribe function. */
@@ -344,6 +344,14 @@ class c {
344
344
  async openDocx(e) {
345
345
  return (await this.ready).openDocx(e);
346
346
  }
347
+ /** Replace the open document with a programmatically-built one (e.g. a
348
+ * DocumentBuilder result). The input is cloned. Like openDocx, this starts
349
+ * a NEW document: undo history and any live collab session are dropped
350
+ * (the next share() forks). Zoom and scroll position are preserved, so
351
+ * calling this on every data change gives a stable live preview. */
352
+ async setDocument(e) {
353
+ (await this.ready).setDocument(e);
354
+ }
347
355
  /** Publish the current document and resolve its shareable link (online only). */
348
356
  async share() {
349
357
  return (await this.ready).share();
@@ -1,4 +1,4 @@
1
- import { W as r } from "./wordcanvas-DPJsuqnm.js";
1
+ import { W as r } from "./wordcanvas-CgZ_uuf9.js";
2
2
  export {
3
3
  r as WordCanvas
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forevka/wordcanvas",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Embeddable canvas-rendered, Word-compatible document editor with live collaboration and DOCX/PDF import & export. Self-contained, zero runtime dependencies.",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  "types": "./types/wordcanvas.d.ts",
33
33
  "import": "./dist-lib/wordcanvas.js"
34
34
  },
35
+ "./builder": "./src/builder/index.ts",
35
36
  "./import": "./src/import/docx/pipeline.ts",
36
37
  "./export": "./src/export/pipeline.ts",
37
38
  "./export/measure": "./src/export/shared/measureHost.ts"
@@ -42,6 +43,10 @@
42
43
  ".": {
43
44
  "types": "./types/wordcanvas.d.ts",
44
45
  "import": "./dist-lib/wordcanvas.js"
46
+ },
47
+ "./builder": {
48
+ "types": "./types/builder.d.ts",
49
+ "import": "./dist-lib/builder.js"
45
50
  }
46
51
  }
47
52
  },
@@ -0,0 +1,231 @@
1
+ // Public type surface for @forevka/wordcanvas/builder — the fluent
2
+ // programmatic document composer. Hand-written, self-contained (model types
3
+ // come from ./model, shared with wordcanvas.d.ts).
4
+ //
5
+ // import { DocumentBuilder } from "@forevka/wordcanvas/builder";
6
+ // const b = await DocumentBuilder.fromTemplate(templateBytes);
7
+ // const doc = b.paragraph(data.title).withStyle("Heading1").build();
8
+ //
9
+ // Works in the browser (live preview via WordCanvas.setDocument) and in Node
10
+ // (server-side DOCX/PDF generation via the export subpath).
11
+
12
+ import type { CellBorders, CellMargin, CharStyle, Document, NamedStyle, ParaStyle, Stylesheet } from "./model";
13
+
14
+ export type { Block, CharStyle, Document, NamedStyle, ParaStyle, Stylesheet } from "./model";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Units
18
+
19
+ /** Inches → px @96dpi. */
20
+ export declare function inches(n: number): number;
21
+ /** Centimeters → px @96dpi. */
22
+ export declare function cm(n: number): number;
23
+ /** Points (1/72in) → px @96dpi. */
24
+ export declare function pt(n: number): number;
25
+ /** Twips (1/20pt) → px @96dpi. */
26
+ export declare function twips(n: number): number;
27
+
28
+ export interface PageSize {
29
+ pageWidthPx: number;
30
+ pageHeightPx: number;
31
+ }
32
+
33
+ /** Common page sizes in px @96dpi. */
34
+ export declare const PAGE_SIZES: Record<"Letter" | "Legal" | "A4" | "A3" | "Tabloid", PageSize>;
35
+ export type PageSizeName = keyof typeof PAGE_SIZES;
36
+
37
+ /** Base64-encode image bytes as a portable data: URL. */
38
+ export declare function bytesToDataUrl(bytes: Uint8Array | ArrayBuffer, mime: string): string;
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Options
42
+
43
+ export interface BuilderWarning {
44
+ code: string;
45
+ message: string;
46
+ }
47
+
48
+ export interface CreateOptions {
49
+ pageSize?: PageSizeName | PageSize;
50
+ margins?: Partial<{ top: number; right: number; bottom: number; left: number }>;
51
+ /** Start from a custom stylesheet (default: the editor's default gallery). */
52
+ stylesheet?: Stylesheet;
53
+ /** Deterministic id namespace (tests); default is a random per-builder seed. */
54
+ idSeed?: string;
55
+ }
56
+
57
+ export interface TemplateOptions {
58
+ /** Keep the template's body content and append after it (default: discard —
59
+ * the template contributes styles, lists, page setup, and bands). */
60
+ keepBody?: boolean;
61
+ idSeed?: string;
62
+ }
63
+
64
+ export interface PageSetup {
65
+ pageSize?: PageSizeName | PageSize;
66
+ orientation?: "portrait" | "landscape";
67
+ margins?: Partial<{ top: number; right: number; bottom: number; left: number }>;
68
+ columns?: { count: number; gapPx?: number };
69
+ headerDistancePx?: number;
70
+ footerDistancePx?: number;
71
+ pageNumberStart?: number;
72
+ }
73
+
74
+ export interface BandOptions {
75
+ /** Which page band variant: default (all pages), first page, or even pages. */
76
+ variant?: "default" | "first" | "even";
77
+ }
78
+
79
+ export interface ImageOptions {
80
+ /** Required — the builder runs in Node too, where images can't be measured. */
81
+ widthPx: number;
82
+ heightPx: number;
83
+ align?: "left" | "center" | "right";
84
+ wrap?: "block" | "square";
85
+ }
86
+
87
+ export interface ListItem {
88
+ text: string;
89
+ /** Nesting level 0..8 (default 0, or ListOptions.level). */
90
+ level?: number;
91
+ style?: Partial<CharStyle>;
92
+ }
93
+
94
+ export interface ListOptions {
95
+ kind?: "bullet" | "number";
96
+ /** Reference an existing list definition (e.g. one carried by the template). */
97
+ listId?: string;
98
+ level?: number;
99
+ }
100
+
101
+ export interface CellSpec {
102
+ text?: string;
103
+ colSpan?: number;
104
+ rowSpan?: number;
105
+ /** Background fill (CSS color). */
106
+ shading?: string;
107
+ borders?: CellBorders;
108
+ margin?: CellMargin;
109
+ /** Char formatting for the cell's text. */
110
+ style?: Partial<CharStyle>;
111
+ align?: ParaStyle["align"];
112
+ }
113
+
114
+ export type CellContent = string | CellSpec;
115
+ export type CellOptions = Omit<CellSpec, "text">;
116
+
117
+ export interface TableOptions {
118
+ /** Column widths as fractions of the content width (normalized to sum 1). */
119
+ colFractions?: number[];
120
+ /** Bold every run in the first row. */
121
+ headerRow?: boolean;
122
+ }
123
+
124
+ export interface SpacingOptions {
125
+ before?: number;
126
+ after?: number;
127
+ lineHeight?: number;
128
+ }
129
+
130
+ export interface IndentOptions {
131
+ left?: number;
132
+ right?: number;
133
+ firstLine?: number;
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Scopes
138
+
139
+ /** Block-scope surface shared by the document body, header/footer bands, and
140
+ * table cells. Methods append eagerly and return the scope (or a paragraph
141
+ * scope that delegates back), so chains never need a seal step. */
142
+ export declare class StoryBuilder {
143
+ paragraph(text?: string, style?: Partial<CharStyle>): ParagraphBuilder<this>;
144
+ table(rows: CellContent[][], opts?: TableOptions): this;
145
+ table(build: (t: TableBuilder) => void, opts?: TableOptions): this;
146
+ image(src: string | { data: Uint8Array | ArrayBuffer; mime: string }, opts: ImageOptions): this;
147
+ list(items: (string | ListItem)[], opts?: ListOptions): this;
148
+ bulletList(items: (string | ListItem)[]): this;
149
+ numberedList(items: (string | ListItem)[]): this;
150
+ /** The NEXT block starts a new page. */
151
+ pageBreak(): this;
152
+ }
153
+
154
+ /** Paragraph scope. Char formatting (bold/color/font/…) patches every run
155
+ * already in the paragraph and becomes the default for later text() runs;
156
+ * mixed formatting uses text(t, { …patch }). Any block-starting call pops
157
+ * back to the parent scope. */
158
+ export declare class ParagraphBuilder<P extends StoryBuilder> {
159
+ /** Apply a named style: sets the reference and patches exactly the fields
160
+ * the style defines. Direct formatting applied AFTER this call wins. */
161
+ withStyle(id: string): this;
162
+ /** Append a run. Inherits the scope's accrued formatting; `style` wins. */
163
+ text(text: string, style?: Partial<CharStyle>): this;
164
+ bold(on?: boolean): this;
165
+ italic(on?: boolean): this;
166
+ underline(on?: boolean): this;
167
+ strikethrough(on?: boolean): this;
168
+ color(cssColor: string): this;
169
+ highlight(cssColor: string): this;
170
+ fontSize(px: number): this;
171
+ font(family: string): this;
172
+ link(url: string): this;
173
+ align(align: ParaStyle["align"]): this;
174
+ spacing(opts: SpacingOptions): this;
175
+ indent(opts: IndentOptions): this;
176
+ keepWithNext(on?: boolean): this;
177
+ /** Escape the paragraph scope explicitly (block-starting calls do it implicitly). */
178
+ end(): P;
179
+
180
+ // Delegated block-starting surface (pops this scope):
181
+ paragraph(text?: string, style?: Partial<CharStyle>): ParagraphBuilder<P>;
182
+ table(rows: CellContent[][], opts?: TableOptions): P;
183
+ table(build: (t: TableBuilder) => void, opts?: TableOptions): P;
184
+ image(src: string | { data: Uint8Array | ArrayBuffer; mime: string }, opts: ImageOptions): P;
185
+ list(items: (string | ListItem)[], opts?: ListOptions): P;
186
+ bulletList(items: (string | ListItem)[]): P;
187
+ numberedList(items: (string | ListItem)[]): P;
188
+ pageBreak(): P;
189
+
190
+ // Document-level delegators (only when the parent is the root builder):
191
+ header(this: ParagraphBuilder<DocumentBuilder>, build: (s: StoryBuilder) => void, opts?: BandOptions): DocumentBuilder;
192
+ footer(this: ParagraphBuilder<DocumentBuilder>, build: (s: StoryBuilder) => void, opts?: BandOptions): DocumentBuilder;
193
+ pageSetup(this: ParagraphBuilder<DocumentBuilder>, setup: PageSetup): DocumentBuilder;
194
+ style(this: ParagraphBuilder<DocumentBuilder>, def: NamedStyle): DocumentBuilder;
195
+ build(this: ParagraphBuilder<DocumentBuilder>): Document;
196
+ }
197
+
198
+ export declare class TableBuilder {
199
+ row(cells: CellContent[]): this;
200
+ row(build: (r: RowBuilder) => void): this;
201
+ rows(data: CellContent[][]): this;
202
+ colFractions(fractions: number[]): this;
203
+ }
204
+
205
+ export declare class RowBuilder {
206
+ cell(content: CellContent, opts?: CellOptions): this;
207
+ cell(build: (s: StoryBuilder) => void, opts?: CellOptions): this;
208
+ }
209
+
210
+ /** Root of the fluent builder. Produces the same plain-data Document the
211
+ * editor renders (WordCanvas.setDocument) and the exporters write. build()
212
+ * returns a deep clone; the builder stays usable afterwards. */
213
+ export declare class DocumentBuilder extends StoryBuilder {
214
+ /** Start from a blank document (default stylesheet, US Letter, 1in margins). */
215
+ static create(opts?: CreateOptions): DocumentBuilder;
216
+ /** Start from a .docx template: its stylesheet, list definitions, page setup,
217
+ * and header/footer bands carry over (body discarded unless keepBody). */
218
+ static fromTemplate(docx: ArrayBuffer | Uint8Array, opts?: TemplateOptions): Promise<DocumentBuilder>;
219
+
220
+ /** Lossy decisions (unknown style ids, template import warnings, …). */
221
+ readonly warnings: readonly BuilderWarning[];
222
+ /** Compose a header band (replaces the variant's existing story). */
223
+ header(build: (s: StoryBuilder) => void, opts?: BandOptions): this;
224
+ footer(build: (s: StoryBuilder) => void, opts?: BandOptions): this;
225
+ /** Merge page geometry onto the section (template values are the base). */
226
+ pageSetup(setup: PageSetup): this;
227
+ /** Register (or override) a named style — BEFORE applying it via withStyle(). */
228
+ style(def: NamedStyle): this;
229
+ /** Snapshot the document: deep clone, guaranteed at least one paragraph. */
230
+ build(): Document;
231
+ }
@@ -0,0 +1,225 @@
1
+ // Public document-model types for @forevka/wordcanvas. Hand-written (like
2
+ // wordcanvas.d.ts) so the published surface stays self-contained and stable;
3
+ // mirrors shared/src/model — the editor, builder, and exporters all consume
4
+ // this same plain-data shape.
5
+
6
+ export interface CharStyle {
7
+ fontFamily: string;
8
+ fontSizePx: number;
9
+ bold: boolean;
10
+ italic: boolean;
11
+ underline: boolean;
12
+ strikethrough: boolean;
13
+ /** CSS color, e.g. "#202124". */
14
+ color: string;
15
+ /** Hidden text (OOXML w:vanish) — preserved but never laid out or painted. */
16
+ hidden?: boolean;
17
+ letterSpacingPx?: number;
18
+ /** Background highlight (Word's text highlight). */
19
+ highlightColor?: string | undefined;
20
+ /** Sub/superscript. */
21
+ verticalAlign?: "sub" | "super" | undefined;
22
+ /** Hyperlink target; linked runs paint blue+underlined. */
23
+ link?: string | undefined;
24
+ /** Footnote reference id — this run is the marker. */
25
+ footnoteRef?: string | undefined;
26
+ /** Content-control membership (properties live in Document.sdts). */
27
+ sdtId?: string | undefined;
28
+ }
29
+
30
+ export interface TabStop {
31
+ /** Position from the left content edge, px. */
32
+ posPx: number;
33
+ align?: "left" | "center" | "right" | "decimal";
34
+ leader?: "none" | "dot" | "dash" | "underscore";
35
+ }
36
+
37
+ export interface ParaStyle {
38
+ align: "left" | "center" | "right" | "justify";
39
+ /** Line height multiplier. */
40
+ lineHeight: number;
41
+ spaceBeforePx: number;
42
+ spaceAfterPx: number;
43
+ indentFirstLinePx: number;
44
+ indentLeftPx: number;
45
+ indentRightPx?: number;
46
+ keepWithNext?: boolean;
47
+ keepLinesTogether?: boolean;
48
+ /** This paragraph starts a new page. */
49
+ pageBreakBefore?: boolean;
50
+ /** List membership: definition id + level 0..8. */
51
+ list?: { listId: string; level: number } | undefined;
52
+ /** Named style reference into Document.stylesheet (e.g. "Heading1"). */
53
+ namedStyle?: string;
54
+ columnBreakBefore?: boolean;
55
+ tabStops?: TabStop[];
56
+ }
57
+
58
+ /** Style-homogeneous span of text. */
59
+ export interface Run {
60
+ text: string;
61
+ style: CharStyle;
62
+ }
63
+
64
+ export interface Paragraph {
65
+ kind: "paragraph";
66
+ /** Stable unique id. */
67
+ id: string;
68
+ revision: number;
69
+ runs: Run[];
70
+ style: ParaStyle;
71
+ }
72
+
73
+ export interface ImageBlock {
74
+ kind: "image";
75
+ id: string;
76
+ revision: number;
77
+ /** Image URL — use data: URLs for portable documents. */
78
+ src: string;
79
+ /** Content address of the bytes (sha256 hex), when registered. */
80
+ mediaId?: string;
81
+ widthPx: number;
82
+ heightPx: number;
83
+ align: "left" | "center" | "right";
84
+ /** 'block' (default): own line. 'square': floats per align, text wraps. */
85
+ wrap?: "block" | "square";
86
+ }
87
+
88
+ export interface CellBorder {
89
+ color: string;
90
+ widthPx: number;
91
+ style?: "single" | "double" | "dashed" | "dotted";
92
+ }
93
+
94
+ export interface CellBorders {
95
+ top?: CellBorder;
96
+ right?: CellBorder;
97
+ bottom?: CellBorder;
98
+ left?: CellBorder;
99
+ }
100
+
101
+ export interface CellMargin {
102
+ top: number;
103
+ right: number;
104
+ bottom: number;
105
+ left: number;
106
+ }
107
+
108
+ export interface TableCell {
109
+ id: string;
110
+ blocks: Block[];
111
+ colSpan?: number;
112
+ rowSpan?: number;
113
+ /** Background fill (CSS color). */
114
+ shading?: string;
115
+ borders?: CellBorders;
116
+ margin?: CellMargin;
117
+ }
118
+
119
+ export interface TableRow {
120
+ cells: TableCell[];
121
+ }
122
+
123
+ export interface TableBlock {
124
+ kind: "table";
125
+ id: string;
126
+ revision: number;
127
+ rows: TableRow[];
128
+ /** Column widths as fractions of content width (sum = 1). Absent = equal. */
129
+ colFractions?: number[];
130
+ }
131
+
132
+ export type Block = Paragraph | ImageBlock | TableBlock;
133
+
134
+ export type BandContainer = "header" | "footer" | "headerFirst" | "headerEven" | "footerFirst" | "footerEven";
135
+
136
+ export interface SectionProps {
137
+ pageWidthPx: number;
138
+ pageHeightPx: number;
139
+ marginPx: { top: number; right: number; bottom: number; left: number };
140
+ /** Newspaper columns. Absent = single column. */
141
+ columns?: { count: number; gapPx: number };
142
+ pageNumberStart?: number;
143
+ headerDistancePx?: number;
144
+ footerDistancePx?: number;
145
+ /** Header/footer block stories. {page}/{pages} tokens in run text are
146
+ * substituted per page at layout time. */
147
+ header?: Block[];
148
+ footer?: Block[];
149
+ headerFirst?: Block[];
150
+ headerEven?: Block[];
151
+ footerFirst?: Block[];
152
+ footerEven?: Block[];
153
+ }
154
+
155
+ export interface NamedStyle {
156
+ /** Shares the docx styleId space ("Normal", "Heading1", …). */
157
+ id: string;
158
+ /** Display name. */
159
+ name: string;
160
+ basedOn?: string;
161
+ char: Partial<CharStyle>;
162
+ para: Partial<ParaStyle>;
163
+ }
164
+
165
+ export interface Stylesheet {
166
+ styles: NamedStyle[];
167
+ defaultStyleId: string;
168
+ }
169
+
170
+ export type ListNumberFormat = "bullet" | "decimal" | "lowerLetter" | "upperLetter" | "lowerRoman" | "upperRoman";
171
+
172
+ export interface ListLevel {
173
+ format: ListNumberFormat;
174
+ /** Marker pattern; %N is level N-1's counter (e.g. "%1."). Ignored for bullets. */
175
+ text: string;
176
+ bulletChar?: string;
177
+ indentLeftPx: number;
178
+ hangingPx: number;
179
+ start: number;
180
+ markerStyle?: Partial<CharStyle>;
181
+ }
182
+
183
+ export interface ListDefinition {
184
+ id: string;
185
+ /** Up to 9 levels (0..8). */
186
+ levels: ListLevel[];
187
+ }
188
+
189
+ export interface DocPosition {
190
+ blockId: string;
191
+ offset: number;
192
+ }
193
+
194
+ export interface BookmarkRange {
195
+ start: DocPosition;
196
+ end: DocPosition;
197
+ }
198
+
199
+ export type SdtType = "richText" | "plainText" | "checkbox" | "dropDown" | "comboBox" | "date";
200
+
201
+ export interface SdtProps {
202
+ type: SdtType;
203
+ alias?: string;
204
+ tag?: string;
205
+ placeholder?: boolean;
206
+ listItems?: { display: string; value: string }[];
207
+ dateFormat?: string;
208
+ checked?: boolean;
209
+ lockContent?: boolean;
210
+ lockControl?: boolean;
211
+ }
212
+
213
+ export interface Document {
214
+ section: SectionProps;
215
+ blocks: Block[];
216
+ stylesheet?: Stylesheet;
217
+ /** List definitions keyed by id. */
218
+ lists?: Record<string, ListDefinition>;
219
+ /** Footnote bodies keyed by ref id. */
220
+ footnotes?: Record<string, Paragraph[]>;
221
+ /** Content-control properties keyed by sdtId. */
222
+ sdts?: Record<string, SdtProps>;
223
+ /** Bookmark name → character range. */
224
+ bookmarks?: Record<string, BookmarkRange>;
225
+ }
@@ -2,6 +2,10 @@
2
2
  // Hand-written so the published types stay small, self-contained, and stable
3
3
  // regardless of internal refactors — no dependency on internal workspace types.
4
4
 
5
+ import type { Document } from "./model";
6
+
7
+ export type { Document } from "./model";
8
+
5
9
  /** Caller-supplied identity, used for change attribution and presence. */
6
10
  export interface UserInfo {
7
11
  id: string;
@@ -43,6 +47,12 @@ export interface WordCanvasEventMap {
43
47
 
44
48
  /** Handle resolved once the editor is mounted (via `whenReady()`). */
45
49
  export interface EditorHandle {
50
+ /** Snapshot of the current document (plain data). */
51
+ getDocument(): Document;
52
+ /** Replace the open document with a programmatically-built one (e.g. a
53
+ * DocumentBuilder result). Cloned; drops undo history and any live collab
54
+ * session; preserves zoom + scroll. */
55
+ setDocument(doc: Document): void;
46
56
  /** Open a .docx (auto-publishes when online); resolves when loaded. */
47
57
  openDocx(file: File | ArrayBuffer): Promise<void>;
48
58
  /** Publish the current document and resolve its shareable link (online only). */
@@ -62,6 +72,12 @@ export declare class WordCanvas {
62
72
  whenReady(): Promise<EditorHandle>;
63
73
  /** Open a .docx. When online, auto-publishes it and surfaces a share link. */
64
74
  openDocx(file: File | ArrayBuffer): Promise<void>;
75
+ /** Replace the open document with a programmatically-built one (e.g. a
76
+ * DocumentBuilder result). The input is cloned. Like openDocx, this starts a
77
+ * NEW document: undo history and any live collab session are dropped (the
78
+ * next share() forks). Zoom and scroll are preserved, so calling this on
79
+ * every data change gives a stable live preview. */
80
+ setDocument(doc: Document): Promise<void>;
65
81
  /** Publish the current document and resolve its shareable link (online only). */
66
82
  share(): Promise<string>;
67
83
  getDocId(): string | null;