aberdeen 1.0.8 → 1.0.10

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.
@@ -0,0 +1,354 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aberdeen",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "author": "Frank van Viegen",
5
5
  "main": "dist-min/aberdeen.js",
6
6
  "devDependencies": {
package/src/aberdeen.ts CHANGED
@@ -114,9 +114,9 @@ function partToStr(part: number | string): string {
114
114
  * 1 - separator between string array items
115
115
  * 65535 - for compatibility
116
116
  */
117
- result += String.fromCharCode(
117
+ result = String.fromCharCode(
118
118
  negative ? 65534 - (num % 65533) : 2 + (num % 65533),
119
- );
119
+ ) + result;
120
120
  num = Math.floor(num / 65533);
121
121
  }
122
122
  // Prefix the number of digits, counting down from 128 for negative and up for positive
@@ -823,7 +823,7 @@ function subscribe(
823
823
  }
824
824
 
825
825
  export function onEach<T>(
826
- target: Array<undefined | T>,
826
+ target: ReadonlyArray<undefined | T>,
827
827
  render: (value: T, index: number) => void,
828
828
  makeKey?: (value: T, key: any) => SortKeyType,
829
829
  ): void;
@@ -1584,9 +1584,9 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1584
1584
  addNode(document.createTextNode(value));
1585
1585
  },
1586
1586
  element: (value: any) => {
1587
- if (!(value instanceof Node))
1588
- throw new Error(`Unexpected element-argument: ${JSON.parse(value)}`);
1587
+ console.log("Aberdeen: $({element: myElement}) is deprecated, use $(myElement) instead");
1589
1588
  addNode(value);
1589
+ SPECIAL_PROPS.element = addNode; // Avoid the console.log next time
1590
1590
  },
1591
1591
  };
1592
1592
 
@@ -1623,7 +1623,7 @@ const SPECIAL_PROPS: { [key: string]: (value: any) => void } = {
1623
1623
  * This is often used together with {@link ref}, in order to use properties other than `.value`.
1624
1624
  * - `{text: string|number}`: Add the value as a `TextNode` to the *current* element.
1625
1625
  * - `{html: string}`: Add the value as HTML to the *current* element. This should only be used in exceptional situations. And of course, beware of XSS.
1626
- * - `{element: Node}`: Add a pre-existing HTML `Node` to the *current* element.
1626
+ - `Node`: If a DOM Node (Element or TextNode) is passed in, it is added as a child to the *current* element. If the Node is an Element, it becomes the new *current* element for the rest of this `$` function execution.
1627
1627
  *
1628
1628
  * @returns The most inner DOM element that was created (not counting text nodes nor elements created by content functions),
1629
1629
  * or undefined if no elements were created.
@@ -1746,12 +1746,23 @@ export function $(
1746
1746
  }
1747
1747
  } else if (typeof arg === "object") {
1748
1748
  if (arg.constructor !== Object) {
1749
- err = `Unexpected argument: ${arg}`;
1750
- break;
1751
- }
1752
- for (const key in arg) {
1753
- const val = arg[key];
1754
- applyArg(key, val);
1749
+ if (arg instanceof Node) {
1750
+ addNode(arg);
1751
+ if (arg instanceof Element) {
1752
+ // If it's an Element, it may contain children, so we make it the current scope
1753
+ if (!savedCurrentScope) savedCurrentScope = currentScope;
1754
+ currentScope = new ChainedScope(arg, true);
1755
+ currentScope.lastChild = arg.lastChild || undefined;
1756
+ }
1757
+ } else {
1758
+ err = `Unexpected argument: ${arg}`;
1759
+ break;
1760
+ }
1761
+ } else {
1762
+ for (const key in arg) {
1763
+ const val = arg[key];
1764
+ applyArg(key, val);
1765
+ }
1755
1766
  }
1756
1767
  } else if (typeof arg === "function") {
1757
1768
  new RegularScope(currentScope.parentElement, arg);