tn-elections-mcp 0.5.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 (77) hide show
  1. package/README.md +87 -0
  2. package/dist/api/easyvote-client.d.ts +51 -0
  3. package/dist/api/easyvote-client.js +254 -0
  4. package/dist/api/easyvote-client.js.map +1 -0
  5. package/dist/api/easyvote-client.test.d.ts +1 -0
  6. package/dist/api/easyvote-client.test.js +47 -0
  7. package/dist/api/easyvote-client.test.js.map +1 -0
  8. package/dist/api/gemini-pdf.d.ts +23 -0
  9. package/dist/api/gemini-pdf.js +261 -0
  10. package/dist/api/gemini-pdf.js.map +1 -0
  11. package/dist/api/sos-client.d.ts +15 -0
  12. package/dist/api/sos-client.js +74 -0
  13. package/dist/api/sos-client.js.map +1 -0
  14. package/dist/api/sumner-client.d.ts +12 -0
  15. package/dist/api/sumner-client.js +57 -0
  16. package/dist/api/sumner-client.js.map +1 -0
  17. package/dist/api/tncamp-client.d.ts +61 -0
  18. package/dist/api/tncamp-client.js +310 -0
  19. package/dist/api/tncamp-client.js.map +1 -0
  20. package/dist/data/sumner-cycles.d.ts +3 -0
  21. package/dist/data/sumner-cycles.js +19 -0
  22. package/dist/data/sumner-cycles.js.map +1 -0
  23. package/dist/data/sumner-cycles.test.d.ts +1 -0
  24. package/dist/data/sumner-cycles.test.js +29 -0
  25. package/dist/data/sumner-cycles.test.js.map +1 -0
  26. package/dist/data/tn-counties.d.ts +23 -0
  27. package/dist/data/tn-counties.js +162 -0
  28. package/dist/data/tn-counties.js.map +1 -0
  29. package/dist/data/tn-counties.test.d.ts +1 -0
  30. package/dist/data/tn-counties.test.js +71 -0
  31. package/dist/data/tn-counties.test.js.map +1 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +31 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/parsers/sos-xlsx.d.ts +24 -0
  36. package/dist/parsers/sos-xlsx.js +149 -0
  37. package/dist/parsers/sos-xlsx.js.map +1 -0
  38. package/dist/parsers/sos-xlsx.test.d.ts +1 -0
  39. package/dist/parsers/sos-xlsx.test.js +111 -0
  40. package/dist/parsers/sos-xlsx.test.js.map +1 -0
  41. package/dist/parsers/sumner-html.d.ts +8 -0
  42. package/dist/parsers/sumner-html.js +200 -0
  43. package/dist/parsers/sumner-html.js.map +1 -0
  44. package/dist/parsers/sumner-html.test.d.ts +1 -0
  45. package/dist/parsers/sumner-html.test.js +79 -0
  46. package/dist/parsers/sumner-html.test.js.map +1 -0
  47. package/dist/parsers/tncamp-form.d.ts +15 -0
  48. package/dist/parsers/tncamp-form.js +54 -0
  49. package/dist/parsers/tncamp-form.js.map +1 -0
  50. package/dist/parsers/tncamp-form.test.d.ts +1 -0
  51. package/dist/parsers/tncamp-form.test.js +114 -0
  52. package/dist/parsers/tncamp-form.test.js.map +1 -0
  53. package/dist/parsers/tncamp-report.d.ts +6 -0
  54. package/dist/parsers/tncamp-report.js +189 -0
  55. package/dist/parsers/tncamp-report.js.map +1 -0
  56. package/dist/parsers/tncamp-results.d.ts +10 -0
  57. package/dist/parsers/tncamp-results.js +113 -0
  58. package/dist/parsers/tncamp-results.js.map +1 -0
  59. package/dist/parsers/tncamp-results.test.d.ts +1 -0
  60. package/dist/parsers/tncamp-results.test.js +86 -0
  61. package/dist/parsers/tncamp-results.test.js.map +1 -0
  62. package/dist/tools/easyvote.d.ts +2 -0
  63. package/dist/tools/easyvote.js +382 -0
  64. package/dist/tools/easyvote.js.map +1 -0
  65. package/dist/tools/sos.d.ts +2 -0
  66. package/dist/tools/sos.js +130 -0
  67. package/dist/tools/sos.js.map +1 -0
  68. package/dist/tools/sumner.d.ts +2 -0
  69. package/dist/tools/sumner.js +134 -0
  70. package/dist/tools/sumner.js.map +1 -0
  71. package/dist/tools/tncamp.d.ts +2 -0
  72. package/dist/tools/tncamp.js +401 -0
  73. package/dist/tools/tncamp.js.map +1 -0
  74. package/dist/types.d.ts +270 -0
  75. package/dist/types.js +5 -0
  76. package/dist/types.js.map +1 -0
  77. package/package.json +47 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sumner-html.js","sourceRoot":"","sources":["../../src/parsers/sumner-html.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseSumnerCyclePage } from './sumner-html.js';
3
+ const SAMPLE_HTML = `
4
+ <div class="fbox-content fr-view">
5
+ <p><span style="background-color: rgb(71, 85, 119); color: rgb(255, 255, 255);">COUNTY MAYOR</span></p>
6
+ <p><span style="color: rgb(71, 85, 119);"><strong>John Isbell</strong></span><br>
7
+ <a class="fr-file" href="/uploads/files/abc123.pdf" target="_blank">Mid-Year 2023 Report</a>
8
+ <a class="fr-file" href="/uploads/files/def456.pdf" target="_blank">YE 2023 Report</a></p>
9
+
10
+ <p><span style="background-color: rgb(71, 85, 119); color: rgb(255, 255, 255);">COUNTY COMMISSIONERS</span></p>
11
+ <p><span style="color: rgb(71, 85, 119);"><strong>District 2</strong></span><br>
12
+ <span style="color: rgb(71, 85, 119);"><strong>Christopher Shoemaker</strong></span><br>
13
+ <a class="fr-file" href="/uploads/files/ghi789.pdf" target="_blank">YE 2025 Report</a></p>
14
+
15
+ <p><span style="color: rgb(71, 85, 119);"><strong>District 8</strong></span><br>
16
+ <span style="color: rgb(71, 85, 119);"><strong>Rory Edmonds</strong></span><br>
17
+ <a class="fr-file" href="/uploads/files/jkl012.pdf" target="_blank">YE 2025 Report</a></p>
18
+ <p><span style="color: rgb(71, 85, 119);"><strong>Baker Ring</strong></span><br>
19
+ <a class="fr-file" href="/uploads/files/mno345.pdf" target="_blank">YE 2025 Report</a></p>
20
+
21
+ <p><span style="color: rgb(41, 105, 176);"><strong>CITY OF GALLATIN</strong></span><br>
22
+ <span style="color: rgb(71, 85, 119);"><strong>Mayor</strong></span><br>
23
+ <span style="color: rgb(71, 85, 119);"><strong>Mary Genung</strong></span><br>
24
+ <a class="fr-file" href="/uploads/files/pqr678.pdf" target="_blank">YE 2025 Report</a></p>
25
+ </div>
26
+ `;
27
+ describe('parseSumnerCyclePage', () => {
28
+ const cycle = { year: '2026', url: 'https://example.com', label: '2026 Cycle' };
29
+ it('extracts county officers', () => {
30
+ const result = parseSumnerCyclePage(SAMPLE_HTML, cycle);
31
+ const isbell = result.candidates.find(c => c.name === 'John Isbell');
32
+ expect(isbell).toBeDefined();
33
+ expect(isbell.office.name).toBe('COUNTY MAYOR');
34
+ expect(isbell.reports).toHaveLength(2);
35
+ expect(isbell.reports[0].reportName).toBe('Mid-Year 2023 Report');
36
+ expect(isbell.reports[0].pdfUrl).toContain('abc123.pdf');
37
+ });
38
+ it('extracts commissioners with districts', () => {
39
+ const result = parseSumnerCyclePage(SAMPLE_HTML, cycle);
40
+ const shoemaker = result.candidates.find(c => c.name === 'Christopher Shoemaker');
41
+ expect(shoemaker).toBeDefined();
42
+ expect(shoemaker.office.name).toBe('COUNTY COMMISSIONERS');
43
+ expect(shoemaker.office.district).toBe('District 2');
44
+ });
45
+ it('handles multiple candidates per district', () => {
46
+ const result = parseSumnerCyclePage(SAMPLE_HTML, cycle);
47
+ const d8 = result.candidates.filter(c => c.office.district === 'District 8' && c.office.name === 'COUNTY COMMISSIONERS');
48
+ expect(d8).toHaveLength(2);
49
+ expect(d8.map(c => c.name).sort()).toEqual(['Baker Ring', 'Rory Edmonds']);
50
+ });
51
+ it('handles municipal sections', () => {
52
+ const result = parseSumnerCyclePage(SAMPLE_HTML, cycle);
53
+ const genung = result.candidates.find(c => c.name === 'Mary Genung');
54
+ expect(genung).toBeDefined();
55
+ expect(genung.office.isMunicipal).toBe(true);
56
+ expect(genung.office.municipality).toBe('CITY OF GALLATIN');
57
+ });
58
+ it('filters out office titles as candidates', () => {
59
+ const result = parseSumnerCyclePage(SAMPLE_HTML, cycle);
60
+ const mayor = result.candidates.find(c => c.name === 'Mayor');
61
+ expect(mayor).toBeUndefined();
62
+ });
63
+ it('filters out district identifiers as candidates', () => {
64
+ const result = parseSumnerCyclePage(SAMPLE_HTML, cycle);
65
+ const d2 = result.candidates.find(c => c.name === 'District 2');
66
+ expect(d2).toBeUndefined();
67
+ });
68
+ it('returns empty for HTML with no matching structure', () => {
69
+ const result = parseSumnerCyclePage('<html><body>Nothing here</body></html>', cycle);
70
+ expect(result.candidates).toHaveLength(0);
71
+ });
72
+ it('attaches PDF links to correct candidates', () => {
73
+ const result = parseSumnerCyclePage(SAMPLE_HTML, cycle);
74
+ const ring = result.candidates.find(c => c.name === 'Baker Ring');
75
+ expect(ring.reports).toHaveLength(1);
76
+ expect(ring.reports[0].pdfUrl).toContain('mno345.pdf');
77
+ });
78
+ });
79
+ //# sourceMappingURL=sumner-html.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sumner-html.test.js","sourceRoot":"","sources":["../../src/parsers/sumner-html.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Parser for tncamp cpsearch.htm form dropdowns and districts.htm JSON.
3
+ * Pure functions — no network calls.
4
+ */
5
+ import type { TncampFormOptions, TncampDistrict } from '../types.js';
6
+ /**
7
+ * Parse the cpsearch.htm form page to extract office, election year, and party dropdowns.
8
+ * Options are in <select> elements with <option value="X">Label</option>.
9
+ */
10
+ export declare function parseFormOptions(html: string): TncampFormOptions;
11
+ /**
12
+ * Parse the districts.htm JSON response.
13
+ * Response format: {"districts":[{"id":369,"name":"18-2","office_id":10,"nameWithoutSeat":"18"}, ...]}
14
+ */
15
+ export declare function parseDistrictsJson(json: string, officeId: string): TncampDistrict[];
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Parser for tncamp cpsearch.htm form dropdowns and districts.htm JSON.
3
+ * Pure functions — no network calls.
4
+ */
5
+ /**
6
+ * Parse the cpsearch.htm form page to extract office, election year, and party dropdowns.
7
+ * Options are in <select> elements with <option value="X">Label</option>.
8
+ */
9
+ export function parseFormOptions(html) {
10
+ const offices = [];
11
+ const electionYears = [];
12
+ const parties = [];
13
+ // Extract all <select> blocks with name attribute
14
+ const selectRegex = /<select[^>]*name="(\w+)"[^>]*>([\s\S]*?)<\/select>/g;
15
+ let selectMatch;
16
+ while ((selectMatch = selectRegex.exec(html)) !== null) {
17
+ const name = selectMatch[1];
18
+ const content = selectMatch[2];
19
+ // Extract <option> tags with non-empty values
20
+ const optRegex = /<option\s+value="([^"]+)"[^>]*>([\s\S]*?)<\/option>/g;
21
+ let optMatch;
22
+ while ((optMatch = optRegex.exec(content)) !== null) {
23
+ const id = optMatch[1].trim();
24
+ const label = optMatch[2].replace(/\s+/g, ' ').trim();
25
+ if (!id)
26
+ continue;
27
+ switch (name) {
28
+ case 'officeSelection':
29
+ offices.push({ id, name: label });
30
+ break;
31
+ case 'electionYearSelection':
32
+ electionYears.push({ id, label });
33
+ break;
34
+ case 'partySelection':
35
+ parties.push({ id, name: label });
36
+ break;
37
+ }
38
+ }
39
+ }
40
+ return { offices, electionYears, parties };
41
+ }
42
+ /**
43
+ * Parse the districts.htm JSON response.
44
+ * Response format: {"districts":[{"id":369,"name":"18-2","office_id":10,"nameWithoutSeat":"18"}, ...]}
45
+ */
46
+ export function parseDistrictsJson(json, officeId) {
47
+ const data = JSON.parse(json);
48
+ return data.districts.map(d => ({
49
+ id: String(d.id),
50
+ label: d.name,
51
+ officeId,
52
+ }));
53
+ }
54
+ //# sourceMappingURL=tncamp-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tncamp-form.js","sourceRoot":"","sources":["../../src/parsers/tncamp-form.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,OAAO,GAAmB,EAAE,CAAA;IAClC,MAAM,aAAa,GAAyB,EAAE,CAAA;IAC9C,MAAM,OAAO,GAAmC,EAAE,CAAA;IAElD,kDAAkD;IAClD,MAAM,WAAW,GAAG,qDAAqD,CAAA;IACzE,IAAI,WAAW,CAAA;IACf,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACvD,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;QAC3B,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;QAE9B,8CAA8C;QAC9C,MAAM,QAAQ,GAAG,sDAAsD,CAAA;QACvE,IAAI,QAAQ,CAAA;QACZ,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACpD,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;YAC7B,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;YACrD,IAAI,CAAC,EAAE;gBAAE,SAAQ;YAEjB,QAAQ,IAAI,EAAE,CAAC;gBACb,KAAK,iBAAiB;oBACpB,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;oBACjC,MAAK;gBACP,KAAK,uBAAuB;oBAC1B,aAAa,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;oBACjC,MAAK;gBACP,KAAK,gBAAgB;oBACnB,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;oBACjC,MAAK;YACT,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,CAAA;AAC5C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,QAAgB;IAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAE3B,CAAA;IAED,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC9B,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAChB,KAAK,EAAE,CAAC,CAAC,IAAI;QACb,QAAQ;KACT,CAAC,CAAC,CAAA;AACL,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseFormOptions, parseDistrictsJson } from './tncamp-form.js';
3
+ const FORM_HTML = `
4
+ <select id="offices" name="officeSelection">
5
+ <option value="" selected>- Select Office -</option>
6
+ <option value="2">Governor</option>
7
+ <option value="3">Senate</option>
8
+ <option value="10">Circuit Court</option>
9
+ <option value="13">Probate Court</option>
10
+ </select>
11
+ <select id="years" name="electionYearSelection">
12
+ <option value="" selected>- Select Election Year-</option>
13
+ <option value="234">2026</option>
14
+ <option value="230">2024</option>
15
+ <option value="237">2023 (HOUSE 3)</option>
16
+ </select>
17
+ <select id="districts" name="districtSelection">
18
+ <option value="" selected>- Select District -</option>
19
+ <option value="" selected>Please select an office first.</option>
20
+ </select>
21
+ <select name="partySelection">
22
+ <option value="" selected>- Select Party -</option>
23
+ <option value="1">Democrat</option>
24
+ <option value="2">Republican</option>
25
+ <option value="3">Independent</option>
26
+ <option value="4">Green</option>
27
+ </select>
28
+ `;
29
+ // Whitespace-heavy HTML matching the actual tncamp page
30
+ const FORM_HTML_WHITESPACE = `
31
+ <select id="offices" name="officeSelection">
32
+ <option value="" selected >- Select Office -</option>
33
+ <option value="2" >
34
+ Governor
35
+ </option>
36
+ <option value="10" >
37
+ Circuit Court
38
+ </option>
39
+ </select>
40
+ <select name="electionYearSelection">
41
+ <option value="" selected >- Select Election Year- </option>
42
+ <option value="234" >
43
+ 2026
44
+
45
+ </option>
46
+ <option value="237" >
47
+ 2023
48
+
49
+ (HOUSE 3)
50
+
51
+ </option>
52
+ </select>
53
+ <select name="partySelection">
54
+ <option value="" selected >- Select Party -</option>
55
+ <option value="2" >Republican
56
+ </option>
57
+ </select>
58
+ `;
59
+ const DISTRICTS_JSON = `{"districts":[{"id":204,"name":"1-1","office_id":10,"nameWithoutSeat":"01"},{"id":205,"name":"1-2","office_id":10,"nameWithoutSeat":"01"},{"id":369,"name":"18-2","office_id":10,"nameWithoutSeat":"18"}]}`;
60
+ const DISTRICTS_EMPTY = `{"districts":[]}`;
61
+ describe('parseFormOptions', () => {
62
+ it('extracts offices', () => {
63
+ const result = parseFormOptions(FORM_HTML);
64
+ expect(result.offices).toHaveLength(4);
65
+ expect(result.offices[0]).toEqual({ id: '2', name: 'Governor' });
66
+ expect(result.offices[2]).toEqual({ id: '10', name: 'Circuit Court' });
67
+ });
68
+ it('extracts election years', () => {
69
+ const result = parseFormOptions(FORM_HTML);
70
+ expect(result.electionYears).toHaveLength(3);
71
+ expect(result.electionYears[0]).toEqual({ id: '234', label: '2026' });
72
+ expect(result.electionYears[2]).toEqual({ id: '237', label: '2023 (HOUSE 3)' });
73
+ });
74
+ it('extracts parties', () => {
75
+ const result = parseFormOptions(FORM_HTML);
76
+ expect(result.parties).toHaveLength(4);
77
+ expect(result.parties[0]).toEqual({ id: '1', name: 'Democrat' });
78
+ expect(result.parties[1]).toEqual({ id: '2', name: 'Republican' });
79
+ });
80
+ it('skips empty-value options (placeholders)', () => {
81
+ const result = parseFormOptions(FORM_HTML);
82
+ // Districts should have 0 options (both have empty values)
83
+ // Offices should not include "- Select Office -"
84
+ expect(result.offices.every(o => o.id !== '')).toBe(true);
85
+ });
86
+ it('handles whitespace-heavy HTML from real tncamp page', () => {
87
+ const result = parseFormOptions(FORM_HTML_WHITESPACE);
88
+ expect(result.offices).toHaveLength(2);
89
+ expect(result.offices[0]).toEqual({ id: '2', name: 'Governor' });
90
+ expect(result.offices[1]).toEqual({ id: '10', name: 'Circuit Court' });
91
+ expect(result.electionYears[0]).toEqual({ id: '234', label: '2026' });
92
+ expect(result.electionYears[1]).toEqual({ id: '237', label: '2023 (HOUSE 3)' });
93
+ expect(result.parties[0]).toEqual({ id: '2', name: 'Republican' });
94
+ });
95
+ it('returns empty arrays for missing selects', () => {
96
+ const result = parseFormOptions('<html></html>');
97
+ expect(result.offices).toHaveLength(0);
98
+ expect(result.electionYears).toHaveLength(0);
99
+ expect(result.parties).toHaveLength(0);
100
+ });
101
+ });
102
+ describe('parseDistrictsJson', () => {
103
+ it('parses district JSON with officeId', () => {
104
+ const result = parseDistrictsJson(DISTRICTS_JSON, '10');
105
+ expect(result).toHaveLength(3);
106
+ expect(result[0]).toEqual({ id: '204', label: '1-1', officeId: '10' });
107
+ expect(result[2]).toEqual({ id: '369', label: '18-2', officeId: '10' });
108
+ });
109
+ it('returns empty array for empty districts', () => {
110
+ const result = parseDistrictsJson(DISTRICTS_EMPTY, '2');
111
+ expect(result).toHaveLength(0);
112
+ });
113
+ });
114
+ //# sourceMappingURL=tncamp-form.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tncamp-form.test.js","sourceRoot":"","sources":["../../src/parsers/tncamp-form.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * tncamp report_full.htm parser.
3
+ * Extracts financial aggregates and itemized line items from h-tag sections.
4
+ */
5
+ import type { TncampReportDetail } from '../types.js';
6
+ export declare function parseReportDetail(html: string, reportId: number): TncampReportDetail;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * tncamp report_full.htm parser.
3
+ * Extracts financial aggregates and itemized line items from h-tag sections.
4
+ */
5
+ function parseDollar(text) {
6
+ const match = text.match(/\$([\d,]+\.?\d*)/);
7
+ if (!match)
8
+ return 0;
9
+ return parseFloat(match[1].replace(/,/g, '')) || 0;
10
+ }
11
+ /**
12
+ * Extract a dollar amount following a label in the full text.
13
+ * Looks for patterns like "TOTAL CONTRIBUTIONS ... $38,910.00"
14
+ */
15
+ function extractAggregate(fullText, label) {
16
+ // Try exact label followed by dollar amount (possibly with text between)
17
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ const regex = new RegExp(escaped + '[^$]*\\$([\\.\\d,]+)', 'i');
19
+ const match = fullText.match(regex);
20
+ if (match)
21
+ return parseFloat(match[1].replace(/,/g, '')) || 0;
22
+ return 0;
23
+ }
24
+ /**
25
+ * Split HTML content into sections based on h3 tags.
26
+ * Returns array of { header, bodyText } pairs.
27
+ */
28
+ function splitSections(html) {
29
+ const sections = [];
30
+ // Split on h3 tags (which tncamp uses for section headers)
31
+ const parts = html.split(/<h3[^>]*>/i);
32
+ for (let i = 1; i < parts.length; i++) {
33
+ const closeTag = parts[i].indexOf('</h3>');
34
+ if (closeTag === -1)
35
+ continue;
36
+ const header = parts[i].substring(0, closeTag).replace(/<[^>]+>/g, '').trim();
37
+ const body = parts[i].substring(closeTag + 5);
38
+ // Get text until next h-tag or end
39
+ const nextH = body.match(/<h[2-5][^>]*>/i);
40
+ const bodyHtml = nextH ? body.substring(0, nextH.index) : body;
41
+ const bodyText = bodyHtml.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
42
+ sections.push({ header, bodyText });
43
+ }
44
+ return sections;
45
+ }
46
+ /**
47
+ * Parse itemized line items from section body text.
48
+ * Each entry follows a pattern: Name, Address, optional fields, Date, Amount
49
+ */
50
+ function parseLineItems(bodyText) {
51
+ const items = [];
52
+ if (!bodyText || !bodyText.includes('$'))
53
+ return items;
54
+ // Remove the summary lines (Unitemized, TOTAL, etc.)
55
+ const cleanText = bodyText
56
+ .replace(/Monetary Contributions,\s*Unitemized\s*\$[\d,.]+/gi, '')
57
+ .replace(/Monetary Contributions,\s*Itemized/gi, '')
58
+ .replace(/Expenditures,\s*Unitemized\s*\$[\d,.]+/gi, '')
59
+ .replace(/Expenditures,\s*Itemized/gi, '')
60
+ .replace(/TOTAL[^$]*\$[\d,.]+/gi, '')
61
+ .replace(/Contribution Adjustments[^$]*\$[\d,.]+/gi, '')
62
+ .replace(/Loans? Received[^$]*\$[\d,.]+/gi, '')
63
+ .replace(/Interest Received[^$]*\$[\d,.]+/gi, '')
64
+ .replace(/Loan Payments[^$]*\$[\d,.]+/gi, '')
65
+ .replace(/Obligation Payments[^$]*\$[\d,.]+/gi, '')
66
+ .replace(/Expenditures?, Adjustments[^$]*\$[\d,.]+/gi, '')
67
+ .replace(/Unitemized\s*\$[\d,.]+/gi, '')
68
+ .replace(/Itemized\s*\$[\d,.]+/gi, '')
69
+ .replace(/Obligations?, Outstanding from Previous[^$]*\$[\d,.]+/gi, '')
70
+ .trim();
71
+ if (!cleanText)
72
+ return items;
73
+ // Split on patterns that look like: date followed by dollar amount
74
+ // Each itemized entry ends with a date (MM/DD/YYYY) and amount ($X,XXX.XX)
75
+ // There may also be an aggregate amount after the contribution amount
76
+ const entryPattern = /(\d{2}\/\d{2}\/\d{4})\s+\$([\d,]+\.?\d*)/g;
77
+ let match;
78
+ let lastEnd = 0;
79
+ const entries = [];
80
+ while ((match = entryPattern.exec(cleanText)) !== null) {
81
+ const textBefore = cleanText.substring(lastEnd, match.index).trim();
82
+ entries.push({
83
+ textBefore,
84
+ date: match[1],
85
+ amount: parseFloat(match[2].replace(/,/g, '')) || 0,
86
+ });
87
+ // Skip past the amount and any aggregate that follows
88
+ lastEnd = match.index + match[0].length;
89
+ // Check if there's an aggregate amount right after (e.g., "$1,500.00 $1,500.00")
90
+ const afterMatch = cleanText.substring(lastEnd).match(/^\s+\$([\d,]+\.?\d*)/);
91
+ if (afterMatch) {
92
+ lastEnd += afterMatch[0].length;
93
+ }
94
+ }
95
+ for (const entry of entries) {
96
+ if (!entry.textBefore)
97
+ continue;
98
+ // The text before the date contains: Name, Address, optional Occupation/Employer
99
+ // Split into lines by looking for state+zip patterns or C/P markers
100
+ const parts = entry.textBefore.split(/(?:Contributor|Vendor|C\/P|Rec'd For|Primary|General)\s*/i);
101
+ const mainText = parts[0] || entry.textBefore;
102
+ // Try to extract name and address
103
+ // Name is typically all caps, address follows
104
+ // Address often contains state abbreviation + zip
105
+ const addrMatch = mainText.match(/(.+?)\s+((?:\d+|P\.?O\.?\s*BOX).+)/i);
106
+ let name = mainText;
107
+ let address = '';
108
+ if (addrMatch) {
109
+ name = addrMatch[1].trim();
110
+ address = addrMatch[2].trim();
111
+ }
112
+ items.push({
113
+ name,
114
+ address,
115
+ date: entry.date,
116
+ amount: entry.amount,
117
+ });
118
+ }
119
+ return items;
120
+ }
121
+ export function parseReportDetail(html, reportId) {
122
+ // Extract full text for aggregate searching
123
+ const fullText = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ');
124
+ // Extract candidate name and report title from the page
125
+ const titleMatch = html.match(/<h2[^>]*>(.*?)<\/h2>/is);
126
+ let candidateName = '';
127
+ let reportTitle = '';
128
+ if (titleMatch) {
129
+ const titleText = titleMatch[1].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
130
+ // Format: "2028 \n Annual Year End Supplemental \n (2025) \n for BILL POWERS submitted on 02/02/2026"
131
+ const forMatch = titleText.match(/for\s+(.+?)\s+submitted/i);
132
+ if (forMatch)
133
+ candidateName = forMatch[1].trim();
134
+ const reportMatch = titleText.match(/^\d{4}\s+(.*?)(?:\(\d{4}\))?(?:\s+for)/i);
135
+ if (reportMatch)
136
+ reportTitle = reportMatch[1].trim();
137
+ }
138
+ // Extract aggregates
139
+ const beginningBalance = extractAggregate(fullText, 'Beginning Balance');
140
+ const totalContributions = extractAggregate(fullText, 'TOTAL CONTRIBUTIONS');
141
+ const totalReceipts = extractAggregate(fullText, 'TOTAL RECEIPTS');
142
+ const totalExpenditures = extractAggregate(fullText, 'TOTAL EXPENDITURES');
143
+ const totalDisbursements = extractAggregate(fullText, 'TOTAL DISBURSEMENTS');
144
+ const endingBalance = extractAggregate(fullText, 'ENDING BALANCE');
145
+ const totalOutstandingLoans = extractAggregate(fullText, 'TOTAL OUTSTANDING LOAN BALANCE');
146
+ // Extract itemized sections
147
+ const sections = splitSections(html);
148
+ let receipts = [];
149
+ let disbursements = [];
150
+ let inKindContributions = [];
151
+ let loans = [];
152
+ let obligations = [];
153
+ for (const section of sections) {
154
+ const h = section.header.toLowerCase();
155
+ if (h.includes('receipt')) {
156
+ receipts = parseLineItems(section.bodyText);
157
+ }
158
+ else if (h.includes('disbursement')) {
159
+ disbursements = parseLineItems(section.bodyText);
160
+ }
161
+ else if (h.includes('in-kind')) {
162
+ inKindContributions = parseLineItems(section.bodyText);
163
+ }
164
+ else if (h.includes('outstanding loan')) {
165
+ loans = parseLineItems(section.bodyText);
166
+ }
167
+ else if (h.includes('obligation')) {
168
+ obligations = parseLineItems(section.bodyText);
169
+ }
170
+ }
171
+ return {
172
+ reportId,
173
+ candidateName,
174
+ reportTitle,
175
+ beginningBalance,
176
+ totalContributions,
177
+ totalReceipts,
178
+ totalExpenditures,
179
+ totalDisbursements,
180
+ endingBalance,
181
+ totalOutstandingLoans,
182
+ receipts,
183
+ disbursements,
184
+ inKindContributions,
185
+ loans,
186
+ obligations,
187
+ };
188
+ }
189
+ //# sourceMappingURL=tncamp-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tncamp-report.js","sourceRoot":"","sources":["../../src/parsers/tncamp-report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * tncamp DisplayTag HTML table parser.
3
+ * All tncamp result pages use <table id="results"> with <thead> and <tbody>.
4
+ */
5
+ import type { TncampCandidate, TncampReport, TncampContribution, TncampExpenditure, TncampPaginationInfo } from '../types.js';
6
+ type TableType = 'candidate' | 'report' | 'contribution' | 'expenditure';
7
+ type RowResult = TncampCandidate | TncampReport | TncampContribution | TncampExpenditure;
8
+ export declare function parseResultsTable<T extends RowResult>(html: string, tableType: TableType): T[];
9
+ export declare function parsePaginationInfo(html: string): TncampPaginationInfo | null;
10
+ export {};
@@ -0,0 +1,113 @@
1
+ /**
2
+ * tncamp DisplayTag HTML table parser.
3
+ * All tncamp result pages use <table id="results"> with <thead> and <tbody>.
4
+ */
5
+ import { parse } from 'node-html-parser';
6
+ function text(cell) {
7
+ return cell?.text?.trim() || '';
8
+ }
9
+ function parseAmount(raw) {
10
+ const cleaned = raw.replace(/[$,]/g, '').trim();
11
+ return parseFloat(cleaned) || 0;
12
+ }
13
+ function parseCandidateRow(cells) {
14
+ if (cells.length < 6)
15
+ return null;
16
+ const reportLink = cells[5]?.querySelector('a');
17
+ const href = reportLink?.getAttribute('href') || '';
18
+ const idMatch = href.match(/id=(\d+)/);
19
+ return {
20
+ name: text(cells[0]),
21
+ party: text(cells[1]),
22
+ officeSought: text(cells[2]),
23
+ district: text(cells[3]),
24
+ electionYear: text(cells[4]),
25
+ candidateId: idMatch ? parseInt(idMatch[1], 10) : 0,
26
+ reportListUrl: href,
27
+ };
28
+ }
29
+ function parseReportRow(cells) {
30
+ if (cells.length < 4)
31
+ return null;
32
+ // Report link is in the second cell (Report Name)
33
+ const reportLink = cells[1]?.querySelector('a');
34
+ const href = reportLink?.getAttribute('href') || '';
35
+ const idMatch = href.match(/reportId=(\d+)/);
36
+ return {
37
+ election: text(cells[0]),
38
+ reportName: text(cells[1]),
39
+ isAmendment: text(cells[2]).toUpperCase() === 'Y',
40
+ submittedOn: text(cells[3]),
41
+ reportId: idMatch ? parseInt(idMatch[1], 10) : 0,
42
+ reportUrl: href,
43
+ };
44
+ }
45
+ function parseContributionRow(cells) {
46
+ if (cells.length < 9)
47
+ return null;
48
+ return {
49
+ type: text(cells[0]),
50
+ amount: parseAmount(text(cells[1])),
51
+ date: text(cells[2]),
52
+ electionYear: text(cells[3]),
53
+ recipientName: text(cells[4]),
54
+ contributorName: text(cells[5]),
55
+ contributorAddress: text(cells[6]),
56
+ contributorOccupation: text(cells[7]),
57
+ contributorEmployer: text(cells[8]),
58
+ };
59
+ }
60
+ function parseExpenditureRow(cells) {
61
+ if (cells.length < 7)
62
+ return null;
63
+ return {
64
+ type: text(cells[0]),
65
+ amount: parseAmount(text(cells[1])),
66
+ date: text(cells[2]),
67
+ vendorName: text(cells[3]),
68
+ vendorAddress: text(cells[4]),
69
+ purpose: text(cells[5]),
70
+ candidateFor: text(cells[6]),
71
+ };
72
+ }
73
+ const ROW_PARSERS = {
74
+ candidate: parseCandidateRow,
75
+ report: parseReportRow,
76
+ contribution: parseContributionRow,
77
+ expenditure: parseExpenditureRow,
78
+ };
79
+ export function parseResultsTable(html, tableType) {
80
+ const root = parse(html);
81
+ const table = root.querySelector('table#results');
82
+ if (!table)
83
+ return [];
84
+ const rows = table.querySelectorAll('tbody tr');
85
+ if (rows.length === 0)
86
+ return [];
87
+ const parser = ROW_PARSERS[tableType];
88
+ const results = [];
89
+ for (const row of rows) {
90
+ const cells = row.querySelectorAll('td');
91
+ const parsed = parser(cells);
92
+ if (parsed)
93
+ results.push(parsed);
94
+ }
95
+ return results;
96
+ }
97
+ export function parsePaginationInfo(html) {
98
+ // Pattern: "845 results found, displaying 1 to 50."
99
+ const bannerMatch = html.match(/(\d+)\s*results?\s*found,\s*displaying\s+(\d+)\s+to\s+(\d+)/i);
100
+ if (!bannerMatch)
101
+ return null;
102
+ const totalItems = parseInt(bannerMatch[1], 10);
103
+ const startItem = parseInt(bannerMatch[2], 10);
104
+ const endItem = parseInt(bannerMatch[3], 10);
105
+ const itemsPerPage = endItem - startItem + 1;
106
+ const currentPage = Math.ceil(startItem / itemsPerPage);
107
+ const totalPages = Math.ceil(totalItems / itemsPerPage);
108
+ // Extract pagination param name dynamically
109
+ const paramMatch = html.match(/(d-\d+-p)=\d+/);
110
+ const paginationParam = paramMatch ? paramMatch[1] : 'd-1341904-p';
111
+ return { totalItems, currentPage, totalPages, itemsPerPage, paginationParam };
112
+ }
113
+ //# sourceMappingURL=tncamp-results.js.map