@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.
@@ -1,3 +1,3 @@
1
- @prairielearn/html:build: cache hit, replaying output c36dba0838eb5e15
1
+ @prairielearn/html:build: cache hit, replaying output 1b5ba27b475657ab
2
2
  @prairielearn/html:build: warning package.json: No license field
3
3
  @prairielearn/html:build: $ tsc
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
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.unsafeHtml = exports.escapeHtml = exports.html = exports.HtmlSafeString = void 0;
4
+ const ENCODE_HTML_RULES = {
5
+ '&': '&amp;',
6
+ '<': '&lt;',
7
+ '>': '&gt;',
8
+ '"': '&#34;',
9
+ "'": '&#39;',
4
10
  };
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"));
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(function (val) { return escapeValue(val); }).join('');
27
+ return value.map((val) => escapeValue(val)).join('');
16
28
  }
17
29
  else if (typeof value === 'string' || typeof value === 'number') {
18
- return ejs_1["default"].escapeXML(String(value));
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('Cannot interpolate object in template');
37
+ throw new Error(`Cannot interpolate object in template: ${JSON.stringify(value)}`);
22
38
  }
23
39
  else {
24
- // This is undefined, null, or a boolean - don't render anything here.
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
- var HtmlSafeString = /** @class */ (function () {
30
- function HtmlSafeString(strings, values) {
45
+ class HtmlSafeString {
46
+ constructor(strings, values) {
31
47
  this.strings = strings;
32
48
  this.values = values;
33
49
  }
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];
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(ejs_1["default"].escapeXML(html.toString()));
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":";;;;;;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"}
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"}
@@ -1,52 +1,42 @@
1
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'];
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>&lt;script&gt;</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)(templateObject_3 || (templateObject_3 = __makeTemplateObject(["<ul>", "</ul>"], ["<ul>", "</ul>"])), arr).toString(), '<ul>cats&gt;&lt;dogs</ul>');
16
+ (0, index_1.html) `<ul>${arr}</ul>`.toString(), '<ul>cats&gt;&lt;dogs</ul>');
21
17
  });
22
- it('does not double-escape values when rendering array', function () {
23
- var arr = ['cats', 'dogs'];
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)(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>');
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', 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');
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', function () {
32
- chai_1.assert.equal((0, index_1.html)(templateObject_7 || (templateObject_7 = __makeTemplateObject(["<p>", "", "</p>"], ["<p>", "", "</p>"])), true, false).toString(), '<p></p>');
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
- 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');
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('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>');
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(), '&lt;p&gt;Hello&lt;/p&gt;');
46
37
  });
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>');
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&lt;p&gt;&lt;/p&gt;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
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
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": "1.0.0",
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
- "@types/ejs": "^3.1.0",
15
- "mocha": "^9.2.0",
16
- "ts-node": "^10.5.0",
17
- "typescript": "^4.5.5"
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, renderEjs } from './index';
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&lt;p&gt;&lt;/p&gt;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
- import ejs from 'ejs';
2
- import path from 'path';
1
+ const ENCODE_HTML_RULES = {
2
+ '&': '&amp;',
3
+ '<': '&lt;',
4
+ '>': '&gt;',
5
+ '"': '&#34;',
6
+ "'": '&#39;',
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 ejs.escapeXML(String(value));
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('Cannot interpolate object in template');
33
+ throw new Error(`Cannot interpolate object in template: ${JSON.stringify(value)}`);
14
34
  } else {
15
- // This is undefined, null, or a boolean - don't render anything here.
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(ejs.escapeXML(html.toString()));
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
- }
package/tsconfig.json CHANGED
@@ -1,10 +1,7 @@
1
1
  {
2
+ "extends": "@prairielearn/tsconfig",
2
3
  "compilerOptions": {
3
- "allowJs": true,
4
- "declaration": true,
5
- "esModuleInterop": true,
6
4
  "outDir": "./dist",
7
- "rootDir": "src/",
8
- "sourceMap": true
5
+ "rootDir": "./src",
9
6
  }
10
7
  }