import { enableDebugMode } from './logger'; /** * 解析进度阶段标识 */ export type ParseProgressStage = 'verifying' | 'parsing_fib' | 'parsing_clx' | 'parsing_formats' | 'parsing_fields' | 'parsing_shapes' | 'building_paragraphs' | 'extracting_properties' | 'extracting_images' | 'finalizing'; /** * 解析进度回调 * @param stage 阶段标识 * @param percent 进度百分比 0-100 */ export type ProgressCallback = (stage: ParseProgressStage, percent: number) => void; export { enableDebugMode }; /** * Synchronous .doc (OLE2/CFB) file parser. * * Extracts plain text and infers formatting (font size, bold, alignment, list type). * Format detection is heuristic-based since the parser does not read the full * Word CHP/PAP format tables — common patterns (short bold Chinese lines, etc.) * are mapped to likely document structure. */ export declare class DocParser { private buffer; private ole; private text; /** Max bytes to scan in the WordDocument stream (configurable). */ maxScanBytes: number; /** * @param buffer - The .doc file as ArrayBuffer. * @param maxScanBytes - Max bytes to scan. Default: 10MB. Pass a smaller value for memory-constrained environments. */ constructor(buffer: ArrayBuffer, maxScanBytes?: number); /** * Parse the document and return plain text only. * Uses FIB to locate the text stream; falls back to whole-file scanning. * @returns Parse result with `{ text, success, error? }`. */ parse(): { text: string; success: boolean; error?: string; }; /** * Parse the document and return formatted paragraphs (with inferred styles). * @returns Parse result with `{ document, text, success, error? }`. * `document.paragraphs` is an array of `{ text, charFormat, paraFormat }`. */ parseWithFormat(onProgress?: ProgressCallback): { success: boolean; document?: any; text?: string; error?: string; }; private extractTextWithFib; /** * Read the CLX blob that `fib.fcClx`/`lcbClx` points at. * * Per MS-DOC, the CLX lives in the table stream selected by `fWhichTblStm` * (0 → 0Table, 1 → 1Table), not in the WordDocument stream. We try the * correct table stream first, then fall back to the WordDocument stream for * files where the table stream is missing or the FIB points elsewhere — this * preserves the previous behavior for those edge cases. */ private readClxData; /** * Read the table stream (0Table or 1Table) data. * Returns the stream data, or null if not found. */ private readTableStream; private extractTextWithAutoDetect; /** * Common "junk" / binary-noise characters that appear in FIB headers * when 8-bit text is misinterpreted as UTF-16LE or vice versa. * Extracted to a constant to avoid regex duplication. */ private static readonly JUNK_CHAR_CLASS; private static readonly EXTRA_JUNK_CHARS; /** * Check whether a UTF-16 code unit is a printable character that should * be preserved in extracted text. Uses a "blocklist + range" strategy: * accept most Unicode printable characters, only reject control chars * and surrogate halves. */ private static isValidPrintableChar; /** * Mapping from 8-bit compressed (fComplex) high-byte characters (0x80-0xFF) * to their Unicode equivalents, following Windows-1252 / Word's conventions. * Index 0 corresponds to byte 0x80; index 127 corresponds to byte 0xFF. * null / undefined means "use the byte value as-is" (Latin-1 fallback). */ private static readonly HIGH_BYTE_MAP; /** * Scan UTF-16LE bytes and yield characters into a buffer. * Returns the number of bytes consumed (the loop advances i by this - 1). * Shared between extractTextSimple and extractParagraphsWithFormat. */ private static scanUtf16Char; private extractTextSimple; private static readonly MAX_PIECE_COUNT; private static readonly MAX_TOTAL_CHARS; /** Maximum number of images to extract from a single document. */ private static readonly MAX_IMAGES; private static readUint32; private static readUint16; /** * Parse the Clx (complex file information) structure to extract text from pieces. * * Clx structure: * clxt (1 byte) = 0x02 → indicates Pcdt follows * lcb (4 bytes) → length of Pcdt data * Pcdt (variable) * * Pcdt structure: * clxt (1 byte) = 0x01 * reserved (2 bytes) * lcbPlcPcd (4 bytes) → length of PlcPcd * PlcPcd (variable) * * PlcPcd structure: * n (4 bytes) → number of pieces * rgCcp ((n+1)*4 bytes) → character positions for each piece * rgPcd (n*8 bytes) → PCD entries for each piece * * PCD entry (8 bytes each): * reserved (2 bytes) → flags / unused * fc (4 bytes) → file offset + compression flag * bit 30: fCompressed (1 = 8-bit, 0 = UTF-16LE) * bits 0-29: actual file offset * prm (2 bytes) → property modifier (CHP/PAP info, unused here) * * This overload concatenates text from ALL pieces. When the FIB exposes * per-story character counts (rgCcp), prefer parseClxWithStories() — it * splits pieces by story boundary so header/footer/footnote text does * not leak into the main body. */ private parseClx; /** * Parse the PlcPcd inside a CLX blob and return piece metadata. * Returns an empty array when the structure is malformed or exceeds * safety limits — callers should fall back to extractTextSimple. */ private parseClxPieces; /** * Split pieces into per-story text using FibRgLw's rgCcp boundaries. * * Word stores all stories as one continuous character stream: * [main][footnotes][headers/footers][macro][comments][endnotes][textboxes][header textboxes] * * Each piece's global CP range is intersected with each story's CP range. * When a piece straddles a story boundary, the byte range is sliced by * character offset so each story gets only its own characters. * * Returns null when there are no pieces or ccpText is zero (caller should * fall back to parseClx's whole-document concatenation). */ private splitPiecesByStory; /** * Parse CLX and split by story. Returns null when story splitting is not * applicable (no pieces, or ccpText is zero); callers should fall back to * parseClx() which concatenates all pieces. */ private parseClxWithStories; private static readonly BINARY_SIGNATURES; private containsBinarySignature; private extractTextFromRange; /** * Parse CHPX (character property) and PAPX (paragraph property) runs from * the table stream. Returns empty arrays if the data is missing or malformed. * * The CHP/PAP tables are stored in the table stream (0Table / 1Table) and * their offsets are given by fcPlcfBteChpx / fcPlcfBtePapx in the FIB. */ private parseFormatRuns; /** * Create formatted paragraphs with real CHP/PAP format data. * Uses single newline splitting and original paragraph indices for accurate * PAPX matching, and text offset as cp estimate for CHPX matching. * * @param pieceMap - Optional piece map from splitPiecesByStory that gives * per-character CHPX associations via piece-level chpxIndex. */ private createParagraphsWithRealFormats; private extractFormattedText; /** * Convert internal StoryText to the public DocumentStories shape. * Drops empty fields so the UI can simply check `stories?.footnotes`. * * 当 DOP 标志位启用首页不同/奇偶页不同时,尝试通过 PlcfHdd 将 headers * story 拆分为首页/奇数页/偶数页页眉页脚。PlcfHdd 不可用时回退到 * 启发式段落拆分。 */ private toDocumentStories; /** * 拆分页眉页脚 story 文本为首页/奇数页/偶数页页眉页脚。 * * 优先使用 PlcfHdd 精确拆分;PlcfHdd 不可用时回退到启发式段落拆分。 * 如果提供了 chpxRuns,同时提取页眉页脚区域中的图片。 */ private splitHeaderParts; /** * Extract embedded images from the document. * * Word 97-2003 stores embedded pictures as binary blobs inside the `Data` * stream. Each picture is referenced from the document text via a special * character and a CHP `fcPic` pointer, but parsing that requires the CHP * table which this project does not yet read. * * As a pragmatic fallback, we scan the `Data` stream (and the WordDocument * stream as a secondary fallback) for well-known image magic numbers * (PNG / JPEG / BMP / GIF) and slice out each image. Returns an array of * data URLs ready for inline `` rendering. */ private extractImages; /** * Extract embedded pictures with structured info (format, dimensions, etc.) * * Uses PICF envelope detection when possible, falling back to magic-number * scanning. Returns structured picture objects instead of plain data URLs. */ private extractPictures; /** * Extract document properties from the SummaryInformation stream. * * Word documents store metadata (title, author, keywords, etc.) in the * SummaryInformation stream using the OLE Property Set format (MS-OLEPS). * * @param directory - Directory entries from the OLE container. * @returns Document properties, or null if not found/parse failed. */ private extractProperties; /** * Extract DOP (Document Properties) from the table stream. * * Per MS-DOC §2.5.6, the DOP sits at offset `fcDop` in the table stream * (0Table or 1Table selected by fWhichTblStm) with length `lcbDop`. * Returns null if FIB has no DOP offset or the data is invalid. */ private extractDop; private extractTextWithFormatFromFib; private extractParagraphsWithFormat; private filterParagraphsWithGenericLogic; private filterAndEnhanceParagraphs; /** * Apply heuristic formats based on paragraph structure (length, position, continuity). * This is used as a fallback when FIB format offsets are invalid (e.g. libwv files). * Does NOT use string matching on content — only structural features. */ private applyStructuralFormats; private detectEncodingFromBinary; private scoreRawParagraphs; private scorePlainText; private guessCharFormat; private detectCharacterStyles; private shouldHaveUnderline; private isSameStyle; private getChineseFont; private detectParagraphFormat; /** * Detect list marker at the start of a paragraph. * * Supported markers (ordered): * - Arabic numerals: `1.`, `2)`, `(1)`, `(1)` * - Multi-level Arabic: `1.1`, `1.2.3` * - Single Latin letter: `a.`, `B)`, `(a)` * - Roman numerals: `i.`, `ii.`, `iv.`, `I.`, `II.` * - CJK ideographic: `一、`, `二、`, `(一)`, `甲、`, `乙、` * - Circled numbers: `① ② ... ㊿` * * Supported markers (unordered): * - ASCII bullets: `-`, `*`, `+` * - CJK bullets: `• ○ ● ▪ ▸ ► → ◇ ◆` * * The list level is inferred from leading whitespace (per 2 spaces ≈ 1 level). * Returns null if the paragraph does not look like a list item. */ private detectListInfo; private detectAlignment; private cleanParagraph; private removeInternalDuplicates; private stripBinaryPrefix; /** * 替换 mainText 中的页码域占位文本。 * * 由于 extractTextFromRange 跳过了 0x13/0x14/0x15 域字符,mainText 中页码域的 * instruction 和 result 连在一起(如 "PAGE1"、"NUMPAGES5")。本方法将每个域的 * instruction+result 连接串替换为纯 result,使段落显示为页码而非域代码。 * * 策略: * 1. 对每个页码域,构建搜索串 = instruction.trim() + result * 2. 在 mainText 中搜索该串,替换为 result * 3. 由于 instruction 通常包含开关(如 "PAGE \* MERGEFORMAT"),搜索串足够独特 * 4. 仅替换第一个匹配,避免误伤正文 * * 若搜索串未找到匹配(mainText 偏移与 cp 不一致等情况),该域保留原状, * 由 cleanWordFieldCodes 的启发式清理兜底。 */ private replacePageFieldsInText; private isValidChar; private cleanWordFieldCodes; private hasSignificantContent; private shouldSkipParagraph; private hasTooManyJunkChars; private createFormattedParagraphsFromText; /** * Split paragraphs that contain multiple table rows into separate row paragraphs. * Some non-standard .doc files (e.g., libwv-generated) store entire tables as a * single paragraph, using runs of 2+ consecutive \u0007 as row boundaries. */ private splitMultiRowTables; /** * Try to parse format data directly from 1Table stream when FIB offsets are invalid. * Used for non-standard files (like libwv-generated docs) that have valid format data * but invalid FIB offset tables. */ private tryParseFormatsFromTableStream; /** * Apply parsed styles to paragraphs (used when FIB offsets are invalid but 1Table has format data). */ private applyParsedStylesToParagraphs; private extractTextFromFullFile; } /** * Parse a .doc File object and extract plain text. * @param file - The .doc file to parse. * @param debug - If true, enables debug logging. * @returns A promise that resolves to the parse result. */ export declare function parseDocFile(file: File, debug?: boolean): Promise<{ text: string; success: boolean; error?: string; }>; /** * Parse a .doc file from an ArrayBuffer and return the formatted document. * @param buffer - The .doc file as ArrayBuffer. * @param _fileName - Optional file name (for logging). * @returns The parse result with paragraphs and text. */ export declare function parseDocFileFromBuffer(buffer: ArrayBuffer, _fileName?: string): { success: boolean; document?: any; text?: string; error?: string; }; /** * Parse a .doc File and return the formatted document with paragraphs and text. * @param file - The .doc file to parse. * @param debug - If true, enables debug logging. * @returns A promise that resolves to the parse result. */ export declare function parseDocFileWithFormat(file: File, debug?: boolean): Promise<{ success: boolean; document?: any; text?: string; error?: string; }>;