spb/social-runtime-crawler
Public
TypeScript 91.8%
HTML 3.2%
JavaScript 3%
SQL 1.4%
CSS 0.7%
1import fs from "node:fs";2import path from "node:path";3import { fileURLToPath } from "node:url";4import type { Page } from "playwright";56/**7 * Raw DOM candidates scanned in-page (§13–§15). Generic: relies on links, landmarks, ARIA roles,8 * media elements and text density — never on generated class names. Adapters refine these later.9 *10 * The in-page code lives in `snapshot.page.js` (plain JS) because TypeScript transpilers inject11 * helpers such as `__name` into function bodies, which do not exist inside the page.12 */13export interface DomCandidate {14 kind: "link" | "article" | "video" | "image" | "heading" | "search" | "button";15 href?: string;16 text: string; // visible text (trimmed)17 aria_label?: string;18 role?: string;19 region: string; // main / feed / sidebar / dialog / header / navigation / comments / unknown20 index: number; // document order among candidates21 visible: boolean; // within viewport22 top: number; // bounding rect top (for feed position)23 meta?: Record<string, string>; // duration, views, author text found in the same card24 locator_hint: string; // a stable-ish CSS selector path we can use to click (prefers href / aria-label)25}2627export interface DomSnapshot {28 url: string;29 title: string;30 lang?: string;31 h1?: string;32 landmarks: string[];33 candidates: DomCandidate[];34 video_elements: { src?: string; current_src?: string; duration?: number; width: number; height: number; playing: boolean; poster?: string }[];35 text_excerpt: string; // first ~600 chars of main text36 scroll: { y: number; height: number; viewport: number };37 has_login_form: boolean;38 dialog_open: boolean;39}4041const here = path.dirname(fileURLToPath(import.meta.url));42const SNAPSHOT_SOURCE = fs43 .readFileSync(path.join(here, "snapshot.page.js"), "utf8")44 .replace(/^\s*\/\/.*$/gm, "") // strip comment lines so the string is a bare function expression45 .trim();4647export async function snapshotDom(page: Page, opts: { maxCandidates?: number } = {}): Promise<DomSnapshot> {48 const max = opts.maxCandidates ?? 250;49 return (await page.evaluate(`(${SNAPSHOT_SOURCE})(${max})`)) as DomSnapshot;50}51