extract-webpage 1.2.35 → 1.2.37
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/dist/extract-webpage.cjs.js.map +1 -1
- package/dist/extract-webpage.es.js.map +1 -1
- package/package.json +4 -4
- package/src/fs-mock.js +22 -22
- package/src/html-to-cite/extract-date/extract-date.ts +1049 -1049
- package/src/html-to-content/extract-content/extract-selectors-per-domain.json +3452 -3452
- package/src/search/index.ts +45 -45
- package/src/search/meta-search-agent-reexport.ts +38 -38
- package/src/search/url-to-html.ts +278 -278
- package/src/seektopic/seektopic-keyphrases.ts +279 -279
- package/src/suggest-next-words/autocomplete-search-engines.ts +435 -435
- package/src/url-to-content/.fuse_hidden003bd28a0000000d +332 -332
- package/src/url-to-content/url-to-content.ts +367 -367
- package/src/url-to-content/url-to-html.ts +436 -436
- package/src/utils/grab.ts +51 -51
|
@@ -1,1049 +1,1049 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Comprehensive date extraction orchestrator for HTML documents.
|
|
3
|
-
* Integrates pattern searching, JSON-LD parsing, and image metadata analysis.
|
|
4
|
-
*/
|
|
5
|
-
import {
|
|
6
|
-
discard_unwanted,
|
|
7
|
-
extract_url_date,
|
|
8
|
-
idiosyncrasies_search,
|
|
9
|
-
img_search,
|
|
10
|
-
json_search,
|
|
11
|
-
regex_parse,
|
|
12
|
-
pattern_search,
|
|
13
|
-
try_date_expr,
|
|
14
|
-
DATE_EXPRESSIONS,
|
|
15
|
-
FAST_PREPEND,
|
|
16
|
-
SLOW_PREPEND,
|
|
17
|
-
FREE_TEXT_EXPRESSIONS,
|
|
18
|
-
MAX_SEGMENT_LEN,
|
|
19
|
-
MIN_SEGMENT_LEN,
|
|
20
|
-
YEAR_PATTERN,
|
|
21
|
-
YMD_PATTERN,
|
|
22
|
-
COPYRIGHT_PATTERN,
|
|
23
|
-
TIMESTAMP_PATTERN,
|
|
24
|
-
THREE_PATTERN,
|
|
25
|
-
THREE_CATCH,
|
|
26
|
-
THREE_LOOSE_PATTERN,
|
|
27
|
-
THREE_LOOSE_CATCH,
|
|
28
|
-
SELECT_YMD_PATTERN,
|
|
29
|
-
SELECT_YMD_YEAR,
|
|
30
|
-
YMD_YEAR,
|
|
31
|
-
DATESTRINGS_PATTERN,
|
|
32
|
-
DATESTRINGS_CATCH,
|
|
33
|
-
SLASHES_PATTERN,
|
|
34
|
-
SLASHES_YEAR,
|
|
35
|
-
YYYYMM_PATTERN,
|
|
36
|
-
YYYYMM_CATCH,
|
|
37
|
-
MMYYYY_PATTERN,
|
|
38
|
-
MMYYYY_YEAR,
|
|
39
|
-
SIMPLE_PATTERN,
|
|
40
|
-
THREE_COMP_REGEX_A,
|
|
41
|
-
THREE_COMP_REGEX_B,
|
|
42
|
-
TWO_COMP_REGEX,
|
|
43
|
-
} from "./date-extractors";
|
|
44
|
-
|
|
45
|
-
import {
|
|
46
|
-
check_extracted_reference,
|
|
47
|
-
compare_values,
|
|
48
|
-
filter_ymd_candidate,
|
|
49
|
-
get_min_date,
|
|
50
|
-
get_max_date,
|
|
51
|
-
is_valid_date,
|
|
52
|
-
is_valid_format,
|
|
53
|
-
plausible_year_filter,
|
|
54
|
-
convert_date,
|
|
55
|
-
} from "./date-validators";
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Format a date object into the desired output format
|
|
59
|
-
*/
|
|
60
|
-
function format_date(date: Date, format: string): string {
|
|
61
|
-
return convert_date(date, "YYYY-MM-DD", format);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Global debug flag
|
|
65
|
-
let DEBUG = false;
|
|
66
|
-
const LOGGER = {
|
|
67
|
-
debug: (...args) => DEBUG && console.log(...args),
|
|
68
|
-
warn: (...args) => DEBUG && console.warn(...args),
|
|
69
|
-
error: (...args) => console.error(...args),
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
var MAX_POSSIBLE_CANDIDATES = 1000;
|
|
73
|
-
|
|
74
|
-
var CLEANING_LIST = [
|
|
75
|
-
"applet",
|
|
76
|
-
"audio",
|
|
77
|
-
"canvas",
|
|
78
|
-
"datalist",
|
|
79
|
-
"embed",
|
|
80
|
-
"frame",
|
|
81
|
-
"frameset",
|
|
82
|
-
"iframe",
|
|
83
|
-
"label",
|
|
84
|
-
"map",
|
|
85
|
-
"math",
|
|
86
|
-
"noframes",
|
|
87
|
-
"object",
|
|
88
|
-
"picture",
|
|
89
|
-
"rdf",
|
|
90
|
-
"svg",
|
|
91
|
-
"track",
|
|
92
|
-
"video",
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
// Date-related meta tag attributes
|
|
96
|
-
const DATE_ATTRIBUTES = new Set([
|
|
97
|
-
"analyticsattributes.articledate",
|
|
98
|
-
"article.created",
|
|
99
|
-
"article_date_original",
|
|
100
|
-
"article:post_date",
|
|
101
|
-
"article.published",
|
|
102
|
-
"article:published",
|
|
103
|
-
"article:published_date",
|
|
104
|
-
"article:published_time",
|
|
105
|
-
"article:publicationdate",
|
|
106
|
-
"bt:pubdate",
|
|
107
|
-
"citation_date",
|
|
108
|
-
"citation_publication_date",
|
|
109
|
-
"content_create_date",
|
|
110
|
-
"created",
|
|
111
|
-
"cxenseparse:recs:publishtime",
|
|
112
|
-
"date",
|
|
113
|
-
"date_created",
|
|
114
|
-
"date_published",
|
|
115
|
-
"datecreated",
|
|
116
|
-
"dateposted",
|
|
117
|
-
"datepublished",
|
|
118
|
-
// Dublin Core
|
|
119
|
-
"dc.date",
|
|
120
|
-
"dc.created",
|
|
121
|
-
"dc.date.created",
|
|
122
|
-
"dc.date.issued",
|
|
123
|
-
"dc.date.publication",
|
|
124
|
-
"dcsext.articlefirstpublished",
|
|
125
|
-
"dcterms.created",
|
|
126
|
-
"dcterms.date",
|
|
127
|
-
"dcterms.issued",
|
|
128
|
-
"dc:created",
|
|
129
|
-
"dc:date",
|
|
130
|
-
"displaydate",
|
|
131
|
-
"doc_date",
|
|
132
|
-
"field-name-post-date",
|
|
133
|
-
"gentime",
|
|
134
|
-
"mediator_published_time",
|
|
135
|
-
"meta",
|
|
136
|
-
// Open Graph
|
|
137
|
-
"og:article:published",
|
|
138
|
-
"og:article:published_time",
|
|
139
|
-
"og:datepublished",
|
|
140
|
-
"og:pubdate",
|
|
141
|
-
"og:publish_date",
|
|
142
|
-
"og:published_time",
|
|
143
|
-
"og:question:published_time",
|
|
144
|
-
"og:regdate",
|
|
145
|
-
"originalpublicationdate",
|
|
146
|
-
"parsely-pub-date",
|
|
147
|
-
"pdate",
|
|
148
|
-
"ptime",
|
|
149
|
-
"pubdate",
|
|
150
|
-
"publishdate",
|
|
151
|
-
"publish_date",
|
|
152
|
-
"publish_time",
|
|
153
|
-
"publish-date",
|
|
154
|
-
"published-date",
|
|
155
|
-
"published_date",
|
|
156
|
-
"published_time",
|
|
157
|
-
"publisheddate",
|
|
158
|
-
"publication_date",
|
|
159
|
-
"rbpubdate",
|
|
160
|
-
"release_date",
|
|
161
|
-
"rnews:datepublished",
|
|
162
|
-
"sailthru.date",
|
|
163
|
-
"shareaholic:article_published_time",
|
|
164
|
-
"timestamp",
|
|
165
|
-
"twt-published-at",
|
|
166
|
-
"video:release_date",
|
|
167
|
-
"vr:published_time",
|
|
168
|
-
]);
|
|
169
|
-
|
|
170
|
-
// Modified date attributes
|
|
171
|
-
const NAME_MODIFIED = new Set([
|
|
172
|
-
"lastdate",
|
|
173
|
-
"lastmod",
|
|
174
|
-
"lastmodified",
|
|
175
|
-
"last-modified",
|
|
176
|
-
"modified",
|
|
177
|
-
"utime",
|
|
178
|
-
]);
|
|
179
|
-
|
|
180
|
-
// Property modified attributes
|
|
181
|
-
const PROPERTY_MODIFIED = new Set([
|
|
182
|
-
"article:modified",
|
|
183
|
-
"article:modified_date",
|
|
184
|
-
"article:modified_time",
|
|
185
|
-
"article:post_modified",
|
|
186
|
-
"bt:moddate",
|
|
187
|
-
"datemodified",
|
|
188
|
-
"dc.modified",
|
|
189
|
-
"dcterms.modified",
|
|
190
|
-
"lastmodified",
|
|
191
|
-
"modified_time",
|
|
192
|
-
"modificationdate",
|
|
193
|
-
"og:article:modified_time",
|
|
194
|
-
"og:modified_time",
|
|
195
|
-
"og:updated_time",
|
|
196
|
-
"release_date",
|
|
197
|
-
"revision_date",
|
|
198
|
-
"updated_time",
|
|
199
|
-
]);
|
|
200
|
-
|
|
201
|
-
// ItemProp attributes
|
|
202
|
-
const ITEMPROP_ATTRS_ORIGINAL = new Set([
|
|
203
|
-
"datecreated",
|
|
204
|
-
"datepublished",
|
|
205
|
-
"pubyear",
|
|
206
|
-
]);
|
|
207
|
-
|
|
208
|
-
const ITEMPROP_ATTRS_MODIFIED = new Set(["datemodified", "dateupdate"]);
|
|
209
|
-
|
|
210
|
-
// Combine ItemProp sets
|
|
211
|
-
const ITEMPROP_ATTRS = new Set([
|
|
212
|
-
...ITEMPROP_ATTRS_ORIGINAL,
|
|
213
|
-
...ITEMPROP_ATTRS_MODIFIED,
|
|
214
|
-
]);
|
|
215
|
-
|
|
216
|
-
// Class attributes
|
|
217
|
-
const CLASS_ATTRS = new Set(["date-published", "published", "time published"]);
|
|
218
|
-
|
|
219
|
-
// Regular expressions
|
|
220
|
-
const NON_DIGITS_REGEX = /\D+$/;
|
|
221
|
-
|
|
222
|
-
// Note: THREE_PATTERN and THREE_CATCH would need to be defined
|
|
223
|
-
const THREE_COMP_PATTERNS = [
|
|
224
|
-
[THREE_PATTERN, THREE_CATCH],
|
|
225
|
-
[THREE_LOOSE_PATTERN, THREE_LOOSE_CATCH],
|
|
226
|
-
];
|
|
227
|
-
|
|
228
|
-
// Export all constants
|
|
229
|
-
export {
|
|
230
|
-
DATE_ATTRIBUTES,
|
|
231
|
-
NAME_MODIFIED,
|
|
232
|
-
PROPERTY_MODIFIED,
|
|
233
|
-
ITEMPROP_ATTRS_ORIGINAL,
|
|
234
|
-
ITEMPROP_ATTRS_MODIFIED,
|
|
235
|
-
ITEMPROP_ATTRS,
|
|
236
|
-
CLASS_ATTRS,
|
|
237
|
-
NON_DIGITS_REGEX,
|
|
238
|
-
// THREE_COMP_PATTERNS // Uncomment once patterns are defined
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
function examine_text(text, options) {
|
|
242
|
-
if (text.length <= MIN_SEGMENT_LEN) {
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
text = text.slice(0, MAX_SEGMENT_LEN).replace(NON_DIGITS_REGEX, "");
|
|
247
|
-
return try_date_expr(
|
|
248
|
-
text,
|
|
249
|
-
options.format,
|
|
250
|
-
options.extensive,
|
|
251
|
-
options.min,
|
|
252
|
-
options.max
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function examine_date_elements(tree, expression, options) {
|
|
257
|
-
const elements = tree.querySelectorAll(expression);
|
|
258
|
-
if (!elements || elements.length > MAX_POSSIBLE_CANDIDATES) {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
for (const elem of elements) {
|
|
263
|
-
for (const text of [elem.textContent, elem.getAttribute("title") || ""]) {
|
|
264
|
-
const attempt = examine_text(text, options);
|
|
265
|
-
if (attempt) {
|
|
266
|
-
return attempt;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function examine_header(tree, options) {
|
|
275
|
-
let headerdate = null;
|
|
276
|
-
let reserve = null;
|
|
277
|
-
const tryfunc = (content) =>
|
|
278
|
-
try_date_expr(
|
|
279
|
-
content,
|
|
280
|
-
options.format,
|
|
281
|
-
options.extensive,
|
|
282
|
-
options.min,
|
|
283
|
-
options.max
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
for (const elem of tree.querySelectorAll("meta")) {
|
|
287
|
-
if (
|
|
288
|
-
!elem.attributes.length ||
|
|
289
|
-
(!elem.getAttribute("content") && !elem.getAttribute("datetime"))
|
|
290
|
-
) {
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (elem.hasAttribute("name")) {
|
|
295
|
-
const attribute = elem.getAttribute("name")?.toLowerCase();
|
|
296
|
-
if (attribute === "og:url") {
|
|
297
|
-
reserve = extract_url_date(elem.getAttribute("content"), options);
|
|
298
|
-
} else if (DATE_ATTRIBUTES.has(attribute)) {
|
|
299
|
-
LOGGER.debug(`examining meta name: ${elem.outerHTML}`);
|
|
300
|
-
headerdate = tryfunc(elem.getAttribute("content"));
|
|
301
|
-
} else if (NAME_MODIFIED.has(attribute)) {
|
|
302
|
-
LOGGER.debug(`examining meta name: ${elem.outerHTML}`);
|
|
303
|
-
if (!options.original) {
|
|
304
|
-
headerdate = tryfunc(elem.getAttribute("content"));
|
|
305
|
-
} else {
|
|
306
|
-
reserve = tryfunc(elem.getAttribute("content"));
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
} else if (elem.hasAttribute("property")) {
|
|
310
|
-
const attribute = elem.getAttribute("property").toLowerCase();
|
|
311
|
-
if (DATE_ATTRIBUTES.has(attribute) || PROPERTY_MODIFIED.has(attribute)) {
|
|
312
|
-
LOGGER.debug(`examining meta property: ${elem.outerHTML}`);
|
|
313
|
-
const attempt = tryfunc(elem.getAttribute("content"));
|
|
314
|
-
if (attempt !== null) {
|
|
315
|
-
if (
|
|
316
|
-
(DATE_ATTRIBUTES.has(attribute) && options.original) ||
|
|
317
|
-
(PROPERTY_MODIFIED.has(attribute) && !options.original)
|
|
318
|
-
) {
|
|
319
|
-
headerdate = attempt;
|
|
320
|
-
} else {
|
|
321
|
-
reserve = attempt;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
} else if (elem.hasAttribute("itemprop")) {
|
|
326
|
-
const attribute = elem.getAttribute("itemprop").toLowerCase();
|
|
327
|
-
if (ITEMPROP_ATTRS.has(attribute)) {
|
|
328
|
-
LOGGER.debug(`examining meta itemprop: ${elem.outerHTML}`);
|
|
329
|
-
const attempt = tryfunc(
|
|
330
|
-
elem.getAttribute("datetime") || elem.getAttribute("content")
|
|
331
|
-
);
|
|
332
|
-
if (attempt !== null) {
|
|
333
|
-
if (
|
|
334
|
-
(ITEMPROP_ATTRS_ORIGINAL.has(attribute) && options.original) ||
|
|
335
|
-
(ITEMPROP_ATTRS_MODIFIED.has(attribute) && !options.original)
|
|
336
|
-
) {
|
|
337
|
-
headerdate = attempt;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
} else if (attribute === "copyrightyear") {
|
|
341
|
-
LOGGER.debug(`examining meta itemprop: ${elem.outerHTML}`);
|
|
342
|
-
if (elem.hasAttribute("content")) {
|
|
343
|
-
const attempt = `${elem.getAttribute("content")}-01-01`;
|
|
344
|
-
if (is_valid_date(attempt, "%Y-%m-%d", options.min, options.max)) {
|
|
345
|
-
reserve = attempt;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
} else if (elem.hasAttribute("pubdate")) {
|
|
350
|
-
if (elem.getAttribute("pubdate").toLowerCase() === "pubdate") {
|
|
351
|
-
LOGGER.debug(`examining meta pubdate: ${elem.outerHTML}`);
|
|
352
|
-
headerdate = tryfunc(elem.getAttribute("content"));
|
|
353
|
-
}
|
|
354
|
-
} else if (elem.hasAttribute("http-equiv")) {
|
|
355
|
-
const attribute = elem.getAttribute("http-equiv").toLowerCase();
|
|
356
|
-
if (attribute === "date") {
|
|
357
|
-
LOGGER.debug(`examining meta http-equiv: ${elem.outerHTML}`);
|
|
358
|
-
if (options.original) {
|
|
359
|
-
headerdate = tryfunc(elem.getAttribute("content"));
|
|
360
|
-
} else {
|
|
361
|
-
reserve = tryfunc(elem.getAttribute("content"));
|
|
362
|
-
}
|
|
363
|
-
} else if (attribute === "last-modified") {
|
|
364
|
-
LOGGER.debug(`examining meta http-equiv: ${elem.outerHTML}`);
|
|
365
|
-
if (!options.original) {
|
|
366
|
-
headerdate = tryfunc(elem.getAttribute("content"));
|
|
367
|
-
} else {
|
|
368
|
-
reserve = tryfunc(elem.getAttribute("content"));
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (headerdate !== null) {
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (headerdate === null && reserve !== null) {
|
|
379
|
-
LOGGER.debug("opting for reserve date with less granularity");
|
|
380
|
-
headerdate = reserve;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return headerdate;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function select_candidate(occurrences, catch_regex, yearpat, options) {
|
|
387
|
-
if (!occurrences || occurrences.size > MAX_POSSIBLE_CANDIDATES) {
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// console.log( JSON.stringify(occurrences) + " 444444444444" + occurrences + " 444444444444")
|
|
392
|
-
|
|
393
|
-
if (occurrences.size === 1) {
|
|
394
|
-
const pattern = occurrences.keys().next().value;
|
|
395
|
-
return catch_regex.exec(pattern);
|
|
396
|
-
}
|
|
397
|
-
///if null
|
|
398
|
-
// if (occurrences.size === 0) {
|
|
399
|
-
return occurrences;
|
|
400
|
-
//todo fix
|
|
401
|
-
// }
|
|
402
|
-
|
|
403
|
-
const firstselect = Array.from(occurrences.entries())
|
|
404
|
-
.sort((a, b) => b[1] - a[1])
|
|
405
|
-
.slice(0, 10);
|
|
406
|
-
LOGGER.debug(`firstselect: ${JSON.stringify(firstselect)}`);
|
|
407
|
-
|
|
408
|
-
const bestones = firstselect
|
|
409
|
-
.sort((a, b) => (options.original ? a[1] - b[1] : b[1] - a[1]))
|
|
410
|
-
.slice(0, 2);
|
|
411
|
-
LOGGER.debug(`bestones: ${JSON.stringify(bestones)}`);
|
|
412
|
-
|
|
413
|
-
const patterns = bestones.map((item) => item[0]);
|
|
414
|
-
const counts = bestones.map((item) => item[1]);
|
|
415
|
-
|
|
416
|
-
const years = patterns
|
|
417
|
-
.map((pattern) => {
|
|
418
|
-
const year_match = yearpat.exec(pattern);
|
|
419
|
-
return year_match ? year_match[1] : null;
|
|
420
|
-
})
|
|
421
|
-
.filter((year) => year !== null);
|
|
422
|
-
|
|
423
|
-
const validation = years.map((year) =>
|
|
424
|
-
is_valid_date(
|
|
425
|
-
new Date(parseInt(year), 0, 1),
|
|
426
|
-
"%Y",
|
|
427
|
-
options.min,
|
|
428
|
-
options.max
|
|
429
|
-
)
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
let match = null;
|
|
433
|
-
if (validation.every((v) => v)) {
|
|
434
|
-
if (counts[0] === counts[1]) {
|
|
435
|
-
match = catch_regex.exec(patterns[0]);
|
|
436
|
-
} else if (years[1] !== years[0] && counts[1] / counts[0] > 0.5) {
|
|
437
|
-
match = catch_regex.exec(patterns[1]);
|
|
438
|
-
} else {
|
|
439
|
-
match = catch_regex.exec(patterns[0]);
|
|
440
|
-
}
|
|
441
|
-
} else if (validation.some((v) => v)) {
|
|
442
|
-
match = catch_regex.exec(patterns[validation.indexOf(true)]);
|
|
443
|
-
} else {
|
|
444
|
-
LOGGER.debug(`no suitable candidate: ${years[0]} ${years[1]}`);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return match;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function search_pattern(htmlstring, pattern, catch_regex, yearpat, options) {
|
|
451
|
-
const candidates = plausible_year_filter(
|
|
452
|
-
htmlstring,
|
|
453
|
-
pattern,
|
|
454
|
-
yearpat,
|
|
455
|
-
options.min,
|
|
456
|
-
options.max
|
|
457
|
-
);
|
|
458
|
-
return select_candidate(candidates, catch_regex, yearpat, options);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function compare_reference(reference, expression, options) {
|
|
462
|
-
const attempt = try_date_expr(
|
|
463
|
-
expression,
|
|
464
|
-
options.format,
|
|
465
|
-
options.extensive,
|
|
466
|
-
options.min,
|
|
467
|
-
options.max
|
|
468
|
-
);
|
|
469
|
-
if (attempt !== null) {
|
|
470
|
-
return compare_values(reference, attempt, options);
|
|
471
|
-
}
|
|
472
|
-
return reference;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function examine_abbr_elements(tree, options) {
|
|
476
|
-
const elements = tree.querySelectorAll("abbr");
|
|
477
|
-
if (elements.length > 0 && elements.length < MAX_POSSIBLE_CANDIDATES) {
|
|
478
|
-
let reference = 0;
|
|
479
|
-
for (const elem of elements) {
|
|
480
|
-
if (elem.hasAttribute("data-utime")) {
|
|
481
|
-
try {
|
|
482
|
-
const candidate = parseInt(elem.getAttribute("data-utime"));
|
|
483
|
-
LOGGER.debug(`data-utime found: ${candidate}`);
|
|
484
|
-
if (options.original && (reference === 0 || candidate < reference)) {
|
|
485
|
-
reference = candidate;
|
|
486
|
-
} else if (!options.original && candidate > reference) {
|
|
487
|
-
reference = candidate;
|
|
488
|
-
}
|
|
489
|
-
} catch (err) {
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
} else if (CLASS_ATTRS.has(elem.getAttribute("class"))) {
|
|
493
|
-
if (elem.hasAttribute("title")) {
|
|
494
|
-
const trytext = elem.getAttribute("title");
|
|
495
|
-
LOGGER.debug(`abbr published-title found: ${trytext}`);
|
|
496
|
-
if (options.original) {
|
|
497
|
-
const attempt = try_date_expr(
|
|
498
|
-
trytext,
|
|
499
|
-
options.format,
|
|
500
|
-
options.extensive,
|
|
501
|
-
options.min,
|
|
502
|
-
options.max
|
|
503
|
-
);
|
|
504
|
-
if (attempt !== null) {
|
|
505
|
-
return attempt;
|
|
506
|
-
}
|
|
507
|
-
} else {
|
|
508
|
-
reference = compare_reference(reference, trytext, options);
|
|
509
|
-
if (reference > 0) {
|
|
510
|
-
break;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
} else if (elem.textContent && elem.textContent.length > 10) {
|
|
514
|
-
LOGGER.debug(`abbr published found: ${elem.textContent}`);
|
|
515
|
-
reference = compare_reference(reference, elem.textContent, options);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return (
|
|
520
|
-
check_extracted_reference(reference, options) ||
|
|
521
|
-
examine_date_elements(tree, "abbr", options)
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function examine_time_elements(tree, options) {
|
|
528
|
-
const elements = tree.querySelectorAll("time");
|
|
529
|
-
if (elements.length > 0 && elements.length < MAX_POSSIBLE_CANDIDATES) {
|
|
530
|
-
let reference = 0;
|
|
531
|
-
for (const elem of elements) {
|
|
532
|
-
let shortcut_flag = false;
|
|
533
|
-
if (!elem || !elem.getAttribute) continue;
|
|
534
|
-
|
|
535
|
-
const datetime_attr = elem.getAttribute("datetime") || "";
|
|
536
|
-
if (datetime_attr.length > 6) {
|
|
537
|
-
if (
|
|
538
|
-
elem.hasAttribute("pubdate") &&
|
|
539
|
-
elem.getAttribute("pubdate") === "pubdate" &&
|
|
540
|
-
options.original
|
|
541
|
-
) {
|
|
542
|
-
shortcut_flag = true;
|
|
543
|
-
LOGGER.debug(`shortcut for time pubdate found: ${datetime_attr}`);
|
|
544
|
-
} else if (elem.hasAttribute("class")) {
|
|
545
|
-
if (
|
|
546
|
-
options.original &&
|
|
547
|
-
(elem.getAttribute("class").startsWith("entry-date") ||
|
|
548
|
-
elem.getAttribute("class").startsWith("entry-time"))
|
|
549
|
-
) {
|
|
550
|
-
shortcut_flag = true;
|
|
551
|
-
LOGGER.debug(`shortcut for time/datetime found: ${datetime_attr}`);
|
|
552
|
-
} else if (
|
|
553
|
-
!options.original &&
|
|
554
|
-
elem.getAttribute("class") === "updated"
|
|
555
|
-
) {
|
|
556
|
-
shortcut_flag = true;
|
|
557
|
-
LOGGER.debug(
|
|
558
|
-
`shortcut for updated time/datetime found: ${datetime_attr}`
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
} else {
|
|
562
|
-
LOGGER.debug(`time/datetime found: ${datetime_attr}`);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (shortcut_flag) {
|
|
566
|
-
const attempt = try_date_expr(
|
|
567
|
-
datetime_attr,
|
|
568
|
-
options.format,
|
|
569
|
-
options.extensive,
|
|
570
|
-
options.min,
|
|
571
|
-
options.max
|
|
572
|
-
);
|
|
573
|
-
if (attempt !== null) {
|
|
574
|
-
return attempt;
|
|
575
|
-
}
|
|
576
|
-
} else {
|
|
577
|
-
reference = compare_reference(reference, datetime_attr, options);
|
|
578
|
-
}
|
|
579
|
-
} else if (elem.textContent && elem.textContent.length > 6) {
|
|
580
|
-
LOGGER.debug(`time/datetime found in text: ${elem.textContent}`);
|
|
581
|
-
reference = compare_reference(reference, elem.textContent, options);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
return check_extracted_reference(reference, options);
|
|
585
|
-
}
|
|
586
|
-
return null;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function normalize_match(match) {
|
|
590
|
-
if (!match) return null;
|
|
591
|
-
const groups = match.slice(1).filter((g) => g);
|
|
592
|
-
let [day, month, year] = groups.map((g) => g.padStart(2, "0"));
|
|
593
|
-
if (year.length === 2) {
|
|
594
|
-
year = year[0] === "9" ? `19${year}` : `20${year}`;
|
|
595
|
-
}
|
|
596
|
-
return `${year}-${month}-${day}`;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
function search_page(htmlstring, options) {
|
|
600
|
-
LOGGER.debug("looking for copyright/footer information");
|
|
601
|
-
let copyear = 0;
|
|
602
|
-
|
|
603
|
-
let bestmatch = search_pattern(
|
|
604
|
-
htmlstring,
|
|
605
|
-
COPYRIGHT_PATTERN,
|
|
606
|
-
YEAR_PATTERN,
|
|
607
|
-
YEAR_PATTERN,
|
|
608
|
-
options
|
|
609
|
-
);
|
|
610
|
-
if (bestmatch) {
|
|
611
|
-
const year = parseInt(bestmatch[0]);
|
|
612
|
-
if (is_valid_date(new Date(year, 0, 1), "%Y", options.min, options.max)) {
|
|
613
|
-
LOGGER.debug(`copyright year/footer pattern found: ${year}`);
|
|
614
|
-
copyear = year;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
LOGGER.debug("3 components");
|
|
619
|
-
for (const patterns of THREE_COMP_PATTERNS) {
|
|
620
|
-
bestmatch = search_pattern(
|
|
621
|
-
htmlstring,
|
|
622
|
-
patterns[0],
|
|
623
|
-
patterns[1],
|
|
624
|
-
YEAR_PATTERN,
|
|
625
|
-
options
|
|
626
|
-
);
|
|
627
|
-
let result = filter_ymd_candidate(
|
|
628
|
-
bestmatch,
|
|
629
|
-
patterns[0],
|
|
630
|
-
options.original,
|
|
631
|
-
copyear,
|
|
632
|
-
options.format,
|
|
633
|
-
options.min,
|
|
634
|
-
options.max
|
|
635
|
-
);
|
|
636
|
-
if (result !== null) {
|
|
637
|
-
return result;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
let candidates = plausible_year_filter(
|
|
642
|
-
htmlstring,
|
|
643
|
-
SELECT_YMD_PATTERN,
|
|
644
|
-
SELECT_YMD_YEAR,
|
|
645
|
-
options.min,
|
|
646
|
-
options.max
|
|
647
|
-
);
|
|
648
|
-
let replacement = {};
|
|
649
|
-
for (const [item, count] of Object.entries(candidates)) {
|
|
650
|
-
const match = THREE_COMP_REGEX_A.exec(item);
|
|
651
|
-
if (match) {
|
|
652
|
-
const candidate = normalize_match(match);
|
|
653
|
-
replacement[candidate] = count;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
bestmatch = select_candidate(
|
|
658
|
-
new Counter(replacement),
|
|
659
|
-
YMD_PATTERN,
|
|
660
|
-
YMD_YEAR,
|
|
661
|
-
options
|
|
662
|
-
);
|
|
663
|
-
let result = filter_ymd_candidate(
|
|
664
|
-
bestmatch,
|
|
665
|
-
SELECT_YMD_PATTERN,
|
|
666
|
-
options.original,
|
|
667
|
-
copyear,
|
|
668
|
-
options.format,
|
|
669
|
-
options.min,
|
|
670
|
-
options.max
|
|
671
|
-
);
|
|
672
|
-
if (result !== null) {
|
|
673
|
-
return result;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
bestmatch = search_pattern(
|
|
677
|
-
htmlstring,
|
|
678
|
-
DATESTRINGS_PATTERN,
|
|
679
|
-
DATESTRINGS_CATCH,
|
|
680
|
-
YEAR_PATTERN,
|
|
681
|
-
options
|
|
682
|
-
);
|
|
683
|
-
result = filter_ymd_candidate(
|
|
684
|
-
bestmatch,
|
|
685
|
-
DATESTRINGS_PATTERN,
|
|
686
|
-
options.original,
|
|
687
|
-
copyear,
|
|
688
|
-
options.format,
|
|
689
|
-
options.min,
|
|
690
|
-
options.max
|
|
691
|
-
);
|
|
692
|
-
if (result !== null) {
|
|
693
|
-
return result;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
LOGGER.debug("switching to two components");
|
|
697
|
-
|
|
698
|
-
// First option: YYYY-MM pattern
|
|
699
|
-
bestmatch = search_pattern(
|
|
700
|
-
htmlstring,
|
|
701
|
-
YYYYMM_PATTERN,
|
|
702
|
-
YYYYMM_CATCH,
|
|
703
|
-
YEAR_PATTERN,
|
|
704
|
-
options
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
if (bestmatch) {
|
|
708
|
-
const dateObject = new Date(
|
|
709
|
-
parseInt(bestmatch[1]),
|
|
710
|
-
parseInt(bestmatch[2]) - 1,
|
|
711
|
-
1
|
|
712
|
-
);
|
|
713
|
-
if (
|
|
714
|
-
is_valid_date(dateObject, "%Y-%m-%d", options.min, options.max) &&
|
|
715
|
-
(copyear === 0 || dateObject.getFullYear() >= copyear)
|
|
716
|
-
) {
|
|
717
|
-
LOGGER.debug(
|
|
718
|
-
'date found for pattern "%s": %s, %s',
|
|
719
|
-
YYYYMM_PATTERN,
|
|
720
|
-
bestmatch[1],
|
|
721
|
-
bestmatch[2]
|
|
722
|
-
);
|
|
723
|
-
return format_date(dateObject, options.format);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// 2 components, second option: MM-YYYY pattern
|
|
728
|
-
candidates = plausible_year_filter(
|
|
729
|
-
htmlstring,
|
|
730
|
-
MMYYYY_PATTERN,
|
|
731
|
-
MMYYYY_YEAR,
|
|
732
|
-
options.min,
|
|
733
|
-
options.max,
|
|
734
|
-
options.original
|
|
735
|
-
);
|
|
736
|
-
|
|
737
|
-
// Revert DD-MM-YYYY patterns before sorting
|
|
738
|
-
replacement = {};
|
|
739
|
-
for (const [item, count] of Object.entries(candidates)) {
|
|
740
|
-
const match = item?.match(TWO_COMP_REGEX);
|
|
741
|
-
if (!match) continue;
|
|
742
|
-
|
|
743
|
-
let month = match[1];
|
|
744
|
-
if (month.length === 1) {
|
|
745
|
-
month = `0${month}`;
|
|
746
|
-
}
|
|
747
|
-
const candidate = `${match[2]}-${month}-01`;
|
|
748
|
-
replacement[candidate] = count;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
candidates = new Counter(replacement);
|
|
752
|
-
|
|
753
|
-
// Select best candidate
|
|
754
|
-
bestmatch = select_candidate(candidates, YMD_PATTERN, YMD_YEAR, options);
|
|
755
|
-
result = filter_ymd_candidate(
|
|
756
|
-
bestmatch,
|
|
757
|
-
MMYYYY_PATTERN,
|
|
758
|
-
options.original,
|
|
759
|
-
copyear,
|
|
760
|
-
options.format,
|
|
761
|
-
options.min,
|
|
762
|
-
options.max
|
|
763
|
-
);
|
|
764
|
-
|
|
765
|
-
if (result !== null) {
|
|
766
|
-
return result;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
// Try full-blown text regex on all HTML
|
|
770
|
-
const dateObject = regex_parse(htmlstring);
|
|
771
|
-
|
|
772
|
-
// Find all candidates and disambiguate
|
|
773
|
-
if (
|
|
774
|
-
dateObject &&
|
|
775
|
-
is_valid_date(dateObject, options.format, options.min, options.max) &&
|
|
776
|
-
(copyear === 0 || dateObject.getFullYear() >= copyear)
|
|
777
|
-
) {
|
|
778
|
-
try {
|
|
779
|
-
LOGGER.debug("regex result on HTML: %s", dateObject);
|
|
780
|
-
return format_date(dateObject, options.format);
|
|
781
|
-
} catch (err) {
|
|
782
|
-
LOGGER.error("value error during conversion: %s %s", dateObject, err);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Catchall: copyright mention
|
|
787
|
-
if (copyear !== 0) {
|
|
788
|
-
LOGGER.debug("using copyright year as default");
|
|
789
|
-
const dateObject = new Date(copyear, 0, 1); // Month is 0-based in JavaScript
|
|
790
|
-
return format_date(dateObject, options.format);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
LOGGER.debug("switching to one component");
|
|
794
|
-
bestmatch = search_pattern(
|
|
795
|
-
htmlstring,
|
|
796
|
-
SIMPLE_PATTERN,
|
|
797
|
-
YEAR_PATTERN,
|
|
798
|
-
YEAR_PATTERN,
|
|
799
|
-
options
|
|
800
|
-
);
|
|
801
|
-
if (bestmatch) {
|
|
802
|
-
const dateobject = new Date(parseInt(bestmatch[0]), 0, 1);
|
|
803
|
-
if (
|
|
804
|
-
is_valid_date(dateobject, "%Y-%m-%d", options.min, options.max) &&
|
|
805
|
-
dateobject.getFullYear() >= copyear
|
|
806
|
-
) {
|
|
807
|
-
LOGGER.debug(
|
|
808
|
-
`date found for pattern "${SIMPLE_PATTERN}": ${bestmatch[0]}`
|
|
809
|
-
);
|
|
810
|
-
return dateobject.toISOString().slice(0, 10);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
return null;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
class Extractor {
|
|
818
|
-
extensive: any;
|
|
819
|
-
format: any;
|
|
820
|
-
max: any;
|
|
821
|
-
min: any;
|
|
822
|
-
original: any;
|
|
823
|
-
constructor(
|
|
824
|
-
extensive_search,
|
|
825
|
-
max_date,
|
|
826
|
-
min_date,
|
|
827
|
-
original_date,
|
|
828
|
-
outputformat
|
|
829
|
-
) {
|
|
830
|
-
this.extensive = extensive_search;
|
|
831
|
-
this.format = outputformat;
|
|
832
|
-
this.max = max_date;
|
|
833
|
-
this.min = min_date;
|
|
834
|
-
this.original = original_date;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
class Counter extends Map {
|
|
838
|
-
constructor(iterable = null) {
|
|
839
|
-
super();
|
|
840
|
-
if (iterable === null) {
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// Handle string input
|
|
845
|
-
if (typeof iterable === "string") {
|
|
846
|
-
for (const char of iterable) {
|
|
847
|
-
this.set(char, (this.get(char) || 0) + 1);
|
|
848
|
-
}
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// Handle array input
|
|
853
|
-
if (Array.isArray(iterable)) {
|
|
854
|
-
for (const item of iterable) {
|
|
855
|
-
this.set(item, (this.get(item) || 0) + 1);
|
|
856
|
-
}
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// Handle object/mapping input
|
|
861
|
-
if (typeof iterable === "object") {
|
|
862
|
-
for (const [key, value] of Object.entries(iterable)) {
|
|
863
|
-
if (value !== 0) {
|
|
864
|
-
this.set(key, value);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// Counter-specific methods
|
|
872
|
-
|
|
873
|
-
// Return n most common elements and their counts
|
|
874
|
-
most_common(n = null) {
|
|
875
|
-
const items = Array.from(this.entries());
|
|
876
|
-
items.sort((a, b) => b[1] - a[1]);
|
|
877
|
-
return n ? items.slice(0, n) : items;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Return the total of all counts
|
|
881
|
-
total() {
|
|
882
|
-
return Array.from(this.values()).reduce((a, b) => a + b, 0);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// Return an iterator over elements, repeating each as many times as its count
|
|
886
|
-
*elements() {
|
|
887
|
-
for (const [item, count] of this.entries()) {
|
|
888
|
-
for (let i = 0; i < count; i++) {
|
|
889
|
-
yield item;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Update counts from an iterable
|
|
895
|
-
update(iterable) {
|
|
896
|
-
const other =
|
|
897
|
-
iterable instanceof Counter ? iterable : new Counter(iterable);
|
|
898
|
-
for (const [key, value] of other.entries()) {
|
|
899
|
-
this.set(key, (this.get(key) || 0) + value);
|
|
900
|
-
if (this.get(key) === 0) {
|
|
901
|
-
this.delete(key);
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Subtract counts from an iterable
|
|
907
|
-
subtract(iterable) {
|
|
908
|
-
const other =
|
|
909
|
-
iterable instanceof Counter ? iterable : new Counter(iterable);
|
|
910
|
-
for (const [key, value] of other.entries()) {
|
|
911
|
-
this.set(key, (this.get(key) || 0) - value);
|
|
912
|
-
if (this.get(key) === 0) {
|
|
913
|
-
this.delete(key);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Extract date from document using various methods
|
|
921
|
-
*
|
|
922
|
-
* @param {Document} htmlobject - DOM object with article content
|
|
923
|
-
* @param {boolean} [extensive_search=true] - perform extensive search if true
|
|
924
|
-
* @param {boolean} [original_date=false] - return original date if true
|
|
925
|
-
* @param {string} [outputformat="%Y-%m-%d"] - output format
|
|
926
|
-
* @param {string} [url=null] - URL of the page
|
|
927
|
-
* @param {boolean} [verbose=false] - log debug messages if true
|
|
928
|
-
* @param {Date} [min_date=null] - minimum date to consider
|
|
929
|
-
* @param {Date} [max_date=null] - maximum date to consider
|
|
930
|
-
* @param {boolean} [deferred_url_extractor=false] - if true, do not extract date from URL
|
|
931
|
-
* @returns {string|null} Extracted date or null if not found
|
|
932
|
-
* @author [vtempest (2025)](https://github.com/vtempest)
|
|
933
|
-
* Based on [Barbaresi (2020)](https://github.com/adbar/htmldate/)
|
|
934
|
-
*/
|
|
935
|
-
export function extractDate(
|
|
936
|
-
htmlobject,
|
|
937
|
-
extensive_search = true,
|
|
938
|
-
original_date = false,
|
|
939
|
-
outputformat = "%Y-%m-%d",
|
|
940
|
-
url = null,
|
|
941
|
-
verbose = false,
|
|
942
|
-
min_date = null,
|
|
943
|
-
max_date = null,
|
|
944
|
-
deferred_url_extractor = false
|
|
945
|
-
) {
|
|
946
|
-
if (verbose) {
|
|
947
|
-
DEBUG = true;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
const tree = htmlobject;
|
|
951
|
-
|
|
952
|
-
if (!tree) {
|
|
953
|
-
return null;
|
|
954
|
-
}
|
|
955
|
-
if (outputformat !== "%Y-%m-%d" && !is_valid_format(outputformat)) {
|
|
956
|
-
return null;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
const options = new Extractor(
|
|
960
|
-
extensive_search,
|
|
961
|
-
get_max_date(max_date),
|
|
962
|
-
get_min_date(min_date),
|
|
963
|
-
original_date,
|
|
964
|
-
outputformat
|
|
965
|
-
);
|
|
966
|
-
|
|
967
|
-
let url_result = null;
|
|
968
|
-
if (!url) {
|
|
969
|
-
const urlelem = tree.querySelector('link[rel="canonical"]');
|
|
970
|
-
if (urlelem) {
|
|
971
|
-
url = urlelem.getAttribute("href");
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
url_result = extract_url_date(url, options);
|
|
976
|
-
if (url_result && !deferred_url_extractor) {
|
|
977
|
-
return url_result;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
let result = examine_header(tree, options) || json_search(tree, options);
|
|
981
|
-
if (result) {
|
|
982
|
-
return result;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
if (deferred_url_extractor && url_result) {
|
|
986
|
-
return url_result;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
const abbr_result = examine_abbr_elements(tree, options);
|
|
990
|
-
if (abbr_result) {
|
|
991
|
-
return abbr_result;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
let search_tree, discarded;
|
|
995
|
-
try {
|
|
996
|
-
[search_tree, discarded] = discard_unwanted(tree.cloneNode(true));
|
|
997
|
-
} catch (error) {
|
|
998
|
-
search_tree = tree;
|
|
999
|
-
LOGGER.error("DOM cleaner error");
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
const date_expr = extensive_search
|
|
1003
|
-
? SLOW_PREPEND + DATE_EXPRESSIONS
|
|
1004
|
-
: FAST_PREPEND + DATE_EXPRESSIONS;
|
|
1005
|
-
|
|
1006
|
-
result =
|
|
1007
|
-
examine_date_elements(search_tree, date_expr, options) ||
|
|
1008
|
-
examine_date_elements(search_tree, "title,h1", options) ||
|
|
1009
|
-
examine_time_elements(search_tree, options);
|
|
1010
|
-
if (result) {
|
|
1011
|
-
return result;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
const htmlstring = search_tree.innerHTML;
|
|
1015
|
-
// console.log(htmlstring)
|
|
1016
|
-
|
|
1017
|
-
result =
|
|
1018
|
-
pattern_search(htmlstring, TIMESTAMP_PATTERN, options) ||
|
|
1019
|
-
img_search(search_tree, options) ||
|
|
1020
|
-
idiosyncrasies_search(htmlstring, options);
|
|
1021
|
-
if (result) {
|
|
1022
|
-
return result;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (extensive_search) {
|
|
1026
|
-
LOGGER.debug("extensive search started");
|
|
1027
|
-
let reference = 0;
|
|
1028
|
-
|
|
1029
|
-
const textNodes = Array.from(search_tree.childNodes).filter(
|
|
1030
|
-
(node: any) => node.nodeType === 3
|
|
1031
|
-
);
|
|
1032
|
-
|
|
1033
|
-
// Now iterate through them
|
|
1034
|
-
for (let i = 0; i < textNodes.length; i++) {
|
|
1035
|
-
const segment = (textNodes[i] as any).textContent.trim();
|
|
1036
|
-
if (
|
|
1037
|
-
segment.length > MIN_SEGMENT_LEN &&
|
|
1038
|
-
segment.length < MAX_SEGMENT_LEN
|
|
1039
|
-
) {
|
|
1040
|
-
reference = compare_reference(reference, segment, options);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
const converted = check_extracted_reference(reference, options);
|
|
1045
|
-
return converted || search_page(htmlstring, options);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
return null;
|
|
1049
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Comprehensive date extraction orchestrator for HTML documents.
|
|
3
|
+
* Integrates pattern searching, JSON-LD parsing, and image metadata analysis.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
discard_unwanted,
|
|
7
|
+
extract_url_date,
|
|
8
|
+
idiosyncrasies_search,
|
|
9
|
+
img_search,
|
|
10
|
+
json_search,
|
|
11
|
+
regex_parse,
|
|
12
|
+
pattern_search,
|
|
13
|
+
try_date_expr,
|
|
14
|
+
DATE_EXPRESSIONS,
|
|
15
|
+
FAST_PREPEND,
|
|
16
|
+
SLOW_PREPEND,
|
|
17
|
+
FREE_TEXT_EXPRESSIONS,
|
|
18
|
+
MAX_SEGMENT_LEN,
|
|
19
|
+
MIN_SEGMENT_LEN,
|
|
20
|
+
YEAR_PATTERN,
|
|
21
|
+
YMD_PATTERN,
|
|
22
|
+
COPYRIGHT_PATTERN,
|
|
23
|
+
TIMESTAMP_PATTERN,
|
|
24
|
+
THREE_PATTERN,
|
|
25
|
+
THREE_CATCH,
|
|
26
|
+
THREE_LOOSE_PATTERN,
|
|
27
|
+
THREE_LOOSE_CATCH,
|
|
28
|
+
SELECT_YMD_PATTERN,
|
|
29
|
+
SELECT_YMD_YEAR,
|
|
30
|
+
YMD_YEAR,
|
|
31
|
+
DATESTRINGS_PATTERN,
|
|
32
|
+
DATESTRINGS_CATCH,
|
|
33
|
+
SLASHES_PATTERN,
|
|
34
|
+
SLASHES_YEAR,
|
|
35
|
+
YYYYMM_PATTERN,
|
|
36
|
+
YYYYMM_CATCH,
|
|
37
|
+
MMYYYY_PATTERN,
|
|
38
|
+
MMYYYY_YEAR,
|
|
39
|
+
SIMPLE_PATTERN,
|
|
40
|
+
THREE_COMP_REGEX_A,
|
|
41
|
+
THREE_COMP_REGEX_B,
|
|
42
|
+
TWO_COMP_REGEX,
|
|
43
|
+
} from "./date-extractors";
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
check_extracted_reference,
|
|
47
|
+
compare_values,
|
|
48
|
+
filter_ymd_candidate,
|
|
49
|
+
get_min_date,
|
|
50
|
+
get_max_date,
|
|
51
|
+
is_valid_date,
|
|
52
|
+
is_valid_format,
|
|
53
|
+
plausible_year_filter,
|
|
54
|
+
convert_date,
|
|
55
|
+
} from "./date-validators";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Format a date object into the desired output format
|
|
59
|
+
*/
|
|
60
|
+
function format_date(date: Date, format: string): string {
|
|
61
|
+
return convert_date(date, "YYYY-MM-DD", format);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Global debug flag
|
|
65
|
+
let DEBUG = false;
|
|
66
|
+
const LOGGER = {
|
|
67
|
+
debug: (...args) => DEBUG && console.log(...args),
|
|
68
|
+
warn: (...args) => DEBUG && console.warn(...args),
|
|
69
|
+
error: (...args) => console.error(...args),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
var MAX_POSSIBLE_CANDIDATES = 1000;
|
|
73
|
+
|
|
74
|
+
var CLEANING_LIST = [
|
|
75
|
+
"applet",
|
|
76
|
+
"audio",
|
|
77
|
+
"canvas",
|
|
78
|
+
"datalist",
|
|
79
|
+
"embed",
|
|
80
|
+
"frame",
|
|
81
|
+
"frameset",
|
|
82
|
+
"iframe",
|
|
83
|
+
"label",
|
|
84
|
+
"map",
|
|
85
|
+
"math",
|
|
86
|
+
"noframes",
|
|
87
|
+
"object",
|
|
88
|
+
"picture",
|
|
89
|
+
"rdf",
|
|
90
|
+
"svg",
|
|
91
|
+
"track",
|
|
92
|
+
"video",
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Date-related meta tag attributes
|
|
96
|
+
const DATE_ATTRIBUTES = new Set([
|
|
97
|
+
"analyticsattributes.articledate",
|
|
98
|
+
"article.created",
|
|
99
|
+
"article_date_original",
|
|
100
|
+
"article:post_date",
|
|
101
|
+
"article.published",
|
|
102
|
+
"article:published",
|
|
103
|
+
"article:published_date",
|
|
104
|
+
"article:published_time",
|
|
105
|
+
"article:publicationdate",
|
|
106
|
+
"bt:pubdate",
|
|
107
|
+
"citation_date",
|
|
108
|
+
"citation_publication_date",
|
|
109
|
+
"content_create_date",
|
|
110
|
+
"created",
|
|
111
|
+
"cxenseparse:recs:publishtime",
|
|
112
|
+
"date",
|
|
113
|
+
"date_created",
|
|
114
|
+
"date_published",
|
|
115
|
+
"datecreated",
|
|
116
|
+
"dateposted",
|
|
117
|
+
"datepublished",
|
|
118
|
+
// Dublin Core
|
|
119
|
+
"dc.date",
|
|
120
|
+
"dc.created",
|
|
121
|
+
"dc.date.created",
|
|
122
|
+
"dc.date.issued",
|
|
123
|
+
"dc.date.publication",
|
|
124
|
+
"dcsext.articlefirstpublished",
|
|
125
|
+
"dcterms.created",
|
|
126
|
+
"dcterms.date",
|
|
127
|
+
"dcterms.issued",
|
|
128
|
+
"dc:created",
|
|
129
|
+
"dc:date",
|
|
130
|
+
"displaydate",
|
|
131
|
+
"doc_date",
|
|
132
|
+
"field-name-post-date",
|
|
133
|
+
"gentime",
|
|
134
|
+
"mediator_published_time",
|
|
135
|
+
"meta",
|
|
136
|
+
// Open Graph
|
|
137
|
+
"og:article:published",
|
|
138
|
+
"og:article:published_time",
|
|
139
|
+
"og:datepublished",
|
|
140
|
+
"og:pubdate",
|
|
141
|
+
"og:publish_date",
|
|
142
|
+
"og:published_time",
|
|
143
|
+
"og:question:published_time",
|
|
144
|
+
"og:regdate",
|
|
145
|
+
"originalpublicationdate",
|
|
146
|
+
"parsely-pub-date",
|
|
147
|
+
"pdate",
|
|
148
|
+
"ptime",
|
|
149
|
+
"pubdate",
|
|
150
|
+
"publishdate",
|
|
151
|
+
"publish_date",
|
|
152
|
+
"publish_time",
|
|
153
|
+
"publish-date",
|
|
154
|
+
"published-date",
|
|
155
|
+
"published_date",
|
|
156
|
+
"published_time",
|
|
157
|
+
"publisheddate",
|
|
158
|
+
"publication_date",
|
|
159
|
+
"rbpubdate",
|
|
160
|
+
"release_date",
|
|
161
|
+
"rnews:datepublished",
|
|
162
|
+
"sailthru.date",
|
|
163
|
+
"shareaholic:article_published_time",
|
|
164
|
+
"timestamp",
|
|
165
|
+
"twt-published-at",
|
|
166
|
+
"video:release_date",
|
|
167
|
+
"vr:published_time",
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
// Modified date attributes
|
|
171
|
+
const NAME_MODIFIED = new Set([
|
|
172
|
+
"lastdate",
|
|
173
|
+
"lastmod",
|
|
174
|
+
"lastmodified",
|
|
175
|
+
"last-modified",
|
|
176
|
+
"modified",
|
|
177
|
+
"utime",
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
// Property modified attributes
|
|
181
|
+
const PROPERTY_MODIFIED = new Set([
|
|
182
|
+
"article:modified",
|
|
183
|
+
"article:modified_date",
|
|
184
|
+
"article:modified_time",
|
|
185
|
+
"article:post_modified",
|
|
186
|
+
"bt:moddate",
|
|
187
|
+
"datemodified",
|
|
188
|
+
"dc.modified",
|
|
189
|
+
"dcterms.modified",
|
|
190
|
+
"lastmodified",
|
|
191
|
+
"modified_time",
|
|
192
|
+
"modificationdate",
|
|
193
|
+
"og:article:modified_time",
|
|
194
|
+
"og:modified_time",
|
|
195
|
+
"og:updated_time",
|
|
196
|
+
"release_date",
|
|
197
|
+
"revision_date",
|
|
198
|
+
"updated_time",
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
// ItemProp attributes
|
|
202
|
+
const ITEMPROP_ATTRS_ORIGINAL = new Set([
|
|
203
|
+
"datecreated",
|
|
204
|
+
"datepublished",
|
|
205
|
+
"pubyear",
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const ITEMPROP_ATTRS_MODIFIED = new Set(["datemodified", "dateupdate"]);
|
|
209
|
+
|
|
210
|
+
// Combine ItemProp sets
|
|
211
|
+
const ITEMPROP_ATTRS = new Set([
|
|
212
|
+
...ITEMPROP_ATTRS_ORIGINAL,
|
|
213
|
+
...ITEMPROP_ATTRS_MODIFIED,
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
// Class attributes
|
|
217
|
+
const CLASS_ATTRS = new Set(["date-published", "published", "time published"]);
|
|
218
|
+
|
|
219
|
+
// Regular expressions
|
|
220
|
+
const NON_DIGITS_REGEX = /\D+$/;
|
|
221
|
+
|
|
222
|
+
// Note: THREE_PATTERN and THREE_CATCH would need to be defined
|
|
223
|
+
const THREE_COMP_PATTERNS = [
|
|
224
|
+
[THREE_PATTERN, THREE_CATCH],
|
|
225
|
+
[THREE_LOOSE_PATTERN, THREE_LOOSE_CATCH],
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
// Export all constants
|
|
229
|
+
export {
|
|
230
|
+
DATE_ATTRIBUTES,
|
|
231
|
+
NAME_MODIFIED,
|
|
232
|
+
PROPERTY_MODIFIED,
|
|
233
|
+
ITEMPROP_ATTRS_ORIGINAL,
|
|
234
|
+
ITEMPROP_ATTRS_MODIFIED,
|
|
235
|
+
ITEMPROP_ATTRS,
|
|
236
|
+
CLASS_ATTRS,
|
|
237
|
+
NON_DIGITS_REGEX,
|
|
238
|
+
// THREE_COMP_PATTERNS // Uncomment once patterns are defined
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
function examine_text(text, options) {
|
|
242
|
+
if (text.length <= MIN_SEGMENT_LEN) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
text = text.slice(0, MAX_SEGMENT_LEN).replace(NON_DIGITS_REGEX, "");
|
|
247
|
+
return try_date_expr(
|
|
248
|
+
text,
|
|
249
|
+
options.format,
|
|
250
|
+
options.extensive,
|
|
251
|
+
options.min,
|
|
252
|
+
options.max
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function examine_date_elements(tree, expression, options) {
|
|
257
|
+
const elements = tree.querySelectorAll(expression);
|
|
258
|
+
if (!elements || elements.length > MAX_POSSIBLE_CANDIDATES) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
for (const elem of elements) {
|
|
263
|
+
for (const text of [elem.textContent, elem.getAttribute("title") || ""]) {
|
|
264
|
+
const attempt = examine_text(text, options);
|
|
265
|
+
if (attempt) {
|
|
266
|
+
return attempt;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function examine_header(tree, options) {
|
|
275
|
+
let headerdate = null;
|
|
276
|
+
let reserve = null;
|
|
277
|
+
const tryfunc = (content) =>
|
|
278
|
+
try_date_expr(
|
|
279
|
+
content,
|
|
280
|
+
options.format,
|
|
281
|
+
options.extensive,
|
|
282
|
+
options.min,
|
|
283
|
+
options.max
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
for (const elem of tree.querySelectorAll("meta")) {
|
|
287
|
+
if (
|
|
288
|
+
!elem.attributes.length ||
|
|
289
|
+
(!elem.getAttribute("content") && !elem.getAttribute("datetime"))
|
|
290
|
+
) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (elem.hasAttribute("name")) {
|
|
295
|
+
const attribute = elem.getAttribute("name")?.toLowerCase();
|
|
296
|
+
if (attribute === "og:url") {
|
|
297
|
+
reserve = extract_url_date(elem.getAttribute("content"), options);
|
|
298
|
+
} else if (DATE_ATTRIBUTES.has(attribute)) {
|
|
299
|
+
LOGGER.debug(`examining meta name: ${elem.outerHTML}`);
|
|
300
|
+
headerdate = tryfunc(elem.getAttribute("content"));
|
|
301
|
+
} else if (NAME_MODIFIED.has(attribute)) {
|
|
302
|
+
LOGGER.debug(`examining meta name: ${elem.outerHTML}`);
|
|
303
|
+
if (!options.original) {
|
|
304
|
+
headerdate = tryfunc(elem.getAttribute("content"));
|
|
305
|
+
} else {
|
|
306
|
+
reserve = tryfunc(elem.getAttribute("content"));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else if (elem.hasAttribute("property")) {
|
|
310
|
+
const attribute = elem.getAttribute("property").toLowerCase();
|
|
311
|
+
if (DATE_ATTRIBUTES.has(attribute) || PROPERTY_MODIFIED.has(attribute)) {
|
|
312
|
+
LOGGER.debug(`examining meta property: ${elem.outerHTML}`);
|
|
313
|
+
const attempt = tryfunc(elem.getAttribute("content"));
|
|
314
|
+
if (attempt !== null) {
|
|
315
|
+
if (
|
|
316
|
+
(DATE_ATTRIBUTES.has(attribute) && options.original) ||
|
|
317
|
+
(PROPERTY_MODIFIED.has(attribute) && !options.original)
|
|
318
|
+
) {
|
|
319
|
+
headerdate = attempt;
|
|
320
|
+
} else {
|
|
321
|
+
reserve = attempt;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else if (elem.hasAttribute("itemprop")) {
|
|
326
|
+
const attribute = elem.getAttribute("itemprop").toLowerCase();
|
|
327
|
+
if (ITEMPROP_ATTRS.has(attribute)) {
|
|
328
|
+
LOGGER.debug(`examining meta itemprop: ${elem.outerHTML}`);
|
|
329
|
+
const attempt = tryfunc(
|
|
330
|
+
elem.getAttribute("datetime") || elem.getAttribute("content")
|
|
331
|
+
);
|
|
332
|
+
if (attempt !== null) {
|
|
333
|
+
if (
|
|
334
|
+
(ITEMPROP_ATTRS_ORIGINAL.has(attribute) && options.original) ||
|
|
335
|
+
(ITEMPROP_ATTRS_MODIFIED.has(attribute) && !options.original)
|
|
336
|
+
) {
|
|
337
|
+
headerdate = attempt;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} else if (attribute === "copyrightyear") {
|
|
341
|
+
LOGGER.debug(`examining meta itemprop: ${elem.outerHTML}`);
|
|
342
|
+
if (elem.hasAttribute("content")) {
|
|
343
|
+
const attempt = `${elem.getAttribute("content")}-01-01`;
|
|
344
|
+
if (is_valid_date(attempt, "%Y-%m-%d", options.min, options.max)) {
|
|
345
|
+
reserve = attempt;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} else if (elem.hasAttribute("pubdate")) {
|
|
350
|
+
if (elem.getAttribute("pubdate").toLowerCase() === "pubdate") {
|
|
351
|
+
LOGGER.debug(`examining meta pubdate: ${elem.outerHTML}`);
|
|
352
|
+
headerdate = tryfunc(elem.getAttribute("content"));
|
|
353
|
+
}
|
|
354
|
+
} else if (elem.hasAttribute("http-equiv")) {
|
|
355
|
+
const attribute = elem.getAttribute("http-equiv").toLowerCase();
|
|
356
|
+
if (attribute === "date") {
|
|
357
|
+
LOGGER.debug(`examining meta http-equiv: ${elem.outerHTML}`);
|
|
358
|
+
if (options.original) {
|
|
359
|
+
headerdate = tryfunc(elem.getAttribute("content"));
|
|
360
|
+
} else {
|
|
361
|
+
reserve = tryfunc(elem.getAttribute("content"));
|
|
362
|
+
}
|
|
363
|
+
} else if (attribute === "last-modified") {
|
|
364
|
+
LOGGER.debug(`examining meta http-equiv: ${elem.outerHTML}`);
|
|
365
|
+
if (!options.original) {
|
|
366
|
+
headerdate = tryfunc(elem.getAttribute("content"));
|
|
367
|
+
} else {
|
|
368
|
+
reserve = tryfunc(elem.getAttribute("content"));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (headerdate !== null) {
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (headerdate === null && reserve !== null) {
|
|
379
|
+
LOGGER.debug("opting for reserve date with less granularity");
|
|
380
|
+
headerdate = reserve;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return headerdate;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function select_candidate(occurrences, catch_regex, yearpat, options) {
|
|
387
|
+
if (!occurrences || occurrences.size > MAX_POSSIBLE_CANDIDATES) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// console.log( JSON.stringify(occurrences) + " 444444444444" + occurrences + " 444444444444")
|
|
392
|
+
|
|
393
|
+
if (occurrences.size === 1) {
|
|
394
|
+
const pattern = occurrences.keys().next().value;
|
|
395
|
+
return catch_regex.exec(pattern);
|
|
396
|
+
}
|
|
397
|
+
///if null
|
|
398
|
+
// if (occurrences.size === 0) {
|
|
399
|
+
return occurrences;
|
|
400
|
+
//todo fix
|
|
401
|
+
// }
|
|
402
|
+
|
|
403
|
+
const firstselect = Array.from(occurrences.entries())
|
|
404
|
+
.sort((a, b) => b[1] - a[1])
|
|
405
|
+
.slice(0, 10);
|
|
406
|
+
LOGGER.debug(`firstselect: ${JSON.stringify(firstselect)}`);
|
|
407
|
+
|
|
408
|
+
const bestones = firstselect
|
|
409
|
+
.sort((a, b) => (options.original ? a[1] - b[1] : b[1] - a[1]))
|
|
410
|
+
.slice(0, 2);
|
|
411
|
+
LOGGER.debug(`bestones: ${JSON.stringify(bestones)}`);
|
|
412
|
+
|
|
413
|
+
const patterns = bestones.map((item) => item[0]);
|
|
414
|
+
const counts = bestones.map((item) => item[1]);
|
|
415
|
+
|
|
416
|
+
const years = patterns
|
|
417
|
+
.map((pattern) => {
|
|
418
|
+
const year_match = yearpat.exec(pattern);
|
|
419
|
+
return year_match ? year_match[1] : null;
|
|
420
|
+
})
|
|
421
|
+
.filter((year) => year !== null);
|
|
422
|
+
|
|
423
|
+
const validation = years.map((year) =>
|
|
424
|
+
is_valid_date(
|
|
425
|
+
new Date(parseInt(year), 0, 1),
|
|
426
|
+
"%Y",
|
|
427
|
+
options.min,
|
|
428
|
+
options.max
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
let match = null;
|
|
433
|
+
if (validation.every((v) => v)) {
|
|
434
|
+
if (counts[0] === counts[1]) {
|
|
435
|
+
match = catch_regex.exec(patterns[0]);
|
|
436
|
+
} else if (years[1] !== years[0] && counts[1] / counts[0] > 0.5) {
|
|
437
|
+
match = catch_regex.exec(patterns[1]);
|
|
438
|
+
} else {
|
|
439
|
+
match = catch_regex.exec(patterns[0]);
|
|
440
|
+
}
|
|
441
|
+
} else if (validation.some((v) => v)) {
|
|
442
|
+
match = catch_regex.exec(patterns[validation.indexOf(true)]);
|
|
443
|
+
} else {
|
|
444
|
+
LOGGER.debug(`no suitable candidate: ${years[0]} ${years[1]}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return match;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function search_pattern(htmlstring, pattern, catch_regex, yearpat, options) {
|
|
451
|
+
const candidates = plausible_year_filter(
|
|
452
|
+
htmlstring,
|
|
453
|
+
pattern,
|
|
454
|
+
yearpat,
|
|
455
|
+
options.min,
|
|
456
|
+
options.max
|
|
457
|
+
);
|
|
458
|
+
return select_candidate(candidates, catch_regex, yearpat, options);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function compare_reference(reference, expression, options) {
|
|
462
|
+
const attempt = try_date_expr(
|
|
463
|
+
expression,
|
|
464
|
+
options.format,
|
|
465
|
+
options.extensive,
|
|
466
|
+
options.min,
|
|
467
|
+
options.max
|
|
468
|
+
);
|
|
469
|
+
if (attempt !== null) {
|
|
470
|
+
return compare_values(reference, attempt, options);
|
|
471
|
+
}
|
|
472
|
+
return reference;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function examine_abbr_elements(tree, options) {
|
|
476
|
+
const elements = tree.querySelectorAll("abbr");
|
|
477
|
+
if (elements.length > 0 && elements.length < MAX_POSSIBLE_CANDIDATES) {
|
|
478
|
+
let reference = 0;
|
|
479
|
+
for (const elem of elements) {
|
|
480
|
+
if (elem.hasAttribute("data-utime")) {
|
|
481
|
+
try {
|
|
482
|
+
const candidate = parseInt(elem.getAttribute("data-utime"));
|
|
483
|
+
LOGGER.debug(`data-utime found: ${candidate}`);
|
|
484
|
+
if (options.original && (reference === 0 || candidate < reference)) {
|
|
485
|
+
reference = candidate;
|
|
486
|
+
} else if (!options.original && candidate > reference) {
|
|
487
|
+
reference = candidate;
|
|
488
|
+
}
|
|
489
|
+
} catch (err) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
} else if (CLASS_ATTRS.has(elem.getAttribute("class"))) {
|
|
493
|
+
if (elem.hasAttribute("title")) {
|
|
494
|
+
const trytext = elem.getAttribute("title");
|
|
495
|
+
LOGGER.debug(`abbr published-title found: ${trytext}`);
|
|
496
|
+
if (options.original) {
|
|
497
|
+
const attempt = try_date_expr(
|
|
498
|
+
trytext,
|
|
499
|
+
options.format,
|
|
500
|
+
options.extensive,
|
|
501
|
+
options.min,
|
|
502
|
+
options.max
|
|
503
|
+
);
|
|
504
|
+
if (attempt !== null) {
|
|
505
|
+
return attempt;
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
reference = compare_reference(reference, trytext, options);
|
|
509
|
+
if (reference > 0) {
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} else if (elem.textContent && elem.textContent.length > 10) {
|
|
514
|
+
LOGGER.debug(`abbr published found: ${elem.textContent}`);
|
|
515
|
+
reference = compare_reference(reference, elem.textContent, options);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return (
|
|
520
|
+
check_extracted_reference(reference, options) ||
|
|
521
|
+
examine_date_elements(tree, "abbr", options)
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function examine_time_elements(tree, options) {
|
|
528
|
+
const elements = tree.querySelectorAll("time");
|
|
529
|
+
if (elements.length > 0 && elements.length < MAX_POSSIBLE_CANDIDATES) {
|
|
530
|
+
let reference = 0;
|
|
531
|
+
for (const elem of elements) {
|
|
532
|
+
let shortcut_flag = false;
|
|
533
|
+
if (!elem || !elem.getAttribute) continue;
|
|
534
|
+
|
|
535
|
+
const datetime_attr = elem.getAttribute("datetime") || "";
|
|
536
|
+
if (datetime_attr.length > 6) {
|
|
537
|
+
if (
|
|
538
|
+
elem.hasAttribute("pubdate") &&
|
|
539
|
+
elem.getAttribute("pubdate") === "pubdate" &&
|
|
540
|
+
options.original
|
|
541
|
+
) {
|
|
542
|
+
shortcut_flag = true;
|
|
543
|
+
LOGGER.debug(`shortcut for time pubdate found: ${datetime_attr}`);
|
|
544
|
+
} else if (elem.hasAttribute("class")) {
|
|
545
|
+
if (
|
|
546
|
+
options.original &&
|
|
547
|
+
(elem.getAttribute("class").startsWith("entry-date") ||
|
|
548
|
+
elem.getAttribute("class").startsWith("entry-time"))
|
|
549
|
+
) {
|
|
550
|
+
shortcut_flag = true;
|
|
551
|
+
LOGGER.debug(`shortcut for time/datetime found: ${datetime_attr}`);
|
|
552
|
+
} else if (
|
|
553
|
+
!options.original &&
|
|
554
|
+
elem.getAttribute("class") === "updated"
|
|
555
|
+
) {
|
|
556
|
+
shortcut_flag = true;
|
|
557
|
+
LOGGER.debug(
|
|
558
|
+
`shortcut for updated time/datetime found: ${datetime_attr}`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
LOGGER.debug(`time/datetime found: ${datetime_attr}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (shortcut_flag) {
|
|
566
|
+
const attempt = try_date_expr(
|
|
567
|
+
datetime_attr,
|
|
568
|
+
options.format,
|
|
569
|
+
options.extensive,
|
|
570
|
+
options.min,
|
|
571
|
+
options.max
|
|
572
|
+
);
|
|
573
|
+
if (attempt !== null) {
|
|
574
|
+
return attempt;
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
reference = compare_reference(reference, datetime_attr, options);
|
|
578
|
+
}
|
|
579
|
+
} else if (elem.textContent && elem.textContent.length > 6) {
|
|
580
|
+
LOGGER.debug(`time/datetime found in text: ${elem.textContent}`);
|
|
581
|
+
reference = compare_reference(reference, elem.textContent, options);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return check_extracted_reference(reference, options);
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function normalize_match(match) {
|
|
590
|
+
if (!match) return null;
|
|
591
|
+
const groups = match.slice(1).filter((g) => g);
|
|
592
|
+
let [day, month, year] = groups.map((g) => g.padStart(2, "0"));
|
|
593
|
+
if (year.length === 2) {
|
|
594
|
+
year = year[0] === "9" ? `19${year}` : `20${year}`;
|
|
595
|
+
}
|
|
596
|
+
return `${year}-${month}-${day}`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function search_page(htmlstring, options) {
|
|
600
|
+
LOGGER.debug("looking for copyright/footer information");
|
|
601
|
+
let copyear = 0;
|
|
602
|
+
|
|
603
|
+
let bestmatch = search_pattern(
|
|
604
|
+
htmlstring,
|
|
605
|
+
COPYRIGHT_PATTERN,
|
|
606
|
+
YEAR_PATTERN,
|
|
607
|
+
YEAR_PATTERN,
|
|
608
|
+
options
|
|
609
|
+
);
|
|
610
|
+
if (bestmatch) {
|
|
611
|
+
const year = parseInt(bestmatch[0]);
|
|
612
|
+
if (is_valid_date(new Date(year, 0, 1), "%Y", options.min, options.max)) {
|
|
613
|
+
LOGGER.debug(`copyright year/footer pattern found: ${year}`);
|
|
614
|
+
copyear = year;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
LOGGER.debug("3 components");
|
|
619
|
+
for (const patterns of THREE_COMP_PATTERNS) {
|
|
620
|
+
bestmatch = search_pattern(
|
|
621
|
+
htmlstring,
|
|
622
|
+
patterns[0],
|
|
623
|
+
patterns[1],
|
|
624
|
+
YEAR_PATTERN,
|
|
625
|
+
options
|
|
626
|
+
);
|
|
627
|
+
let result = filter_ymd_candidate(
|
|
628
|
+
bestmatch,
|
|
629
|
+
patterns[0],
|
|
630
|
+
options.original,
|
|
631
|
+
copyear,
|
|
632
|
+
options.format,
|
|
633
|
+
options.min,
|
|
634
|
+
options.max
|
|
635
|
+
);
|
|
636
|
+
if (result !== null) {
|
|
637
|
+
return result;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
let candidates = plausible_year_filter(
|
|
642
|
+
htmlstring,
|
|
643
|
+
SELECT_YMD_PATTERN,
|
|
644
|
+
SELECT_YMD_YEAR,
|
|
645
|
+
options.min,
|
|
646
|
+
options.max
|
|
647
|
+
);
|
|
648
|
+
let replacement = {};
|
|
649
|
+
for (const [item, count] of Object.entries(candidates)) {
|
|
650
|
+
const match = THREE_COMP_REGEX_A.exec(item);
|
|
651
|
+
if (match) {
|
|
652
|
+
const candidate = normalize_match(match);
|
|
653
|
+
replacement[candidate] = count;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
bestmatch = select_candidate(
|
|
658
|
+
new Counter(replacement),
|
|
659
|
+
YMD_PATTERN,
|
|
660
|
+
YMD_YEAR,
|
|
661
|
+
options
|
|
662
|
+
);
|
|
663
|
+
let result = filter_ymd_candidate(
|
|
664
|
+
bestmatch,
|
|
665
|
+
SELECT_YMD_PATTERN,
|
|
666
|
+
options.original,
|
|
667
|
+
copyear,
|
|
668
|
+
options.format,
|
|
669
|
+
options.min,
|
|
670
|
+
options.max
|
|
671
|
+
);
|
|
672
|
+
if (result !== null) {
|
|
673
|
+
return result;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
bestmatch = search_pattern(
|
|
677
|
+
htmlstring,
|
|
678
|
+
DATESTRINGS_PATTERN,
|
|
679
|
+
DATESTRINGS_CATCH,
|
|
680
|
+
YEAR_PATTERN,
|
|
681
|
+
options
|
|
682
|
+
);
|
|
683
|
+
result = filter_ymd_candidate(
|
|
684
|
+
bestmatch,
|
|
685
|
+
DATESTRINGS_PATTERN,
|
|
686
|
+
options.original,
|
|
687
|
+
copyear,
|
|
688
|
+
options.format,
|
|
689
|
+
options.min,
|
|
690
|
+
options.max
|
|
691
|
+
);
|
|
692
|
+
if (result !== null) {
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
LOGGER.debug("switching to two components");
|
|
697
|
+
|
|
698
|
+
// First option: YYYY-MM pattern
|
|
699
|
+
bestmatch = search_pattern(
|
|
700
|
+
htmlstring,
|
|
701
|
+
YYYYMM_PATTERN,
|
|
702
|
+
YYYYMM_CATCH,
|
|
703
|
+
YEAR_PATTERN,
|
|
704
|
+
options
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
if (bestmatch) {
|
|
708
|
+
const dateObject = new Date(
|
|
709
|
+
parseInt(bestmatch[1]),
|
|
710
|
+
parseInt(bestmatch[2]) - 1,
|
|
711
|
+
1
|
|
712
|
+
);
|
|
713
|
+
if (
|
|
714
|
+
is_valid_date(dateObject, "%Y-%m-%d", options.min, options.max) &&
|
|
715
|
+
(copyear === 0 || dateObject.getFullYear() >= copyear)
|
|
716
|
+
) {
|
|
717
|
+
LOGGER.debug(
|
|
718
|
+
'date found for pattern "%s": %s, %s',
|
|
719
|
+
YYYYMM_PATTERN,
|
|
720
|
+
bestmatch[1],
|
|
721
|
+
bestmatch[2]
|
|
722
|
+
);
|
|
723
|
+
return format_date(dateObject, options.format);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// 2 components, second option: MM-YYYY pattern
|
|
728
|
+
candidates = plausible_year_filter(
|
|
729
|
+
htmlstring,
|
|
730
|
+
MMYYYY_PATTERN,
|
|
731
|
+
MMYYYY_YEAR,
|
|
732
|
+
options.min,
|
|
733
|
+
options.max,
|
|
734
|
+
options.original
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
// Revert DD-MM-YYYY patterns before sorting
|
|
738
|
+
replacement = {};
|
|
739
|
+
for (const [item, count] of Object.entries(candidates)) {
|
|
740
|
+
const match = item?.match(TWO_COMP_REGEX);
|
|
741
|
+
if (!match) continue;
|
|
742
|
+
|
|
743
|
+
let month = match[1];
|
|
744
|
+
if (month.length === 1) {
|
|
745
|
+
month = `0${month}`;
|
|
746
|
+
}
|
|
747
|
+
const candidate = `${match[2]}-${month}-01`;
|
|
748
|
+
replacement[candidate] = count;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
candidates = new Counter(replacement);
|
|
752
|
+
|
|
753
|
+
// Select best candidate
|
|
754
|
+
bestmatch = select_candidate(candidates, YMD_PATTERN, YMD_YEAR, options);
|
|
755
|
+
result = filter_ymd_candidate(
|
|
756
|
+
bestmatch,
|
|
757
|
+
MMYYYY_PATTERN,
|
|
758
|
+
options.original,
|
|
759
|
+
copyear,
|
|
760
|
+
options.format,
|
|
761
|
+
options.min,
|
|
762
|
+
options.max
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
if (result !== null) {
|
|
766
|
+
return result;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Try full-blown text regex on all HTML
|
|
770
|
+
const dateObject = regex_parse(htmlstring);
|
|
771
|
+
|
|
772
|
+
// Find all candidates and disambiguate
|
|
773
|
+
if (
|
|
774
|
+
dateObject &&
|
|
775
|
+
is_valid_date(dateObject, options.format, options.min, options.max) &&
|
|
776
|
+
(copyear === 0 || dateObject.getFullYear() >= copyear)
|
|
777
|
+
) {
|
|
778
|
+
try {
|
|
779
|
+
LOGGER.debug("regex result on HTML: %s", dateObject);
|
|
780
|
+
return format_date(dateObject, options.format);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
LOGGER.error("value error during conversion: %s %s", dateObject, err);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Catchall: copyright mention
|
|
787
|
+
if (copyear !== 0) {
|
|
788
|
+
LOGGER.debug("using copyright year as default");
|
|
789
|
+
const dateObject = new Date(copyear, 0, 1); // Month is 0-based in JavaScript
|
|
790
|
+
return format_date(dateObject, options.format);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
LOGGER.debug("switching to one component");
|
|
794
|
+
bestmatch = search_pattern(
|
|
795
|
+
htmlstring,
|
|
796
|
+
SIMPLE_PATTERN,
|
|
797
|
+
YEAR_PATTERN,
|
|
798
|
+
YEAR_PATTERN,
|
|
799
|
+
options
|
|
800
|
+
);
|
|
801
|
+
if (bestmatch) {
|
|
802
|
+
const dateobject = new Date(parseInt(bestmatch[0]), 0, 1);
|
|
803
|
+
if (
|
|
804
|
+
is_valid_date(dateobject, "%Y-%m-%d", options.min, options.max) &&
|
|
805
|
+
dateobject.getFullYear() >= copyear
|
|
806
|
+
) {
|
|
807
|
+
LOGGER.debug(
|
|
808
|
+
`date found for pattern "${SIMPLE_PATTERN}": ${bestmatch[0]}`
|
|
809
|
+
);
|
|
810
|
+
return dateobject.toISOString().slice(0, 10);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
class Extractor {
|
|
818
|
+
extensive: any;
|
|
819
|
+
format: any;
|
|
820
|
+
max: any;
|
|
821
|
+
min: any;
|
|
822
|
+
original: any;
|
|
823
|
+
constructor(
|
|
824
|
+
extensive_search,
|
|
825
|
+
max_date,
|
|
826
|
+
min_date,
|
|
827
|
+
original_date,
|
|
828
|
+
outputformat
|
|
829
|
+
) {
|
|
830
|
+
this.extensive = extensive_search;
|
|
831
|
+
this.format = outputformat;
|
|
832
|
+
this.max = max_date;
|
|
833
|
+
this.min = min_date;
|
|
834
|
+
this.original = original_date;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
class Counter extends Map {
|
|
838
|
+
constructor(iterable = null) {
|
|
839
|
+
super();
|
|
840
|
+
if (iterable === null) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Handle string input
|
|
845
|
+
if (typeof iterable === "string") {
|
|
846
|
+
for (const char of iterable) {
|
|
847
|
+
this.set(char, (this.get(char) || 0) + 1);
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Handle array input
|
|
853
|
+
if (Array.isArray(iterable)) {
|
|
854
|
+
for (const item of iterable) {
|
|
855
|
+
this.set(item, (this.get(item) || 0) + 1);
|
|
856
|
+
}
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Handle object/mapping input
|
|
861
|
+
if (typeof iterable === "object") {
|
|
862
|
+
for (const [key, value] of Object.entries(iterable)) {
|
|
863
|
+
if (value !== 0) {
|
|
864
|
+
this.set(key, value);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Counter-specific methods
|
|
872
|
+
|
|
873
|
+
// Return n most common elements and their counts
|
|
874
|
+
most_common(n = null) {
|
|
875
|
+
const items = Array.from(this.entries());
|
|
876
|
+
items.sort((a, b) => b[1] - a[1]);
|
|
877
|
+
return n ? items.slice(0, n) : items;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Return the total of all counts
|
|
881
|
+
total() {
|
|
882
|
+
return Array.from(this.values()).reduce((a, b) => a + b, 0);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Return an iterator over elements, repeating each as many times as its count
|
|
886
|
+
*elements() {
|
|
887
|
+
for (const [item, count] of this.entries()) {
|
|
888
|
+
for (let i = 0; i < count; i++) {
|
|
889
|
+
yield item;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Update counts from an iterable
|
|
895
|
+
update(iterable) {
|
|
896
|
+
const other =
|
|
897
|
+
iterable instanceof Counter ? iterable : new Counter(iterable);
|
|
898
|
+
for (const [key, value] of other.entries()) {
|
|
899
|
+
this.set(key, (this.get(key) || 0) + value);
|
|
900
|
+
if (this.get(key) === 0) {
|
|
901
|
+
this.delete(key);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Subtract counts from an iterable
|
|
907
|
+
subtract(iterable) {
|
|
908
|
+
const other =
|
|
909
|
+
iterable instanceof Counter ? iterable : new Counter(iterable);
|
|
910
|
+
for (const [key, value] of other.entries()) {
|
|
911
|
+
this.set(key, (this.get(key) || 0) - value);
|
|
912
|
+
if (this.get(key) === 0) {
|
|
913
|
+
this.delete(key);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Extract date from document using various methods
|
|
921
|
+
*
|
|
922
|
+
* @param {Document} htmlobject - DOM object with article content
|
|
923
|
+
* @param {boolean} [extensive_search=true] - perform extensive search if true
|
|
924
|
+
* @param {boolean} [original_date=false] - return original date if true
|
|
925
|
+
* @param {string} [outputformat="%Y-%m-%d"] - output format
|
|
926
|
+
* @param {string} [url=null] - URL of the page
|
|
927
|
+
* @param {boolean} [verbose=false] - log debug messages if true
|
|
928
|
+
* @param {Date} [min_date=null] - minimum date to consider
|
|
929
|
+
* @param {Date} [max_date=null] - maximum date to consider
|
|
930
|
+
* @param {boolean} [deferred_url_extractor=false] - if true, do not extract date from URL
|
|
931
|
+
* @returns {string|null} Extracted date or null if not found
|
|
932
|
+
* @author [vtempest (2025)](https://github.com/vtempest)
|
|
933
|
+
* Based on [Barbaresi (2020)](https://github.com/adbar/htmldate/)
|
|
934
|
+
*/
|
|
935
|
+
export function extractDate(
|
|
936
|
+
htmlobject,
|
|
937
|
+
extensive_search = true,
|
|
938
|
+
original_date = false,
|
|
939
|
+
outputformat = "%Y-%m-%d",
|
|
940
|
+
url = null,
|
|
941
|
+
verbose = false,
|
|
942
|
+
min_date = null,
|
|
943
|
+
max_date = null,
|
|
944
|
+
deferred_url_extractor = false
|
|
945
|
+
) {
|
|
946
|
+
if (verbose) {
|
|
947
|
+
DEBUG = true;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const tree = htmlobject;
|
|
951
|
+
|
|
952
|
+
if (!tree) {
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
if (outputformat !== "%Y-%m-%d" && !is_valid_format(outputformat)) {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const options = new Extractor(
|
|
960
|
+
extensive_search,
|
|
961
|
+
get_max_date(max_date),
|
|
962
|
+
get_min_date(min_date),
|
|
963
|
+
original_date,
|
|
964
|
+
outputformat
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
let url_result = null;
|
|
968
|
+
if (!url) {
|
|
969
|
+
const urlelem = tree.querySelector('link[rel="canonical"]');
|
|
970
|
+
if (urlelem) {
|
|
971
|
+
url = urlelem.getAttribute("href");
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
url_result = extract_url_date(url, options);
|
|
976
|
+
if (url_result && !deferred_url_extractor) {
|
|
977
|
+
return url_result;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
let result = examine_header(tree, options) || json_search(tree, options);
|
|
981
|
+
if (result) {
|
|
982
|
+
return result;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (deferred_url_extractor && url_result) {
|
|
986
|
+
return url_result;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const abbr_result = examine_abbr_elements(tree, options);
|
|
990
|
+
if (abbr_result) {
|
|
991
|
+
return abbr_result;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
let search_tree, discarded;
|
|
995
|
+
try {
|
|
996
|
+
[search_tree, discarded] = discard_unwanted(tree.cloneNode(true));
|
|
997
|
+
} catch (error) {
|
|
998
|
+
search_tree = tree;
|
|
999
|
+
LOGGER.error("DOM cleaner error");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const date_expr = extensive_search
|
|
1003
|
+
? SLOW_PREPEND + DATE_EXPRESSIONS
|
|
1004
|
+
: FAST_PREPEND + DATE_EXPRESSIONS;
|
|
1005
|
+
|
|
1006
|
+
result =
|
|
1007
|
+
examine_date_elements(search_tree, date_expr, options) ||
|
|
1008
|
+
examine_date_elements(search_tree, "title,h1", options) ||
|
|
1009
|
+
examine_time_elements(search_tree, options);
|
|
1010
|
+
if (result) {
|
|
1011
|
+
return result;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const htmlstring = search_tree.innerHTML;
|
|
1015
|
+
// console.log(htmlstring)
|
|
1016
|
+
|
|
1017
|
+
result =
|
|
1018
|
+
pattern_search(htmlstring, TIMESTAMP_PATTERN, options) ||
|
|
1019
|
+
img_search(search_tree, options) ||
|
|
1020
|
+
idiosyncrasies_search(htmlstring, options);
|
|
1021
|
+
if (result) {
|
|
1022
|
+
return result;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (extensive_search) {
|
|
1026
|
+
LOGGER.debug("extensive search started");
|
|
1027
|
+
let reference = 0;
|
|
1028
|
+
|
|
1029
|
+
const textNodes = Array.from(search_tree.childNodes).filter(
|
|
1030
|
+
(node: any) => node.nodeType === 3
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
// Now iterate through them
|
|
1034
|
+
for (let i = 0; i < textNodes.length; i++) {
|
|
1035
|
+
const segment = (textNodes[i] as any).textContent.trim();
|
|
1036
|
+
if (
|
|
1037
|
+
segment.length > MIN_SEGMENT_LEN &&
|
|
1038
|
+
segment.length < MAX_SEGMENT_LEN
|
|
1039
|
+
) {
|
|
1040
|
+
reference = compare_reference(reference, segment, options);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const converted = check_extracted_reference(reference, options);
|
|
1045
|
+
return converted || search_page(htmlstring, options);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|