Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "section_map",
"name": "Section Map",
"version": "1.1.0",
"version": "1.1.1",
"private": false,
"script": "screen.js",
"category": "practice",
Expand Down
17 changes: 14 additions & 3 deletions screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,11 @@ function _smRender() {
let label = sec.name.replace(/\d+$/, '').trim();
label = label.charAt(0).toUpperCase() + label.slice(1);

const safeLabel = _smEscapeHtml(label);
const safeTitle = _smEscapeHtml(`${label} (${_smFmt(sec.time)})`);
html += `<div class="sm-block" style="position:absolute;left:${startPct}%;width:${widthPct}%;top:0;bottom:0;background:${color};border-right:1px solid rgba(0,0,0,0.3);display:flex;align-items:center;justify-content:center;overflow:hidden;transition:opacity 0.15s;"
title="${label} (${_smFmt(sec.time)})">
<span style="font-size:9px;color:rgba(255,255,255,0.8);white-space:nowrap;text-overflow:ellipsis;overflow:hidden;padding:0 3px;">${label}</span>
title="${safeTitle}">
<span style="font-size:9px;color:rgba(255,255,255,0.8);white-space:nowrap;text-overflow:ellipsis;overflow:hidden;padding:0 3px;">${safeLabel}</span>
</div>`;
}

Expand All @@ -182,12 +184,21 @@ function _smFmt(s) {
return Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0');
}

function _smEscapeHtml(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

// Node-only export hook for tests; browsers fall through to the side-effect
// IIFE below (poller + playSong/showScreen wrapping).
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
_smGetColor, _smFmt, _smCreate, _smRemove, _smUpdate, _smRender,
_smOnClick, _smOnWheel,
_smOnClick, _smOnWheel, _smEscapeHtml,
_getState: () => ({ bar: _smBar, sections: _smSections, duration: _smDuration }),
_setState(next) {
if ('sections' in next) _smSections = next.sections;
Expand Down
8 changes: 8 additions & 0 deletions tests/screen.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ test('_smFmt formats seconds as m:ss with zero-padded seconds', () => {
assert.equal(mod._smFmt(600), '10:00');
});

test('_smEscapeHtml neutralizes section-name markup in text and attributes', () => {
const mod = freshPlugin();
assert.equal(
mod._smEscapeHtml('<img src=x onerror="alert(1)">\'&'),
'&lt;img src=x onerror=&quot;alert(1)&quot;&gt;&#39;&amp;',
);
});

test('_smRender builds one .sm-block-tagged div per section plus a position marker', () => {
const mod = freshPlugin();
const bar = new FakeBar();
Expand Down