did canvas text whatever and changed UI, but thils will probably be overwritten#315
did canvas text whatever and changed UI, but thils will probably be overwritten#315
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new HTML text-obfuscation pipeline that replaces text nodes with encoded markers and renders text via canvas on the client, alongside some UI styling tweaks and missing TypeScript module declarations.
Changes:
- Add
transformTextInHtml()+ a client-side canvas renderer script, wired into the HTML response transformation pipeline. - Extend obfuscation maps with a per-run
textKeyused for encoding/decoding. - Update UI typography/layout styles and add
.d.tsshims for MercuryWorkshop modules.
Reviewed changes
Copilot reviewed 8 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/text-canvas.ts | New HTML transformer + injected client script that draws decoded text to canvas. |
| src/lib/obfuscate.ts | Adds textKey to ObfuscationMaps and initializes it in generateMaps(). |
| index.ts | Injects the text-canvas client script and applies HTML text transform in the middleware. |
| package.json | Adds node-html-parser dependency for server-side HTML parsing. |
| bun.lock | Lockfile updates for the new dependency and override changes. |
| src/types/wisp.d.ts | Adds a module declaration shim for @mercuryworkshop/wisp-js/server. |
| src/types/epoxy.d.ts | Adds a module declaration shim for @mercuryworkshop/epoxy-transport. |
| src/pages/index.astro | Adjusts landing page typography and search input sizing. |
| src/pages/settings.astro | Minor typography tweak for the Settings heading. |
| src/layouts/Main.astro | Updates nav layout/spacing/typography and menu sizing. |
| src/global.css | Expands imported Inter font weights. |
Comments suppressed due to low confidence (1)
src/lib/text-canvas.ts:287
- The code attaches a MutationObserver to the entire document body (subtree + characterData + attributes) and also a ResizeObserver to many elements, triggering canvas re-renders. This is likely to be expensive on pages with lots of DOM updates/resize events. Consider narrowing the observation scope, debouncing/throttling redraws, and/or using requestAnimationFrame batching to avoid repeated synchronous reflows and canvas work.
var par=n.parentNode;
if(par&&par.nodeType===1){
var tag=par.tagName&&par.tagName.toLowerCase();
if(tag&&SKIP.indexOf(tag)===-1&&!par.hasAttribute("data-t")){
var text=n.nodeValue;
var sp=document.createElement("span");
sp.setAttribute("data-t-live","1");
par.replaceChild(sp,n);
draw(sp,text);
ro.observe(par);
}
}
}
}
}else if(m.type==="characterData"){
var tn=m.target;
if(tn&&tn.nodeType===3){
var par2=tn.parentNode;
if(par2&&par2.nodeType===1){
var tag2=par2.tagName&&par2.tagName.toLowerCase();
if(tag2&&SKIP.indexOf(tag2)===-1&&!par2.hasAttribute("data-t")&&/\\S/.test(tn.nodeValue)){
var text2=tn.nodeValue;
var sp2=document.createElement("span");
sp2.setAttribute("data-t-live","1");
par2.replaceChild(sp2,tn);
draw(sp2,text2);
ro.observe(par2);
}
}
}
}else if(m.type==="attributes"){
var t=m.target;
if(m.attributeName==="data-t")renderSpan(t);
else if(m.attributeName==="data-rph")renderPh(t);
else if(m.attributeName==="value"&&t.hasAttribute&&t.hasAttribute("data-rph")&&t.__phtoggle)t.__phtoggle();
}
}
});
mo.observe(document.body,{childList:true,subtree:true,characterData:true,attributes:true,attributeFilter:["data-t","data-rph","value"]});
});
});
window.__dt=function(el,str){
if(!el||el.nodeType!==1)return;
var sp=document.createElement("span");
sp.setAttribute("data-t-live","1");
while(el.firstChild)el.removeChild(el.firstChild);
el.appendChild(sp);
draw(sp,str);
};
})();`;
return `<script>${script}</script>`;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| doctype = m; | ||
| return ""; | ||
| }); | ||
| const root = parse(stripped, { comment: true }); | ||
| walk(root as unknown as Node, key); | ||
| return doctype + root.toString(); | ||
| } |
There was a problem hiding this comment.
aria-label and alt are cleared and only preserved in data-r-*, but the client script never restores these attributes. This will break accessibility (buttons/icons without labels) and image alternative text. Either do not clear these attributes, or add client-side decoding/restoration for data-r-aria-label and data-r-alt.
| ctx.clearRect(0,0,c.width,c.height); | ||
| ctx.font=f; | ||
| ctx.fillStyle=color; | ||
| ctx.textBaseline="alphabetic"; | ||
| if(!ls){ctx.fillText(disp,0,baseline)} |
There was a problem hiding this comment.
The canvas-rendered text is forced to white-space: pre and rendered as a single inline-block canvas sized to the full measured string. This prevents natural line wrapping, so longer text (paragraphs, headings, menu items, etc.) will overflow instead of wrapping. If this feature needs to support general text, it should implement wrapping (split into words/lines) or avoid applying to multi-word/long text nodes.
| }else if(m.type==="characterData"){ | ||
| var tn=m.target; | ||
| if(tn&&tn.nodeType===3){ | ||
| var par2=tn.parentNode; | ||
| if(par2&&par2.nodeType===1){ | ||
| var tag2=par2.tagName&&par2.tagName.toLowerCase(); | ||
| if(tag2&&SKIP.indexOf(tag2)===-1&&!par2.hasAttribute("data-t")&&/\\S/.test(tn.nodeValue)){ | ||
| var text2=tn.nodeValue; | ||
| var sp2=document.createElement("span"); | ||
| sp2.setAttribute("data-t-live","1"); | ||
| par2.replaceChild(sp2,tn); | ||
| draw(sp2,text2); |
There was a problem hiding this comment.
Live text nodes are replaced with spans marked data-t-live, but the ResizeObserver callback only re-renders elements with data-t or descendants with [data-t]. data-t-live spans won’t be redrawn on resize/font changes, so they can become visually stale. Consider encoding live spans with data-t as well, or teaching the resize/font hooks how to re-render data-t-live nodes.
| if (tag === "meta") { | ||
| const name = (el.getAttribute("name") || "").toLowerCase(); | ||
| const prop = (el.getAttribute("property") || "").toLowerCase(); | ||
| const match = name === "description" || name === "twitter:description" || name === "twitter:title" || prop === "og:description" || prop === "og:title" || prop === "og:site_name"; | ||
| if (match) { | ||
| const content = el.getAttribute("content") || ""; | ||
| if (content && !isWhitespace(content)) { |
There was a problem hiding this comment.
The transform clears the <title> text (set_content("")) and only stores an encoded copy in data-rt, but the client script never decodes/restores data-rt. This will leave document titles blank for all pages. Either avoid modifying <title> at transform time, or extend the client script to decode data-rt and restore document.title on load and when it changes.
| } | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (SKIP_TAGS.has(tag)) return; | ||
|
|
||
| const ph = el.getAttribute("placeholder"); | ||
| if (ph && !isWhitespace(ph)) { | ||
| el.setAttribute("data-rph", encodeXor(ph, key)); | ||
| el.removeAttribute("placeholder"); | ||
| } | ||
| for (const k of ATTR_KEYS) { | ||
| const v = el.getAttribute(k); | ||
| if (v && !isWhitespace(v)) { | ||
| el.setAttribute(`data-r-${k}`, encodeXor(v, key)); | ||
| el.setAttribute(k, ""); | ||
| } |
There was a problem hiding this comment.
Meta tags (description / og:* / twitter:*) have their content attribute blanked and moved to data-rc, but the client script does not restore data-rc back into the content attribute. This will break SEO/snippets and social previews. Either keep meta content intact server-side or add client-side restoration for the affected meta tags.
| } | ||
|
|
||
| export function transformTextInHtml(html: string, key: number): string { | ||
| let doctype = ""; | ||
| const stripped = html.replace(/^\s*<!DOCTYPE[^>]*>\s*/i, (m) => { |
There was a problem hiding this comment.
This removes the native placeholder attribute and replaces it with a custom overlay, but the client script does not provide an equivalent accessibility story (e.g., restoring placeholder/aria-label or ensuring assistive tech can read it). Consider keeping placeholder intact and only visually obfuscating it, or explicitly restoring placeholder/adding an accessible label after decoding.
did canvas text whatever and changed UI, but thils will probably be overwritten