From 493553ee94215a00578092173a126599b664ff7d Mon Sep 17 00:00:00 2001 From: Yousef Wadi Date: Mon, 2 Jun 2025 14:36:40 +0300 Subject: [PATCH 1/2] feat: Implement watermark text replacement and update snapshots --- .gitignore | 1 + .../__snapshots__/templating.test.ts.snap | 188 +++++++++++++++++ src/__tests__/fixtures/watermarked.docx | Bin 0 -> 23381 bytes src/__tests__/templating.test.ts | 76 +++++++ src/processTemplate.ts | 197 ++++++++++++++---- 5 files changed, 418 insertions(+), 44 deletions(-) create mode 100644 src/__tests__/fixtures/watermarked.docx diff --git a/.gitignore b/.gitignore index 18a180db..31332639 100755 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ coverage # Project-specific **/~$*.docx lib +package-lock.json diff --git a/src/__tests__/__snapshots__/templating.test.ts.snap b/src/__tests__/__snapshots__/templating.test.ts.snap index 0eff73f8..3265886c 100644 --- a/src/__tests__/__snapshots__/templating.test.ts.snap +++ b/src/__tests__/__snapshots__/templating.test.ts.snap @@ -29990,6 +29990,100 @@ AAAAAAAAAAAAkQQAAGRycy9kb3ducmV2LnhtbFBLBQYAAAAABAAEAPMAAACeBQAAAAA= } `; +exports[`noSandbox Template processing Processes INS command in watermark: debug_raw_word_header1_xml 1`] = ` +" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`noSandbox Template processing Processes INS command in watermark: watermark in word_header1.xml 1`] = ` +" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + exports[`noSandbox Template processing avoids confusion between variable name and built-in command 1`] = ` { "_attrs": { @@ -62646,6 +62740,100 @@ AAAAAAAAAAAAkQQAAGRycy9kb3ducmV2LnhtbFBLBQYAAAAABAAEAPMAAACeBQAAAAA= } `; +exports[`sandbox Template processing Processes INS command in watermark: debug_raw_word_header1_xml 1`] = ` +" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + +exports[`sandbox Template processing Processes INS command in watermark: watermark in word_header1.xml 1`] = ` +" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +" +`; + exports[`sandbox Template processing avoids confusion between variable name and built-in command 1`] = ` { "_attrs": { diff --git a/src/__tests__/fixtures/watermarked.docx b/src/__tests__/fixtures/watermarked.docx new file mode 100644 index 0000000000000000000000000000000000000000..0299746b03f76baba5f56757003fb1e9be838c65 GIT binary patch literal 23381 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 DAJTOs literal 0 HcmV?d00001 diff --git a/src/__tests__/templating.test.ts b/src/__tests__/templating.test.ts index b656f6a0..9d08bbe0 100755 --- a/src/__tests__/templating.test.ts +++ b/src/__tests__/templating.test.ts @@ -109,6 +109,82 @@ Morbi dignissim consequat ex, non finibus est faucibus sodales. Integer sed just expect(result).toMatchSnapshot(); }); + it('Processes INS command in watermark', async () => { + const template = await fs.promises.readFile( + path.join(__dirname, 'fixtures', 'watermarked.docx') + ); + const data = { companyName: 'Windsurf Inc.' }; + const report = await createReport({ + noSandbox, + template, + data, + cmdDelimiter: ['+++', '+++'], + }); + + const zip = await JSZip.loadAsync(report); + + // --- Debug Snapshot Start --- + // Snapshot a common header file to see its content post-processing. + let actualDebugContentForSnapshot = + 'Neither word/headerdefault.xml nor word/header1.xml found for debug snapshot.'; + let snapshotNameForDebug = 'debug_header_not_found'; + + const defaultHeaderFile = zip.file('word/headerdefault.xml'); + if (defaultHeaderFile) { + actualDebugContentForSnapshot = await defaultHeaderFile.async( + 'string' + ); + snapshotNameForDebug = 'debug_raw_word_headerdefault_xml'; + } else { + const header1File = zip.file('word/header1.xml'); + if (header1File) { + actualDebugContentForSnapshot = await header1File.async('string'); + snapshotNameForDebug = 'debug_raw_word_header1_xml'; + } + } + expect(actualDebugContentForSnapshot).toMatchSnapshot( + snapshotNameForDebug + ); + // --- Debug Snapshot End --- + + const headerFilesToCheck = [ + 'word/header1.xml', // Common/default + 'word/header2.xml', + 'word/header3.xml', + 'word/headerdefault.xml', // Explicit default header + 'word/headerfirst.xml', // First page header + 'word/headereven.xml', // Even page header + ]; + + let foundHeaderContent: string | undefined; + let foundHeaderName: string | undefined; + + for (const headerFile of headerFilesToCheck) { + const file = zip.file(headerFile); + if (file) { + const currentHeaderContent = await file.async('string'); + if (currentHeaderContent.includes('Windsurf Inc.')) { + foundHeaderContent = currentHeaderContent; + foundHeaderName = headerFile; + break; // Found the relevant header + } + } + } + + // Assert that a header containing the watermark was found + expect(foundHeaderContent).toBeDefined(); + // Assert that the found content indeed contains the processed text + // Note: TypeScript might complain foundHeaderContent could be undefined here if expect above wasn't enough. + // We can add a check or use a non-null assertion operator if needed, but expect should fail first. + expect(foundHeaderContent).toContain('Windsurf Inc.'); + + // Take a snapshot of the found header content + // The foundHeaderName will be undefined if the loop didn't find anything, but expect(foundHeaderContent).toBeDefined() should catch that. + expect(foundHeaderContent).toMatchSnapshot( + `watermark in ${foundHeaderName?.replace(/\//g, '_')}` + ); + }); + it('05 Processes 1-level FOR loops', async () => { const template = await fs.promises.readFile( path.join(__dirname, 'fixtures', 'for1.docx') diff --git a/src/processTemplate.ts b/src/processTemplate.ts index 9d8d72d2..8bbdc059 100755 --- a/src/processTemplate.ts +++ b/src/processTemplate.ts @@ -36,6 +36,118 @@ import { } from './errors'; import { logger } from './debug'; +/* + * Processes a raw string (from a text node or an attribute) for template commands. + * It splits the string by command delimiters, executes commands via onCommand, + * and reconstructs the string with command results. + */ +async function processStringContent( + rawString: string, + data: ReportData | undefined, + nodeForCommandContext: Node, // The node to pass to onCommand for contextual information + ctx: Context, + onCommand: CommandProcessor +): Promise { + const { cmdDelimiter, failFast } = ctx.options; + if (rawString == null || rawString === '') return ''; + + const segments = rawString + .split(cmdDelimiter[0]) + .map(s => s.split(cmdDelimiter[1])) + .reduce((x, y) => x.concat(y)); + + let outText = ''; + const errors: Error[] = []; + + const isTextInWT = + nodeForCommandContext._fTextNode && + nodeForCommandContext._parent && + !nodeForCommandContext._parent._fTextNode && + nodeForCommandContext._parent._tag === 'w:t'; + + // If processing an attribute (not text in w:t), we need to save and restore ctx.cmd and ctx.fCmd + // and initialize them for this specific string processing task. + // If processing text in w:t, we use ctx.cmd and ctx.fCmd as they are, allowing them to carry state + // across multiple calls to processStringContent (e.g. for commands spanning multiple text nodes). + const shouldIsolateContext = !isTextInWT; + let originalSavedCtxCmd = ''; + let originalSavedCtxFCmd = false; + + if (shouldIsolateContext) { + originalSavedCtxCmd = ctx.cmd; + originalSavedCtxFCmd = ctx.fCmd; + ctx.cmd = ''; // Clear for isolated processing + // Determine initial fCmd for the attribute string. + // If it starts with a delimiter, the first segment is empty (processed as text), then command mode. + ctx.fCmd = false; // Assume first part is text, unless string starts with {{ and first segment is empty + if ( + segments.length > 0 && + segments[0] === '' && + rawString.startsWith(cmdDelimiter[0]) + ) { + // String like "{{cmd}}...". First segment "" is text (fCmd=false). Next is cmd (fCmd=true). + // So, initial ctx.fCmd for the loop should be false to handle the first empty segment as text. + // The toggle logic within the loop will then set it to true for the actual command part. + } else { + // String like "text{{cmd}}..." or just "text". First segment is text. + } + } + // If !shouldIsolateContext (i.e., isTextInWT), ctx.cmd and ctx.fCmd are used as is from the global context. + + for (let idx = 0; idx < segments.length; idx++) { + const segment = segments[idx]; + + if (ctx.fCmd) { + // Current segment is part of a command + ctx.cmd += segment; + if (isTextInWT) appendTextToTagBuffers(segment, ctx, { fCmd: true }); + } else { + // Current segment is text + if (!isLoopExploring(ctx)) outText += segment; + if (isTextInWT) appendTextToTagBuffers(segment, ctx, { fCmd: false }); + } + + if (idx < segments.length - 1) { + // If there's another segment, a mode switch occurs + if (ctx.fCmd) { + // If we just processed a command segment + if (isTextInWT) + appendTextToTagBuffers(cmdDelimiter[1], ctx, { fCmd: true }); // Closing delimiter + + const cmdResultText = await onCommand(data, nodeForCommandContext, ctx); + // onCommand is expected to clear ctx.cmd. + if (cmdResultText != null) { + if (typeof cmdResultText === 'string') { + if (!isLoopExploring(ctx)) outText += cmdResultText; + if (isTextInWT) { + appendTextToTagBuffers(cmdResultText, ctx, { + fCmd: false, + fInsertedText: true, + }); + } + } else { + if (failFast) throw cmdResultText; + errors.push(cmdResultText); + } + } + } else { + // If we just processed a text segment and a command follows + if (isTextInWT) + appendTextToTagBuffers(cmdDelimiter[0], ctx, { fCmd: true }); // Opening delimiter + } + ctx.fCmd = !ctx.fCmd; // Toggle mode for the next segment + } + } + + if (shouldIsolateContext) { + ctx.cmd = originalSavedCtxCmd; + ctx.fCmd = originalSavedCtxFCmd; + } + + if (errors.length > 0) return errors; + return outText; +} + export function newContext( options: CreateReportOptions, imageAndShapeIdIncrement = 0 @@ -497,6 +609,45 @@ export async function walkTemplate( // Execute the move in the output tree nodeOut = newNode; + + // Process 'string' attribute of v:textpath for watermarks + if (!nodeOut._fTextNode && nodeOut._tag === 'v:textpath') { + const textPathNodeOut = nodeOut as NonTextNode; // textPathNodeOut is the node in the output tree + // We need to check attributes on nodeIn (original template node) as well, + // though processing happens on nodeOut's attribute which is initially a clone of nodeIn's. + if ( + textPathNodeOut._attrs && + typeof textPathNodeOut._attrs.string === 'string' + ) { + const originalStringAttr = textPathNodeOut._attrs.string as string; + const cmdDelimiter = ctx.options.cmdDelimiter; + + // Only process if command delimiters are potentially present to avoid unnecessary processing + if ( + originalStringAttr.includes(cmdDelimiter[0]) || + originalStringAttr.includes(cmdDelimiter[1]) + ) { + // Use nodeIn (original template node) for command context, as commands might depend on original structure. + const processedAttribute = await processStringContent( + originalStringAttr, + data, + nodeIn, + ctx, + processor // processor is the onCommand function passed to walkTemplate + ); + + if (typeof processedAttribute === 'string') { + // Update the attribute on the output node + textPathNodeOut._attrs.string = processedAttribute; + } else if (Array.isArray(processedAttribute)) { + // errors from processStringContent + errors.push(...processedAttribute); + // Optionally, decide how to handle attribute processing errors, e.g., keep original or clear + // For now, if errors occur, the attribute might remain unchanged or partially processed depending on processStringContent behavior + } + } + } + } } // JUMP to the target level of the tree. @@ -559,50 +710,8 @@ const processText = async ( ctx: Context, onCommand: CommandProcessor ): Promise => { - const { cmdDelimiter, failFast } = ctx.options; - const text = node._text; - if (text == null || text === '') return ''; - const segments = text - .split(cmdDelimiter[0]) - .map(s => s.split(cmdDelimiter[1])) - .reduce((x, y) => x.concat(y)); - let outText = ''; - const errors: Error[] = []; - for (let idx = 0; idx < segments.length; idx++) { - // Include the separators in the `buffers` field (used for deleting paragraphs if appropriate) - if (idx > 0) appendTextToTagBuffers(cmdDelimiter[0], ctx, { fCmd: true }); - - // Append segment either to the `ctx.cmd` buffer (to be executed), if we are in "command mode", - // or to the output text - const segment = segments[idx]; - // logger.debug(`Token: '${segment}' (${ctx.fCmd})`); - if (ctx.fCmd) ctx.cmd += segment; - else if (!isLoopExploring(ctx)) outText += segment; - appendTextToTagBuffers(segment, ctx, { fCmd: ctx.fCmd }); - - // If there are more segments, execute the command (if we are in "command mode"), - // and toggle "command mode" - if (idx < segments.length - 1) { - if (ctx.fCmd) { - const cmdResultText = await onCommand(data, node, ctx); - if (cmdResultText != null) { - if (typeof cmdResultText === 'string') { - outText += cmdResultText; - appendTextToTagBuffers(cmdResultText, ctx, { - fCmd: false, - fInsertedText: true, - }); - } else { - if (failFast) throw cmdResultText; - errors.push(cmdResultText); - } - } - } - ctx.fCmd = !ctx.fCmd; - } - } - if (errors.length > 0) return errors; - return outText; + // Delegate to processStringContent. The TextNode itself provides context for commands within its text. + return processStringContent(node._text, data, node, ctx, onCommand); }; // ========================================== From 3831d5761bb28c9cff41efdd256db4e27b6bf1a0 Mon Sep 17 00:00:00 2001 From: Yousef Wadi Date: Tue, 3 Jun 2025 09:47:43 +0300 Subject: [PATCH 2/2] Refactor: Remove redundant code and update .gitignore --- .gitignore | 1 - src/processTemplate.ts | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/.gitignore b/.gitignore index 31332639..18a180db 100755 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,3 @@ coverage # Project-specific **/~$*.docx lib -package-lock.json diff --git a/src/processTemplate.ts b/src/processTemplate.ts index 8bbdc059..25204566 100755 --- a/src/processTemplate.ts +++ b/src/processTemplate.ts @@ -80,17 +80,6 @@ async function processStringContent( // Determine initial fCmd for the attribute string. // If it starts with a delimiter, the first segment is empty (processed as text), then command mode. ctx.fCmd = false; // Assume first part is text, unless string starts with {{ and first segment is empty - if ( - segments.length > 0 && - segments[0] === '' && - rawString.startsWith(cmdDelimiter[0]) - ) { - // String like "{{cmd}}...". First segment "" is text (fCmd=false). Next is cmd (fCmd=true). - // So, initial ctx.fCmd for the loop should be false to handle the first empty segment as text. - // The toggle logic within the loop will then set it to true for the actual command part. - } else { - // String like "text{{cmd}}..." or just "text". First segment is text. - } } // If !shouldIsolateContext (i.e., isTextInWT), ctx.cmd and ctx.fCmd are used as is from the global context.