editors/vscode/panel.js
1
'use strict';
3
const statusTone = {
4
ok: 'ok',
5
moved: 'moved',
6
ambiguous: 'failing',
7
drifted: 'failing',
8
orphaned: 'failing'
9
};
11
function escape(text) {
12
return String(text ?? '').replace(
13
/[&<>"']/g,
14
(character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]
15
);
16
}
18
function paragraphs(body) {
19
return String(body ?? '')
20
.split(/\n{2,}/)
21
.map((block) => block.trim())
22
.filter(Boolean)
23
.map((block) => `<p>${escape(block).replace(/\n/g, '<br>')}</p>`)
24
.join('');
25
}
27
function entry(item, index) {
28
const tone = statusTone[item.status] ?? 'failing';
29
const warning = item.warning ? `<p class="warning">${escape(item.warning)}</p>` : '';
30
return `<li>
31
<button type="button" data-reveal="${index}">
32
<span class="meta"><span class="kind">${escape(item.kind)}</span><span class="status ${tone}">${escape(item.status)}</span><span class="line">L${Number(item.line) || 0}</span></span>
33
<span class="body">${paragraphs(item.body)}${warning}</span>
34
</button>
35
</li>`;
36
}
38
// The panel exists because a body is a paragraph and every inline surface VS
39
// Code offers is one line (ADR 0114). Rendering prose is the whole job, so the
40
// markup is prose, not rows.
41
function panelHTML({ items, file, nonce, styleNonce }) {
42
const list = items.length
43
? `<ul>${items.map(entry).join('')}</ul>`
44
: `<p class="empty">No annotations in this file.</p>`;
45
return `<!DOCTYPE html>
46
<html lang="en">
47
<head>
48
<meta charset="utf-8">
49
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'nonce-${styleNonce}'; script-src 'nonce-${nonce}';">
50
<style nonce="${styleNonce}">
51
:root { color-scheme: light dark; }
52
body {
53
margin: 0; padding: 0.4rem 0.2rem;
54
font-family: var(--vscode-font-family); font-size: var(--vscode-font-size);
55
color: var(--vscode-foreground); background: transparent;
56
}
57
ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.15rem; }
58
button {
59
display: block; width: 100%; text-align: left; cursor: pointer;
60
background: none; border: none; border-left: 2px solid transparent;
61
padding: 0.45rem 0.6rem; color: inherit; font: inherit;
62
}
63
button:hover { background: var(--vscode-list-hoverBackground); }
64
button:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; }
65
.meta { display: flex; gap: 0.5rem; align-items: baseline; margin-bottom: 0.2rem; }
66
.kind { font-weight: 600; }
67
.status { font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.04em; }
68
.status.ok { color: var(--vscode-descriptionForeground); }
69
.status.moved { color: var(--vscode-editorWarning-foreground); }
70
.status.failing { color: var(--vscode-editorError-foreground); }
71
.line { margin-left: auto; color: var(--vscode-descriptionForeground); font-variant-numeric: tabular-nums; }
72
.body p { margin: 0 0 0.4rem; line-height: 1.45; white-space: normal; }
73
.body p:last-child { margin-bottom: 0; }
74
.warning { color: var(--vscode-editorError-foreground); }
75
.empty { color: var(--vscode-descriptionForeground); padding: 0.6rem; }
76
.file { color: var(--vscode-descriptionForeground); padding: 0 0.6rem 0.4rem; }
77
</style>
78
</head>
79
<body>
80
${file ? `<p class="file">${escape(file)}</p>` : ''}
81
${list}
82
<script nonce="${nonce}">
83
const vscode = acquireVsCodeApi();
84
document.addEventListener('click', (event) => {
85
const target = event.target.closest('[data-reveal]');
86
if (target) {
87
vscode.postMessage({ reveal: Number(target.dataset.reveal) });
88
}
89
});
90
</script>
91
</body>
92
</html>`;
93
}
95
module.exports = { escape, panelHTML, paragraphs };