@fin.cx/opendata 3.6.0 → 4.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/.smartconfig.json +23 -20
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/handelsregister/classes.handelsregisterclient.d.ts +64 -0
- package/dist_ts/handelsregister/classes.handelsregisterclient.js +397 -0
- package/dist_ts/handelsregister/classes.handelsregistererror.d.ts +56 -0
- package/dist_ts/handelsregister/classes.handelsregistererror.js +37 -0
- package/dist_ts/handelsregister/classes.portalsession.d.ts +46 -0
- package/dist_ts/handelsregister/classes.portalsession.js +160 -0
- package/dist_ts/handelsregister/classes.slidingwindowgate.d.ts +32 -0
- package/dist_ts/handelsregister/classes.slidingwindowgate.js +55 -0
- package/dist_ts/handelsregister/constants.d.ts +13 -0
- package/dist_ts/handelsregister/constants.js +19 -0
- package/dist_ts/handelsregister/data.codelists.d.ts +29 -0
- package/dist_ts/handelsregister/data.codelists.js +616 -0
- package/dist_ts/handelsregister/forms.d.ts +27 -0
- package/dist_ts/handelsregister/forms.js +77 -0
- package/dist_ts/handelsregister/html.d.ts +26 -0
- package/dist_ts/handelsregister/html.js +82 -0
- package/dist_ts/handelsregister/index.d.ts +9 -0
- package/dist_ts/handelsregister/index.js +10 -0
- package/dist_ts/handelsregister/interfaces.d.ts +240 -0
- package/dist_ts/handelsregister/interfaces.js +2 -0
- package/dist_ts/handelsregister/parse.searchresults.d.ts +35 -0
- package/dist_ts/handelsregister/parse.searchresults.js +175 -0
- package/dist_ts/handelsregister/parse.structuredcontent.d.ts +6 -0
- package/dist_ts/handelsregister/parse.structuredcontent.js +306 -0
- package/dist_ts/handelsregister/plugins.d.ts +4 -0
- package/dist_ts/handelsregister/plugins.js +8 -0
- package/dist_ts/handelsregister/references.d.ts +38 -0
- package/dist_ts/handelsregister/references.js +146 -0
- package/dist_ts/holidays/calendar.d.ts +32 -0
- package/dist_ts/holidays/calendar.js +200 -0
- package/dist_ts/holidays/classes.holidaycalendarerror.d.ts +14 -0
- package/dist_ts/holidays/classes.holidaycalendarerror.js +10 -0
- package/dist_ts/holidays/data.holidays.d.ts +46 -0
- package/dist_ts/holidays/data.holidays.js +124 -0
- package/dist_ts/holidays/index.d.ts +4 -0
- package/dist_ts/holidays/index.js +7 -0
- package/dist_ts/holidays/interfaces.d.ts +42 -0
- package/dist_ts/holidays/interfaces.js +2 -0
- package/dist_ts/index.d.ts +0 -2
- package/dist_ts/index.js +1 -3
- package/dist_ts/plugins.d.ts +2 -20
- package/dist_ts/plugins.js +3 -22
- package/package.json +40 -37
- package/readme.md +250 -119
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/handelsregister/classes.handelsregisterclient.ts +474 -0
- package/ts/handelsregister/classes.handelsregistererror.ts +82 -0
- package/ts/handelsregister/classes.portalsession.ts +191 -0
- package/ts/handelsregister/classes.slidingwindowgate.ts +74 -0
- package/ts/handelsregister/constants.ts +30 -0
- package/ts/handelsregister/data.codelists.ts +640 -0
- package/ts/handelsregister/forms.ts +99 -0
- package/ts/handelsregister/html.ts +113 -0
- package/ts/handelsregister/index.ts +27 -0
- package/ts/handelsregister/interfaces.ts +272 -0
- package/ts/handelsregister/parse.searchresults.ts +241 -0
- package/ts/handelsregister/parse.structuredcontent.ts +338 -0
- package/ts/handelsregister/plugins.ts +15 -0
- package/ts/handelsregister/references.ts +189 -0
- package/ts/holidays/calendar.ts +261 -0
- package/ts/holidays/classes.holidaycalendarerror.ts +20 -0
- package/ts/holidays/data.holidays.ts +177 -0
- package/ts/holidays/index.ts +18 -0
- package/ts/holidays/interfaces.ts +87 -0
- package/ts/index.ts +0 -2
- package/ts/plugins.ts +0 -39
- package/dist_ts/classes.businessrecord.d.ts +0 -38
- package/dist_ts/classes.businessrecord.js +0 -90
- package/dist_ts/classes.handelsregister.d.ts +0 -134
- package/dist_ts/classes.handelsregister.js +0 -307
- package/dist_ts/classes.jsonldata.d.ts +0 -43
- package/dist_ts/classes.jsonldata.js +0 -68
- package/dist_ts/classes.main.opendata.d.ts +0 -25
- package/dist_ts/classes.main.opendata.js +0 -91
- package/dist_ts/laws/classes.lawrecord.d.ts +0 -24
- package/dist_ts/laws/classes.lawrecord.js +0 -181
- package/dist_ts/laws/classes.lawservice.d.ts +0 -52
- package/dist_ts/laws/classes.lawservice.js +0 -740
- package/dist_ts/laws/index.d.ts +0 -3
- package/dist_ts/laws/index.js +0 -4
- package/dist_ts/laws/interfaces.law.d.ts +0 -35
- package/dist_ts/laws/interfaces.law.js +0 -2
- package/readme.hints.md +0 -89
- package/ts/classes.businessrecord.ts +0 -67
- package/ts/classes.handelsregister.ts +0 -385
- package/ts/classes.jsonldata.ts +0 -114
- package/ts/classes.main.opendata.ts +0 -120
- package/ts/laws/classes.lawrecord.ts +0 -81
- package/ts/laws/classes.lawservice.ts +0 -1010
- package/ts/laws/index.ts +0 -3
- package/ts/laws/interfaces.law.ts +0 -48
|
@@ -1,1010 +0,0 @@
|
|
|
1
|
-
import * as plugins from '../plugins.js';
|
|
2
|
-
import * as paths from '../paths.js';
|
|
3
|
-
import { LawRecord } from './classes.lawrecord.js';
|
|
4
|
-
import type {
|
|
5
|
-
ILawLookupRequest,
|
|
6
|
-
ILawSearchRequest,
|
|
7
|
-
ILawServiceConfig,
|
|
8
|
-
ILawSyncRequest,
|
|
9
|
-
ILawSyncResult,
|
|
10
|
-
TJurisdiction,
|
|
11
|
-
TLawSource,
|
|
12
|
-
TRawLawFormat,
|
|
13
|
-
TUsLawCollection,
|
|
14
|
-
} from './interfaces.law.js';
|
|
15
|
-
|
|
16
|
-
interface IStoredLawData {
|
|
17
|
-
jurisdiction: TJurisdiction;
|
|
18
|
-
source: TLawSource;
|
|
19
|
-
identifier: string;
|
|
20
|
-
title: string;
|
|
21
|
-
shortTitle?: string;
|
|
22
|
-
citation?: string;
|
|
23
|
-
type?: string;
|
|
24
|
-
language?: string;
|
|
25
|
-
sourceUrl: string;
|
|
26
|
-
rawFormat: TRawLawFormat;
|
|
27
|
-
rawBody: string;
|
|
28
|
-
text: string;
|
|
29
|
-
dateIssued?: string;
|
|
30
|
-
lastModified?: string;
|
|
31
|
-
sourceMeta?: Record<string, string>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface IGermanyLawTocEntry {
|
|
35
|
-
title: string;
|
|
36
|
-
xmlZipUrl: string;
|
|
37
|
-
slug: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface IEuLawMetadata {
|
|
41
|
-
celex: string;
|
|
42
|
-
title: string;
|
|
43
|
-
type: string;
|
|
44
|
-
dateIssued: string;
|
|
45
|
-
eli: string;
|
|
46
|
-
lastModified: string;
|
|
47
|
-
language: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface ISparqlJsonResult {
|
|
51
|
-
results?: {
|
|
52
|
-
bindings?: Array<Record<string, { value: string }>>;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface IGovInfoCollectionResponse {
|
|
57
|
-
nextPage?: string;
|
|
58
|
-
packages?: Array<{
|
|
59
|
-
packageId: string;
|
|
60
|
-
}>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface IGovInfoSummary {
|
|
64
|
-
packageId: string;
|
|
65
|
-
collectionCode: string;
|
|
66
|
-
title?: string;
|
|
67
|
-
shortTitle?: Array<{
|
|
68
|
-
title: string;
|
|
69
|
-
}>;
|
|
70
|
-
detailsLink?: string;
|
|
71
|
-
dateIssued?: string;
|
|
72
|
-
lastModified?: string;
|
|
73
|
-
titleNumber?: string;
|
|
74
|
-
congress?: string;
|
|
75
|
-
documentNumber?: string;
|
|
76
|
-
documentType?: string;
|
|
77
|
-
download?: {
|
|
78
|
-
txtLink?: string;
|
|
79
|
-
uslmLink?: string;
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface IEuPageContent {
|
|
84
|
-
html: string;
|
|
85
|
-
text: string;
|
|
86
|
-
title: string;
|
|
87
|
-
dateIssued: string;
|
|
88
|
-
type: string;
|
|
89
|
-
eli: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface IUsCodeCitation {
|
|
93
|
-
titleNumber: string;
|
|
94
|
-
sectionNumber: string;
|
|
95
|
-
subsectionPath: string;
|
|
96
|
-
canonicalIdentifier: string;
|
|
97
|
-
canonicalCitation: string;
|
|
98
|
-
sourceUrl: string;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export class LawService {
|
|
102
|
-
public db!: plugins.smartdata.SmartdataDb;
|
|
103
|
-
public CLawRecord = plugins.smartdata.setDefaultManagerForDoc(this, LawRecord);
|
|
104
|
-
|
|
105
|
-
private localSmartDb?: plugins.smartdb.LocalSmartDb;
|
|
106
|
-
private smartBrowser?: plugins.smartbrowser.SmartBrowser;
|
|
107
|
-
private browserStarted = false;
|
|
108
|
-
private started = false;
|
|
109
|
-
|
|
110
|
-
private config: Required<Pick<ILawServiceConfig, 'dbFolderPath' | 'dbName' | 'govInfoApiKey'>>;
|
|
111
|
-
|
|
112
|
-
constructor(configArg: ILawServiceConfig = {}) {
|
|
113
|
-
this.config = {
|
|
114
|
-
dbFolderPath:
|
|
115
|
-
configArg.dbFolderPath ??
|
|
116
|
-
plugins.path.join(paths.packageDir, '.nogit', 'law-smartdb'),
|
|
117
|
-
dbName: configArg.dbName ?? 'laws',
|
|
118
|
-
govInfoApiKey: configArg.govInfoApiKey ?? 'DEMO_KEY',
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
public async start() {
|
|
123
|
-
if (this.started) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
await plugins.smartfs.directory(this.config.dbFolderPath).create();
|
|
128
|
-
this.localSmartDb = new plugins.smartdb.LocalSmartDb({
|
|
129
|
-
folderPath: this.config.dbFolderPath,
|
|
130
|
-
});
|
|
131
|
-
const connectionInfo = await this.localSmartDb.start();
|
|
132
|
-
|
|
133
|
-
this.db = new plugins.smartdata.SmartdataDb({
|
|
134
|
-
mongoDbUrl: connectionInfo.connectionUri,
|
|
135
|
-
mongoDbName: this.config.dbName,
|
|
136
|
-
});
|
|
137
|
-
await this.db.init();
|
|
138
|
-
await this.db.mongoDb.collection('_opendata_bootstrap').insertOne({
|
|
139
|
-
createdAt: new Date(),
|
|
140
|
-
});
|
|
141
|
-
await this.db.mongoDb.collection('_opendata_bootstrap').deleteMany({});
|
|
142
|
-
this.started = true;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
public async stop() {
|
|
146
|
-
if (this.browserStarted && this.smartBrowser) {
|
|
147
|
-
await this.smartBrowser.stop();
|
|
148
|
-
this.browserStarted = false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (this.started) {
|
|
152
|
-
await this.db.close();
|
|
153
|
-
await this.localSmartDb?.stop();
|
|
154
|
-
this.started = false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
public async syncLaw(requestArg: ILawLookupRequest): Promise<LawRecord> {
|
|
159
|
-
await this.ensureStarted();
|
|
160
|
-
|
|
161
|
-
switch (requestArg.jurisdiction) {
|
|
162
|
-
case 'de':
|
|
163
|
-
return this.syncGermanyLaw(requestArg.identifier);
|
|
164
|
-
case 'eu':
|
|
165
|
-
return this.syncEuLaw(requestArg.identifier, requestArg.language ?? 'EN');
|
|
166
|
-
case 'us':
|
|
167
|
-
return this.syncUsLaw(
|
|
168
|
-
requestArg.identifier,
|
|
169
|
-
requestArg.usCollection,
|
|
170
|
-
this.config.govInfoApiKey
|
|
171
|
-
);
|
|
172
|
-
default:
|
|
173
|
-
throw new Error(`Unsupported jurisdiction: ${requestArg.jurisdiction}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
public async getLaw(requestArg: ILawLookupRequest): Promise<LawRecord | null> {
|
|
178
|
-
await this.ensureStarted();
|
|
179
|
-
|
|
180
|
-
const lookupKey = this.createLookupKey(requestArg.jurisdiction, requestArg.identifier);
|
|
181
|
-
const existingLaw = await this.getLawByLookupKey(lookupKey);
|
|
182
|
-
if (existingLaw && !requestArg.forceSync) {
|
|
183
|
-
return existingLaw;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return this.syncLaw(requestArg);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
public async searchLaws(requestArg: ILawSearchRequest): Promise<LawRecord[]> {
|
|
190
|
-
await this.ensureStarted();
|
|
191
|
-
|
|
192
|
-
const limit = requestArg.limit ?? 20;
|
|
193
|
-
const baseFilter = requestArg.jurisdiction
|
|
194
|
-
? ({ jurisdiction: requestArg.jurisdiction } as Record<string, unknown>)
|
|
195
|
-
: undefined;
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
const results = await this.CLawRecord.search(
|
|
199
|
-
requestArg.query,
|
|
200
|
-
baseFilter ? { filter: baseFilter } : undefined
|
|
201
|
-
);
|
|
202
|
-
return results.slice(0, limit);
|
|
203
|
-
} catch {
|
|
204
|
-
const regex = new RegExp(this.escapeRegex(requestArg.query), 'i');
|
|
205
|
-
const searchFilter = {
|
|
206
|
-
$or: [
|
|
207
|
-
{ identifier: { $regex: regex } },
|
|
208
|
-
{ title: { $regex: regex } },
|
|
209
|
-
{ shortTitle: { $regex: regex } },
|
|
210
|
-
{ citation: { $regex: regex } },
|
|
211
|
-
{ text: { $regex: regex } },
|
|
212
|
-
],
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const mongoFilter = baseFilter
|
|
216
|
-
? {
|
|
217
|
-
$and: [baseFilter, searchFilter],
|
|
218
|
-
}
|
|
219
|
-
: searchFilter;
|
|
220
|
-
|
|
221
|
-
const results = await this.CLawRecord.getInstances(mongoFilter as any);
|
|
222
|
-
return results.slice(0, limit);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
public async syncLaws(requestArg: ILawSyncRequest): Promise<ILawSyncResult> {
|
|
227
|
-
await this.ensureStarted();
|
|
228
|
-
|
|
229
|
-
switch (requestArg.jurisdiction) {
|
|
230
|
-
case 'de':
|
|
231
|
-
return this.syncGermanyLaws(requestArg);
|
|
232
|
-
case 'eu':
|
|
233
|
-
return this.syncEuLaws(requestArg);
|
|
234
|
-
case 'us':
|
|
235
|
-
return this.syncUsLaws(requestArg);
|
|
236
|
-
default:
|
|
237
|
-
throw new Error(`Unsupported jurisdiction: ${requestArg.jurisdiction}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private async ensureStarted() {
|
|
242
|
-
if (!this.started) {
|
|
243
|
-
await this.start();
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private async ensureBrowser() {
|
|
248
|
-
if (!this.smartBrowser) {
|
|
249
|
-
this.smartBrowser = new plugins.smartbrowser.SmartBrowser();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (!this.browserStarted) {
|
|
253
|
-
await this.smartBrowser.start();
|
|
254
|
-
this.browserStarted = true;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return this.smartBrowser;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private createLookupKey(jurisdictionArg: TJurisdiction, identifierArg: string) {
|
|
261
|
-
return `${jurisdictionArg}:${this.normalizeIdentifierForLookup(jurisdictionArg, identifierArg)}`;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private normalizeIdentifierForLookup(jurisdictionArg: TJurisdiction, identifierArg: string) {
|
|
265
|
-
switch (jurisdictionArg) {
|
|
266
|
-
case 'de':
|
|
267
|
-
return this.normalizeGermanySlug(identifierArg);
|
|
268
|
-
case 'eu':
|
|
269
|
-
return identifierArg.trim().replace(/^CELEX:/i, '').toUpperCase();
|
|
270
|
-
case 'us': {
|
|
271
|
-
const usCodeCitation = this.parseUsCodeCitation(identifierArg);
|
|
272
|
-
return usCodeCitation
|
|
273
|
-
? usCodeCitation.canonicalIdentifier
|
|
274
|
-
: identifierArg.trim().toUpperCase();
|
|
275
|
-
}
|
|
276
|
-
default:
|
|
277
|
-
return identifierArg.trim();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private async getLawByLookupKey(lookupKeyArg: string) {
|
|
282
|
-
return LawRecord.getByLookupKey(lookupKeyArg);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private async upsertLaw(dataArg: IStoredLawData): Promise<LawRecord> {
|
|
286
|
-
const lookupKey = this.createLookupKey(dataArg.jurisdiction, dataArg.identifier);
|
|
287
|
-
let lawRecord = await this.getLawByLookupKey(lookupKey);
|
|
288
|
-
|
|
289
|
-
if (!lawRecord) {
|
|
290
|
-
lawRecord = new this.CLawRecord();
|
|
291
|
-
lawRecord.id = await this.CLawRecord.getNewId();
|
|
292
|
-
lawRecord.lookupKey = lookupKey;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
lawRecord.jurisdiction = dataArg.jurisdiction;
|
|
296
|
-
lawRecord.source = dataArg.source;
|
|
297
|
-
lawRecord.identifier = dataArg.identifier;
|
|
298
|
-
lawRecord.title = dataArg.title;
|
|
299
|
-
lawRecord.shortTitle = dataArg.shortTitle ?? '';
|
|
300
|
-
lawRecord.citation = dataArg.citation ?? '';
|
|
301
|
-
lawRecord.type = dataArg.type ?? '';
|
|
302
|
-
lawRecord.language = dataArg.language ?? '';
|
|
303
|
-
lawRecord.sourceUrl = dataArg.sourceUrl;
|
|
304
|
-
lawRecord.rawFormat = dataArg.rawFormat;
|
|
305
|
-
lawRecord.rawBody = dataArg.rawBody;
|
|
306
|
-
lawRecord.text = dataArg.text;
|
|
307
|
-
lawRecord.dateIssued = dataArg.dateIssued ?? '';
|
|
308
|
-
lawRecord.lastModified = dataArg.lastModified ?? '';
|
|
309
|
-
lawRecord.sourceMeta = dataArg.sourceMeta ?? {};
|
|
310
|
-
lawRecord.fetchedAt = new Date();
|
|
311
|
-
lawRecord.syncedAt = new Date();
|
|
312
|
-
|
|
313
|
-
await lawRecord.save();
|
|
314
|
-
return lawRecord;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
private async syncGermanyLaws(requestArg: ILawSyncRequest): Promise<ILawSyncResult> {
|
|
318
|
-
const tocXml = await this.fetchText('https://www.gesetze-im-internet.de/gii-toc.xml');
|
|
319
|
-
const tocEntries = this.parseGermanyToc(tocXml);
|
|
320
|
-
const offset = requestArg.offset ?? 0;
|
|
321
|
-
const targetEntries = typeof requestArg.limit === 'number'
|
|
322
|
-
? tocEntries.slice(offset, offset + requestArg.limit)
|
|
323
|
-
: tocEntries.slice(offset);
|
|
324
|
-
|
|
325
|
-
const identifiers: string[] = [];
|
|
326
|
-
for (const entry of targetEntries) {
|
|
327
|
-
const syncedLaw = await this.syncGermanyLaw(entry);
|
|
328
|
-
identifiers.push(syncedLaw.identifier);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
jurisdiction: 'de',
|
|
333
|
-
syncedCount: identifiers.length,
|
|
334
|
-
identifiers,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
private async syncGermanyLaw(identifierOrEntryArg: string | IGermanyLawTocEntry): Promise<LawRecord> {
|
|
339
|
-
const germanyEntry =
|
|
340
|
-
typeof identifierOrEntryArg === 'string'
|
|
341
|
-
? {
|
|
342
|
-
slug: this.normalizeGermanySlug(identifierOrEntryArg),
|
|
343
|
-
title: '',
|
|
344
|
-
xmlZipUrl: `https://www.gesetze-im-internet.de/${this.normalizeGermanySlug(identifierOrEntryArg)}/xml.zip`,
|
|
345
|
-
}
|
|
346
|
-
: identifierOrEntryArg;
|
|
347
|
-
|
|
348
|
-
const extractedFiles = await plugins.smartarchive.SmartArchive.create()
|
|
349
|
-
.url(germanyEntry.xmlZipUrl)
|
|
350
|
-
.toSmartFiles();
|
|
351
|
-
const xmlFile =
|
|
352
|
-
extractedFiles.find((fileArg: plugins.smartfile.SmartFile) =>
|
|
353
|
-
fileArg.relative.toLowerCase().endsWith('.xml')
|
|
354
|
-
) ?? extractedFiles[0];
|
|
355
|
-
if (!xmlFile) {
|
|
356
|
-
throw new Error(`No XML file found for German law ${germanyEntry.slug}`);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const xmlBody = xmlFile.parseContentAsString('utf8');
|
|
360
|
-
const title = this.extractTagValue(xmlBody, 'langue') || germanyEntry.title || germanyEntry.slug;
|
|
361
|
-
const citation = this.extractTagValue(xmlBody, 'jurabk') || germanyEntry.slug.toUpperCase();
|
|
362
|
-
const dateIssued = this.extractTagValue(xmlBody, 'ausfertigung-datum');
|
|
363
|
-
|
|
364
|
-
return this.upsertLaw({
|
|
365
|
-
jurisdiction: 'de',
|
|
366
|
-
source: 'gesetze-im-internet',
|
|
367
|
-
identifier: germanyEntry.slug,
|
|
368
|
-
title,
|
|
369
|
-
shortTitle: citation,
|
|
370
|
-
citation,
|
|
371
|
-
type: 'law',
|
|
372
|
-
language: 'de',
|
|
373
|
-
sourceUrl: germanyEntry.xmlZipUrl,
|
|
374
|
-
rawFormat: 'xml',
|
|
375
|
-
rawBody: xmlBody,
|
|
376
|
-
text: this.markupToText(xmlBody),
|
|
377
|
-
dateIssued,
|
|
378
|
-
sourceMeta: {
|
|
379
|
-
slug: germanyEntry.slug,
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
private parseGermanyToc(tocXmlArg: string): IGermanyLawTocEntry[] {
|
|
385
|
-
const items: IGermanyLawTocEntry[] = [];
|
|
386
|
-
const itemRegex = /<item>\s*<title>([\s\S]*?)<\/title>\s*<link>([\s\S]*?)<\/link>\s*<\/item>/g;
|
|
387
|
-
for (const match of tocXmlArg.matchAll(itemRegex)) {
|
|
388
|
-
const title = this.decodeHtmlEntities(match[1].trim());
|
|
389
|
-
const xmlZipUrl = match[2].trim().replace('http://', 'https://');
|
|
390
|
-
const slug = this.normalizeGermanySlug(xmlZipUrl);
|
|
391
|
-
items.push({
|
|
392
|
-
title,
|
|
393
|
-
xmlZipUrl,
|
|
394
|
-
slug,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return items;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
private normalizeGermanySlug(identifierArg: string) {
|
|
402
|
-
return identifierArg
|
|
403
|
-
.trim()
|
|
404
|
-
.replace(/^https?:\/\/www\.gesetze-im-internet\.de\//, '')
|
|
405
|
-
.replace(/\/xml\.zip$/i, '')
|
|
406
|
-
.replace(/\/index\.html$/i, '')
|
|
407
|
-
.replace(/^\/+/, '')
|
|
408
|
-
.replace(/\/+$/, '');
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
private async syncEuLaws(requestArg: ILawSyncRequest): Promise<ILawSyncResult> {
|
|
412
|
-
const language = (requestArg.language ?? 'EN').toUpperCase();
|
|
413
|
-
const offset = requestArg.offset ?? 0;
|
|
414
|
-
const identifiers: string[] = [];
|
|
415
|
-
const pageSize = 25;
|
|
416
|
-
let currentOffset = offset;
|
|
417
|
-
let remaining = requestArg.limit;
|
|
418
|
-
|
|
419
|
-
while (remaining === undefined || remaining > 0) {
|
|
420
|
-
const currentPageSize = remaining === undefined ? pageSize : Math.min(pageSize, remaining);
|
|
421
|
-
const metadataPage = await this.fetchEuMetadataPage(currentPageSize, currentOffset, language);
|
|
422
|
-
if (metadataPage.length === 0) {
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
for (const metadata of metadataPage) {
|
|
427
|
-
const syncedLaw = await this.syncEuLaw(metadata.celex, language, metadata);
|
|
428
|
-
identifiers.push(syncedLaw.identifier);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
currentOffset += metadataPage.length;
|
|
432
|
-
if (remaining !== undefined) {
|
|
433
|
-
remaining -= metadataPage.length;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
return {
|
|
438
|
-
jurisdiction: 'eu',
|
|
439
|
-
syncedCount: identifiers.length,
|
|
440
|
-
identifiers,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
private async syncEuLaw(
|
|
445
|
-
celexArg: string,
|
|
446
|
-
languageArg = 'EN',
|
|
447
|
-
metadataArg?: IEuLawMetadata
|
|
448
|
-
): Promise<LawRecord> {
|
|
449
|
-
const language = languageArg.toUpperCase();
|
|
450
|
-
const pageUrl = `https://eur-lex.europa.eu/legal-content/${language}/TXT/?uri=CELEX:${celexArg}`;
|
|
451
|
-
const euPageContent = await this.fetchEuLawPageContent(pageUrl);
|
|
452
|
-
const metadata = metadataArg ?? {
|
|
453
|
-
celex: celexArg,
|
|
454
|
-
title: euPageContent.title,
|
|
455
|
-
type: euPageContent.type,
|
|
456
|
-
dateIssued: euPageContent.dateIssued,
|
|
457
|
-
eli: euPageContent.eli,
|
|
458
|
-
lastModified: '',
|
|
459
|
-
language,
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
return this.upsertLaw({
|
|
463
|
-
jurisdiction: 'eu',
|
|
464
|
-
source: 'eur-lex',
|
|
465
|
-
identifier: metadata.celex,
|
|
466
|
-
title: metadata.title || euPageContent.title || this.firstMeaningfulLine(euPageContent.text) || metadata.celex,
|
|
467
|
-
citation: metadata.eli || euPageContent.eli || metadata.celex,
|
|
468
|
-
type: metadata.type || euPageContent.type,
|
|
469
|
-
language: metadata.language.toLowerCase(),
|
|
470
|
-
sourceUrl: pageUrl,
|
|
471
|
-
rawFormat: 'html',
|
|
472
|
-
rawBody: euPageContent.html,
|
|
473
|
-
text: euPageContent.text,
|
|
474
|
-
dateIssued: metadata.dateIssued || euPageContent.dateIssued,
|
|
475
|
-
lastModified: metadata.lastModified,
|
|
476
|
-
sourceMeta: {
|
|
477
|
-
celex: metadata.celex,
|
|
478
|
-
eli: metadata.eli || euPageContent.eli,
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
private async fetchEuLawPageContent(pageUrlArg: string): Promise<IEuPageContent> {
|
|
484
|
-
const smartBrowser = await this.ensureBrowser();
|
|
485
|
-
const page = await smartBrowser.headlessBrowser.newPage();
|
|
486
|
-
|
|
487
|
-
try {
|
|
488
|
-
await page.goto(pageUrlArg, {
|
|
489
|
-
waitUntil: 'domcontentloaded',
|
|
490
|
-
timeout: 60000,
|
|
491
|
-
});
|
|
492
|
-
await page.waitForSelector('#text', {
|
|
493
|
-
timeout: 60000,
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
const pageContent = await page.evaluate(() => {
|
|
497
|
-
const textNode = document.querySelector('#text') as HTMLElement | null;
|
|
498
|
-
const titleParts = Array.from(document.querySelectorAll('#document1 p.oj-doc-ti'))
|
|
499
|
-
.map((elementArg) => elementArg.textContent?.trim() || '')
|
|
500
|
-
.filter((itemArg) => itemArg.length > 0);
|
|
501
|
-
const dateIssued =
|
|
502
|
-
(document.querySelector('#document1 p.oj-hd-date') as HTMLElement | null)?.innerText?.trim() ||
|
|
503
|
-
'';
|
|
504
|
-
const eli =
|
|
505
|
-
(document.querySelector('a[href*="data.europa.eu/eli"]') as HTMLAnchorElement | null)?.href ||
|
|
506
|
-
'';
|
|
507
|
-
|
|
508
|
-
if (!textNode) {
|
|
509
|
-
return {
|
|
510
|
-
html: document.body.innerHTML,
|
|
511
|
-
text: document.body.innerText,
|
|
512
|
-
title: titleParts.join(' '),
|
|
513
|
-
dateIssued,
|
|
514
|
-
type: titleParts[0] || '',
|
|
515
|
-
eli,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
return {
|
|
520
|
-
html: textNode.innerHTML,
|
|
521
|
-
text: textNode.innerText,
|
|
522
|
-
title: titleParts.join(' '),
|
|
523
|
-
dateIssued,
|
|
524
|
-
type: titleParts[0] || '',
|
|
525
|
-
eli,
|
|
526
|
-
};
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
if (!pageContent.text.trim()) {
|
|
530
|
-
throw new Error(`EUR-Lex returned empty text for ${pageUrlArg}`);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
return pageContent;
|
|
534
|
-
} finally {
|
|
535
|
-
await page.close().catch(() => {});
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
private async fetchEuMetadataPage(
|
|
540
|
-
limitArg: number,
|
|
541
|
-
offsetArg: number,
|
|
542
|
-
languageArg: string
|
|
543
|
-
): Promise<IEuLawMetadata[]> {
|
|
544
|
-
const sparqlQuery = `
|
|
545
|
-
PREFIX cdm: <http://publications.europa.eu/ontology/cdm#>
|
|
546
|
-
PREFIX resource-type: <http://publications.europa.eu/resource/authority/resource-type/>
|
|
547
|
-
PREFIX owl: <http://www.w3.org/2002/07/owl#>
|
|
548
|
-
PREFIX cmr: <http://publications.europa.eu/ontology/cdm/cmr#>
|
|
549
|
-
|
|
550
|
-
SELECT
|
|
551
|
-
?celex
|
|
552
|
-
?title
|
|
553
|
-
?date
|
|
554
|
-
?type_code
|
|
555
|
-
(SAMPLE(?eli_candidate) AS ?eli)
|
|
556
|
-
(MAX(?modified_candidate) AS ?modified_at)
|
|
557
|
-
WHERE {
|
|
558
|
-
VALUES ?type {
|
|
559
|
-
resource-type:REG
|
|
560
|
-
resource-type:DIR
|
|
561
|
-
resource-type:DEC
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
?work a cdm:resource_legal ;
|
|
565
|
-
cdm:resource_legal_id_celex ?celex ;
|
|
566
|
-
cdm:work_title ?title ;
|
|
567
|
-
cdm:work_date_document ?date ;
|
|
568
|
-
cdm:work_has_resource-type ?type .
|
|
569
|
-
|
|
570
|
-
FILTER(LANG(?title) = "${languageArg.toLowerCase()}")
|
|
571
|
-
|
|
572
|
-
BIND(REPLACE(STR(?type), "^.*/resource-type/", "") AS ?type_code)
|
|
573
|
-
|
|
574
|
-
OPTIONAL {
|
|
575
|
-
?work owl:sameAs ?eli_candidate .
|
|
576
|
-
FILTER(CONTAINS(STR(?eli_candidate), "/resource/eli/"))
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
OPTIONAL {
|
|
580
|
-
?work cmr:lastModificationDate ?modified_candidate .
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
GROUP BY ?celex ?title ?date ?type_code
|
|
584
|
-
ORDER BY DESC(?date) DESC(?celex)
|
|
585
|
-
LIMIT ${limitArg}
|
|
586
|
-
OFFSET ${offsetArg}
|
|
587
|
-
`.trim();
|
|
588
|
-
|
|
589
|
-
return this.fetchEuMetadataFromQuery(sparqlQuery, languageArg);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
private async fetchEuMetadataByCelex(
|
|
593
|
-
celexArg: string,
|
|
594
|
-
languageArg: string
|
|
595
|
-
): Promise<IEuLawMetadata> {
|
|
596
|
-
const sparqlQuery = `
|
|
597
|
-
PREFIX cdm: <http://publications.europa.eu/ontology/cdm#>
|
|
598
|
-
PREFIX resource-type: <http://publications.europa.eu/resource/authority/resource-type/>
|
|
599
|
-
PREFIX owl: <http://www.w3.org/2002/07/owl#>
|
|
600
|
-
PREFIX cmr: <http://publications.europa.eu/ontology/cdm/cmr#>
|
|
601
|
-
|
|
602
|
-
SELECT
|
|
603
|
-
?celex
|
|
604
|
-
?title
|
|
605
|
-
?date
|
|
606
|
-
?type_code
|
|
607
|
-
(SAMPLE(?eli_candidate) AS ?eli)
|
|
608
|
-
(MAX(?modified_candidate) AS ?modified_at)
|
|
609
|
-
WHERE {
|
|
610
|
-
VALUES ?celex { "${celexArg}" }
|
|
611
|
-
|
|
612
|
-
?work a cdm:resource_legal ;
|
|
613
|
-
cdm:resource_legal_id_celex ?celex ;
|
|
614
|
-
cdm:work_title ?title ;
|
|
615
|
-
cdm:work_date_document ?date ;
|
|
616
|
-
cdm:work_has_resource-type ?type .
|
|
617
|
-
|
|
618
|
-
FILTER(LANG(?title) = "${languageArg.toLowerCase()}")
|
|
619
|
-
|
|
620
|
-
BIND(REPLACE(STR(?type), "^.*/resource-type/", "") AS ?type_code)
|
|
621
|
-
|
|
622
|
-
OPTIONAL {
|
|
623
|
-
?work owl:sameAs ?eli_candidate .
|
|
624
|
-
FILTER(CONTAINS(STR(?eli_candidate), "/resource/eli/"))
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
OPTIONAL {
|
|
628
|
-
?work cmr:lastModificationDate ?modified_candidate .
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
GROUP BY ?celex ?title ?date ?type_code
|
|
632
|
-
LIMIT 1
|
|
633
|
-
`.trim();
|
|
634
|
-
|
|
635
|
-
const results = await this.fetchEuMetadataFromQuery(sparqlQuery, languageArg);
|
|
636
|
-
if (!results[0]) {
|
|
637
|
-
throw new Error(`No EUR-Lex metadata found for CELEX ${celexArg}`);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return results[0];
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
private async fetchEuMetadataFromQuery(
|
|
644
|
-
queryArg: string,
|
|
645
|
-
languageArg: string
|
|
646
|
-
): Promise<IEuLawMetadata[]> {
|
|
647
|
-
const searchParams = new URLSearchParams({
|
|
648
|
-
query: queryArg,
|
|
649
|
-
format: 'application/sparql-results+json',
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
const response = await fetch(`https://publications.europa.eu/webapi/rdf/sparql?${searchParams}`, {
|
|
653
|
-
headers: {
|
|
654
|
-
'Accept': 'application/sparql-results+json',
|
|
655
|
-
},
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
if (!response.ok) {
|
|
659
|
-
throw new Error(`Failed to query EU metadata: ${response.status} ${response.statusText}`);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const responseData = await response.json() as ISparqlJsonResult;
|
|
663
|
-
const bindings = responseData.results?.bindings ?? [];
|
|
664
|
-
return bindings.map((bindingArg) => ({
|
|
665
|
-
celex: bindingArg.celex?.value ?? '',
|
|
666
|
-
title: bindingArg.title?.value ?? '',
|
|
667
|
-
type: bindingArg.type_code?.value ?? '',
|
|
668
|
-
dateIssued: bindingArg.date?.value ?? '',
|
|
669
|
-
eli: bindingArg.eli?.value ?? '',
|
|
670
|
-
lastModified: bindingArg.modified_at?.value ?? '',
|
|
671
|
-
language: languageArg,
|
|
672
|
-
}));
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
private async syncUsLaws(requestArg: ILawSyncRequest): Promise<ILawSyncResult> {
|
|
676
|
-
const collection = requestArg.usCollection ?? 'PLAW';
|
|
677
|
-
const apiKey = requestArg.govInfoApiKey ?? this.config.govInfoApiKey;
|
|
678
|
-
const pageSize = 25;
|
|
679
|
-
let remaining = requestArg.limit;
|
|
680
|
-
const identifiers: string[] = [];
|
|
681
|
-
let nextPageUrl = this.buildGovInfoCollectionUrl(
|
|
682
|
-
collection,
|
|
683
|
-
requestArg.since ?? new Date('1900-01-01T00:00:00.000Z'),
|
|
684
|
-
pageSize,
|
|
685
|
-
requestArg.offset ? String(requestArg.offset) : '*',
|
|
686
|
-
apiKey
|
|
687
|
-
);
|
|
688
|
-
|
|
689
|
-
while (nextPageUrl && (remaining === undefined || remaining > 0)) {
|
|
690
|
-
const collectionResponse = await this.fetchJson<IGovInfoCollectionResponse>(nextPageUrl);
|
|
691
|
-
const packageEntries = collectionResponse.packages ?? [];
|
|
692
|
-
if (packageEntries.length === 0) {
|
|
693
|
-
break;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const currentBatch = remaining === undefined
|
|
697
|
-
? packageEntries
|
|
698
|
-
: packageEntries.slice(0, remaining);
|
|
699
|
-
|
|
700
|
-
for (const packageEntry of currentBatch) {
|
|
701
|
-
const syncedLaw = await this.syncUsLaw(packageEntry.packageId, collection, apiKey);
|
|
702
|
-
identifiers.push(syncedLaw.identifier);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if (remaining !== undefined) {
|
|
706
|
-
remaining -= currentBatch.length;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
nextPageUrl = collectionResponse.nextPage
|
|
710
|
-
? this.withGovInfoApiKey(collectionResponse.nextPage, apiKey)
|
|
711
|
-
: '';
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
return {
|
|
715
|
-
jurisdiction: 'us',
|
|
716
|
-
syncedCount: identifiers.length,
|
|
717
|
-
identifiers,
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
private async syncUsLaw(
|
|
722
|
-
packageIdArg: string,
|
|
723
|
-
collectionArg?: TUsLawCollection,
|
|
724
|
-
apiKeyArg?: string
|
|
725
|
-
): Promise<LawRecord> {
|
|
726
|
-
const usCodeCitation = this.parseUsCodeCitation(packageIdArg);
|
|
727
|
-
if (usCodeCitation) {
|
|
728
|
-
return this.syncUsCodeCitation(usCodeCitation);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
const packageCollection = collectionArg ?? (packageIdArg.startsWith('USCODE-') ? 'USCODE' : 'PLAW');
|
|
732
|
-
const directContentLaw = await this.trySyncUsLawFromPublicContent(packageIdArg, packageCollection);
|
|
733
|
-
if (directContentLaw) {
|
|
734
|
-
return directContentLaw;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const apiKey = apiKeyArg ?? this.config.govInfoApiKey;
|
|
738
|
-
const summary = await this.fetchJson<IGovInfoSummary>(
|
|
739
|
-
this.withGovInfoApiKey(
|
|
740
|
-
`https://api.govinfo.gov/packages/${encodeURIComponent(packageIdArg)}/summary`,
|
|
741
|
-
apiKey
|
|
742
|
-
)
|
|
743
|
-
);
|
|
744
|
-
|
|
745
|
-
const collection = (packageCollection ?? summary.collectionCode ?? 'PLAW') as TUsLawCollection;
|
|
746
|
-
const rawUrl = collection === 'PLAW'
|
|
747
|
-
? summary.download?.uslmLink ?? summary.download?.txtLink
|
|
748
|
-
: summary.download?.txtLink ?? summary.download?.uslmLink;
|
|
749
|
-
|
|
750
|
-
if (!rawUrl) {
|
|
751
|
-
throw new Error(`No downloadable GovInfo text found for ${packageIdArg}`);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
const rawFormat: TRawLawFormat = rawUrl.includes('/uslm') ? 'xml' : 'text';
|
|
755
|
-
const rawBody = await this.fetchText(this.withGovInfoApiKey(rawUrl, apiKey));
|
|
756
|
-
const citation = collection === 'USCODE'
|
|
757
|
-
? `Title ${summary.titleNumber ?? ''} U.S. Code`.trim()
|
|
758
|
-
: `Public Law ${summary.congress ?? ''}-${summary.documentNumber ?? ''}`.trim();
|
|
759
|
-
const source = collection === 'USCODE' ? 'govinfo-uscode' : 'govinfo-plaw';
|
|
760
|
-
|
|
761
|
-
return this.upsertLaw({
|
|
762
|
-
jurisdiction: 'us',
|
|
763
|
-
source,
|
|
764
|
-
identifier: packageIdArg,
|
|
765
|
-
title: summary.title ?? packageIdArg,
|
|
766
|
-
shortTitle: summary.shortTitle?.[0]?.title ?? '',
|
|
767
|
-
citation,
|
|
768
|
-
type: summary.documentType ?? collection,
|
|
769
|
-
language: 'en',
|
|
770
|
-
sourceUrl: summary.detailsLink ?? rawUrl,
|
|
771
|
-
rawFormat,
|
|
772
|
-
rawBody,
|
|
773
|
-
text: rawFormat === 'xml' ? this.markupToText(rawBody) : this.decodeHtmlEntities(rawBody),
|
|
774
|
-
dateIssued: summary.dateIssued,
|
|
775
|
-
lastModified: summary.lastModified,
|
|
776
|
-
sourceMeta: {
|
|
777
|
-
packageId: packageIdArg,
|
|
778
|
-
collection,
|
|
779
|
-
},
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
private async syncUsCodeCitation(citationArg: IUsCodeCitation): Promise<LawRecord> {
|
|
784
|
-
const rawBody = await this.fetchText(citationArg.sourceUrl);
|
|
785
|
-
const pageTitle = this.extractFirstMatch(
|
|
786
|
-
rawBody,
|
|
787
|
-
/<h1[^>]*id="page_title"[^>]*>([\s\S]*?)<\/h1>/i
|
|
788
|
-
);
|
|
789
|
-
const sectionMarkup =
|
|
790
|
-
this.extractFirstMatch(
|
|
791
|
-
rawBody,
|
|
792
|
-
/<div class="tab-pane active" id="tab_default_1">([\s\S]*?)<\/div>\s*<div class="tab-pane" id="tab_default_2">/i
|
|
793
|
-
) || rawBody;
|
|
794
|
-
const resolvedTitle = this.extractUsLiiHeading(pageTitle) || citationArg.canonicalCitation;
|
|
795
|
-
|
|
796
|
-
return this.upsertLaw({
|
|
797
|
-
jurisdiction: 'us',
|
|
798
|
-
source: 'law-cornell-lii',
|
|
799
|
-
identifier: citationArg.canonicalIdentifier,
|
|
800
|
-
title: resolvedTitle,
|
|
801
|
-
shortTitle: citationArg.canonicalCitation,
|
|
802
|
-
citation: citationArg.canonicalCitation,
|
|
803
|
-
type: 'USCODE_SECTION',
|
|
804
|
-
language: 'en',
|
|
805
|
-
sourceUrl: citationArg.sourceUrl,
|
|
806
|
-
rawFormat: 'html',
|
|
807
|
-
rawBody: sectionMarkup,
|
|
808
|
-
text: this.markupToText(sectionMarkup),
|
|
809
|
-
sourceMeta: {
|
|
810
|
-
titleNumber: citationArg.titleNumber,
|
|
811
|
-
sectionNumber: citationArg.sectionNumber,
|
|
812
|
-
subsectionPath: citationArg.subsectionPath,
|
|
813
|
-
},
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
private async trySyncUsLawFromPublicContent(
|
|
818
|
-
packageIdArg: string,
|
|
819
|
-
collectionArg: TUsLawCollection
|
|
820
|
-
): Promise<LawRecord | null> {
|
|
821
|
-
const rawUrl = collectionArg === 'USCODE'
|
|
822
|
-
? `https://www.govinfo.gov/content/pkg/${packageIdArg}/html/${packageIdArg}.htm`
|
|
823
|
-
: `https://www.govinfo.gov/content/pkg/${packageIdArg}/uslm/${packageIdArg}.xml`;
|
|
824
|
-
|
|
825
|
-
try {
|
|
826
|
-
const rawBody = await this.fetchText(rawUrl);
|
|
827
|
-
const citationValues = this.extractTagValues(rawBody, 'citableAs');
|
|
828
|
-
const title =
|
|
829
|
-
this.extractTagValue(rawBody, 'dc:title')
|
|
830
|
-
.replace(/^Public Law\s+[^:]+:\s*/i, '')
|
|
831
|
-
.trim() || packageIdArg;
|
|
832
|
-
const shortTitle = this.extractTagValue(rawBody, 'shortTitle');
|
|
833
|
-
const dateIssued =
|
|
834
|
-
this.extractTagValue(rawBody, 'approvedDate') || this.extractTagValue(rawBody, 'dc:date');
|
|
835
|
-
const type = this.extractTagValue(rawBody, 'dc:type') || collectionArg;
|
|
836
|
-
|
|
837
|
-
return this.upsertLaw({
|
|
838
|
-
jurisdiction: 'us',
|
|
839
|
-
source: collectionArg === 'USCODE' ? 'govinfo-uscode' : 'govinfo-plaw',
|
|
840
|
-
identifier: packageIdArg,
|
|
841
|
-
title,
|
|
842
|
-
shortTitle,
|
|
843
|
-
citation: citationValues[0] || packageIdArg,
|
|
844
|
-
type,
|
|
845
|
-
language: 'en',
|
|
846
|
-
sourceUrl: rawUrl,
|
|
847
|
-
rawFormat: rawUrl.endsWith('.xml') ? 'xml' : 'html',
|
|
848
|
-
rawBody,
|
|
849
|
-
text: this.markupToText(rawBody),
|
|
850
|
-
dateIssued,
|
|
851
|
-
sourceMeta: {
|
|
852
|
-
packageId: packageIdArg,
|
|
853
|
-
collection: collectionArg,
|
|
854
|
-
},
|
|
855
|
-
});
|
|
856
|
-
} catch {
|
|
857
|
-
return null;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
private parseUsCodeCitation(identifierArg: string): IUsCodeCitation | null {
|
|
862
|
-
const cleanedIdentifier = identifierArg
|
|
863
|
-
.trim()
|
|
864
|
-
.replace(/\u00a0/g, ' ')
|
|
865
|
-
.replace(/\s+/g, ' ')
|
|
866
|
-
.replace(/§+/g, ' ');
|
|
867
|
-
|
|
868
|
-
if (!cleanedIdentifier || /^(PLAW|USCODE)-/i.test(cleanedIdentifier)) {
|
|
869
|
-
return null;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const citationMatch = cleanedIdentifier.match(
|
|
873
|
-
/^(\d+[A-Za-z]*)\s*(?:U\.?\s*S\.?\s*C\.?|USC(?:ODE)?)\s+([0-9A-Za-z.-]+)((?:\([A-Za-z0-9]+\))*)$/i
|
|
874
|
-
);
|
|
875
|
-
if (!citationMatch) {
|
|
876
|
-
return null;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
const titleNumber = citationMatch[1].toUpperCase();
|
|
880
|
-
const sectionNumber = citationMatch[2];
|
|
881
|
-
const subsectionPath = citationMatch[3] ?? '';
|
|
882
|
-
const canonicalCitation = `${titleNumber} USC ${sectionNumber}`;
|
|
883
|
-
|
|
884
|
-
return {
|
|
885
|
-
titleNumber,
|
|
886
|
-
sectionNumber,
|
|
887
|
-
subsectionPath,
|
|
888
|
-
canonicalIdentifier: canonicalCitation,
|
|
889
|
-
canonicalCitation,
|
|
890
|
-
sourceUrl: `https://www.law.cornell.edu/uscode/text/${encodeURIComponent(titleNumber)}/${encodeURIComponent(sectionNumber)}`,
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
private buildGovInfoCollectionUrl(
|
|
895
|
-
collectionArg: TUsLawCollection,
|
|
896
|
-
sinceArg: Date,
|
|
897
|
-
pageSizeArg: number,
|
|
898
|
-
offsetMarkArg: string,
|
|
899
|
-
apiKeyArg: string
|
|
900
|
-
) {
|
|
901
|
-
const sinceIso = sinceArg.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
902
|
-
return this.withGovInfoApiKey(
|
|
903
|
-
`https://api.govinfo.gov/collections/${collectionArg}/${sinceIso}?pageSize=${pageSizeArg}&offsetMark=${encodeURIComponent(offsetMarkArg)}`,
|
|
904
|
-
apiKeyArg
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
private withGovInfoApiKey(urlArg: string, apiKeyArg: string) {
|
|
909
|
-
const url = new URL(urlArg);
|
|
910
|
-
url.searchParams.set('api_key', apiKeyArg);
|
|
911
|
-
return url.toString();
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
private async fetchText(urlArg: string) {
|
|
915
|
-
const response = await fetch(urlArg, {
|
|
916
|
-
headers: {
|
|
917
|
-
'User-Agent': '@fin.cx/opendata laws sync',
|
|
918
|
-
},
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
if (!response.ok) {
|
|
922
|
-
throw new Error(`Failed to fetch ${urlArg}: ${response.status} ${response.statusText}`);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
return response.text();
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
private async fetchJson<TResponse>(urlArg: string): Promise<TResponse> {
|
|
929
|
-
const response = await fetch(urlArg, {
|
|
930
|
-
headers: {
|
|
931
|
-
'User-Agent': '@fin.cx/opendata laws sync',
|
|
932
|
-
'Accept': 'application/json',
|
|
933
|
-
},
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
if (!response.ok) {
|
|
937
|
-
throw new Error(`Failed to fetch ${urlArg}: ${response.status} ${response.statusText}`);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
return response.json() as Promise<TResponse>;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
private extractTagValue(xmlArg: string, tagNameArg: string) {
|
|
944
|
-
const tagRegex = new RegExp(`<${tagNameArg}[^>]*>([\\s\\S]*?)<\/${tagNameArg}>`, 'i');
|
|
945
|
-
const match = xmlArg.match(tagRegex);
|
|
946
|
-
return match ? this.decodeHtmlEntities(match[1].trim()) : '';
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
private extractTagValues(xmlArg: string, tagNameArg: string) {
|
|
950
|
-
const tagRegex = new RegExp(`<${tagNameArg}[^>]*>([\\s\\S]*?)<\/${tagNameArg}>`, 'gi');
|
|
951
|
-
const matches = Array.from(xmlArg.matchAll(tagRegex));
|
|
952
|
-
return matches.map((matchArg) => this.decodeHtmlEntities(matchArg[1].trim()));
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
private extractFirstMatch(textArg: string, regexArg: RegExp) {
|
|
956
|
-
const match = textArg.match(regexArg);
|
|
957
|
-
return match ? this.decodeHtmlEntities(match[1].trim()) : '';
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
private extractUsLiiHeading(pageTitleArg: string) {
|
|
961
|
-
const normalizedTitle = pageTitleArg.replace(/\s+/g, ' ').trim();
|
|
962
|
-
const headingMatch = normalizedTitle.match(/^\d+\s+U\.?S\.? Code\s*§\s*[^-]+-\s*(.+)$/i);
|
|
963
|
-
return headingMatch ? headingMatch[1].trim() : normalizedTitle;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
private markupToText(markupArg: string) {
|
|
967
|
-
return this.decodeHtmlEntities(
|
|
968
|
-
markupArg
|
|
969
|
-
.replace(/<\/?(BR|br)\s*\/?>/g, '\n')
|
|
970
|
-
.replace(/<\/(P|p|div|section|article|li|tr|h1|h2|h3|h4|h5|h6)>/g, '\n')
|
|
971
|
-
.replace(/<[^>]+>/g, ' ')
|
|
972
|
-
.replace(/\r/g, '')
|
|
973
|
-
.replace(/[ \t]+\n/g, '\n')
|
|
974
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
975
|
-
.replace(/[ \t]{2,}/g, ' ')
|
|
976
|
-
.trim()
|
|
977
|
-
);
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
private decodeHtmlEntities(textArg: string) {
|
|
981
|
-
const entityMap: Record<string, string> = {
|
|
982
|
-
'&': '&',
|
|
983
|
-
'<': '<',
|
|
984
|
-
'>': '>',
|
|
985
|
-
'"': '"',
|
|
986
|
-
''': "'",
|
|
987
|
-
' ': ' ',
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
return textArg
|
|
991
|
-
.replace(/&(amp|lt|gt|quot|apos|nbsp);/g, (matchArg) => entityMap[matchArg] ?? matchArg)
|
|
992
|
-
.replace(/&#(\d+);/g, (_matchArg, valueArg: string) =>
|
|
993
|
-
String.fromCodePoint(Number.parseInt(valueArg, 10))
|
|
994
|
-
)
|
|
995
|
-
.replace(/&#x([0-9a-f]+);/gi, (_matchArg, valueArg: string) =>
|
|
996
|
-
String.fromCodePoint(Number.parseInt(valueArg, 16))
|
|
997
|
-
);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
private escapeRegex(valueArg: string) {
|
|
1001
|
-
return valueArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
private firstMeaningfulLine(textArg: string) {
|
|
1005
|
-
return textArg
|
|
1006
|
-
.split('\n')
|
|
1007
|
-
.map((lineArg) => lineArg.trim())
|
|
1008
|
-
.find((lineArg) => lineArg.length > 20) ?? '';
|
|
1009
|
-
}
|
|
1010
|
-
}
|