/**
 * Excel Spreadsheet (XLSX) Parser
 *
 * **XLSX Format Overview:**
 * XLSX is the default format for Microsoft Excel since Office 2007, based on OOXML.
 *
 * **File Structure:**
 * - `xl/workbook.xml` - Workbook structure and sheet list
 * - `xl/worksheets/sheet1.xml` - Individual sheet data
 * - `xl/sharedStrings.xml` - Shared string table (cell text)
 * - `xl/styles.xml` - Cell styling information
 * - `xl/drawings/*` - Charts and drawings
 * - `xl/media/*` - Embedded images
 *
 * **Key Elements:**
 * - `<row>` - Table row with row index
 * - `<c r="A1">` - Cell with reference (A1, B2, etc.)
 * - `<v>` - Cell value (number or shared string index)
 * - `<t="s">` - Cell type (s=string, n=number, b=boolean)
 *
 * @module ExcelParser
 * @see https://www.ecma-international.org/publications-and-standards/standards/ecma-376/
 */
import { extractChartData } from '../utils/chartUtils';
import { logWarning } from '../utils/errorUtils';
import { createAttachment } from '../utils/imageUtils';
import { performOcr } from '../utils/ocrUtils';
import { getElementsByTagName, parseOfficeMetadata, parseXmlString } from '../utils/xmlUtils';
import { extractFiles } from '../utils/zipUtils';
/** 工作表单元格 `<t>` 内可能对非 ASCII 使用 `&#dddd;` / `&#xhhhh;`，需解码为 Unicode。 */
function decodeOoxmlNumericCharRefs(text) {
    if (!text.includes('&'))
        return text;
    return text
        .replace(/&#x([0-9a-fA-F]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16)))
        .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10)));
}
/**
 * 从 `t="inlineStr"` 的单元格块提取文本（`<is>` 内可有多个 `<t>`，且可带 `xml:space` 等属性）。
 * 无 `xml:space="preserve"` 时对段首尾空白 trim，避免 OOXML 中多余换行/空格进入展示文本。
 */
function extractInlineStrPlainText(cXml) {
    const isInner = cXml.match(/<is>([\s\S]*?)<\/is>/);
    if (!isInner)
        return '';
    const inner = isInner[1];
    const parts = [];
    const tRe = /<t([^>]*)>([\s\S]*?)<\/t>/g;
    let m;
    while ((m = tRe.exec(inner))) {
        const attrs = m[1] || '';
        const raw = decodeOoxmlNumericCharRefs(m[2]);
        const preserve = /\bxml:space\s*=\s*["']preserve["']/.test(attrs);
        parts.push(preserve ? raw : raw.trim());
    }
    return parts.join('');
}
/** OOXML `tint`：在主题色与黑/白之间插值（与 Excel 展示接近）。 */
function applyTintToRgbHex(hex, tint) {
    const h = hex.replace(/^#/, '');
    if (h.length !== 6)
        return hex;
    const r = parseInt(h.slice(0, 2), 16);
    const g = parseInt(h.slice(2, 4), 16);
    const b = parseInt(h.slice(4, 6), 16);
    const ch = (c) => {
        if (tint < 0) {
            return Math.max(0, Math.min(255, Math.round(c * (1 + tint))));
        }
        return Math.max(0, Math.min(255, Math.round(c + (255 - c) * tint)));
    };
    const nr = ch(r);
    const ng = ch(g);
    const nb = ch(b);
    return `#${nr.toString(16).padStart(2, '0')}${ng.toString(16).padStart(2, '0')}${nb.toString(16).padStart(2, '0')}`;
}
function colorFromSpreadsheetFgBgColor(el) {
    if (!el)
        return undefined;
    const rgb = el.getAttribute('rgb');
    if (rgb && rgb.length >= 6) {
        const core = rgb.length === 8 ? rgb.slice(2) : rgb.length === 6 ? rgb : rgb.slice(-6);
        if (core.length === 6) {
            return `#${core.toUpperCase()}`;
        }
    }
    const indexed = el.getAttribute('indexed');
    if (indexed !== null && indexed !== '') {
        const idx = parseInt(indexed, 10);
        if (idx === 64)
            return undefined;
        const hex = EXCEL_INDEXED_RGB_HEX[idx];
        if (hex)
            return `#${hex}`;
        const legacy = OOXML_INDEXED_FILL[idx];
        if (legacy)
            return legacy;
    }
    const theme = el.getAttribute('theme');
    if (theme !== null && theme !== '') {
        const tIdx = parseInt(theme, 10);
        const base = OOXML_THEME_FILL_APPROX[tIdx];
        const tintStr = el.getAttribute('tint');
        if (base && tintStr !== null && tintStr !== '') {
            const tint = parseFloat(tintStr);
            if (!Number.isNaN(tint)) {
                return applyTintToRgbHex(base, tint);
            }
        }
        if (base)
            return base;
    }
    return undefined;
}
function resolvePatternFillBackground(patternFill) {
    const fg = getElementsByTagName(patternFill, 'fgColor')[0];
    const bg = getElementsByTagName(patternFill, 'bgColor')[0];
    const fromFg = colorFromSpreadsheetFgBgColor(fg);
    const fromBg = colorFromSpreadsheetFgBgColor(bg);
    return fromFg ?? fromBg;
}
/** `fgColor@indexed` → 背景色（与 Excel 调色板一致，节选） */
const OOXML_INDEXED_FILL = {
    0: '#000000',
    1: '#FFFFFF',
    2: '#FF0000',
    3: '#00FF00',
    4: '#0000FF',
    5: '#FFFF00',
    6: '#FF00FF',
    7: '#00FFFF',
    8: '#800000',
    9: '#008000',
    10: '#000080',
    11: '#808000',
    12: '#800080',
    13: '#008080',
    14: '#C0C0C0',
    15: '#808080',
    18: '#FFFFCC',
    26: '#FFFF00',
    35: '#FFFF99',
};
/**
 * Excel 默认索引调色板（0–63，`fgColor indexed` / `bgColor indexed`）。
 * 见 ECMA-376 / 与 Excel 2007+ 兼容的 legacy palette。
 */
const EXCEL_INDEXED_RGB_HEX = [
    '000000', 'FFFFFF', 'FF0000', '00FF00', '0000FF', 'FFFF00', 'FF00FF', '00FFFF',
    '000000', 'FFFFFF', 'FF0000', '00FF00', '0000FF', 'FFFF00', 'FF00FF', '00FFFF',
    '800000', '008000', '000080', '808000', '800080', '008080', 'C0C0C0', '808080',
    '9999FF', '993366', 'FFFFCC', 'CCFFFF', '660066', 'FF8080', '0066CC', 'CCCCCC',
    '000080', 'FF00FF', 'FFFF00', '00FFFF', '800080', '800000', '008000', '0000FF',
    '00CCFF', 'CCFFFF', 'CCFFCC', 'FFFF99', '99CCFF', 'FF99CC', 'CC99FF', 'FFCC99',
    '3366FF', '33CCCC', '99CC00', 'FFCC00', 'FF9900', 'FF6600', '666699', '969696',
    '003366', '339966', '003300', '333300', '993300', '993366', '333399', '333333',
];
/** `fgColor@theme` 无 theme 文件时的近似填充色 */
const OOXML_THEME_FILL_APPROX = {
    0: '#FFFFFF',
    1: '#000000',
    2: '#EEECE1',
    3: '#1F497D',
    4: '#4472C4',
    5: '#ED7D31',
    6: '#A5A5A5',
    7: '#FFC000',
    8: '#5B9BD5',
    9: '#70AD47',
    10: '#264478',
};
/**
 * ECMA-376 内置 numFmtId → formatCode（节选；工作簿中 &lt;numFmts&gt; 会覆盖或补充自定义 id≥164）。
 */
const OOXML_BUILTIN_NUMFMT = {
    0: 'General',
    1: '0',
    2: '0.00',
    3: '#,##0',
    4: '#,##0.00',
    5: '"$"#,##0_);("$"#,##0)',
    6: '"$"#,##0_);[Red]("$"#,##0)',
    7: '"$"#,##0.00_);("$"#,##0.00)',
    8: '"$"#,##0.00_);[Red]("$"#,##0.00)',
    9: '0%',
    10: '0.00%',
    11: '0.00E+00',
    12: '# ?/?',
    13: '# ??/?',
    14: 'mm-dd-yy',
    15: 'd-mmm-yy',
    16: 'd-mmm',
    17: 'mmm-yy',
    18: 'h:mm AM/PM',
    19: 'h:mm:ss AM/PM',
    20: 'h:mm',
    21: 'h:mm:ss',
    22: 'm/d/yy\\ h:mm',
    23: '0.000E+00',
    24: '# ?/?',
    25: '# ??/?',
    26: '#,##0.00_);(#,##0.00)',
    27: 'mm-dd-yy',
    28: 'm/d/yy',
    29: 'm/d/yy',
    30: 'm/d/yy',
    31: 'yyyy"年"m"月"d"日"',
    32: 'h:mm',
    33: 'h:mm:ss',
    34: 'h:mm',
    35: 'h:mm:ss',
    36: 'm/d/yy',
    37: '#,##0_);(#,##0)',
    38: '#,##0_);[Red](#,##0)',
    39: '#,##0.00_);(#,##0.00)',
    40: '#,##0.00_);[Red](#,##0.00)',
    45: 'mm:ss',
    46: '[h]:mm:ss',
    47: 'mmss.0',
    48: '##0.0E+0',
    49: '@',
};
function parseXdrCellMarker(el) {
    if (!el)
        return null;
    const colN = getElementsByTagName(el, 'xdr:col')[0];
    const rowN = getElementsByTagName(el, 'xdr:row')[0];
    if (!colN || !rowN)
        return null;
    const colOffN = getElementsByTagName(el, 'xdr:colOff')[0];
    const rowOffN = getElementsByTagName(el, 'xdr:rowOff')[0];
    return {
        col: parseInt(colN.textContent || '0', 10),
        row: parseInt(rowN.textContent || '0', 10),
        colOffEmu: parseInt(colOffN?.textContent || '0', 10),
        rowOffEmu: parseInt(rowOffN?.textContent || '0', 10),
    };
}
function parseAnchorGeometry(anchor, anchorType) {
    if (anchorType === 'twoCell') {
        const from = parseXdrCellMarker(getElementsByTagName(anchor, 'xdr:from')[0]);
        if (!from)
            return null;
        const toMarker = parseXdrCellMarker(getElementsByTagName(anchor, 'xdr:to')[0]);
        return toMarker
            ? { anchorType: 'twoCell', from, to: toMarker }
            : { anchorType: 'twoCell', from };
    }
    if (anchorType === 'oneCell') {
        const from = parseXdrCellMarker(getElementsByTagName(anchor, 'xdr:from')[0]);
        if (!from)
            return null;
        const extEl = getElementsByTagName(anchor, 'xdr:ext')[0];
        const cx = parseInt(extEl?.getAttribute('cx') || '0', 10);
        const cy = parseInt(extEl?.getAttribute('cy') || '0', 10);
        return { anchorType: 'oneCell', from, extentEmu: { cx, cy } };
    }
    const posEl = getElementsByTagName(anchor, 'xdr:pos')[0];
    const extEl = getElementsByTagName(anchor, 'xdr:ext')[0];
    const x = parseInt(posEl?.getAttribute('x') || '0', 10);
    const y = parseInt(posEl?.getAttribute('y') || '0', 10);
    const cx = parseInt(extEl?.getAttribute('cx') || '0', 10);
    const cy = parseInt(extEl?.getAttribute('cy') || '0', 10);
    return {
        anchorType: 'absolute',
        positionEmu: { x, y },
        extentEmu: { cx, cy },
    };
}
/** Plain text from DrawingML body under spreadsheet shape (`xdr:txBody`). */
function extractDrawingmlTxBodyPlainText(txBody) {
    let acc = '';
    const tNodes = getElementsByTagName(txBody, 'a:t');
    for (const t of tNodes) {
        acc += t.textContent || '';
    }
    return acc.trim();
}
function anchorTypeFromTag(tagName) {
    if (tagName === 'xdr:twoCellAnchor')
        return 'twoCell';
    if (tagName === 'xdr:oneCellAnchor')
        return 'oneCell';
    return 'absolute';
}
/** `xdr:pic/xdr:spPr/a:xfrm@rot`，60000 分之一度 */
function parsePicRotationEmu(pic) {
    const spPr = getElementsByTagName(pic, 'xdr:spPr')[0];
    const xfrm = spPr ? getElementsByTagName(spPr, 'a:xfrm')[0] : undefined;
    if (!xfrm) {
        return undefined;
    }
    const rot = xfrm.getAttribute('rot');
    if (rot == null || rot === '') {
        return undefined;
    }
    const v = parseInt(rot, 10);
    return Number.isNaN(v) || v === 0 ? undefined : v;
}
/** 图片形状实际显示尺寸：`xdr:pic/xdr:spPr/a:xfrm/a:ext`（可与锚点 `xdr:ext` 不同，对应拉伸/裁剪） */
function parsePicShapeExtentEmu(pic) {
    const spPr = getElementsByTagName(pic, 'xdr:spPr')[0];
    const xfrm = spPr ? getElementsByTagName(spPr, 'a:xfrm')[0] : undefined;
    const ext = xfrm ? getElementsByTagName(xfrm, 'a:ext')[0] : undefined;
    if (!ext) {
        return null;
    }
    const cx = parseInt(ext.getAttribute('cx') || '0', 10);
    const cy = parseInt(ext.getAttribute('cy') || '0', 10);
    if (cx <= 0 || cy <= 0) {
        return null;
    }
    return { cx, cy };
}
/** OOXML `<cols>` → 0-based column index → `width` (character width). */
function parseSheetColumnWidths(sheetXml) {
    const out = {};
    const doc = parseXmlString(sheetXml);
    const colNodes = getElementsByTagName(doc, 'col');
    for (const col of colNodes) {
        const wAttr = col.getAttribute('width');
        if (!wAttr)
            continue;
        const w = parseFloat(wAttr);
        if (Number.isNaN(w))
            continue;
        const min = parseInt(col.getAttribute('min') || '1', 10);
        const max = parseInt(col.getAttribute('max') || String(min), 10);
        for (let c = min; c <= max; c++) {
            out[c - 1] = w;
        }
    }
    return out;
}
/** `<cols>` 中带 `style` 的列段 → 供 Univer 铺无 `<c>` 格子的列默认填充 */
function parseSheetColumnStyleSegments(sheetXml, cellFormatMap) {
    const doc = parseXmlString(sheetXml);
    const colNodes = getElementsByTagName(doc, 'col');
    const segments = [];
    for (const col of colNodes) {
        const styleAttr = col.getAttribute('style');
        if (!styleAttr) {
            continue;
        }
        const xfIdx = parseInt(styleAttr, 10);
        if (Number.isNaN(xfIdx)) {
            continue;
        }
        const formatting = cellFormatMap[xfIdx];
        if (!formatting || Object.keys(formatting).length === 0) {
            continue;
        }
        const min = parseInt(col.getAttribute('min') || '1', 10);
        const max = parseInt(col.getAttribute('max') || String(min), 10);
        segments.push({
            minCol: min - 1,
            maxCol: max - 1,
            formatting: { ...formatting },
        });
    }
    return segments;
}
/** e.g. `AA` → 26 (0-based). */
function excelColLettersToIndex(letters) {
    let n = 0;
    const u = letters.toUpperCase();
    for (let i = 0; i < u.length; i++) {
        const code = u.charCodeAt(i);
        if (code < 65 || code > 90) {
            continue;
        }
        n = n * 26 + (code - 64);
    }
    return n - 1;
}
function parseExcelCellRef(ref) {
    const m = ref.trim().match(/^([A-Za-z]+)(\d+)$/u);
    if (!m) {
        return { row: 0, col: 0 };
    }
    return {
        row: parseInt(m[2], 10) - 1,
        col: excelColLettersToIndex(m[1]),
    };
}
/** 当前工作表对应 `xl/comments/commentN.xml`，键为 `row,col`（0-based）。 */
function buildSheetCommentMap(files, sheetFilename) {
    const out = new Map();
    const relsFilename = `xl/worksheets/_rels/${sheetFilename}.rels`;
    const relsFile = files.find(f => f.path === relsFilename);
    if (!relsFile)
        return out;
    const relsXml = parseXmlString(relsFile.content.toString());
    const relationships = getElementsByTagName(relsXml, 'Relationship');
    let commentsTarget = null;
    for (const rel of relationships) {
        const type = rel.getAttribute('Type') || '';
        if (type.includes('comments')) {
            commentsTarget = rel.getAttribute('Target');
            break;
        }
    }
    if (!commentsTarget)
        return out;
    const t = commentsTarget.trim();
    let commentsPath;
    if (t.startsWith('/')) {
        commentsPath = t.replace(/^\//, '');
    }
    else if (t.startsWith('..')) {
        commentsPath = `xl/${t.replace(/^\.\.\//, '')}`;
    }
    else if (t.startsWith('xl/')) {
        commentsPath = t;
    }
    else {
        commentsPath = `xl/${t.replace(/^\//, '')}`;
    }
    const cf = files.find(f => f.path === commentsPath);
    if (!cf)
        return out;
    const doc = parseXmlString(cf.content.toString());
    const authorsRoot = getElementsByTagName(doc, 'authors')[0];
    const authorEls = authorsRoot ? getElementsByTagName(authorsRoot, 'author') : [];
    const authors = authorEls.map(a => a.textContent || '');
    const commentEls = getElementsByTagName(doc, 'comment');
    for (const c of commentEls) {
        const ref = c.getAttribute('ref');
        if (!ref)
            continue;
        const { row, col } = parseExcelCellRef(ref);
        const authorId = parseInt(c.getAttribute('authorId') || '0', 10);
        const author = authors[authorId];
        const textEl = getElementsByTagName(c, 'text')[0];
        let text = '';
        if (textEl) {
            const tNodes = getElementsByTagName(textEl, 't');
            for (const t of tNodes) {
                text += t.textContent || '';
            }
        }
        text = text.replace(/\r\n/g, '\n').trim();
        // OOXML 常在正文里再写一遍「作者:」行，与 `authorId` 重复；导出时若再拼 `author:` 会双行 admin
        if (author && text.length > 0) {
            const esc = author.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            text = text.replace(new RegExp(`^${esc}:\\s*`, 'i'), '').trim();
        }
        out.set(`${row},${col}`, { author, text });
    }
    return out;
}
function parseMergeCellRef(ref) {
    const parts = ref.split(':');
    const a = parseExcelCellRef(parts[0] || 'A1');
    const b = parseExcelCellRef(parts[1] || parts[0] || 'A1');
    return {
        start: { r: Math.min(a.row, b.row), c: Math.min(a.col, b.col) },
        end: { r: Math.max(a.row, b.row), c: Math.max(a.col, b.col) },
    };
}
function parseSheetMergeRanges(sheetXml) {
    const doc = parseXmlString(sheetXml);
    const mergeCells = getElementsByTagName(doc, 'mergeCell');
    const out = [];
    for (const mc of mergeCells) {
        const ref = mc.getAttribute('ref');
        if (ref) {
            out.push(parseMergeCellRef(ref));
        }
    }
    return out;
}
/** `<row ht="..."` → 0-based row → height in **points** (SpreadsheetML `ht`). */
function parseSheetRowHeights(sheetXml) {
    const doc = parseXmlString(sheetXml);
    const rowNodes = getElementsByTagName(doc, 'row');
    const out = {};
    for (const row of rowNodes) {
        const rAttr = row.getAttribute('r');
        const htAttr = row.getAttribute('ht');
        if (!rAttr || !htAttr) {
            continue;
        }
        const rowIdx = parseInt(rAttr, 10) - 1;
        const ht = parseFloat(htAttr);
        if (Number.isNaN(rowIdx) || Number.isNaN(ht)) {
            continue;
        }
        out[rowIdx] = ht;
    }
    return out;
}
/** Preserves z-order: direct children of `xdr:wsDr` only, in document order. */
function getSpreadsheetDrawingAnchorsInDocumentOrder(drawingXml) {
    const root = drawingXml.documentElement;
    if (!root)
        return [];
    const out = [];
    for (let i = 0; i < root.childNodes.length; i++) {
        const n = root.childNodes[i];
        if (n.nodeType !== 1)
            continue;
        const el = n;
        const tn = el.tagName;
        if (tn === 'xdr:twoCellAnchor' || tn === 'xdr:oneCellAnchor' || tn === 'xdr:absoluteAnchor') {
            out.push(el);
        }
    }
    return out;
}
/**
 * Walks spreadsheet drawing anchors in document order: pic, graphicFrame (chart), xdr:sp (text box / WordArt).
 */
function extractExcelAnchorPlacements(drawingXml, mediaRels, chartRels) {
    const ordered = [];
    const anchors = getSpreadsheetDrawingAnchorsInDocumentOrder(drawingXml);
    for (const anchor of anchors) {
        const anchorType = anchorTypeFromTag(anchor.tagName);
        const baseDrawing = parseAnchorGeometry(anchor, anchorType);
        if (!baseDrawing)
            continue;
        const pic = getElementsByTagName(anchor, 'xdr:pic')[0];
        if (pic) {
            const blipFill = getElementsByTagName(pic, 'xdr:blipFill')[0];
            const blip = blipFill ? getElementsByTagName(blipFill, 'a:blip')[0] : null;
            const embedId = blip?.getAttribute('r:embed');
            if (embedId && mediaRels[embedId]) {
                const mediaPath = mediaRels[embedId].path;
                const nvPicPr = getElementsByTagName(pic, 'xdr:nvPicPr')[0];
                const cNvPr = nvPicPr ? getElementsByTagName(nvPicPr, 'xdr:cNvPr')[0] : null;
                const altText = cNvPr
                    ? cNvPr.getAttribute('descr') || cNvPr.getAttribute('name') || undefined
                    : undefined;
                const rotEmu = parsePicRotationEmu(pic);
                let imageDrawing = baseDrawing;
                if (baseDrawing.anchorType === 'oneCell') {
                    const innerExt = parsePicShapeExtentEmu(pic);
                    let one = baseDrawing;
                    if (innerExt &&
                        (innerExt.cx !== baseDrawing.extentEmu.cx ||
                            innerExt.cy !== baseDrawing.extentEmu.cy)) {
                        one = { ...baseDrawing, extentEmu: innerExt };
                    }
                    const cNvPicPr = getElementsByTagName(pic, 'xdr:cNvPicPr')[0];
                    const picLocksEl = cNvPicPr &&
                        (getElementsByTagName(cNvPicPr, 'a:picLocks')[0] ||
                            getElementsByTagName(cNvPicPr, 'picLocks')[0]);
                    const nca = picLocksEl?.getAttribute('noChangeAspect');
                    const blipFillForStretch = getElementsByTagName(pic, 'xdr:blipFill')[0];
                    const hasStretch = !!(blipFillForStretch &&
                        (getElementsByTagName(blipFillForStretch, 'a:stretch')[0] ||
                            getElementsByTagName(blipFillForStretch, 'stretch')[0]));
                    one = {
                        ...one,
                        ...(nca === '1' ? { picNoChangeAspect: true } : {}),
                        ...(nca === '0' ? { picNoChangeAspect: false } : {}),
                        ...(hasStretch ? { picBlipStretch: true } : {}),
                    };
                    imageDrawing = one;
                }
                const excelDrawing = rotEmu !== undefined ? { ...imageDrawing, rotationEmu: rotEmu } : imageDrawing;
                ordered.push({ kind: 'image', mediaPath, altText, excelDrawing });
            }
        }
        const graphicFrames = getElementsByTagName(anchor, 'xdr:graphicFrame');
        for (const gf of graphicFrames) {
            const chartEl = getElementsByTagName(gf, 'c:chart')[0] ||
                Array.from(gf.getElementsByTagName('*')).find(el => el.localName === 'chart' && el.namespaceURI?.includes('chart'));
            const chartRId = chartEl?.getAttribute('r:id');
            if (!chartRId || !chartRels[chartRId])
                continue;
            const nvGf = getElementsByTagName(gf, 'xdr:nvGraphicFramePr')[0];
            const cNvPr = nvGf ? getElementsByTagName(nvGf, 'xdr:cNvPr')[0] : null;
            const frameName = cNvPr?.getAttribute('descr') || cNvPr?.getAttribute('name') || undefined;
            ordered.push({
                kind: 'chart',
                chartAttachmentName: chartRels[chartRId],
                frameName,
                excelDrawing: baseDrawing,
            });
        }
        const sps = getElementsByTagName(anchor, 'xdr:sp');
        for (const sp of sps) {
            const txBody = getElementsByTagName(sp, 'xdr:txBody')[0];
            if (!txBody)
                continue;
            const text = extractDrawingmlTxBodyPlainText(txBody);
            if (!text)
                continue;
            const nv = getElementsByTagName(sp, 'xdr:nvSpPr')[0];
            const cNvPr = nv ? getElementsByTagName(nv, 'xdr:cNvPr')[0] : null;
            const shapeName = cNvPr?.getAttribute('name') || cNvPr?.getAttribute('descr') || undefined;
            const cNvSp = nv ? getElementsByTagName(nv, 'xdr:cNvSpPr')[0] : null;
            const shapeKind = cNvSp?.getAttribute('txBox') === '1' ? 'textBox' : 'wordArt';
            ordered.push({
                kind: 'shape',
                text,
                shapeKind,
                shapeName,
                excelDrawing: baseDrawing,
            });
        }
    }
    return { ordered };
}
/**
 * Parses an Excel spreadsheet (.xlsx) and extracts sheets, rows, and cells.
 *
 * @param buffer - The XLSX file as a Buffer
 * @param config - Parser configuration
 * @returns A promise resolving to the parsed AST
 */
export const parseExcel = async (buffer, config) => {
    const sheetsRegex = /xl\/worksheets\/sheet\d+.xml/g;
    const drawingsRegex = /xl\/drawings\/drawing\d+.xml/g;
    const chartsRegex = /xl\/charts\/chart\d+.xml/g;
    const stringsFilePath = 'xl/sharedStrings.xml';
    const corePropsFileRegex = /docProps\/core\.xml/;
    const relsRegex = /xl\/worksheets\/_rels\/sheet\d+\.xml\.rels/g;
    const drawingRelsRegex = /xl\/drawings\/_rels\/drawing\d+\.xml\.rels/g;
    /** Excel 标准路径 `xl/comments/commentN.xml`；ExcelJS 导出可能为 `xl/commentsN.xml` */
    const commentsRegex = /^xl\/comments\/comment\d+\.xml$|^xl\/comments\d+\.xml$/;
    const files = await extractFiles(buffer, (x) => !!x.match(sheetsRegex) ||
        !!x.match(drawingsRegex) ||
        !!x.match(chartsRegex) ||
        /** chart 的 colors/style 与 _rels，供附件与导出合并 */
        !!x.match(/^xl\/charts\/(colors|style)\d+\.xml$/u) ||
        !!x.match(/^xl\/charts\/_rels\/chart\d+\.xml\.rels$/u) ||
        !!x.match(commentsRegex) ||
        /** 工作表 rels：批注、绘图等关系；与 extractAttachments 无关 */
        !!x.match(relsRegex) ||
        x === stringsFilePath ||
        x === 'xl/styles.xml' ||
        x === 'xl/workbook.xml' ||
        x === 'xl/_rels/workbook.xml.rels' ||
        !!x.match(corePropsFileRegex) ||
        (!!config.extractAttachments &&
            (!!x.match(/^xl\/media\/[^/]+$/) || !!x.match(drawingRelsRegex))));
    const sharedStringsFile = files.find(f => f.path === stringsFilePath);
    // Updated to store structured content (rich text runs) or simple string
    const sharedStrings = [];
    if (sharedStringsFile) {
        const xml = parseXmlString(sharedStringsFile.content.toString());
        const siNodes = getElementsByTagName(xml, "si");
        for (const si of siNodes) {
            const runNodes = getElementsByTagName(si, "r");
            if (runNodes.length > 0) {
                // Rich text with runs
                const runs = [];
                for (const run of runNodes) {
                    const tNode = getElementsByTagName(run, "t")[0];
                    if (tNode) {
                        const text = tNode.textContent || '';
                        // Extract run formatting 
                        const rPr = getElementsByTagName(run, "rPr")[0];
                        const formatting = {};
                        if (rPr) {
                            if (getElementsByTagName(rPr, "b").length > 0)
                                formatting.bold = true;
                            if (getElementsByTagName(rPr, "i").length > 0)
                                formatting.italic = true;
                            if (getElementsByTagName(rPr, "u").length > 0)
                                formatting.underline = true;
                            if (getElementsByTagName(rPr, "strike").length > 0)
                                formatting.strikethrough = true;
                            const sz = getElementsByTagName(rPr, "sz")[0];
                            if (sz)
                                formatting.size = sz.getAttribute("val") + 'pt';
                            const color = getElementsByTagName(rPr, "color")[0];
                            if (color) {
                                const rgb = color.getAttribute("rgb");
                                if (rgb)
                                    formatting.color = '#' + rgb.substring(2);
                            }
                            const rFont = getElementsByTagName(rPr, "rFont")[0];
                            if (rFont)
                                formatting.font = rFont.getAttribute("val") || undefined;
                            const vertAlign = getElementsByTagName(rPr, "vertAlign")[0];
                            if (vertAlign) {
                                const val = vertAlign.getAttribute("val");
                                if (val === "subscript")
                                    formatting.subscript = true;
                                if (val === "superscript")
                                    formatting.superscript = true;
                            }
                        }
                        runs.push({
                            type: 'text',
                            text: text,
                            formatting: Object.keys(formatting).length > 0 ? formatting : undefined
                        });
                    }
                }
                sharedStrings.push(runs);
            }
            else {
                // Simple text case
                const tNodes = getElementsByTagName(si, "t");
                let text = '';
                for (const t of tNodes) {
                    text += t.textContent || '';
                }
                sharedStrings.push(text);
            }
        }
    }
    // Parse styles to build formatting map
    const stylesFile = files.find(f => f.path === 'xl/styles.xml');
    const cellFormatMap = {};
    /** 与 `cellXfs` 下标一致：`s="k"` → `cellXfNumFmt[k]` */
    let cellXfNumFmt = [];
    if (stylesFile) {
        const xml = parseXmlString(stylesFile.content.toString());
        const numFmtIdToCode = { ...OOXML_BUILTIN_NUMFMT };
        const numFmtsRoot = getElementsByTagName(xml, 'numFmts')[0];
        if (numFmtsRoot) {
            for (const nf of getElementsByTagName(numFmtsRoot, 'numFmt')) {
                const id = parseInt(nf.getAttribute('numFmtId') || '0', 10);
                const code = nf.getAttribute('formatCode');
                if (code) {
                    numFmtIdToCode[id] = code;
                }
            }
        }
        // Parse fonts
        const fontsNode = getElementsByTagName(xml, "fonts")[0];
        const fonts = [];
        if (fontsNode) {
            const fontNodes = getElementsByTagName(fontsNode, "font");
            for (const font of fontNodes) {
                const formatting = {};
                if (getElementsByTagName(font, "b").length > 0)
                    formatting.bold = true;
                if (getElementsByTagName(font, "i").length > 0)
                    formatting.italic = true;
                if (getElementsByTagName(font, "u").length > 0)
                    formatting.underline = true;
                if (getElementsByTagName(font, "strike").length > 0)
                    formatting.strikethrough = true;
                const szNode = getElementsByTagName(font, "sz")[0];
                if (szNode) {
                    const val = szNode.getAttribute("val");
                    if (val)
                        formatting.size = val + 'pt';
                }
                const colorNode = getElementsByTagName(font, "color")[0];
                if (colorNode) {
                    const rgb = colorNode.getAttribute("rgb");
                    if (rgb)
                        formatting.color = '#' + rgb.substring(2); // Remove alpha channel
                }
                const nameNode = getElementsByTagName(font, "name")[0];
                if (nameNode) {
                    const val = nameNode.getAttribute("val");
                    if (val)
                        formatting.font = val;
                }
                const vertAlignNode = getElementsByTagName(font, "vertAlign")[0];
                if (vertAlignNode) {
                    const val = vertAlignNode.getAttribute("val");
                    if (val === "subscript")
                        formatting.subscript = true;
                    if (val === "superscript")
                        formatting.superscript = true;
                }
                fonts.push(formatting);
            }
        }
        // Parse fills (for background color)：fg/bg、indexed 0–63、theme+tint
        const fillsNode = getElementsByTagName(xml, "fills")[0];
        const fills = [];
        if (fillsNode) {
            const fillNodes = getElementsByTagName(fillsNode, "fill");
            for (const fill of fillNodes) {
                const formatting = {};
                const patternFill = getElementsByTagName(fill, "patternFill")[0];
                if (patternFill) {
                    const bg = resolvePatternFillBackground(patternFill);
                    if (bg) {
                        formatting.backgroundColor = bg;
                    }
                }
                fills.push(formatting);
            }
        }
        // Parse cellXfs (cell format definitions)
        const cellXfsNode = getElementsByTagName(xml, "cellXfs")[0];
        if (cellXfsNode) {
            const xfNodes = getElementsByTagName(cellXfsNode, "xf");
            for (let i = 0; i < xfNodes.length; i++) {
                const xf = xfNodes[i];
                const formatting = {};
                const fontId = xf.getAttribute("fontId");
                if (fontId) {
                    const fontIdx = parseInt(fontId);
                    if (fonts[fontIdx]) {
                        Object.assign(formatting, fonts[fontIdx]);
                    }
                }
                const fillId = xf.getAttribute("fillId");
                if (fillId) {
                    const fillIdx = parseInt(fillId);
                    if (fills[fillIdx] && fills[fillIdx].backgroundColor) {
                        formatting.backgroundColor = fills[fillIdx].backgroundColor;
                    }
                }
                const alignmentNode = getElementsByTagName(xf, "alignment")[0];
                if (alignmentNode) {
                    const horizontal = alignmentNode.getAttribute("horizontal");
                    if (horizontal === 'center' || horizontal === 'right' || horizontal === 'justify' || horizontal === 'left') {
                        formatting.alignment = horizontal;
                    }
                }
                const numFmtIdAttr = xf.getAttribute('numFmtId');
                const nfi = numFmtIdAttr !== null && numFmtIdAttr !== ''
                    ? parseInt(numFmtIdAttr, 10)
                    : 0;
                cellFormatMap[i] = formatting;
                const applyNum = xf.getAttribute('applyNumberFormat');
                if (applyNum === '0' || applyNum === 'false') {
                    cellXfNumFmt[i] = undefined;
                }
                else {
                    cellXfNumFmt[i] = numFmtIdToCode[nfi];
                }
            }
        }
    }
    const attachments = [];
    const mediaFiles = files.filter(f => f.path.match(/xl\/media\/.*/));
    const chartFiles = files.filter(f => f.path.match(chartsRegex));
    // Map drawing part path -> relationship Id -> packaged media path (xl/media/...)
    const drawingImageMap = {};
    /** drawing part path -> rId -> chart xml filename (e.g. chart1.xml) */
    const drawingChartRelMap = {};
    const drawingOrderedPlacements = {};
    if (config.extractAttachments) {
        // 1. Parse drawing .rels: media targets + chart targets
        const drawingRelsFiles = files.filter(f => f.path.match(drawingRelsRegex));
        for (const relFile of drawingRelsFiles) {
            const drawingFilename = relFile.path.split('/').pop()?.replace('.rels', '') || '';
            const drawingPath = `xl/drawings/${drawingFilename}`;
            const relsXml = parseXmlString(relFile.content.toString());
            const relationships = getElementsByTagName(relsXml, "Relationship");
            if (!drawingImageMap[drawingPath]) {
                drawingImageMap[drawingPath] = {};
            }
            if (!drawingChartRelMap[drawingPath]) {
                drawingChartRelMap[drawingPath] = {};
            }
            for (const rel of relationships) {
                const id = rel.getAttribute("Id");
                const target = rel.getAttribute("Target");
                const type = rel.getAttribute("Type");
                if (!id || !target)
                    continue;
                if (target.includes('media/')) {
                    const mediaPath = 'xl/' + target.replace('../', '');
                    drawingImageMap[drawingPath][id] = { path: mediaPath };
                }
                else if (type && type.includes('chart')) {
                    const chartName = target.split('/').pop() || '';
                    drawingChartRelMap[drawingPath][id] = chartName;
                }
            }
        }
        // 2. Parse drawings: anchors for xdr:pic (images) and xdr:graphicFrame (charts)
        const drawingFiles = files.filter(f => f.path.match(drawingsRegex));
        for (const drawingFile of drawingFiles) {
            const dp = drawingFile.path;
            const mediaRels = drawingImageMap[dp] || {};
            const chartRels = drawingChartRelMap[dp] || {};
            const xml = parseXmlString(drawingFile.content.toString());
            const { ordered } = extractExcelAnchorPlacements(xml, mediaRels, chartRels);
            drawingOrderedPlacements[dp] = ordered;
        }
        // 3. Process Media Files
        for (const media of mediaFiles) {
            const attachment = createAttachment(media.path.split('/').pop() || 'image', media.content);
            // Alt text from drawing placements (same file may appear in multiple anchors)
            let altText = '';
            for (const drawingPath in drawingOrderedPlacements) {
                for (const p of drawingOrderedPlacements[drawingPath]) {
                    if (p.kind === 'image' && p.mediaPath === media.path && p.altText) {
                        altText = p.altText;
                        break;
                    }
                }
                if (altText)
                    break;
            }
            if (altText)
                attachment.altText = altText;
            attachments.push(attachment);
            if (config.ocr) {
                if (attachment.mimeType.startsWith('image/')) {
                    try {
                        const ocrText = await performOcr(media.content, config.ocrLanguage);
                        if (ocrText.trim()) {
                            attachment.ocrText = ocrText.trim();
                        }
                    }
                    catch (e) {
                        logWarning(`OCR failed for ${attachment.name}:`, config, e);
                    }
                }
            }
        }
        for (const chart of chartFiles) {
            const attachment = {
                type: 'chart',
                mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                data: chart.content.toString('base64'),
                name: chart.path.split('/').pop() || '',
                extension: 'xml'
            };
            // Extract structured chart data
            try {
                const chartData = extractChartData(chart.content);
                attachment.chartData = chartData;
            }
            catch (e) {
                logWarning(`Failed to extract chart data from ${chart.path}:`, config, e);
            }
            attachments.push(attachment);
        }
        // Chart 辅助文件（colors/style/rels），导出时需一并写回 OOXML
        const chartAuxFiles = files.filter(f => /^xl\/charts\/(colors|style)\d+\.xml$/u.test(f.path) ||
            /^xl\/charts\/_rels\/chart\d+\.xml\.rels$/u.test(f.path));
        for (const f of chartAuxFiles) {
            attachments.push({
                type: 'packagePart',
                mimeType: 'application/xml',
                data: f.content.toString('base64'),
                name: f.path.split('/').pop() || 'part.xml',
                extension: 'xml',
                packagePath: f.path,
            });
        }
        // 整块绘图部件（含图表框、形状、艺术字、浮动图锚点），供导出合并进 xlsx
        const drawingMainFiles = files.filter(f => /^xl\/drawings\/drawing\d+\.xml$/u.test(f.path));
        for (const df of drawingMainFiles) {
            const base = df.path.split('/').pop() || '';
            const relPath = `xl/drawings/_rels/${base}.rels`;
            const relFile = files.find(x => x.path === relPath);
            let targetWs;
            for (const f of files) {
                if (!/^xl\/worksheets\/_rels\/sheet\d+\.xml\.rels$/u.test(f.path)) {
                    continue;
                }
                const s = f.content.toString();
                if (s.includes(`drawings/${base}`)) {
                    targetWs = f.path
                        .replace('xl/worksheets/_rels/', 'xl/worksheets/')
                        .replace('.rels', '');
                    break;
                }
            }
            attachments.push({
                type: 'packagePart',
                mimeType: 'application/xml',
                data: df.content.toString('base64'),
                name: base,
                extension: 'xml',
                packagePath: df.path,
                relsPackagePath: relFile?.path,
                relsData: relFile ? relFile.content.toString('base64') : undefined,
                targetWorksheetXml: targetWs,
            });
        }
    }
    // Parse workbook.xml to get sheet names and map them to sheet files
    const sheetNameMap = {};
    /** Worksheet XML filenames (e.g. `sheet3.xml`) skipped for `state="hidden"` / `veryHidden`. */
    const hiddenSheetFilenames = new Set();
    const workbookFile = files.find(f => f.path === 'xl/workbook.xml');
    const workbookRelsFile = files.find(f => f.path === 'xl/_rels/workbook.xml.rels');
    if (workbookFile && workbookRelsFile) {
        // Parse rels to get rId -> file mapping
        const relsXml = parseXmlString(workbookRelsFile.content.toString());
        const relationships = getElementsByTagName(relsXml, "Relationship");
        const rIdToFile = {};
        for (const rel of relationships) {
            const rId = rel.getAttribute("Id");
            const target = rel.getAttribute("Target");
            if (rId && target) {
                // Target is like "worksheets/sheet1.xml"
                const filename = target.split('/').pop() || '';
                rIdToFile[rId] = filename;
            }
        }
        // Parse workbook.xml to get sheet name -> rId mapping
        const workbookXml = parseXmlString(workbookFile.content.toString());
        const sheets = getElementsByTagName(workbookXml, "sheet");
        for (const sheet of sheets) {
            const name = sheet.getAttribute("name");
            const rId = sheet.getAttribute("r:id");
            const state = sheet.getAttribute("state");
            if (rId && rIdToFile[rId] && (state === 'hidden' || state === 'veryHidden')) {
                hiddenSheetFilenames.add(rIdToFile[rId]);
            }
            if (name && rId && rIdToFile[rId]) {
                sheetNameMap[rIdToFile[rId]] = name;
            }
        }
    }
    const content = [];
    const rawContents = [];
    for (const file of files) {
        if (commentsRegex.test(file.path))
            continue;
        if (/^xl\/media\/[^/]+$/.test(file.path))
            continue;
        if (file.path === stringsFilePath)
            continue;
        if (file.path === 'xl/styles.xml')
            continue;
        if (file.path.match(drawingsRegex))
            continue;
        if (file.path.match(chartsRegex))
            continue;
        if (file.path.match(relsRegex))
            continue;
        if (file.path.match(drawingRelsRegex))
            continue;
        if (file.path.match(sheetsRegex)) {
            const sheetFileNameEarly = file.path.split('/').pop() || '';
            if (hiddenSheetFilenames.has(sheetFileNameEarly)) {
                continue;
            }
            if (config.includeRawContent) {
                rawContents.push(file.content.toString());
            }
            const sheetXmlStr = file.content.toString();
            const columnWidths = parseSheetColumnWidths(sheetXmlStr);
            const columnStyleSegments = parseSheetColumnStyleSegments(sheetXmlStr, cellFormatMap);
            const mergeRangesFromXml = parseSheetMergeRanges(sheetXmlStr);
            const rowHeights = parseSheetRowHeights(sheetXmlStr);
            const sheetCommentMap = buildSheetCommentMap(files, sheetFileNameEarly);
            const rows = [];
            const rowRegex = /<row.*?>[\s\S]*?<\/row>/g;
            const rowMatches = sheetXmlStr.match(rowRegex);
            if (rowMatches) {
                for (const rowXml of rowMatches) {
                    const cells = [];
                    /** 同时匹配 `<c .../>` 与 `<c ...>...</c>`。旧版 `/<c.*?>...<\/c>/` 会把自闭合格与下一格粘成一段，导致 C/F/G 等列丢失。 */
                    const cRegex = /<c\b[\s\S]*?(?:\/>|>[\s\S]*?<\/c>)/g;
                    const cMatches = rowXml.match(cRegex);
                    const rMatch = rowXml.match(/r="(\d+)"/);
                    const rowIndex = rMatch ? parseInt(rMatch[1]) - 1 : 0;
                    if (cMatches) {
                        for (const cXml of cMatches) {
                            // Extract cell value（`t` 须从 `<c` 起始标签读取：`inlineStr` 含大写字母，旧版 `[a-z]+` 会匹配失败）
                            const typeMatch = cXml.match(/^<c\b[^>]*\bt="([^"]+)"/);
                            const type = typeMatch ? typeMatch[1] : 'n'; // n = number (default)
                            const vMatch = cXml.match(/<v>(.*?)<\/v>/);
                            let text = '';
                            let cellNodes = [];
                            if (type === 's' && vMatch) {
                                const idx = parseInt(vMatch[1]);
                                const content = sharedStrings[idx];
                                if (Array.isArray(content)) {
                                    // Rich text runs
                                    // Deep copy runs to avoid reference issues if reused
                                    cellNodes = JSON.parse(JSON.stringify(content));
                                    text = cellNodes.map(n => n.text).join('');
                                }
                                else {
                                    text = content || '';
                                }
                            }
                            else if (type === 'inlineStr') {
                                text = extractInlineStrPlainText(cXml);
                            }
                            else if (vMatch) {
                                text = vMatch[1];
                            }
                            // Parse cell coordinate (supports `AA1`, `AB3`, …)
                            const coordMatch = cXml.match(/r="([A-Za-z]+)(\d+)"/);
                            const colStr = coordMatch ? coordMatch[1] : '';
                            const colIndex = colStr ? excelColLettersToIndex(colStr) : 0;
                            const excelComment = sheetCommentMap.get(`${rowIndex},${colIndex}`);
                            const styleMatch = cXml.match(/\bs="(\d+)"/);
                            const styleIdx = styleMatch ? parseInt(styleMatch[1], 10) : undefined;
                            if (text || cellNodes.length > 0 || excelComment || styleIdx !== undefined) {
                                const cellFormatting = styleIdx !== undefined && cellFormatMap[styleIdx]
                                    ? cellFormatMap[styleIdx]
                                    : {};
                                if (cellNodes.length > 0) {
                                    // If we have specific runs, merge cell styles into them if run style is missing
                                    // But usually run style overrides cell style (except maybe background)
                                    for (const node of cellNodes) {
                                        if (!node.formatting)
                                            node.formatting = {};
                                        // Cell background always applies
                                        if (cellFormatting.backgroundColor)
                                            node.formatting.backgroundColor = cellFormatting.backgroundColor;
                                        // Cell alignment always applies
                                        if (cellFormatting.alignment)
                                            node.formatting.alignment = cellFormatting.alignment;
                                        // Font defaults from cell style if not in run
                                        if (!node.formatting.font && cellFormatting.font)
                                            node.formatting.font = cellFormatting.font;
                                        if (!node.formatting.size && cellFormatting.size)
                                            node.formatting.size = cellFormatting.size;
                                    }
                                }
                                else {
                                    // Simple text node
                                    cellNodes.push({
                                        type: 'text',
                                        text: text,
                                        formatting: cellFormatting
                                    });
                                }
                                const numFmtFromXf = styleIdx !== undefined && cellXfNumFmt[styleIdx]
                                    ? cellXfNumFmt[styleIdx]
                                    : undefined;
                                const cellNode = {
                                    type: 'cell',
                                    text: text,
                                    children: cellNodes,
                                    metadata: {
                                        row: rowIndex,
                                        col: colIndex,
                                        ...(numFmtFromXf ? { numberFormat: numFmtFromXf } : {}),
                                        ...(excelComment ? { excelComment } : {}),
                                    },
                                };
                                if (config.includeRawContent) {
                                    cellNode.rawContent = cXml;
                                }
                                cells.push(cellNode);
                            }
                        }
                    }
                    if (cells.length > 0) {
                        const rowNode = {
                            type: 'row',
                            children: cells,
                            metadata: undefined
                        };
                        if (config.includeRawContent) {
                            rowNode.rawContent = rowXml;
                        }
                        rows.push(rowNode);
                    }
                }
            }
            // Handle Drawings in Sheet (images and charts)
            if (config.extractAttachments) {
                // Parse Sheet Rels to map drawing rIds
                const sheetFilename = file.path.split('/').pop() || '';
                const relsFilename = `xl/worksheets/_rels/${sheetFilename}.rels`;
                const relsFile = files.find(f => f.path === relsFilename);
                const drawingMap = {}; // rId -> drawingPath
                if (relsFile) {
                    const relsXml = parseXmlString(relsFile.content.toString());
                    const relationships = getElementsByTagName(relsXml, "Relationship");
                    for (const rel of relationships) {
                        const id = rel.getAttribute("Id");
                        const target = rel.getAttribute("Target");
                        const type = rel.getAttribute("Type");
                        if (id && target && type && type.includes('drawing')) {
                            drawingMap[id] = 'xl/drawings/' + target.replace('../drawings/', '');
                        }
                    }
                }
                const drawingMatches = sheetXmlStr.match(/<drawing r:id="(.*?)"/g);
                /** 同一工作表内多个 `<drawing>` 若指向同一 `drawingN.xml`，避免重复灌入全部锚点（会导致重复图元 / 跨表错觉） */
                const seenDrawingPartPaths = new Set();
                if (drawingMatches) {
                    for (const match of drawingMatches) {
                        const rIdMatch = match.match(/r:id="(.*?)"/);
                        const rId = rIdMatch ? rIdMatch[1] : null;
                        if (rId && drawingMap[rId]) {
                            const drawingPath = drawingMap[rId];
                            if (seenDrawingPartPaths.has(drawingPath)) {
                                continue;
                            }
                            seenDrawingPartPaths.add(drawingPath);
                            const ordered = drawingOrderedPlacements[drawingPath];
                            if (ordered) {
                                for (const item of ordered) {
                                    if (item.kind === 'image') {
                                        const attachment = attachments.find(a => a.name === item.mediaPath.split('/').pop());
                                        if (attachment) {
                                            rows.push({
                                                type: 'image',
                                                text: '',
                                                children: [],
                                                metadata: {
                                                    attachmentName: attachment.name || 'unknown',
                                                    altText: item.altText,
                                                    excelDrawing: item.excelDrawing,
                                                },
                                            });
                                        }
                                    }
                                    else if (item.kind === 'chart') {
                                        const attachment = attachments.find(a => a.name === item.chartAttachmentName);
                                        if (attachment) {
                                            const chartTitle = attachment.chartData?.title;
                                            rows.push({
                                                type: 'chart',
                                                text: '',
                                                children: [],
                                                metadata: {
                                                    attachmentName: item.chartAttachmentName,
                                                    excelDrawing: item.excelDrawing,
                                                    title: item.frameName,
                                                    chartTitle,
                                                },
                                            });
                                        }
                                    }
                                    else if (item.kind === 'shape') {
                                        rows.push({
                                            type: 'drawing',
                                            text: item.text,
                                            children: [],
                                            metadata: {
                                                excelShapeKind: item.shapeKind,
                                                excelDrawing: item.excelDrawing,
                                                shapeName: item.shapeName,
                                            },
                                        });
                                    }
                                }
                            }
                        }
                    }
                }
            }
            // Get proper sheet name from workbook.xml mapping, fallback to filename
            const sheetFileName = file.path.split('/').pop() || 'Sheet';
            const sheetName = sheetNameMap[sheetFileName] || sheetFileName;
            const sheetMeta = { sheetName };
            if (Object.keys(columnWidths).length > 0) {
                sheetMeta.columnWidths = columnWidths;
            }
            if (columnStyleSegments.length > 0) {
                sheetMeta.columnStyleSegments = columnStyleSegments;
            }
            if (mergeRangesFromXml.length > 0) {
                sheetMeta.mergeRanges = mergeRangesFromXml;
            }
            if (Object.keys(rowHeights).length > 0) {
                sheetMeta.rowHeights = rowHeights;
            }
            content.push({
                type: 'sheet',
                children: rows,
                metadata: sheetMeta,
                rawContent: config.includeRawContent ? sheetXmlStr : undefined
            });
        }
    }
    const corePropsFile = files.find(f => f.path.match(corePropsFileRegex));
    const metadata = corePropsFile ? parseOfficeMetadata(corePropsFile.content.toString()) : {};
    // Link OCR text and chart data to content nodes (like PPTX parser)
    const assignAttachmentData = (nodes) => {
        for (const node of nodes) {
            if ('attachmentName' in (node.metadata || {})) {
                const meta = node.metadata;
                const attachment = attachments.find(a => a.name === meta.attachmentName);
                if (attachment) {
                    if (node.type === 'image') {
                        // Link OCR text to image node
                        if (attachment.ocrText) {
                            node.text = attachment.ocrText;
                        }
                        // Copy altText to attachment
                        if (meta.altText) {
                            attachment.altText = meta.altText;
                        }
                    }
                    if (node.type === 'chart') {
                        // Link chart data text to chart node
                        if (attachment.chartData) {
                            node.text = attachment.chartData.rawTexts.join(config.newlineDelimiter || '\n');
                        }
                    }
                }
            }
            if (node.children) {
                assignAttachmentData(node.children);
            }
        }
    };
    assignAttachmentData(content);
    return {
        type: 'xlsx',
        metadata: metadata,
        content: content,
        attachments: attachments,
        toText: () => content.map(c => {
            // Recursive text extraction (treat empty children[] as leaf so shape/chart text is kept)
            const getText = (node) => {
                if (node.children && node.children.length > 0) {
                    const sep = node.children[0]?.children?.length ? (config.newlineDelimiter ?? '\n') : '';
                    return node.children.map(getText).filter(t => t !== '').join(sep);
                }
                return node.text || '';
            };
            return getText(c);
        }).filter(t => t != '').join(config.newlineDelimiter ?? '\n')
    };
};
