web: clickable links in terminal rows and dashboard messages (issue #233)

This commit is contained in:
iris 2026-05-22 01:06:23 +02:00
parent 4b51c198d5
commit 15e44955a8
5 changed files with 82 additions and 8 deletions

View file

@ -213,6 +213,9 @@ details.row > pre.diff-body .diff-ctx { color: var(--fg); }
border-radius: 0;
}
.live .row .md a { color: var(--cyan); text-decoration: underline; }
/* Auto-linkified bare URLs in plain rows + tool-body blocks (issue #233). */
.live .row a { color: var(--cyan); text-decoration: underline; }
.live .row a:hover { color: var(--fg); }
.live .row .md strong { color: inherit; font-weight: bold; }
.live .row .md em { color: inherit; font-style: italic; }
.live .row .md ul, .live .row .md ol { margin: 0.2em 0 0.2em 1.4em; padding: 0; }

View file

@ -113,7 +113,7 @@
clearPlaceholder();
const e = document.createElement('div');
e.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : '');
e.textContent = text;
e.appendChild(linkify(text));
log.appendChild(e);
afterAppend();
return e;
@ -127,7 +127,7 @@
d.appendChild(s);
const pre = document.createElement('pre');
pre.className = 'tool-body';
pre.textContent = body;
pre.appendChild(linkify(body));
d.appendChild(pre);
log.appendChild(d);
afterAppend();
@ -158,7 +158,7 @@
function api(extra) {
return Object.assign({
row, details, detailsDiff, placeholder,
row, details, detailsDiff, placeholder, linkify,
fromHistory: false,
}, extra || {});
}
@ -296,5 +296,49 @@
return { row, details, detailsDiff, placeholder, ready };
}
window.HiveTerminal = { create };
// Build a DocumentFragment from `text`, turning bare http(s) URLs into
// clickable links that open in a new tab. Non-URL text stays as plain
// text nodes — no innerHTML, so this is XSS-safe. Trailing sentence
// punctuation is kept out of the link. (issue #233)
const LINKIFY_URL_RE = /https?:\/\/[^\s<>"']+/g;
function linkify(text) {
const str = text == null ? '' : String(text);
const frag = document.createDocumentFragment();
if (str.indexOf('://') === -1) { // fast path: no URLs
if (str) frag.appendChild(document.createTextNode(str));
return frag;
}
let last = 0;
let m;
LINKIFY_URL_RE.lastIndex = 0;
while ((m = LINKIFY_URL_RE.exec(str)) !== null) {
let url = m[0];
// Don't swallow trailing punctuation that's really sentence text.
const trail = url.match(/[.,;:!?)\]}'"]+$/);
const tail = trail ? trail[0] : '';
if (tail) url = url.slice(0, -tail.length);
if (m.index > last) {
frag.appendChild(document.createTextNode(str.slice(last, m.index)));
}
if (!url.slice(url.indexOf('://') + 3)) {
// Nothing past the scheme — not a real URL, emit verbatim.
frag.appendChild(document.createTextNode(m[0]));
} else {
const a = document.createElement('a');
a.href = url; // regex only matches https?:// — safe
a.textContent = url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
frag.appendChild(a);
if (tail) frag.appendChild(document.createTextNode(tail));
}
last = m.index + m[0].length;
}
if (last < str.length) {
frag.appendChild(document.createTextNode(str.slice(last)));
}
return frag;
}
window.HiveTerminal = { create, linkify };
})();