gumbo-html 0.2.4 → 0.3.1
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/API.md +439 -0
- package/README.md +30 -10
- package/binding.gyp +49 -0
- package/examples/example.js +87 -0
- package/examples/scrape.js +301 -0
- package/index.d.ts +58 -7
- package/index.js +7 -2
- package/lib/wrapper.js +381 -0
- package/package.json +38 -6
- package/src/addon.cc +19 -0
- package/src/gumbo-parser/COPYING +201 -0
- package/src/gumbo-parser/README.md +8 -0
- package/src/gumbo-parser/src/attribute.c +44 -0
- package/src/gumbo-parser/src/attribute.h +37 -0
- package/src/gumbo-parser/src/char_ref.c +23069 -0
- package/src/gumbo-parser/src/char_ref.h +60 -0
- package/src/gumbo-parser/src/error.c +279 -0
- package/src/gumbo-parser/src/error.h +225 -0
- package/src/gumbo-parser/src/gumbo.h +671 -0
- package/src/gumbo-parser/src/insertion_mode.h +57 -0
- package/src/gumbo-parser/src/parser.c +4192 -0
- package/src/gumbo-parser/src/parser.h +57 -0
- package/src/gumbo-parser/src/string_buffer.c +110 -0
- package/src/gumbo-parser/src/string_buffer.h +84 -0
- package/src/gumbo-parser/src/string_piece.c +48 -0
- package/src/gumbo-parser/src/string_piece.h +38 -0
- package/src/gumbo-parser/src/tag.c +95 -0
- package/src/gumbo-parser/src/tag_enum.h +153 -0
- package/src/gumbo-parser/src/tag_gperf.h +105 -0
- package/src/gumbo-parser/src/tag_sizes.h +4 -0
- package/src/gumbo-parser/src/tag_strings.h +153 -0
- package/src/gumbo-parser/src/token_type.h +41 -0
- package/src/gumbo-parser/src/tokenizer.c +2897 -0
- package/src/gumbo-parser/src/tokenizer.h +123 -0
- package/src/gumbo-parser/src/tokenizer_states.h +103 -0
- package/src/gumbo-parser/src/utf8.c +270 -0
- package/src/gumbo-parser/src/utf8.h +132 -0
- package/src/gumbo-parser/src/util.c +58 -0
- package/src/gumbo-parser/src/util.h +60 -0
- package/src/gumbo-parser/src/vector.c +123 -0
- package/src/gumbo-parser/src/vector.h +67 -0
- package/src/html_document.cc +409 -0
- package/src/html_document.h +56 -0
- package/src/html_element.cc +961 -0
- package/src/html_element.h +70 -0
- package/src/include/win/strings.h +11 -0
- package/src/jsa.c +182 -0
- package/src/jsa.h +44 -0
- package/src/xnode.c +372 -0
- package/src/xnode_query.c +330 -0
- package/src/xnode_query.h +186 -0
- package/src/xnode_query_parser.c +414 -0
- package/install.js +0 -15
package/API.md
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# API
|
|
2
|
+
|
|
3
|
+
`gumbo-html` is a native Node.js addon that parses HTML with Gumbo and exposes
|
|
4
|
+
CSS-selector helpers for querying, traversal, and common scraping tasks.
|
|
5
|
+
|
|
6
|
+
The package ships TypeScript declarations in `index.d.ts`. The examples below
|
|
7
|
+
use both CommonJS and TypeScript-style imports where useful.
|
|
8
|
+
|
|
9
|
+
## Importing
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { parse } = require('gumbo-html');
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { parse, type XDocument, type XElement } from 'gumbo-html';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## `parse(html, options?)`
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
function parse(html: string, options?: { baseUrl?: string }): XDocument;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Parses an HTML string and returns an `XDocument`.
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const { parse } = require('gumbo-html');
|
|
29
|
+
|
|
30
|
+
const doc = parse(`
|
|
31
|
+
<article class="post">
|
|
32
|
+
<h1>Hello</h1>
|
|
33
|
+
<a href="/posts/hello">Read more</a>
|
|
34
|
+
</article>
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
console.log(doc.firstOrThrow('h1').innerText); // "Hello"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`html` must be a string. Passing `undefined`, `null`, or another type throws a
|
|
41
|
+
`TypeError` with the message `html must be a string`.
|
|
42
|
+
|
|
43
|
+
### `options.baseUrl`
|
|
44
|
+
|
|
45
|
+
`baseUrl` is used by URL helper methods to resolve relative URLs.
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const doc = parse('<a href="/docs">Docs</a>', {
|
|
49
|
+
baseUrl: 'https://example.com/app/',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(doc.url('a', 'href')); // "https://example.com/docs"
|
|
53
|
+
console.log(doc.links()); // [{ text: "Docs", href: "https://example.com/docs" }]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Selector Support
|
|
57
|
+
|
|
58
|
+
Selectors are CSS-like and scoped to the document or element you call from.
|
|
59
|
+
|
|
60
|
+
Supported selector features:
|
|
61
|
+
|
|
62
|
+
- Type selectors: `article`, `h1`
|
|
63
|
+
- Universal selector: `*`
|
|
64
|
+
- ID selectors: `#main`
|
|
65
|
+
- Class selectors: `.post`, `article.featured`
|
|
66
|
+
- Attribute selectors: `[href]`, `[rel="canonical"]`, `[class~="active"]`
|
|
67
|
+
- Attribute operators: `=`, `~=`, `|=`, `^=`, `$=`, `*=`
|
|
68
|
+
- Combinators: descendant `A B`, child `A > B`, adjacent sibling `A + B`,
|
|
69
|
+
general sibling `A ~ B`
|
|
70
|
+
- Relative selectors beginning with a combinator, such as `> li`, when querying
|
|
71
|
+
from a specific context
|
|
72
|
+
|
|
73
|
+
Unsupported selector features include selector lists (`a, img`), pseudo-classes
|
|
74
|
+
(`:first-child`, `:not(...)`, `:nth-child(...)`), pseudo-elements, namespaces,
|
|
75
|
+
and general CSS identifier escaping.
|
|
76
|
+
|
|
77
|
+
Invalid selectors throw `Error: Bad selector.`. Missing or non-string selector
|
|
78
|
+
arguments throw `TypeError: selector must be a string`.
|
|
79
|
+
|
|
80
|
+
Tag and attribute names are matched case-insensitively. Attribute values are
|
|
81
|
+
matched case-sensitively.
|
|
82
|
+
|
|
83
|
+
## Return Conventions
|
|
84
|
+
|
|
85
|
+
Many methods have optional and throwing forms:
|
|
86
|
+
|
|
87
|
+
- Optional element lookups return `null` when no matching element exists.
|
|
88
|
+
- Optional attribute lookups return `undefined` when the attribute is missing.
|
|
89
|
+
- Throwing lookup methods end in `OrThrow` and throw when the required match is
|
|
90
|
+
missing or not unique.
|
|
91
|
+
- Query methods return JavaScript arrays.
|
|
92
|
+
- `childNodes` can include text, whitespace, comment, CDATA, and element nodes.
|
|
93
|
+
|
|
94
|
+
## Types
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
type NodeType =
|
|
98
|
+
| 'DOCUMENT'
|
|
99
|
+
| 'ELEMENT'
|
|
100
|
+
| 'TEXT'
|
|
101
|
+
| 'CDATA'
|
|
102
|
+
| 'COMMENT'
|
|
103
|
+
| 'WHITESPACE'
|
|
104
|
+
| 'TEMPLATE'
|
|
105
|
+
| 'UNKNOWN';
|
|
106
|
+
|
|
107
|
+
type TextOptions = {
|
|
108
|
+
normalize?: boolean;
|
|
109
|
+
separator?: string;
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`normalize: true` trims leading/trailing whitespace and collapses ASCII
|
|
114
|
+
whitespace runs into single spaces. `separator` joins non-empty descendant text
|
|
115
|
+
nodes with the provided separator.
|
|
116
|
+
|
|
117
|
+
## `XDocument`
|
|
118
|
+
|
|
119
|
+
An `XDocument` represents the parsed document. `doc.documentElement` is the root
|
|
120
|
+
HTML element produced by Gumbo.
|
|
121
|
+
|
|
122
|
+
### Properties
|
|
123
|
+
|
|
124
|
+
| Property | Type | Description |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| `documentElement` | `XElement` | Root element, usually `<html>`. |
|
|
127
|
+
| `innerText` | `string` | Text from `documentElement`. |
|
|
128
|
+
| `textContent` | `string` | Same text extraction as `innerText`. |
|
|
129
|
+
| `outerHTML` | `string` | Serialized root element HTML. |
|
|
130
|
+
| `tagName` | `null` | Documents do not have a tag name. |
|
|
131
|
+
| `nodeType` | `'DOCUMENT'` | Document node type. |
|
|
132
|
+
|
|
133
|
+
### Query Methods
|
|
134
|
+
|
|
135
|
+
| Method | Returns | Description |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `find(selector)` | `XElement[]` | All matching descendants. |
|
|
138
|
+
| `first(selector)` | `XElement \| null` | First matching descendant or `null`. |
|
|
139
|
+
| `firstOrThrow(selector)` | `XElement` | First match, or throws `No element found`. |
|
|
140
|
+
| `only(selector)` | `XElement \| null` | Match only when exactly one element exists; otherwise `null`. |
|
|
141
|
+
| `onlyOrThrow(selector)` | `XElement` | Exactly one match, or throws `Not a single element`. |
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
const doc = parse('<main><p>A</p><p>B</p></main>');
|
|
145
|
+
|
|
146
|
+
console.log(doc.find('p').length); // 2
|
|
147
|
+
console.log(doc.first('h1')); // null
|
|
148
|
+
console.log(doc.only('main').tagName); // "main"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Convenience Methods
|
|
152
|
+
|
|
153
|
+
| Method | Returns | Description |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| `text(selector, opts?)` | `string \| null` | Text of the first match, or `null`. |
|
|
156
|
+
| `textOrThrow(selector)` | `string` | Text of the first match, or throws `No element found`. |
|
|
157
|
+
| `attr(selector, name)` | `string \| undefined` | Attribute from the first match. |
|
|
158
|
+
| `attrOrThrow(selector, name)` | `string` | Attribute from the first match, or throws `Attribute not found`. |
|
|
159
|
+
| `exists(selector)` | `boolean` | Whether at least one element matches. |
|
|
160
|
+
| `count(selector)` | `number` | Number of matching elements. |
|
|
161
|
+
| `url(selector, attr)` | `string \| undefined` | Resolved URL attribute from the first match. |
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
const doc = parse(`
|
|
165
|
+
<article>
|
|
166
|
+
<h1> Hello World </h1>
|
|
167
|
+
<a href="/hello">Read</a>
|
|
168
|
+
</article>
|
|
169
|
+
`, { baseUrl: 'https://example.com/' });
|
|
170
|
+
|
|
171
|
+
console.log(doc.text('h1', { normalize: true })); // "Hello World"
|
|
172
|
+
console.log(doc.attr('a', 'href')); // "/hello"
|
|
173
|
+
console.log(doc.url('a', 'href')); // "https://example.com/hello"
|
|
174
|
+
console.log(doc.exists('article')); // true
|
|
175
|
+
console.log(doc.count('a')); // 1
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Scraping Helpers
|
|
179
|
+
|
|
180
|
+
| Method | Returns | Description |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| `meta()` | `{ [key: string]: string }` | Metadata from `meta[name]`, `meta[property]`, and `meta[http-equiv]` using `content` values. |
|
|
183
|
+
| `links()` | `{ text: string; href: string }[]` | All `a[href]` elements. `href` is resolved with `baseUrl` when provided. |
|
|
184
|
+
| `images()` | `{ alt: string; src: string }[]` | All `img[src]` elements. Missing `alt` becomes `''`; `src` is resolved with `baseUrl` when provided. |
|
|
185
|
+
| `forms()` | `XElement[]` | All `form` elements. |
|
|
186
|
+
| `tables()` | `XElement[]` | All `table` elements. |
|
|
187
|
+
| `table(selector?)` | `Array<{ [header: string]: string }>` | Rows from the first matching table, or the first table when no selector is given. |
|
|
188
|
+
| `title()` | `string \| null` | Text from the first `title` element. |
|
|
189
|
+
| `description()` | `string \| undefined` | `content` from `meta[name="description"]`. |
|
|
190
|
+
| `canonicalUrl()` | `string \| undefined` | Raw `href` from `link[rel="canonical"]`. |
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
const doc = parse(`
|
|
194
|
+
<head>
|
|
195
|
+
<title>Example</title>
|
|
196
|
+
<meta name="description" content="Demo page">
|
|
197
|
+
<meta property="og:title" content="Example OG">
|
|
198
|
+
</head>
|
|
199
|
+
<body>
|
|
200
|
+
<a href="/a">A</a>
|
|
201
|
+
<img src="/a.png" alt="A">
|
|
202
|
+
</body>
|
|
203
|
+
`, { baseUrl: 'https://example.com/' });
|
|
204
|
+
|
|
205
|
+
console.log(doc.title()); // "Example"
|
|
206
|
+
console.log(doc.description()); // "Demo page"
|
|
207
|
+
console.log(doc.meta()); // { description: "Demo page", "og:title": "Example OG" }
|
|
208
|
+
console.log(doc.links()); // [{ text: "A", href: "https://example.com/a" }]
|
|
209
|
+
console.log(doc.images()); // [{ alt: "A", src: "https://example.com/a.png" }]
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Structured Extraction
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
type ExtractSchema = {
|
|
216
|
+
[key: string]: [string, string | ExtractSchema | 'exists' | 'text' | 'count'];
|
|
217
|
+
};
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`extract(schema)` evaluates each field relative to the current document or
|
|
221
|
+
element.
|
|
222
|
+
|
|
223
|
+
Schema actions:
|
|
224
|
+
|
|
225
|
+
- `[selector, 'text']` returns text from the first match, or `null`.
|
|
226
|
+
- `[selector, 'exists']` returns a boolean.
|
|
227
|
+
- `[selector, 'count']` returns a number.
|
|
228
|
+
- `[selector, attrName]` returns an attribute from the first match, or
|
|
229
|
+
`undefined`.
|
|
230
|
+
- `[selector, nestedSchema]` returns an array by applying `nestedSchema` to each
|
|
231
|
+
matching element.
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
const doc = parse(`
|
|
235
|
+
<article class="post">
|
|
236
|
+
<h2>First</h2>
|
|
237
|
+
<a href="/first">Read</a>
|
|
238
|
+
</article>
|
|
239
|
+
<article class="post">
|
|
240
|
+
<h2>Second</h2>
|
|
241
|
+
</article>
|
|
242
|
+
`);
|
|
243
|
+
|
|
244
|
+
const data = doc.extract({
|
|
245
|
+
postCount: ['article.post', 'count'],
|
|
246
|
+
posts: ['article.post', {
|
|
247
|
+
heading: ['h2', 'text'],
|
|
248
|
+
href: ['a', 'href'],
|
|
249
|
+
hasLink: ['a', 'exists'],
|
|
250
|
+
}],
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
console.log(data);
|
|
254
|
+
// {
|
|
255
|
+
// postCount: 2,
|
|
256
|
+
// posts: [
|
|
257
|
+
// { heading: "First", href: "/first", hasLink: true },
|
|
258
|
+
// { heading: "Second", href: undefined, hasLink: false }
|
|
259
|
+
// ]
|
|
260
|
+
// }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Attribute values returned by `extract` are raw values. Use `doc.url()` or
|
|
264
|
+
`el.urlAttr()` when you need URL resolution.
|
|
265
|
+
|
|
266
|
+
## `XElement`
|
|
267
|
+
|
|
268
|
+
An `XElement` represents an element or another Gumbo node returned by
|
|
269
|
+
`childNodes`.
|
|
270
|
+
|
|
271
|
+
### Properties
|
|
272
|
+
|
|
273
|
+
| Property | Type | Description |
|
|
274
|
+
| --- | --- | --- |
|
|
275
|
+
| `childNodes` | `XElement[]` | Direct child nodes. Includes non-element nodes. |
|
|
276
|
+
| `nodeType` | `NodeType` | Gumbo node type. |
|
|
277
|
+
| `parent` | `XElement \| null` | Parent element, or `null` for the document root. |
|
|
278
|
+
| `outerHTML` | `string` | Source HTML for this node. |
|
|
279
|
+
| `innerText` | `string` | Descendant text for text/element/template nodes. |
|
|
280
|
+
| `textContent` | `string` | Same text extraction as `innerText`. |
|
|
281
|
+
| `tagName` | `string \| null` | Normalized tag name for element/template nodes; `null` for non-elements. |
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
const doc = parse('<div><!-- note -->Text <b>bold</b></div>');
|
|
285
|
+
const div = doc.firstOrThrow('div');
|
|
286
|
+
|
|
287
|
+
console.log(div.childNodes.map((node) => node.nodeType));
|
|
288
|
+
// ["COMMENT", "TEXT", "ELEMENT"]
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Query Methods
|
|
292
|
+
|
|
293
|
+
Element query methods have the same behavior as document query methods, but the
|
|
294
|
+
search is scoped to descendants of the element.
|
|
295
|
+
|
|
296
|
+
| Method | Returns |
|
|
297
|
+
| --- | --- |
|
|
298
|
+
| `find(selector)` | `XElement[]` |
|
|
299
|
+
| `first(selector)` | `XElement \| null` |
|
|
300
|
+
| `firstOrThrow(selector)` | `XElement` |
|
|
301
|
+
| `only(selector)` | `XElement \| null` |
|
|
302
|
+
| `onlyOrThrow(selector)` | `XElement` |
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
const article = parse(`
|
|
306
|
+
<article>
|
|
307
|
+
<h2>Title</h2>
|
|
308
|
+
<p>Summary</p>
|
|
309
|
+
</article>
|
|
310
|
+
`).firstOrThrow('article');
|
|
311
|
+
|
|
312
|
+
console.log(article.firstOrThrow('h2').innerText); // "Title"
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Attribute Methods
|
|
316
|
+
|
|
317
|
+
| Method | Returns | Description |
|
|
318
|
+
| --- | --- | --- |
|
|
319
|
+
| `attr(name)` | `string \| undefined` | Attribute value, or `undefined`. |
|
|
320
|
+
| `attr_s(name)` | `string` | Attribute value, or throws `Attribute not found`. |
|
|
321
|
+
| `attrOrThrow(name)` | `string` | Alias for `attr_s`. |
|
|
322
|
+
| `hasClass(name)` | `boolean` | Whether the `class` attribute contains `name`. |
|
|
323
|
+
| `hasAttribute(name)` | `boolean` | Whether the attribute exists. |
|
|
324
|
+
| `urlAttr(name)` | `string \| undefined` | Resolved URL attribute when the document was parsed with `baseUrl`. |
|
|
325
|
+
|
|
326
|
+
```js
|
|
327
|
+
const doc = parse('<a class="button primary" href="/start">Start</a>', {
|
|
328
|
+
baseUrl: 'https://example.com/',
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const link = doc.firstOrThrow('a');
|
|
332
|
+
console.log(link.attr('href')); // "/start"
|
|
333
|
+
console.log(link.urlAttr('href')); // "https://example.com/start"
|
|
334
|
+
console.log(link.hasClass('primary')); // true
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Text Methods
|
|
338
|
+
|
|
339
|
+
| Method | Returns | Description |
|
|
340
|
+
| --- | --- | --- |
|
|
341
|
+
| `text(opts?)` | `string` | Text for this element. |
|
|
342
|
+
| `text(selector, opts?)` | `string \| null` | Text for the first matching descendant. |
|
|
343
|
+
| `textOrThrow(selector)` | `string` | Text for the first matching descendant, or throws `No element found`. |
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
const el = parse('<div><span>Hello</span><b>World</b></div>').firstOrThrow('div');
|
|
347
|
+
|
|
348
|
+
console.log(el.text()); // "HelloWorld"
|
|
349
|
+
console.log(el.text({ separator: ' ' })); // "Hello World"
|
|
350
|
+
console.log(el.text('span')); // "Hello"
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Traversal and Matching
|
|
354
|
+
|
|
355
|
+
| Method | Returns | Description |
|
|
356
|
+
| --- | --- | --- |
|
|
357
|
+
| `prev(selector?)` | `XElement \| null` | Previous element sibling, optionally filtered. |
|
|
358
|
+
| `next(selector?)` | `XElement \| null` | Next element sibling, optionally filtered. |
|
|
359
|
+
| `closest(selector)` | `XElement \| null` | Nearest ancestor matching `selector`. This starts at the parent, not the element itself. |
|
|
360
|
+
| `children(selector?)` | `XElement[]` | Direct element children, optionally filtered. Non-element nodes are excluded. |
|
|
361
|
+
| `siblings(selector?)` | `XElement[]` | Element siblings, optionally filtered. The current element is excluded. |
|
|
362
|
+
| `matches(selector)` | `boolean` | Whether this element matches the full selector. |
|
|
363
|
+
| `is(selector)` | `boolean` | Alias for `matches`. |
|
|
364
|
+
|
|
365
|
+
```js
|
|
366
|
+
const doc = parse(`
|
|
367
|
+
<section>
|
|
368
|
+
<article class="post featured"><h2>One</h2></article>
|
|
369
|
+
<article class="post"><h2>Two</h2></article>
|
|
370
|
+
</section>
|
|
371
|
+
`);
|
|
372
|
+
|
|
373
|
+
const first = doc.firstOrThrow('article.featured');
|
|
374
|
+
const second = first.next('article.post');
|
|
375
|
+
|
|
376
|
+
console.log(second.prev('.featured') === null); // false
|
|
377
|
+
console.log(first.children('h2').length); // 1
|
|
378
|
+
console.log(first.matches('section article')); // true
|
|
379
|
+
console.log(first.closest('section').tagName); // "section"
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Table Rows
|
|
383
|
+
|
|
384
|
+
`rows()` extracts rows from a table-like element.
|
|
385
|
+
|
|
386
|
+
- If the first row contains `th` cells, those values become object keys and the
|
|
387
|
+
first row is treated as a header row.
|
|
388
|
+
- With headers, data rows use `td` cells.
|
|
389
|
+
- Without headers, cells are assigned numeric object keys.
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
const doc = parse(`
|
|
393
|
+
<table>
|
|
394
|
+
<tr><th>Name</th><th>Age</th></tr>
|
|
395
|
+
<tr><td>Alice</td><td>30</td></tr>
|
|
396
|
+
<tr><td>Bob</td><td>25</td></tr>
|
|
397
|
+
</table>
|
|
398
|
+
`);
|
|
399
|
+
|
|
400
|
+
console.log(doc.firstOrThrow('table').rows());
|
|
401
|
+
// [
|
|
402
|
+
// { Name: "Alice", Age: "30" },
|
|
403
|
+
// { Name: "Bob", Age: "25" }
|
|
404
|
+
// ]
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`doc.table(selector?)` is a convenience wrapper around
|
|
408
|
+
`doc.first(selector || 'table')?.rows()`, returning `[]` when no table matches.
|
|
409
|
+
|
|
410
|
+
### Element Extraction
|
|
411
|
+
|
|
412
|
+
`el.extract(schema)` uses the same structured extraction rules as
|
|
413
|
+
`doc.extract(schema)`, scoped to the element.
|
|
414
|
+
|
|
415
|
+
```js
|
|
416
|
+
const cards = parse(`
|
|
417
|
+
<div class="card"><h3>A</h3><span>New</span></div>
|
|
418
|
+
<div class="card"><h3>B</h3></div>
|
|
419
|
+
`).find('.card');
|
|
420
|
+
|
|
421
|
+
const data = cards.map((card) => card.extract({
|
|
422
|
+
title: ['h3', 'text'],
|
|
423
|
+
hasBadge: ['span', 'exists'],
|
|
424
|
+
}));
|
|
425
|
+
|
|
426
|
+
console.log(data);
|
|
427
|
+
// [{ title: "A", hasBadge: true }, { title: "B", hasBadge: false }]
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Error Summary
|
|
431
|
+
|
|
432
|
+
| Operation | Error |
|
|
433
|
+
| --- | --- |
|
|
434
|
+
| `parse()` without a string | `TypeError: html must be a string` |
|
|
435
|
+
| Query method without a string selector | `TypeError: selector must be a string` |
|
|
436
|
+
| Invalid selector syntax | `Error: Bad selector.` |
|
|
437
|
+
| `firstOrThrow()` with no match | `Error: No element found` |
|
|
438
|
+
| `onlyOrThrow()` with zero or multiple matches | `Error: Not a single element` |
|
|
439
|
+
| `attr_s()` / `attrOrThrow()` with missing attribute | `Error: Attribute not found` |
|
package/README.md
CHANGED
|
@@ -1,26 +1,46 @@
|
|
|
1
|
+
# gumbo-html
|
|
2
|
+
|
|
1
3
|
CSS selector based on Gumbo HTML parser.
|
|
2
4
|
|
|
3
5
|
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install gumbo-html
|
|
4
9
|
```
|
|
5
|
-
|
|
6
|
-
|
|
10
|
+
|
|
11
|
+
`gumbo-html` is a native Node.js addon and is compiled from source on install.
|
|
12
|
+
You'll need the standard `node-gyp` toolchain:
|
|
13
|
+
|
|
14
|
+
- Python 3
|
|
15
|
+
- A C/C++ compiler (Xcode Command Line Tools on macOS, `build-essential` on
|
|
16
|
+
Linux, or Visual Studio Build Tools on Windows)
|
|
17
|
+
|
|
18
|
+
See the [node-gyp docs](https://github.com/nodejs/node-gyp#installation) for
|
|
19
|
+
platform-specific setup details.
|
|
7
20
|
|
|
8
21
|
## Usage
|
|
9
22
|
|
|
10
|
-
Example:
|
|
11
23
|
```ts
|
|
12
|
-
import {parse} from 'gumbo-html';
|
|
24
|
+
import { parse } from 'gumbo-html';
|
|
13
25
|
|
|
14
|
-
const html = `
|
|
26
|
+
const html = `
|
|
15
27
|
<html>
|
|
16
28
|
<p class="foo bar blah">Foo</p>
|
|
17
29
|
<p class="bar">Bar</p>
|
|
18
30
|
</html>
|
|
19
|
-
|
|
31
|
+
`;
|
|
20
32
|
|
|
21
|
-
const
|
|
33
|
+
const doc = parse(html);
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
console.log(el.innerText)
|
|
35
|
+
doc.find('.bar').forEach((el) => {
|
|
36
|
+
console.log(el.innerText);
|
|
25
37
|
});
|
|
26
|
-
```
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For the complete API reference, including selector support, traversal helpers,
|
|
41
|
+
structured extraction, table parsing, and URL resolution, see [API.md](API.md).
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT. Bundles [google/gumbo-parser](https://github.com/google/gumbo-parser)
|
|
46
|
+
(Apache-2.0) under `src/gumbo-parser/`.
|
package/binding.gyp
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [
|
|
3
|
+
{
|
|
4
|
+
"target_name": "html",
|
|
5
|
+
"sources": [
|
|
6
|
+
"src/addon.cc",
|
|
7
|
+
"src/html_document.cc",
|
|
8
|
+
"src/html_element.cc",
|
|
9
|
+
"src/xnode.c",
|
|
10
|
+
"src/xnode_query.c",
|
|
11
|
+
"src/xnode_query_parser.c",
|
|
12
|
+
"src/jsa.c",
|
|
13
|
+
"src/gumbo-parser/src/attribute.c",
|
|
14
|
+
"src/gumbo-parser/src/error.c",
|
|
15
|
+
"src/gumbo-parser/src/string_buffer.c",
|
|
16
|
+
"src/gumbo-parser/src/tag.c",
|
|
17
|
+
"src/gumbo-parser/src/utf8.c",
|
|
18
|
+
"src/gumbo-parser/src/vector.c",
|
|
19
|
+
"src/gumbo-parser/src/char_ref.c",
|
|
20
|
+
"src/gumbo-parser/src/parser.c",
|
|
21
|
+
"src/gumbo-parser/src/string_piece.c",
|
|
22
|
+
"src/gumbo-parser/src/tokenizer.c",
|
|
23
|
+
"src/gumbo-parser/src/util.c"
|
|
24
|
+
],
|
|
25
|
+
"include_dirs": [
|
|
26
|
+
"<!@(node -p \"require('node-addon-api').include\")",
|
|
27
|
+
"src",
|
|
28
|
+
"src/gumbo-parser/src"
|
|
29
|
+
],
|
|
30
|
+
"cflags!": ["-fno-exceptions"],
|
|
31
|
+
"cflags_cc!": ["-fno-exceptions"],
|
|
32
|
+
"xcode_settings": {
|
|
33
|
+
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
|
34
|
+
"CLANG_CXX_LIBRARY": "libc++",
|
|
35
|
+
"MACOSX_DEPLOYMENT_TARGET": "10.15"
|
|
36
|
+
},
|
|
37
|
+
"msvs_settings": {
|
|
38
|
+
"VCCLCompilerTool": {
|
|
39
|
+
"ExceptionHandling": 1
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"conditions": [
|
|
43
|
+
["OS==\"win\"", {
|
|
44
|
+
"include_dirs": ["src/include/win"]
|
|
45
|
+
}]
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parse } = require('..');
|
|
4
|
+
|
|
5
|
+
const html = `
|
|
6
|
+
<!doctype html>
|
|
7
|
+
<html>
|
|
8
|
+
<body>
|
|
9
|
+
<main id="content">
|
|
10
|
+
<article class="post featured" data-slug="hello-world">
|
|
11
|
+
<h1>Hello world</h1>
|
|
12
|
+
<p class="summary">A short introduction.</p>
|
|
13
|
+
<a class="cta primary" href="/hello">Read more</a>
|
|
14
|
+
</article>
|
|
15
|
+
|
|
16
|
+
<article class="post" data-slug="second-post">
|
|
17
|
+
<h1>Second post</h1>
|
|
18
|
+
<p class="summary">A follow-up note.</p>
|
|
19
|
+
<a class="cta" href="/second">Open post</a>
|
|
20
|
+
</article>
|
|
21
|
+
</main>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const doc = parse(html);
|
|
27
|
+
|
|
28
|
+
// documentElement returns the parsed <html> element.
|
|
29
|
+
console.log('Root tag:', doc.documentElement.tagName);
|
|
30
|
+
|
|
31
|
+
// find(selector) returns every matching element under the document or element.
|
|
32
|
+
const posts = doc.find('article.post');
|
|
33
|
+
console.log('Post count:', posts.length);
|
|
34
|
+
|
|
35
|
+
posts.forEach((post, index) => {
|
|
36
|
+
// attr(name) returns undefined when the attribute is missing.
|
|
37
|
+
console.log(`Post ${index + 1}:`, post.attr('data-slug'));
|
|
38
|
+
|
|
39
|
+
// first(selector) returns the first match or null.
|
|
40
|
+
const title = post.first('h1');
|
|
41
|
+
console.log(' title:', title ? title.innerText : '(missing)');
|
|
42
|
+
|
|
43
|
+
// hasClass(name) and hasAttribute(name) are convenience checks.
|
|
44
|
+
console.log(' featured:', post.hasClass('featured'));
|
|
45
|
+
console.log(' has slug:', post.hasAttribute('data-slug'));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// firstOrThrow(selector) is the throwing version of first(selector).
|
|
49
|
+
// Use it when the element is required for the rest of your code.
|
|
50
|
+
const content = doc.firstOrThrow('#content');
|
|
51
|
+
console.log('Main outerHTML starts with:', content.outerHTML.slice(0, 20));
|
|
52
|
+
|
|
53
|
+
// only(selector) returns the match only when exactly one element is found.
|
|
54
|
+
const featuredPost = doc.only('article.featured');
|
|
55
|
+
console.log('Featured slug:', featuredPost.attr_s('data-slug'));
|
|
56
|
+
|
|
57
|
+
// onlyOrThrow(selector) throws unless exactly one element is found.
|
|
58
|
+
try {
|
|
59
|
+
doc.onlyOrThrow('article.post');
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.log('onlyOrThrow on many posts:', error.message);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Element-scoped queries search only inside that element.
|
|
65
|
+
const firstPost = doc.firstOrThrow('article.post');
|
|
66
|
+
console.log('CTA href:', firstPost.firstOrThrow('a.cta').attr_s('href'));
|
|
67
|
+
|
|
68
|
+
// next(selector) and prev(selector) walk element siblings.
|
|
69
|
+
const secondPost = firstPost.next('article.post');
|
|
70
|
+
console.log('Next post slug:', secondPost.attr_s('data-slug'));
|
|
71
|
+
console.log('Previous post slug:', secondPost.prev('article.post').attr_s('data-slug'));
|
|
72
|
+
|
|
73
|
+
// childNodes includes text/whitespace nodes as well as element nodes.
|
|
74
|
+
// nodeType helps distinguish them.
|
|
75
|
+
const childTypes = content.childNodes.map((node) => node.nodeType);
|
|
76
|
+
console.log('Main child node types:', childTypes.join(', '));
|
|
77
|
+
|
|
78
|
+
// parent returns the parent element, or null for the document root.
|
|
79
|
+
console.log('First post parent:', firstPost.parent.tagName);
|
|
80
|
+
console.log('Document root parent:', doc.documentElement.parent);
|
|
81
|
+
|
|
82
|
+
// attr_s(name) throws when an attribute is required but missing.
|
|
83
|
+
try {
|
|
84
|
+
firstPost.attr_s('missing');
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.log('attr_s on missing attribute:', error.message);
|
|
87
|
+
}
|