@open-agreements/open-agreements 0.3.1 → 0.5.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/README.md +13 -1
- package/content/external/yc-safe-discount/metadata.yaml +1 -1
- package/content/external/yc-safe-mfn/metadata.yaml +1 -1
- package/content/external/yc-safe-pro-rata-side-letter/metadata.yaml +1 -1
- package/content/external/yc-safe-valuation-cap/metadata.yaml +1 -1
- package/content/recipes/QUALITY_TRACKER.md +35 -0
- package/content/recipes/nvca-certificate-of-incorporation/clean.json +14 -2
- package/content/recipes/nvca-certificate-of-incorporation/computed.json +78 -0
- package/content/recipes/nvca-certificate-of-incorporation/metadata.yaml +115 -2
- package/content/recipes/nvca-certificate-of-incorporation/replacements.json +50 -2
- package/content/recipes/nvca-certificate-of-incorporation/selections-roadmap.md +47 -0
- package/content/recipes/nvca-indemnification-agreement/metadata.yaml +5 -1
- package/content/recipes/nvca-indemnification-agreement/replacements.json +30 -5
- package/content/recipes/nvca-investors-rights-agreement/clean.json +9 -1
- package/content/recipes/nvca-investors-rights-agreement/metadata.yaml +1 -1
- package/content/recipes/nvca-investors-rights-agreement/replacements.json +4 -1
- package/content/recipes/nvca-management-rights-letter/metadata.yaml +1 -1
- package/content/recipes/nvca-rofr-co-sale-agreement/clean.json +11 -1
- package/content/recipes/nvca-rofr-co-sale-agreement/metadata.yaml +5 -1
- package/content/recipes/nvca-rofr-co-sale-agreement/replacements.json +62 -3
- package/content/recipes/nvca-stock-purchase-agreement/clean.json +8 -3
- package/content/recipes/nvca-stock-purchase-agreement/metadata.yaml +6 -1
- package/content/recipes/nvca-stock-purchase-agreement/normalize.json +0 -16
- package/content/recipes/nvca-stock-purchase-agreement/replacements.json +15 -12
- package/content/recipes/nvca-stock-purchase-agreement/selections.json +17 -0
- package/content/recipes/nvca-voting-agreement/clean.json +11 -1
- package/content/recipes/nvca-voting-agreement/metadata.yaml +1 -1
- package/content/recipes/nvca-voting-agreement/replacements.json +2 -1
- package/content/templates/bonterms-mutual-nda/metadata.yaml +1 -1
- package/content/templates/bonterms-professional-services-agreement/metadata.yaml +1 -1
- package/content/templates/closing-checklist/metadata.yaml +1 -1
- package/content/templates/closing-checklist/template.docx +0 -0
- package/content/templates/common-paper-ai-addendum/metadata.yaml +1 -1
- package/content/templates/common-paper-ai-addendum-in-app/metadata.yaml +1 -1
- package/content/templates/common-paper-amendment/metadata.yaml +1 -1
- package/content/templates/common-paper-business-associate-agreement/metadata.yaml +1 -1
- package/content/templates/common-paper-cloud-service-agreement/metadata.yaml +1 -1
- package/content/templates/common-paper-csa-click-through/metadata.yaml +1 -1
- package/content/templates/common-paper-csa-with-ai/metadata.yaml +1 -1
- package/content/templates/common-paper-csa-with-sla/metadata.yaml +1 -1
- package/content/templates/common-paper-csa-without-sla/metadata.yaml +1 -1
- package/content/templates/common-paper-data-processing-agreement/metadata.yaml +1 -1
- package/content/templates/common-paper-design-partner-agreement/metadata.yaml +1 -1
- package/content/templates/common-paper-independent-contractor-agreement/metadata.yaml +1 -1
- package/content/templates/common-paper-letter-of-intent/metadata.yaml +1 -1
- package/content/templates/common-paper-mutual-nda/metadata.yaml +1 -1
- package/content/templates/common-paper-one-way-nda/metadata.yaml +1 -1
- package/content/templates/common-paper-order-form/metadata.yaml +1 -1
- package/content/templates/common-paper-order-form-with-sla/metadata.yaml +1 -1
- package/content/templates/common-paper-partnership-agreement/metadata.yaml +1 -1
- package/content/templates/common-paper-pilot-agreement/metadata.yaml +1 -1
- package/content/templates/common-paper-professional-services-agreement/metadata.yaml +1 -1
- package/content/templates/common-paper-software-license-agreement/metadata.yaml +1 -1
- package/content/templates/common-paper-statement-of-work/metadata.yaml +1 -1
- package/content/templates/common-paper-term-sheet/metadata.yaml +1 -1
- package/content/templates/openagreements-employee-ip-inventions-assignment/metadata.yaml +1 -1
- package/content/templates/openagreements-employment-confidentiality-acknowledgement/metadata.yaml +1 -1
- package/content/templates/openagreements-employment-offer-letter/metadata.yaml +1 -1
- package/content/templates/working-group-list/metadata.yaml +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +8 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/fill.js +3 -3
- package/dist/commands/fill.js.map +1 -1
- package/dist/commands/list.js +19 -15
- package/dist/commands/list.js.map +1 -1
- package/dist/core/checklist/docx-import.d.ts +50 -0
- package/dist/core/checklist/docx-import.d.ts.map +1 -0
- package/dist/core/checklist/docx-import.js +613 -0
- package/dist/core/checklist/docx-import.js.map +1 -0
- package/dist/core/checklist/docx-table-helpers.d.ts +33 -0
- package/dist/core/checklist/docx-table-helpers.d.ts.map +1 -0
- package/dist/core/checklist/docx-table-helpers.js +154 -0
- package/dist/core/checklist/docx-table-helpers.js.map +1 -0
- package/dist/core/checklist/format-checklist-docx.d.ts.map +1 -1
- package/dist/core/checklist/format-checklist-docx.js +37 -88
- package/dist/core/checklist/format-checklist-docx.js.map +1 -1
- package/dist/core/checklist/index.d.ts +15 -12
- package/dist/core/checklist/index.d.ts.map +1 -1
- package/dist/core/checklist/index.js +48 -30
- package/dist/core/checklist/index.js.map +1 -1
- package/dist/core/checklist/status-labels.d.ts +6 -0
- package/dist/core/checklist/status-labels.d.ts.map +1 -1
- package/dist/core/checklist/status-labels.js +8 -0
- package/dist/core/checklist/status-labels.js.map +1 -1
- package/dist/core/command-generation/adapters/claude.js +4 -4
- package/dist/core/employment/memo.js +3 -3
- package/dist/core/engine.js +1 -1
- package/dist/core/external/index.js +1 -1
- package/dist/core/fill-pipeline.d.ts +3 -3
- package/dist/core/fill-pipeline.js +6 -6
- package/dist/core/fill-pipeline.js.map +1 -1
- package/dist/core/metadata.d.ts +13 -3
- package/dist/core/metadata.d.ts.map +1 -1
- package/dist/core/metadata.js +19 -11
- package/dist/core/metadata.js.map +1 -1
- package/dist/core/recipe/bracket-normalizer.d.ts +4 -5
- package/dist/core/recipe/bracket-normalizer.d.ts.map +1 -1
- package/dist/core/recipe/bracket-normalizer.js +67 -149
- package/dist/core/recipe/bracket-normalizer.js.map +1 -1
- package/dist/core/recipe/cleaner.d.ts.map +1 -1
- package/dist/core/recipe/cleaner.js +91 -7
- package/dist/core/recipe/cleaner.js.map +1 -1
- package/dist/core/recipe/downloader.d.ts.map +1 -1
- package/dist/core/recipe/downloader.js +8 -3
- package/dist/core/recipe/downloader.js.map +1 -1
- package/dist/core/recipe/index.d.ts +2 -1
- package/dist/core/recipe/index.d.ts.map +1 -1
- package/dist/core/recipe/index.js +11 -4
- package/dist/core/recipe/index.js.map +1 -1
- package/dist/core/recipe/patcher.d.ts +9 -5
- package/dist/core/recipe/patcher.d.ts.map +1 -1
- package/dist/core/recipe/patcher.js +183 -136
- package/dist/core/recipe/patcher.js.map +1 -1
- package/dist/core/recipe/replacement-keys.d.ts +2 -7
- package/dist/core/recipe/replacement-keys.d.ts.map +1 -1
- package/dist/core/recipe/replacement-keys.js +3 -20
- package/dist/core/recipe/replacement-keys.js.map +1 -1
- package/dist/core/recipe/source-drift.js +1 -1
- package/dist/core/recipe/source-drift.js.map +1 -1
- package/dist/core/recipe/verifier.d.ts +7 -1
- package/dist/core/recipe/verifier.d.ts.map +1 -1
- package/dist/core/recipe/verifier.js +69 -3
- package/dist/core/recipe/verifier.js.map +1 -1
- package/dist/core/selector.d.ts +2 -0
- package/dist/core/selector.d.ts.map +1 -1
- package/dist/core/selector.js +97 -1
- package/dist/core/selector.js.map +1 -1
- package/dist/core/template-listing.d.ts +3 -1
- package/dist/core/template-listing.d.ts.map +1 -1
- package/dist/core/template-listing.js +4 -3
- package/dist/core/template-listing.js.map +1 -1
- package/dist/core/unified-pipeline.d.ts +1 -1
- package/dist/core/unified-pipeline.d.ts.map +1 -1
- package/dist/core/unified-pipeline.js +21 -3
- package/dist/core/unified-pipeline.js.map +1 -1
- package/dist/core/validation/external.d.ts.map +1 -1
- package/dist/core/validation/external.js +0 -2
- package/dist/core/validation/external.js.map +1 -1
- package/dist/core/validation/recipe.d.ts.map +1 -1
- package/dist/core/validation/recipe.js +0 -2
- package/dist/core/validation/recipe.js.map +1 -1
- package/dist/core/validation/scan-metadata.d.ts.map +1 -1
- package/dist/core/validation/scan-metadata.js +17 -1
- package/dist/core/validation/scan-metadata.js.map +1 -1
- package/dist/core/validation/template.js +5 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +11 -5
- package/server.json +31 -0
- package/skills/client-email/SKILL.md +143 -0
- package/skills/cloud-service-agreement/SKILL.md +8 -99
- package/skills/data-privacy-agreement/SKILL.md +10 -97
- package/skills/delaware-franchise-tax/SKILL.md +7 -1
- package/skills/edit-docx-agreement/SKILL.md +10 -3
- package/skills/employment-contract/SKILL.md +10 -95
- package/skills/iso-27001-evidence-collection/SKILL.md +7 -1
- package/skills/iso-27001-internal-audit/SKILL.md +6 -0
- package/skills/nda/SKILL.md +7 -99
- package/skills/open-agreements/CONNECTORS.md +1 -1
- package/skills/open-agreements/SKILL.md +17 -104
- package/skills/recipe-quality-audit/SKILL.md +116 -0
- package/skills/safe/SKILL.md +12 -97
- package/skills/services-agreement/SKILL.md +8 -99
- package/skills/shared/template-filling-execution.md +92 -0
- package/skills/soc2-readiness/SKILL.md +7 -1
- package/skills/unit-test-philosophy/SKILL.md +12 -1
- package/skills/venture-financing/SKILL.md +11 -96
|
@@ -2,9 +2,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
|
|
5
|
+
// Lazy — must be a function so ESM import hoisting doesn't evaluate before
|
|
6
|
+
// api/_shared.ts sets OPEN_AGREEMENTS_CACHE_ROOT for Vercel.
|
|
7
|
+
function getCacheRoot() {
|
|
8
|
+
return process.env['OPEN_AGREEMENTS_CACHE_ROOT']
|
|
9
|
+
?? join(homedir(), '.open-agreements', 'cache');
|
|
10
|
+
}
|
|
6
11
|
function getCachePath(recipeId) {
|
|
7
|
-
return join(
|
|
12
|
+
return join(getCacheRoot(), recipeId, 'source.docx');
|
|
8
13
|
}
|
|
9
14
|
function verifyHash(buf, expected) {
|
|
10
15
|
const actual = createHash('sha256').update(buf).digest('hex');
|
|
@@ -49,7 +54,7 @@ export async function ensureSourceDocx(recipeId, metadata) {
|
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
56
|
// Write to cache
|
|
52
|
-
const cacheDir = join(
|
|
57
|
+
const cacheDir = join(getCacheRoot(), recipeId);
|
|
53
58
|
mkdirSync(cacheDir, { recursive: true });
|
|
54
59
|
writeFileSync(cachePath, buf);
|
|
55
60
|
console.log(`Cached: ${cachePath}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"downloader.js","sourceRoot":"","sources":["../../../src/core/recipe/downloader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,
|
|
1
|
+
{"version":3,"file":"downloader.js","sourceRoot":"","sources":["../../../src/core/recipe/downloader.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,7 +7,8 @@ export declare function runRecipe(options: RecipeRunOptions): Promise<RecipeRunR
|
|
|
7
7
|
export { cleanDocument } from './cleaner.js';
|
|
8
8
|
export type { CleanResult, CleanOptions } from './cleaner.js';
|
|
9
9
|
export { patchDocument } from './patcher.js';
|
|
10
|
-
export {
|
|
10
|
+
export type { PatchResult } from './patcher.js';
|
|
11
|
+
export { verifyOutput, normalizeText, extractAllText, countFormattingAnomalies } from './verifier.js';
|
|
11
12
|
export { ensureSourceDocx } from './downloader.js';
|
|
12
13
|
export { checkRecipeSourceDrift, computeSourceStructureSignature } from './source-drift.js';
|
|
13
14
|
export { enumerateTextParts, getGeneralTextPartNames } from './ooxml-parts.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/recipe/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/recipe/index.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEpE;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAqFnF;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACtG,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,+BAA+B,EAAE,MAAM,mBAAmB,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC/E,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC/F,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { loadRecipeMetadata, loadCleanConfig, loadNormalizeConfig } from '../metadata.js';
|
|
4
|
+
import { loadSelectionsConfig } from '../selector.js';
|
|
4
5
|
import { resolveRecipeDir } from '../../utils/paths.js';
|
|
5
6
|
import { verifyOutput } from './verifier.js';
|
|
6
7
|
import { ensureSourceDocx } from './downloader.js';
|
|
@@ -17,6 +18,11 @@ export async function runRecipe(options) {
|
|
|
17
18
|
const metadata = loadRecipeMetadata(recipeDir);
|
|
18
19
|
const cleanConfig = loadCleanConfig(recipeDir);
|
|
19
20
|
const normalizeConfig = loadNormalizeConfig(recipeDir);
|
|
21
|
+
// Load selectionsConfig if selections.json exists (mirrors engine.ts template path)
|
|
22
|
+
const selectionsPath = join(recipeDir, 'selections.json');
|
|
23
|
+
const selectionsConfig = existsSync(selectionsPath)
|
|
24
|
+
? loadSelectionsConfig(selectionsPath)
|
|
25
|
+
: undefined;
|
|
20
26
|
// Resolve input: explicit path or auto-download
|
|
21
27
|
const inputPath = options.inputPath ?? await ensureSourceDocx(recipeId, metadata);
|
|
22
28
|
// Load replacements.json
|
|
@@ -53,14 +59,15 @@ export async function runRecipe(options) {
|
|
|
53
59
|
if (computedOutPath && computedArtifact) {
|
|
54
60
|
writeComputedArtifact(computedOutPath, computedArtifact);
|
|
55
61
|
}
|
|
56
|
-
const shouldNormalizeBracketArtifacts = options.normalizeBracketArtifacts ??
|
|
62
|
+
const shouldNormalizeBracketArtifacts = options.normalizeBracketArtifacts ?? normalizeConfig.paragraph_rules.length > 0;
|
|
57
63
|
const result = await runFillPipeline({
|
|
58
64
|
inputPath,
|
|
59
65
|
outputPath,
|
|
60
66
|
values: effectiveValues,
|
|
61
67
|
fields: metadata.fields,
|
|
62
|
-
|
|
68
|
+
priorityFieldNames: metadata.priority_fields,
|
|
63
69
|
cleanPatch: { cleanConfig, replacements },
|
|
70
|
+
selectionsConfig,
|
|
64
71
|
postProcess: shouldNormalizeBracketArtifacts
|
|
65
72
|
? async (outputDocPath) => {
|
|
66
73
|
await normalizeBracketArtifacts(outputDocPath, outputDocPath, {
|
|
@@ -83,7 +90,7 @@ export async function runRecipe(options) {
|
|
|
83
90
|
}
|
|
84
91
|
export { cleanDocument } from './cleaner.js';
|
|
85
92
|
export { patchDocument } from './patcher.js';
|
|
86
|
-
export { verifyOutput, normalizeText, extractAllText } from './verifier.js';
|
|
93
|
+
export { verifyOutput, normalizeText, extractAllText, countFormattingAnomalies } from './verifier.js';
|
|
87
94
|
export { ensureSourceDocx } from './downloader.js';
|
|
88
95
|
export { checkRecipeSourceDrift, computeSourceStructureSignature } from './source-drift.js';
|
|
89
96
|
export { enumerateTextParts, getGeneralTextPartNames } from './ooxml-parts.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/recipe/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/recipe/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type { Element } from '@xmldom/xmldom';
|
|
2
|
+
export interface PatchResult {
|
|
3
|
+
outputPath: string;
|
|
4
|
+
zeroMatchKeys: string[];
|
|
5
|
+
}
|
|
2
6
|
/**
|
|
3
7
|
* Patch a DOCX document by replacing bracketed placeholders with template tags.
|
|
4
8
|
* Uses a char_map algorithm to handle cross-run replacements where Word splits
|
|
5
9
|
* placeholder text across multiple XML run elements.
|
|
6
10
|
*
|
|
7
|
-
* Supports
|
|
11
|
+
* Supports two key syntax types:
|
|
8
12
|
* - Simple: "search text" → replaces all occurrences
|
|
9
|
-
* - Context: "
|
|
10
|
-
*
|
|
13
|
+
* - Context: "context > placeholder" → in tables, replaces placeholder in rows
|
|
14
|
+
* where label matches; in paragraphs, replaces first placeholder after context
|
|
11
15
|
*
|
|
12
|
-
* Processing order: context keys first,
|
|
16
|
+
* Processing order: context keys first, simple keys last.
|
|
13
17
|
* This ensures qualified rules claim their targets before simple rules sweep up the rest.
|
|
14
18
|
*/
|
|
15
19
|
export interface PatchOptions {
|
|
@@ -18,7 +22,7 @@ export interface PatchOptions {
|
|
|
18
22
|
* overriding any inherited placeholder styling (e.g. gray). */
|
|
19
23
|
replacementColor?: string;
|
|
20
24
|
}
|
|
21
|
-
export declare function patchDocument(inputPath: string, outputPath: string, replacements: Record<string, string>, options?: PatchOptions): Promise<
|
|
25
|
+
export declare function patchDocument(inputPath: string, outputPath: string, replacements: Record<string, string>, options?: PatchOptions): Promise<PatchResult>;
|
|
22
26
|
/**
|
|
23
27
|
* Get the text content of the first table cell (label cell) in the same row
|
|
24
28
|
* as the paragraph. Returns null if the paragraph is not inside a table cell.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patcher.d.ts","sourceRoot":"","sources":["../../../src/core/recipe/patcher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAY,OAAO,EAAQ,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"patcher.d.ts","sourceRoot":"","sources":["../../../src/core/recipe/patcher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAY,OAAO,EAAQ,MAAM,gBAAgB,CAAC;AA4B9D,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAcD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B;;oEAEgE;IAChE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,WAAW,CAAC,CAsItB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwB/D;AA0BD,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAO/C;AAuFD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAcvD"}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import AdmZip from 'adm-zip';
|
|
2
2
|
import { writeFileSync } from 'node:fs';
|
|
3
3
|
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
|
|
4
|
+
import { replaceParagraphTextRange, getParagraphText, SafeDocxError, } from '@usejunior/docx-core';
|
|
4
5
|
import { enumerateTextParts, getGeneralTextPartNames } from './ooxml-parts.js';
|
|
5
6
|
import { parseReplacementKey } from './replacement-keys.js';
|
|
6
7
|
const W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
8
|
+
/**
|
|
9
|
+
* Normalize smart/typographic quotes to ASCII equivalents for matching.
|
|
10
|
+
* Uses the same canonical quote set as verifier's normalizeText().
|
|
11
|
+
* This is a 1-to-1 character mapping so charMap positions remain valid.
|
|
12
|
+
*/
|
|
13
|
+
function normalizeQuotes(text) {
|
|
14
|
+
return text
|
|
15
|
+
.replace(/[\u2018\u2019\u2039\u203A]/g, "'")
|
|
16
|
+
.replace(/[\u201C\u201D\u201A\u201E\u00AB\u00BB]/g, '"');
|
|
17
|
+
}
|
|
7
18
|
/** Maximum replacement iterations per key per paragraph to prevent infinite loops. */
|
|
8
19
|
const MAX_REPLACEMENTS_PER_KEY = 200;
|
|
9
20
|
/**
|
|
@@ -23,30 +34,48 @@ export async function patchDocument(inputPath, outputPath, replacements, options
|
|
|
23
34
|
if (partNames.length === 0) {
|
|
24
35
|
throw new Error('No OOXML text parts found in DOCX');
|
|
25
36
|
}
|
|
26
|
-
// Parse and classify all keys
|
|
37
|
+
// Parse and classify all keys, normalizing smart quotes
|
|
27
38
|
const parsedKeys = [];
|
|
28
39
|
for (const [key, value] of Object.entries(replacements)) {
|
|
29
|
-
|
|
40
|
+
const parsed = parseReplacementKey(key, value);
|
|
41
|
+
// Normalize smart quotes in search text and context for matching
|
|
42
|
+
if (parsed.type === 'context') {
|
|
43
|
+
parsed.searchText = normalizeQuotes(parsed.searchText);
|
|
44
|
+
parsed.context = normalizeQuotes(parsed.context);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
parsed.searchText = normalizeQuotes(parsed.searchText);
|
|
48
|
+
}
|
|
49
|
+
parsedKeys.push(parsed);
|
|
50
|
+
}
|
|
51
|
+
// Deduplicate keys that collide after quote normalization
|
|
52
|
+
const seenSearchKeys = new Map();
|
|
53
|
+
const deduplicatedKeys = [];
|
|
54
|
+
for (const pk of parsedKeys) {
|
|
55
|
+
const dedupKey = pk.type === 'context' ? `${pk.context} > ${pk.searchText}` : pk.searchText;
|
|
56
|
+
const existing = seenSearchKeys.get(dedupKey);
|
|
57
|
+
if (existing) {
|
|
58
|
+
if (existing.value !== pk.value) {
|
|
59
|
+
console.warn(`Patcher: quote-normalized key collision for "${dedupKey}" with different values`);
|
|
60
|
+
}
|
|
61
|
+
// Skip duplicate
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
seenSearchKeys.set(dedupKey, pk);
|
|
65
|
+
deduplicatedKeys.push(pk);
|
|
66
|
+
}
|
|
30
67
|
}
|
|
31
68
|
// Separate by type and sort
|
|
32
|
-
const contextKeys =
|
|
69
|
+
const contextKeys = deduplicatedKeys
|
|
33
70
|
.filter((k) => k.type === 'context')
|
|
34
71
|
.sort((a, b) => b.searchText.length - a.searchText.length);
|
|
35
|
-
const
|
|
36
|
-
.filter((k) => k.type === 'nth')
|
|
37
|
-
.sort((a, b) => {
|
|
38
|
-
// Group by searchText, then by N ascending
|
|
39
|
-
if (a.searchText !== b.searchText)
|
|
40
|
-
return b.searchText.length - a.searchText.length;
|
|
41
|
-
return a.n - b.n;
|
|
42
|
-
});
|
|
43
|
-
const simpleKeys = parsedKeys
|
|
72
|
+
const simpleKeys = deduplicatedKeys
|
|
44
73
|
.filter((k) => k.type === 'simple')
|
|
45
74
|
.sort((a, b) => b.searchText.length - a.searchText.length);
|
|
46
75
|
// Track which parts we modify so we can rebuild the zip cleanly
|
|
47
76
|
const modifiedParts = new Map();
|
|
48
|
-
//
|
|
49
|
-
const
|
|
77
|
+
// Collect all pre-patch paragraph text for zero-match detection
|
|
78
|
+
const preMatchTexts = [];
|
|
50
79
|
for (const partName of partNames) {
|
|
51
80
|
const entry = zip.getEntry(partName);
|
|
52
81
|
if (!entry)
|
|
@@ -54,53 +83,38 @@ export async function patchDocument(inputPath, outputPath, replacements, options
|
|
|
54
83
|
const xml = entry.getData().toString('utf-8');
|
|
55
84
|
const doc = parser.parseFromString(xml, 'text/xml');
|
|
56
85
|
const allParagraphs = doc.getElementsByTagNameNS(W_NS, 'p');
|
|
86
|
+
// Collect paragraph text before any replacements for zero-match detection
|
|
87
|
+
for (let i = 0; i < allParagraphs.length; i++) {
|
|
88
|
+
const text = getParagraphText(allParagraphs[i]);
|
|
89
|
+
if (!text)
|
|
90
|
+
continue;
|
|
91
|
+
preMatchTexts.push(normalizeQuotes(text));
|
|
92
|
+
}
|
|
57
93
|
// Phase 1: Context keys
|
|
58
94
|
for (const ck of contextKeys) {
|
|
59
95
|
for (let i = 0; i < allParagraphs.length; i++) {
|
|
60
96
|
const para = allParagraphs[i];
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
97
|
+
const paraText = getParagraphText(para);
|
|
98
|
+
if (!paraText)
|
|
63
99
|
continue;
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
100
|
+
const normalizedText = normalizeQuotes(paraText);
|
|
101
|
+
if (!normalizedText.includes(ck.searchText))
|
|
66
102
|
continue;
|
|
67
103
|
const rowContext = getTableRowContext(para);
|
|
68
|
-
if (rowContext
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
nthGroups.set(nk.searchText, group);
|
|
79
|
-
}
|
|
80
|
-
for (const [searchText, group] of nthGroups) {
|
|
81
|
-
// Build a map from occurrence number → value for this group
|
|
82
|
-
const nthMap = new Map();
|
|
83
|
-
for (const nk of group) {
|
|
84
|
-
nthMap.set(nk.n, nk.value);
|
|
85
|
-
}
|
|
86
|
-
let counter = nthCounters.get(searchText) ?? 0;
|
|
87
|
-
for (let i = 0; i < allParagraphs.length; i++) {
|
|
88
|
-
const para = allParagraphs[i];
|
|
89
|
-
const runs = getRunElements(para);
|
|
90
|
-
if (runs.length === 0)
|
|
91
|
-
continue;
|
|
92
|
-
const { fullText } = buildCharMap(runs);
|
|
93
|
-
if (!fullText.includes(searchText))
|
|
94
|
-
continue;
|
|
95
|
-
counter++;
|
|
96
|
-
const value = nthMap.get(counter);
|
|
97
|
-
if (value !== undefined) {
|
|
98
|
-
replaceInParagraphOnce(para, searchText, value, options?.replacementColor);
|
|
104
|
+
if (rowContext !== null) {
|
|
105
|
+
const normalizedRowContext = normalizeQuotes(rowContext);
|
|
106
|
+
if (!normalizedRowContext.includes(ck.context))
|
|
107
|
+
continue;
|
|
108
|
+
replaceInParagraph(para, { [ck.searchText]: ck.value }, [ck.searchText], options?.replacementColor);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
if (!normalizedText.includes(ck.context))
|
|
112
|
+
continue;
|
|
113
|
+
replaceFirstAfterContext(para, ck.searchText, ck.context, ck.value, options?.replacementColor);
|
|
99
114
|
}
|
|
100
115
|
}
|
|
101
|
-
nthCounters.set(searchText, counter);
|
|
102
116
|
}
|
|
103
|
-
// Phase
|
|
117
|
+
// Phase 2: Simple keys
|
|
104
118
|
if (simpleKeys.length > 0) {
|
|
105
119
|
const simpleReplacements = {};
|
|
106
120
|
const simpleSortedKeys = [];
|
|
@@ -114,6 +128,15 @@ export async function patchDocument(inputPath, outputPath, replacements, options
|
|
|
114
128
|
}
|
|
115
129
|
modifiedParts.set(partName, Buffer.from(serializer.serializeToString(doc), 'utf-8'));
|
|
116
130
|
}
|
|
131
|
+
// Compute zero-match keys by checking pre-patch paragraph text
|
|
132
|
+
const preMatchFullText = preMatchTexts.join('\n');
|
|
133
|
+
const zeroMatchKeys = [];
|
|
134
|
+
for (const pk of deduplicatedKeys) {
|
|
135
|
+
const keyLabel = pk.type === 'context' ? `${pk.context} > ${pk.searchText}` : pk.searchText;
|
|
136
|
+
if (!preMatchFullText.includes(pk.searchText)) {
|
|
137
|
+
zeroMatchKeys.push(keyLabel);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
117
140
|
// Rebuild the zip from scratch using addFile() to avoid adm-zip data
|
|
118
141
|
// descriptor issues. Some DOCX files use streaming (bit 3) flags which
|
|
119
142
|
// adm-zip's updateFile/writeZip/toBuffer handle incorrectly.
|
|
@@ -123,7 +146,7 @@ export async function patchDocument(inputPath, outputPath, replacements, options
|
|
|
123
146
|
outZip.addFile(entry.entryName, data);
|
|
124
147
|
}
|
|
125
148
|
writeFileSync(outputPath, outZip.toBuffer());
|
|
126
|
-
return outputPath;
|
|
149
|
+
return { outputPath, zeroMatchKeys };
|
|
127
150
|
}
|
|
128
151
|
/**
|
|
129
152
|
* Get the text content of the first table cell (label cell) in the same row
|
|
@@ -288,44 +311,16 @@ export function isRunSafeToRemove(run) {
|
|
|
288
311
|
return true;
|
|
289
312
|
}
|
|
290
313
|
/**
|
|
291
|
-
*
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
i++;
|
|
297
|
-
return i;
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Compute the length of the common suffix between two strings,
|
|
301
|
-
* not overlapping with a known common prefix.
|
|
302
|
-
*/
|
|
303
|
-
function commonSuffixLen(a, b, prefixLen) {
|
|
304
|
-
let i = 0;
|
|
305
|
-
while (i < a.length - prefixLen &&
|
|
306
|
-
i < b.length - prefixLen &&
|
|
307
|
-
a[a.length - 1 - i] === b[b.length - 1 - i])
|
|
308
|
-
i++;
|
|
309
|
-
return i;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Perform a single replacement of searchText with value in the paragraph.
|
|
313
|
-
* Does NOT loop — replaces only the first match found.
|
|
314
|
-
* Used for nth-occurrence keys where the value may contain the searchText.
|
|
314
|
+
* Legacy charMap-based splice: replace `searchTextLength` characters starting at
|
|
315
|
+
* `matchStart` with `value`, cleaning up empty runs afterwards.
|
|
316
|
+
* Used as a fallback when replaceParagraphTextRange throws UNSAFE_CONTAINER_BOUNDARY
|
|
317
|
+
* (e.g. replacements spanning across hyperlinks or SDTs) or UNSUPPORTED_EDIT
|
|
318
|
+
* (e.g. replacement spans field results).
|
|
315
319
|
*/
|
|
316
|
-
function
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const rebuilt = buildCharMap(runs);
|
|
321
|
-
const matchStart = rebuilt.fullText.indexOf(searchText);
|
|
322
|
-
if (matchStart === -1)
|
|
323
|
-
return;
|
|
324
|
-
// Always use full replacement for single-shot mode
|
|
325
|
-
const start = matchStart;
|
|
326
|
-
const end = matchStart + searchText.length;
|
|
327
|
-
const firstEntry = rebuilt.charMap[start];
|
|
328
|
-
const lastEntry = rebuilt.charMap[end - 1];
|
|
320
|
+
function replaceAtPositionLegacy(runs, charMap, matchStart, searchTextLength, value, replacementColor) {
|
|
321
|
+
const end = matchStart + searchTextLength;
|
|
322
|
+
const firstEntry = charMap[matchStart];
|
|
323
|
+
const lastEntry = charMap[end - 1];
|
|
329
324
|
if (firstEntry.runIndex === lastEntry.runIndex) {
|
|
330
325
|
const runText = getRunText(runs[firstEntry.runIndex]);
|
|
331
326
|
setRunText(runs[firstEntry.runIndex], runText.slice(0, firstEntry.charOffset) + value + runText.slice(lastEntry.charOffset + 1));
|
|
@@ -349,74 +344,126 @@ function replaceInParagraphOnce(para, searchText, value, replacementColor) {
|
|
|
349
344
|
}
|
|
350
345
|
}
|
|
351
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Perform a single replacement of searchText with value in the paragraph.
|
|
349
|
+
* Does NOT loop — replaces only the first match found.
|
|
350
|
+
* Uses docx-core's replaceParagraphTextRange for correct formatting preservation,
|
|
351
|
+
* with a legacy charMap fallback for container-boundary cases (hyperlinks, SDTs).
|
|
352
|
+
*/
|
|
353
|
+
function replaceInParagraphOnce(para, searchText, value, replacementColor) {
|
|
354
|
+
const fullText = getParagraphText(para);
|
|
355
|
+
const matchStart = normalizeQuotes(fullText).indexOf(searchText);
|
|
356
|
+
if (matchStart === -1)
|
|
357
|
+
return;
|
|
358
|
+
try {
|
|
359
|
+
replaceParagraphTextRange(para, matchStart, matchStart + searchText.length, replacementColor
|
|
360
|
+
? [{ text: value, addRunProps: { color: replacementColor } }]
|
|
361
|
+
: value);
|
|
362
|
+
}
|
|
363
|
+
catch (e) {
|
|
364
|
+
if (e instanceof SafeDocxError && (e.code === 'UNSAFE_CONTAINER_BOUNDARY' || e.code === 'UNSUPPORTED_EDIT')) {
|
|
365
|
+
const runs = getRunElements(para);
|
|
366
|
+
const { fullText: legacyText, charMap } = buildCharMap(runs);
|
|
367
|
+
const legacyPos = normalizeQuotes(legacyText).indexOf(searchText);
|
|
368
|
+
if (legacyPos === -1)
|
|
369
|
+
return;
|
|
370
|
+
replaceAtPositionLegacy(runs, charMap, legacyPos, searchText.length, value, replacementColor);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
throw e;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Replace the first occurrence of searchText that appears AFTER contextText
|
|
379
|
+
* in the same paragraph. Each call is self-contained — no chaining or order dependency.
|
|
380
|
+
* Uses docx-core's replaceParagraphTextRange with legacy charMap fallback.
|
|
381
|
+
*/
|
|
382
|
+
function replaceFirstAfterContext(para, searchText, contextText, value, replacementColor) {
|
|
383
|
+
const fullText = getParagraphText(para);
|
|
384
|
+
const normalizedFull = normalizeQuotes(fullText);
|
|
385
|
+
const ctxPos = normalizedFull.indexOf(contextText);
|
|
386
|
+
if (ctxPos === -1)
|
|
387
|
+
return;
|
|
388
|
+
const matchStart = normalizedFull.indexOf(searchText, ctxPos + contextText.length);
|
|
389
|
+
if (matchStart === -1)
|
|
390
|
+
return;
|
|
391
|
+
try {
|
|
392
|
+
replaceParagraphTextRange(para, matchStart, matchStart + searchText.length, replacementColor
|
|
393
|
+
? [{ text: value, addRunProps: { color: replacementColor } }]
|
|
394
|
+
: value);
|
|
395
|
+
}
|
|
396
|
+
catch (e) {
|
|
397
|
+
if (e instanceof SafeDocxError && (e.code === 'UNSAFE_CONTAINER_BOUNDARY' || e.code === 'UNSUPPORTED_EDIT')) {
|
|
398
|
+
const runs = getRunElements(para);
|
|
399
|
+
const { fullText: legacyText, charMap } = buildCharMap(runs);
|
|
400
|
+
const normalizedLegacy = normalizeQuotes(legacyText);
|
|
401
|
+
const legacyCtxPos = normalizedLegacy.indexOf(contextText);
|
|
402
|
+
if (legacyCtxPos === -1)
|
|
403
|
+
return;
|
|
404
|
+
const legacyPos = normalizedLegacy.indexOf(searchText, legacyCtxPos + contextText.length);
|
|
405
|
+
if (legacyPos === -1)
|
|
406
|
+
return;
|
|
407
|
+
replaceAtPositionLegacy(runs, charMap, legacyPos, searchText.length, value, replacementColor);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
throw e;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
352
414
|
function replaceInParagraph(para, replacements, sortedKeys, replacementColor) {
|
|
353
|
-
|
|
354
|
-
|
|
415
|
+
// Quick check: skip if no keys match
|
|
416
|
+
const initialText = getParagraphText(para);
|
|
417
|
+
if (!initialText)
|
|
355
418
|
return;
|
|
356
|
-
const
|
|
357
|
-
if (!sortedKeys.some((key) =>
|
|
419
|
+
const normalizedInitial = normalizeQuotes(initialText);
|
|
420
|
+
if (!sortedKeys.some((key) => normalizedInitial.includes(key)))
|
|
358
421
|
return;
|
|
359
422
|
for (const key of sortedKeys) {
|
|
360
|
-
let rebuilt = buildCharMap(runs);
|
|
361
423
|
let iterations = 0;
|
|
362
|
-
|
|
424
|
+
let paraText = getParagraphText(para);
|
|
425
|
+
while (normalizeQuotes(paraText).includes(key)) {
|
|
363
426
|
iterations++;
|
|
364
427
|
if (iterations > MAX_REPLACEMENTS_PER_KEY) {
|
|
365
428
|
throw new Error(`Patcher: exceeded ${MAX_REPLACEMENTS_PER_KEY} replacements for key "${key}" ` +
|
|
366
429
|
`in a single paragraph. This usually means the replacement value contains ` +
|
|
367
430
|
`the search key, creating an infinite loop.`);
|
|
368
431
|
}
|
|
369
|
-
const prevText =
|
|
370
|
-
const matchStart =
|
|
432
|
+
const prevText = paraText;
|
|
433
|
+
const matchStart = normalizeQuotes(paraText).indexOf(key);
|
|
371
434
|
const replacement = replacements[key];
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
let start;
|
|
377
|
-
let end;
|
|
378
|
-
let replText;
|
|
379
|
-
if (cpLen + csLen >= key.length || cpLen + csLen >= replacement.length) {
|
|
380
|
-
// No useful common prefix/suffix — full replacement (current behavior)
|
|
381
|
-
start = matchStart;
|
|
382
|
-
end = matchStart + key.length;
|
|
383
|
-
replText = replacement;
|
|
435
|
+
try {
|
|
436
|
+
replaceParagraphTextRange(para, matchStart, matchStart + key.length, replacementColor
|
|
437
|
+
? [{ text: replacement, addRunProps: { color: replacementColor } }]
|
|
438
|
+
: replacement);
|
|
384
439
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
else {
|
|
398
|
-
const firstRunText = getRunText(runs[firstEntry.runIndex]);
|
|
399
|
-
setRunText(runs[firstEntry.runIndex], firstRunText.slice(0, firstEntry.charOffset) + replText);
|
|
400
|
-
const lastRunText = getRunText(runs[lastEntry.runIndex]);
|
|
401
|
-
setRunText(runs[lastEntry.runIndex], lastRunText.slice(lastEntry.charOffset + 1));
|
|
402
|
-
for (let mid = firstEntry.runIndex + 1; mid < lastEntry.runIndex; mid++) {
|
|
403
|
-
setRunText(runs[mid], '');
|
|
440
|
+
catch (e) {
|
|
441
|
+
if (e instanceof SafeDocxError && (e.code === 'UNSAFE_CONTAINER_BOUNDARY' || e.code === 'UNSUPPORTED_EDIT')) {
|
|
442
|
+
// Fallback: use legacy charMap splice for this specific match
|
|
443
|
+
const runs = getRunElements(para);
|
|
444
|
+
const { fullText: legacyText, charMap } = buildCharMap(runs);
|
|
445
|
+
const legacyPos = normalizeQuotes(legacyText).indexOf(key);
|
|
446
|
+
if (legacyPos === -1)
|
|
447
|
+
break;
|
|
448
|
+
replaceAtPositionLegacy(runs, charMap, legacyPos, key.length, replacement, replacementColor);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
throw e;
|
|
404
452
|
}
|
|
405
453
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
rebuilt = buildCharMap(runs);
|
|
454
|
+
// Re-read paragraph text after DOM modification
|
|
455
|
+
paraText = getParagraphText(para);
|
|
410
456
|
// Progress guard: if text didn't change, we're stuck
|
|
411
|
-
if (
|
|
457
|
+
if (paraText === prevText) {
|
|
412
458
|
throw new Error(`Patcher: no progress replacing key "${key}" — replacement value may contain the search key.`);
|
|
413
459
|
}
|
|
414
460
|
}
|
|
415
461
|
}
|
|
416
462
|
// Sweep: remove empty runs that are safe to remove (no drawings, fields, etc.)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
463
|
+
const finalRuns = getRunElements(para);
|
|
464
|
+
for (let i = finalRuns.length - 1; i >= 0; i--) {
|
|
465
|
+
if (getRunText(finalRuns[i]) === '' && isRunSafeToRemove(finalRuns[i])) {
|
|
466
|
+
finalRuns[i].parentNode?.removeChild(finalRuns[i]);
|
|
420
467
|
}
|
|
421
468
|
}
|
|
422
469
|
}
|