@d-zero/beholder 3.1.6 → 4.0.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/CHANGELOG.md +10 -0
- package/README.md +38 -3
- package/dist/get-main-contents.d.ts +48 -0
- package/dist/get-main-contents.js +270 -0
- package/dist/index.d.ts +1 -1
- package/dist/is-html-content-type.d.ts +18 -0
- package/dist/is-html-content-type.js +20 -0
- package/dist/measure-scroll-height.d.ts +23 -0
- package/dist/measure-scroll-height.js +44 -0
- package/dist/scraper.js +53 -23
- package/dist/types.d.ts +183 -0
- package/package.json +2 -2
- package/src/get-main-contents.spec.ts +282 -0
- package/src/get-main-contents.ts +306 -0
- package/src/index.ts +11 -0
- package/src/is-html-content-type.spec.ts +17 -0
- package/src/is-html-content-type.ts +20 -0
- package/src/measure-scroll-height.spec.ts +47 -0
- package/src/measure-scroll-height.ts +52 -0
- package/src/scraper.ts +67 -31
- package/src/types.ts +196 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [4.0.0](https://github.com/d-zero-dev/tools/compare/@d-zero/beholder@3.1.6...@d-zero/beholder@4.0.0) (2026-07-21)
|
|
7
|
+
|
|
8
|
+
- feat(beholder)!: enrich PageData with mainContents metrics and scrollHeight ([8a0fc72](https://github.com/d-zero-dev/tools/commit/8a0fc72412295a92850b2c2fa2fa8cd8145eb5f6))
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
- PageData now requires mainContents and scrollHeight fields.
|
|
13
|
+
|
|
14
|
+
Co-authored-by: Cursor <cursoragent@cursor.com>
|
|
15
|
+
|
|
6
16
|
## [3.1.6](https://github.com/d-zero-dev/tools/compare/@d-zero/beholder@3.1.5...@d-zero/beholder@3.1.6) (2026-07-14)
|
|
7
17
|
|
|
8
18
|
**Note:** Version bump only for package @d-zero/beholder
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@d-zero/beholder`
|
|
2
2
|
|
|
3
|
-
Puppeteer の `Page`
|
|
3
|
+
Puppeteer の `Page` を受け取り、単一ページのメタデータ・リンク・画像・ネットワークリソースに加え、メインコンテンツの定量データと desktop/mobile の `scrollHeight` を収集するインプロセス型スクレイパー。結果は `ScrapeResult` として戻り値で返却される(イベント経由ではない)。ブラウザ管理は呼び出し側の責任。
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -26,13 +26,48 @@ const result = await scraper.scrapeStart(page, parseUrl('https://example.com'),
|
|
|
26
26
|
isExternal: false,
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
if (result.type === 'success') {
|
|
30
|
-
console.log(result.pageData
|
|
29
|
+
if (result.type === 'success' && result.pageData) {
|
|
30
|
+
console.log(result.pageData.meta.title);
|
|
31
|
+
console.log(result.pageData.mainContents?.wordCount);
|
|
32
|
+
console.log(result.pageData.scrollHeight);
|
|
31
33
|
}
|
|
32
34
|
```
|
|
33
35
|
|
|
34
36
|
設計判断(イベントではなく戻り値で返す理由、`page` のライフサイクル責務、リトライ機構など)は `src/scraper.ts` の JSDoc を参照。
|
|
35
37
|
|
|
38
|
+
## 取得フロー
|
|
39
|
+
|
|
40
|
+
```mermaid
|
|
41
|
+
flowchart TD
|
|
42
|
+
goto[page.goto]
|
|
43
|
+
ct{content-type is text/html?}
|
|
44
|
+
early[both null]
|
|
45
|
+
html[get HTML]
|
|
46
|
+
ext{isExternal?}
|
|
47
|
+
extOut[title-only; both null]
|
|
48
|
+
idle[networkidle]
|
|
49
|
+
mc["page.evaluate: mainContents"]
|
|
50
|
+
anchors[getAnchorList / getMeta]
|
|
51
|
+
imgs{captureImages?}
|
|
52
|
+
fetchImg["#fetchImages + collect scrollHeight"]
|
|
53
|
+
light["measureScrollHeight light path"]
|
|
54
|
+
done[return PageData]
|
|
55
|
+
|
|
56
|
+
goto --> ct
|
|
57
|
+
ct -->|no| early
|
|
58
|
+
ct -->|yes| html --> ext
|
|
59
|
+
ext -->|yes| extOut
|
|
60
|
+
ext -->|no| idle --> mc --> anchors --> imgs
|
|
61
|
+
imgs -->|yes| fetchImg --> done
|
|
62
|
+
imgs -->|no| light --> done
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 返却データ(`PageData` の追加分)
|
|
66
|
+
|
|
67
|
+
- `mainContents`: メイン領域の定量(`wordCount` / `bodyWordCount`、見出し・画像・表・ボタン・iframe / video / audio / canvas の配列、検出した `main` の識別情報)。メイン未検出時もオブジェクトは返り、配列は空・`wordCount` は 0(非 HTML 等で取得しなかったときだけ `null`)
|
|
68
|
+
- `scrollHeight`: `{ desktop, mobile }`(各 `number | null`)。未計測時はフィールド全体が `null`
|
|
69
|
+
- 配列要素のフィールド詳細は型(`MainContents*` / `ScrollHeightData`)を参照
|
|
70
|
+
|
|
36
71
|
## DOM 文字列からメタ抽出(Puppeteer なし)
|
|
37
72
|
|
|
38
73
|
HTML 文字列を jsdom などでパースしてから `Meta` を取り出したい場合、`extractMetaFromDocument` を使う。`Scraper` が内部で呼ぶ `collectHead → detectTags → classify` パイプラインと同じ実装を再利用するため、戻り値の `Meta` 形状は `scrapeStart` と同一。DOM ライブラリ(jsdom 等)はユーザランドの責務。
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-content quantitative extraction for scraped HTML pages.
|
|
3
|
+
*
|
|
4
|
+
* Runs entirely against a `Document` (Puppeteer page realm or jsdom) so the
|
|
5
|
+
* same logic is unit-tested without a browser. Puppeteer callers use
|
|
6
|
+
* {@link getMainContents}, which passes this function into `page.evaluate`.
|
|
7
|
+
*
|
|
8
|
+
* WHY no `@medv/finder`: selector strings are diagnostic only; a tag+id+class // cspell:disable-line
|
|
9
|
+
* path is enough and avoids a Node-only dependency inside the page realm.
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import type { MainContentsData } from './types.js';
|
|
13
|
+
import type { Page } from 'puppeteer';
|
|
14
|
+
/**
|
|
15
|
+
* Extract main-content metrics from a `Document`.
|
|
16
|
+
*
|
|
17
|
+
* This function must remain free of closure over imports so Puppeteer can
|
|
18
|
+
* serialize it into `page.evaluate`.
|
|
19
|
+
*
|
|
20
|
+
* Argument order matches `page.evaluate(fn, mainContentSelector)`: the selector
|
|
21
|
+
* is the first argument; `doc` defaults to the page-realm global `document`
|
|
22
|
+
* when omitted (jsdom tests pass an explicit `Document` as the second argument).
|
|
23
|
+
* @param mainContentSelector - Optional selector prepended to the default list.
|
|
24
|
+
* @param doc - The document to inspect (defaults to the global `document`).
|
|
25
|
+
* @returns Quantitative main-content data (never `null`; empty when no main region).
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* // In page.evaluate / browser:
|
|
29
|
+
* const data = extractMainContentsFromDocument('#page-body');
|
|
30
|
+
* // In jsdom tests:
|
|
31
|
+
* const data = extractMainContentsFromDocument('#page-body', document);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function extractMainContentsFromDocument(mainContentSelector?: string | null, doc?: Document): MainContentsData;
|
|
35
|
+
/**
|
|
36
|
+
* Extract main-content metrics from a Puppeteer page via a single `page.evaluate`.
|
|
37
|
+
* @param page - Puppeteer page whose DOM has finished loading.
|
|
38
|
+
* @param options - Optional main-content selector override.
|
|
39
|
+
* @param options.mainContentSelector
|
|
40
|
+
* @returns Quantitative main-content data.
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const mainContents = await getMainContents(page, { mainContentSelector: '#page-body' });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function getMainContents(page: Page, options?: {
|
|
47
|
+
mainContentSelector?: string | null;
|
|
48
|
+
}): Promise<MainContentsData>;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-content quantitative extraction for scraped HTML pages.
|
|
3
|
+
*
|
|
4
|
+
* Runs entirely against a `Document` (Puppeteer page realm or jsdom) so the
|
|
5
|
+
* same logic is unit-tested without a browser. Puppeteer callers use
|
|
6
|
+
* {@link getMainContents}, which passes this function into `page.evaluate`.
|
|
7
|
+
*
|
|
8
|
+
* WHY no `@medv/finder`: selector strings are diagnostic only; a tag+id+class // cspell:disable-line
|
|
9
|
+
* path is enough and avoids a Node-only dependency inside the page realm.
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Extract main-content metrics from a `Document`.
|
|
14
|
+
*
|
|
15
|
+
* This function must remain free of closure over imports so Puppeteer can
|
|
16
|
+
* serialize it into `page.evaluate`.
|
|
17
|
+
*
|
|
18
|
+
* Argument order matches `page.evaluate(fn, mainContentSelector)`: the selector
|
|
19
|
+
* is the first argument; `doc` defaults to the page-realm global `document`
|
|
20
|
+
* when omitted (jsdom tests pass an explicit `Document` as the second argument).
|
|
21
|
+
* @param mainContentSelector - Optional selector prepended to the default list.
|
|
22
|
+
* @param doc - The document to inspect (defaults to the global `document`).
|
|
23
|
+
* @returns Quantitative main-content data (never `null`; empty when no main region).
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* // In page.evaluate / browser:
|
|
27
|
+
* const data = extractMainContentsFromDocument('#page-body');
|
|
28
|
+
* // In jsdom tests:
|
|
29
|
+
* const data = extractMainContentsFromDocument('#page-body', document);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function extractMainContentsFromDocument(mainContentSelector = null, doc = document) {
|
|
33
|
+
/**
|
|
34
|
+
* @param text
|
|
35
|
+
*/
|
|
36
|
+
function removeSpaces(text) {
|
|
37
|
+
return text?.trim().replaceAll(/\s+/g, '') || null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @param value
|
|
41
|
+
*/
|
|
42
|
+
function cssEscape(value) {
|
|
43
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
44
|
+
return CSS.escape(value);
|
|
45
|
+
}
|
|
46
|
+
return value.replaceAll(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* @param el
|
|
50
|
+
*/
|
|
51
|
+
function buildSelector(el) {
|
|
52
|
+
const tag = el.nodeName.toLowerCase();
|
|
53
|
+
const id = el.id ? `#${cssEscape(el.id)}` : '';
|
|
54
|
+
const classes = [...el.classList].map((c) => `.${cssEscape(c)}`).join('');
|
|
55
|
+
return `${tag}${id}${classes}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* @param media
|
|
59
|
+
*/
|
|
60
|
+
function resolveMediaSrc(media) {
|
|
61
|
+
if (media.currentSrc) {
|
|
62
|
+
return media.currentSrc;
|
|
63
|
+
}
|
|
64
|
+
const attrSrc = media.getAttribute('src');
|
|
65
|
+
if (attrSrc) {
|
|
66
|
+
try {
|
|
67
|
+
return new URL(attrSrc, doc.baseURI).href;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return attrSrc;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const source = media.querySelector('source[src]');
|
|
74
|
+
const sourceSrc = source?.getAttribute('src');
|
|
75
|
+
if (sourceSrc) {
|
|
76
|
+
try {
|
|
77
|
+
return new URL(sourceSrc, doc.baseURI).href;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return sourceSrc;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
const title = doc.title?.trim() ?? '';
|
|
86
|
+
const bodyWordCount = removeSpaces(doc.body?.textContent ?? null)?.length ?? 0;
|
|
87
|
+
const selectors = [
|
|
88
|
+
'main',
|
|
89
|
+
'[role="main"]',
|
|
90
|
+
'#main',
|
|
91
|
+
'.main',
|
|
92
|
+
'#content',
|
|
93
|
+
'.content',
|
|
94
|
+
'#contents',
|
|
95
|
+
'.contents',
|
|
96
|
+
'#main-content',
|
|
97
|
+
'.main-content',
|
|
98
|
+
'#main_content',
|
|
99
|
+
'.main_content',
|
|
100
|
+
'#mainContent',
|
|
101
|
+
'.mainContent',
|
|
102
|
+
];
|
|
103
|
+
if (mainContentSelector) {
|
|
104
|
+
selectors.unshift(mainContentSelector);
|
|
105
|
+
}
|
|
106
|
+
let $main = null;
|
|
107
|
+
try {
|
|
108
|
+
$main = doc.querySelector(selectors.join(','));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Invalid custom selector: retry without it so built-in selectors still run.
|
|
112
|
+
if (mainContentSelector) {
|
|
113
|
+
selectors.shift();
|
|
114
|
+
$main = doc.querySelector(selectors.join(','));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!$main) {
|
|
118
|
+
const fallbackSelectors = [
|
|
119
|
+
'[id*="main" i]',
|
|
120
|
+
'[class*="main" i]',
|
|
121
|
+
'[id*="content" i]',
|
|
122
|
+
'[class*="content" i]',
|
|
123
|
+
];
|
|
124
|
+
for (const sel of fallbackSelectors) {
|
|
125
|
+
const candidate = doc.querySelector(sel);
|
|
126
|
+
if (candidate && candidate !== doc.body && candidate !== doc.documentElement) {
|
|
127
|
+
$main = candidate;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!$main) {
|
|
133
|
+
return {
|
|
134
|
+
title,
|
|
135
|
+
main: null,
|
|
136
|
+
wordCount: 0,
|
|
137
|
+
bodyWordCount,
|
|
138
|
+
headings: [],
|
|
139
|
+
images: [],
|
|
140
|
+
tables: [],
|
|
141
|
+
buttons: [],
|
|
142
|
+
iframes: [],
|
|
143
|
+
videos: [],
|
|
144
|
+
audios: [],
|
|
145
|
+
canvases: [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const headings = [];
|
|
149
|
+
for (const $heading of $main.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
|
150
|
+
headings.push({
|
|
151
|
+
text: removeSpaces($heading.textContent),
|
|
152
|
+
level: Number.parseInt($heading.nodeName.replace(/h/i, ''), 10),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const images = [];
|
|
156
|
+
for (const $img of $main.querySelectorAll('img, input[type="image"]')) {
|
|
157
|
+
images.push({
|
|
158
|
+
src: $img.src,
|
|
159
|
+
alt: $img.alt,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const tables = [];
|
|
163
|
+
for (const $table of $main.querySelectorAll('table')) {
|
|
164
|
+
tables.push({
|
|
165
|
+
rows: $table.querySelectorAll('tr').length,
|
|
166
|
+
cols: $table.querySelector('tr')?.querySelectorAll('th, td').length || 0,
|
|
167
|
+
hasHeader: !!$table.querySelector('thead'),
|
|
168
|
+
hasFooter: !!$table.querySelector('tfoot'),
|
|
169
|
+
hasMergedCell: !!$table.querySelector('[colspan], [rowspan]'),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const win = doc.defaultView;
|
|
173
|
+
const HTMLButton = win?.HTMLButtonElement;
|
|
174
|
+
const HTMLInput = win?.HTMLInputElement;
|
|
175
|
+
const buttons = [];
|
|
176
|
+
for (const $el of $main.querySelectorAll('button, [role="button"], [class*="button"], [class*="btn"]')) {
|
|
177
|
+
const isButton = HTMLButton ? $el instanceof HTMLButton : $el.nodeName === 'BUTTON';
|
|
178
|
+
const isInput = HTMLInput ? $el instanceof HTMLInput : $el.nodeName === 'INPUT';
|
|
179
|
+
const isFormControl = isButton || isInput;
|
|
180
|
+
let text = removeSpaces($el.textContent);
|
|
181
|
+
if (!text && isInput) {
|
|
182
|
+
text = removeSpaces($el.getAttribute('value'));
|
|
183
|
+
}
|
|
184
|
+
const disabledProp = isFormControl && 'disabled' in $el
|
|
185
|
+
? Boolean($el.disabled)
|
|
186
|
+
: false;
|
|
187
|
+
buttons.push({
|
|
188
|
+
nodeName: $el.nodeName,
|
|
189
|
+
role: $el.getAttribute('role'),
|
|
190
|
+
type: isFormControl ? $el.type : null,
|
|
191
|
+
text,
|
|
192
|
+
disabled: disabledProp || $el.getAttribute('aria-disabled') === 'true',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const iframes = [];
|
|
196
|
+
for (const $el of $main.querySelectorAll('iframe')) {
|
|
197
|
+
iframes.push({
|
|
198
|
+
src: $el.src,
|
|
199
|
+
title: $el.hasAttribute('title') ? $el.getAttribute('title') : null,
|
|
200
|
+
width: $el.getAttribute('width'),
|
|
201
|
+
height: $el.getAttribute('height'),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const videos = [];
|
|
205
|
+
for (const $el of $main.querySelectorAll('video')) {
|
|
206
|
+
const posterAttr = $el.getAttribute('poster');
|
|
207
|
+
let poster = null;
|
|
208
|
+
if (posterAttr) {
|
|
209
|
+
try {
|
|
210
|
+
poster = new URL(posterAttr, doc.baseURI).href;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
poster = posterAttr;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
videos.push({
|
|
217
|
+
src: resolveMediaSrc($el),
|
|
218
|
+
poster,
|
|
219
|
+
width: $el.width,
|
|
220
|
+
height: $el.height,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const audios = [];
|
|
224
|
+
for (const $el of $main.querySelectorAll('audio')) {
|
|
225
|
+
audios.push({
|
|
226
|
+
src: resolveMediaSrc($el),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const canvases = [];
|
|
230
|
+
for (const $el of $main.querySelectorAll('canvas')) {
|
|
231
|
+
canvases.push({
|
|
232
|
+
width: $el.width,
|
|
233
|
+
height: $el.height,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
title,
|
|
238
|
+
main: {
|
|
239
|
+
nodeName: $main.nodeName,
|
|
240
|
+
id: $main.id || null,
|
|
241
|
+
classList: [...$main.classList],
|
|
242
|
+
role: $main.getAttribute('role'),
|
|
243
|
+
selector: buildSelector($main),
|
|
244
|
+
},
|
|
245
|
+
wordCount: removeSpaces($main.textContent)?.length ?? 0,
|
|
246
|
+
bodyWordCount,
|
|
247
|
+
headings,
|
|
248
|
+
images,
|
|
249
|
+
tables,
|
|
250
|
+
buttons,
|
|
251
|
+
iframes,
|
|
252
|
+
videos,
|
|
253
|
+
audios,
|
|
254
|
+
canvases,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Extract main-content metrics from a Puppeteer page via a single `page.evaluate`.
|
|
259
|
+
* @param page - Puppeteer page whose DOM has finished loading.
|
|
260
|
+
* @param options - Optional main-content selector override.
|
|
261
|
+
* @param options.mainContentSelector
|
|
262
|
+
* @returns Quantitative main-content data.
|
|
263
|
+
* @example
|
|
264
|
+
* ```ts
|
|
265
|
+
* const mainContents = await getMainContents(page, { mainContentSelector: '#page-body' });
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
export async function getMainContents(page, options) {
|
|
269
|
+
return page.evaluate(extractMainContentsFromDocument, options?.mainContentSelector ?? null);
|
|
270
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,4 +20,4 @@ export { detectCDN } from '@d-zero/shared/detect-cdn';
|
|
|
20
20
|
export type { CDNType } from '@d-zero/shared/detect-cdn';
|
|
21
21
|
export type { ScrapeResult, ResourceEntry, PageData } from './types.js';
|
|
22
22
|
export type { ScraperOptions, ChangePhaseEvent, ScraperEventTypes } from './types.js';
|
|
23
|
-
export type { Resource, AnchorData, Meta, ImageElement, SkippedPageData, NetworkLog, OpenGraphMeta, OgArticleMeta, OgBookMeta, OgProfileMeta, OgMusicMeta, OgVideoNsMeta, TwitterMeta, FbMeta, FediverseMeta, AppleMeta, MsApplicationMeta, VerificationMeta, GoogleMeta, GeoMeta, CitationMeta, RdfaMeta, MicrodataMeta, AmpMeta, LegacyMeta, MobileMeta, MicroformatsMeta, PinterestMeta, SlackMeta, LinkedInMeta, ExperimentalMeta, WikiMeta, LinkMeta, LinkEntry, JsonLdEntry, OthersBucket, ScriptEntry, IframeEntry, TagsMeta, TagDetail, TagEntry, TagSource, ViewportMeta, RobotsMeta, ReferrerMeta, FormatDetectionMeta, HttpEquivMeta, HttpEquivRefresh, RawHeadEntry, } from './types.js';
|
|
23
|
+
export type { Resource, AnchorData, Meta, ImageElement, SkippedPageData, NetworkLog, ScrollHeightData, MainContentsData, MainContentsMainTag, MainContentsHeading, MainContentsImage, MainContentsTable, MainContentsButton, MainContentsIframe, MainContentsVideo, MainContentsAudio, MainContentsCanvas, OpenGraphMeta, OgArticleMeta, OgBookMeta, OgProfileMeta, OgMusicMeta, OgVideoNsMeta, TwitterMeta, FbMeta, FediverseMeta, AppleMeta, MsApplicationMeta, VerificationMeta, GoogleMeta, GeoMeta, CitationMeta, RdfaMeta, MicrodataMeta, AmpMeta, LegacyMeta, MobileMeta, MicroformatsMeta, PinterestMeta, SlackMeta, LinkedInMeta, ExperimentalMeta, WikiMeta, LinkMeta, LinkEntry, JsonLdEntry, OthersBucket, ScriptEntry, IframeEntry, TagsMeta, TagDetail, TagEntry, TagSource, ViewportMeta, RobotsMeta, ReferrerMeta, FormatDetectionMeta, HttpEquivMeta, HttpEquivRefresh, RawHeadEntry, } from './types.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determine whether a Content-Type media type is HTML.
|
|
3
|
+
*
|
|
4
|
+
* MIME types are case-insensitive (RFC 2045). Surrounding whitespace after
|
|
5
|
+
* parameter stripping is also tolerated so `text/html `, `Text/HTML`, and
|
|
6
|
+
* `text/html; charset=utf-8` (after `;` split) all classify as HTML.
|
|
7
|
+
* @param contentType - The media type portion of a Content-Type header
|
|
8
|
+
* (parameters already stripped), or `null` when unknown.
|
|
9
|
+
* @returns `true` when the media type is `text/html` in any letter case.
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* isHtmlContentType('text/html'); // true
|
|
13
|
+
* isHtmlContentType('Text/HTML'); // true
|
|
14
|
+
* isHtmlContentType('application/pdf'); // false
|
|
15
|
+
* // Callers should strip parameters first (e.g. split on ';') before calling.
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function isHtmlContentType(contentType: string | null): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determine whether a Content-Type media type is HTML.
|
|
3
|
+
*
|
|
4
|
+
* MIME types are case-insensitive (RFC 2045). Surrounding whitespace after
|
|
5
|
+
* parameter stripping is also tolerated so `text/html `, `Text/HTML`, and
|
|
6
|
+
* `text/html; charset=utf-8` (after `;` split) all classify as HTML.
|
|
7
|
+
* @param contentType - The media type portion of a Content-Type header
|
|
8
|
+
* (parameters already stripped), or `null` when unknown.
|
|
9
|
+
* @returns `true` when the media type is `text/html` in any letter case.
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* isHtmlContentType('text/html'); // true
|
|
13
|
+
* isHtmlContentType('Text/HTML'); // true
|
|
14
|
+
* isHtmlContentType('application/pdf'); // false
|
|
15
|
+
* // Callers should strip parameters first (e.g. split on ';') before calling.
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function isHtmlContentType(contentType) {
|
|
19
|
+
return contentType !== null && contentType.trim().toLowerCase() === 'text/html';
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight scrollHeight measurement without `scrollAllOver`.
|
|
3
|
+
*
|
|
4
|
+
* Used when `captureImages` is false so callers still receive desktop/mobile
|
|
5
|
+
* heights without the cost of full-page scrolling for image extraction.
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import type { ScrollHeightData } from './types.js';
|
|
9
|
+
import type { Page } from 'puppeteer';
|
|
10
|
+
/**
|
|
11
|
+
* Measure `document.body.scrollHeight` at desktop-compact and mobile-small viewports.
|
|
12
|
+
*
|
|
13
|
+
* Sets each viewport, reads height once, and does **not** call `scrollAllOver`.
|
|
14
|
+
* A failed preset yields `null` for that side only.
|
|
15
|
+
* @param page - Puppeteer page whose DOM has finished loading.
|
|
16
|
+
* @returns Heights for desktop and mobile presets.
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const scrollHeight = await measureScrollHeight(page);
|
|
20
|
+
* // { desktop: 2400, mobile: 5200 }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function measureScrollHeight(page: Page): Promise<ScrollHeightData>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight scrollHeight measurement without `scrollAllOver`.
|
|
3
|
+
*
|
|
4
|
+
* Used when `captureImages` is false so callers still receive desktop/mobile
|
|
5
|
+
* heights without the cost of full-page scrolling for image extraction.
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { devicePresets } from '@d-zero/puppeteer-page-scan';
|
|
9
|
+
/**
|
|
10
|
+
* Measure `document.body.scrollHeight` at desktop-compact and mobile-small viewports.
|
|
11
|
+
*
|
|
12
|
+
* Sets each viewport, reads height once, and does **not** call `scrollAllOver`.
|
|
13
|
+
* A failed preset yields `null` for that side only.
|
|
14
|
+
* @param page - Puppeteer page whose DOM has finished loading.
|
|
15
|
+
* @returns Heights for desktop and mobile presets.
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const scrollHeight = await measureScrollHeight(page);
|
|
19
|
+
* // { desktop: 2400, mobile: 5200 }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export async function measureScrollHeight(page) {
|
|
23
|
+
const desktop = await measureAtPreset(page, 'desktop-compact');
|
|
24
|
+
const mobile = await measureAtPreset(page, 'mobile-small');
|
|
25
|
+
return { desktop, mobile };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* @param page
|
|
29
|
+
* @param key
|
|
30
|
+
*/
|
|
31
|
+
async function measureAtPreset(page, key) {
|
|
32
|
+
const preset = devicePresets[key];
|
|
33
|
+
try {
|
|
34
|
+
await page.setViewport({
|
|
35
|
+
width: preset.width,
|
|
36
|
+
height: 800,
|
|
37
|
+
deviceScaleFactor: preset.resolution ?? 1,
|
|
38
|
+
});
|
|
39
|
+
return await page.evaluate(() => document.body.scrollHeight);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|