From a712b3d61cc8b10208f991e97e499a87ea41b913 Mon Sep 17 00:00:00 2001
From: Rolando Santamaria Maso
Date: Sun, 26 Jul 2026 15:26:05 +0200
Subject: [PATCH] =?UTF-8?q?feat(ui):=20WebUI=20modernization=20PR6=20?=
=?UTF-8?q?=E2=80=94=20streaming-safe=20markdown=20tokenizer,=20golden=20t?=
=?UTF-8?q?ests,=20strict=20CSP?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Replace the regex-pipeline markdownToHtml with a hand-written
line-based block parser + inline tokenizer. Unterminated fenced code
blocks render their collected lines as a code block (EOF = fence
close); unterminated inline constructs (**, backtick, ~~) render
literally, so partial markdown no longer flashes broken markup while
streaming. DOM contract preserved (h1-h4, hr, code-block/cb-header/
cb-lang/cb-copy, ul/ol, p/br); .cb-copy is now a
');
+ // URL with parens: parsing stops at the first ')' (same as the old
+ // regex pipeline), leaving the trailing ')' as literal text.
+ const html2 = markdownToHtml('[click](javascript:alert(1))');
+ assert.ok(!html2.includes('click)');
+});
+
+test('data: link renders as plain text', () => {
+ const html = markdownToHtml('[click](data:text/html;base64,xxxx)');
+ assert.ok(!html.includes('click');
+});
+
+// ── Escaping & paragraphs ──
+
+test('raw HTML in input is escaped', () => {
+ assert.equal(
+ markdownToHtml(''),
+ '<script>alert(1)</script>
'
+ );
+});
+
+test('paragraphs split on blank lines, single newline becomes
', () => {
+ assert.equal(
+ markdownToHtml('one\ntwo\n\nthree'),
+ 'one
two
\nthree
'
+ );
+});
+
+test('empty input', () => {
+ assert.equal(markdownToHtml(''), '');
+});
+
+// ── Golden multi-feature document ──
+
+test('golden document', () => {
+ const doc = [
+ '# Title',
+ '',
+ 'Some **bold** and *italic* text with `code`.',
+ '',
+ '- one',
+ '- two',
+ '',
+ '```js',
+ 'console.log("hi");',
+ '```',
+ '',
+ 'Check [link](https://example.com) out.',
+ ].join('\n');
+
+ const expected = [
+ 'Title
',
+ 'Some bold and italic text with code.
',
+ '',
+ '' +
+ '
console.log("hi");\n
',
+ 'Check link out.
',
+ ].join('\n');
+
+ assert.equal(markdownToHtml(doc), expected);
+});
diff --git a/cmd/odek/ui/js/utils.js b/cmd/odek/ui/js/utils.js
index ac99f2f..b31f5fd 100644
--- a/cmd/odek/ui/js/utils.js
+++ b/cmd/odek/ui/js/utils.js
@@ -3,19 +3,9 @@
import { S } from './state.js';
import { messagesEl, scrollBottomBtn, cancelBtn } from './dom.js';
-// ── Escape helpers ──
-export function escapeHtml(s) {
- if (!s) return '';
- return s.replace(/&/g,'&').replace(//g,'>');
-}
-
-export function escapeAttr(s) {
- if (!s) return '';
- // & must be replaced first — doing it last double-escapes the entities
- // introduced by the quote replacements (" → ").
- return s.replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''')
- .replace(//g,'>');
-}
+// ── Escape helpers (implemented in escape.js so markdown.js can use them
+// without importing the browser-dependent modules) ──
+export { escapeHtml, escapeAttr } from './escape.js';
// ── Number formatting ──
export function formatNum(n) {
diff --git a/cmd/odek/ui/style.css b/cmd/odek/ui/style.css
index 8243035..91ab7c9 100644
--- a/cmd/odek/ui/style.css
+++ b/cmd/odek/ui/style.css
@@ -863,7 +863,7 @@ body.light {
text-transform: uppercase;
letter-spacing: .1em;
}
-.cb-copy { cursor: pointer; opacity: .5; transition: opacity .12s; padding: 2px 6px; border-radius: 2px; }
+.cb-copy { cursor: pointer; opacity: .5; transition: opacity .12s; padding: 2px 6px; border-radius: 2px; background: none; border: 0; font: inherit; color: inherit; }
.cb-copy:hover { opacity: 1; background: rgba(255,255,255,.05); }
.cb-copy.copied { opacity: 1; color: var(--green); }
.code-block pre {
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index d8fad07..ddb424a 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -119,8 +119,10 @@ cmd/odek/
ui/
index.html Single-page web UI (vanilla JS + CSS)
app.js ES-module entry point (imports ./js/main.js)
- js/ Native ES modules (state, dom, utils, markdown, render,
- approvals, sessions, input, ws, main, net) — no build step
+ js/ Native ES modules (state, dom, utils, escape, markdown,
+ render, approvals, sessions, input, ws, main, net) — no build step.
+ UI unit tests (node:test golden tests for markdown.js, etc.):
+ node --test "cmd/odek/ui/js/**/*.test.js"
style.css Stylesheet
docs/ Documentation
CLI.md CLI reference