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