@burger-editor/frozen-patty 4.0.0-alpha.2 → 4.0.0-alpha.22

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.d.ts CHANGED
@@ -1,4 +1,8 @@
1
- import type { FrozenPattyData, PrimitiveDatum } from './types.js';
1
+ import type { FieldDefinition, FrozenPattyData, FrozenPattyFlattenData, PrimitiveDatum } from './types.js';
2
+ /**
3
+ * List of dangerous HTML elements to block for XSS protection
4
+ */
5
+ export declare const DANGEROUS_ELEMENTS: string[];
2
6
  /**
3
7
  *
4
8
  * @param kvs
@@ -8,11 +12,74 @@ export declare function arrayToHash(kvs: [keyof FrozenPattyData, PrimitiveDatum,
8
12
  };
9
13
  /**
10
14
  *
11
- * @param str
15
+ * @param el
16
+ * @param nodeName
17
+ * @param attr
18
+ * @param xssSanitize
19
+ */
20
+ export declare function replaceNode(el: Element, nodeName: string, attr: string, xssSanitize?: boolean): Element;
21
+ /**
22
+ * Removes dangerous script elements from HTML string
23
+ * @param html HTML string
24
+ * @returns Sanitized HTML
25
+ */
26
+ export declare function sanitizeHtml(html: string): string;
27
+ /**
28
+ * Sanitizes attribute value
29
+ * @param name Attribute name
30
+ * @param value Attribute value
31
+ * @returns Safe attribute value, or null if dangerous
32
+ */
33
+ export declare function sanitizeAttributeValue<T extends PrimitiveDatum>(name: string, value: T): T | null;
34
+ /**
35
+ *
36
+ * @param data
37
+ * @param definedFields
38
+ */
39
+ export declare function maxLengthOf(data: FrozenPattyData, definedFields: string[]): number;
40
+ /**
41
+ *
42
+ * @param el
43
+ * @param attr
44
+ */
45
+ export declare function definedFields(el: Element, attr: string): string[];
46
+ /**
47
+ *
48
+ * @param el
49
+ * @param attr
50
+ */
51
+ export declare function getFields(el: Element, attr: string): readonly FieldDefinition[];
52
+ /**
53
+ *
54
+ * @param el
55
+ * @param attr
56
+ * @param dataKeyName
57
+ */
58
+ export declare function hasField(el: Element, attr: string, dataKeyName: string): boolean;
59
+ /**
60
+ *
61
+ * @param fields
62
+ * @param oldPropName
63
+ * @param newPropName
64
+ */
65
+ export declare function replaceProp(fields: readonly FieldDefinition[], oldPropName: string, newPropName: string): FieldDefinition[];
66
+ /**
67
+ *
68
+ * @param fields
69
+ * @param propName
70
+ */
71
+ export declare function removeProp(fields: readonly FieldDefinition[], propName: string): FieldDefinition[];
72
+ /**
73
+ * - 値が配列の場合は、index に対応する値を返す。
74
+ * - 値が配列でない場合は、そのまま返す。
75
+ * - ⚠️ 値が配列で、index に対応する値が undefined の場合は、配列の最初の値を返す。
76
+ * @param data
77
+ * @param index
12
78
  */
13
- export declare function kebabCase(str: string): string;
79
+ export declare function flattenData(data: FrozenPattyData, index: number): FrozenPattyFlattenData;
14
80
  /**
15
81
  *
16
- * @param str
82
+ * @param el
83
+ * @param name
17
84
  */
18
- export declare function camelCase(str: string): string;
85
+ export declare function propInElement(el: Element, name: string): boolean;
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
@@ -23,15 +38,240 @@ export function arrayToHash(kvs) {
23
38
  }
24
39
  /**
25
40
  *
26
- * @param str
41
+ * @param el
42
+ * @param nodeName
43
+ * @param attr
44
+ * @param xssSanitize
45
+ */
46
+ export function replaceNode(el, nodeName, attr, xssSanitize = true) {
47
+ nodeName = nodeName.toLowerCase();
48
+ const currentName = el.localName ?? el.nodeName.toLowerCase();
49
+ if (currentName === nodeName) {
50
+ return el;
51
+ }
52
+ // Check element name if XSS protection is enabled
53
+ if (xssSanitize && // Disallow dangerous elements
54
+ DANGEROUS_ELEMENTS.includes(nodeName)) {
55
+ return el;
56
+ }
57
+ const node = (el.ownerDocument ?? document).createElement(nodeName);
58
+ for (const child of el.childNodes) {
59
+ node.append(child.cloneNode(true));
60
+ }
61
+ const fields = getFields(el, attr);
62
+ const attrs = new Set(fields.map((field) => field.propName));
63
+ for (const { name, value } of el.attributes) {
64
+ if (attrs.has(name)) {
65
+ continue;
66
+ }
67
+ // Check attribute value if XSS protection is enabled
68
+ if (xssSanitize) {
69
+ const sanitizedValue = sanitizeAttributeValue(name, value);
70
+ if (sanitizedValue !== null) {
71
+ node.setAttribute(name, sanitizedValue);
72
+ }
73
+ continue;
74
+ }
75
+ node.setAttribute(name, value);
76
+ }
77
+ el.replaceWith(node);
78
+ return node;
79
+ }
80
+ /**
81
+ * Removes dangerous script elements from HTML string
82
+ * @param html HTML string
83
+ * @returns Sanitized HTML
84
+ */
85
+ export function sanitizeHtml(html) {
86
+ if (typeof html !== 'string') {
87
+ return '';
88
+ }
89
+ // Simple approach to handle script tags content
90
+ // First remove script tags but keep their content
91
+ let sanitized = html;
92
+ // For each dangerous element, replace tag but keep content using regex
93
+ // Example: <script>alert("XSS")</script> -> alert("XSS")
94
+ for (const tagName of DANGEROUS_ELEMENTS) {
95
+ const regex = new RegExp(`<${tagName}[^>]*>(.*?)<\\/${tagName}>`, 'gis');
96
+ sanitized = sanitized.replace(regex, '$1');
97
+ }
98
+ // Parse HTML safely using DOM parser for remaining sanitization
99
+ const doc = new DOMParser().parseFromString(sanitized, 'text/html');
100
+ const body = doc.body;
101
+ // Remove event handler attributes from all elements
102
+ sanitizeElementAndChildren(body);
103
+ return body.innerHTML;
104
+ }
105
+ /**
106
+ * Sanitizes an element and all its child elements
107
+ * @param element Target element
108
+ */
109
+ function sanitizeElementAndChildren(element) {
110
+ // Remove dangerous attributes from current element
111
+ sanitizeAttributes(element);
112
+ // Process child elements recursively
113
+ const children = element.children;
114
+ for (const child of children) {
115
+ sanitizeElementAndChildren(child);
116
+ }
117
+ }
118
+ /**
119
+ * Sanitizes attribute value
120
+ * @param name Attribute name
121
+ * @param value Attribute value
122
+ * @returns Safe attribute value, or null if dangerous
123
+ */
124
+ export function sanitizeAttributeValue(name, value) {
125
+ if (typeof value !== 'string') {
126
+ return value;
127
+ }
128
+ name = name.toLowerCase();
129
+ // Disallow event handler attributes
130
+ if (name.startsWith('on')) {
131
+ return null;
132
+ }
133
+ // Check for dangerous protocol in link attributes
134
+ if (name === 'href' ||
135
+ name === 'src' ||
136
+ name === 'action' ||
137
+ name === 'formaction' ||
138
+ name === 'xlink:href') {
139
+ const trimmedValue = value.trim().toLowerCase();
140
+ if (trimmedValue.startsWith('javascript:') ||
141
+ trimmedValue.startsWith('data:') ||
142
+ trimmedValue.startsWith('vbscript:')) {
143
+ return null;
144
+ }
145
+ }
146
+ // Other dangerous attributes
147
+ if (['manifest'].includes(name)) {
148
+ return null;
149
+ }
150
+ return value;
151
+ }
152
+ /**
153
+ * Removes dangerous attributes from an element
154
+ * @param element Target element
155
+ */
156
+ function sanitizeAttributes(element) {
157
+ // Collect list of dangerous attributes
158
+ const dangerousAttrs = [...element.attributes]
159
+ .filter((attr) => sanitizeAttributeValue(attr.name, attr.value) === null)
160
+ .map((attr) => attr.name);
161
+ // Remove dangerous attributes
162
+ for (const attrName of dangerousAttrs) {
163
+ element.removeAttribute(attrName);
164
+ }
165
+ }
166
+ /**
167
+ *
168
+ * @param data
169
+ * @param definedFields
170
+ */
171
+ export function maxLengthOf(data, definedFields) {
172
+ let maxLength = 0;
173
+ for (const key in data) {
174
+ if (definedFields.includes(key) && Array.isArray(data[key])) {
175
+ maxLength = Math.max(maxLength, data[key].length);
176
+ }
177
+ }
178
+ return maxLength;
179
+ }
180
+ /**
181
+ *
182
+ * @param el
183
+ * @param attr
184
+ */
185
+ export function definedFields(el, attr) {
186
+ const fieldDefinitions = getFields(el, attr);
187
+ return fieldDefinitions.map((field) => field.fieldName);
188
+ }
189
+ /**
190
+ *
191
+ * @param el
192
+ * @param attr
193
+ */
194
+ export function getFields(el, attr) {
195
+ const fields = el.getAttribute(`data-${attr}`);
196
+ if (!fields) {
197
+ return [];
198
+ }
199
+ return parseFields(fields);
200
+ }
201
+ /**
202
+ *
203
+ * @param el
204
+ * @param attr
205
+ * @param dataKeyName
206
+ */
207
+ export function hasField(el, attr, dataKeyName) {
208
+ const fieldDefinitions = getFields(el, attr);
209
+ return fieldDefinitions.some((field) => field.fieldName === dataKeyName);
210
+ }
211
+ /**
212
+ *
213
+ * @param fields
214
+ * @param oldPropName
215
+ * @param newPropName
27
216
  */
28
- export function kebabCase(str) {
29
- return str.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
217
+ export function replaceProp(fields, oldPropName, newPropName) {
218
+ return fields.map((field) => {
219
+ if (field.propName === oldPropName) {
220
+ return {
221
+ ...field,
222
+ propName: newPropName,
223
+ };
224
+ }
225
+ return field;
226
+ });
30
227
  }
31
228
  /**
32
229
  *
33
- * @param str
230
+ * @param fields
231
+ * @param propName
34
232
  */
35
- export function camelCase(str) {
36
- return str.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
233
+ export function removeProp(fields, propName) {
234
+ return fields.filter((field) => field.propName !== propName);
235
+ }
236
+ /**
237
+ * - 値が配列の場合は、index に対応する値を返す。
238
+ * - 値が配列でない場合は、そのまま返す。
239
+ * - ⚠️ 値が配列で、index に対応する値が undefined の場合は、配列の最初の値を返す。
240
+ * @param data
241
+ * @param index
242
+ */
243
+ export function flattenData(data, index) {
244
+ const result = {};
245
+ for (const key in data) {
246
+ result[key] = Array.isArray(data[key])
247
+ ? data[key][index] === undefined
248
+ ? data[key][0]
249
+ : data[key][index]
250
+ : data[key];
251
+ }
252
+ return result;
253
+ }
254
+ /**
255
+ * 特定の要素で、`propName in element`で判定できない属性名
256
+ */
257
+ const specificProp = {
258
+ /*
259
+ * JSDOMの未実装対策
260
+ * @see https://github.com/jsdom/jsdom/blob/main/lib/jsdom/living/nodes/HTMLSourceElement.webidl
261
+ * Living Standardでは実装済み
262
+ * @see https://html.spec.whatwg.org/multipage/embedded-content.html#htmlsourceelement
263
+ */
264
+ source: ['width', 'height'],
265
+ };
266
+ /**
267
+ *
268
+ * @param el
269
+ * @param name
270
+ */
271
+ export function propInElement(el, name) {
272
+ const has = name in el;
273
+ if (has) {
274
+ return true;
275
+ }
276
+ return specificProp[el.localName]?.includes(name) ?? false;
37
277
  }
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@burger-editor/frozen-patty",
3
- "version": "4.0.0-alpha.2",
4
- "description": "Pure HTML to JSON converter that not use template engine.",
3
+ "version": "4.0.0-alpha.22",
4
+ "description": "Pure HTML to JSON converter that does not use a template engine.",
5
5
  "author": "D-ZERO",
6
- "license": "MIT",
6
+ "license": "(MIT OR Apache-2.0)",
7
7
  "repository": {
8
8
  "url": "https://github.com/d-zero-dev/BurgerEditor.git"
9
9
  },
10
- "private": false,
11
10
  "publishConfig": {
12
11
  "access": "public"
13
12
  },
@@ -41,5 +40,8 @@
41
40
  "dev": "tsc --watch --project tsconfig.build.json",
42
41
  "clean": "tsc --build --clean"
43
42
  },
44
- "gitHead": "1cea9c5b36023cad1acfc2b3ef8b18c0d5bf79c2"
43
+ "dependencies": {
44
+ "@burger-editor/utils": "4.0.0-alpha.22"
45
+ },
46
+ "gitHead": "2ef680acadafee63247905c7efd92e134d89ae19"
45
47
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2017-2024 D-ZERO Co., Ltd.
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,8 +0,0 @@
1
- /**
2
- *
3
- * @param field
4
- */
5
- export declare function fieldNameParser(field: string): {
6
- fieldName: string;
7
- propName: string;
8
- };
@@ -1,18 +0,0 @@
1
- import { camelCase } from './utils.js';
2
- /**
3
- *
4
- * @param field
5
- */
6
- export function fieldNameParser(field) {
7
- field = field.trim().toLowerCase();
8
- if (!field) {
9
- throw new Error('Field name is empty.');
10
- }
11
- let [fieldName, propName] = field.split(':');
12
- propName = propName?.trim() || '';
13
- fieldName = fieldName?.trim() || propName || field;
14
- return {
15
- fieldName: camelCase(fieldName),
16
- propName,
17
- };
18
- }
package/dist/get.js DELETED
@@ -1,19 +0,0 @@
1
- import { getValues } from './get-values.js';
2
- import { arrayToHash } from './utils.js';
3
- /**
4
- *
5
- * @param el
6
- * @param attr
7
- * @param typeConvert
8
- * @param filter
9
- */
10
- export default function (el, attr, typeConvert, filter) {
11
- // eslint-disable-next-line unicorn/prefer-spread
12
- const filedElements = Array.from(el.querySelectorAll(`[data-${attr}]`));
13
- let values = [];
14
- for (const _el of filedElements) {
15
- values = [...values, ...getValues(_el, typeConvert, attr, filter)];
16
- }
17
- const result = arrayToHash(values);
18
- return result;
19
- }
package/dist/set.d.ts DELETED
@@ -1,9 +0,0 @@
1
- import type { Filter, FrozenPattyData } from './types.js';
2
- /**
3
- *
4
- * @param el
5
- * @param data
6
- * @param attr
7
- * @param filter
8
- */
9
- export default function (el: Element, data: FrozenPattyData, attr: string, filter?: Filter): Element;
package/dist/set.js DELETED
@@ -1,60 +0,0 @@
1
- import { setValue } from './set-value.js';
2
- import { kebabCase } from './utils.js';
3
- /**
4
- *
5
- * @param el
6
- * @param data
7
- * @param attr
8
- * @param filter
9
- */
10
- export default function (el, data, attr, filter) {
11
- el = el.cloneNode(true);
12
- for (const dataKeyName in data) {
13
- if (dataKeyName in data) {
14
- const datum = data[dataKeyName];
15
- const selector = `[data-${attr}*="${kebabCase(dataKeyName)}"]`;
16
- // eslint-disable-next-line unicorn/prefer-spread
17
- const targetList = Array.from(el.querySelectorAll(selector));
18
- if (Array.isArray(datum)) {
19
- const targetEl = targetList[0];
20
- if (!targetEl) {
21
- continue;
22
- }
23
- const listRoot = targetEl.closest(`[data-${attr}-list]`);
24
- if (!listRoot || (listRoot && listRoot.children.length === 0)) {
25
- continue;
26
- }
27
- const listItem = listRoot.children[0]?.cloneNode(true);
28
- while (listItem && datum.length > listRoot.children.length) {
29
- listRoot.append(listItem.cloneNode(true));
30
- }
31
- // eslint-disable-next-line unicorn/prefer-spread
32
- const newChildren = Array.from(listRoot.querySelectorAll(selector));
33
- // eslint-disable-next-line unicorn/prefer-spread
34
- const oldChildList = Array.from(listRoot.children);
35
- let deleteNodeList = [];
36
- for (const [i, child] of [...newChildren].entries()) {
37
- if (datum[i] == null) {
38
- setValue(child, dataKeyName, '', attr, filter);
39
- const oldChild = oldChildList[i];
40
- if (oldChild) {
41
- deleteNodeList.push(oldChild);
42
- }
43
- }
44
- else {
45
- setValue(child, dataKeyName, datum[i], attr, filter);
46
- deleteNodeList = [];
47
- }
48
- }
49
- for (const node of deleteNodeList)
50
- node.remove();
51
- }
52
- else {
53
- for (const [, targetEl] of [...targetList].entries()) {
54
- setValue(targetEl, dataKeyName, datum, attr, filter);
55
- }
56
- }
57
- }
58
- }
59
- return el;
60
- }