@hazem3500/palette 0.9.111 → 0.9.112

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/setup.js CHANGED
@@ -1 +1 @@
1
- import{a as n,d as o}from"./chunk-DDYHX4RZ.js";import"./chunk-KLPQTFVH.js";import"./chunk-RBHY7UDS.js";import"./chunk-LMXFSXO6.js";import"./chunk-DU7H4TEX.js";import"./chunk-5HZI7HRR.js";import"./chunk-2BS2OWWH.js";import"./chunk-J47R52WX.js";import"./chunk-VX6XCU4E.js";import"./chunk-4HBBEVFK.js";function r(r){return(t={})=>o(n({...r,...t}))}var t=r;export{t as defineKomo,r as definePalette};
1
+ import{a as n,d as o}from"./chunk-CF4GJN3B.js";import"./chunk-KLPQTFVH.js";import"./chunk-RBHY7UDS.js";import"./chunk-LMXFSXO6.js";import"./chunk-DU7H4TEX.js";import"./chunk-5HZI7HRR.js";import"./chunk-2BS2OWWH.js";import"./chunk-J47R52WX.js";import"./chunk-VX6XCU4E.js";import"./chunk-4HBBEVFK.js";function r(r){return(t={})=>o(n({...r,...t}))}var t=r;export{t as defineKomo,r as definePalette};
@@ -1 +1 @@
1
- var e="data-palette-no-snapshot",t=`[${e}],[data-komo-no-snapshot]`,a=e=>e.matches(t),n="[data-branch-comments],[data-palette-move-clone],[data-palette-viewport],[data-palette-snapshot-sandbox]",r="[redacted]",o=new Set(["script","noscript","template","style","link","meta","base","title"]),i=/^(?:href|src|action|formaction|xlink:href|poster|data|background)$/i,l=e=>"textarea"===e.localName||"input"===e.localName&&!/^(?:button|submit|reset|image|checkbox|radio|range|color)$/i.test(e.getAttribute("type")??""),s=2e4,c=200,h=500,d=(e,t)=>e.length>t?`${e.slice(0,t-1)}…`:e,u=e=>(new TextEncoder).encode(e).length,m=e=>!!e.closest(`${t},${n}`);function f(e,t=1/0){for(let{name:a,value:n}of[...e.attributes])/^on/i.test(a)||"srcdoc"===a||/^data-(palette|komo)-/.test(a)&&!/-no-snapshot$/.test(a)||i.test(a)&&/^\s*(?:javascript|vbscript):/i.test(n)?e.removeAttribute(a):n.length>t&&e.setAttribute(a,d(n,t))}function p(e,t,i=s){let c=e.ownerDocument,h=i,m=e=>{if(3===e.nodeType){let t=d(e.nodeValue??"",500);return h-=t.length,c.createTextNode(t)}if(1!==e.nodeType)return null;let i=e;if(o.has(i.localName)||i.matches(n))return null;let s=i.cloneNode(!1);if(f(s,200),l(i)&&function(e,t){let a=e.value??e.getAttribute("value")??"";"textarea"===e.localName?t.textContent=a?r:"":(a||t.hasAttribute("value"))&&t.setAttribute("value",r)}(i,s),h-=s.outerHTML.length,a(i))return s;if(t?.(i))return l(i)||(s.textContent=r),s;if("textarea"===i.localName)return s;for(let e of i.childNodes){if(h<=0){s.append(c.createComment(" trimmed "));break}let t=m(e);t&&s.append(t)}return s},p=m(e);if(!p)return"";let g=p.outerHTML;if(u(g)<=i)return g;let w=c.createComment(" trimmed ");p.append(w);for(let e=u(p.outerHTML)-i;e>0&&p.firstChild!==w;e=u(p.outerHTML)-i)for(;e>0&&p.firstChild!==w;){let t=w.previousSibling;for(;t.lastChild;)t=t.lastChild;e-=1===t.nodeType?t.outerHTML.length:(t.nodeValue?.length??0)+7,t.parentNode.removeChild(t)}return g=p.outerHTML,u(g)>i?"":g}var g=class extends Error{constructor(e){super(e),this.reason=e}reason},w=25e4,y=e=>new Promise((t,a)=>{let n=new FileReader;n.onload=()=>t(n.result),n.onerror=()=>a(n.error),n.readAsDataURL(e)});async function b(e,t){if(e.startsWith("data:"))return e;let a=new URL(e,t.document.baseURI);try{let e=await fetch(a,{mode:"cors",signal:AbortSignal.timeout(5e3)});return e.ok?await y(await e.blob()):""}catch{if(a.origin!==t.location.origin)throw new g("cross-origin");return""}}function x(e,t,a){let n="";for(let r=0;r<e.length;r++){let o=e[r];if(o.startsWith("--"))continue;let i=e.getPropertyValue(o);(i!==t.get(o)||a&&i!==a.getPropertyValue(o))&&(n+=`${o}:${i};`)}return n}var v=/url\((['"]?)(.*?)\1\)/g;async function $(e,t,i){let s=e.ownerDocument,c=s.defaultView,h=e.getBoundingClientRect(),d=e.offsetWidth??h.width,u=Math.min(e.offsetHeight??h.height,2e3);if(!d||!u)throw new g("failed");let m=function(){let e=document.createElement("iframe");e.setAttribute("data-palette-snapshot-sandbox",""),e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.style.cssText="position:fixed;left:-10px;top:0;width:1px;height:1px;border:0;opacity:0;pointer-events:none",document.documentElement.append(e);let t=e.contentDocument;try{t.open(),t.write("<!doctype html><html><head></head><body></body></html>"),t.close()}catch{}let a=new Map;return{of(e){let n=`${e.namespaceURI} ${e.localName}`,r=a.get(n);if(!r){let o=t.createElementNS(e.namespaceURI,e.localName);(t.body??t.documentElement).append(o);let i=t.defaultView.getComputedStyle(o);r=new Map;for(let e=0;e<i.length;e++)r.set(i[e],i.getPropertyValue(i[e]));o.remove(),a.set(n,r)}return r},dispose:()=>e.remove()}}(),p=[],w=new Set,y=new Map,$=[],M="",C=0,T=e=>(e.catch(()=>{}),e),A=e=>{for(let[,,t]of e.matchAll(v))t&&!t.startsWith("data:")&&!y.has(t)&&y.set(t,T(b(t,c)));return e},E=(e,h)=>{if(3===e.nodeType)return s.createTextNode(e.nodeValue??"");if(1!==e.nodeType)return null;let y=e,v=y.localName;if(o.has(v)||y.matches(n))return null;if(++C>3e3)throw new g("too-large");let I,L=c.getComputedStyle(y);if("none"===L.display)return null;if("canvas"===v||"video"===v){I=s.createElement("img");let e=document.createElement("canvas");e.width=y.width||y.videoWidth||1,e.height=y.height||y.videoHeight||1;try{e.getContext("2d").drawImage(y,0,0,e.width,e.height),I.setAttribute("src",e.toDataURL("image/png"))}catch{throw new g("cross-origin")}}else if("iframe"===v||"embed"===v||"object"===v)I=s.createElement("div");else{I=y.cloneNode(!1),f(I);for(let e of["id","class","srcset","sizes","loading","style"])I.removeAttribute(e)}l(y)&&(I.removeAttribute("value"),I.hasAttribute("placeholder")&&t?.(y)&&I.setAttribute("placeholder",r)),"img"===v&&p.push(T(async function(e,t,a){let n=e.currentSrc||e.src;if(!n||n.startsWith("data:"))return n;await e.decode().catch(()=>{});let r=Math.min(e.naturalWidth,Math.ceil(e.offsetWidth*t)||e.naturalWidth),o=Math.round(r*e.naturalHeight/(e.naturalWidth||1));if(r&&o){let t=document.createElement("canvas");t.width=r,t.height=o;try{return t.getContext("2d").drawImage(e,0,0,r,o),t.toDataURL("image/png")}catch{}}return b(n,a)}(y,i,c).then(e=>I.setAttribute("src",e)))),w.add(L.fontFamily.split(",")[0].replace(/["']/g,"").trim().toLowerCase());let R=x(L,m.of(y),h);h||(R+=`${"inline"===L.display?"display:inline-block;":""}margin:0;position:relative;inset:auto;float:none;transform:none;translate:none;rotate:none;scale:none;flex:none;box-sizing:border-box;width:${d}px;height:${u}px;max-width:none;max-height:none;min-width:0;min-height:0;`);let P=a(y)&&!!h;P&&(R+="color:transparent;background:#e5e5e5;background-image:none;"),$.push([I,A(R)]);for(let e of["::before","::after"]){let t=c.getComputedStyle(y,e);P||/^(?:none|normal)$/.test(t.content)||(I.setAttribute("data-snapshot",String(C)),M+=`[data-snapshot="${C}"]${e}{${A(x(t,m.of(y),L))}content:${t.content};}`)}if(P)return I;if(t?.(y))return l(y)||(I.textContent=r),I;if("textarea"===v)return I;for(let e of y.childNodes){if("picture"===v&&"img"!==e.localName)continue;let t=E(e,L);t&&I.append(t)}return I};try{await(s.fonts?.ready);let t=E(e),a=new Map(await Promise.all([...y].map(async([e,t])=>[e,await t]))),n=e=>e.replace(v,(e,t,n)=>a.has(n)?`url("${a.get(n)}")`:e);for(let[e,t]of $)e.setAttribute("style",n(t));M=n(M),await Promise.all(p);let r=s.createElement("div");r.setAttribute("style",`width:${d+32}px;height:${u+32}px;padding:16px;box-sizing:border-box;overflow:hidden;background:${function(e,t){for(let a=e.parentElement;a;a=a.parentElement){let e=t.getComputedStyle(a).backgroundColor;if(e&&!/^(?:transparent|rgba\(.*,\s*0\))$/.test(e))return e}return"#fff"}(e,c)}`);let o=s.createElement("style");o.textContent=await async function(e,t,a){let n=[],r=2e6,o=async(e,i)=>{for(let l of e){if(l.cssRules&&await o(l.cssRules,i),!l.cssText.startsWith("@font-face"))continue;let e=l.style,s=e.getPropertyValue("font-family").replace(/["']/g,"").trim().toLowerCase(),c=e.getPropertyValue("src").match(/url\((['"]?)(.*?)\1\)/)?.[2];if(!t.has(s)||!c||r<=0)continue;let h=await b(new URL(c,i).href,a).catch(()=>"");h&&(r-=h.length,n.push(`@font-face{font-family:"${s}";src:url("${h}");${["font-weight","font-style","font-stretch","unicode-range"].map(t=>e.getPropertyValue(t)?`${t}:${e.getPropertyValue(t)};`:"").join("")}}`))}};for(let t of e.styleSheets){let a;try{a=t.cssRules}catch{continue}await o(a,t.href??e.baseURI)}return n.join("")}(s,w,c)+M,r.append(o,t);let i={width:d+32,height:u+32},l=`<svg xmlns="http://www.w3.org/2000/svg" width="${i.width}" height="${i.height}"><foreignObject x="0" y="0" width="100%" height="100%">${(new XMLSerializer).serializeToString(r)}</foreignObject></svg>`,h=new Image;return h.src=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(l)}`,await h.decode(),{image:h,...i}}finally{m.dispose()}}var M=(e,t)=>new Promise((a,n)=>{try{e.toBlob(a,"image/webp",t)}catch(e){n(e)}});async function C(e,t={}){let a=t.maxBytes??w,n=e.ownerDocument.defaultView??window,r=e.offsetWidth||e.getBoundingClientRect().width,o=Math.min(n.devicePixelRatio||1,2,1200/(r+32)),i=await $(e,t.redact,o),l=document.createElement("canvas"),s=.8;for(let e=0;e<12&&i.width*o>=48;e++){l.width=Math.max(1,Math.round(i.width*o)),l.height=Math.max(1,Math.round(i.height*o));let e,t=l.getContext("2d");if(!t)throw new g("unsupported");t.drawImage(i.image,0,0,l.width,l.height);try{e=await M(l,s)}catch{throw new g("cross-origin")}if(!e||"image/webp"!==e.type)throw new g("unsupported");if(e.size<=a)return y(e);s>.5?s=Math.round(100*(s-.15))/100:o*=Math.min(.85,Math.max(.3,.95*Math.sqrt(a/e.size)))}throw new g("too-large")}var T={"cross-origin":"the element shows images from another origin","too-large":"the element is too large to draw",unsupported:"this browser can't encode WebP",failed:"it couldn't be drawn"},A=e=>`\x3c!-- palette: no image snapshot, ${T[e]}. --\x3e`;async function E(e,t,a=s){if(!e?.isConnected||m(e))return null;let n={},r=()=>p(e,t.redact,a-120);if(t.html&&(n.html=r()),t.image){await new Promise(e=>"function"==typeof requestIdleCallback?requestIdleCallback(e,{timeout:500}):setTimeout(e));try{n.image=await C(e,t)}catch(e){n.note=e instanceof g?e.reason:"failed","cross-origin"===n.note&&(n.html??=r()),n.html&&(n.html=A(n.note)+n.html)}}return n.image||n.html?n:null}var I=e=>{let[t,a]=e.split(","),n=atob(a),r=new Uint8Array(n.length);for(let e=0;e<n.length;e++)r[e]=n.charCodeAt(e);return new Blob([r],{type:t.slice(5,t.indexOf(";"))})},L=new WeakMap;function R(e,t,a,n,r){let o=L.get(e);if(!o){let r={...a,maxBytes:Math.min(a.maxBytes??w,n.maxImageBytes||1/0)};o=a.image||a.html?E(t(e),r,Math.min(s,n.maxHtmlBytes||1/0)).catch(()=>null):Promise.resolve(null),L.set(e,o)}return r&&o.then(async e=>{for(let t of["image","html"])e?.[t]&&await r(t,"image"===t?I(e.image):new Blob([e.html],{type:"text/html;charset=utf-8"})).catch(e=>console.info(`Palette: the comment posted without its ${t} snapshot.`,e instanceof Error?e.message:e))}),o}export{c as ATTRIBUTE_LIMIT,s as HTML_LIMIT,w as IMAGE_BYTES,e as OPT_OUT,r as PLACEHOLDER,h as TEXT_LIMIT,E as captureSnapshot,A as noteComment,m as skipped,R as snapshotFor,p as snapshotHtml,C as snapshotImage};
1
+ var e="data-palette-no-snapshot",t=`[${e}],[data-komo-no-snapshot]`,a=e=>e.matches(t),n="[data-branch-comments],[data-palette-move-clone],[data-palette-viewport],[data-palette-snapshot-sandbox]",r="[redacted]",o=new Set(["script","noscript","template","style","link","meta","base","title"]),i=/^(?:href|src|action|formaction|xlink:href|poster|data|background)$/i,l=e=>"textarea"===e.localName||"input"===e.localName&&!/^(?:button|submit|reset|image|checkbox|radio|range|color)$/i.test(e.getAttribute("type")??""),s=2e4,c=200,h=500,d=(e,t)=>e.length>t?`${e.slice(0,t-1)}…`:e,u=e=>(new TextEncoder).encode(e).length,m=e=>!!e.closest(`${t},${n}`);function f(e,t=1/0){for(let{name:a,value:n}of[...e.attributes])/^on/i.test(a)||"srcdoc"===a||/^data-(palette|komo)-/.test(a)&&!/-no-snapshot$/.test(a)||i.test(a)&&/^\s*(?:javascript|vbscript):/i.test(n)?e.removeAttribute(a):n.length>t&&e.setAttribute(a,d(n,t))}function p(e,t,i=s){let c=e.ownerDocument,h=i,m=e=>{if(3===e.nodeType){let t=d(e.nodeValue??"",500);return h-=t.length,c.createTextNode(t)}if(1!==e.nodeType)return null;let i=e;if(o.has(i.localName)||i.matches(n))return null;let s=i.cloneNode(!1);if(f(s,200),l(i)&&function(e,t){let a=e.value??e.getAttribute("value")??"";"textarea"===e.localName?t.textContent=a?r:"":(a||t.hasAttribute("value"))&&t.setAttribute("value",r)}(i,s),h-=s.outerHTML.length,a(i))return s;if(t?.(i))return l(i)||(s.textContent=r),s;if("textarea"===i.localName)return s;for(let e of i.childNodes){if(h<=0){s.append(c.createComment(" trimmed "));break}let t=m(e);t&&s.append(t)}return s},p=m(e);if(!p)return"";let g=p.outerHTML;if(u(g)<=i)return g;let w=c.createComment(" trimmed ");p.append(w);for(let e=u(p.outerHTML)-i;e>0&&p.firstChild!==w;e=u(p.outerHTML)-i)for(;e>0&&p.firstChild!==w;){let t=w.previousSibling;for(;t.lastChild;)t=t.lastChild;e-=1===t.nodeType?t.outerHTML.length:(t.nodeValue?.length??0)+7,t.parentNode.removeChild(t)}return g=p.outerHTML,u(g)>i?"":g}var g=class extends Error{constructor(e){super(e),this.reason=e}reason},w=25e4,y=e=>new Promise((t,a)=>{let n=new FileReader;n.onload=()=>t(n.result),n.onerror=()=>a(n.error),n.readAsDataURL(e)});async function b(e,t){if(e.startsWith("data:"))return e;let a=new URL(e,t.document.baseURI);try{let e=await fetch(a,{mode:"cors",signal:AbortSignal.timeout(5e3)});return e.ok?await y(await e.blob()):""}catch{if(a.origin!==t.location.origin)throw new g("cross-origin");return""}}function x(e,t,a){let n="";for(let r=0;r<e.length;r++){let o=e[r];if(o.startsWith("--"))continue;let i=e.getPropertyValue(o);(i!==t.get(o)||a&&i!==a.getPropertyValue(o))&&(n+=`${o}:${i};`)}return n}var v=/url\((['"]?)(.*?)\1\)/g;async function $(e,t,i){let s=e.ownerDocument,c=s.defaultView,h=e.getBoundingClientRect(),d=e.offsetWidth??h.width,u=Math.min(e.offsetHeight??h.height,2e3);if(!d||!u)throw new g("failed");let m=function(){let e=document.createElement("iframe");e.setAttribute("data-palette-snapshot-sandbox",""),e.setAttribute("aria-hidden","true"),e.tabIndex=-1,e.style.cssText="position:fixed;left:-10px;top:0;width:1px;height:1px;border:0;opacity:0;pointer-events:none",document.documentElement.append(e);let t=e.contentDocument;try{t.open(),t.write("<!doctype html><html><head></head><body></body></html>"),t.close()}catch{}let a=new Map;return{of(e){let n=`${e.namespaceURI} ${e.localName}`,r=a.get(n);if(!r){let o=t.createElementNS(e.namespaceURI,e.localName);(t.body??t.documentElement).append(o);let i=t.defaultView.getComputedStyle(o);r=new Map;for(let e=0;e<i.length;e++)r.set(i[e],i.getPropertyValue(i[e]));o.remove(),a.set(n,r)}return r},dispose:()=>e.remove()}}(),p=[],w=new Set,y=new Map,$=[],M="",C=0,T=e=>(e.catch(()=>{}),e),A=e=>{for(let[,,t]of e.matchAll(v))t&&!t.startsWith("data:")&&!y.has(t)&&y.set(t,T(b(t,c)));return e},I=(e,h)=>{if(3===e.nodeType)return s.createTextNode(e.nodeValue??"");if(1!==e.nodeType)return null;let y=e,v=y.localName;if(o.has(v)||y.matches(n))return null;if(++C>3e3)throw new g("too-large");let E,L=c.getComputedStyle(y);if("none"===L.display)return null;if("canvas"===v||"video"===v){E=s.createElement("img");let e=document.createElement("canvas");e.width=y.width||y.videoWidth||1,e.height=y.height||y.videoHeight||1;try{e.getContext("2d").drawImage(y,0,0,e.width,e.height),E.setAttribute("src",e.toDataURL("image/png"))}catch{throw new g("cross-origin")}}else if("iframe"===v||"embed"===v||"object"===v)E=s.createElement("div");else{E=y.cloneNode(!1),f(E);for(let e of["id","class","srcset","sizes","loading","style"])E.removeAttribute(e)}l(y)&&(E.removeAttribute("value"),E.hasAttribute("placeholder")&&t?.(y)&&E.setAttribute("placeholder",r)),"img"===v&&p.push(T(async function(e,t,a){let n=e.currentSrc||e.src;if(!n||n.startsWith("data:"))return n;await e.decode().catch(()=>{});let r=Math.min(e.naturalWidth,Math.ceil(e.offsetWidth*t)||e.naturalWidth),o=Math.round(r*e.naturalHeight/(e.naturalWidth||1));if(r&&o){let t=document.createElement("canvas");t.width=r,t.height=o;try{return t.getContext("2d").drawImage(e,0,0,r,o),t.toDataURL("image/png")}catch{}}return b(n,a)}(y,i,c).then(e=>E.setAttribute("src",e)))),w.add(L.fontFamily.split(",")[0].replace(/["']/g,"").trim().toLowerCase());let R=x(L,m.of(y),h);h||(R+=`${"inline"===L.display?"display:inline-block;":""}margin:0;position:relative;inset:auto;float:none;transform:none;translate:none;rotate:none;scale:none;flex:none;box-sizing:border-box;width:${d}px;height:${u}px;max-width:none;max-height:none;min-width:0;min-height:0;`);let k=a(y)&&!!h;k&&(R+="color:transparent;background:#e5e5e5;background-image:none;"),$.push([E,A(R)]);for(let e of["::before","::after"]){let t=c.getComputedStyle(y,e);k||/^(?:none|normal)$/.test(t.content)||(E.setAttribute("data-snapshot",String(C)),M+=`[data-snapshot="${C}"]${e}{${A(x(t,m.of(y),L))}content:${t.content};}`)}if(k)return E;if(t?.(y))return l(y)||(E.textContent=r),E;if("textarea"===v)return E;for(let e of y.childNodes){if("picture"===v&&"img"!==e.localName)continue;let t=I(e,L);t&&E.append(t)}return E};try{await(s.fonts?.ready);let t=I(e),a=new Map(await Promise.all([...y].map(async([e,t])=>[e,await t]))),n=e=>e.replace(v,(e,t,n)=>a.has(n)?`url("${a.get(n)}")`:e);for(let[e,t]of $)e.setAttribute("style",n(t));M=n(M),await Promise.all(p);let r=s.createElement("div");r.setAttribute("style",`width:${d+32}px;height:${u+32}px;padding:16px;box-sizing:border-box;overflow:hidden;background:${function(e,t){for(let a=e.parentElement;a;a=a.parentElement){let e=t.getComputedStyle(a).backgroundColor;if(e&&!/^(?:transparent|rgba\(.*,\s*0\))$/.test(e))return e}return"#fff"}(e,c)}`);let o=s.createElement("style");o.textContent=await async function(e,t,a){let n=[],r=2e6,o=async(e,i)=>{for(let l of e){if(l.cssRules&&await o(l.cssRules,i),!l.cssText.startsWith("@font-face"))continue;let e=l.style,s=e.getPropertyValue("font-family").replace(/["']/g,"").trim().toLowerCase(),c=e.getPropertyValue("src").match(/url\((['"]?)(.*?)\1\)/)?.[2];if(!t.has(s)||!c||r<=0)continue;let h=await b(new URL(c,i).href,a).catch(()=>"");h&&(r-=h.length,n.push(`@font-face{font-family:"${s}";src:url("${h}");${["font-weight","font-style","font-stretch","unicode-range"].map(t=>e.getPropertyValue(t)?`${t}:${e.getPropertyValue(t)};`:"").join("")}}`))}};for(let t of e.styleSheets){let a;try{a=t.cssRules}catch{continue}await o(a,t.href??e.baseURI)}return n.join("")}(s,w,c)+M,r.append(o,t);let i={width:d+32,height:u+32},l=`<svg xmlns="http://www.w3.org/2000/svg" width="${i.width}" height="${i.height}"><foreignObject x="0" y="0" width="100%" height="100%">${(new XMLSerializer).serializeToString(r)}</foreignObject></svg>`,h=new Image;return h.src=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(l)}`,await h.decode(),{image:h,...i}}finally{m.dispose()}}var M=(e,t)=>new Promise((a,n)=>{try{e.toBlob(a,"image/webp",t)}catch(e){n(e)}});async function C(e,t={}){let a=t.maxBytes??w,n=e.ownerDocument.defaultView??window,r=e.offsetWidth||e.getBoundingClientRect().width,o=Math.min(n.devicePixelRatio||1,2,1200/(r+32)),i=await $(e,t.redact,o),l=document.createElement("canvas"),s=.8;for(let e=0;e<12&&i.width*o>=48;e++){l.width=Math.max(1,Math.round(i.width*o)),l.height=Math.max(1,Math.round(i.height*o));let e,t=l.getContext("2d");if(!t)throw new g("unsupported");t.drawImage(i.image,0,0,l.width,l.height);try{e=await M(l,s)}catch{throw new g("cross-origin")}if(!e||"image/webp"!==e.type)throw new g("unsupported");if(e.size<=a)return y(e);s>.5?s=Math.round(100*(s-.15))/100:o*=Math.min(.85,Math.max(.3,.95*Math.sqrt(a/e.size)))}throw new g("too-large")}var T={"cross-origin":"cross-origin images","too-large":"the element is too large to draw",unsupported:"this browser can't encode WebP",failed:"it couldn't be drawn"},A=e=>`\x3c!-- palette: no image snapshot, ${T[e]}. --\x3e`;async function I(e,t,a=s){if(!e?.isConnected||m(e))return null;let n={},r=()=>p(e,t.redact,a-120);if(t.html&&(n.html=r()),t.image){await new Promise(e=>"function"==typeof requestIdleCallback?requestIdleCallback(e,{timeout:500}):setTimeout(e));try{n.image=await C(e,t)}catch(e){n.note=e instanceof g?e.reason:"failed","cross-origin"===n.note&&(n.html??=r()),n.html&&(n.html=A(n.note)+n.html)}}return n}var E=e=>{let[t,a]=e.split(","),n=Uint8Array.from(atob(a),e=>e.charCodeAt(0));return new Blob([n],{type:t.slice(5,-7)})},L=new WeakMap;function R(e,t,a,n,r){let o=L.get(e);if(!o){let r={...a,maxBytes:Math.min(a.maxBytes??w,n.maxImageBytes||1/0)};o=a.image||a.html?I(t(e),r,Math.min(s,n.maxHtmlBytes||1/0)).catch(()=>({note:"failed"})):Promise.resolve(null),L.set(e,o)}return r&&o.then(async e=>{let t="failed"===e?.note;for(let a of["image","html"]){let n=e?.[a];n&&await r(a,"image"===a?E(n):new Blob([n],{type:"text/html;charset=utf-8"})).catch(()=>t=!0)}if(n.readiness&&(t||!e?.image&&!e?.html))return r("status",new Blob([`{"status":"${t?"failed":"skipped"}"}`],{type:"application/json"})).catch(()=>{})}),o}export{c as ATTRIBUTE_LIMIT,s as HTML_LIMIT,w as IMAGE_BYTES,e as OPT_OUT,r as PLACEHOLDER,h as TEXT_LIMIT,I as captureSnapshot,A as noteComment,m as skipped,R as snapshotFor,p as snapshotHtml,C as snapshotImage};
@@ -28,6 +28,7 @@ export declare const IMAGE_BYTES = 250000;
28
28
  export declare function snapshotImage(element: Element, options?: PaletteSnapshots): Promise<string>;
29
29
  /** The server's snapshot support, from `/config`. */
30
30
  export interface SnapshotLimits {
31
+ readiness?: boolean;
31
32
  /** Whether guest sessions may attach snapshots. */
32
33
  guests: boolean;
33
34
  maxImageBytes: number;
@@ -44,6 +45,6 @@ export declare function captureSnapshot(element: Element | null, options: Palett
44
45
  /**
45
46
  * The snapshot of a draft's target, captured once per anchor: the composer reads it
46
47
  * for a preview, and posting passes `post` to upload each kind as its raw file after
47
- * the thread is saved. Never rejects; a failed upload is logged and the comment stays posted.
48
+ * the thread is saved. Never rejects; failed uploads are reported when supported and the comment stays posted.
48
49
  */
49
- export declare function snapshotFor<Anchor extends object>(anchor: Anchor, resolve: (anchor: Anchor) => Element | null, options: PaletteSnapshots, limits: SnapshotLimits, post?: (kind: "image" | "html", body: Blob) => Promise<unknown>): Promise<CapturedSnapshot | null>;
50
+ export declare function snapshotFor<Anchor extends object>(anchor: Anchor, resolve: (anchor: Anchor) => Element | null, options: PaletteSnapshots, limits: SnapshotLimits, post?: (kind: "image" | "html" | "status", body: Blob) => Promise<unknown>): Promise<CapturedSnapshot | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hazem3500/palette",
3
- "version": "0.9.111",
3
+ "version": "0.9.112",
4
4
  "description": "Point, area, and threaded comments for website branches.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/server/index.ts CHANGED
@@ -20,7 +20,8 @@ import { authOptions, type ProxyAuthProvider, type ProxyIdentity } from "./proxy
20
20
  import { paletteEnv } from "./public-env";
21
21
  import setupClient from "./setup-client.txt";
22
22
  import {
23
- dispatchWebhook,
23
+ enqueueWebhook,
24
+ drainWebhooks,
24
25
  type WebhookEvent,
25
26
  } from "./webhooks";
26
27
  import {
@@ -202,14 +203,16 @@ async function body(request: Request): Promise<Record<string, unknown>> {
202
203
  : path === "/me"
203
204
  ? ["name", "avatarUrl", "accentColor"]
204
205
  : path === "/threads"
205
- ? ["body", "page", "anchor", "tags", "scope"]
206
+ ? ["body", "page", "anchor", "tags", "scope", "snapshotIntent"]
206
207
  : path.endsWith("/comments")
207
208
  ? ["body"]
208
209
  : path.includes("/comments/")
209
210
  ? ["body"]
210
- : path.startsWith("/threads/")
211
- ? ["anchor", "resolved", "tag", "untag", "scope"]
212
- : [];
211
+ : path.endsWith("/snapshot")
212
+ ? ["status"]
213
+ : path.startsWith("/threads/")
214
+ ? ["anchor", "resolved", "tag", "untag", "scope"]
215
+ : [];
213
216
  check(
214
217
  Object.keys(result).every((key) => allowed.includes(key)),
215
218
  400,
@@ -1149,6 +1152,7 @@ async function route(
1149
1152
  tokens: !auth.proxy || auth.proxyAccessTokens,
1150
1153
  // Element snapshots are opt-in on the client; guests need project consent.
1151
1154
  snapshots: {
1155
+ readiness: true,
1152
1156
  guests: auth.allowGuests && config.allowGuestSnapshots === true,
1153
1157
  maxImageBytes: SNAPSHOT_LIMITS.image,
1154
1158
  maxHtmlBytes: SNAPSHOT_LIMITS.html,
@@ -1537,6 +1541,8 @@ async function route(
1537
1541
  }
1538
1542
  const id = crypto.randomUUID();
1539
1543
  const firstCommentId = crypto.randomUUID();
1544
+ check(data.snapshotIntent === undefined || typeof data.snapshotIntent === "boolean", 400, "Invalid snapshot intent.");
1545
+ const snapshotStatus = data.snapshotIntent === undefined ? null : data.snapshotIntent ? "pending" : "skipped";
1540
1546
  await env.DB.batch([
1541
1547
  // Page-only threads rely on the column default, as before the scope column existed.
1542
1548
  scope === "all"
@@ -1549,18 +1555,8 @@ async function route(
1549
1555
  env.DB.prepare(
1550
1556
  "INSERT INTO comments(id,thread_id,user_id,body,created_at) VALUES(?,?,?,?,?)"
1551
1557
  ).bind(firstCommentId, id, user.id, text, now),
1552
- ...retainReviewerComments(
1553
- env.DB,
1554
- project,
1555
- user.id,
1556
- config.retainedCommentsPerUser
1557
- ),
1558
- ]);
1559
- ctx.waitUntil(
1560
- dispatchWebhook(
1561
- env,
1562
- project,
1563
- webhookEvent(
1558
+ env.DB.prepare("UPDATE threads SET snapshot_status=? WHERE id=?").bind(snapshotStatus, id),
1559
+ enqueueWebhook(env.DB, project, { ...webhookEvent(
1564
1560
  "comment.created",
1565
1561
  project,
1566
1562
  repo,
@@ -1571,9 +1567,15 @@ async function route(
1571
1567
  user.name,
1572
1568
  text,
1573
1569
  origin || url.origin
1574
- )
1575
- ).catch(() => {})
1576
- );
1570
+ ), ...(snapshotStatus ? { snapshotStatus } : {}) }, firstCommentId),
1571
+ ...retainReviewerComments(
1572
+ env.DB,
1573
+ project,
1574
+ user.id,
1575
+ config.retainedCommentsPerUser
1576
+ ),
1577
+ ]);
1578
+ ctx.waitUntil(drainWebhooks(env));
1577
1579
  return json({ id, commentId: firstCommentId }, 201);
1578
1580
  }
1579
1581
  const snapshotRoute = url.pathname.match(
@@ -1582,12 +1584,18 @@ async function route(
1582
1584
  if (snapshotRoute) {
1583
1585
  const [, threadId, pathKind] = snapshotRoute;
1584
1586
  const thread = await env.DB.prepare(
1585
- "SELECT id FROM threads WHERE id=? AND project=? AND repo=? AND branch=?"
1587
+ "SELECT id,snapshot_status,created_at FROM threads WHERE id=? AND project=? AND repo=? AND branch=?"
1586
1588
  )
1587
1589
  .bind(threadId, project, repo, branch)
1588
- .first<{ id: string }>();
1590
+ .first<{ id: string; snapshot_status: string | null; created_at: number }>();
1589
1591
  check(thread, 404, "Thread not found on this branch.");
1590
1592
  if (request.method === "GET") {
1593
+ if (!pathKind) {
1594
+ const rows = await env.DB.prepare("SELECT kind FROM snapshots WHERE thread_id=?").bind(threadId).all<{kind:string}>();
1595
+ let status = rows.results.length ? "ready" : thread.snapshot_status ?? "unknown";
1596
+ if (status === "pending" && now - thread.created_at > 600000) status = "failed";
1597
+ return json({ status, kinds: rows.results.map(row => row.kind) });
1598
+ }
1591
1599
  const kind = snapshotKind(pathKind);
1592
1600
  const row = await env.DB.prepare(
1593
1601
  "SELECT mime,bytes FROM snapshots WHERE thread_id=? AND kind=?"
@@ -1635,13 +1643,20 @@ async function route(
1635
1643
  threadId,
1636
1644
  kind
1637
1645
  ),
1638
- env.DB.prepare("UPDATE threads SET updated_at=? WHERE id=?").bind(
1646
+ env.DB.prepare("UPDATE threads SET snapshot_status=CASE WHEN EXISTS (SELECT 1 FROM snapshots WHERE thread_id=?) THEN 'ready' ELSE 'skipped' END,updated_at=? WHERE id=?").bind(
1647
+ threadId,
1639
1648
  now,
1640
1649
  threadId
1641
1650
  ),
1642
1651
  ]);
1643
1652
  return json({ ok: true, deleted: deleted.meta.changes });
1644
1653
  }
1654
+ if (!pathKind && request.headers.get("Content-Type")?.startsWith("application/json")) {
1655
+ const data = await body(request);
1656
+ check(data.status === "skipped" || data.status === "failed", 400, "Report skipped or failed capture.");
1657
+ await env.DB.prepare("UPDATE threads SET snapshot_status=?,updated_at=? WHERE id=? AND snapshot_status='pending'").bind(data.status, now, threadId).run();
1658
+ return json({ ok: true });
1659
+ }
1645
1660
  await limit(env, `${project}:snapshot:${user.id}`, 60, 600);
1646
1661
  const snapshot = await readSnapshot(
1647
1662
  request,
@@ -1657,8 +1672,9 @@ async function route(
1657
1672
  snapshotCap(config.maxSnapshotBytes),
1658
1673
  true
1659
1674
  ),
1660
- env.DB.prepare("UPDATE threads SET updated_at=? WHERE id=?").bind(
1675
+ env.DB.prepare("UPDATE threads SET snapshot_status='ready',updated_at=? WHERE id=? AND EXISTS (SELECT 1 FROM snapshots WHERE thread_id=?)").bind(
1661
1676
  now,
1677
+ threadId,
1662
1678
  threadId
1663
1679
  ),
1664
1680
  ]);
@@ -1774,35 +1790,20 @@ async function route(
1774
1790
  );
1775
1791
  check(typeof data.resolved === "boolean", 400, "Invalid resolved state.");
1776
1792
  const becameResolved = !thread.resolved && data.resolved;
1777
- await env.DB.prepare(
1778
- "UPDATE threads SET resolved=?,resolved_by=?,updated_at=? WHERE id=?"
1779
- )
1780
- .bind(
1781
- Number(data.resolved),
1782
- data.resolved ? user.id : null,
1783
- now,
1784
- threadId
1785
- )
1786
- .run();
1787
- if (becameResolved)
1788
- ctx.waitUntil(
1789
- dispatchWebhook(
1790
- env,
1791
- project,
1792
- webhookEvent(
1793
- "comment.resolved",
1794
- project,
1795
- repo,
1796
- branch,
1797
- threadId,
1798
- thread.page,
1799
- thread.anchor,
1800
- user.name,
1801
- null,
1802
- origin || url.origin
1803
- )
1804
- ).catch(() => {})
1805
- );
1793
+ const firstComment = becameResolved
1794
+ ? await env.DB.prepare("SELECT id FROM comments WHERE thread_id=? ORDER BY created_at,id LIMIT 1")
1795
+ .bind(threadId).first<{ id: string }>()
1796
+ : null;
1797
+ await env.DB.batch([
1798
+ env.DB.prepare(
1799
+ "UPDATE threads SET resolved=?,resolved_by=?,updated_at=? WHERE id=?"
1800
+ ).bind(Number(data.resolved), data.resolved ? user.id : null, now, threadId),
1801
+ ...(firstComment ? [enqueueWebhook(env.DB, project, webhookEvent(
1802
+ "comment.resolved", project, repo, branch, threadId, thread.page,
1803
+ thread.anchor, user.name, null, origin || url.origin
1804
+ ), firstComment.id)] : []),
1805
+ ]);
1806
+ ctx.waitUntil(drainWebhooks(env));
1806
1807
  return json({ ok: true });
1807
1808
  }
1808
1809
  if (
@@ -1820,6 +1821,18 @@ async function route(
1820
1821
  now,
1821
1822
  threadId
1822
1823
  ),
1824
+ enqueueWebhook(env.DB, project, webhookEvent(
1825
+ "comment.replied",
1826
+ project,
1827
+ repo,
1828
+ branch,
1829
+ threadId,
1830
+ thread.page,
1831
+ thread.anchor,
1832
+ user.name,
1833
+ text,
1834
+ origin || url.origin
1835
+ ), replyId),
1823
1836
  ...retainReviewerComments(
1824
1837
  env.DB,
1825
1838
  project,
@@ -1832,24 +1845,7 @@ async function route(
1832
1845
  409,
1833
1846
  "This thread has reached 200 comments. Start another thread."
1834
1847
  );
1835
- ctx.waitUntil(
1836
- dispatchWebhook(
1837
- env,
1838
- project,
1839
- webhookEvent(
1840
- "comment.replied",
1841
- project,
1842
- repo,
1843
- branch,
1844
- threadId,
1845
- thread.page,
1846
- thread.anchor,
1847
- user.name,
1848
- text,
1849
- origin || url.origin
1850
- )
1851
- ).catch(() => {})
1852
- );
1848
+ ctx.waitUntil(drainWebhooks(env));
1853
1849
  return json({ ok: true, id: replyId }, 201);
1854
1850
  }
1855
1851
  check(commentId, 405, "Method not allowed.");
@@ -1954,6 +1950,7 @@ const settingsFor = new WeakMap<Env, Env>();
1954
1950
  export default {
1955
1951
  async scheduled(_event, env, ctx) {
1956
1952
  ctx.waitUntil(maintain(env.DB, true));
1953
+ ctx.waitUntil(drainWebhooks(paletteEnv(env)));
1957
1954
  },
1958
1955
  async fetch(request, settings, ctx) {
1959
1956
  // Deployments configured before the rename set KOMO_ names. One copy per
@@ -2044,3 +2041,5 @@ export default {
2044
2041
  return new Response(response.body, { status: response.status, headers });
2045
2042
  },
2046
2043
  } satisfies ExportedHandler<Env>;
2044
+
2045
+ export { drainWebhooks } from "./webhooks";
@@ -0,0 +1,14 @@
1
+ CREATE TABLE webhook_outbox (
2
+ id TEXT PRIMARY KEY,
3
+ project TEXT NOT NULL,
4
+ body TEXT NOT NULL,
5
+ url TEXT NOT NULL,
6
+ secret TEXT NOT NULL,
7
+ attempts INTEGER NOT NULL DEFAULT 0,
8
+ next_attempt BIGINT NOT NULL,
9
+ claim_until BIGINT NOT NULL DEFAULT 0,
10
+ claim_token TEXT NOT NULL DEFAULT '',
11
+ state TEXT NOT NULL DEFAULT 'pending'
12
+ );
13
+ CREATE INDEX webhook_outbox_due ON webhook_outbox(state,next_attempt,claim_until);
14
+ ALTER TABLE threads ADD COLUMN snapshot_status TEXT;
@@ -101,6 +101,11 @@ export async function sendWebhookPinned(
101
101
  lookup: pinnedLookup(address),
102
102
  },
103
103
  (response) => {
104
+ if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
105
+ response.destroy();
106
+ finish(new Error("Webhook returned a non-success status."));
107
+ return;
108
+ }
104
109
  // Read and discard a bounded amount. Never follow redirects or log bodies.
105
110
  let read = 0;
106
111
  response.on("data", (chunk: Buffer) => {