@fin.cx/opendata 4.6.0 → 4.8.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 (67) hide show
  1. package/.smartconfig.json +6 -2
  2. package/dist_ts/00_commitinfo_data.js +3 -3
  3. package/dist_ts/financialstatements/amounts.d.ts +58 -0
  4. package/dist_ts/financialstatements/amounts.js +130 -0
  5. package/dist_ts/financialstatements/classes.financialstatementerror.d.ts +24 -0
  6. package/dist_ts/financialstatements/classes.financialstatementerror.js +10 -0
  7. package/dist_ts/financialstatements/constants.d.ts +16 -0
  8. package/dist_ts/financialstatements/constants.js +20 -0
  9. package/dist_ts/financialstatements/dates.d.ts +16 -0
  10. package/dist_ts/financialstatements/dates.js +72 -0
  11. package/dist_ts/financialstatements/html.d.ts +21 -0
  12. package/dist_ts/financialstatements/html.js +92 -0
  13. package/dist_ts/financialstatements/index.d.ts +6 -0
  14. package/dist_ts/financialstatements/index.js +11 -0
  15. package/dist_ts/financialstatements/interfaces.d.ts +258 -0
  16. package/dist_ts/financialstatements/interfaces.js +2 -0
  17. package/dist_ts/financialstatements/layout.d.ts +20 -0
  18. package/dist_ts/financialstatements/layout.html.d.ts +8 -0
  19. package/dist_ts/financialstatements/layout.html.js +125 -0
  20. package/dist_ts/financialstatements/layout.js +2 -0
  21. package/dist_ts/financialstatements/layout.pdf.d.ts +6 -0
  22. package/dist_ts/financialstatements/layout.pdf.js +229 -0
  23. package/dist_ts/financialstatements/mediatypes.d.ts +3 -0
  24. package/dist_ts/financialstatements/mediatypes.js +11 -0
  25. package/dist_ts/financialstatements/outline.d.ts +22 -0
  26. package/dist_ts/financialstatements/outline.js +26 -0
  27. package/dist_ts/financialstatements/parse.esef.d.ts +13 -0
  28. package/dist_ts/financialstatements/parse.esef.js +349 -0
  29. package/dist_ts/financialstatements/parse.hgb.d.ts +9 -0
  30. package/dist_ts/financialstatements/parse.hgb.js +1014 -0
  31. package/dist_ts/financialstatements/parse.statement.d.ts +8 -0
  32. package/dist_ts/financialstatements/parse.statement.js +25 -0
  33. package/dist_ts/financialstatements/plugins.d.ts +10 -0
  34. package/dist_ts/financialstatements/plugins.js +15 -0
  35. package/dist_ts/financialstatements/read.d.ts +10 -0
  36. package/dist_ts/financialstatements/read.js +113 -0
  37. package/dist_ts/handelsregister/classes.handelsregisterclient.d.ts +8 -2
  38. package/dist_ts/handelsregister/classes.handelsregisterclient.js +14 -5
  39. package/dist_ts/handelsregister/classes.handelsregistererror.d.ts +3 -1
  40. package/dist_ts/handelsregister/classes.handelsregistererror.js +1 -1
  41. package/dist_ts/handelsregister/classes.portalsession.d.ts +19 -3
  42. package/dist_ts/handelsregister/classes.portalsession.js +67 -9
  43. package/dist_ts/handelsregister/interfaces.d.ts +9 -2
  44. package/package.json +16 -6
  45. package/readme.md +86 -5
  46. package/ts/00_commitinfo_data.ts +2 -2
  47. package/ts/financialstatements/amounts.ts +181 -0
  48. package/ts/financialstatements/classes.financialstatementerror.ts +35 -0
  49. package/ts/financialstatements/constants.ts +24 -0
  50. package/ts/financialstatements/dates.ts +89 -0
  51. package/ts/financialstatements/html.ts +107 -0
  52. package/ts/financialstatements/index.ts +17 -0
  53. package/ts/financialstatements/interfaces.ts +268 -0
  54. package/ts/financialstatements/layout.html.ts +130 -0
  55. package/ts/financialstatements/layout.pdf.ts +268 -0
  56. package/ts/financialstatements/layout.ts +16 -0
  57. package/ts/financialstatements/mediatypes.ts +14 -0
  58. package/ts/financialstatements/outline.ts +44 -0
  59. package/ts/financialstatements/parse.esef.ts +394 -0
  60. package/ts/financialstatements/parse.hgb.ts +1235 -0
  61. package/ts/financialstatements/parse.statement.ts +26 -0
  62. package/ts/financialstatements/plugins.ts +29 -0
  63. package/ts/financialstatements/read.ts +135 -0
  64. package/ts/handelsregister/classes.handelsregisterclient.ts +18 -4
  65. package/ts/handelsregister/classes.handelsregistererror.ts +3 -0
  66. package/ts/handelsregister/classes.portalsession.ts +78 -9
  67. package/ts/handelsregister/interfaces.ts +9 -2
@@ -0,0 +1,107 @@
1
+ import * as plugins from './plugins.js';
2
+ import { FinancialStatementError } from './classes.financialstatementerror.js';
3
+
4
+ export type THtmlDocument = plugins.parse5.DefaultTreeAdapterTypes.Document;
5
+ export type THtmlElement = plugins.parse5.DefaultTreeAdapterTypes.Element;
6
+ type THtmlParentNode = plugins.parse5.DefaultTreeAdapterTypes.ParentNode;
7
+ type THtmlChildNode = plugins.parse5.DefaultTreeAdapterTypes.ChildNode;
8
+
9
+ export const parseHtml = (htmlArg: string): THtmlDocument => plugins.parse5.parse(htmlArg);
10
+
11
+ export const isElement = (nodeArg: THtmlChildNode): nodeArg is THtmlElement => 'tagName' in nodeArg;
12
+
13
+ /** the element children of a node */
14
+ export const elementChildren = (nodeArg: THtmlParentNode): THtmlElement[] =>
15
+ nodeArg.childNodes.filter(isElement);
16
+
17
+ /** every element below a node, in document order */
18
+ export const descendants = (nodeArg: THtmlParentNode): THtmlElement[] => {
19
+ const result: THtmlElement[] = [];
20
+ const stack: THtmlElement[] = elementChildren(nodeArg).reverse();
21
+ while (stack.length > 0) {
22
+ const element = stack.pop() as THtmlElement;
23
+ result.push(element);
24
+ const children = element.tagName === 'template' ? [] : elementChildren(element);
25
+ for (let index = children.length - 1; index >= 0; index--) {
26
+ stack.push(children[index]);
27
+ }
28
+ }
29
+ return result;
30
+ };
31
+
32
+ export const getAttribute = (elementArg: THtmlElement, nameArg: string): string | undefined =>
33
+ elementArg.attrs.find((attrArg) => attrArg.name === nameArg)?.value;
34
+
35
+ export const hasClass = (elementArg: THtmlElement, classArg: string): boolean =>
36
+ (getAttribute(elementArg, 'class') ?? '').split(/\s+/).includes(classArg);
37
+
38
+ /**
39
+ * the raw text of a node and everything below it, scripts and styles left out: the text nodes in
40
+ * document order, separated by spaces. Walks the tree with its own stack, so no nesting depth a page
41
+ * can have exhausts the call stack.
42
+ */
43
+ const rawText = (nodeArg: THtmlChildNode | THtmlParentNode): string => {
44
+ const pieces: string[] = [];
45
+ const stack: Array<THtmlChildNode | THtmlParentNode> = [nodeArg];
46
+ for (let node = stack.pop(); node !== undefined; node = stack.pop()) {
47
+ if ('value' in node && node.nodeName === '#text') {
48
+ pieces.push(node.value);
49
+ continue;
50
+ }
51
+ if (!('childNodes' in node)) {
52
+ continue;
53
+ }
54
+ if ('tagName' in node && (node.tagName === 'script' || node.tagName === 'style')) {
55
+ continue;
56
+ }
57
+ for (let index = node.childNodes.length - 1; index >= 0; index--) {
58
+ stack.push(node.childNodes[index]);
59
+ }
60
+ }
61
+ return pieces.join(' ');
62
+ };
63
+
64
+ /** the text of a node with white space collapsed */
65
+ export const textOf = (nodeArg: THtmlChildNode | THtmlParentNode): string =>
66
+ rawText(nodeArg).replace(/\s+/g, ' ').trim();
67
+
68
+ /** the encodings an HTML statement may declare, by the names it may declare them with */
69
+ const knownEncodings: Record<string, string> = {
70
+ 'utf-8': 'utf-8',
71
+ utf8: 'utf-8',
72
+ 'iso-8859-1': 'windows-1252',
73
+ 'iso-8859-15': 'iso-8859-15',
74
+ latin1: 'windows-1252',
75
+ 'windows-1252': 'windows-1252',
76
+ cp1252: 'windows-1252',
77
+ };
78
+
79
+ /**
80
+ * Decodes an HTML document: by its byte order mark, else by the charset its first 1,024 bytes
81
+ * declare, else as UTF-8. Bytes that are not text in that encoding are `unreadable`.
82
+ */
83
+ export const decodeHtml = (bytesArg: Uint8Array): string => {
84
+ let encoding = 'utf-8';
85
+ let start = 0;
86
+ if (bytesArg[0] === 0xef && bytesArg[1] === 0xbb && bytesArg[2] === 0xbf) {
87
+ start = 3;
88
+ } else if ((bytesArg[0] === 0xff && bytesArg[1] === 0xfe) || (bytesArg[0] === 0xfe && bytesArg[1] === 0xff)) {
89
+ encoding = bytesArg[0] === 0xff ? 'utf-16le' : 'utf-16be';
90
+ start = 2;
91
+ } else {
92
+ const head = new TextDecoder('windows-1252').decode(bytesArg.subarray(0, 1024));
93
+ const declared = /<meta[^>]+charset\s*=\s*["']?\s*([\w-]+)/i.exec(head)?.[1].toLowerCase();
94
+ if (declared !== undefined) {
95
+ const known = knownEncodings[declared];
96
+ if (known === undefined) {
97
+ throw new FinancialStatementError('unreadable', `The document declares the encoding "${declared}", which is not read here.`);
98
+ }
99
+ encoding = known;
100
+ }
101
+ }
102
+ try {
103
+ return new TextDecoder(encoding, { fatal: true }).decode(bytesArg.subarray(start));
104
+ } catch (error) {
105
+ throw new FinancialStatementError('unreadable', `The document is not text in ${encoding}.`, { cause: error });
106
+ }
107
+ };
@@ -0,0 +1,17 @@
1
+ // This entry point stands alone: it imports nothing from the rest of the package and keeps nothing.
2
+ // It reads published financial statements from the bytes a caller obtained: statements under the
3
+ // HGB from the HTML or the PDF a register shows, and ESEF reports. The caller stores the document
4
+ // and what was read from it; `parsed` is null rather than guessed.
5
+ export * from './interfaces.js';
6
+ export { FinancialStatementError, type TFinancialStatementErrorCode } from './classes.financialstatementerror.js';
7
+ export {
8
+ maxEsefFacts,
9
+ maxEsefPackageEntries,
10
+ maxPdfPages,
11
+ maxStatementBytes,
12
+ unternehmensregisterNotice,
13
+ unternehmensregisterOrigin,
14
+ } from './constants.js';
15
+ export { parseStatementDocument } from './parse.statement.js';
16
+ export { parseEsefPackage, parseEsefReport } from './parse.esef.js';
17
+ export { readFinancialStatement } from './read.js';
@@ -0,0 +1,268 @@
1
+ /** An ISO date, `2024-12-31`. */
2
+ export type TIsoDate = string;
3
+
4
+ /**
5
+ * Where a statement was published:
6
+ * - `unternehmensregister`: the Unternehmensregister, for financial years beginning after 31.12.2021,
7
+ * and for the balance sheets micro companies deposit there (§ 326 Abs. 2 HGB);
8
+ * - `bundesanzeiger`: the Bundesanzeiger, for earlier financial years;
9
+ * - `issuer`: the company's own publication, as listed issuers publish their ESEF reports;
10
+ * - `unknown`: a document whose origin the caller does not know.
11
+ */
12
+ export type TFilingSource = 'unternehmensregister' | 'bundesanzeiger' | 'issuer' | 'unknown';
13
+
14
+ /**
15
+ * What a filing is:
16
+ * - `annual_accounts`: the Jahresabschluss of one company (§§ 242, 264 HGB);
17
+ * - `consolidated_accounts`: a group's Konzernabschluss (§ 290 HGB);
18
+ * - `deposited_balance_sheet`: the balance sheet a micro company deposits instead of publishing
19
+ * (§ 326 Abs. 2 HGB);
20
+ * - `annual_financial_report`: a listed issuer's Jahresfinanzbericht (§ 114 WpHG), as an ESEF report.
21
+ */
22
+ export type TFilingKind = 'annual_accounts' | 'consolidated_accounts' | 'deposited_balance_sheet' | 'annual_financial_report';
23
+
24
+ /** The size classes of § 267 and § 267a HGB, as a filing states them. */
25
+ export type TSizeClass = 'micro' | 'small' | 'medium' | 'large';
26
+
27
+ /**
28
+ * One published statement as its publisher lists it. Every field the caller does not know is null;
29
+ * none is derived from the document.
30
+ */
31
+ export interface IFiling {
32
+ source: TFilingSource;
33
+ kind: TFilingKind;
34
+ /** the company's name as the publisher writes it */
35
+ companyName: string | null;
36
+ /** the register sheet, as the caller writes it: `Amtsgericht Bremen HRB 35230 HB` */
37
+ registerReference: string | null;
38
+ /** the company's Legal Entity Identifier */
39
+ lei: string | null;
40
+ fiscalYearStart: TIsoDate | null;
41
+ fiscalYearEnd: TIsoDate | null;
42
+ /**
43
+ * the day the publisher posted the document; for a deposited balance sheet, the day it was
44
+ * deposited. The Unternehmensregister's origin note names this day.
45
+ */
46
+ publishedOn: TIsoDate | null;
47
+ /** the publisher's own title of the document */
48
+ title: string | null;
49
+ sizeClass: TSizeClass | null;
50
+ /** the publisher's identifier of the document */
51
+ documentId: string | null;
52
+ /** where the publisher shows the document */
53
+ sourceUrl: string | null;
54
+ }
55
+
56
+ /** What a caller states about a filing: `source` and `kind`, and whatever else it knows. */
57
+ export interface IFilingInput extends Pick<IFiling, 'source' | 'kind'>, Partial<Omit<IFiling, 'source' | 'kind'>> {}
58
+
59
+ /**
60
+ * The media types this entry point reads:
61
+ * - `text/html`: a statement as the Unternehmensregister or the Bundesanzeiger shows it;
62
+ * - `application/pdf`: the same as a PDF with a text layer;
63
+ * - `application/xhtml+xml`: an ESEF report (inline XBRL) on its own;
64
+ * - `application/zip`: an ESEF report package (`.zip` or `.xbri`).
65
+ */
66
+ export type TStatementMediaType = 'text/html' | 'application/pdf' | 'application/xhtml+xml' | 'application/zip';
67
+
68
+ /** The document as it was obtained, byte for byte. */
69
+ export interface IRawStatementDocument {
70
+ bytes: Uint8Array;
71
+ mediaType: TStatementMediaType;
72
+ /** the SHA-256 of `bytes`, lowercase hex */
73
+ sha256: string;
74
+ }
75
+
76
+ /**
77
+ * The unit a statement prints its amounts in. Amounts in this entry point are always in the
78
+ * currency itself, the unit's factor applied: `1.234` printed in TEUR is `'1234000'`.
79
+ */
80
+ export interface IStatementUnit {
81
+ /** as this entry point names it */
82
+ label: 'EUR' | 'TEUR' | 'Mio. EUR';
83
+ /** the number of euros one printed unit stands for */
84
+ factor: '1' | '1000' | '1000000';
85
+ }
86
+
87
+ /** One of the two years a statement shows. */
88
+ export interface IStatementPeriod {
89
+ /** the column heading as printed, `Geschäftsjahr Euro` or `31.12.2024`; null where none is printed */
90
+ heading: string | null;
91
+ /** the first day, where the statement states it */
92
+ start: TIsoDate | null;
93
+ /** the balance sheet date, the last day of the period, where the statement states it */
94
+ end: TIsoDate | null;
95
+ }
96
+
97
+ /**
98
+ * The figures most readers look for first, as decimal strings in the currency. Each is null where
99
+ * the statement does not print it:
100
+ * - `totalAssets`: the balance sheet total;
101
+ * - `equity`: the equity section of the balance sheet, its printed total or, where the statement
102
+ * prints only its parts, their sum;
103
+ * - `netIncome`: the net income or loss (Jahresüberschuss or Jahresfehlbetrag), from the income
104
+ * statement or, where none is published, from the balance sheet's equity section;
105
+ * - `revenue`: the revenue (Umsatzerlöse); small companies may print only a gross result
106
+ * (Rohergebnis, § 276 HGB), and then it is null.
107
+ */
108
+ export interface IKeyFigures {
109
+ totalAssets: string | null;
110
+ equity: string | null;
111
+ netIncome: string | null;
112
+ revenue: string | null;
113
+ }
114
+
115
+ /**
116
+ * The kind of a printed line:
117
+ * - `position`: a position of the outline of § 266 or § 275 HGB, or a line the statement adds to it;
118
+ * - `subtotal`: a printed sum of the lines above it, such as Summe Anlagevermögen, or an
119
+ * intermediate result of the income statement, such as Rohergebnis or Ergebnis nach Steuern;
120
+ * no total adds it again;
121
+ * - `total`: the total of a side of the balance sheet, or the income statement's net income;
122
+ * - `memo`: a "davon" line, part of the line above it and never added.
123
+ */
124
+ export type TStatementLineKind = 'position' | 'subtotal' | 'total' | 'memo';
125
+
126
+ /** One printed line of a balance sheet or an income statement. */
127
+ export interface IStatementPosition {
128
+ /** the outline path of the line, its printed codes joined: `A.I.1`; null for a line without a code */
129
+ code: string | null;
130
+ /** the code as printed: `1.` or `a)` */
131
+ printedCode: string | null;
132
+ /** the line's text as printed, without its code */
133
+ label: string;
134
+ /** the depth in the outline, 0 for the top level */
135
+ level: number;
136
+ kind: TStatementLineKind;
137
+ /** the amount for the current year as a decimal string in the currency, null where none is printed */
138
+ current: string | null;
139
+ /** the same for the prior year */
140
+ prior: string | null;
141
+ }
142
+
143
+ export interface IBalanceSheet {
144
+ unit: IStatementUnit;
145
+ /** Aktiva, in printed order, with the total last */
146
+ assets: IStatementPosition[];
147
+ /** Passiva, in printed order, with the total last */
148
+ equityAndLiabilities: IStatementPosition[];
149
+ }
150
+
151
+ /**
152
+ * The method of an income statement: `total_cost` (Gesamtkostenverfahren, § 275 Abs. 2 HGB) or
153
+ * `cost_of_sales` (Umsatzkostenverfahren, § 275 Abs. 3 HGB); null where its lines do not tell.
154
+ */
155
+ export type TIncomeStatementMethod = 'total_cost' | 'cost_of_sales';
156
+
157
+ export interface IIncomeStatement {
158
+ unit: IStatementUnit;
159
+ method: TIncomeStatementMethod | null;
160
+ /**
161
+ * how the statement prints its expenses: `negative`, with a minus, so that its lines add up as
162
+ * printed, or `positive`, as amounts to subtract. The positions keep the amounts as printed.
163
+ */
164
+ expenses: 'negative' | 'positive';
165
+ positions: IStatementPosition[];
166
+ }
167
+
168
+ /**
169
+ * A statement under the HGB read from the HTML or the PDF a register shows. Everything in it was
170
+ * printed and adds up: each position with parts equals their sum, each side of the balance sheet its
171
+ * total, both totals each other, and the income statement's lines its net income. A prior year whose
172
+ * figures do not add up, and an income statement that does not, are left out with a note.
173
+ */
174
+ export interface IHgbStatement {
175
+ standard: 'hgb';
176
+ currency: 'EUR';
177
+ periods: { current: IStatementPeriod; prior: IStatementPeriod | null };
178
+ keyFigures: { current: IKeyFigures; prior: IKeyFigures | null };
179
+ balanceSheet: IBalanceSheet;
180
+ /** null where the document publishes none, as small companies may (§ 326 Abs. 1 HGB) */
181
+ incomeStatement: IIncomeStatement | null;
182
+ /** what was left out and why, in English */
183
+ notes: string[];
184
+ }
185
+
186
+ /** The period of an ESEF fact: an instant (`start` null) or a duration. */
187
+ export interface IEsefPeriod {
188
+ start: TIsoDate | null;
189
+ end: TIsoDate;
190
+ }
191
+
192
+ /** One numeric fact of an ESEF report (`ix:nonFraction`). */
193
+ export interface IEsefFact {
194
+ /** the concept with the prefix the report declares for it: `ifrs-full:Revenue` */
195
+ concept: string;
196
+ /** the namespace of the concept */
197
+ namespace: string;
198
+ contextId: string;
199
+ period: IEsefPeriod;
200
+ /** the explicit dimension members of the context, dimension to member, both as written */
201
+ dimensions: Record<string, string>;
202
+ /** the unit: an ISO 4217 code such as `EUR`, `shares`, `pure`, or a ratio such as `EUR/shares` */
203
+ unit: string | null;
204
+ /** the value as a decimal string, scale and sign applied; null for a nil fact */
205
+ value: string | null;
206
+ /** the `decimals` attribute as written: `0`, `-3`, `INF` */
207
+ decimals: string | null;
208
+ /** the fact's `id`, where it has one */
209
+ id: string | null;
210
+ }
211
+
212
+ /**
213
+ * A listed issuer's annual financial report in the European Single Electronic Format: the numeric
214
+ * facts its inline XBRL tags, and the key figures of its consolidated statements (IFRS).
215
+ */
216
+ export interface IEsefStatement {
217
+ standard: 'esef';
218
+ /** the LEI the report's contexts identify the issuer with */
219
+ lei: string | null;
220
+ /** the path of the report in its package; null for a report read on its own */
221
+ reportPath: string | null;
222
+ /** the language the report declares */
223
+ language: string | null;
224
+ /** the currency of the key figures, ISO 4217 */
225
+ currency: string | null;
226
+ periods: { current: IStatementPeriod; prior: IStatementPeriod | null };
227
+ keyFigures: { current: IKeyFigures; prior: IKeyFigures | null };
228
+ facts: IEsefFact[];
229
+ /** what could not be read and why, in English */
230
+ notes: string[];
231
+ }
232
+
233
+ export type TParsedStatement = IHgbStatement | IEsefStatement;
234
+
235
+ /** Why a document was kept without a parsed statement. */
236
+ export interface IStatementParseFailure {
237
+ code: 'invalid_package' | 'unreadable' | 'no_text' | 'unparseable' | 'too_large';
238
+ message: string;
239
+ }
240
+
241
+ /**
242
+ * A statement as a caller keeps it: the filing, the document byte for byte, and what could be read
243
+ * from it. `parsed` is null rather than guessed: a document this entry point cannot read with
244
+ * certainty keeps its raw bytes, and `parseFailure` says why.
245
+ */
246
+ export interface IFinancialStatement {
247
+ filing: IFiling;
248
+ raw: IRawStatementDocument;
249
+ parsed: TParsedStatement | null;
250
+ parseFailure: IStatementParseFailure | null;
251
+ /** when the document was obtained, milliseconds since the epoch */
252
+ obtainedAt: number;
253
+ /**
254
+ * the origin note the publisher marks the document with, for a copy to keep it: for the
255
+ * Unternehmensregister "Auszug aus dem Unternehmensregister" and the day the document was posted
256
+ * or deposited (Nutzungsbedingungen § 3 Abs. 5); null for other publishers
257
+ */
258
+ notice: string | null;
259
+ }
260
+
261
+ export interface IReadFinancialStatementOptions {
262
+ filing: IFilingInput;
263
+ bytes: Uint8Array;
264
+ /** the media type of `bytes`; parameters such as `; charset=utf-8` are ignored */
265
+ mediaType: string;
266
+ /** clock for `obtainedAt`; default `Date.now` */
267
+ now?: () => number;
268
+ }
@@ -0,0 +1,130 @@
1
+ import * as plugins from './plugins.js';
2
+ import { descendants, elementChildren, getAttribute, hasClass, isElement, parseHtml, textOf, type THtmlElement } from './html.js';
3
+ import { normalizeSpaces } from './amounts.js';
4
+ import type { ILayoutCell, TLayoutBlock } from './layout.js';
5
+
6
+ type THtmlChildNode = plugins.parse5.DefaultTreeAdapterTypes.ChildNode;
7
+
8
+ const headingTags = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
9
+ const skippedTags = new Set(['head', 'script', 'style', 'template', 'noscript', 'svg', 'math', 'img', 'button', 'select', 'input']);
10
+
11
+ /** elements that end a line of text before and after them */
12
+ const blockTags = new Set([
13
+ 'address', 'article', 'aside', 'blockquote', 'body', 'caption', 'center', 'dd', 'details', 'dialog', 'div',
14
+ 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'hr', 'li', 'main',
15
+ 'nav', 'ol', 'p', 'pre', 'section', 'summary', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'ul',
16
+ ]);
17
+
18
+ /** colspan and rowspan as a statement may write them, kept to 1 to 50 */
19
+ const spanOf = (valueArg: string | undefined): number => {
20
+ const value = Number.parseInt(valueArg ?? '1', 10);
21
+ return Number.isFinite(value) ? Math.min(Math.max(value, 1), 50) : 1;
22
+ };
23
+
24
+ /** the rows of a table without nested tables, every cell placed in the columns it covers */
25
+ const tableRows = (tableArg: THtmlElement): ILayoutCell[][] => {
26
+ const rowElements = descendants(tableArg).filter((elementArg) => elementArg.tagName === 'tr');
27
+ const occupied: Array<Set<number>> = rowElements.map(() => new Set<number>());
28
+ const rows: ILayoutCell[][] = [];
29
+ rowElements.forEach((rowElement, rowIndex) => {
30
+ const cells: ILayoutCell[] = [];
31
+ let column = 0;
32
+ for (const cellElement of elementChildren(rowElement)) {
33
+ if (cellElement.tagName !== 'td' && cellElement.tagName !== 'th') {
34
+ continue;
35
+ }
36
+ while (occupied[rowIndex].has(column)) {
37
+ column++;
38
+ }
39
+ const colspan = spanOf(getAttribute(cellElement, 'colspan'));
40
+ const rowspan = spanOf(getAttribute(cellElement, 'rowspan'));
41
+ for (let spannedRow = rowIndex; spannedRow < Math.min(rowIndex + rowspan, rowElements.length); spannedRow++) {
42
+ for (let spannedColumn = column; spannedColumn < column + colspan; spannedColumn++) {
43
+ occupied[spannedRow].add(spannedColumn);
44
+ }
45
+ }
46
+ const text = normalizeSpaces(textOf(cellElement));
47
+ if (text !== '') {
48
+ cells.push({ text, first: column, last: column + colspan - 1 });
49
+ }
50
+ column += colspan;
51
+ }
52
+ if (cells.length > 0) {
53
+ rows.push(cells);
54
+ }
55
+ });
56
+ return rows;
57
+ };
58
+
59
+ /**
60
+ * The blocks of an HTML statement in document order: headings, lines of text and tables. Reads the
61
+ * publication itself (`div.publication_container`, as the Unternehmensregister and the Bundesanzeiger
62
+ * show it) where the page has one, else the whole body. A table that only lays out other tables is
63
+ * read through to them.
64
+ */
65
+ export const htmlLayout = (htmlArg: string): TLayoutBlock[] => {
66
+ const document = parseHtml(htmlArg);
67
+ const elements = descendants(document);
68
+ const root =
69
+ elements.find((elementArg) => elementArg.tagName === 'div' && hasClass(elementArg, 'publication_container')) ??
70
+ elements.find((elementArg) => elementArg.tagName === 'body');
71
+ if (!root) {
72
+ return [];
73
+ }
74
+ const blocks: TLayoutBlock[] = [];
75
+ let run: string[] = [];
76
+ const flush = () => {
77
+ const text = normalizeSpaces(run.join(' '));
78
+ run = [];
79
+ if (text !== '') {
80
+ blocks.push({ kind: 'text', text, heading: false });
81
+ }
82
+ };
83
+ const stack: Array<{ children: THtmlChildNode[]; index: number; block: boolean }> = [
84
+ { children: root.childNodes, index: 0, block: true },
85
+ ];
86
+ while (stack.length > 0) {
87
+ const frame = stack[stack.length - 1];
88
+ if (frame.index >= frame.children.length) {
89
+ stack.pop();
90
+ if (frame.block) {
91
+ flush();
92
+ }
93
+ continue;
94
+ }
95
+ const child = frame.children[frame.index++];
96
+ if (!isElement(child)) {
97
+ if ('value' in child && child.nodeName === '#text') {
98
+ run.push(child.value);
99
+ }
100
+ continue;
101
+ }
102
+ const tag = child.tagName;
103
+ if (skippedTags.has(tag)) {
104
+ continue;
105
+ }
106
+ if (tag === 'br') {
107
+ flush();
108
+ } else if (headingTags.has(tag)) {
109
+ flush();
110
+ const text = normalizeSpaces(textOf(child));
111
+ if (text !== '') {
112
+ blocks.push({ kind: 'text', text, heading: true });
113
+ }
114
+ } else if (tag === 'table' && !descendants(child).some((elementArg) => elementArg.tagName === 'table')) {
115
+ flush();
116
+ const rows = tableRows(child);
117
+ if (rows.length > 0) {
118
+ blocks.push({ kind: 'table', rows });
119
+ }
120
+ } else {
121
+ const block = tag === 'table' || blockTags.has(tag);
122
+ if (block) {
123
+ flush();
124
+ }
125
+ stack.push({ children: child.childNodes, index: 0, block });
126
+ }
127
+ }
128
+ flush();
129
+ return blocks;
130
+ };