@burger-editor/frozen-patty 4.0.0-alpha.3 → 4.0.0-alpha.4

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/dist/utils.js CHANGED
@@ -1,3 +1,18 @@
1
+ import { parseFields } from './parse-fields.js';
2
+ /**
3
+ * List of dangerous HTML elements to block for XSS protection
4
+ */
5
+ export const DANGEROUS_ELEMENTS = [
6
+ 'script',
7
+ 'style',
8
+ 'template',
9
+ 'object',
10
+ 'embed',
11
+ 'iframe',
12
+ 'frame',
13
+ 'frameset',
14
+ 'applet',
15
+ ];
1
16
  /**
2
17
  *
3
18
  * @param kvs
@@ -21,17 +36,268 @@ export function arrayToHash(kvs) {
21
36
  }
22
37
  return result;
23
38
  }
39
+ const kebabCaseCache = new Map();
24
40
  /**
25
41
  *
26
42
  * @param str
27
43
  */
28
44
  export function kebabCase(str) {
29
- return str.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
45
+ if (kebabCaseCache.has(str)) {
46
+ return kebabCaseCache.get(str);
47
+ }
48
+ const result = str.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
49
+ kebabCaseCache.set(str, result);
50
+ return result;
30
51
  }
52
+ const camelCaseCache = new Map();
31
53
  /**
32
54
  *
33
55
  * @param str
34
56
  */
35
57
  export function camelCase(str) {
36
- return str.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
58
+ if (camelCaseCache.has(str)) {
59
+ return camelCaseCache.get(str);
60
+ }
61
+ const result = str.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
62
+ camelCaseCache.set(str, result);
63
+ return result;
64
+ }
65
+ /**
66
+ *
67
+ * @param el
68
+ * @param nodeName
69
+ * @param attr
70
+ * @param xssSanitize
71
+ */
72
+ export function replaceNode(el, nodeName, attr, xssSanitize = true) {
73
+ nodeName = nodeName.toLowerCase();
74
+ const currentName = el.localName ?? el.nodeName.toLowerCase();
75
+ if (currentName === nodeName) {
76
+ return el;
77
+ }
78
+ // Check element name if XSS protection is enabled
79
+ if (xssSanitize && // Disallow dangerous elements
80
+ DANGEROUS_ELEMENTS.includes(nodeName)) {
81
+ return el;
82
+ }
83
+ const node = (el.ownerDocument ?? document).createElement(nodeName);
84
+ for (const child of el.childNodes) {
85
+ node.append(child.cloneNode(true));
86
+ }
87
+ const fields = getFields(el, attr);
88
+ const attrs = new Set(fields.map((field) => field.propName));
89
+ for (const { name, value } of el.attributes) {
90
+ if (attrs.has(name)) {
91
+ continue;
92
+ }
93
+ // Check attribute value if XSS protection is enabled
94
+ if (xssSanitize) {
95
+ const sanitizedValue = sanitizeAttributeValue(name, value);
96
+ if (sanitizedValue !== null) {
97
+ node.setAttribute(name, sanitizedValue);
98
+ }
99
+ continue;
100
+ }
101
+ node.setAttribute(name, value);
102
+ }
103
+ el.replaceWith(node);
104
+ return node;
105
+ }
106
+ /**
107
+ * Removes dangerous script elements from HTML string
108
+ * @param html HTML string
109
+ * @returns Sanitized HTML
110
+ */
111
+ export function sanitizeHtml(html) {
112
+ if (typeof html !== 'string') {
113
+ return '';
114
+ }
115
+ // Simple approach to handle script tags content
116
+ // First remove script tags but keep their content
117
+ let sanitized = html;
118
+ // For each dangerous element, replace tag but keep content using regex
119
+ // Example: <script>alert("XSS")</script> -> alert("XSS")
120
+ for (const tagName of DANGEROUS_ELEMENTS) {
121
+ const regex = new RegExp(`<${tagName}[^>]*>(.*?)<\\/${tagName}>`, 'gis');
122
+ sanitized = sanitized.replace(regex, '$1');
123
+ }
124
+ // Parse HTML safely using DOM parser for remaining sanitization
125
+ const doc = new DOMParser().parseFromString(sanitized, 'text/html');
126
+ const body = doc.body;
127
+ // Remove event handler attributes from all elements
128
+ sanitizeElementAndChildren(body);
129
+ return body.innerHTML;
130
+ }
131
+ /**
132
+ * Sanitizes an element and all its child elements
133
+ * @param element Target element
134
+ */
135
+ function sanitizeElementAndChildren(element) {
136
+ // Remove dangerous attributes from current element
137
+ sanitizeAttributes(element);
138
+ // Process child elements recursively
139
+ const children = element.children;
140
+ for (const child of children) {
141
+ sanitizeElementAndChildren(child);
142
+ }
143
+ }
144
+ /**
145
+ * Sanitizes attribute value
146
+ * @param name Attribute name
147
+ * @param value Attribute value
148
+ * @returns Safe attribute value, or null if dangerous
149
+ */
150
+ export function sanitizeAttributeValue(name, value) {
151
+ if (typeof value !== 'string') {
152
+ return value;
153
+ }
154
+ name = name.toLowerCase();
155
+ // Disallow event handler attributes
156
+ if (name.startsWith('on')) {
157
+ return null;
158
+ }
159
+ // Check for dangerous protocol in link attributes
160
+ if (name === 'href' ||
161
+ name === 'src' ||
162
+ name === 'action' ||
163
+ name === 'formaction' ||
164
+ name === 'xlink:href') {
165
+ const trimmedValue = value.trim().toLowerCase();
166
+ if (trimmedValue.startsWith('javascript:') ||
167
+ trimmedValue.startsWith('data:') ||
168
+ trimmedValue.startsWith('vbscript:')) {
169
+ return null;
170
+ }
171
+ }
172
+ // Other dangerous attributes
173
+ if (['manifest'].includes(name)) {
174
+ return null;
175
+ }
176
+ return value;
177
+ }
178
+ /**
179
+ * Removes dangerous attributes from an element
180
+ * @param element Target element
181
+ */
182
+ function sanitizeAttributes(element) {
183
+ // Collect list of dangerous attributes
184
+ const dangerousAttrs = [...element.attributes]
185
+ .filter((attr) => sanitizeAttributeValue(attr.name, attr.value) === null)
186
+ .map((attr) => attr.name);
187
+ // Remove dangerous attributes
188
+ for (const attrName of dangerousAttrs) {
189
+ element.removeAttribute(attrName);
190
+ }
191
+ }
192
+ /**
193
+ *
194
+ * @param data
195
+ * @param definedFields
196
+ */
197
+ export function maxLengthOf(data, definedFields) {
198
+ let maxLength = 0;
199
+ for (const key in data) {
200
+ if (definedFields.includes(key) && Array.isArray(data[key])) {
201
+ maxLength = Math.max(maxLength, data[key].length);
202
+ }
203
+ }
204
+ return maxLength;
205
+ }
206
+ /**
207
+ *
208
+ * @param el
209
+ * @param attr
210
+ */
211
+ export function definedFields(el, attr) {
212
+ const fieldDefinitions = getFields(el, attr);
213
+ return fieldDefinitions.map((field) => field.fieldName);
214
+ }
215
+ /**
216
+ *
217
+ * @param el
218
+ * @param attr
219
+ */
220
+ export function getFields(el, attr) {
221
+ const fields = el.getAttribute(`data-${attr}`);
222
+ if (!fields) {
223
+ return [];
224
+ }
225
+ return parseFields(fields);
226
+ }
227
+ /**
228
+ *
229
+ * @param el
230
+ * @param attr
231
+ * @param dataKeyName
232
+ */
233
+ export function hasField(el, attr, dataKeyName) {
234
+ const fieldDefinitions = getFields(el, attr);
235
+ return fieldDefinitions.some((field) => field.fieldName === dataKeyName);
236
+ }
237
+ /**
238
+ *
239
+ * @param fields
240
+ * @param oldPropName
241
+ * @param newPropName
242
+ */
243
+ export function replaceProp(fields, oldPropName, newPropName) {
244
+ return fields.map((field) => {
245
+ if (field.propName === oldPropName) {
246
+ return {
247
+ ...field,
248
+ propName: newPropName,
249
+ };
250
+ }
251
+ return field;
252
+ });
253
+ }
254
+ /**
255
+ *
256
+ * @param fields
257
+ * @param propName
258
+ */
259
+ export function removeProp(fields, propName) {
260
+ return fields.filter((field) => field.propName !== propName);
261
+ }
262
+ /**
263
+ * - 値が配列の場合は、index に対応する値を返す。
264
+ * - 値が配列でない場合は、そのまま返す。
265
+ * - ⚠️ 値が配列で、index に対応する値が undefined の場合は、配列の最初の値を返す。
266
+ * @param data
267
+ * @param index
268
+ */
269
+ export function flattenData(data, index) {
270
+ const result = {};
271
+ for (const key in data) {
272
+ result[key] = Array.isArray(data[key])
273
+ ? data[key][index] === undefined
274
+ ? data[key][0]
275
+ : data[key][index]
276
+ : data[key];
277
+ }
278
+ return result;
279
+ }
280
+ /**
281
+ * 特定の要素で、`propName in element`で判定できない属性名
282
+ */
283
+ const specificProp = {
284
+ /*
285
+ * JSDOMの未実装対策
286
+ * @see https://github.com/jsdom/jsdom/blob/main/lib/jsdom/living/nodes/HTMLSourceElement.webidl
287
+ * Living Standardでは実装済み
288
+ * @see https://html.spec.whatwg.org/multipage/embedded-content.html#htmlsourceelement
289
+ */
290
+ source: ['width', 'height'],
291
+ };
292
+ /**
293
+ *
294
+ * @param el
295
+ * @param name
296
+ */
297
+ export function propInElement(el, name) {
298
+ const has = name in el;
299
+ if (has) {
300
+ return true;
301
+ }
302
+ return specificProp[el.localName]?.includes(name) ?? false;
37
303
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@burger-editor/frozen-patty",
3
- "version": "4.0.0-alpha.3",
4
- "description": "Pure HTML to JSON converter that not use template engine.",
3
+ "version": "4.0.0-alpha.4",
4
+ "description": "Pure HTML to JSON converter that does not use a template engine.",
5
5
  "author": "D-ZERO",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -41,5 +41,5 @@
41
41
  "dev": "tsc --watch --project tsconfig.build.json",
42
42
  "clean": "tsc --build --clean"
43
43
  },
44
- "gitHead": "23c89d351064c391a45ba6a95ab7602f2a644ff0"
44
+ "gitHead": "823486d58579275ae556942c5d7a4aef4e1f634e"
45
45
  }