attrs-in-props 3.8.0 → 3.8.1

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 CHANGED
@@ -74,8 +74,65 @@ const resolved = executeAttr(componentDef, element)
74
74
  // -> { src: '/resolved-path.jpg', alt: 'Photo' }
75
75
  ```
76
76
 
77
+ ## ARIA Attributes
78
+
79
+ All `aria-*` attributes are valid on any element. Three syntax forms are supported:
80
+
81
+ ```javascript
82
+ // 1. Kebab-case (standard HTML)
83
+ { 'aria-label': 'Close', 'aria-expanded': true }
84
+
85
+ // 2. camelCase (JS-friendly) — auto-converted to kebab-case
86
+ { ariaLabel: 'Close', ariaExpanded: true }
87
+
88
+ // 3. Object shorthand
89
+ { aria: { label: 'Close', expanded: true, hidden: false } }
90
+ ```
91
+
92
+ All three produce `aria-label="Close"`, `aria-expanded="true"` in the DOM.
93
+
94
+ ## Data Attributes
95
+
96
+ All `data-*` attributes are valid on any element, with the same three syntax forms:
97
+
98
+ ```javascript
99
+ { 'data-testid': 'btn' } // kebab-case
100
+ { dataTestId: 'btn' } // camelCase → data-test-id
101
+ { data: { testId: 'btn' } } // object shorthand → data-test-id
102
+ ```
103
+
104
+ ## Conditional Attributes
105
+
106
+ Attributes inside `$`, `.`, `!` prefix blocks are conditionally applied — same prefixes as css-in-props:
107
+
108
+ ```javascript
109
+ const Button = {
110
+ // $ prefix: global case from context.cases
111
+ '$isSafari': { disabled: true, 'aria-label': 'Safari' },
112
+
113
+ // . prefix: truthy (props/state first, then context.cases)
114
+ '.isActive': { aria: { expanded: true }, 'data-state': 'open' },
115
+
116
+ // ! prefix: falsy
117
+ '!isActive': { ariaHidden: true }
118
+ }
119
+ ```
120
+
121
+ Conditional attribute values are wrapped in functions and re-evaluated on every `update()` call.
122
+
123
+ ### `extractConditionalAttrs(props, tag, cssProps?)`
124
+
125
+ Extract HTML attributes from conditional blocks in props.
126
+
127
+ ```javascript
128
+ import { extractConditionalAttrs } from 'attrs-in-props'
129
+
130
+ const conditionalAttrs = extractConditionalAttrs(props, 'button', cssPropsRegistry)
131
+ // Returns attr functions that evaluate conditions on each call
132
+ ```
133
+
77
134
  ## Default attributes
78
135
 
79
136
  All HTML elements support these attributes by default: `id`, `title`, `class`, `style`, `dir`, `lang`, `hidden`, `tabindex`, `draggable`, `contenteditable`, `spellcheck`, `translate`, `role`, `slot`, and more.
80
137
 
81
- Element-specific attributes are supported for 50+ HTML tags including `a`, `img`, `input`, `video`, `iframe`, `form`, `select`, `textarea`, `svg`, and all ARIA attributes.
138
+ All `aria-*` and `data-*` attributes are valid on any element. Element-specific attributes are supported for 50+ HTML tags including `a`, `img`, `input`, `video`, `iframe`, `form`, `select`, `textarea`, and `svg`.
package/dist/cjs/index.js CHANGED
@@ -26,7 +26,9 @@ __export(index_exports, {
26
26
  checkAttributeByTagName: () => checkAttributeByTagName,
27
27
  checkEventFunctions: () => checkEventFunctions,
28
28
  executeAttr: () => executeAttr,
29
+ extractConditionalAttrs: () => extractConditionalAttrs,
29
30
  filterAttributesByTagName: () => filterAttributesByTagName,
31
+ resolveFileSource: () => resolveFileSource,
30
32
  resolvePropValue: () => resolvePropValue
31
33
  });
32
34
  module.exports = __toCommonJS(index_exports);
@@ -992,7 +994,18 @@ const DOM_EVENTS = [
992
994
  "onfullscreenchange",
993
995
  "onfullscreenerror"
994
996
  ];
997
+ const camelToAttr = (key) => {
998
+ if (key.startsWith("aria") && key.length > 4 && key.charCodeAt(4) >= 65 && key.charCodeAt(4) <= 90) {
999
+ return "aria-" + key.charAt(4).toLowerCase() + key.slice(5).replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
1000
+ }
1001
+ if (key.startsWith("data") && key.length > 4 && key.charCodeAt(4) >= 65 && key.charCodeAt(4) <= 90) {
1002
+ return "data-" + key.charAt(4).toLowerCase() + key.slice(5).replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
1003
+ }
1004
+ return null;
1005
+ };
995
1006
  const checkAttributeByTagName = (tag, attribute) => {
1007
+ if (attribute.startsWith("aria-") || attribute.startsWith("data-")) return true;
1008
+ if (camelToAttr(attribute)) return true;
996
1009
  if (Object.prototype.hasOwnProperty.call(HTML_ATTRIBUTES, tag)) {
997
1010
  const attributes = HTML_ATTRIBUTES[tag];
998
1011
  return attributes.includes(attribute) || attributes.includes("default");
@@ -1011,10 +1024,28 @@ const filterAttributesByTagName = (tag, props, cssProps) => {
1011
1024
  for (const key in props) {
1012
1025
  if (Object.prototype.hasOwnProperty.call(props, key)) {
1013
1026
  if (cssProps && key in cssProps) continue;
1027
+ if (key === "aria" && props[key] && typeof props[key] === "object") {
1028
+ for (const ariaKey in props[key]) {
1029
+ if ((0, import_utils.isDefined)(props[key][ariaKey])) {
1030
+ filteredObject["aria-" + ariaKey] = props[key][ariaKey];
1031
+ }
1032
+ }
1033
+ continue;
1034
+ }
1035
+ if (key === "data" && props[key] && typeof props[key] === "object") {
1036
+ for (const dataKey in props[key]) {
1037
+ if ((0, import_utils.isDefined)(props[key][dataKey])) {
1038
+ const kebab = dataKey.replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
1039
+ filteredObject["data-" + kebab] = props[key][dataKey];
1040
+ }
1041
+ }
1042
+ continue;
1043
+ }
1014
1044
  const isAttribute = checkAttributeByTagName(tag, key);
1015
1045
  const isEvent = checkEventFunctions(key);
1016
1046
  if ((0, import_utils.isDefined)(props[key]) && (isAttribute || isEvent)) {
1017
- filteredObject[key] = props[key];
1047
+ const attrName = camelToAttr(key) || key;
1048
+ filteredObject[attrName] = props[key];
1018
1049
  }
1019
1050
  }
1020
1051
  }
@@ -1037,11 +1068,26 @@ const resolvePropValue = (el, value) => {
1037
1068
  }
1038
1069
  return resolved;
1039
1070
  };
1071
+ const resolveFileSource = (el, value) => {
1072
+ let src = (el.props.preSrc || "") + (resolvePropValue(el, value) || "");
1073
+ if (!src) return;
1074
+ try {
1075
+ new URL(src);
1076
+ return src;
1077
+ } catch (e) {
1078
+ }
1079
+ const { context } = el;
1080
+ if (!context.files) return src;
1081
+ const fileSrc = src.startsWith("/files/") ? src.slice(7) : src;
1082
+ const file = context.files[src] || context.files[fileSrc];
1083
+ if (file && file.content) return file.content.src;
1084
+ return src;
1085
+ };
1040
1086
  const ATTR_TRANSFORMS = {
1041
- src: (el) => resolvePropValue(el, el.props.src),
1087
+ src: (el) => resolveFileSource(el, el.props.src),
1042
1088
  href: (el) => resolvePropValue(el, el.props.href),
1043
1089
  action: (el) => resolvePropValue(el, el.props.action),
1044
- poster: (el) => resolvePropValue(el, el.props.poster),
1090
+ poster: (el) => resolveFileSource(el, el.props.poster),
1045
1091
  data: (el) => resolvePropValue(el, el.props.data)
1046
1092
  };
1047
1093
  const applyAttrTransforms = (element) => {
@@ -1056,3 +1102,62 @@ const applyAttrTransforms = (element) => {
1056
1102
  }
1057
1103
  return result;
1058
1104
  };
1105
+ const resolveCase = (caseKey, element) => {
1106
+ const caseFn = element.context?.cases?.[caseKey];
1107
+ if (caseFn === void 0) return void 0;
1108
+ if ((0, import_utils.isFunction)(caseFn)) return caseFn.call(element, element);
1109
+ return !!caseFn;
1110
+ };
1111
+ const evaluateCondition = (prefix, caseKey, element) => {
1112
+ if (prefix === "$") {
1113
+ let result = resolveCase(caseKey, element);
1114
+ if (result === void 0) result = !!element.props?.[caseKey];
1115
+ return result;
1116
+ }
1117
+ let isTruthy = element.props[caseKey] === true || element.state[caseKey] || element[caseKey];
1118
+ if (!isTruthy) {
1119
+ const caseResult = resolveCase(caseKey, element);
1120
+ if (caseResult !== void 0) isTruthy = caseResult;
1121
+ }
1122
+ return prefix === "." ? !!isTruthy : !isTruthy;
1123
+ };
1124
+ const CONDITIONAL_PREFIXES = /* @__PURE__ */ new Set(["$", ".", "!"]);
1125
+ const extractConditionalAttrs = (props, tag, cssProps) => {
1126
+ const result = {};
1127
+ const addConditionalAttr = (attrName, attrVal, prefix, caseKey) => {
1128
+ const capturedVal = attrVal;
1129
+ result[attrName] = (el) => {
1130
+ if (!evaluateCondition(prefix, caseKey, el)) return void 0;
1131
+ return (0, import_utils.isFunction)(capturedVal) ? capturedVal(el) : capturedVal;
1132
+ };
1133
+ };
1134
+ for (const key in props) {
1135
+ const prefix = key.charAt(0);
1136
+ if (!CONDITIONAL_PREFIXES.has(prefix)) continue;
1137
+ const block = props[key];
1138
+ if (!block || typeof block !== "object") continue;
1139
+ const caseKey = key.slice(1);
1140
+ for (const attrKey in block) {
1141
+ if (cssProps && attrKey in cssProps) continue;
1142
+ if (attrKey === "aria" && block[attrKey] && typeof block[attrKey] === "object") {
1143
+ for (const ariaKey in block[attrKey]) {
1144
+ addConditionalAttr("aria-" + ariaKey, block[attrKey][ariaKey], prefix, caseKey);
1145
+ }
1146
+ continue;
1147
+ }
1148
+ if (attrKey === "data" && block[attrKey] && typeof block[attrKey] === "object") {
1149
+ for (const dataKey in block[attrKey]) {
1150
+ const kebab = dataKey.replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
1151
+ addConditionalAttr("data-" + kebab, block[attrKey][dataKey], prefix, caseKey);
1152
+ }
1153
+ continue;
1154
+ }
1155
+ const isAttribute = checkAttributeByTagName(tag, attrKey);
1156
+ const isEvent = checkEventFunctions(attrKey);
1157
+ if (!isAttribute && !isEvent) continue;
1158
+ const attrName = camelToAttr(attrKey) || attrKey;
1159
+ addConditionalAttr(attrName, block[attrKey], prefix, caseKey);
1160
+ }
1161
+ }
1162
+ return result;
1163
+ };
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isDefined, isString } from "@domql/utils";
1
+ import { isDefined, isFunction, isString } from "@domql/utils";
2
2
  const ARIA_ROLES = [
3
3
  "alert",
4
4
  "alertdialog",
@@ -960,7 +960,18 @@ const DOM_EVENTS = [
960
960
  "onfullscreenchange",
961
961
  "onfullscreenerror"
962
962
  ];
963
+ const camelToAttr = (key) => {
964
+ if (key.startsWith("aria") && key.length > 4 && key.charCodeAt(4) >= 65 && key.charCodeAt(4) <= 90) {
965
+ return "aria-" + key.charAt(4).toLowerCase() + key.slice(5).replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
966
+ }
967
+ if (key.startsWith("data") && key.length > 4 && key.charCodeAt(4) >= 65 && key.charCodeAt(4) <= 90) {
968
+ return "data-" + key.charAt(4).toLowerCase() + key.slice(5).replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
969
+ }
970
+ return null;
971
+ };
963
972
  const checkAttributeByTagName = (tag, attribute) => {
973
+ if (attribute.startsWith("aria-") || attribute.startsWith("data-")) return true;
974
+ if (camelToAttr(attribute)) return true;
964
975
  if (Object.prototype.hasOwnProperty.call(HTML_ATTRIBUTES, tag)) {
965
976
  const attributes = HTML_ATTRIBUTES[tag];
966
977
  return attributes.includes(attribute) || attributes.includes("default");
@@ -979,10 +990,28 @@ const filterAttributesByTagName = (tag, props, cssProps) => {
979
990
  for (const key in props) {
980
991
  if (Object.prototype.hasOwnProperty.call(props, key)) {
981
992
  if (cssProps && key in cssProps) continue;
993
+ if (key === "aria" && props[key] && typeof props[key] === "object") {
994
+ for (const ariaKey in props[key]) {
995
+ if (isDefined(props[key][ariaKey])) {
996
+ filteredObject["aria-" + ariaKey] = props[key][ariaKey];
997
+ }
998
+ }
999
+ continue;
1000
+ }
1001
+ if (key === "data" && props[key] && typeof props[key] === "object") {
1002
+ for (const dataKey in props[key]) {
1003
+ if (isDefined(props[key][dataKey])) {
1004
+ const kebab = dataKey.replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
1005
+ filteredObject["data-" + kebab] = props[key][dataKey];
1006
+ }
1007
+ }
1008
+ continue;
1009
+ }
982
1010
  const isAttribute = checkAttributeByTagName(tag, key);
983
1011
  const isEvent = checkEventFunctions(key);
984
1012
  if (isDefined(props[key]) && (isAttribute || isEvent)) {
985
- filteredObject[key] = props[key];
1013
+ const attrName = camelToAttr(key) || key;
1014
+ filteredObject[attrName] = props[key];
986
1015
  }
987
1016
  }
988
1017
  }
@@ -1005,11 +1034,26 @@ const resolvePropValue = (el, value) => {
1005
1034
  }
1006
1035
  return resolved;
1007
1036
  };
1037
+ const resolveFileSource = (el, value) => {
1038
+ let src = (el.props.preSrc || "") + (resolvePropValue(el, value) || "");
1039
+ if (!src) return;
1040
+ try {
1041
+ new URL(src);
1042
+ return src;
1043
+ } catch (e) {
1044
+ }
1045
+ const { context } = el;
1046
+ if (!context.files) return src;
1047
+ const fileSrc = src.startsWith("/files/") ? src.slice(7) : src;
1048
+ const file = context.files[src] || context.files[fileSrc];
1049
+ if (file && file.content) return file.content.src;
1050
+ return src;
1051
+ };
1008
1052
  const ATTR_TRANSFORMS = {
1009
- src: (el) => resolvePropValue(el, el.props.src),
1053
+ src: (el) => resolveFileSource(el, el.props.src),
1010
1054
  href: (el) => resolvePropValue(el, el.props.href),
1011
1055
  action: (el) => resolvePropValue(el, el.props.action),
1012
- poster: (el) => resolvePropValue(el, el.props.poster),
1056
+ poster: (el) => resolveFileSource(el, el.props.poster),
1013
1057
  data: (el) => resolvePropValue(el, el.props.data)
1014
1058
  };
1015
1059
  const applyAttrTransforms = (element) => {
@@ -1024,6 +1068,65 @@ const applyAttrTransforms = (element) => {
1024
1068
  }
1025
1069
  return result;
1026
1070
  };
1071
+ const resolveCase = (caseKey, element) => {
1072
+ const caseFn = element.context?.cases?.[caseKey];
1073
+ if (caseFn === void 0) return void 0;
1074
+ if (isFunction(caseFn)) return caseFn.call(element, element);
1075
+ return !!caseFn;
1076
+ };
1077
+ const evaluateCondition = (prefix, caseKey, element) => {
1078
+ if (prefix === "$") {
1079
+ let result = resolveCase(caseKey, element);
1080
+ if (result === void 0) result = !!element.props?.[caseKey];
1081
+ return result;
1082
+ }
1083
+ let isTruthy = element.props[caseKey] === true || element.state[caseKey] || element[caseKey];
1084
+ if (!isTruthy) {
1085
+ const caseResult = resolveCase(caseKey, element);
1086
+ if (caseResult !== void 0) isTruthy = caseResult;
1087
+ }
1088
+ return prefix === "." ? !!isTruthy : !isTruthy;
1089
+ };
1090
+ const CONDITIONAL_PREFIXES = /* @__PURE__ */ new Set(["$", ".", "!"]);
1091
+ const extractConditionalAttrs = (props, tag, cssProps) => {
1092
+ const result = {};
1093
+ const addConditionalAttr = (attrName, attrVal, prefix, caseKey) => {
1094
+ const capturedVal = attrVal;
1095
+ result[attrName] = (el) => {
1096
+ if (!evaluateCondition(prefix, caseKey, el)) return void 0;
1097
+ return isFunction(capturedVal) ? capturedVal(el) : capturedVal;
1098
+ };
1099
+ };
1100
+ for (const key in props) {
1101
+ const prefix = key.charAt(0);
1102
+ if (!CONDITIONAL_PREFIXES.has(prefix)) continue;
1103
+ const block = props[key];
1104
+ if (!block || typeof block !== "object") continue;
1105
+ const caseKey = key.slice(1);
1106
+ for (const attrKey in block) {
1107
+ if (cssProps && attrKey in cssProps) continue;
1108
+ if (attrKey === "aria" && block[attrKey] && typeof block[attrKey] === "object") {
1109
+ for (const ariaKey in block[attrKey]) {
1110
+ addConditionalAttr("aria-" + ariaKey, block[attrKey][ariaKey], prefix, caseKey);
1111
+ }
1112
+ continue;
1113
+ }
1114
+ if (attrKey === "data" && block[attrKey] && typeof block[attrKey] === "object") {
1115
+ for (const dataKey in block[attrKey]) {
1116
+ const kebab = dataKey.replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
1117
+ addConditionalAttr("data-" + kebab, block[attrKey][dataKey], prefix, caseKey);
1118
+ }
1119
+ continue;
1120
+ }
1121
+ const isAttribute = checkAttributeByTagName(tag, attrKey);
1122
+ const isEvent = checkEventFunctions(attrKey);
1123
+ if (!isAttribute && !isEvent) continue;
1124
+ const attrName = camelToAttr(attrKey) || attrKey;
1125
+ addConditionalAttr(attrName, block[attrKey], prefix, caseKey);
1126
+ }
1127
+ }
1128
+ return result;
1129
+ };
1027
1130
  export {
1028
1131
  ARIA_ROLES,
1029
1132
  ATTR_TRANSFORMS,
@@ -1033,6 +1136,8 @@ export {
1033
1136
  checkAttributeByTagName,
1034
1137
  checkEventFunctions,
1035
1138
  executeAttr,
1139
+ extractConditionalAttrs,
1036
1140
  filterAttributesByTagName,
1141
+ resolveFileSource,
1037
1142
  resolvePropValue
1038
1143
  };
@@ -37,10 +37,11 @@ var AttrsInProps = (() => {
37
37
  });
38
38
 
39
39
  // ../utils/dist/esm/types.js
40
- var isString, isDefined;
40
+ var isString, isFunction, isDefined;
41
41
  var init_types = __esm({
42
42
  "../utils/dist/esm/types.js"() {
43
43
  isString = (arg) => typeof arg === "string";
44
+ isFunction = (arg) => typeof arg === "function";
44
45
  isDefined = (arg) => arg !== void 0;
45
46
  }
46
47
  });
@@ -296,7 +297,9 @@ var AttrsInProps = (() => {
296
297
  checkAttributeByTagName: () => checkAttributeByTagName,
297
298
  checkEventFunctions: () => checkEventFunctions,
298
299
  executeAttr: () => executeAttr,
300
+ extractConditionalAttrs: () => extractConditionalAttrs,
299
301
  filterAttributesByTagName: () => filterAttributesByTagName,
302
+ resolveFileSource: () => resolveFileSource,
300
303
  resolvePropValue: () => resolvePropValue
301
304
  });
302
305
  init_esm();
@@ -1261,7 +1264,18 @@ var AttrsInProps = (() => {
1261
1264
  "onfullscreenchange",
1262
1265
  "onfullscreenerror"
1263
1266
  ];
1267
+ var camelToAttr = (key) => {
1268
+ if (key.startsWith("aria") && key.length > 4 && key.charCodeAt(4) >= 65 && key.charCodeAt(4) <= 90) {
1269
+ return "aria-" + key.charAt(4).toLowerCase() + key.slice(5).replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
1270
+ }
1271
+ if (key.startsWith("data") && key.length > 4 && key.charCodeAt(4) >= 65 && key.charCodeAt(4) <= 90) {
1272
+ return "data-" + key.charAt(4).toLowerCase() + key.slice(5).replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
1273
+ }
1274
+ return null;
1275
+ };
1264
1276
  var checkAttributeByTagName = (tag, attribute) => {
1277
+ if (attribute.startsWith("aria-") || attribute.startsWith("data-")) return true;
1278
+ if (camelToAttr(attribute)) return true;
1265
1279
  if (Object.prototype.hasOwnProperty.call(HTML_ATTRIBUTES, tag)) {
1266
1280
  const attributes = HTML_ATTRIBUTES[tag];
1267
1281
  return attributes.includes(attribute) || attributes.includes("default");
@@ -1280,10 +1294,28 @@ var AttrsInProps = (() => {
1280
1294
  for (const key in props) {
1281
1295
  if (Object.prototype.hasOwnProperty.call(props, key)) {
1282
1296
  if (cssProps && key in cssProps) continue;
1297
+ if (key === "aria" && props[key] && typeof props[key] === "object") {
1298
+ for (const ariaKey in props[key]) {
1299
+ if (isDefined(props[key][ariaKey])) {
1300
+ filteredObject["aria-" + ariaKey] = props[key][ariaKey];
1301
+ }
1302
+ }
1303
+ continue;
1304
+ }
1305
+ if (key === "data" && props[key] && typeof props[key] === "object") {
1306
+ for (const dataKey in props[key]) {
1307
+ if (isDefined(props[key][dataKey])) {
1308
+ const kebab = dataKey.replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
1309
+ filteredObject["data-" + kebab] = props[key][dataKey];
1310
+ }
1311
+ }
1312
+ continue;
1313
+ }
1283
1314
  const isAttribute = checkAttributeByTagName(tag, key);
1284
1315
  const isEvent = checkEventFunctions(key);
1285
1316
  if (isDefined(props[key]) && (isAttribute || isEvent)) {
1286
- filteredObject[key] = props[key];
1317
+ const attrName = camelToAttr(key) || key;
1318
+ filteredObject[attrName] = props[key];
1287
1319
  }
1288
1320
  }
1289
1321
  }
@@ -1306,11 +1338,26 @@ var AttrsInProps = (() => {
1306
1338
  }
1307
1339
  return resolved;
1308
1340
  };
1341
+ var resolveFileSource = (el, value) => {
1342
+ let src = (el.props.preSrc || "") + (resolvePropValue(el, value) || "");
1343
+ if (!src) return;
1344
+ try {
1345
+ new URL(src);
1346
+ return src;
1347
+ } catch (e) {
1348
+ }
1349
+ const { context } = el;
1350
+ if (!context.files) return src;
1351
+ const fileSrc = src.startsWith("/files/") ? src.slice(7) : src;
1352
+ const file = context.files[src] || context.files[fileSrc];
1353
+ if (file && file.content) return file.content.src;
1354
+ return src;
1355
+ };
1309
1356
  var ATTR_TRANSFORMS = {
1310
- src: (el) => resolvePropValue(el, el.props.src),
1357
+ src: (el) => resolveFileSource(el, el.props.src),
1311
1358
  href: (el) => resolvePropValue(el, el.props.href),
1312
1359
  action: (el) => resolvePropValue(el, el.props.action),
1313
- poster: (el) => resolvePropValue(el, el.props.poster),
1360
+ poster: (el) => resolveFileSource(el, el.props.poster),
1314
1361
  data: (el) => resolvePropValue(el, el.props.data)
1315
1362
  };
1316
1363
  var applyAttrTransforms = (element) => {
@@ -1325,6 +1372,65 @@ var AttrsInProps = (() => {
1325
1372
  }
1326
1373
  return result;
1327
1374
  };
1375
+ var resolveCase = (caseKey, element) => {
1376
+ const caseFn = element.context?.cases?.[caseKey];
1377
+ if (caseFn === void 0) return void 0;
1378
+ if (isFunction(caseFn)) return caseFn.call(element, element);
1379
+ return !!caseFn;
1380
+ };
1381
+ var evaluateCondition = (prefix, caseKey, element) => {
1382
+ if (prefix === "$") {
1383
+ let result = resolveCase(caseKey, element);
1384
+ if (result === void 0) result = !!element.props?.[caseKey];
1385
+ return result;
1386
+ }
1387
+ let isTruthy = element.props[caseKey] === true || element.state[caseKey] || element[caseKey];
1388
+ if (!isTruthy) {
1389
+ const caseResult = resolveCase(caseKey, element);
1390
+ if (caseResult !== void 0) isTruthy = caseResult;
1391
+ }
1392
+ return prefix === "." ? !!isTruthy : !isTruthy;
1393
+ };
1394
+ var CONDITIONAL_PREFIXES = /* @__PURE__ */ new Set(["$", ".", "!"]);
1395
+ var extractConditionalAttrs = (props, tag, cssProps) => {
1396
+ const result = {};
1397
+ const addConditionalAttr = (attrName, attrVal, prefix, caseKey) => {
1398
+ const capturedVal = attrVal;
1399
+ result[attrName] = (el) => {
1400
+ if (!evaluateCondition(prefix, caseKey, el)) return void 0;
1401
+ return isFunction(capturedVal) ? capturedVal(el) : capturedVal;
1402
+ };
1403
+ };
1404
+ for (const key in props) {
1405
+ const prefix = key.charAt(0);
1406
+ if (!CONDITIONAL_PREFIXES.has(prefix)) continue;
1407
+ const block = props[key];
1408
+ if (!block || typeof block !== "object") continue;
1409
+ const caseKey = key.slice(1);
1410
+ for (const attrKey in block) {
1411
+ if (cssProps && attrKey in cssProps) continue;
1412
+ if (attrKey === "aria" && block[attrKey] && typeof block[attrKey] === "object") {
1413
+ for (const ariaKey in block[attrKey]) {
1414
+ addConditionalAttr("aria-" + ariaKey, block[attrKey][ariaKey], prefix, caseKey);
1415
+ }
1416
+ continue;
1417
+ }
1418
+ if (attrKey === "data" && block[attrKey] && typeof block[attrKey] === "object") {
1419
+ for (const dataKey in block[attrKey]) {
1420
+ const kebab = dataKey.replace(/([A-Z])/g, (m) => "-" + m.toLowerCase());
1421
+ addConditionalAttr("data-" + kebab, block[attrKey][dataKey], prefix, caseKey);
1422
+ }
1423
+ continue;
1424
+ }
1425
+ const isAttribute = checkAttributeByTagName(tag, attrKey);
1426
+ const isEvent = checkEventFunctions(attrKey);
1427
+ if (!isAttribute && !isEvent) continue;
1428
+ const attrName = camelToAttr(attrKey) || attrKey;
1429
+ addConditionalAttr(attrName, block[attrKey], prefix, caseKey);
1430
+ }
1431
+ }
1432
+ return result;
1433
+ };
1328
1434
  return __toCommonJS(index_exports);
1329
1435
  })();
1330
1436
  // @preserve-env
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- import { isDefined, isString } from '@domql/utils'
3
+ import { isDefined, isFunction, isString } from '@domql/utils'
4
4
 
5
5
  export const ARIA_ROLES = [
6
6
  'alert',
@@ -1040,7 +1040,23 @@ export const DOM_EVENTS = [
1040
1040
  'onfullscreenerror'
1041
1041
  ]
1042
1042
 
1043
+ // Convert camelCase aria/data attrs to kebab-case: ariaLabel → aria-label, dataTestId → data-test-id
1044
+ const camelToAttr = (key) => {
1045
+ if (key.startsWith('aria') && key.length > 4 && key.charCodeAt(4) >= 65 && key.charCodeAt(4) <= 90) {
1046
+ return 'aria-' + key.charAt(4).toLowerCase() + key.slice(5).replace(/([A-Z])/g, (m) => '-' + m.toLowerCase())
1047
+ }
1048
+ if (key.startsWith('data') && key.length > 4 && key.charCodeAt(4) >= 65 && key.charCodeAt(4) <= 90) {
1049
+ return 'data-' + key.charAt(4).toLowerCase() + key.slice(5).replace(/([A-Z])/g, (m) => '-' + m.toLowerCase())
1050
+ }
1051
+ return null
1052
+ }
1053
+
1043
1054
  export const checkAttributeByTagName = (tag, attribute) => {
1055
+ // aria-* and data-* are valid on all elements
1056
+ if (attribute.startsWith('aria-') || attribute.startsWith('data-')) return true
1057
+ // camelCase aria/data attrs
1058
+ if (camelToAttr(attribute)) return true
1059
+
1044
1060
  if (Object.prototype.hasOwnProperty.call(HTML_ATTRIBUTES, tag)) {
1045
1061
  const attributes = HTML_ATTRIBUTES[tag]
1046
1062
  return attributes.includes(attribute) || attributes.includes('default')
@@ -1062,10 +1078,33 @@ export const filterAttributesByTagName = (tag, props, cssProps) => {
1062
1078
  for (const key in props) {
1063
1079
  if (Object.prototype.hasOwnProperty.call(props, key)) {
1064
1080
  if (cssProps && key in cssProps) continue
1081
+
1082
+ // aria: { label: 'foo', expanded: true } → aria-label, aria-expanded
1083
+ if (key === 'aria' && props[key] && typeof props[key] === 'object') {
1084
+ for (const ariaKey in props[key]) {
1085
+ if (isDefined(props[key][ariaKey])) {
1086
+ filteredObject['aria-' + ariaKey] = props[key][ariaKey]
1087
+ }
1088
+ }
1089
+ continue
1090
+ }
1091
+
1092
+ // data: { testId: 'foo' } → data-test-id
1093
+ if (key === 'data' && props[key] && typeof props[key] === 'object') {
1094
+ for (const dataKey in props[key]) {
1095
+ if (isDefined(props[key][dataKey])) {
1096
+ const kebab = dataKey.replace(/([A-Z])/g, (m) => '-' + m.toLowerCase())
1097
+ filteredObject['data-' + kebab] = props[key][dataKey]
1098
+ }
1099
+ }
1100
+ continue
1101
+ }
1102
+
1065
1103
  const isAttribute = checkAttributeByTagName(tag, key)
1066
1104
  const isEvent = checkEventFunctions(key)
1067
1105
  if (isDefined(props[key]) && (isAttribute || isEvent)) {
1068
- filteredObject[key] = props[key]
1106
+ const attrName = camelToAttr(key) || key
1107
+ filteredObject[attrName] = props[key]
1069
1108
  }
1070
1109
  }
1071
1110
  }
@@ -1096,16 +1135,38 @@ export const resolvePropValue = (el, value) => {
1096
1135
  return resolved
1097
1136
  }
1098
1137
 
1138
+ /**
1139
+ * Resolve a file URL from context.files.
1140
+ * Handles absolute URLs (passthrough), /files/ prefix stripping,
1141
+ * and file lookup for bundler-resolved paths.
1142
+ */
1143
+ export const resolveFileSource = (el, value) => {
1144
+ let src = (el.props.preSrc || '') + (resolvePropValue(el, value) || '')
1145
+ if (!src) return
1146
+
1147
+ try { new URL(src); return src } catch (e) { } // absolute URL — passthrough
1148
+
1149
+ const { context } = el
1150
+ if (!context.files) return src
1151
+
1152
+ const fileSrc = src.startsWith('/files/') ? src.slice(7) : src
1153
+ const file = context.files[src] || context.files[fileSrc]
1154
+ if (file && file.content) return file.content.src
1155
+
1156
+ return src
1157
+ }
1158
+
1099
1159
  /**
1100
1160
  * Auto-resolve attribute transformers.
1101
1161
  * Attributes listed here are automatically resolved from props
1102
1162
  * via resolvePropValue (exec + template literal replacement).
1163
+ * src and poster also resolve from context.files for media elements.
1103
1164
  */
1104
1165
  export const ATTR_TRANSFORMS = {
1105
- src: (el) => resolvePropValue(el, el.props.src),
1166
+ src: (el) => resolveFileSource(el, el.props.src),
1106
1167
  href: (el) => resolvePropValue(el, el.props.href),
1107
1168
  action: (el) => resolvePropValue(el, el.props.action),
1108
- poster: (el) => resolvePropValue(el, el.props.poster),
1169
+ poster: (el) => resolveFileSource(el, el.props.poster),
1109
1170
  data: (el) => resolvePropValue(el, el.props.data)
1110
1171
  }
1111
1172
 
@@ -1125,3 +1186,93 @@ export const applyAttrTransforms = (element) => {
1125
1186
  }
1126
1187
  return result
1127
1188
  }
1189
+
1190
+ /**
1191
+ * Resolve a case value from context.cases.
1192
+ * Returns true/false, or undefined if case is not defined.
1193
+ */
1194
+ const resolveCase = (caseKey, element) => {
1195
+ const caseFn = element.context?.cases?.[caseKey]
1196
+ if (caseFn === undefined) return undefined
1197
+ if (isFunction(caseFn)) return caseFn.call(element, element)
1198
+ return !!caseFn
1199
+ }
1200
+
1201
+ /**
1202
+ * Evaluate whether a conditional prefix key is active.
1203
+ * Supports $ (global cases), . (truthy), ! (falsy) prefixes.
1204
+ */
1205
+ const evaluateCondition = (prefix, caseKey, element) => {
1206
+ if (prefix === '$') {
1207
+ let result = resolveCase(caseKey, element)
1208
+ if (result === undefined) result = !!element.props?.[caseKey]
1209
+ return result
1210
+ }
1211
+
1212
+ // . and ! prefixes: check props/state/element first, then context.cases
1213
+ let isTruthy = element.props[caseKey] === true || element.state[caseKey] || element[caseKey]
1214
+ if (!isTruthy) {
1215
+ const caseResult = resolveCase(caseKey, element)
1216
+ if (caseResult !== undefined) isTruthy = caseResult
1217
+ }
1218
+
1219
+ return prefix === '.' ? !!isTruthy : !isTruthy
1220
+ }
1221
+
1222
+ const CONDITIONAL_PREFIXES = new Set(['$', '.', '!'])
1223
+
1224
+ /**
1225
+ * Extract HTML attributes from conditional blocks ($, ., !) in props.
1226
+ * Returns an attr object with functions that re-evaluate on each update.
1227
+ */
1228
+ export const extractConditionalAttrs = (props, tag, cssProps) => {
1229
+ const result = {}
1230
+
1231
+ const addConditionalAttr = (attrName, attrVal, prefix, caseKey) => {
1232
+ const capturedVal = attrVal
1233
+ result[attrName] = (el) => {
1234
+ if (!evaluateCondition(prefix, caseKey, el)) return undefined
1235
+ return isFunction(capturedVal) ? capturedVal(el) : capturedVal
1236
+ }
1237
+ }
1238
+
1239
+ for (const key in props) {
1240
+ const prefix = key.charAt(0)
1241
+ if (!CONDITIONAL_PREFIXES.has(prefix)) continue
1242
+
1243
+ const block = props[key]
1244
+ if (!block || typeof block !== 'object') continue
1245
+
1246
+ const caseKey = key.slice(1)
1247
+
1248
+ for (const attrKey in block) {
1249
+ if (cssProps && attrKey in cssProps) continue
1250
+
1251
+ // aria: { label: 'foo' } inside conditional block
1252
+ if (attrKey === 'aria' && block[attrKey] && typeof block[attrKey] === 'object') {
1253
+ for (const ariaKey in block[attrKey]) {
1254
+ addConditionalAttr('aria-' + ariaKey, block[attrKey][ariaKey], prefix, caseKey)
1255
+ }
1256
+ continue
1257
+ }
1258
+
1259
+ // data: { testId: 'foo' } inside conditional block
1260
+ if (attrKey === 'data' && block[attrKey] && typeof block[attrKey] === 'object') {
1261
+ for (const dataKey in block[attrKey]) {
1262
+ const kebab = dataKey.replace(/([A-Z])/g, (m) => '-' + m.toLowerCase())
1263
+ addConditionalAttr('data-' + kebab, block[attrKey][dataKey], prefix, caseKey)
1264
+ }
1265
+ continue
1266
+ }
1267
+
1268
+ const isAttribute = checkAttributeByTagName(tag, attrKey)
1269
+ const isEvent = checkEventFunctions(attrKey)
1270
+ if (!isAttribute && !isEvent) continue
1271
+
1272
+ const attrName = camelToAttr(attrKey) || attrKey
1273
+ addConditionalAttr(attrName, block[attrKey], prefix, caseKey)
1274
+ }
1275
+ }
1276
+
1277
+ return result
1278
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "attrs-in-props",
3
3
  "description": "Utilize props as attributes",
4
4
  "author": "symbo.ls",
5
- "version": "3.8.0",
5
+ "version": "3.8.1",
6
6
  "repository": "https://github.com/symbo-ls/smbls",
7
7
  "type": "module",
8
8
  "module": "./dist/esm/index.js",
@@ -11,7 +11,7 @@
11
11
  "main": "./dist/cjs/index.js",
12
12
  "gitHead": "9fc1b79b41cdc725ca6b24aec64920a599634681",
13
13
  "dependencies": {
14
- "@domql/utils": "^3.8.0"
14
+ "@domql/utils": "^3.8.1"
15
15
  },
16
16
  "source": "index.js",
17
17
  "browser": "./dist/esm/index.js",