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/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, key: any) => SortKeyType,
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, key: K) => SortKeyType,
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<string | symbol, IN>,
2226
- func: (value: IN, index: string | symbol) => undefined | OUT,
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, IN_V>> {
2452
- const unproxiedOut = {} as Record<OUT_K, Record<IN_K, IN_V>>;
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, IN_V>;
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
- }