dsh-browser-playwright-codex 0.5.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/CHANGELOG.md +486 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/SECURITY.md +114 -0
- package/cordis.patch.yml +66 -0
- package/lib/cdp-policy.d.ts +69 -0
- package/lib/cdp-policy.js +105 -0
- package/lib/compat.d.ts +37 -0
- package/lib/compat.js +31 -0
- package/lib/contract.d.ts +74 -0
- package/lib/contract.js +117 -0
- package/lib/errors.d.ts +25 -0
- package/lib/errors.js +21 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +12 -0
- package/lib/injected.d.ts +35 -0
- package/lib/injected.js +350 -0
- package/lib/playwright.d.ts +647 -0
- package/lib/playwright.js +2515 -0
- package/lib/runtime-state.d.ts +45 -0
- package/lib/runtime-state.js +181 -0
- package/lib/service.d.ts +56 -0
- package/lib/service.js +96 -0
- package/lib/snapshot-aria.d.ts +121 -0
- package/lib/snapshot-aria.js +485 -0
- package/lib/snapshot-render.d.ts +28 -0
- package/lib/snapshot-render.js +100 -0
- package/lib/tool.d.ts +77 -0
- package/lib/tool.js +907 -0
- package/lib/types.d.ts +242 -0
- package/lib/types.js +5 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +108 -0
- package/scripts/analysis/hl-compare.mjs +149 -0
- package/scripts/analysis/live-handoff-probe.mjs +97 -0
- package/scripts/analysis/needs-human-probe.mjs +83 -0
- package/scripts/analysis/repro-minimize.mjs +112 -0
- package/scripts/autocommit.ps1 +23 -0
- package/scripts/check-doc-links.py +112 -0
- package/scripts/doctor.mjs +144 -0
- package/scripts/dsh-browser-verify.mjs +475 -0
- package/scripts/measure-prompt-cost.mjs +57 -0
- package/scripts/prompt-cost.mjs +73 -0
- package/scripts/shipped-gates.mjs +43 -0
package/lib/injected.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser-side snapshot script. Installed with page.addInitScript and
|
|
3
|
+
* invoked as window.__dshSnapshot(opts). The returned value crosses the
|
|
4
|
+
* evaluate boundary as plain JSON.
|
|
5
|
+
*/
|
|
6
|
+
export const SNAPSHOT_SCRIPT = String.raw `
|
|
7
|
+
;(() => {
|
|
8
|
+
if (window.__dshSnapshot) return // one install per document
|
|
9
|
+
// Refs are stable for an element's whole lifetime and unique across the
|
|
10
|
+
// session: the per-document nonce separates documents (navigations), and
|
|
11
|
+
// the per-document sequence never reuses a number. Re-rendered elements
|
|
12
|
+
// are new nodes, so they mint new refs — a stale ref can never silently
|
|
13
|
+
// match a different element.
|
|
14
|
+
const DOC_NONCE = String(Math.floor(Math.random() * 1e9)).padStart(9, '0')
|
|
15
|
+
let refSeq = 0
|
|
16
|
+
|
|
17
|
+
const SKIP_TAGS = new Set(['script', 'style', 'noscript', 'template', 'meta', 'link', 'head', 'br', 'hr', 'svg'])
|
|
18
|
+
const INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'tab', 'switch', 'combobox', 'listbox', 'option', 'textbox', 'searchbox', 'slider', 'spinbutton', 'row', 'gridcell'])
|
|
19
|
+
const ROLE_BY_TAG = {
|
|
20
|
+
a: 'link', button: 'button', select: 'combobox', textarea: 'textarea', img: 'img',
|
|
21
|
+
nav: 'navigation', main: 'main', footer: 'contentinfo', header: 'banner',
|
|
22
|
+
h1: 'heading', h2: 'heading', h3: 'heading', h4: 'heading', h5: 'heading', h6: 'heading',
|
|
23
|
+
ul: 'list', ol: 'list', li: 'listitem', option: 'option', table: 'table', tr: 'row', td: 'cell',
|
|
24
|
+
th: 'columnheader', form: 'form', dialog: 'dialog', article: 'article',
|
|
25
|
+
aside: 'complementary', figure: 'figure', iframe: 'iframe', summary: 'summary',
|
|
26
|
+
section: 'region', p: 'paragraph',
|
|
27
|
+
}
|
|
28
|
+
const INPUT_ROLE = { checkbox: 'checkbox', radio: 'radio', button: 'button', submit: 'button', reset: 'button', image: 'button', range: 'slider', search: 'searchbox', hidden: null }
|
|
29
|
+
/** input types that label themselves with their value instead of their text. */
|
|
30
|
+
const INPUT_BUTTON_TYPES = new Set(['button', 'submit', 'reset'])
|
|
31
|
+
// Interactive roles whose DESCENDANTS are content, not a label: a <select>'s
|
|
32
|
+
// options or a table row's cells must not be pasted together into the name.
|
|
33
|
+
const CONTENT_ROLES = new Set(['combobox', 'listbox', 'row', 'gridcell'])
|
|
34
|
+
|
|
35
|
+
function roleOf(el) {
|
|
36
|
+
const explicit = el.getAttribute('role')
|
|
37
|
+
if (explicit && explicit.trim()) return explicit.trim().toLowerCase()
|
|
38
|
+
const tag = el.tagName.toLowerCase()
|
|
39
|
+
if (tag === 'input') return INPUT_ROLE[(el.getAttribute('type') || 'text').toLowerCase()] || 'textbox'
|
|
40
|
+
return ROLE_BY_TAG[tag] || 'generic'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isVisible(el) {
|
|
44
|
+
if (!(el instanceof Element)) return false
|
|
45
|
+
if (el.getAttribute('hidden') !== null || el.getAttribute('aria-hidden') === 'true') return false
|
|
46
|
+
const style = getComputedStyle(el)
|
|
47
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false
|
|
48
|
+
// A 'display: contents' box has no rect, but its children lay out
|
|
49
|
+
// normally: keep it, or the whole subtree is pruned with it.
|
|
50
|
+
if (style.display === 'contents') return true
|
|
51
|
+
const rect = el.getBoundingClientRect()
|
|
52
|
+
return rect.width > 0 || rect.height > 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isActionable(el, role) {
|
|
56
|
+
if (el.getAttribute('data-dsh-force-ref') !== null) return true
|
|
57
|
+
const tag = el.tagName.toLowerCase()
|
|
58
|
+
if (tag === 'a' || tag === 'button' || tag === 'select' || tag === 'textarea' || tag === 'option' || tag === 'summary') return true
|
|
59
|
+
if (tag === 'input' && role !== null) return true
|
|
60
|
+
if (INTERACTIVE_ROLES.has(role)) return true
|
|
61
|
+
if (el.hasAttribute('tabindex') || el.hasAttribute('contenteditable')) return true
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function clip(text, max) {
|
|
66
|
+
return text.length > max ? text.slice(0, max) + '…' : text
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ownText(el, maxText) {
|
|
70
|
+
let out = ''
|
|
71
|
+
for (const node of el.childNodes) {
|
|
72
|
+
if (node.nodeType === 3) out += node.textContent
|
|
73
|
+
else if (node.nodeType === 1 && node.tagName.toLowerCase() === 'br') out += ' '
|
|
74
|
+
}
|
|
75
|
+
const flat = out.replace(/\s+/g, ' ').trim()
|
|
76
|
+
return flat.length > maxText ? flat.slice(0, maxText) + '…' : flat
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function nameOf(el, maxName, maxText, actionable) {
|
|
80
|
+
const aria = el.getAttribute('aria-label')
|
|
81
|
+
if (aria && aria.trim()) return clip(aria.trim(), maxName)
|
|
82
|
+
if (el.id) {
|
|
83
|
+
try {
|
|
84
|
+
const labelEl = document.querySelector('label[for="' + CSS.escape(el.id) + '"]')
|
|
85
|
+
if (labelEl) {
|
|
86
|
+
const t = (labelEl.textContent || '').replace(/\s+/g, ' ').trim()
|
|
87
|
+
if (t) return clip(t, maxName)
|
|
88
|
+
}
|
|
89
|
+
} catch (_) { /* invalid id selector */ }
|
|
90
|
+
}
|
|
91
|
+
const labelEl = el.closest('label')
|
|
92
|
+
if (labelEl) {
|
|
93
|
+
const t = (labelEl.textContent || '').replace(/\s+/g, ' ').trim()
|
|
94
|
+
if (t) return clip(t, maxName)
|
|
95
|
+
}
|
|
96
|
+
const alt = el.getAttribute('alt')
|
|
97
|
+
if (alt && alt.trim()) return clip(alt.trim(), maxName)
|
|
98
|
+
const placeholder = el.getAttribute('placeholder')
|
|
99
|
+
if (placeholder && placeholder.trim()) return clip(placeholder.trim(), maxName)
|
|
100
|
+
const title = el.getAttribute('title')
|
|
101
|
+
if (title && title.trim()) return clip(title.trim(), maxName)
|
|
102
|
+
const tag = el.tagName.toLowerCase()
|
|
103
|
+
// Button-like controls label themselves with their value: <button>提交</button>
|
|
104
|
+
// and <input type="button|submit|reset" value="提交">. Without this an input
|
|
105
|
+
// button renders as a bare ref with no name.
|
|
106
|
+
// An input button labels itself with its value; a <button> is labelled by
|
|
107
|
+
// its text, which the interactive path below collects from the subtree.
|
|
108
|
+
if (tag === 'input' && INPUT_BUTTON_TYPES.has((el.getAttribute('type') || '').toLowerCase())) {
|
|
109
|
+
const value = (el.getAttribute('value') || el.value || '').trim()
|
|
110
|
+
if (value) return clip(value, maxName)
|
|
111
|
+
}
|
|
112
|
+
// Free-text inputs show their LIVE value (el.value, not the stale HTML
|
|
113
|
+
// attribute) so a snapshot after fill reflects what was typed. Password
|
|
114
|
+
// fields are skipped for privacy; fixed-value types (checkbox "on",
|
|
115
|
+
// hidden, range, color) keep their accessible name only.
|
|
116
|
+
if (tag === 'input' || tag === 'textarea') {
|
|
117
|
+
const type = tag === 'input' ? (el.getAttribute('type') || 'text').toLowerCase() : ''
|
|
118
|
+
const freeText = tag === 'textarea'
|
|
119
|
+
|| ['text', 'search', 'tel', 'url', 'email', 'number', 'date', 'time', 'datetime-local', 'month', 'week'].includes(type)
|
|
120
|
+
if (freeText) {
|
|
121
|
+
const value = el.value
|
|
122
|
+
if (value && value.trim()) return clip(value.trim(), maxName)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Non-interactive content elements carry their own direct text as the name.
|
|
126
|
+
// An interactive element's accessible name is its whole subtree text, which
|
|
127
|
+
// buildNode collects next; returning '' hands it that job.
|
|
128
|
+
if (actionable) return ''
|
|
129
|
+
return clip(ownText(el, maxText), maxName)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Bounded name taken from an element's DESCENDANTS, used only when an
|
|
134
|
+
* actionable element has no text of its own. Shared markup patterns put the
|
|
135
|
+
* visible label one level down — <a><span>搜索设置</span></a>,
|
|
136
|
+
* <button><span>提交</span></button>, <a><img alt="首页"></a> — and without
|
|
137
|
+
* this fallback those nodes render as a bare [ref=...] with no name, so the
|
|
138
|
+
* model cannot tell which ref carries the label it can see. Depth-first,
|
|
139
|
+
* skips script/style/hidden subtrees, prefers text and falls back to
|
|
140
|
+
* aria-label/alt/title, and is capped so a huge card cannot bloat the tree.
|
|
141
|
+
*/
|
|
142
|
+
function descendantName(el, maxName, maxText) {
|
|
143
|
+
const texts = []
|
|
144
|
+
const labels = []
|
|
145
|
+
let total = 0
|
|
146
|
+
let visited = 0
|
|
147
|
+
const walk = (node, depth) => {
|
|
148
|
+
if (total >= maxText || depth > 6 || visited > 400) return
|
|
149
|
+
for (const child of node.childNodes) {
|
|
150
|
+
if (total >= maxText || visited > 400) return
|
|
151
|
+
if (child.nodeType === 3) {
|
|
152
|
+
const text = (child.textContent || '').replace(/\s+/g, ' ').trim()
|
|
153
|
+
if (text) {
|
|
154
|
+
texts.push(text)
|
|
155
|
+
total += text.length + 1
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (child.nodeType === 1) {
|
|
159
|
+
const tag = child.tagName.toLowerCase()
|
|
160
|
+
if (SKIP_TAGS.has(tag)) continue
|
|
161
|
+
if (child.getAttribute('hidden') !== null || child.getAttribute('aria-hidden') === 'true') continue
|
|
162
|
+
visited += 1
|
|
163
|
+
walk(child, depth + 1)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const collectLabel = (node, depth) => {
|
|
168
|
+
if (labels.length > 0 || depth > 6) return
|
|
169
|
+
for (const child of node.children) {
|
|
170
|
+
if (labels.length > 0) return
|
|
171
|
+
if (!isVisible(child)) continue
|
|
172
|
+
const aria = child.getAttribute('aria-label')
|
|
173
|
+
const alt = child.getAttribute('alt')
|
|
174
|
+
const title = child.getAttribute('title')
|
|
175
|
+
const label = (aria && aria.trim()) || (alt && alt.trim()) || (title && title.trim())
|
|
176
|
+
if (label) {
|
|
177
|
+
labels.push(label)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
collectLabel(child, depth + 1)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
walk(el, 0)
|
|
184
|
+
if (texts.length > 0) return clip(texts.join(' ').replace(/\s+/g, ' ').trim(), maxName)
|
|
185
|
+
collectLabel(el, 0)
|
|
186
|
+
return labels.length > 0 ? clip(labels[0], maxName) : ''
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function assignRef(el, state) {
|
|
190
|
+
state.refCount += 1
|
|
191
|
+
// Reuse the ref minted for this element by an earlier snapshot of this
|
|
192
|
+
// document, so refs stay stable across snapshots while the DOM is intact.
|
|
193
|
+
const existing = el.getAttribute('data-dsh-ref')
|
|
194
|
+
if (existing !== null) return existing
|
|
195
|
+
const ref = 'e' + DOC_NONCE + String(++refSeq)
|
|
196
|
+
try { el.setAttribute('data-dsh-ref', ref) } catch (_) { /* foreign objects */ }
|
|
197
|
+
return ref
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildNode(el, opts, state, inheritedName) {
|
|
201
|
+
const role = roleOf(el) || 'generic'
|
|
202
|
+
const actionable = isActionable(el, role)
|
|
203
|
+
// Nearest ancestor name, used to fold a child that only repeats it.
|
|
204
|
+
const inherited = typeof inheritedName === 'string' ? inheritedName : ''
|
|
205
|
+
if (opts.interactiveOnly && !actionable) {
|
|
206
|
+
const kids = []
|
|
207
|
+
for (const child of el.children) {
|
|
208
|
+
if (SKIP_TAGS.has(child.tagName.toLowerCase())) continue
|
|
209
|
+
if (!isVisible(child)) continue
|
|
210
|
+
const node = buildNode(child, opts, state, inherited)
|
|
211
|
+
if (node) kids.push(node)
|
|
212
|
+
}
|
|
213
|
+
return kids.length ? { role: 'generic', name: '', children: kids } : null
|
|
214
|
+
}
|
|
215
|
+
state.count += 1
|
|
216
|
+
if (state.count > opts.maxNodes) {
|
|
217
|
+
state.truncated = true
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
let nodeName = nameOf(el, opts.maxNameLength, opts.maxTextLength, actionable)
|
|
221
|
+
if (nodeName === '' && actionable && !CONTENT_ROLES.has(role)) nodeName = descendantName(el, opts.maxNameLength, opts.maxTextLength)
|
|
222
|
+
// A button with no text of its own may still label itself with its value.
|
|
223
|
+
if (nodeName === '' && actionable) {
|
|
224
|
+
const controlTag = el.tagName.toLowerCase()
|
|
225
|
+
if (controlTag === 'button' || (controlTag === 'input' && INPUT_BUTTON_TYPES.has((el.getAttribute('type') || '').toLowerCase()))) {
|
|
226
|
+
const fallbackValue = (el.getAttribute('value') || el.value || '').trim()
|
|
227
|
+
if (fallbackValue) nodeName = clip(fallbackValue, opts.maxNameLength)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// A <select> shows its current value the way a text input does: the chosen
|
|
231
|
+
// option, not every option it happens to contain.
|
|
232
|
+
if (nodeName === '' && role === 'combobox' && el.tagName.toLowerCase() === 'select' && el.selectedIndex >= 0) {
|
|
233
|
+
const chosen = el.options[el.selectedIndex]
|
|
234
|
+
if (chosen) nodeName = clip((chosen.textContent || '').replace(/\s+/g, ' ').trim(), opts.maxNameLength)
|
|
235
|
+
}
|
|
236
|
+
const childInherited = nodeName !== '' ? nodeName : inherited
|
|
237
|
+
const node = { role, name: nodeName }
|
|
238
|
+
if (actionable) node.ref = assignRef(el, state)
|
|
239
|
+
if (role === 'heading') {
|
|
240
|
+
const m = /^h([1-6])$/.exec(el.tagName.toLowerCase())
|
|
241
|
+
if (m) node.level = Number(m[1])
|
|
242
|
+
}
|
|
243
|
+
if (el.tagName.toLowerCase() === 'input' && (role === 'checkbox' || role === 'radio')) node.checked = !!el.checked
|
|
244
|
+
if (role === 'option') node.selected = !!el.selected
|
|
245
|
+
if (el.disabled) node.disabled = true
|
|
246
|
+
if (role === 'link') {
|
|
247
|
+
const href = el.getAttribute('href')
|
|
248
|
+
if (href && href.trim() && href.indexOf('javascript:') !== 0) node.href = href
|
|
249
|
+
}
|
|
250
|
+
if (role === 'iframe') {
|
|
251
|
+
try {
|
|
252
|
+
if (el.contentDocument && el.contentDocument.body && state.depth < 2) {
|
|
253
|
+
state.depth += 1
|
|
254
|
+
const kids = []
|
|
255
|
+
for (const child of el.contentDocument.body.children) {
|
|
256
|
+
if (SKIP_TAGS.has(child.tagName.toLowerCase())) continue
|
|
257
|
+
const sub = buildNode(child, opts, state)
|
|
258
|
+
if (sub) kids.push(sub)
|
|
259
|
+
}
|
|
260
|
+
state.depth -= 1
|
|
261
|
+
node.children = kids
|
|
262
|
+
return node
|
|
263
|
+
}
|
|
264
|
+
} catch (_) { /* cross-origin frame */ }
|
|
265
|
+
node.children = []
|
|
266
|
+
if (!node.name) node.name = '(frame)'
|
|
267
|
+
return node
|
|
268
|
+
}
|
|
269
|
+
const kids = []
|
|
270
|
+
for (const child of el.children) {
|
|
271
|
+
if (SKIP_TAGS.has(child.tagName.toLowerCase())) continue
|
|
272
|
+
// Collapsed native <select> options have zero layout rects but stay
|
|
273
|
+
// part of the element's accessibility tree: include them regardless.
|
|
274
|
+
const isOption = child.tagName.toLowerCase() === 'option' && el.tagName.toLowerCase() === 'select'
|
|
275
|
+
if (!isOption && !isVisible(child)) continue
|
|
276
|
+
const sub = buildNode(child, opts, state, childInherited)
|
|
277
|
+
if (!sub) continue
|
|
278
|
+
// Fold a child that merely repeats the name an ancestor already carries
|
|
279
|
+
// (e.g. <a><span>搜索设置</span></a>): hoist its subtree and drop the
|
|
280
|
+
// duplicate line. A ref-bearing child is a distinct handle and stays.
|
|
281
|
+
if (sub.ref === undefined && sub.name.length >= 2 && childInherited !== '' && childInherited.indexOf(sub.name) !== -1) {
|
|
282
|
+
for (const grand of sub.children || []) kids.push(grand)
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
kids.push(sub)
|
|
286
|
+
}
|
|
287
|
+
node.children = kids
|
|
288
|
+
if (!actionable && !node.name && !kids.length && !node.checked && !node.selected && !node.disabled) return null
|
|
289
|
+
return node
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
window.__dshSnapshot = (opts) => {
|
|
293
|
+
const o = {
|
|
294
|
+
interactiveOnly: !!opts.interactiveOnly,
|
|
295
|
+
maxNodes: opts.maxNodes || 500,
|
|
296
|
+
maxNameLength: opts.maxNameLength || 120,
|
|
297
|
+
maxTextLength: opts.maxTextLength || 300,
|
|
298
|
+
}
|
|
299
|
+
const state = { count: 0, truncated: false, depth: 0, refCount: 0 }
|
|
300
|
+
const nodes = []
|
|
301
|
+
if (document.body) {
|
|
302
|
+
for (const child of document.body.children) {
|
|
303
|
+
if (SKIP_TAGS.has(child.tagName.toLowerCase())) continue
|
|
304
|
+
if (!isVisible(child)) continue
|
|
305
|
+
const node = buildNode(child, o, state)
|
|
306
|
+
if (node) nodes.push(node)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { nodes, truncated: state.truncated, totalRefs: state.refCount }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
window.__dshPageData = (opts) => {
|
|
313
|
+
const o = { maxTextChars: opts.maxTextChars || 30000, maxLinks: opts.maxLinks || 300, maxInputs: opts.maxInputs || 200 }
|
|
314
|
+
const text = (document.body ? document.body.innerText : '').replace(/\n{3,}/g, '\n\n').trim()
|
|
315
|
+
const links = []
|
|
316
|
+
for (const a of document.querySelectorAll('a[href]')) {
|
|
317
|
+
if (links.length >= o.maxLinks) break
|
|
318
|
+
const label = (a.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 200)
|
|
319
|
+
links.push({ text: label, href: a.getAttribute('href') })
|
|
320
|
+
}
|
|
321
|
+
const inputs = []
|
|
322
|
+
for (const el of document.querySelectorAll('input, select, textarea')) {
|
|
323
|
+
if (inputs.length >= o.maxInputs) break
|
|
324
|
+
const tag = el.tagName.toLowerCase()
|
|
325
|
+
let label = ''
|
|
326
|
+
if (el.id) {
|
|
327
|
+
try {
|
|
328
|
+
const labelEl = document.querySelector('label[for="' + CSS.escape(el.id) + '"]')
|
|
329
|
+
if (labelEl) label = (labelEl.textContent || '').replace(/\s+/g, ' ').trim()
|
|
330
|
+
} catch (_) { /* ignore */ }
|
|
331
|
+
}
|
|
332
|
+
if (!label) {
|
|
333
|
+
const labelEl = el.closest('label')
|
|
334
|
+
if (labelEl) label = (labelEl.textContent || '').replace(/\s+/g, ' ').trim()
|
|
335
|
+
}
|
|
336
|
+
let value = el.value || ''
|
|
337
|
+
if (tag === 'select' && el.selectedOptions && el.selectedOptions[0]) value = el.selectedOptions[0].textContent
|
|
338
|
+
inputs.push({ tag, type: el.getAttribute('type') || '', name: el.getAttribute('name') || '', value, label: label.slice(0, 200), checked: tag === 'input' ? !!el.checked : null })
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
url: location.href,
|
|
342
|
+
title: document.title,
|
|
343
|
+
text: text.length > o.maxTextChars ? text.slice(0, o.maxTextChars) + '…' : text,
|
|
344
|
+
truncated: text.length > o.maxTextChars,
|
|
345
|
+
links,
|
|
346
|
+
inputs,
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
})();
|
|
350
|
+
`;
|