docs: a README with a face, in English and Portuguese; decode every HTML entity in previews

The README opens like a product page: the mark, a line on what it is, the
links, the language switch and a picture of the real interface running on an
invented mailbox, in dark and light and in both languages. scripts/screenshots
holds that invented mailbox and the script that photographs it, so the pictures
can be redone. Previews now decode any HTML entity through the browser rather
than a short list, so a Portuguese or marketing preview no longer prints
ç or ·.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-23 07:55:12 -03:00
1 parent 8226c76d7a
commit 1e06491019
16 files changed
+679 -5

No files matched your search

+14
View File
@@ -16,7 +16,21 @@ const NAMED: Record<string, string> = {
nbsp: " ",
};
/**
* Turns `&aacute;`, `&mdash;`, `&#39;` and the rest into the characters they
* stand for. The browser already knows all of them (HTML defines over two
* thousand), so it is asked rather than given a short list: a list stops at
* `&amp;` and leaves `&ccedil;` and `&middot;` printed in a Portuguese or a
* marketing preview. A textarea holds text only, so nothing in it runs.
*/
function decodeEntities(text: string): string {
if (!text.includes("&")) return text;
if (typeof document !== "undefined") {
const holder = document.createElement("textarea");
holder.innerHTML = text;
return holder.value;
}
// No DOM (rendered on the server): the few that matter most.
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (whole, body: string) => {
if (body[0] === "#") {
const code = body[1].toLowerCase() === "x" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
+12
View File
@@ -26,4 +26,16 @@ describe("plainSnippet", () => {
it("keeps an entity it does not know", () => {
expect(plainSnippet("a &madeup; b")).toBe("a &madeup; b");
});
// The preview of a Portuguese or a marketing message is full of these, and a
// short list of entities printed them as text.
it("decodes any named entity, not only the common few", () => {
expect(plainSnippet("Atualiza&ccedil;&atilde;o &mdash; n&uacute;mero 3 &rarr; ok &middot; fim")).toBe("Atualiza\u00e7\u00e3o \u2014 n\u00famero 3 \u2192 ok \u00b7 fim");
expect(plainSnippet("Caf&eacute; &amp; cr&egrave;me &hellip;")).toBe("Caf\u00e9 & cr\u00e8me \u2026");
});
it("does not run markup that an entity decodes into", () => {
expect(plainSnippet("&lt;img src=x onerror=alert(1)&gt; safe")).toBe("<img src=x onerror=alert(1)> safe");
expect(document.querySelector("img")).toBeNull();
});
});