/**
 * Word (.docx) → Markdown — the docx extraction heuristics, isolated from the
 * authoritative convert pipeline (../convert-service.ts).
 *
 * Best-effort, frequently-tuned "make the extracted text readable" code with NO
 * graph/storage side effects: buffer in → markdown out, deterministic, no LLM.
 * mammoth extracts semantic HTML; turndown renders it; the rules below handle
 * the structures Word leans on that plain turndown / GFM otherwise mangle:
 *
 *  - Images are DROPPED (mammoth would otherwise inline multi-hundred-KB base64
 *    `data:` URIs straight into the body).
 *  - Tables: Word emits `<td>` for EVERY cell (never `<th>`) and uses tables for
 *    LAYOUT as much as for data, so neither plain turndown nor turndown-plugin-
 *    gfm handles them (gfm only fires on `<th>` heading rows; everything else it
 *    keeps as raw HTML). We classify each table by SHAPE and render accordingly:
 *      • clean rectangular grid (no merged cells), every cell single-line → a
 *        real GFM markdown table (row 1 as the header) — e.g. a Fees/Schedule.
 *      • 2-column that reads as section layout (short left LABEL + substantial
 *        right BODY in most rows) → left cell becomes a `##` heading, right its
 *        body — e.g. "Assignment | …".
 *      • 2-column that reads as data (short values both sides) → `label: value`
 *        pairs, so the row relationship survives (not a wall of paragraphs).
 *      • anything else → flatten: each cell its own block.
 *    A cell's inner table is converted independently (nested tables recurse, up
 *    to a depth cap); `colspan`/`rowspan` force the flatten path since they
 *    break the clean-grid assumption GFM + the heading layout rely on.
 *  - Word wraps ordered lists as `<ul><li><ol>…</ol></li></ul>` (a bullet whose
 *    only child is a list); that LI is unwrapped so it doesn't render as a
 *    spurious "* 1." double marker.
 *  - Turndown's backslash escaping of `[ ] _` is relaxed for reading text
 *    (placeholders, signature underscore runs), but ONLY outside code spans /
 *    fenced blocks so literal code is never rewritten.
 */
import TurndownService from 'turndown';
/** Left-cell text longer than this isn't treated as a section label (so the
 *  side-by-side signature block, whose cells are long, flattens instead). */
const MAX_LABEL_LEN = 60;
/** Nested-table recursion cap — past this, a table is emitted as plain text
 *  rather than recursing further (guards pathological deeply-nested docs). */
const MAX_TABLE_DEPTH = 8;
export async function convertDocxToMarkdown(buffer) {
    const mammoth = await import('mammoth');
    const { value: html } = await mammoth.convertToHtml({ buffer }, 
    // Drop embedded images entirely (no base64 src reaches turndown).
    { convertImage: mammoth.images.imgElement(() => Promise.resolve({ src: '' })) });
    const md = buildTurndown().turndown(html ?? '');
    return cleanup(md);
}
function buildTurndown() {
    const turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
    // Emit nothing for any <img> (convertImage already stripped the base64 src;
    // this drops the element so no empty `![]()` litters the body).
    turndown.addRule('dropImages', { filter: 'img', replacement: () => '' });
    // Word wraps ordered lists as <ul><li><ol>…</ol></li></ul>. Unwrap a list item
    // whose ONLY child is a nested list so it emits just the inner list (no leading
    // "* " bullet stacked on the "1." marker).
    turndown.addRule('unwrapNestedListItem', {
        filter: (node) => node.nodeName === 'LI' && onlyChildIsList(node),
        replacement: (content) => content,
    });
    // Tables: walk the DOM directly and convert each cell with a nested turndown
    // pass, so one rule can choose a GFM grid, a heading/label layout, or a block
    // flatten per table (see module doc). Returning the whole table from here also
    // short-circuits turndown's per-cell processing, so no `<table>` HTML leaks.
    // `depth` bounds the nested-turndown recursion: each table-rule entry is one
    // level, so a table-in-a-cell-in-a-table can't recurse without bound.
    let depth = 0;
    turndown.addRule('table', {
        filter: 'table',
        replacement: (_content, node) => {
            const table = node;
            if (depth >= MAX_TABLE_DEPTH) {
                return `\n\n${(table.textContent ?? '').trim()}\n\n`;
            }
            depth++;
            try {
                // `depth` (1 = top-level, 2 = nested in a cell, …) sets heading level so
                // a nested table's section labels sit UNDER their parent, not as peers.
                return `\n\n${renderTable(table, turndown, depth)}\n\n`;
            }
            finally {
                depth--;
            }
        },
    });
    return turndown;
}
/** True for a list item whose only meaningful child is a nested <ol>/<ul>
 *  (ignoring whitespace-only text nodes) — Word's ordered-list wrapper shape. */
function onlyChildIsList(li) {
    const meaningful = Array.from(li.childNodes).filter((n) => !(n.nodeType === 3 && !(n.textContent ?? '').trim()));
    return (meaningful.length === 1 &&
        (meaningful[0].nodeName === 'OL' || meaningful[0].nodeName === 'UL'));
}
/** This table's DIRECT rows only — `<tr>` that are direct children or one level
 *  under `<thead>/<tbody>/<tfoot>`. Never descends into a cell, so a nested
 *  table's rows stay with the nested table (DOM `.rows` wrongly includes them). */
function directRows(table) {
    const out = [];
    for (const child of Array.from(table.childNodes)) {
        if (child.nodeName === 'TR')
            out.push(child);
        else if (['THEAD', 'TBODY', 'TFOOT'].includes(child.nodeName)) {
            for (const tr of Array.from(child.childNodes)) {
                if (tr.nodeName === 'TR')
                    out.push(tr);
            }
        }
    }
    return out;
}
/** A row's DIRECT cells — `<td>/<th>` children of this `<tr>` (a nested table's
 *  cells belong to its own rows, never directly to this one). */
function directCells(row) {
    return Array.from(row.childNodes).filter((n) => n.nodeName === 'TD' || n.nodeName === 'TH');
}
/** A cell that spans multiple columns/rows. Merged cells break the rectangular
 *  assumption GFM + the 2-col layout rely on, so a table with any forces flatten. */
function cellSpans(cell) {
    const col = Number(cell.getAttribute?.('colspan') ?? '1') || 1;
    const row = Number(cell.getAttribute?.('rowspan') ?? '1') || 1;
    return col > 1 || row > 1;
}
/** Convert one table to markdown by shape. `td`/`th` content is produced by a
 *  nested turndown pass (so nested lists/paragraphs/tables come through).
 *  `depth` (1 = top-level) sets the heading level for the section-layout path. */
function renderTable(table, turndown, depth) {
    const rowNodes = directRows(table);
    const spanned = rowNodes.some((r) => directCells(r).some(cellSpans));
    const rows = rowNodes.map((r) => directCells(r).map((c) => turndown.turndown(c.innerHTML).trim()));
    if (!rows.length)
        return '';
    const cols = rows[0].length;
    const uniform = rows.every((r) => r.length === cols);
    const anyMultiline = rows.some((r) => r.some((c) => c.includes('\n')));
    // 1) Clean rectangular grid of single-line cells → a real GFM table (row 1 =
    //    header). Merged cells (`spanned`) skip this — the grid isn't rectangular.
    if (!spanned && uniform && rows.length >= 2 && cols >= 2 && !anyMultiline) {
        return renderGfmTable(rows);
    }
    // 2) Two-column table. Section layout (short LABEL + substantial BODY in most
    //    rows) → headings; otherwise data → `label: value`. Merged cells fall
    //    through to flatten.
    if (!spanned && uniform && cols === 2) {
        const isSection = (row) => isLabel(row[0]) && isSubstantial(row[1]);
        if (rows.filter(isSection).length >= Math.ceil(rows.length / 2)) {
            // Heading level tracks nesting depth (## top-level, ### one level in, …),
            // so a nested table's labels read as children, not peers of the document's
            // top sections.
            const h = '#'.repeat(Math.min(depth + 1, 6));
            // Drop a leading column-header row (e.g. "Phases | Deliverables") when every
            // BODY row is section-like — it's a table header, noise once headingized.
            const body = rows.length > 1 && !isSection(rows[0]) && rows.slice(1).every(isSection)
                ? rows.slice(1)
                : rows;
            return body
                .map((row) => isSection(row)
                ? `${h} ${headingLabel(row[0])}\n\n${row[1]}`
                : row.filter(Boolean).join('\n\n'))
                .join('\n\n');
        }
        return rows
            .map(([left, right]) => isLabel(left) && right
            ? `${inlineText(left)}: ${inlineText(right)}`
            : [left, right].filter(Boolean).join('\n\n'))
            .join('\n\n');
    }
    // 3) Anything else → flatten: each non-empty cell as its own block.
    return rows.flat().filter(Boolean).join('\n\n');
}
function renderGfmTable(rows) {
    const cell = (s) => s.replace(/\|/g, '\\|').replace(/\n+/g, ' ').trim();
    const line = (cells) => `| ${cells.map(cell).join(' | ')} |`;
    const [header, ...body] = rows;
    return [
        line(header),
        `| ${header.map(() => '---').join(' | ')} |`,
        ...body.map(line),
    ].join('\n');
}
/** Collapse a cell's markdown to a single line (for headings / value cells). */
function inlineText(md) {
    return md.replace(/\s*\n+\s*/g, ' ').trim();
}
/** A short, single-line cell that can act as a section label / key. */
function isLabel(cell) {
    return !!cell && !cell.includes('\n') && cell.length <= MAX_LABEL_LEN;
}
/** A cell substantial enough to be a section BODY (not a short data value):
 *  multi-line, long, or a full sentence. Keeps a 2-col DATA table from turning
 *  every left value into a heading. */
function isSubstantial(cell) {
    return cell.includes('\n') || cell.length >= 40 || /[.!?](\s|$)/.test(cell);
}
/** A section label for a heading: single line, with one layer of surrounding
 *  bold/italic stripped (Word styles the label; the `##` already conveys it, so
 *  `**Project Scope**` → `Project Scope`, `_Phases_` → `Phases`). */
function headingLabel(md) {
    return inlineText(md)
        .replace(/^\*\*(.+?)\*\*$/, '$1')
        .replace(/^\*(.+?)\*$/, '$1')
        .replace(/^_(.+?)_$/, '$1')
        .trim();
}
/** Reading-text escape cleanup: turndown escapes `[ ] _` to protect markdown
 *  syntax, which litters placeholders (`\[MSA date\]`) and signature lines
 *  (`By: \_\_\_…`). Unescape those and collapse a long underscore run to a
 *  short rule — but ONLY outside code spans / fenced blocks, so literal code
 *  (snake_case, `a____b`, escaped brackets) is never rewritten. */
function cleanup(md) {
    // Split on code regions (fenced blocks first, then inline spans); transform
    // only the prose between them, re-emitting code verbatim.
    const codeRegion = /(```[\s\S]*?```|`[^`\n]*`)/g;
    let out = '';
    let last = 0;
    for (const m of md.matchAll(codeRegion)) {
        out += unescapeProse(md.slice(last, m.index));
        out += m[0];
        last = (m.index ?? 0) + m[0].length;
    }
    out += unescapeProse(md.slice(last));
    return out.replace(/\n{3,}/g, '\n\n').trim();
}
function unescapeProse(s) {
    return s
        // A run of 6+ underscores (escaped or not) — a signature line → short rule.
        // 6+ (not 4+) so it can't catch a stray escaped pair in ordinary prose.
        .replace(/(?:\\?_){6,}/g, '______')
        // Unescape the punctuation turndown guards that only adds noise to reading
        // text (brackets around placeholders, stray underscores).
        .replace(/\\([[\]_])/g, '$1');
}
//# sourceMappingURL=docx.js.map