88 lines
5.1 KiB
JavaScript
88 lines
5.1 KiB
JavaScript
// Execute the shipped read-only observers on an in-memory discussion tree.
|
|
// This fixture never starts a browser or touches the network/platform state.
|
|
const vm = require('node:vm');
|
|
let input = '';
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', chunk => input += chunk);
|
|
process.stdin.on('end', () => {
|
|
const { code, arg, tree } = JSON.parse(input);
|
|
let lastMeasured = null;
|
|
const location = { href: 'https://www.xiaohongshu.com/explore/' + 'a'.repeat(24) };
|
|
class El {
|
|
constructor(value) {
|
|
this.tagName = (value.tag || 'div').toUpperCase();
|
|
this.attrs = value.attrs || {}; this.id = this.attrs.id || '';
|
|
this.rect = value.rect;
|
|
this.style = value.style || {};
|
|
this.scrollTop = value.scrollTop ?? 0;
|
|
this.clientHeight = value.clientHeight ?? 35;
|
|
this.scrollHeight = value.scrollHeight ?? this.clientHeight;
|
|
this.text = value.text || ''; this.children = (value.children || []).map(child => new El(child));
|
|
for (const child of this.children) child.parentElement = this;
|
|
}
|
|
get href() { return this.attrs.href ? new URL(this.attrs.href, location.href).href : ''; }
|
|
get innerText() { return [this.text, ...this.children.map(child => child.innerText)].filter(Boolean).join('\n'); }
|
|
getAttribute(name) { return this.attrs[name] ?? null; }
|
|
getBoundingClientRect() { lastMeasured=this;return this.rect || {left:0,right:300,top:10,bottom:45,width:300,height:35}; }
|
|
matches(query) { return query.split(',').some(part => matches(this, part.trim())); }
|
|
closest(query) { let node = this; while (node) { if (node.matches(query)) return node; node = node.parentElement; } return null; }
|
|
contains(node) { return node === this || this.children.some(child => child.contains(node)); }
|
|
querySelectorAll(query) { return flat(this).slice(1).filter(node => node.matches(query)); }
|
|
querySelector(query) { return this.querySelectorAll(query)[0] || null; }
|
|
}
|
|
function flat(node) { return [node, ...node.children.flatMap(flat)]; }
|
|
function atom(node, raw) {
|
|
let query = raw;
|
|
for (const [, key, op, value] of query.matchAll(/\[([\w-]+)(\*=|=)?["']?([^\]"']*)["']?\]/g)) {
|
|
const actual = node.attrs[key];
|
|
if (actual === undefined || op === '=' && actual !== value || op === '*=' && !actual.includes(value)) return false;
|
|
}
|
|
query = query.replace(/\[[^\]]+\]/g, '');
|
|
const id = query.match(/#([\w-]+)/); if (id && node.id !== id[1]) return false;
|
|
const classes = [...query.matchAll(/\.([\w-]+)/g)].map(match => match[1]);
|
|
if (classes.some(value => !(node.attrs.class || '').split(/\s+/).includes(value))) return false;
|
|
const tag = query.match(/^[A-Za-z]+/); return !tag || tag[0].toUpperCase() === node.tagName;
|
|
}
|
|
function matches(node, raw) {
|
|
const parts = raw.trim().split(/\s+/); let current = node;
|
|
if (!atom(current, parts.pop())) return false;
|
|
while (parts.length) {
|
|
const part = parts.pop(); current = current.parentElement;
|
|
while (current && !atom(current, part)) current = current.parentElement;
|
|
if (!current) return false;
|
|
}
|
|
return true;
|
|
}
|
|
const body = new El({tag:'body',children:[tree]});
|
|
const html = new El({tag:'html'}); html.children = [body]; body.parentElement = html;
|
|
if(tree.document_scroll){Object.assign(html,tree.document_scroll);body.rect={left:0,right:400,top:0,bottom:0,width:400,height:0};}
|
|
const document = {
|
|
body, documentElement:html, scrollingElement:html,
|
|
elementFromPoint(x,y) {
|
|
const target=tree.hit_target_id?flat(html).find(node=>node.id===tree.hit_target_id):lastMeasured;
|
|
if(!target)return body;
|
|
const own=target.getBoundingClientRect();
|
|
let hit=x>=own.left&&x<own.right&&y>=own.top&&y<own.bottom&&x>=0&&x<400&&y>=0&&y<600;
|
|
for(let parent=target.parentElement;parent;parent=parent.parentElement){
|
|
if(['BODY','HTML'].includes(parent.tagName))continue;
|
|
const s=parent.style,r=parent.getBoundingClientRect();
|
|
if(/auto|scroll|hidden|clip/.test(s.overflowX||s.overflow||''))hit=hit&&x>=r.left&&x<r.right;
|
|
if(/auto|scroll|hidden|clip/.test(s.overflowY||s.overflow||''))hit=hit&&y>=r.top&&y<r.bottom;
|
|
}
|
|
lastMeasured=target;
|
|
return hit&&!tree.occluded?target:body;
|
|
},
|
|
querySelectorAll(query) { return flat(html).filter(node => node.matches(query)); },
|
|
querySelector(query) { return this.querySelectorAll(query)[0] || null; },
|
|
createTreeWalker(element) {
|
|
const nodes = flat(element).filter(node => node.text).map(node => ({nodeValue:node.text,parentElement:node}));
|
|
let index = 0; return {nextNode() { return nodes[index++] || null; }};
|
|
},
|
|
createRange() { let node; return {selectNodeContents(value) { node = value; },getClientRects() { return [node.parentElement.getBoundingClientRect()]; }}; }
|
|
};
|
|
const context = {document,location,URL,Set,Array,Math,JSON,NodeFilter:{SHOW_TEXT:4},innerHeight:600,innerWidth:400,
|
|
getComputedStyle(node) { return {display:'block',visibility:'visible',opacity:'1',...node.style}; }};
|
|
const fn = vm.runInNewContext('(' + code + ')',context,{timeout:1000});
|
|
process.stdout.write(JSON.stringify(fn(arg)));
|
|
});
|