@burger-editor/frozen-patty 0.11.0 → 4.0.0-alpha.10

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/set.d.ts CHANGED
@@ -1,2 +1,9 @@
1
1
  import type { Filter, FrozenPattyData } from './types.js';
2
+ /**
3
+ *
4
+ * @param el
5
+ * @param data
6
+ * @param attr
7
+ * @param filter
8
+ */
2
9
  export default function (el: Element, data: FrozenPattyData, attr: string, filter?: Filter): Element;
package/dist/set.js CHANGED
@@ -1,10 +1,18 @@
1
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
+ */
2
10
  export default function (el, data, attr, filter) {
3
11
  el = el.cloneNode(true);
4
12
  for (const dataKeyName in data) {
5
13
  if (dataKeyName in data) {
6
14
  const datum = data[dataKeyName];
7
- const selector = `[data-${attr}*="${dataKeyName}"]`;
15
+ const selector = `[data-${attr}*="${kebabCase(dataKeyName)}"]`;
8
16
  // eslint-disable-next-line unicorn/prefer-spread
9
17
  const targetList = Array.from(el.querySelectorAll(selector));
10
18
  if (Array.isArray(datum)) {
@@ -0,0 +1,14 @@
1
+ import type { FieldDefinition } from './types.js';
2
+ /**
3
+ * Combines multiple field definitions with comma separation
4
+ * @param fields Array of field definitions
5
+ * @returns Comma-separated field definition string
6
+ */
7
+ export declare function stringifyFields(fields: readonly FieldDefinition[]): string;
8
+ /**
9
+ * Generates a field definition string from field name and attribute name
10
+ * @param fieldName Field name
11
+ * @param propName Attribute name (optional)
12
+ * @returns Field definition string
13
+ */
14
+ export declare function stringifyField(fieldName: string, propName?: string): string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Combines multiple field definitions with comma separation
3
+ * @param fields Array of field definitions
4
+ * @returns Comma-separated field definition string
5
+ */
6
+ export function stringifyFields(fields) {
7
+ return fields
8
+ .map((field) => stringifyField(field.fieldName, field.propName))
9
+ .join(', ');
10
+ }
11
+ /**
12
+ * Generates a field definition string from field name and attribute name
13
+ * @param fieldName Field name
14
+ * @param propName Attribute name (optional)
15
+ * @returns Field definition string
16
+ */
17
+ export function stringifyField(fieldName, propName) {
18
+ if (!propName) {
19
+ return fieldName;
20
+ }
21
+ // Use shorthand notation when fieldName and propName are the same
22
+ if (fieldName === propName) {
23
+ return `:${propName}`;
24
+ }
25
+ return `${fieldName}:${propName}`;
26
+ }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  export interface FrozenPattyData {
2
2
  [filed: string]: PrimitiveData;
3
3
  }
4
+ export interface FrozenPattyFlattenData {
5
+ [field: string]: PrimitiveDatum;
6
+ }
7
+ export interface FieldDefinition {
8
+ fieldName: string;
9
+ propName?: string;
10
+ }
4
11
  export type PrimitiveDatum = string | number | boolean | null | undefined;
5
12
  export type PrimitiveData = PrimitiveDatum | PrimitiveDatum[];
6
13
  export type Filter = <T>(value: T) => T;
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,85 @@
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[];
6
+ /**
7
+ *
8
+ * @param kvs
9
+ */
2
10
  export declare function arrayToHash(kvs: [keyof FrozenPattyData, PrimitiveDatum, boolean][]): {
3
11
  [index: string]: PrimitiveDatum | PrimitiveDatum[];
4
12
  };
13
+ /**
14
+ *
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
78
+ */
79
+ export declare function flattenData(data: FrozenPattyData, index: number): FrozenPattyFlattenData;
80
+ /**
81
+ *
82
+ * @param el
83
+ * @param name
84
+ */
85
+ export declare function propInElement(el: Element, name: string): boolean;
package/dist/utils.js CHANGED
@@ -1,21 +1,277 @@
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
+ ];
16
+ /**
17
+ *
18
+ * @param kvs
19
+ */
1
20
  export function arrayToHash(kvs) {
2
21
  const result = {};
3
22
  for (const kv of kvs) {
4
23
  const k = kv[0];
5
24
  const v = kv[1];
6
25
  const toArray = kv[2];
7
- if (toArray) {
8
- const arrayPropVal = result[k];
9
- if (Array.isArray(arrayPropVal)) {
10
- arrayPropVal.push(v);
11
- }
12
- else {
13
- result[k] = k in result ? [arrayPropVal, v] : [v];
26
+ if (!toArray) {
27
+ result[k] = v;
28
+ continue;
29
+ }
30
+ const arrayPropVal = result[k];
31
+ if (Array.isArray(arrayPropVal)) {
32
+ arrayPropVal.push(v);
33
+ continue;
34
+ }
35
+ result[k] = k in result ? [arrayPropVal, v] : [v];
36
+ }
37
+ return result;
38
+ }
39
+ /**
40
+ *
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);
14
72
  }
73
+ continue;
15
74
  }
16
- else {
17
- result[k] = v;
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);
18
176
  }
19
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
216
+ */
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
+ });
227
+ }
228
+ /**
229
+ *
230
+ * @param fields
231
+ * @param propName
232
+ */
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
+ }
20
252
  return result;
21
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;
277
+ }
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@burger-editor/frozen-patty",
3
- "version": "0.11.0",
4
- "description": "Pure HTML to JSON converter that not use template engine.",
3
+ "version": "4.0.0-alpha.10",
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
- "url": "https://github.com/d-zero-dev/burger-editor.git"
8
+ "url": "https://github.com/d-zero-dev/BurgerEditor.git"
9
9
  },
10
- "private": false,
11
10
  "publishConfig": {
12
11
  "access": "public"
13
12
  },
@@ -37,8 +36,12 @@
37
36
  "dist"
38
37
  ],
39
38
  "scripts": {
40
- "build": "tsc",
39
+ "build": "tsc --project tsconfig.build.json",
40
+ "dev": "tsc --watch --project tsconfig.build.json",
41
41
  "clean": "tsc --build --clean"
42
42
  },
43
- "gitHead": "3cc9deb4a7316211f6d75cc14c346775bdb4f907"
43
+ "dependencies": {
44
+ "@burger-editor/utils": "4.0.0-alpha.10"
45
+ },
46
+ "gitHead": "73bc904105e8057397fc9b7970c4b4bf3944a901"
44
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.