From 0d5bcac3ae46a8769dd49e40dfd39d0260658d89 Mon Sep 17 00:00:00 2001 From: Gabriel Chittolina Date: Tue, 7 Apr 2026 14:08:45 -0300 Subject: [PATCH] fix: floating textbox not rendering --- .../v3/handlers/wp/helpers/drawingml-utils.js | 58 ++++++++++++ .../wp/helpers/encode-image-node-helpers.js | 60 ++++++------- .../helpers/encode-image-node-helpers.test.js | 19 ++++ .../wp/helpers/vector-shape-helpers.js | 90 ++++++++++--------- 4 files changed, 153 insertions(+), 74 deletions(-) create mode 100644 packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/drawingml-utils.js diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/drawingml-utils.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/drawingml-utils.js new file mode 100644 index 0000000000..81c66efbd9 --- /dev/null +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/drawingml-utils.js @@ -0,0 +1,58 @@ +/** + * Utilities for working with DrawingML nodes whose namespace prefixes may vary (e.g. `a:` vs `ns6:`). + */ + +/** + * Extract the local name from a qualified XML node name. + * @param {string|undefined|null} name + * @returns {string} + */ +export const getLocalName = (name) => { + if (typeof name !== 'string') return ''; + const parts = name.split(':'); + return parts.length ? parts[parts.length - 1] : name; +}; + +/** + * Check if a node has the requested local name, ignoring namespace prefix. + * @param {Object|undefined|null} node + * @param {string} localName + * @returns {boolean} + */ +export const hasLocalName = (node, localName) => { + if (!node || typeof node !== 'object') return false; + return getLocalName(node.name) === localName; +}; + +/** + * Find the first child element with the requested local name. + * @param {Array|undefined|null} elements + * @param {string} localName + * @returns {Object|undefined} + */ +export const findChildByLocalName = (elements, localName) => { + if (!Array.isArray(elements)) return undefined; + return elements.find((el) => hasLocalName(el, localName)); +}; + +/** + * Filter child elements by local name. + * @param {Array|undefined|null} elements + * @param {string} localName + * @returns {Array} + */ +export const filterChildrenByLocalName = (elements, localName) => { + if (!Array.isArray(elements)) return []; + return elements.filter((el) => hasLocalName(el, localName)); +}; + +/** + * Returns true when any child element has the requested local name. + * @param {Array|undefined|null} elements + * @param {string} localName + * @returns {boolean} + */ +export const someChildHasLocalName = (elements, localName) => { + if (!Array.isArray(elements)) return false; + return elements.some((el) => hasLocalName(el, localName)); +}; diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js index e38403058d..8fc22ee87b 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.js @@ -20,6 +20,7 @@ import { } from './textbox-content-helpers.js'; import { parseRelativeHeight } from './relative-height.js'; import { CHART_URI, resolveChartPart, parseChartXml } from './chart-helpers.js'; +import { findChildByLocalName, someChildHasLocalName, hasLocalName } from './drawingml-utils.js'; const DRAWING_XML_TAG = 'w:drawing'; const SHAPE_URI = 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape'; @@ -275,8 +276,8 @@ export function handleImageNode(node, params, isAnchor) { }; } - const graphic = node.elements.find((el) => el.name === 'a:graphic'); - const graphicData = graphic?.elements.find((el) => el.name === 'a:graphicData'); + const graphic = findChildByLocalName(node.elements, 'graphic'); + const graphicData = findChildByLocalName(graphic?.elements, 'graphicData'); const { uri } = graphicData?.attributes || {}; if (!graphicData) { return null; @@ -321,14 +322,14 @@ export function handleImageNode(node, params, isAnchor) { } const blipFill = picture.elements.find((el) => el.name === 'pic:blipFill'); - const blip = blipFill?.elements.find((el) => el.name === 'a:blip'); + const blip = findChildByLocalName(blipFill?.elements, 'blip'); if (!blip) { return null; } // Check for image effects (grayscale, luminance, etc.) - const hasGrayscale = blip.elements?.some((el) => el.name === 'a:grayscl'); - const lumEl = blip.elements?.find((el) => el.name === 'a:lum'); + const hasGrayscale = someChildHasLocalName(blip.elements, 'grayscl'); + const lumEl = findChildByLocalName(blip.elements, 'lum'); const rawBright = Number(lumEl?.attributes?.bright); const rawContrast = Number(lumEl?.attributes?.contrast); const lum = @@ -349,9 +350,9 @@ export function handleImageNode(node, params, isAnchor) { // // Skip cover mode when srcRect already emitted explicit clipping or when srcRect has // negative values (Word already adjusted the mapping). - const stretch = blipFill?.elements?.find((el) => el.name === 'a:stretch'); - const fillRect = stretch?.elements?.find((el) => el.name === 'a:fillRect'); - const srcRect = blipFill?.elements?.find((el) => el.name === 'a:srcRect'); + const stretch = findChildByLocalName(blipFill?.elements, 'stretch'); + const fillRect = findChildByLocalName(stretch?.elements, 'fillRect'); + const srcRect = findChildByLocalName(blipFill?.elements, 'srcRect'); const srcRectAttrs = srcRect?.attributes || {}; const clipPath = buildClipPathFromSrcRect(srcRectAttrs); @@ -370,7 +371,7 @@ export function handleImageNode(node, params, isAnchor) { const spPr = picture.elements.find((el) => el.name === 'pic:spPr'); if (spPr) { - const xfrm = spPr.elements?.find((el) => el.name === 'a:xfrm'); + const xfrm = findChildByLocalName(spPr.elements, 'xfrm'); if (xfrm?.attributes) { transformData = { ...transformData, @@ -384,7 +385,7 @@ export function handleImageNode(node, params, isAnchor) { // --- Parse pic:nvPicPr for lockAspectRatio, hyperlink --- const nvPicPr = picture.elements.find((el) => el.name === 'pic:nvPicPr'); const cNvPicPr = nvPicPr?.elements?.find((el) => el.name === 'pic:cNvPicPr'); - const picLocks = cNvPicPr?.elements?.find((el) => el.name === 'a:picLocks'); + const picLocks = findChildByLocalName(cNvPicPr?.elements, 'picLocks'); // Per OOXML §20.1.2.2.31, noChangeAspect defaults to false when not specified. // When a:picLocks is absent entirely, there is no lock → false. const lockAspectRatio = picLocks @@ -395,8 +396,7 @@ export function handleImageNode(node, params, isAnchor) { // wp:docPr > a:hlinkClick (Word's canonical placement per §20.4.2.5). const cNvPr = nvPicPr?.elements?.find((el) => el.name === 'pic:cNvPr'); const hlinkClick = - cNvPr?.elements?.find((el) => el.name === 'a:hlinkClick') || - docPr?.elements?.find((el) => el.name === 'a:hlinkClick'); + findChildByLocalName(cNvPr?.elements, 'hlinkClick') || findChildByLocalName(docPr?.elements, 'hlinkClick'); let hyperlink = null; if (hlinkClick?.attributes?.['r:id']) { const hlinkRId = hlinkClick.attributes['r:id']; @@ -415,10 +415,10 @@ export function handleImageNode(node, params, isAnchor) { // --- Parse decorative flag from wp:docPr > a:extLst > a:ext > adec:decorative --- let decorative = false; - const docPrExtLst = docPr?.elements?.find((el) => el.name === 'a:extLst'); + const docPrExtLst = findChildByLocalName(docPr?.elements, 'extLst'); if (docPrExtLst) { for (const ext of docPrExtLst.elements || []) { - if (ext.name !== 'a:ext') continue; + if (!hasLocalName(ext, 'ext')) continue; const decEl = ext.elements?.find((el) => el.name === 'adec:decorative' || el.name === 'a16:decorative'); if (decEl && (decEl.attributes?.['val'] === '1' || decEl.attributes?.['val'] === 1)) { decorative = true; @@ -603,7 +603,7 @@ const handleShapeDrawing = ( const textBoxContent = textBox?.elements?.find((el) => el.name === 'w:txbxContent'); const spPr = wsp.elements.find((el) => el.name === 'wps:spPr'); - const prstGeom = spPr?.elements.find((el) => el.name === 'a:prstGeom'); + const prstGeom = findChildByLocalName(spPr?.elements, 'prstGeom'); const shapeType = prstGeom?.attributes['prst']; // Check for custom geometry when no preset geometry is found @@ -681,15 +681,15 @@ const handleShapeGroup = (params, node, graphicData, size, padding, marginOffset // Extract group properties const grpSpPr = wgp.elements.find((el) => el.name === 'wpg:grpSpPr'); - const xfrm = grpSpPr?.elements?.find((el) => el.name === 'a:xfrm'); + const xfrm = findChildByLocalName(grpSpPr?.elements, 'xfrm'); // Get group transform data const groupTransform = {}; if (xfrm) { - const off = xfrm.elements?.find((el) => el.name === 'a:off'); - const ext = xfrm.elements?.find((el) => el.name === 'a:ext'); - const chOff = xfrm.elements?.find((el) => el.name === 'a:chOff'); - const chExt = xfrm.elements?.find((el) => el.name === 'a:chExt'); + const off = findChildByLocalName(xfrm.elements, 'off'); + const ext = findChildByLocalName(xfrm.elements, 'ext'); + const chOff = findChildByLocalName(xfrm.elements, 'chOff'); + const chExt = findChildByLocalName(xfrm.elements, 'chExt'); if (off) { groupTransform.x = emuToPixels(off.attributes?.['x'] || 0); @@ -723,14 +723,14 @@ const handleShapeGroup = (params, node, graphicData, size, padding, marginOffset if (!spPr) return null; // Extract shape kind (preset geometry) or custom geometry - const prstGeom = spPr.elements?.find((el) => el.name === 'a:prstGeom'); + const prstGeom = findChildByLocalName(spPr.elements, 'prstGeom'); const shapeKind = prstGeom?.attributes?.['prst']; const customGeom = !shapeKind ? extractCustomGeometry(spPr) : null; // Extract size and transformations - const shapeXfrm = spPr.elements?.find((el) => el.name === 'a:xfrm'); - const shapeOff = shapeXfrm?.elements?.find((el) => el.name === 'a:off'); - const shapeExt = shapeXfrm?.elements?.find((el) => el.name === 'a:ext'); + const shapeXfrm = findChildByLocalName(spPr.elements, 'xfrm'); + const shapeOff = findChildByLocalName(shapeXfrm?.elements, 'off'); + const shapeExt = findChildByLocalName(shapeXfrm?.elements, 'ext'); // Get raw child coordinates in EMU const rawX = shapeOff?.attributes?.['x'] ? parseFloat(shapeOff.attributes['x']) : 0; @@ -826,9 +826,9 @@ const handleShapeGroup = (params, node, graphicData, size, padding, marginOffset if (!spPr) return null; // Extract size and transformations - const xfrm = spPr.elements?.find((el) => el.name === 'a:xfrm'); - const off = xfrm?.elements?.find((el) => el.name === 'a:off'); - const ext = xfrm?.elements?.find((el) => el.name === 'a:ext'); + const xfrm = findChildByLocalName(spPr.elements, 'xfrm'); + const off = findChildByLocalName(xfrm?.elements, 'off'); + const ext = findChildByLocalName(xfrm?.elements, 'ext'); // Get raw coordinates in EMU const rawX = off?.attributes?.['x'] ? parseFloat(off.attributes['x']) : 0; @@ -857,7 +857,7 @@ const handleShapeGroup = (params, node, graphicData, size, padding, marginOffset // Extract image reference from blipFill const blipFill = pic.elements?.find((el) => el.name === 'pic:blipFill'); - const blip = blipFill?.elements?.find((el) => el.name === 'a:blip'); + const blip = findChildByLocalName(blipFill?.elements, 'blip'); if (!blip) return null; const rEmbed = blip.attributes?.['r:embed']; @@ -1300,7 +1300,7 @@ export function getVectorShape({ } // Extract shape kind (preset geometry) or custom geometry - const prstGeom = spPr.elements?.find((el) => el.name === 'a:prstGeom'); + const prstGeom = findChildByLocalName(spPr.elements, 'prstGeom'); const shapeKind = prstGeom?.attributes?.['prst']; schemaAttrs.kind = shapeKind; @@ -1320,7 +1320,7 @@ export function getVectorShape({ const height = size?.height ?? DEFAULT_SHAPE_HEIGHT; // Extract transformations from a:xfrm (rotation and flips are still valid) - const xfrm = spPr.elements?.find((el) => el.name === 'a:xfrm'); + const xfrm = findChildByLocalName(spPr.elements, 'xfrm'); const rotation = xfrm?.attributes?.['rot'] ? rotToDegrees(xfrm.attributes['rot']) : 0; const flipH = xfrm?.attributes?.['flipH'] === '1'; const flipV = xfrm?.attributes?.['flipV'] === '1'; diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js index 7ac26d4d67..d499be8872 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/encode-image-node-helpers.test.js @@ -166,6 +166,16 @@ describe('handleImageNode', () => { }; }; + const renameDrawingMlPrefix = (node, prefix) => { + if (!node || typeof node !== 'object') return; + if (typeof node.name === 'string' && node.name.startsWith('a:')) { + node.name = `${prefix}:${node.name.slice(2)}`; + } + if (Array.isArray(node.elements)) { + node.elements.forEach((child) => renameDrawingMlPrefix(child, prefix)); + } + }; + it('returns null if picture is missing', () => { const node = makeNode(); node.elements[1].elements[0].elements = []; @@ -530,6 +540,15 @@ describe('handleImageNode', () => { expect(extractStrokeWidth).toHaveBeenCalled(); }); + it('handles DrawingML nodes with non-a prefixes', () => { + const node = makeShapeNode({ prst: 'rect' }); + renameDrawingMlPrefix(node, 'ns6'); + + const result = handleImageNode(node, makeParams(), false); + expect(result.type).toBe('vectorShape'); + expect(result.attrs.kind).toBe('rect'); + }); + it('renders textbox shapes as vectorShapes with text content', () => { const node = makeShapeNode({ includeTextbox: true }); const result = handleImageNode(node, makeParams(), false); diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.js index 3dfaedf9e3..7ced1213e0 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.js @@ -1,3 +1,5 @@ +import { findChildByLocalName, filterChildrenByLocalName, hasLocalName, getLocalName } from './drawingml-utils.js'; + /** * Converts a preset color name (a:prstClr) to its hex value. * Per ECMA-376 Part 1, Section 20.1.10.47 (ST_PresetColorVal). @@ -161,15 +163,15 @@ function applyModifiersAndAlpha(color, elements) { let alpha = null; const modifiers = elements || []; modifiers.forEach((mod) => { - if (mod.name === 'a:shade') { + if (hasLocalName(mod, 'shade')) { color = applyColorModifier(color, 'shade', mod.attributes['val']); - } else if (mod.name === 'a:tint') { + } else if (hasLocalName(mod, 'tint')) { color = applyColorModifier(color, 'tint', mod.attributes['val']); - } else if (mod.name === 'a:lumMod') { + } else if (hasLocalName(mod, 'lumMod')) { color = applyColorModifier(color, 'lumMod', mod.attributes['val']); - } else if (mod.name === 'a:lumOff') { + } else if (hasLocalName(mod, 'lumOff')) { color = applyColorModifier(color, 'lumOff', mod.attributes['val']); - } else if (mod.name === 'a:alpha') { + } else if (hasLocalName(mod, 'alpha')) { alpha = parseInt(mod.attributes['val']) / 100000; } }); @@ -186,20 +188,20 @@ function applyModifiersAndAlpha(color, elements) { function extractColorFromElement(element) { if (!element?.elements) return null; - const schemeClr = element.elements.find((el) => el.name === 'a:schemeClr'); + const schemeClr = findChildByLocalName(element.elements, 'schemeClr'); if (schemeClr) { const themeName = schemeClr.attributes?.['val']; const baseColor = getThemeColor(themeName); return applyModifiersAndAlpha(baseColor, schemeClr.elements); } - const srgbClr = element.elements.find((el) => el.name === 'a:srgbClr'); + const srgbClr = findChildByLocalName(element.elements, 'srgbClr'); if (srgbClr) { const baseColor = '#' + srgbClr.attributes?.['val']; return applyModifiersAndAlpha(baseColor, srgbClr.elements); } - const prstClr = element.elements.find((el) => el.name === 'a:prstClr'); + const prstClr = findChildByLocalName(element.elements, 'prstClr'); if (prstClr) { const presetName = prstClr.attributes?.['val']; const baseColor = getPresetColor(presetName); @@ -290,7 +292,7 @@ export function applyColorModifier(hexColor, modifier, value) { * @returns {number} The stroke width in pixels, or 1 if not found */ export function extractStrokeWidth(spPr) { - const ln = spPr?.elements?.find((el) => el.name === 'a:ln'); + const ln = findChildByLocalName(spPr?.elements, 'ln'); if (!ln) return 1; const w = ln.attributes?.['w']; @@ -316,11 +318,11 @@ export function extractStrokeWidth(spPr) { * Line end configuration, or null when not present. */ export function extractLineEnds(spPr) { - const ln = spPr?.elements?.find((el) => el.name === 'a:ln'); + const ln = findChildByLocalName(spPr?.elements, 'ln'); if (!ln?.elements) return null; - const parseEnd = (name) => { - const end = ln.elements.find((el) => el.name === name); + const parseEnd = (localName) => { + const end = findChildByLocalName(ln.elements, localName); if (!end?.attributes) return null; const type = end.attributes?.['type']; if (!type || type === 'none') return null; @@ -329,11 +331,11 @@ export function extractLineEnds(spPr) { return { type, width, length }; }; - const head = parseEnd('a:headEnd'); - const tail = parseEnd('a:tailEnd'); + const headConfig = parseEnd('headEnd'); + const tailConfig = parseEnd('tailEnd'); - if (!head && !tail) return null; - return { head: head ?? undefined, tail: tail ?? undefined }; + if (!headConfig && !tailConfig) return null; + return { head: headConfig ?? undefined, tail: tailConfig ?? undefined }; } /** @@ -344,15 +346,15 @@ export function extractLineEnds(spPr) { * @returns {string|null} Hex color value */ export function extractStrokeColor(spPr, style) { - const ln = spPr?.elements?.find((el) => el.name === 'a:ln'); + const ln = findChildByLocalName(spPr?.elements, 'ln'); if (ln) { - const noFill = ln.elements?.find((el) => el.name === 'a:noFill'); + const noFill = findChildByLocalName(ln.elements, 'noFill'); if (noFill) { return null; } - const solidFill = ln.elements?.find((el) => el.name === 'a:solidFill'); + const solidFill = findChildByLocalName(ln.elements, 'solidFill'); if (solidFill) { const result = extractColorFromElement(solidFill); if (result) return result.color; @@ -365,7 +367,7 @@ export function extractStrokeColor(spPr, style) { return null; } - const lnRef = style.elements?.find((el) => el.name === 'a:lnRef'); + const lnRef = findChildByLocalName(style.elements, 'lnRef'); if (!lnRef) { // No lnRef in style means no stroke specified - return null return null; @@ -392,12 +394,12 @@ export function extractStrokeColor(spPr, style) { * @returns {string|null} Hex color value */ export function extractFillColor(spPr, style) { - const noFill = spPr?.elements?.find((el) => el.name === 'a:noFill'); + const noFill = findChildByLocalName(spPr?.elements, 'noFill'); if (noFill) { return null; } - const solidFill = spPr?.elements?.find((el) => el.name === 'a:solidFill'); + const solidFill = findChildByLocalName(spPr?.elements, 'solidFill'); if (solidFill) { const result = extractColorFromElement(solidFill); if (result) { @@ -408,12 +410,12 @@ export function extractFillColor(spPr, style) { } } - const gradFill = spPr?.elements?.find((el) => el.name === 'a:gradFill'); + const gradFill = findChildByLocalName(spPr?.elements, 'gradFill'); if (gradFill) { return extractGradientFill(gradFill); } - const blipFill = spPr?.elements?.find((el) => el.name === 'a:blipFill'); + const blipFill = findChildByLocalName(spPr?.elements, 'blipFill'); if (blipFill) { return '#cccccc'; // placeholder color for now } @@ -424,7 +426,7 @@ export function extractFillColor(spPr, style) { return null; } - const fillRef = style.elements?.find((el) => el.name === 'a:fillRef'); + const fillRef = findChildByLocalName(style.elements, 'fillRef'); if (!fillRef) { // No fillRef in style means no fill specified - return transparent return null; @@ -458,14 +460,14 @@ export function extractFillColor(spPr, style) { * @returns {{ paths: Array<{ d: string, w: number, h: number }> } | null} */ export function extractCustomGeometry(spPr) { - const custGeom = spPr?.elements?.find((el) => el.name === 'a:custGeom'); + const custGeom = findChildByLocalName(spPr?.elements, 'custGeom'); if (!custGeom) return null; - const pathLst = custGeom.elements?.find((el) => el.name === 'a:pathLst'); + const pathLst = findChildByLocalName(custGeom.elements, 'pathLst'); if (!pathLst?.elements) return null; const paths = pathLst.elements - .filter((el) => el.name === 'a:path') + .filter((el) => hasLocalName(el, 'path')) .map((pathEl) => { const w = parseInt(pathEl.attributes?.['w'] || '0', 10); const h = parseInt(pathEl.attributes?.['h'] || '0', 10); @@ -490,23 +492,23 @@ function convertDrawingMLPathToSvg(pathEl) { const parts = []; for (const cmd of pathEl.elements) { - switch (cmd.name) { - case 'a:moveTo': { - const pt = cmd.elements?.find((el) => el.name === 'a:pt'); + switch (getLocalName(cmd.name)) { + case 'moveTo': { + const pt = findChildByLocalName(cmd.elements, 'pt'); if (pt) { parts.push(`M ${pt.attributes?.['x'] || 0} ${pt.attributes?.['y'] || 0}`); } break; } - case 'a:lnTo': { - const pt = cmd.elements?.find((el) => el.name === 'a:pt'); + case 'lnTo': { + const pt = findChildByLocalName(cmd.elements, 'pt'); if (pt) { parts.push(`L ${pt.attributes?.['x'] || 0} ${pt.attributes?.['y'] || 0}`); } break; } - case 'a:cubicBezTo': { - const pts = cmd.elements?.filter((el) => el.name === 'a:pt') || []; + case 'cubicBezTo': { + const pts = filterChildrenByLocalName(cmd.elements, 'pt') || []; if (pts.length === 3) { parts.push( `C ${pts[0].attributes?.['x'] || 0} ${pts[0].attributes?.['y'] || 0} ` + @@ -516,8 +518,8 @@ function convertDrawingMLPathToSvg(pathEl) { } break; } - case 'a:quadBezTo': { - const pts = cmd.elements?.filter((el) => el.name === 'a:pt') || []; + case 'quadBezTo': { + const pts = filterChildrenByLocalName(cmd.elements, 'pt') || []; if (pts.length === 2) { parts.push( `Q ${pts[0].attributes?.['x'] || 0} ${pts[0].attributes?.['y'] || 0} ` + @@ -526,7 +528,7 @@ function convertDrawingMLPathToSvg(pathEl) { } break; } - case 'a:close': + case 'close': parts.push('Z'); break; default: @@ -550,14 +552,14 @@ function extractGradientFill(gradFill) { }; // Extract gradient stops - const gsLst = gradFill.elements?.find((el) => el.name === 'a:gsLst'); + const gsLst = findChildByLocalName(gradFill.elements, 'gsLst'); if (gsLst) { - const stops = gsLst.elements?.filter((el) => el.name === 'a:gs') || []; + const stops = filterChildrenByLocalName(gsLst.elements, 'gs') || []; gradient.stops = stops.map((stop) => { const pos = parseInt(stop.attributes?.['pos'] || '0', 10) / 100000; // Convert from 0-100000 to 0-1 // Extract color from the stop - const srgbClr = stop.elements?.find((el) => el.name === 'a:srgbClr'); + const srgbClr = findChildByLocalName(stop.elements, 'srgbClr'); let color = '#000000'; let alpha = 1; @@ -565,7 +567,7 @@ function extractGradientFill(gradFill) { color = '#' + srgbClr.attributes?.['val']; // Extract alpha if present - const alphaEl = srgbClr.elements?.find((el) => el.name === 'a:alpha'); + const alphaEl = findChildByLocalName(srgbClr.elements, 'alpha'); if (alphaEl) { alpha = parseInt(alphaEl.attributes?.['val'] || '100000', 10) / 100000; } @@ -576,7 +578,7 @@ function extractGradientFill(gradFill) { } // Extract gradient direction (linear angle) - const lin = gradFill.elements?.find((el) => el.name === 'a:lin'); + const lin = findChildByLocalName(gradFill.elements, 'lin'); if (lin) { // Convert from 60000ths of a degree to degrees const ang = parseInt(lin.attributes?.['ang'] || '0', 10) / 60000; @@ -584,7 +586,7 @@ function extractGradientFill(gradFill) { } // Check if it's a radial gradient - const path = gradFill.elements?.find((el) => el.name === 'a:path'); + const path = findChildByLocalName(gradFill.elements, 'path'); if (path) { gradient.gradientType = 'radial'; gradient.path = path.attributes?.['path'] || 'circle';