<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>noyalib · WASM playground</title>
<style>
:root {
--bg: #0a0a0a;
--panel: #161616;
--panel-2: #1c1c1c;
--line: #2a2a2a;
--fg: #e8e8e8;
--dim: #888;
--accent: #4ade80;
--warn: #fbbf24;
--err: #f87171;
--diff-add: rgba(74, 222, 128, 0.18);
--diff-del: rgba(248, 113, 113, 0.18);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
font: 14px/1.45 ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", monospace;
background: var(--bg);
color: var(--fg);
padding: 1.25rem;
}
header { margin-bottom: 1rem; }
header h1 { font-size: 1rem; font-weight: 600; letter-spacing: 0.02em; color: #fff; }
header p { font-size: 0.78rem; color: var(--dim); margin-top: 0.25rem; }
header a { color: var(--accent); text-decoration: none; }
main { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.75rem;
display: flex;
flex-direction: column;
min-height: 320px;
}
.panel h2 {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--dim);
margin-bottom: 0.5rem;
}
.source, .output, .json-view {
flex: 1;
width: 100%;
background: var(--panel-2);
color: var(--fg);
border: 1px solid var(--line);
border-radius: 5px;
padding: 0.6rem;
font: inherit;
resize: none;
white-space: pre;
overflow: auto;
}
.source:focus { outline: 1px solid var(--accent); border-color: var(--accent); }
.output, .json-view { white-space: pre-wrap; }
.output mark.add { background: var(--diff-add); padding: 0 2px; border-radius: 2px; }
.output mark.del { background: var(--diff-del); padding: 0 2px; border-radius: 2px; text-decoration: line-through; }
.demo-bar {
grid-column: 1 / -1;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.75rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
align-items: center;
}
.demo-bar label { font-size: 0.72rem; color: var(--dim); margin-right: -0.3rem; }
.demo-bar input {
flex: 1 1 160px;
min-width: 120px;
background: var(--panel-2);
color: var(--fg);
border: 1px solid var(--line);
border-radius: 4px;
padding: 0.4rem 0.55rem;
font: inherit;
}
.demo-bar input:focus { outline: none; border-color: var(--accent); }
button {
background: var(--panel-2);
color: var(--accent);
border: 1px solid var(--line);
border-radius: 4px;
padding: 0.45rem 0.85rem;
font: inherit;
cursor: pointer;
transition: border-color 0.1s ease, background 0.1s ease;
}
button:hover { background: #222; border-color: var(--accent); }
button.primary { color: var(--bg); background: var(--accent); border-color: var(--accent); }
button.primary:hover { background: #6ee79a; }
.status {
grid-column: 1 / -1;
font-size: 0.78rem;
min-height: 1em;
}
.status.ok { color: var(--accent); }
.status.warn { color: var(--warn); }
.status.err { color: var(--err); }
.more-actions {
grid-column: 1 / -1;
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
padding-top: 0.25rem;
border-top: 1px dashed var(--line);
}
.more-actions h3 { font-size: 0.7rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.08em; align-self: center; margin-right: 0.5rem; }
.comments {
grid-column: 1 / -1;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.75rem;
}
.comments h2 { font-size: 0.7rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 0.4rem; }
.comments .row { display: flex; gap: 0.4rem; align-items: baseline; padding: 0.15rem 0; }
.comments .pos { color: var(--dim); width: 70px; flex-shrink: 0; font-size: 0.78rem; }
.comments .text { color: var(--fg); }
.comments .text:before { content: "#"; color: var(--accent); margin-right: 0.2rem; }
</style>
</head>
<body>
<header>
<h1>noyalib · WASM playground</h1>
<p>
Edit YAML losslessly in the browser. Powered by
<a href="https://github.com/sebastienrousseau/noyalib">noyalib</a>'s green-tree CST —
every untouched byte (comments, indentation, blank lines) is preserved verbatim across edits.
</p>
</header>
<main>
<section class="panel">
<h2>YAML source</h2>
<textarea class="source" id="src" spellcheck="false"># noyalib service config
# (edited losslessly via the CST below)
name: noyalib # the project
version: 0.0.1 # bumped by Renovate
# server section is read by the bootstrapper
server:
host: localhost # bind address
port: 8080 # main HTTP port
features:
- auth # OIDC + passkeys
- api # public REST surface
</textarea>
</section>
<section class="panel">
<h2>After lossless edit</h2>
<pre class="output" id="out"></pre>
</section>
<section class="demo-bar">
<label>path</label>
<input id="path" value="version" />
<label>=</label>
<input id="value" value="0.0.2" />
<button class="primary" id="btn-set">set (lossless)</button>
<button id="btn-get">get</button>
<button id="btn-comments">comments_at</button>
<button id="btn-reset">reset</button>
</section>
<div class="status" id="status">ready.</div>
<section class="comments" id="comments-panel" hidden>
<h2>comments at <code id="comments-path"></code></h2>
<div id="comments-body"></div>
</section>
<section class="panel" style="grid-column: 1 / -1;">
<h2>parsed value (Value-as-JSON)</h2>
<pre class="json-view" id="json"></pre>
</section>
<section class="more-actions">
<h3>more</h3>
<button id="btn-parse">parse</button>
<button id="btn-stringify">stringify (re-emit)</button>
<button id="btn-validate">validate (YAML-1.2 JSON schema)</button>
<button id="btn-bench">bench parse × 1000</button>
</section>
</main>
<script type="module">
import init, {
WasmDocument,
parse, stringify, validate_json,
} from './pkg/noyalib_wasm.js';
await init();
say('ok', 'WASM module loaded.');
const $src = document.getElementById('src');
const $out = document.getElementById('out');
const $json = document.getElementById('json');
const $path = document.getElementById('path');
const $value = document.getElementById('value');
const ORIGINAL = $src.value;
function say(level, msg) {
const el = document.getElementById('status');
el.className = 'status ' + level;
el.textContent = msg;
}
function renderDiff(before, after) {
let i = 0;
while (i < before.length && i < after.length && before[i] === after[i]) i++;
let j = 0;
while (j < before.length - i && j < after.length - i &&
before[before.length - 1 - j] === after[after.length - 1 - j]) j++;
const prefix = after.slice(0, i);
const middle = after.slice(i, after.length - j);
const suffix = after.slice(after.length - j);
$out.innerHTML = escapeHtml(prefix) +
'<mark class="add">' + escapeHtml(middle) + '</mark>' +
escapeHtml(suffix);
}
function escapeHtml(s) {
return s
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
function refreshJson() {
try {
const v = parse($src.value);
$json.textContent = JSON.stringify(v, null, 2);
} catch (e) {
$json.textContent = '(parse error: ' + e.message + ')';
}
}
refreshJson();
$src.addEventListener('input', refreshJson);
document.getElementById('btn-set').addEventListener('click', () => {
try {
const before = $src.value;
const doc = new WasmDocument(before);
doc.set($path.value, $value.value);
const after = doc.to_string();
$src.value = after;
renderDiff(before, after);
refreshJson();
say('ok',
`set ${$path.value} = ${$value.value}. ` +
`Bytes changed: ${after.length - before.length >= 0 ? '+' : ''}${after.length - before.length}. ` +
`Comments preserved.`);
} catch (e) { say('err', e.message); }
});
document.getElementById('btn-get').addEventListener('click', () => {
try {
const doc = new WasmDocument($src.value);
const v = doc.get($path.value);
$out.textContent = JSON.stringify(v, null, 2) ?? 'null';
say('ok', `get ${$path.value}: ${JSON.stringify(v)}`);
} catch (e) { say('err', e.message); }
});
document.getElementById('btn-comments').addEventListener('click', () => {
try {
const doc = new WasmDocument($src.value);
const b = doc.comments_at($path.value);
const panel = document.getElementById('comments-panel');
document.getElementById('comments-path').textContent = $path.value;
const body = document.getElementById('comments-body');
body.innerHTML = '';
const before = b.before || [];
for (const text of before) {
body.insertAdjacentHTML('beforeend',
`<div class="row"><span class="pos">before</span><span class="text">${escapeHtml(text)}</span></div>`);
}
if (b.inline != null) {
body.insertAdjacentHTML('beforeend',
`<div class="row"><span class="pos">inline</span><span class="text">${escapeHtml(b.inline)}</span></div>`);
}
if (before.length === 0 && b.inline == null) {
body.innerHTML = '<div class="row"><span class="pos">·</span><span style="color:var(--dim)">(no comments at this path)</span></div>';
}
panel.hidden = false;
say('ok', `read comments at ${$path.value}.`);
} catch (e) { say('err', e.message); }
});
document.getElementById('btn-reset').addEventListener('click', () => {
$src.value = ORIGINAL;
$out.textContent = '';
refreshJson();
document.getElementById('comments-panel').hidden = true;
say('ok', 'source reset.');
});
document.getElementById('btn-parse').addEventListener('click', () => {
try {
const v = parse($src.value);
$out.textContent = JSON.stringify(v, null, 2);
say('ok', 'parsed.');
} catch (e) { say('err', e.message); }
});
document.getElementById('btn-stringify').addEventListener('click', () => {
try {
const v = parse($src.value);
$out.textContent = stringify(v);
say('ok', 're-emitted via the data-binding path (no CST — comments dropped).');
} catch (e) { say('err', e.message); }
});
document.getElementById('btn-validate').addEventListener('click', () => {
try {
validate_json($src.value);
say('ok', 'valid against YAML 1.2 JSON-compatible schema.');
} catch (e) { say('err', `schema: ${e.message}`); }
});
document.getElementById('btn-bench').addEventListener('click', () => {
const yaml = $src.value;
const t0 = performance.now();
for (let i = 0; i < 1000; i++) parse(yaml);
const t = performance.now() - t0;
say('ok', `1000× parse in ${t.toFixed(1)} ms (${(t).toFixed(2)} µs/op).`);
});
</script>
</body>
</html>