@hraness/wordcell 0.26.3 → 0.27.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/README.md +36 -21
- package/dist/capture.js +15 -7
- package/dist/cli-bin.js +2 -2
- package/dist/cli.js +21 -19
- package/dist/clip/acquire.js +4 -3
- package/dist/clip/bundle-reader.js +3 -2
- package/dist/clip/cli.js +8 -7
- package/dist/clip/defuddle-worker.js +4 -1
- package/dist/clip/doctor.js +5 -4
- package/dist/clip/jobs.js +2 -1
- package/dist/clip/persist.js +2 -1
- package/dist/clip/refresh.js +4 -3
- package/dist/evaluation-builder-bin.js +2 -2
- package/dist/evaluation-builder.js +3 -3
- package/dist/evaluation-kb.js +3 -3
- package/dist/{index-mp5y3ty3.js → index-30v9w4cy.js} +36 -20
- package/dist/{index-9paxge84.js → index-3hts6yy9.js} +1 -1
- package/dist/{index-h9vyd03z.js → index-58azscg5.js} +7 -6
- package/dist/{index-9zkba8hr.js → index-7bqwzjba.js} +7 -6
- package/dist/{index-j4zgmzjr.js → index-8djdwjjd.js} +1 -1
- package/dist/{index-5h5awwh4.js → index-b40e2dkx.js} +2 -2
- package/dist/index-e2wstc7t.js +323 -0
- package/dist/{index-hacpnaew.js → index-fd8j527z.js} +1 -1
- package/dist/{index-mxxxytys.js → index-g63d3937.js} +4 -0
- package/dist/{index-k86wepd8.js → index-h2w7syy2.js} +5 -1
- package/dist/{index-9fywek7g.js → index-ht14xne3.js} +1 -1
- package/dist/{index-82r6g4py.js → index-k9yzc4vn.js} +1 -1
- package/dist/{index-m9p2a4mj.js → index-my50xsa3.js} +221 -36
- package/dist/{index-ncwzmsge.js → index-nwdzhj1h.js} +2 -2
- package/dist/{index-9rf81m0p.js → index-pbz3qw1s.js} +35 -20
- package/dist/index-pn14a2m5.js +59 -0
- package/dist/{index-nd8v7r0z.js → index-sp43012q.js} +1 -1
- package/dist/{index-8pabwzqg.js → index-t4hjm1kk.js} +2 -1
- package/dist/{index-npg9z1a4.js → index-xn5q4kne.js} +5 -1
- package/dist/{index-j2jt49gr.js → index-yatn62qq.js} +1 -1
- package/dist/index.js +14 -4
- package/dist/pdf.js +438 -4
- package/dist/portfolio.js +3 -3
- package/dist/rerank-clef.js +16 -0
- package/dist/rerank-typesafe.js +3 -1
- package/dist/sdk.js +2 -2
- package/dist/{support-9qdr4hb8.js → support-s059eytv.js} +20 -18
- package/dist/url-intelligence.js +4 -3
- package/dist/workflows/decision-context.js +3 -3
- package/dist/workflows/index.js +3 -3
- package/package.json +23 -9
- package/skills/wordcell/SKILL.md +1 -1
- package/skills/wordcell/references/query.md +11 -6
- package/src/capture.ts +5 -0
- package/src/cli-help.ts +2 -1
- package/src/cli-program.ts +23 -8
- package/src/clip/bundle-reader.ts +3 -0
- package/src/clip/capture.ts +1 -0
- package/src/clip/extract.ts +2 -0
- package/src/clip/lib.ts +3 -0
- package/src/clip/metadata-search-tool/Cargo.lock +6 -6
- package/src/clip/metadata.ts +232 -0
- package/src/clip/persist.ts +3 -0
- package/src/index.ts +2 -1
- package/src/pdf/bundle-reader.ts +457 -0
- package/src/pdf/capture.ts +6 -2
- package/src/pdf/cli.ts +2 -1
- package/src/pdf/errors.ts +11 -0
- package/src/pdf/extract.ts +16 -13
- package/src/pdf/layout.ts +136 -3
- package/src/pdf/markdown.ts +7 -1
- package/src/pdf/model.ts +9 -0
- package/src/pdf/persist.ts +8 -2
- package/src/pdf/source.ts +5 -4
- package/src/pdf/tools.ts +8 -6
- package/src/pdf.ts +10 -0
- package/src/rerank-clef.ts +52 -0
- package/src/rerank-credentials.ts +11 -2
- package/src/rerank-typesafe.ts +44 -20
- package/src/rerank.ts +1 -1
- package/src/sdk.ts +4 -2
- /package/dist/{index-cd75vky9.js → index-jbtyp6q5.js} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
acquireArchiveTodaySnapshot,
|
|
4
4
|
discoverArchiveTodaySnapshot
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-fd8j527z.js";
|
|
6
6
|
import {
|
|
7
7
|
canonicalizeUrl,
|
|
8
8
|
chooseBestExtraction,
|
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
localizeAssets,
|
|
13
13
|
scoreExtraction,
|
|
14
14
|
sniffImage
|
|
15
|
-
} from "./index-
|
|
15
|
+
} from "./index-h2w7syy2.js";
|
|
16
16
|
import {
|
|
17
17
|
adapterCapabilities,
|
|
18
18
|
inspectClipEnvironment,
|
|
19
19
|
renderAdapterCapabilities,
|
|
20
20
|
renderDoctorReport
|
|
21
|
-
} from "./index-
|
|
21
|
+
} from "./index-sp43012q.js";
|
|
22
22
|
import {
|
|
23
23
|
cloneBrowserProfile
|
|
24
24
|
} from "./index-hnvx7m1j.js";
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
renderCookiePermissionNotice,
|
|
35
35
|
renderCookiePermissionRecovery,
|
|
36
36
|
setCookiePermissionReporter
|
|
37
|
-
} from "./index-
|
|
37
|
+
} from "./index-nwdzhj1h.js";
|
|
38
38
|
import {
|
|
39
39
|
CONTENT_REWRITE_TRUNCATION_WARNING,
|
|
40
40
|
buildClipMarkdown,
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
stderrStyle,
|
|
52
52
|
terminalOutput,
|
|
53
53
|
writesToTerminal
|
|
54
|
-
} from "./index-
|
|
54
|
+
} from "./index-jbtyp6q5.js";
|
|
55
55
|
import {
|
|
56
56
|
MAX_COOKIE_BYTES,
|
|
57
57
|
filterCookieProviderResult,
|
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
redactSensitiveTextWithCount,
|
|
69
69
|
sanitizeArtifactUrl,
|
|
70
70
|
writeCaptureBundle
|
|
71
|
-
} from "./index-
|
|
71
|
+
} from "./index-g63d3937.js";
|
|
72
72
|
import {
|
|
73
73
|
startNetworkProxy
|
|
74
74
|
} from "./index-w2zc0vwa.js";
|
|
@@ -2285,6 +2285,7 @@ async function runCapture(rawOptions, dependencies = {}) {
|
|
|
2285
2285
|
const finalWarnings = finalizedWarnings(combinedWarnings, redactedMarkdown.count);
|
|
2286
2286
|
const includeSource = options.evidence === "source" || options.evidence === "all";
|
|
2287
2287
|
const manifestInput = {
|
|
2288
|
+
...article.metadata === undefined ? {} : { metadata: article.metadata },
|
|
2288
2289
|
sourceUrl,
|
|
2289
2290
|
canonicalUrl: best.canonicalUrl.href,
|
|
2290
2291
|
capturedAt,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
import {
|
|
3
|
+
UntrustedContentBudgetError,
|
|
4
|
+
createUntrustedToolResult
|
|
5
|
+
} from "./index-4j3tt0c3.js";
|
|
2
6
|
import {
|
|
3
7
|
expandSearchRequest,
|
|
4
8
|
parseSearchRules,
|
|
@@ -8,10 +12,6 @@ import {
|
|
|
8
12
|
openSemanticSearchSession,
|
|
9
13
|
recommendedEmbeddingModel
|
|
10
14
|
} from "./index-fp732bgg.js";
|
|
11
|
-
import {
|
|
12
|
-
UntrustedContentBudgetError,
|
|
13
|
-
createUntrustedToolResult
|
|
14
|
-
} from "./index-4j3tt0c3.js";
|
|
15
15
|
import {
|
|
16
16
|
percolateWithGraph
|
|
17
17
|
} from "./index-wqx1x32f.js";
|
|
@@ -529,8 +529,8 @@ function checkedRerankRequest(value, rerankers) {
|
|
|
529
529
|
throw new TypeError("Search rerank options must be an options object.");
|
|
530
530
|
}
|
|
531
531
|
const options = value;
|
|
532
|
-
if (options.engine !== "typesafe") {
|
|
533
|
-
throw new Error('Search rerank engine must be "typesafe".');
|
|
532
|
+
if (options.engine !== "clef" && options.engine !== "typesafe") {
|
|
533
|
+
throw new Error('Search rerank engine must be "clef" or legacy "typesafe".');
|
|
534
534
|
}
|
|
535
535
|
const reranker = rerankers?.find((entry) => entry.id === options.engine);
|
|
536
536
|
if (reranker === undefined) {
|
|
@@ -856,6 +856,7 @@ async function openKnowledgeBase(options, dependencies = {}) {
|
|
|
856
856
|
const rerankMessage = applied.status === "ready" ? details.length === 0 ? undefined : details.join("; ") : applied.message ?? "Rerank engine did not return a result.";
|
|
857
857
|
diagnostics.push({
|
|
858
858
|
lane: "rerank",
|
|
859
|
+
engine: rerankRequest.reranker.id,
|
|
859
860
|
...Object.keys(resultDetails).length === 0 ? {} : { rerank: resultDetails },
|
|
860
861
|
status: applied.status === "failed" ? "degraded" : applied.status,
|
|
861
862
|
results: applied.status === "ready" ? applied.placements.size : 0,
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
parseArchiveTodayMementoUrl,
|
|
6
6
|
parseMetadataSearchResponse,
|
|
7
7
|
rankMetadataSearchResults
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-fd8j527z.js";
|
|
9
9
|
import {
|
|
10
10
|
sanitizeArtifactUrl
|
|
11
|
-
} from "./index-
|
|
11
|
+
} from "./index-g63d3937.js";
|
|
12
12
|
import {
|
|
13
13
|
assertSafeNetworkUrl,
|
|
14
14
|
isPrivateHostname,
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/clip/metadata.ts
|
|
3
|
+
import { decodeHTML } from "entities";
|
|
4
|
+
var metadataLimits = { htmlBytes: 8 * 1024 * 1024, claims: 256, value: 8192, totalBytes: 256 * 1024, jsonBytes: 128 * 1024, jsonNodes: 2048, jsonDepth: 12 };
|
|
5
|
+
var record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6
|
+
var sources = new Set(["html-title", "html-meta", "html-link", "html-language", "json-ld", "extractor"]);
|
|
7
|
+
var warningKinds = new Set(["input-limit", "claim-limit", "value-limit", "json-limit", "json-invalid"]);
|
|
8
|
+
var credentialName = /(?:csrf|xsrf|token|nonce|password|secret|session|authorization|api[-_]?key|signature)/i;
|
|
9
|
+
var urlFields = new Set(["canonical", "alternate", "image", "url"]);
|
|
10
|
+
var dateFields = new Set(["published", "modified"]);
|
|
11
|
+
function publicUrl(value, base) {
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(value, base);
|
|
14
|
+
if (!/^https?:$/.test(url.protocol))
|
|
15
|
+
return null;
|
|
16
|
+
url.username = "";
|
|
17
|
+
url.password = "";
|
|
18
|
+
url.search = "";
|
|
19
|
+
url.hash = "";
|
|
20
|
+
return url.href;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function cleanText(value) {
|
|
26
|
+
return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu, "").replace(/https?:\/\/[^\s<>"']+/giu, (url) => publicUrl(url) ?? "[invalid URL]").replace(/\s+/gu, " ").trim();
|
|
27
|
+
}
|
|
28
|
+
function dateValue(value) {
|
|
29
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
30
|
+
const zoned = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})$/i.test(value);
|
|
31
|
+
const local = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?$/.test(value);
|
|
32
|
+
const parsed = Date.parse(dateOnly ? value + "T00:00:00Z" : local ? value + "Z" : value);
|
|
33
|
+
const validDay = Number.isFinite(parsed) && new Date(Date.UTC(Number(value.slice(0, 4)), Number(value.slice(5, 7)) - 1, Number(value.slice(8, 10)))).toISOString().slice(0, 10) === value.slice(0, 10);
|
|
34
|
+
if (!dateOnly && !zoned && !local || !validDay)
|
|
35
|
+
return { value, datePrecision: "invalid", timezone: "unknown" };
|
|
36
|
+
if (dateOnly)
|
|
37
|
+
return { value, datePrecision: "date", timezone: "not-applicable" };
|
|
38
|
+
return { value: zoned ? new Date(parsed).toISOString() : value, datePrecision: "datetime", timezone: zoned ? "explicit" : "unknown" };
|
|
39
|
+
}
|
|
40
|
+
function rank(claim) {
|
|
41
|
+
if (claim.datePrecision === "invalid")
|
|
42
|
+
return 1000;
|
|
43
|
+
if (claim.source === "json-ld")
|
|
44
|
+
return claim.rule.includes("jsonld-type=WebPage") ? 15 : 10;
|
|
45
|
+
if (claim.source === "html-link")
|
|
46
|
+
return 5;
|
|
47
|
+
if (claim.locator.includes("property=article:"))
|
|
48
|
+
return 20;
|
|
49
|
+
if (claim.locator.includes("property=og:"))
|
|
50
|
+
return 30;
|
|
51
|
+
if (claim.locator.includes("name=twitter:"))
|
|
52
|
+
return 40;
|
|
53
|
+
if (claim.source === "html-meta")
|
|
54
|
+
return 50;
|
|
55
|
+
if (claim.source === "html-title" || claim.source === "html-language")
|
|
56
|
+
return 60;
|
|
57
|
+
return 90;
|
|
58
|
+
}
|
|
59
|
+
function select(claims) {
|
|
60
|
+
const ranks = new Map;
|
|
61
|
+
for (const claim of claims)
|
|
62
|
+
ranks.set(claim.field, Math.min(ranks.get(claim.field) ?? 1000, rank(claim)));
|
|
63
|
+
const chosen = new Set;
|
|
64
|
+
return claims.map((claim) => {
|
|
65
|
+
const multiple = ["authors", "tags", "alternate"].includes(claim.field);
|
|
66
|
+
const selected = rank(claim) < 1000 && rank(claim) === ranks.get(claim.field) && (multiple || !chosen.has(claim.field));
|
|
67
|
+
if (selected)
|
|
68
|
+
chosen.add(claim.field);
|
|
69
|
+
return { ...claim, selected, rule: `metadata-precedence-v1:${claim.rule}` };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function parseArticleMetadata(input, redact = cleanText) {
|
|
73
|
+
if (!record(input) || input.schemaVersion !== 1 || input.extractorVersion !== "wordcell-metadata-v1" || !Array.isArray(input.claims) || input.claims.length > metadataLimits.claims || !Array.isArray(input.warnings))
|
|
74
|
+
throw new Error("Unsupported or malformed article metadata envelope");
|
|
75
|
+
let bytes = 0;
|
|
76
|
+
const text = (value2, limit = metadataLimits.value) => {
|
|
77
|
+
if (typeof value2 !== "string" || value2.length > limit)
|
|
78
|
+
throw new Error("Invalid article metadata text");
|
|
79
|
+
const result = cleanText(redact(value2));
|
|
80
|
+
bytes += Buffer.byteLength(result);
|
|
81
|
+
if (bytes > metadataLimits.totalBytes)
|
|
82
|
+
throw new Error("Article metadata exceeds byte limit");
|
|
83
|
+
return result;
|
|
84
|
+
};
|
|
85
|
+
const value = (v) => Array.isArray(v) ? v.length <= 32 ? v.map((part) => text(part)) : (() => {
|
|
86
|
+
throw new Error("Article metadata list exceeds limit");
|
|
87
|
+
})() : text(v);
|
|
88
|
+
const claims = input.claims.map((raw) => {
|
|
89
|
+
if (!record(raw) || !sources.has(raw.source) || typeof raw.selected !== "boolean")
|
|
90
|
+
throw new Error("Invalid article metadata claim");
|
|
91
|
+
const field = text(raw.field, 128);
|
|
92
|
+
if (!/^[a-z][a-z0-9:_-]{0,127}$/.test(field) || credentialName.test(field))
|
|
93
|
+
throw new Error("Invalid article metadata field");
|
|
94
|
+
let observed = value(raw.observed), normalized = value(raw.value);
|
|
95
|
+
if (urlFields.has(field)) {
|
|
96
|
+
const urls = (input2) => {
|
|
97
|
+
const normalize = (part) => {
|
|
98
|
+
const parsed = publicUrl(part);
|
|
99
|
+
if (parsed === null)
|
|
100
|
+
throw new Error("Invalid article metadata URL");
|
|
101
|
+
return parsed;
|
|
102
|
+
};
|
|
103
|
+
return typeof input2 === "string" ? normalize(input2) : input2.map(normalize);
|
|
104
|
+
};
|
|
105
|
+
observed = urls(observed);
|
|
106
|
+
normalized = urls(normalized);
|
|
107
|
+
}
|
|
108
|
+
const temporal = raw.datePrecision === undefined ? {} : dateFields.has(field) && typeof observed === "string" ? dateValue(observed) : (() => {
|
|
109
|
+
throw new Error("Invalid article metadata date");
|
|
110
|
+
})();
|
|
111
|
+
if (raw.datePrecision !== undefined && (raw.datePrecision !== temporal.datePrecision || raw.timezone !== temporal.timezone))
|
|
112
|
+
throw new Error("Invalid article metadata date precision");
|
|
113
|
+
return { field, observed, value: normalized, source: raw.source, locator: text(raw.locator, 512), rule: text(raw.rule, 256), selected: raw.selected, ...temporal };
|
|
114
|
+
});
|
|
115
|
+
if (input.warnings.length > warningKinds.size || input.warnings.some((warning) => !warningKinds.has(warning)))
|
|
116
|
+
throw new Error("Invalid article metadata warnings");
|
|
117
|
+
return { schemaVersion: 1, extractorVersion: "wordcell-metadata-v1", claims, warnings: [...input.warnings] };
|
|
118
|
+
}
|
|
119
|
+
var fields = {
|
|
120
|
+
"og:title": "title",
|
|
121
|
+
"twitter:title": "title",
|
|
122
|
+
title: "title",
|
|
123
|
+
citation_title: "title",
|
|
124
|
+
"dc.title": "title",
|
|
125
|
+
description: "description",
|
|
126
|
+
"og:description": "description",
|
|
127
|
+
"twitter:description": "description",
|
|
128
|
+
"dc.description": "description",
|
|
129
|
+
author: "authors",
|
|
130
|
+
"article:author": "authors",
|
|
131
|
+
citation_author: "authors",
|
|
132
|
+
"dc.creator": "authors",
|
|
133
|
+
"article:published_time": "published",
|
|
134
|
+
datepublished: "published",
|
|
135
|
+
citation_publication_date: "published",
|
|
136
|
+
"dc.date": "published",
|
|
137
|
+
"article:modified_time": "modified",
|
|
138
|
+
datemodified: "modified",
|
|
139
|
+
"last-modified": "modified",
|
|
140
|
+
"og:url": "canonical",
|
|
141
|
+
"twitter:url": "canonical",
|
|
142
|
+
"og:image": "image",
|
|
143
|
+
"og:image:url": "image",
|
|
144
|
+
"twitter:image": "image",
|
|
145
|
+
"twitter:image:src": "image",
|
|
146
|
+
"og:site_name": "site",
|
|
147
|
+
publisher: "publisher",
|
|
148
|
+
"dc.publisher": "publisher",
|
|
149
|
+
citation_journal_title: "publisher",
|
|
150
|
+
"og:locale": "language",
|
|
151
|
+
language: "language",
|
|
152
|
+
"dc.language": "language",
|
|
153
|
+
"og:type": "type",
|
|
154
|
+
"twitter:card": "card",
|
|
155
|
+
"article:tag": "tags",
|
|
156
|
+
keywords: "tags",
|
|
157
|
+
news_keywords: "tags",
|
|
158
|
+
citation_keywords: "tags",
|
|
159
|
+
citation_doi: "doi",
|
|
160
|
+
"dc.identifier": "identifier",
|
|
161
|
+
citation_pdf_url: "alternate"
|
|
162
|
+
};
|
|
163
|
+
async function extractArticleMetadata(html, base, fallback = {}) {
|
|
164
|
+
const claims = [];
|
|
165
|
+
const warnings = new Set;
|
|
166
|
+
let totalBytes = 0, title = "", script = "", scriptIndex = 0, activeScript = false, jsonBytes = 0;
|
|
167
|
+
const add = (field, raw, source, locator, decode = false, evidenceRule) => {
|
|
168
|
+
if (claims.length >= metadataLimits.claims) {
|
|
169
|
+
warnings.add("claim-limit");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
let values = Array.isArray(raw) ? raw : [raw];
|
|
173
|
+
if (values.length > 32) {
|
|
174
|
+
warnings.add("value-limit");
|
|
175
|
+
values = values.slice(0, 32);
|
|
176
|
+
}
|
|
177
|
+
const strings = [];
|
|
178
|
+
for (const value2 of values) {
|
|
179
|
+
if (typeof value2 !== "string")
|
|
180
|
+
continue;
|
|
181
|
+
if (value2.length > metadataLimits.value) {
|
|
182
|
+
warnings.add("value-limit");
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const decoded = decode ? decodeHTML(value2) : value2;
|
|
186
|
+
const cleaned = cleanText(urlFields.has(field) ? publicUrl(decoded, base) ?? "" : decoded);
|
|
187
|
+
if (cleaned)
|
|
188
|
+
strings.push(cleaned);
|
|
189
|
+
}
|
|
190
|
+
if (!strings.length)
|
|
191
|
+
return;
|
|
192
|
+
const observed = Array.isArray(raw) ? strings : strings[0];
|
|
193
|
+
let value = observed;
|
|
194
|
+
if (urlFields.has(field)) {
|
|
195
|
+
const urls = strings.map((part) => publicUrl(part, base)).filter((part) => part !== null);
|
|
196
|
+
if (!urls.length)
|
|
197
|
+
return;
|
|
198
|
+
value = Array.isArray(raw) ? urls : urls[0];
|
|
199
|
+
}
|
|
200
|
+
const temporal = dateFields.has(field) && typeof observed === "string" ? dateValue(observed) : {};
|
|
201
|
+
const claim = { field, observed, value, source, locator, rule: `trim-whitespace-url-query-redaction${evidenceRule === undefined ? "" : `:${evidenceRule}`}`, selected: false, ...temporal };
|
|
202
|
+
const bytes = Buffer.byteLength(JSON.stringify(claim));
|
|
203
|
+
if (totalBytes + bytes > metadataLimits.totalBytes / 2) {
|
|
204
|
+
warnings.add("claim-limit");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
totalBytes += bytes;
|
|
208
|
+
claims.push(claim);
|
|
209
|
+
};
|
|
210
|
+
const parseLd = (raw, index) => {
|
|
211
|
+
if (!raw.trim())
|
|
212
|
+
return;
|
|
213
|
+
if (Buffer.byteLength(raw) > metadataLimits.jsonBytes || (jsonBytes += Buffer.byteLength(raw)) > metadataLimits.jsonBytes * 4) {
|
|
214
|
+
warnings.add("json-limit");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let parsed;
|
|
218
|
+
try {
|
|
219
|
+
parsed = JSON.parse(raw);
|
|
220
|
+
} catch {
|
|
221
|
+
warnings.add("json-invalid");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let nodes = 0;
|
|
225
|
+
const stack = [{ value: parsed, depth: 0 }];
|
|
226
|
+
while (stack.length) {
|
|
227
|
+
const node = stack.pop();
|
|
228
|
+
if (++nodes > metadataLimits.jsonNodes || node.depth > metadataLimits.jsonDepth) {
|
|
229
|
+
warnings.add("json-limit");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const children = Array.isArray(node.value) ? node.value : record(node.value) ? Object.values(node.value) : [];
|
|
233
|
+
if (children.length + nodes + stack.length > metadataLimits.jsonNodes) {
|
|
234
|
+
warnings.add("json-limit");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
for (const child of children)
|
|
238
|
+
stack.push({ value: child, depth: node.depth + 1 });
|
|
239
|
+
}
|
|
240
|
+
const items = Array.isArray(parsed) ? parsed.map((value, i) => ({ value, path: `$[${i}]` })) : [{ value: parsed, path: "$" }];
|
|
241
|
+
for (let cursor = 0;cursor < items.length && cursor < metadataLimits.jsonNodes; cursor++) {
|
|
242
|
+
const item = items[cursor];
|
|
243
|
+
if (!record(item.value))
|
|
244
|
+
continue;
|
|
245
|
+
const object = item.value;
|
|
246
|
+
if (Array.isArray(object["@graph"]))
|
|
247
|
+
items.push(...object["@graph"].map((value, i) => ({ value, path: `${item.path}.@graph[${i}]` })));
|
|
248
|
+
const types = Array.isArray(object["@type"]) ? object["@type"] : [object["@type"]];
|
|
249
|
+
const entityTypes = types.flatMap((type) => {
|
|
250
|
+
const match = typeof type === "string" ? /^(?:(?:https?:\/\/)?schema\.org\/)?(Article|NewsArticle|BlogPosting|ScholarlyArticle|TechArticle|Report|WebPage|VideoObject|AudioObject|PodcastEpisode)$/.exec(type) : null;
|
|
251
|
+
return match?.[1] === undefined ? [] : [match[1]];
|
|
252
|
+
});
|
|
253
|
+
const entityType = entityTypes.find((type) => type !== "WebPage") ?? entityTypes[0];
|
|
254
|
+
if (entityType === undefined)
|
|
255
|
+
continue;
|
|
256
|
+
const names = (v) => Array.isArray(v) ? v.slice(0, 32).map((entry) => record(entry) ? entry.name : entry) : record(v) ? v.name : v;
|
|
257
|
+
const props = { headline: "title", name: "title", description: "description", author: "authors", creator: "authors", datePublished: "published", dateModified: "modified", inLanguage: "language", publisher: "publisher", keywords: "tags", image: "image", thumbnailUrl: "image", url: "canonical", mainEntityOfPage: "canonical", duration: "duration", episodeNumber: "episode", identifier: "identifier" };
|
|
258
|
+
for (const [key, field] of Object.entries(props)) {
|
|
259
|
+
let value = object[key];
|
|
260
|
+
if (value === undefined)
|
|
261
|
+
continue;
|
|
262
|
+
if (["author", "creator", "publisher"].includes(key))
|
|
263
|
+
value = names(value);
|
|
264
|
+
if (key === "image")
|
|
265
|
+
value = Array.isArray(value) ? value.map((v) => record(v) ? v.url ?? v.contentUrl : v) : record(value) ? value.url ?? value.contentUrl : value;
|
|
266
|
+
if (key === "mainEntityOfPage" && record(value))
|
|
267
|
+
value = value["@id"] ?? value.url;
|
|
268
|
+
if (key === "episodeNumber" && typeof value === "number" && Number.isFinite(value))
|
|
269
|
+
value = String(value);
|
|
270
|
+
add(field, value, "json-ld", `script[${index}]${item.path}.${key}`, false, `jsonld-type=${entityType}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
let input = html;
|
|
275
|
+
if (Buffer.byteLength(input) > metadataLimits.htmlBytes) {
|
|
276
|
+
input = Buffer.from(input).subarray(0, metadataLimits.htmlBytes).toString("utf8");
|
|
277
|
+
warnings.add("input-limit");
|
|
278
|
+
}
|
|
279
|
+
await new HTMLRewriter().on("html", { element(element) {
|
|
280
|
+
add("language", element.getAttribute("lang"), "html-language", "html@lang", true);
|
|
281
|
+
} }).on("title", { text(chunk) {
|
|
282
|
+
if (title.length <= metadataLimits.value)
|
|
283
|
+
title += chunk.text;
|
|
284
|
+
} }).on("meta", { element(element) {
|
|
285
|
+
const attribute = element.getAttribute("property") ? "property" : "name";
|
|
286
|
+
const key = decodeHTML(element.getAttribute(attribute) ?? "").toLowerCase();
|
|
287
|
+
if (!/^[a-z][a-z0-9:._-]{0,100}$/.test(key) || credentialName.test(key))
|
|
288
|
+
return;
|
|
289
|
+
const field = Object.hasOwn(fields, key) ? fields[key] : `meta:${key.replaceAll(".", "_")}`;
|
|
290
|
+
add(field, element.getAttribute("content"), "html-meta", `meta[${attribute}=${key}]@content`, true);
|
|
291
|
+
} }).on("link", { element(element) {
|
|
292
|
+
const rel = (element.getAttribute("rel") ?? "").toLowerCase().split(/\s+/);
|
|
293
|
+
const field = rel.includes("canonical") ? "canonical" : rel.includes("alternate") ? "alternate" : null;
|
|
294
|
+
if (field)
|
|
295
|
+
add(field, element.getAttribute("href"), "html-link", `link[rel=${field}]@href`, true);
|
|
296
|
+
} }).on("script", { element(element) {
|
|
297
|
+
activeScript = (element.getAttribute("type") ?? "").toLowerCase().trim() === "application/ld+json";
|
|
298
|
+
script = "";
|
|
299
|
+
if (activeScript) {
|
|
300
|
+
const index = scriptIndex++;
|
|
301
|
+
if (index >= 64) {
|
|
302
|
+
activeScript = false;
|
|
303
|
+
warnings.add("json-limit");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
element.onEndTag(() => {
|
|
307
|
+
parseLd(script, index);
|
|
308
|
+
activeScript = false;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}, text(chunk) {
|
|
312
|
+
if (activeScript && script.length <= metadataLimits.jsonBytes)
|
|
313
|
+
script += chunk.text;
|
|
314
|
+
} }).transform(new Response(input)).text();
|
|
315
|
+
if (activeScript)
|
|
316
|
+
warnings.add("json-invalid");
|
|
317
|
+
add("title", title, "html-title", "title", true);
|
|
318
|
+
for (const [key, field] of Object.entries({ title: "title", author: "authors", published: "published", description: "description" }))
|
|
319
|
+
add(field, fallback[key], "extractor", `defuddle.${key}`);
|
|
320
|
+
return parseArticleMetadata({ schemaVersion: 1, extractorVersion: "wordcell-metadata-v1", claims: select(claims), warnings: [...warnings] });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export { parseArticleMetadata, extractArticleMetadata };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
import {
|
|
3
|
+
parseArticleMetadata
|
|
4
|
+
} from "./index-e2wstc7t.js";
|
|
2
5
|
import {
|
|
3
6
|
sanitizeTerminalText
|
|
4
7
|
} from "./index-1xxnjn0d.js";
|
|
@@ -1070,6 +1073,7 @@ function normalizeManifest(input, hasSourceHtml, document) {
|
|
|
1070
1073
|
return {
|
|
1071
1074
|
schemaVersion: CAPTURE_MANIFEST_SCHEMA_VERSION,
|
|
1072
1075
|
document,
|
|
1076
|
+
...input.metadata === undefined ? {} : { metadata: parseArticleMetadata(input.metadata, redactSensitiveText) },
|
|
1073
1077
|
sourceUrl: sanitizeArtifactUrl(input.sourceUrl),
|
|
1074
1078
|
canonicalUrl: sanitizeArtifactUrl(input.canonicalUrl),
|
|
1075
1079
|
capturedAt: new Date(capturedAtTime).toISOString(),
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
resolveRemote,
|
|
7
7
|
rewriteContentWithStatus,
|
|
8
8
|
scanImageSources
|
|
9
|
-
} from "./index-
|
|
9
|
+
} from "./index-jbtyp6q5.js";
|
|
10
|
+
import {
|
|
11
|
+
parseArticleMetadata
|
|
12
|
+
} from "./index-e2wstc7t.js";
|
|
10
13
|
import {
|
|
11
14
|
FetchFailure,
|
|
12
15
|
safeFetch
|
|
@@ -812,6 +815,7 @@ async function extractPage(acquisition, scope, timeoutMs = 30000) {
|
|
|
812
815
|
article = {
|
|
813
816
|
content: retainDefuddleMedia(restoredContent, response, acquisition.finalUrl),
|
|
814
817
|
title: boundedMetadata(response.title, articleMetadataLimits.title) ?? boundedMetadata(acquisition.browserTitle, articleMetadataLimits.title),
|
|
818
|
+
...response.metadata === undefined ? {} : { metadata: parseArticleMetadata(response.metadata) },
|
|
815
819
|
author: boundedMetadata(response.author, articleMetadataLimits.author),
|
|
816
820
|
published: boundedMetadata(response.published, articleMetadataLimits.published),
|
|
817
821
|
description
|