Skip to content

Commit bc23055

Browse files
committed
Restrict “unsafe” usage for initial “2.x” release.
Changes: * So far, we only use “unsafe” for html — so we just keep that for now. * Stopped assuming that the “template engine” discussed in the “CHANGELOG.md” is the “default template engine” — i.e., we explicitly write that now. * Simplified formatting related to bindings in “TEMPLATES.md”.
1 parent f920e80 commit bc23055

File tree

4 files changed

+172
-252
lines changed

4 files changed

+172
-252
lines changed

CHANGELOG.md

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010

11-
- You can now bind attributes with `??foo="${bar}"` syntax. This is functionally
12-
equivalent to the `nullish` updater and will replace that functionality later.
13-
- A new `unsafe` updater was added to replace `unsafeHTML` and `unsafeSVG`. You
14-
use it like `unsafe(value, 'html')` and `unsafe(value, 'svg')`.
11+
- You can now bind attributes with `??foo="${bar}"` syntax in the default
12+
template engine. This is functionally equivalent to the `nullish` updater from
13+
the default template engine and will replace that functionality later.
14+
- A new `unsafe` updater was added to replace `unsafeHTML` in the default
15+
template engine. You use it like `unsafe(value)`. Only unsafe html injection
16+
will be supported in the default template engine.
1517

1618
### Changed
1719

1820
- Template errors now include approximate line numbers from the offending
19-
template. They also print the registered custom element tag name (#201).
21+
template in the default template engine. They also print the registered custom
22+
element tag name (#201).
2023
- The `ifDefined` updater now deletes the attribute on `null` in addition to
21-
`undefined`. This makes it behave identically to `nullish`. However, both
22-
updaters are deprecated and the `??attr` binding should be used instead.
23-
- Interpolation of `textarea` is stricter. This used to be handled with some
24-
leniency — `<textarea>\n ${value} \n</textarea>`. Now, you have to fit the
25-
interpolation exactly — `<textarea></textarea>`.
24+
`undefined` in the default template engine. This makes it behave identically
25+
to `nullish` in the default template engine. However, both updaters are
26+
deprecated — the `??attr` binding should be used instead when using the
27+
default template engine (#204).
28+
- Interpolation of `textarea` is more strict in the default template engine.
29+
This used to be handled with some leniency for newlines in templates —
30+
`<textarea>\n ${value} \n</textarea>`. Now, you have to interpolate exactly —
31+
`<textarea>${value}</textarea>`.
2632

2733
### Deprecated
2834

2935
- The `ifDefined` and `nullish` updaters are deprecated, update templates to use
3036
syntax like `??foo="${bar}"`.
31-
- The `repeat` updater is deprecated, use `map` instead.
32-
- The `unsafeHTML` and `unsafeSVG` updaters are deprecated, use `unsafe`.
37+
- The `repeat` updater is deprecated, use `map` instead (#204).
38+
- The `unsafeHTML` and `unsafeSVG` updaters are deprecated, use `unsafe` (#204).
3339
- The `plaintext` tag is no longer handled. This is a deprecated html tag which
3440
required special handling… but it’s unlikely that anyone is using that.
3541

3642
### Fixed
3743

38-
- Transitions from different content values should all now work. For example,
39-
you previously could not change from a text value to an array. Additionally,
40-
state is properly cleared when going from one value type to another — e.g.,
41-
when going from `unsafe` back to `null`.
42-
- The `map` updater throws immediately when given non-array input. Previously,
43-
it only threw _just before_ it was bound as content.
44-
- Dummy content cursor is no longer appended to end of template. This was an
45-
innocuous off-by-one error when creating instrumented html from the tagged
46-
template strings.
44+
- Transitions from different content values should all now work for the default
45+
template engine. For example, you previously could not change from a text
46+
value to an array. Additionally, state is properly cleared when going from one
47+
value type to another — e.g., when going from `unsafe` back to `null`.
48+
- The `map` updater throws immediately when given non-array input for the
49+
default template engine. Previously, it only threw _just before_ it was bound.
50+
- Dummy content cursor is no longer appended to end of template for the default
51+
template engine. This was an innocuous off-by-one error when creating
52+
instrumented html from the tagged template strings.
4753

4854
## [1.1.1] - 2024-11-09
4955

doc/TEMPLATES.md

Lines changed: 13 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,49 +21,19 @@ static template(html, { map }) {
2121
}
2222
```
2323

24-
The following binding types are supported:
25-
26-
| Type | Example |
27-
| :------------------ | :----------------------------------------- |
28-
| attribute | `<span id="target" foo="${bar}"></span>` |
29-
| attribute (boolean) | `<span id="target" ?foo="${bar}"></span>` |
30-
| attribute (defined) | `<span id="target" ??foo="${bar}"></span>` |
31-
| property | `<span id="target" .foo="${bar}"></span>` |
32-
| content | `<span id="target">${foo}</span>` |
33-
34-
Emulates:
35-
36-
```javascript
37-
const el = document.createElement('div');
38-
el.attachShadow({ mode: 'open' });
39-
el.innerHTML = '<span id="target"></span>';
40-
const target = el.shadowRoot.getElementById('target');
41-
42-
// attribute value bindings set the attribute value
43-
target.setAttribute('foo', bar);
44-
45-
// attribute boolean bindings set the attribute to an empty string or remove
46-
target.setAttribute('foo', ''); // when bar is truthy
47-
target.removeAttribute('foo'); // when bar is falsy
48-
49-
// attribute defined bindings set the attribute if the value is non-nullish
50-
target.setAttribute('foo', bar); // when bar is non-nullish
51-
target.removeAttribute('foo'); // when bar is nullish
52-
53-
// property bindings assign the value to the property of the node
54-
target.foo = bar;
55-
56-
// content bindings create text nodes for basic content
57-
const text = document.createTextNode('');
58-
text.textContent = foo;
59-
target.append(text);
60-
61-
// content bindings append a child for singular, nested content
62-
target.append(foo);
63-
64-
// content binding maps and appends children for arrays of nested content
65-
target.append(...foo);
66-
```
24+
The following bindings are supported:
25+
26+
| Binding | Template | Emulates |
27+
| :------------------ | :--------------------------- | :------------------------------------------------------------ |
28+
| -- | -- | `const el = document.createElement('div');` |
29+
| attribute | `<div foo="${bar}"></div>` | `el.setAttribute('foo', bar);` |
30+
| attribute (boolean) | `<div ?foo="${bar}"></div>` | `el.setAttribute('foo', ''); // if “bar” is truthy` |
31+
| -- | -- | `el.removeAttribute('foo'); // if “bar” is falsy` |
32+
| attribute (defined) | `<div ??foo="${bar}"></div>` | `el.setAttribute('foo', bar); // if “bar” is non-nullish` |
33+
| -- | -- | `el.removeAttribute('foo'); // if “bar” is nullish` |
34+
| property | `<div .foo="${bar}"></div>` | `el.foo = bar;` |
35+
| content | `<div>${foo}</div>` | `el.append(document.createTextNode(foo)) // if “bar” is text` |
36+
| -- | -- | (see [content binding](#content-binding) for composition) |
6737

6838
**Important note on serialization during data binding:**
6939

test/test-template-engine.js

Lines changed: 8 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ describe('html updaters', () => {
564564

565565
it('unsafe html', () => {
566566
const getTemplate = ({ content }) => {
567-
return html`<div id="target">${unsafe(content, 'html')}</div>`;
567+
return html`<div id="target">${unsafe(content)}</div>`;
568568
};
569569
const container = document.createElement('div');
570570
document.body.append(container);
@@ -892,7 +892,7 @@ describe('html updaters', () => {
892892
const resolve = (type, value) => {
893893
switch(type) {
894894
case 'map': return map(value, item => item.id, item => html`<div id="${item.id}"></div>`);
895-
case 'html': return unsafe(value, 'html');
895+
case 'html': return unsafe(value);
896896
default: return value; // E.g., an array, some text, null, undefined, etc.
897897
}
898898
};
@@ -1032,31 +1032,6 @@ describe('svg rendering', () => {
10321032
});
10331033

10341034
describe('svg updaters', () => {
1035-
it('unsafe svg', () => {
1036-
const getTemplate = ({ content }) => {
1037-
return html`
1038-
<svg
1039-
id="target"
1040-
xmlns="http://www.w3.org/2000/svg"
1041-
viewBox="0 0 100 100"
1042-
style="width: 100px; height: 100px;">
1043-
${unsafe(content, 'svg')}
1044-
</svg>
1045-
`;
1046-
};
1047-
const container = document.createElement('div');
1048-
document.body.append(container);
1049-
render(container, getTemplate({ content: '<circle id="injected" r="10" cx="50" cy="50"></circle>' }));
1050-
assert(!!container.querySelector('#injected'));
1051-
assert(container.querySelector('#injected').getBoundingClientRect().height = 20);
1052-
assert(container.querySelector('#injected').getBoundingClientRect().width = 20);
1053-
render(container, getTemplate({ content: '<circle id="injected" r="5" cx="50" cy="50"></circle>' }));
1054-
assert(!!container.querySelector('#injected'));
1055-
assert(container.querySelector('#injected').getBoundingClientRect().height = 10);
1056-
assert(container.querySelector('#injected').getBoundingClientRect().width = 10);
1057-
container.remove();
1058-
});
1059-
10601035
it('unsafeSVG', () => {
10611036
const getTemplate = ({ content }) => {
10621037
return html`
@@ -1476,28 +1451,10 @@ describe('rendering errors', () => {
14761451

14771452

14781453
describe('unsafe', () => {
1479-
it('throws if used on an unexpected language', () => {
1480-
const expected = 'Unexpected unsafe language "css". Expected "html" or "svg".';
1481-
const getTemplate = ({ maybe }) => {
1482-
return html`<div id="target" maybe="${unsafe(maybe, 'css')}"></div>`;
1483-
};
1484-
const container = document.createElement('div');
1485-
document.body.append(container);
1486-
let actual;
1487-
try {
1488-
render(container, getTemplate({ maybe: 'yes' }));
1489-
} catch (error) {
1490-
actual = error.message;
1491-
}
1492-
assert(!!actual, 'No error was thrown.');
1493-
assert(actual === expected, actual);
1494-
container.remove();
1495-
});
1496-
14971454
it('throws if used on an "attribute"', () => {
14981455
const expected = 'The unsafe update must be used on content, not on an attribute.';
14991456
const getTemplate = ({ maybe }) => {
1500-
return html`<div id="target" maybe="${unsafe(maybe, 'html')}"></div>`;
1457+
return html`<div id="target" maybe="${unsafe(maybe)}"></div>`;
15011458
};
15021459
const container = document.createElement('div');
15031460
document.body.append(container);
@@ -1515,7 +1472,7 @@ describe('rendering errors', () => {
15151472
it('throws if used on a "boolean"', () => {
15161473
const expected = 'The unsafe update must be used on content, not on a boolean attribute.';
15171474
const getTemplate = ({ maybe }) => {
1518-
return html`<div id="target" ?maybe="${unsafe(maybe, 'html')}"></div>`;
1475+
return html`<div id="target" ?maybe="${unsafe(maybe)}"></div>`;
15191476
};
15201477
const container = document.createElement('div');
15211478
document.body.append(container);
@@ -1533,7 +1490,7 @@ describe('rendering errors', () => {
15331490
it('throws if used on a "defined"', () => {
15341491
const expected = 'The unsafe update must be used on content, not on a defined attribute.';
15351492
const getTemplate = ({ maybe }) => {
1536-
return html`<div id="target" ??maybe="${unsafe(maybe, 'html')}"></div>`;
1493+
return html`<div id="target" ??maybe="${unsafe(maybe)}"></div>`;
15371494
};
15381495
const container = document.createElement('div');
15391496
document.body.append(container);
@@ -1551,7 +1508,7 @@ describe('rendering errors', () => {
15511508
it('throws if used with a "property"', () => {
15521509
const expected = 'The unsafe update must be used on content, not on a property.';
15531510
const getTemplate = ({ maybe }) => {
1554-
return html`<div id="target" .maybe="${unsafe(maybe, 'html')}"></div>`;
1511+
return html`<div id="target" .maybe="${unsafe(maybe)}"></div>`;
15551512
};
15561513
const container = document.createElement('div');
15571514
document.body.append(container);
@@ -1569,7 +1526,7 @@ describe('rendering errors', () => {
15691526
it('throws if used with "text"', () => {
15701527
const expected = 'The unsafe update must be used on content, not on text content.';
15711528
const getTemplate = ({ maybe }) => {
1572-
return html`<textarea id="target">${unsafe(maybe, 'html')}</textarea>`;
1529+
return html`<textarea id="target">${unsafe(maybe)}</textarea>`;
15731530
};
15741531
const container = document.createElement('div');
15751532
document.body.append(container);
@@ -1588,7 +1545,7 @@ describe('rendering errors', () => {
15881545
const getTemplate = ({ content }) => {
15891546
return html`
15901547
<div id="target">
1591-
${unsafe(content, 'html')}
1548+
${unsafe(content)}
15921549
</div>
15931550
`;
15941551
};

0 commit comments

Comments
 (0)