@patrickdk77/office-defuse 0.0.0-stage → 0.1.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +317 -2
  3. package/dist/biff.d.ts +62 -0
  4. package/dist/biff.js +95 -0
  5. package/dist/cfb.d.ts +58 -0
  6. package/dist/cfb.js +456 -0
  7. package/dist/context.d.ts +66 -0
  8. package/dist/context.js +121 -0
  9. package/dist/crc32.d.ts +8 -0
  10. package/dist/crc32.js +58 -0
  11. package/dist/engine.d.ts +5 -0
  12. package/dist/engine.js +200 -0
  13. package/dist/errors.d.ts +19 -0
  14. package/dist/errors.js +33 -0
  15. package/dist/fields.d.ts +29 -0
  16. package/dist/fields.js +97 -0
  17. package/dist/findings.d.ts +25 -0
  18. package/dist/findings.js +188 -0
  19. package/dist/formats.d.ts +10 -0
  20. package/dist/formats.js +46 -0
  21. package/dist/formulas.d.ts +22 -0
  22. package/dist/formulas.js +77 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +30 -0
  25. package/dist/index.mjs +3 -0
  26. package/dist/io.d.ts +23 -0
  27. package/dist/io.js +139 -0
  28. package/dist/links.d.ts +26 -0
  29. package/dist/links.js +145 -0
  30. package/dist/msg.d.ts +7 -0
  31. package/dist/msg.js +97 -0
  32. package/dist/objects.d.ts +42 -0
  33. package/dist/objects.js +134 -0
  34. package/dist/odf.d.ts +3 -0
  35. package/dist/odf.js +297 -0
  36. package/dist/offcrypto.d.ts +31 -0
  37. package/dist/offcrypto.js +408 -0
  38. package/dist/ole.d.ts +12 -0
  39. package/dist/ole.js +127 -0
  40. package/dist/oleobj.d.ts +74 -0
  41. package/dist/oleobj.js +198 -0
  42. package/dist/ooxml.d.ts +8 -0
  43. package/dist/ooxml.js +468 -0
  44. package/dist/package.d.ts +3 -0
  45. package/dist/package.js +21 -0
  46. package/dist/ppt.d.ts +8 -0
  47. package/dist/ppt.js +241 -0
  48. package/dist/rtf.d.ts +8 -0
  49. package/dist/rtf.js +539 -0
  50. package/dist/score.d.ts +17 -0
  51. package/dist/score.js +63 -0
  52. package/dist/types.d.ts +274 -0
  53. package/dist/types.js +152 -0
  54. package/dist/word.d.ts +9 -0
  55. package/dist/word.js +160 -0
  56. package/dist/xls.d.ts +27 -0
  57. package/dist/xls.js +174 -0
  58. package/dist/xlsb.d.ts +19 -0
  59. package/dist/xlsb.js +101 -0
  60. package/dist/xml.d.ts +256 -0
  61. package/dist/xml.js +1105 -0
  62. package/dist/zip-writer.d.ts +38 -0
  63. package/dist/zip-writer.js +182 -0
  64. package/dist/zip.d.ts +55 -0
  65. package/dist/zip.js +535 -0
  66. package/package.json +96 -4
package/dist/crc32.js ADDED
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.crc32 = void 0;
37
+ exports.tableCrc32 = tableCrc32;
38
+ const zlib = __importStar(require("node:zlib"));
39
+ const TABLE = new Uint32Array(256);
40
+ for (let n = 0; n < 256; n++) {
41
+ let c = n;
42
+ for (let k = 0; k < 8; k++)
43
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
44
+ TABLE[n] = c >>> 0;
45
+ }
46
+ /** Continues a CRC-32, the one ZIP uses, over `bytes`, with a lookup table. Start from 0. */
47
+ function tableCrc32(bytes, crc = 0) {
48
+ let c = ~crc >>> 0;
49
+ for (const b of bytes)
50
+ c = TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
51
+ return ~c >>> 0;
52
+ }
53
+ /**
54
+ * Continues a CRC-32, the one ZIP uses, over `bytes`. Start from 0. This is zlib.crc32 where Node has it, about 50
55
+ * times faster, and the table otherwise. Node 18 and early releases of Node 20 and 22 lack it. The choice is made
56
+ * once, when the module loads.
57
+ */
58
+ exports.crc32 = zlib.crc32 ?? tableCrc32;
@@ -0,0 +1,5 @@
1
+ import { type ByteSource, type OfficeInspection, type OfficeOptions } from './types';
2
+ /** Inspects an Office or OpenDocument file without changing it. The source is closed when the run ends. */
3
+ export declare function inspectOfficeSource(source: ByteSource, options?: OfficeOptions): Promise<OfficeInspection>;
4
+ /** Inspects Office or OpenDocument bytes. */
5
+ export declare function inspectOffice(bytes: Uint8Array, options?: OfficeOptions): Promise<OfficeInspection>;
package/dist/engine.js ADDED
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inspectOfficeSource = inspectOfficeSource;
4
+ exports.inspectOffice = inspectOffice;
5
+ const cfb_1 = require("./cfb");
6
+ const context_1 = require("./context");
7
+ const errors_1 = require("./errors");
8
+ const findings_1 = require("./findings");
9
+ const io_1 = require("./io");
10
+ const ole_1 = require("./ole");
11
+ const package_1 = require("./package");
12
+ const rtf_1 = require("./rtf");
13
+ const score_1 = require("./score");
14
+ const types_1 = require("./types");
15
+ const zip_1 = require("./zip");
16
+ const DEFAULT_LIMITS = {
17
+ entries: 10_000,
18
+ decompressedBytes: 512 * 1024 * 1024,
19
+ totalDecompressedBytes: 2 * 1024 * 1024 * 1024,
20
+ xmlDepth: 256,
21
+ xmlTokenSize: 16 * 1024 * 1024,
22
+ xmlEntityDepth: 16,
23
+ xmlEntityExpansions: 100_000,
24
+ xmlEntitySize: 16 * 1024 * 1024,
25
+ rtfDepth: 256,
26
+ };
27
+ function resolveLimits(given = {}) {
28
+ return {
29
+ entries: given.entries ?? DEFAULT_LIMITS.entries,
30
+ decompressedBytes: given.decompressedBytes ?? DEFAULT_LIMITS.decompressedBytes,
31
+ totalDecompressedBytes: given.totalDecompressedBytes ?? DEFAULT_LIMITS.totalDecompressedBytes,
32
+ xmlDepth: given.xmlDepth ?? DEFAULT_LIMITS.xmlDepth,
33
+ xmlTokenSize: given.xmlTokenSize ?? DEFAULT_LIMITS.xmlTokenSize,
34
+ xmlEntityDepth: given.xmlEntityDepth ?? DEFAULT_LIMITS.xmlEntityDepth,
35
+ xmlEntityExpansions: given.xmlEntityExpansions ?? DEFAULT_LIMITS.xmlEntityExpansions,
36
+ xmlEntitySize: given.xmlEntitySize ?? DEFAULT_LIMITS.xmlEntitySize,
37
+ rtfDepth: given.rtfDepth ?? DEFAULT_LIMITS.rtfDepth,
38
+ fileSize: given.fileSize,
39
+ timeMs: given.timeMs,
40
+ };
41
+ }
42
+ /**
43
+ * `incomplete` marks a run that stopped before it read the whole package. It is rejected without a score, whatever
44
+ * the actions say, because an override can make the finding that stopped it look harmless.
45
+ */
46
+ function finish(findings, options, incomplete, format) {
47
+ const named = { format: format?.format ?? '', mimeType: format?.mimeType ?? '' };
48
+ if (incomplete)
49
+ return { status: 'rejected', score: null, risk: types_1.OfficeRisk.Unknown, findings, ...named };
50
+ const rejected = findings.some(f => f.action === 'reject');
51
+ const strippable = findings.some(f => f.action === 'strip');
52
+ const { score, risk } = (0, score_1.scoreFindings)(findings, options);
53
+ return { status: rejected ? 'rejected' : strippable ? 'strippable' : 'clean', score, risk, findings, ...named };
54
+ }
55
+ /**
56
+ * Wraps the source for one run. A read that returns more or fewer bytes than the source's size allows fails with
57
+ * code EIO, since the reader would take the shifted bytes for the file. Any error the source throws is kept, and the
58
+ * engine throws it instead of a verdict on the file.
59
+ */
60
+ function guarded(source) {
61
+ let size = 0;
62
+ const g = {
63
+ size: async () => {
64
+ try {
65
+ size = await source.size();
66
+ return size;
67
+ }
68
+ catch (e) {
69
+ g.failure ??= e;
70
+ throw e;
71
+ }
72
+ },
73
+ read: async (offset, length) => {
74
+ try {
75
+ const b = await source.read(offset, length);
76
+ const want = Math.max(0, Math.min(length, size - offset));
77
+ if (b.length !== want)
78
+ throw Object.assign(new Error(`The source returned ${b.length} bytes at offset ${offset} where ${want} were expected`), { code: 'EIO' });
79
+ return b;
80
+ }
81
+ catch (e) {
82
+ g.failure ??= e;
83
+ throw e;
84
+ }
85
+ },
86
+ };
87
+ return g;
88
+ }
89
+ const ambiguous = (reason) => new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, `The file is ${reason}`, { reason });
90
+ /**
91
+ * Reads the file's container. A file that starts with the compound file signature is read as one, and one that
92
+ * starts with {\rt is RTF, which is how Word tells them. The ZIP reader runs too, from the end: a file both read is
93
+ * ambiguous, since Office goes by the first bytes and a ZIP reader by the last. A compound file that does not read is
94
+ * a ZIP archive with data in front, if the ZIP reader read one. Word reads any file that starts with {\rt as RTF, so
95
+ * that does not fail.
96
+ */
97
+ async function openContainer(src, size, limits, settings) {
98
+ const head = await src.read(0, Math.min(8, size));
99
+ const cfbSignature = head.length === 8 && head.every((b, i) => b === cfb_1.CFB_SIGNATURE[i]);
100
+ const rtfSignature = Buffer.from(head.subarray(0, 4)).toString('latin1') === '{\\rt';
101
+ const probe = cfbSignature || rtfSignature
102
+ ? await zip_1.ZipArchive.open(src, limits, settings.deadline).catch((e) => {
103
+ if (e instanceof errors_1.StopError)
104
+ return undefined;
105
+ throw e;
106
+ })
107
+ : undefined;
108
+ if (rtfSignature) {
109
+ if (probe)
110
+ throw ambiguous('both RTF and a ZIP archive');
111
+ return { kind: 'rtf' };
112
+ }
113
+ if (cfbSignature) {
114
+ try {
115
+ const cfb = await cfb_1.CompoundFile.open(src, settings, settings.deadline);
116
+ if (probe)
117
+ throw ambiguous('both a compound file and a ZIP archive');
118
+ return { kind: 'cfb', cfb };
119
+ }
120
+ catch (e) {
121
+ if (!probe || !(e instanceof errors_1.StopError) || e.category !== types_1.OfficeCategory.Container)
122
+ throw e;
123
+ }
124
+ }
125
+ return { kind: 'zip', zip: probe ?? (await zip_1.ZipArchive.open(src, limits, settings.deadline)) };
126
+ }
127
+ /** Inspects an Office or OpenDocument file without changing it. The source is closed when the run ends. */
128
+ async function inspectOfficeSource(source, options = {}) {
129
+ const limits = resolveLimits(options.limits);
130
+ const deadline = limits.timeMs !== undefined ? Date.now() + limits.timeMs : undefined;
131
+ const factory = new findings_1.FindingFactory(options.actionOverrides);
132
+ const findings = [];
133
+ const src = guarded(source);
134
+ const settings = {
135
+ entries: limits.entries,
136
+ decompressedBytes: limits.decompressedBytes,
137
+ totalDecompressedBytes: limits.totalDecompressedBytes,
138
+ rtfDepth: limits.rtfDepth,
139
+ deadline,
140
+ password: options.password,
141
+ };
142
+ const ctx = new context_1.RunContext(factory, {
143
+ depth: limits.xmlDepth,
144
+ tokenSize: limits.xmlTokenSize,
145
+ entityDepth: limits.xmlEntityDepth,
146
+ entityExpansions: limits.xmlEntityExpansions,
147
+ entitySize: limits.xmlEntitySize,
148
+ }, settings);
149
+ try {
150
+ const size = await src.size();
151
+ if (limits.fileSize !== undefined && size > limits.fileSize)
152
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.FileSize, `${size} bytes`, { size, limit: limits.fileSize });
153
+ (0, errors_1.checkTime)(deadline);
154
+ const container = await openContainer(src, size, limits, settings);
155
+ if (container.kind === 'rtf')
156
+ await (0, rtf_1.inspectRtf)(ctx, src, size);
157
+ else if (container.kind === 'cfb') {
158
+ for (const n of container.cfb.notes)
159
+ findings.push(factory.make(types_1.OfficeCategory.Container, n.detail, undefined, { bytes: n.bytes }));
160
+ await (0, ole_1.inspectCompound)(ctx, container.cfb);
161
+ }
162
+ else {
163
+ const zip = container.zip;
164
+ for (const n of zip.notes)
165
+ findings.push(factory.make(types_1.OfficeCategory.Container, n.detail, undefined, { bytes: n.bytes }));
166
+ await (0, package_1.inspectPackage)(ctx.withZip(zip));
167
+ }
168
+ findings.push(...ctx.findings);
169
+ return finish(findings, options, false, ctx.format);
170
+ }
171
+ catch (e) {
172
+ if (src.failure !== undefined)
173
+ throw src.failure;
174
+ findings.push(...ctx.findings);
175
+ if (e instanceof errors_1.StopError) {
176
+ findings.push(factory.make(e.category, e.detail, e.location ?? (typeof e.data?.name === 'string' ? e.data.name : undefined), { ...e.data, reason: e.data?.reason ?? e.message }));
177
+ }
178
+ else if (e?.code === 'DEFUSE_LIMIT') {
179
+ // The family's time limit, from this package or any other.
180
+ findings.push(factory.make(types_1.OfficeCategory.Limit, types_1.OfficeDetail.Time));
181
+ }
182
+ else {
183
+ // Anything unexpected rejects the file rather than escaping as an exception.
184
+ findings.push(factory.make(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unreadable, undefined, { reason: String(e?.message ?? e).slice(0, 200) }));
185
+ }
186
+ return finish(findings, options, true, ctx.format);
187
+ }
188
+ finally {
189
+ try {
190
+ await source.close?.();
191
+ }
192
+ catch {
193
+ // The verdict stands whether or not the source closes.
194
+ }
195
+ }
196
+ }
197
+ /** Inspects Office or OpenDocument bytes. */
198
+ async function inspectOffice(bytes, options = {}) {
199
+ return inspectOfficeSource((0, io_1.bufferSource)(bytes), options);
200
+ }
@@ -0,0 +1,19 @@
1
+ import type { OfficeCategory, OfficeDetail } from './types';
2
+ /** The run passed its deadline. The code makes every package of the family pass it up instead of treating it as one plugin failing. */
3
+ export declare class TimeLimitError extends Error {
4
+ readonly code = "DEFUSE_LIMIT";
5
+ readonly limit = "time";
6
+ }
7
+ /**
8
+ * A refusal or a limit that stops the run. The engine turns it into the finding it names and rejects the file with
9
+ * no score, whatever the overrides say.
10
+ */
11
+ export declare class StopError extends Error {
12
+ readonly category: OfficeCategory;
13
+ readonly detail: OfficeDetail;
14
+ readonly data?: Record<string, string | number> | undefined;
15
+ readonly location?: string | undefined;
16
+ constructor(category: OfficeCategory, detail: OfficeDetail, message: string, data?: Record<string, string | number> | undefined, location?: string | undefined);
17
+ }
18
+ /** Throws the time limit once `deadline`, a Date.now() value, has passed. */
19
+ export declare function checkTime(deadline: number | undefined): void;
package/dist/errors.js ADDED
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StopError = exports.TimeLimitError = void 0;
4
+ exports.checkTime = checkTime;
5
+ /** The run passed its deadline. The code makes every package of the family pass it up instead of treating it as one plugin failing. */
6
+ class TimeLimitError extends Error {
7
+ code = 'DEFUSE_LIMIT';
8
+ limit = 'time';
9
+ }
10
+ exports.TimeLimitError = TimeLimitError;
11
+ /**
12
+ * A refusal or a limit that stops the run. The engine turns it into the finding it names and rejects the file with
13
+ * no score, whatever the overrides say.
14
+ */
15
+ class StopError extends Error {
16
+ category;
17
+ detail;
18
+ data;
19
+ location;
20
+ constructor(category, detail, message, data, location) {
21
+ super(message);
22
+ this.category = category;
23
+ this.detail = detail;
24
+ this.data = data;
25
+ this.location = location;
26
+ }
27
+ }
28
+ exports.StopError = StopError;
29
+ /** Throws the time limit once `deadline`, a Date.now() value, has passed. */
30
+ function checkTime(deadline) {
31
+ if (deadline !== undefined && Date.now() > deadline)
32
+ throw new TimeLimitError('Time limit exceeded');
33
+ }
@@ -0,0 +1,29 @@
1
+ import { type RunContext } from './context';
2
+ /**
3
+ * Reports a Word field by its code. A field whose name holds the content of an external entity has a type the file
4
+ * does not say, as one whose name another field builds does.
5
+ */
6
+ export declare function evaluateField(ctx: RunContext, location: string, code: string, computed: boolean): void;
7
+ /**
8
+ * Word field codes, followed through nested fields as the text of a document arrives: a field begins, its code
9
+ * comes, a separator starts its result, and it ends. Each format says where those marks are.
10
+ */
11
+ export declare class FieldStack {
12
+ private readonly ctx;
13
+ private readonly location;
14
+ private readonly stack;
15
+ constructor(ctx: RunContext, location: string);
16
+ /** A field starts. One that starts before its parent's code has a word in it makes that code at run time. */
17
+ begin(): void;
18
+ /** A field that is all code, such as OOXML's fldSimple. */
19
+ simple(code: string): void;
20
+ separate(): void;
21
+ end(): void;
22
+ /** Whether a field's code is being read now. */
23
+ get inCode(): boolean;
24
+ /** Text of the innermost field's code. White space before the first word counts for nothing, so no amount of it can push the field's name out. */
25
+ text(t: string): void;
26
+ /** A document can end inside a field. Word still reads the code it has. */
27
+ finish(): void;
28
+ private nest;
29
+ }
package/dist/fields.js ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FieldStack = void 0;
4
+ exports.evaluateField = evaluateField;
5
+ const context_1 = require("./context");
6
+ const formulas_1 = require("./formulas");
7
+ const links_1 = require("./links");
8
+ const types_1 = require("./types");
9
+ const xml_1 = require("./xml");
10
+ /** How much of a field code is kept, counted from its first word, which names the field. */
11
+ const MAX_FIELD_CODE = 4096;
12
+ /**
13
+ * Reports a Word field by its code. A field whose name holds the content of an external entity has a type the file
14
+ * does not say, as one whose name another field builds does.
15
+ */
16
+ function evaluateField(ctx, location, code, computed) {
17
+ const data = { code: (0, context_1.clip)(code.trim()) };
18
+ const words = (0, formulas_1.fieldWords)(code);
19
+ const first = words[0]?.text ?? '';
20
+ const entity = (0, xml_1.unknownEntity)(first);
21
+ if (computed || entity !== undefined)
22
+ ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Computed, location, entity === undefined ? data : { ...data, entity: (0, context_1.clip)(entity) });
23
+ const name = first.toUpperCase();
24
+ // An argument comes before any switch, and a switch is an unquoted word starting with a backslash.
25
+ const switchAt = words.findIndex(w => !w.quoted && w.text.startsWith('\\'));
26
+ const arg = (n) => (words[n] !== undefined && (switchAt < 0 || n < switchAt) ? words[n].text : undefined);
27
+ if (name === 'DDE' || name === 'DDEAUTO')
28
+ ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Field, location, data);
29
+ else if (name === 'MACROBUTTON')
30
+ ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Macrobutton, location, { ...data, macro: (0, context_1.clip)(words[1]?.text ?? '') });
31
+ else if (name === 'INCLUDETEXT' || name === 'INCLUDEPICTURE' || name === 'IMPORT' || name === 'LINK') {
32
+ const target = arg(name === 'LINK' ? 2 : 1);
33
+ if (target === undefined)
34
+ ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Include, location, data);
35
+ else
36
+ ctx.external(types_1.OfficeDetail.Include, target, location, data, 'field');
37
+ }
38
+ else if (name === 'HYPERLINK') {
39
+ const target = arg(1);
40
+ if (target !== undefined && !(0, links_1.isFragment)(target))
41
+ ctx.link(target, location, { ...data, source: 'HYPERLINK field' });
42
+ }
43
+ }
44
+ /**
45
+ * Word field codes, followed through nested fields as the text of a document arrives: a field begins, its code
46
+ * comes, a separator starts its result, and it ends. Each format says where those marks are.
47
+ */
48
+ class FieldStack {
49
+ ctx;
50
+ location;
51
+ stack = [];
52
+ constructor(ctx, location) {
53
+ this.ctx = ctx;
54
+ this.location = location;
55
+ }
56
+ /** A field starts. One that starts before its parent's code has a word in it makes that code at run time. */
57
+ begin() {
58
+ this.nest();
59
+ this.stack.push({ code: '', inCode: true, computed: false });
60
+ }
61
+ /** A field that is all code, such as OOXML's fldSimple. */
62
+ simple(code) {
63
+ this.nest();
64
+ evaluateField(this.ctx, this.location, code, false);
65
+ }
66
+ separate() {
67
+ const top = this.stack[this.stack.length - 1];
68
+ if (top)
69
+ top.inCode = false;
70
+ }
71
+ end() {
72
+ const f = this.stack.pop();
73
+ if (f)
74
+ evaluateField(this.ctx, this.location, f.code, f.computed);
75
+ }
76
+ /** Whether a field's code is being read now. */
77
+ get inCode() {
78
+ return this.stack[this.stack.length - 1]?.inCode === true;
79
+ }
80
+ /** Text of the innermost field's code. White space before the first word counts for nothing, so no amount of it can push the field's name out. */
81
+ text(t) {
82
+ const top = this.stack[this.stack.length - 1];
83
+ if (top?.inCode && top.code.length < MAX_FIELD_CODE)
84
+ top.code = top.code ? top.code + t : t.trimStart();
85
+ }
86
+ /** A document can end inside a field. Word still reads the code it has. */
87
+ finish() {
88
+ for (let f = this.stack.pop(); f; f = this.stack.pop())
89
+ evaluateField(this.ctx, this.location, f.code, f.computed);
90
+ }
91
+ nest() {
92
+ const top = this.stack[this.stack.length - 1];
93
+ if (top?.inCode && top.code === '')
94
+ top.computed = true;
95
+ }
96
+ }
97
+ exports.FieldStack = FieldStack;
@@ -0,0 +1,25 @@
1
+ import { OfficeCategory as C, OfficeDetail as D, type OfficeActionOverride, type OfficeFinding, type OfficeFindingAction } from './types';
2
+ interface Spec {
3
+ action: OfficeFindingAction;
4
+ /**
5
+ * May contain <first|second> choices, which take the first alternative whose {key} values from the data are all
6
+ * set and not empty, or that names none. A value has control and format characters replaced by "?" and is cut to
7
+ * 60 characters.
8
+ */
9
+ description: string;
10
+ weight: number;
11
+ }
12
+ /** The built-in spec for a kind of finding, or undefined for a kind office-defuse does not make, such as one from another package. */
13
+ export declare function findingSpec(category: string, detail: string): Spec | undefined;
14
+ /** Every category and detail pair office-defuse makes, with its default action, description and score weight. */
15
+ export declare function allFindingSpecs(): Array<{
16
+ category: C;
17
+ detail: D;
18
+ } & Spec>;
19
+ export declare class FindingFactory {
20
+ private readonly overrides;
21
+ constructor(overrides?: OfficeActionOverride[]);
22
+ private actionFor;
23
+ make(category: C, detail: D, location?: string, data?: Record<string, string | number>): OfficeFinding;
24
+ }
25
+ export {};