@crawlee/utils 4.0.0-beta.159 → 4.0.0-beta.160

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/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { EnqueueStrategy } from './internals/url.js';
4
4
  export type { DownloadListOfUrlsOptions, ExtractUrlsOptions } from './internals/extract-urls.js';
5
5
  export { sleep, expandShadowRoots } from './internals/general.js';
6
6
  export * as social from './internals/social.js';
7
+ export * from './internals/extract-microdata.js';
7
8
  export * from './internals/open_graph_parser.js';
8
9
  export * from './internals/robots.js';
9
10
  export * from './internals/sitemap.js';
package/index.js CHANGED
@@ -3,6 +3,7 @@ export { downloadListOfUrls, extractUrls } from './internals/extract-urls.js';
3
3
  export { EnqueueStrategy } from './internals/url.js';
4
4
  export { sleep, expandShadowRoots } from './internals/general.js';
5
5
  export * as social from './internals/social.js';
6
+ export * from './internals/extract-microdata.js';
6
7
  export * from './internals/open_graph_parser.js';
7
8
  export * from './internals/robots.js';
8
9
  export * from './internals/sitemap.js';
@@ -0,0 +1,24 @@
1
+ import type { CheerioAPI } from 'cheerio';
2
+ /** The value of a microdata property: either text or a nested item. */
3
+ export type MicrodataValue = string | MicrodataItem;
4
+ /** A single schema.org item extracted from a document. */
5
+ export interface MicrodataItem {
6
+ /** Tokens of the item's `itemtype` attribute. */
7
+ type?: string[];
8
+ /** The item's `itemid` attribute. */
9
+ id?: string;
10
+ /** Values keyed by `itemprop` name, an array where the property repeats. */
11
+ properties: Record<string, MicrodataValue | MicrodataValue[]>;
12
+ }
13
+ /**
14
+ * Easily parse all schema.org microdata from a page with just a `CheerioAPI` object or raw HTML,
15
+ * following the [microdata processing model](https://html.spec.whatwg.org/multipage/microdata.html#microdata).
16
+ *
17
+ * Text values are trimmed and their inner whitespace collapsed. URL-valued attributes are returned
18
+ * verbatim rather than resolved against the document's base URL.
19
+ *
20
+ * @param htmlOrCheerioElement A `CheerioAPI` object, or a string of raw HTML.
21
+ * @returns The document's top-level items. Nested items are the property values of their parent.
22
+ */
23
+ export declare function extractMicrodata(raw: string): Promise<MicrodataItem[]>;
24
+ export declare function extractMicrodata($: CheerioAPI): Promise<MicrodataItem[]>;
@@ -0,0 +1,118 @@
1
+ import { isTag } from 'domhandler';
2
+ export async function extractMicrodata(htmlOrCheerioElement) {
3
+ // Dynamic so that importing `@crawlee/utils` does not pull in cheerio - see #3836.
4
+ const { load } = await import('cheerio');
5
+ const $ = typeof htmlOrCheerioElement === 'string' ? load(htmlOrCheerioElement) : htmlOrCheerioElement;
6
+ const context = { $ };
7
+ return $('[itemscope]')
8
+ .toArray()
9
+ .filter((element) => !('itemprop' in element.attribs))
10
+ .map((element) => parseItem(context, element, new Set()));
11
+ }
12
+ function parseItem(context, element, ancestors) {
13
+ const item = { properties: {} };
14
+ const type = uniqueTokens(element.attribs.itemtype);
15
+ const id = element.attribs.itemid;
16
+ if (type.length > 0) {
17
+ item.type = type;
18
+ }
19
+ if (id) {
20
+ item.id = id.trim();
21
+ }
22
+ ancestors.add(element);
23
+ for (const propertyElement of collectPropertyElements(context, element)) {
24
+ const value = getPropertyValue(context, propertyElement, ancestors);
25
+ for (const name of uniqueTokens(propertyElement.attribs.itemprop)) {
26
+ addProperty(item.properties, name, value);
27
+ }
28
+ }
29
+ ancestors.delete(element);
30
+ return item;
31
+ }
32
+ function collectPropertyElements(context, scope) {
33
+ const elements = [];
34
+ collectFromNodes(scope.children, elements);
35
+ for (const id of uniqueTokens(scope.attribs.itemref)) {
36
+ const referenced = (context.idIndex ??= indexIds(context.$)).get(id);
37
+ if (referenced) {
38
+ collectFromNodes([referenced], elements);
39
+ }
40
+ }
41
+ return elements;
42
+ }
43
+ function collectFromNodes(nodes, elements) {
44
+ for (const node of nodes) {
45
+ if (!isTag(node)) {
46
+ continue;
47
+ }
48
+ if ('itemprop' in node.attribs) {
49
+ elements.push(node);
50
+ }
51
+ // A nested item owns everything below it, so its subtree is not part of the enclosing item.
52
+ if (!('itemscope' in node.attribs)) {
53
+ collectFromNodes(node.children, elements);
54
+ }
55
+ }
56
+ }
57
+ function indexIds($) {
58
+ const index = new Map();
59
+ for (const element of $('[id]').toArray()) {
60
+ // Duplicate ids are invalid HTML; `getElementById` resolves them to the first element.
61
+ if (!index.has(element.attribs.id)) {
62
+ index.set(element.attribs.id, element);
63
+ }
64
+ }
65
+ return index;
66
+ }
67
+ function getPropertyValue(context, element, ancestors) {
68
+ if ('itemscope' in element.attribs) {
69
+ // `itemref` can point back at an enclosing item, which the spec treats as an error.
70
+ return ancestors.has(element) ? { properties: {} } : parseItem(context, element, ancestors);
71
+ }
72
+ const { attribs } = element;
73
+ switch (element.tagName.toLowerCase()) {
74
+ case 'meta':
75
+ return attribs.content ?? '';
76
+ case 'audio':
77
+ case 'embed':
78
+ case 'iframe':
79
+ case 'img':
80
+ case 'source':
81
+ case 'track':
82
+ case 'video':
83
+ return attribs.src ?? '';
84
+ case 'a':
85
+ case 'area':
86
+ case 'link':
87
+ return attribs.href ?? '';
88
+ case 'object':
89
+ return attribs.data ?? '';
90
+ case 'data':
91
+ case 'meter':
92
+ return attribs.value ?? '';
93
+ case 'time':
94
+ return attribs.datetime ?? context.$(element).text().replace(/\s+/g, ' ').trim();
95
+ default:
96
+ return context.$(element).text().replace(/\s+/g, ' ').trim();
97
+ }
98
+ }
99
+ function addProperty(properties, name, value) {
100
+ const existing = properties[name];
101
+ if (existing === undefined) {
102
+ properties[name] = value;
103
+ }
104
+ else if (Array.isArray(existing)) {
105
+ existing.push(value);
106
+ }
107
+ else {
108
+ properties[name] = [existing, value];
109
+ }
110
+ }
111
+ /** `itemtype`, `itemprop` and `itemref` are all unordered sets of unique space-separated tokens. */
112
+ function uniqueTokens(value) {
113
+ if (!value) {
114
+ return [];
115
+ }
116
+ const tokens = value.split(/\s+/).filter(Boolean);
117
+ return tokens.length > 1 ? [...new Set(tokens)] : tokens;
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/utils",
3
- "version": "4.0.0-beta.159",
3
+ "version": "4.0.0-beta.160",
4
4
  "description": "A set of shared utilities that can be used by crawlers",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -43,8 +43,8 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@apify/ps-tree": "^1.2.0",
46
- "@crawlee/http-client": "4.0.0-beta.159",
47
- "@crawlee/types": "4.0.0-beta.159",
46
+ "@crawlee/http-client": "4.0.0-beta.160",
47
+ "@crawlee/types": "4.0.0-beta.160",
48
48
  "@types/sax": "^1.2.7",
49
49
  "cheerio": "^1.0.0",
50
50
  "domhandler": "^5.0.3",
@@ -63,5 +63,5 @@
63
63
  }
64
64
  }
65
65
  },
66
- "gitHead": "9f91c03967915be4e9b077a717e0e3494ad0a101"
66
+ "gitHead": "09fbce5ade9b84a2b191c635463e172388e52678"
67
67
  }