@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/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/set-value.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { kebabCase } from './utils.js';
|
|
1
|
+
import { parseFields } from './parse-fields.js';
|
|
2
|
+
import { kebabCase, propInElement, replaceNode, sanitizeAttributeValue, sanitizeHtml, } from './utils.js';
|
|
3
3
|
/**
|
|
4
4
|
* Set value to an element
|
|
5
5
|
*
|
|
@@ -11,58 +11,67 @@ import { kebabCase } from './utils.js';
|
|
|
11
11
|
* @param datum A datum of value
|
|
12
12
|
* @param attr Data attribute name for specifying the node that FrozenPatty treats as a field
|
|
13
13
|
* @param filter
|
|
14
|
+
* @param xssSanitize Enable XSS protection
|
|
14
15
|
*/
|
|
15
|
-
export function setValue(el, name, datum, attr = 'field', filter) {
|
|
16
|
+
export function setValue(el, name, datum, attr = 'field', filter, xssSanitize = true) {
|
|
16
17
|
const rawValue = el.getAttribute(`data-${attr}`);
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
default: {
|
|
49
|
-
cssValue = `${datum}`;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
el.setAttribute('style', `${cssPropertyName}: ${cssValue}`);
|
|
53
|
-
}
|
|
54
|
-
else if (el instanceof HTMLElement) {
|
|
55
|
-
// HTMLElement
|
|
56
|
-
set(el, attr, propName, datum);
|
|
18
|
+
const fields = parseFields(rawValue ?? '');
|
|
19
|
+
const field = fields.find((field) => field.fieldName === name);
|
|
20
|
+
if (!field) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const { propName } = field;
|
|
24
|
+
if (filter) {
|
|
25
|
+
datum = filter(datum);
|
|
26
|
+
}
|
|
27
|
+
// console.log({ name, field, fieldName, propName, datum, el: el.innerHTML });
|
|
28
|
+
if (!propName) {
|
|
29
|
+
setContent(el, datum, undefined, xssSanitize);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (propName === 'style' && el instanceof HTMLElement) {
|
|
33
|
+
el.style.cssText = `${datum}`;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (/^style\([a-z-]+\)$/i.test(propName)) {
|
|
37
|
+
const cssPropertyName = propName.replace(/^style\(([a-z-]+)\)$/i, '$1');
|
|
38
|
+
let cssValue;
|
|
39
|
+
switch (cssPropertyName) {
|
|
40
|
+
case 'background-image': {
|
|
41
|
+
// Using CSS method would create absolute URLs with host
|
|
42
|
+
// This would prevent migration from demo to production server
|
|
43
|
+
// Using simple string insertion (setAttribute) instead
|
|
44
|
+
// URL may not contain multibyte or whitespace characters, but escape anyway
|
|
45
|
+
const url = encodeURI(`${datum}`);
|
|
46
|
+
cssValue = `url(${url})`;
|
|
47
|
+
break;
|
|
57
48
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
//
|
|
50
|
+
// TODO: Handle other cases where values need units
|
|
51
|
+
//
|
|
52
|
+
default: {
|
|
53
|
+
cssValue = `${datum}`;
|
|
61
54
|
}
|
|
62
|
-
return;
|
|
63
55
|
}
|
|
64
|
-
|
|
56
|
+
el.setAttribute('style', `${cssPropertyName}: ${cssValue}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (el instanceof HTMLElement) {
|
|
60
|
+
// HTMLElement
|
|
61
|
+
set(el, attr, propName, datum, xssSanitize);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// SVGElement or more
|
|
65
|
+
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
|
66
|
+
// Check attribute value if XSS protection is enabled
|
|
67
|
+
if (xssSanitize) {
|
|
68
|
+
const safeValue = sanitizeAttributeValue(propName, `${datum}`);
|
|
69
|
+
if (safeValue !== null) {
|
|
70
|
+
el.setAttribute(propName, safeValue);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
65
73
|
}
|
|
74
|
+
el.setAttribute(propName, `${datum}`);
|
|
66
75
|
}
|
|
67
76
|
/**
|
|
68
77
|
*
|
|
@@ -70,41 +79,54 @@ export function setValue(el, name, datum, attr = 'field', filter) {
|
|
|
70
79
|
* @param prefix
|
|
71
80
|
* @param name
|
|
72
81
|
* @param datum
|
|
82
|
+
* @param xssSanitize
|
|
73
83
|
*/
|
|
74
|
-
function set(el, prefix, name, datum) {
|
|
84
|
+
function set(el, prefix, name, datum, xssSanitize = true) {
|
|
75
85
|
if (datum == null) {
|
|
76
86
|
el.removeAttribute(name);
|
|
77
87
|
return;
|
|
78
88
|
}
|
|
79
89
|
switch (name) {
|
|
80
90
|
case 'text': {
|
|
81
|
-
setContent(el, datum, false);
|
|
91
|
+
setContent(el, datum, false, xssSanitize);
|
|
82
92
|
return;
|
|
83
93
|
}
|
|
84
94
|
case 'html': {
|
|
85
|
-
|
|
95
|
+
// Sanitize HTML if XSS protection is enabled
|
|
96
|
+
if (xssSanitize && typeof datum === 'string') {
|
|
97
|
+
setContent(el, sanitizeHtml(datum), true, xssSanitize);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
setContent(el, datum, true, xssSanitize);
|
|
101
|
+
}
|
|
86
102
|
return;
|
|
87
103
|
}
|
|
88
104
|
case 'node': {
|
|
89
105
|
if (typeof datum !== 'string') {
|
|
90
106
|
return;
|
|
91
107
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (const { name, value } of el.attributes) {
|
|
101
|
-
node.setAttribute(name, value);
|
|
102
|
-
}
|
|
103
|
-
el.replaceWith(node);
|
|
108
|
+
replaceNode(el, datum, prefix, xssSanitize);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// If XSS protection is enabled
|
|
113
|
+
if (xssSanitize) {
|
|
114
|
+
// Don't set event handler attributes (starting with 'on')
|
|
115
|
+
if (name.toLowerCase().startsWith('on')) {
|
|
104
116
|
return;
|
|
105
117
|
}
|
|
118
|
+
const beforeSanitize = datum;
|
|
119
|
+
datum = sanitizeAttributeValue(name, datum);
|
|
120
|
+
// If sanitization results in null value (except when original value was null), don't change the value
|
|
121
|
+
if (datum === null && beforeSanitize !== null) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (datum == null) {
|
|
126
|
+
el.removeAttribute(name);
|
|
127
|
+
return;
|
|
106
128
|
}
|
|
107
|
-
if (!name.startsWith('data-') && !(name
|
|
129
|
+
if (!name.startsWith('data-') && !propInElement(el, name)) {
|
|
108
130
|
const dataAttr = `data-${prefix}-${kebabCase(name)}`;
|
|
109
131
|
if (el.hasAttribute(dataAttr)) {
|
|
110
132
|
el.setAttribute(dataAttr, `${datum}`);
|
|
@@ -676,8 +698,9 @@ function set(el, prefix, name, datum) {
|
|
|
676
698
|
* @param el
|
|
677
699
|
* @param datum
|
|
678
700
|
* @param asHtml
|
|
701
|
+
* @param xssSanitize
|
|
679
702
|
*/
|
|
680
|
-
export function setContent(el, datum, asHtml = true) {
|
|
703
|
+
export function setContent(el, datum, asHtml = true, xssSanitize = true) {
|
|
681
704
|
if (el instanceof HTMLInputElement ||
|
|
682
705
|
el instanceof HTMLSelectElement ||
|
|
683
706
|
el instanceof HTMLTextAreaElement ||
|
|
@@ -695,9 +718,18 @@ export function setContent(el, datum, asHtml = true) {
|
|
|
695
718
|
return;
|
|
696
719
|
}
|
|
697
720
|
if (asHtml) {
|
|
698
|
-
|
|
721
|
+
// Sanitize HTML if XSS protection is enabled
|
|
722
|
+
const htmlStr = datum == null ? '' : `${datum}`;
|
|
723
|
+
if (xssSanitize && typeof htmlStr === 'string') {
|
|
724
|
+
// sanitizeHtml will now replace dangerous elements instead of removing them
|
|
725
|
+
el.innerHTML = sanitizeHtml(htmlStr);
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
el.innerHTML = htmlStr;
|
|
729
|
+
}
|
|
699
730
|
return;
|
|
700
731
|
}
|
|
732
|
+
// No need to sanitize textContent (automatically escaped)
|
|
701
733
|
el.textContent = datum == null ? '' : `${datum}`;
|
|
702
734
|
}
|
|
703
735
|
/**
|
package/dist/set.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ import type { Filter, FrozenPattyData } from './types.js';
|
|
|
5
5
|
* @param data
|
|
6
6
|
* @param attr
|
|
7
7
|
* @param filter
|
|
8
|
+
* @param xssSanitize Enable XSS protection
|
|
8
9
|
*/
|
|
9
|
-
export default function (el: Element, data: FrozenPattyData, attr: string, filter?: Filter): Element;
|
|
10
|
+
export default function (el: Element, data: FrozenPattyData, attr: string, filter?: Filter, xssSanitize?: boolean): Element;
|
package/dist/set.js
CHANGED
|
@@ -6,8 +6,9 @@ import { kebabCase } from './utils.js';
|
|
|
6
6
|
* @param data
|
|
7
7
|
* @param attr
|
|
8
8
|
* @param filter
|
|
9
|
+
* @param xssSanitize Enable XSS protection
|
|
9
10
|
*/
|
|
10
|
-
export default function (el, data, attr, filter) {
|
|
11
|
+
export default function (el, data, attr, filter, xssSanitize = true) {
|
|
11
12
|
el = el.cloneNode(true);
|
|
12
13
|
for (const dataKeyName in data) {
|
|
13
14
|
if (dataKeyName in data) {
|
|
@@ -35,14 +36,14 @@ export default function (el, data, attr, filter) {
|
|
|
35
36
|
let deleteNodeList = [];
|
|
36
37
|
for (const [i, child] of [...newChildren].entries()) {
|
|
37
38
|
if (datum[i] == null) {
|
|
38
|
-
setValue(child, dataKeyName, '', attr, filter);
|
|
39
|
+
setValue(child, dataKeyName, '', attr, filter, xssSanitize);
|
|
39
40
|
const oldChild = oldChildList[i];
|
|
40
41
|
if (oldChild) {
|
|
41
42
|
deleteNodeList.push(oldChild);
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
else {
|
|
45
|
-
setValue(child, dataKeyName, datum[i], attr, filter);
|
|
46
|
+
setValue(child, dataKeyName, datum[i], attr, filter, xssSanitize);
|
|
46
47
|
deleteNodeList = [];
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -51,7 +52,7 @@ export default function (el, data, attr, filter) {
|
|
|
51
52
|
}
|
|
52
53
|
else {
|
|
53
54
|
for (const [, targetEl] of [...targetList].entries()) {
|
|
54
|
-
setValue(targetEl, dataKeyName, datum, attr, filter);
|
|
55
|
+
setValue(targetEl, dataKeyName, datum, attr, filter, xssSanitize);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
}
|
|
@@ -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,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
|
|
@@ -16,3 +20,76 @@ export declare function kebabCase(str: string): string;
|
|
|
16
20
|
* @param str
|
|
17
21
|
*/
|
|
18
22
|
export declare function camelCase(str: string): string;
|
|
23
|
+
/**
|
|
24
|
+
*
|
|
25
|
+
* @param el
|
|
26
|
+
* @param nodeName
|
|
27
|
+
* @param attr
|
|
28
|
+
* @param xssSanitize
|
|
29
|
+
*/
|
|
30
|
+
export declare function replaceNode(el: Element, nodeName: string, attr: string, xssSanitize?: boolean): Element;
|
|
31
|
+
/**
|
|
32
|
+
* Removes dangerous script elements from HTML string
|
|
33
|
+
* @param html HTML string
|
|
34
|
+
* @returns Sanitized HTML
|
|
35
|
+
*/
|
|
36
|
+
export declare function sanitizeHtml(html: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Sanitizes attribute value
|
|
39
|
+
* @param name Attribute name
|
|
40
|
+
* @param value Attribute value
|
|
41
|
+
* @returns Safe attribute value, or null if dangerous
|
|
42
|
+
*/
|
|
43
|
+
export declare function sanitizeAttributeValue<T extends PrimitiveDatum>(name: string, value: T): T | null;
|
|
44
|
+
/**
|
|
45
|
+
*
|
|
46
|
+
* @param data
|
|
47
|
+
* @param definedFields
|
|
48
|
+
*/
|
|
49
|
+
export declare function maxLengthOf(data: FrozenPattyData, definedFields: string[]): number;
|
|
50
|
+
/**
|
|
51
|
+
*
|
|
52
|
+
* @param el
|
|
53
|
+
* @param attr
|
|
54
|
+
*/
|
|
55
|
+
export declare function definedFields(el: Element, attr: string): string[];
|
|
56
|
+
/**
|
|
57
|
+
*
|
|
58
|
+
* @param el
|
|
59
|
+
* @param attr
|
|
60
|
+
*/
|
|
61
|
+
export declare function getFields(el: Element, attr: string): readonly FieldDefinition[];
|
|
62
|
+
/**
|
|
63
|
+
*
|
|
64
|
+
* @param el
|
|
65
|
+
* @param attr
|
|
66
|
+
* @param dataKeyName
|
|
67
|
+
*/
|
|
68
|
+
export declare function hasField(el: Element, attr: string, dataKeyName: string): boolean;
|
|
69
|
+
/**
|
|
70
|
+
*
|
|
71
|
+
* @param fields
|
|
72
|
+
* @param oldPropName
|
|
73
|
+
* @param newPropName
|
|
74
|
+
*/
|
|
75
|
+
export declare function replaceProp(fields: readonly FieldDefinition[], oldPropName: string, newPropName: string): FieldDefinition[];
|
|
76
|
+
/**
|
|
77
|
+
*
|
|
78
|
+
* @param fields
|
|
79
|
+
* @param propName
|
|
80
|
+
*/
|
|
81
|
+
export declare function removeProp(fields: readonly FieldDefinition[], propName: string): FieldDefinition[];
|
|
82
|
+
/**
|
|
83
|
+
* - 値が配列の場合は、index に対応する値を返す。
|
|
84
|
+
* - 値が配列でない場合は、そのまま返す。
|
|
85
|
+
* - ⚠️ 値が配列で、index に対応する値が undefined の場合は、配列の最初の値を返す。
|
|
86
|
+
* @param data
|
|
87
|
+
* @param index
|
|
88
|
+
*/
|
|
89
|
+
export declare function flattenData(data: FrozenPattyData, index: number): FrozenPattyFlattenData;
|
|
90
|
+
/**
|
|
91
|
+
*
|
|
92
|
+
* @param el
|
|
93
|
+
* @param name
|
|
94
|
+
*/
|
|
95
|
+
export declare function propInElement(el: Element, name: string): boolean;
|