@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.
@@ -0,0 +1,29 @@
1
+ import { getValues } from './get-values.js';
2
+ import { arrayToHash } from './utils.js';
3
+ const reverseListElementSelectors = ['picture'];
4
+ /**
5
+ *
6
+ * @param el
7
+ * @param attr
8
+ * @param typeConvert
9
+ * @param filter
10
+ */
11
+ export function getComponent(el, attr, typeConvert, filter) {
12
+ el = el.cloneNode(true);
13
+ const reverseListElements = el.querySelectorAll(`:is(${reverseListElementSelectors.join(',')})[data-${attr}-list]`);
14
+ for (const reverseListElement of reverseListElements) {
15
+ const children = [];
16
+ while (reverseListElement.lastElementChild) {
17
+ children.push(reverseListElement.lastElementChild);
18
+ reverseListElement.lastElementChild.remove();
19
+ }
20
+ reverseListElement.append(...children);
21
+ }
22
+ const filedElements = el.querySelectorAll(`[data-${attr}]`);
23
+ let values = [];
24
+ for (const _el of filedElements) {
25
+ values = [...values, ...getValues(_el, typeConvert, attr, filter)];
26
+ }
27
+ const result = arrayToHash(values);
28
+ return result;
29
+ }
@@ -1,5 +1,5 @@
1
- import { fieldNameParser } from './field-name-parser.js';
2
- import { kebabCase } from './utils.js';
1
+ import { kebabCase } from '@burger-editor/utils';
2
+ import { parseFields } from './parse-fields.js';
3
3
  /**
4
4
  * Get value from an element
5
5
  * @param el A target element
@@ -9,16 +9,16 @@ import { kebabCase } from './utils.js';
9
9
  */
10
10
  export function getValues(el, convertType = false, attr = 'field', filter) {
11
11
  const result = [];
12
- const rawValue = el.getAttribute(`data-${attr}`);
13
- const listRoot = el.closest(`[data-${attr}-list]`);
14
- const forceArray = !!listRoot;
15
- if (rawValue == null) {
12
+ const query = el.getAttribute(`data-${attr}`);
13
+ if (query == null) {
16
14
  throw new Error(`data-${attr} attriblute is empty.`);
17
15
  }
18
- const fieldList = rawValue.split(/\s*,\s*/);
19
- for (const field of fieldList) {
16
+ const listRoot = el.closest(`[data-${attr}-list]`);
17
+ const forceArray = !!listRoot;
18
+ const fields = parseFields(query);
19
+ for (const field of fields) {
20
20
  let value;
21
- const { fieldName, propName } = fieldNameParser(field);
21
+ const { fieldName, propName } = field;
22
22
  if (propName) {
23
23
  switch (propName) {
24
24
  case 'node': {
@@ -33,6 +33,12 @@ export function getValues(el, convertType = false, attr = 'field', filter) {
33
33
  value = el.innerHTML.trim();
34
34
  break;
35
35
  }
36
+ case 'style': {
37
+ if (el instanceof HTMLElement) {
38
+ value = el.style.cssText;
39
+ }
40
+ break;
41
+ }
36
42
  default: {
37
43
  if (/^style\([a-z-]+\)$/i.test(propName)) {
38
44
  const css = propName.replace(/^style\(([a-z-]+)\)$/i, '$1');
@@ -49,7 +55,7 @@ export function getValues(el, convertType = false, attr = 'field', filter) {
49
55
  }
50
56
  break;
51
57
  }
52
- value = getAttribute(el, attr, propName, convertType);
58
+ value = getAttribute(el, propName, attr, convertType);
53
59
  }
54
60
  }
55
61
  }
@@ -77,11 +83,11 @@ export function getValues(el, convertType = false, attr = 'field', filter) {
77
83
  /**
78
84
  *
79
85
  * @param el
80
- * @param attr
81
86
  * @param keyAttr
87
+ * @param attr
82
88
  * @param typeConvert
83
89
  */
84
- function getAttribute(el, attr, keyAttr, typeConvert) {
90
+ function getAttribute(el, keyAttr, attr, typeConvert) {
85
91
  switch (keyAttr) {
86
92
  case 'contenteditable': {
87
93
  if (el instanceof HTMLElement) {
@@ -102,25 +108,37 @@ function getAttribute(el, attr, keyAttr, typeConvert) {
102
108
  // So to get inactive it is necessary to use the "hasAttribute" method.
103
109
  return el.hasAttribute('download') ? el.getAttribute('download') : null;
104
110
  }
105
- case 'href': {
106
- // return (el as HTMLAnchorElement).href;
107
- return el.getAttribute(keyAttr) ?? ''; // return plain string
111
+ case 'href':
112
+ case 'src': {
113
+ // Example: (el as HTMLAnchorElement).href;
114
+ // Expected: return defined value of plain string
115
+ return el.getAttribute(keyAttr) ?? '';
108
116
  }
109
- default: {
110
- let value;
111
- const dataAttr = ['data', attr, kebabCase(keyAttr)].join('-');
112
- if (el.hasAttribute(dataAttr)) {
113
- value = el.getAttribute(dataAttr) || '';
114
- }
115
- else {
116
- value = el.getAttribute(keyAttr) || '';
117
- }
118
- if (typeConvert) {
119
- value = convert(value);
120
- }
121
- return value;
117
+ }
118
+ let value =
119
+ // @ts-ignore
120
+ el[keyAttr];
121
+ if (value !== undefined) {
122
+ if (typeConvert) {
123
+ value = convert(value);
122
124
  }
125
+ return value;
123
126
  }
127
+ // For shorthand notation, get value from data-field-* attribute
128
+ const dataAttr = ['data', attr, kebabCase(keyAttr)].join('-');
129
+ if (el.hasAttribute(dataAttr)) {
130
+ return el.getAttribute(dataAttr) ?? '';
131
+ }
132
+ value = el.getAttribute(keyAttr) ?? null;
133
+ if ((value === '' || value == null) &&
134
+ typeof keyAttr === 'string' &&
135
+ keyAttr.startsWith('data-')) {
136
+ return '';
137
+ }
138
+ if (typeConvert) {
139
+ value = convert(value);
140
+ }
141
+ return value;
124
142
  }
125
143
  /**
126
144
  *
@@ -128,8 +146,9 @@ function getAttribute(el, attr, keyAttr, typeConvert) {
128
146
  */
129
147
  function convert(value) {
130
148
  value = parse(value);
131
- if (URL.canParse(value)) {
132
- const url = new URL(value, location.href);
149
+ const str = `${value}`;
150
+ if (URL.canParse(str)) {
151
+ const url = new URL(str, location.href);
133
152
  if (url.origin === location.origin) {
134
153
  return url.pathname;
135
154
  }
@@ -142,7 +161,7 @@ function convert(value) {
142
161
  */
143
162
  function parse(value) {
144
163
  try {
145
- return JSON.parse(value);
164
+ return JSON.parse(`${value}`);
146
165
  }
147
166
  catch {
148
167
  return value;
@@ -0,0 +1,6 @@
1
+ import type { FieldDefinition } from './types.js';
2
+ /**
3
+ *
4
+ * @param query
5
+ */
6
+ export declare function parseFields(query: string): readonly FieldDefinition[];
@@ -0,0 +1,38 @@
1
+ import { camelCase } from '@burger-editor/utils';
2
+ const cache = new Map();
3
+ /**
4
+ *
5
+ * @param query
6
+ */
7
+ export function parseFields(query) {
8
+ const cached = cache.get(query);
9
+ if (cached) {
10
+ return cached;
11
+ }
12
+ const fields = query.split(',');
13
+ const result = fields.map(parseField);
14
+ cache.set(query, result);
15
+ return result;
16
+ }
17
+ /**
18
+ *
19
+ * @param field
20
+ */
21
+ function parseField(field) {
22
+ field = field.trim().toLowerCase();
23
+ if (!field) {
24
+ throw new Error('Field name is empty.');
25
+ }
26
+ let [fieldName, propName,
27
+ // eslint-disable-next-line prefer-const
28
+ ...rest] = field.split(':');
29
+ if (rest.length > 0) {
30
+ throw new Error('Invalid field format.');
31
+ }
32
+ propName = propName?.trim() || '';
33
+ fieldName = fieldName?.trim() || propName || field;
34
+ return {
35
+ fieldName: camelCase(fieldName),
36
+ propName,
37
+ };
38
+ }
@@ -0,0 +1,10 @@
1
+ import type { Filter, FrozenPattyData } from './types.js';
2
+ /**
3
+ *
4
+ * @param el
5
+ * @param data
6
+ * @param attr
7
+ * @param filter
8
+ * @param xssSanitize Enable XSS protection
9
+ */
10
+ export declare function setComponent(el: Element, data: FrozenPattyData, attr: string, filter?: Filter, xssSanitize?: boolean): Element;
@@ -0,0 +1,91 @@
1
+ import { kebabCase } from '@burger-editor/utils';
2
+ import { setValue } from './set-value.js';
3
+ import { stringifyFields } from './stringify-fields.js';
4
+ import { definedFields, flattenData, getFields, hasField, maxLengthOf, removeProp, replaceNode, replaceProp, } from './utils.js';
5
+ /**
6
+ *
7
+ * @param el
8
+ * @param data
9
+ * @param attr
10
+ * @param filter
11
+ * @param xssSanitize Enable XSS protection
12
+ */
13
+ export function setComponent(el, data, attr, filter, xssSanitize = true) {
14
+ el = el.cloneNode(true);
15
+ for (const dataKeyName in data) {
16
+ const datum = data[dataKeyName];
17
+ if (Array.isArray(datum)) {
18
+ continue;
19
+ }
20
+ const targetList = [
21
+ // Include self
22
+ el,
23
+ // Perform rough filtering for performance
24
+ ...el.querySelectorAll(`[data-${attr}*="${kebabCase(dataKeyName)}"]`),
25
+ ]
26
+ // Perform more accurate filtering
27
+ .filter((el) => hasField(el, attr, dataKeyName));
28
+ for (const targetEl of targetList) {
29
+ setValue(targetEl, dataKeyName, datum, attr, filter, xssSanitize);
30
+ }
31
+ }
32
+ for (const listRoot of el.querySelectorAll(`[data-${attr}-list]`)) {
33
+ const decendants = listRoot.querySelectorAll('*');
34
+ const definedFieldsOfDecendants = new Set([...decendants].flatMap((el) => definedFields(el, attr)));
35
+ const maxLength = maxLengthOf(data, [...definedFieldsOfDecendants]);
36
+ const listItem = listRoot.children[0]?.cloneNode(true);
37
+ if (!listItem) {
38
+ continue;
39
+ }
40
+ while (listRoot.firstChild) {
41
+ listRoot.firstChild.remove();
42
+ }
43
+ // Track used paths for picture elements to avoid duplicates
44
+ const usedPaths = new Set();
45
+ for (let i = 0; i < maxLength; i++) {
46
+ let item = listItem.cloneNode(true);
47
+ const itemData = flattenData(data, i);
48
+ let reverse = false;
49
+ switch (listRoot.localName) {
50
+ case 'picture': {
51
+ reverse = true;
52
+ // Check for duplicate paths and skip if found
53
+ const pathValue = itemData.path;
54
+ if (pathValue != null && usedPaths.has(String(pathValue))) {
55
+ continue;
56
+ }
57
+ if (pathValue != null) {
58
+ usedPaths.add(String(pathValue));
59
+ }
60
+ let fields = getFields(item, attr);
61
+ if (i === 0) {
62
+ // Convert first item to img element
63
+ item = replaceNode(item, 'img', attr, xssSanitize);
64
+ fields = replaceProp(fields, 'srcset', 'src');
65
+ fields = removeProp(fields, 'sizes');
66
+ const fieldQuery = stringifyFields(fields);
67
+ item.setAttribute(`data-${attr}`, fieldQuery);
68
+ }
69
+ else {
70
+ // Convert subsequent items to source elements
71
+ item = replaceNode(item, 'source', attr, xssSanitize);
72
+ fields = replaceProp(fields, 'src', 'srcset');
73
+ fields = removeProp(fields, 'alt');
74
+ fields = removeProp(fields, 'loading');
75
+ const fieldQuery = stringifyFields(fields);
76
+ item.setAttribute(`data-${attr}`, fieldQuery);
77
+ }
78
+ break;
79
+ }
80
+ }
81
+ const newEl = setComponent(item, itemData, attr, filter, xssSanitize);
82
+ if (reverse) {
83
+ listRoot.prepend(newEl);
84
+ }
85
+ else {
86
+ listRoot.append(newEl);
87
+ }
88
+ }
89
+ }
90
+ return el;
91
+ }
@@ -10,12 +10,14 @@ import type { Filter, PrimitiveDatum } from './types.js';
10
10
  * @param datum A datum of value
11
11
  * @param attr Data attribute name for specifying the node that FrozenPatty treats as a field
12
12
  * @param filter
13
+ * @param xssSanitize Enable XSS protection
13
14
  */
14
- export declare function setValue(el: Element, name: string, datum: PrimitiveDatum, attr?: string, filter?: Filter): void;
15
+ export declare function setValue(el: Element, name: string, datum: PrimitiveDatum, attr?: string, filter?: Filter, xssSanitize?: boolean): void;
15
16
  /**
16
17
  *
17
18
  * @param el
18
19
  * @param datum
19
20
  * @param asHtml
21
+ * @param xssSanitize
20
22
  */
21
- export declare function setContent(el: Element, datum: PrimitiveDatum, asHtml?: boolean): void;
23
+ export declare function setContent(el: Element, datum: PrimitiveDatum, asHtml?: boolean, xssSanitize?: boolean): void;
package/dist/set-value.js CHANGED
@@ -1,5 +1,6 @@
1
- import { fieldNameParser } from './field-name-parser.js';
2
- import { kebabCase } from './utils.js';
1
+ import { kebabCase } from '@burger-editor/utils';
2
+ import { parseFields } from './parse-fields.js';
3
+ import { propInElement, replaceNode, sanitizeAttributeValue, sanitizeHtml, } from './utils.js';
3
4
  /**
4
5
  * Set value to an element
5
6
  *
@@ -11,58 +12,67 @@ import { kebabCase } from './utils.js';
11
12
  * @param datum A datum of value
12
13
  * @param attr Data attribute name for specifying the node that FrozenPatty treats as a field
13
14
  * @param filter
15
+ * @param xssSanitize Enable XSS protection
14
16
  */
15
- export function setValue(el, name, datum, attr = 'field', filter) {
17
+ export function setValue(el, name, datum, attr = 'field', filter, xssSanitize = true) {
16
18
  const rawValue = el.getAttribute(`data-${attr}`);
17
- const fieldList = rawValue?.split(/\s*,\s*/) ?? [];
18
- for (const field of fieldList) {
19
- const { fieldName, propName } = fieldNameParser(field);
20
- if (name !== fieldName) {
21
- continue;
22
- }
23
- if (filter) {
24
- datum = filter(datum);
25
- }
26
- // console.log({ name, field, fieldName, propName, datum, el: el.innerHTML });
27
- if (propName) {
28
- if (/^style\([a-z-]+\)$/i.test(propName)) {
29
- const cssPropertyName = propName.replace(/^style\(([a-z-]+)\)$/i, '$1');
30
- let cssValue;
31
- switch (cssPropertyName) {
32
- case 'background-image': {
33
- //
34
- // NGパターン
35
- // $changeDom.css(cssPropertyName, 'url("' + value + '")');
36
- //
37
- // cssメソッドを経由すると styleAPIを使用するので URLがホストを含めた絶対パスになる
38
- // デモサーバーから本番サーバーへの移行ができなくなってしまうので避ける
39
- // 単純な文字列を流し込む(setAttributeを利用)
40
- // urlはマルチバイト文字や空白記号を含まないはずであるがエスケープする
41
- const url = encodeURI(`${datum}`);
42
- cssValue = `url(${url})`;
43
- break;
44
- }
45
- //
46
- // TODO: 他にもvalueに単位が必要なケースなどに対応したい
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);
19
+ const fields = parseFields(rawValue ?? '');
20
+ const field = fields.find((field) => field.fieldName === name);
21
+ if (!field) {
22
+ return;
23
+ }
24
+ const { propName } = field;
25
+ if (filter) {
26
+ datum = filter(datum);
27
+ }
28
+ // console.log({ name, field, fieldName, propName, datum, el: el.innerHTML });
29
+ if (!propName) {
30
+ setContent(el, datum, undefined, xssSanitize);
31
+ return;
32
+ }
33
+ if (propName === 'style' && el instanceof HTMLElement) {
34
+ el.style.cssText = `${datum}`;
35
+ return;
36
+ }
37
+ if (/^style\([a-z-]+\)$/i.test(propName)) {
38
+ const cssPropertyName = propName.replace(/^style\(([a-z-]+)\)$/i, '$1');
39
+ let cssValue;
40
+ switch (cssPropertyName) {
41
+ case 'background-image': {
42
+ // Using CSS method would create absolute URLs with host
43
+ // This would prevent migration from demo to production server
44
+ // Using simple string insertion (setAttribute) instead
45
+ // URL may not contain multibyte or whitespace characters, but escape anyway
46
+ const url = encodeURI(`${datum}`);
47
+ cssValue = `url(${url})`;
48
+ break;
57
49
  }
58
- else {
59
- // SVGElement or more
60
- el.setAttribute(propName, `${datum}`);
50
+ //
51
+ // TODO: Handle other cases where values need units
52
+ //
53
+ default: {
54
+ cssValue = `${datum}`;
61
55
  }
62
- return;
63
56
  }
64
- setContent(el, datum);
57
+ el.setAttribute('style', `${cssPropertyName}: ${cssValue}`);
58
+ return;
59
+ }
60
+ if (el instanceof HTMLElement) {
61
+ // HTMLElement
62
+ set(el, attr, propName, datum, xssSanitize);
63
+ return;
64
+ }
65
+ // SVGElement or more
66
+ // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
67
+ // Check attribute value if XSS protection is enabled
68
+ if (xssSanitize) {
69
+ const safeValue = sanitizeAttributeValue(propName, `${datum}`);
70
+ if (safeValue !== null) {
71
+ el.setAttribute(propName, safeValue);
72
+ }
73
+ return;
65
74
  }
75
+ el.setAttribute(propName, `${datum}`);
66
76
  }
67
77
  /**
68
78
  *
@@ -70,41 +80,48 @@ export function setValue(el, name, datum, attr = 'field', filter) {
70
80
  * @param prefix
71
81
  * @param name
72
82
  * @param datum
83
+ * @param xssSanitize
73
84
  */
74
- function set(el, prefix, name, datum) {
85
+ function set(el, prefix, name, datum, xssSanitize = true) {
75
86
  if (datum == null) {
76
87
  el.removeAttribute(name);
77
88
  return;
78
89
  }
79
90
  switch (name) {
80
91
  case 'text': {
81
- setContent(el, datum, false);
92
+ setContent(el, datum, false, xssSanitize);
82
93
  return;
83
94
  }
84
95
  case 'html': {
85
- setContent(el, datum, true);
96
+ setContent(el, datum, true, xssSanitize);
86
97
  return;
87
98
  }
88
99
  case 'node': {
89
100
  if (typeof datum !== 'string') {
90
101
  return;
91
102
  }
92
- const nodeName = el.localName ?? el.nodeName.toLowerCase();
93
- if (nodeName === datum.toLowerCase()) {
94
- return;
95
- }
96
- const node = (el.ownerDocument ?? document).createElement(`${datum}`);
97
- for (const child of el.childNodes) {
98
- node.append(child.cloneNode(true));
99
- }
100
- for (const { name, value } of el.attributes) {
101
- node.setAttribute(name, value);
102
- }
103
- el.replaceWith(node);
103
+ replaceNode(el, datum, prefix, xssSanitize);
104
104
  return;
105
105
  }
106
106
  }
107
- if (!name.startsWith('data-') && !(name in el)) {
107
+ // If XSS protection is enabled
108
+ if (xssSanitize) {
109
+ // Don't set event handler attributes (starting with 'on')
110
+ if (name.toLowerCase().startsWith('on')) {
111
+ return;
112
+ }
113
+ const beforeSanitize = datum;
114
+ datum = sanitizeAttributeValue(name, datum);
115
+ // If sanitization results in null value (except when original value was null), don't change the value
116
+ if (datum === null && beforeSanitize !== null) {
117
+ return;
118
+ }
119
+ }
120
+ if (datum == null) {
121
+ el.removeAttribute(name);
122
+ return;
123
+ }
124
+ if (!name.startsWith('data-') && !propInElement(el, name)) {
108
125
  const dataAttr = `data-${prefix}-${kebabCase(name)}`;
109
126
  if (el.hasAttribute(dataAttr)) {
110
127
  el.setAttribute(dataAttr, `${datum}`);
@@ -676,28 +693,54 @@ function set(el, prefix, name, datum) {
676
693
  * @param el
677
694
  * @param datum
678
695
  * @param asHtml
696
+ * @param xssSanitize
679
697
  */
680
- export function setContent(el, datum, asHtml = true) {
698
+ export function setContent(el, datum, asHtml = true, xssSanitize = true) {
681
699
  if (el instanceof HTMLInputElement ||
682
700
  el instanceof HTMLSelectElement ||
683
701
  el instanceof HTMLTextAreaElement ||
684
702
  el instanceof HTMLOutputElement) {
685
703
  if (el instanceof HTMLInputElement &&
686
704
  (el.type === 'radio' || el.type === 'checkbox')) {
705
+ // Boolean values are always set directly regardless of value attribute
687
706
  if (typeof datum === 'boolean') {
688
707
  el.checked = datum;
689
708
  return;
690
709
  }
691
- el.checked = el.value === `${datum}`;
710
+ // Checkbox/radio with value attribute: traditional value comparison
711
+ if (el.hasAttribute('value')) {
712
+ el.checked = el.value === `${datum}`;
713
+ return;
714
+ }
715
+ // Checkbox without value attribute: switch-like behavior (string boolean processing)
716
+ if (datum === 'true') {
717
+ el.checked = true;
718
+ return;
719
+ }
720
+ if (datum === 'false') {
721
+ el.checked = false;
722
+ return;
723
+ }
724
+ // Other values default to false
725
+ el.checked = false;
692
726
  return;
693
727
  }
694
728
  el.value = `${datum}`;
695
729
  return;
696
730
  }
697
731
  if (asHtml) {
698
- el.innerHTML = datum == null ? '' : `${datum}`;
732
+ // Sanitize HTML if XSS protection is enabled
733
+ const htmlStr = datum == null ? '' : `${datum}`;
734
+ if (xssSanitize && typeof htmlStr === 'string') {
735
+ // sanitizeHtml will now replace dangerous elements instead of removing them
736
+ el.innerHTML = sanitizeHtml(htmlStr);
737
+ }
738
+ else {
739
+ el.innerHTML = htmlStr;
740
+ }
699
741
  return;
700
742
  }
743
+ // No need to sanitize textContent (automatically escaped)
701
744
  el.textContent = datum == null ? '' : `${datum}`;
702
745
  }
703
746
  /**
@@ -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;