doc-codec 0.0.0 → 1.0.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/dist/bytes.cjs +44 -0
  4. package/dist/bytes.d.cts +9 -0
  5. package/dist/bytes.d.ts +9 -0
  6. package/dist/bytes.js +38 -0
  7. package/dist/detect.cjs +22 -0
  8. package/dist/detect.d.cts +6 -0
  9. package/dist/detect.d.ts +6 -0
  10. package/dist/detect.js +20 -0
  11. package/dist/errors.cjs +17 -0
  12. package/dist/errors.d.cts +9 -0
  13. package/dist/errors.d.ts +9 -0
  14. package/dist/errors.js +15 -0
  15. package/dist/fib/fib.cjs +50 -0
  16. package/dist/fib/fib.d.cts +29 -0
  17. package/dist/fib/fib.d.ts +29 -0
  18. package/dist/fib/fib.js +48 -0
  19. package/dist/fib/offsets.cjs +61 -0
  20. package/dist/fib/offsets.d.cts +47 -0
  21. package/dist/fib/offsets.d.ts +47 -0
  22. package/dist/fib/offsets.js +47 -0
  23. package/dist/index.cjs +79 -0
  24. package/dist/index.d.cts +16 -0
  25. package/dist/index.d.ts +16 -0
  26. package/dist/index.js +16 -0
  27. package/dist/plc.cjs +44 -0
  28. package/dist/plc.d.cts +12 -0
  29. package/dist/plc.d.ts +12 -0
  30. package/dist/plc.js +42 -0
  31. package/dist/prop/chp.cjs +172 -0
  32. package/dist/prop/chp.d.cts +16 -0
  33. package/dist/prop/chp.d.ts +16 -0
  34. package/dist/prop/chp.js +171 -0
  35. package/dist/prop/fkp.cjs +141 -0
  36. package/dist/prop/fkp.d.cts +33 -0
  37. package/dist/prop/fkp.d.ts +33 -0
  38. package/dist/prop/fkp.js +137 -0
  39. package/dist/prop/pap.cjs +134 -0
  40. package/dist/prop/pap.d.cts +25 -0
  41. package/dist/prop/pap.d.ts +25 -0
  42. package/dist/prop/pap.js +133 -0
  43. package/dist/prop/sprm.cjs +68 -0
  44. package/dist/prop/sprm.d.cts +26 -0
  45. package/dist/prop/sprm.d.ts +26 -0
  46. package/dist/prop/sprm.js +64 -0
  47. package/dist/read.cjs +171 -0
  48. package/dist/read.d.cts +12 -0
  49. package/dist/read.d.ts +12 -0
  50. package/dist/read.js +169 -0
  51. package/dist/style/stsh.cjs +65 -0
  52. package/dist/style/stsh.d.cts +29 -0
  53. package/dist/style/stsh.d.ts +29 -0
  54. package/dist/style/stsh.js +61 -0
  55. package/dist/text/characters.cjs +69 -0
  56. package/dist/text/characters.d.cts +14 -0
  57. package/dist/text/characters.d.ts +14 -0
  58. package/dist/text/characters.js +67 -0
  59. package/dist/text/piece-table.cjs +68 -0
  60. package/dist/text/piece-table.d.cts +28 -0
  61. package/dist/text/piece-table.d.ts +28 -0
  62. package/dist/text/piece-table.js +65 -0
  63. package/dist/text/special.cjs +48 -0
  64. package/dist/text/special.d.cts +30 -0
  65. package/dist/text/special.d.ts +30 -0
  66. package/dist/text/special.js +34 -0
  67. package/package.json +93 -2
package/dist/read.js ADDED
@@ -0,0 +1,169 @@
1
+ import { DocFormatError } from "./errors.js";
2
+ import { slice } from "./bytes.js";
3
+ import { WORD_DOCUMENT_STREAM } from "./detect.js";
4
+ import { parseFib, tableStreamName } from "./fib/fib.js";
5
+ import { parseClx } from "./text/piece-table.js";
6
+ import { readTextRange } from "./text/characters.js";
7
+ import { endsParagraph, isAnchorOnly } from "./text/special.js";
8
+ import { readGrpprl } from "./prop/sprm.js";
9
+ import { PropertyBinTable } from "./prop/fkp.js";
10
+ import { applyCharacterSprms } from "./prop/chp.js";
11
+ import { applyParagraphSprms } from "./prop/pap.js";
12
+ import { headingLevelFromIstd, parseStsh } from "./style/stsh.js";
13
+ import { readCompoundFile } from "archive-codec";
14
+ //#region src/read.ts
15
+ /** The page geometry every section is given, because this reader does not yet read a document's own. US Letter with one-inch margins is Word's own default for a new document; a document that states otherwise is not yet consulted, so this is a placeholder the schema requires rather than a fact read from the file. */
16
+ const DEFAULT_PAGE_SIZE = {
17
+ widthPt: 612,
18
+ heightPt: 792
19
+ };
20
+ const DEFAULT_MARGINS = {
21
+ topPt: 72,
22
+ rightPt: 72,
23
+ bottomPt: 72,
24
+ leftPt: 72
25
+ };
26
+ function readDocStreams(bytes) {
27
+ const streams = readCompoundFile(bytes);
28
+ const wordDocument = streams.find((stream) => stream.path === WORD_DOCUMENT_STREAM);
29
+ if (wordDocument === void 0) throw new DocFormatError(`this compound file has no "${WORD_DOCUMENT_STREAM}" stream, so it is not a Word Binary File (it holds: ${streams.map((stream) => stream.path).join(", ")})`);
30
+ const fib = parseFib(wordDocument.bytes);
31
+ const wanted = tableStreamName(fib);
32
+ const table = streams.find((stream) => stream.path === wanted);
33
+ if (table === void 0) throw new DocFormatError(`FibBase.fWhichTblStm selects the "${wanted}" stream, which this compound file does not contain`);
34
+ return {
35
+ wordDocument: wordDocument.bytes,
36
+ table: table.bytes,
37
+ fib
38
+ };
39
+ }
40
+ function readDocContent(bytes) {
41
+ const { wordDocument, table, fib } = readDocStreams(bytes);
42
+ const pieceTable = parseClx(slice(table, fib.fcClx, fib.lcbClx, "Clx in the Table stream"));
43
+ const styles = fib.lcbStshf > 0 ? parseStsh(slice(table, fib.fcStshf, fib.lcbStshf, "STSH in the Table stream")) : void 0;
44
+ const chpxTable = new PropertyBinTable(wordDocument, slice(table, fib.fcPlcfBteChpx, fib.lcbPlcfBteChpx, "PlcBteChpx in the Table stream"), "PlcBteChpx");
45
+ const papxTable = new PropertyBinTable(wordDocument, slice(table, fib.fcPlcfBtePapx, fib.lcbPlcfBtePapx, "PlcBtePapx in the Table stream"), "PlcBtePapx");
46
+ const range = readTextRange(wordDocument, pieceTable, 0, fib.ccpText);
47
+ const blocks = readParagraphs(range.text, range.fcs, {
48
+ chpxTable,
49
+ papxTable,
50
+ styles,
51
+ characterProperties: /* @__PURE__ */ new Map()
52
+ });
53
+ return {
54
+ kind: "wordprocessing",
55
+ metadata: {},
56
+ sections: [{
57
+ pageSize: DEFAULT_PAGE_SIZE,
58
+ margins: DEFAULT_MARGINS,
59
+ blocks
60
+ }]
61
+ };
62
+ }
63
+ function readParagraphs(text, fcs, context) {
64
+ const blocks = [];
65
+ let start = 0;
66
+ for (let index = 0; index < text.length; index += 1) {
67
+ const code = text.charCodeAt(index);
68
+ if (!endsParagraph(code)) continue;
69
+ const markFc = fcs[index];
70
+ if (markFc === void 0) throw new DocFormatError(`character ${index} has no byte offset, so its paragraph's properties cannot be located`);
71
+ blocks.push(buildParagraph(text.slice(start, index), fcs.slice(start, index), markFc, context));
72
+ start = index + 1;
73
+ }
74
+ if (start < text.length) {
75
+ const firstFc = fcs[start];
76
+ if (firstFc === void 0) throw new DocFormatError(`character ${start} has no byte offset, so the trailing paragraph's properties cannot be located`);
77
+ blocks.push(buildParagraph(text.slice(start), fcs.slice(start), firstFc, context));
78
+ }
79
+ return blocks;
80
+ }
81
+ function buildParagraph(text, fcs, propertyFc, context) {
82
+ const papx = context.papxTable.papx(propertyFc);
83
+ const properties = {};
84
+ if (papx !== void 0) {
85
+ properties.istd = papx.istd;
86
+ applyParagraphSprms(readGrpprl(papx.grpprl), properties);
87
+ }
88
+ return {
89
+ kind: "paragraph",
90
+ runs: buildRuns(text, fcs, context),
91
+ ...paragraphAttributes(properties, context)
92
+ };
93
+ }
94
+ function paragraphAttributes(properties, context) {
95
+ const attributes = {};
96
+ const istd = properties.istd;
97
+ if (istd !== void 0) {
98
+ const style = context.styles?.styles[istd];
99
+ if (style !== void 0 && style.name !== "") attributes.styleId = style.name;
100
+ const headingLevel = headingLevelFromIstd(istd);
101
+ if (headingLevel !== void 0) attributes.headingLevel = headingLevel;
102
+ }
103
+ if (attributes.headingLevel === void 0 && properties.outlineLevel !== void 0) attributes.headingLevel = properties.outlineLevel + 1;
104
+ if (properties.alignment !== void 0) attributes.alignment = properties.alignment;
105
+ if (properties.spacingBeforePt !== void 0) attributes.spacingBeforePt = properties.spacingBeforePt;
106
+ if (properties.spacingAfterPt !== void 0) attributes.spacingAfterPt = properties.spacingAfterPt;
107
+ if (properties.lineSpacing !== void 0) attributes.lineSpacing = properties.lineSpacing;
108
+ if (properties.indentLeftPt !== void 0) attributes.indentLeftPt = properties.indentLeftPt;
109
+ if (properties.indentFirstLinePt !== void 0) attributes.indentFirstLinePt = properties.indentFirstLinePt;
110
+ if (properties.pageBreakBefore === true) attributes.pageBreakBefore = true;
111
+ if (properties.listId !== void 0) attributes.list = {
112
+ numId: String(properties.listId),
113
+ level: properties.listLevel ?? 0
114
+ };
115
+ return attributes;
116
+ }
117
+ function buildRuns(text, fcs, context) {
118
+ const runs = [];
119
+ let currentKey;
120
+ let currentText = "";
121
+ let currentProperties = {};
122
+ const enclosingInstruction = [];
123
+ let inInstruction = false;
124
+ const flush = () => {
125
+ if (currentText !== "") runs.push({
126
+ text: currentText,
127
+ ...currentProperties
128
+ });
129
+ currentText = "";
130
+ };
131
+ for (let index = 0; index < text.length; index += 1) {
132
+ const code = text.charCodeAt(index);
133
+ if (code === 19) {
134
+ flush();
135
+ enclosingInstruction.push(inInstruction);
136
+ inInstruction = true;
137
+ continue;
138
+ }
139
+ if (code === 20) {
140
+ inInstruction = false;
141
+ continue;
142
+ }
143
+ if (code === 21) {
144
+ inInstruction = enclosingInstruction.pop() ?? inInstruction;
145
+ continue;
146
+ }
147
+ if (inInstruction || isAnchorOnly(code)) continue;
148
+ const fc = fcs[index];
149
+ if (fc === void 0) throw new DocFormatError(`character ${index} of a paragraph has no byte offset, so its formatting cannot be located`);
150
+ const grpprl = context.chpxTable.chpxGrpprl(fc);
151
+ const key = grpprl === void 0 ? "none" : `${grpprl.byteOffset}:${grpprl.byteLength}`;
152
+ if (key !== currentKey) {
153
+ flush();
154
+ currentKey = key;
155
+ let properties = context.characterProperties.get(key);
156
+ if (properties === void 0) {
157
+ properties = {};
158
+ if (grpprl !== void 0) applyCharacterSprms(readGrpprl(grpprl), properties);
159
+ context.characterProperties.set(key, properties);
160
+ }
161
+ currentProperties = properties;
162
+ }
163
+ currentText += String.fromCharCode(code === 11 ? 10 : code);
164
+ }
165
+ flush();
166
+ return runs;
167
+ }
168
+ //#endregion
169
+ export { readDocContent, readDocStreams };
@@ -0,0 +1,65 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_errors = require("../errors.cjs");
3
+ const require_bytes = require("../bytes.cjs");
4
+ //#region src/style/stsh.ts
5
+ /** Stshif is a fixed 18 bytes: cstd, cbSTDBaseInFile, a bit field, stiMaxWhenSaved, istdMaxFixedWhenSaved, nVerBuiltInNamesWhenSaved, and the three default font indexes. */
6
+ const STSHIF_SIZE = 18;
7
+ /** Stshif.cbSTDBaseInFile "MUST be 0x000A when the Stdf structure does not contain an StdfPost2000 structure and MUST be 0x0012 when [it] does". */
8
+ const STDF_SIZE_WITHOUT_POST_2000 = 10;
9
+ const STDF_SIZE_WITH_POST_2000 = 18;
10
+ /** StdfBase.sti's "0x0FFE for user-defined styles"; anything else is an application-defined style whose sti identifies it. */
11
+ const STI_USER_DEFINED = 4094;
12
+ /** StdfBase.stk, [MS-DOC] 2.9.269 -- which of ECMA-376's ST_StyleType values this style is. */
13
+ const STK = {
14
+ paragraph: 1,
15
+ character: 2,
16
+ table: 3,
17
+ numbering: 4
18
+ };
19
+ /** StdfBase.istdBase's "0x0FFF if this style does not inherit from any other style". */
20
+ const ISTD_BASE_NONE = 4095;
21
+ function parseStsh(stsh) {
22
+ const cbStshi = require_bytes.readUint16LE(stsh, 0);
23
+ const stshi = require_bytes.slice(stsh, 2, cbStshi, "STSH.lpstshi.stshi");
24
+ if (stshi.length < STSHIF_SIZE) throw new require_errors.DocFormatError(`STSHI is ${stshi.length} bytes, shorter than the fixed ${STSHIF_SIZE}-byte Stshif it must begin with`);
25
+ const cstd = require_bytes.readUint16LE(stshi, 0);
26
+ const cbStdBaseInFile = require_bytes.readUint16LE(stshi, 2);
27
+ if (cbStdBaseInFile !== STDF_SIZE_WITHOUT_POST_2000 && cbStdBaseInFile !== STDF_SIZE_WITH_POST_2000) throw new require_errors.DocFormatError(`Stshif.cbSTDBaseInFile is 0x${cbStdBaseInFile.toString(16)}, neither of the two sizes [MS-DOC] permits for an Stdf (0x000A without an StdfPost2000, 0x0012 with one)`);
28
+ const styles = [];
29
+ let cursor = 2 + cbStshi;
30
+ for (let istd = 0; istd < cstd; istd += 1) {
31
+ const cbStd = require_bytes.readInt16LE(stsh, cursor);
32
+ if (cbStd < 0) throw new require_errors.DocFormatError(`LPStd for istd ${istd} declares cbStd ${cbStd}; [MS-DOC] requires it not to be less than 0`);
33
+ const std = require_bytes.slice(stsh, cursor + 2, cbStd, `STD for istd ${istd}`);
34
+ styles.push(cbStd === 0 ? void 0 : parseStd(std, istd, cbStdBaseInFile));
35
+ cursor += 2 + cbStd + cbStd % 2;
36
+ }
37
+ return { styles };
38
+ }
39
+ function parseStd(std, istd, cbStdBaseInFile) {
40
+ const word0 = require_bytes.readUint16LE(std, 0);
41
+ const word1 = require_bytes.readUint16LE(std, 2);
42
+ const istdBase = word1 >> 4 & 4095;
43
+ return {
44
+ istd,
45
+ sti: word0 & 4095,
46
+ stk: word1 & 15,
47
+ istdBase: istdBase === ISTD_BASE_NONE ? void 0 : istdBase,
48
+ name: readXstz(std, cbStdBaseInFile, `name of the style at istd ${istd}`)
49
+ };
50
+ }
51
+ function readXstz(bytes, offset, what) {
52
+ const cch = require_bytes.readUint16LE(bytes, offset);
53
+ const chars = require_bytes.slice(bytes, offset + 2, cch * 2, what);
54
+ let out = "";
55
+ for (let index = 0; index < cch; index += 1) out += String.fromCharCode(require_bytes.readUint16LE(chars, index * 2));
56
+ return out;
57
+ }
58
+ function headingLevelFromIstd(istd) {
59
+ return istd >= 1 && istd <= 9 ? istd : void 0;
60
+ }
61
+ //#endregion
62
+ exports.STI_USER_DEFINED = STI_USER_DEFINED;
63
+ exports.STK = STK;
64
+ exports.headingLevelFromIstd = headingLevelFromIstd;
65
+ exports.parseStsh = parseStsh;
@@ -0,0 +1,29 @@
1
+ //#region src/style/stsh.d.ts
2
+ /** StdfBase.sti's "0x0FFE for user-defined styles"; anything else is an application-defined style whose sti identifies it. */
3
+ declare const STI_USER_DEFINED = 4094;
4
+ /** StdfBase.stk, [MS-DOC] 2.9.269 -- which of ECMA-376's ST_StyleType values this style is. */
5
+ declare const STK: {
6
+ readonly paragraph: 1;
7
+ readonly character: 2;
8
+ readonly table: 3;
9
+ readonly numbering: 4;
10
+ };
11
+ interface Style {
12
+ /** The style's index in STSH.rglpstd, which is what sprmPIstd and sprmCIstd name. */
13
+ readonly istd: number;
14
+ /** The invariant application-defined style identifier, or STI_USER_DEFINED for a style the document itself defines. */
15
+ readonly sti: number;
16
+ readonly stk: number;
17
+ /** The istd this style inherits from, or undefined when StdfBase.istdBase is 0x0FFF ("this style does not inherit from any other style"). */
18
+ readonly istdBase: number | undefined;
19
+ /** The style's primary name, from the STD's own Xstz. */
20
+ readonly name: string;
21
+ }
22
+ interface StyleSheet {
23
+ /** Indexed by istd; a hole is a slot [MS-DOC] permits to be empty ("A style definition can be empty, in which case cbStd MUST be 0"). */
24
+ readonly styles: readonly (Style | undefined)[];
25
+ }
26
+ declare function parseStsh(stsh: Uint8Array): StyleSheet;
27
+ declare function headingLevelFromIstd(istd: number): number | undefined;
28
+ //#endregion
29
+ export { STI_USER_DEFINED, STK, Style, StyleSheet, headingLevelFromIstd, parseStsh };
@@ -0,0 +1,29 @@
1
+ //#region src/style/stsh.d.ts
2
+ /** StdfBase.sti's "0x0FFE for user-defined styles"; anything else is an application-defined style whose sti identifies it. */
3
+ declare const STI_USER_DEFINED = 4094;
4
+ /** StdfBase.stk, [MS-DOC] 2.9.269 -- which of ECMA-376's ST_StyleType values this style is. */
5
+ declare const STK: {
6
+ readonly paragraph: 1;
7
+ readonly character: 2;
8
+ readonly table: 3;
9
+ readonly numbering: 4;
10
+ };
11
+ interface Style {
12
+ /** The style's index in STSH.rglpstd, which is what sprmPIstd and sprmCIstd name. */
13
+ readonly istd: number;
14
+ /** The invariant application-defined style identifier, or STI_USER_DEFINED for a style the document itself defines. */
15
+ readonly sti: number;
16
+ readonly stk: number;
17
+ /** The istd this style inherits from, or undefined when StdfBase.istdBase is 0x0FFF ("this style does not inherit from any other style"). */
18
+ readonly istdBase: number | undefined;
19
+ /** The style's primary name, from the STD's own Xstz. */
20
+ readonly name: string;
21
+ }
22
+ interface StyleSheet {
23
+ /** Indexed by istd; a hole is a slot [MS-DOC] permits to be empty ("A style definition can be empty, in which case cbStd MUST be 0"). */
24
+ readonly styles: readonly (Style | undefined)[];
25
+ }
26
+ declare function parseStsh(stsh: Uint8Array): StyleSheet;
27
+ declare function headingLevelFromIstd(istd: number): number | undefined;
28
+ //#endregion
29
+ export { STI_USER_DEFINED, STK, Style, StyleSheet, headingLevelFromIstd, parseStsh };
@@ -0,0 +1,61 @@
1
+ import { DocFormatError } from "../errors.js";
2
+ import { readInt16LE, readUint16LE, slice } from "../bytes.js";
3
+ //#region src/style/stsh.ts
4
+ /** Stshif is a fixed 18 bytes: cstd, cbSTDBaseInFile, a bit field, stiMaxWhenSaved, istdMaxFixedWhenSaved, nVerBuiltInNamesWhenSaved, and the three default font indexes. */
5
+ const STSHIF_SIZE = 18;
6
+ /** Stshif.cbSTDBaseInFile "MUST be 0x000A when the Stdf structure does not contain an StdfPost2000 structure and MUST be 0x0012 when [it] does". */
7
+ const STDF_SIZE_WITHOUT_POST_2000 = 10;
8
+ const STDF_SIZE_WITH_POST_2000 = 18;
9
+ /** StdfBase.sti's "0x0FFE for user-defined styles"; anything else is an application-defined style whose sti identifies it. */
10
+ const STI_USER_DEFINED = 4094;
11
+ /** StdfBase.stk, [MS-DOC] 2.9.269 -- which of ECMA-376's ST_StyleType values this style is. */
12
+ const STK = {
13
+ paragraph: 1,
14
+ character: 2,
15
+ table: 3,
16
+ numbering: 4
17
+ };
18
+ /** StdfBase.istdBase's "0x0FFF if this style does not inherit from any other style". */
19
+ const ISTD_BASE_NONE = 4095;
20
+ function parseStsh(stsh) {
21
+ const cbStshi = readUint16LE(stsh, 0);
22
+ const stshi = slice(stsh, 2, cbStshi, "STSH.lpstshi.stshi");
23
+ if (stshi.length < STSHIF_SIZE) throw new DocFormatError(`STSHI is ${stshi.length} bytes, shorter than the fixed ${STSHIF_SIZE}-byte Stshif it must begin with`);
24
+ const cstd = readUint16LE(stshi, 0);
25
+ const cbStdBaseInFile = readUint16LE(stshi, 2);
26
+ if (cbStdBaseInFile !== STDF_SIZE_WITHOUT_POST_2000 && cbStdBaseInFile !== STDF_SIZE_WITH_POST_2000) throw new DocFormatError(`Stshif.cbSTDBaseInFile is 0x${cbStdBaseInFile.toString(16)}, neither of the two sizes [MS-DOC] permits for an Stdf (0x000A without an StdfPost2000, 0x0012 with one)`);
27
+ const styles = [];
28
+ let cursor = 2 + cbStshi;
29
+ for (let istd = 0; istd < cstd; istd += 1) {
30
+ const cbStd = readInt16LE(stsh, cursor);
31
+ if (cbStd < 0) throw new DocFormatError(`LPStd for istd ${istd} declares cbStd ${cbStd}; [MS-DOC] requires it not to be less than 0`);
32
+ const std = slice(stsh, cursor + 2, cbStd, `STD for istd ${istd}`);
33
+ styles.push(cbStd === 0 ? void 0 : parseStd(std, istd, cbStdBaseInFile));
34
+ cursor += 2 + cbStd + cbStd % 2;
35
+ }
36
+ return { styles };
37
+ }
38
+ function parseStd(std, istd, cbStdBaseInFile) {
39
+ const word0 = readUint16LE(std, 0);
40
+ const word1 = readUint16LE(std, 2);
41
+ const istdBase = word1 >> 4 & 4095;
42
+ return {
43
+ istd,
44
+ sti: word0 & 4095,
45
+ stk: word1 & 15,
46
+ istdBase: istdBase === ISTD_BASE_NONE ? void 0 : istdBase,
47
+ name: readXstz(std, cbStdBaseInFile, `name of the style at istd ${istd}`)
48
+ };
49
+ }
50
+ function readXstz(bytes, offset, what) {
51
+ const cch = readUint16LE(bytes, offset);
52
+ const chars = slice(bytes, offset + 2, cch * 2, what);
53
+ let out = "";
54
+ for (let index = 0; index < cch; index += 1) out += String.fromCharCode(readUint16LE(chars, index * 2));
55
+ return out;
56
+ }
57
+ function headingLevelFromIstd(istd) {
58
+ return istd >= 1 && istd <= 9 ? istd : void 0;
59
+ }
60
+ //#endregion
61
+ export { STI_USER_DEFINED, STK, headingLevelFromIstd, parseStsh };
@@ -0,0 +1,69 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_errors = require("../errors.cjs");
3
+ const require_bytes = require("../bytes.cjs");
4
+ const require_plc = require("../plc.cjs");
5
+ const require_text_piece_table = require("./piece-table.cjs");
6
+ //#region src/text/characters.ts
7
+ const COMPRESSED_CHARACTER_MAP = /* @__PURE__ */ new Map([
8
+ [130, 8218],
9
+ [131, 402],
10
+ [132, 8222],
11
+ [133, 8230],
12
+ [134, 8224],
13
+ [135, 8225],
14
+ [136, 710],
15
+ [137, 8240],
16
+ [138, 352],
17
+ [139, 8249],
18
+ [140, 338],
19
+ [145, 8216],
20
+ [146, 8217],
21
+ [147, 8220],
22
+ [148, 8221],
23
+ [149, 8226],
24
+ [150, 8211],
25
+ [151, 8212],
26
+ [152, 732],
27
+ [153, 8482],
28
+ [154, 353],
29
+ [155, 8250],
30
+ [156, 339],
31
+ [159, 376]
32
+ ]);
33
+ function readTextRange(wordDocument, table, cpStart, cpEnd) {
34
+ if (!Number.isInteger(cpStart) || !Number.isInteger(cpEnd) || cpStart < 0) throw new require_errors.DocFormatError(`text range [${cpStart}, ${cpEnd}) is not a pair of non-negative integer character positions`);
35
+ if (cpEnd < cpStart) throw new require_errors.DocFormatError(`text range [${cpStart}, ${cpEnd}) ends before it begins`);
36
+ if (cpEnd > table.lastCp) throw new require_errors.DocFormatError(`text range [${cpStart}, ${cpEnd}) extends past character position ${table.lastCp}, the last the piece table defines`);
37
+ const codeUnits = [];
38
+ const fcs = [];
39
+ let cp = cpStart;
40
+ while (cp < cpEnd) {
41
+ const index = require_plc.findLargestAtMost(table.cpKeys, cp);
42
+ if (index === void 0) throw new require_errors.DocFormatError(`character position ${cp} falls outside every piece in the piece table`);
43
+ const piece = table.pieces[index];
44
+ if (piece === void 0) throw new require_errors.DocFormatError(`piece ${index} is absent from a piece table of ${table.pieces.length} pieces`);
45
+ const stop = Math.min(cpEnd, piece.cpEnd);
46
+ for (; cp < stop; cp += 1) {
47
+ const fc = require_text_piece_table.characterOffset(piece, cp);
48
+ fcs.push(fc);
49
+ if (piece.compressed) {
50
+ const byte = require_bytes.readUint8(wordDocument, fc);
51
+ codeUnits.push(COMPRESSED_CHARACTER_MAP.get(byte) ?? byte);
52
+ } else codeUnits.push(require_bytes.readUint16LE(wordDocument, fc));
53
+ }
54
+ }
55
+ return {
56
+ text: fromCodeUnits(codeUnits),
57
+ fcs,
58
+ cpStart
59
+ };
60
+ }
61
+ const FROM_CHAR_CODE_CHUNK = 4096;
62
+ function fromCodeUnits(codeUnits) {
63
+ let out = "";
64
+ for (let start = 0; start < codeUnits.length; start += FROM_CHAR_CODE_CHUNK) out += String.fromCharCode(...codeUnits.slice(start, start + FROM_CHAR_CODE_CHUNK));
65
+ return out;
66
+ }
67
+ //#endregion
68
+ exports.COMPRESSED_CHARACTER_MAP = COMPRESSED_CHARACTER_MAP;
69
+ exports.readTextRange = readTextRange;
@@ -0,0 +1,14 @@
1
+ import { PieceTable } from "./piece-table.cjs";
2
+ //#region src/text/characters.d.ts
3
+ declare const COMPRESSED_CHARACTER_MAP: ReadonlyMap<number, number>;
4
+ interface TextRange {
5
+ /** The reconstructed characters, one UTF-16 code unit per character position. */
6
+ readonly text: string;
7
+ /** The WordDocument byte offset of each character, parallel to `text`. This is the key every formatting lookup is performed on. */
8
+ readonly fcs: readonly number[];
9
+ /** The character position `text[0]` and `fcs[0]` correspond to, so a caller can convert an index in this range back to a document-wide CP. */
10
+ readonly cpStart: number;
11
+ }
12
+ declare function readTextRange(wordDocument: Uint8Array, table: PieceTable, cpStart: number, cpEnd: number): TextRange;
13
+ //#endregion
14
+ export { COMPRESSED_CHARACTER_MAP, TextRange, readTextRange };
@@ -0,0 +1,14 @@
1
+ import { PieceTable } from "./piece-table.js";
2
+ //#region src/text/characters.d.ts
3
+ declare const COMPRESSED_CHARACTER_MAP: ReadonlyMap<number, number>;
4
+ interface TextRange {
5
+ /** The reconstructed characters, one UTF-16 code unit per character position. */
6
+ readonly text: string;
7
+ /** The WordDocument byte offset of each character, parallel to `text`. This is the key every formatting lookup is performed on. */
8
+ readonly fcs: readonly number[];
9
+ /** The character position `text[0]` and `fcs[0]` correspond to, so a caller can convert an index in this range back to a document-wide CP. */
10
+ readonly cpStart: number;
11
+ }
12
+ declare function readTextRange(wordDocument: Uint8Array, table: PieceTable, cpStart: number, cpEnd: number): TextRange;
13
+ //#endregion
14
+ export { COMPRESSED_CHARACTER_MAP, TextRange, readTextRange };
@@ -0,0 +1,67 @@
1
+ import { DocFormatError } from "../errors.js";
2
+ import { readUint16LE, readUint8 } from "../bytes.js";
3
+ import { findLargestAtMost } from "../plc.js";
4
+ import { characterOffset } from "./piece-table.js";
5
+ //#region src/text/characters.ts
6
+ const COMPRESSED_CHARACTER_MAP = /* @__PURE__ */ new Map([
7
+ [130, 8218],
8
+ [131, 402],
9
+ [132, 8222],
10
+ [133, 8230],
11
+ [134, 8224],
12
+ [135, 8225],
13
+ [136, 710],
14
+ [137, 8240],
15
+ [138, 352],
16
+ [139, 8249],
17
+ [140, 338],
18
+ [145, 8216],
19
+ [146, 8217],
20
+ [147, 8220],
21
+ [148, 8221],
22
+ [149, 8226],
23
+ [150, 8211],
24
+ [151, 8212],
25
+ [152, 732],
26
+ [153, 8482],
27
+ [154, 353],
28
+ [155, 8250],
29
+ [156, 339],
30
+ [159, 376]
31
+ ]);
32
+ function readTextRange(wordDocument, table, cpStart, cpEnd) {
33
+ if (!Number.isInteger(cpStart) || !Number.isInteger(cpEnd) || cpStart < 0) throw new DocFormatError(`text range [${cpStart}, ${cpEnd}) is not a pair of non-negative integer character positions`);
34
+ if (cpEnd < cpStart) throw new DocFormatError(`text range [${cpStart}, ${cpEnd}) ends before it begins`);
35
+ if (cpEnd > table.lastCp) throw new DocFormatError(`text range [${cpStart}, ${cpEnd}) extends past character position ${table.lastCp}, the last the piece table defines`);
36
+ const codeUnits = [];
37
+ const fcs = [];
38
+ let cp = cpStart;
39
+ while (cp < cpEnd) {
40
+ const index = findLargestAtMost(table.cpKeys, cp);
41
+ if (index === void 0) throw new DocFormatError(`character position ${cp} falls outside every piece in the piece table`);
42
+ const piece = table.pieces[index];
43
+ if (piece === void 0) throw new DocFormatError(`piece ${index} is absent from a piece table of ${table.pieces.length} pieces`);
44
+ const stop = Math.min(cpEnd, piece.cpEnd);
45
+ for (; cp < stop; cp += 1) {
46
+ const fc = characterOffset(piece, cp);
47
+ fcs.push(fc);
48
+ if (piece.compressed) {
49
+ const byte = readUint8(wordDocument, fc);
50
+ codeUnits.push(COMPRESSED_CHARACTER_MAP.get(byte) ?? byte);
51
+ } else codeUnits.push(readUint16LE(wordDocument, fc));
52
+ }
53
+ }
54
+ return {
55
+ text: fromCodeUnits(codeUnits),
56
+ fcs,
57
+ cpStart
58
+ };
59
+ }
60
+ const FROM_CHAR_CODE_CHUNK = 4096;
61
+ function fromCodeUnits(codeUnits) {
62
+ let out = "";
63
+ for (let start = 0; start < codeUnits.length; start += FROM_CHAR_CODE_CHUNK) out += String.fromCharCode(...codeUnits.slice(start, start + FROM_CHAR_CODE_CHUNK));
64
+ return out;
65
+ }
66
+ //#endregion
67
+ export { COMPRESSED_CHARACTER_MAP, readTextRange };
@@ -0,0 +1,68 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_errors = require("../errors.cjs");
3
+ const require_bytes = require("../bytes.cjs");
4
+ const require_plc = require("../plc.cjs");
5
+ //#region src/text/piece-table.ts
6
+ /** A Clx's leading Prc marker byte, [MS-DOC] 2.9.20: "This value MUST be 0x01." */
7
+ const CLXT_PRC = 1;
8
+ /** A Clx's Pcdt marker byte, [MS-DOC] 2.9.19: "This value MUST be 0x02." */
9
+ const CLXT_PCDT = 2;
10
+ /** Pcd is 8 bytes: a 2-byte bit field, a 4-byte FcCompressed, and a 2-byte Prm. */
11
+ const PCD_SIZE = 8;
12
+ /** FcCompressed's low 30 bits hold the offset; bit 30 is fCompressed and bit 31 a reserved bit the spec says MUST be zero and MUST be ignored. */
13
+ const FC_MASK = 1073741823;
14
+ const FC_COMPRESSED_BIT = 1073741824;
15
+ /** PrcData.cbGrpprl is a signed integer that "MUST be less than or equal to 0x3FA2". */
16
+ const MAX_CB_GRPPRL = 16290;
17
+ function parseClx(clx) {
18
+ let cursor = 0;
19
+ for (;;) {
20
+ const clxt = require_bytes.readUint8(clx, cursor);
21
+ if (clxt === CLXT_PCDT) break;
22
+ if (clxt !== CLXT_PRC) throw new require_errors.DocFormatError(`Clx element at offset ${cursor} begins with clxt 0x${clxt.toString(16).padStart(2, "0")}, which is neither a Prc (0x01) nor the Pcdt (0x02)`);
23
+ const cbGrpprl = require_bytes.readInt16LE(clx, cursor + 1);
24
+ if (cbGrpprl < 0 || cbGrpprl > MAX_CB_GRPPRL) throw new require_errors.DocFormatError(`Clx Prc at offset ${cursor} declares cbGrpprl ${cbGrpprl}, outside the 0..0x3FA2 range [MS-DOC] permits`);
25
+ cursor += 3 + cbGrpprl;
26
+ if (cursor > clx.length) throw new require_errors.DocFormatError(`Clx Prc at offset ${cursor - 3 - cbGrpprl} declares a ${cbGrpprl}-byte GrpPrl that runs past the end of the ${clx.length}-byte Clx`);
27
+ }
28
+ const lcb = require_bytes.readUint32LE(clx, cursor + 1);
29
+ const plcPcd = require_bytes.slice(clx, cursor + 5, lcb, "Clx Pcdt PlcPcd");
30
+ const plc = require_plc.parsePlc(plcPcd, PCD_SIZE, "PlcPcd");
31
+ const pieces = [];
32
+ for (let index = 0; index < plc.count; index += 1) {
33
+ const element = plc.element(index);
34
+ const bits = require_bytes.readUint16LE(element, 0);
35
+ const fcCompressed = require_bytes.readUint32LE(element, 2);
36
+ const cpStart = plc.keys[index];
37
+ const cpEnd = plc.keys[index + 1];
38
+ if (cpStart === void 0 || cpEnd === void 0) throw new require_errors.DocFormatError(`PlcPcd element ${index} has no bracketing character positions, so its text range is undefined`);
39
+ pieces.push({
40
+ cpStart,
41
+ cpEnd,
42
+ fc: fcCompressed & FC_MASK,
43
+ compressed: (fcCompressed & FC_COMPRESSED_BIT) !== 0,
44
+ noParaLast: (bits & 1) !== 0,
45
+ prm: require_bytes.readUint16LE(element, 6)
46
+ });
47
+ }
48
+ const lastCp = plc.keys[plc.keys.length - 1];
49
+ if (lastCp === void 0) throw new require_errors.DocFormatError("PlcPcd carries no character positions at all");
50
+ return {
51
+ pieces,
52
+ cpKeys: plc.keys,
53
+ lastCp
54
+ };
55
+ }
56
+ function characterOffset(piece, cp) {
57
+ if (!Number.isInteger(cp) || cp < piece.cpStart || cp >= piece.cpEnd) throw new require_errors.DocFormatError(`character position ${cp} is outside the piece covering [${piece.cpStart}, ${piece.cpEnd})`);
58
+ const delta = cp - piece.cpStart;
59
+ return piece.compressed ? Math.floor(piece.fc / 2) + delta : piece.fc + 2 * delta;
60
+ }
61
+ /** The number of bytes one character occupies in this piece: one for a compressed (8-bit) piece, two for an uncompressed (16-bit) one. */
62
+ function characterSize(piece) {
63
+ return piece.compressed ? 1 : 2;
64
+ }
65
+ //#endregion
66
+ exports.characterOffset = characterOffset;
67
+ exports.characterSize = characterSize;
68
+ exports.parseClx = parseClx;
@@ -0,0 +1,28 @@
1
+ //#region src/text/piece-table.d.ts
2
+ interface Piece {
3
+ /** The first character position this piece supplies, PlcPcd.aCp[i]. */
4
+ readonly cpStart: number;
5
+ /** One past the last character position this piece supplies, PlcPcd.aCp[i + 1]. */
6
+ readonly cpEnd: number;
7
+ /** FcCompressed's 30-bit offset field as stored -- NOT yet halved for a compressed piece. Use characterOffset() rather than this directly. */
8
+ readonly fc: number;
9
+ /** True when the piece's characters occupy one byte each and its real byte offset is `fc / 2`. */
10
+ readonly compressed: boolean;
11
+ /** Pcd's fNoParaLast: "If this bit is 1, the text MUST NOT contain a paragraph mark." */
12
+ readonly noParaLast: boolean;
13
+ /** Pcd.Prm, [MS-DOC] 2.8.36 -- further property modifications for this piece's text, carried verbatim and not yet applied (see README's scope note). */
14
+ readonly prm: number;
15
+ }
16
+ interface PieceTable {
17
+ readonly pieces: readonly Piece[];
18
+ /** PlcPcd.aCp itself: one more entry than there are pieces, so a lookup can bracket every piece and terminate at the document's end. */
19
+ readonly cpKeys: readonly number[];
20
+ /** The final aCp entry, one past the last character position the document defines. */
21
+ readonly lastCp: number;
22
+ }
23
+ declare function parseClx(clx: Uint8Array): PieceTable;
24
+ declare function characterOffset(piece: Piece, cp: number): number;
25
+ /** The number of bytes one character occupies in this piece: one for a compressed (8-bit) piece, two for an uncompressed (16-bit) one. */
26
+ declare function characterSize(piece: Piece): 1 | 2;
27
+ //#endregion
28
+ export { Piece, PieceTable, characterOffset, characterSize, parseClx };
@@ -0,0 +1,28 @@
1
+ //#region src/text/piece-table.d.ts
2
+ interface Piece {
3
+ /** The first character position this piece supplies, PlcPcd.aCp[i]. */
4
+ readonly cpStart: number;
5
+ /** One past the last character position this piece supplies, PlcPcd.aCp[i + 1]. */
6
+ readonly cpEnd: number;
7
+ /** FcCompressed's 30-bit offset field as stored -- NOT yet halved for a compressed piece. Use characterOffset() rather than this directly. */
8
+ readonly fc: number;
9
+ /** True when the piece's characters occupy one byte each and its real byte offset is `fc / 2`. */
10
+ readonly compressed: boolean;
11
+ /** Pcd's fNoParaLast: "If this bit is 1, the text MUST NOT contain a paragraph mark." */
12
+ readonly noParaLast: boolean;
13
+ /** Pcd.Prm, [MS-DOC] 2.8.36 -- further property modifications for this piece's text, carried verbatim and not yet applied (see README's scope note). */
14
+ readonly prm: number;
15
+ }
16
+ interface PieceTable {
17
+ readonly pieces: readonly Piece[];
18
+ /** PlcPcd.aCp itself: one more entry than there are pieces, so a lookup can bracket every piece and terminate at the document's end. */
19
+ readonly cpKeys: readonly number[];
20
+ /** The final aCp entry, one past the last character position the document defines. */
21
+ readonly lastCp: number;
22
+ }
23
+ declare function parseClx(clx: Uint8Array): PieceTable;
24
+ declare function characterOffset(piece: Piece, cp: number): number;
25
+ /** The number of bytes one character occupies in this piece: one for a compressed (8-bit) piece, two for an uncompressed (16-bit) one. */
26
+ declare function characterSize(piece: Piece): 1 | 2;
27
+ //#endregion
28
+ export { Piece, PieceTable, characterOffset, characterSize, parseClx };