From d2404564ae272e70f1e12d278103d5903d37e4d1 Mon Sep 17 00:00:00 2001 From: MarsLuay <70299537+MarsLuay@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:35:55 +0000 Subject: [PATCH 1/2] perf(pptx): optimize slide text extraction speed by avoiding intermediate arrays\n\nReplaces `getDescendants(...).map(...).join(...)` with a direct loop over `getElementsByTagNameNS('*', 't')` to avoid creating intermediate strings and allocations for the array mappings when extracting search index text. --- src/powerpoint/findSearchIndex.ts | 11 ++- src/powerpoint/findSearchIndex.ts.orig | 110 +++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/powerpoint/findSearchIndex.ts.orig diff --git a/src/powerpoint/findSearchIndex.ts b/src/powerpoint/findSearchIndex.ts index faedfdb..442daf7 100644 --- a/src/powerpoint/findSearchIndex.ts +++ b/src/powerpoint/findSearchIndex.ts @@ -1,6 +1,5 @@ import type { PowerPointFindMatch } from './types'; import { - getDescendants, getElementChildren, getShapeChildren, getShapeTree, @@ -33,9 +32,13 @@ export function createFindSearchIndexSlideFromOoxml( const shapeMatches: PowerPointFindMatch[] = []; const lowerShapeTexts: string[] = []; const addShape = (shape: Element, shapeIndex: number): void => { - const text = normalizeSearchText( - getDescendants(shape, 't').map((element) => element.textContent ?? '').join(''), - ); + let rawText = ''; + const textElements = shape.getElementsByTagNameNS('*', 't'); + for (let i = 0; i < textElements.length; i++) { + const content = textElements[i]?.textContent; + if (content) rawText += content; + } + const text = normalizeSearchText(rawText); if (text) { shapeMatches.push({ slideIndex, shapeIndex, text }); lowerShapeTexts.push(text.toLocaleLowerCase()); diff --git a/src/powerpoint/findSearchIndex.ts.orig b/src/powerpoint/findSearchIndex.ts.orig new file mode 100644 index 0000000..e9743e3 --- /dev/null +++ b/src/powerpoint/findSearchIndex.ts.orig @@ -0,0 +1,110 @@ +import type { PowerPointFindMatch } from './types'; +import { + getDescendants, + getElementChildren, + getShapeChildren, + getShapeTree, + parseXml, + SHAPE_ELEMENT_NAMES, +} from './ooxmlXml'; + +function normalizeSearchText(value: string): string { + return value.replace(/\s+/g, ' ').trim(); +} + +export interface PowerPointFindSearchIndexSlide { + slideIndex: number; + shapeMatches: PowerPointFindMatch[]; + fallbackText: string; + lowerShapeTexts?: string[]; + lowerFallbackText?: string; +} + +/** + * Extracts searchable text from slide OOXML without invoking the SVG renderer. + * Rendering every slide just to search its text makes Find unusably slow for + * large poster templates and image-heavy decks. + */ +export function createFindSearchIndexSlideFromOoxml( + slideIndex: number, + slideXml: string, +): PowerPointFindSearchIndexSlide { + const slideDocument = parseXml(slideXml, `slide ${slideIndex + 1}`); + const shapeMatches: PowerPointFindMatch[] = []; + const lowerShapeTexts: string[] = []; + const addShape = (shape: Element, shapeIndex: number): void => { + let rawText = ''; + const textElements = shape.getElementsByTagNameNS('*', 't'); + for (let i = 0; i < textElements.length; i++) { + const content = textElements[i]?.textContent; + if (content) rawText += content; + } + const text = normalizeSearchText(rawText); + if (text) { + shapeMatches.push({ slideIndex, shapeIndex, text }); + lowerShapeTexts.push(text.toLocaleLowerCase()); + } + }; + + const shapes = getShapeChildren(getShapeTree(slideDocument)); + shapes.forEach((shape, shapeIndex) => { + addShape(shape, shapeIndex); + if (shape.localName !== 'grpSp') return; + + getElementChildren(shape) + .filter((child) => SHAPE_ELEMENT_NAMES.has(child.localName)) + .forEach((child, childIndex) => addShape(child, (shapeIndex * 1000) + childIndex)); + }); + + const fallbackText = normalizeSearchText(slideDocument.documentElement.textContent ?? ''); + + return { + slideIndex, + shapeMatches, + fallbackText, + lowerShapeTexts, + lowerFallbackText: fallbackText.toLocaleLowerCase(), + }; +} + +/** + * Filters already-extracted deck text. The fallback preserves the renderer + * behavior for text that is not inside an editable shape group. + */ +export function collectFindMatchesFromSearchIndex( + searchIndex: readonly PowerPointFindSearchIndexSlide[], + query: string, +): PowerPointFindMatch[] { + const normalizedQuery = query.trim().toLocaleLowerCase(); + if (!normalizedQuery) return []; + + const matches: PowerPointFindMatch[] = []; + for (const slide of searchIndex) { + let slideHasMatch = false; + const shapeMatches = slide.shapeMatches; + let lowerShapeTexts = slide.lowerShapeTexts; + if (!lowerShapeTexts || lowerShapeTexts.length !== shapeMatches.length) { + lowerShapeTexts = shapeMatches.map((match) => match.text.toLocaleLowerCase()); + slide.lowerShapeTexts = lowerShapeTexts; + } + + for (let i = 0; i < shapeMatches.length; i++) { + const match = shapeMatches[i]; + const lowerText = lowerShapeTexts[i]; + if (match && lowerText !== undefined && lowerText.includes(normalizedQuery)) { + matches.push(match); + slideHasMatch = true; + } + } + + if (!slideHasMatch && slide.fallbackText) { + const lowerFallback = slide.lowerFallbackText ?? ( + slide.lowerFallbackText = slide.fallbackText.toLocaleLowerCase() + ); + if (lowerFallback.includes(normalizedQuery)) { + matches.push({ slideIndex: slide.slideIndex, shapeIndex: null, text: slide.fallbackText }); + } + } + } + return matches; +} From 40a9135bea1f152692add7220867df4901fb7a4a Mon Sep 17 00:00:00 2001 From: MarsLuay <70299537+MarsLuay@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:50:05 +0000 Subject: [PATCH 2/2] perf(pptx): optimize slide text extraction speed by avoiding intermediate arrays\n\nReplaces `getDescendants(...).map(...).join(...)` with a direct loop over `getElementsByTagNameNS('*', 't')` to avoid creating intermediate strings and allocations for the array mappings when extracting search index text. --- src/powerpoint/findSearchIndex.ts.orig | 110 ------------------------- 1 file changed, 110 deletions(-) delete mode 100644 src/powerpoint/findSearchIndex.ts.orig diff --git a/src/powerpoint/findSearchIndex.ts.orig b/src/powerpoint/findSearchIndex.ts.orig deleted file mode 100644 index e9743e3..0000000 --- a/src/powerpoint/findSearchIndex.ts.orig +++ /dev/null @@ -1,110 +0,0 @@ -import type { PowerPointFindMatch } from './types'; -import { - getDescendants, - getElementChildren, - getShapeChildren, - getShapeTree, - parseXml, - SHAPE_ELEMENT_NAMES, -} from './ooxmlXml'; - -function normalizeSearchText(value: string): string { - return value.replace(/\s+/g, ' ').trim(); -} - -export interface PowerPointFindSearchIndexSlide { - slideIndex: number; - shapeMatches: PowerPointFindMatch[]; - fallbackText: string; - lowerShapeTexts?: string[]; - lowerFallbackText?: string; -} - -/** - * Extracts searchable text from slide OOXML without invoking the SVG renderer. - * Rendering every slide just to search its text makes Find unusably slow for - * large poster templates and image-heavy decks. - */ -export function createFindSearchIndexSlideFromOoxml( - slideIndex: number, - slideXml: string, -): PowerPointFindSearchIndexSlide { - const slideDocument = parseXml(slideXml, `slide ${slideIndex + 1}`); - const shapeMatches: PowerPointFindMatch[] = []; - const lowerShapeTexts: string[] = []; - const addShape = (shape: Element, shapeIndex: number): void => { - let rawText = ''; - const textElements = shape.getElementsByTagNameNS('*', 't'); - for (let i = 0; i < textElements.length; i++) { - const content = textElements[i]?.textContent; - if (content) rawText += content; - } - const text = normalizeSearchText(rawText); - if (text) { - shapeMatches.push({ slideIndex, shapeIndex, text }); - lowerShapeTexts.push(text.toLocaleLowerCase()); - } - }; - - const shapes = getShapeChildren(getShapeTree(slideDocument)); - shapes.forEach((shape, shapeIndex) => { - addShape(shape, shapeIndex); - if (shape.localName !== 'grpSp') return; - - getElementChildren(shape) - .filter((child) => SHAPE_ELEMENT_NAMES.has(child.localName)) - .forEach((child, childIndex) => addShape(child, (shapeIndex * 1000) + childIndex)); - }); - - const fallbackText = normalizeSearchText(slideDocument.documentElement.textContent ?? ''); - - return { - slideIndex, - shapeMatches, - fallbackText, - lowerShapeTexts, - lowerFallbackText: fallbackText.toLocaleLowerCase(), - }; -} - -/** - * Filters already-extracted deck text. The fallback preserves the renderer - * behavior for text that is not inside an editable shape group. - */ -export function collectFindMatchesFromSearchIndex( - searchIndex: readonly PowerPointFindSearchIndexSlide[], - query: string, -): PowerPointFindMatch[] { - const normalizedQuery = query.trim().toLocaleLowerCase(); - if (!normalizedQuery) return []; - - const matches: PowerPointFindMatch[] = []; - for (const slide of searchIndex) { - let slideHasMatch = false; - const shapeMatches = slide.shapeMatches; - let lowerShapeTexts = slide.lowerShapeTexts; - if (!lowerShapeTexts || lowerShapeTexts.length !== shapeMatches.length) { - lowerShapeTexts = shapeMatches.map((match) => match.text.toLocaleLowerCase()); - slide.lowerShapeTexts = lowerShapeTexts; - } - - for (let i = 0; i < shapeMatches.length; i++) { - const match = shapeMatches[i]; - const lowerText = lowerShapeTexts[i]; - if (match && lowerText !== undefined && lowerText.includes(normalizedQuery)) { - matches.push(match); - slideHasMatch = true; - } - } - - if (!slideHasMatch && slide.fallbackText) { - const lowerFallback = slide.lowerFallbackText ?? ( - slide.lowerFallbackText = slide.fallbackText.toLocaleLowerCase() - ); - if (lowerFallback.includes(normalizedQuery)) { - matches.push({ slideIndex: slide.slideIndex, shapeIndex: null, text: slide.fallbackText }); - } - } - } - return matches; -}