aberdeen 1.0.10 → 1.0.11
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/dist/aberdeen.d.ts +6 -4
- package/dist/aberdeen.js.map +2 -2
- package/dist-min/aberdeen.js.map +2 -2
- package/package.json +1 -1
- package/src/aberdeen.ts +16 -12
- package/html-to-aberdeen +0 -354
package/src/aberdeen.ts
CHANGED
|
@@ -822,15 +822,19 @@ function subscribe(
|
|
|
822
822
|
}
|
|
823
823
|
}
|
|
824
824
|
|
|
825
|
+
// Records in TypeScript pretend that they can have number keys, but in reality they are converted to string.
|
|
826
|
+
// This type changes (number | something) types to (string | something) types, maintaining typing precision as much as possible.
|
|
827
|
+
type KeyToString<K> = K extends number ? string : K extends string | symbol ? K : K extends number | infer U ? string | U : K;
|
|
828
|
+
|
|
825
829
|
export function onEach<T>(
|
|
826
830
|
target: ReadonlyArray<undefined | T>,
|
|
827
831
|
render: (value: T, index: number) => void,
|
|
828
|
-
makeKey?: (value: T,
|
|
832
|
+
makeKey?: (value: T, index: number) => SortKeyType,
|
|
829
833
|
): void;
|
|
830
834
|
export function onEach<K extends string | number | symbol, T>(
|
|
831
835
|
target: Record<K, undefined | T>,
|
|
832
|
-
render: (value: T, index: K) => void,
|
|
833
|
-
makeKey?: (value: T,
|
|
836
|
+
render: (value: T, index: KeyToString<K>) => void,
|
|
837
|
+
makeKey?: (value: T, index: KeyToString<K>) => SortKeyType,
|
|
834
838
|
): void;
|
|
835
839
|
|
|
836
840
|
/**
|
|
@@ -2221,9 +2225,9 @@ export function peek<T>(func: () => T): T {
|
|
|
2221
2225
|
}
|
|
2222
2226
|
|
|
2223
2227
|
/** When using an object as `source`. */
|
|
2224
|
-
export function map<IN, OUT>(
|
|
2225
|
-
source: Record<
|
|
2226
|
-
func: (value: IN, index:
|
|
2228
|
+
export function map<IN, const IN_KEY extends string | number | symbol, OUT>(
|
|
2229
|
+
source: Record<IN_KEY, IN>,
|
|
2230
|
+
func: (value: IN, index: KeyToString<IN_KEY>) => undefined | OUT,
|
|
2227
2231
|
): Record<string | symbol, OUT>;
|
|
2228
2232
|
/** When using an array as `source`. */
|
|
2229
2233
|
export function map<IN, OUT>(
|
|
@@ -2300,7 +2304,7 @@ export function multiMap<
|
|
|
2300
2304
|
K extends string | number | symbol,
|
|
2301
2305
|
IN,
|
|
2302
2306
|
OUT extends { [key: string | symbol]: DatumType },
|
|
2303
|
-
>(source: Record<K, IN>, func: (value: IN, index: K) => OUT | undefined): OUT;
|
|
2307
|
+
>(source: Record<K, IN>, func: (value: IN, index: KeyToString<K>) => OUT | undefined): OUT;
|
|
2304
2308
|
/**
|
|
2305
2309
|
* Reactively maps items from a source proxy (array or object) to a target proxied object,
|
|
2306
2310
|
* where each source item can contribute multiple key-value pairs to the target.
|
|
@@ -2447,18 +2451,18 @@ export function partition<
|
|
|
2447
2451
|
IN_V,
|
|
2448
2452
|
>(
|
|
2449
2453
|
source: Record<IN_K, IN_V>,
|
|
2450
|
-
func: (value: IN_V, key: IN_K) => undefined | OUT_K | OUT_K[],
|
|
2451
|
-
): Record<OUT_K, Record<IN_K
|
|
2452
|
-
const unproxiedOut = {} as Record<OUT_K, Record<IN_K
|
|
2454
|
+
func: (value: IN_V, key: KeyToString<IN_K>) => undefined | OUT_K | OUT_K[],
|
|
2455
|
+
): Record<OUT_K, Record<KeyToString<IN_K>, IN_V>> {
|
|
2456
|
+
const unproxiedOut = {} as Record<OUT_K, Record<KeyToString<IN_K>, IN_V>>;
|
|
2453
2457
|
const out = proxy(unproxiedOut);
|
|
2454
|
-
onEach(source, (item: IN_V, key: IN_K) => {
|
|
2458
|
+
onEach(source, (item: IN_V, key: KeyToString<IN_K>) => {
|
|
2455
2459
|
const rsp = func(item, key);
|
|
2456
2460
|
if (rsp != null) {
|
|
2457
2461
|
const buckets = rsp instanceof Array ? rsp : [rsp];
|
|
2458
2462
|
if (buckets.length) {
|
|
2459
2463
|
for (const bucket of buckets) {
|
|
2460
2464
|
if (unproxiedOut[bucket]) out[bucket][key] = item;
|
|
2461
|
-
else out[bucket] = { [key]: item } as Record<IN_K
|
|
2465
|
+
else out[bucket] = { [key]: item } as Record<KeyToString<IN_K>, IN_V>;
|
|
2462
2466
|
}
|
|
2463
2467
|
clean(() => {
|
|
2464
2468
|
for (const bucket of buckets) {
|
package/html-to-aberdeen
DELETED
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// WARNING: This script was created by Claude Sonnet 3.7, and hasn't
|
|
4
|
-
// received any human code review. It seems to do the job though!
|
|
5
|
-
|
|
6
|
-
export function parseHTML(html) {
|
|
7
|
-
const result = {
|
|
8
|
-
body: []
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
let currentPosition = 0;
|
|
12
|
-
let currentParent = result;
|
|
13
|
-
const stack = [];
|
|
14
|
-
|
|
15
|
-
while (currentPosition < html.length) {
|
|
16
|
-
// Skip whitespace
|
|
17
|
-
while (currentPosition < html.length && /\s/.test(html[currentPosition])) {
|
|
18
|
-
currentPosition++;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (currentPosition >= html.length) break;
|
|
22
|
-
|
|
23
|
-
// Check for comment
|
|
24
|
-
if (html.substring(currentPosition, currentPosition + 4) === '<!--') {
|
|
25
|
-
const endComment = html.indexOf('-->', currentPosition);
|
|
26
|
-
if (endComment === -1) break;
|
|
27
|
-
|
|
28
|
-
const commentContent = html.substring(currentPosition + 4, endComment);
|
|
29
|
-
currentParent.children = currentParent.children || [];
|
|
30
|
-
currentParent.children.push({
|
|
31
|
-
type: 'comment',
|
|
32
|
-
content: commentContent
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
currentPosition = endComment + 3;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Check for tag
|
|
40
|
-
if (html[currentPosition] === '<') {
|
|
41
|
-
// Check if it's a closing tag
|
|
42
|
-
if (html[currentPosition + 1] === '/') {
|
|
43
|
-
const endTag = html.indexOf('>', currentPosition);
|
|
44
|
-
if (endTag === -1) break;
|
|
45
|
-
|
|
46
|
-
const tagName = html.substring(currentPosition + 2, endTag).trim().toLowerCase();
|
|
47
|
-
|
|
48
|
-
// Pop from stack
|
|
49
|
-
if (stack.length > 0) {
|
|
50
|
-
currentParent = stack.pop();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
currentPosition = endTag + 1;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// It's an opening tag
|
|
58
|
-
const endTag = html.indexOf('>', currentPosition);
|
|
59
|
-
if (endTag === -1) break;
|
|
60
|
-
|
|
61
|
-
const selfClosing = html[endTag - 1] === '/';
|
|
62
|
-
const tagContent = html.substring(currentPosition + 1, selfClosing ? endTag - 1 : endTag).trim();
|
|
63
|
-
const spaceIndex = tagContent.search(/\s/);
|
|
64
|
-
|
|
65
|
-
let tagName, attributesStr;
|
|
66
|
-
if (spaceIndex === -1) {
|
|
67
|
-
tagName = tagContent;
|
|
68
|
-
attributesStr = '';
|
|
69
|
-
} else {
|
|
70
|
-
tagName = tagContent.substring(0, spaceIndex);
|
|
71
|
-
attributesStr = tagContent.substring(spaceIndex + 1);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
tagName = tagName.toLowerCase();
|
|
75
|
-
|
|
76
|
-
// Parse attributes
|
|
77
|
-
const attributes = [];
|
|
78
|
-
let attrMatch;
|
|
79
|
-
const attrRegex = /([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
80
|
-
|
|
81
|
-
while ((attrMatch = attrRegex.exec(attributesStr)) !== null) {
|
|
82
|
-
const name = attrMatch[1];
|
|
83
|
-
const value = attrMatch[2] || attrMatch[3] || attrMatch[4] || '';
|
|
84
|
-
attributes.push({ name, value });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const newElement = {
|
|
88
|
-
type: 'element',
|
|
89
|
-
tagName,
|
|
90
|
-
attributes,
|
|
91
|
-
children: []
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// Add to current parent
|
|
95
|
-
if (currentParent === result) {
|
|
96
|
-
currentParent.body = currentParent.body || [];
|
|
97
|
-
currentParent.body.push(newElement);
|
|
98
|
-
} else {
|
|
99
|
-
currentParent.children = currentParent.children || [];
|
|
100
|
-
currentParent.children.push(newElement);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (!selfClosing && !['br', 'hr', 'img', 'input', 'link', 'meta'].includes(tagName)) {
|
|
104
|
-
stack.push(currentParent);
|
|
105
|
-
currentParent = newElement;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
currentPosition = endTag + 1;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// It's text content
|
|
113
|
-
let endText = html.indexOf('<', currentPosition);
|
|
114
|
-
if (endText === -1) endText = html.length;
|
|
115
|
-
|
|
116
|
-
const textContent = html.substring(currentPosition, endText);
|
|
117
|
-
if (textContent.trim()) {
|
|
118
|
-
if (currentParent === result) {
|
|
119
|
-
currentParent.body = currentParent.body || [];
|
|
120
|
-
currentParent.body.push({
|
|
121
|
-
type: 'text',
|
|
122
|
-
content: textContent
|
|
123
|
-
});
|
|
124
|
-
} else {
|
|
125
|
-
currentParent.children = currentParent.children || [];
|
|
126
|
-
currentParent.children.push({
|
|
127
|
-
type: 'text',
|
|
128
|
-
content: textContent
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
currentPosition = endText;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return result;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Read from stdin
|
|
140
|
-
let html = '';
|
|
141
|
-
process.stdin.setEncoding('utf8');
|
|
142
|
-
process.stdin.on('data', (chunk) => {
|
|
143
|
-
html += chunk;
|
|
144
|
-
});
|
|
145
|
-
process.stdin.on('end', () => {
|
|
146
|
-
// Convert HTML to Aberdeen code
|
|
147
|
-
const aberdeenCode = convertHTMLToAberdeen(html);
|
|
148
|
-
|
|
149
|
-
// Output to stdout
|
|
150
|
-
process.stdout.write(aberdeenCode);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// Main conversion function
|
|
154
|
-
function convertHTMLToAberdeen(html) {
|
|
155
|
-
// Parse HTML into a simple AST
|
|
156
|
-
const ast = parseHTML(html);
|
|
157
|
-
|
|
158
|
-
// Generate the Aberdeen code
|
|
159
|
-
let aberdeenCode = ``;
|
|
160
|
-
|
|
161
|
-
// Process the body's children
|
|
162
|
-
for (const node of ast.body) {
|
|
163
|
-
aberdeenCode += processNode(node);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return aberdeenCode;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Process a node and return Aberdeen code
|
|
170
|
-
function processNode(node, indentLevel = 0) {
|
|
171
|
-
const indent = ' '.repeat(indentLevel);
|
|
172
|
-
|
|
173
|
-
if (node.type === 'text') {
|
|
174
|
-
const text = node.content.trim();
|
|
175
|
-
if (text) {
|
|
176
|
-
return `${indent}$(':${escapeString(text)}');\n`;
|
|
177
|
-
}
|
|
178
|
-
return '';
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (node.type === 'comment') {
|
|
182
|
-
return `${indent}// ${node.content.trim()}\n`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (node.type === 'element') {
|
|
186
|
-
// Get tag name
|
|
187
|
-
const tagName = node.tagName.toLowerCase();
|
|
188
|
-
|
|
189
|
-
// Get classes
|
|
190
|
-
const classAttr = node.attributes.find(attr => attr.name === 'class');
|
|
191
|
-
const classes = classAttr
|
|
192
|
-
? classAttr.value.split(/\s+/).filter(Boolean).join('.')
|
|
193
|
-
: '';
|
|
194
|
-
|
|
195
|
-
// Get other attributes
|
|
196
|
-
const attributes = {};
|
|
197
|
-
for (const attr of node.attributes) {
|
|
198
|
-
if (attr.name !== 'class') {
|
|
199
|
-
attributes[attr.name] = attr.value;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Check if node has only text content
|
|
204
|
-
const hasOnlyTextContent =
|
|
205
|
-
node.children.length === 1 &&
|
|
206
|
-
node.children[0].type === 'text' &&
|
|
207
|
-
node.children[0].content.trim();
|
|
208
|
-
|
|
209
|
-
// Build the tag string
|
|
210
|
-
let tagString = tagName;
|
|
211
|
-
if (classes) {
|
|
212
|
-
tagString += `.${classes}`;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (hasOnlyTextContent) {
|
|
216
|
-
tagString += `:${escapeString(node.children[0].content.trim())}`;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
let result = `${indent}$('${tagString}'`;
|
|
220
|
-
|
|
221
|
-
// Add attributes if any
|
|
222
|
-
if (Object.keys(attributes).length > 0) {
|
|
223
|
-
result += `, ${formatAttributes(attributes, indent)}`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Process child nodes
|
|
227
|
-
const childNodes = node.children.filter(child =>
|
|
228
|
-
child.type === 'element' ||
|
|
229
|
-
(child.type === 'text' && child.content.trim())
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
if (childNodes.length > 0 && !hasOnlyTextContent) {
|
|
233
|
-
// Get all descendants in a single-child chain
|
|
234
|
-
const singleChildChain = getSingleChildChain(node);
|
|
235
|
-
|
|
236
|
-
if (singleChildChain.length > 1) {
|
|
237
|
-
// We have a chain of single children, add them all on the same line
|
|
238
|
-
for (let i = 1; i < singleChildChain.length; i++) {
|
|
239
|
-
const chainNode = singleChildChain[i];
|
|
240
|
-
|
|
241
|
-
// Get tag name
|
|
242
|
-
const chainTagName = chainNode.tagName.toLowerCase();
|
|
243
|
-
|
|
244
|
-
// Get classes
|
|
245
|
-
const chainClassAttr = chainNode.attributes.find(attr => attr.name === 'class');
|
|
246
|
-
const chainClasses = chainClassAttr
|
|
247
|
-
? chainClassAttr.value.split(/\s+/).filter(Boolean).join('.')
|
|
248
|
-
: '';
|
|
249
|
-
|
|
250
|
-
// Build the tag string
|
|
251
|
-
let chainTagString = chainTagName;
|
|
252
|
-
if (chainClasses) {
|
|
253
|
-
chainTagString += `.${chainClasses}`;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Check if node has only text content
|
|
257
|
-
const chainHasOnlyTextContent =
|
|
258
|
-
chainNode.children.length === 1 &&
|
|
259
|
-
chainNode.children[0].type === 'text' &&
|
|
260
|
-
chainNode.children[0].content.trim();
|
|
261
|
-
|
|
262
|
-
if (chainHasOnlyTextContent) {
|
|
263
|
-
chainTagString += `:${escapeString(chainNode.children[0].content.trim())}`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
result += `, '${chainTagString}'`;
|
|
267
|
-
|
|
268
|
-
// Add attributes if any
|
|
269
|
-
const chainAttributes = {};
|
|
270
|
-
for (const attr of chainNode.attributes) {
|
|
271
|
-
if (attr.name !== 'class') {
|
|
272
|
-
chainAttributes[attr.name] = attr.value;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (Object.keys(chainAttributes).length > 0) {
|
|
277
|
-
result += `, ${formatAttributes(chainAttributes, indent)}`;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Check if the last node in the chain has any non-text children
|
|
282
|
-
const lastNode = singleChildChain[singleChildChain.length - 1];
|
|
283
|
-
const lastNodeChildren = lastNode.children.filter(child =>
|
|
284
|
-
child.type === 'element' ||
|
|
285
|
-
(child.type === 'text' && child.content.trim() &&
|
|
286
|
-
!(lastNode.children.length === 1 && lastNode.children[0].type === 'text'))
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
if (lastNodeChildren.length > 0) {
|
|
290
|
-
result += `, () => {\n`;
|
|
291
|
-
for (const child of lastNodeChildren) {
|
|
292
|
-
result += processNode(child, indentLevel + 1);
|
|
293
|
-
}
|
|
294
|
-
result += `${indent}}`;
|
|
295
|
-
}
|
|
296
|
-
} else {
|
|
297
|
-
// Multiple children, use a content function
|
|
298
|
-
result += `, () => {\n`;
|
|
299
|
-
for (const child of childNodes) {
|
|
300
|
-
result += processNode(child, indentLevel + 1);
|
|
301
|
-
}
|
|
302
|
-
result += `${indent}}`;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
result += `);\n`;
|
|
307
|
-
return result;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return '';
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Get a chain of nodes where each node has exactly one element child
|
|
314
|
-
function getSingleChildChain(node) {
|
|
315
|
-
const chain = [node];
|
|
316
|
-
let current = node;
|
|
317
|
-
|
|
318
|
-
while (true) {
|
|
319
|
-
// Get element children
|
|
320
|
-
const elementChildren = current.children.filter(child => child.type === 'element');
|
|
321
|
-
|
|
322
|
-
// If there's exactly one element child, add it to the chain
|
|
323
|
-
if (elementChildren.length === 1) {
|
|
324
|
-
current = elementChildren[0];
|
|
325
|
-
chain.push(current);
|
|
326
|
-
} else {
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return chain;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Format attributes object with proper indentation
|
|
335
|
-
function formatAttributes(attributes, indent) {
|
|
336
|
-
const attrLines = JSON.stringify(attributes, null, 4).split('\n');
|
|
337
|
-
|
|
338
|
-
if (attrLines.length <= 1) {
|
|
339
|
-
return JSON.stringify(attributes);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return attrLines.map((line, i) => {
|
|
343
|
-
if (i === 0) return line;
|
|
344
|
-
return indent + line;
|
|
345
|
-
}).join('\n');
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Escape special characters in strings
|
|
349
|
-
function escapeString(str) {
|
|
350
|
-
return str
|
|
351
|
-
.replace(/\\/g, '\\\\')
|
|
352
|
-
.replace(/'/g, "\\'")
|
|
353
|
-
.replace(/\n/g, '\\n');
|
|
354
|
-
}
|