@prairielearn/html 1.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.
@@ -0,0 +1,3 @@
1
+ @prairielearn/html:build: cache hit, replaying output c36dba0838eb5e15
2
+ @prairielearn/html:build: warning package.json: No license field
3
+ @prairielearn/html:build: $ tsc
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # `@prairielearn/html`
2
+
3
+ Utilities for easily rendering HTML from within JavaScript.
4
+
5
+ ## Usage
6
+
7
+ The `html` tagged template literal can be used to render HTML while ensuring that any interpolated values are properly escaped.
8
+
9
+ By convention, HTML templates are located in `*.html.tmpl.js` files.
10
+
11
+ ```js
12
+ // Hello.html.tmpl.js
13
+ const { hmtl } = require('@prairielearn/html');
14
+
15
+ module.exports.Hello = function Hello({ name }) {
16
+ return html`<div>Hello, ${name}!</div>`;
17
+ };
18
+ ```
19
+
20
+ This can then be used to render a string:
21
+
22
+ ```js
23
+ const { Hello } = require('./Hello');
24
+
25
+ console.log(Hello({ name: 'Anjali' }).toString());
26
+ // Prints "<div>Hello, Anjali!</div>"
27
+ ```
28
+
29
+ ### Using escaped HTML
30
+
31
+ If you want to pre-escape some HTML, you can wrap it in `escapeHtml` to avoid escaping it twice. This is useful if you want to inline some HTML into an attribute, for instance with a Bootstrap popover.
32
+
33
+ ```js
34
+ const { html, escapeHtml } = require('@prairielearn/html');
35
+
36
+ console.log(html`
37
+ <button data-bs-toggle="popover" data-bs-content="${escapeHtml(html`<div>Content here</div>`)}">
38
+ Open popover
39
+ </button>
40
+ `);
41
+ ```
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
+ ## Why not EJS?
64
+
65
+ 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:
66
+
67
+ - Prettier will automatically format the content of any `html` tagged template literal; EJS does not have any automatic formatters.
68
+ - Authoring views in pure JavaScript allows for easier and more explicit composition of components.
69
+ - It's possible to use ESLint and TypeScript to type-check JavaScript views; EJS does not offer support for either.
@@ -0,0 +1,34 @@
1
+ export declare class HtmlSafeString {
2
+ private readonly strings;
3
+ private readonly values;
4
+ constructor(strings: ReadonlyArray<string>, values: unknown[]);
5
+ toString(): string;
6
+ }
7
+ export declare function html(strings: TemplateStringsArray, ...values: any[]): HtmlSafeString;
8
+ /**
9
+ * Pre-escpapes the rendered HTML. Useful for when you want to inline the HTML
10
+ * in something else, for instance in a `data-content` attribute for a Bootstrap
11
+ * popover.
12
+ */
13
+ export declare function escapeHtml(html: HtmlSafeString): HtmlSafeString;
14
+ /**
15
+ * Will render the provided value without any additional escaping. Use carefully
16
+ * with user-provided data.
17
+ *
18
+ * @param value The value to render.
19
+ * @returns An {@link HtmlSafeString} representing the provided value.
20
+ */
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 ADDED
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ exports.__esModule = true;
6
+ exports.renderEjs = exports.unsafeHtml = exports.escapeHtml = exports.html = exports.HtmlSafeString = void 0;
7
+ var ejs_1 = __importDefault(require("ejs"));
8
+ var path_1 = __importDefault(require("path"));
9
+ function escapeValue(value) {
10
+ if (value instanceof HtmlSafeString) {
11
+ // Already escaped!
12
+ return value.toString();
13
+ }
14
+ else if (Array.isArray(value)) {
15
+ return value.map(function (val) { return escapeValue(val); }).join('');
16
+ }
17
+ else if (typeof value === 'string' || typeof value === 'number') {
18
+ return ejs_1["default"].escapeXML(String(value));
19
+ }
20
+ else if (typeof value === 'object') {
21
+ throw new Error('Cannot interpolate object in template');
22
+ }
23
+ else {
24
+ // This is undefined, null, or a boolean - don't render anything here.
25
+ return '';
26
+ }
27
+ }
28
+ // Based on https://github.com/Janpot/escape-html-template-tag
29
+ var HtmlSafeString = /** @class */ (function () {
30
+ function HtmlSafeString(strings, values) {
31
+ this.strings = strings;
32
+ this.values = values;
33
+ }
34
+ HtmlSafeString.prototype.toString = function () {
35
+ var _this = this;
36
+ return this.values.reduce(function (acc, val, i) {
37
+ return acc + escapeValue(val) + _this.strings[i + 1];
38
+ }, 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
+ }
48
+ return new HtmlSafeString(strings, values);
49
+ }
50
+ exports.html = html;
51
+ /**
52
+ * Pre-escpapes the rendered HTML. Useful for when you want to inline the HTML
53
+ * in something else, for instance in a `data-content` attribute for a Bootstrap
54
+ * popover.
55
+ */
56
+ function escapeHtml(html) {
57
+ return unsafeHtml(ejs_1["default"].escapeXML(html.toString()));
58
+ }
59
+ exports.escapeHtml = escapeHtml;
60
+ /**
61
+ * Will render the provided value without any additional escaping. Use carefully
62
+ * with user-provided data.
63
+ *
64
+ * @param value The value to render.
65
+ * @returns An {@link HtmlSafeString} representing the provided value.
66
+ */
67
+ function unsafeHtml(value) {
68
+ return new HtmlSafeString([value], []);
69
+ }
70
+ 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
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAsB;AACtB,8CAAwB;AAExB,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,UAAC,GAAG,IAAK,OAAA,WAAW,CAAC,GAAG,CAAC,EAAhB,CAAgB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;KACtD;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QACjE,OAAO,gBAAG,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;KACrC;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;QACpC,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;KAC1D;SAAM;QACL,sEAAsE;QACtE,OAAO,EAAE,CAAC;KACX;AACH,CAAC;AAED,8DAA8D;AAC9D;IAIE,wBAAY,OAA8B,EAAE,MAAiB;QAC3D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,iCAAQ,GAAR;QAAA,iBAIC;QAHC,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAS,UAAC,GAAG,EAAE,GAAG,EAAE,CAAC;YAC5C,OAAO,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,KAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACtD,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC;IACH,qBAAC;AAAD,CAAC,AAdD,IAcC;AAdY,wCAAc;AAgB3B,SAAgB,IAAI,CAAC,OAA6B;IAAE,gBAAgB;SAAhB,UAAgB,EAAhB,qBAAgB,EAAhB,IAAgB;QAAhB,+BAAgB;;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,gBAAG,CAAC,SAAS,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;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,SAAS,CAAC,QAAgB,EAAE,QAAgB,EAAE,IAAc;IAAd,qBAAA,EAAA,SAAc;IAC1E,OAAO,UAAU,CAAC,gBAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,iBAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACrF,CAAC;AAFD,8BAEC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
3
+ if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
4
+ return cooked;
5
+ };
6
+ exports.__esModule = true;
7
+ var chai_1 = require("chai");
8
+ var index_1 = require("./index");
9
+ describe('html', function () {
10
+ it('escapes string value', function () {
11
+ chai_1.assert.equal((0, index_1.html)(templateObject_1 || (templateObject_1 = __makeTemplateObject(["<p>", "</p>"], ["<p>", "</p>"])), '<script>').toString(), '<p>&lt;script&gt;</p>');
12
+ });
13
+ it('interpolates multiple values', function () {
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'];
18
+ chai_1.assert.equal(
19
+ // prettier-ignore
20
+ (0, index_1.html)(templateObject_3 || (templateObject_3 = __makeTemplateObject(["<ul>", "</ul>"], ["<ul>", "</ul>"])), arr).toString(), '<ul>cats&gt;&lt;dogs</ul>');
21
+ });
22
+ it('does not double-escape values when rendering array', function () {
23
+ var arr = ['cats', 'dogs'];
24
+ chai_1.assert.equal(
25
+ // prettier-ignore
26
+ (0, index_1.html)(templateObject_5 || (templateObject_5 = __makeTemplateObject(["<ul>", "</ul>"], ["<ul>", "</ul>"])), arr.map(function (e) { return (0, index_1.html)(templateObject_4 || (templateObject_4 = __makeTemplateObject(["<li>", "</li>"], ["<li>", "</li>"])), e); })).toString(), '<ul><li>cats</li><li>dogs</li></ul>');
27
+ });
28
+ it('errors when interpolating object', function () {
29
+ chai_1.assert.throws(function () { return (0, index_1.html)(templateObject_6 || (templateObject_6 = __makeTemplateObject(["<p>", "</p>"], ["<p>", "</p>"])), { foo: 'bar' }).toString(); }, 'Cannot interpolate object in template');
30
+ });
31
+ it('omits boolean values from template', function () {
32
+ chai_1.assert.equal((0, index_1.html)(templateObject_7 || (templateObject_7 = __makeTemplateObject(["<p>", "", "</p>"], ["<p>", "", "</p>"])), true, false).toString(), '<p></p>');
33
+ });
34
+ });
35
+ describe('escapeHtml', function () {
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(), '&lt;p&gt;Hello&lt;/p&gt;');
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&lt;p&gt;&lt;/p&gt;b');
41
+ });
42
+ });
43
+ describe('renderEjs', function () {
44
+ it('renders EJS template without data', function () {
45
+ chai_1.assert.equal((0, index_1.renderEjs)(__filename, '<p>Hello</p>', {}).toString(), '<p>Hello</p>');
46
+ });
47
+ it('renders EJS template with data', function () {
48
+ chai_1.assert.equal((0, index_1.renderEjs)(__filename, '<p>Hello <%= name %></p>', { name: 'Divya' }).toString(), '<p>Hello Divya</p>');
49
+ });
50
+ });
51
+ var templateObject_1, templateObject_2, templateObject_3, templateObject_4, templateObject_5, templateObject_6, templateObject_7, templateObject_8, templateObject_9, templateObject_10;
52
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@prairielearn/html",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "scripts": {
6
+ "build": "tsc",
7
+ "dev": "tsc --watch --preserveWatchOutput",
8
+ "test": "mocha --require ts-node/register src/index.test.ts"
9
+ },
10
+ "dependencies": {
11
+ "ejs": "^3.1.6"
12
+ },
13
+ "devDependencies": {
14
+ "@types/ejs": "^3.1.0",
15
+ "mocha": "^9.2.0",
16
+ "ts-node": "^10.5.0",
17
+ "typescript": "^4.5.5"
18
+ }
19
+ }
@@ -0,0 +1,65 @@
1
+ import { assert } from 'chai';
2
+
3
+ import { escapeHtml, html, renderEjs } from './index';
4
+
5
+ describe('html', () => {
6
+ it('escapes string value', () => {
7
+ assert.equal(html`<p>${'<script>'}</p>`.toString(), '<p>&lt;script&gt;</p>');
8
+ });
9
+
10
+ it('interpolates multiple values', () => {
11
+ assert.equal(html`<p>${'cats'} and ${'dogs'}</p>`.toString(), '<p>cats and dogs</p>');
12
+ });
13
+
14
+ it('escapes values when rendering array', () => {
15
+ const arr = ['cats>', '<dogs'];
16
+ assert.equal(
17
+ // prettier-ignore
18
+ html`<ul>${arr}</ul>`.toString(),
19
+ '<ul>cats&gt;&lt;dogs</ul>'
20
+ );
21
+ });
22
+
23
+ it('does not double-escape values when rendering array', () => {
24
+ const arr = ['cats', 'dogs'];
25
+ assert.equal(
26
+ // prettier-ignore
27
+ html`<ul>${arr.map((e) => html`<li>${e}</li>`)}</ul>`.toString(),
28
+ '<ul><li>cats</li><li>dogs</li></ul>'
29
+ );
30
+ });
31
+
32
+ it('errors when interpolating object', () => {
33
+ assert.throws(
34
+ () => html`<p>${{ foo: 'bar' }}</p>`.toString(),
35
+ 'Cannot interpolate object in template'
36
+ );
37
+ });
38
+
39
+ it('omits boolean values from template', () => {
40
+ assert.equal(html`<p>${true}${false}</p>`.toString(), '<p></p>');
41
+ });
42
+ });
43
+
44
+ describe('escapeHtml', () => {
45
+ it('escapes rendered HTML', () => {
46
+ assert.equal(escapeHtml(html`<p>Hello</p>`).toString(), '&lt;p&gt;Hello&lt;/p&gt;');
47
+ });
48
+
49
+ it('works when nested inside html tag', () => {
50
+ assert.equal(html`a${escapeHtml(html`<p></p>`)}b`.toString(), 'a&lt;p&gt;&lt;/p&gt;b');
51
+ });
52
+ });
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 ADDED
@@ -0,0 +1,75 @@
1
+ import ejs from 'ejs';
2
+ import path from 'path';
3
+
4
+ function escapeValue(value: unknown): string {
5
+ if (value instanceof HtmlSafeString) {
6
+ // Already escaped!
7
+ return value.toString();
8
+ } else if (Array.isArray(value)) {
9
+ return value.map((val) => escapeValue(val)).join('');
10
+ } else if (typeof value === 'string' || typeof value === 'number') {
11
+ return ejs.escapeXML(String(value));
12
+ } else if (typeof value === 'object') {
13
+ throw new Error('Cannot interpolate object in template');
14
+ } else {
15
+ // This is undefined, null, or a boolean - don't render anything here.
16
+ return '';
17
+ }
18
+ }
19
+
20
+ // Based on https://github.com/Janpot/escape-html-template-tag
21
+ export class HtmlSafeString {
22
+ private readonly strings: ReadonlyArray<string>;
23
+ private readonly values: unknown[];
24
+
25
+ constructor(strings: ReadonlyArray<string>, values: unknown[]) {
26
+ this.strings = strings;
27
+ this.values = values;
28
+ }
29
+
30
+ toString(): string {
31
+ return this.values.reduce<string>((acc, val, i) => {
32
+ return acc + escapeValue(val) + this.strings[i + 1];
33
+ }, this.strings[0]);
34
+ }
35
+ }
36
+
37
+ export function html(strings: TemplateStringsArray, ...values: any[]): HtmlSafeString {
38
+ return new HtmlSafeString(strings, values);
39
+ }
40
+
41
+ /**
42
+ * Pre-escpapes the rendered HTML. Useful for when you want to inline the HTML
43
+ * in something else, for instance in a `data-content` attribute for a Bootstrap
44
+ * popover.
45
+ */
46
+ export function escapeHtml(html: HtmlSafeString): HtmlSafeString {
47
+ return unsafeHtml(ejs.escapeXML(html.toString()));
48
+ }
49
+
50
+ /**
51
+ * Will render the provided value without any additional escaping. Use carefully
52
+ * with user-provided data.
53
+ *
54
+ * @param value The value to render.
55
+ * @returns An {@link HtmlSafeString} representing the provided value.
56
+ */
57
+ export function unsafeHtml(value: string): HtmlSafeString {
58
+ return new HtmlSafeString([value], []);
59
+ }
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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "declaration": true,
5
+ "esModuleInterop": true,
6
+ "outDir": "./dist",
7
+ "rootDir": "src/",
8
+ "sourceMap": true
9
+ }
10
+ }