Een dating-app op basis van brieven, niet van gezichten!

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

8.1 kB Ruw bestand
import { esc, LICHT, DONKER } from "./huisstijl.js";

/*
 * Het briefpapier van de post, en de opmaak die erop mag.
 *
 * Elke brief die deze app verstuurt gaat in twee versies weg (zie server/mail.js): de tekst is de
 * hoofdzaak — dit is post van een mens — en de HTML is dezelfde woorden op papier. Dat papier stond
 * eerst in server/promotie.js en nergens anders; de herstelmail bij "wachtwoord vergeten" ging als
 * kale <p>'s de deur uit en de welkomstmail had helemaal geen HTML-versie. Vandaar dit bestand: één
 * vel, één opmaak, alle brieven.
 *
 * Het is hetzelfde vel als de pagina's (server/huisstijl.js): luchtpostrand boven, wit vel op
 * papierkleur, hetzelfde palet. Drie dingen gaan anders dan daar, en niet uit luiheid:
 *
 *   * Alles staat inline in de stijl-attributen. Mailclients gooien een <style>-blok weg, en een
 *     CSS-variabele kent geen van allen.
 *   * Systeemletters. Een client die /lettertype/… van deze server ophaalt, vertelt daarmee wanneer
 *     en vanaf welk netwerk de brief geopend werd; dat is een leesbevestiging en die hoort er niet
 *     stilletjes in te zitten. Een schreefletter voor de brieftekst, want dat is wat de app met een
 *     brief ook doet, en een schreefloze voor het kleine grut eronder.
 *   * Geen enkel plaatje. Om dezelfde reden, en omdat dit post hoort te blijven.
 *
 * De donkere modus is het enige stuk dat wél een <style>-blok nodig heeft: alleen zo is er een
 * `prefers-color-scheme` te stellen. Apple Mail en iOS doen daar iets mee, Gmail niet — die keert de
 * boel zelf om. Beide gaan goed zolang kleur en achtergrond overal als paar gezet zijn, en dat is
 * hier het geval: wie omkeert draait allebei om en houdt het contrast dus intact.
 *
 * De opmaak is markdown, en met opzet een kleine: **vet**, *cursief*, [tekst](url), een opsomming
 * met streepjes, en kale URL's die vanzelf een link worden. Meer niet — koppen en citaten maken er
 * een nieuwsbrief van. Wat je typt blijft leesbaar zonder opmaak, want de tekstversie krijgt
 * precies de tekens die je intypte.
 */

/* ---------- Markdown ---------- */

/*
 * Waar een link heen mag wijzen: het web, de post, en Postmarks eigen {{{ pm:unsubscribe }}} (die
 * vullen zij bij het versturen in). Wat daar niet op lijkt — javascript:, data: — blijft gewoon
 * tekst staan in plaats van een link te worden.
 */
const VEILIG = /^(?:https?:\/\/|mailto:|\{\{\{)/i;

const NADRUK = /\*\*([^*\n]+)\*\*|\*([^*\n]+)\*/g;
const nadruk = (s) => s.replace(NADRUK, (heel, vet, cursief) =>
  vet ? `<strong>${vet}</strong>` : `<em>${cursief}</em>`);

/*
 * Wat er in een regel gevonden moet worden vóór de nadruk erop losgelaten wordt: een teken dat met
 * een backslash letterlijk is gemaakt, een link met een label, of een kale URL. In die volgorde —
 * anders eet de kale URL de binnenkant van een link op. En in één doorloop, want een sterretje in
 * een webadres hoort geen cursief te beginnen.
 */
const INLINE = /\\([[\]*\\])|\[([^\]\n]+)\]\(([^)\n]+)\)|(https?:\/\/[^\s<]+)/g;

/*
 * Wat aan het eind van een kale URL zit maar er niet bij hoort: een punt, een sluithaakje, een
 * aanhalingsteken dat na het ontsnappen &quot; heet.
 */
const STAARTJE = /(?:&quot;|[.,;:!?)])+$/;

const anker = (url, inhoud) => `<a href="${url}" class="b-web" style="color:${LICHT.accent}">${inhoud}</a>`;

const inline = (regel) => {
  let uit = "", tot = 0;
  for (const m of regel.matchAll(INLINE)) {
    const [heel, letterlijkTeken, label, url, kaal] = m;
    uit += nadruk(regel.slice(tot, m.index));
    tot = m.index + heel.length;

    if (letterlijkTeken) uit += letterlijkTeken;
    else if (kaal) {
      const staart = STAARTJE.exec(kaal)?.[0] ?? "";
      const schoon = kaal.slice(0, kaal.length - staart.length);
      uit += anker(schoon, schoon) + staart;
    } else if (VEILIG.test(url.trim())) uit += anker(url.trim(), nadruk(label));
    /* Wijst hij ergens heen waar wij niet heen linken, dan blijft er staan wat er getypt is. Niet
       alleen het label: er is dan niets weggemoffeld, en dat is te zien in het voorbeeld. */
    else uit += heel;
  }
  return uit + nadruk(regel.slice(tot));
};

/*
 * Een naam die ergens ingevuld wordt, is niet van de schrijver van de brief: iemand die zich
 * "[klik](http://…)" noemt hoort geen link in zijn eigen post te krijgen. Zo'n stuk gaat hierlangs
 * en komt er letterlijk uit.
 */
export const letterlijk = (s) => String(s ?? "").replace(/[[\]*\\]/g, (ch) => `\\${ch}`);

const LIJST = /^\s*-\s+/;

const S = {
  p: "margin:0 0 14px",
  ul: "margin:0 0 14px;padding-left:20px",
  li: "margin:0 0 9px",
};

/*
 * Markdown → de blokken van een brief. Regel voor regel in plaats van per alinea, omdat een
 * opsomming even goed midden in een alinea kan beginnen: "Drie dingen:" met daaronder drie
 * streepjes is één blok en hoort toch een lijst te worden. Een lege regel sluit een alinea maar
 * niet de lijst — zo blijven punten met wit ertussen één opsomming.
 */
export function inhoud(markdown) {
  const uit = [];
  let alinea = [], lijst = [];
  const sluitAlinea = () => {
    if (alinea.length) uit.push(`<p style="${S.p}">${alinea.map(inline).join("<br>")}</p>`);
    alinea = [];
  };
  const sluitLijst = () => {
    if (lijst.length) uit.push(`<ul style="${S.ul}">${lijst.map((r) => `<li style="${S.li}">${inline(r)}</li>`).join("")}</ul>`);
    lijst = [];
  };

  for (const regel of esc(markdown).split("\n")) {
    if (LIJST.test(regel)) { sluitAlinea(); lijst.push(regel.replace(LIJST, "").trim()); continue; }
    if (!regel.trim()) { sluitAlinea(); continue; }
    sluitLijst();
    alinea.push(regel.trim());
  }
  sluitAlinea();
  sluitLijst();
  return uit.join("\n");
}

/* ---------- Het vel ---------- */

const SCHREEF = "Georgia,'Times New Roman',Times,serif";
const SCHREEFLOOS = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif";

const luchtpost = (gap) =>
  `repeating-linear-gradient(-45deg,${LICHT.alert} 0 10px,${gap} 10px 20px,${LICHT.accent} 20px 30px,${gap} 30px 40px)`;

/*
 * Het vel zonder omhulsel — ook wat de voorbeeldweergave op /promotieteam laat zien, want "zo gaat
 * hij eruit" hoort letterlijk hetzelfde te zijn als wat eruit gaat.
 *
 * De rand is een verloop met een effen kleur eronder: clients die geen verloop tekenen (oudere
 * Outlook) houden een blauwe streep over in plaats van een gat.
 */
export const vel = (markdown, staart = "") => `<div class="b-vel" style="max-width:600px;margin:0 auto;background-color:${LICHT.panel}">
<div class="b-rand" style="height:6px;line-height:6px;font-size:0;background-color:${LICHT.accent};background-image:${luchtpost(LICHT.panel)}">&nbsp;</div>
<div class="b-ink" style="padding:28px 26px 34px;font-family:${SCHREEF};font-size:16px;line-height:1.62;color:${LICHT.ink};background-color:${LICHT.panel}">
${inhoud(markdown)}${staart ? `
<div class="b-lijn b-stil" style="margin:26px 0 0;padding-top:14px;border-top:1px solid ${LICHT.rule};font-family:${SCHREEFLOOS};font-size:13.5px;line-height:1.55;color:${LICHT.mute}">
${inhoud(staart)}
</div>` : ""}
</div>
</div>`;

/*
 * De omslag. `color-scheme` en `supported-color-schemes` vertellen de client dat deze brief allebei
 * de standen aankan — zonder dat gaan sommige clients zelf lopen omkeren terwijl het niet nodig is.
 */
export const brief = (markdown, staart = "") => `<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<style>
@media (prefers-color-scheme: dark) {
  .b-papier { background-color: ${DONKER.paper} !important; }
  .b-vel, .b-ink { background-color: ${DONKER.panel} !important; }
  .b-ink { color: ${DONKER.ink} !important; }
  .b-rand { background-color: ${DONKER.accent} !important; background-image: ${luchtpost(DONKER.panel)} !important; }
  .b-lijn { border-top-color: ${DONKER.rule} !important; }
  .b-stil { color: ${DONKER.mute} !important; }
  .b-web { color: ${DONKER.accent} !important; }
}
</style>
</head>
<body class="b-papier" style="margin:0;padding:24px 12px;background-color:${LICHT.paper}">
${vel(markdown, staart)}
</body>
</html>`;