@burger-editor/frozen-patty 4.0.0-alpha.2 → 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/README.md +548 -21
- package/dist/field-name-stringifier.d.ts +14 -0
- package/dist/field-name-stringifier.js +26 -0
- package/dist/frozen-patty.d.ts +7 -0
- package/dist/frozen-patty.js +18 -5
- package/dist/get-component.d.ts +9 -0
- package/dist/get-component.js +29 -0
- package/dist/get-values.js +47 -29
- package/dist/handle-picture-element.d.ts +6 -0
- package/dist/handle-picture-element.js +31 -0
- package/dist/parse-fields.d.ts +6 -0
- package/dist/parse-fields.js +38 -0
- package/dist/set-comonent.d.ts +10 -0
- package/dist/set-comonent.js +71 -0
- package/dist/set-component.d.ts +10 -0
- package/dist/set-component.js +80 -0
- package/dist/set-value.d.ts +4 -2
- package/dist/set-value.js +98 -66
- package/dist/set.d.ts +2 -1
- package/dist/set.js +5 -4
- package/dist/stringify-fields.d.ts +14 -0
- package/dist/stringify-fields.js +26 -0
- package/dist/types.d.ts +7 -0
- package/dist/utils.d.ts +78 -1
- package/dist/utils.js +268 -2
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
44
|
+
"gitHead": "823486d58579275ae556942c5d7a4aef4e1f634e"
|
|
45
45
|
}
|