import { createHash } from 'node:crypto' import { hauptplan, modusUndFenster } from './zeitlogik.js' /** * Oeffnungszeiten live: statische Ersatz-Tabelle und Wochenplan-Hash (Spec 4.5, rein). * Kein Appwrite, keine Config, keine Uhr. Die Tabelle steht in den Platzhaltern der Kundenseite * (ohne JavaScript und nach Abschaltung der Funktion) und zeigt nur den Wochenplan eines Plans, * keine Ausnahmen, Feiertage oder Hinweise. * * Ausgabe ist reines ASCII: Umlaute und ss-Ligatur als benannte Entities (ä ... ß), Gedankenstrich * als –, jedes andere Nicht-ASCII-Zeichen als numerische Entity (&#NNNN;). Kundentext (Planname) wird * zuerst HTML-maskiert (& < > " '). * * Mitternacht: Ein Fensterende '24:00' steht in der Tabelle als '00:00' ("18:00-00:00"), nie als "24:00". * Ein einziges Fenster 00:00-24:00 gilt als "rund um die Uhr". */ const WOCHENTAGE = Object.freeze(['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']) const GEDANKENSTRICH = '–' const STANDARD_UEBERSCHRIFT = 'Öffnungszeiten' const ENTITIES = new Map([ ['&', '&'], ['<', '<'], ['>', '>'], ['"', '"'], ["'", '''], ['\u00e4', 'ä'], ['\u00f6', 'ö'], ['\u00fc', 'ü'], ['\u00c4', 'Ä'], ['\u00d6', 'Ö'], ['\u00dc', 'Ü'], ['\u00df', 'ß'], ]) /** * Kundentext -> sicheres ASCII-HTML. Steuerzeichen (auch C1) werden zu Leerzeichen (der HTML-Parser * wuerde numerische Verweise darauf umdeuten), einzelne Surrogate zu U+FFFD. */ function asciiText(roh) { let aus = '' for (const zeichen of String(roh)) { const cp = zeichen.codePointAt(0) if (cp < 0x20 || (cp >= 0x7f && cp <= 0x9f)) aus += ' ' else if (ENTITIES.has(zeichen)) aus += ENTITIES.get(zeichen) else if (cp < 0x7f) aus += zeichen else if (cp >= 0xd800 && cp <= 0xdfff) aus += '�' else aus += `&#${cp};` } return aus } /** Text eines Tages; modus/fenster kommen bereits bereinigt aus modusUndFenster (gueltige 'HH:MM'-Fenster). */ function tagText({ modus, fenster }) { if (modus === 'rund') return 'rund um die Uhr' if (modus === 'vereinbarung') return 'nach Vereinbarung' if (modus !== 'zeiten' || fenster.length === 0) return 'geschlossen' if (fenster.length === 1 && fenster[0][0] === '00:00' && fenster[0][1] === '24:00') return 'rund um die Uhr' return fenster .map(([von, bis]) => `${von}${GEDANKENSTRICH}${bis === '24:00' ? '00:00' : bis}`) .join(', ') } /** * Schlichte Wochentabelle des Plans: mit ` }) return [ '
= Planname (leer => "Oeffnungszeiten") * und einer Zeile je Wochentag Montag bis Sonntag (
+ ). Ohne 'plan' gilt der Hauptplan; * gibt es keinen Plan, ist das Ergebnis '' (der Aufrufer entscheidet, z. B. 409). Kaputte Tageseintraege * (unbekannter Modus, fehlende Tage, unlesbare Fenster) zaehlen als geschlossen. */ export function ersatzHtml(daten, plan) { const p = plan && typeof plan === 'object' ? plan : hauptplan(daten) if (!p) return '' const name = typeof p.name === 'string' ? p.name.trim() : '' const zeilen = WOCHENTAGE.map((tag, i) => { const text = tagText(modusUndFenster(p.tage?.[i + 1])) return `
${tag}${text}
', ``, '', ...zeilen, '', '
${name ? asciiText(name) : STANDARD_UEBERSCHRIFT}
', ].join('\n') } /** * JSON mit rekursiv sortierten Objektschluesseln (Arrays behalten ihre Reihenfolge), sonst wie JSON.stringify: * undefined-Werte in Objekten entfallen, der Gesamtwert undefined ergibt undefined. */ function stabilesJson(wert) { if (Array.isArray(wert)) return `[${wert.map((w) => stabilesJson(w) ?? 'null').join(',')}]` if (wert && typeof wert === 'object') { const teile = [] for (const schluessel of Object.keys(wert).sort()) { const v = stabilesJson(wert[schluessel]) if (v !== undefined) teile.push(`${JSON.stringify(schluessel)}:${v}`) } return `{${teile.join(',')}}` } return JSON.stringify(wert) } /** * Hash des Wochenplans des Hauptplans: sha1 ueber das stabile JSON von 'tage', die ersten 16 Hex-Zeichen. * Aendert sich nur mit 'tage' des Hauptplans (nicht mit Name, Ausnahmen, Hinweisen, Feiertagen, Land oder anderen * Plaenen); '' ohne Hauptplan. Hauptplan ohne 'tage' ergibt den Hash von 'null'. */ export function wochenplanHash(daten) { const haupt = hauptplan(daten) if (!haupt) return '' const json = stabilesJson(haupt.tage) ?? 'null' return createHash('sha1').update(json).digest('hex').slice(0, 16) }