@symbo.ls/shorthand 2.34.33 → 2.34.35
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 +181 -0
- package/dist/cjs/further.js +263 -0
- package/dist/cjs/index.js +4 -1
- package/package.json +2 -2
- package/src/further.js +373 -0
- package/src/index.js +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# @symbo.ls/shorthand
|
|
2
|
+
|
|
3
|
+
Bidirectional shorthand transpiler for [Symbols](https://github.com/symbo-ls/smbls) component properties. Compresses DOMQL component objects using abbreviated property names and compact string encoding — and losslessly expands them back.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @symbo.ls/shorthand
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
Six functions in three complementary pairs:
|
|
14
|
+
|
|
15
|
+
| Pair | Forward | Inverse | Description |
|
|
16
|
+
| ---------- | ----------- | -------- | ------------------------------------------------------------ |
|
|
17
|
+
| **String** | `encode` | `decode` | Flat object ↔ single-line shorthand string |
|
|
18
|
+
| **Object** | `shorten` | `expand` | Recursive key abbreviation preserving structure |
|
|
19
|
+
| **Hybrid** | `stringify` | `parse` | Primitive props → `in` string, structural props stay as keys |
|
|
20
|
+
|
|
21
|
+
### encode / decode
|
|
22
|
+
|
|
23
|
+
Converts flat primitive props into a compact single-line string.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import { encode, decode } from '@symbo.ls/shorthand'
|
|
27
|
+
|
|
28
|
+
encode({ padding: 'A B', background: 'red', hidden: true })
|
|
29
|
+
// → 'p:A_B bg:red hid'
|
|
30
|
+
|
|
31
|
+
decode('p:A_B bg:red hid')
|
|
32
|
+
// → { padding: 'A B', background: 'red', hidden: true }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Syntax rules:**
|
|
36
|
+
|
|
37
|
+
- `abbr:value` — key-value pair
|
|
38
|
+
- `_` — represents spaces inside values
|
|
39
|
+
- `,` — array separator (`ext:Flex,Box` → `extends: ['Flex', 'Box']`)
|
|
40
|
+
- bare `abbr` — boolean `true`
|
|
41
|
+
- `!abbr` — boolean `false`
|
|
42
|
+
|
|
43
|
+
Functions, objects, and other non-serializable values are skipped.
|
|
44
|
+
|
|
45
|
+
### shorten / expand
|
|
46
|
+
|
|
47
|
+
Recursively abbreviates (or expands) property keys throughout the component tree while preserving the full object structure — child components, selectors, functions, arrays, and everything else stays intact.
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
import { shorten, expand } from '@symbo.ls/shorthand'
|
|
51
|
+
|
|
52
|
+
const component = {
|
|
53
|
+
extends: 'Flex',
|
|
54
|
+
padding: 'A B',
|
|
55
|
+
gap: 'C',
|
|
56
|
+
flexDirection: 'column',
|
|
57
|
+
onClick: (e, el) => {},
|
|
58
|
+
Header: { fontSize: 'B' },
|
|
59
|
+
':hover': { background: 'blue' }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
shorten(component)
|
|
63
|
+
// {
|
|
64
|
+
// ext: 'Flex',
|
|
65
|
+
// p: 'A B',
|
|
66
|
+
// g: 'C',
|
|
67
|
+
// fxd: 'column',
|
|
68
|
+
// '@ck': (e, el) => {},
|
|
69
|
+
// Header: { fs: 'B' },
|
|
70
|
+
// ':hover': { bg: 'blue' }
|
|
71
|
+
// }
|
|
72
|
+
|
|
73
|
+
expand(shorten(component)) // deeply equals original
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Preservation rules:**
|
|
77
|
+
|
|
78
|
+
- **PascalCase keys** (child components) — key kept as-is, value recursed
|
|
79
|
+
- **Selector keys** (`:hover`, `@dark`, `.isActive`, `> *`) — key kept, value recursed
|
|
80
|
+
- **`state`, `scope`, `attr`, `style`, `data`, `context`, `query`, `class`** — values preserved as-is (no key abbreviation inside)
|
|
81
|
+
- **Functions** — preserved, only the key is shortened
|
|
82
|
+
|
|
83
|
+
### stringify / parse
|
|
84
|
+
|
|
85
|
+
Hybrid encoding: flat primitive props go into a compact `in` string, while structural props (functions, nested objects, child components, selectors) remain as shortened object keys.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { stringify, parse } from '@symbo.ls/shorthand'
|
|
89
|
+
|
|
90
|
+
const component = {
|
|
91
|
+
extends: 'Flex',
|
|
92
|
+
padding: 'A',
|
|
93
|
+
background: 'surface',
|
|
94
|
+
borderRadius: 'B',
|
|
95
|
+
onClick: (e, el) => {},
|
|
96
|
+
Header: { fontSize: 'B', color: 'title' }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
stringify(component)
|
|
100
|
+
// {
|
|
101
|
+
// in: 'ext:Flex p:A bg:surface bdr:B',
|
|
102
|
+
// '@ck': (e, el) => {},
|
|
103
|
+
// Header: { in: 'fs:B c:title' }
|
|
104
|
+
// }
|
|
105
|
+
|
|
106
|
+
parse(stringify(component)) // deeply equals original
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**What goes into `in`:**
|
|
110
|
+
|
|
111
|
+
- String props (except `text`, `html`, `content`, `placeholder`, `src`, `href`)
|
|
112
|
+
- Boolean props
|
|
113
|
+
- Primitive arrays (length > 1)
|
|
114
|
+
|
|
115
|
+
**What stays as object keys:**
|
|
116
|
+
|
|
117
|
+
- Functions, `null`, `undefined`
|
|
118
|
+
- Nested objects, arrays of objects
|
|
119
|
+
- PascalCase children, selector keys
|
|
120
|
+
- Preserved keys (`state`, `scope`, `style`, etc.)
|
|
121
|
+
- Skip-inline keys (`text`, `html`, `content`, `placeholder`, `src`, `href`)
|
|
122
|
+
- Numbers (to preserve type through round-trip)
|
|
123
|
+
- Strings containing `,` or `_` (to avoid encoding ambiguity)
|
|
124
|
+
|
|
125
|
+
## Registry
|
|
126
|
+
|
|
127
|
+
The package ships with 300+ bidirectional abbreviation mappings covering:
|
|
128
|
+
|
|
129
|
+
- **DOMQL core** — `extends` → `ext`, `childExtends` → `cex`, `state` → `st`, `tag` → `tg`
|
|
130
|
+
- **Symbols shorthand** — `flow` → `fl`, `align` → `aln`, `round` → `rnd`, `boxSize` → `bsz`
|
|
131
|
+
- **CSS properties** — `padding` → `p`, `background` → `bg`, `flexDirection` → `fxd`, `zIndex` → `zi`
|
|
132
|
+
- **HTML attributes** — `placeholder` → `phd`, `disabled` → `dis`, `required` → `req`
|
|
133
|
+
- **ARIA attributes** — `ariaLabel` → `alb`, `ariaHidden` → `ahid`, `role` → `role`
|
|
134
|
+
- **Events** — `onClick` → `@ck`, `onRender` → `@rn`, `onSubmit` → `@sm`, `onKeyDown` → `@kd`
|
|
135
|
+
|
|
136
|
+
Access the maps directly:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
import { propToAbbr, abbrToProp } from '@symbo.ls/shorthand'
|
|
140
|
+
|
|
141
|
+
propToAbbr['padding'] // 'p'
|
|
142
|
+
propToAbbr['onClick'] // '@ck'
|
|
143
|
+
abbrToProp['bg'] // 'background'
|
|
144
|
+
abbrToProp['@rn'] // 'onRender'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Helpers
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
import {
|
|
151
|
+
isComponentKey,
|
|
152
|
+
isSelectorKey,
|
|
153
|
+
PRESERVE_VALUE_KEYS,
|
|
154
|
+
SKIP_INLINE_KEYS
|
|
155
|
+
} from '@symbo.ls/shorthand'
|
|
156
|
+
|
|
157
|
+
isComponentKey('Header') // true (PascalCase)
|
|
158
|
+
isComponentKey('padding') // false
|
|
159
|
+
|
|
160
|
+
isSelectorKey(':hover') // true
|
|
161
|
+
isSelectorKey('@dark') // true
|
|
162
|
+
isSelectorKey('.isActive') // true
|
|
163
|
+
isSelectorKey('> *') // true
|
|
164
|
+
isSelectorKey('padding') // false
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Round-trip guarantee
|
|
168
|
+
|
|
169
|
+
All three pairs are lossless — the inverse function always reproduces the original:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
decode(encode(obj)) // ≈ obj (flat primitives only)
|
|
173
|
+
expand(shorten(obj)) // ≡ obj (full structure)
|
|
174
|
+
parse(stringify(obj)) // ≡ obj (full structure)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The test suite verifies round-trip correctness against 200+ real-world Symbols components.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
ISC
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var further_exports = {};
|
|
20
|
+
__export(further_exports, {
|
|
21
|
+
parseFurther: () => parseFurther,
|
|
22
|
+
stringifyFurther: () => stringifyFurther
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(further_exports);
|
|
25
|
+
var import_registry = require("./registry.js");
|
|
26
|
+
function escapeValue(val) {
|
|
27
|
+
const str = String(val);
|
|
28
|
+
return str.replace(/\\/g, "\\\\").replace(/_/g, "\\_").replace(/,/g, "\\,").replace(/ /g, "_");
|
|
29
|
+
}
|
|
30
|
+
function unescapeValue(val) {
|
|
31
|
+
let result = "";
|
|
32
|
+
let i = 0;
|
|
33
|
+
while (i < val.length) {
|
|
34
|
+
if (val[i] === "\\" && i + 1 < val.length) {
|
|
35
|
+
const next = val[i + 1];
|
|
36
|
+
if (next === "," || next === "_" || next === "\\") {
|
|
37
|
+
result += next;
|
|
38
|
+
i += 2;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (val[i] === "_") {
|
|
43
|
+
result += " ";
|
|
44
|
+
} else {
|
|
45
|
+
result += val[i];
|
|
46
|
+
}
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function splitOnUnescapedCommas(str) {
|
|
52
|
+
const parts = [];
|
|
53
|
+
let current = "";
|
|
54
|
+
let i = 0;
|
|
55
|
+
while (i < str.length) {
|
|
56
|
+
if (str[i] === "\\" && i + 1 < str.length) {
|
|
57
|
+
current += str[i] + str[i + 1];
|
|
58
|
+
i += 2;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (str[i] === ",") {
|
|
62
|
+
parts.push(current);
|
|
63
|
+
current = "";
|
|
64
|
+
i++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
current += str[i];
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
parts.push(current);
|
|
71
|
+
return parts;
|
|
72
|
+
}
|
|
73
|
+
function canCollapse(val) {
|
|
74
|
+
if (val === null || val === void 0) return false;
|
|
75
|
+
if (typeof val !== "object" || Array.isArray(val)) return false;
|
|
76
|
+
const keys = Object.keys(val);
|
|
77
|
+
return keys.length === 1 && keys[0] === "in" && typeof val.in === "string";
|
|
78
|
+
}
|
|
79
|
+
function stringifyFurther(obj) {
|
|
80
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
81
|
+
if (Array.isArray(obj)) {
|
|
82
|
+
return obj.map(function(item) {
|
|
83
|
+
if (item !== null && typeof item === "object")
|
|
84
|
+
return stringifyFurther(item);
|
|
85
|
+
return item;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const result = {};
|
|
89
|
+
const tokens = [];
|
|
90
|
+
for (const key in obj) {
|
|
91
|
+
const val = obj[key];
|
|
92
|
+
if ((0, import_registry.isComponentKey)(key)) {
|
|
93
|
+
const child = stringifyFurtherVal(val);
|
|
94
|
+
result[key] = canCollapse(child) ? child.in : child;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if ((0, import_registry.isSelectorKey)(key)) {
|
|
98
|
+
const child = stringifyFurtherVal(val);
|
|
99
|
+
result[key] = canCollapse(child) ? child.in : child;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const shortKey = import_registry.propToAbbr[key] || key;
|
|
103
|
+
if (import_registry.PRESERVE_VALUE_KEYS.has(key) || import_registry.PRESERVE_VALUE_KEYS.has(shortKey)) {
|
|
104
|
+
result[shortKey] = val;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (typeof val === "function") {
|
|
108
|
+
result[shortKey] = val;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (val === null || val === void 0) {
|
|
112
|
+
result[shortKey] = val;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (import_registry.SKIP_INLINE_KEYS.has(key) || import_registry.SKIP_INLINE_KEYS.has(shortKey)) {
|
|
116
|
+
result[shortKey] = val;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(val)) {
|
|
120
|
+
const hasObjects = val.some(function(item) {
|
|
121
|
+
return item !== null && typeof item === "object";
|
|
122
|
+
});
|
|
123
|
+
if (hasObjects) {
|
|
124
|
+
result[shortKey] = val.map(function(item) {
|
|
125
|
+
if (item !== null && typeof item === "object")
|
|
126
|
+
return stringifyFurther(item);
|
|
127
|
+
return item;
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (val.length <= 1) {
|
|
132
|
+
result[shortKey] = val;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
tokens.push(shortKey + ":" + val.map(escapeValue).join(","));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (typeof val === "object") {
|
|
139
|
+
result[shortKey] = stringifyFurther(val);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (val === true) {
|
|
143
|
+
tokens.push(shortKey);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (val === false) {
|
|
147
|
+
tokens.push("!" + shortKey);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (typeof val === "number") {
|
|
151
|
+
tokens.push(shortKey + ":#" + val);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
tokens.push(shortKey + ":" + escapeValue(val));
|
|
155
|
+
}
|
|
156
|
+
if (tokens.length) {
|
|
157
|
+
result.in = tokens.join(" ");
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function stringifyFurtherVal(val) {
|
|
162
|
+
if (val === null || val === void 0) return val;
|
|
163
|
+
if (typeof val === "function") return val;
|
|
164
|
+
if (Array.isArray(val)) {
|
|
165
|
+
return val.map(function(item) {
|
|
166
|
+
if (item !== null && typeof item === "object")
|
|
167
|
+
return stringifyFurther(item);
|
|
168
|
+
return item;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (typeof val === "object") return stringifyFurther(val);
|
|
172
|
+
return val;
|
|
173
|
+
}
|
|
174
|
+
function decodeFurtherValue(val) {
|
|
175
|
+
if (val.startsWith("#") && /^#-?\d+(\.\d+)?$/.test(val)) {
|
|
176
|
+
return Number(val.slice(1));
|
|
177
|
+
}
|
|
178
|
+
return unescapeValue(val);
|
|
179
|
+
}
|
|
180
|
+
function decodeFurtherInline(str) {
|
|
181
|
+
if (!str || typeof str !== "string") return {};
|
|
182
|
+
const obj = {};
|
|
183
|
+
const tokens = str.trim().split(/\s+/).filter(Boolean);
|
|
184
|
+
for (const token of tokens) {
|
|
185
|
+
if (token.startsWith("!")) {
|
|
186
|
+
const abbr2 = token.slice(1);
|
|
187
|
+
const prop2 = import_registry.abbrToProp[abbr2] || abbr2;
|
|
188
|
+
obj[prop2] = false;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const colonIdx = token.indexOf(":");
|
|
192
|
+
if (colonIdx === -1) {
|
|
193
|
+
const prop2 = import_registry.abbrToProp[token] || token;
|
|
194
|
+
obj[prop2] = true;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const abbr = token.slice(0, colonIdx);
|
|
198
|
+
const rawVal = token.slice(colonIdx + 1);
|
|
199
|
+
const prop = import_registry.abbrToProp[abbr] || abbr;
|
|
200
|
+
const parts = splitOnUnescapedCommas(rawVal);
|
|
201
|
+
if (parts.length > 1) {
|
|
202
|
+
obj[prop] = parts.map(decodeFurtherValue);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
obj[prop] = decodeFurtherValue(rawVal);
|
|
206
|
+
}
|
|
207
|
+
return obj;
|
|
208
|
+
}
|
|
209
|
+
function parseFurther(obj) {
|
|
210
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
211
|
+
if (Array.isArray(obj)) {
|
|
212
|
+
return obj.map(function(item) {
|
|
213
|
+
if (item !== null && typeof item === "object") return parseFurther(item);
|
|
214
|
+
return item;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const result = {};
|
|
218
|
+
if (typeof obj.in === "string") {
|
|
219
|
+
const decoded = decodeFurtherInline(obj.in);
|
|
220
|
+
for (const prop in decoded) {
|
|
221
|
+
result[prop] = decoded[prop];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const key in obj) {
|
|
225
|
+
if (key === "in") continue;
|
|
226
|
+
const val = obj[key];
|
|
227
|
+
if ((0, import_registry.isComponentKey)(key)) {
|
|
228
|
+
if (typeof val === "string") {
|
|
229
|
+
result[key] = decodeFurtherInline(val);
|
|
230
|
+
} else {
|
|
231
|
+
result[key] = parseFurtherVal(val);
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const fullKey = import_registry.abbrToProp[key] || key;
|
|
236
|
+
if ((0, import_registry.isSelectorKey)(key) && fullKey === key) {
|
|
237
|
+
if (typeof val === "string") {
|
|
238
|
+
result[key] = decodeFurtherInline(val);
|
|
239
|
+
} else {
|
|
240
|
+
result[key] = parseFurtherVal(val);
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (import_registry.PRESERVE_VALUE_KEYS.has(fullKey) || import_registry.PRESERVE_VALUE_KEYS.has(key)) {
|
|
245
|
+
result[fullKey] = val;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
result[fullKey] = parseFurtherVal(val);
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
function parseFurtherVal(val) {
|
|
253
|
+
if (val === null || val === void 0) return val;
|
|
254
|
+
if (typeof val === "function") return val;
|
|
255
|
+
if (Array.isArray(val)) {
|
|
256
|
+
return val.map(function(item) {
|
|
257
|
+
if (item !== null && typeof item === "object") return parseFurther(item);
|
|
258
|
+
return item;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (typeof val === "object") return parseFurther(val);
|
|
262
|
+
return val;
|
|
263
|
+
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -27,11 +27,14 @@ __export(index_exports, {
|
|
|
27
27
|
isComponentKey: () => import_registry.isComponentKey,
|
|
28
28
|
isSelectorKey: () => import_registry.isSelectorKey,
|
|
29
29
|
parse: () => import_decode.parse,
|
|
30
|
+
parseFurther: () => import_further.parseFurther,
|
|
30
31
|
propToAbbr: () => import_registry.propToAbbr,
|
|
31
32
|
shorten: () => import_encode.shorten,
|
|
32
|
-
stringify: () => import_encode.stringify
|
|
33
|
+
stringify: () => import_encode.stringify,
|
|
34
|
+
stringifyFurther: () => import_further.stringifyFurther
|
|
33
35
|
});
|
|
34
36
|
module.exports = __toCommonJS(index_exports);
|
|
35
37
|
var import_encode = require("./encode.js");
|
|
36
38
|
var import_decode = require("./decode.js");
|
|
39
|
+
var import_further = require("./further.js");
|
|
37
40
|
var import_registry = require("./registry.js");
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@symbo.ls/shorthand",
|
|
3
3
|
"description": "Shorthand syntax transpiler for Symbols properties",
|
|
4
4
|
"author": "symbo.ls",
|
|
5
|
-
"version": "2.34.
|
|
5
|
+
"version": "2.34.35",
|
|
6
6
|
"repository": "https://github.com/symbo-ls/smbls",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/index.js",
|
|
@@ -23,5 +23,5 @@
|
|
|
23
23
|
"docs"
|
|
24
24
|
],
|
|
25
25
|
"license": "ISC",
|
|
26
|
-
"gitHead": "
|
|
26
|
+
"gitHead": "0be12bcdd99c265fdeded6d756a1a3616a943ffc"
|
|
27
27
|
}
|
package/src/further.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
propToAbbr,
|
|
5
|
+
abbrToProp,
|
|
6
|
+
PRESERVE_VALUE_KEYS,
|
|
7
|
+
SKIP_INLINE_KEYS,
|
|
8
|
+
isComponentKey,
|
|
9
|
+
isSelectorKey
|
|
10
|
+
} from './registry.js'
|
|
11
|
+
|
|
12
|
+
// ── Value encoding ──
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Escape a value for the further-inlined `in` string.
|
|
16
|
+
*
|
|
17
|
+
* Handles characters that conflict with the token format:
|
|
18
|
+
* \ → \\ (literal backslash)
|
|
19
|
+
* _ → \_ (literal underscore, since _ normally means space)
|
|
20
|
+
* , → \, (literal comma, since , normally means array separator)
|
|
21
|
+
* ' ' → _ (space encoded as underscore)
|
|
22
|
+
*/
|
|
23
|
+
function escapeValue(val) {
|
|
24
|
+
const str = String(val)
|
|
25
|
+
return str
|
|
26
|
+
.replace(/\\/g, '\\\\')
|
|
27
|
+
.replace(/_/g, '\\_')
|
|
28
|
+
.replace(/,/g, '\\,')
|
|
29
|
+
.replace(/ /g, '_')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Unescape a value from the further-inlined `in` string.
|
|
34
|
+
* Processes character-by-character to handle escape sequences.
|
|
35
|
+
*/
|
|
36
|
+
function unescapeValue(val) {
|
|
37
|
+
let result = ''
|
|
38
|
+
let i = 0
|
|
39
|
+
while (i < val.length) {
|
|
40
|
+
if (val[i] === '\\' && i + 1 < val.length) {
|
|
41
|
+
const next = val[i + 1]
|
|
42
|
+
if (next === ',' || next === '_' || next === '\\') {
|
|
43
|
+
result += next
|
|
44
|
+
i += 2
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (val[i] === '_') {
|
|
49
|
+
result += ' '
|
|
50
|
+
} else {
|
|
51
|
+
result += val[i]
|
|
52
|
+
}
|
|
53
|
+
i++
|
|
54
|
+
}
|
|
55
|
+
return result
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Split a string on commas that are NOT escaped with backslash.
|
|
60
|
+
*/
|
|
61
|
+
function splitOnUnescapedCommas(str) {
|
|
62
|
+
const parts = []
|
|
63
|
+
let current = ''
|
|
64
|
+
let i = 0
|
|
65
|
+
while (i < str.length) {
|
|
66
|
+
if (str[i] === '\\' && i + 1 < str.length) {
|
|
67
|
+
current += str[i] + str[i + 1]
|
|
68
|
+
i += 2
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
if (str[i] === ',') {
|
|
72
|
+
parts.push(current)
|
|
73
|
+
current = ''
|
|
74
|
+
i++
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
current += str[i]
|
|
78
|
+
i++
|
|
79
|
+
}
|
|
80
|
+
parts.push(current)
|
|
81
|
+
return parts
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Encode ──
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns true if a recursed value can be collapsed from { in: '...' } to a bare string.
|
|
88
|
+
*/
|
|
89
|
+
function canCollapse(val) {
|
|
90
|
+
if (val === null || val === undefined) return false
|
|
91
|
+
if (typeof val !== 'object' || Array.isArray(val)) return false
|
|
92
|
+
const keys = Object.keys(val)
|
|
93
|
+
return keys.length === 1 && keys[0] === 'in' && typeof val.in === 'string'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Aggressively compress a Symbols component object.
|
|
98
|
+
*
|
|
99
|
+
* Beyond stringify(), this:
|
|
100
|
+
* - Inlines numbers using # prefix: zi:#100
|
|
101
|
+
* - Inlines comma/underscore strings via escaping: bxsh:black_.10\,_0px\,_2px
|
|
102
|
+
* - Collapses children/selectors that only have `in` to bare strings:
|
|
103
|
+
* Icon: 'nm:search bsz:A' (instead of Icon: { in: '...' })
|
|
104
|
+
* "@mobile": 'p:Y2_A' (instead of "@mobile": { in: '...' })
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} obj — Symbols component object
|
|
107
|
+
* @returns {Object} — further-compressed object
|
|
108
|
+
*/
|
|
109
|
+
export function stringifyFurther(obj) {
|
|
110
|
+
if (!obj || typeof obj !== 'object') return obj
|
|
111
|
+
if (Array.isArray(obj)) {
|
|
112
|
+
return obj.map(function (item) {
|
|
113
|
+
if (item !== null && typeof item === 'object')
|
|
114
|
+
return stringifyFurther(item)
|
|
115
|
+
return item
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = {}
|
|
120
|
+
const tokens = []
|
|
121
|
+
|
|
122
|
+
for (const key in obj) {
|
|
123
|
+
const val = obj[key]
|
|
124
|
+
|
|
125
|
+
// PascalCase child → recurse, possibly collapse
|
|
126
|
+
if (isComponentKey(key)) {
|
|
127
|
+
const child = stringifyFurtherVal(val)
|
|
128
|
+
result[key] = canCollapse(child) ? child.in : child
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Selector/media/case → recurse, possibly collapse
|
|
133
|
+
if (isSelectorKey(key)) {
|
|
134
|
+
const child = stringifyFurtherVal(val)
|
|
135
|
+
result[key] = canCollapse(child) ? child.in : child
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const shortKey = propToAbbr[key] || key
|
|
140
|
+
|
|
141
|
+
// Preserved keys (state, scope, attr, style, etc.) → keep as-is
|
|
142
|
+
if (PRESERVE_VALUE_KEYS.has(key) || PRESERVE_VALUE_KEYS.has(shortKey)) {
|
|
143
|
+
result[shortKey] = val
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Functions → keep
|
|
148
|
+
if (typeof val === 'function') {
|
|
149
|
+
result[shortKey] = val
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// null/undefined → keep
|
|
154
|
+
if (val === null || val === undefined) {
|
|
155
|
+
result[shortKey] = val
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Skip-inline keys (text, html, content, placeholder, src, href) → keep
|
|
160
|
+
if (SKIP_INLINE_KEYS.has(key) || SKIP_INLINE_KEYS.has(shortKey)) {
|
|
161
|
+
result[shortKey] = val
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Arrays
|
|
166
|
+
if (Array.isArray(val)) {
|
|
167
|
+
const hasObjects = val.some(function (item) {
|
|
168
|
+
return item !== null && typeof item === 'object'
|
|
169
|
+
})
|
|
170
|
+
if (hasObjects) {
|
|
171
|
+
result[shortKey] = val.map(function (item) {
|
|
172
|
+
if (item !== null && typeof item === 'object')
|
|
173
|
+
return stringifyFurther(item)
|
|
174
|
+
return item
|
|
175
|
+
})
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
// Single-element arrays can't round-trip (decoded as scalar)
|
|
179
|
+
if (val.length <= 1) {
|
|
180
|
+
result[shortKey] = val
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
tokens.push(shortKey + ':' + val.map(escapeValue).join(','))
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Objects → recurse
|
|
188
|
+
if (typeof val === 'object') {
|
|
189
|
+
result[shortKey] = stringifyFurther(val)
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Booleans
|
|
194
|
+
if (val === true) {
|
|
195
|
+
tokens.push(shortKey)
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (val === false) {
|
|
199
|
+
tokens.push('!' + shortKey)
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Numbers → inline with # prefix for lossless round-trip
|
|
204
|
+
if (typeof val === 'number') {
|
|
205
|
+
tokens.push(shortKey + ':#' + val)
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Strings → inline with escape sequences
|
|
210
|
+
tokens.push(shortKey + ':' + escapeValue(val))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (tokens.length) {
|
|
214
|
+
result.in = tokens.join(' ')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return result
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function stringifyFurtherVal(val) {
|
|
221
|
+
if (val === null || val === undefined) return val
|
|
222
|
+
if (typeof val === 'function') return val
|
|
223
|
+
if (Array.isArray(val)) {
|
|
224
|
+
return val.map(function (item) {
|
|
225
|
+
if (item !== null && typeof item === 'object')
|
|
226
|
+
return stringifyFurther(item)
|
|
227
|
+
return item
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
if (typeof val === 'object') return stringifyFurther(val)
|
|
231
|
+
return val
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Decode ──
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Decode a single further-inline value.
|
|
238
|
+
* Handles # prefix for numbers and escape sequences.
|
|
239
|
+
* Only treats # as a number marker when followed by a strict decimal number
|
|
240
|
+
* (avoids conflict with CSS hex colors like #1E2397).
|
|
241
|
+
*/
|
|
242
|
+
function decodeFurtherValue(val) {
|
|
243
|
+
if (val.startsWith('#') && /^#-?\d+(\.\d+)?$/.test(val)) {
|
|
244
|
+
return Number(val.slice(1))
|
|
245
|
+
}
|
|
246
|
+
return unescapeValue(val)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Decode a further `in` string into key-value pairs with full property names.
|
|
251
|
+
*/
|
|
252
|
+
function decodeFurtherInline(str) {
|
|
253
|
+
if (!str || typeof str !== 'string') return {}
|
|
254
|
+
|
|
255
|
+
const obj = {}
|
|
256
|
+
const tokens = str.trim().split(/\s+/).filter(Boolean)
|
|
257
|
+
|
|
258
|
+
for (const token of tokens) {
|
|
259
|
+
if (token.startsWith('!')) {
|
|
260
|
+
const abbr = token.slice(1)
|
|
261
|
+
const prop = abbrToProp[abbr] || abbr
|
|
262
|
+
obj[prop] = false
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const colonIdx = token.indexOf(':')
|
|
267
|
+
|
|
268
|
+
if (colonIdx === -1) {
|
|
269
|
+
const prop = abbrToProp[token] || token
|
|
270
|
+
obj[prop] = true
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const abbr = token.slice(0, colonIdx)
|
|
275
|
+
const rawVal = token.slice(colonIdx + 1)
|
|
276
|
+
const prop = abbrToProp[abbr] || abbr
|
|
277
|
+
|
|
278
|
+
const parts = splitOnUnescapedCommas(rawVal)
|
|
279
|
+
if (parts.length > 1) {
|
|
280
|
+
obj[prop] = parts.map(decodeFurtherValue)
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
obj[prop] = decodeFurtherValue(rawVal)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return obj
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Decode a stringifyFurther result back to the original component object.
|
|
292
|
+
*
|
|
293
|
+
* Handles:
|
|
294
|
+
* - String PascalCase values → decode as inline props
|
|
295
|
+
* - String selector/media/case values → decode as inline props
|
|
296
|
+
* - #number prefix → actual JS number
|
|
297
|
+
* - Escape sequences: \, → , \_ → _ \\ → \
|
|
298
|
+
* - Abbreviated keys → full property names
|
|
299
|
+
*
|
|
300
|
+
* @param {Object} obj — stringifyFurther result
|
|
301
|
+
* @returns {Object} — original Symbols component object
|
|
302
|
+
*/
|
|
303
|
+
export function parseFurther(obj) {
|
|
304
|
+
if (!obj || typeof obj !== 'object') return obj
|
|
305
|
+
if (Array.isArray(obj)) {
|
|
306
|
+
return obj.map(function (item) {
|
|
307
|
+
if (item !== null && typeof item === 'object') return parseFurther(item)
|
|
308
|
+
return item
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const result = {}
|
|
313
|
+
|
|
314
|
+
// Decode `in` string first so its props come first
|
|
315
|
+
if (typeof obj.in === 'string') {
|
|
316
|
+
const decoded = decodeFurtherInline(obj.in)
|
|
317
|
+
for (const prop in decoded) {
|
|
318
|
+
result[prop] = decoded[prop]
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const key in obj) {
|
|
323
|
+
if (key === 'in') continue
|
|
324
|
+
|
|
325
|
+
const val = obj[key]
|
|
326
|
+
|
|
327
|
+
// PascalCase child
|
|
328
|
+
if (isComponentKey(key)) {
|
|
329
|
+
if (typeof val === 'string') {
|
|
330
|
+
result[key] = decodeFurtherInline(val)
|
|
331
|
+
} else {
|
|
332
|
+
result[key] = parseFurtherVal(val)
|
|
333
|
+
}
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Resolve abbreviation
|
|
338
|
+
const fullKey = abbrToProp[key] || key
|
|
339
|
+
|
|
340
|
+
// Selector/media/case key (NOT an event abbreviation)
|
|
341
|
+
if (isSelectorKey(key) && fullKey === key) {
|
|
342
|
+
if (typeof val === 'string') {
|
|
343
|
+
result[key] = decodeFurtherInline(val)
|
|
344
|
+
} else {
|
|
345
|
+
result[key] = parseFurtherVal(val)
|
|
346
|
+
}
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Preserved keys (state, scope, etc.) → keep value as-is
|
|
351
|
+
if (PRESERVE_VALUE_KEYS.has(fullKey) || PRESERVE_VALUE_KEYS.has(key)) {
|
|
352
|
+
result[fullKey] = val
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
result[fullKey] = parseFurtherVal(val)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return result
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function parseFurtherVal(val) {
|
|
363
|
+
if (val === null || val === undefined) return val
|
|
364
|
+
if (typeof val === 'function') return val
|
|
365
|
+
if (Array.isArray(val)) {
|
|
366
|
+
return val.map(function (item) {
|
|
367
|
+
if (item !== null && typeof item === 'object') return parseFurther(item)
|
|
368
|
+
return item
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
if (typeof val === 'object') return parseFurther(val)
|
|
372
|
+
return val
|
|
373
|
+
}
|