chrome-cdp-cli 1.7.8 → 1.8.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/dist/cli/CLIInterface.js
CHANGED
|
@@ -373,6 +373,9 @@ class CLIInterface {
|
|
|
373
373
|
}
|
|
374
374
|
if (result.data && typeof result.data === 'object') {
|
|
375
375
|
const data = result.data;
|
|
376
|
+
if (data.snapshot && typeof data.snapshot === 'string') {
|
|
377
|
+
return data.snapshot;
|
|
378
|
+
}
|
|
376
379
|
if (data.messages && Array.isArray(data.messages)) {
|
|
377
380
|
output += dataSourceInfo;
|
|
378
381
|
if (data.messages.length === 0) {
|
|
@@ -47,6 +47,20 @@ class TakeSnapshotHandler {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
if (snapshotArgs.format !== 'html' && typeof processedSnapshot === 'object' && processedSnapshot !== null) {
|
|
51
|
+
const snapshotObj = processedSnapshot;
|
|
52
|
+
if (snapshotObj.snapshot && typeof snapshotObj.snapshot === 'string') {
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
data: {
|
|
56
|
+
snapshot: snapshotObj.snapshot,
|
|
57
|
+
format: 'text',
|
|
58
|
+
documentsCount: response.documents.length,
|
|
59
|
+
nodesCount: response.documents[0]?.nodes?.nodeName?.length || 0
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
50
64
|
return {
|
|
51
65
|
success: true,
|
|
52
66
|
data: {
|
|
@@ -62,29 +76,24 @@ class TakeSnapshotHandler {
|
|
|
62
76
|
if (!docResponse || !docResponse.root) {
|
|
63
77
|
throw new Error('Failed to get document root');
|
|
64
78
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
if (!htmlResponse || !htmlResponse.outerHTML) {
|
|
69
|
-
throw new Error('Failed to get document HTML');
|
|
70
|
-
}
|
|
79
|
+
const url = await this.getCurrentURL(client);
|
|
80
|
+
const title = await this.getCurrentTitle(client);
|
|
71
81
|
let processedSnapshot;
|
|
72
82
|
if (snapshotArgs.format === 'html') {
|
|
83
|
+
const htmlResponse = await client.send('DOM.getOuterHTML', {
|
|
84
|
+
nodeId: docResponse.root.nodeId
|
|
85
|
+
});
|
|
86
|
+
if (!htmlResponse || !htmlResponse.outerHTML) {
|
|
87
|
+
throw new Error('Failed to get document HTML');
|
|
88
|
+
}
|
|
73
89
|
processedSnapshot = htmlResponse.outerHTML;
|
|
74
90
|
}
|
|
75
91
|
else {
|
|
92
|
+
const textSnapshot = this.buildTextFromDOMNode(docResponse.root, url, title);
|
|
76
93
|
processedSnapshot = {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
documentsCount: 1
|
|
81
|
-
},
|
|
82
|
-
documents: [{
|
|
83
|
-
url: await this.getCurrentURL(client),
|
|
84
|
-
title: await this.getCurrentTitle(client),
|
|
85
|
-
html: htmlResponse.outerHTML,
|
|
86
|
-
domTree: docResponse.root
|
|
87
|
-
}]
|
|
94
|
+
url,
|
|
95
|
+
title,
|
|
96
|
+
snapshot: textSnapshot
|
|
88
97
|
};
|
|
89
98
|
}
|
|
90
99
|
if (snapshotArgs.filename) {
|
|
@@ -98,6 +107,18 @@ class TakeSnapshotHandler {
|
|
|
98
107
|
}
|
|
99
108
|
};
|
|
100
109
|
}
|
|
110
|
+
if (snapshotArgs.format !== 'html' && typeof processedSnapshot === 'object' && processedSnapshot !== null) {
|
|
111
|
+
const snapshotObj = processedSnapshot;
|
|
112
|
+
if (snapshotObj.snapshot && typeof snapshotObj.snapshot === 'string') {
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
data: {
|
|
116
|
+
snapshot: snapshotObj.snapshot,
|
|
117
|
+
format: 'text'
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
101
122
|
return {
|
|
102
123
|
success: true,
|
|
103
124
|
data: {
|
|
@@ -141,25 +162,418 @@ class TakeSnapshotHandler {
|
|
|
141
162
|
if (args.format === 'html') {
|
|
142
163
|
return this.convertToHTML(response);
|
|
143
164
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
165
|
+
const doc = response.documents[0];
|
|
166
|
+
if (!doc) {
|
|
167
|
+
return {
|
|
168
|
+
error: 'No documents found'
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const textSnapshot = this.createTextSnapshot(doc, response.strings);
|
|
172
|
+
const result = {
|
|
173
|
+
url: doc.documentURL,
|
|
174
|
+
title: doc.title,
|
|
175
|
+
snapshot: textSnapshot
|
|
176
|
+
};
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
createTextSnapshot(doc, strings) {
|
|
180
|
+
const nodes = doc.nodes;
|
|
181
|
+
if (!nodes.nodeName || !nodes.nodeType) {
|
|
182
|
+
return 'Empty document';
|
|
183
|
+
}
|
|
184
|
+
const nodeTree = this.buildNodeTree(doc, strings);
|
|
185
|
+
let output = `PAGE: ${doc.title || 'Untitled'}\n`;
|
|
186
|
+
const bodyNode = this.findBodyNode(nodeTree);
|
|
187
|
+
if (bodyNode) {
|
|
188
|
+
output += this.renderNodeAsText(bodyNode, 0);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
for (const node of nodeTree) {
|
|
192
|
+
if (this.shouldIncludeNode(node)) {
|
|
193
|
+
output += this.renderNodeAsText(node, 0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return output;
|
|
198
|
+
}
|
|
199
|
+
buildNodeTree(doc, strings) {
|
|
200
|
+
const nodes = doc.nodes;
|
|
201
|
+
const nodeCount = nodes.nodeName.length;
|
|
202
|
+
const nodeMap = new Map();
|
|
203
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
204
|
+
const node = {
|
|
205
|
+
index: i,
|
|
206
|
+
nodeType: nodes.nodeType[i],
|
|
207
|
+
nodeName: nodes.nodeName[i].toLowerCase(),
|
|
208
|
+
children: [],
|
|
209
|
+
textContent: '',
|
|
210
|
+
attributes: {}
|
|
211
|
+
};
|
|
212
|
+
if (nodes.nodeValue?.[i]) {
|
|
213
|
+
node.textContent = nodes.nodeValue[i].trim();
|
|
214
|
+
}
|
|
215
|
+
if (nodes.textValue?.index.includes(i)) {
|
|
216
|
+
const textIndex = nodes.textValue.index.indexOf(i);
|
|
217
|
+
const stringIndex = nodes.textValue.value[textIndex];
|
|
218
|
+
if (typeof stringIndex === 'number' && strings[stringIndex]) {
|
|
219
|
+
node.textContent = strings[stringIndex].trim();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (nodes.attributes?.[i]) {
|
|
223
|
+
const attrs = nodes.attributes[i];
|
|
224
|
+
for (let j = 0; j < attrs.length; j += 2) {
|
|
225
|
+
const name = strings[parseInt(attrs[j])];
|
|
226
|
+
const value = strings[parseInt(attrs[j + 1])];
|
|
227
|
+
if (['id', 'class', 'type', 'name', 'href', 'src', 'alt', 'placeholder', 'value', 'title'].includes(name)) {
|
|
228
|
+
node.attributes[name] = value;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (nodes.inputValue?.index.includes(i)) {
|
|
233
|
+
const inputIndex = nodes.inputValue.index.indexOf(i);
|
|
234
|
+
const stringIndex = nodes.inputValue.value[inputIndex];
|
|
235
|
+
if (typeof stringIndex === 'number') {
|
|
236
|
+
node.inputValue = strings[stringIndex];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (nodes.inputChecked?.index.includes(i)) {
|
|
240
|
+
node.checked = true;
|
|
241
|
+
}
|
|
242
|
+
if (nodes.optionSelected?.index.includes(i)) {
|
|
243
|
+
node.selected = true;
|
|
244
|
+
}
|
|
245
|
+
if (nodes.isClickable?.index.includes(i)) {
|
|
246
|
+
node.clickable = true;
|
|
247
|
+
}
|
|
248
|
+
nodeMap.set(i, node);
|
|
249
|
+
}
|
|
250
|
+
const tree = [];
|
|
251
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
252
|
+
const parentIndex = nodes.parentIndex?.[i];
|
|
253
|
+
const node = nodeMap.get(i);
|
|
254
|
+
if (parentIndex !== undefined && parentIndex >= 0) {
|
|
255
|
+
const parent = nodeMap.get(parentIndex);
|
|
256
|
+
if (parent) {
|
|
257
|
+
parent.children.push(node);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
tree.push(node);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return tree;
|
|
265
|
+
}
|
|
266
|
+
findBodyNode(nodeTree) {
|
|
267
|
+
const findNode = (nodes, tagName) => {
|
|
268
|
+
for (const node of nodes) {
|
|
269
|
+
if (node.nodeName === tagName) {
|
|
270
|
+
return node;
|
|
271
|
+
}
|
|
272
|
+
const found = findNode(node.children, tagName);
|
|
273
|
+
if (found)
|
|
274
|
+
return found;
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
162
277
|
};
|
|
278
|
+
return findNode(nodeTree, 'body') || findNode(nodeTree, 'main') || null;
|
|
279
|
+
}
|
|
280
|
+
shouldIncludeNode(node) {
|
|
281
|
+
const skipTags = ['script', 'style', 'meta', 'link', 'head', 'noscript'];
|
|
282
|
+
if (skipTags.includes(node.nodeName)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
if (node.nodeType === 3) {
|
|
286
|
+
const text = (node.textContent || '').trim();
|
|
287
|
+
return text.length > 0;
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
renderNodeAsText(node, depth, isLast = false, parentIsLast = []) {
|
|
292
|
+
if (!this.shouldIncludeNode(node)) {
|
|
293
|
+
return '';
|
|
294
|
+
}
|
|
295
|
+
let indent = '';
|
|
296
|
+
for (let i = 0; i < parentIsLast.length; i++) {
|
|
297
|
+
indent += parentIsLast[i] ? ' ' : '│ ';
|
|
298
|
+
}
|
|
299
|
+
const prefix = depth > 0 ? (isLast ? '└── ' : '├── ') : '';
|
|
300
|
+
let output = '';
|
|
301
|
+
if (node.nodeType === 3) {
|
|
302
|
+
if (node.textContent) {
|
|
303
|
+
const truncatedText = this.truncateText(node.textContent.trim(), 40);
|
|
304
|
+
output += `${indent}${prefix}"${truncatedText}"\n`;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else if (node.nodeType === 1) {
|
|
308
|
+
const tag = node.nodeName.toUpperCase();
|
|
309
|
+
let description = tag;
|
|
310
|
+
const attrs = [];
|
|
311
|
+
if (node.attributes.id)
|
|
312
|
+
attrs.push(`#${node.attributes.id}`);
|
|
313
|
+
if (node.attributes.class) {
|
|
314
|
+
const classes = node.attributes.class.split(/\s+/).filter((c) => c.trim().length > 0);
|
|
315
|
+
classes.forEach((cls) => {
|
|
316
|
+
attrs.push(`.${cls.trim()}`);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (node.attributes.type)
|
|
320
|
+
attrs.push(`[${node.attributes.type}]`);
|
|
321
|
+
if (node.attributes.name)
|
|
322
|
+
attrs.push(`name="${node.attributes.name}"`);
|
|
323
|
+
if (attrs.length > 0) {
|
|
324
|
+
description += `(${attrs.join(' ')})`;
|
|
325
|
+
}
|
|
326
|
+
if (node.nodeName === 'img' && node.attributes.alt) {
|
|
327
|
+
const altText = this.truncateText(node.attributes.alt, 40);
|
|
328
|
+
description += `: "${altText}"`;
|
|
329
|
+
}
|
|
330
|
+
else if (node.nodeName === 'a' && node.attributes.href) {
|
|
331
|
+
description += `: "${node.attributes.href}"`;
|
|
332
|
+
}
|
|
333
|
+
else if (['input', 'textarea'].includes(node.nodeName)) {
|
|
334
|
+
if (node.attributes.placeholder) {
|
|
335
|
+
const placeholderText = this.truncateText(node.attributes.placeholder, 40);
|
|
336
|
+
description += `: "${placeholderText}"`;
|
|
337
|
+
}
|
|
338
|
+
else if (node.inputValue) {
|
|
339
|
+
const inputText = this.truncateText(node.inputValue, 40);
|
|
340
|
+
description += `: "${inputText}"`;
|
|
341
|
+
}
|
|
342
|
+
else if (node.nodeName === 'textarea') {
|
|
343
|
+
const textContent = this.extractTextContent(node);
|
|
344
|
+
if (textContent) {
|
|
345
|
+
const truncatedText = this.truncateText(textContent, 40);
|
|
346
|
+
description += `: "${truncatedText}"`;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (node.checked)
|
|
350
|
+
description += ' [checked]';
|
|
351
|
+
}
|
|
352
|
+
else if (node.nodeName === 'button' || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.nodeName)) {
|
|
353
|
+
const textContent = this.extractTextContent(node);
|
|
354
|
+
if (textContent) {
|
|
355
|
+
const truncatedText = this.truncateText(textContent, 40);
|
|
356
|
+
description += `: "${truncatedText}"`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
output += `${indent}${prefix}${description}\n`;
|
|
360
|
+
if (node.nodeName === 'textarea') {
|
|
361
|
+
return output;
|
|
362
|
+
}
|
|
363
|
+
const meaningfulChildren = node.children.filter((child) => this.shouldIncludeNode(child));
|
|
364
|
+
if (meaningfulChildren.length === 0) {
|
|
365
|
+
const textContent = this.extractTextContent(node);
|
|
366
|
+
if (textContent && textContent.trim().length > 0) {
|
|
367
|
+
const truncatedText = this.truncateText(textContent.trim(), 40);
|
|
368
|
+
output += `${indent}│ └── "${truncatedText}"\n`;
|
|
369
|
+
return output;
|
|
370
|
+
}
|
|
371
|
+
return output;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const newParentIsLast = [...parentIsLast, isLast];
|
|
375
|
+
for (let i = 0; i < meaningfulChildren.length; i++) {
|
|
376
|
+
const child = meaningfulChildren[i];
|
|
377
|
+
const childIsLast = i === meaningfulChildren.length - 1;
|
|
378
|
+
output += this.renderNodeAsText(child, depth + 1, childIsLast, newParentIsLast);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return output;
|
|
383
|
+
}
|
|
384
|
+
extractTextContent(node) {
|
|
385
|
+
let text = '';
|
|
386
|
+
if (node.nodeType === 3) {
|
|
387
|
+
return node.textContent || '';
|
|
388
|
+
}
|
|
389
|
+
if (node.textContent) {
|
|
390
|
+
text += node.textContent + ' ';
|
|
391
|
+
}
|
|
392
|
+
for (const child of node.children) {
|
|
393
|
+
text += this.extractTextContent(child) + ' ';
|
|
394
|
+
}
|
|
395
|
+
return text.trim().replace(/\s+/g, ' ');
|
|
396
|
+
}
|
|
397
|
+
truncateText(text, maxLength) {
|
|
398
|
+
if (!text)
|
|
399
|
+
return '';
|
|
400
|
+
if (text.length <= maxLength)
|
|
401
|
+
return text;
|
|
402
|
+
return text.substring(0, maxLength) + '...';
|
|
403
|
+
}
|
|
404
|
+
buildTextFromDOMNode(root, _url, title) {
|
|
405
|
+
let output = `PAGE: ${title || 'Untitled'}\n`;
|
|
406
|
+
const bodyNode = this.findBodyInDOMTree(root);
|
|
407
|
+
if (bodyNode) {
|
|
408
|
+
output += this.renderDOMNodeAsText(bodyNode, 0);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
if (root.children) {
|
|
412
|
+
for (const child of root.children) {
|
|
413
|
+
if (this.shouldIncludeDOMNode(child)) {
|
|
414
|
+
output += this.renderDOMNodeAsText(child, 0);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return output;
|
|
420
|
+
}
|
|
421
|
+
findBodyInDOMTree(node) {
|
|
422
|
+
if (node.nodeName && node.nodeName.toLowerCase() === 'body') {
|
|
423
|
+
return node;
|
|
424
|
+
}
|
|
425
|
+
if (node.nodeName && node.nodeName.toLowerCase() === 'main') {
|
|
426
|
+
return node;
|
|
427
|
+
}
|
|
428
|
+
if (node.children) {
|
|
429
|
+
for (const child of node.children) {
|
|
430
|
+
const found = this.findBodyInDOMTree(child);
|
|
431
|
+
if (found)
|
|
432
|
+
return found;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
shouldIncludeDOMNode(node) {
|
|
438
|
+
if (!node)
|
|
439
|
+
return false;
|
|
440
|
+
const nodeName = node.nodeName ? node.nodeName.toLowerCase() : '';
|
|
441
|
+
const skipTags = ['script', 'style', 'meta', 'link', 'head', 'noscript'];
|
|
442
|
+
if (skipTags.includes(nodeName)) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
if (node.nodeType === 3) {
|
|
446
|
+
const text = node.nodeValue || '';
|
|
447
|
+
return text.trim().length > 0;
|
|
448
|
+
}
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
renderDOMNodeAsText(node, depth, isLast = false, parentIsLast = []) {
|
|
452
|
+
if (!this.shouldIncludeDOMNode(node)) {
|
|
453
|
+
return '';
|
|
454
|
+
}
|
|
455
|
+
let indent = '';
|
|
456
|
+
for (let i = 0; i < parentIsLast.length; i++) {
|
|
457
|
+
indent += parentIsLast[i] ? ' ' : '│ ';
|
|
458
|
+
}
|
|
459
|
+
const prefix = depth > 0 ? (isLast ? '└── ' : '├── ') : '';
|
|
460
|
+
let output = '';
|
|
461
|
+
if (node.nodeType === 3) {
|
|
462
|
+
const text = (node.nodeValue || '').trim();
|
|
463
|
+
if (text) {
|
|
464
|
+
const truncatedText = this.truncateText(text, 40);
|
|
465
|
+
output += `${indent}${prefix}"${truncatedText}"\n`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (node.nodeType === 1) {
|
|
469
|
+
const tag = (node.nodeName || '').toUpperCase();
|
|
470
|
+
let description = tag;
|
|
471
|
+
const attrs = [];
|
|
472
|
+
if (node.attributes) {
|
|
473
|
+
for (let i = 0; i < node.attributes.length; i += 2) {
|
|
474
|
+
const name = node.attributes[i];
|
|
475
|
+
const value = node.attributes[i + 1];
|
|
476
|
+
if (['id', 'class', 'type', 'name', 'href', 'src', 'alt', 'placeholder', 'value', 'title'].includes(name)) {
|
|
477
|
+
if (name === 'id')
|
|
478
|
+
attrs.push(`#${value}`);
|
|
479
|
+
else if (name === 'class') {
|
|
480
|
+
const classes = value.split(/\s+/).filter((c) => c.trim().length > 0);
|
|
481
|
+
classes.forEach((cls) => {
|
|
482
|
+
attrs.push(`.${cls.trim()}`);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
else if (name === 'type')
|
|
486
|
+
attrs.push(`[${value}]`);
|
|
487
|
+
else
|
|
488
|
+
attrs.push(`${name}="${value}"`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (attrs.length > 0) {
|
|
493
|
+
description += `(${attrs.join(' ')})`;
|
|
494
|
+
}
|
|
495
|
+
const nodeName = (node.nodeName || '').toLowerCase();
|
|
496
|
+
if (nodeName === 'img' && node.attributes) {
|
|
497
|
+
const altIndex = node.attributes.indexOf('alt');
|
|
498
|
+
if (altIndex >= 0 && altIndex + 1 < node.attributes.length) {
|
|
499
|
+
const altText = this.truncateText(node.attributes[altIndex + 1], 40);
|
|
500
|
+
description += `: "${altText}"`;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else if (nodeName === 'a' && node.attributes) {
|
|
504
|
+
const hrefIndex = node.attributes.indexOf('href');
|
|
505
|
+
if (hrefIndex >= 0 && hrefIndex + 1 < node.attributes.length) {
|
|
506
|
+
description += `: "${node.attributes[hrefIndex + 1]}"`;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else if (['input', 'textarea'].includes(nodeName)) {
|
|
510
|
+
if (node.attributes) {
|
|
511
|
+
const placeholderIndex = node.attributes.indexOf('placeholder');
|
|
512
|
+
if (placeholderIndex >= 0 && placeholderIndex + 1 < node.attributes.length) {
|
|
513
|
+
const placeholderText = this.truncateText(node.attributes[placeholderIndex + 1], 40);
|
|
514
|
+
description += `: "${placeholderText}"`;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (nodeName === 'textarea') {
|
|
518
|
+
const textContent = this.extractTextFromDOMNode(node);
|
|
519
|
+
if (textContent) {
|
|
520
|
+
const truncatedText = this.truncateText(textContent, 40);
|
|
521
|
+
description += `: "${truncatedText}"`;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else if (nodeName === 'button' || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(nodeName)) {
|
|
526
|
+
const textContent = this.extractTextFromDOMNode(node);
|
|
527
|
+
if (textContent) {
|
|
528
|
+
const truncatedText = this.truncateText(textContent, 40);
|
|
529
|
+
description += `: "${truncatedText}"`;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
output += `${indent}${prefix}${description}\n`;
|
|
533
|
+
if (nodeName === 'textarea') {
|
|
534
|
+
return output;
|
|
535
|
+
}
|
|
536
|
+
if (node.children) {
|
|
537
|
+
const meaningfulChildren = node.children.filter((child) => this.shouldIncludeDOMNode(child));
|
|
538
|
+
if (meaningfulChildren.length === 0) {
|
|
539
|
+
const textContent = this.extractTextFromDOMNode(node);
|
|
540
|
+
if (textContent && textContent.trim().length > 0) {
|
|
541
|
+
const truncatedText = this.truncateText(textContent.trim(), 40);
|
|
542
|
+
output += `${indent}│ └── "${truncatedText}"\n`;
|
|
543
|
+
return output;
|
|
544
|
+
}
|
|
545
|
+
return output;
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
const newParentIsLast = [...parentIsLast, isLast];
|
|
549
|
+
for (let i = 0; i < meaningfulChildren.length; i++) {
|
|
550
|
+
const child = meaningfulChildren[i];
|
|
551
|
+
const childIsLast = i === meaningfulChildren.length - 1;
|
|
552
|
+
output += this.renderDOMNodeAsText(child, depth + 1, childIsLast, newParentIsLast);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return output;
|
|
558
|
+
}
|
|
559
|
+
extractTextFromDOMNode(node) {
|
|
560
|
+
if (node.nodeType === 3) {
|
|
561
|
+
return (node.nodeValue || '').trim();
|
|
562
|
+
}
|
|
563
|
+
let text = '';
|
|
564
|
+
if (node.children) {
|
|
565
|
+
for (const child of node.children) {
|
|
566
|
+
text += this.extractTextFromDOMNode(child) + ' ';
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return text.trim().replace(/\s+/g, ' ');
|
|
570
|
+
}
|
|
571
|
+
convertToHTML(response) {
|
|
572
|
+
const doc = response.documents[0];
|
|
573
|
+
if (!doc)
|
|
574
|
+
return '';
|
|
575
|
+
const tree = this.buildDOMTree(doc, response.strings);
|
|
576
|
+
return this.renderNodeAsHTML(tree[0], 0);
|
|
163
577
|
}
|
|
164
578
|
buildDOMTree(doc, strings) {
|
|
165
579
|
const nodes = doc.nodes;
|
|
@@ -227,27 +641,6 @@ class TakeSnapshotHandler {
|
|
|
227
641
|
}
|
|
228
642
|
return tree;
|
|
229
643
|
}
|
|
230
|
-
processLayoutInfo(layout, strings) {
|
|
231
|
-
return {
|
|
232
|
-
nodeCount: layout.nodeIndex.length,
|
|
233
|
-
styles: layout.styles.map(styleArray => styleArray.map(styleIndex => strings[parseInt(styleIndex)])),
|
|
234
|
-
bounds: layout.bounds,
|
|
235
|
-
text: layout.text,
|
|
236
|
-
paintOrders: layout.paintOrders,
|
|
237
|
-
offsetRects: layout.offsetRects,
|
|
238
|
-
scrollRects: layout.scrollRects,
|
|
239
|
-
clientRects: layout.clientRects,
|
|
240
|
-
blendedBackgroundColors: layout.blendedBackgroundColors,
|
|
241
|
-
textColorOpacities: layout.textColorOpacities
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
convertToHTML(response) {
|
|
245
|
-
const doc = response.documents[0];
|
|
246
|
-
if (!doc)
|
|
247
|
-
return '';
|
|
248
|
-
const tree = this.buildDOMTree(doc, response.strings);
|
|
249
|
-
return this.renderNodeAsHTML(tree[0], 0);
|
|
250
|
-
}
|
|
251
644
|
renderNodeAsHTML(node, depth) {
|
|
252
645
|
if (!node)
|
|
253
646
|
return '';
|
|
@@ -312,7 +705,7 @@ class TakeSnapshotHandler {
|
|
|
312
705
|
if (snapshotArgs.filename !== undefined && typeof snapshotArgs.filename !== 'string') {
|
|
313
706
|
return false;
|
|
314
707
|
}
|
|
315
|
-
if (snapshotArgs.format !== undefined && !['json', 'html'].includes(snapshotArgs.format)) {
|
|
708
|
+
if (snapshotArgs.format !== undefined && !['json', 'html', 'text'].includes(snapshotArgs.format)) {
|
|
316
709
|
return false;
|
|
317
710
|
}
|
|
318
711
|
if (snapshotArgs.includeStyles !== undefined && typeof snapshotArgs.includeStyles !== 'boolean') {
|
|
@@ -331,47 +724,58 @@ class TakeSnapshotHandler {
|
|
|
331
724
|
}
|
|
332
725
|
getHelp() {
|
|
333
726
|
return `
|
|
334
|
-
snapshot - Capture a
|
|
727
|
+
snapshot - Capture a text-based DOM representation optimized for LLM understanding
|
|
335
728
|
|
|
336
729
|
Usage:
|
|
337
730
|
snapshot
|
|
338
|
-
snapshot --filename
|
|
731
|
+
snapshot --filename page-structure.txt
|
|
339
732
|
snapshot --format html --filename page-structure.html
|
|
340
|
-
snapshot --include-paint-order --filename detailed-snapshot.json
|
|
341
733
|
|
|
342
734
|
Arguments:
|
|
343
735
|
--filename <path> Output filename (if not provided, returns data directly)
|
|
344
|
-
--format <json|html> Output format (default: json)
|
|
736
|
+
--format <json|html> Output format (default: json with text snapshot)
|
|
345
737
|
--include-styles Include computed styles (default: true)
|
|
346
738
|
--include-attributes Include DOM attributes (default: true)
|
|
347
739
|
--include-paint-order Include paint order information (default: false)
|
|
348
|
-
--include-text-index Include
|
|
740
|
+
--include-text-index Include additional DOM tree data (default: false)
|
|
741
|
+
|
|
742
|
+
Output Format:
|
|
743
|
+
The default JSON output contains a text-based representation of the page structure
|
|
744
|
+
that LLMs can easily understand, showing:
|
|
745
|
+
- Page hierarchy with indentation
|
|
746
|
+
- Element types and key attributes
|
|
747
|
+
- Text content and form values
|
|
748
|
+
- Interactive elements (buttons, links, forms)
|
|
749
|
+
- Semantic structure (headings, lists, etc.)
|
|
349
750
|
|
|
350
|
-
Output
|
|
351
|
-
|
|
352
|
-
|
|
751
|
+
Example Output:
|
|
752
|
+
PAGE: Example Site (https://example.com)
|
|
753
|
+
├── HEADER
|
|
754
|
+
│ ├── IMG(#logo): "Company Logo"
|
|
755
|
+
│ └── NAV
|
|
756
|
+
│ ├── A: "https://example.com/home"
|
|
757
|
+
│ └── "Home"
|
|
758
|
+
├── MAIN
|
|
759
|
+
│ ├── H1: "Welcome to Our Site"
|
|
760
|
+
│ ├── P
|
|
761
|
+
│ │ └── "This is the main content..."
|
|
762
|
+
│ └── FORM
|
|
763
|
+
│ ├── INPUT[email](name="email"): "Enter your email"
|
|
764
|
+
│ └── BUTTON: "Submit"
|
|
353
765
|
|
|
354
766
|
Examples:
|
|
355
|
-
#
|
|
356
|
-
snapshot
|
|
767
|
+
# Get text-based page structure
|
|
768
|
+
snapshot
|
|
769
|
+
|
|
770
|
+
# Save to file
|
|
771
|
+
snapshot --filename page-structure.json
|
|
357
772
|
|
|
358
773
|
# HTML representation
|
|
359
774
|
snapshot --format html --filename page-structure.html
|
|
360
775
|
|
|
361
|
-
# Detailed snapshot with paint order
|
|
362
|
-
snapshot --include-paint-order --filename detailed-snapshot.json
|
|
363
|
-
|
|
364
|
-
# Return data directly (no file)
|
|
365
|
-
snapshot --format json
|
|
366
|
-
|
|
367
776
|
Note:
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
- Computed CSS styles for each element
|
|
371
|
-
- Layout information (bounds, positioning)
|
|
372
|
-
- Text content and form values
|
|
373
|
-
- Visibility and clickability information
|
|
374
|
-
- Paint order and rendering details (optional)
|
|
777
|
+
This format provides a "text screenshot" that LLMs can easily parse to understand
|
|
778
|
+
page layout, content, and interactive elements without overwhelming detail.
|
|
375
779
|
`;
|
|
376
780
|
}
|
|
377
781
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-cdp-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Browser automation CLI via Chrome DevTools Protocol. Designed for developers and AI assistants - combines dedicated commands for common tasks with flexible JavaScript execution for complex scenarios. Features: element interaction, screenshots, DOM snapshots, console/network monitoring. Built-in IDE integration for Cursor and Claude.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|