@symbo.ls/shorthand 2.34.32

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/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@symbo.ls/shorthand",
3
+ "description": "Shorthand syntax transpiler for Symbols properties",
4
+ "author": "symbo.ls",
5
+ "version": "2.34.32",
6
+ "repository": "https://github.com/symbo-ls/smbls",
7
+ "type": "module",
8
+ "module": "src/index.js",
9
+ "main": "src/index.js",
10
+ "exports": "./src/index.js",
11
+ "source": "src/index.js",
12
+ "publishConfig": {},
13
+ "scripts": {
14
+ "test": "node --experimental-vm-modules ../../node_modules/.bin/jest",
15
+ "copy:package:cjs": "cp ../../build/package-cjs.json dist/cjs/package.json",
16
+ "build:cjs": "npx cross-env NODE_ENV=$NODE_ENV npx esbuild ./src/*.js --target=node16 --format=cjs --outdir=dist/cjs",
17
+ "build": "npm run build:cjs",
18
+ "prepublish": "rimraf -I dist && npm run build && npm run copy:package:cjs"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "dist",
23
+ "docs"
24
+ ],
25
+ "license": "ISC",
26
+ "gitHead": "99bd991fb87c092bcfd045ad358c15064a8b8c30"
27
+ }
package/src/decode.js ADDED
@@ -0,0 +1,275 @@
1
+ 'use strict'
2
+
3
+ import {
4
+ abbrToProp,
5
+ PRESERVE_VALUE_KEYS,
6
+ isComponentKey,
7
+ isSelectorKey
8
+ } from './registry.js'
9
+
10
+ /**
11
+ * Decode a shorthand string back into a Symbols component object.
12
+ *
13
+ * Syntax:
14
+ * abbr:value — key-value pair
15
+ * _ — decoded to spaces in values
16
+ * , — array values (ext:Flex,Box → extends: ['Flex', 'Box'])
17
+ * bare abbr — boolean true
18
+ * !abbr — boolean false
19
+ *
20
+ * @param {string} str — shorthand string
21
+ * @returns {Object} — Symbols component object
22
+ */
23
+ export function decode(str) {
24
+ if (!str || typeof str !== 'string') return {}
25
+
26
+ const obj = {}
27
+ const tokens = tokenize(str)
28
+
29
+ for (const token of tokens) {
30
+ // Boolean false: !abbr
31
+ if (token.startsWith('!')) {
32
+ const abbr = token.slice(1)
33
+ const prop = abbrToProp[abbr] || abbr
34
+ obj[prop] = false
35
+ continue
36
+ }
37
+
38
+ const colonIdx = token.indexOf(':')
39
+
40
+ // Boolean true: bare abbreviation
41
+ if (colonIdx === -1) {
42
+ const prop = abbrToProp[token] || token
43
+ obj[prop] = true
44
+ continue
45
+ }
46
+
47
+ const abbr = token.slice(0, colonIdx)
48
+ const rawVal = token.slice(colonIdx + 1)
49
+ const prop = abbrToProp[abbr] || abbr
50
+
51
+ // Array value: comma-separated
52
+ if (rawVal.includes(',')) {
53
+ obj[prop] = rawVal.split(',').map(decodeValue)
54
+ continue
55
+ }
56
+
57
+ obj[prop] = decodeValue(rawVal)
58
+ }
59
+
60
+ return obj
61
+ }
62
+
63
+ /**
64
+ * Tokenize a shorthand string by splitting on spaces,
65
+ * respecting that underscores represent spaces within values.
66
+ */
67
+ function tokenize(str) {
68
+ return str.trim().split(/\s+/).filter(Boolean)
69
+ }
70
+
71
+ /**
72
+ * Decode a single value: underscores → spaces, parse numbers and booleans.
73
+ */
74
+ function decodeValue(val) {
75
+ const str = val.replace(/_/g, ' ')
76
+
77
+ // Try parsing as number
78
+ if (/^-?\d+(\.\d+)?$/.test(str)) {
79
+ return Number(str)
80
+ }
81
+
82
+ return str
83
+ }
84
+
85
+ /**
86
+ * Decode inline `in` string values preserving string types.
87
+ * Unlike decodeValue, this does NOT convert numeric strings to numbers,
88
+ * since CSS values like '0', '1', '.5' must stay as strings.
89
+ */
90
+ function decodeInlineValue(val) {
91
+ return val.replace(/_/g, ' ')
92
+ }
93
+
94
+ /**
95
+ * Decode an `in` string (from stringify) back to key-value pairs.
96
+ * Uses string-preserving value decode to avoid lossy type coercion.
97
+ */
98
+ function decodeInline(str) {
99
+ if (!str || typeof str !== 'string') return {}
100
+
101
+ const obj = {}
102
+ const tokens = tokenize(str)
103
+
104
+ for (const token of tokens) {
105
+ if (token.startsWith('!')) {
106
+ const abbr = token.slice(1)
107
+ const prop = abbrToProp[abbr] || abbr
108
+ obj[prop] = false
109
+ continue
110
+ }
111
+
112
+ const colonIdx = token.indexOf(':')
113
+
114
+ if (colonIdx === -1) {
115
+ const prop = abbrToProp[token] || token
116
+ obj[prop] = true
117
+ continue
118
+ }
119
+
120
+ const abbr = token.slice(0, colonIdx)
121
+ const rawVal = token.slice(colonIdx + 1)
122
+ const prop = abbrToProp[abbr] || abbr
123
+
124
+ // Array value: comma-separated
125
+ if (rawVal.includes(',')) {
126
+ obj[prop] = rawVal.split(',').map(decodeInlineValue)
127
+ continue
128
+ }
129
+
130
+ obj[prop] = decodeInlineValue(rawVal)
131
+ }
132
+
133
+ return obj
134
+ }
135
+
136
+ /**
137
+ * Recursively expand abbreviated property names back to full names.
138
+ *
139
+ * Inverse of shorten(). Handles:
140
+ * - PascalCase keys: kept, values recursed
141
+ * - CSS selectors / media / cases: kept, values recursed
142
+ * - Event abbreviations (@ck → onClick): expanded via registry
143
+ * - state, scope, attr, style, data, context, query, class values: preserved as-is
144
+ * - Everything else: key expanded, value recursed if object/array
145
+ *
146
+ * @param {Object} obj — shortened component object
147
+ * @returns {Object} — expanded component object with full property names
148
+ */
149
+ export function expand(obj) {
150
+ if (!obj || typeof obj !== 'object') return obj
151
+ if (Array.isArray(obj)) {
152
+ return obj.map(function (item) {
153
+ if (item !== null && typeof item === 'object') return expand(item)
154
+ return item
155
+ })
156
+ }
157
+
158
+ const result = {}
159
+
160
+ for (const key in obj) {
161
+ const val = obj[key]
162
+
163
+ // PascalCase = child component → keep key, recurse value
164
+ if (isComponentKey(key)) {
165
+ result[key] = expandVal(val)
166
+ continue
167
+ }
168
+
169
+ // Try to resolve abbreviation first
170
+ const fullKey = abbrToProp[key] || key
171
+
172
+ // Selector/media/case key that is NOT an event abbreviation → keep key, recurse
173
+ if (isSelectorKey(key) && fullKey === key) {
174
+ result[key] = expandVal(val)
175
+ continue
176
+ }
177
+
178
+ // Preserved keys: value NOT recursed
179
+ if (PRESERVE_VALUE_KEYS.has(fullKey) || PRESERVE_VALUE_KEYS.has(key)) {
180
+ result[fullKey] = val
181
+ continue
182
+ }
183
+
184
+ result[fullKey] = expandVal(val)
185
+ }
186
+
187
+ return result
188
+ }
189
+
190
+ function expandVal(val) {
191
+ if (val === null || val === undefined) return val
192
+ if (typeof val === 'function') return val
193
+ if (Array.isArray(val)) {
194
+ return val.map(function (item) {
195
+ if (item !== null && typeof item === 'object') return expand(item)
196
+ return item
197
+ })
198
+ }
199
+ if (typeof val === 'object') return expand(val)
200
+ return val
201
+ }
202
+
203
+ /**
204
+ * Recursively convert a stringified object (with `in` properties) back
205
+ * to a full component object with expanded property names.
206
+ *
207
+ * Inverse of stringify(). Decodes `in` strings into flat key-value props,
208
+ * expands all abbreviated keys, and recurses into children/selectors.
209
+ *
210
+ * @param {Object} obj — stringified object with `in` properties
211
+ * @returns {Object} — full Symbols component object
212
+ */
213
+ export function parse(obj) {
214
+ if (!obj || typeof obj !== 'object') return obj
215
+ if (Array.isArray(obj)) {
216
+ return obj.map(function (item) {
217
+ if (item !== null && typeof item === 'object') return parse(item)
218
+ return item
219
+ })
220
+ }
221
+
222
+ const result = {}
223
+
224
+ // Decode the `in` string first so its props come first in key order
225
+ if (typeof obj.in === 'string') {
226
+ const decoded = decodeInline(obj.in)
227
+ for (const prop in decoded) {
228
+ result[prop] = decoded[prop]
229
+ }
230
+ }
231
+
232
+ for (const key in obj) {
233
+ if (key === 'in') continue
234
+
235
+ const val = obj[key]
236
+
237
+ // PascalCase = child component → keep key, recurse value
238
+ if (isComponentKey(key)) {
239
+ result[key] = parseVal(val)
240
+ continue
241
+ }
242
+
243
+ // Try to resolve abbreviation
244
+ const fullKey = abbrToProp[key] || key
245
+
246
+ // Selector/media/case key that is NOT an event abbreviation → keep key, recurse
247
+ if (isSelectorKey(key) && fullKey === key) {
248
+ result[key] = parseVal(val)
249
+ continue
250
+ }
251
+
252
+ // Preserved keys: value NOT recursed
253
+ if (PRESERVE_VALUE_KEYS.has(fullKey) || PRESERVE_VALUE_KEYS.has(key)) {
254
+ result[fullKey] = val
255
+ continue
256
+ }
257
+
258
+ result[fullKey] = parseVal(val)
259
+ }
260
+
261
+ return result
262
+ }
263
+
264
+ function parseVal(val) {
265
+ if (val === null || val === undefined) return val
266
+ if (typeof val === 'function') return val
267
+ if (Array.isArray(val)) {
268
+ return val.map(function (item) {
269
+ if (item !== null && typeof item === 'object') return parse(item)
270
+ return item
271
+ })
272
+ }
273
+ if (typeof val === 'object') return parse(val)
274
+ return val
275
+ }
package/src/encode.js ADDED
@@ -0,0 +1,260 @@
1
+ 'use strict'
2
+
3
+ import {
4
+ propToAbbr,
5
+ PRESERVE_VALUE_KEYS,
6
+ SKIP_INLINE_KEYS,
7
+ isComponentKey,
8
+ isSelectorKey
9
+ } from './registry.js'
10
+
11
+ /**
12
+ * Encode a Symbols component object into a shorthand string.
13
+ *
14
+ * Syntax:
15
+ * abbr:value — key-value pair
16
+ * _ — replaces spaces inside values
17
+ * , — array separator (extends: ['Flex', 'Box'] → ext:Flex,Box)
18
+ * space — token separator
19
+ * bare abbr — boolean true (e.g. `hid` → hidden: true)
20
+ * !abbr — boolean false (e.g. `!hid` → hidden: false)
21
+ *
22
+ * Functions, objects, and other non-serializable values are skipped.
23
+ *
24
+ * @param {Object} obj — Symbols component object
25
+ * @returns {string} — shorthand string
26
+ */
27
+ export function encode(obj) {
28
+ if (!obj || typeof obj !== 'object') return ''
29
+
30
+ const tokens = []
31
+
32
+ for (const prop in obj) {
33
+ const val = obj[prop]
34
+
35
+ // Skip functions and nested objects (events, childProps, scope, etc.)
36
+ if (typeof val === 'function') continue
37
+ if (val !== null && typeof val === 'object' && !Array.isArray(val)) continue
38
+ if (val === undefined) continue
39
+
40
+ const abbr = propToAbbr[prop] || prop
41
+
42
+ if (val === true) {
43
+ tokens.push(abbr)
44
+ } else if (val === false) {
45
+ tokens.push('!' + abbr)
46
+ } else if (Array.isArray(val)) {
47
+ const items = val.map((item) => encodeValue(item))
48
+ tokens.push(abbr + ':' + items.join(','))
49
+ } else {
50
+ tokens.push(abbr + ':' + encodeValue(val))
51
+ }
52
+ }
53
+
54
+ return tokens.join(' ')
55
+ }
56
+
57
+ /**
58
+ * Encode a single value: replace spaces with underscores.
59
+ */
60
+ function encodeValue(val) {
61
+ const str = String(val)
62
+ return str.replace(/ /g, '_')
63
+ }
64
+
65
+ /**
66
+ * Recursively shorten property names in a Symbols component object.
67
+ *
68
+ * - PascalCase keys (child components) are kept as-is, values recursed
69
+ * - CSS selectors / media queries / cases are kept as-is, values recursed
70
+ * - Functions are preserved as-is (key is shortened)
71
+ * - state, scope, attr, style, data, context, query, class values are preserved as-is
72
+ * - Everything else: key shortened, value recursed if object/array
73
+ *
74
+ * @param {Object} obj — Symbols component object
75
+ * @returns {Object} — shortened component object
76
+ */
77
+ export function shorten(obj) {
78
+ if (!obj || typeof obj !== 'object') return obj
79
+ if (Array.isArray(obj)) {
80
+ return obj.map(function (item) {
81
+ if (item !== null && typeof item === 'object') return shorten(item)
82
+ return item
83
+ })
84
+ }
85
+
86
+ const result = {}
87
+
88
+ for (const key in obj) {
89
+ const val = obj[key]
90
+
91
+ // PascalCase = child component → keep key, recurse value
92
+ if (isComponentKey(key)) {
93
+ result[key] = shortenVal(val)
94
+ continue
95
+ }
96
+
97
+ // CSS selectors, media queries, cases → keep key, recurse value
98
+ if (isSelectorKey(key)) {
99
+ result[key] = shortenVal(val)
100
+ continue
101
+ }
102
+
103
+ // Shorten the key
104
+ const shortKey = propToAbbr[key] || key
105
+
106
+ // Preserved keys: value NOT recursed (state data, scope fns, etc.)
107
+ if (PRESERVE_VALUE_KEYS.has(key) || PRESERVE_VALUE_KEYS.has(shortKey)) {
108
+ result[shortKey] = val
109
+ continue
110
+ }
111
+
112
+ result[shortKey] = shortenVal(val)
113
+ }
114
+
115
+ return result
116
+ }
117
+
118
+ function shortenVal(val) {
119
+ if (val === null || val === undefined) return val
120
+ if (typeof val === 'function') return val
121
+ if (Array.isArray(val)) {
122
+ return val.map(function (item) {
123
+ if (item !== null && typeof item === 'object') return shorten(item)
124
+ return item
125
+ })
126
+ }
127
+ if (typeof val === 'object') return shorten(val)
128
+ return val
129
+ }
130
+
131
+ /**
132
+ * Recursively convert a component object into a stringified form.
133
+ *
134
+ * Flat primitive props (strings, numbers, booleans, primitive arrays)
135
+ * are encoded into an `in` string using abbreviated keys.
136
+ * Structural props (functions, objects, arrays of objects, PascalCase children,
137
+ * selectors, state, scope) remain as shortened object keys.
138
+ *
139
+ * @param {Object} obj — Symbols component object
140
+ * @returns {Object} — stringified object with `in` property
141
+ */
142
+ export function stringify(obj) {
143
+ if (!obj || typeof obj !== 'object') return obj
144
+ if (Array.isArray(obj)) {
145
+ return obj.map(function (item) {
146
+ if (item !== null && typeof item === 'object') return stringify(item)
147
+ return item
148
+ })
149
+ }
150
+
151
+ const result = {}
152
+ const tokens = []
153
+
154
+ for (const key in obj) {
155
+ const val = obj[key]
156
+
157
+ // PascalCase = child component → keep key, recurse value
158
+ if (isComponentKey(key)) {
159
+ result[key] = stringifyVal(val)
160
+ continue
161
+ }
162
+
163
+ // CSS selectors, media queries, cases → keep key, recurse value
164
+ if (isSelectorKey(key)) {
165
+ result[key] = stringifyVal(val)
166
+ continue
167
+ }
168
+
169
+ const shortKey = propToAbbr[key] || key
170
+
171
+ // Preserved keys (state, scope, etc.) → keep as-is
172
+ if (PRESERVE_VALUE_KEYS.has(key) || PRESERVE_VALUE_KEYS.has(shortKey)) {
173
+ result[shortKey] = val
174
+ continue
175
+ }
176
+
177
+ // Functions → keep as shortened key
178
+ if (typeof val === 'function') {
179
+ result[shortKey] = val
180
+ continue
181
+ }
182
+
183
+ // null / undefined → keep as shortened key
184
+ if (val === null || val === undefined) {
185
+ result[shortKey] = val
186
+ continue
187
+ }
188
+
189
+ // Skip-inline keys (text, html, content, placeholder, src, href) → keep as shortened key
190
+ if (SKIP_INLINE_KEYS.has(key) || SKIP_INLINE_KEYS.has(shortKey)) {
191
+ result[shortKey] = val
192
+ continue
193
+ }
194
+
195
+ // Arrays of objects → keep as shortened key, recurse items
196
+ if (Array.isArray(val)) {
197
+ const hasObjects = val.some(function (item) {
198
+ return item !== null && typeof item === 'object'
199
+ })
200
+ if (hasObjects) {
201
+ result[shortKey] = val.map(function (item) {
202
+ if (item !== null && typeof item === 'object') return stringify(item)
203
+ return item
204
+ })
205
+ continue
206
+ }
207
+ // Single-element arrays can't round-trip through in (decoded as scalar)
208
+ if (val.length <= 1) {
209
+ result[shortKey] = val
210
+ continue
211
+ }
212
+ // Primitive array → encode into `in` token
213
+ tokens.push(shortKey + ':' + val.map(encodeValue).join(','))
214
+ continue
215
+ }
216
+
217
+ // Objects → keep as shortened key, recurse
218
+ if (typeof val === 'object') {
219
+ result[shortKey] = stringify(val)
220
+ continue
221
+ }
222
+
223
+ // Primitives (string, number, boolean) → encode into `in` token
224
+ if (val === true) {
225
+ tokens.push(shortKey)
226
+ } else if (val === false) {
227
+ tokens.push('!' + shortKey)
228
+ } else if (typeof val === 'number') {
229
+ // Numbers must stay as object keys to preserve type
230
+ result[shortKey] = val
231
+ } else if (
232
+ typeof val === 'string' &&
233
+ (val.includes(',') || val.includes('_'))
234
+ ) {
235
+ // Strings with commas or underscores can't be safely encoded
236
+ result[shortKey] = val
237
+ } else {
238
+ tokens.push(shortKey + ':' + encodeValue(val))
239
+ }
240
+ }
241
+
242
+ if (tokens.length) {
243
+ result.in = tokens.join(' ')
244
+ }
245
+
246
+ return result
247
+ }
248
+
249
+ function stringifyVal(val) {
250
+ if (val === null || val === undefined) return val
251
+ if (typeof val === 'function') return val
252
+ if (Array.isArray(val)) {
253
+ return val.map(function (item) {
254
+ if (item !== null && typeof item === 'object') return stringify(item)
255
+ return item
256
+ })
257
+ }
258
+ if (typeof val === 'object') return stringify(val)
259
+ return val
260
+ }
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ export { encode, shorten, stringify } from './encode.js'
4
+ export { decode, expand, parse } from './decode.js'
5
+ export {
6
+ propToAbbr,
7
+ abbrToProp,
8
+ PRESERVE_VALUE_KEYS,
9
+ SKIP_INLINE_KEYS,
10
+ isComponentKey,
11
+ isSelectorKey
12
+ } from './registry.js'