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.
Files changed (70) hide show
  1. package/.env.example +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +280 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +295 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/env-file.d.ts +38 -0
  8. package/dist/env-file.js +40 -0
  9. package/dist/env-file.js.map +1 -0
  10. package/dist/extract/csv.d.ts +13 -0
  11. package/dist/extract/csv.js +60 -0
  12. package/dist/extract/csv.js.map +1 -0
  13. package/dist/extract/html-table.d.ts +3 -0
  14. package/dist/extract/html-table.js +77 -0
  15. package/dist/extract/html-table.js.map +1 -0
  16. package/dist/extract/index.d.ts +21 -0
  17. package/dist/extract/index.js +44 -0
  18. package/dist/extract/index.js.map +1 -0
  19. package/dist/extract/pdf.d.ts +47 -0
  20. package/dist/extract/pdf.js +439 -0
  21. package/dist/extract/pdf.js.map +1 -0
  22. package/dist/extract/sniff.d.ts +12 -0
  23. package/dist/extract/sniff.js +57 -0
  24. package/dist/extract/sniff.js.map +1 -0
  25. package/dist/extract/spreadsheetml.d.ts +3 -0
  26. package/dist/extract/spreadsheetml.js +93 -0
  27. package/dist/extract/spreadsheetml.js.map +1 -0
  28. package/dist/extract/types.d.ts +20 -0
  29. package/dist/extract/types.js +2 -0
  30. package/dist/extract/types.js.map +1 -0
  31. package/dist/extract/xlsx.d.ts +2 -0
  32. package/dist/extract/xlsx.js +64 -0
  33. package/dist/extract/xlsx.js.map +1 -0
  34. package/dist/interpret/header.d.ts +24 -0
  35. package/dist/interpret/header.js +57 -0
  36. package/dist/interpret/header.js.map +1 -0
  37. package/dist/interpret/roundtrip.d.ts +29 -0
  38. package/dist/interpret/roundtrip.js +89 -0
  39. package/dist/interpret/roundtrip.js.map +1 -0
  40. package/dist/interpret/rows.d.ts +54 -0
  41. package/dist/interpret/rows.js +151 -0
  42. package/dist/interpret/rows.js.map +1 -0
  43. package/dist/interpret/synonyms.d.ts +27 -0
  44. package/dist/interpret/synonyms.js +55 -0
  45. package/dist/interpret/synonyms.js.map +1 -0
  46. package/dist/interpret/validate.d.ts +27 -0
  47. package/dist/interpret/validate.js +93 -0
  48. package/dist/interpret/validate.js.map +1 -0
  49. package/dist/interpret/values.d.ts +22 -0
  50. package/dist/interpret/values.js +131 -0
  51. package/dist/interpret/values.js.map +1 -0
  52. package/dist/merchants-file.d.ts +14 -0
  53. package/dist/merchants-file.js +40 -0
  54. package/dist/merchants-file.js.map +1 -0
  55. package/dist/narration/merchants.d.ts +46 -0
  56. package/dist/narration/merchants.js +161 -0
  57. package/dist/narration/merchants.js.map +1 -0
  58. package/dist/narration/parse.d.ts +14 -0
  59. package/dist/narration/parse.js +659 -0
  60. package/dist/narration/parse.js.map +1 -0
  61. package/dist/narration/types.d.ts +25 -0
  62. package/dist/narration/types.js +2 -0
  63. package/dist/narration/types.js.map +1 -0
  64. package/dist/out/csv.d.ts +21 -0
  65. package/dist/out/csv.js +40 -0
  66. package/dist/out/csv.js.map +1 -0
  67. package/dist/out/push.d.ts +74 -0
  68. package/dist/out/push.js +103 -0
  69. package/dist/out/push.js.map +1 -0
  70. package/package.json +65 -0
@@ -0,0 +1,40 @@
1
+ import { env, loadEnvFile } from 'node:process';
2
+ /**
3
+ * A missing file is not an error: the CSV path needs no configuration, so most
4
+ * runs legitimately have no `.env` at all. Anything else — unreadable, a
5
+ * directory, malformed contents — is reported, because it means the user
6
+ * believes they have configured something that is in fact being ignored. That
7
+ * would otherwise surface much later as a confusing "ACTUAL_SERVER_URL is not
8
+ * set".
9
+ */
10
+ export function loadEnvironmentFile(path) {
11
+ try {
12
+ loadEnvFile(path);
13
+ return { loaded: true, path };
14
+ }
15
+ catch (error) {
16
+ if (isMissingFile(error)) {
17
+ return { loaded: false, path, reason: 'missing' };
18
+ }
19
+ throw new Error(`Could not read ${path}: ${error instanceof Error ? error.message : String(error)}`);
20
+ }
21
+ }
22
+ /**
23
+ * Read a setting, treating blank as absent.
24
+ *
25
+ * `.env.example` ships its keys with empty values for the user to fill in, so
26
+ * a copied-but-unedited file leaves `ACTUAL_DATA_DIR=` set to the empty
27
+ * string. The empty string is not nullish, so a `??` default would not fire
28
+ * and the Actual API would be handed `''` as its data directory. Trimming also
29
+ * catches the trailing space left by `ACTUAL_SYNC_ID= ` .
30
+ */
31
+ export function setting(name) {
32
+ return env[name]?.trim() || undefined;
33
+ }
34
+ function isMissingFile(error) {
35
+ return (typeof error === 'object' &&
36
+ error !== null &&
37
+ 'code' in error &&
38
+ error.code === 'ENOENT');
39
+ }
40
+ //# sourceMappingURL=env-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-file.js","sourceRoot":"","sources":["../src/env-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAkBhD;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC;QACH,WAAW,CAAC,IAAI,CAAC,CAAC;QAClB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAChC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QACpD,CAAC;QAED,MAAM,IAAI,KAAK,CACb,kBAAkB,IAAI,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACpF,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;AACxC,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,MAAM,IAAI,KAAK;QACd,KAA4B,CAAC,IAAI,KAAK,QAAQ,CAChD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { Table } from './types.js';
2
+ /**
3
+ * Guess the delimiter by counting candidates across the first few lines.
4
+ *
5
+ * Counting per-line and taking the most *consistent* candidate rather than the
6
+ * most frequent one matters here: Indian statements carry preamble lines
7
+ * (branch address, account holder name) whose punctuation would otherwise
8
+ * outvote the real delimiter.
9
+ */
10
+ export declare function sniffDelimiter(contents: string): string;
11
+ export declare function extractCsv(path: string, options?: {
12
+ delimiter?: string;
13
+ }): Promise<Table>;
@@ -0,0 +1,60 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { parse } from 'csv-parse/sync';
3
+ const CANDIDATE_DELIMITERS = [',', '\t', ';', '|'];
4
+ /**
5
+ * Guess the delimiter by counting candidates across the first few lines.
6
+ *
7
+ * Counting per-line and taking the most *consistent* candidate rather than the
8
+ * most frequent one matters here: Indian statements carry preamble lines
9
+ * (branch address, account holder name) whose punctuation would otherwise
10
+ * outvote the real delimiter.
11
+ */
12
+ export function sniffDelimiter(contents) {
13
+ const lines = contents
14
+ .split(/\r?\n/)
15
+ .filter(line => line.trim())
16
+ .slice(0, 30);
17
+ let best = ',';
18
+ let bestScore = -1;
19
+ for (const delimiter of CANDIDATE_DELIMITERS) {
20
+ const counts = lines
21
+ .map(line => line.split(delimiter).length - 1)
22
+ .filter(count => count > 0);
23
+ if (counts.length < 2) {
24
+ continue;
25
+ }
26
+ // Reward the delimiter that appears the same number of times on the most
27
+ // lines — that is the signature of a real column structure.
28
+ const tally = new Map();
29
+ for (const count of counts) {
30
+ tally.set(count, (tally.get(count) ?? 0) + 1);
31
+ }
32
+ for (const [count, lineCount] of tally) {
33
+ const score = count * lineCount;
34
+ if (score > bestScore) {
35
+ bestScore = score;
36
+ best = delimiter;
37
+ }
38
+ }
39
+ }
40
+ return best;
41
+ }
42
+ export async function extractCsv(path, options = {}) {
43
+ const contents = await readFile(path, 'utf8');
44
+ const delimiter = options.delimiter ?? sniffDelimiter(contents);
45
+ const rows = parse(contents, {
46
+ delimiter,
47
+ bom: true,
48
+ // Statement preambles and footers have different column counts than the
49
+ // transaction table, so ragged rows are expected, not an error.
50
+ relax_column_count: true,
51
+ relax_quotes: true,
52
+ skip_empty_lines: true,
53
+ trim: true,
54
+ });
55
+ return {
56
+ rows: rows.map(row => row.map(cell => (cell ?? '').toString().trim())),
57
+ source: { path, format: 'csv' },
58
+ };
59
+ }
60
+ //# sourceMappingURL=csv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csv.js","sourceRoot":"","sources":["../../src/extract/csv.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAIvC,MAAM,oBAAoB,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,CAAU,CAAC;AAE5D;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,MAAM,KAAK,GAAG,QAAQ;SACnB,KAAK,CAAC,OAAO,CAAC;SACd,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC3B,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEhB,IAAI,IAAI,GAAG,GAAG,CAAC;IACf,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC;IAEnB,KAAK,MAAM,SAAS,IAAI,oBAAoB,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,KAAK;aACjB,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;aAC7C,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAE9B,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,SAAS;QACX,CAAC;QAED,yEAAyE;QACzE,4DAA4D;QAC5D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;QACxC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAChD,CAAC;QAED,KAAK,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,KAAK,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,KAAK,GAAG,SAAS,CAAC;YAChC,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;gBACtB,SAAS,GAAG,KAAK,CAAC;gBAClB,IAAI,GAAG,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,UAAkC,EAAE;IAEpC,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,cAAc,CAAC,QAAQ,CAAC,CAAC;IAEhE,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,EAAE;QAC3B,SAAS;QACT,GAAG,EAAE,IAAI;QACT,wEAAwE;QACxE,gEAAgE;QAChE,kBAAkB,EAAE,IAAI;QACxB,YAAY,EAAE,IAAI;QAClB,gBAAgB,EAAE,IAAI;QACtB,IAAI,EAAE,IAAI;KACX,CAAe,CAAC;IAEjB,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;QACtE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;KAChC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Table } from './types.js';
2
+ export declare function tableFromHtml(html: string, path?: string): Table;
3
+ export declare function extractHtmlTable(path: string): Promise<Table>;
@@ -0,0 +1,77 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { parse } from 'node-html-parser';
3
+ const ENTITIES = {
4
+ '&nbsp;': ' ',
5
+ '&amp;': '&',
6
+ '&lt;': '<',
7
+ '&gt;': '>',
8
+ '&quot;': '"',
9
+ '&apos;': "'",
10
+ '&#39;': "'",
11
+ };
12
+ /**
13
+ * Bank exports lean on `&nbsp;` for empty cells and `&amp;` in merchant names,
14
+ * and an undecoded `&nbsp;` is not whitespace — it would survive a `.trim()`
15
+ * and make an empty cell look populated.
16
+ */
17
+ function decodeEntities(value) {
18
+ return value
19
+ .replace(/&nbsp;|&amp;|&lt;|&gt;|&quot;|&apos;|&#39;/g, match => ENTITIES[match] ?? match)
20
+ .replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
21
+ .replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16)));
22
+ }
23
+ function cellsOf(row) {
24
+ const cells = [];
25
+ for (const node of row.childNodes) {
26
+ const element = node;
27
+ const tag = element.tagName?.toUpperCase();
28
+ if (tag !== 'TD' && tag !== 'TH') {
29
+ continue;
30
+ }
31
+ const text = decodeEntities(element.textContent ?? '')
32
+ .replace(/\s+/g, ' ')
33
+ .trim();
34
+ cells.push(text);
35
+ // Expand colspan so later columns keep their index. Without this a
36
+ // merged header cell shifts every column after it by one, and amounts get
37
+ // read out of the wrong column.
38
+ const span = Number.parseInt(element.getAttribute('colspan') ?? '1', 10);
39
+ for (let extra = 1; extra < (Number.isFinite(span) ? span : 1); extra += 1) {
40
+ cells.push('');
41
+ }
42
+ }
43
+ return cells;
44
+ }
45
+ function rowsOf(table) {
46
+ return table.querySelectorAll('tr').map(cellsOf);
47
+ }
48
+ /** Rows with enough populated cells to plausibly be transaction data. */
49
+ function density(rows) {
50
+ return rows.filter(row => row.filter(cell => cell.trim()).length >= 3).length;
51
+ }
52
+ export function tableFromHtml(html, path = 'inline') {
53
+ const root = parse(html);
54
+ // Bank exports nest the data table inside layout tables. No special handling
55
+ // is needed for that: `querySelectorAll('tr')` walks descendants, so an
56
+ // outer table's row list already contains the inner table's rows, each
57
+ // parsed from its own cells. Picking the table with the most dense rows
58
+ // therefore lands on a row set containing the real data, and the surrounding
59
+ // layout rows are dropped later by requiring a parseable date and amount.
60
+ const candidates = root.querySelectorAll('table');
61
+ let best = [];
62
+ let bestDensity = -1;
63
+ for (const candidate of candidates) {
64
+ const rows = rowsOf(candidate);
65
+ const score = density(rows);
66
+ if (score > bestDensity) {
67
+ bestDensity = score;
68
+ best = rows;
69
+ }
70
+ }
71
+ return { rows: best, source: { path, format: 'xlsx', part: 'html-table' } };
72
+ }
73
+ export async function extractHtmlTable(path) {
74
+ const contents = await readFile(path, 'utf8');
75
+ return tableFromHtml(contents, path);
76
+ }
77
+ //# sourceMappingURL=html-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-table.js","sourceRoot":"","sources":["../../src/extract/html-table.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { describeFormat, detectFormat } from './sniff.js';
2
+ import type { DetectedFormat } from './sniff.js';
3
+ import type { Table } from './types.js';
4
+ export type ExtractOptions = {
5
+ /** Only used for delimited text. */
6
+ delimiter?: string;
7
+ /** Only used for encrypted PDFs. */
8
+ password?: string;
9
+ };
10
+ export type Extraction = {
11
+ table: Table;
12
+ /** What the file turned out to be, which is often not what it is named. */
13
+ format: DetectedFormat;
14
+ };
15
+ /**
16
+ * Read a statement file into a table, choosing the parser by inspecting the
17
+ * contents rather than the extension.
18
+ */
19
+ export declare function extractTable(path: string, options?: ExtractOptions): Promise<Extraction>;
20
+ export { describeFormat, detectFormat };
21
+ export type { DetectedFormat, Table };
@@ -0,0 +1,44 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extractCsv } from './csv.js';
3
+ import { extractHtmlTable } from './html-table.js';
4
+ import { extractPdf } from './pdf.js';
5
+ import { describeFormat, detectFormat } from './sniff.js';
6
+ import { extractSpreadsheetMl } from './spreadsheetml.js';
7
+ import { extractXlsx } from './xlsx.js';
8
+ /**
9
+ * Read a statement file into a table, choosing the parser by inspecting the
10
+ * contents rather than the extension.
11
+ */
12
+ export async function extractTable(path, options = {}) {
13
+ const buffer = await readFile(path);
14
+ const format = detectFormat(buffer);
15
+ switch (format) {
16
+ case 'xlsx':
17
+ return { table: await extractXlsx(path), format };
18
+ case 'html':
19
+ return { table: await extractHtmlTable(path), format };
20
+ case 'spreadsheetml':
21
+ return { table: await extractSpreadsheetMl(path), format };
22
+ case 'pdf':
23
+ return {
24
+ table: await extractPdf(path, options.password ? { password: options.password } : {}),
25
+ format,
26
+ };
27
+ case 'biff':
28
+ // Legacy BIFF has no maintained, permissively licensed reader for Node,
29
+ // and re-saving is a one-step fix, so this stays an explicit refusal
30
+ // rather than a half-working parser.
31
+ throw new Error(`${path} is a ${describeFormat(format)}, which is not supported.\n` +
32
+ 'Open it in Excel or LibreOffice and "Save As" .xlsx or .csv, then ' +
33
+ 'run this again. If your bank offers CSV through internet banking, ' +
34
+ 'that is the most reliable option.');
35
+ case 'text':
36
+ default:
37
+ return {
38
+ table: await extractCsv(path, options.delimiter ? { delimiter: options.delimiter } : {}),
39
+ format: 'text',
40
+ };
41
+ }
42
+ }
43
+ export { describeFormat, detectFormat };
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/extract/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAexC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAY,EACZ,UAA0B,EAAE;IAE5B,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAEpC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,MAAM;YACT,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QACpD,KAAK,MAAM;YACT,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QACzD,KAAK,eAAe;YAClB,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAC7D,KAAK,KAAK;YACR,OAAO;gBACL,KAAK,EAAE,MAAM,UAAU,CACrB,IAAI,EACJ,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CACvD;gBACD,MAAM;aACP,CAAC;QACJ,KAAK,MAAM;YACT,wEAAwE;YACxE,qEAAqE;YACrE,qCAAqC;YACrC,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,SAAS,cAAc,CAAC,MAAM,CAAC,6BAA6B;gBACjE,oEAAoE;gBACpE,oEAAoE;gBACpE,mCAAmC,CACtC,CAAC;QACJ,KAAK,MAAM,CAAC;QACZ;YACE,OAAO;gBACL,KAAK,EAAE,MAAM,UAAU,CACrB,IAAI,EACJ,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAC1D;gBACD,MAAM,EAAE,MAAM;aACf,CAAC;IACN,CAAC;AACH,CAAC;AAED,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,47 @@
1
+ import type { Table } from './types.js';
2
+ export type PdfExtractOptions = {
3
+ password?: string;
4
+ };
5
+ export type Item = {
6
+ x: number;
7
+ right: number;
8
+ y: number;
9
+ text: string;
10
+ };
11
+ /**
12
+ * `page` matters for more than reporting: y coordinates restart on every page,
13
+ * so two lines' y values are only comparable within the same page.
14
+ */
15
+ export type Line = {
16
+ y: number;
17
+ page: number;
18
+ items: Item[];
19
+ };
20
+ /** A column, as the horizontal span its text occupies. */
21
+ export type Band = {
22
+ left: number;
23
+ right: number;
24
+ };
25
+ /**
26
+ * Infer columns from the vertical whitespace that separates them.
27
+ *
28
+ * Every column of a statement table is divided from its neighbour by a gutter
29
+ * that stays empty on every row, so accumulating the horizontal extent of all
30
+ * table text and splitting on runs of emptiness recovers the columns directly.
31
+ *
32
+ * This is markedly more robust than comparing where text starts or merging
33
+ * overlapping spans, both of which were tried first and failed on real files:
34
+ *
35
+ * - Start positions alone split a right-aligned amount from its own header,
36
+ * which can begin 36 points to its left.
37
+ * - Merging overlapping spans bridges columns whenever a header label is wider
38
+ * than the column spacing. Federal packs columns 45 points apart with labels
39
+ * nearly that wide, collapsing `Withdrawals` and `Deposits` into one cell.
40
+ *
41
+ * Only header and dated rows take part. Wrapped narration lines and page
42
+ * furniture would otherwise bridge gutters — ICICI's footer URL spans the gap
43
+ * between the serial-number and date columns.
44
+ */
45
+ export declare function inferBands(lines: Line[]): Band[];
46
+ export declare function assembleRows(lines: Line[], bands: Band[]): string[][];
47
+ export declare function extractPdf(path: string, options?: PdfExtractOptions): Promise<Table>;