miki-template 1.3.3 → 1.3.7

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.
Files changed (115) hide show
  1. package/.eslintrc.json +16 -0
  2. package/.github/release-notes/v1.3.1.md +55 -55
  3. package/.github/release-notes/v1.3.3.md +77 -0
  4. package/.github/workflows/ci.yml +38 -54
  5. package/.github/workflows/release.yml +106 -0
  6. package/AGENT.md +71 -71
  7. package/API_REFERENCE.md +314 -314
  8. package/CHANGELOG.md +173 -169
  9. package/CODE_OF_CONDUCT.md +14 -14
  10. package/CONTRIBUTING.md +27 -27
  11. package/README.md +342 -321
  12. package/ROADMAP.md +40 -40
  13. package/benchmarks/report.json +16 -16
  14. package/benchmarks/run.js +49 -49
  15. package/benchmarks/stress.mjs +647 -647
  16. package/benchmarks/templates/large.dtpl +7 -7
  17. package/benchmarks/templates/medium.dtpl +3 -3
  18. package/benchmarks/templates/small.dtpl +7 -7
  19. package/context/component.md +109 -109
  20. package/context/prd.md +131 -131
  21. package/context/project-structure.md +33 -33
  22. package/dir/base.html +22 -22
  23. package/dir/cmpnt.html +10 -10
  24. package/dir/footer.html +2 -2
  25. package/dir/home.html +80 -80
  26. package/dir/index.html +80 -0
  27. package/dir/navbar.html +8 -8
  28. package/docs/README.md +18 -18
  29. package/docs/advanced_usage.md +71 -71
  30. package/docs/api.md +119 -119
  31. package/docs/filters.md +708 -708
  32. package/docs/installation.md +106 -106
  33. package/docs/overview.md +79 -57
  34. package/docs/partialdef.md +70 -70
  35. package/docs/security.md +27 -27
  36. package/docs/tags.md +673 -673
  37. package/docs/usage.md +646 -646
  38. package/eslint.config.mjs +42 -42
  39. package/ex.mjs +34 -32
  40. package/live-test/package-lock.json +915 -0
  41. package/live-test/package.json +9 -0
  42. package/live-test/server.js +14 -0
  43. package/live-test/views/base.html +8 -0
  44. package/live-test/views/child.html +7 -0
  45. package/live-test/views/index.html +1 -0
  46. package/miki-template-extension/.github/workflows/ci.yml +116 -116
  47. package/miki-template-extension/.vscodeignore +7 -7
  48. package/miki-template-extension/CHANGELOG.md +99 -99
  49. package/miki-template-extension/LICENSE +21 -21
  50. package/miki-template-extension/README.md +273 -273
  51. package/miki-template-extension/extension.js +1013 -1013
  52. package/miki-template-extension/icon.svg +10 -10
  53. package/miki-template-extension/package.json +280 -280
  54. package/miki-template-extension/snippets/miki-template.json +717 -717
  55. package/miki-template-extension/syntaxes/language-configuration.json +114 -114
  56. package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +355 -355
  57. package/miki-template-extension/tests/grammar-tests.json +162 -162
  58. package/miki-template-extension/tests/run-grammar-tests.js +82 -82
  59. package/package.json +40 -34
  60. package/sample-app/package-lock.json +901 -0
  61. package/sample-app/package.json +9 -0
  62. package/sample-app/server.js +14 -0
  63. package/sample-app/views/index.html +1 -0
  64. package/scripts/build-vsix.js +129 -129
  65. package/scripts/build-vsix.ps1 +15 -15
  66. package/snippets/miki-template.json +177 -177
  67. package/src/asyncRender.js +20 -20
  68. package/src/cache.js +80 -80
  69. package/src/context.js +126 -126
  70. package/src/context_processors.js +48 -48
  71. package/src/esm.mjs +89 -84
  72. package/src/filters.js +975 -975
  73. package/src/i18n.js +171 -171
  74. package/src/index.js +1112 -940
  75. package/src/lexer.js +114 -114
  76. package/src/libraries.js +371 -371
  77. package/src/parser.js +270 -270
  78. package/src/security.js +53 -53
  79. package/src/tags/control.js +719 -719
  80. package/src/tags/extra.js +154 -154
  81. package/src/tags/helpers.js +26 -26
  82. package/src/tags/i18n.js +256 -256
  83. package/src/tags/inheritance.js +335 -335
  84. package/src/tags/registry.js +18 -18
  85. package/src/tags/util.js +400 -400
  86. package/src/types.d.ts +107 -107
  87. package/syntaxes/language-configuration.json +26 -26
  88. package/syntaxes/miki-template.tmLanguage.json +146 -146
  89. package/tests/asyncRender.test.js +17 -17
  90. package/tests/base.html +6 -6
  91. package/tests/child.html +3 -3
  92. package/tests/context_processors.test.js +13 -13
  93. package/tests/esm.test.mjs +61 -61
  94. package/tests/filters.test.js +254 -254
  95. package/tests/finder-appdirs.test.js +19 -0
  96. package/tests/finder.test.js +17 -0
  97. package/tests/fixtures/views/nested/index.html +1 -0
  98. package/tests/fixtures/views/partial.html +1 -0
  99. package/tests/fixtures/views/sub/deepfile.html +1 -0
  100. package/tests/fixtures/views-appdirs/product/site/detail.html +1 -0
  101. package/tests/include_security.test.js +9 -9
  102. package/tests/integration/README.md +32 -32
  103. package/tests/integration/features.test.cjs +1681 -1681
  104. package/tests/integration/features.test.mjs +1697 -1697
  105. package/tests/integration/finder.esm.test.mjs +13 -0
  106. package/tests/integration/templates/base.miki +6 -6
  107. package/tests/integration/templates/child.miki +6 -6
  108. package/tests/integration/templates/index.html +17 -17
  109. package/tests/lexer.test.js +45 -45
  110. package/tests/parser.test.js +57 -57
  111. package/tests/partial.html +1 -1
  112. package/tests/partialdef.test.js +79 -79
  113. package/tests/production_checks.js +57 -57
  114. package/tests/security.test.js +28 -28
  115. package/tests/tags.test.js +233 -233
package/src/parser.js CHANGED
@@ -1,270 +1,270 @@
1
- /**
2
- * Parser converting token stream into AST Nodes.
3
- */
4
- const { escapeHtml, isSafe } = require('./security');
5
- const filtersModule = require('./filters');
6
-
7
- class TextNode {
8
- constructor(content) {
9
- this.content = content;
10
- }
11
-
12
- render(_context) {
13
- return this.content;
14
- }
15
- }
16
-
17
- class VariableNode {
18
- constructor(varPath, filters, isLiteral = false, literalValue = null) {
19
- this.varPath = varPath;
20
- this.filters = filters;
21
- this.isLiteral = isLiteral;
22
- this.literalValue = literalValue;
23
- }
24
-
25
- render(context) {
26
- let val = this.isLiteral ? this.literalValue : context.get(this.varPath);
27
-
28
- for (const filterInfo of this.filters) {
29
- const filterFn = filtersModule.getFilter(filterInfo.name);
30
- if (!filterFn) {
31
- throw new Error(`Unknown filter: '${filterInfo.name}'`);
32
- }
33
-
34
- let argVal = undefined;
35
- if (filterInfo.arg) {
36
- if (filterInfo.arg.type === 'literal') {
37
- argVal = filterInfo.arg.value;
38
- } else if (filterInfo.arg.type === 'variable') {
39
- argVal = context.get(filterInfo.arg.value);
40
- }
41
- }
42
-
43
- val = filterFn(val, argVal, context);
44
- }
45
-
46
- // Auto-escape logic – skip escaping if value is marked safe
47
- if (context.autoescape && !isSafe(val)) {
48
- return escapeHtml(val);
49
- }
50
-
51
- return val === null || val === undefined ? '' : String(val);
52
- }
53
- }
54
-
55
- /**
56
- * Parses a variable expression including filters and arguments.
57
- * E.g., user.name|lower|default:"Guest"
58
- */
59
- function parseVariableExpression(expr) {
60
- let idx = 0;
61
- const len = expr.length;
62
-
63
- function skipWhitespace() {
64
- while (idx < len && /\s/.test(expr[idx])) {
65
- idx++;
66
- }
67
- }
68
-
69
- let varPath = '';
70
- let isLiteral = false;
71
- let literalValue = null;
72
-
73
- // Check for string literal as base value
74
- if (idx < len && (expr[idx] === '"' || expr[idx] === '\'')) {
75
- const quote = expr[idx];
76
- idx++;
77
- let strVal = '';
78
- while (idx < len && expr[idx] !== quote) {
79
- if (expr[idx] === '\\' && idx + 1 < len) {
80
- idx++;
81
- }
82
- strVal += expr[idx];
83
- idx++;
84
- }
85
- idx++; // Skip closing quote
86
- isLiteral = true;
87
- literalValue = strVal;
88
- skipWhitespace();
89
- }
90
-
91
- // Parse main variable path
92
- if (!isLiteral) {
93
- while (idx < len && expr[idx] !== '|') {
94
- varPath += expr[idx];
95
- idx++;
96
- }
97
- varPath = varPath.trim();
98
- }
99
-
100
- const filters = [];
101
- while (idx < len) {
102
- if (expr[idx] === '|') {
103
- idx++; // Skip '|'
104
- skipWhitespace();
105
-
106
- // Read filter name
107
- let filterName = '';
108
- while (idx < len && expr[idx] !== ':' && expr[idx] !== '|' && !/\s/.test(expr[idx])) {
109
- filterName += expr[idx];
110
- idx++;
111
- }
112
-
113
- skipWhitespace();
114
- let arg = null;
115
-
116
- if (idx < len && expr[idx] === ':') {
117
- idx++; // Skip ':'
118
- skipWhitespace();
119
-
120
- // Parse argument (string literal, number, boolean, null, or variable)
121
- if (expr[idx] === '"' || expr[idx] === '\'') {
122
- const quote = expr[idx];
123
- idx++; // Skip opening quote
124
- let argVal = '';
125
- while (idx < len && expr[idx] !== quote) {
126
- // Track nested {{ ... }} and {% ... %} so the closing
127
- // quote isn't confused with one inside a nested template
128
- // expression. This lets us write
129
- // {{ x|default:"{{ y|upper }}" }}
130
- // where the inner `}}` must not close the outer string.
131
- if (expr[idx] === '{' && idx + 1 < len && (expr[idx + 1] === '{' || expr[idx + 1] === '%')) {
132
- const open = expr.substr(idx, 2);
133
- const close = open === '{{' ? '}}' : '%}';
134
- argVal += open;
135
- idx += 2;
136
- while (idx < len && expr.substr(idx, close.length) !== close) {
137
- argVal += expr[idx];
138
- idx++;
139
- }
140
- if (idx < len) {
141
- argVal += close;
142
- idx += close.length;
143
- }
144
- continue;
145
- }
146
- if (expr[idx] === '\\') {
147
- idx++;
148
- }
149
- if (idx < len) {
150
- argVal += expr[idx];
151
- idx++;
152
- }
153
- }
154
- if (idx < len) idx++; // Skip closing quote
155
- arg = { type: 'literal', value: argVal };
156
- } else {
157
- // Read unquoted token
158
- let argVal = '';
159
- while (idx < len && expr[idx] !== '|' && !/\s/.test(expr[idx])) {
160
- argVal += expr[idx];
161
- idx++;
162
- }
163
- argVal = argVal.trim();
164
-
165
- // Type coercion
166
- if (!isNaN(argVal) && argVal !== '') {
167
- arg = { type: 'literal', value: Number(argVal) };
168
- } else if (argVal === 'true') {
169
- arg = { type: 'literal', value: true };
170
- } else if (argVal === 'false') {
171
- arg = { type: 'literal', value: false };
172
- } else if (argVal === 'none' || argVal === 'None' || argVal === 'null') {
173
- arg = { type: 'literal', value: null };
174
- } else {
175
- // Variable lookup
176
- arg = { type: 'variable', value: argVal };
177
- }
178
- }
179
- }
180
-
181
- filters.push({ name: filterName, arg });
182
- skipWhitespace();
183
- } else {
184
- idx++;
185
- }
186
- }
187
-
188
- return { varPath, filters, isLiteral, literalValue };
189
- }
190
-
191
- class Parser {
192
- // Existing constructor and methods remain unchanged
193
- /**
194
- * Parse tokens until a specific end tag, consuming the end tag token.
195
- * @param {string} endTag - Tag name to stop at (e.g., 'endsleep').
196
- * @returns {Array} Parsed nodes.
197
- */
198
- parseUntilTag(endTag) {
199
- const nodes = this.parse([endTag]);
200
- // Advance past the end tag token if present
201
- const next = this.peek();
202
- if (next && next.type === 'block' && next.content.split(/\s+/)[0] === endTag) {
203
- this.advance();
204
- }
205
- return nodes;
206
- }
207
-
208
- constructor(tokens, tagRegistry = {}) {
209
- this.tokens = tokens;
210
- this.index = 0;
211
- this.tagRegistry = tagRegistry;
212
- }
213
-
214
- /**
215
- * Parses tokens into AST nodes until one of the tags in untilTags is encountered.
216
- */
217
- parse(untilTags = []) {
218
- const nodes = [];
219
- while (this.index < this.tokens.length) {
220
- const token = this.tokens[this.index];
221
-
222
- if (token.type === 'text') {
223
- nodes.push(new TextNode(token.content));
224
- this.index++;
225
- } else if (token.type === 'var') {
226
- const parsed = parseVariableExpression(token.content);
227
- nodes.push(new VariableNode(parsed.varPath, parsed.filters, parsed.isLiteral, parsed.literalValue));
228
- this.index++;
229
- } else if (token.type === 'block') {
230
- const parts = token.content.split(/\s+/);
231
- const tagName = parts[0];
232
-
233
- // Stop parsing if we hit a stop tag
234
- if (untilTags.includes(tagName)) {
235
- break;
236
- }
237
-
238
- this.index++; // Consume block token
239
-
240
- const tagParser = this.tagRegistry[tagName];
241
- if (!tagParser) {
242
- throw new Error(`Unknown template tag: '${tagName}'`);
243
- }
244
-
245
- const node = tagParser(token.content, this);
246
- nodes.push(node);
247
- }
248
- }
249
- // If we were looking for specific end tags but didn't find them, throw
250
- if (untilTags.length > 0 && this.index >= this.tokens.length) {
251
- throw new Error(`Unexpected end of template - expected one of: ${untilTags.map(t => '{% ' + t + ' %}').join(', ')}`);
252
- }
253
- return nodes;
254
- }
255
-
256
- peek() {
257
- return this.tokens[this.index];
258
- }
259
-
260
- advance() {
261
- this.index++;
262
- }
263
- }
264
-
265
- module.exports = {
266
- Parser,
267
- TextNode,
268
- VariableNode,
269
- parseVariableExpression
270
- };
1
+ /**
2
+ * Parser converting token stream into AST Nodes.
3
+ */
4
+ const { escapeHtml, isSafe } = require('./security');
5
+ const filtersModule = require('./filters');
6
+
7
+ class TextNode {
8
+ constructor(content) {
9
+ this.content = content;
10
+ }
11
+
12
+ render(_context) {
13
+ return this.content;
14
+ }
15
+ }
16
+
17
+ class VariableNode {
18
+ constructor(varPath, filters, isLiteral = false, literalValue = null) {
19
+ this.varPath = varPath;
20
+ this.filters = filters;
21
+ this.isLiteral = isLiteral;
22
+ this.literalValue = literalValue;
23
+ }
24
+
25
+ render(context) {
26
+ let val = this.isLiteral ? this.literalValue : context.get(this.varPath);
27
+
28
+ for (const filterInfo of this.filters) {
29
+ const filterFn = filtersModule.getFilter(filterInfo.name);
30
+ if (!filterFn) {
31
+ throw new Error(`Unknown filter: '${filterInfo.name}'`);
32
+ }
33
+
34
+ let argVal = undefined;
35
+ if (filterInfo.arg) {
36
+ if (filterInfo.arg.type === 'literal') {
37
+ argVal = filterInfo.arg.value;
38
+ } else if (filterInfo.arg.type === 'variable') {
39
+ argVal = context.get(filterInfo.arg.value);
40
+ }
41
+ }
42
+
43
+ val = filterFn(val, argVal, context);
44
+ }
45
+
46
+ // Auto-escape logic – skip escaping if value is marked safe
47
+ if (context.autoescape && !isSafe(val)) {
48
+ return escapeHtml(val);
49
+ }
50
+
51
+ return val === null || val === undefined ? '' : String(val);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Parses a variable expression including filters and arguments.
57
+ * E.g., user.name|lower|default:"Guest"
58
+ */
59
+ function parseVariableExpression(expr) {
60
+ let idx = 0;
61
+ const len = expr.length;
62
+
63
+ function skipWhitespace() {
64
+ while (idx < len && /\s/.test(expr[idx])) {
65
+ idx++;
66
+ }
67
+ }
68
+
69
+ let varPath = '';
70
+ let isLiteral = false;
71
+ let literalValue = null;
72
+
73
+ // Check for string literal as base value
74
+ if (idx < len && (expr[idx] === '"' || expr[idx] === '\'')) {
75
+ const quote = expr[idx];
76
+ idx++;
77
+ let strVal = '';
78
+ while (idx < len && expr[idx] !== quote) {
79
+ if (expr[idx] === '\\' && idx + 1 < len) {
80
+ idx++;
81
+ }
82
+ strVal += expr[idx];
83
+ idx++;
84
+ }
85
+ idx++; // Skip closing quote
86
+ isLiteral = true;
87
+ literalValue = strVal;
88
+ skipWhitespace();
89
+ }
90
+
91
+ // Parse main variable path
92
+ if (!isLiteral) {
93
+ while (idx < len && expr[idx] !== '|') {
94
+ varPath += expr[idx];
95
+ idx++;
96
+ }
97
+ varPath = varPath.trim();
98
+ }
99
+
100
+ const filters = [];
101
+ while (idx < len) {
102
+ if (expr[idx] === '|') {
103
+ idx++; // Skip '|'
104
+ skipWhitespace();
105
+
106
+ // Read filter name
107
+ let filterName = '';
108
+ while (idx < len && expr[idx] !== ':' && expr[idx] !== '|' && !/\s/.test(expr[idx])) {
109
+ filterName += expr[idx];
110
+ idx++;
111
+ }
112
+
113
+ skipWhitespace();
114
+ let arg = null;
115
+
116
+ if (idx < len && expr[idx] === ':') {
117
+ idx++; // Skip ':'
118
+ skipWhitespace();
119
+
120
+ // Parse argument (string literal, number, boolean, null, or variable)
121
+ if (expr[idx] === '"' || expr[idx] === '\'') {
122
+ const quote = expr[idx];
123
+ idx++; // Skip opening quote
124
+ let argVal = '';
125
+ while (idx < len && expr[idx] !== quote) {
126
+ // Track nested {{ ... }} and {% ... %} so the closing
127
+ // quote isn't confused with one inside a nested template
128
+ // expression. This lets us write
129
+ // {{ x|default:"{{ y|upper }}" }}
130
+ // where the inner `}}` must not close the outer string.
131
+ if (expr[idx] === '{' && idx + 1 < len && (expr[idx + 1] === '{' || expr[idx + 1] === '%')) {
132
+ const open = expr.substr(idx, 2);
133
+ const close = open === '{{' ? '}}' : '%}';
134
+ argVal += open;
135
+ idx += 2;
136
+ while (idx < len && expr.substr(idx, close.length) !== close) {
137
+ argVal += expr[idx];
138
+ idx++;
139
+ }
140
+ if (idx < len) {
141
+ argVal += close;
142
+ idx += close.length;
143
+ }
144
+ continue;
145
+ }
146
+ if (expr[idx] === '\\') {
147
+ idx++;
148
+ }
149
+ if (idx < len) {
150
+ argVal += expr[idx];
151
+ idx++;
152
+ }
153
+ }
154
+ if (idx < len) idx++; // Skip closing quote
155
+ arg = { type: 'literal', value: argVal };
156
+ } else {
157
+ // Read unquoted token
158
+ let argVal = '';
159
+ while (idx < len && expr[idx] !== '|' && !/\s/.test(expr[idx])) {
160
+ argVal += expr[idx];
161
+ idx++;
162
+ }
163
+ argVal = argVal.trim();
164
+
165
+ // Type coercion
166
+ if (!isNaN(argVal) && argVal !== '') {
167
+ arg = { type: 'literal', value: Number(argVal) };
168
+ } else if (argVal === 'true') {
169
+ arg = { type: 'literal', value: true };
170
+ } else if (argVal === 'false') {
171
+ arg = { type: 'literal', value: false };
172
+ } else if (argVal === 'none' || argVal === 'None' || argVal === 'null') {
173
+ arg = { type: 'literal', value: null };
174
+ } else {
175
+ // Variable lookup
176
+ arg = { type: 'variable', value: argVal };
177
+ }
178
+ }
179
+ }
180
+
181
+ filters.push({ name: filterName, arg });
182
+ skipWhitespace();
183
+ } else {
184
+ idx++;
185
+ }
186
+ }
187
+
188
+ return { varPath, filters, isLiteral, literalValue };
189
+ }
190
+
191
+ class Parser {
192
+ // Existing constructor and methods remain unchanged
193
+ /**
194
+ * Parse tokens until a specific end tag, consuming the end tag token.
195
+ * @param {string} endTag - Tag name to stop at (e.g., 'endsleep').
196
+ * @returns {Array} Parsed nodes.
197
+ */
198
+ parseUntilTag(endTag) {
199
+ const nodes = this.parse([endTag]);
200
+ // Advance past the end tag token if present
201
+ const next = this.peek();
202
+ if (next && next.type === 'block' && next.content.split(/\s+/)[0] === endTag) {
203
+ this.advance();
204
+ }
205
+ return nodes;
206
+ }
207
+
208
+ constructor(tokens, tagRegistry = {}) {
209
+ this.tokens = tokens;
210
+ this.index = 0;
211
+ this.tagRegistry = tagRegistry;
212
+ }
213
+
214
+ /**
215
+ * Parses tokens into AST nodes until one of the tags in untilTags is encountered.
216
+ */
217
+ parse(untilTags = []) {
218
+ const nodes = [];
219
+ while (this.index < this.tokens.length) {
220
+ const token = this.tokens[this.index];
221
+
222
+ if (token.type === 'text') {
223
+ nodes.push(new TextNode(token.content));
224
+ this.index++;
225
+ } else if (token.type === 'var') {
226
+ const parsed = parseVariableExpression(token.content);
227
+ nodes.push(new VariableNode(parsed.varPath, parsed.filters, parsed.isLiteral, parsed.literalValue));
228
+ this.index++;
229
+ } else if (token.type === 'block') {
230
+ const parts = token.content.split(/\s+/);
231
+ const tagName = parts[0];
232
+
233
+ // Stop parsing if we hit a stop tag
234
+ if (untilTags.includes(tagName)) {
235
+ break;
236
+ }
237
+
238
+ this.index++; // Consume block token
239
+
240
+ const tagParser = this.tagRegistry[tagName];
241
+ if (!tagParser) {
242
+ throw new Error(`Unknown template tag: '${tagName}'`);
243
+ }
244
+
245
+ const node = tagParser(token.content, this);
246
+ nodes.push(node);
247
+ }
248
+ }
249
+ // If we were looking for specific end tags but didn't find them, throw
250
+ if (untilTags.length > 0 && this.index >= this.tokens.length) {
251
+ throw new Error(`Unexpected end of template - expected one of: ${untilTags.map(t => '{% ' + t + ' %}').join(', ')}`);
252
+ }
253
+ return nodes;
254
+ }
255
+
256
+ peek() {
257
+ return this.tokens[this.index];
258
+ }
259
+
260
+ advance() {
261
+ this.index++;
262
+ }
263
+ }
264
+
265
+ module.exports = {
266
+ Parser,
267
+ TextNode,
268
+ VariableNode,
269
+ parseVariableExpression
270
+ };
package/src/security.js CHANGED
@@ -1,53 +1,53 @@
1
- /**
2
- * Security and HTML auto-escaping mechanisms.
3
- */
4
- const he = require('he');
5
-
6
- class SafeString {
7
- constructor(value) {
8
- this.value = value === null || value === undefined ? '' : String(value);
9
- }
10
-
11
- toString() {
12
- return this.value;
13
- }
14
- }
15
-
16
- /**
17
- * Mark a string as safe so it is not auto-escaped during rendering.
18
- */
19
- function markSafe(value) {
20
- if (value instanceof SafeString) {
21
- return value;
22
- }
23
- return new SafeString(value);
24
- }
25
-
26
- /**
27
- * Check if a value is marked safe.
28
- */
29
- function isSafe(value) {
30
- return value instanceof SafeString;
31
- }
32
-
33
- /**
34
- * Escape a string's HTML special characters unless it is marked safe.
35
- * If `force` is true (used by the |escape filter), even a SafeString is
36
- * re-escaped. This matches Django's `{{ value|escape }}` semantics.
37
- */
38
- function escapeHtml(value, force = false) {
39
- if (value instanceof SafeString && !force) {
40
- return value.toString();
41
- }
42
-
43
- const str = value === null || value === undefined ? '' : String(value);
44
- // We use he.escape to convert <, >, &, ", ', etc. to entities
45
- return he.escape(str);
46
- }
47
-
48
- module.exports = {
49
- SafeString,
50
- markSafe,
51
- isSafe,
52
- escapeHtml
53
- };
1
+ /**
2
+ * Security and HTML auto-escaping mechanisms.
3
+ */
4
+ const he = require('he');
5
+
6
+ class SafeString {
7
+ constructor(value) {
8
+ this.value = value === null || value === undefined ? '' : String(value);
9
+ }
10
+
11
+ toString() {
12
+ return this.value;
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Mark a string as safe so it is not auto-escaped during rendering.
18
+ */
19
+ function markSafe(value) {
20
+ if (value instanceof SafeString) {
21
+ return value;
22
+ }
23
+ return new SafeString(value);
24
+ }
25
+
26
+ /**
27
+ * Check if a value is marked safe.
28
+ */
29
+ function isSafe(value) {
30
+ return value instanceof SafeString;
31
+ }
32
+
33
+ /**
34
+ * Escape a string's HTML special characters unless it is marked safe.
35
+ * If `force` is true (used by the |escape filter), even a SafeString is
36
+ * re-escaped. This matches Django's `{{ value|escape }}` semantics.
37
+ */
38
+ function escapeHtml(value, force = false) {
39
+ if (value instanceof SafeString && !force) {
40
+ return value.toString();
41
+ }
42
+
43
+ const str = value === null || value === undefined ? '' : String(value);
44
+ // We use he.escape to convert <, >, &, ", ', etc. to entities
45
+ return he.escape(str);
46
+ }
47
+
48
+ module.exports = {
49
+ SafeString,
50
+ markSafe,
51
+ isSafe,
52
+ escapeHtml
53
+ };