126 lines
5.1 KiB
JavaScript
126 lines
5.1 KiB
JavaScript
// Wortlaut-Orakel (P2): lädt das alte public/app.js in eine VM, rendert die vier Wortlaut-Seiten (Bestellen, Wechsel, E-Mail-Speicher,
|
||
// Gutschrift) mit Attrappen und liefert deren HTML. Die neuen Seiten müssen denselben sichtbaren Text haben (C14, Fassung 5).
|
||
// Nur lesend: app.js bleibt unverändert. P10 friert dieses Hilfsmittel vor dem Löschen von app.js ein (N40, N51).
|
||
import vm from 'node:vm'
|
||
import { readFileSync } from 'node:fs'
|
||
import path from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import { setImmediate as takt } from 'node:timers/promises'
|
||
|
||
const APP_JS = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'public', 'app.js')
|
||
|
||
const ENTITAETEN = { '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ' ': ' ' }
|
||
|
||
/** Sichtbarer Text eines HTML-Strings oder Elements: ohne Tags, Kommentare, <script>, <style> und ohne jeden Leerraum (auch U+00A0). */
|
||
export function sichtbarerText(quelle) {
|
||
const roh = typeof quelle === 'string' ? quelle : String(quelle?.textContent ?? '')
|
||
const ohneTags = typeof quelle === 'string'
|
||
? roh
|
||
.replace(/<!--[\s\S]*?-->/g, '')
|
||
.replace(/<script\b[\s\S]*?<\/script\s*>/gi, '')
|
||
.replace(/<style\b[\s\S]*?<\/style\s*>/gi, '')
|
||
.replace(/<[^>]*>/g, '')
|
||
: roh
|
||
return ohneTags.replace(/&(?:amp|lt|gt|quot|#39|nbsp);/g, (e) => ENTITAETEN[e]).replace(/\s+/g, '')
|
||
}
|
||
|
||
/** Streicht den Seitenkopf <header class="kb-head">…</header> (Titel zeichnet die Hülle, Untertitel entfallen); ohne Kopf bleibt alles. */
|
||
export function ohneKopf(html) {
|
||
return String(html).replace(/<header class="kb-head">[\s\S]*?<\/header>/, '')
|
||
}
|
||
|
||
function attrappeElement() {
|
||
return {
|
||
innerHTML: '',
|
||
textContent: '',
|
||
dataset: {},
|
||
style: {},
|
||
value: '',
|
||
checked: false,
|
||
disabled: false,
|
||
classList: { contains: () => false, add() {}, remove() {}, toggle() {} },
|
||
querySelector: () => null,
|
||
querySelectorAll: () => [],
|
||
addEventListener() {},
|
||
removeEventListener() {},
|
||
insertAdjacentHTML() {},
|
||
scrollIntoView() {},
|
||
focus() {},
|
||
}
|
||
}
|
||
|
||
/** app.js in einer VM mit Attrappen laden. Alle Top-Level-Berührungen sind addEventListener, matchMedia, querySelector, navigator und serviceWorker. */
|
||
export function ladeAlteApp() {
|
||
const antworten = new Map()
|
||
const fetchAttrappe = async (url) => {
|
||
const pfad = String(url).split('?')[0]
|
||
const body = antworten.has(pfad) ? antworten.get(pfad) : {}
|
||
return { ok: antworten.has(pfad), status: antworten.has(pfad) ? 200 : 404, json: async () => structuredClone(body) }
|
||
}
|
||
const medien = () => ({ matches: false, addEventListener() {}, removeEventListener() {}, addListener() {} })
|
||
const elemente = new Map()
|
||
const document = {
|
||
body: attrappeElement(),
|
||
addEventListener() {},
|
||
querySelector: () => null,
|
||
querySelectorAll: () => [],
|
||
getElementById: (id) => elemente.get(id) ?? null,
|
||
createElement: () => attrappeElement(),
|
||
}
|
||
const window = {
|
||
addEventListener() {},
|
||
removeEventListener() {},
|
||
matchMedia: medien,
|
||
location: { href: '', hash: '', search: '', pathname: '/dashboard.html' },
|
||
history: { state: null, replaceState() {}, pushState() {}, back() {} },
|
||
isSecureContext: false,
|
||
scrollTo() {},
|
||
}
|
||
class CustomEventAttrappe {
|
||
constructor(typ, init = {}) { this.type = typ; this.detail = init.detail ?? null }
|
||
}
|
||
const sandbox = {
|
||
window, document, navigator: { userAgent: '' }, fetch: fetchAttrappe, CustomEvent: CustomEventAttrappe, Event: CustomEventAttrappe,
|
||
console, URLSearchParams, TextEncoder, structuredClone, setTimeout, clearTimeout, setInterval, clearInterval,
|
||
}
|
||
const ctx = vm.createContext(sandbox)
|
||
vm.runInContext(readFileSync(APP_JS, 'utf8'), ctx, { filename: 'app.js' })
|
||
const view = () => attrappeElement()
|
||
|
||
return {
|
||
/** Bestellseite (#bestellen-hosting / #bestellen-wartung): HTML der Ansicht. */
|
||
bestellen(daten, key) {
|
||
const v = view()
|
||
vm.runInContext('renderOrderView', ctx)(v, daten, key)
|
||
return v.innerHTML
|
||
},
|
||
/** Wechselseite (#wechsel) nach der Vorschau-Antwort. */
|
||
async wechsel(daten, vorschau) {
|
||
antworten.set('/api/abo/switch/preview', vorschau)
|
||
const v = view()
|
||
vm.runInContext('renderSwitchView', ctx)(v, daten)
|
||
await takt()
|
||
await takt()
|
||
return v.innerHTML
|
||
},
|
||
/** Speicher-Block der Verträge-Ansicht; die Vorschau kommt bei geplanter Reduzierung (app.js:1405). */
|
||
async speicher(daten, vorschau) {
|
||
if (vorschau) antworten.set('/api/abo/storage/preview', vorschau)
|
||
const c = view()
|
||
vm.runInContext('renderStoragePanel', ctx)(c, daten)
|
||
await takt()
|
||
await takt()
|
||
return c.innerHTML
|
||
},
|
||
/** Gutschrift-Karte (#gutschrift) mit der Antwort von GET /api/abo/takeover-credits. */
|
||
async gutschrift(daten, antwort) {
|
||
antworten.set('/api/abo/takeover-credits', antwort)
|
||
const karte = view()
|
||
elemente.set('takeover-card', karte)
|
||
vm.runInContext('takeoverState.customer = { email: "anna@example.com" }', ctx)
|
||
await vm.runInContext('loadTakeoverCard', ctx)({ abo: daten })
|
||
return karte.innerHTML
|
||
},
|
||
}
|
||
}
|