@citation-media/html-parser 0.1.1 → 0.1.2

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/clean.js CHANGED
@@ -1,4 +1,4 @@
1
- import { looksBinary } from "./text.js";
1
+ import { looksBinary, readAttribute } from "./text.js";
2
2
  import { absoluteUrl, isResource, linkKind } from "./urls.js";
3
3
  const cookieConsent = [
4
4
  "[id^='brlbs-cmpnt']",
@@ -126,7 +126,7 @@ export const cleanHtml = (input, options) => {
126
126
  if (links) {
127
127
  rewriter = rewriter.on("a", {
128
128
  element(element) {
129
- const href = element.getAttribute("href");
129
+ const href = readAttribute(element, "href");
130
130
  if (!href) {
131
131
  return;
132
132
  }
package/dist/parse.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MarkdownWriter } from "./markdown.js";
2
- import { collapseWhitespace, decodeEntities, removeHyphenation, } from "./text.js";
2
+ import { collapseWhitespace, decodeEntities, readAttribute, removeHyphenation, } from "./text.js";
3
3
  import { absoluteUrl, isResource, linkKind } from "./urls.js";
4
4
  const blockTags = new Set([
5
5
  "br",
@@ -56,7 +56,6 @@ const hiddenFromMarkdown = [
56
56
  ".visually-hidden",
57
57
  ];
58
58
  const maxHeadings = 200;
59
- const maxImages = 500;
60
59
  const maxResources = 500;
61
60
  /**
62
61
  * Follows an element to its end tag. HTMLRewriter keeps only the last end-tag handler an element
@@ -138,7 +137,7 @@ const onStructure = (rewriter, pass) => {
138
137
  if (!collecting(pass)) {
139
138
  return;
140
139
  }
141
- const classes = element.getAttribute("class")?.split(/\s+/u) ?? [];
140
+ const classes = readAttribute(element, "class")?.split(/\s+/u) ?? [];
142
141
  for (const prefix of classPrefixes) {
143
142
  if (classes.some((name) => name.startsWith(prefix))) {
144
143
  pass.classCounts[prefix] = (pass.classCounts[prefix] ?? 0) + 1;
@@ -159,7 +158,7 @@ const onStructure = (rewriter, pass) => {
159
158
  };
160
159
  const onLinks = (rewriter, pass) => rewriter.on("a[href]", {
161
160
  element(element) {
162
- const href = element.getAttribute("href") ?? "";
161
+ const href = readAttribute(element, "href") ?? "";
163
162
  if (!collecting(pass) || !href) {
164
163
  return;
165
164
  }
@@ -167,9 +166,9 @@ const onLinks = (rewriter, pass) => rewriter.on("a[href]", {
167
166
  const text = [];
168
167
  pass.link = text;
169
168
  // Attributes are readable only while this handler runs, not in the end-tag handler.
170
- const label = element.getAttribute("aria-label") ?? undefined;
171
- const rel = element.getAttribute("rel") ?? undefined;
172
- const target = element.getAttribute("target") ?? undefined;
169
+ const label = readAttribute(element, "aria-label") ?? undefined;
170
+ const rel = readAttribute(element, "rel") ?? undefined;
171
+ const target = readAttribute(element, "target") ?? undefined;
173
172
  const written = writing(pass) &&
174
173
  pass.markdownOptions?.links !== false &&
175
174
  !href.startsWith("#");
@@ -189,23 +188,25 @@ const onLinks = (rewriter, pass) => rewriter.on("a[href]", {
189
188
  });
190
189
  const onImages = (rewriter, pass) => rewriter.on("img", {
191
190
  element(element) {
192
- // Lazy-loading scripts often keep the real address in data-src until the image scrolls in.
193
- const src = element.getAttribute("src") ?? element.getAttribute("data-src");
194
- if (!collecting(pass) || !src || src.startsWith("data:")) {
191
+ // Lazy-loading scripts keep the real address in data-src and a placeholder in src.
192
+ const src = [
193
+ readAttribute(element, "src"),
194
+ readAttribute(element, "data-src"),
195
+ readAttribute(element, "data-lazy-src"),
196
+ ].find((value) => value && !value.startsWith("data:"));
197
+ if (!collecting(pass) || !src) {
195
198
  return;
196
199
  }
197
200
  const url = absoluteUrl(src, pass.base);
198
- const alt = element.getAttribute("alt");
199
- if (pass.options.images &&
200
- pass.images.size < maxImages &&
201
- !pass.images.has(url)) {
201
+ const alt = readAttribute(element, "alt");
202
+ if (pass.options.images && !pass.images.has(url)) {
202
203
  pass.images.set(url, {
203
204
  alt: alt === null ? null : clean(alt),
204
- height: element.getAttribute("height") ?? undefined,
205
- loading: element.getAttribute("loading") ?? undefined,
205
+ height: readAttribute(element, "height") ?? undefined,
206
+ loading: readAttribute(element, "loading") ?? undefined,
206
207
  srcset: element.hasAttribute("srcset"),
207
208
  url,
208
- width: element.getAttribute("width") ?? undefined,
209
+ width: readAttribute(element, "width") ?? undefined,
209
210
  });
210
211
  }
211
212
  if (writing(pass) && pass.markdownOptions?.images) {
@@ -219,11 +220,11 @@ const onResources = (rewriter, pass) => {
219
220
  .on("script", {
220
221
  element(element) {
221
222
  if (resources.scripts.length < maxResources) {
222
- const src = element.getAttribute("src");
223
+ const src = readAttribute(element, "src");
223
224
  resources.scripts.push({
224
225
  attributes: [...element.attributes].flatMap(([name]) => name ? [name] : []),
225
226
  src: src ? absoluteUrl(src, base) : undefined,
226
- type: element.getAttribute("type") ?? undefined,
227
+ type: readAttribute(element, "type") ?? undefined,
227
228
  });
228
229
  }
229
230
  },
@@ -231,7 +232,7 @@ const onResources = (rewriter, pass) => {
231
232
  .on("iframe[src]", {
232
233
  element(element) {
233
234
  if (resources.frames.length < maxResources) {
234
- resources.frames.push(absoluteUrl(element.getAttribute("src") ?? "", base));
235
+ resources.frames.push(absoluteUrl(readAttribute(element, "src") ?? "", base));
235
236
  }
236
237
  },
237
238
  })
@@ -239,8 +240,8 @@ const onResources = (rewriter, pass) => {
239
240
  element(element) {
240
241
  if (resources.links.length < maxResources) {
241
242
  resources.links.push({
242
- href: absoluteUrl(element.getAttribute("href") ?? "", base),
243
- rel: element.getAttribute("rel") ?? "",
243
+ href: absoluteUrl(readAttribute(element, "href") ?? "", base),
244
+ rel: readAttribute(element, "rel") ?? "",
244
245
  });
245
246
  }
246
247
  },
@@ -256,7 +257,7 @@ const metaFields = new Map([
256
257
  const onMeta = (rewriter, pass) => rewriter
257
258
  .on("html", {
258
259
  element(element) {
259
- pass.meta.language = element.getAttribute("lang") ?? undefined;
260
+ pass.meta.language = readAttribute(element, "lang") ?? undefined;
260
261
  },
261
262
  })
262
263
  .on("title", {
@@ -270,18 +271,18 @@ const onMeta = (rewriter, pass) => rewriter
270
271
  })
271
272
  .on("meta", {
272
273
  element(element) {
273
- const name = element.getAttribute("name") ??
274
- element.getAttribute("property") ??
274
+ const name = readAttribute(element, "name") ??
275
+ readAttribute(element, "property") ??
275
276
  "";
276
277
  const field = metaFields.get(name.toLowerCase());
277
278
  if (field) {
278
- pass.meta[field] = element.getAttribute("content") ?? undefined;
279
+ pass.meta[field] = readAttribute(element, "content") ?? undefined;
279
280
  }
280
281
  },
281
282
  })
282
283
  .on("link[rel='canonical']", {
283
284
  element(element) {
284
- const href = element.getAttribute("href");
285
+ const href = readAttribute(element, "href");
285
286
  pass.meta.canonical = href ? absoluteUrl(href, pass.base) : undefined;
286
287
  },
287
288
  });
package/dist/text.d.ts CHANGED
@@ -14,3 +14,8 @@ export declare const collapseWhitespace: (text: string) => string;
14
14
  * conservative so umlauts and other normal characters never trigger it.
15
15
  */
16
16
  export declare const looksBinary: (text: string) => boolean;
17
+ /**
18
+ * An attribute's value with its entities decoded. HTMLRewriter returns attributes as written in the
19
+ * source, so `href="?a=1&amp;b=2"` would otherwise keep its `&amp;`.
20
+ */
21
+ export declare const readAttribute: (element: Element, name: string) => string | null;
package/dist/text.js CHANGED
@@ -3,38 +3,261 @@
3
3
  * parser collects chunks per run of text and decodes them here, so an entity split across two
4
4
  * chunks is still decoded correctly.
5
5
  */
6
+ // The named entities of HTML 4, which cover what pages use in practice; numeric ones work anyway.
6
7
  const named = new Map(Object.entries({
7
- Auml: "Ä",
8
- Ouml: "Ö",
9
- Uuml: "Ü",
8
+ AElig: "\u00C6",
9
+ Aacute: "\u00C1",
10
+ Acirc: "\u00C2",
11
+ Agrave: "\u00C0",
12
+ Alpha: "\u0391",
13
+ Aring: "\u00C5",
14
+ Atilde: "\u00C3",
15
+ Auml: "\u00C4",
16
+ Beta: "\u0392",
17
+ Ccedil: "\u00C7",
18
+ Chi: "\u03A7",
19
+ Dagger: "\u2021",
20
+ Delta: "\u0394",
21
+ ETH: "\u00D0",
22
+ Eacute: "\u00C9",
23
+ Ecirc: "\u00CA",
24
+ Egrave: "\u00C8",
25
+ Epsilon: "\u0395",
26
+ Eta: "\u0397",
27
+ Euml: "\u00CB",
28
+ Gamma: "\u0393",
29
+ Iacute: "\u00CD",
30
+ Icirc: "\u00CE",
31
+ Igrave: "\u00CC",
32
+ Iota: "\u0399",
33
+ Iuml: "\u00CF",
34
+ Kappa: "\u039A",
35
+ Lambda: "\u039B",
36
+ Mu: "\u039C",
37
+ Ntilde: "\u00D1",
38
+ Nu: "\u039D",
39
+ OElig: "\u0152",
40
+ Oacute: "\u00D3",
41
+ Ocirc: "\u00D4",
42
+ Ograve: "\u00D2",
43
+ Omega: "\u03A9",
44
+ Omicron: "\u039F",
45
+ Oslash: "\u00D8",
46
+ Otilde: "\u00D5",
47
+ Ouml: "\u00D6",
48
+ Phi: "\u03A6",
49
+ Pi: "\u03A0",
50
+ Prime: "\u2033",
51
+ Psi: "\u03A8",
52
+ Rho: "\u03A1",
53
+ Scaron: "\u0160",
54
+ Sigma: "\u03A3",
55
+ THORN: "\u00DE",
56
+ Tau: "\u03A4",
57
+ Theta: "\u0398",
58
+ Uacute: "\u00DA",
59
+ Ucirc: "\u00DB",
60
+ Ugrave: "\u00D9",
61
+ Upsilon: "\u03A5",
62
+ Uuml: "\u00DC",
63
+ Xi: "\u039E",
64
+ Yacute: "\u00DD",
65
+ Yuml: "\u0178",
66
+ Zeta: "\u0396",
67
+ aacute: "\u00E1",
68
+ acirc: "\u00E2",
69
+ acute: "\u00B4",
70
+ aelig: "\u00E6",
71
+ agrave: "\u00E0",
72
+ alefsym: "\u2135",
73
+ alpha: "\u03B1",
10
74
  amp: "&",
75
+ and: "\u2227",
76
+ ang: "\u2220",
11
77
  apos: "'",
12
- auml: "ä",
13
- bdquo: "„",
14
- bull: "•",
15
- copy: "©",
16
- euro: "€",
78
+ aring: "\u00E5",
79
+ asymp: "\u2248",
80
+ atilde: "\u00E3",
81
+ auml: "\u00E4",
82
+ bdquo: "\u201E",
83
+ beta: "\u03B2",
84
+ brvbar: "\u00A6",
85
+ bull: "\u2022",
86
+ cap: "\u2229",
87
+ ccedil: "\u00E7",
88
+ cedil: "\u00B8",
89
+ cent: "\u00A2",
90
+ chi: "\u03C7",
91
+ circ: "\u02C6",
92
+ clubs: "\u2663",
93
+ cong: "\u2245",
94
+ copy: "\u00A9",
95
+ crarr: "\u21B5",
96
+ cup: "\u222A",
97
+ curren: "\u00A4",
98
+ dArr: "\u21D3",
99
+ dagger: "\u2020",
100
+ darr: "\u2193",
101
+ deg: "\u00B0",
102
+ delta: "\u03B4",
103
+ diams: "\u2666",
104
+ divide: "\u00F7",
105
+ eacute: "\u00E9",
106
+ ecirc: "\u00EA",
107
+ egrave: "\u00E8",
108
+ empty: "\u2205",
109
+ emsp: "\u2003",
110
+ ensp: "\u2002",
111
+ epsilon: "\u03B5",
112
+ equiv: "\u2261",
113
+ eta: "\u03B7",
114
+ eth: "\u00F0",
115
+ euml: "\u00EB",
116
+ euro: "\u20AC",
117
+ exist: "\u2203",
118
+ fnof: "\u0192",
119
+ forall: "\u2200",
120
+ frac12: "\u00BD",
121
+ frac14: "\u00BC",
122
+ frac34: "\u00BE",
123
+ frasl: "\u2044",
124
+ gamma: "\u03B3",
125
+ ge: "\u2265",
17
126
  gt: ">",
18
- hellip: "…",
19
- laquo: "«",
20
- ldquo: "“",
21
- lsquo: "‘",
127
+ hArr: "\u21D4",
128
+ harr: "\u2194",
129
+ hearts: "\u2665",
130
+ hellip: "\u2026",
131
+ iacute: "\u00ED",
132
+ icirc: "\u00EE",
133
+ iexcl: "\u00A1",
134
+ igrave: "\u00EC",
135
+ image: "\u2111",
136
+ infin: "\u221E",
137
+ int: "\u222B",
138
+ iota: "\u03B9",
139
+ iquest: "\u00BF",
140
+ isin: "\u2208",
141
+ iuml: "\u00EF",
142
+ kappa: "\u03BA",
143
+ lArr: "\u21D0",
144
+ lambda: "\u03BB",
145
+ lang: "\u2329",
146
+ laquo: "\u00AB",
147
+ larr: "\u2190",
148
+ lceil: "\u2308",
149
+ ldquo: "\u201C",
150
+ le: "\u2264",
151
+ lfloor: "\u230A",
152
+ lowast: "\u2217",
153
+ loz: "\u25CA",
154
+ lrm: "\u200E",
155
+ lsaquo: "\u2039",
156
+ lsquo: "\u2018",
22
157
  lt: "<",
23
- mdash: "—",
24
- middot: "·",
25
- nbsp: " ",
26
- ndash: "–",
27
- ouml: "ö",
158
+ macr: "\u00AF",
159
+ mdash: "\u2014",
160
+ micro: "\u00B5",
161
+ middot: "\u00B7",
162
+ minus: "\u2212",
163
+ mu: "\u03BC",
164
+ nabla: "\u2207",
165
+ nbsp: "\u00A0",
166
+ ndash: "\u2013",
167
+ ne: "\u2260",
168
+ ni: "\u220B",
169
+ not: "\u00AC",
170
+ notin: "\u2209",
171
+ nsub: "\u2284",
172
+ ntilde: "\u00F1",
173
+ nu: "\u03BD",
174
+ oacute: "\u00F3",
175
+ ocirc: "\u00F4",
176
+ oelig: "\u0153",
177
+ ograve: "\u00F2",
178
+ oline: "\u203E",
179
+ omega: "\u03C9",
180
+ omicron: "\u03BF",
181
+ oplus: "\u2295",
182
+ or: "\u2228",
183
+ ordf: "\u00AA",
184
+ ordm: "\u00BA",
185
+ oslash: "\u00F8",
186
+ otilde: "\u00F5",
187
+ otimes: "\u2297",
188
+ ouml: "\u00F6",
189
+ para: "\u00B6",
190
+ part: "\u2202",
191
+ permil: "\u2030",
192
+ perp: "\u22A5",
193
+ phi: "\u03C6",
194
+ pi: "\u03C0",
195
+ piv: "\u03D6",
196
+ plusmn: "\u00B1",
197
+ pound: "\u00A3",
198
+ prime: "\u2032",
199
+ prod: "\u220F",
200
+ prop: "\u221D",
201
+ psi: "\u03C8",
28
202
  quot: '"',
29
- raquo: "»",
30
- rdquo: "”",
31
- reg: "®",
32
- rsquo: "’",
33
- sbquo: "‚",
203
+ rArr: "\u21D2",
204
+ radic: "\u221A",
205
+ rang: "\u232A",
206
+ raquo: "\u00BB",
207
+ rarr: "\u2192",
208
+ rceil: "\u2309",
209
+ rdquo: "\u201D",
210
+ real: "\u211C",
211
+ reg: "\u00AE",
212
+ rfloor: "\u230B",
213
+ rho: "\u03C1",
214
+ rlm: "\u200F",
215
+ rsaquo: "\u203A",
216
+ rsquo: "\u2019",
217
+ sbquo: "\u201A",
218
+ scaron: "\u0161",
219
+ sdot: "\u22C5",
220
+ sect: "\u00A7",
34
221
  shy: "",
35
- szlig: "ß",
36
- trade: "™",
37
- uuml: "ü",
222
+ sigma: "\u03C3",
223
+ sigmaf: "\u03C2",
224
+ sim: "\u223C",
225
+ spades: "\u2660",
226
+ sub: "\u2282",
227
+ sube: "\u2286",
228
+ sum: "\u2211",
229
+ sup: "\u2283",
230
+ sup1: "\u00B9",
231
+ sup2: "\u00B2",
232
+ sup3: "\u00B3",
233
+ supe: "\u2287",
234
+ szlig: "\u00DF",
235
+ tau: "\u03C4",
236
+ there4: "\u2234",
237
+ theta: "\u03B8",
238
+ thetasym: "\u03D1",
239
+ thinsp: "\u2009",
240
+ thorn: "\u00FE",
241
+ tilde: "\u02DC",
242
+ times: "\u00D7",
243
+ trade: "\u2122",
244
+ uArr: "\u21D1",
245
+ uacute: "\u00FA",
246
+ uarr: "\u2191",
247
+ ucirc: "\u00FB",
248
+ ugrave: "\u00F9",
249
+ uml: "\u00A8",
250
+ upsih: "\u03D2",
251
+ upsilon: "\u03C5",
252
+ uuml: "\u00FC",
253
+ weierp: "\u2118",
254
+ xi: "\u03BE",
255
+ yacute: "\u00FD",
256
+ yen: "\u00A5",
257
+ yuml: "\u00FF",
258
+ zeta: "\u03B6",
259
+ zwj: "\u200D",
260
+ zwnj: "\u200C",
38
261
  }));
39
262
  const fromCodePoint = (value) => {
40
263
  try {
@@ -75,3 +298,11 @@ export const looksBinary = (text) => {
75
298
  const control = text.match(controlCharacters)?.length ?? 0;
76
299
  return (text.includes("\u0000") || control > 20 || control / text.length > 0.05);
77
300
  };
301
+ /**
302
+ * An attribute's value with its entities decoded. HTMLRewriter returns attributes as written in the
303
+ * source, so `href="?a=1&amp;b=2"` would otherwise keep its `&amp;`.
304
+ */
305
+ export const readAttribute = (element, name) => {
306
+ const value = element.getAttribute(name);
307
+ return value === null ? null : decodeEntities(value);
308
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citation-media/html-parser",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Streaming HTML parser for Cloudflare Workers: links, images, resources, and Markdown without a DOM, built on HTMLRewriter.",
5
5
  "keywords": [
6
6
  "cloudflare-workers",