Files
boehmitools-main/plugins/werkzeuge-wandler/static/index.html
T
2026-07-24 21:37:03 +02:00

1491 lines
63 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wandler</title>
<link rel="stylesheet" href="/shared/boehmi.css">
<script src="/shared/boehmi.js"></script>
<style>
.smartbox { position: relative; }
#smart { width: 100%; box-sizing: border-box; padding: 14px 16px; font-size: 17px;
border: 1px solid var(--bt-line); border-radius: var(--bt-r);
background: var(--bt-surface); color: var(--bt-ink); font-weight: 600; }
#smart:focus { outline: none; border-color: var(--bt-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--bt-accent) 18%, transparent); }
.examples { margin: 8px 0 0; display: flex; gap: 6px; flex-wrap: wrap; }
.examples button { padding: 3px 9px; font-size: 11.5px; border-radius: 999px;
color: var(--bt-muted); background: var(--bt-bg-2);
border: 1px solid var(--bt-line-soft); cursor: pointer; }
.examples button:hover { color: var(--bt-ink); }
.res { margin-top: 14px; }
.res .main { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
font-size: 26px; font-weight: 800; letter-spacing: -.02em; }
.res .main .unit { font-size: 17px; font-weight: 650; color: var(--bt-muted); }
.res .kind { font-size: 11px; font-weight: 700; letter-spacing: .06em;
text-transform: uppercase; color: var(--bt-accent); margin-bottom: 4px; }
.res .alts { margin-top: 10px; display: grid;
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 4px; }
.res .err { color: var(--bt-err); font-size: 13px; }
.kv { display: flex; justify-content: space-between; gap: 10px; align-items: baseline;
padding: 5px 9px; border-radius: var(--bt-r-sm); font-size: 13px; cursor: pointer;
border: 1px solid transparent; }
.kv:hover { background: var(--bt-bg-2); border-color: var(--bt-line-soft); }
.kv .k { color: var(--bt-muted); font-size: 12px; }
.kv .v { font-weight: 700; font-variant-numeric: tabular-nums; text-align: right;
word-break: break-all; }
.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; }
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.row input[type=text], .row input[type=number], .row select {
padding: 7px 9px; font-size: 13px; border: 1px solid var(--bt-line);
border-radius: var(--bt-r-sm); background: var(--bt-surface); color: var(--bt-ink); }
.row input.num { width: 110px; }
.row .grow { flex: 1; min-width: 90px; }
textarea.code { width: 100%; box-sizing: border-box; min-height: 130px; padding: 10px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
border: 1px solid var(--bt-line); border-radius: var(--bt-r-sm);
background: var(--bt-surface); color: var(--bt-ink); resize: vertical; }
.swatch { width: 34px; height: 34px; border-radius: var(--bt-r-sm);
border: 1px solid var(--bt-line); flex: none; }
.pins { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.pins .pin { display: inline-flex; align-items: center; gap: 6px; padding: 3px 6px 3px 10px;
font-size: 12px; border-radius: 999px; background: var(--bt-accent-soft);
color: var(--bt-accent); border: 1px solid transparent; cursor: pointer; }
.pins .pin b { font-weight: 700; }
.pins .pin .x { opacity: .6; padding: 0 3px; }
.pins .pin .x:hover { opacity: 1; }
.hist { font-size: 12.5px; color: var(--bt-muted); }
.hist div { padding: 3px 0; cursor: pointer; }
.hist div:hover { color: var(--bt-ink); }
.note { font-size: 11.5px; color: var(--bt-muted); margin: 6px 0 0; }
</style>
</head>
<body>
<main class="bt-main wide">
<div class="bt-pagehead">
<h1>🔀 Wandler</h1>
<p>Einheiten, Formate, Farben und Zeit. Tipp einfach, was du meinst —
gerechnet wird sofort im Browser, ohne Server und ohne Internet.</p>
</div>
<div class="bt-card smartbox">
<input type="text" id="smart" placeholder="z. B. 2 cups Mehl in g · 180 C in F · #3b82f6 · 1699999999"
autocomplete="off" spellcheck="false">
<div class="examples" id="examples"></div>
<div class="res" id="res"></div>
<div class="pins" id="pins"></div>
</div>
<div class="bt-tabs" id="tabs">
<button class="bt-tab active" data-tab="kueche">Küche</button>
<button class="bt-tab" data-tab="masse">Maße</button>
<button class="bt-tab" data-tab="technik">Technik</button>
<button class="bt-tab" data-tab="formate">Formate</button>
<button class="bt-tab" data-tab="zeit">Zeit</button>
</div>
<!-- ─────────────────────────────────────────────────── Küche -->
<section id="tab-kueche" class="tab">
<div class="grid2">
<div class="bt-card">
<h2>Zutat umrechnen</h2>
<p class="hint">Volumen ↔ Gewicht hängt von der Zutat ab: 1 Cup Mehl wiegt
deutlich weniger als 1 Cup Zucker.</p>
<div class="row">
<input type="text" class="num" id="kMenge" value="1">
<select id="kVon"></select>
<span></span>
<select id="kNach"></select>
</div>
<div class="row" style="margin-top:8px">
<label class="bt-inline">Zutat: <select id="kZutat"></select></label>
</div>
<div id="kOut" class="res"></div>
</div>
<div class="bt-card">
<h2>Ofen</h2>
<div class="row">
<input type="text" class="num" id="oTemp" value="180">
<select id="oEinheit">
<option value="c">°C (Ober-/Unterhitze)</option>
<option value="f">°F</option>
<option value="umluft">°C (Umluft)</option>
<option value="gas">Gasstufe</option>
</select>
</div>
<div id="oOut" class="res"></div>
<p class="note">Umluft ≈ 20 °C weniger als Ober-/Unterhitze. Gasstufen nach
britischer Skala (Gas Mark) — bei deutschen Geräten kann das abweichen.</p>
</div>
<div class="bt-card">
<h2>Löffel & Tassen</h2>
<div id="loeffel"></div>
<p class="note">EL = 15 ml, TL = 5 ml, Msp ≈ 0,5 ml, Prise ≈ 0,3 ml.
US-Cup = 236,6 ml (nicht 250 ml).</p>
</div>
<div class="bt-card">
<h2>Portionen umrechnen</h2>
<div class="row">
<label class="bt-inline">von <input type="text" class="num" id="pVon" value="4"></label>
<label class="bt-inline">auf <input type="text" class="num" id="pNach" value="6"></label>
</div>
<div class="row" style="margin-top:8px">
<input type="text" class="grow" id="pMenge" placeholder="Menge, z. B. 250 g oder 1 1/2 EL">
</div>
<div id="pOut" class="res"></div>
</div>
</div>
</section>
<!-- ──────────────────────────────────────────────────── Maße -->
<section id="tab-masse" class="tab bt-hidden">
<div class="bt-card">
<div class="row">
<select id="mDim"></select>
<input type="text" class="num" id="mWert" value="1">
<select id="mVon"></select>
</div>
<div id="mOut" class="res"></div>
</div>
</section>
<!-- ───────────────────────────────────────────────── Technik -->
<section id="tab-technik" class="tab bt-hidden">
<div class="grid2">
<div class="bt-card">
<h2>Datenmengen</h2>
<div class="row">
<input type="text" class="num" id="dWert" value="1">
<select id="dVon"></select>
</div>
<div id="dOut" class="res"></div>
<p class="note">MB (1000) und MiB (1024) werden sauber getrennt — genau da
kommen die „fehlenden“ Gigabyte auf Festplatten her.</p>
</div>
<div class="bt-card">
<h2>Zahlensysteme</h2>
<div class="row">
<input type="text" class="grow" id="zWert" value="255" spellcheck="false">
<select id="zVon">
<option value="10">Dezimal</option>
<option value="16">Hex</option>
<option value="2">Binär</option>
<option value="8">Oktal</option>
</select>
</div>
<div id="zOut" class="res"></div>
</div>
<div class="bt-card">
<h2>Farben</h2>
<div class="row">
<span class="swatch" id="fSwatch"></span>
<input type="text" class="grow" id="fWert" value="#3b82f6" spellcheck="false">
<input type="color" id="fPick" value="#3b82f6" style="width:38px;height:34px;padding:2px;border:1px solid var(--bt-line);border-radius:var(--bt-r-sm);background:var(--bt-surface)">
</div>
<div id="fOut" class="res"></div>
</div>
</div>
</section>
<!-- ───────────────────────────────────────────────── Formate -->
<section id="tab-formate" class="tab bt-hidden">
<div class="bt-card">
<div class="row">
<select id="tOp">
<optgroup label="Kodierung">
<option value="b64e">Base64 kodieren</option>
<option value="b64d">Base64 dekodieren</option>
<option value="urle">URL kodieren</option>
<option value="urld">URL dekodieren</option>
</optgroup>
<optgroup label="Text">
<option value="slug">Slug (dateiname-tauglich)</option>
<option value="upper">GROSSBUCHSTABEN</option>
<option value="lower">kleinbuchstaben</option>
<option value="title">Erste Buchstaben Groß</option>
<option value="camel">camelCase</option>
<option value="snake">snake_case</option>
<option value="kebab">kebab-case</option>
<option value="trimlines">Leerzeilen &amp; Leerraum säubern</option>
<option value="stats">Zeichen zählen</option>
</optgroup>
<optgroup label="Daten">
<option value="jsonp">JSON schön formatieren</option>
<option value="jsonm">JSON kompakt</option>
<option value="json2yaml">JSON → YAML</option>
<option value="yaml2json">YAML → JSON</option>
<option value="json2csv">JSON → CSV</option>
<option value="csv2json">CSV → JSON</option>
</optgroup>
</select>
<button class="ghost" id="tSwap" title="Ergebnis nach oben übernehmen">↑ übernehmen</button>
<span class="bt-spacer"></span>
<button class="ghost" id="tCopy">Ergebnis kopieren</button>
</div>
<div style="margin-top:10px">
<textarea class="code" id="tIn" spellcheck="false" placeholder="Text hier einfügen …"></textarea>
</div>
<div style="margin-top:8px">
<textarea class="code" id="tOut" spellcheck="false" readonly placeholder="Ergebnis"></textarea>
</div>
<p class="note" id="tNote"></p>
</div>
</section>
<!-- ──────────────────────────────────────────────────── Zeit -->
<section id="tab-zeit" class="tab bt-hidden">
<div class="grid2">
<div class="bt-card">
<h2>Zeitstempel</h2>
<div class="row">
<input type="text" class="grow" id="tsWert" spellcheck="false"
placeholder="1699999999 oder 2024-11-14 20:30">
<button class="ghost" id="tsNow">Jetzt</button>
</div>
<div id="tsOut" class="res"></div>
</div>
<div class="bt-card">
<h2>Zeitzonen</h2>
<div class="row">
<input type="text" class="grow" id="tzWert" spellcheck="false" placeholder="leer = jetzt">
<select id="tzZone"></select>
</div>
<div id="tzOut" class="res"></div>
</div>
<div class="bt-card">
<h2>Datumsdifferenz</h2>
<div class="row">
<input type="date" id="dvA"><span></span><input type="date" id="dvB">
</div>
<div id="dvOut" class="res"></div>
</div>
<div class="bt-card">
<h2>Cron im Klartext</h2>
<div class="row">
<input type="text" class="grow" id="crWert" value="0 3 * * 1" spellcheck="false">
</div>
<div id="crOut" class="res"></div>
<p class="note">Reihenfolge: Minute Stunde Tag Monat Wochentag.</p>
</div>
</div>
</section>
<div class="bt-card">
<h2>Zuletzt</h2>
<div class="hist" id="hist"><span class="bt-muted">Noch nichts umgerechnet.</span></div>
</div>
</main>
<script>
/* ═════════════════════════════════════════════ ENGINE START ═══════════
Reine Rechenlogik ohne DOM — damit sie sich einzeln testen lässt.
Alles hier ist deterministisch: gleiche Eingabe, gleiche Ausgabe, sofort.
(Bewusst keine KI — ein Wandler muss verlässlich sein, nicht kreativ.) */
const BTC = (function () {
/* ---------------------------------------------------------- Zahlen */
const VULGAR = { "½":.5, "⅓":1/3, "⅔":2/3, "¼":.25, "¾":.75, "⅕":.2, "⅖":.4,
"⅗":.6, "⅘":.8, "⅙":1/6, "⅚":5/6, "⅛":.125, "⅜":.375,
"⅝":.625, "⅞":.875 };
function parseNumber(raw) {
if (raw === null || raw === undefined) return NaN;
let s = String(raw).trim();
if (!s) return NaN;
let vorzeichen = 1;
if (s[0] === "-") { vorzeichen = -1; s = s.slice(1).trim(); }
// Unicode-Brüche, auch gemischt: "1½"
for (const ch in VULGAR) {
if (s.indexOf(ch) >= 0) {
const rest = s.split(ch).join("").trim();
const ganz = rest ? parseNumber(rest) : 0;
if (isNaN(ganz)) return NaN;
return vorzeichen * (ganz + VULGAR[ch]);
}
}
// "1 1/2" und "3/4"
let m = s.match(/^(\d+)\s+(\d+)\s*\/\s*(\d+)$/);
if (m) return vorzeichen * (Number(m[1]) + Number(m[2]) / Number(m[3]));
m = s.match(/^(\d+(?:[.,]\d+)?)\s*\/\s*(\d+(?:[.,]\d+)?)$/);
if (m) return vorzeichen * (parseNumber(m[1]) / parseNumber(m[2]));
s = s.replace(/\s/g, "");
const hatK = s.indexOf(",") >= 0, hatP = s.indexOf(".") >= 0;
if (hatK && hatP) {
// Das zuletzt stehende Zeichen ist das Dezimaltrennzeichen.
const dez = s.lastIndexOf(",") > s.lastIndexOf(".") ? "," : ".";
const tsd = dez === "," ? "." : ",";
s = s.split(tsd).join("");
s = s.split(dez).join(".");
} else if (hatK) {
const t = s.split(",");
s = t.length > 2 ? t.join("") : t.join(".");
} else if (hatP) {
const t = s.split(".");
if (t.length > 2) s = t.join("");
}
if (!/^\d*\.?\d+(?:e[+-]?\d+)?$/i.test(s)) return NaN;
const n = Number(s);
return isNaN(n) ? NaN : vorzeichen * n;
}
function fmt(n, stellen) {
if (n === null || n === undefined || isNaN(n)) return "—";
if (!isFinite(n)) return "∞";
if (n === 0) return "0";
const a = Math.abs(n);
let max = stellen;
if (max === undefined) {
if (a >= 1e15 || a < 1e-6) return n.toExponential(4).replace(".", ",");
max = a >= 1000 ? 2 : a >= 1 ? 4 : 6;
}
return n.toLocaleString("de-DE", { maximumFractionDigits: max });
}
/* In der Küche sind vier Nachkommastellen Unsinn: „250,7835 g Mehl“ wiegt
niemand ab. Deshalb hier bewusst grob runden. */
function fmtKueche(n) {
if (n === null || n === undefined || isNaN(n)) return "—";
const a = Math.abs(n);
const stellen = a >= 100 ? 0 : a >= 10 ? 1 : a >= 1 ? 1 : 2;
return fmt(n, stellen);
}
/* --------------------------------------------------------- Einheiten
Jede Größe hat eine Basiseinheit; „f“ ist der Faktor dorthin. */
const DIMS = {
laenge: { name: "Länge", basis: "m", units: {
mm:{f:.001,a:["millimeter"]}, cm:{f:.01,a:["zentimeter"]}, dm:{f:.1},
m:{f:1,a:["meter"]}, km:{f:1000,a:["kilometer"]},
in:{f:.0254,a:["inch","zoll","\""]}, ft:{f:.3048,a:["foot","feet","fuss","fuß"]},
yd:{f:.9144,a:["yard"]}, mi:{f:1609.344,a:["mile","meile"]},
sm:{f:1852,a:["nmi","seemeile"]},
}},
masse: { name: "Gewicht", basis: "g", units: {
mg:{f:.001,a:["milligramm"]}, g:{f:1,a:["gramm","gr"]},
dag:{f:10,a:["dkg","dekagramm"]}, kg:{f:1000,a:["kilogramm","kilo"]},
t:{f:1e6,a:["tonne","tonnen"]},
oz:{f:28.349523125,a:["unze","ounce"]},
lb:{f:453.59237,a:["pound","pounds"]},
pfund:{f:500,a:[]},
st:{f:6350.29318,a:["stone"]},
}},
volumen: { name: "Volumen", basis: "ml", units: {
ml:{f:1,a:["milliliter","cc","cm3"]}, cl:{f:10,a:["zentiliter"]},
dl:{f:100,a:["deziliter"]}, l:{f:1000,a:["liter","ltr"]},
m3:{f:1e6,a:["kubikmeter"]},
tl:{f:5,a:["teeloeffel","teelöffel","tsp","teaspoon"]},
el:{f:15,a:["essloeffel","esslöffel","tbsp","tablespoon"]},
msp:{f:.5,a:["messerspitze"]}, prise:{f:.3,a:["prisen"]},
cup:{f:236.5882365,a:["cups","tasse","tassen"]},
floz:{f:29.5735295625,a:["fl oz","fluid ounce"]},
pint:{f:473.176473,a:["pt"]}, quart:{f:946.352946,a:["qt"]},
gallone:{f:3785.411784,a:["gal","gallon"]},
}},
flaeche: { name: "Fläche", basis: "m2", units: {
mm2:{f:1e-6}, cm2:{f:1e-4}, m2:{f:1,a:["qm","quadratmeter"]},
a:{f:100,a:["ar"]}, ha:{f:1e4,a:["hektar"]}, km2:{f:1e6,a:["qkm"]},
sqft:{f:.09290304,a:["ft2"]}, sqyd:{f:.83612736}, acre:{f:4046.8564224},
}},
zeit: { name: "Zeit", basis: "s", units: {
ms:{f:.001,a:["millisekunde","millisekunden"]},
s:{f:1,a:["sek","sekunde","sekunden"]},
min:{f:60,a:["minute","minuten"]},
h:{f:3600,a:["std","stunde","stunden"]},
d:{f:86400,a:["tag","tage"]},
woche:{f:604800,a:["wochen","wk"]},
monat:{f:2629800,a:["monate"]},
jahr:{f:31557600,a:["jahre","a"]},
}},
tempo: { name: "Geschwindigkeit", basis: "m/s", units: {
"m/s":{f:1}, "km/h":{f:1/3.6,a:["kmh"]}, mph:{f:.44704},
kn:{f:.514444,a:["knoten"]}, "ft/s":{f:.3048},
}},
energie: { name: "Energie", basis: "J", units: {
J:{f:1,a:["joule"]}, kJ:{f:1000,a:["kilojoule"]},
cal:{f:4.184,a:["kalorie"]}, kcal:{f:4184,a:["kilokalorie","kalorien"]},
Wh:{f:3600,a:["wattstunde"]}, kWh:{f:3.6e6,a:["kilowattstunde"]},
}},
druck: { name: "Druck", basis: "Pa", units: {
Pa:{f:1,a:["pascal"]}, hPa:{f:100,a:["hektopascal"]}, kPa:{f:1000},
mbar:{f:100,a:["millibar"]}, bar:{f:1e5}, psi:{f:6894.757293},
atm:{f:101325}, mmHg:{f:133.322387415,a:["torr"]},
}},
daten: { name: "Datenmenge", basis: "B", units: {
bit:{f:.125,a:["bits","b"]},
B:{f:1,a:["byte","bytes"]},
kB:{f:1e3,a:["kilobyte"]}, MB:{f:1e6,a:["megabyte"]},
GB:{f:1e9,a:["gigabyte"]}, TB:{f:1e12,a:["terabyte"]}, PB:{f:1e15},
KiB:{f:1024,a:["kibibyte"]}, MiB:{f:1048576,a:["mebibyte"]},
GiB:{f:1073741824,a:["gibibyte"]}, TiB:{f:1099511627776,a:["tebibyte"]},
PiB:{f:1125899906842624},
}},
};
// Temperatur wird gesondert behandelt (Nullpunkt-Verschiebung).
const TEMP = {
C: { a:["c","°c","celsius","grad"], zuK:(v)=>v+273.15, vonK:(k)=>k-273.15 },
F: { a:["f","°f","fahrenheit"], zuK:(v)=>(v-32)*5/9+273.15, vonK:(k)=>(k-273.15)*9/5+32 },
K: { a:["k","kelvin"], zuK:(v)=>v, vonK:(k)=>k },
};
// Alias-Verzeichnis: geschriebene Form → { dim, key }
const LOOKUP = {};
function merken(alias, dim, key) {
const k = String(alias).toLowerCase();
if (!(k in LOOKUP)) LOOKUP[k] = { dim: dim, key: key };
}
for (const dim in DIMS) {
for (const key in DIMS[dim].units) {
merken(key, dim, key);
(DIMS[dim].units[key].a || []).forEach((al) => merken(al, dim, key));
}
}
for (const key in TEMP) {
merken(key, "temperatur", key);
TEMP[key].a.forEach((al) => merken(al, "temperatur", key));
}
function findeEinheit(text) {
if (!text) return null;
return LOOKUP[String(text).trim().toLowerCase()] || null;
}
function umrechnen(wert, von, nach) {
if (von.dim !== nach.dim) return null;
if (von.dim === "temperatur") {
return TEMP[nach.key].vonK(TEMP[von.key].zuK(wert));
}
const u = DIMS[von.dim].units;
return wert * u[von.key].f / u[nach.key].f;
}
/* ------------------------------------------------------ Küche/Dichte
Gramm je Milliliter. Ohne diese Tabelle wäre „1 Cup Mehl in g“ falsch. */
const DICHTE = {
"wasser": 1.0, "milch": 1.03, "sahne": 1.0, "joghurt": 1.03,
"mehl": .53, "vollkornmehl": .55, "speisestärke": .60, "backpulver": .72,
"zucker": .85, "puderzucker": .56, "brauner zucker": .93, "salz": 1.20,
"butter": .911, "margarine": .95, "öl": .918, "honig": 1.42, "sirup": 1.33,
"reis": .85, "haferflocken": .40, "couscous": .80, "linsen": .85,
"semmelbrösel": .40, "kakao": .42, "nüsse gehackt": .50, "mandeln gemahlen": .38,
"parmesan gerieben": .40, "quark": 1.05, "tomatenmark": 1.10,
};
const DICHTE_STD = 1.0;
function dichteVon(zutat) {
if (!zutat) return null;
const s = String(zutat).trim().toLowerCase();
if (!s) return null;
if (s in DICHTE) return DICHTE[s];
// längster passender Teiltreffer, damit „weizenmehl“ auf „mehl“ greift
let treffer = null;
for (const k in DICHTE) {
if (s.indexOf(k) >= 0 && (!treffer || k.length > treffer.length)) treffer = k;
}
return treffer ? DICHTE[treffer] : null;
}
/* Rechnet auch über Größen hinweg, wenn eine Dichte bekannt ist. */
function kuecheRechnen(wert, von, nach, zutat) {
if (von.dim === nach.dim) {
return { wert: umrechnen(wert, von, nach), dichte: null };
}
const paar = von.dim + ">" + nach.dim;
const d = dichteVon(zutat);
const dichte = d === null ? DICHTE_STD : d;
if (paar === "volumen>masse") {
const ml = wert * DIMS.volumen.units[von.key].f;
const g = ml * dichte;
return { wert: g / DIMS.masse.units[nach.key].f, dichte: dichte, geraten: d === null };
}
if (paar === "masse>volumen") {
const g = wert * DIMS.masse.units[von.key].f;
const ml = g / dichte;
return { wert: ml / DIMS.volumen.units[nach.key].f, dichte: dichte, geraten: d === null };
}
return null;
}
/* ------------------------------------------------------------- Ofen */
const GAS = [[1,140],[2,150],[3,170],[4,180],[5,190],[6,200],[7,220],[8,230],[9,240]];
function ofen(wert, art) {
let c;
if (art === "c") c = wert;
else if (art === "f") c = (wert - 32) * 5 / 9;
else if (art === "umluft") c = wert + 20;
else if (art === "gas") {
let best = GAS[0];
for (const g of GAS) if (Math.abs(g[0] - wert) < Math.abs(best[0] - wert)) best = g;
c = best[1];
} else return null;
let gas = null, bestAb = Infinity;
for (const g of GAS) {
const ab = Math.abs(g[1] - c);
if (ab < bestAb) { bestAb = ab; gas = g[0]; }
}
return { c: c, f: c * 9 / 5 + 32, umluft: c - 20, gas: gas, gasGenau: bestAb <= 5 };
}
/* ----------------------------------------------------------- Farben */
function hexZuRgb(hex) {
let h = String(hex).trim().replace(/^#/, "");
if (/^[0-9a-f]{3}$/i.test(h)) h = h.split("").map((c) => c + c).join("");
if (/^[0-9a-f]{4}$/i.test(h)) h = h.split("").map((c) => c + c).join("");
if (/^[0-9a-f]{8}$/i.test(h)) h = h.slice(0, 6);
if (!/^[0-9a-f]{6}$/i.test(h)) return null;
return { r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16) };
}
function rgbZuHex(r, g, b) {
const t = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
return "#" + t(r) + t(g) + t(b);
}
function rgbZuHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r,g,b), min = Math.min(r,g,b);
let h = 0, s = 0; const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > .5 ? d / (2 - max - min) : d / (max + min);
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
else if (max === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
h *= 60;
}
return { h: h, s: s * 100, l: l * 100 };
}
function hslZuRgb(h, s, l) {
h = ((h % 360) + 360) % 360; s /= 100; l /= 100;
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = l - c / 2;
let r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; }
else if (h < 120) { r = x; g = c; }
else if (h < 180) { g = c; b = x; }
else if (h < 240) { g = x; b = c; }
else if (h < 300) { r = x; b = c; }
else { r = c; b = x; }
return { r: (r + m) * 255, g: (g + m) * 255, b: (b + m) * 255 };
}
function farbeLesen(text) {
const s = String(text).trim();
let m = s.match(/^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i);
if (m) return { r: +m[1], g: +m[2], b: +m[3] };
m = s.match(/^hsla?\(\s*(-?[\d.]+)\s*(?:deg)?[,\s]+([\d.]+)%?[,\s]+([\d.]+)%?/i);
if (m) return hslZuRgb(+m[1], +m[2], +m[3]);
return hexZuRgb(s);
}
/* -------------------------------------------------------- Zeit/Cron */
function zeitstempelLesen(text) {
const s = String(text).trim();
if (/^\d{13}$/.test(s)) return new Date(Number(s));
if (/^\d{10}$/.test(s)) return new Date(Number(s) * 1000);
let m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:[ ,]+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
if (m) return new Date(+m[3], +m[2]-1, +m[1], +(m[4]||0), +(m[5]||0), +(m[6]||0));
if (/^\d{4}-\d{2}-\d{2}/.test(s)) {
const d = new Date(s.length <= 10 ? s + "T00:00:00" : s);
return isNaN(d.getTime()) ? null : d;
}
return null;
}
function kalenderwoche(d) {
const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
const tag = t.getUTCDay() || 7;
t.setUTCDate(t.getUTCDate() + 4 - tag);
const start = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
return Math.ceil((((t - start) / 86400000) + 1) / 7);
}
const WOTAG = ["sonntags","montags","dienstags","mittwochs","donnerstags","freitags","samstags"];
const MONATE = ["Januar","Februar","März","April","Mai","Juni","Juli","August",
"September","Oktober","November","Dezember"];
function cronText(ausdruck) {
const t = String(ausdruck).trim().split(/\s+/);
if (t.length !== 5) return null;
const [mi, st, tg, mo, wt] = t;
const gueltig = /^[\d*/,\-]+$/;
if (![mi, st, tg, mo, wt].every((x) => gueltig.test(x))) return null;
let takt = "";
if (mi.startsWith("*/") && st === "*") takt = `alle ${mi.slice(2)} Minuten`;
else if (mi === "*" && st === "*") takt = "jede Minute";
else if (st.startsWith("*/")) takt = `alle ${st.slice(2)} Stunden zur Minute ${mi}`;
else if (/^\d+$/.test(mi) && /^\d+$/.test(st))
takt = `um ${String(st).padStart(2,"0")}:${String(mi).padStart(2,"0")} Uhr`;
else if (/^\d+$/.test(mi) && st === "*") takt = `stündlich zur Minute ${mi}`;
else takt = `Minute ${mi}, Stunde ${st}`;
const teile = [];
if (wt !== "*") {
const namen = wt.split(",").map((x) => WOTAG[Number(x) % 7] || `Wochentag ${x}`);
teile.push(namen.join(" und "));
}
if (tg !== "*") {
if (tg.startsWith("*/")) teile.push(`alle ${tg.slice(2)} Tage`);
else teile.push(`am ${tg}. des Monats`);
}
if (mo !== "*") {
const namen = mo.split(",").map((x) => MONATE[Number(x) - 1] || `Monat ${x}`);
teile.push("im " + namen.join(" und "));
}
if (!teile.length) teile.push(tg === "*" && wt === "*" ? "täglich" : "");
return (teile.filter(Boolean).join(", ") + " " + takt).trim();
}
/* ------------------------------------------------------ Textformate */
function slug(s) {
const map = { "ä":"ae","ö":"oe","ü":"ue","ß":"ss","Ä":"ae","Ö":"oe","Ü":"ue" };
return String(s).replace(/[äöüßÄÖÜ]/g, (c) => map[c])
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.toLowerCase().replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function woerter(s) {
return String(s).replace(/[_\-]+/g, " ")
.replace(/([a-zäöü])([A-ZÄÖÜ])/g, "$1 $2")
.split(/\s+/).filter(Boolean);
}
function camel(s) {
const w = woerter(s).map((x) => x.toLowerCase());
return w.map((x, i) => i ? x[0].toUpperCase() + x.slice(1) : x).join("");
}
function snake(s) { return woerter(s).map((x) => x.toLowerCase()).join("_"); }
function kebab(s) { return woerter(s).map((x) => x.toLowerCase()).join("-"); }
function titel(s) {
return String(s).replace(/\S+/g, (w) => w[0].toUpperCase() + w.slice(1).toLowerCase());
}
function textStat(s) {
const t = String(s);
const w = t.trim() ? t.trim().split(/\s+/).length : 0;
return {
zeichen: t.length,
ohneLeer: t.replace(/\s/g, "").length,
woerter: w,
zeilen: t ? t.split(/\r?\n/).length : 0,
absaetze: t.trim() ? t.trim().split(/\n\s*\n/).length : 0,
bytes: (typeof TextEncoder !== "undefined") ? new TextEncoder().encode(t).length : t.length,
};
}
/* JSON → YAML (vollständig für JSON-Datentypen). */
function jsonZuYaml(wert, tiefe) {
tiefe = tiefe || 0;
const ein = " ".repeat(tiefe);
const skalar = (v) => {
if (v === null) return "null";
if (typeof v === "boolean" || typeof v === "number") return String(v);
const s = String(v);
if (s === "") return '""';
if (/^[\s]|[\s]$|[:#\-?&*!|>'"%@`{}\[\],]|^(true|false|null|yes|no|on|off)$/i.test(s)
|| /^-?\d/.test(s) || s.indexOf("\n") >= 0) {
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n") + '"';
}
return s;
};
if (Array.isArray(wert)) {
if (!wert.length) return ein + "[]";
return wert.map((v) => {
if (v !== null && typeof v === "object") {
const inner = jsonZuYaml(v, tiefe + 1);
return ein + "-\n" + inner;
}
return ein + "- " + skalar(v);
}).join("\n");
}
if (wert !== null && typeof wert === "object") {
const keys = Object.keys(wert);
if (!keys.length) return ein + "{}";
return keys.map((k) => {
const v = wert[k];
if (v !== null && typeof v === "object" &&
(Array.isArray(v) ? v.length : Object.keys(v).length)) {
return ein + k + ":\n" + jsonZuYaml(v, tiefe + 1);
}
if (v !== null && typeof v === "object") {
return ein + k + ": " + (Array.isArray(v) ? "[]" : "{}");
}
return ein + k + ": " + skalar(v);
}).join("\n");
}
return ein + skalar(wert);
}
/* YAML → JSON, bewusst nur für den einfachen Teil der Sprache:
verschachtelte Zuordnungen, Listen und Skalare. Keine Anker,
keine mehrzeiligen Blöcke, keine Fluss-Syntax. */
function yamlZuJson(text) {
const zeilen = String(text).replace(/\r/g, "").split("\n")
.filter((z) => z.trim() && !/^\s*#/.test(z));
let i = 0;
function skalar(s) {
const t = s.trim();
if (t === "") return null;
if (/^".*"$/.test(t)) return t.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"');
if (/^'.*'$/.test(t)) return t.slice(1, -1);
if (t === "null" || t === "~") return null;
if (t === "true") return true;
if (t === "false") return false;
if (/^-?\d+$/.test(t)) return parseInt(t, 10);
if (/^-?\d*\.\d+$/.test(t)) return parseFloat(t);
if (t === "[]") return [];
if (t === "{}") return {};
return t;
}
function einzug(z) { return z.match(/^\s*/)[0].length; }
function block(minEin) {
// Liste?
if (i < zeilen.length && einzug(zeilen[i]) >= minEin && /^\s*-\s?/.test(zeilen[i])) {
const liste = [];
const ein = einzug(zeilen[i]);
while (i < zeilen.length && einzug(zeilen[i]) === ein && /^\s*-\s?/.test(zeilen[i])) {
const rest = zeilen[i].replace(/^\s*-\s?/, "");
i++;
if (!rest.trim()) {
liste.push(i < zeilen.length && einzug(zeilen[i]) > ein ? block(ein + 1) : null);
} else if (/^[^:\s][^:]*:(\s|$)/.test(rest)) {
// "- schluessel: wert" — eigene Zuordnung, Einzug künstlich erhöhen
const unter = [rest];
const tiefer = ein + 2;
while (i < zeilen.length && einzug(zeilen[i]) >= tiefer) { unter.push(zeilen[i].slice(tiefer - 0)); i++; }
liste.push(yamlZuJson(unter.join("\n")));
} else {
liste.push(skalar(rest));
}
}
return liste;
}
// Zuordnung
const obj = {};
let ein = null;
while (i < zeilen.length) {
const z = zeilen[i];
const e = einzug(z);
if (ein === null) ein = e;
if (e < ein) break;
if (e > ein) { i++; continue; }
const m = z.trim().match(/^([^:]+):\s*(.*)$/);
if (!m) { i++; continue; }
const key = m[1].trim().replace(/^["']|["']$/g, "");
const rest = m[2];
i++;
if (rest.trim() === "") {
if (i < zeilen.length && einzug(zeilen[i]) > e) obj[key] = block(e + 1);
else if (i < zeilen.length && /^\s*-\s?/.test(zeilen[i]) && einzug(zeilen[i]) === e) obj[key] = block(e);
else obj[key] = null;
} else {
obj[key] = skalar(rest);
}
}
return obj;
}
return block(0);
}
/* CSV */
function csvLesen(text, trenner) {
const t = trenner || (String(text).split("\n")[0].indexOf(";") >= 0 ? ";" : ",");
const zeilen = [];
let feld = "", zeile = [], inQ = false;
const s = String(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
for (let i = 0; i < s.length; i++) {
const c = s[i];
if (inQ) {
if (c === '"' && s[i+1] === '"') { feld += '"'; i++; }
else if (c === '"') inQ = false;
else feld += c;
} else if (c === '"') inQ = true;
else if (c === t) { zeile.push(feld); feld = ""; }
else if (c === "\n") { zeile.push(feld); zeilen.push(zeile); zeile = []; feld = ""; }
else feld += c;
}
if (feld !== "" || zeile.length) { zeile.push(feld); zeilen.push(zeile); }
return zeilen.filter((z) => z.length > 1 || (z[0] || "").trim() !== "");
}
function csvZuJson(text) {
const z = csvLesen(text);
if (!z.length) return [];
const kopf = z[0].map((h) => h.trim());
return z.slice(1).map((r) => {
const o = {};
kopf.forEach((h, i) => {
const v = (r[i] === undefined ? "" : r[i]).trim();
o[h] = v === "" ? null
: /^-?\d+$/.test(v) ? parseInt(v, 10)
: /^-?\d*[.,]\d+$/.test(v) ? parseFloat(v.replace(",", "."))
: v;
});
return o;
});
}
function jsonZuCsv(daten, trenner) {
const t = trenner || ";";
const arr = Array.isArray(daten) ? daten : [daten];
if (!arr.length) return "";
const kopf = [];
arr.forEach((o) => Object.keys(o || {}).forEach((k) => {
if (kopf.indexOf(k) < 0) kopf.push(k);
}));
const feld = (v) => {
if (v === null || v === undefined) return "";
const s = typeof v === "object" ? JSON.stringify(v) : String(v);
return /["\n;,]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
};
return [kopf.join(t)].concat(
arr.map((o) => kopf.map((k) => feld((o || {})[k])).join(t))
).join("\n");
}
function b64Kodieren(s) {
const bytes = new TextEncoder().encode(String(s));
let bin = "";
bytes.forEach((b) => { bin += String.fromCharCode(b); });
return btoa(bin);
}
function b64Dekodieren(s) {
const bin = atob(String(s).trim());
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return new TextDecoder().decode(bytes);
}
/* ----------------------------------------------------- Die Smart-Zeile
Erkennt selbst, was gemeint ist — deterministisch, in fester
Reihenfolge, damit dieselbe Eingabe immer dasselbe liefert. */
const TRENNER = /\s+(?:in|nach|to|als|zu)\s+|\s*(?:→|->|=>|>)\s*/i;
function zerlegeMenge(text) {
const s = String(text).trim();
// Zahl (auch Bruch) am Anfang, danach optional Einheit und Zutat
const m = s.match(/^(-?[\d.,\s/]*[\d½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞](?:\s*\/\s*\d+)?)\s*(.*)$/);
if (!m) return null;
const zahl = parseNumber(m[1]);
if (isNaN(zahl)) return null;
const rest = (m[2] || "").trim();
if (!rest) return { zahl: zahl, einheit: null, zutat: "" };
// längste passende Einheit vom Anfang des Rests
const teile = rest.split(/\s+/);
for (let n = Math.min(teile.length, 3); n >= 1; n--) {
const kand = teile.slice(0, n).join(" ");
const e = findeEinheit(kand);
if (e) return { zahl: zahl, einheit: e, zutat: teile.slice(n).join(" ") };
}
const e1 = findeEinheit(rest);
if (e1) return { zahl: zahl, einheit: e1, zutat: "" };
return { zahl: zahl, einheit: null, zutat: rest };
}
function smart(eingabe) {
const roh = String(eingabe || "").trim();
if (!roh) return null;
/* 1 — Farbe */
if (/^#[0-9a-f]{3,8}$/i.test(roh) || /^(rgb|hsl)a?\(/i.test(roh)) {
const c = farbeLesen(roh);
if (c) {
const hsl = rgbZuHsl(c.r, c.g, c.b);
return { art: "Farbe", haupt: rgbZuHex(c.r, c.g, c.b), einheit: "",
farbe: rgbZuHex(c.r, c.g, c.b), alt: [
{ k: "RGB", v: `rgb(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)})` },
{ k: "HSL", v: `hsl(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%)` },
{ k: "RGB-Anteile", v: `${Math.round(c.r)} / ${Math.round(c.g)} / ${Math.round(c.b)}` },
] };
}
}
/* 2 — Cron (fünf Felder) */
if (/^[\d*/,\-]+(\s+[\d*/,\-]+){4}$/.test(roh)) {
const txt = cronText(roh);
if (txt) return { art: "Cron", haupt: txt, einheit: "", alt: [
{ k: "Ausdruck", v: roh },
] };
}
/* 3 — Zeitstempel / Datum */
if (/^\d{10}$/.test(roh) || /^\d{13}$/.test(roh) ||
/^\d{4}-\d{2}-\d{2}/.test(roh) || /^\d{1,2}\.\d{1,2}\.\d{4}/.test(roh)) {
const d = zeitstempelLesen(roh);
if (d && !isNaN(d.getTime())) {
return { art: "Zeitpunkt",
haupt: d.toLocaleString("de-DE", { dateStyle: "full", timeStyle: "short" }),
einheit: "", alt: [
{ k: "Unix (s)", v: String(Math.floor(d.getTime() / 1000)) },
{ k: "Unix (ms)", v: String(d.getTime()) },
{ k: "ISO 8601", v: d.toISOString() },
{ k: "Kalenderwoche", v: "KW " + kalenderwoche(d) },
] };
}
}
/* 4 — Zahlensysteme */
let m = roh.match(/^0x([0-9a-f]+)$/i) || roh.match(/^0b([01]+)$/i);
if (m) {
const basis = roh[1].toLowerCase() === "x" ? 16 : 2;
const n = parseInt(m[1], basis);
return { art: "Zahl", haupt: fmt(n), einheit: "", alt: [
{ k: "Hex", v: "0x" + n.toString(16).toUpperCase() },
{ k: "Binär", v: "0b" + n.toString(2) },
{ k: "Oktal", v: "0o" + n.toString(8) },
] };
}
m = roh.match(/^(-?[\d.,]+)\s*(?:in|nach|to|als)\s*(hex|hexadezimal|binär|binaer|bin|oktal|dezimal)$/i);
if (m) {
const n = parseNumber(m[1]);
if (!isNaN(n)) {
const z = Math.round(n);
const ziel = m[2].toLowerCase();
const wert = ziel.startsWith("hex") ? "0x" + z.toString(16).toUpperCase()
: ziel.startsWith("bin") ? "0b" + z.toString(2)
: ziel.startsWith("okt") ? "0o" + z.toString(8)
: fmt(z);
return { art: "Zahl", haupt: wert, einheit: "", alt: [
{ k: "Dezimal", v: fmt(z) },
{ k: "Hex", v: "0x" + z.toString(16).toUpperCase() },
{ k: "Binär", v: "0b" + z.toString(2) },
] };
}
}
/* 5 — Einheiten mit Ziel: „2 cups Mehl in g“ */
const stueck = roh.split(TRENNER);
if (stueck.length >= 2 && stueck[0].trim() && stueck[stueck.length - 1].trim()) {
const links = zerlegeMenge(stueck[0]);
const rechtsText = stueck[stueck.length - 1].trim();
const rechts = findeEinheit(rechtsText);
if (links && links.einheit && rechts) {
const zutat = links.zutat;
if (links.einheit.dim === rechts.dim) {
const w = umrechnen(links.zahl, links.einheit, rechts);
return { art: DIMS[rechts.dim] ? DIMS[rechts.dim].name : "Temperatur",
haupt: fmt(w), einheit: rechts.key,
alt: nachbarn(links.zahl, links.einheit, rechts) };
}
const k = kuecheRechnen(links.zahl, links.einheit, rechts, zutat);
if (k) {
const hinweis = k.geraten
? `Dichte unbekannt — mit ${fmt(k.dichte)} g/ml gerechnet (wie Wasser)`
: `Dichte ${fmt(k.dichte)} g/ml${zutat ? " für „" + zutat + "“" : ""}`;
return { art: "Küche", haupt: fmtKueche(k.wert), einheit: rechts.key,
warn: k.geraten ? hinweis : null,
alt: [{ k: "Grundlage", v: hinweis }] };
}
return { fehler: `„${links.einheit.key}“ und „${rechts.key}“ passen nicht zusammen.` };
}
if (links && !links.einheit) return { fehler: `Einheit „${links.zutat || stueck[0]}“ kenne ich nicht.` };
if (links && !rechts) return { fehler: `Zieleinheit „${rechtsText}“ kenne ich nicht.` };
}
/* 6 — nur Menge: Übersicht in derselben Größe */
const nur = zerlegeMenge(roh);
if (nur && nur.einheit) {
const dim = nur.einheit.dim;
const alt = [];
if (dim === "temperatur") {
for (const key in TEMP) {
if (key === nur.einheit.key) continue;
alt.push({ k: "°" + key, v: fmt(umrechnen(nur.zahl, nur.einheit, { dim: dim, key: key })) });
}
return { art: "Temperatur", haupt: fmt(nur.zahl), einheit: "°" + nur.einheit.key, alt: alt };
}
const u = DIMS[dim].units;
const sortiert = Object.keys(u).sort((a, b) => u[a].f - u[b].f);
sortiert.forEach((key) => {
if (key === nur.einheit.key) return;
alt.push({ k: key, v: fmt(umrechnen(nur.zahl, nur.einheit, { dim: dim, key: key })) });
});
return { art: DIMS[dim].name, haupt: fmt(nur.zahl), einheit: nur.einheit.key, alt: alt };
}
return { fehler: "Das habe ich nicht verstanden. Beispiel: „2 cups Mehl in g“." };
}
/* Sinnvolle Nachbareinheiten zum Ergebnis (max. 5). */
function nachbarn(wert, von, ziel) {
const dim = ziel.dim;
const alt = [];
if (dim === "temperatur") {
for (const key in TEMP) {
if (key === ziel.key) continue;
alt.push({ k: "°" + key, v: fmt(umrechnen(wert, von, { dim: dim, key: key })) });
}
return alt;
}
const u = DIMS[dim].units;
const keys = Object.keys(u).sort((a, b) => u[a].f - u[b].f);
const pos = keys.indexOf(ziel.key);
keys.slice(Math.max(0, pos - 2), pos + 4).forEach((key) => {
if (key === ziel.key) return;
alt.push({ k: key, v: fmt(umrechnen(wert, von, { dim: dim, key: key })) });
});
return alt.slice(0, 6);
}
return {
parseNumber, fmt, fmtKueche, DIMS, TEMP, LOOKUP, findeEinheit, umrechnen,
DICHTE, dichteVon, kuecheRechnen, ofen,
hexZuRgb, rgbZuHex, rgbZuHsl, hslZuRgb, farbeLesen,
zeitstempelLesen, kalenderwoche, cronText,
slug, camel, snake, kebab, titel, textStat,
jsonZuYaml, yamlZuJson, csvZuJson, jsonZuCsv, csvLesen,
b64Kodieren, b64Dekodieren,
zerlegeMenge, smart, nachbarn,
};
})();
/* ══════════════════════════════════════════════ ENGINE ENDE ═══════════ */
/* ------------------------------------------------------------ Oberfläche */
(function () {
if (typeof document === "undefined") return;
const $ = (id) => document.getElementById(id);
const esc = (s) => (window.BT && BT.escape) ? BT.escape(s)
: String(s == null ? "" : s).replace(/[&<>"]/g, (c) =>
({ "&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;" }[c]));
/* Reiter */
$("tabs").addEventListener("click", (e) => {
const b = e.target.closest("button[data-tab]");
if (!b) return;
document.querySelectorAll("#tabs button").forEach((x) => x.classList.remove("active"));
document.querySelectorAll("main > .tab").forEach((x) => x.classList.add("bt-hidden"));
b.classList.add("active");
$("tab-" + b.dataset.tab).classList.remove("bt-hidden");
});
/* Ablage im Browser — bewusst lokal, es verlässt nichts das Gerät. */
const Speicher = {
lesen(key, standard) {
try { const r = localStorage.getItem("btc." + key); return r ? JSON.parse(r) : standard; }
catch (_) { return standard; }
},
schreiben(key, wert) {
try { localStorage.setItem("btc." + key, JSON.stringify(wert)); } catch (_) {}
},
};
let verlauf = Speicher.lesen("verlauf", []);
let merkliste = Speicher.lesen("pins", []);
function kopieren(text) {
const fertig = () => { if (window.BT && BT.toast) BT.toast("Kopiert.", "ok"); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(fertig, () => {});
} else {
const ta = document.createElement("textarea");
ta.value = text; document.body.appendChild(ta); ta.select();
try { document.execCommand("copy"); fertig(); } catch (_) {}
document.body.removeChild(ta);
}
}
/* Klick auf eine Zeile kopiert deren Wert. */
document.addEventListener("click", (e) => {
const kv = e.target.closest(".kv");
if (kv && kv.dataset.copy) kopieren(kv.dataset.copy);
});
function zeile(k, v) {
return `<div class="kv" data-copy="${esc(v)}" title="Klicken zum Kopieren">
<span class="k">${esc(k)}</span><span class="v">${esc(v)}</span></div>`;
}
function ergebnisHtml(r) {
if (!r) return "";
if (r.fehler) return `<div class="err">${esc(r.fehler)}</div>`;
const kopf = `<div class="kind">${esc(r.art)}</div>
<div class="main">${r.farbe ? `<span class="swatch" style="background:${esc(r.farbe)}"></span>` : ""}
<span>${esc(r.haupt)}</span>${r.einheit ? `<span class="unit">${esc(r.einheit)}</span>` : ""}</div>`;
const warn = r.warn ? `<p class="note" style="color:var(--bt-warn)">⚠ ${esc(r.warn)}</p>` : "";
const alt = (r.alt && r.alt.length)
? `<div class="alts">${r.alt.map((a) => zeile(a.k, a.v)).join("")}</div>` : "";
return kopf + warn + alt;
}
/* ---------------------------------------------------------- Smart-Zeile */
const BEISPIELE = ["2 cups Mehl in g", "180 C in F", "1699999999", "#3b82f6",
"2,5 TB in GiB", "1 1/2 EL in ml", "5 km in mi", "0 3 * * 1"];
$("examples").innerHTML = BEISPIELE.map((b) =>
`<button type="button" data-bsp="${esc(b)}">${esc(b)}</button>`).join("");
$("examples").addEventListener("click", (e) => {
const b = e.target.closest("button[data-bsp]");
if (!b) return;
$("smart").value = b.dataset.bsp;
smartRechnen(true);
});
let letzteEingabe = "";
function smartRechnen(merken) {
const roh = $("smart").value;
const r = BTC.smart(roh);
$("res").innerHTML = ergebnisHtml(r);
if (merken && r && !r.fehler && roh.trim() && roh.trim() !== letzteEingabe) {
letzteEingabe = roh.trim();
verlauf = [{ q: roh.trim(), a: r.haupt + (r.einheit ? " " + r.einheit : "") }]
.concat(verlauf.filter((v) => v.q !== roh.trim())).slice(0, 12);
Speicher.schreiben("verlauf", verlauf);
zeichneVerlauf();
}
}
$("smart").addEventListener("input", () => smartRechnen(false));
$("smart").addEventListener("change", () => smartRechnen(true));
$("smart").addEventListener("keydown", (e) => {
if (e.key === "Enter") { smartRechnen(true); anheften(); }
});
function anheften() {
const q = $("smart").value.trim();
if (!q) return;
const r = BTC.smart(q);
if (!r || r.fehler) return;
if (!merkliste.some((p) => p.q === q)) {
merkliste = [{ q: q }].concat(merkliste).slice(0, 10);
Speicher.schreiben("pins", merkliste);
zeichnePins();
}
}
function zeichnePins() {
if (!merkliste.length) { $("pins").innerHTML = ""; return; }
$("pins").innerHTML = merkliste.map((p, i) => {
const r = BTC.smart(p.q);
const wert = r && !r.fehler ? r.haupt + (r.einheit ? " " + r.einheit : "") : "?";
return `<span class="pin" data-pin="${i}"><span>${esc(p.q)}</span>
<b>${esc(wert)}</b><span class="x" data-del="${i}">✕</span></span>`;
}).join("");
}
$("pins").addEventListener("click", (e) => {
const del = e.target.closest("[data-del]");
if (del) {
merkliste.splice(Number(del.dataset.del), 1);
Speicher.schreiben("pins", merkliste); zeichnePins(); return;
}
const pin = e.target.closest("[data-pin]");
if (pin) { $("smart").value = merkliste[Number(pin.dataset.pin)].q; smartRechnen(false); }
});
function zeichneVerlauf() {
if (!verlauf.length) { $("hist").innerHTML = `<span class="bt-muted">Noch nichts umgerechnet.</span>`; return; }
$("hist").innerHTML = verlauf.map((v, i) =>
`<div data-h="${i}">${esc(v.q)} &nbsp;→&nbsp; <b>${esc(v.a)}</b></div>`).join("");
}
$("hist").addEventListener("click", (e) => {
const d = e.target.closest("[data-h]");
if (!d) return;
$("smart").value = verlauf[Number(d.dataset.h)].q;
$("smart").scrollIntoView({ behavior: "smooth", block: "start" });
smartRechnen(false);
});
/* ------------------------------------------------------------- Küche */
const KUECHE_EINHEITEN = ["ml","l","tl","el","cup","floz","g","kg","oz","lb"];
function fuelleSelect(el, keys, standard) {
el.innerHTML = keys.map((k) => `<option value="${k}"${k === standard ? " selected" : ""}>${k}</option>`).join("");
}
fuelleSelect($("kVon"), KUECHE_EINHEITEN, "cup");
fuelleSelect($("kNach"), KUECHE_EINHEITEN, "g");
$("kZutat").innerHTML = `<option value="">— ohne (wie Wasser) —</option>` +
Object.keys(BTC.DICHTE).sort().map((z) => `<option value="${esc(z)}">${esc(z)}</option>`).join("");
$("kZutat").value = "mehl";
function kuecheRechnenUI() {
const w = BTC.parseNumber($("kMenge").value);
if (isNaN(w)) { $("kOut").innerHTML = `<div class="err">Menge nicht lesbar.</div>`; return; }
const von = BTC.findeEinheit($("kVon").value), nach = BTC.findeEinheit($("kNach").value);
const k = BTC.kuecheRechnen(w, von, nach, $("kZutat").value);
if (!k) { $("kOut").innerHTML = `<div class="err">Passt nicht zusammen.</div>`; return; }
const alt = [];
if (k.dichte !== null) alt.push({ k: "Dichte", v: BTC.fmt(k.dichte) + " g/ml" });
$("kOut").innerHTML = ergebnisHtml({
art: "Küche", haupt: BTC.fmtKueche(k.wert), einheit: nach.key,
warn: k.geraten ? "Dichte unbekannt — wie Wasser gerechnet." : null, alt: alt,
});
}
["kMenge","kVon","kNach","kZutat"].forEach((id) =>
$(id).addEventListener("input", kuecheRechnenUI));
kuecheRechnenUI();
function ofenUI() {
const w = BTC.parseNumber($("oTemp").value);
if (isNaN(w)) { $("oOut").innerHTML = `<div class="err">Zahl nicht lesbar.</div>`; return; }
const o = BTC.ofen(w, $("oEinheit").value);
if (!o) { $("oOut").innerHTML = ""; return; }
$("oOut").innerHTML = ergebnisHtml({
art: "Ofen", haupt: BTC.fmt(o.c, 0), einheit: "°C Ober-/Unterhitze",
alt: [
{ k: "Umluft", v: BTC.fmt(o.umluft, 0) + " °C" },
{ k: "Fahrenheit", v: BTC.fmt(o.f, 0) + " °F" },
{ k: "Gasstufe", v: (o.gasGenau ? "" : "≈ ") + o.gas },
],
});
}
["oTemp","oEinheit"].forEach((id) => $(id).addEventListener("input", ofenUI));
ofenUI();
$("loeffel").innerHTML = [
["1 TL", "5 ml"], ["1 EL", "15 ml"], ["1 EL", "3 TL"],
["1 US-Cup", "236,6 ml"], ["1 US-Cup", "16 EL"],
["1 Msp", "≈ 0,5 ml"], ["1 Prise", "≈ 0,3 ml"],
].map((r) => zeile(r[0], r[1])).join("");
function portionUI() {
const a = BTC.parseNumber($("pVon").value), b = BTC.parseNumber($("pNach").value);
const teil = BTC.zerlegeMenge($("pMenge").value);
if (isNaN(a) || isNaN(b) || !a) { $("pOut").innerHTML = ""; return; }
if (!teil) { $("pOut").innerHTML = `<div class="err">Menge nicht lesbar.</div>`; return; }
const faktor = b / a;
const neu = teil.zahl * faktor;
$("pOut").innerHTML = ergebnisHtml({
art: "Portionen", haupt: BTC.fmtKueche(neu),
einheit: (teil.einheit ? teil.einheit.key : "") + (teil.zutat ? " " + teil.zutat : ""),
alt: [{ k: "Faktor", v: "× " + BTC.fmt(faktor) }],
});
}
["pVon","pNach","pMenge"].forEach((id) => $(id).addEventListener("input", portionUI));
/* -------------------------------------------------------------- Maße */
const MASS_DIMS = ["laenge","masse","volumen","flaeche","zeit","tempo","energie","druck"];
$("mDim").innerHTML = MASS_DIMS.map((d) =>
`<option value="${d}">${esc(BTC.DIMS[d].name)}</option>`).join("") +
`<option value="temperatur">Temperatur</option>`;
function massEinheiten() {
const dim = $("mDim").value;
const keys = dim === "temperatur" ? Object.keys(BTC.TEMP) : Object.keys(BTC.DIMS[dim].units);
fuelleSelect($("mVon"), keys, keys[0]);
massUI();
}
function massUI() {
const dim = $("mDim").value;
const w = BTC.parseNumber($("mWert").value);
if (isNaN(w)) { $("mOut").innerHTML = `<div class="err">Zahl nicht lesbar.</div>`; return; }
const von = { dim: dim, key: $("mVon").value };
const keys = dim === "temperatur" ? Object.keys(BTC.TEMP)
: Object.keys(BTC.DIMS[dim].units).sort((a, b) =>
BTC.DIMS[dim].units[a].f - BTC.DIMS[dim].units[b].f);
const alt = keys.filter((k) => k !== von.key).map((k) => ({
k: dim === "temperatur" ? "°" + k : k,
v: BTC.fmt(BTC.umrechnen(w, von, { dim: dim, key: k })),
}));
$("mOut").innerHTML = ergebnisHtml({
art: dim === "temperatur" ? "Temperatur" : BTC.DIMS[dim].name,
haupt: BTC.fmt(w), einheit: dim === "temperatur" ? "°" + von.key : von.key, alt: alt,
});
}
$("mDim").addEventListener("change", massEinheiten);
["mWert","mVon"].forEach((id) => $(id).addEventListener("input", massUI));
massEinheiten();
/* ----------------------------------------------------------- Technik */
fuelleSelect($("dVon"), Object.keys(BTC.DIMS.daten.units), "GB");
function datenUI() {
const w = BTC.parseNumber($("dWert").value);
if (isNaN(w)) { $("dOut").innerHTML = `<div class="err">Zahl nicht lesbar.</div>`; return; }
const von = { dim: "daten", key: $("dVon").value };
const keys = Object.keys(BTC.DIMS.daten.units)
.sort((a, b) => BTC.DIMS.daten.units[a].f - BTC.DIMS.daten.units[b].f);
$("dOut").innerHTML = ergebnisHtml({
art: "Datenmenge", haupt: BTC.fmt(w), einheit: von.key,
alt: keys.filter((k) => k !== von.key).map((k) => ({
k: k, v: BTC.fmt(BTC.umrechnen(w, von, { dim: "daten", key: k })) })),
});
}
["dWert","dVon"].forEach((id) => $(id).addEventListener("input", datenUI));
datenUI();
function zahlUI() {
const basis = Number($("zVon").value);
const roh = $("zWert").value.trim().replace(/^0[xbo]/i, "");
if (!roh) { $("zOut").innerHTML = ""; return; }
const n = parseInt(roh, basis);
if (isNaN(n)) { $("zOut").innerHTML = `<div class="err">Keine gültige Zahl zur Basis ${basis}.</div>`; return; }
$("zOut").innerHTML = ergebnisHtml({
art: "Zahl", haupt: BTC.fmt(n), einheit: "dezimal", alt: [
{ k: "Hex", v: n.toString(16).toUpperCase() },
{ k: "Binär", v: n.toString(2) },
{ k: "Oktal", v: n.toString(8) },
{ k: "Als Bytes", v: BTC.fmt(n / 1024) + " KiB" },
],
});
}
["zWert","zVon"].forEach((id) => $(id).addEventListener("input", zahlUI));
zahlUI();
function farbeUI(quelle) {
const c = BTC.farbeLesen($("fWert").value);
if (!c) { $("fOut").innerHTML = `<div class="err">Farbe nicht erkannt.</div>`; return; }
const hex = BTC.rgbZuHex(c.r, c.g, c.b);
const hsl = BTC.rgbZuHsl(c.r, c.g, c.b);
$("fSwatch").style.background = hex;
if (quelle !== "pick") $("fPick").value = hex;
$("fOut").innerHTML = ergebnisHtml({
art: "Farbe", haupt: hex, einheit: "", alt: [
{ k: "RGB", v: `rgb(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)})` },
{ k: "HSL", v: `hsl(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%)` },
{ k: "Helligkeit", v: BTC.fmt(hsl.l, 0) + " %" },
],
});
}
$("fWert").addEventListener("input", () => farbeUI("text"));
$("fPick").addEventListener("input", () => { $("fWert").value = $("fPick").value; farbeUI("pick"); });
farbeUI("text");
/* ----------------------------------------------------------- Formate */
function formatRechnen() {
const op = $("tOp").value;
const q = $("tIn").value;
let out = "", note = "";
try {
if (op === "b64e") out = BTC.b64Kodieren(q);
else if (op === "b64d") out = BTC.b64Dekodieren(q);
else if (op === "urle") out = encodeURIComponent(q);
else if (op === "urld") out = decodeURIComponent(q);
else if (op === "slug") out = BTC.slug(q);
else if (op === "upper") out = q.toUpperCase();
else if (op === "lower") out = q.toLowerCase();
else if (op === "title") out = BTC.titel(q);
else if (op === "camel") out = BTC.camel(q);
else if (op === "snake") out = BTC.snake(q);
else if (op === "kebab") out = BTC.kebab(q);
else if (op === "trimlines") {
out = q.split(/\r?\n/).map((z) => z.replace(/\s+$/g, ""))
.filter((z, i, a) => z.trim() || (a[i-1] || "").trim()).join("\n").trim();
}
else if (op === "stats") {
const s = BTC.textStat(q);
out = [`Zeichen: ${s.zeichen}`, `ohne Leerzeichen: ${s.ohneLeer}`,
`Wörter: ${s.woerter}`, `Zeilen: ${s.zeilen}`,
`Absätze: ${s.absaetze}`, `Bytes (UTF-8): ${s.bytes}`].join("\n");
}
else if (op === "jsonp") out = JSON.stringify(JSON.parse(q), null, 2);
else if (op === "jsonm") out = JSON.stringify(JSON.parse(q));
else if (op === "json2yaml") out = BTC.jsonZuYaml(JSON.parse(q));
else if (op === "yaml2json") {
out = JSON.stringify(BTC.yamlZuJson(q), null, 2);
note = "YAML wird nur im einfachen Teil der Sprache gelesen: verschachtelte "
+ "Zuordnungen, Listen und Skalare. Keine Anker, keine mehrzeiligen Blöcke.";
}
else if (op === "json2csv") out = BTC.jsonZuCsv(JSON.parse(q));
else if (op === "csv2json") out = JSON.stringify(BTC.csvZuJson(q), null, 2);
} catch (err) {
out = "";
note = "Fehler: " + err.message;
}
$("tOut").value = out;
$("tNote").textContent = note;
$("tNote").style.color = note.startsWith("Fehler") ? "var(--bt-err)" : "";
}
["tOp","tIn"].forEach((id) => $(id).addEventListener("input", formatRechnen));
$("tSwap").addEventListener("click", () => {
if (!$("tOut").value) return;
$("tIn").value = $("tOut").value; formatRechnen();
});
$("tCopy").addEventListener("click", () => kopieren($("tOut").value));
/* -------------------------------------------------------------- Zeit */
function tsUI() {
const roh = $("tsWert").value.trim();
if (!roh) { $("tsOut").innerHTML = ""; return; }
const d = BTC.zeitstempelLesen(roh);
if (!d || isNaN(d.getTime())) { $("tsOut").innerHTML = `<div class="err">Nicht erkannt.</div>`; return; }
const jetzt = Date.now();
const diff = Math.round((d.getTime() - jetzt) / 1000);
const rel = Math.abs(diff) < 60 ? `${Math.abs(diff)} s`
: Math.abs(diff) < 3600 ? `${Math.round(Math.abs(diff)/60)} min`
: Math.abs(diff) < 86400 ? `${Math.round(Math.abs(diff)/3600)} h`
: `${Math.round(Math.abs(diff)/86400)} Tage`;
$("tsOut").innerHTML = ergebnisHtml({
art: "Zeitpunkt",
haupt: d.toLocaleString("de-DE", { dateStyle: "full", timeStyle: "medium" }),
einheit: "", alt: [
{ k: "Unix (s)", v: String(Math.floor(d.getTime() / 1000)) },
{ k: "Unix (ms)", v: String(d.getTime()) },
{ k: "ISO 8601", v: d.toISOString() },
{ k: "Kalenderwoche", v: "KW " + BTC.kalenderwoche(d) },
{ k: diff < 0 ? "her" : "hin", v: rel },
],
});
}
$("tsWert").addEventListener("input", tsUI);
$("tsNow").addEventListener("click", () => {
$("tsWert").value = String(Math.floor(Date.now() / 1000)); tsUI();
});
const ZONEN = ["Europe/Berlin","Europe/London","Europe/Lisbon","Europe/Moscow",
"America/New_York","America/Chicago","America/Denver","America/Los_Angeles",
"America/Sao_Paulo","Asia/Dubai","Asia/Kolkata","Asia/Bangkok",
"Asia/Shanghai","Asia/Tokyo","Australia/Sydney","Pacific/Auckland","UTC"];
$("tzZone").innerHTML = ZONEN.map((z) =>
`<option value="${z}"${z === "America/New_York" ? " selected" : ""}>${z}</option>`).join("");
function tzUI() {
const roh = $("tzWert").value.trim();
const d = roh ? BTC.zeitstempelLesen(roh) : new Date();
if (!d || isNaN(d.getTime())) { $("tzOut").innerHTML = `<div class="err">Nicht erkannt.</div>`; return; }
const ziel = $("tzZone").value;
const zeig = (tz) => {
try {
return d.toLocaleString("de-DE", { timeZone: tz, dateStyle: "medium", timeStyle: "short" });
} catch (_) { return "—"; }
};
$("tzOut").innerHTML = ergebnisHtml({
art: "Zeitzone", haupt: zeig(ziel), einheit: ziel, alt: [
{ k: "Hier", v: d.toLocaleString("de-DE", { dateStyle: "medium", timeStyle: "short" }) },
{ k: "UTC", v: zeig("UTC") },
],
});
}
["tzWert","tzZone"].forEach((id) => $(id).addEventListener("input", tzUI));
tzUI();
function dvUI() {
const a = $("dvA").value, b = $("dvB").value;
if (!a || !b) { $("dvOut").innerHTML = ""; return; }
const da = new Date(a + "T00:00:00"), db = new Date(b + "T00:00:00");
const tage = Math.round((db - da) / 86400000);
const wochen = tage / 7;
let jahre = db.getFullYear() - da.getFullYear();
let monate = jahre * 12 + (db.getMonth() - da.getMonth());
if (db.getDate() < da.getDate()) monate--;
$("dvOut").innerHTML = ergebnisHtml({
art: "Differenz", haupt: BTC.fmt(tage, 0), einheit: "Tage", alt: [
{ k: "Wochen", v: BTC.fmt(wochen) },
{ k: "Monate (ganz)", v: String(monate) },
{ k: "Stunden", v: BTC.fmt(tage * 24, 0) },
{ k: "KW von / bis", v: "KW " + BTC.kalenderwoche(da) + " → KW " + BTC.kalenderwoche(db) },
],
});
}
["dvA","dvB"].forEach((id) => $(id).addEventListener("input", dvUI));
(function () {
const heute = new Date();
$("dvA").value = heute.toISOString().slice(0, 10);
const spaeter = new Date(heute.getTime() + 30 * 86400000);
$("dvB").value = spaeter.toISOString().slice(0, 10);
dvUI();
})();
function crUI() {
const t = BTC.cronText($("crWert").value);
$("crOut").innerHTML = t
? ergebnisHtml({ art: "Cron", haupt: t, einheit: "", alt: [] })
: `<div class="err">Fünf Felder erwartet, z. B. <code>0 3 * * 1</code>.</div>`;
}
$("crWert").addEventListener("input", crUI);
crUI();
/* Start */
zeichnePins();
zeichneVerlauf();
$("smart").focus();
})();
</script>
</body>
</html>