ersatzHtml liefert die schlichte Wochentabelle eines Plans als reines ASCII (Entities, maskierter Planname), wochenplanHash den 16-stelligen sha1 der Tage des Hauptplans. Dafuer wird modusUndFenster aus der Zeitlogik exportiert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
4.6 KiB
JavaScript
117 lines
4.6 KiB
JavaScript
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: <table class="wkz-ersatz"> mit <caption> = Planname (leer => "Oeffnungszeiten")
|
|
* und einer Zeile je Wochentag Montag bis Sonntag (<th scope="row"> + <td>). 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 `<tr><th scope="row">${tag}</th><td>${text}</td></tr>`
|
|
})
|
|
return [
|
|
'<table class="wkz-ersatz">',
|
|
`<caption>${name ? asciiText(name) : STANDARD_UEBERSCHRIFT}</caption>`,
|
|
'<tbody>',
|
|
...zeilen,
|
|
'</tbody>',
|
|
'</table>',
|
|
].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)
|
|
}
|