@stll/folio-core 0.20.0 → 0.22.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.
@@ -0,0 +1,265 @@
1
+ import { TaggedError } from "better-result";
2
+ //#region src/docx/server/build.ts
3
+ /**
4
+ * Headless document builders.
5
+ *
6
+ * Thin constructors for the `@stll/docx-core` model so a server can assemble
7
+ * a report in code (`createEmptyDocument` → push blocks → `createDocx`)
8
+ * without hand-writing model literals. They build model values only; the
9
+ * serializer owns the OOXML.
10
+ */
11
+ /** Character style applied to hyperlink text; present in the bundled style sets. */
12
+ const HYPERLINK_STYLE_ID = "Hyperlink";
13
+ /** Character style applied to the endnote reference mark. */
14
+ const ENDNOTE_REFERENCE_STYLE_ID = "EndnoteReference";
15
+ /** Paragraph style applied to endnote body paragraphs. */
16
+ const ENDNOTE_TEXT_STYLE_ID = "EndnoteText";
17
+ /** Table style used by `table()`; present in the bundled style sets. */
18
+ const TABLE_STYLE_ID = "TableGrid";
19
+ /** `w:tblW w:type="pct"` is in fiftieths of a percent: 5000 = 100%. */
20
+ const FULL_WIDTH_PCT = 5e3;
21
+ const HEADING_LEVELS = [
22
+ 1,
23
+ 2,
24
+ 3,
25
+ 4,
26
+ 5,
27
+ 6
28
+ ];
29
+ /** Outline levels a `TOC \o` switch may name (ECMA-376 `w:outlineLvl` 0-8). */
30
+ const TOC_LEVEL_MIN = 1;
31
+ const TOC_LEVEL_MAX = 9;
32
+ /**
33
+ * A builder received a value outside the model's domain (a zero `gridSpan`,
34
+ * a negative column width, a reversed TOC range). Thrown at the boundary so
35
+ * the invalid value never reaches the serializer.
36
+ */
37
+ var InvalidFolioReportBuilderOptionsError = class extends TaggedError("InvalidFolioReportBuilderOptionsError")() {};
38
+ const assertPositiveInteger = (value, path) => {
39
+ if (!Number.isInteger(value) || value < 1) throw new InvalidFolioReportBuilderOptionsError({
40
+ message: `${path} must be a positive integer, got ${String(value)}`,
41
+ path
42
+ });
43
+ };
44
+ const assertHeadingLevel = (level) => {
45
+ if (!HEADING_LEVELS.some((known) => known === level)) throw new InvalidFolioReportBuilderOptionsError({
46
+ message: `level must be one of ${HEADING_LEVELS.join(", ")}, got ${String(level)}`,
47
+ path: "level"
48
+ });
49
+ };
50
+ const assertTocLevels = ({ from, to }) => {
51
+ const inRange = (value) => Number.isInteger(value) && value >= TOC_LEVEL_MIN && value <= TOC_LEVEL_MAX;
52
+ if (!inRange(from) || !inRange(to) || from > to) throw new InvalidFolioReportBuilderOptionsError({
53
+ message: `levels must satisfy ${TOC_LEVEL_MIN} <= from <= to <= ${TOC_LEVEL_MAX}, got ${String(from)}-${String(to)}`,
54
+ path: "levels"
55
+ });
56
+ };
57
+ const run = (text, formatting) => ({
58
+ type: "run",
59
+ ...formatting ? { formatting } : {},
60
+ content: [{
61
+ type: "text",
62
+ text
63
+ }]
64
+ });
65
+ const paragraph = (content, formatting) => ({
66
+ type: "paragraph",
67
+ ...formatting ? { formatting } : {},
68
+ content: typeof content === "string" ? [run(content)] : content
69
+ });
70
+ /** A paragraph in the `Heading<level>` style. */
71
+ const heading = ({ text, level }) => {
72
+ assertHeadingLevel(level);
73
+ return paragraph(text, { styleId: `Heading${level}` });
74
+ };
75
+ /** An empty paragraph carrying a hard page break. */
76
+ const pageBreak = () => ({
77
+ type: "paragraph",
78
+ content: [{
79
+ type: "run",
80
+ content: [{
81
+ type: "break",
82
+ breakType: "page"
83
+ }]
84
+ }]
85
+ });
86
+ /**
87
+ * An external (`href`) or in-document (`anchor`, a bookmark name) link. The
88
+ * relationship for an external link is minted when the document is written.
89
+ */
90
+ const hyperlink = ({ text, formatting, tooltip, href, anchor }) => ({
91
+ type: "hyperlink",
92
+ ...href !== void 0 ? { href } : { anchor },
93
+ ...tooltip !== void 0 ? { tooltip } : {},
94
+ children: [run(text, {
95
+ styleId: HYPERLINK_STYLE_ID,
96
+ ...formatting
97
+ })]
98
+ });
99
+ let nextBookmarkId = 0;
100
+ /** `content` wrapped in a named bookmark, the target of `hyperlink({ anchor })`. */
101
+ const bookmark = ({ name, content, id }) => {
102
+ const bookmarkId = id ?? nextBookmarkId++;
103
+ const start = {
104
+ type: "bookmarkStart",
105
+ id: bookmarkId,
106
+ name
107
+ };
108
+ const end = {
109
+ type: "bookmarkEnd",
110
+ id: bookmarkId
111
+ };
112
+ return [
113
+ start,
114
+ ...content,
115
+ end
116
+ ];
117
+ };
118
+ const cellWidth = (columnWidths, gridIndex, gridSpan) => {
119
+ if (!columnWidths) return;
120
+ let width = 0;
121
+ for (let column = gridIndex; column < gridIndex + gridSpan; column++) {
122
+ const columnWidth = columnWidths.at(column);
123
+ if (columnWidth === void 0) return;
124
+ width += columnWidth;
125
+ }
126
+ return width;
127
+ };
128
+ const buildCell = ({ spec, columnWidths, gridIndex, shading, textFormatting }) => {
129
+ const resolved = typeof spec === "string" ? {
130
+ content: [paragraph([run(spec, textFormatting)])],
131
+ shading
132
+ } : {
133
+ ...spec,
134
+ shading: spec.shading ?? shading
135
+ };
136
+ const gridSpan = resolved.gridSpan ?? 1;
137
+ assertPositiveInteger(gridSpan, "gridSpan");
138
+ const width = cellWidth(columnWidths, gridIndex, gridSpan);
139
+ const formatting = {
140
+ ...width !== void 0 ? { width: {
141
+ type: "dxa",
142
+ value: width
143
+ } } : {},
144
+ ...resolved.shading ? { shading: resolved.shading } : {},
145
+ ...resolved.gridSpan !== void 0 ? { gridSpan: resolved.gridSpan } : {},
146
+ ...resolved.vMerge !== void 0 ? { vMerge: resolved.vMerge } : {}
147
+ };
148
+ return {
149
+ type: "tableCell",
150
+ ...Object.keys(formatting).length > 0 ? { formatting } : {},
151
+ content: resolved.content.length > 0 ? resolved.content : [paragraph([])]
152
+ };
153
+ };
154
+ const buildRow = ({ cells, columnWidths, shading, textFormatting, header }) => {
155
+ const built = [];
156
+ let gridIndex = 0;
157
+ for (const spec of cells) {
158
+ const cell = buildCell({
159
+ spec,
160
+ columnWidths,
161
+ gridIndex,
162
+ shading,
163
+ textFormatting
164
+ });
165
+ built.push(cell);
166
+ gridIndex += cell.formatting?.gridSpan ?? 1;
167
+ }
168
+ return {
169
+ type: "tableRow",
170
+ ...header ? { formatting: {
171
+ header: true,
172
+ cantSplit: true
173
+ } } : {},
174
+ cells: built
175
+ };
176
+ };
177
+ /**
178
+ * A full-width grid table in the `TableGrid` style. A string cell becomes one
179
+ * plain paragraph; an object cell supplies its own paragraphs plus optional
180
+ * shading and horizontal (`gridSpan`) or vertical (`vMerge`) merge.
181
+ */
182
+ const table = ({ header, rows, columnWidths, headerShading, repeatHeader = true }) => {
183
+ columnWidths?.forEach((width, index) => assertPositiveInteger(width, `columnWidths[${index}]`));
184
+ const builtRows = [];
185
+ if (header) builtRows.push(buildRow({
186
+ cells: header,
187
+ columnWidths,
188
+ shading: headerShading,
189
+ textFormatting: { bold: true },
190
+ header: repeatHeader
191
+ }));
192
+ for (const cells of rows) builtRows.push(buildRow({
193
+ cells,
194
+ columnWidths,
195
+ shading: void 0,
196
+ textFormatting: void 0,
197
+ header: false
198
+ }));
199
+ return {
200
+ type: "table",
201
+ formatting: {
202
+ styleId: TABLE_STYLE_ID,
203
+ width: {
204
+ type: "pct",
205
+ value: FULL_WIDTH_PCT
206
+ },
207
+ layout: columnWidths ? "fixed" : "autofit"
208
+ },
209
+ ...columnWidths ? { columnWidths } : {},
210
+ rows: builtRows
211
+ };
212
+ };
213
+ /**
214
+ * Register an endnote on `doc` and return the reference run to place in body
215
+ * text. Allocates the next free endnote id (Word reserves 0 and -1 for the
216
+ * separator notes) and pushes the note into `doc.package.endnotes`.
217
+ */
218
+ const endnote = (doc, content) => {
219
+ const endnotes = doc.package.endnotes ?? [];
220
+ doc.package.endnotes = endnotes;
221
+ const id = Math.max(0, ...endnotes.map((note) => note.id)) + 1;
222
+ const note = {
223
+ type: "endnote",
224
+ id,
225
+ content: typeof content === "string" ? [paragraph(content, { styleId: ENDNOTE_TEXT_STYLE_ID })] : content
226
+ };
227
+ endnotes.push(note);
228
+ return {
229
+ type: "run",
230
+ formatting: { styleId: ENDNOTE_REFERENCE_STYLE_ID },
231
+ content: [{
232
+ type: "endnoteRef",
233
+ id
234
+ }]
235
+ };
236
+ };
237
+ const DEFAULT_TOC_LEVELS = {
238
+ from: 1,
239
+ to: 3
240
+ };
241
+ const DEFAULT_TOC_PLACEHOLDER = "Update the field to build the table of contents.";
242
+ /**
243
+ * A paragraph holding a dirty `TOC` field, so the consumer recomputes the
244
+ * table on open. Set `package.settings.updateFields` as well to have Word
245
+ * recompute without prompting for each field.
246
+ */
247
+ const createTableOfContentsField = ({ levels = DEFAULT_TOC_LEVELS, hyperlinks = true, placeholderText = DEFAULT_TOC_PLACEHOLDER } = {}) => {
248
+ assertTocLevels(levels);
249
+ const switches = [`\\o "${levels.from}-${levels.to}"`];
250
+ if (hyperlinks) switches.push("\\h");
251
+ switches.push("\\z", "\\u");
252
+ return {
253
+ type: "paragraph",
254
+ content: [{
255
+ type: "complexField",
256
+ instruction: `TOC ${switches.join(" ")}`,
257
+ fieldType: "TOC",
258
+ fieldCode: [],
259
+ fieldResult: [run(placeholderText)],
260
+ dirty: true
261
+ }]
262
+ };
263
+ };
264
+ //#endregion
265
+ export { HEADING_LEVELS, InvalidFolioReportBuilderOptionsError, bookmark, createTableOfContentsField, endnote, heading, hyperlink, pageBreak, paragraph, run, table };
@@ -0,0 +1,62 @@
1
+ import { document_d_exports } from "../../types/document.js";
2
+ //#region src/docx/server/createBilingualDocument.d.ts
3
+ type BilingualRowKind = "paragraph" | "heading" | "listItem";
4
+ /** One translatable unit: a source paragraph and its right-column copy. */
5
+ type BilingualParagraphRef = {
6
+ /** `paraId` of the untouched source paragraph (left column). */
7
+ sourceParaId: string | undefined;
8
+ /** `paraId` minted for the right-column copy; stable across re-runs. */
9
+ targetParaId: string;
10
+ /** Plain text of the source paragraph. */
11
+ sourceText: string;
12
+ };
13
+ type BilingualRow = ({
14
+ kind: BilingualRowKind;
15
+ /** Equals `targetParaId`; the handle callers use to address the row. */
16
+ rowId: string;
17
+ } & BilingualParagraphRef) | {
18
+ kind: "table";
19
+ rowId: string;
20
+ /**
21
+ * Every paragraph inside the table, in document order. The table is not
22
+ * copied, so these are the paragraphs to translate in place.
23
+ */
24
+ paragraphs: BilingualTableParagraphRef[];
25
+ };
26
+ type BilingualTableParagraphRef = {
27
+ paraId: string | undefined;
28
+ sourceText: string;
29
+ };
30
+ type BilingualBorders = "none" | "grid";
31
+ type CreateBilingualDocumentOptions = {
32
+ /**
33
+ * Suffix for cloned style ids and names (for example `"en"` turns
34
+ * `Heading1` into `Heading1-en`). Must be a non-empty token of letters,
35
+ * digits, or `-`.
36
+ */
37
+ targetStyleSuffix: string;
38
+ /** Table borders; legal practice is usually `"none"`. Default `"none"`. */
39
+ borders?: BilingualBorders;
40
+ };
41
+ type CreateBilingualDocumentResult = {
42
+ document: document_d_exports.Document;
43
+ rows: BilingualRow[];
44
+ /** Non-fatal fidelity notes (for example an unresolvable numbering style link). */
45
+ warnings: string[];
46
+ };
47
+ declare const InvalidBilingualDocumentOptionsError_base: import("better-result").TaggedErrorClass<"InvalidBilingualDocumentOptionsError", {
48
+ message: string;
49
+ option: "targetStyleSuffix";
50
+ }>;
51
+ declare class InvalidBilingualDocumentOptionsError extends InvalidBilingualDocumentOptionsError_base {}
52
+ declare function createBilingualDocument(source: document_d_exports.Document, options: CreateBilingualDocumentOptions): CreateBilingualDocumentResult;
53
+ /**
54
+ * Re-derive the row manifest from a document produced by
55
+ * {@link createBilingualDocument}. Detection is structural: a top-level table
56
+ * whose rows are all either a left | right pair of single-paragraph cells or
57
+ * one cell spanning both columns. Rows are returned in document order; the
58
+ * right paragraph's `paraId` is the row handle, as at creation.
59
+ */
60
+ declare function readBilingualDocument(document: document_d_exports.Document): BilingualRow[];
61
+ //#endregion
62
+ export { BilingualBorders, BilingualParagraphRef, BilingualRow, BilingualRowKind, BilingualTableParagraphRef, CreateBilingualDocumentOptions, CreateBilingualDocumentResult, InvalidBilingualDocumentOptionsError, createBilingualDocument, readBilingualDocument };