char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
ac59009
main
1// Functions that run inside the loaded page: serialized with toString(), so each is self-contained. 2 3export type SearchResult = { title :string ; url :string ; snippet :string }; 4 5/** The page's own value of `fn()`, as json text for the browser to hand back. */ 6export function expressionFor ( fn :() => unknown ) :string { 7return `Promise.resolve(( ${ fn . toString ()} )()).then((result) => JSON.stringify(result))` ; 8} 9 10/** Results of a duckduckgo search page, without its ads. */ 11export async function searchPage () :Promise < SearchResult []> { 12const results = () => document . querySelectorAll < HTMLElement >( 'article[data-testid="result"]' ); 13// the page arrives empty and its own script fills in the results, so wait for them; a query 14// that matches nothing renders a message instead of any 15const deadline = Date . now () + 15_000 ; 16while ( 17Date . now () < deadline && 18results (). length === 0 && 19! /no results found for / i . test ( document . body . textContent ?? "" ) 20) { 21await new Promise (( resolve ) => setTimeout ( resolve , 100 )); 22} 23if ( document . querySelector ( '[data-area="mainline"]' ) === null ) { 24throw new Error ( "DuckDuckGo did not render a results page" ); 25} 26 27// innerText glues adjacent spans, such as a result's date, to the text after them 28const text = ( element :HTMLElement ) :string => 29[ ...element . childNodes ] 30. map (( node ) => ( node instanceof HTMLElement ?text ( node ) :node . textContent ?? "" )) 31. join ( element . children . length > 1 ?" " :"" ) 32. replace ( / \s+ / g , " " ) 33. trim (); 34 35return [ ...results ()] 36. map (( result ) => { 37const anchor = result . querySelector < HTMLAnchorElement >( 'a[data-testid="result-title-a"]' ); 38if ( ! anchor ) return undefined ; 39const redirect = new URL ( anchor . href ); 40const url = redirect . searchParams . get ( "uddg" ) ?? anchor . href ; 41const snippet = result . querySelector < HTMLElement >( '[data-result="snippet"]' ); 42return { title :text ( anchor ), url, snippet :snippet ?text ( snippet ) :"" }; 43}) 44. filter (( result ) :result isSearchResult => Boolean ( result ?. title && result . url )); 45} 46 47/** The response body as served, whatever its type. */ 48export async function pageAsRaw () :Promise < string > { 49document . querySelector < HTMLElement >( "#rawdata-tab" )?. click (); 50const rawJson = document . querySelector < HTMLElement >( "#rawdata-panel .data" )?. textContent ; 51if ( rawJson !== undefined && rawJson !== null ) return rawJson ; 52try { 53return await ( await fetch ( location . href )). text (); 54} catch { 55return document . querySelector < HTMLElement >( "body > pre" )?. textContent ?? 56document . documentElement . outerHTML ; 57} 58} 59 60/** The page's main content as markdown; other documents as their text. */ 61export async function pageAsMarkdown () :Promise < string > { 62if ( ! /^(?:text\/html|application\/xhtml\+xml)$ / i . test ( document . contentType )) { 63document . querySelector < HTMLElement >( "#rawdata-tab" )?. click (); 64const rawJson = document . querySelector < HTMLElement >( "#rawdata-panel .data" )?. textContent ; 65if ( rawJson !== undefined && rawJson !== null ) return rawJson ; 66try { 67return await ( await fetch ( location . href )). text (); 68} catch { 69return document . querySelector < HTMLElement >( "body > pre" )?. textContent ?? 70document . documentElement . textContent ?? "" ; 71} 72} 73 74const source = document . querySelector < HTMLElement >( "article" ) ?? 75document . querySelector < HTMLElement >( "main, [role=main]" ) ?? 76document . body ; 77const root = source . cloneNode ( true ) as HTMLElement ; 78const sourceElements = source . querySelectorAll ( "*" ); 79const clonedElements = root . querySelectorAll ( "*" ); 80sourceElements . forEach (( element , index ) => { 81const style = getComputedStyle ( element ); 82if ( style . display === "none" || style . contentVisibility === "hidden" ) { 83clonedElements [ index ]?. remove (); 84} 85}); 86root 87. querySelectorAll ( 88"script, style, noscript, template, svg, canvas, nav, header, footer, form, button, input, select, textarea, dialog, [hidden], [aria-hidden=true]" , 89) 90. forEach (( element ) => element . remove ()); 91 92const escapeText = ( text :string ) => text . replace ( / \s+ / g , " " ). replace ( / ([\\`*_[\]]) / g , "\\$1" ); 93const children = ( element :Element ) => [ ...element . childNodes ]. map ( render ). join ( "" ); 94const block = ( text :string ) => `\n\n ${ text . trim ()} \n\n` ; 95 96function render ( node :Node ) :string { 97if ( node . nodeType === Node . TEXT_NODE ) return escapeText ( node . textContent ?? "" ); 98if ( ! ( node instanceof Element )) return "" ; 99 100const tag = node . tagName . toLowerCase (); 101if ( tag === "br" ) return "\n" ; 102if ( tag === "hr" ) return "\n\n---\n\n" ; 103if ( / ^h[1-6]$ / . test ( tag )) { 104return block ( ` ${ "#" . repeat ( Number ( tag [ 1 ]))} ${ children ( node ). trim ()} ` ); 105} 106if ( tag === "p" ) return block ( children ( node )); 107if ( tag === "strong" || tag === "b" ) return `** ${ children ( node ). trim ()} **` ; 108if ( tag === "em" || tag === "i" ) return `* ${ children ( node ). trim ()} *` ; 109if ( tag === "del" || tag === "s" ) return `~~ ${ children ( node ). trim ()} ~~` ; 110if ( tag === "code" && node . parentElement ?. tagName . toLowerCase () !== "pre" ) { 111const text = node . textContent ?? "" ; 112const fence = text . includes ( "`" ) ?"``" :"`" ; 113return ` ${ fence } ${ text } ${ fence } ` ; 114} 115if ( tag === "pre" ) { 116return `\n\n\`\`\`\n ${ node . textContent ?. replace ( / ^\n|\n$ / g , "" ) ?? "" } \n\`\`\`\n\n` ; 117} 118if ( tag === "a" ) { 119const text = children ( node ). trim (); 120const href = ( node as HTMLAnchorElement ). href ; 121if ( ! href || href . startsWith ( "javascript:" )) return text ; 122return text ?`[ ${ text } ](< ${ href . replace ( / > / g , "%3E" )} >)` :`< ${ href } >` ; 123} 124if ( tag === "img" ) { 125const image = node as HTMLImageElement ; 126if ( ! image . src || image . src . startsWith ( "data:" )) return "" ; 127return `} >)` ; 128} 129if ( tag === "blockquote" ) { 130return block ( children ( node ). trim (). split ( "\n" ). map (( line ) => `> ${ line } ` ). join ( "\n" )); 131} 132if ( tag === "ul" || tag === "ol" ) { 133const items = [ ...node . children ] 134. filter (( child ) => child . tagName . toLowerCase () === "li" ) 135. map (( item , index ) => 136` ${ tag === "ol" ? ` ${ index + 1 } .` : "-" } ${ children ( item ). trim (). replace ( / \n / g , "\n " )} ` 137); 138return block ( items . join ( "\n" )); 139} 140if ( tag === "table" ) { 141const rows = [ ...node . querySelectorAll ( "tr" )]. map (( row ) => 142[ ...row . querySelectorAll ( ":scope > th, :scope > td" )]. map (( cell ) => 143children ( cell ). trim (). replace ( / \| / g , "\\|" ). replace ( / \n+ / g , " " ) 144) 145); 146if ( ! rows . length ) return "" ; 147const width = Math . max ( ...rows . map (( row ) => row . length )); 148const line = ( row :string []) => 149`| ${[ ... row , ... Array ( width - row . length ). fill ( "" )]. join ( " | " )} |` ; 150return block ( 151[ line ( rows [ 0 ]), line ( Array ( width ). fill ( "---" )), ...rows . slice ( 1 ). map ( line )]. join ( "\n" ), 152); 153} 154if ( tag === "dt" ) return block ( `** ${ children ( node ). trim ()} **` ); 155if ( tag === "dd" ) return block ( children ( node )); 156if ( 157[ "article" , "aside" , "details" , "div" , "figcaption" , "figure" , "main" , "section" , "summary" ] 158. includes ( tag ) 159) { 160return block ( children ( node )); 161} 162return children ( node ); 163} 164 165const markdown = render ( root ) 166. replace ( / [ \t]+\n / g , "\n" ) 167. replace ( / \n[ \t]+ / g , "\n" ) 168. replace ( / \n{3,} / g , "\n\n" ) 169. trim (); 170return markdown || document . body . innerText . trim (); 171}