@botiverse/porthole 0.1.8 → 0.2.0
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/dist/conformance/clipboard.js +5 -2
- package/dist/conformance/entry.d.ts +37 -0
- package/dist/conformance/entry.js +120 -0
- package/dist/conformance/harness/page.d.ts +32 -0
- package/dist/conformance/harness/page.js +215 -0
- package/dist/conformance/harness.js +1662 -0
- package/dist/conformance/host-kit.d.ts +85 -0
- package/dist/conformance/host-kit.js +341 -0
- package/dist/conformance/index.d.ts +2 -0
- package/dist/conformance/index.js +4 -0
- package/dist/conformance/served.js +13 -3
- package/dist/host/locator.d.ts +39 -0
- package/dist/host/locator.js +72 -0
- package/dist/host/outcome.js +1 -0
- package/dist/host/types.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node/index.d.ts +41 -0
- package/dist/node/index.js +160 -0
- package/dist/platform/web.js +12 -1
- package/dist/server/domain.d.ts +43 -0
- package/dist/server/domain.js +100 -0
- package/dist/server/host.d.ts +158 -0
- package/dist/server/host.js +416 -0
- package/dist/server/index.d.ts +9 -2
- package/dist/server/index.js +7 -0
- package/dist/server/naming.d.ts +12 -1
- package/dist/server/naming.js +9 -8
- package/dist/server/pages.d.ts +9 -0
- package/dist/server/pages.js +32 -0
- package/dist/server/server.js +287 -50
- package/dist/server/types.d.ts +37 -0
- package/dist/transport/post-message.d.ts +9 -1
- package/dist/transport/post-message.js +11 -2
- package/package.json +6 -2
- package/src/conformance/clipboard.ts +5 -2
- package/src/conformance/entry.ts +206 -0
- package/src/conformance/harness/page.ts +267 -0
- package/src/conformance/host-kit.ts +466 -0
- package/src/conformance/index.ts +12 -0
- package/src/conformance/served.ts +14 -3
- package/src/host/locator.ts +111 -0
- package/src/host/outcome.ts +1 -0
- package/src/host/types.ts +2 -0
- package/src/index.ts +7 -0
- package/src/node/index.ts +221 -0
- package/src/platform/web.ts +19 -1
- package/src/server/domain.ts +158 -0
- package/src/server/host.ts +706 -0
- package/src/server/index.ts +27 -1
- package/src/server/naming.ts +22 -2
- package/src/server/pages.ts +44 -0
- package/src/server/server.ts +355 -63
- package/src/server/types.ts +33 -0
- package/src/transport/post-message.ts +12 -1
|
@@ -0,0 +1,1662 @@
|
|
|
1
|
+
"use strict";(()=>{var O={start:"porthole/annotate.start",stop:"porthole/annotate.stop",show:"porthole/annotate.show",focus:"porthole/annotate.focus",clear:"porthole/annotate.clear",selected:"porthole/annotate.selected",unselected:"porthole/annotate.unselected",cancelled:"porthole/annotate.cancelled",anchored:"porthole/annotate.anchored"},N={label:120,cssSelector:1e3,exact:4e3,context:64,marks:500,id:128,position:5e7,coordinate:1e7};var te=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),J=(e,t,n=0)=>typeof e=="string"&&e.length>=n&&e.length<=t,Ue=(e,t)=>typeof e=="number"&&Number.isInteger(e)&&e>=0&&e<=t;function Kt(e){if(!te(e))return null;switch(e.type){case"CssSelector":return J(e.value,N.cssSelector,1)?{type:"CssSelector",value:e.value}:null;case"TextQuoteSelector":{let{exact:t,prefix:n,suffix:o}=e;return!J(t,N.exact,1)||n!==void 0&&!J(n,N.context)||o!==void 0&&!J(o,N.context)?null:{type:"TextQuoteSelector",exact:t,...n===void 0||n===""?{}:{prefix:n},...o===void 0||o===""?{}:{suffix:o}}}case"TextPositionSelector":return Ue(e.start,N.position)&&Ue(e.end,N.position)&&e.start<=e.end?{type:"TextPositionSelector",start:e.start,end:e.end}:null;default:return null}}function Ve(e){if(!te(e)||e.kind!=="element"&&e.kind!=="text"||!Array.isArray(e.selectors)||e.selectors.length===0||e.selectors.length>3||!J(e.label,N.label))return null;let t=[];for(let r of e.selectors){let a=Kt(r);if(a===null||t.some(i=>i.type===a.type))return null;t.push(a)}let n=t.some(r=>r.type==="TextQuoteSelector"),o=t.some(r=>r.type==="CssSelector");return(e.kind==="text"||!o)&&!n?null:{kind:e.kind,selectors:t,label:e.label}}var le=e=>typeof e=="number"&&Number.isFinite(e)&&Math.abs(e)<=N.coordinate;function Ge(e){if(!te(e))return null;let{x:t,y:n,width:o,height:r}=e;return!le(t)||!le(n)||!le(o)||!le(r)||o<0||r<0?null:{x:t,y:n,width:o,height:r}}var Je=e=>J(e,N.id,1);function Xt(e){if(!Array.isArray(e)||e.length>N.marks)return null;let t=[];for(let n of e){if(!te(n)||!Je(n.id))return null;if(n.state==="orphaned"){t.push({id:n.id,state:"orphaned"});continue}if(n.state!=="attached")return null;let o=n.rect===void 0?void 0:Ge(n.rect);if(o===null)return null;t.push({id:n.id,state:"attached",...o===void 0?{}:{rect:o}})}return t}function de(e){if(!te(e))return null;switch(e.type){case"selected":{let t=Ve(e.anchor),n=Ge(e.rect);return t===null||n===null?null:{type:"selected",anchor:t,rect:n}}case"unselected":return{type:"unselected"};case"cancelled":return{type:"cancelled"};case"anchored":{let t=Xt(e.results);return t===null?null:{type:"anchored",results:t}}default:return null}}function Ke(e){let t=e?.elements??!0,n=e?.text??!0;if(typeof t!="boolean"||typeof n!="boolean"||!t&&!n)throw new TypeError("annotations.start: elements, text, or both");return{elements:t,text:n}}function Xe(e){if(!Array.isArray(e)||e.length>N.marks)throw new TypeError(`annotations.show: at most ${N.marks} marks`);let t=new Set;return e.map((n,o)=>{let r=Ve(n?.anchor);if(!Je(n?.id)||t.has(n.id)||r===null)throw new TypeError(`annotations.show: mark ${o} needs a unique id (1 to ${N.id} characters) and a valid anchor`);return t.add(n.id),{id:n.id,anchor:r}})}var $e=["sendPrompt","openLink","downloadFile","callTool","readData","setModelContext","reportError","resize","copyText","annotate"],ne=e=>({rateLimit:{perMinute:e}});var ce={sendPrompt:{defaultGates:["user-activation","confirm"],served:"handler",global:"sendPrompt",request:{method:"ui/message",sdk:"app.sendMessage"}},openLink:{defaultGates:["user-activation","confirm"],served:"handler",global:"openLink",request:{method:"ui/open-link",sdk:"app.openLink"}},downloadFile:{defaultGates:["user-activation","confirm"],served:"handler",request:{method:"ui/download-file",sdk:"app.downloadFile"}},callTool:{defaultGates:["confirm"],served:"handler",request:{method:"tools/call",sdk:"app.callServerTool"}},readData:{defaultGates:[ne(120)],served:"handler",request:{method:"resources/read",sdk:"app.readServerResource"}},setModelContext:{defaultGates:[ne(30)],served:"handler",request:{method:"ui/update-model-context",sdk:"app.updateModelContext"}},reportError:{defaultGates:[ne(10)],served:"event"},resize:{defaultGates:[],served:"event"},copyText:{defaultGates:["user-activation",ne(30)],served:"handler",global:"copyText",request:{method:"porthole/copy-text"}},annotate:{defaultGates:[ne(120)],served:"event"}};function B(e){return ce[e]}function Ye(e){return typeof e=="string"&&$e.includes(e)}function ue(e,t){return(e.egress.anyHttps??[]).some(n=>n===t)}function D(e){return Object.keys(ce).filter(t=>e.capabilities[t]!==void 0)}function K(e,t){return e.capabilities[t]?.gates??ce[t].defaultGates}var Qe=e=>[...new Set(e)];function F(e,t){let n=D(e).flatMap(o=>{let r=e.capabilities[o];return(t==="connect"?r?.connect:r?.resource)??[]});return Qe([...e.egress[t]??[],...n])}var Yt=e=>e.length===0?["'none'"]:[...e];function Ze(e){if(e.mode==="url")return[];let t=o=>F(e,o),n=o=>ue(e,o)?["https:"]:[];return[["default-src","'none'"],["script-src","'unsafe-inline'","'unsafe-eval'","blob:",...t("script")],["style-src","'unsafe-inline'",...t("style")],["font-src","data:",...n("font"),...t("font")],["img-src","data:","blob:",...n("img"),...t("img")],["media-src","data:","blob:",...n("media"),...t("media")],["connect-src",...Yt(t("connect"))],["worker-src","blob:"],["child-src","blob:"],["frame-src","'none'"],["object-src","'none'"],["base-uri","'none'"],["form-action","'none'"],["webrtc","'block'"]]}function et(e){return e.map(t=>t.join(" ")).join("; ")}function pe(e){switch(e.isolation){case"opaque":return["allow-scripts"];case"domain":return["allow-scripts","allow-same-origin"];case"external":return["allow-scripts","allow-same-origin","allow-forms",...e.sandbox??[]]}}function tt(e){return e.mode==="url"?(e.permissions??[]).join("; "):"clipboard-read 'none'; clipboard-write 'none'"}function nt(e){return Qe([...e.contentOrigins,...e.embedder.frameSrc??[]])}var H=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),ot=4096;function Qt(e){if(typeof e!="string"||e.length===0||e.length>ot)return null;let t;try{t=new URL(e.trim())}catch{return null}return t.protocol!=="https:"&&t.protocol!=="http:"||t.username!==""||t.password!==""?null:t.href}var Zt={sendPrompt:(e,t)=>{let n=H(e)?e.text:void 0;return typeof n=="string"&&n.trim()!==""&&n.length<=t.limits.maxPromptChars?{text:n}:null},openLink:e=>{let t=Qt(H(e)?e.url:void 0);return t===null?null:{url:t}},downloadFile:e=>{let t=H(e)?e.contents:void 0;return Array.isArray(t)&&t.length>0&&t.length<=100?{contents:t}:null},callTool:e=>{if(!H(e)||typeof e.name!="string"||e.name===""||e.name.length>256)return null;let t=e.arguments??{};return H(t)?{name:e.name,arguments:t}:null},readData:e=>{let t=H(e)?e.uri:void 0;return typeof t=="string"&&t!==""&&t.length<=ot?{uri:t}:null},setModelContext:e=>{if(!H(e))return null;let{content:t,structuredContent:n}=e;return t!==void 0&&!Array.isArray(t)||n!==void 0&&!H(n)?null:{...t===void 0?{}:{content:t},...n===void 0?{}:{structuredContent:n}}},reportError:e=>{let t=H(e)?e.message:void 0;return typeof t=="string"&&t!==""?{message:t.slice(0,2e3)}:null},copyText:e=>{let t=H(e)?e.text:void 0;return typeof t=="string"&&t!==""&&t.length<=2e4?{text:t}:null},annotate:e=>de(e),resize:e=>{if(!H(e)||typeof e.height!="number"||!Number.isFinite(e.height)||e.height<0)return null;let t=e.width;return typeof t=="number"&&Number.isFinite(t)&&t>=0?{height:e.height,width:t}:{height:e.height}}};function rt(e){let{config:t,display:n,handlers:o,onEvent:r}=e,a=e.now??(()=>Date.now()),i=new Map,s=0,c=!1,l=(m,v,d)=>(r({type:"refused",capability:m,refusal:v}),{ok:!1,refusal:v,...d===void 0?{}:{message:d}}),f=(m,v)=>{let d=a(),g=(i.get(m)??[]).filter(u=>d-u<6e4);return g.length>=v?(i.set(m,g),!1):(g.push(d),i.set(m,g),!0)};return{declared:m=>t.capabilities[m]!==void 0,async call(m,v,d){if(t.capabilities[m]===void 0)return l(m,"not-declared");let g=Zt[m](v,t);if(g===null)return l(m,"invalid");let u=K(t,m),p=u.includes("confirm");if(p&&c)return l(m,"muted");for(let A of u)if(typeof A=="object"&&!f(m,A.rateLimit.perMinute))return l(m,"rate-limited");if(u.includes("user-activation")&&(d?.userActivated??e.userActivation?.())===!1)return l(m,"no-user-activation");let y=null,C=()=>y??=k(m,g);if(p){if(e.confirm===void 0)return l(m,"no-confirm");let A=!1;try{A=await e.confirm({capability:m,args:g,display:n},()=>{C()})}catch{A=y===null?!1:"accepted"}if(y===null&&A!==!0&&A!=="accepted")return A==="redirected"?l(m,"redirected"):(s+=1,s>=t.limits.declinesBeforeMute&&!c&&(c=!0,r({type:"muted"})),l(m,"declined"));s=0}return C()}};async function k(m,v){{if(B(m).served==="event")return r(m==="resize"?{type:"resize",...v}:m==="annotate"?{type:"annotation",report:v}:{type:"error",message:v.message}),{ok:!0,value:void 0};let d=o[m];if(d===void 0)return l(m,"not-declared");try{return{ok:!0,value:await d(v,{displayId:n.id,title:n.title,capability:m})}}catch(g){return l(m,"failed",g instanceof Error?g.message:String(g))}}}}var Le=["img","media","font"],Ne=["allow-popups","allow-popups-to-escape-sandbox","allow-modals","allow-downloads"],Oe=["clipboard-write","fullscreen"];var Ie=class extends Error{path;constructor(t,n){super(`${t}: ${n}`),this.name="ConfigError",this.path=t}},De=["widget","chart","interactive"],en=/^(localhost|127\.0\.0\.1|\[::1\])$|\.localhost$/u,tn=new Set([...["co","org","ac","gov","me","net","ltd","plc","sch","nhs","police"].map(e=>`${e}.uk`),...["com","net","org","edu","gov","asn","id"].map(e=>`${e}.au`),...["co","org","net","ac","govt","geek","school"].map(e=>`${e}.nz`),...["co","ne","or","ac","go","ad","ed","gr","lg"].map(e=>`${e}.jp`),...["co","or","ne","ac","go","re"].map(e=>`${e}.kr`),...["com","net","org","gov","edu"].flatMap(e=>["br","cn","hk","tw","sg","my","mx","ar","tr","ua","pl","ph","pk","ng","eg","sa","vn","co"].map(t=>`${e}.${t}`)),...["co","net","org","gen","firm","ind","ac","edu","gov","res"].map(e=>`${e}.in`),...["co","org","ac","gov","net","web"].map(e=>`${e}.za`),...["co","org","ac","gov","net","muni"].map(e=>`${e}.il`),"co.id","or.id","ac.id","co.th","or.th","ac.th","co.ke","co.tz","co.ug","com.de","co.at","or.at","ac.at","co.it","com.es","nom.es","com.fr","asso.fr","eu.org","us.com","uk.com","eu.com","uk.net","gb.net","jpn.com","kr.com","github.io","githubusercontent.com","gitlab.io","bitbucket.io","pages.dev","workers.dev","r2.dev","vercel.app","now.sh","netlify.app","netlify.com","herokuapp.com","herokussl.com","appspot.com","web.app","firebaseapp.com","cloudfunctions.net","run.app","googleusercontent.com","blogspot.com","azurewebsites.net","azurestaticapps.net","azureedge.net","blob.core.windows.net","cloudapp.net","trafficmanager.net","cloudfront.net","amazonaws.com","s3.amazonaws.com","elasticbeanstalk.com","awsapprunner.com","amplifyapp.com","fly.dev","onrender.com","railway.app","up.railway.app","glitch.me","repl.co","replit.app","replit.dev","surge.sh","deno.dev","ngrok.io","ngrok-free.app","ngrok.app","loca.lt","trycloudflare.com","codesandbox.io","csb.app","stackblitz.io","webcontainer.io","neocities.org","wordpress.com","tumblr.com","wixsite.com","squarespace.com","myshopify.com","carrd.co","notion.site","framer.app","webflow.io","readthedocs.io","gitbook.io","sourceforge.net","digitaloceanspaces.com","ondigitalocean.app","b-cdn.net","jsdelivr.net"]);function nn(e){return e==="localhost"?null:!e.includes(".")||tn.has(e)?`is a wildcard over a public suffix (*.${e}): anyone can serve a site under it; name a domain of your own (https://*.cdn.example.com)`:null}var on=/^(\*\.)?([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/u;function rn(e){let t="is not an origin (scheme://host[:port], lowercase, no path)";if(typeof e!="string"||e.length>269)return`${JSON.stringify(e)} ${t}`;let n=/^(https|http|wss|ws):\/\/([^/:]+|\[::1\])(:(\d{1,5}))?$/u.exec(e);if(n===null)return`${JSON.stringify(e)} ${t}`;let[,o,r="",,a]=n;if(a!==void 0&&(Number(a)<1||Number(a)>65535))return`${JSON.stringify(e)} has no valid port`;if(r!=="[::1]"&&!on.test(r))return`${JSON.stringify(e)} ${t}`;if(r.startsWith("*.")){let i=nn(r.slice(2));if(i!==null)return`${JSON.stringify(e)} ${i}`}return(o==="http"||o==="ws")&&!en.test(r.replace(/^\*\./u,"x."))?`${JSON.stringify(e)} uses ${o} off loopback: ${o} is allowed only for localhost, *.localhost, 127.0.0.1 and [::1] (loopback development); use ${o==="http"?"https":"wss"}`:null}var x=(e,t)=>{throw new Ie(e,t)},re=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);function I(e,t){return re(e)?e:x(t,"must be an object")}function _(e,t,n){for(let o of Object.keys(e))t.includes(o)||x(`${n}.${o}`,"is not a known field")}function oe(e,t){if(e!==void 0){if(!Array.isArray(e)){x(t,"must be an array of origins");return}e.forEach((n,o)=>{let r=rn(n);r!==null&&x(`${t}[${o}]`,r)})}}function me(e,t,n=Number.MAX_SAFE_INTEGER){(typeof e!="number"||!Number.isInteger(e)||e<1||e>n)&&x(t,`must be an integer from 1 to ${n}`)}function an(e,t){if(e==="user-activation"||e==="confirm")return;let n=re(e)&&re(e.rateLimit)?e.rateLimit:null;if(n===null||Object.keys(e).length!==1){x(t,'must be "user-activation", "confirm" or { rateLimit: { perMinute } }');return}_(n,["perMinute"],`${t}.rateLimit`),me(n.perMinute,`${t}.rateLimit.perMinute`,1e4)}var at=["inline","artifact","mcp-app","url"],st=["opaque","domain","external"];function it(e){let t=I(e,"config");_(t,["mode","isolation","capabilities","egress","theme","limits","layout","guide","contentOrigins","embedder","sandbox","permissions","storage","errors"],"config"),at.includes(t.mode)||x("config.mode",`must be one of ${at.join(", ")}`),st.includes(t.isolation)||x("config.isolation",`must be one of ${st.join(", ")}`),t.mode==="url"!=(t.isolation==="external")&&x("config.isolation",'url mode, and only url mode, is "external"');let n=I(t.capabilities,"config.capabilities");for(let[d,g]of Object.entries(n)){let u=`config.capabilities.${d}`;Ye(d)||x(u,`is not a capability (${$e.join(", ")})`);let p=I(g,u);_(p,["gates","connect","resource","guide"],u),p.gates!==void 0&&(Array.isArray(p.gates)||x(`${u}.gates`,"must be an array"),p.gates.forEach((y,C)=>an(y,`${u}.gates[${C}]`))),oe(p.connect,`${u}.connect`),oe(p.resource,`${u}.resource`),p.guide!==void 0&&typeof p.guide!="string"&&x(`${u}.guide`,"must be a string")}t.mode==="url"&&Object.keys(n).length>0&&x("config.capabilities","url mode has no shell, so it can declare no capabilities");let o=I(t.egress,"config.egress"),r=["connect","script","style","img","font","media"];_(o,[...r,"anyHttps"],"config.egress");for(let d of r)oe(o[d],`config.egress.${d}`);o.anyHttps!==void 0&&(Array.isArray(o.anyHttps)||x("config.egress.anyHttps",`must be a list of ${Le.join(", ")}`),o.anyHttps.forEach((d,g)=>{Le.includes(d)||x(`config.egress.anyHttps[${g}]`,`${JSON.stringify(d)}: only images, media and fonts may load from any https site (P059), never scripts, styles or data`)}),t.mode==="url"&&x("config.egress.anyHttps","url mode has no egress control"));let a=I(t.theme,"config.theme");_(a,["variables","event","documents"],"config.theme"),a.documents!==void 0&&a.documents!=="declared"&&a.documents!=="always"&&a.documents!=="never"&&x("config.theme.documents",'must be "declared", "always" or "never"'),a.documents==="never"&&t.mode!=="artifact"&&x("config.theme.documents",'"never" is for whole pages: a fragment takes the theme it is drawn in');let i=I(a.variables,"config.theme.variables");for(let[d,g]of Object.entries(i)){let u=`config.theme.variables[${JSON.stringify(d)}]`;/^--[a-z0-9]+(-[a-z0-9]+)*$/u.test(d)||x(u,"must be a CSS custom property name (--lowercase-words)");let p=re(g)?[g.light,g.dark]:[g];re(g)&&_(g,["light","dark"],u);for(let y of p)(typeof y!="string"||y.length===0||y.length>500||/[;{}<>]/u.test(y))&&x(u,"must be a CSS value (1\u2013500 characters, no ; { } < >), or { light, dark } of them")}(typeof a.event!="string"||!/^[a-z][a-z0-9]*([:.-][a-z0-9]+)*$/u.test(a.event))&&x("config.theme.event",'must be an event name such as "porthole:theme"');let s=I(t.limits,"config.limits"),c=["maxHeightPx","minHeightPx","maxBytes","maxLiveFrames","declinesBeforeMute","maxPromptChars","maxErrorReports"];_(s,[...c,"loadTimeoutMs"],"config.limits");for(let d of c)me(s[d],`config.limits.${d}`);s.loadTimeoutMs!==void 0&&me(s.loadTimeoutMs,"config.limits.loadTimeoutMs",6e5),s.minHeightPx>s.maxHeightPx&&x("config.limits.minHeightPx","must not exceed maxHeightPx");let l=I(t.layout,"config.layout");_(l,["minWidthPx","narrowWidthPx","wideWidthPx","kind"],"config.layout"),l.kind!==void 0&&l.kind!=="flow"&&l.kind!=="fill"&&x("config.layout.kind",'must be "flow" or "fill"');for(let d of["minWidthPx","narrowWidthPx","wideWidthPx"])me(l[d],`config.layout.${d}`,1e4);(l.minWidthPx>l.narrowWidthPx||l.narrowWidthPx>l.wideWidthPx)&&x("config.layout","must hold minWidthPx \u2264 narrowWidthPx \u2264 wideWidthPx");let f=I(t.guide,"config.guide");_(f,["toolName","guideToolName","topics"],"config.guide");for(let d of["toolName","guideToolName"]){let g=f[d];g!==void 0&&(typeof g!="string"||!/^[A-Za-z0-9_.-]{1,64}$/u.test(g))&&x(`config.guide.${d}`,"must be a tool name (letters, digits, _ . -)")}if(f.topics!==void 0)for(let[d,g]of Object.entries(I(f.topics,"config.guide.topics"))){let u=`config.guide.topics.${d}`;(!/^[a-z][a-z0-9-]{0,31}$/u.test(d)||De.includes(d))&&x(u,"must be a new topic name (lowercase), not a built-in one"),(typeof g!="string"||g.trim()==="")&&x(u,"must be the topic's text")}oe(t.contentOrigins,"config.contentOrigins");let k=t.isolation==="domain"||t.isolation==="external",m=t.contentOrigins;k!==m.length>0&&x("config.contentOrigins",k?`${String(t.isolation)} isolation needs the origins its content is served from`:"opaque isolation serves nothing: leave it empty");let v=I(t.embedder,"config.embedder");if(_(v,["frameSrc"],"config.embedder"),oe(Array.isArray(v.frameSrc)?v.frameSrc.filter(d=>d!=="'self'"):v.frameSrc,"config.embedder.frameSrc"),t.sandbox!==void 0&&(t.mode!=="url"&&x("config.sandbox","only url mode may add sandbox tokens"),Array.isArray(t.sandbox)||x("config.sandbox","must be an array of sandbox tokens"),t.sandbox.forEach((d,g)=>{Ne.includes(d)||x(`config.sandbox[${g}]`,/top-navigation|same-origin/u.test(String(d))?`${JSON.stringify(d)} is never granted (P037)`:`must be one of ${Ne.join(", ")}`)})),t.storage!==void 0){let d=I(t.storage,"config.storage");_(d,["shared","scope"],"config.storage"),d.shared!==void 0&&d.scope!==void 0&&x("config.storage","shared (several displays at once) or scope (one at a time), not both"),d.scope!==void 0&&(typeof d.scope!="string"||!/^[a-z][a-z ,'-]{0,59}$/u.test(d.scope))&&x("config.storage.scope",'must be short lowercase words ("this artifact, across its versions")'),t.isolation!=="domain"&&x("config.storage","only domain isolation keeps storage across displays"),d.shared!==void 0&&(typeof d.shared!="string"||!/^[a-z][a-z -]{0,39}$/u.test(d.shared))&&x("config.storage.shared",'must be a short lowercase noun ("conversation")')}return t.errors!==void 0&&(t.errors!=="agent"&&t.errors!=="host"&&t.errors!=="none"&&x("config.errors",'must be "agent", "host" or "none"'),t.errors==="none"!=(n.reportError===void 0)&&x("config.errors",t.errors==="none"?'"none" means no reportError: drop the capability':`${JSON.stringify(t.errors)} needs the reportError capability, which carries them`)),t.permissions!==void 0&&(t.mode!=="url"&&x("config.permissions","only url mode may be delegated features"),Array.isArray(t.permissions)||x("config.permissions","must be an array of features"),t.permissions.forEach((d,g)=>{Oe.includes(d)||x(`config.permissions[${g}]`,`must be one of ${Oe.join(", ")}`)})),e}var he="2026-01-26",M={initialize:"ui/initialize",message:"ui/message",openLink:"ui/open-link",downloadFile:"ui/download-file",requestDisplayMode:"ui/request-display-mode",updateModelContext:"ui/update-model-context",callTool:"tools/call",readResource:"resources/read",initialized:"ui/notifications/initialized",sizeChanged:"ui/notifications/size-changed",requestTeardown:"ui/notifications/request-teardown",log:"notifications/message",toolInput:"ui/notifications/tool-input",toolInputPartial:"ui/notifications/tool-input-partial",toolResult:"ui/notifications/tool-result",toolCancelled:"ui/notifications/tool-cancelled",hostContextChanged:"ui/notifications/host-context-changed",resourceTeardown:"ui/resource-teardown",sandboxProxyReady:"ui/notifications/sandbox-proxy-ready",sandboxResourceReady:"ui/notifications/sandbox-resource-ready"},lt=[...["background","text","border","ring"].flatMap(e=>(e==="background"?["primary","secondary","tertiary","inverse","ghost","info","danger","success","warning","disabled"]:e==="text"?["primary","secondary","tertiary","inverse","ghost","info","danger","success","warning","disabled"]:e==="border"?["primary","secondary","tertiary","inverse","ghost","info","danger","success","warning","disabled"]:["primary","secondary","inverse","info","danger","success","warning"]).map(t=>`--color-${e}-${t}`)),"--font-sans","--font-mono",...["normal","medium","semibold","bold"].map(e=>`--font-weight-${e}`),...["xs","sm","md","lg"].map(e=>`--font-text-${e}-size`),...["xs","sm","md","lg","xl","2xl","3xl"].map(e=>`--font-heading-${e}-size`),...["xs","sm","md","lg"].map(e=>`--font-text-${e}-line-height`),...["xs","sm","md","lg","xl","2xl","3xl"].map(e=>`--font-heading-${e}-line-height`),...["xs","sm","md","lg","xl","full"].map(e=>`--border-radius-${e}`),"--border-width-regular",...["hairline","sm","md","lg"].map(e=>`--shadow-${e}`)],dt="porthole/styles",ct="porthole/copy-text";function Y(e){if(e.mode==="url")return null;let t=e.mode==="mcp-app"?"none":"shell",n=D(e),o={};for(let a of n)o[a]={userActivation:K(e,a).includes("user-activation")};let r=t==="shell"?n.flatMap(a=>{let i=B(a);return i.global===void 0?[]:[i.global]}):[];return{protocol:t,content:e.mode==="artifact"?"document":"fragment",capabilities:o,globals:r,themeVariables:Object.keys(e.theme.variables),themeEvent:e.theme.event,themeDocuments:e.theme.documents??"declared",storage:e.isolation==="opaque"?"memory":"native",maxErrorReports:e.limits.maxErrorReports,maxPromptChars:e.limits.maxPromptChars,maxCopyChars:2e4,protocolVersion:he}}var S=e=>e,w=(e,t)=>({light:e,dark:t}),fe={categorical:[{hue:"blue",light:"#2a78d6",dark:"#3987e5"},{hue:"orange",light:"#eb6834",dark:"#d95926"},{hue:"aqua",light:"#1baf7a",dark:"#199e70"},{hue:"yellow",light:"#eda100",dark:"#c98500"},{hue:"magenta",light:"#e87ba4",dark:"#d55181"},{hue:"green",light:"#008300",dark:"#008300"},{hue:"violet",light:"#4a3aa7",dark:"#9085e9"},{hue:"red",light:"#e34948",dark:"#e66767"}],sequential:{light:["#cde2fb","#9ec5f4","#6da7ec","#3987e5","#256abf","#184f95","#0d366b"],dark:["#0d366b","#184f95","#256abf","#3987e5","#6da7ec","#9ec5f4","#cde2fb"]},diverging:{neg:{light:"#e34948",dark:"#e66767"},mid:{light:"#f0efec",dark:"#383835"},pos:{light:"#2a78d6",dark:"#3987e5"}}},ge=(e,t)=>Object.entries(t).map(([n,o])=>[`--color-${e}-${n}`,o]),V=(e,t,n)=>[[`--font-heading-${e}-size`,S(`${t}px`)],[`--font-heading-${e}-line-height`,S(`${n}px`)]],io=Object.fromEntries([["--color-background-primary",w("#f9f9f7","#191918")],["--color-background-secondary",w("#ffffff","#232321")],["--color-background-tertiary",w("#f3f3f1","#2b2b29")],["--color-background-inverse",w("#1a1a19","#f4f4f2")],["--color-background-ghost",w("rgba(11, 11, 11, 0.04)","rgba(255, 255, 255, 0.05)")],["--color-background-disabled",w("#efefec","#2e2e2c")],...ge("background",{info:w("#e8f1fc","#16283d"),success:w("#e7f5ec","#15301f"),warning:w("#fdf3e1","#3a2a10"),danger:w("#fdecec","#3a1a18")}),["--color-text-primary",w("#0b0b0b","#f4f4f2")],["--color-text-secondary",w("#52514e","#b5b4ae")],["--color-text-tertiary",w("#898781","#8f8e88")],["--color-text-inverse",w("#ffffff","#0b0b0b")],["--color-text-ghost",w("#a3a29c","#6e6d68")],["--color-text-disabled",w("#b5b4ae","#5c5b57")],...ge("text",{info:w("#1f5fae","#8cbcf5"),success:w("#1d7a3e","#86d3a2"),warning:w("#8a5a00","#e8b85c"),danger:w("#b42318","#f19a92")}),["--color-border-primary",w("#c3c2b7","#4a4a46")],["--color-border-secondary",w("#e1e0d9","#393936")],["--color-border-tertiary",w("#ebeae5","#2f2f2d")],["--color-border-inverse",w("#3a3a37","#d6d5cf")],["--color-border-ghost",w("rgba(11, 11, 11, 0.06)","rgba(255, 255, 255, 0.08)")],["--color-border-disabled",w("#e6e5e0","#333331")],...ge("border",{info:w("#a9c8f0","#2e5584"),success:w("#a6d9b8","#2c6141"),warning:w("#f0cf8c","#6e5221"),danger:w("#f1b0ab","#7a2f29")}),["--color-ring-primary",w("#2a78d6","#3987e5")],["--color-ring-secondary",w("#898781","#8f8e88")],["--color-ring-inverse",w("#ffffff","#0b0b0b")],...ge("ring",{info:w("#2a78d6","#3987e5"),success:w("#1d7a3e","#86d3a2"),warning:w("#b07400","#e8b85c"),danger:w("#d92d20","#f19a92")}),["--color-accent",w("#2a78d6","#3987e5")],["--color-accent-soft",w("#e8f1fc","#16283d")],...fe.categorical.map((e,t)=>[`--color-chart-${t+1}`,w(e.light,e.dark)]),...fe.sequential.light.map((e,t)=>[`--color-chart-seq-${t+1}`,w(e,fe.sequential.dark[t]??e)]),...["neg","mid","pos"].map(e=>[`--color-chart-${e}`,fe.diverging[e]]),["--font-sans",S('system-ui, -apple-system, "Segoe UI", Roboto, sans-serif')],["--font-mono",S('ui-monospace, "SF Mono", Menlo, Consolas, monospace')],["--font-weight-normal",S("400")],["--font-weight-medium",S("500")],["--font-weight-semibold",S("600")],["--font-weight-bold",S("700")],["--font-text-xs-size",S("12px")],["--font-text-xs-line-height",S("16px")],["--font-text-sm-size",S("13px")],["--font-text-sm-line-height",S("18px")],["--font-text-md-size",S("14px")],["--font-text-md-line-height",S("20px")],["--font-text-lg-size",S("16px")],["--font-text-lg-line-height",S("24px")],...V("xs",14,20),...V("sm",16,24),...V("md",20,28),...V("lg",24,32),...V("xl",28,36),...V("2xl",32,40),...V("3xl",40,48),["--border-radius-xs",S("4px")],["--border-radius-sm",S("6px")],["--border-radius-md",S("8px")],["--border-radius-lg",S("12px")],["--border-radius-xl",S("16px")],["--border-radius-full",S("9999px")],["--border-width-regular",S("1px")],["--shadow-hairline",w("0 0 0 1px rgba(11, 11, 11, 0.06)","0 0 0 1px rgba(255, 255, 255, 0.08)")],["--shadow-sm",w("0 1px 2px rgba(11, 11, 11, 0.06)","0 1px 2px rgba(0, 0, 0, 0.4)")],["--shadow-md",w("0 2px 8px rgba(11, 11, 11, 0.08)","0 2px 8px rgba(0, 0, 0, 0.45)")],["--shadow-lg",w("0 8px 24px rgba(11, 11, 11, 0.12)","0 8px 24px rgba(0, 0, 0, 0.5)")]]);function He(e,t){return typeof e=="string"?e:e[t]}function ut(e,t,n){let o={};for(let[r,a]of Object.entries(e.variables)){let i=n?.[r]?.trim();o[r]=i===void 0||i===""||/[;{}<>]/u.test(i)?He(a,t):i}return o}var q={stats:`<style>
|
|
2
|
+
.stats { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
3
|
+
.stat { flex: 1 1 100px; background: var(--color-background-secondary); border: var(--border-width-regular) solid var(--color-border-tertiary); border-radius: var(--border-radius-lg); padding: 12px 16px; }
|
|
4
|
+
.stat div { font-size: var(--font-text-sm-size); color: var(--color-text-secondary); }
|
|
5
|
+
.stat b { font-size: var(--font-heading-md-size); font-weight: var(--font-weight-semibold); }
|
|
6
|
+
</style>
|
|
7
|
+
<div class="stats">
|
|
8
|
+
<div class="stat"><div>Monthly cost</div><b>$1,240</b></div>
|
|
9
|
+
<div class="stat"><div>Failed builds</div><b>2.4%</b></div>
|
|
10
|
+
</div>`,diagram:`<div style="font-weight: var(--font-weight-semibold); margin-bottom: 8px">Uploads reach storage only through the API</div>
|
|
11
|
+
<svg viewBox="0 0 320 260" width="100%" style="max-width: 400px" font-size="13" role="img" aria-label="Uploads reach storage only through the API">
|
|
12
|
+
<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="var(--color-text-tertiary)"/></marker></defs>
|
|
13
|
+
<g fill="none" stroke="var(--color-border-secondary)" stroke-width="1.25"><rect x="16" y="8" width="136" height="52" rx="8"/><rect x="168" y="8" width="136" height="52" rx="8"/><rect x="16" y="200" width="136" height="52" rx="8"/><rect x="168" y="200" width="136" height="52" rx="8"/></g>
|
|
14
|
+
<rect x="16" y="104" width="288" height="52" rx="8" fill="var(--color-chart-1)" fill-opacity="0.12" stroke="var(--color-chart-1)" stroke-width="2"/>
|
|
15
|
+
<g fill="none" stroke="var(--color-text-tertiary)" stroke-width="1.25" marker-end="url(#arrow)"><path d="M84 60V102"/><path d="M236 60V102"/><path d="M84 156V198"/><path d="M236 156V198"/></g>
|
|
16
|
+
<g text-anchor="middle" font-size="14" font-weight="600" fill="var(--color-text-primary)"><text x="84" y="30">Phone app</text><text x="236" y="30">Web app</text><text x="160" y="126">API server</text><text x="84" y="222">Object storage</text><text x="236" y="222">Database</text></g>
|
|
17
|
+
<g text-anchor="middle" fill="var(--color-text-secondary)"><text x="84" y="48">iOS, Android</text><text x="236" y="48">any browser</text><text x="160" y="144">checks every upload</text><text x="84" y="240">the files</text><text x="236" y="240">who sent what</text></g>
|
|
18
|
+
<g fill="var(--color-text-secondary)"><text x="92" y="86">HTTPS</text><text x="244" y="86">HTTPS</text><text x="92" y="182">writes</text><text x="244" y="182">records</text></g>
|
|
19
|
+
</svg>`,line:'<style>\n .note { font-size: var(--font-text-sm-size); color: var(--color-text-secondary); }\n</style>\n<div style="font-weight: var(--font-weight-semibold)">Failed builds fell from 9 to 2 a week</div>\n<div class="note">Builds per week, last 8 weeks</div>\n<div class="note"><b style="color: var(--color-chart-1)">\u2501</b> Failed <b style="color: var(--color-text-disabled)">\u2501</b> All builds</div>\n<div id="plot" style="height: 200px"></div>\n<div class="note" id="readout"></div>\n<script>\n const rows = [["W1", 41, 9], ["W2", 44, 8], ["W3", 39, 7], ["W4", 47, 6], ["W5", 52, 4], ["W6", 49, 3], ["W7", 55, 3], ["W8", 58, 2]];\n const series = [[1, "builds", "var(--color-text-disabled)"], [2, "failed", "var(--color-chart-1)"]];\n const plot = document.getElementById("plot");\n const show = (i) => (document.getElementById("readout").textContent = `${rows[i][0]}: ${series.map(([at, name]) => `${rows[i][at]} ${name}`).join(", ")}`);\n function draw() {\n const w = plot.clientWidth, h = plot.clientHeight, left = 36, base = h - 24;\n const x = (i) => left + 12 + (i * (w - left - 36)) / (rows.length - 1);\n const y = (v) => base - (v / 60) * (base - 8);\n const step = Math.ceil((rows.length * 44) / w); // x labels at least 44 px apart\n let svg = `<svg width="${w}" height="${h}" font-size="12" fill="var(--color-text-secondary)">`;\n for (const v of [0, 30, 60]) svg += `<line x1="${left}" x2="${w}" y1="${y(v)}" y2="${y(v)}" stroke="var(--color-border-tertiary)"/><text x="${left - 6}" y="${y(v) + 4}" text-anchor="end">${v}</text>`;\n rows.forEach((row, i) => { if (i % step === 0) svg += `<text x="${x(i)}" y="${h - 6}" text-anchor="middle">${row[0]}</text>`; });\n for (const [at, , color] of series) svg += `<polyline points="${rows.map((row, i) => `${x(i)},${y(row[at])}`).join(" ")}" fill="none" stroke="${color}" stroke-width="2"/>`;\n // The two values the title rests on; then a column per week to tap.\n for (const i of [0, rows.length - 1]) svg += `<text x="${x(i)}" y="${y(rows[i][2]) - 10}" text-anchor="middle" fill="var(--color-text-primary)">${rows[i][2]}</text>`;\n const band = (w - left) / rows.length;\n rows.forEach((row, i) => (svg += `<rect data-i="${i}" x="${x(i) - band / 2}" width="${band}" height="${base}" fill="transparent"/>`));\n plot.innerHTML = svg + "</svg>";\n }\n plot.addEventListener("click", (event) => event.target.dataset.i && show(Number(event.target.dataset.i)));\n show(rows.length - 1);\n new ResizeObserver(draw).observe(plot);\n<\/script>',bars:`<div style="display: grid; grid-template-columns: 5em 1fr 3em; gap: 8px; align-items: center; font-size: var(--font-text-sm-size)">
|
|
20
|
+
<span>Web</span><div style="height: 12px; width: 100%; background: var(--color-chart-1)"></div><span>412</span>
|
|
21
|
+
<span>iOS</span><div style="height: 12px; width: 77%; background: var(--color-text-disabled)"></div><span>318</span>
|
|
22
|
+
<span>Docs</span><div style="height: 12px; width: 16%; background: var(--color-text-disabled)"></div><span>64</span>
|
|
23
|
+
</div>`};var sn={layout:"where",theme:"where",storage:"interactive",errors:"interactive",silent:"sandbox",network:"sandbox","inline-assets":"sandbox",dialogs:"sandbox",print:"sandbox",downloads:"sandbox",forms:"sandbox","link-schemes":"sandbox",windows:"sandbox",devices:"sandbox",workers:"sandbox",sound:"sandbox",address:"sandbox",shell:"sandbox"},ln=e=>e.mode==="artifact"?"page":e.mode==="mcp-app"?"view":"widget",ye=(e,t)=>F(e,t).length>0||(e.egress.anyHttps??[]).some(n=>n===t);function we(e,t){if(e.mode==="url")return[];let n=pe(e),o=new Set(D(e)),r=new Set(Y(e)?.globals??[]),a=ln(e),i=[],s=(p,y)=>{y!==null&&i.push({id:p,text:y,topic:sn[p]})};s("layout",e.layout.kind==="fill"?"It is as tall as the window: the page scrolls itself, and position: fixed and sticky work.":o.has("resize")?`Its height follows its content, at most ${e.limits.maxHeightPx.toLocaleString("en-US")} px (then it scrolls inside).`:"Its height is fixed by the app: keep it short, or let it scroll inside.");let c=e.theme.documents??"declared";s("theme",e.mode!=="artifact"||c==="always"?"It runs in the person's light or dark theme.":c==="never"?"It always shows in light: the app doesn't theme it, so style it yourself; prefers-color-scheme may report the reader's system setting, so don't rely on it.":`It shows in light, as you style it; to follow the person's light or dark theme, declare it in its head (<meta name="color-scheme" content="light dark">) and draw with the variables below.`);let l="a blocked script, style or image just doesn't load and a blocked fetch rejects";s("silent",o.has("reportError")?e.errors==="agent"?`What the sandbox blocks shows nothing on the page: ${l}. Each block comes back to you as an error, so check them.`:e.errors==="host"?`What the sandbox blocks shows nothing on the page: ${l}. The app is told of each block, but you aren't: test what you write.`:`What the sandbox blocks shows nothing on the page: ${l}. Each block is reported to the app.`:"What the sandbox blocks fails silently: a blocked script, style or image just doesn't load, a blocked fetch rejects, and nothing shows an error."),s("network",t);let f=p=>(e.egress.anyHttps??[]).some(y=>y===p),k=["img","font","media"].filter(p=>!f(p));if(k.some(p=>ye(e,p))||ye(e,"style")||F(e,"script").length>0||F(e,"connect").length>0||k.length<3){let p=k.length===0?null:k.every(A=>!ye(e,A))?"images, icons, fonts and sound go inline (data: URLs or SVG)":"images, fonts and sound from anywhere not listed go inline (data: URLs or SVG)",y=ye(e,"style")?"other CSS goes in a <style>":"CSS goes in a <style>",C=[p,y].filter(A=>A!==null).join("; ");s("inline-assets",`${C.charAt(0).toUpperCase()}${C.slice(1)}.`)}n.includes("allow-modals")||(s("dialogs","alert, confirm and prompt show nothing: confirm returns false and prompt null. Draw your own dialogs."),s("print","window.print() does nothing.")),n.includes("allow-downloads")||s("downloads",`Downloads don't work: <a download>, data: and blob: links, saves from script.${o.has("downloadFile")?" To give the person a file, ask the app (ui/download-file; with the App SDK, app.downloadFile).":""}`),n.includes("allow-forms")||s("forms",'Forms never submit, and some browsers fire no submit event: use them as UI, give buttons type="button" and handle their click.'),s("link-schemes",`mailto:, tel: and sms: links open nothing: show the address or number as text${r.has("copyText")?" (with a copy button)":""}.`),n.includes("allow-popups")||s("windows",r.has("openLink")||o.has("openLink")?"window.open and target=_blank open no window (window.open returns null or throws): links open in a new tab through the app (openLink; a click on <a href> does it).":"Links to other pages, window.open and target=_blank open nothing (window.open returns null or throws)."),s("devices","Camera, microphone, location and sharing are denied without asking (they reject, or aren't there); file inputs and FileReader work."),s("workers","Web Workers from blob: URLs work; service workers can't be registered (even reading navigator.serviceWorker can throw), and WebRTC is gone."),s("sound","Sound (and video with sound) plays only from a click: call play() in its handler."),s("address",e.isolation==="opaque"?`The ${a}'s address is about:srcdoc: it has no query string or hash to read.`:`The ${a}'s address is its own (with Porthole's one-time ticket), never the app's: read no parameters from it.`),s("shell",`document.write and document.open throw; nested frames (iframe, object, embed) are removed; full screen and the clipboard are closed${r.has("copyText")?" (copy buttons call copyText)":""}.`),s("storage",e.isolation==="opaque"?`State isn't saved: localStorage works but lasts only while the ${a} is on screen, and a reload or another device starts fresh. Keep what matters in the ${a}'s code.`:e.storage?.scope!==void 0?`localStorage is kept for ${e.storage.scope}, in this browser; another browser or device starts fresh. Reads and writes can throw (private windows, strict settings): wrap them in try/catch and keep only what can be lost.`:e.storage?.shared===void 0?`localStorage is kept for this ${a} in this browser; another device starts fresh. Reads and writes can throw (private windows, strict settings): wrap them in try/catch and keep only what can be lost.`:`localStorage is shared by every ${a} in this ${e.storage.shared}, in this browser (another device starts fresh): prefix your keys with a name of this ${a}'s own ("budget:filters", not "filters"), or ${a}s overwrite each other. Reads and writes can throw (private windows, strict settings): wrap them in try/catch and keep only what can be lost.`);let d=e.limits.maxErrorReports,g=(e.capabilities.reportError?.gates??[{rateLimit:{perMinute:10}}]).find(p=>typeof p=="object"),u=g===void 0?"":` At most ${g.rateLimit.perMinute} a minute.`;return s("errors",o.has("reportError")?e.errors==="agent"?`Errors: what your code throws, and anything the sandbox blocks, comes back to you (at most ${d} a run), so you can fix it.${u}`:e.errors==="host"?`Errors: what your code throws, and anything the sandbox blocks, goes to the app (at most ${d} a run), not to you: test what you write.${u}`:`Errors: what your code throws, and anything the sandbox blocks, is reported to the app (at most ${d} a run).${u}`:"Errors: nothing reports what your code throws, to you or the app: test what you write."),i}function pt(e){return e<1e6?`${Math.floor(e/1e3)} KB`:`${Math.round(e/1048576*10)/10} MB`}var ft=/--[a-z0-9]+(?:-[a-z0-9]+)*/gu;function gt(e){let t=Object.keys(e.theme.variables),n=(...a)=>a.every(i=>t.includes(i)),o=new Set(D(e)),r=new Set(Y(e)?.globals??[]);return{config:e,noun:e.mode==="artifact"?"page":e.mode==="mcp-app"?"view":"widget",has:n,when:a=>n(...a.match(ft)??[])?a:null,names:(a,i=[])=>t.filter(s=>s.startsWith(a)&&!i.some(c=>s.startsWith(c))).join(", "),declared:a=>o.has(a),global:a=>r.has(a)}}var je=(...e)=>e.filter(t=>t!==null).join(`
|
|
24
|
+
`).replace(/\n{3,}/gu,`
|
|
25
|
+
|
|
26
|
+
`).trim(),yt={script:"scripts",style:"styles",font:"fonts",img:"images",media:"audio and video",connect:"data (fetch, WebSocket)"},wt="any https site",dn=(e,t)=>[...ue(e,t)?[wt]:[],...F(e,t)];function cn(e){let n=["script","style","font","img","media","connect"].map(i=>[i,dn(e,i)]).filter(([,i])=>i.length>0);if(n.length===0)return"Network: none. Nothing loads from outside (no scripts, styles, fonts, images or data from any server) and any fetch fails, so write everything inline: SVG and CSS draw charts well.";let o=new Map;for(let[i,s]of n)for(let c of s)o.set(c,[...o.get(c)??[],i]);let r=new Map;for(let[i,s]of o){let c=s.map(f=>yt[f]),l=c.length===1?c[0]??"":`${c.slice(0,-1).join(", ")} and ${c.at(-1)}`;r.set(l,[...r.get(l)??[],i])}return`Network: only ${[...r].map(([i,s])=>`${i} from ${s.join(", ")}`).join("; ")}. Anything else fails.`}var un=e=>e.mode==="artifact"&&(e.theme.documents??"declared")==="declared",_e=e=>F(e,"script").length>0;function pn(e){let{config:t,noun:n}=e,{layout:o,limits:r}=t,a=n==="page"?"Pages":n==="view"?"Views":"Widgets",i=t.guide.toolName===void 0?"":` (${t.guide.toolName})`,s=t.mode==="artifact"?"What you pass: a whole HTML document (<!doctype html>, <head>, <body>). It is written into its frame once all of it has arrived, then its scripts run in order.":t.mode==="mcp-app"?"What you pass: an MCP Apps view, a whole HTML document that talks to its host through the official App SDK (@modelcontextprotocol/ext-apps) or the protocol's own messages.":"What you pass: SVG that starts with <svg, or an HTML fragment: no <!doctype>, <html>, <head> or <body>. Order it styles, then markup, then scripts: people see the styled markup as you write it, and scripts run once all of it is written, in order, each external one loaded before the next runs.",c=we(t,cn(t)),l=u=>c.find(p=>p.id===u)?.text??"",f=o.kind==="fill",k=t.theme.documents==="never",m=o.narrowWidthPx+20,v=Math.round(13*o.minWidthPx/m*10)/10,d=Math.round(13*400/m*10)/10,g=He(t.theme.variables["--font-text-md-size"]??"14px","light").replace(/^(\d+(?:\.\d+)?)px$/u,"$1 px");return je(`# ${a}${i}`,"",s,"",`Where it runs: ${f?`in a sandboxed frame that fills the viewer, about ${o.narrowWidthPx} px wide on a phone (${o.minWidthPx} on a small one) and as wide as the window on a desktop`:`in a sandboxed frame as wide as the column it sits in, about ${o.narrowWidthPx} px on a phone (${o.minWidthPx} on a small one) and ${o.wideWidthPx} on a desktop`}; the same ${n} is read on both, so never write for one of them. Design for ${o.narrowWidthPx} first; where the layout depends on the width, read it as it runs (the ${f?"window's innerWidth, again on resize":"box's clientWidth, again from a ResizeObserver"}). ${l("layout")} ${l("theme")} It runs again each time it is shown${_e(t)?": pin library versions":""}.`,"","Sizes that read on a phone:",e.when(`- Words go in HTML (titles, notes, legends, readouts): it keeps its size and wraps. The frame's text is already ${g} --font-sans in --color-text-primary; notes --font-text-sm-size, never under 12 px.`),`- An SVG scales with the frame: its text shows at font-size \xD7 frame width \xF7 viewBox width, so a 680-wide viewBox turns 12 into 5 px on a phone. Draw it for the phone: viewBox="0 0 ${m} H", no text under 13, width="100%" style="max-width: 400px"; its text then shows at ${v} to ${d} px on every screen. Or draw it 1 unit = 1 px with a script (topic chart).`,`- The frame never scrolls sideways: nothing fixed wider than ${o.minWidthPx} px; images max-width: 100%; a grid or flex child with long text min-width: 0; a wide table or code block in its own box with overflow-x: auto.`,"- Cards in a row wrap (flex-wrap: wrap; flex: 1 1 100px), or a grid of repeat(auto-fit, minmax(160px, 1fr)): at most 2 columns under 500 px, never a fixed repeat(3, \u2026).","- Touch: anything clickable is at least 44 px tall. Phones don't hover: what a hover shows, a tap (click) shows too.",f?null:"- No position: fixed: the frame grows with its content, so a fixed element covers it.","","The sandbox:",...c.filter(u=>u.topic==="sandbox").map(u=>`- ${u.text}`),`Keep it under ${pt(r.maxBytes)}.`,_e(t)?`
|
|
27
|
+
Libraries: pin an exact version at least two weeks old (chart.js@4.4.1 in its URL), never @latest or a range, so the code can't be swapped under you and draws the same every time.`:null,"",k?`Colors: these variables hold the app's light palette; use them, or colors of your own. Give the ${n} its own background.`:un(t)?`Colors, for a ${n} that declares light and dark: every color from these variables, never hard-coded, so both work; its own background then stays transparent (it sits on the app's surface).`:`Colors: every color from these variables, never hard-coded, so light and dark both work. The ${n}'s own background is transparent (it sits on the app's surface).`,e.names("--color-background-")===""?null:`- Surfaces: ${e.names("--color-background-")}`,e.names("--color-text-")===""?null:`- Text: ${e.names("--color-text-")}`,e.names("--color-border-")===""?null:`- Borders: ${e.names("--color-border-")}${e.names("--color-ring-")===""?"":`; focus rings: ${e.names("--color-ring-")}`}`,e.names("--color-accent")===""?null:`- Accent: ${[e.when("--color-accent (a button)"),e.when("--color-accent-soft (a selected item's fill)"),e.when("in a figure, --color-chart-1 (topic chart)")].filter(u=>u!==null).join("; ")}`,e.names("--font-")===""?null:`- Type: ${e.names("--font-")}`,e.names("--border-")===""&&e.names("--shadow-")===""?null:`- Shape: ${[e.names("--border-"),e.names("--shadow-")].filter(u=>u!=="").join(", ")}`,mt(e)===""?null:`- Also: ${mt(e)}`,"",be(e,q.stats)?`Cards that read in both themes, and wrap on a phone:
|
|
28
|
+
${q.stats}
|
|
29
|
+
`:null,e.when("Don't use --color-background-primary as a card's fill: it is the app's own surface, so a card on it vanishes."),k?null:`A theme change updates the variables in place and fires window "${t.theme.event}": CSS that uses var(...) follows by itself; a canvas or a library you gave colors to must redraw.`,t.mode==="inline"?"Buttons and inputs already match the app (its borders, radius, colors and focus ring, 44 px tall): use them as they are, or restyle them with your own CSS.":null,"","Diagrams (boxes and arrows): show the mechanism asked about (what calls what, where data goes), only the parts the ask names; label each arrow with what moves.","- Top to bottom, at most 2 boxes a row, in the phone viewBox; the title in HTML above. Too much for one glance on a phone: make two.","- SVG doesn't wrap: break lines yourself. A box is its longest line + 24 wide (13 px text takes about 7 units a character, a 14 px bold name 8.5: a 136-wide box holds 16); a name and a line, 52 tall; 16 between boxes.",e.when("- Boxes fill none, stroke --color-border-secondary; at most one stands out: stroke --color-chart-1, 2 wide, fill --color-chart-1 at fill-opacity 0.12. Arrows --color-text-tertiary, in the gaps, never across a box, their labels beside them. Every <text> sets fill: names --color-text-primary, the rest --color-text-secondary."),be(e,q.diagram)?`
|
|
30
|
+
${q.diagram}`:null)}function mt(e){let t=["--color-background-","--color-text-","--color-border-","--color-ring-","--color-accent","--color-chart-","--font-","--border-","--shadow-"];return Object.keys(e.config.theme.variables).filter(n=>!t.some(o=>n.startsWith(o))).join(", ")}function be(e,t){return e.has(...t.match(ft)??[])&&!/https?:\/\//u.test(t)}function mn(e){let{config:t}=e,n=Object.keys(t.theme.variables).filter(a=>/^--color-chart-\d+$/u.test(a)),o=Object.keys(t.theme.variables).filter(a=>/^--color-chart-seq-\d+$/u.test(a)),r=_e(t)?`Libraries (Chart.js, ECharts, d3) draw in px too, in a box with a set height (Chart.js responsive: true, maintainAspectRatio: false). They don't read CSS variables: pass in getComputedStyle(document.documentElement).getPropertyValue("${n[0]??"--color-text-primary"}").trim()${t.theme.documents==="never"?"":`, redraw on window "${t.theme.event}"`}, and override their palette and white background.`:"No chart library loads here: draw the SVG yourself, from a script, as below.";return je(`# Charts (a chart is a ${e.noun})`,"","Pick the form first: a trend over time \u2192 line; compare categories \u2192 horizontal bars; x against y \u2192 scatter; one number \u2192 a big number, not a chart; a few numbers \u2192 a table, not a chart.","","The look:",'- The title states the finding, with its number ("Failed builds fell from 9 to 2 a week"), not the topic; a line under it says what is measured, in what unit.',e.when("- Grey first: the data in a quiet grey (--color-text-disabled), one accent (--color-chart-1) on what the title is about. More hues only for series that differ in kind and must each be found."),"- Values on the marks or a light axis, not both; label only the points the finding rests on. Bars start at zero.","- Write the numbers once, as rows, and draw marks, labels and readout from them.","","Size: draw it at the frame's width, 1 unit = 1 px: read the box's clientWidth, draw an <svg> that wide with 12 px text, and again from a ResizeObserver. Its text stays 12 px on every screen; keep x labels 44 px apart. No script: the phone viewBox (topic widget). Bars need no SVG: an HTML grid row per category.","",r,"",n.length===0?null:"Colors:",n.length===0?null:`- Series of different kinds: ${n[0]} \u2026 ${n.at(-1)}, in this order, never cycled, at most ${n.length} (fold the rest into "Other"); a series keeps its color when a filter hides others.`,o.length===0?null:`- Magnitude (a heatmap): ${o[0]} \u2026 ${o.at(-1)}, 1 nearest zero.${e.has("--color-chart-neg","--color-chart-mid","--color-chart-pos")?" Above or below a midpoint: --color-chart-neg, --color-chart-mid, --color-chart-pos.":""}`,e.when("- Never a rainbow. Status (good, bad) uses --color-text-success, --color-text-warning, --color-text-danger with a word, never as a series color."),e.when("- Text is never in a series color: --color-text-primary or --color-text-secondary; the mark beside it carries identity."),"",e.when("Marks: 2 px lines, bars with a 2 px gap; grid lines --color-border-tertiary, 1 px; one y axis (two scales: two charts); the legend in HTML."),"",`Reading values: a readout line in HTML that a tap on a column fills (each column a hit area the plot's height), the latest values shown before any tap; a "Table" toggle for the numbers.`,be(e,q.line)?`
|
|
31
|
+
A line chart drawn at the frame's width, its numbers on a tap:
|
|
32
|
+
${q.line}`:null,be(e,q.bars)?`
|
|
33
|
+
Bars in HTML, under a title like the line chart's:
|
|
34
|
+
${q.bars}`:null)}function j(e,t){let n=[];e.includes("confirm")&&n.push(`The app shows the person ${t} first, and nothing happens unless they confirm.`),e.includes("user-activation")&&n.push("Call it from a click (it needs one).");for(let o of e)typeof o=="object"&&n.push(`At most ${o.rateLimit.perMinute} a minute.`);return n.join(" ")}var hn={sendPrompt:(e,t)=>e.global("sendPrompt")?`- sendPrompt(text): asks to send text to you as the person's message (at most ${e.config.limits.maxPromptChars.toLocaleString("en-US")} characters). Use it for "explain this point", "try these settings". ${j(t,"the whole text")} It returns a promise: true if it was sent.`:`- Messages: the MCP Apps request ui/message (with the App SDK, app.sendMessage) asks to send text to you as the person's message. ${j(t,"the whole text")}`,openLink:(e,t)=>e.global("openLink")?`- openLink(url): asks to open an http(s) link in a new tab; a click on an <a href> does the same for you. ${j(t,"the link")} It returns a promise: true if it opened.`:`- Links: the MCP Apps request ui/open-link (with the App SDK, app.openLink) asks to open an http(s) link in a new tab. ${j(t,"the link")}`,downloadFile:(e,t)=>`- Downloads: the MCP Apps request ui/download-file (with the App SDK, app.downloadFile). ${j(t,"the file")}`,callTool:(e,t)=>`- Tools: the MCP Apps request tools/call (with the App SDK, app.callServerTool). ${j(t,"the call")}`,readData:(e,t)=>`- Data: the MCP Apps request resources/read (with the App SDK, app.readServerResource). ${j(t,"the request")}`,setModelContext:(e,t)=>`- Context for the agent: the MCP Apps request ui/update-model-context (with the App SDK, app.updateModelContext). ${j(t,"it")}`,copyText:(e,t)=>e.global("copyText")?`- copyText(text): copies text to the person's clipboard (at most ${2e4.toLocaleString("en-US")} characters), and the app shows them what was copied. Use it for every copy button: nothing else in the ${e.noun} can write the clipboard. ${j(t,"the text")} It returns a promise: true if it was copied.`:`- Copying: the request porthole/copy-text with { text } (at most ${2e4.toLocaleString("en-US")} characters) copies text to the person's clipboard, and the app shows them what was copied; nothing else in the ${e.noun} can write the clipboard. ${j(t,"the text")}`,annotate:e=>`- Comments: the person may point at parts of the ${e.noun} to comment on them. Give headings and the parts that matter stable ids (id="revenue"), so comments stay on them when the ${e.noun} is written again.`,reportError:e=>`- ${we(e.config,"").find(t=>t.id==="errors")?.text??""}`,resize:()=>""};function fn(e){let{config:t,noun:n}=e,o=D(t).map(l=>{let f=t.capabilities[l]?.guide;return f===void 0?hn[l](e,K(t,l)):`- ${f}`}).filter(l=>l!==""),r=we(t,""),a=r.find(l=>l.id==="storage"),i=a===void 0?null:`- ${a.text}`,s=e.declared("reportError")?null:`- ${r.find(l=>l.id==="errors")?.text??""}`,c=o.length===0?"- It can only draw: it has no way to send anything to you or the app.":null;return je(`# Interactive ${n}s`,"",...o,s,c,i)}var ht={widget:pn,chart:mn,interactive:fn};function bt(e,t){let n=gt(e),o=e.guide.topics??{};return(t??De).map(r=>{if(Object.hasOwn(ht,r))return ht[r](n);let a=o[r];if(a===void 0)throw new RangeError(`No guide topic ${JSON.stringify(r)}`);return a}).join(`
|
|
35
|
+
|
|
36
|
+
`)}function xt(e){let t=gt(e),{layout:n}=e,o=e.mode==="artifact"?"Show a whole HTML page you write: a report, a document, a small app.":e.mode==="mcp-app"?"Show an MCP Apps view.":"Render an SVG or an HTML fragment you write, shown inline: charts, diagrams, explainers, small interactive tools.",r=[...new Set(["script","style","font","img","media","connect"].flatMap(m=>F(e,m)))],a=["img","media","font"].filter(m=>ue(e,m)).map(m=>yt[m]),i=a.length===0?"":`${a.length===1?a[0]:`${a.slice(0,-1).join(", ")} and ${a.at(-1)}`} from ${wt}`,s=r.length===0&&i===""?"with no network (nothing loads from outside)":r.length===0?`loading only ${i}`:`loading only from ${r.join(", ")}${i===""?"":`, and ${i}`}`,c=D(e).flatMap(m=>{let v=B(m);return v.global===void 0||!t.global(v.global)?[]:v.global==="sendPrompt"?["sendPrompt(text) asks the person to send text to you as their message."]:v.global==="openLink"?["openLink(url) asks the person to open a link."]:[]}),l=e.guide.guideToolName===void 0?"":` Read ${e.guide.guideToolName} once before your first ${t.noun}${e.mode==="inline"?" (topic chart before your first chart)":""}.`,f=n.kind==="fill"?`It fills the viewer in a sandbox about ${n.narrowWidthPx} px wide on a phone, from the same code on every screen`:`It runs in a sandbox about ${n.narrowWidthPx} px wide on a phone and ${n.wideWidthPx} on a desktop, from the same code${t.declared("resize")?" (its height follows the content)":""}`,k=e.theme.documents==="never"?"always in light (the app doesn't theme it)":"in the person's light or dark theme through CSS variables";return`${o} ${f}, ${k}, ${s}. ${c.join(" ")}${l} Each time it is shown it runs again from this code.`.replace(/ {2,}/gu," ")}var gn=/^(https?):\/\/([a-z0-9.-]+|\[[0-9a-f:.]+\])(?::(\d{1,5}))?$/u,vt=/^\d{1,3}(\.\d{1,3}){3}$/u,kt={http:"80",https:"443"};function xe(e){let t=gn.exec(e);if(t===null)return null;let[,n="",o="",r]=t;return{scheme:n,host:o,port:r??kt[n]??""}}var yn=/^[a-z][a-z0-9+.-]*:\/\/([a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*|\[[0-9a-f:.]+\])(:\d{1,5})?$/u;function wn(e){return typeof e=="string"&&e.length<=300&&yn.test(e)}function St(e){return wn(e)?null:e==="null"?`the host page's origin is opaque ("null"): serve it from a real origin (an Electron custom protocol must be registered as standard, and file: pages have none)`:`${JSON.stringify(e)} is not an exact origin (scheme://host[:port], lowercase, no path)`}function bn(e,t){let n=e.toLowerCase().replace(/\.$/u,""),o=t.toLowerCase().replace(/\.$/u,"");if(n===o)return!0;if(vt.test(n)||vt.test(o)||n.startsWith("[")||o.startsWith("["))return!1;let r=n.split(".").toReversed(),a=o.split(".").toReversed(),i=0;for(;i<r.length&&i<a.length&&r[i]===a[i];)i+=1;return i>=2}function xn(e,t){let n=xe(e),o=/^(https?):\/\/\*\.([a-z0-9.-]+)(?::(\d{1,5}))?$/u.exec(t);if(n===null)return!1;if(o===null){let s=xe(t);return s!==null&&s.scheme===n.scheme&&s.host===n.host&&s.port===n.port}let[,r="",a="",i]=o;return r===n.scheme&&(i??kt[r])===n.port&&n.host.endsWith(`.${a}`)&&n.host.length>a.length+1}function Ct(e,t,n){let o=xe(e.origin),r;try{r=new URL(e.url)}catch{throw new TypeError(`The located URL ${JSON.stringify(e.url)} is not a URL`)}if(o===null||r.origin!==e.origin)throw new TypeError(`The located URL ${JSON.stringify(e.url)} is not on its origin ${JSON.stringify(e.origin)}`);if(!t.some(i=>xn(e.origin,i)))throw new TypeError(`The located origin ${e.origin} is not one of the config's content origins`);let a=xe(n);if(a!==null&&bn(o.host,a.host))throw new TypeError(`The located origin ${e.origin} may be the same site as the host (${n}): content must be served from a registrable domain the host uses for nothing else`)}var Tt=String.raw`if (declared("annotate")) {
|
|
37
|
+
(() => {
|
|
38
|
+
const LIMIT = { exact: 4000, quote: 300, context: 32, label: 120, css: 1000, marks: 500, boxes: 200 };
|
|
39
|
+
const M = {
|
|
40
|
+
start: "porthole/annotate.start",
|
|
41
|
+
stop: "porthole/annotate.stop",
|
|
42
|
+
show: "porthole/annotate.show",
|
|
43
|
+
focus: "porthole/annotate.focus",
|
|
44
|
+
clear: "porthole/annotate.clear",
|
|
45
|
+
selected: "porthole/annotate.selected",
|
|
46
|
+
unselected: "porthole/annotate.unselected",
|
|
47
|
+
cancelled: "porthole/annotate.cancelled",
|
|
48
|
+
anchored: "porthole/annotate.anchored",
|
|
49
|
+
};
|
|
50
|
+
// The DOM's own functions, taken before content runs.
|
|
51
|
+
const elementFromPoint = Document.prototype.elementFromPoint;
|
|
52
|
+
const querySelector = Document.prototype.querySelector;
|
|
53
|
+
const createRange = Document.prototype.createRange;
|
|
54
|
+
const createTreeWalker = Document.prototype.createTreeWalker;
|
|
55
|
+
const createElement = Document.prototype.createElement;
|
|
56
|
+
const selectionOf = Document.prototype.getSelection;
|
|
57
|
+
const elementRect = Element.prototype.getBoundingClientRect;
|
|
58
|
+
const rangeRect = Range.prototype.getBoundingClientRect;
|
|
59
|
+
const rangeRects = Range.prototype.getClientRects;
|
|
60
|
+
const attachShadow = Element.prototype.attachShadow;
|
|
61
|
+
const post = window.parent.postMessage.bind(window.parent);
|
|
62
|
+
const tell = (method, params) => {
|
|
63
|
+
if (window.parent !== window) {
|
|
64
|
+
post({ jsonrpc: "2.0", method, params }, "*");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const squash = (text) => String(text).replace(/\s+/g, " ").trim();
|
|
68
|
+
const box = (rect) => ({ x: rect.left, y: rect.top, width: rect.width, height: rect.height });
|
|
69
|
+
|
|
70
|
+
// The document's text: the body's text nodes in order, without scripts, styles, templates.
|
|
71
|
+
const SKIPPED = { script: 1, style: 1, noscript: 1, template: 1 };
|
|
72
|
+
const textIndex = () => {
|
|
73
|
+
const nodes = [];
|
|
74
|
+
const starts = [];
|
|
75
|
+
const at = new Map();
|
|
76
|
+
let text = "";
|
|
77
|
+
const body = document.body;
|
|
78
|
+
if (body) {
|
|
79
|
+
const walker = createTreeWalker.call(document, body, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, {
|
|
80
|
+
acceptNode: (node) => (node.nodeType === 1 ? (SKIPPED[node.localName] ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_SKIP) : NodeFilter.FILTER_ACCEPT),
|
|
81
|
+
});
|
|
82
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
83
|
+
at.set(node, nodes.length);
|
|
84
|
+
nodes.push(node);
|
|
85
|
+
starts.push(text.length);
|
|
86
|
+
text += node.nodeValue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { nodes, starts, at, text };
|
|
90
|
+
};
|
|
91
|
+
// A boundary point as an offset into the text: in a text node, where it is; elsewhere, the
|
|
92
|
+
// first text at or after it.
|
|
93
|
+
const offsetOf = (index, container, offset) => {
|
|
94
|
+
if (container.nodeType === 3 && index.at.has(container)) {
|
|
95
|
+
return index.starts[index.at.get(container)] + Math.min(offset, container.nodeValue.length);
|
|
96
|
+
}
|
|
97
|
+
const point = createRange.call(document);
|
|
98
|
+
try {
|
|
99
|
+
point.setStart(container, offset);
|
|
100
|
+
} catch (_) {
|
|
101
|
+
return index.text.length;
|
|
102
|
+
}
|
|
103
|
+
point.collapse(true);
|
|
104
|
+
let low = 0;
|
|
105
|
+
let high = index.nodes.length;
|
|
106
|
+
while (low < high) {
|
|
107
|
+
const middle = (low + high) >> 1;
|
|
108
|
+
if (point.comparePoint(index.nodes[middle], 0) >= 0) {
|
|
109
|
+
high = middle;
|
|
110
|
+
} else {
|
|
111
|
+
low = middle + 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return low < index.nodes.length ? index.starts[low] : index.text.length;
|
|
115
|
+
};
|
|
116
|
+
const pointAt = (index, at, end) => {
|
|
117
|
+
let low = 0;
|
|
118
|
+
let high = index.nodes.length - 1;
|
|
119
|
+
let found = -1;
|
|
120
|
+
while (low <= high) {
|
|
121
|
+
const middle = (low + high) >> 1;
|
|
122
|
+
if (index.starts[middle] <= at) {
|
|
123
|
+
found = middle;
|
|
124
|
+
low = middle + 1;
|
|
125
|
+
} else {
|
|
126
|
+
high = middle - 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (found === -1) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
let node = index.nodes[found];
|
|
133
|
+
let offset = at - index.starts[found];
|
|
134
|
+
if (end) {
|
|
135
|
+
// An end at a node's very start belongs to the text before it.
|
|
136
|
+
while (offset === 0 && found > 0) {
|
|
137
|
+
found -= 1;
|
|
138
|
+
node = index.nodes[found];
|
|
139
|
+
offset = node.nodeValue.length;
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
while (offset >= node.nodeValue.length && found + 1 < index.nodes.length) {
|
|
143
|
+
found += 1;
|
|
144
|
+
node = index.nodes[found];
|
|
145
|
+
offset = 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { node, offset: Math.min(offset, node.nodeValue.length) };
|
|
149
|
+
};
|
|
150
|
+
const rangeAt = (index, start, end) => {
|
|
151
|
+
const from = pointAt(index, start, false);
|
|
152
|
+
const to = pointAt(index, end, true);
|
|
153
|
+
if (from === null || to === null) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const range = createRange.call(document);
|
|
157
|
+
try {
|
|
158
|
+
range.setStart(from.node, from.offset);
|
|
159
|
+
range.setEnd(to.node, to.offset);
|
|
160
|
+
} catch (_) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return range;
|
|
164
|
+
};
|
|
165
|
+
const spanOf = (index, node) => {
|
|
166
|
+
const range = createRange.call(document);
|
|
167
|
+
range.selectNodeContents(node);
|
|
168
|
+
return [offsetOf(index, range.startContainer, range.startOffset), offsetOf(index, range.endContainer, range.endOffset)];
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// A CSS selector that matches this element and no other: from the nearest id, else the body.
|
|
172
|
+
const escape = typeof CSS === "object" && CSS !== null && typeof CSS.escape === "function" ? CSS.escape : (value) => String(value).replace(/[^a-zA-Z0-9_-]/g, (each) => "\\" + each);
|
|
173
|
+
const only = (selector, element) => {
|
|
174
|
+
try {
|
|
175
|
+
return querySelector.call(document, selector) === element;
|
|
176
|
+
} catch (_) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const cssPath = (element) => {
|
|
181
|
+
const steps = [];
|
|
182
|
+
for (let node = element; node !== null && node.nodeType === 1 && steps.length < 16; node = node.parentElement) {
|
|
183
|
+
if (node.id && only("#" + escape(node.id), node)) {
|
|
184
|
+
steps.unshift("#" + escape(node.id));
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
if (node.localName === "body" || node.localName === "html") {
|
|
188
|
+
steps.unshift(node.localName);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
let step = escape(node.localName);
|
|
192
|
+
const parent = node.parentElement;
|
|
193
|
+
if (parent !== null) {
|
|
194
|
+
const same = Array.from(parent.children).filter((child) => child.localName === node.localName);
|
|
195
|
+
if (same.length > 1) {
|
|
196
|
+
step += ":nth-of-type(" + (same.indexOf(node) + 1) + ")";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
steps.unshift(step);
|
|
200
|
+
}
|
|
201
|
+
const selector = steps.join(" > ");
|
|
202
|
+
return selector.length <= LIMIT.css && only(selector, element) ? selector : null;
|
|
203
|
+
};
|
|
204
|
+
const describe = (element) => {
|
|
205
|
+
let name = element.localName + (element.id ? "#" + element.id : "");
|
|
206
|
+
const classes = typeof element.className === "string" ? element.className.trim().split(/\s+/).filter(Boolean).slice(0, 2) : [];
|
|
207
|
+
for (const each of classes) {
|
|
208
|
+
name += "." + each;
|
|
209
|
+
}
|
|
210
|
+
const words = squash(element.textContent || "").slice(0, 40);
|
|
211
|
+
return (words ? name + " " + words : name).slice(0, LIMIT.label);
|
|
212
|
+
};
|
|
213
|
+
const quoteOf = (index, start, end) => {
|
|
214
|
+
const quote = { type: "TextQuoteSelector", exact: index.text.slice(start, end) };
|
|
215
|
+
const prefix = index.text.slice(Math.max(0, start - LIMIT.context), start);
|
|
216
|
+
const suffix = index.text.slice(end, end + LIMIT.context);
|
|
217
|
+
if (prefix) {
|
|
218
|
+
quote.prefix = prefix;
|
|
219
|
+
}
|
|
220
|
+
if (suffix) {
|
|
221
|
+
quote.suffix = suffix;
|
|
222
|
+
}
|
|
223
|
+
return quote;
|
|
224
|
+
};
|
|
225
|
+
const elementAnchor = (element, index) => {
|
|
226
|
+
const selectors = [];
|
|
227
|
+
const css = cssPath(element);
|
|
228
|
+
if (css !== null) {
|
|
229
|
+
selectors.push({ type: "CssSelector", value: css });
|
|
230
|
+
}
|
|
231
|
+
const span = spanOf(index, element);
|
|
232
|
+
const end = Math.min(span[1], span[0] + LIMIT.quote);
|
|
233
|
+
if (index.text.slice(span[0], end).trim() !== "") {
|
|
234
|
+
selectors.push(quoteOf(index, span[0], end), { type: "TextPositionSelector", start: span[0], end });
|
|
235
|
+
}
|
|
236
|
+
return selectors.length === 0 ? null : { kind: "element", selectors, label: describe(element) };
|
|
237
|
+
};
|
|
238
|
+
const textAnchor = (range, index) => {
|
|
239
|
+
const start = offsetOf(index, range.startContainer, range.startOffset);
|
|
240
|
+
const end = Math.min(offsetOf(index, range.endContainer, range.endOffset), start + LIMIT.exact);
|
|
241
|
+
const exact = index.text.slice(start, end);
|
|
242
|
+
if (exact.trim() === "") {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const selectors = [quoteOf(index, start, end), { type: "TextPositionSelector", start, end }];
|
|
246
|
+
let container = range.commonAncestorContainer;
|
|
247
|
+
if (container.nodeType !== 1) {
|
|
248
|
+
container = container.parentElement;
|
|
249
|
+
}
|
|
250
|
+
const css = container !== null && container !== document.body ? cssPath(container) : null;
|
|
251
|
+
if (css !== null) {
|
|
252
|
+
selectors.push({ type: "CssSelector", value: css });
|
|
253
|
+
}
|
|
254
|
+
return { kind: "text", selectors, label: squash(exact).slice(0, LIMIT.label) };
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Finding an anchor again, best-effort: where it was, its words anywhere (the occurrence
|
|
258
|
+
// whose surroundings match best), then its words with any spacing.
|
|
259
|
+
const shared = (a, b, fromEnd) => {
|
|
260
|
+
let n = 0;
|
|
261
|
+
while (n < a.length && n < b.length && (fromEnd ? a.charAt(a.length - 1 - n) === b.charAt(b.length - 1 - n) : a.charAt(n) === b.charAt(n))) {
|
|
262
|
+
n += 1;
|
|
263
|
+
}
|
|
264
|
+
return n;
|
|
265
|
+
};
|
|
266
|
+
const locate = (index, quote, position) => {
|
|
267
|
+
const text = index.text;
|
|
268
|
+
const length = quote.exact.length;
|
|
269
|
+
if (position && text.slice(position.start, position.start + length) === quote.exact) {
|
|
270
|
+
return [position.start, position.start + length];
|
|
271
|
+
}
|
|
272
|
+
let best = null;
|
|
273
|
+
let bestScore = -1;
|
|
274
|
+
let seen = 0;
|
|
275
|
+
for (let at = text.indexOf(quote.exact); at !== -1 && seen < 1000; at = text.indexOf(quote.exact, at + 1)) {
|
|
276
|
+
seen += 1;
|
|
277
|
+
const score =
|
|
278
|
+
shared(text.slice(Math.max(0, at - LIMIT.context), at), quote.prefix || "", true) +
|
|
279
|
+
shared(text.slice(at + length, at + length + LIMIT.context), quote.suffix || "", false) -
|
|
280
|
+
(position ? Math.min(1, Math.abs(at - position.start) / 1e6) : 0);
|
|
281
|
+
if (score > bestScore) {
|
|
282
|
+
bestScore = score;
|
|
283
|
+
best = [at, at + length];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (best !== null) {
|
|
287
|
+
return best;
|
|
288
|
+
}
|
|
289
|
+
const wanted = squash(quote.exact);
|
|
290
|
+
if (wanted === "") {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const map = [];
|
|
294
|
+
let loose = "";
|
|
295
|
+
let space = false;
|
|
296
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
297
|
+
const each = text.charAt(i);
|
|
298
|
+
if (/\s/.test(each)) {
|
|
299
|
+
if (!space && loose !== "") {
|
|
300
|
+
loose += " ";
|
|
301
|
+
map.push(i);
|
|
302
|
+
}
|
|
303
|
+
space = true;
|
|
304
|
+
} else {
|
|
305
|
+
loose += each;
|
|
306
|
+
map.push(i);
|
|
307
|
+
space = false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const at = loose.indexOf(wanted);
|
|
311
|
+
return at === -1 ? null : [map[at], map[at + wanted.length - 1] + 1];
|
|
312
|
+
};
|
|
313
|
+
const find = (anchor, index) => {
|
|
314
|
+
const of = (type) => anchor.selectors.filter((each) => each && each.type === type)[0];
|
|
315
|
+
const css = of("CssSelector");
|
|
316
|
+
const quote = of("TextQuoteSelector");
|
|
317
|
+
const position = of("TextPositionSelector");
|
|
318
|
+
if (anchor.kind === "text") {
|
|
319
|
+
const span = quote ? locate(index, quote, position) : null;
|
|
320
|
+
const range = span === null ? null : rangeAt(index, span[0], span[1]);
|
|
321
|
+
return range === null ? null : { range };
|
|
322
|
+
}
|
|
323
|
+
let element = null;
|
|
324
|
+
if (css && typeof css.value === "string") {
|
|
325
|
+
try {
|
|
326
|
+
element = querySelector.call(document, css.value);
|
|
327
|
+
} catch (_) {
|
|
328
|
+
element = null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (element !== null && quote) {
|
|
332
|
+
const span = spanOf(index, element);
|
|
333
|
+
if (index.text.slice(span[0], span[1]).indexOf(quote.exact) === 0) {
|
|
334
|
+
return { element };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Without words to check, or by an id, the selector is enough.
|
|
338
|
+
if (element !== null && (!quote || css.value.charAt(0) === "#")) {
|
|
339
|
+
return { element };
|
|
340
|
+
}
|
|
341
|
+
const span = quote ? locate(index, quote, position) : null;
|
|
342
|
+
const range = span === null ? null : rangeAt(index, span[0], span[1]);
|
|
343
|
+
if (range !== null) {
|
|
344
|
+
let node = range.commonAncestorContainer;
|
|
345
|
+
if (node.nodeType !== 1) {
|
|
346
|
+
node = node.parentElement;
|
|
347
|
+
}
|
|
348
|
+
if (node !== null) {
|
|
349
|
+
return { element: node };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
};
|
|
354
|
+
const rectOf = (found) => (found.range ? rangeRect.call(found.range) : elementRect.call(found.element));
|
|
355
|
+
|
|
356
|
+
// The shell's own layer, over the content: marks, the outline of what would be picked.
|
|
357
|
+
let layer = null;
|
|
358
|
+
let shadow = null;
|
|
359
|
+
const STYLE = ".mark{position:absolute;border-radius:2px;background:rgba(250,204,21,.3);box-shadow:inset 0 -2px 0 rgba(202,138,4,.8)}.mark.element{background:rgba(250,204,21,.12);box-shadow:none;outline:2px solid rgba(202,138,4,.85);outline-offset:1px}.mark.active{background:rgba(250,204,21,.5)}.mark.element.active{background:rgba(250,204,21,.22);outline-width:3px}.mark.flash{animation:flash 1.8s ease-out}@keyframes flash{from{background:rgba(250,204,21,.9)}}.hover{position:absolute;outline:2px solid #2563eb;outline-offset:1px;background:rgba(37,99,235,.12)}.tag{position:absolute;max-width:70vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:12px/18px ui-monospace,SFMono-Regular,Menlo,monospace;padding:0 6px;border-radius:4px;background:#1e293b;color:#fff}";
|
|
360
|
+
const ensureLayer = () => {
|
|
361
|
+
if (layer === null) {
|
|
362
|
+
layer = createElement.call(document, "div");
|
|
363
|
+
layer.setAttribute("aria-hidden", "true");
|
|
364
|
+
layer.setAttribute("data-porthole-annotations", "");
|
|
365
|
+
// Inline and important: the content's own styles can't hide or move it by accident.
|
|
366
|
+
layer.style.cssText = "position:fixed!important;inset:0!important;display:block!important;visibility:visible!important;opacity:1!important;pointer-events:none!important;z-index:2147483647!important;overflow:hidden!important;margin:0!important;padding:0!important;border:0!important;background:none!important;transform:none!important";
|
|
367
|
+
shadow = attachShadow.call(layer, { mode: "closed" });
|
|
368
|
+
}
|
|
369
|
+
if (!layer.isConnected) {
|
|
370
|
+
document.documentElement.appendChild(layer);
|
|
371
|
+
}
|
|
372
|
+
return shadow;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
let mode = { elements: false, text: false };
|
|
376
|
+
let hover = null;
|
|
377
|
+
let marks = [];
|
|
378
|
+
let active = null;
|
|
379
|
+
let flashing = null;
|
|
380
|
+
let flashUntil = 0;
|
|
381
|
+
const draw = () => {
|
|
382
|
+
if (marks.length === 0 && hover === null && layer === null) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const root = ensureLayer();
|
|
386
|
+
const parts = [];
|
|
387
|
+
const style = createElement.call(document, "style");
|
|
388
|
+
if (nonce !== "") {
|
|
389
|
+
style.nonce = nonce;
|
|
390
|
+
}
|
|
391
|
+
style.textContent = STYLE;
|
|
392
|
+
parts.push(style);
|
|
393
|
+
const flashOn = Date.now() < flashUntil;
|
|
394
|
+
for (const mark of marks) {
|
|
395
|
+
if (mark.found === null) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const rects = mark.found.range ? Array.from(rangeRects.call(mark.found.range)) : [elementRect.call(mark.found.element)];
|
|
399
|
+
for (const rect of rects.slice(0, LIMIT.boxes)) {
|
|
400
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const each = createElement.call(document, "div");
|
|
404
|
+
each.className = "mark " + (mark.found.range ? "text" : "element") + (mark.id === active ? " active" : "") + (flashOn && mark.id === flashing ? " flash" : "");
|
|
405
|
+
each.style.cssText = "left:" + rect.left + "px;top:" + rect.top + "px;width:" + rect.width + "px;height:" + rect.height + "px";
|
|
406
|
+
parts.push(each);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (hover !== null && hover.isConnected) {
|
|
410
|
+
const rect = elementRect.call(hover);
|
|
411
|
+
const outline = createElement.call(document, "div");
|
|
412
|
+
outline.className = "hover";
|
|
413
|
+
outline.style.cssText = "left:" + rect.left + "px;top:" + rect.top + "px;width:" + rect.width + "px;height:" + rect.height + "px";
|
|
414
|
+
const tag = createElement.call(document, "div");
|
|
415
|
+
tag.className = "tag";
|
|
416
|
+
tag.textContent = describe(hover).slice(0, 80);
|
|
417
|
+
tag.style.cssText = "left:" + Math.max(4, rect.left) + "px;top:" + (rect.top >= 24 ? rect.top - 22 : Math.max(4, rect.bottom + 4)) + "px";
|
|
418
|
+
parts.push(outline, tag);
|
|
419
|
+
}
|
|
420
|
+
root.replaceChildren(...parts);
|
|
421
|
+
};
|
|
422
|
+
let drawing = false;
|
|
423
|
+
const redraw = () => {
|
|
424
|
+
if (!drawing) {
|
|
425
|
+
drawing = true;
|
|
426
|
+
requestAnimationFrame(() => {
|
|
427
|
+
drawing = false;
|
|
428
|
+
draw();
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const resultOf = (mark) => (mark.found === null ? { id: mark.id, state: "orphaned" } : { id: mark.id, state: "attached", rect: box(rectOf(mark.found)) });
|
|
434
|
+
// Finds every mark again; tells the host every result (after show) or only when one changed.
|
|
435
|
+
const anchorAll = (all) => {
|
|
436
|
+
const index = textIndex();
|
|
437
|
+
let changed = false;
|
|
438
|
+
for (const mark of marks) {
|
|
439
|
+
let found = null;
|
|
440
|
+
try {
|
|
441
|
+
found = find(mark.anchor, index);
|
|
442
|
+
} catch (_) {
|
|
443
|
+
found = null;
|
|
444
|
+
}
|
|
445
|
+
const state = found === null ? "orphaned" : "attached";
|
|
446
|
+
if (state !== mark.state) {
|
|
447
|
+
changed = true;
|
|
448
|
+
}
|
|
449
|
+
mark.found = found;
|
|
450
|
+
mark.state = state;
|
|
451
|
+
}
|
|
452
|
+
if (all || changed) {
|
|
453
|
+
tell(M.anchored, { results: marks.map(resultOf) });
|
|
454
|
+
}
|
|
455
|
+
draw();
|
|
456
|
+
};
|
|
457
|
+
// Once the content has been quiet a moment (it may still be drawing), at most a second late.
|
|
458
|
+
let anchorTimer = 0;
|
|
459
|
+
let deadline = 0;
|
|
460
|
+
let reportAll = false;
|
|
461
|
+
// Nothing is reported before the content is in place (an empty shell would orphan every
|
|
462
|
+
// mark), nor before a run of changes settles; a focus asked for meanwhile waits for it.
|
|
463
|
+
let waitingForContent = false;
|
|
464
|
+
let pendingFocus = null;
|
|
465
|
+
const settling = () => anchorTimer !== 0 || waitingForContent || marks.some((mark) => mark.state === null);
|
|
466
|
+
const scheduleAnchor = (all) => {
|
|
467
|
+
if (all) {
|
|
468
|
+
reportAll = true;
|
|
469
|
+
}
|
|
470
|
+
if (!contentIsIn()) {
|
|
471
|
+
if (!waitingForContent) {
|
|
472
|
+
waitingForContent = true;
|
|
473
|
+
whenContentIsIn(() => {
|
|
474
|
+
waitingForContent = false;
|
|
475
|
+
scheduleAnchor(false);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const now = Date.now();
|
|
481
|
+
if (deadline === 0) {
|
|
482
|
+
deadline = now + 1000;
|
|
483
|
+
}
|
|
484
|
+
clearTimeout(anchorTimer);
|
|
485
|
+
anchorTimer = setTimeout(() => {
|
|
486
|
+
anchorTimer = 0;
|
|
487
|
+
deadline = 0;
|
|
488
|
+
const every = reportAll;
|
|
489
|
+
reportAll = false;
|
|
490
|
+
anchorAll(every);
|
|
491
|
+
if (pendingFocus !== null) {
|
|
492
|
+
const id = pendingFocus;
|
|
493
|
+
pendingFocus = null;
|
|
494
|
+
focus(id);
|
|
495
|
+
}
|
|
496
|
+
}, Math.max(0, Math.min(150, deadline - now)));
|
|
497
|
+
};
|
|
498
|
+
new MutationObserver((records) => {
|
|
499
|
+
if (marks.length === 0) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
for (const record of records) {
|
|
503
|
+
if (record.target === layer || (record.addedNodes.length + record.removedNodes.length === 1 && (record.addedNodes[0] === layer || record.removedNodes[0] === layer))) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
scheduleAnchor(false);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
}).observe(document, { childList: true, subtree: true, characterData: true });
|
|
510
|
+
window.addEventListener("resize", redraw);
|
|
511
|
+
window.addEventListener("scroll", redraw, true);
|
|
512
|
+
|
|
513
|
+
const focus = (id) => {
|
|
514
|
+
const mark = marks.filter((each) => each.id === id)[0];
|
|
515
|
+
if (!mark) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (settling()) {
|
|
519
|
+
// Where it is isn't known yet: focus it once anchoring has settled, then say where.
|
|
520
|
+
pendingFocus = id;
|
|
521
|
+
active = id;
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
active = id;
|
|
525
|
+
if (mark.found !== null) {
|
|
526
|
+
const rect = rectOf(mark.found);
|
|
527
|
+
if (rect.top < 0 || rect.bottom > window.innerHeight) {
|
|
528
|
+
window.scrollBy(0, rect.top - (window.innerHeight - Math.min(rect.height, window.innerHeight)) / 2);
|
|
529
|
+
}
|
|
530
|
+
flashing = id;
|
|
531
|
+
flashUntil = Date.now() + 1800;
|
|
532
|
+
setTimeout(redraw, 1850);
|
|
533
|
+
}
|
|
534
|
+
draw();
|
|
535
|
+
tell(M.anchored, { results: [resultOf(mark)] });
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// Picking an element: what is under the pointer, never the page itself or the shell's layer.
|
|
539
|
+
let cursor = null;
|
|
540
|
+
const setCursor = (on) => {
|
|
541
|
+
if (typeof CSSStyleSheet !== "function" || !Array.isArray(document.adoptedStyleSheets)) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (cursor === null) {
|
|
545
|
+
cursor = new CSSStyleSheet();
|
|
546
|
+
cursor.replaceSync("*, *::before, *::after { cursor: crosshair !important; }");
|
|
547
|
+
}
|
|
548
|
+
const others = document.adoptedStyleSheets.filter((sheet) => sheet !== cursor);
|
|
549
|
+
document.adoptedStyleSheets = on ? others.concat([cursor]) : others;
|
|
550
|
+
};
|
|
551
|
+
const pickable = (element) => (element && element.nodeType === 1 && element !== document.documentElement && element !== document.body && element !== layer ? element : null);
|
|
552
|
+
const under = (x, y) => pickable(elementFromPoint.call(document, x, y));
|
|
553
|
+
const stop = () => {
|
|
554
|
+
mode = { elements: false, text: false };
|
|
555
|
+
hover = null;
|
|
556
|
+
reported = null;
|
|
557
|
+
setCursor(false);
|
|
558
|
+
draw();
|
|
559
|
+
};
|
|
560
|
+
// The content's own handlers never see the pointer while an element is being picked; a
|
|
561
|
+
// finger outlines what it is on as it goes down.
|
|
562
|
+
let pressed = null;
|
|
563
|
+
const swallow = (event) => {
|
|
564
|
+
if (mode.elements) {
|
|
565
|
+
if (event.type === "pointerdown") {
|
|
566
|
+
pressed = { x: event.clientX, y: event.clientY };
|
|
567
|
+
hover = under(event.clientX, event.clientY);
|
|
568
|
+
redraw();
|
|
569
|
+
}
|
|
570
|
+
event.stopImmediatePropagation();
|
|
571
|
+
if (!mode.text && (event.type === "mousedown" || event.type === "mouseup")) {
|
|
572
|
+
event.preventDefault();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
for (const type of ["pointerdown", "pointerup", "mousedown", "mouseup", "dblclick", "auxclick", "touchstart", "touchend"]) {
|
|
577
|
+
window.addEventListener(type, swallow, true);
|
|
578
|
+
}
|
|
579
|
+
window.addEventListener("pointermove", (event) => {
|
|
580
|
+
if (mode.elements) {
|
|
581
|
+
hover = under(event.clientX, event.clientY);
|
|
582
|
+
redraw();
|
|
583
|
+
}
|
|
584
|
+
}, true);
|
|
585
|
+
window.addEventListener("click", (event) => {
|
|
586
|
+
if (!mode.elements) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
event.preventDefault();
|
|
590
|
+
event.stopImmediatePropagation();
|
|
591
|
+
// A drag that selected text: the selection is what was meant. A press and release in
|
|
592
|
+
// one place is a pick.
|
|
593
|
+
const selection = selectionOf.call(document);
|
|
594
|
+
const dragged = pressed !== null && event.detail !== 0 && Math.abs(event.clientX - pressed.x) + Math.abs(event.clientY - pressed.y) > 4;
|
|
595
|
+
if (mode.text && dragged && selection !== null && !selection.isCollapsed) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const element = event.detail === 0 ? pickable(event.target) : under(event.clientX, event.clientY);
|
|
599
|
+
if (element === null) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const anchor = elementAnchor(element, textIndex());
|
|
603
|
+
if (anchor === null) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
mode = { elements: false, text: mode.text };
|
|
607
|
+
hover = null;
|
|
608
|
+
setCursor(false);
|
|
609
|
+
draw();
|
|
610
|
+
tell(M.selected, { anchor, rect: box(elementRect.call(element)) });
|
|
611
|
+
}, true);
|
|
612
|
+
window.addEventListener("keydown", (event) => {
|
|
613
|
+
if (mode.elements && event.key === "Escape") {
|
|
614
|
+
event.preventDefault();
|
|
615
|
+
event.stopImmediatePropagation();
|
|
616
|
+
mode = { elements: false, text: mode.text };
|
|
617
|
+
hover = null;
|
|
618
|
+
setCursor(false);
|
|
619
|
+
draw();
|
|
620
|
+
tell(M.cancelled, {});
|
|
621
|
+
}
|
|
622
|
+
}, true);
|
|
623
|
+
|
|
624
|
+
// Selecting text: each selection that settles, and its clearing; again after a scroll.
|
|
625
|
+
let reported = null;
|
|
626
|
+
let selectionTimer = 0;
|
|
627
|
+
const reportSelection = (again) => {
|
|
628
|
+
if (!mode.text) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const selection = selectionOf.call(document);
|
|
632
|
+
if (selection === null || selection.isCollapsed || selection.rangeCount === 0) {
|
|
633
|
+
if (reported !== null) {
|
|
634
|
+
reported = null;
|
|
635
|
+
tell(M.unselected, {});
|
|
636
|
+
}
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const range = selection.getRangeAt(0);
|
|
640
|
+
if (!document.body || !document.body.contains(range.commonAncestorContainer)) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const anchor = textAnchor(range, textIndex());
|
|
644
|
+
if (anchor === null) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const key = anchor.selectors[1].start + ":" + anchor.selectors[1].end;
|
|
648
|
+
if (key === reported && !again) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
reported = key;
|
|
652
|
+
tell(M.selected, { anchor, rect: box(rangeRect.call(range)) });
|
|
653
|
+
};
|
|
654
|
+
document.addEventListener("selectionchange", () => {
|
|
655
|
+
if (mode.text) {
|
|
656
|
+
clearTimeout(selectionTimer);
|
|
657
|
+
selectionTimer = setTimeout(() => reportSelection(false), 250);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
let scrollTimer = 0;
|
|
661
|
+
window.addEventListener("scroll", () => {
|
|
662
|
+
if (reported !== null) {
|
|
663
|
+
clearTimeout(scrollTimer);
|
|
664
|
+
scrollTimer = setTimeout(() => reportSelection(true), 150);
|
|
665
|
+
}
|
|
666
|
+
}, true);
|
|
667
|
+
|
|
668
|
+
window.addEventListener("message", (event) => {
|
|
669
|
+
if (event.source !== window.parent || plan.origins.indexOf(event.origin) === -1) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const data = event.data;
|
|
673
|
+
if (data === null || typeof data !== "object" || data.jsonrpc !== "2.0" || typeof data.method !== "string") {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const params = data.params !== null && typeof data.params === "object" ? data.params : {};
|
|
677
|
+
switch (data.method) {
|
|
678
|
+
case M.start:
|
|
679
|
+
mode = { elements: params.elements === true, text: params.text === true };
|
|
680
|
+
hover = null;
|
|
681
|
+
reported = null;
|
|
682
|
+
setCursor(mode.elements);
|
|
683
|
+
draw();
|
|
684
|
+
reportSelection(false);
|
|
685
|
+
break;
|
|
686
|
+
case M.stop:
|
|
687
|
+
stop();
|
|
688
|
+
break;
|
|
689
|
+
case M.show:
|
|
690
|
+
marks = (Array.isArray(params.marks) ? params.marks : [])
|
|
691
|
+
.slice(0, LIMIT.marks)
|
|
692
|
+
.filter((mark) => mark && typeof mark.id === "string" && mark.anchor && Array.isArray(mark.anchor.selectors))
|
|
693
|
+
.map((mark) => ({ id: mark.id, anchor: mark.anchor, found: null, state: null }));
|
|
694
|
+
if (typeof params.active === "string") {
|
|
695
|
+
active = params.active;
|
|
696
|
+
}
|
|
697
|
+
if (!marks.some((mark) => mark.id === active)) {
|
|
698
|
+
active = null;
|
|
699
|
+
}
|
|
700
|
+
scheduleAnchor(true);
|
|
701
|
+
break;
|
|
702
|
+
case M.focus:
|
|
703
|
+
focus(String(params.id));
|
|
704
|
+
break;
|
|
705
|
+
case M.clear:
|
|
706
|
+
pendingFocus = null;
|
|
707
|
+
marks = [];
|
|
708
|
+
active = null;
|
|
709
|
+
draw();
|
|
710
|
+
break;
|
|
711
|
+
default:
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
})();
|
|
716
|
+
}`;var At=String.raw`(() => {
|
|
717
|
+
"use strict";
|
|
718
|
+
const plan = __PLAN__;
|
|
719
|
+
// The embedding page's nonce, where it gave one (a srcdoc frame inherits its policy): every
|
|
720
|
+
// script the shell runs for the content carries it.
|
|
721
|
+
const nonce = document.currentScript && document.currentScript.nonce ? document.currentScript.nonce : "";
|
|
722
|
+
const declared = (name) => Object.prototype.hasOwnProperty.call(plan.capabilities, name);
|
|
723
|
+
// Fixed in place: content can neither restore what is removed nor replace what is wrapped.
|
|
724
|
+
const lock = (target, name, value) => {
|
|
725
|
+
try {
|
|
726
|
+
Object.defineProperty(target, name, { configurable: false, writable: false, value });
|
|
727
|
+
} catch (_) {}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
if (plan.storage === "memory") {
|
|
731
|
+
// An opaque origin has no storage: a map stands in, for as long as the frame lives.
|
|
732
|
+
const store = () => {
|
|
733
|
+
const items = new Map();
|
|
734
|
+
return {
|
|
735
|
+
getItem: (key) => (items.has(String(key)) ? items.get(String(key)) : null),
|
|
736
|
+
setItem: (key, value) => void items.set(String(key), String(value)),
|
|
737
|
+
removeItem: (key) => void items.delete(String(key)),
|
|
738
|
+
clear: () => items.clear(),
|
|
739
|
+
key: (index) => Array.from(items.keys())[index] ?? null,
|
|
740
|
+
get length() {
|
|
741
|
+
return items.size;
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
};
|
|
745
|
+
for (const name of ["localStorage", "sessionStorage"]) {
|
|
746
|
+
try {
|
|
747
|
+
Object.defineProperty(window, name, { configurable: true, value: store() });
|
|
748
|
+
} catch (_) {}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// WebRTC leaves the frame outside connect-src. Only RTCPeerConnection (and its prefixed alias)
|
|
753
|
+
// opens a connection; the rest are value types, removed too so nothing is assembled from them.
|
|
754
|
+
for (const name of Object.getOwnPropertyNames(window)) {
|
|
755
|
+
if (/^(RTC|webkitRTC)/.test(name)) {
|
|
756
|
+
lock(window, name, undefined);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// No nested frames (each would be a fresh realm with WebRTC intact), no resource hints and no
|
|
761
|
+
// refreshes (they reach the network outside the CSP's fetch directives). Removed as they are
|
|
762
|
+
// added: a mutation observer's microtask runs before a child frame's document loads.
|
|
763
|
+
const FRAMES = "iframe,frame,frameset,object,embed,portal,fencedframe";
|
|
764
|
+
const HINTS = 'link[rel~="dns-prefetch" i],link[rel~="preconnect" i],link[rel~="prefetch" i],link[rel~="prerender" i],script[type="speculationrules" i],meta[http-equiv="refresh" i],meta[http-equiv="x-dns-prefetch-control" i]';
|
|
765
|
+
const GUARDED = FRAMES + "," + HINTS;
|
|
766
|
+
const guardedRoots = new WeakSet();
|
|
767
|
+
let guardRoot = null;
|
|
768
|
+
const strip = (node) => {
|
|
769
|
+
if (node === null || (node.nodeType !== 1 && node.nodeType !== 9 && node.nodeType !== 11)) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (node.nodeType === 1 && node.matches(GUARDED)) {
|
|
773
|
+
node.remove();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
for (const element of Array.from(node.querySelectorAll(GUARDED))) {
|
|
777
|
+
element.remove();
|
|
778
|
+
}
|
|
779
|
+
// Open shadow roots made without attachShadow (declarative ones the parser built elsewhere).
|
|
780
|
+
if (node.nodeType === 1 && node.shadowRoot) {
|
|
781
|
+
guardRoot(node.shadowRoot);
|
|
782
|
+
}
|
|
783
|
+
for (const element of Array.from(node.querySelectorAll("*"))) {
|
|
784
|
+
if (element.shadowRoot) {
|
|
785
|
+
guardRoot(element.shadowRoot);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
const frameGuard = new MutationObserver((records) => {
|
|
790
|
+
for (const record of records) {
|
|
791
|
+
for (const node of Array.from(record.addedNodes)) {
|
|
792
|
+
strip(node);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
guardRoot = (root) => {
|
|
797
|
+
if (guardedRoots.has(root)) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
guardedRoots.add(root);
|
|
801
|
+
strip(root);
|
|
802
|
+
frameGuard.observe(root, { childList: true, subtree: true });
|
|
803
|
+
};
|
|
804
|
+
guardRoot(document);
|
|
805
|
+
|
|
806
|
+
// Every shadow root content makes, open or closed, is guarded as it is made.
|
|
807
|
+
const attachShadow = Element.prototype.attachShadow;
|
|
808
|
+
lock(Element.prototype, "attachShadow", function (init) {
|
|
809
|
+
const root = attachShadow.call(this, init);
|
|
810
|
+
guardRoot(root);
|
|
811
|
+
return root;
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// The parser builds declarative shadow roots (template shadowrootmode) without attachShadow, and
|
|
815
|
+
// a closed one is out of the guard's reach. Fragment parsing doesn't build them, so the entry
|
|
816
|
+
// points that would are given fragment parsing's behavior, and a document can't be rewritten.
|
|
817
|
+
const parseInert = (html) => {
|
|
818
|
+
const template = document.createElement("template");
|
|
819
|
+
template.innerHTML = String(html);
|
|
820
|
+
return template.content;
|
|
821
|
+
};
|
|
822
|
+
for (const proto of [Element.prototype, typeof ShadowRoot === "function" ? ShadowRoot.prototype : null]) {
|
|
823
|
+
if (proto !== null && "setHTMLUnsafe" in proto) {
|
|
824
|
+
lock(proto, "setHTMLUnsafe", function (html) {
|
|
825
|
+
this.innerHTML = String(html);
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (typeof Document.parseHTMLUnsafe === "function") {
|
|
830
|
+
lock(Document, "parseHTMLUnsafe", (html) => new DOMParser().parseFromString(String(html), "text/html"));
|
|
831
|
+
}
|
|
832
|
+
const noRewrite = function () {
|
|
833
|
+
throw new Error("document.open and document.write aren't available here: build the page with DOM methods");
|
|
834
|
+
};
|
|
835
|
+
for (const name of ["open", "write", "writeln"]) {
|
|
836
|
+
lock(Document.prototype, name, noRewrite);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Some engines act on a hint the moment its element is connected or changed (WebKit opens a
|
|
840
|
+
// preconnect then, outside the CSP), before the observer's microtask. So every way to connect a
|
|
841
|
+
// node drops guarded elements first, synchronously, and a connected link or meta can't be
|
|
842
|
+
// changed into one. The observer stays as the backstop for anything these miss.
|
|
843
|
+
const admit = (node) => {
|
|
844
|
+
if (node === null || typeof node !== "object" || typeof node.nodeType !== "number") {
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
if (node.nodeType === 1 && node.matches(GUARDED)) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
strip(node);
|
|
851
|
+
return true;
|
|
852
|
+
};
|
|
853
|
+
const admitted = (args) => args.map((arg) => (admit(arg) ? arg : document.createTextNode("")));
|
|
854
|
+
const wrapInsert = (proto, name, positions) => {
|
|
855
|
+
const original = proto && proto[name];
|
|
856
|
+
if (typeof original !== "function") {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
lock(proto, name, function (...args) {
|
|
860
|
+
const checked = positions === "all" ? admitted(args) : args.map((arg, index) => (positions.indexOf(index) !== -1 && !admit(arg) ? document.createTextNode("") : arg));
|
|
861
|
+
return original.apply(this, checked);
|
|
862
|
+
});
|
|
863
|
+
};
|
|
864
|
+
for (const proto of [Node.prototype]) {
|
|
865
|
+
wrapInsert(proto, "appendChild", [0]);
|
|
866
|
+
wrapInsert(proto, "insertBefore", [0]);
|
|
867
|
+
wrapInsert(proto, "replaceChild", [0]);
|
|
868
|
+
}
|
|
869
|
+
for (const proto of [Element.prototype, Document.prototype, DocumentFragment.prototype]) {
|
|
870
|
+
for (const name of ["append", "prepend", "replaceChildren"]) {
|
|
871
|
+
wrapInsert(proto, name, "all");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
for (const proto of [Element.prototype, CharacterData.prototype, DocumentType.prototype]) {
|
|
875
|
+
for (const name of ["before", "after", "replaceWith"]) {
|
|
876
|
+
wrapInsert(proto, name, "all");
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
wrapInsert(Element.prototype, "insertAdjacentElement", [1]);
|
|
880
|
+
wrapInsert(Range.prototype, "insertNode", [0]);
|
|
881
|
+
wrapInsert(Range.prototype, "surroundContents", [0]);
|
|
882
|
+
|
|
883
|
+
// Markup parsed in place: parsed first in a detached element of the same kind (so it parses as it
|
|
884
|
+
// would in place, and nothing in it is connected yet), guarded, then moved in.
|
|
885
|
+
const accessor = (proto, name) => (proto ? Object.getOwnPropertyDescriptor(proto, name) : undefined);
|
|
886
|
+
const contextFor = (element) => {
|
|
887
|
+
if (element === null || element.nodeType !== 1) {
|
|
888
|
+
return document.createElement("div");
|
|
889
|
+
}
|
|
890
|
+
// A fresh element of the same kind; a custom element's own class is not run again.
|
|
891
|
+
const name = element.localName.indexOf("-") === -1 ? element.localName : "div";
|
|
892
|
+
return document.createElementNS(element.namespaceURI, name);
|
|
893
|
+
};
|
|
894
|
+
const innerHTMLs = [accessor(Element.prototype, "innerHTML"), accessor(typeof ShadowRoot === "function" ? ShadowRoot.prototype : null, "innerHTML")];
|
|
895
|
+
const setInner = (innerHTMLs[0] || innerHTMLs[1]).set;
|
|
896
|
+
const parseIn = (contextElement, html) => {
|
|
897
|
+
const box = contextFor(contextElement);
|
|
898
|
+
setInner.call(box, String(html));
|
|
899
|
+
strip(box);
|
|
900
|
+
return Array.from(box.childNodes);
|
|
901
|
+
};
|
|
902
|
+
[Element.prototype, typeof ShadowRoot === "function" ? ShadowRoot.prototype : null].forEach((proto, index) => {
|
|
903
|
+
const descriptor = innerHTMLs[index];
|
|
904
|
+
if (!proto || !descriptor || !descriptor.set) {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
Object.defineProperty(proto, "innerHTML", {
|
|
908
|
+
configurable: false,
|
|
909
|
+
enumerable: descriptor.enumerable,
|
|
910
|
+
get: descriptor.get,
|
|
911
|
+
set(html) {
|
|
912
|
+
if (this.nodeType === 1 && this.localName === "template") {
|
|
913
|
+
descriptor.set.call(this, html);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
this.replaceChildren(...parseIn(this.nodeType === 11 ? this.host || null : this, html));
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
const outer = accessor(Element.prototype, "outerHTML");
|
|
921
|
+
if (outer && outer.set) {
|
|
922
|
+
Object.defineProperty(Element.prototype, "outerHTML", {
|
|
923
|
+
configurable: false,
|
|
924
|
+
enumerable: outer.enumerable,
|
|
925
|
+
get: outer.get,
|
|
926
|
+
set(html) {
|
|
927
|
+
const parent = this.parentNode;
|
|
928
|
+
if (parent === null || parent.nodeType === 9) {
|
|
929
|
+
outer.set.call(this, html);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
this.replaceWith(...parseIn(parent.nodeType === 1 ? parent : null, html));
|
|
933
|
+
},
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
const insertAdjacentHTML = Element.prototype.insertAdjacentHTML;
|
|
937
|
+
lock(Element.prototype, "insertAdjacentHTML", function (position, html) {
|
|
938
|
+
insertAdjacentHTML.call(this, position, "");
|
|
939
|
+
const where = String(position).toLowerCase();
|
|
940
|
+
const outside = where === "beforebegin" || where === "afterend";
|
|
941
|
+
const nodes = parseIn(outside ? this.parentElement : this, html);
|
|
942
|
+
if (where === "beforebegin") {
|
|
943
|
+
this.before(...nodes);
|
|
944
|
+
} else if (where === "afterbegin") {
|
|
945
|
+
this.prepend(...nodes);
|
|
946
|
+
} else if (where === "beforeend") {
|
|
947
|
+
this.append(...nodes);
|
|
948
|
+
} else {
|
|
949
|
+
this.after(...nodes);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
if (typeof Element.prototype.setHTML === "function") {
|
|
953
|
+
lock(Element.prototype, "setHTML", function (html) {
|
|
954
|
+
this.innerHTML = String(html);
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
// insertHTML parses markup outside these guards; copy, cut and paste reach the clipboard (P051).
|
|
958
|
+
const REFUSED_COMMANDS = ["inserthtml", "copy", "cut", "paste"];
|
|
959
|
+
const execCommand = Document.prototype.execCommand;
|
|
960
|
+
if (typeof execCommand === "function") {
|
|
961
|
+
lock(Document.prototype, "execCommand", function (command, ...rest) {
|
|
962
|
+
return REFUSED_COMMANDS.indexOf(String(command).toLowerCase()) !== -1 ? false : execCommand.call(this, command, ...rest);
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// The clipboard is the host's to write (P051): content can neither write nor read it. A copy
|
|
967
|
+
// the person makes (their keys, their menu) copies their selection as the browser makes it: no
|
|
968
|
+
// copy or cut listener of the content's runs, so none can swap what goes on the clipboard.
|
|
969
|
+
const noClipboard = function () {
|
|
970
|
+
return Promise.reject(new DOMException("Content can't use the clipboard here" + (declared("copyText") ? ": call copyText(text) from a click" : ""), "NotAllowedError"));
|
|
971
|
+
};
|
|
972
|
+
if (typeof Clipboard === "function") {
|
|
973
|
+
for (const name of ["write", "writeText", "read", "readText"]) {
|
|
974
|
+
if (typeof Clipboard.prototype[name] === "function") {
|
|
975
|
+
lock(Clipboard.prototype, name, noClipboard);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
for (const type of ["copy", "cut"]) {
|
|
980
|
+
window.addEventListener(type, (event) => event.stopImmediatePropagation(), true);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// A connected link or meta can't become a guarded one: the change is tried on a detached copy
|
|
984
|
+
// first, and if the copy would be guarded, the element leaves the document before it changes.
|
|
985
|
+
const watched = (element) => element && element.nodeType === 1 && (element.localName === "link" || element.localName === "meta");
|
|
986
|
+
const beforeChange = (element, change) => {
|
|
987
|
+
if (!watched(element) || !element.isConnected) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const copy = element.cloneNode(false);
|
|
991
|
+
try {
|
|
992
|
+
change(copy);
|
|
993
|
+
} catch (_) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (copy.matches(GUARDED)) {
|
|
997
|
+
element.remove();
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
const guardMethod = (proto, name, change) => {
|
|
1001
|
+
const original = proto && proto[name];
|
|
1002
|
+
if (typeof original !== "function") {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
lock(proto, name, function (...args) {
|
|
1006
|
+
beforeChange(this, (copy) => change(copy, args, original));
|
|
1007
|
+
return original.apply(this, args);
|
|
1008
|
+
});
|
|
1009
|
+
};
|
|
1010
|
+
for (const name of ["setAttribute", "setAttributeNS", "toggleAttribute", "removeAttribute", "removeAttributeNS"]) {
|
|
1011
|
+
guardMethod(Element.prototype, name, (copy, args, original) => original.apply(copy, args));
|
|
1012
|
+
}
|
|
1013
|
+
for (const name of ["setAttributeNode", "setAttributeNodeNS"]) {
|
|
1014
|
+
guardMethod(Element.prototype, name, (copy, args) => copy.setAttributeNS(args[0].namespaceURI, args[0].name, args[0].value));
|
|
1015
|
+
}
|
|
1016
|
+
for (const [proto, name] of [[Attr.prototype, "value"], [Node.prototype, "nodeValue"], [Node.prototype, "textContent"]]) {
|
|
1017
|
+
const descriptor = accessor(proto, name);
|
|
1018
|
+
if (!descriptor || !descriptor.set) {
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
Object.defineProperty(proto, name, {
|
|
1022
|
+
configurable: false,
|
|
1023
|
+
enumerable: descriptor.enumerable,
|
|
1024
|
+
get: descriptor.get,
|
|
1025
|
+
set(value) {
|
|
1026
|
+
if (this.nodeType === 2 && this.ownerElement) {
|
|
1027
|
+
const attribute = this;
|
|
1028
|
+
beforeChange(attribute.ownerElement, (copy) => copy.setAttributeNS(attribute.namespaceURI, attribute.name, String(value)));
|
|
1029
|
+
}
|
|
1030
|
+
descriptor.set.call(this, value);
|
|
1031
|
+
},
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
// Attribute maps and token lists don't name their element: remember it when content gets one.
|
|
1035
|
+
const owners = new WeakMap();
|
|
1036
|
+
const remember = (proto, name) => {
|
|
1037
|
+
const descriptor = accessor(proto, name);
|
|
1038
|
+
if (!descriptor || !descriptor.get) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
Object.defineProperty(proto, name, {
|
|
1042
|
+
configurable: false,
|
|
1043
|
+
enumerable: descriptor.enumerable,
|
|
1044
|
+
get() {
|
|
1045
|
+
const value = descriptor.get.call(this);
|
|
1046
|
+
if (value !== null && typeof value === "object") {
|
|
1047
|
+
owners.set(value, this);
|
|
1048
|
+
}
|
|
1049
|
+
return value;
|
|
1050
|
+
},
|
|
1051
|
+
// relList = "…" forwards to the list's value, natively: guarded here instead.
|
|
1052
|
+
set: descriptor.set
|
|
1053
|
+
? function (value) {
|
|
1054
|
+
beforeChange(this, (copy) => descriptor.set.call(copy, value));
|
|
1055
|
+
descriptor.set.call(this, value);
|
|
1056
|
+
}
|
|
1057
|
+
: undefined,
|
|
1058
|
+
});
|
|
1059
|
+
};
|
|
1060
|
+
remember(Element.prototype, "attributes");
|
|
1061
|
+
for (const proto of [
|
|
1062
|
+
typeof HTMLLinkElement === "function" ? HTMLLinkElement.prototype : null,
|
|
1063
|
+
typeof HTMLMetaElement === "function" ? HTMLMetaElement.prototype : null,
|
|
1064
|
+
]) {
|
|
1065
|
+
remember(proto, "relList");
|
|
1066
|
+
for (const name of Object.getOwnPropertyNames(proto || {})) {
|
|
1067
|
+
const descriptor = accessor(proto, name);
|
|
1068
|
+
if (descriptor && descriptor.set && name !== "relList") {
|
|
1069
|
+
Object.defineProperty(proto, name, {
|
|
1070
|
+
configurable: false,
|
|
1071
|
+
enumerable: descriptor.enumerable,
|
|
1072
|
+
get: descriptor.get,
|
|
1073
|
+
set(value) {
|
|
1074
|
+
beforeChange(this, (copy) => descriptor.set.call(copy, value));
|
|
1075
|
+
descriptor.set.call(this, value);
|
|
1076
|
+
},
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
for (const name of ["setNamedItem", "setNamedItemNS", "removeNamedItem", "removeNamedItemNS"]) {
|
|
1082
|
+
const original = NamedNodeMap.prototype[name];
|
|
1083
|
+
if (typeof original !== "function") {
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
lock(NamedNodeMap.prototype, name, function (...args) {
|
|
1087
|
+
beforeChange(owners.get(this), (copy) => {
|
|
1088
|
+
if (name.indexOf("set") === 0) {
|
|
1089
|
+
copy.setAttributeNS(args[0].namespaceURI, args[0].name, args[0].value);
|
|
1090
|
+
} else {
|
|
1091
|
+
copy.removeAttribute(args[args.length - 1]);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
return original.apply(this, args);
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
const tokens = typeof DOMTokenList === "function" ? DOMTokenList.prototype : null;
|
|
1098
|
+
for (const name of ["add", "remove", "replace", "toggle"]) {
|
|
1099
|
+
const original = tokens && tokens[name];
|
|
1100
|
+
if (typeof original === "function") {
|
|
1101
|
+
lock(tokens, name, function (...args) {
|
|
1102
|
+
const element = owners.get(this);
|
|
1103
|
+
beforeChange(element, (copy) => original.apply(copy.relList, args));
|
|
1104
|
+
return original.apply(this, args);
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const tokenValue = accessor(tokens, "value");
|
|
1109
|
+
if (tokenValue && tokenValue.set) {
|
|
1110
|
+
Object.defineProperty(tokens, "value", {
|
|
1111
|
+
configurable: false,
|
|
1112
|
+
enumerable: tokenValue.enumerable,
|
|
1113
|
+
get: tokenValue.get,
|
|
1114
|
+
set(value) {
|
|
1115
|
+
beforeChange(owners.get(this), (copy) => tokenValue.set.call(copy.relList, value));
|
|
1116
|
+
tokenValue.set.call(this, value);
|
|
1117
|
+
},
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
let root = document.getElementById("porthole-root");
|
|
1122
|
+
let reported = 0;
|
|
1123
|
+
const report = (message) => {
|
|
1124
|
+
if (!declared("reportError") || reported >= plan.maxErrorReports) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
reported += 1;
|
|
1128
|
+
send({ method: "notifications/message", params: { level: "error", logger: "porthole", data: String(message).slice(0, 2000) } });
|
|
1129
|
+
};
|
|
1130
|
+
const send = (message) => {
|
|
1131
|
+
if (plan.protocol === "shell" && window.parent !== window) {
|
|
1132
|
+
// Nothing secret is ever said from here, so any parent may hear it; what it hears is the
|
|
1133
|
+
// content's word, which the host gates before it acts on it.
|
|
1134
|
+
window.parent.postMessage(Object.assign({ jsonrpc: "2.0" }, message), "*");
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
// A parsed tree's scripts are held (each replaced by a numbered marker) and run later, in order,
|
|
1139
|
+
// as the page's parser would run them: each made again, live, in its marker's place, and each
|
|
1140
|
+
// external one loaded before the next runs. Markers are found in the live document by number, so
|
|
1141
|
+
// this works the same for a fragment that was moved in and a document that was imported.
|
|
1142
|
+
let held = 0;
|
|
1143
|
+
const holdScripts = (tree, owner) => {
|
|
1144
|
+
const scripts = [];
|
|
1145
|
+
for (const script of Array.from(tree.querySelectorAll("script"))) {
|
|
1146
|
+
const marker = owner.createComment("porthole-script:" + held);
|
|
1147
|
+
held += 1;
|
|
1148
|
+
script.replaceWith(marker);
|
|
1149
|
+
scripts.push({ script, id: marker.data });
|
|
1150
|
+
}
|
|
1151
|
+
return scripts;
|
|
1152
|
+
};
|
|
1153
|
+
const markers = () => {
|
|
1154
|
+
const found = new Map();
|
|
1155
|
+
const walker = document.createTreeWalker(document, NodeFilter.SHOW_COMMENT);
|
|
1156
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
1157
|
+
if (node.data.indexOf("porthole-script:") === 0) {
|
|
1158
|
+
found.set(node.data, node);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return found;
|
|
1162
|
+
};
|
|
1163
|
+
const runScripts = async (scripts) => {
|
|
1164
|
+
const placed = markers();
|
|
1165
|
+
for (const { script, id } of scripts) {
|
|
1166
|
+
const marker = placed.get(id);
|
|
1167
|
+
if (marker === undefined) {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
const live = document.createElement("script");
|
|
1171
|
+
for (const attribute of Array.from(script.attributes)) {
|
|
1172
|
+
live.setAttribute(attribute.name, attribute.value);
|
|
1173
|
+
}
|
|
1174
|
+
// A script made by a script is async unless told otherwise: keep the page's order.
|
|
1175
|
+
live.async = script.hasAttribute("async");
|
|
1176
|
+
if (nonce !== "") {
|
|
1177
|
+
live.nonce = nonce;
|
|
1178
|
+
}
|
|
1179
|
+
live.textContent = script.textContent;
|
|
1180
|
+
const external = live.hasAttribute("src") && !live.async && live.type !== "module";
|
|
1181
|
+
const loaded = external
|
|
1182
|
+
? new Promise((resolve) => {
|
|
1183
|
+
live.addEventListener("load", resolve);
|
|
1184
|
+
live.addEventListener("error", () => {
|
|
1185
|
+
report("Couldn't load " + live.src);
|
|
1186
|
+
resolve();
|
|
1187
|
+
});
|
|
1188
|
+
})
|
|
1189
|
+
: null;
|
|
1190
|
+
marker.replaceWith(live);
|
|
1191
|
+
if (loaded !== null) {
|
|
1192
|
+
await loaded;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
// The page's own load events, for code that waits for them: its scripts ran after the shell's
|
|
1197
|
+
// document had loaded, so they fire again, once, after them.
|
|
1198
|
+
const announceLoaded = () => {
|
|
1199
|
+
document.dispatchEvent(new Event("DOMContentLoaded", { bubbles: true }));
|
|
1200
|
+
window.dispatchEvent(new Event("load"));
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
// Whether the content is in place: a fragment or page the shell draws, once drawn whole; a
|
|
1204
|
+
// view the plan carries, once rendered; a view that is the document itself, from the start.
|
|
1205
|
+
const contentHooks = [];
|
|
1206
|
+
let contentIn = typeof plan.document !== "string" && plan.protocol !== "shell";
|
|
1207
|
+
const contentIsIn = () => contentIn;
|
|
1208
|
+
const whenContentIsIn = (run) => {
|
|
1209
|
+
if (contentIn) {
|
|
1210
|
+
run();
|
|
1211
|
+
} else {
|
|
1212
|
+
contentHooks.push(run);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
const contentArrived = () => {
|
|
1216
|
+
if (!contentIn) {
|
|
1217
|
+
contentIn = true;
|
|
1218
|
+
for (const run of contentHooks.splice(0)) {
|
|
1219
|
+
run();
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
// A fragment takes the person's scheme; a whole document only if it declares that it supports
|
|
1225
|
+
// both (or the config says always), else it shows as its author styled it (P039).
|
|
1226
|
+
let lastContext = null;
|
|
1227
|
+
let themed = plan.content !== "document";
|
|
1228
|
+
const schemeFollows = () => {
|
|
1229
|
+
const root = document.documentElement;
|
|
1230
|
+
const theme = lastContext && lastContext.theme;
|
|
1231
|
+
if (themed && (theme === "light" || theme === "dark")) {
|
|
1232
|
+
root.style.colorScheme = theme;
|
|
1233
|
+
root.setAttribute("data-theme", theme);
|
|
1234
|
+
} else {
|
|
1235
|
+
root.style.removeProperty("color-scheme");
|
|
1236
|
+
root.removeAttribute("data-theme");
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
// A whole document, parsed inert (no scripts run, no declarative shadow roots built), guarded,
|
|
1240
|
+
// imported in place of the shell's, then its scripts run in order. The theme on the root stays.
|
|
1241
|
+
const renderDocument = async (html) => {
|
|
1242
|
+
const parsed = new DOMParser().parseFromString(String(html), "text/html");
|
|
1243
|
+
strip(parsed);
|
|
1244
|
+
const scripts = holdScripts(parsed, parsed);
|
|
1245
|
+
const element = document.documentElement;
|
|
1246
|
+
for (const attribute of Array.from(parsed.documentElement.attributes)) {
|
|
1247
|
+
if (attribute.name !== "style") {
|
|
1248
|
+
element.setAttribute(attribute.name, attribute.value);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
document.head.replaceChildren(...Array.from(parsed.head.childNodes, (node) => document.importNode(node, true)));
|
|
1252
|
+
document.body.replaceWith(document.importNode(parsed.body, true));
|
|
1253
|
+
root = null;
|
|
1254
|
+
const declaredScheme = parsed.querySelector('meta[name="color-scheme" i]');
|
|
1255
|
+
themed = plan.themeDocuments !== "never" && (plan.themeDocuments === "always" || (declaredScheme !== null && /\bdark\b/i.test(declaredScheme.getAttribute("content") || "")));
|
|
1256
|
+
schemeFollows();
|
|
1257
|
+
await runScripts(scripts);
|
|
1258
|
+
announceLoaded();
|
|
1259
|
+
contentArrived();
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
// A document the plan carries (a served page, an MCP Apps view) is rendered once the shell's own
|
|
1263
|
+
// document has loaded, whichever protocol the frame speaks.
|
|
1264
|
+
const whenLoaded = (run) => {
|
|
1265
|
+
if (document.readyState === "complete") {
|
|
1266
|
+
run();
|
|
1267
|
+
} else {
|
|
1268
|
+
window.addEventListener("load", run, { once: true });
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
__ANNOTATE__
|
|
1273
|
+
|
|
1274
|
+
if (plan.protocol !== "shell") {
|
|
1275
|
+
// A view that speaks for itself.
|
|
1276
|
+
if (typeof plan.document === "string") {
|
|
1277
|
+
whenLoaded(() => void renderDocument(plan.document));
|
|
1278
|
+
}
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Content in this window may speak MCP Apps too (the official App SDK numbers its requests):
|
|
1283
|
+
// the shell's ids are its own strings, so neither takes the other's answers.
|
|
1284
|
+
let nextId = 1;
|
|
1285
|
+
const pending = new Map();
|
|
1286
|
+
const request = (method, params) =>
|
|
1287
|
+
new Promise((resolve, reject) => {
|
|
1288
|
+
const id = "porthole-shell-" + nextId++;
|
|
1289
|
+
pending.set(id, { resolve, reject });
|
|
1290
|
+
send({ id, method, params });
|
|
1291
|
+
});
|
|
1292
|
+
const notify = (method, params) => send({ method, params });
|
|
1293
|
+
|
|
1294
|
+
const themeNames = new Set(plan.themeVariables);
|
|
1295
|
+
// What the content was last told of (its scheme and values): the theme it starts with is no
|
|
1296
|
+
// change, and a host context that changes neither (a host re-sending it) is none either.
|
|
1297
|
+
let announced = null;
|
|
1298
|
+
const applyTheme = (context) => {
|
|
1299
|
+
if (context === null || typeof context !== "object") {
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
// The spec's variables, and Porthole's own beside them (its own-prefixed field).
|
|
1303
|
+
const own = context["porthole/styles"];
|
|
1304
|
+
const applied = [];
|
|
1305
|
+
for (const variables of [context.styles && context.styles.variables, own && own.variables]) {
|
|
1306
|
+
if (variables === null || typeof variables !== "object") {
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
for (const name of Object.keys(variables)) {
|
|
1310
|
+
const value = variables[name];
|
|
1311
|
+
if (themeNames.has(name) && typeof value === "string") {
|
|
1312
|
+
document.documentElement.style.setProperty(name, value);
|
|
1313
|
+
applied.push(name + ":" + value);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
lastContext = context;
|
|
1318
|
+
schemeFollows();
|
|
1319
|
+
const now = JSON.stringify([context.theme, applied.sort()]);
|
|
1320
|
+
if (announced === null) {
|
|
1321
|
+
announced = now;
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (now !== announced) {
|
|
1325
|
+
announced = now;
|
|
1326
|
+
window.dispatchEvent(new CustomEvent(plan.themeEvent, { detail: { theme: context.theme } }));
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
// No size before there is something to size: an empty shell's height would make the host
|
|
1331
|
+
// forget the height it remembered for this display, and the frame would jump.
|
|
1332
|
+
let drawn = false;
|
|
1333
|
+
let lastHeight = -1;
|
|
1334
|
+
const measure = () => {
|
|
1335
|
+
if (!declared("resize") || !drawn) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const box = root !== null && root.isConnected ? root : document.documentElement;
|
|
1339
|
+
const rect = box.getBoundingClientRect();
|
|
1340
|
+
const height = Math.ceil(box === root ? Math.max(rect.height, root.scrollHeight) : rect.height);
|
|
1341
|
+
if (height !== lastHeight) {
|
|
1342
|
+
lastHeight = height;
|
|
1343
|
+
notify("ui/notifications/size-changed", { width: Math.ceil(rect.width), height });
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
let scheduled = false;
|
|
1347
|
+
const schedule = () => {
|
|
1348
|
+
if (!scheduled) {
|
|
1349
|
+
scheduled = true;
|
|
1350
|
+
requestAnimationFrame(() => {
|
|
1351
|
+
scheduled = false;
|
|
1352
|
+
measure();
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
const gestured = (name) => {
|
|
1358
|
+
const capability = plan.capabilities[name];
|
|
1359
|
+
const activation = navigator.userActivation;
|
|
1360
|
+
return !(capability && capability.userActivation) || !activation || activation.isActive;
|
|
1361
|
+
};
|
|
1362
|
+
const refuse = (message) => Promise.reject(new Error(message));
|
|
1363
|
+
if (declared("sendPrompt")) {
|
|
1364
|
+
lock(window, "sendPrompt", (text) => {
|
|
1365
|
+
if (!gestured("sendPrompt")) {
|
|
1366
|
+
return refuse("sendPrompt needs a click: call it from a button's handler");
|
|
1367
|
+
}
|
|
1368
|
+
const words = String(text);
|
|
1369
|
+
if (words.trim() === "" || words.length > plan.maxPromptChars) {
|
|
1370
|
+
return refuse("sendPrompt takes 1 to " + plan.maxPromptChars + " characters");
|
|
1371
|
+
}
|
|
1372
|
+
return request("ui/message", { role: "user", content: [{ type: "text", text: words }] }).then(
|
|
1373
|
+
(result) => !(result && result.isError),
|
|
1374
|
+
);
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
if (declared("copyText")) {
|
|
1378
|
+
lock(window, "copyText", (text) => {
|
|
1379
|
+
if (!gestured("copyText")) {
|
|
1380
|
+
return refuse("copyText needs a click: call it from a button's handler");
|
|
1381
|
+
}
|
|
1382
|
+
const words = String(text);
|
|
1383
|
+
if (words === "" || words.length > plan.maxCopyChars) {
|
|
1384
|
+
return refuse("copyText takes 1 to " + plan.maxCopyChars + " characters");
|
|
1385
|
+
}
|
|
1386
|
+
return request("porthole/copy-text", { text: words }).then((result) => !(result && result.isError));
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
if (declared("openLink")) {
|
|
1390
|
+
lock(window, "openLink", (url) => {
|
|
1391
|
+
if (!gestured("openLink")) {
|
|
1392
|
+
return refuse("openLink needs a click: call it from a link's or button's handler");
|
|
1393
|
+
}
|
|
1394
|
+
return request("ui/open-link", { url: String(url) }).then((result) => !(result && result.isError));
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
window.addEventListener("error", (event) => {
|
|
1399
|
+
const error = event.error;
|
|
1400
|
+
const where = event.lineno ? " (line " + event.lineno + ")" : "";
|
|
1401
|
+
// V8's stack starts with the message; WebKit's and Firefox's don't, so it goes first there.
|
|
1402
|
+
const stack = error && error.stack ? String(error.stack).split("\n").slice(0, 3).join(" ") : "";
|
|
1403
|
+
const said = error && error.message !== undefined ? String(error.name || "Error") + ": " + String(error.message) : event.message;
|
|
1404
|
+
report(stack === "" ? said + where : stack.indexOf(String(error.message)) === -1 ? said + " " + stack : stack);
|
|
1405
|
+
});
|
|
1406
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
1407
|
+
const reason = event.reason;
|
|
1408
|
+
report("Unhandled rejection: " + (reason && reason.message ? reason.message : String(reason)));
|
|
1409
|
+
});
|
|
1410
|
+
document.addEventListener("securitypolicyviolation", (event) => {
|
|
1411
|
+
report("Blocked by the sandbox: " + (event.blockedURI || "inline") + " (" + event.effectiveDirective + ")");
|
|
1412
|
+
});
|
|
1413
|
+
// A link never navigates this frame: an http(s) one opens outside, through the host, and an
|
|
1414
|
+
// in-page anchor still jumps. SVG links (href or xlink:href) and image-map areas too.
|
|
1415
|
+
document.addEventListener(
|
|
1416
|
+
"click",
|
|
1417
|
+
(event) => {
|
|
1418
|
+
const target = event.target;
|
|
1419
|
+
const link = target && target.closest ? target.closest("a, area") : null;
|
|
1420
|
+
if (link === null) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const href = link.getAttribute("href") || link.getAttributeNS("http://www.w3.org/1999/xlink", "href") || "";
|
|
1424
|
+
if (href === "" || href.charAt(0) === "#") {
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
event.preventDefault();
|
|
1428
|
+
let url = "";
|
|
1429
|
+
try {
|
|
1430
|
+
url = new URL(href, document.baseURI).href;
|
|
1431
|
+
} catch (_) {}
|
|
1432
|
+
if (/^https?:/i.test(url) && typeof window.openLink === "function") {
|
|
1433
|
+
window.openLink(url).catch(() => {});
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1436
|
+
true,
|
|
1437
|
+
);
|
|
1438
|
+
if (typeof ResizeObserver === "function") {
|
|
1439
|
+
const sizes = new ResizeObserver(schedule);
|
|
1440
|
+
sizes.observe(document.documentElement);
|
|
1441
|
+
if (root !== null) {
|
|
1442
|
+
sizes.observe(root);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
window.addEventListener("load", schedule);
|
|
1446
|
+
|
|
1447
|
+
let rendered = false;
|
|
1448
|
+
|
|
1449
|
+
// The content so far, drawn inert: what runs, loads a frame or navigates is left out, and every
|
|
1450
|
+
// attribute that runs code (on…) or names a javascript: URL. Parsed in a template, where nothing
|
|
1451
|
+
// runs; its scripts run only in the whole content, once (render).
|
|
1452
|
+
const INERT = "script,iframe,frame,frameset,object,embed,applet,portal,fencedframe,meta,base,noscript,link:not([rel~=stylesheet i])";
|
|
1453
|
+
const inert = (fragment) => {
|
|
1454
|
+
for (const element of Array.from(fragment.querySelectorAll(INERT))) {
|
|
1455
|
+
element.remove();
|
|
1456
|
+
}
|
|
1457
|
+
for (const element of Array.from(fragment.querySelectorAll("*"))) {
|
|
1458
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
1459
|
+
const name = attribute.name.toLowerCase();
|
|
1460
|
+
if (name.startsWith("on") || /^\s*javascript:/i.test(attribute.value)) {
|
|
1461
|
+
element.removeAttributeNode(attribute);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return fragment;
|
|
1466
|
+
};
|
|
1467
|
+
// Change only what changed: what was drawn stays (an image doesn't load again, an animation
|
|
1468
|
+
// doesn't start over) as the content grows at its end.
|
|
1469
|
+
const morph = (into, from) => {
|
|
1470
|
+
const wanted = Array.from(from.childNodes);
|
|
1471
|
+
wanted.forEach((want, index) => {
|
|
1472
|
+
const have = into.childNodes[index];
|
|
1473
|
+
if (have === undefined) {
|
|
1474
|
+
into.appendChild(want);
|
|
1475
|
+
} else if (have.nodeType !== want.nodeType || have.nodeName !== want.nodeName || have.namespaceURI !== want.namespaceURI) {
|
|
1476
|
+
into.replaceChild(want, have);
|
|
1477
|
+
} else if (want.nodeType === 1) {
|
|
1478
|
+
for (const attribute of Array.from(have.attributes)) {
|
|
1479
|
+
if (!want.hasAttributeNS(attribute.namespaceURI, attribute.localName)) {
|
|
1480
|
+
have.removeAttributeNode(attribute);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
for (const attribute of Array.from(want.attributes)) {
|
|
1484
|
+
if (have.getAttributeNS(attribute.namespaceURI, attribute.localName) !== attribute.value) {
|
|
1485
|
+
have.setAttributeNS(attribute.namespaceURI, attribute.name, attribute.value);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
morph(have, want);
|
|
1489
|
+
} else if (have.nodeValue !== want.nodeValue) {
|
|
1490
|
+
have.nodeValue = want.nodeValue;
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
while (into.childNodes.length > wanted.length) {
|
|
1494
|
+
into.removeChild(into.lastChild);
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
let drafted = false;
|
|
1498
|
+
const draft = (input) => {
|
|
1499
|
+
if (rendered || root === null || input === null || typeof input !== "object" || plan.content === "document") {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
morph(root, inert(parseInert(input.html || "")));
|
|
1503
|
+
drafted = true;
|
|
1504
|
+
drawn = true;
|
|
1505
|
+
schedule();
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
const SVG = /^\s*(<\?xml[\s\S]*?\?>\s*)?(<!--[\s\S]*?-->\s*)*<svg[\s>]/i;
|
|
1509
|
+
const render = async (input) => {
|
|
1510
|
+
if (rendered || input === null || typeof input !== "object") {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
rendered = true;
|
|
1514
|
+
drawn = true;
|
|
1515
|
+
const html = String(input.html || "");
|
|
1516
|
+
if (plan.content === "document") {
|
|
1517
|
+
await renderDocument(html);
|
|
1518
|
+
schedule();
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (root === null) {
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
if (drafted) {
|
|
1525
|
+
// The whole content replaces its draft, and runs as if drawn first.
|
|
1526
|
+
root.textContent = "";
|
|
1527
|
+
}
|
|
1528
|
+
// SVG goes in as drawn: its scripts, if any, stay inert.
|
|
1529
|
+
const fragment = parseInert(html);
|
|
1530
|
+
strip(fragment);
|
|
1531
|
+
const scripts = SVG.test(html) ? [] : holdScripts(fragment, document);
|
|
1532
|
+
root.appendChild(fragment);
|
|
1533
|
+
schedule();
|
|
1534
|
+
await runScripts(scripts);
|
|
1535
|
+
announceLoaded();
|
|
1536
|
+
contentArrived();
|
|
1537
|
+
schedule();
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
const receive = (data) => {
|
|
1541
|
+
if (data === null || typeof data !== "object" || data.jsonrpc !== "2.0") {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
if (data.id !== undefined && data.method === undefined) {
|
|
1545
|
+
const waiting = pending.get(data.id);
|
|
1546
|
+
if (waiting !== undefined) {
|
|
1547
|
+
pending.delete(data.id);
|
|
1548
|
+
if (data.error) {
|
|
1549
|
+
waiting.reject(new Error(String(data.error.message || "refused")));
|
|
1550
|
+
} else {
|
|
1551
|
+
waiting.resolve(data.result);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
const params = data.params !== null && typeof data.params === "object" ? data.params : {};
|
|
1557
|
+
switch (data.method) {
|
|
1558
|
+
case "ui/notifications/tool-input-partial":
|
|
1559
|
+
draft(params.arguments);
|
|
1560
|
+
break;
|
|
1561
|
+
case "ui/notifications/tool-input":
|
|
1562
|
+
void render(params.arguments);
|
|
1563
|
+
break;
|
|
1564
|
+
case "ui/notifications/host-context-changed":
|
|
1565
|
+
applyTheme(params);
|
|
1566
|
+
break;
|
|
1567
|
+
case "ui/resource-teardown":
|
|
1568
|
+
case "ping":
|
|
1569
|
+
if (data.id !== undefined) {
|
|
1570
|
+
send({ id: data.id, result: {} });
|
|
1571
|
+
}
|
|
1572
|
+
break;
|
|
1573
|
+
default:
|
|
1574
|
+
if (data.id !== undefined) {
|
|
1575
|
+
send({ id: data.id, error: { code: -32601, message: String(data.method) + " isn't offered here" } });
|
|
1576
|
+
}
|
|
1577
|
+
break;
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
window.addEventListener("message", (event) => {
|
|
1581
|
+
if (event.source !== window.parent || plan.origins.indexOf(event.origin) === -1) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
receive(event.data);
|
|
1585
|
+
});
|
|
1586
|
+
if (typeof plan.document === "string") {
|
|
1587
|
+
whenLoaded(() => void render({ html: plan.document }));
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// Ask until answered: the host may start listening after this runs.
|
|
1591
|
+
let initialized = false;
|
|
1592
|
+
let tries = 0;
|
|
1593
|
+
const initialize = () => {
|
|
1594
|
+
if (initialized || tries >= 40) {
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
tries += 1;
|
|
1598
|
+
request("ui/initialize", {
|
|
1599
|
+
protocolVersion: plan.protocolVersion,
|
|
1600
|
+
appInfo: { name: "porthole-shell", version: "1" },
|
|
1601
|
+
appCapabilities: {},
|
|
1602
|
+
}).then((result) => {
|
|
1603
|
+
if (initialized) {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
initialized = true;
|
|
1607
|
+
applyTheme(result && result.hostContext);
|
|
1608
|
+
notify("ui/notifications/initialized", {});
|
|
1609
|
+
schedule();
|
|
1610
|
+
}, () => {});
|
|
1611
|
+
setTimeout(initialize, 250);
|
|
1612
|
+
};
|
|
1613
|
+
initialize();
|
|
1614
|
+
})();`;var vn=/^[A-Za-z0-9+/_=-]{8,256}$/u,kn=e=>e.replace(/&/gu,"&").replace(/"/gu,""").replace(/</gu,"<"),Sn=e=>e.replace(/&/gu,"&").replace(/</gu,"<").replace(/>/gu,">"),Cn=e=>JSON.stringify(e).replace(/</gu,"\\u003c").replace(/\u2028/gu,"\\u2028").replace(/\u2029/gu,"\\u2029"),Tn=`:root { color-scheme: light dark; }
|
|
1615
|
+
html, body { margin: 0; padding: 0; background: transparent; }
|
|
1616
|
+
body {
|
|
1617
|
+
font-family: var(--font-sans, system-ui, sans-serif);
|
|
1618
|
+
font-size: var(--font-text-md-size, 14px);
|
|
1619
|
+
line-height: var(--font-text-md-line-height, 20px);
|
|
1620
|
+
color: var(--color-text-primary, CanvasText);
|
|
1621
|
+
overflow-wrap: anywhere;
|
|
1622
|
+
-webkit-text-size-adjust: 100%;
|
|
1623
|
+
}
|
|
1624
|
+
#porthole-root { display: flow-root; }
|
|
1625
|
+
#porthole-root > svg { display: block; max-width: 100%; height: auto; }
|
|
1626
|
+
:where(button, input, select, textarea) { font: inherit; color: inherit; box-sizing: border-box; }
|
|
1627
|
+
:where(button, select, input:is([type="button" i], [type="submit" i], [type="reset" i])) {
|
|
1628
|
+
min-height: 44px;
|
|
1629
|
+
padding: 0 14px;
|
|
1630
|
+
border: var(--border-width-regular, 1px) solid var(--color-border-primary, ButtonBorder);
|
|
1631
|
+
border-radius: var(--border-radius-md, 8px);
|
|
1632
|
+
background: var(--color-background-secondary, ButtonFace);
|
|
1633
|
+
color: var(--color-text-primary, ButtonText);
|
|
1634
|
+
cursor: pointer;
|
|
1635
|
+
}
|
|
1636
|
+
:where(input:not([type]), input:is([type="text" i], [type="search" i], [type="number" i], [type="email" i], [type="url" i], [type="tel" i], [type="password" i], [type="date" i], [type="time" i]), textarea) {
|
|
1637
|
+
min-height: 44px;
|
|
1638
|
+
padding: 0 10px;
|
|
1639
|
+
border: var(--border-width-regular, 1px) solid var(--color-border-primary, FieldText);
|
|
1640
|
+
border-radius: var(--border-radius-md, 8px);
|
|
1641
|
+
background: var(--color-background-secondary, Field);
|
|
1642
|
+
}
|
|
1643
|
+
:where(textarea) { padding: 8px 10px; }
|
|
1644
|
+
:where(input:is([type="checkbox" i], [type="radio" i], [type="range" i])) { accent-color: var(--color-accent, auto); }
|
|
1645
|
+
:where(button, input, select, textarea):focus-visible { outline: 2px solid var(--color-ring-primary, Highlight); outline-offset: 1px; }
|
|
1646
|
+
:where(button, input, select, textarea):disabled { color: var(--color-text-disabled, GrayText); cursor: default; }`;function Et(e,t,n){for(let i of n.origins){let s=St(i);if(s!==null)throw new TypeError(`shellDocument: ${s}`)}if(n.nonce!==void 0&&!vn.test(n.nonce))throw new TypeError("shellDocument: a nonce is 8 to 256 base64 characters");let o=n.nonce===void 0?"":` nonce="${n.nonce}"`,r=At.replace("__ANNOTATE__",()=>e.capabilities.annotate===void 0?"":Tt).replace("__PLAN__",()=>Cn({...e,origins:n.origins,...n.document===void 0?{}:{document:n.document}}));return`<!doctype html>
|
|
1647
|
+
<html lang="en">
|
|
1648
|
+
<head>
|
|
1649
|
+
${['<meta charset="utf-8">',n.cspMeta===!1?null:`<meta http-equiv="Content-Security-Policy" content="${kn(t)}">`,'<meta name="referrer" content="no-referrer">','<meta http-equiv="x-dns-prefetch-control" content="off">'].filter(i=>i!==null).join(`
|
|
1650
|
+
`)}
|
|
1651
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1652
|
+
<title>${Sn(n.title??"Content")}</title>
|
|
1653
|
+
<style${o}>
|
|
1654
|
+
${Tn}
|
|
1655
|
+
</style>
|
|
1656
|
+
</head>
|
|
1657
|
+
<body>
|
|
1658
|
+
<div id="porthole-root"></div>
|
|
1659
|
+
<script${o}>${r}<\/script>
|
|
1660
|
+
</body>
|
|
1661
|
+
</html>
|
|
1662
|
+
`}function Rt(e){it(e);let t=Ze(e),n=et(t),o=Y(e);return{config:e,cspDirectives:t,csp:n,sandbox:pe(e).join(" "),allow:tt(e),referrerPolicy:"no-referrer",embedderFrameSources:nt(e),shell:o,shellDocument:r=>{if(o===null)throw new TypeError("url mode has no shell: the frame loads the app's own document");return Et(o,n,r)},guide:r=>bt(e,r),toolDescription:()=>xt(e)}}async function Mt(e,t,n,o,r){let a=await e.locate(t,r);return Ct(a,n.contentOrigins,o),{kind:"url",url:new URL(a.url).href,origin:a.origin,...a.reports===!0?{reports:!0}:{}}}var ve="porthole/notifications/frame-status";var Pt=400,An=["framing","cookies","sign-in","session","upstream","denied","not-found","error"];function En(e){if(e===null||typeof e!="object")return null;let t=e;if(t.jsonrpc!=="2.0"||t.method!==ve)return null;let n=t.params;return n===null||typeof n!="object"?null:n.state==="entering"?{state:"entering"}:n.state!=="blocked"?null:{state:"blocked",reason:An.includes(n.reason)?n.reason:"error",...typeof n.status=="number"&&Number.isInteger(n.status)?{status:n.status}:{},...typeof n.code=="string"&&/^[a-z0-9-]{1,40}$/u.test(n.code)?{code:n.code}:{}}}function $t(e){let{surface:t,source:n,speaks:o}=e,r=n.kind==="url"&&n.reports===!0,a=null,i=!1,s=!1,c=0,l=new Set,f=(p,y)=>{let C=setTimeout(()=>{l.delete(C),y()},p);l.add(C)},k=()=>{for(let p of l)clearTimeout(p);l.clear()},m=()=>{k(),d(),u()},v=p=>{a===null?(a=p.state,k(),e.onOutcome(p)):a==="loaded"&&p.state==="blocked"&&(a="blocked",e.onOutcome(p))},d=t.transport.listen(p=>{i=!0;let y=En(p);y?.state==="entering"?s=!0:y?.state==="blocked"&&v({state:"blocked",reason:y.reason??"error",...y.status===void 0?{}:{status:y.status},...y.code===void 0?{}:{code:y.code}})}),g=()=>f(Pt,()=>{i||v({state:"blocked",reason:"framing"})}),u=t.onLoad(()=>{if(c+=1,a===null){if(o){n.kind==="url"&&g();return}if(!r){f(Pt,()=>v({state:"loaded"}));return}s&&c>=2?v({state:"loaded"}):s||g()}});return f(e.timeoutMs,()=>v({state:"timeout"})),{initialized:()=>v({state:"loaded"}),stop:m}}function Lt(e,t,n=window,o){let r=new Set,a=!1,i=c=>{let l=e();if(!(a||l===null||c.source!==l)&&!(c.origin!==t&&(o===void 0||c.origin!==o.origin||!o.accept(c.data))))for(let f of r)f(c.data)};n.addEventListener("message",i);let s=t==="null"?"*":t;return{send(c){a||e()?.postMessage(c,s)},listen(c){return r.add(c),()=>r.delete(c)},close(){a=!0,r.clear(),n.removeEventListener("message",i)}}}var Rn=e=>e!==null&&typeof e=="object"&&e.jsonrpc==="2.0"&&e.method===ve,Nt={name:"web",createSurface(e,t){let n=e.ownerDocument.createElement("iframe");n.setAttribute("sandbox",t.sandbox),n.setAttribute("allow",t.allow),n.setAttribute("referrerpolicy",t.referrerPolicy),n.setAttribute("loading","eager"),n.title=t.title,n.dataset.portholeFrame="",Object.assign(n.style,{display:"block",width:"100%",border:"0",background:"transparent",colorScheme:t.colorScheme,...t.fill===!0?{height:"100%",minHeight:`${Math.max(0,Math.round(t.initialHeightPx))}px`}:{height:`${Math.max(0,Math.round(t.initialHeightPx))}px`}});let o=t.source.kind==="srcdoc"?"null":t.source.origin;t.source.kind==="srcdoc"?n.srcdoc=t.source.html:n.src=t.source.url;let r=new Set,a=new Set,i=0;n.addEventListener("load",()=>{i+=1;for(let f of a)f();if(i>1)for(let f of r)f()}),e.append(n);let s=e.ownerDocument.defaultView??window,c=s.origin,l=Lt(()=>n.contentWindow,o,s,o==="null"||c==="null"||c===o?void 0:{origin:c,accept:Rn});return{transport:l,element:n,setHeight(f){t.fill!==!0&&(n.style.height=`${Math.max(0,Math.round(f))}px`)},setColorScheme(f){n.style.colorScheme=f},userActivation(){let f=s.navigator.userActivation;return f===void 0?void 0:f.isActive},onLoad(f){return a.add(f),()=>a.delete(f)},onNavigated(f){return r.add(f),()=>r.delete(f)},destroy(){r.clear(),a.clear(),l.close(),n.remove()}}}};var It={invalidRequest:-32600,methodNotFound:-32601,invalidParams:-32602,internal:-32603},ze=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Ot=e=>typeof e=="string"&&e.length<=256||typeof e=="number"&&Number.isSafeInteger(e);function Dt(e){if(!ze(e)||e.jsonrpc!=="2.0"||e.params!==void 0&&!ze(e.params))return null;if(typeof e.method=="string")return e.method.length===0||e.method.length>256?null:e.id===void 0||Ot(e.id)?e:null;if(!Ot(e.id)||e.method!==void 0)return null;let t="result"in e,n="error"in e;if(t===n)return null;if(n){let o=e.error;if(!ze(o)||typeof o.code!="number"||typeof o.message!="string")return null}return e}var Mn=new Set(["error","critical","alert","emergency"]),Pn={[M.message]:{capability:"sendPrompt",args:e=>({text:Array.isArray(e.content)?e.content.map(t=>{let n=t;return n!==null&&typeof n=="object"&&n.type==="text"&&typeof n.text=="string"?n.text:""}).join(""):void 0})},[M.openLink]:{capability:"openLink",args:e=>({url:e.url})},[ct]:{capability:"copyText",args:e=>({text:e.text})},[M.downloadFile]:{capability:"downloadFile",args:e=>({contents:e.contents})},[M.callTool]:{capability:"callTool",args:e=>({name:e.name,arguments:e.arguments})},[M.readResource]:{capability:"readData",args:e=>({uri:e.uri})},[M.updateModelContext]:{capability:"setModelContext",args:e=>({content:e.content,structuredContent:e.structuredContent})}};function $n(e,t){let n=e==="sendPrompt"||e==="openLink"||e==="downloadFile"||e==="copyText";return t.ok?n?{result:t.value===!0?{}:{isError:!0}}:{result:t.value??{}}:n?{result:{isError:!0}}:{error:{code:-32e3,message:`Refused (${t.refusal})`}}}function Ln(e){let t={};return e("openLink")&&(t.openLinks={}),e("sendPrompt")&&(t.message={text:{}}),e("downloadFile")&&(t.downloadFile={}),e("callTool")&&(t.serverTools={}),e("readData")&&(t.serverResources={}),e("setModelContext")&&(t.updateModelContext={text:{}}),e("reportError")&&(t.logging={}),t}var Ht={name:"porthole-shell",connect(e){let{transport:t,bridge:n}=e,o=!1,r=!1,a=null,i=null,s=!1,c=1,l=new Map,f=(u,p)=>t.send({jsonrpc:"2.0",id:u,...p}),k=(u,p)=>t.send({jsonrpc:"2.0",method:u,params:p}),m=()=>{if(!(!o||r||i===null||s)){if(i.partial){a!==i.html&&(a=i.html,k(M.toolInputPartial,{arguments:{html:i.html}}));return}s=!0,k(M.toolInput,{arguments:{html:i.html}})}},v=async u=>{let p=u.params??{};if(u.method===M.initialize){f(u.id,{result:{protocolVersion:he,hostInfo:e.hostInfo,hostCapabilities:Ln(A=>n.declared(A)),hostContext:e.hostContext()}});return}if(u.method==="ping"){f(u.id,{result:{}});return}if(u.method===M.requestDisplayMode){f(u.id,{result:{mode:"inline"}});return}let y=Pn[u.method];if(y===void 0){f(u.id,{error:{code:It.methodNotFound,message:`${u.method} isn't offered here`}});return}let C=await n.call(y.capability,y.args(p));r||f(u.id,$n(y.capability,C))},d=(u,p)=>{switch(u){case M.initialized:o||(o=!0,e.emit({type:"initialized"}),m());return;case M.sizeChanged:n.call("resize",{height:p.height,width:p.width});return;case M.log:if(Mn.has(String(p.level))){let y=p.data;n.call("reportError",{message:typeof y=="string"?y:JSON.stringify(y??null).slice(0,2e3)})}return;case M.requestTeardown:e.emit({type:"teardown-requested"});return;default:return}},g=t.listen(u=>{let p=Dt(u);if(!(p===null||r)){if("method"in p){"id"in p?v(p):d(p.method,p.params??{});return}l.get(p.id)?.(p),l.delete(p.id)}});return{setContent(u,p){s||(i={html:u,partial:p?.partial===!0},m())},hostContextChanged(u){o&&!r&&k(M.hostContextChanged,{...u})},teardown(){if(!o||r)return Promise.resolve();let u=`teardown-${c++}`;return new Promise(p=>{let y=setTimeout(()=>{l.delete(u),p()},500);l.set(u,()=>{clearTimeout(y),p()}),t.send({jsonrpc:"2.0",id:u,method:M.resourceTeardown,params:{}})})},close(){r=!0,g(),l.clear()}}}};var Nn=15e3,On=3,In=new Set(["hostContextChanged","teardown","close"]),Dn={[O.selected]:"selected",[O.unselected]:"unselected",[O.cancelled]:"cancelled",[O.anchored]:"anchored"},_t=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Hn=new Set(lt);function Fe(e,t={}){let n=Rt(e);if((e.isolation==="domain"||e.isolation==="external")&&t.locator===void 0)throw new TypeError(`${e.isolation} isolation needs a locator (HostOptions.locator)`);let o=e.mode!=="url",r=t.adapter??Ht,a=t.platform??Nt,i=t.hostInfo??{name:"porthole",version:"0"};return{compiled:n,mount(s,c){let l={...t.handlers,...c.handlers},f=D(e).filter(h=>B(h).served==="handler"&&l[h]===void 0);if(f.length>0)throw new TypeError(`No handler for the declared capabilities: ${f.join(", ")}`);if(e.isolation!=="opaque"&&c.instance===void 0)throw new TypeError(`${e.isolation} isolation needs MountOptions.instance`);let k=new Map,m=(h,b)=>{for(let E of k.get(h)??[])E(b)},v=h=>{m("contentError",{message:h}),m("error",{message:h,source:"content"})},d=(h,b)=>{m("hostError",{message:h,kind:b}),m("error",{message:h,source:"host"})},g=e.theme.documents==="never",u=g?"light":c.scheme,p=c.initialHeightPx??0,y=null,C=null,A=!1,Te=0,ae=0,Ae=null,Ee=null,Q=null,G=s.ownerDocument.defaultView,Re=new Map,We=e.capabilities.annotate!==void 0,L=null,W=[],z=null,se=!1,U=(h,b)=>{se&&y!==null&&y.transport.send({jsonrpc:"2.0",method:h,params:b})},Bt=()=>{W.length>0&&U(O.show,{marks:W,...z===null?{}:{active:z}}),L!==null&&U(O.start,{...L})},Ut=(h,b)=>{if(!_t(h)||h.jsonrpc!=="2.0"||"id"in h||typeof h.method!="string")return;let E=Dn[h.method];if(E===void 0)return;let R=de({..._t(h.params)?h.params:{},type:E});if(R===null){b.call("annotate",null);return}switch(R.type){case"selected":{let P=R.anchor.kind;if(L===null||!(P==="element"?L.elements:L.text))return;P==="element"&&(L=L.text?{elements:!1,text:!0}:null);break}case"unselected":if(L?.text!==!0)return;break;case"cancelled":if(L?.elements!==!0)return;L=L.text?{elements:!1,text:!0}:null;break;case"anchored":{let P=new Set(W.map(ee=>ee.id)),T=R.results.filter(ee=>P.has(ee.id));if(T.length===0)return;R={type:"anchored",results:T};break}}b.call("annotate",R)},Z=h=>{if(!We)throw new TypeError(`annotations.${h}: the config doesn't declare annotate`)},Vt={start(h){Z("start"),L=Ke(h),U(O.start,{...L})},stop(){Z("stop"),L=null,U(O.stop,{})},show(h){Z("show"),W=Xe(h),z!==null&&!W.some(b=>b.id===z)&&(z=null),U(O.show,{marks:W,...z===null?{}:{active:z}})},focus(h){if(Z("focus"),!W.some(b=>b.id===h))throw new TypeError(`annotations.focus: no mark ${JSON.stringify(h)} is shown`);z=h,U(O.focus,{id:h})},clear(){Z("clear"),W=[],z=null,U(O.clear,{})}},ie=()=>{let h=s.getBoundingClientRect().width,b=ut(e.theme,u,t.theme?.(u)),E={},R={};for(let[P,T]of Object.entries(b))(Hn.has(P)?E:R)[P]=T;return{theme:u,styles:{variables:E},...Object.keys(R).length===0?{}:{[dt]:{variables:R}},displayMode:"inline",availableDisplayModes:["inline"],...h>0?{containerDimensions:{maxWidth:Math.round(h)}}:{},...G===null?{}:{locale:G.navigator.language,timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,platform:"web"},...t.hostContext?.()}},Gt=h=>{switch(h.type){case"resize":{let b=Math.min(e.limits.maxHeightPx,Math.max(e.limits.minHeightPx,Math.ceil(h.height)));p=b,y?.setHeight(b),m("resize",{height:b,...h.width===void 0?{}:{width:h.width}});return}case"error":v(h.message);return;case"refused":m("refused",{capability:h.capability,refusal:h.refusal});return;case"muted":m("muted",void 0);return;case"annotation":m("annotation",h.report);return}},Me=()=>{se=!1,Ae?.abort(),Ae=null,Ee?.stop(),Ee=null,C?.close(),C=null,y?.destroy(),y=null},Be=h=>{let b=a.createSurface(s,{source:h,title:c.title,sandbox:n.sandbox,allow:n.allow,referrerPolicy:n.referrerPolicy,colorScheme:u,...o&&c.fill!==!0&&e.layout.kind!=="fill"?{initialHeightPx:p}:{fill:!0,initialHeightPx:e.limits.minHeightPx}});y=b;let E=$t({surface:b,source:h,speaks:o,timeoutMs:e.limits.loadTimeoutMs??Nn,onOutcome:T=>m("outcome",T)});if(Ee=E,!o){let T=!1;b.onLoad(()=>{T||(T=!0,m("initialized",void 0))});return}let R=rt({config:e,display:{id:c.displayId,title:c.title},handlers:l,...t.confirm===void 0?{}:{confirm:t.confirm},userActivation:()=>b.userActivation(),onEvent:Gt});We&&b.transport.listen(T=>Ut(T,R));let P=r.connect({display:{id:c.displayId,title:c.title},compiled:n,transport:b.transport,bridge:R,hostInfo:i,hostContext:ie,emit:T=>{T.type==="initialized"&&(E.initialized(),se||(se=!0,Bt()),m("initialized",void 0))},...c.document===void 0?{}:{document:c.document}});C=P;for(let[T,ee]of Re)P[T]?.(...ee);b.onNavigated(()=>{m("navigated",void 0),Te<On?(Te+=1,Pe()):(Me(),v("The content kept navigating its frame away; it was stopped."))})},Pe=()=>{if(Me(),A)return;if(ae+=1,e.isolation==="opaque"){Be({kind:"srcdoc",html:n.shellDocument({origins:[s.ownerDocument.defaultView?.location.origin??""],title:c.title,..._n(t.nonce),...c.document===void 0?{}:{document:c.document}})});return}let h=ae,b=new AbortController;Ae=b;let E=t.locator,R=c.instance;if(E===void 0||R===void 0)return;let P=s.ownerDocument.defaultView?.location.origin??"";Mt(E,R,e,P,b.signal).then(T=>{h===ae&&!A&&Be(T)},T=>{h===ae&&!b.signal.aborted&&(d(T instanceof Error?T.message:String(T),"locate"),m("outcome",{state:"blocked",reason:"locate"}))})};Pe();let Jt={displayId:c.displayId,annotations:Vt,setTheme(h){if(!g){if(u=h,y?.setColorScheme(h),G===null){C?.hostContextChanged(ie());return}Q!==null&&G.cancelAnimationFrame(Q),Q=G.requestAnimationFrame(()=>{Q=G.requestAnimationFrame(()=>{Q=null,C?.hostContextChanged(ie())})})}},refreshTheme(){C?.hostContextChanged(ie())},on(h,b){let E=k.get(h)??new Set;return E.add(b),k.set(h,E),()=>E.delete(b)},reload(){Te=0,Pe()},destroy(){A=!0,Me(),k.clear()}};return new Proxy(Jt,{get(h,b,E){return typeof b=="string"&&!(b in h)&&!In.has(b)?(...R)=>{Re.delete(b),Re.set(b,R);let P=C?.[b];return typeof P=="function"?P.apply(C,R):void 0}:Reflect.get(h,b,E)}})}}}function _n(e){let t=typeof e=="function"?e():e;return t===void 0||t===""?{}:{nonce:t}}var ke=class extends Error{status;code;constructor(t,n,o){super(t),this.name="LocateError",this.status=n,this.code=o}},qe=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);function jt(e,t={}){return{async locate(n,o){let r=typeof t.headers=="function"?t.headers():t.headers,a=new Headers(r);a.set("content-type","application/json"),a.set("accept","application/json");let i=await(t.fetch??fetch)(e,{method:"POST",headers:a,credentials:t.credentials??"same-origin",redirect:"error",body:JSON.stringify({instance:n}),...o===void 0?{}:{signal:o}}),s=await i.json().catch(()=>null);if(!i.ok){let c=qe(s)&&qe(s.error)?s.error:{},l=typeof c.code=="string"?c.code:"failed";throw l==="not-served"&&t.onNotServed?.(),new ke(typeof c.message=="string"?c.message:`The app's issue endpoint answered ${i.status}`,i.status,l)}if(!qe(s)||typeof s.url!="string"||typeof s.origin!="string"||s.reports!==void 0&&typeof s.reports!="boolean")throw new ke("The app's issue endpoint gave no frame location",i.status,"bad-location");return{url:s.url,origin:s.origin,...typeof s.reports=="boolean"?{reports:s.reports}:{}}}}}var jn=["initialized","resize","error","contentError","hostError","navigated","refused","muted","outcome","annotation"],Se=document.currentScript instanceof HTMLScriptElement&&document.currentScript.nonce!==""?document.currentScript.nonce:void 0,$=new Map,Ce=0,zt=e=>new Promise(t=>setTimeout(t,e));function zn(){let e=document.getElementById("porthole-conformance");return e===null&&(e=document.createElement("main"),e.id="porthole-conformance",e.style.cssText="position:fixed;inset:0;overflow:auto;z-index:2147483000;background:#fff;color:#000;padding:8px",document.body.append(e)),e}function Ft(e,t,n){let o=document.createElement("section");return o.id=`porthole-display-${e}`,o.style.width=`${t}px`,n!==void 0&&(o.style.height=`${n}px`),zn().append(o),o}function qt(e){let t=(n,o)=>r=>(e.push({capability:n,args:r}),o);return{sendPrompt:t("sendPrompt",!0),openLink:t("openLink",!0),downloadFile:t("downloadFile",!0),callTool:t("callTool",{content:[]}),readData:t("readData",{contents:[]}),setModelContext:t("setModelContext",void 0),copyText:t("copyText",!0)}}function Wt(e,t){for(let n of jn)t.handle.on(n,o=>t.events.push({type:n,detail:o}));$.set(e,t)}addEventListener("message",e=>{for(let t of $.values())t.frame?.contentWindow===e.source&&e.origin===t.origin&&t.received.push(e.data)});var Fn={async launch(e){let t=Ce;Ce+=1;let n=Ft(t,e.widthPx??600,e.heightPx),o=[],r=[],i=Fe(e.config,{handlers:qt(o),...e.confirms===!1?{}:{confirm:l=>new Promise(f=>r.push({request:l,resolve:f}))},theme:l=>e.hostTheme?.[l]??{},hostInfo:{name:"porthole-conformance",version:"1"},...Se===void 0?{}:{nonce:Se}}).mount(n,{displayId:`display-${t}`,title:e.title??"Probe",scheme:e.scheme??"light",...e.document===void 0?{}:{document:e.document}}),s={handle:i,calls:o,events:[],confirmations:r,received:[],origin:null,failure:null,frame:null},c=new Promise(l=>i.on("initialized",()=>l()));if(Wt(t,s),e.document===void 0){await Promise.race([c,zt(5e3)]);for(let l of e.partials??[])i.setContent(l,{partial:!0}),await zt(150);e.content!==void 0&&i.setContent(e.content)}return t},show(e,t,n,o){let r=Ce;Ce+=1;let a=Ft(r,600,400),i=[],s={handle:null,calls:i,events:[],confirmations:[],received:[],origin:null,failure:null,frame:null},c="issue"in n?jt(n.issue,n.headers===void 0?{}:{headers:n.headers}):{locate:()=>Promise.resolve(n.location)},l={async locate(m,v){try{let d=await c.locate(m,v);return s.origin=d.origin,d}catch(d){throw s.failure=d instanceof Error?`${d.name}: ${d.message}`:String(d),d}}},f=Fe(e,{handlers:qt(i),locator:l,hostInfo:{name:"porthole-conformance",version:"1"},...Se===void 0?{}:{nonce:Se}}).mount(a,{displayId:`display-${r}`,title:"Probe",scheme:"light",instance:t});s.handle=f,Wt(r,s);let k=()=>{s.frame=a.querySelector("iframe"),s.frame===null&&requestAnimationFrame(k)};return k(),o!==void 0&&f.setContent(o),r},origin:e=>$.get(e)?.origin??null,failure:e=>$.get(e)?.failure??null,received:e=>$.get(e)?.received??[],post(e,t){let n=$.get(e);n?.frame?.contentWindow&&n.origin!==null&&n.frame.contentWindow.postMessage(t,n.origin)},setContent(e,t,n){$.get(e)?.handle.setContent(t,{partial:n})},calls:e=>$.get(e)?.calls??[],events:e=>$.get(e)?.events??[],errors:e=>($.get(e)?.events??[]).flatMap(t=>t.type==="error"?[t.detail.message]:[]),pendingConfirmations:e=>$.get(e)?.confirmations.length??0,answer(e,t){let n=$.get(e)?.confirmations.shift();return n===void 0?null:(n.resolve(t),n.request)},setTheme:(e,t)=>$.get(e)?.handle.setTheme(t),destroy(e){$.get(e)?.handle.destroy(),document.getElementById(`porthole-display-${e}`)?.remove(),$.delete(e)}};window.portholeConformance=Fn;document.documentElement.dataset.portholeConformance="ready";})();
|