Skip to content

Commit 2ad451c

Browse files
author
Shane Osbourne
committed
added Template util for escaped strings
1 parent daf41f4 commit 2ad451c

File tree

5 files changed

+98
-8
lines changed

5 files changed

+98
-8
lines changed

packages/special-pages/pages/duckplayer/src/js/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
MessagingContext, TestTransportConfig
3737
} from '../../../../../messaging/index.js'
3838
import { DuckPlayerPageMessages, UserValues } from './messages'
39+
import { escapeHTML } from '../../../../../../src/dom-utils'
3940

4041
// for docs
4142
export { DuckPlayerPageMessages, UserValues }
@@ -107,7 +108,7 @@ const VideoPlayer = {
107108
* Show an error instead of the video player iframe
108109
*/
109110
showVideoError: (errorMessage) => {
110-
VideoPlayer.playerContainer().innerHTML = '<div class="player-error"><b>ERROR:</b> <span class="player-error-message"></span></div>'
111+
VideoPlayer.playerContainer().innerHTML = escapeHTML`<div class="player-error"><b>ERROR:</b> <span class="player-error-message"></span></div>`.toString()
111112

112113
// @ts-expect-error - Type 'HTMLElement | null' is not assignable to type 'HTMLElement'.
113114
document.querySelector('.player-error-message').textContent = errorMessage

src/dom-utils.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
class Template {
2+
constructor (strings, values) {
3+
this.values = values
4+
this.strings = strings
5+
}
6+
7+
/**
8+
* Escapes any occurrences of &, ", <, > or / with XML entities.
9+
*
10+
* @param {string} str
11+
* The string to escape.
12+
* @return {string} The escaped string.
13+
*/
14+
escapeXML (str) {
15+
const replacements = {
16+
'&': '&amp;',
17+
'"': '&quot;',
18+
"'": '&apos;',
19+
'<': '&lt;',
20+
'>': '&gt;',
21+
'/': '&#x2F;'
22+
}
23+
return String(str).replace(/[&"'<>/]/g, m => replacements[m])
24+
}
25+
26+
potentiallyEscape (value) {
27+
if (typeof value === 'object') {
28+
if (value instanceof Array) {
29+
return value.map(val => this.potentiallyEscape(val)).join('')
30+
}
31+
32+
// If we are an escaped template let join call toString on it
33+
if (value instanceof Template) {
34+
return value
35+
}
36+
37+
throw new Error('Unknown object to escape')
38+
}
39+
return this.escapeXML(value)
40+
}
41+
42+
toString () {
43+
const result = []
44+
45+
for (const [i, string] of this.strings.entries()) {
46+
result.push(string)
47+
if (i < this.values.length) {
48+
result.push(this.potentiallyEscape(this.values[i]))
49+
}
50+
}
51+
return result.join('')
52+
}
53+
}
54+
55+
export function escapeHTML (strings, ...values) {
56+
return new Template(strings, values)
57+
}

src/features/duckplayer/components/ddg-video-overlay.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import dax from '../assets/dax.svg'
33
import { i18n } from '../text.js'
44
import { appendImageAsBackground } from '../util.js'
55
import { VideoOverlayManager } from '../video-overlay-manager.js'
6+
import { escapeHTML } from '../../../dom-utils.js'
67

78
/**
89
* The custom element that we use to present our UI elements
@@ -54,10 +55,11 @@ export class DDGVideoOverlay extends HTMLElement {
5455
createOverlay () {
5556
const overlayElement = document.createElement('div')
5657
overlayElement.classList.add('ddg-video-player-overlay')
57-
overlayElement.innerHTML = `
58+
const svgIcon = escapeHTML([dax], [])
59+
overlayElement.innerHTML = escapeHTML`
5860
<div class="ddg-vpo-bg"></div>
5961
<div class="ddg-vpo-content">
60-
<div class="ddg-eyeball">${dax}</div>
62+
<div class="ddg-eyeball">${svgIcon}</div>
6163
<div class="ddg-vpo-title">${i18n.t('videoOverlayTitle')}</div>
6264
<div class="ddg-vpo-text">${i18n.t('videoOverlaySubtitle')}</div>
6365
<div class="ddg-vpo-buttons">
@@ -70,7 +72,7 @@ export class DDGVideoOverlay extends HTMLElement {
7072
</label>
7173
</div>
7274
</div>
73-
`
75+
`.toString()
7476
/**
7577
* Set the link
7678
* @type {string}

src/features/duckplayer/icon-overlay.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { addTrustedEventListener, appendElement, VideoParams } from './util'
22
import dax from './assets/dax.svg'
33
import { i18n } from './text.js'
44
import { OpenInDuckPlayerMsg } from './overlay-messages.js'
5+
import { escapeHTML } from '../../dom-utils.js'
56

67
export const IconOverlay = {
78
/**
@@ -41,20 +42,20 @@ export const IconOverlay = {
4142

4243
overlayElement.setAttribute('class', 'ddg-overlay' + (extraClass ? ' ' + extraClass : ''))
4344
overlayElement.setAttribute('data-size', size)
44-
overlayElement.innerHTML = `
45+
const svgIcon = escapeHTML([dax], [])
46+
overlayElement.innerHTML = escapeHTML`
4547
<a class="ddg-play-privately" href="#">
4648
<div class="ddg-dax">
47-
${dax}
49+
${svgIcon}
4850
</div>
4951
<div class="ddg-play-text-container">
5052
<div class="ddg-play-text">
5153
${i18n.t('playText')}
5254
</div>
5355
</div>
54-
</a>`
56+
</a>`.toString()
5557

5658
overlayElement.querySelector('a.ddg-play-privately')?.setAttribute('href', href)
57-
5859
overlayElement.querySelector('a.ddg-play-privately')?.addEventListener('click', (event) => {
5960
event.preventDefault()
6061
event.stopPropagation()

unit-test/dom-utils.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { escapeHTML } from '../src/dom-utils.js'
2+
3+
describe('dom-utils.js - escapedTemplate', () => {
4+
const tests = [
5+
{ title: 'single', input: () => escapeHTML`<p>Foo</p>`, expected: '<p>Foo</p>' },
6+
{ title: 'siblings', input: () => escapeHTML`<p>Foo</p><p>Bar</p>`, expected: '<p>Foo</p><p>Bar</p>' },
7+
{ title: 'nested', input: () => escapeHTML`<div>${escapeHTML`<p>${'Nested'}</p>`}</div>`, expected: '<div><p>Nested</p></div>' },
8+
{
9+
title: 'loop',
10+
input: () => {
11+
const items = [{ value: 'foo' }, { value: 'bar' }]
12+
return escapeHTML`<h1>Heading</h1>
13+
<ul>
14+
${items.map(item => escapeHTML`<li>${item.value}</li>`)};
15+
</ul>`
16+
},
17+
expected: `<h1>Heading</h1>
18+
<ul>
19+
<li>foo</li><li>bar</li>;
20+
</ul>`
21+
}
22+
]
23+
for (const test of tests) {
24+
it(`should generate ${test.title}`, () => {
25+
const actual = test.input().toString()
26+
expect(actual).toEqual(test.expected)
27+
})
28+
}
29+
})

0 commit comments

Comments
 (0)