@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.
- package/dist/docx/paragraphParser.js +6 -4
- package/dist/docx/parser.js +6 -2
- package/dist/docx/rezip.d.ts +9 -1
- package/dist/docx/rezip.js +108 -41
- package/dist/docx/selectiveSave.js +6 -0
- package/dist/docx/selectiveXmlPatch.d.ts +19 -1
- package/dist/docx/selectiveXmlPatch.js +72 -1
- package/dist/docx/serializer/paragraphSerializer.js +1 -0
- package/dist/docx/serializer/settingsSerializer.js +1 -0
- package/dist/docx/serializer/stylesSerializer.d.ts +2 -1
- package/dist/docx/serializer/stylesSerializer.js +1 -1
- package/dist/docx/server/build.d.ts +103 -0
- package/dist/docx/server/build.js +265 -0
- package/dist/docx/server/createBilingualDocument.d.ts +62 -0
- package/dist/docx/server/createBilingualDocument.js +511 -0
- package/dist/docx/server/createBilingualDocx.d.ts +19 -0
- package/dist/docx/server/createBilingualDocx.js +26 -0
- package/dist/docx/settingsParser.js +8 -5
- package/dist/docx/xmlParser.d.ts +9 -1
- package/dist/docx/xmlParser.js +13 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.js +5 -1
- package/dist/style-sets/stellaStyle.js +146 -0
- package/dist/utils/createDocument.js +1 -0
- package/package.json +2 -2
|
@@ -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 };
|