Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added src/tools/documents/__fixtures__/sample.doc
Binary file not shown.
Binary file added src/tools/documents/__fixtures__/table.doc
Binary file not shown.
31 changes: 31 additions & 0 deletions src/tools/documents/doc.fixtures.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { extractDocText } from './doc.lib';

// Real Word 97-2003 binary .doc files (generated with macOS `textutil`, neutral
// synthetic content) — regression coverage for the OLE + piece-table extractor.
const load = (name: string) =>
new Uint8Array(readFileSync(resolve(process.cwd(), 'src/tools/documents/__fixtures__', name)));

describe('extractDocText on real .doc fixtures', () => {
it('extracts plain and Unicode text from a simple .doc', () => {
const text = extractDocText(load('sample.doc'));
expect(text).toContain('Hello World');
expect(text).toContain('This is a test document.');
expect(text).toContain('Café résumé'); // accented characters
expect(text).toContain('90% done');
expect(text).toContain('Line A');
expect(text).toContain('Line B');
});

it('extracts a table as rows (tab-separated cells) plus a bulleted list', () => {
const text = extractDocText(load('table.doc'));
expect(text).toContain('Quarterly Report');
expect(text).toContain('Item\tQ1\tQ2');
expect(text).toContain('Revenue\t100\t150');
expect(text).toContain('Costs\t40\t55');
expect(text).toContain('First bullet');
expect(text).toContain('Closing line.');
});
});
10 changes: 8 additions & 2 deletions src/tools/documents/doc.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,18 @@ function decodePieces(wd: Uint8Array, table: Uint8Array, pcdt: Pcdt, ccpText: nu
return out;
}

/** Tidy extracted text: collapse runs of blank lines and trailing spaces. */
/**
* Tidy extracted text. Table cells are separated by single tab marks; a run of
* two or more marks (a cell mark plus the row-end mark) is a row boundary, so
* those become line breaks. Leading tabs and blank-line runs are trimmed.
*/
export function cleanDocText(s: string): string {
return s
.replace(/\r\n?/g, '\n')
.replace(/[ \t]+\n/g, '\n')
.replace(/\uFEFF/g, '')
.replace(/\t{2,}/g, '\n') // consecutive cell/row marks \u2192 new row
.replace(/[ \t]+\n/g, '\n')
.replace(/\n[ \t]+/g, '\n') // strip leading tabs/spaces on a line
.replace(/\n{3,}/g, '\n\n')
.trim();
}
Expand Down
Loading