@prairielearn/html 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +21 -0
- package/README.md +2 -20
- package/dist/index.d.ts +0 -13
- package/dist/index.js +35 -42
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +27 -37
- package/dist/index.test.js.map +1 -1
- package/package.json +6 -9
- package/src/index.test.ts +5 -14
- package/src/index.ts +26 -22
- package/tsconfig.json +2 -5
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @prairielearn/html
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 56f1333fc: `renderEjs` function moved to `@prairielearn/html-ejs`
|
|
8
|
+
|
|
9
|
+
In order to be able to use the `@prairielearn/html` package inside client scripts, EJS functionality was moved to a separate package (`@prairielearn/html-ejs`). The `ejs` package relies on Node-only packages like `fs` and `path`, which renders it unusable in browsers.
|
|
10
|
+
|
|
11
|
+
## 1.0.2
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- d3ed76de3: Change transpiled code to use ES2020 syntax
|
|
16
|
+
|
|
17
|
+
## 1.0.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 5752fab5b: Fix rendering of null values in templates
|
package/README.md
CHANGED
|
@@ -40,26 +40,6 @@ console.log(html`
|
|
|
40
40
|
`);
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
### Using with EJS
|
|
44
|
-
|
|
45
|
-
If you have an EJS partial that you'd like to use inside of an `html` tagged template literal, you can use the `renderEjs` helper:
|
|
46
|
-
|
|
47
|
-
```html
|
|
48
|
-
<!-- hello.ejs -->
|
|
49
|
-
Hello, <%= name %>!
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
```js
|
|
53
|
-
const { hmtl, renderEjs } = require('@prairielearn/html');
|
|
54
|
-
|
|
55
|
-
console.log(
|
|
56
|
-
html`
|
|
57
|
-
<div>Hello, world!</div>
|
|
58
|
-
<div>${renderEjs(__filename, "<%- include('./hello'); %>", { name: 'Anjali' })}</div>
|
|
59
|
-
`.toString()
|
|
60
|
-
);
|
|
61
|
-
```
|
|
62
|
-
|
|
63
43
|
## Why not EJS?
|
|
64
44
|
|
|
65
45
|
PrairieLearn used (and still uses) EJS to render most views. However, using a tagged template literal and pure JavaScript to render views has a number of advantages:
|
|
@@ -67,3 +47,5 @@ PrairieLearn used (and still uses) EJS to render most views. However, using a ta
|
|
|
67
47
|
- Prettier will automatically format the content of any `html` tagged template literal; EJS does not have any automatic formatters.
|
|
68
48
|
- Authoring views in pure JavaScript allows for easier and more explicit composition of components.
|
|
69
49
|
- It's possible to use ESLint and TypeScript to type-check JavaScript views; EJS does not offer support for either.
|
|
50
|
+
|
|
51
|
+
If you want to use existing EJS partials inside of an `html` tagged template literal, check out the `@prairielearn/html-ejs` package. EJS-related functionality is deliberately located in a separate package so that `@prairielearn/html` can be used in the browser, since the `ejs` package makes use of Node-only features.
|
package/dist/index.d.ts
CHANGED
|
@@ -19,16 +19,3 @@ export declare function escapeHtml(html: HtmlSafeString): HtmlSafeString;
|
|
|
19
19
|
* @returns An {@link HtmlSafeString} representing the provided value.
|
|
20
20
|
*/
|
|
21
21
|
export declare function unsafeHtml(value: string): HtmlSafeString;
|
|
22
|
-
/**
|
|
23
|
-
* This is a shim to allow for the use of EJS templates inside of HTML tagged
|
|
24
|
-
* template literals.
|
|
25
|
-
*
|
|
26
|
-
* The resulting string is assumed to be appropriately escaped and will be used
|
|
27
|
-
* verbatim in the resulting HTML.
|
|
28
|
-
*
|
|
29
|
-
* @param filename The name of the file from which relative includes should be resolved.
|
|
30
|
-
* @param template The raw EJS template string.
|
|
31
|
-
* @param data Any data to be made available to the template.
|
|
32
|
-
* @returns The rendered EJS.
|
|
33
|
-
*/
|
|
34
|
-
export declare function renderEjs(filename: string, template: string, data?: any): HtmlSafeString;
|
package/dist/index.js
CHANGED
|
@@ -1,50 +1,60 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.unsafeHtml = exports.escapeHtml = exports.html = exports.HtmlSafeString = void 0;
|
|
4
|
+
const ENCODE_HTML_RULES = {
|
|
5
|
+
'&': '&',
|
|
6
|
+
'<': '<',
|
|
7
|
+
'>': '>',
|
|
8
|
+
'"': '"',
|
|
9
|
+
"'": ''',
|
|
4
10
|
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
var MATCH_HTML = /[&<>'"]/g;
|
|
12
|
+
function encodeCharacter(c) {
|
|
13
|
+
return ENCODE_HTML_RULES[c] || c;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Based on the `escapeXML` function from the `ejs` library.
|
|
17
|
+
*/
|
|
18
|
+
function escapeHtmlRaw(value) {
|
|
19
|
+
return value == undefined ? '' : String(value).replace(MATCH_HTML, encodeCharacter);
|
|
20
|
+
}
|
|
9
21
|
function escapeValue(value) {
|
|
10
22
|
if (value instanceof HtmlSafeString) {
|
|
11
23
|
// Already escaped!
|
|
12
24
|
return value.toString();
|
|
13
25
|
}
|
|
14
26
|
else if (Array.isArray(value)) {
|
|
15
|
-
return value.map(
|
|
27
|
+
return value.map((val) => escapeValue(val)).join('');
|
|
16
28
|
}
|
|
17
29
|
else if (typeof value === 'string' || typeof value === 'number') {
|
|
18
|
-
return
|
|
30
|
+
return escapeHtmlRaw(String(value));
|
|
31
|
+
}
|
|
32
|
+
else if (value == null) {
|
|
33
|
+
// undefined or null -- render nothing
|
|
34
|
+
return '';
|
|
19
35
|
}
|
|
20
36
|
else if (typeof value === 'object') {
|
|
21
|
-
throw new Error(
|
|
37
|
+
throw new Error(`Cannot interpolate object in template: ${JSON.stringify(value)}`);
|
|
22
38
|
}
|
|
23
39
|
else {
|
|
24
|
-
// This is
|
|
40
|
+
// This is boolean - don't render anything here.
|
|
25
41
|
return '';
|
|
26
42
|
}
|
|
27
43
|
}
|
|
28
44
|
// Based on https://github.com/Janpot/escape-html-template-tag
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
class HtmlSafeString {
|
|
46
|
+
constructor(strings, values) {
|
|
31
47
|
this.strings = strings;
|
|
32
48
|
this.values = values;
|
|
33
49
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return acc + escapeValue(val) + _this.strings[i + 1];
|
|
50
|
+
toString() {
|
|
51
|
+
return this.values.reduce((acc, val, i) => {
|
|
52
|
+
return acc + escapeValue(val) + this.strings[i + 1];
|
|
38
53
|
}, this.strings[0]);
|
|
39
|
-
};
|
|
40
|
-
return HtmlSafeString;
|
|
41
|
-
}());
|
|
42
|
-
exports.HtmlSafeString = HtmlSafeString;
|
|
43
|
-
function html(strings) {
|
|
44
|
-
var values = [];
|
|
45
|
-
for (var _i = 1; _i < arguments.length; _i++) {
|
|
46
|
-
values[_i - 1] = arguments[_i];
|
|
47
54
|
}
|
|
55
|
+
}
|
|
56
|
+
exports.HtmlSafeString = HtmlSafeString;
|
|
57
|
+
function html(strings, ...values) {
|
|
48
58
|
return new HtmlSafeString(strings, values);
|
|
49
59
|
}
|
|
50
60
|
exports.html = html;
|
|
@@ -54,7 +64,7 @@ exports.html = html;
|
|
|
54
64
|
* popover.
|
|
55
65
|
*/
|
|
56
66
|
function escapeHtml(html) {
|
|
57
|
-
return unsafeHtml(
|
|
67
|
+
return unsafeHtml(escapeHtmlRaw(html.toString()));
|
|
58
68
|
}
|
|
59
69
|
exports.escapeHtml = escapeHtml;
|
|
60
70
|
/**
|
|
@@ -68,21 +78,4 @@ function unsafeHtml(value) {
|
|
|
68
78
|
return new HtmlSafeString([value], []);
|
|
69
79
|
}
|
|
70
80
|
exports.unsafeHtml = unsafeHtml;
|
|
71
|
-
/**
|
|
72
|
-
* This is a shim to allow for the use of EJS templates inside of HTML tagged
|
|
73
|
-
* template literals.
|
|
74
|
-
*
|
|
75
|
-
* The resulting string is assumed to be appropriately escaped and will be used
|
|
76
|
-
* verbatim in the resulting HTML.
|
|
77
|
-
*
|
|
78
|
-
* @param filename The name of the file from which relative includes should be resolved.
|
|
79
|
-
* @param template The raw EJS template string.
|
|
80
|
-
* @param data Any data to be made available to the template.
|
|
81
|
-
* @returns The rendered EJS.
|
|
82
|
-
*/
|
|
83
|
-
function renderEjs(filename, template, data) {
|
|
84
|
-
if (data === void 0) { data = {}; }
|
|
85
|
-
return unsafeHtml(ejs_1["default"].render(template, data, { views: [path_1["default"].dirname(filename)] }));
|
|
86
|
-
}
|
|
87
|
-
exports.renderEjs = renderEjs;
|
|
88
81
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,MAAM,iBAAiB,GAAG;IACxB,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,MAAM;IACX,GAAG,EAAE,OAAO;IACZ,GAAG,EAAE,OAAO;CACb,CAAC;AACF,IAAI,UAAU,GAAG,UAAU,CAAC;AAE5B,SAAS,eAAe,CAAC,CAAS;IAChC,OAAO,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED;;GAEG;AACH,SAAS,aAAa,CAAC,KAAa;IAClC,OAAO,KAAK,IAAI,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;AACtF,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,KAAK,YAAY,cAAc,EAAE;QACnC,mBAAmB;QACnB,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;KACzB;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;QAC/B,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;KACtD;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QACjE,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;KACrC;SAAM,IAAI,KAAK,IAAI,IAAI,EAAE;QACxB,sCAAsC;QACtC,OAAO,EAAE,CAAC;KACX;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QACpC,MAAM,IAAI,KAAK,CAAC,0CAA0C,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;KACpF;SAAM;QACL,gDAAgD;QAChD,OAAO,EAAE,CAAC;KACX;AACH,CAAC;AAED,8DAA8D;AAC9D,MAAa,cAAc;IAIzB,YAAY,OAA8B,EAAE,MAAiB;QAC3D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAS,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE;YAChD,OAAO,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACtD,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC;CACF;AAdD,wCAcC;AAED,SAAgB,IAAI,CAAC,OAA6B,EAAE,GAAG,MAAa;IAClE,OAAO,IAAI,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;AAC7C,CAAC;AAFD,oBAEC;AAED;;;;GAIG;AACH,SAAgB,UAAU,CAAC,IAAoB;IAC7C,OAAO,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;AACpD,CAAC;AAFD,gCAEC;AAED;;;;;;GAMG;AACH,SAAgB,UAAU,CAAC,KAAa;IACtC,OAAO,IAAI,cAAc,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;AACzC,CAAC;AAFD,gCAEC"}
|
package/dist/index.test.js
CHANGED
|
@@ -1,52 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
chai_1.assert.equal((0, index_1.html)(templateObject_2 || (templateObject_2 = __makeTemplateObject(["<p>", " and ", "</p>"], ["<p>", " and ", "</p>"])), 'cats', 'dogs').toString(), '<p>cats and dogs</p>');
|
|
15
|
-
});
|
|
16
|
-
it('escapes values when rendering array', function () {
|
|
17
|
-
var arr = ['cats>', '<dogs'];
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chai_1 = require("chai");
|
|
4
|
+
const index_1 = require("./index");
|
|
5
|
+
describe('html', () => {
|
|
6
|
+
it('escapes string value', () => {
|
|
7
|
+
chai_1.assert.equal((0, index_1.html) `<p>${'<script>'}</p>`.toString(), '<p><script></p>');
|
|
8
|
+
});
|
|
9
|
+
it('interpolates multiple values', () => {
|
|
10
|
+
chai_1.assert.equal((0, index_1.html) `<p>${'cats'} and ${'dogs'}</p>`.toString(), '<p>cats and dogs</p>');
|
|
11
|
+
});
|
|
12
|
+
it('escapes values when rendering array', () => {
|
|
13
|
+
const arr = ['cats>', '<dogs'];
|
|
18
14
|
chai_1.assert.equal(
|
|
19
15
|
// prettier-ignore
|
|
20
|
-
(0, index_1.html)
|
|
16
|
+
(0, index_1.html) `<ul>${arr}</ul>`.toString(), '<ul>cats><dogs</ul>');
|
|
21
17
|
});
|
|
22
|
-
it('does not double-escape values when rendering array',
|
|
23
|
-
|
|
18
|
+
it('does not double-escape values when rendering array', () => {
|
|
19
|
+
const arr = ['cats', 'dogs'];
|
|
24
20
|
chai_1.assert.equal(
|
|
25
21
|
// prettier-ignore
|
|
26
|
-
(0, index_1.html)
|
|
22
|
+
(0, index_1.html) `<ul>${arr.map((e) => (0, index_1.html) `<li>${e}</li>`)}</ul>`.toString(), '<ul><li>cats</li><li>dogs</li></ul>');
|
|
27
23
|
});
|
|
28
|
-
it('errors when interpolating object',
|
|
29
|
-
chai_1.assert.throws(
|
|
24
|
+
it('errors when interpolating object', () => {
|
|
25
|
+
chai_1.assert.throws(() => (0, index_1.html) `<p>${{ foo: 'bar' }}</p>`.toString(), 'Cannot interpolate object in template');
|
|
30
26
|
});
|
|
31
|
-
it('omits boolean values from template',
|
|
32
|
-
chai_1.assert.equal((0, index_1.html)
|
|
27
|
+
it('omits boolean values from template', () => {
|
|
28
|
+
chai_1.assert.equal((0, index_1.html) `<p>${true}${false}</p>`.toString(), '<p></p>');
|
|
33
29
|
});
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
it('escapes rendered HTML', function () {
|
|
37
|
-
chai_1.assert.equal((0, index_1.escapeHtml)((0, index_1.html)(templateObject_8 || (templateObject_8 = __makeTemplateObject(["<p>Hello</p>"], ["<p>Hello</p>"])))).toString(), '<p>Hello</p>');
|
|
38
|
-
});
|
|
39
|
-
it('works when nested inside html tag', function () {
|
|
40
|
-
chai_1.assert.equal((0, index_1.html)(templateObject_10 || (templateObject_10 = __makeTemplateObject(["a", "b"], ["a", "b"])), (0, index_1.escapeHtml)((0, index_1.html)(templateObject_9 || (templateObject_9 = __makeTemplateObject(["<p></p>"], ["<p></p>"]))))).toString(), 'a<p></p>b');
|
|
30
|
+
it('omits nullish values from template', () => {
|
|
31
|
+
chai_1.assert.equal((0, index_1.html) `<p>${null}${undefined}</p>`.toString(), '<p></p>');
|
|
41
32
|
});
|
|
42
33
|
});
|
|
43
|
-
describe('
|
|
44
|
-
it('
|
|
45
|
-
chai_1.assert.equal((0, index_1.
|
|
34
|
+
describe('escapeHtml', () => {
|
|
35
|
+
it('escapes rendered HTML', () => {
|
|
36
|
+
chai_1.assert.equal((0, index_1.escapeHtml)((0, index_1.html) `<p>Hello</p>`).toString(), '<p>Hello</p>');
|
|
46
37
|
});
|
|
47
|
-
it('
|
|
48
|
-
chai_1.assert.equal((0, index_1.
|
|
38
|
+
it('works when nested inside html tag', () => {
|
|
39
|
+
chai_1.assert.equal((0, index_1.html) `a${(0, index_1.escapeHtml)((0, index_1.html) `<p></p>`)}b`.toString(), 'a<p></p>b');
|
|
49
40
|
});
|
|
50
41
|
});
|
|
51
|
-
var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10;
|
|
52
42
|
//# sourceMappingURL=index.test.js.map
|
package/dist/index.test.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":";;AAAA,+BAA8B;AAE9B,mCAA2C;AAE3C,QAAQ,CAAC,MAAM,EAAE,GAAG,EAAE;IACpB,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,aAAM,CAAC,KAAK,CAAC,IAAA,YAAI,EAAA,MAAM,UAAU,MAAM,CAAC,QAAQ,EAAE,EAAE,uBAAuB,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,aAAM,CAAC,KAAK,CAAC,IAAA,YAAI,EAAA,MAAM,MAAM,QAAQ,MAAM,MAAM,CAAC,QAAQ,EAAE,EAAE,sBAAsB,CAAC,CAAC;IACxF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,GAAG,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC/B,aAAM,CAAC,KAAK;QACV,kBAAkB;QAClB,IAAA,YAAI,EAAA,OAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,EAChC,2BAA2B,CAC5B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC7B,aAAM,CAAC,KAAK;QACV,kBAAkB;QAClB,IAAA,YAAI,EAAA,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAA,YAAI,EAAA,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,EAChE,qCAAqC,CACtC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,aAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,IAAA,YAAI,EAAA,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,EAC/C,uCAAuC,CACxC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,aAAM,CAAC,KAAK,CAAC,IAAA,YAAI,EAAA,MAAM,IAAI,GAAG,KAAK,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,aAAM,CAAC,KAAK,CAAC,IAAA,YAAI,EAAA,MAAM,IAAI,GAAG,SAAS,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,aAAM,CAAC,KAAK,CAAC,IAAA,kBAAU,EAAC,IAAA,YAAI,EAAA,cAAc,CAAC,CAAC,QAAQ,EAAE,EAAE,0BAA0B,CAAC,CAAC;IACtF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,aAAM,CAAC,KAAK,CAAC,IAAA,YAAI,EAAA,IAAI,IAAA,kBAAU,EAAC,IAAA,YAAI,EAAA,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,uBAAuB,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/html",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsc",
|
|
7
7
|
"dev": "tsc --watch --preserveWatchOutput",
|
|
8
|
-
"test": "mocha --require ts-node/register src/index.test.ts"
|
|
9
|
-
},
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"ejs": "^3.1.6"
|
|
8
|
+
"test": "mocha --no-config --require ts-node/register src/index.test.ts"
|
|
12
9
|
},
|
|
13
10
|
"devDependencies": {
|
|
14
|
-
"@
|
|
15
|
-
"mocha": "^9.2.
|
|
16
|
-
"ts-node": "^10.
|
|
17
|
-
"typescript": "^4.
|
|
11
|
+
"@prairielearn/tsconfig": "*",
|
|
12
|
+
"mocha": "^9.2.2",
|
|
13
|
+
"ts-node": "^10.7.0",
|
|
14
|
+
"typescript": "^4.6.3"
|
|
18
15
|
}
|
|
19
16
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assert } from 'chai';
|
|
2
2
|
|
|
3
|
-
import { escapeHtml, html
|
|
3
|
+
import { escapeHtml, html } from './index';
|
|
4
4
|
|
|
5
5
|
describe('html', () => {
|
|
6
6
|
it('escapes string value', () => {
|
|
@@ -39,6 +39,10 @@ describe('html', () => {
|
|
|
39
39
|
it('omits boolean values from template', () => {
|
|
40
40
|
assert.equal(html`<p>${true}${false}</p>`.toString(), '<p></p>');
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
it('omits nullish values from template', () => {
|
|
44
|
+
assert.equal(html`<p>${null}${undefined}</p>`.toString(), '<p></p>');
|
|
45
|
+
});
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
describe('escapeHtml', () => {
|
|
@@ -50,16 +54,3 @@ describe('escapeHtml', () => {
|
|
|
50
54
|
assert.equal(html`a${escapeHtml(html`<p></p>`)}b`.toString(), 'a<p></p>b');
|
|
51
55
|
});
|
|
52
56
|
});
|
|
53
|
-
|
|
54
|
-
describe('renderEjs', () => {
|
|
55
|
-
it('renders EJS template without data', () => {
|
|
56
|
-
assert.equal(renderEjs(__filename, '<p>Hello</p>', {}).toString(), '<p>Hello</p>');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('renders EJS template with data', () => {
|
|
60
|
-
assert.equal(
|
|
61
|
-
renderEjs(__filename, '<p>Hello <%= name %></p>', { name: 'Divya' }).toString(),
|
|
62
|
-
'<p>Hello Divya</p>'
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
});
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const ENCODE_HTML_RULES = {
|
|
2
|
+
'&': '&',
|
|
3
|
+
'<': '<',
|
|
4
|
+
'>': '>',
|
|
5
|
+
'"': '"',
|
|
6
|
+
"'": ''',
|
|
7
|
+
};
|
|
8
|
+
var MATCH_HTML = /[&<>'"]/g;
|
|
9
|
+
|
|
10
|
+
function encodeCharacter(c: string) {
|
|
11
|
+
return ENCODE_HTML_RULES[c] || c;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Based on the `escapeXML` function from the `ejs` library.
|
|
16
|
+
*/
|
|
17
|
+
function escapeHtmlRaw(value: string): string {
|
|
18
|
+
return value == undefined ? '' : String(value).replace(MATCH_HTML, encodeCharacter);
|
|
19
|
+
}
|
|
3
20
|
|
|
4
21
|
function escapeValue(value: unknown): string {
|
|
5
22
|
if (value instanceof HtmlSafeString) {
|
|
@@ -8,11 +25,14 @@ function escapeValue(value: unknown): string {
|
|
|
8
25
|
} else if (Array.isArray(value)) {
|
|
9
26
|
return value.map((val) => escapeValue(val)).join('');
|
|
10
27
|
} else if (typeof value === 'string' || typeof value === 'number') {
|
|
11
|
-
return
|
|
28
|
+
return escapeHtmlRaw(String(value));
|
|
29
|
+
} else if (value == null) {
|
|
30
|
+
// undefined or null -- render nothing
|
|
31
|
+
return '';
|
|
12
32
|
} else if (typeof value === 'object') {
|
|
13
|
-
throw new Error(
|
|
33
|
+
throw new Error(`Cannot interpolate object in template: ${JSON.stringify(value)}`);
|
|
14
34
|
} else {
|
|
15
|
-
// This is
|
|
35
|
+
// This is boolean - don't render anything here.
|
|
16
36
|
return '';
|
|
17
37
|
}
|
|
18
38
|
}
|
|
@@ -44,7 +64,7 @@ export function html(strings: TemplateStringsArray, ...values: any[]): HtmlSafeS
|
|
|
44
64
|
* popover.
|
|
45
65
|
*/
|
|
46
66
|
export function escapeHtml(html: HtmlSafeString): HtmlSafeString {
|
|
47
|
-
return unsafeHtml(
|
|
67
|
+
return unsafeHtml(escapeHtmlRaw(html.toString()));
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
/**
|
|
@@ -57,19 +77,3 @@ export function escapeHtml(html: HtmlSafeString): HtmlSafeString {
|
|
|
57
77
|
export function unsafeHtml(value: string): HtmlSafeString {
|
|
58
78
|
return new HtmlSafeString([value], []);
|
|
59
79
|
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* This is a shim to allow for the use of EJS templates inside of HTML tagged
|
|
63
|
-
* template literals.
|
|
64
|
-
*
|
|
65
|
-
* The resulting string is assumed to be appropriately escaped and will be used
|
|
66
|
-
* verbatim in the resulting HTML.
|
|
67
|
-
*
|
|
68
|
-
* @param filename The name of the file from which relative includes should be resolved.
|
|
69
|
-
* @param template The raw EJS template string.
|
|
70
|
-
* @param data Any data to be made available to the template.
|
|
71
|
-
* @returns The rendered EJS.
|
|
72
|
-
*/
|
|
73
|
-
export function renderEjs(filename: string, template: string, data: any = {}): HtmlSafeString {
|
|
74
|
-
return unsafeHtml(ejs.render(template, data, { views: [path.dirname(filename)] }));
|
|
75
|
-
}
|