@@ -6,23 +6,26 @@ import {
66 isEncryptedOfficeParserError ,
77 toFileParserError ,
88} from '@/lib/file-parsers/errors'
9+ import {
10+ assertHtmlStringWithinLimits ,
11+ htmlToStructuredText ,
12+ isHtmlComplexityError ,
13+ } from '@/lib/file-parsers/html-parser'
914import { parseOfficeText } from '@/lib/file-parsers/officeparser-module'
1015import type { FileParseOptions , FileParseResult , FileParser } from '@/lib/file-parsers/types'
1116import { sanitizeTextForUTF8 } from '@/lib/file-parsers/utils'
1217import { assertOoxmlArchiveWithinLimits } from '@/lib/file-parsers/zip-guard'
1318
1419const logger = createLogger ( 'DocxParser' )
1520
16- interface MammothMessage {
17- type : 'warning' | 'error'
18- message : string
19- }
20-
21- interface MammothResult {
22- value : string
23- messages : MammothMessage [ ]
24- }
25-
21+ /**
22+ * Extracts DOCX text by rendering the document to HTML with mammoth and walking
23+ * that HTML with the shared structured-text walker. mammoth's HTML keeps the
24+ * heading levels, list nesting, table rows, and footnotes that its raw-text mode
25+ * flattens to one paragraph per cell, so the output matches what the HTML parser
26+ * produces for the same document. (mammoth's Markdown mode is deprecated and
27+ * drops tables, so it is deliberately not used.)
28+ */
2629export class DocxParser implements FileParser {
2730 async parseFile ( filePath : string , options : FileParseOptions = { } ) : Promise < FileParseResult > {
2831 if ( ! filePath ) {
@@ -46,24 +49,29 @@ export class DocxParser implements FileParser {
4649 let parserReturnedEmpty = false
4750
4851 try {
49- const result = await mammoth . extractRawText ( { buffer } )
52+ const htmlResult = await mammoth . convertToHtml ( { buffer } )
5053 options . signal ?. throwIfAborted ( )
5154
52- if ( result . value && result . value . trim ( ) . length > 0 ) {
53- let htmlResult : MammothResult = { value : '' , messages : [ ] }
54- try {
55- htmlResult = await mammoth . convertToHtml ( { buffer } )
56- } catch {
57- // HTML conversion is optional
55+ const structured = this . structuredTextFromHtml ( htmlResult . value )
56+ if ( structured ) {
57+ return {
58+ content : sanitizeTextForUTF8 ( structured ) ,
59+ metadata : {
60+ extractionMethod : 'mammoth-html' ,
61+ messages : htmlResult . messages ,
62+ } ,
5863 }
59- options . signal ?. throwIfAborted ( )
64+ }
6065
66+ const rawResult = await mammoth . extractRawText ( { buffer } )
67+ options . signal ?. throwIfAborted ( )
68+
69+ if ( rawResult . value && rawResult . value . trim ( ) . length > 0 ) {
6170 return {
62- content : sanitizeTextForUTF8 ( result . value ) ,
71+ content : sanitizeTextForUTF8 ( rawResult . value ) ,
6372 metadata : {
6473 extractionMethod : 'mammoth' ,
65- messages : [ ...result . messages , ...htmlResult . messages ] ,
66- html : htmlResult . value ,
74+ messages : [ ...htmlResult . messages , ...rawResult . messages ] ,
6775 } ,
6876 }
6977 }
@@ -140,4 +148,23 @@ export class DocxParser implements FileParser {
140148 throw toFileParserError ( error , 'invalid_format' , 'Failed to parse DOCX buffer' )
141149 }
142150 }
151+
152+ /**
153+ * Walks mammoth's HTML rendering under the HTML parser's size caps. A rendering
154+ * too large to walk safely falls back to the raw-text path by returning empty,
155+ * since mammoth has already materialised the document once at that point.
156+ */
157+ private structuredTextFromHtml ( html : string ) : string {
158+ if ( ! html || html . trim ( ) . length === 0 ) return ''
159+ try {
160+ assertHtmlStringWithinLimits ( html )
161+ } catch ( error ) {
162+ if ( isHtmlComplexityError ( error ) ) {
163+ logger . warn ( 'mammoth HTML exceeds walker limits, using raw text:' , error . message )
164+ return ''
165+ }
166+ throw error
167+ }
168+ return htmlToStructuredText ( html ) . trim ( )
169+ }
143170}
0 commit comments