india2actual 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +23 -0
- package/LICENSE +21 -0
- package/README.md +280 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +295 -0
- package/dist/cli.js.map +1 -0
- package/dist/env-file.d.ts +38 -0
- package/dist/env-file.js +40 -0
- package/dist/env-file.js.map +1 -0
- package/dist/extract/csv.d.ts +13 -0
- package/dist/extract/csv.js +60 -0
- package/dist/extract/csv.js.map +1 -0
- package/dist/extract/html-table.d.ts +3 -0
- package/dist/extract/html-table.js +77 -0
- package/dist/extract/html-table.js.map +1 -0
- package/dist/extract/index.d.ts +21 -0
- package/dist/extract/index.js +44 -0
- package/dist/extract/index.js.map +1 -0
- package/dist/extract/pdf.d.ts +47 -0
- package/dist/extract/pdf.js +439 -0
- package/dist/extract/pdf.js.map +1 -0
- package/dist/extract/sniff.d.ts +12 -0
- package/dist/extract/sniff.js +57 -0
- package/dist/extract/sniff.js.map +1 -0
- package/dist/extract/spreadsheetml.d.ts +3 -0
- package/dist/extract/spreadsheetml.js +93 -0
- package/dist/extract/spreadsheetml.js.map +1 -0
- package/dist/extract/types.d.ts +20 -0
- package/dist/extract/types.js +2 -0
- package/dist/extract/types.js.map +1 -0
- package/dist/extract/xlsx.d.ts +2 -0
- package/dist/extract/xlsx.js +64 -0
- package/dist/extract/xlsx.js.map +1 -0
- package/dist/interpret/header.d.ts +24 -0
- package/dist/interpret/header.js +57 -0
- package/dist/interpret/header.js.map +1 -0
- package/dist/interpret/roundtrip.d.ts +29 -0
- package/dist/interpret/roundtrip.js +89 -0
- package/dist/interpret/roundtrip.js.map +1 -0
- package/dist/interpret/rows.d.ts +54 -0
- package/dist/interpret/rows.js +151 -0
- package/dist/interpret/rows.js.map +1 -0
- package/dist/interpret/synonyms.d.ts +27 -0
- package/dist/interpret/synonyms.js +55 -0
- package/dist/interpret/synonyms.js.map +1 -0
- package/dist/interpret/validate.d.ts +27 -0
- package/dist/interpret/validate.js +93 -0
- package/dist/interpret/validate.js.map +1 -0
- package/dist/interpret/values.d.ts +22 -0
- package/dist/interpret/values.js +131 -0
- package/dist/interpret/values.js.map +1 -0
- package/dist/merchants-file.d.ts +14 -0
- package/dist/merchants-file.js +40 -0
- package/dist/merchants-file.js.map +1 -0
- package/dist/narration/merchants.d.ts +46 -0
- package/dist/narration/merchants.js +161 -0
- package/dist/narration/merchants.js.map +1 -0
- package/dist/narration/parse.d.ts +14 -0
- package/dist/narration/parse.js +659 -0
- package/dist/narration/parse.js.map +1 -0
- package/dist/narration/types.d.ts +25 -0
- package/dist/narration/types.js +2 -0
- package/dist/narration/types.js.map +1 -0
- package/dist/out/csv.d.ts +21 -0
- package/dist/out/csv.js +40 -0
- package/dist/out/csv.js.map +1 -0
- package/dist/out/push.d.ts +74 -0
- package/dist/out/push.js +103 -0
- package/dist/out/push.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs';
|
|
2
|
+
import { roleForHeader } from '../interpret/synonyms.js';
|
|
3
|
+
import { parseStatementDate } from '../interpret/values.js';
|
|
4
|
+
/**
|
|
5
|
+
* Text items on the same visual line can differ slightly in baseline, so
|
|
6
|
+
* y values are clustered rather than compared exactly.
|
|
7
|
+
*/
|
|
8
|
+
const LINE_TOLERANCE = 2;
|
|
9
|
+
/**
|
|
10
|
+
* How far above its own row a wrapped cell's first line may sit.
|
|
11
|
+
*
|
|
12
|
+
* Statement PDFs render a multi-line description cell centred on its row, so
|
|
13
|
+
* its first line can begin slightly *above* the line carrying the date and
|
|
14
|
+
* amounts. Without this, that line would attach to the previous transaction.
|
|
15
|
+
*/
|
|
16
|
+
const ATTACH_ABOVE = 6;
|
|
17
|
+
/**
|
|
18
|
+
* How far short of its column's wrap width a fragment must end to count as a
|
|
19
|
+
* deliberate line break rather than a wrap.
|
|
20
|
+
*/
|
|
21
|
+
const SHORT_LINE_MARGIN = 8;
|
|
22
|
+
/**
|
|
23
|
+
* How far below its row a wrapped line may sit and still belong to it.
|
|
24
|
+
*
|
|
25
|
+
* Page footers live well below the last transaction on a page, and without a
|
|
26
|
+
* limit they are folded into it — which is how ICICI's `www.icici.bank.in`
|
|
27
|
+
* and its toll-free number ended up inside a narration and an amount.
|
|
28
|
+
*/
|
|
29
|
+
const MAX_CONTINUATION_BELOW = 40;
|
|
30
|
+
async function readLines(path, options) {
|
|
31
|
+
const doc = await getDocument({
|
|
32
|
+
url: path,
|
|
33
|
+
useSystemFonts: true,
|
|
34
|
+
...(options.password ? { password: options.password } : {}),
|
|
35
|
+
}).promise;
|
|
36
|
+
const lines = [];
|
|
37
|
+
for (let pageNumber = 1; pageNumber <= doc.numPages; pageNumber += 1) {
|
|
38
|
+
const page = await doc.getPage(pageNumber);
|
|
39
|
+
const content = await page.getTextContent();
|
|
40
|
+
const items = [];
|
|
41
|
+
for (const raw of content.items) {
|
|
42
|
+
if (!('str' in raw) || !raw.str.trim()) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const x = raw.transform[4];
|
|
46
|
+
const width = typeof raw.width === 'number' ? raw.width : 0;
|
|
47
|
+
items.push({
|
|
48
|
+
x,
|
|
49
|
+
right: x + width,
|
|
50
|
+
y: raw.transform[5],
|
|
51
|
+
text: raw.str,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Pages are independent: y coordinates restart, so lines must not be
|
|
55
|
+
// clustered across a page boundary.
|
|
56
|
+
lines.push(...clusterLines(items, pageNumber));
|
|
57
|
+
}
|
|
58
|
+
return { lines, pages: doc.numPages };
|
|
59
|
+
}
|
|
60
|
+
/** Group items into lines, top of page first. */
|
|
61
|
+
function clusterLines(items, page) {
|
|
62
|
+
const sorted = [...items].sort((a, b) => b.y - a.y);
|
|
63
|
+
const lines = [];
|
|
64
|
+
for (const item of sorted) {
|
|
65
|
+
const current = lines[lines.length - 1];
|
|
66
|
+
if (current && Math.abs(current.y - item.y) <= LINE_TOLERANCE) {
|
|
67
|
+
current.items.push(item);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
lines.push({ y: item.y, page, items: [item] });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
line.items.sort((a, b) => a.x - b.x);
|
|
75
|
+
}
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Does this line start a transaction?
|
|
80
|
+
*
|
|
81
|
+
* Judged from the raw items rather than from assembled cells, because column
|
|
82
|
+
* inference depends on this and must not depend on columns in turn.
|
|
83
|
+
*/
|
|
84
|
+
function isAnchorLine(line) {
|
|
85
|
+
return line.items.some(item => parseStatementDate(item.text) !== null);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Is this a summary row rather than a transaction?
|
|
89
|
+
*
|
|
90
|
+
* Statements bracket the table with totals and carried-forward balances:
|
|
91
|
+
* Federal prints `GRAND TOTAL` with per-column sums, and an
|
|
92
|
+
* `Opening Balance` row above the first transaction. Neither has a date, so
|
|
93
|
+
* both would otherwise be folded into the nearest transaction — the grand
|
|
94
|
+
* total's column sums landing in an amount field, which the balance check
|
|
95
|
+
* then rejects.
|
|
96
|
+
*/
|
|
97
|
+
function isSummaryLine(line) {
|
|
98
|
+
const text = line.items
|
|
99
|
+
.map(item => item.text)
|
|
100
|
+
.join(' ')
|
|
101
|
+
.trim();
|
|
102
|
+
return /^(grand\s*total|sub\s*total|total|opening\s*balance|closing\s*balance|balance\s*(carried|brought)\s*forward|[bc]\/f)\b/i.test(text);
|
|
103
|
+
}
|
|
104
|
+
/** Does this line contribute column labels? */
|
|
105
|
+
function isHeaderLine(line) {
|
|
106
|
+
if (isAnchorLine(line) || isSummaryLine(line)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return (line.items.filter(item => roleForHeader(item.text) !== null).length >= 2);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Narrowest run of horizontal whitespace that separates two columns.
|
|
113
|
+
*
|
|
114
|
+
* Measured against real statements: Federal's tightest genuine gutter is 7
|
|
115
|
+
* points and ICICI's is 5, while gaps *within* a column are smaller still.
|
|
116
|
+
*/
|
|
117
|
+
const MIN_GUTTER = 4;
|
|
118
|
+
/** Index of the first line contributing column labels, or -1. */
|
|
119
|
+
function firstHeaderIndex(lines) {
|
|
120
|
+
return lines.findIndex(isHeaderLine);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The lines making up the transaction table: the header, plus the dated rows
|
|
124
|
+
* below it.
|
|
125
|
+
*
|
|
126
|
+
* Anchors are restricted to lines *after* the header because statement
|
|
127
|
+
* preambles contain dates of their own — Federal prints `Address Last Updated
|
|
128
|
+
* On : 13/08/2024` and `Account Open Date : 25/03/2013` above the table, and
|
|
129
|
+
* without this each becomes a phantom transaction.
|
|
130
|
+
*/
|
|
131
|
+
function tableLines(lines) {
|
|
132
|
+
const header = firstHeaderIndex(lines);
|
|
133
|
+
return lines.filter((line, index) => {
|
|
134
|
+
if (isHeaderLine(line)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return isAnchorLine(line) && (header < 0 || index > header);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Infer columns from the vertical whitespace that separates them.
|
|
142
|
+
*
|
|
143
|
+
* Every column of a statement table is divided from its neighbour by a gutter
|
|
144
|
+
* that stays empty on every row, so accumulating the horizontal extent of all
|
|
145
|
+
* table text and splitting on runs of emptiness recovers the columns directly.
|
|
146
|
+
*
|
|
147
|
+
* This is markedly more robust than comparing where text starts or merging
|
|
148
|
+
* overlapping spans, both of which were tried first and failed on real files:
|
|
149
|
+
*
|
|
150
|
+
* - Start positions alone split a right-aligned amount from its own header,
|
|
151
|
+
* which can begin 36 points to its left.
|
|
152
|
+
* - Merging overlapping spans bridges columns whenever a header label is wider
|
|
153
|
+
* than the column spacing. Federal packs columns 45 points apart with labels
|
|
154
|
+
* nearly that wide, collapsing `Withdrawals` and `Deposits` into one cell.
|
|
155
|
+
*
|
|
156
|
+
* Only header and dated rows take part. Wrapped narration lines and page
|
|
157
|
+
* furniture would otherwise bridge gutters — ICICI's footer URL spans the gap
|
|
158
|
+
* between the serial-number and date columns.
|
|
159
|
+
*/
|
|
160
|
+
export function inferBands(lines) {
|
|
161
|
+
const source = tableLines(lines);
|
|
162
|
+
const items = (source.length ? source : lines).flatMap(line => line.items);
|
|
163
|
+
if (!items.length) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const width = Math.ceil(Math.max(...items.map(item => item.right))) + MIN_GUTTER;
|
|
167
|
+
const occupancy = new Uint32Array(width + 1);
|
|
168
|
+
for (const item of items) {
|
|
169
|
+
const from = Math.max(0, Math.floor(item.x));
|
|
170
|
+
const to = Math.min(width, Math.ceil(Math.max(item.right, item.x + 1)));
|
|
171
|
+
for (let x = from; x < to; x += 1) {
|
|
172
|
+
occupancy[x] = (occupancy[x] ?? 0) + 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const bands = [];
|
|
176
|
+
let x = 0;
|
|
177
|
+
while (x < width) {
|
|
178
|
+
if (!occupancy[x]) {
|
|
179
|
+
x += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const left = x;
|
|
183
|
+
let right = x;
|
|
184
|
+
while (x < width) {
|
|
185
|
+
if (occupancy[x]) {
|
|
186
|
+
x += 1;
|
|
187
|
+
right = x;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// Bridge gaps too narrow to be a column separator.
|
|
191
|
+
let gapEnd = x;
|
|
192
|
+
while (gapEnd < width && !occupancy[gapEnd]) {
|
|
193
|
+
gapEnd += 1;
|
|
194
|
+
}
|
|
195
|
+
if (gapEnd - x < MIN_GUTTER && gapEnd < width) {
|
|
196
|
+
x = gapEnd;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
bands.push({ left, right });
|
|
202
|
+
}
|
|
203
|
+
return bands;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Assign an item to a column.
|
|
207
|
+
*
|
|
208
|
+
* Normally the band it overlaps most. When it overlaps none, it goes to the
|
|
209
|
+
* first band starting at or after it, because a cell's text begins at the
|
|
210
|
+
* cell's left edge, which sits left of the header label naming it — ICICI's
|
|
211
|
+
* description cell starts at x=192 while its `Transaction Remarks` header
|
|
212
|
+
* starts at x=247. Choosing the merely *nearest* band would instead drop a
|
|
213
|
+
* short wrapped line such as `9f03689dd` (x=192..237) into the preceding
|
|
214
|
+
* column, whose right edge is closer to it.
|
|
215
|
+
*/
|
|
216
|
+
function bandOf(item, bands) {
|
|
217
|
+
let bestIndex = -1;
|
|
218
|
+
let bestOverlap = 0;
|
|
219
|
+
for (const [index, band] of bands.entries()) {
|
|
220
|
+
const overlap = Math.min(item.right, band.right) - Math.max(item.x, band.left);
|
|
221
|
+
if (overlap > bestOverlap) {
|
|
222
|
+
bestOverlap = overlap;
|
|
223
|
+
bestIndex = index;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (bestIndex >= 0) {
|
|
227
|
+
return bestIndex;
|
|
228
|
+
}
|
|
229
|
+
const toTheRight = bands.findIndex(band => band.left >= item.x);
|
|
230
|
+
return toTheRight >= 0 ? toTheRight : Math.max(0, bands.length - 1);
|
|
231
|
+
}
|
|
232
|
+
/** Split one line into per-column fragments. */
|
|
233
|
+
function toFragments(line, bands) {
|
|
234
|
+
const fragments = [];
|
|
235
|
+
for (const item of line.items) {
|
|
236
|
+
const column = bandOf(item, bands);
|
|
237
|
+
const existing = fragments[column];
|
|
238
|
+
fragments[column] = existing
|
|
239
|
+
? {
|
|
240
|
+
text: `${existing.text} ${item.text}`,
|
|
241
|
+
right: Math.max(existing.right, item.right),
|
|
242
|
+
}
|
|
243
|
+
: { text: item.text, right: item.right };
|
|
244
|
+
}
|
|
245
|
+
return fragments;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The right-hand edge each column's text reaches, i.e. its wrap width.
|
|
249
|
+
* Used to tell a wrapped line from a deliberate line break.
|
|
250
|
+
*/
|
|
251
|
+
function wrapWidths(rows) {
|
|
252
|
+
const widths = [];
|
|
253
|
+
for (const row of rows) {
|
|
254
|
+
for (const [column, fragment] of row.entries()) {
|
|
255
|
+
if (!fragment) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
widths[column] = Math.max(widths[column] ?? 0, fragment.right);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return widths;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Join a cell's fragments back into one string.
|
|
265
|
+
*
|
|
266
|
+
* A fragment ending well short of its column's wrap width was a deliberate
|
|
267
|
+
* line break, so the next fragment starts a new word and needs a space. One
|
|
268
|
+
* that runs to the wrap width was broken mid-flow, where inserting a space
|
|
269
|
+
* would corrupt a reference split across lines (`...837e576` + `b9cc0dd2ee`).
|
|
270
|
+
*
|
|
271
|
+
* Statement PDFs need this distinction: ICICI prints a short descriptor line
|
|
272
|
+
* (`NACH trxn`) above each wrapped narration, and joining that without a
|
|
273
|
+
* separator yields `NACH trxnACH/HDFC...`.
|
|
274
|
+
*
|
|
275
|
+
* A space lost at a wrap that happened to fall on a word boundary is not
|
|
276
|
+
* recoverable from the PDF, so `FEDERAL BA` can come back as `FEDERALBA`.
|
|
277
|
+
*/
|
|
278
|
+
function joinFragments(fragments, wrapWidth) {
|
|
279
|
+
let text = '';
|
|
280
|
+
let previous;
|
|
281
|
+
for (const fragment of fragments) {
|
|
282
|
+
if (previous && previous.right < wrapWidth - SHORT_LINE_MARGIN) {
|
|
283
|
+
text += ' ';
|
|
284
|
+
}
|
|
285
|
+
text += fragment.text;
|
|
286
|
+
previous = fragment;
|
|
287
|
+
}
|
|
288
|
+
return text;
|
|
289
|
+
}
|
|
290
|
+
function mergeHeader(rows) {
|
|
291
|
+
const merged = [];
|
|
292
|
+
for (const row of rows) {
|
|
293
|
+
for (const [column, fragment] of row.entries()) {
|
|
294
|
+
if (!fragment) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
merged[column] = merged[column]
|
|
298
|
+
? `${merged[column]} ${fragment.text}`
|
|
299
|
+
: fragment.text;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
for (let index = 0; index < merged.length; index += 1) {
|
|
303
|
+
merged[index] ??= '';
|
|
304
|
+
}
|
|
305
|
+
return merged;
|
|
306
|
+
}
|
|
307
|
+
export function assembleRows(lines, bands) {
|
|
308
|
+
const fragmentsFor = lines.map(line => toFragments(line, bands));
|
|
309
|
+
const isAnchor = lines.map(isAnchorLine);
|
|
310
|
+
const isHeader = lines.map(isHeaderLine);
|
|
311
|
+
const widths = wrapWidths(fragmentsFor);
|
|
312
|
+
const rows = [];
|
|
313
|
+
// Only the first run of header lines is used. Statement PDFs repeat the
|
|
314
|
+
// header on every page, and merging all of them would produce labels like
|
|
315
|
+
// "Balance (INR) Balance (INR) Balance (INR)".
|
|
316
|
+
const firstHeader = isHeader.indexOf(true);
|
|
317
|
+
if (firstHeader >= 0) {
|
|
318
|
+
const group = [];
|
|
319
|
+
for (let index = firstHeader; index < lines.length; index += 1) {
|
|
320
|
+
if (isAnchor[index]) {
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
if (isHeader[index]) {
|
|
324
|
+
group.push(fragmentsFor[index] ?? []);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
rows.push(mergeHeader(group));
|
|
328
|
+
}
|
|
329
|
+
// Restricted to lines below the header for the same reason as in
|
|
330
|
+
// `tableLines`: dates in the statement preamble are not transactions.
|
|
331
|
+
const anchorIndexes = isAnchor
|
|
332
|
+
.map((anchor, index) => (anchor ? index : -1))
|
|
333
|
+
.filter(index => index >= 0 && (firstHeader < 0 || index > firstHeader));
|
|
334
|
+
// Which transaction each continuation line belongs to. Lines left unowned
|
|
335
|
+
// are discarded, which is what removes the statement preamble.
|
|
336
|
+
const owner = new Map();
|
|
337
|
+
for (const [position, anchorIndex] of anchorIndexes.entries()) {
|
|
338
|
+
const previousAnchor = anchorIndexes[position - 1];
|
|
339
|
+
const start = previousAnchor === undefined ? 0 : previousAnchor + 1;
|
|
340
|
+
const anchor = lines[anchorIndex];
|
|
341
|
+
if (!anchor) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
for (let index = start; index < anchorIndex; index += 1) {
|
|
345
|
+
const line = lines[index];
|
|
346
|
+
if (!line || isHeader[index] || isSummaryLine(line)) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const previous = previousAnchor === undefined ? undefined : lines[previousAnchor];
|
|
350
|
+
const gap = line.y - anchor.y;
|
|
351
|
+
// Requiring the same page, and a small *positive* gap, is what keeps a
|
|
352
|
+
// page footer off the next page's first transaction: across a page
|
|
353
|
+
// boundary y restarts, making the gap wildly negative and so trivially
|
|
354
|
+
// "just above".
|
|
355
|
+
if (line.page === anchor.page && gap >= 0 && gap <= ATTACH_ABOVE) {
|
|
356
|
+
owner.set(index, anchorIndex);
|
|
357
|
+
}
|
|
358
|
+
else if (previousAnchor !== undefined &&
|
|
359
|
+
previous &&
|
|
360
|
+
previous.page === line.page &&
|
|
361
|
+
previous.y - line.y <= MAX_CONTINUATION_BELOW) {
|
|
362
|
+
owner.set(index, previousAnchor);
|
|
363
|
+
}
|
|
364
|
+
// Otherwise it belongs to no transaction on this page — a preamble,
|
|
365
|
+
// page header or footer — and is dropped.
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Lines after the last anchor continue it, but only on its own page and
|
|
369
|
+
// only while they are close enough to be part of its cell.
|
|
370
|
+
const lastAnchor = anchorIndexes[anchorIndexes.length - 1];
|
|
371
|
+
const lastAnchorLine = lastAnchor === undefined ? undefined : lines[lastAnchor];
|
|
372
|
+
if (lastAnchor !== undefined && lastAnchorLine) {
|
|
373
|
+
for (let index = lastAnchor + 1; index < lines.length; index += 1) {
|
|
374
|
+
const line = lines[index];
|
|
375
|
+
if (!isHeader[index] &&
|
|
376
|
+
line &&
|
|
377
|
+
!isSummaryLine(line) &&
|
|
378
|
+
line.page === lastAnchorLine.page &&
|
|
379
|
+
lastAnchorLine.y - line.y <= MAX_CONTINUATION_BELOW) {
|
|
380
|
+
owner.set(index, lastAnchor);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const continuations = new Map();
|
|
385
|
+
for (const [index, anchorIndex] of owner) {
|
|
386
|
+
const list = continuations.get(anchorIndex) ?? [];
|
|
387
|
+
list.push(index);
|
|
388
|
+
continuations.set(anchorIndex, list);
|
|
389
|
+
}
|
|
390
|
+
for (const anchorIndex of anchorIndexes) {
|
|
391
|
+
// Document order, so wrapped text reassembles in reading order.
|
|
392
|
+
const lineIndexes = [
|
|
393
|
+
anchorIndex,
|
|
394
|
+
...(continuations.get(anchorIndex) ?? []).filter(index => index !== anchorIndex),
|
|
395
|
+
].sort((a, b) => a - b);
|
|
396
|
+
// Split by position relative to the anchor. ICICI prints a descriptor
|
|
397
|
+
// above each row (`Debit trxn`, `NACH trxn`) that is *not* part of the
|
|
398
|
+
// bank's narration — the source spreadsheet contains no occurrence of
|
|
399
|
+
// "trxn" at all. Where such a line does carry real text, that text also
|
|
400
|
+
// appears in the wrapped narration below. So below-anchor content wins,
|
|
401
|
+
// and the line above is used only when there is nothing below it.
|
|
402
|
+
const main = new Map();
|
|
403
|
+
const above = new Map();
|
|
404
|
+
for (const index of lineIndexes) {
|
|
405
|
+
const target = index < anchorIndex ? above : main;
|
|
406
|
+
for (const [column, fragment] of (fragmentsFor[index] ?? []).entries()) {
|
|
407
|
+
if (!fragment) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const list = target.get(column) ?? [];
|
|
411
|
+
list.push(fragment);
|
|
412
|
+
target.set(column, list);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const cells = [];
|
|
416
|
+
for (const [column, fragments] of main) {
|
|
417
|
+
cells[column] = joinFragments(fragments, widths[column] ?? 0);
|
|
418
|
+
}
|
|
419
|
+
for (const [column, fragments] of above) {
|
|
420
|
+
if (!cells[column]) {
|
|
421
|
+
cells[column] = joinFragments(fragments, widths[column] ?? 0);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
for (let index = 0; index < cells.length; index += 1) {
|
|
425
|
+
cells[index] ??= '';
|
|
426
|
+
}
|
|
427
|
+
rows.push(cells);
|
|
428
|
+
}
|
|
429
|
+
return rows;
|
|
430
|
+
}
|
|
431
|
+
export async function extractPdf(path, options = {}) {
|
|
432
|
+
const { lines, pages } = await readLines(path, options);
|
|
433
|
+
const bands = inferBands(lines);
|
|
434
|
+
return {
|
|
435
|
+
rows: assembleRows(lines, bands),
|
|
436
|
+
source: { path, format: 'pdf', part: `${pages} page(s)` },
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
//# sourceMappingURL=pdf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdf.js","sourceRoot":"","sources":["../../src/extract/pdf.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a file actually is, as opposed to what its extension claims.
|
|
3
|
+
*
|
|
4
|
+
* This distinction is not pedantry for Indian bank statements: a file named
|
|
5
|
+
* `.xls` is very often an HTML table or a SpreadsheetML document rather than a
|
|
6
|
+
* real Excel workbook, because that is the cheapest thing for a bank's
|
|
7
|
+
* reporting layer to emit. Trusting the extension gets you a parser error on a
|
|
8
|
+
* perfectly readable file.
|
|
9
|
+
*/
|
|
10
|
+
export type DetectedFormat = 'xlsx' | 'html' | 'spreadsheetml' | 'biff' | 'pdf' | 'text';
|
|
11
|
+
export declare function detectFormat(buffer: Buffer): DetectedFormat;
|
|
12
|
+
export declare function describeFormat(format: DetectedFormat): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** OOXML/ZIP: `PK\x03\x04`. */
|
|
2
|
+
const ZIP_MAGIC = [0x50, 0x4b, 0x03, 0x04];
|
|
3
|
+
/** OLE2 compound document, used by legacy .xls and .doc. */
|
|
4
|
+
const OLE2_MAGIC = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
|
|
5
|
+
/** `%PDF-`. */
|
|
6
|
+
const PDF_MAGIC = [0x25, 0x50, 0x44, 0x46, 0x2d];
|
|
7
|
+
function startsWithBytes(buffer, magic) {
|
|
8
|
+
if (buffer.length < magic.length) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
return magic.every((byte, index) => buffer[index] === byte);
|
|
12
|
+
}
|
|
13
|
+
export function detectFormat(buffer) {
|
|
14
|
+
if (startsWithBytes(buffer, ZIP_MAGIC)) {
|
|
15
|
+
return 'xlsx';
|
|
16
|
+
}
|
|
17
|
+
if (startsWithBytes(buffer, OLE2_MAGIC)) {
|
|
18
|
+
return 'biff';
|
|
19
|
+
}
|
|
20
|
+
if (startsWithBytes(buffer, PDF_MAGIC)) {
|
|
21
|
+
return 'pdf';
|
|
22
|
+
}
|
|
23
|
+
// Only the head matters, and only as text. A binary file decoded this way
|
|
24
|
+
// produces junk that matches none of the patterns below, which is fine.
|
|
25
|
+
const head = buffer.subarray(0, 4096).toString('utf8').toLowerCase();
|
|
26
|
+
// Excel 2003 XML declares this namespace. Checked before the HTML test
|
|
27
|
+
// because such files can also contain table-ish markup.
|
|
28
|
+
if (head.includes('urn:schemas-microsoft-com:office:spreadsheet') ||
|
|
29
|
+
/<workbook[\s>]/.test(head)) {
|
|
30
|
+
return 'spreadsheetml';
|
|
31
|
+
}
|
|
32
|
+
if (/<table[\s>]/.test(head) ||
|
|
33
|
+
/<html[\s>]/.test(head) ||
|
|
34
|
+
head.includes('<!doctype html')) {
|
|
35
|
+
return 'html';
|
|
36
|
+
}
|
|
37
|
+
return 'text';
|
|
38
|
+
}
|
|
39
|
+
export function describeFormat(format) {
|
|
40
|
+
switch (format) {
|
|
41
|
+
case 'xlsx':
|
|
42
|
+
return 'Excel workbook (xlsx)';
|
|
43
|
+
case 'html':
|
|
44
|
+
return 'HTML table (a .xls file that is really HTML)';
|
|
45
|
+
case 'spreadsheetml':
|
|
46
|
+
return 'Excel 2003 XML (SpreadsheetML)';
|
|
47
|
+
case 'biff':
|
|
48
|
+
return 'legacy binary Excel (.xls, OLE2)';
|
|
49
|
+
case 'pdf':
|
|
50
|
+
return 'PDF';
|
|
51
|
+
case 'text':
|
|
52
|
+
return 'delimited text';
|
|
53
|
+
default:
|
|
54
|
+
return format;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=sniff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sniff.js","sourceRoot":"","sources":["../../src/extract/sniff.ts"],"names":[],"mappings":"AAiBA,+BAA+B;AAC/B,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAE3C,4DAA4D;AAC5D,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAEpE,eAAe;AACf,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAEjD,SAAS,eAAe,CAAC,MAAc,EAAE,KAAe;IACtD,IAAI,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACjC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,IAAI,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC;QACvC,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,eAAe,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;QACxC,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC;QACvC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,0EAA0E;IAC1E,wEAAwE;IACxE,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IAErE,uEAAuE;IACvE,wDAAwD;IACxD,IACE,IAAI,CAAC,QAAQ,CAAC,8CAA8C,CAAC;QAC7D,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAC3B,CAAC;QACD,OAAO,eAAe,CAAC;IACzB,CAAC;IAED,IACE,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;QACxB,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAC/B,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAsB;IACnD,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,MAAM;YACT,OAAO,uBAAuB,CAAC;QACjC,KAAK,MAAM;YACT,OAAO,8CAA8C,CAAC;QACxD,KAAK,eAAe;YAClB,OAAO,gCAAgC,CAAC;QAC1C,KAAK,MAAM;YACT,OAAO,kCAAkC,CAAC;QAC5C,KAAK,KAAK;YACR,OAAO,KAAK,CAAC;QACf,KAAK,MAAM;YACT,OAAO,gBAAgB,CAAC;QAC1B;YACE,OAAO,MAAM,CAAC;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
3
|
+
/**
|
|
4
|
+
* Excel 2003 XML (SpreadsheetML). Shape, once namespace prefixes are stripped:
|
|
5
|
+
*
|
|
6
|
+
* <Workbook>
|
|
7
|
+
* <Worksheet Name="Sheet1">
|
|
8
|
+
* <Table>
|
|
9
|
+
* <Row>
|
|
10
|
+
* <Cell><Data Type="String">Date</Data></Cell>
|
|
11
|
+
* <Cell Index="3"><Data Type="Number">450.5</Data></Cell>
|
|
12
|
+
* </Row>
|
|
13
|
+
*
|
|
14
|
+
* `Index` is the load-bearing detail: it is a 1-based absolute column, emitted
|
|
15
|
+
* when a run of cells is empty. Ignoring it silently shifts every following
|
|
16
|
+
* value left, which reads amounts out of the wrong column — the kind of bug
|
|
17
|
+
* that produces plausible numbers rather than an error.
|
|
18
|
+
*/
|
|
19
|
+
const parser = new XMLParser({
|
|
20
|
+
ignoreAttributes: false,
|
|
21
|
+
attributeNamePrefix: '@_',
|
|
22
|
+
textNodeName: '#text',
|
|
23
|
+
// Strips `ss:` from both tags and attributes, so `ss:Index` -> `@_Index`.
|
|
24
|
+
removeNSPrefix: true,
|
|
25
|
+
// Keep everything as text; the interpreter handles coercion.
|
|
26
|
+
parseTagValue: false,
|
|
27
|
+
parseAttributeValue: false,
|
|
28
|
+
trimValues: true,
|
|
29
|
+
});
|
|
30
|
+
function toArray(value) {
|
|
31
|
+
if (value === undefined || value === null) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
return (Array.isArray(value) ? value : [value]);
|
|
35
|
+
}
|
|
36
|
+
function dataText(cell) {
|
|
37
|
+
const data = cell['Data'];
|
|
38
|
+
if (data === undefined || data === null) {
|
|
39
|
+
// Some writers put the value directly on the Cell.
|
|
40
|
+
const direct = cell['#text'];
|
|
41
|
+
return direct === undefined || direct === null ? '' : String(direct);
|
|
42
|
+
}
|
|
43
|
+
if (typeof data === 'object') {
|
|
44
|
+
const text = data['#text'];
|
|
45
|
+
return text === undefined || text === null ? '' : String(text);
|
|
46
|
+
}
|
|
47
|
+
return String(data);
|
|
48
|
+
}
|
|
49
|
+
function rowsOf(table) {
|
|
50
|
+
const rows = [];
|
|
51
|
+
for (const row of toArray(table['Row'])) {
|
|
52
|
+
const cells = [];
|
|
53
|
+
for (const cell of toArray(row['Cell'])) {
|
|
54
|
+
const index = Number.parseInt(String(cell['@_Index'] ?? ''), 10);
|
|
55
|
+
if (Number.isFinite(index) && index >= 1) {
|
|
56
|
+
// Pad out the columns this cell skipped over.
|
|
57
|
+
while (cells.length < index - 1) {
|
|
58
|
+
cells.push('');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
cells.push(dataText(cell).trim());
|
|
62
|
+
}
|
|
63
|
+
rows.push(cells);
|
|
64
|
+
}
|
|
65
|
+
return rows;
|
|
66
|
+
}
|
|
67
|
+
export function tableFromSpreadsheetMl(xml, path = 'inline') {
|
|
68
|
+
const parsed = parser.parse(xml);
|
|
69
|
+
const workbook = (parsed['Workbook'] ?? parsed);
|
|
70
|
+
let best = null;
|
|
71
|
+
for (const worksheet of toArray(workbook['Worksheet'])) {
|
|
72
|
+
const name = String(worksheet['@_Name'] ?? 'Sheet');
|
|
73
|
+
for (const table of toArray(worksheet['Table'])) {
|
|
74
|
+
const rows = rowsOf(table);
|
|
75
|
+
if (!best || rows.length > best.rows.length) {
|
|
76
|
+
best = { rows, name };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
rows: best?.rows ?? [],
|
|
82
|
+
source: {
|
|
83
|
+
path,
|
|
84
|
+
format: 'xlsx',
|
|
85
|
+
part: best ? `${best.name} (SpreadsheetML)` : 'SpreadsheetML',
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export async function extractSpreadsheetMl(path) {
|
|
90
|
+
const contents = await readFile(path, 'utf8');
|
|
91
|
+
return tableFromSpreadsheetMl(contents, path);
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=spreadsheetml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spreadsheetml.js","sourceRoot":"","sources":["../../src/extract/spreadsheetml.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,gBAAgB,EAAE,KAAK;IACvB,mBAAmB,EAAE,IAAI;IACzB,YAAY,EAAE,OAAO;IACrB,0EAA0E;IAC1E,cAAc,EAAE,IAAI;IACpB,6DAA6D;IAC7D,aAAa,EAAE,KAAK;IACpB,mBAAmB,EAAE,KAAK;IAC1B,UAAU,EAAE,IAAI;CACjB,CAAC,CAAC;AAIH,SAAS,OAAO,CAAC,KAAc;IAC7B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1C,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAW,CAAC;AAC5D,CAAC;AAED,SAAS,QAAQ,CAAC,IAAU;IAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAE1B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QACxC,mDAAmD;QACnD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;QAC7B,OAAO,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvE,CAAC;IAED,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAI,IAAa,CAAC,OAAO,CAAC,CAAC;QACrC,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACjE,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED,SAAS,MAAM,CAAC,KAAW;IACzB,MAAM,IAAI,GAAe,EAAE,CAAC;IAE5B,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;QACxC,MAAM,KAAK,GAAa,EAAE,CAAC;QAE3B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;YACjE,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;gBACzC,8CAA8C;gBAC9C,OAAO,KAAK,CAAC,MAAM,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC;oBAChC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACjB,CAAC;YACH,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACpC,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,GAAW,EAAE,IAAI,GAAG,QAAQ;IACjE,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAS,CAAC;IACzC,MAAM,QAAQ,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,MAAM,CAAS,CAAC;IAExD,IAAI,IAAI,GAA8C,IAAI,CAAC;IAE3D,KAAK,MAAM,SAAS,IAAI,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,CAAC;QACpD,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YAChD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;YAC3B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC5C,IAAI,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YACxB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE;QACtB,MAAM,EAAE;YACN,IAAI;YACJ,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,kBAAkB,CAAC,CAAC,CAAC,eAAe;SAC9D;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,IAAY;IACrD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9C,OAAO,sBAAsB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type SourceFormat = 'csv' | 'xlsx' | 'pdf';
|
|
2
|
+
/**
|
|
3
|
+
* The single boundary between extraction and interpretation.
|
|
4
|
+
*
|
|
5
|
+
* Every extractor — CSV, XLS/XLSX, PDF — produces this and nothing else, and
|
|
6
|
+
* the interpreter never sees anything else. That is what keeps a bank's column
|
|
7
|
+
* logic written once even when the same bank ships two formats.
|
|
8
|
+
*
|
|
9
|
+
* Cells are always strings, exactly as they appeared. Type coercion (dates,
|
|
10
|
+
* amounts) belongs to interpretation, not extraction.
|
|
11
|
+
*/
|
|
12
|
+
export type Table = {
|
|
13
|
+
rows: string[][];
|
|
14
|
+
source: {
|
|
15
|
+
path: string;
|
|
16
|
+
format: SourceFormat;
|
|
17
|
+
/** Sheet name (spreadsheets) or page number (PDF), when meaningful. */
|
|
18
|
+
part?: string;
|
|
19
|
+
};
|
|
20
|
+
};
|