1pub const GET_DOM_JS: &str = r#"
5(function(selector) {
6 const root = selector ? document.querySelector(selector) : document.documentElement;
7 if (!root) return null;
8 if (typeof root.getHTML === 'function') {
9 try { return root.getHTML({ serializableShadowRoots: true }); } catch (e) {}
10 }
11 return root.outerHTML;
12})
13"#;
14
15pub const GET_CLIP_RECT_JS: &str = r#"
19(function(selector) {
20 const el = document.querySelector(selector);
21 if (!el) return null;
22 el.scrollIntoView({ block: 'center', inline: 'center' });
23 const r = el.getBoundingClientRect();
24 if (r.width === 0 || r.height === 0) return null;
25 return {
26 x: r.left + window.scrollX,
27 y: r.top + window.scrollY,
28 width: r.width,
29 height: r.height,
30 };
31})
32"#;
33
34pub const SELECT_ELEMENT_JS: &str = r#"
36(function() {
37 function cssPath(el) {
38 if (!(el instanceof Element)) return '';
39 const path = [];
40 while (el && el.nodeType === 1) {
41 let selector = el.nodeName.toLowerCase();
42 if (el.id) { selector += '#' + el.id; path.unshift(selector); break; }
43 else {
44 let sib = el, nth = 1;
45 while ((sib = sib.previousElementSibling)) { if (sib.nodeName === el.nodeName) nth++; }
46 selector += ':nth-of-type(' + nth + ')';
47 }
48 path.unshift(selector);
49 el = el.parentNode;
50 }
51 return path.join(' > ');
52 }
53 return new Promise((resolve) => {
54 const overlay = document.createElement('div');
55 overlay.style.cssText = 'position:fixed;inset:0;z-index:2147483647;cursor:crosshair;background:rgba(0,150,255,0.05);';
56 document.body.appendChild(overlay);
57 overlay.addEventListener('click', (e) => {
58 e.preventDefault(); e.stopPropagation();
59 overlay.remove();
60 const x = e.clientX, y = e.clientY;
61 const el = document.elementFromPoint(x, y);
62 resolve(cssPath(el));
63 }, { capture: true, once: true });
64 });
65})()
66"#;
67
68pub const GET_PAGE_TEXT_JS: &str = r#"
78(function(maxChars, selector) {
79 const doc = document;
80 const SKIP = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG', 'CANVAS', 'IFRAME', 'OBJECT', 'EMBED', 'INPUT', 'TEXTAREA', 'SELECT']);
81 const CHROME = new Set(['NAV', 'HEADER', 'FOOTER', 'ASIDE']);
82 const CHROME_ROLES = new Set(['navigation', 'banner', 'contentinfo', 'complementary']);
83 const BLOCK = new Set(['P', 'DIV', 'LI', 'TR', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SECTION', 'ARTICLE', 'BLOCKQUOTE', 'PRE', 'DT', 'DD', 'BR', 'HR', 'UL', 'OL', 'TABLE', 'MAIN', 'FORM', 'FIGURE', 'FIGCAPTION', 'DETAILS', 'SUMMARY', 'LABEL', 'OPTION', 'TD', 'TH']);
84 function visible(el) {
85 if (el.hidden || el.getAttribute('aria-hidden') === 'true') return false;
86 try { if (typeof el.checkVisibility === 'function') return el.checkVisibility(); } catch (e) {}
87 const cs = getComputedStyle(el);
88 return cs.display !== 'none' && cs.visibility !== 'hidden';
89 }
90 let root = null, source = 'body';
91 if (selector) {
92 root = doc.querySelector(selector);
93 if (!root) return JSON.stringify({ error: 'selector matched no element: ' + selector });
94 source = 'selector';
95 } else {
96 root = doc.querySelector('main, [role="main"], article');
97 if (root) {
98 source = root.tagName === 'ARTICLE' ? 'article' : 'main';
99 } else {
100 let best = null, bestScore = 0;
101 for (const el of doc.querySelectorAll('article, section, div')) {
102 const len = (el.innerText || '').length;
103 if (len < 200) continue;
104 let linkLen = 0;
105 for (const a of el.querySelectorAll('a')) linkLen += (a.innerText || '').length;
106 const score = len * (1 - Math.min(1, linkLen / len));
107 if (score > bestScore) { bestScore = score; best = el; }
108 }
109 if (best) { root = best; source = 'heuristic'; }
110 else root = doc.body || doc.documentElement;
111 }
112 }
113 const parts = [];
114 function walk(node, isRoot) {
115 if (node.nodeType === 3) {
116 const t = node.nodeValue;
117 if (t && t.trim()) parts.push(t.replace(/\s+/g, ' '));
118 return;
119 }
120 if (node.nodeType !== 1) return;
121 const tag = node.tagName;
122 if (SKIP.has(tag)) return;
123 if (!isRoot && (CHROME.has(tag) || CHROME_ROLES.has(node.getAttribute('role')))) return;
124 if (!visible(node)) return;
125 const block = BLOCK.has(tag);
126 if (block) parts.push('\n');
127 if (/^H[1-6]$/.test(tag)) parts.push('#'.repeat(+tag[1]) + ' ');
128 else if (tag === 'LI') parts.push('- ');
129 const kids = node.shadowRoot ? node.shadowRoot.childNodes : node.childNodes;
130 for (const c of kids) walk(c, false);
131 if (tag === 'TD' || tag === 'TH') parts.push(' | ');
132 if (block) parts.push('\n');
133 }
134 walk(root, true);
135 let text = parts.join('')
136 .replace(/[ \t]+\n/g, '\n')
137 .replace(/\n[ \t]+/g, '\n')
138 .replace(/[ \t]{2,}/g, ' ')
139 .replace(/\n{3,}/g, '\n\n')
140 .trim();
141 const total = text.length;
142 let truncated = false;
143 if (text.length > maxChars) {
144 let cut = text.lastIndexOf('\n', maxChars);
145 if (cut < maxChars / 2) cut = maxChars;
146 text = text.slice(0, cut);
147 truncated = true;
148 }
149 return JSON.stringify({ title: doc.title, url: location.href, source, text, truncated, total_chars: total });
150})
151"#;
152
153pub const SELECT_ALL_JS: &str = r#"
159(function(clear) {
160 const el = this;
161 const tag = (el.tagName || '').toLowerCase();
162 if (tag === 'input' || tag === 'textarea') {
163 if (clear) {
164 el.value = '';
165 el.dispatchEvent(new Event('input', { bubbles: true }));
166 el.dispatchEvent(new Event('change', { bubbles: true }));
167 } else {
168 try { el.select(); } catch (e) {
169 try { el.setSelectionRange(0, el.value.length); } catch (e2) {}
170 }
171 }
172 return 'field';
173 }
174 if (el.isContentEditable) {
175 if (clear) {
176 el.textContent = '';
177 el.dispatchEvent(new InputEvent('input', { bubbles: true }));
178 } else {
179 const sel = window.getSelection();
180 sel.removeAllRanges();
181 const range = document.createRange();
182 range.selectNodeContents(el);
183 sel.addRange(range);
184 }
185 return 'contenteditable';
186 }
187 return 'other';
188})
189"#;
190
191pub const SNAPSHOT_TREE_JS: &str = include_str!("js/snapshot_tree.js");
203
204pub const DOC_TOKEN_JS: &str = "String(window.__bcDocToken ?? (window.__bcDocToken = 4294967296 + Math.floor(Math.random() * (9007199254740992 - 4294967296))))";
207
208pub const DOC_SIZE_JS: &str = "JSON.stringify({width: Math.max(document.documentElement.scrollWidth, document.body ? document.body.scrollWidth : 0), height: Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0)})";
211
212pub const REF_CENTER_JS: &str = r#"
215/* bc:center */
216(function(id) {
217 const reg = window.__bcRefs;
218 const r = reg && reg.byId.get(Number(id));
219 const el = r && typeof r.deref === 'function' ? r.deref() : r;
220 if (!el || !el.isConnected) return JSON.stringify({ gone: true });
221 try { el.scrollIntoView({ block: 'center', inline: 'center' }); } catch (e) {}
222 const rect = el.getBoundingClientRect();
223 const vw = window.innerWidth, vh = window.innerHeight;
224 const x1 = Math.max(0, rect.left), y1 = Math.max(0, rect.top);
225 const x2 = Math.min(vw, rect.right), y2 = Math.min(vh, rect.bottom);
226 if (x2 - x1 <= 1 || y2 - y1 <= 1) {
227 return JSON.stringify({ error: 'element has no visible box (hidden, zero-size, or outside the viewport after scrolling)' });
228 }
229 return JSON.stringify({ x: (x1 + x2) / 2, y: (y1 + y2) / 2 });
230})
231"#;
232
233pub const REF_CLIP_RECT_JS: &str = r#"
236/* bc:clip */
237(function(id) {
238 const reg = window.__bcRefs;
239 const r = reg && reg.byId.get(Number(id));
240 const el = r && typeof r.deref === 'function' ? r.deref() : r;
241 if (!el || !el.isConnected) return JSON.stringify({ gone: true });
242 try { el.scrollIntoView({ block: 'center', inline: 'center' }); } catch (e) {}
243 const rect = el.getBoundingClientRect();
244 if (rect.width === 0 || rect.height === 0) return JSON.stringify({ error: 'element has zero area' });
245 return JSON.stringify({
246 x: rect.left + window.scrollX,
247 y: rect.top + window.scrollY,
248 width: rect.width,
249 height: rect.height,
250 });
251})
252"#;
253
254pub const REF_TYPE_JS: &str = r#"
259/* bc:type */
260(function(id, text, mode) {
261 const reg = window.__bcRefs;
262 const r = reg && reg.byId.get(Number(id));
263 const el = r && typeof r.deref === 'function' ? r.deref() : r;
264 if (!el || !el.isConnected) return JSON.stringify({ gone: true });
265 try { el.focus(); } catch (e) {}
266 const tag = (el.tagName || '').toLowerCase();
267 const isField = tag === 'input' || tag === 'textarea';
268 const editable = !isField && el.isContentEditable;
269 const kind = isField ? 'field' : editable ? 'contenteditable' : 'other';
270 function setValue(v) {
271 const proto = tag === 'textarea' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
272 const d = Object.getOwnPropertyDescriptor(proto, 'value');
273 if (d && d.set) d.set.call(el, v); else el.value = v;
274 el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: v }));
275 el.dispatchEvent(new Event('change', { bubbles: true }));
276 }
277 if (mode === 'clear') {
278 if (isField) setValue('');
279 else if (editable) { el.textContent = ''; el.dispatchEvent(new InputEvent('input', { bubbles: true })); }
280 return JSON.stringify({ kind: kind, method: 'clear' });
281 }
282 if (isField) {
283 try { el.select(); } catch (e) { try { el.setSelectionRange(0, el.value.length); } catch (e2) {} }
284 } else if (editable) {
285 const sel = window.getSelection();
286 sel.removeAllRanges();
287 const range = document.createRange();
288 range.selectNodeContents(el);
289 sel.addRange(range);
290 }
291 if (mode === 'select') return JSON.stringify({ kind: kind, method: 'select' });
292 let ok = false;
293 try { ok = document.execCommand('insertText', false, text); } catch (e) { ok = false; }
294 if (isField && el.value !== text) ok = false;
295 let method = 'execCommand';
296 if (!ok) {
297 method = 'fallback';
298 if (isField) setValue(text);
299 else if (editable) {
300 el.textContent = text;
301 el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
302 }
303 }
304 return JSON.stringify({ kind: kind, method: method });
305})
306"#;
307
308pub const FETCH_JS: &str = r#"
310(async function(argsJson) {
311 const args = JSON.parse(argsJson);
312 const timeoutMs = Number(args.timeoutMs || 0);
313 const controller = timeoutMs > 0 && typeof AbortController !== 'undefined'
314 ? new AbortController()
315 : null;
316 const timer = controller
317 ? setTimeout(() => controller.abort(), timeoutMs)
318 : null;
319 try {
320 const r = await fetch(args.url, {
321 method: args.method || 'GET',
322 headers: args.headers || {},
323 body: args.body,
324 credentials: 'include',
325 signal: controller ? controller.signal : undefined,
326 });
327 const text = await r.text();
328 const headers = {};
329 r.headers.forEach((v,k) => { headers[k] = v; });
330 return JSON.stringify({ ok: true, status: r.status, statusText: r.statusText, headers, body: text });
331 } catch (e) {
332 const aborted = controller && controller.signal.aborted;
333 return JSON.stringify({
334 ok: false,
335 error: aborted ? `fetch timed out after ${timeoutMs}ms` : String(e),
336 errorName: e && e.name ? e.name : null,
337 });
338 } finally {
339 if (timer) clearTimeout(timer);
340 }
341})
342"#;