Kundenansicht neu P2: Wortlaut-Orakel für die Texte der Fassung 5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
91e5bca1c5
commit
b77c3291c5
2 files changed
+147
No files matched your search
@@ -0,0 +1,125 @@
|
||||
// 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
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user