aberdeen 1.2.0 → 1.3.1

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,397 @@
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 pos = 0;
79
+
80
+ while (pos < attributesStr.length) {
81
+ // Skip whitespace
82
+ while (pos < attributesStr.length && /\s/.test(attributesStr[pos])) {
83
+ pos++;
84
+ }
85
+ if (pos >= attributesStr.length) break;
86
+
87
+ // Get attribute name
88
+ const nameMatch = attributesStr.substring(pos).match(/^([\w-]+)/);
89
+ if (!nameMatch) break;
90
+
91
+ const name = nameMatch[1];
92
+ pos += name.length;
93
+
94
+ // Skip whitespace
95
+ while (pos < attributesStr.length && /\s/.test(attributesStr[pos])) {
96
+ pos++;
97
+ }
98
+
99
+ // Check for '='
100
+ if (pos < attributesStr.length && attributesStr[pos] === '=') {
101
+ pos++;
102
+
103
+ // Skip whitespace
104
+ while (pos < attributesStr.length && /\s/.test(attributesStr[pos])) {
105
+ pos++;
106
+ }
107
+
108
+ let value = '';
109
+
110
+ if (pos < attributesStr.length) {
111
+ const quoteChar = attributesStr[pos];
112
+
113
+ if (quoteChar === '"' || quoteChar === "'") {
114
+ // Quoted value - handle escaped quotes
115
+ pos++;
116
+ while (pos < attributesStr.length) {
117
+ if (attributesStr[pos] === '\\' && pos + 1 < attributesStr.length) {
118
+ // Escaped character
119
+ value += attributesStr[pos + 1];
120
+ pos += 2;
121
+ } else if (attributesStr[pos] === quoteChar) {
122
+ // End quote
123
+ pos++;
124
+ break;
125
+ } else {
126
+ value += attributesStr[pos];
127
+ pos++;
128
+ }
129
+ }
130
+ } else {
131
+ // Unquoted value
132
+ const unquotedMatch = attributesStr.substring(pos).match(/^(\S+)/);
133
+ if (unquotedMatch) {
134
+ value = unquotedMatch[1];
135
+ pos += value.length;
136
+ }
137
+ }
138
+ }
139
+
140
+ attributes.push({ name, value });
141
+ } else {
142
+ // Boolean attribute
143
+ attributes.push({ name, value: '' });
144
+ }
145
+ }
146
+
147
+ const newElement = {
148
+ type: 'element',
149
+ tagName,
150
+ attributes,
151
+ children: []
152
+ };
153
+
154
+ // Add to current parent
155
+ if (currentParent === result) {
156
+ currentParent.body = currentParent.body || [];
157
+ currentParent.body.push(newElement);
158
+ } else {
159
+ currentParent.children = currentParent.children || [];
160
+ currentParent.children.push(newElement);
161
+ }
162
+
163
+ if (!selfClosing && !['br', 'hr', 'img', 'input', 'link', 'meta'].includes(tagName)) {
164
+ stack.push(currentParent);
165
+ currentParent = newElement;
166
+ }
167
+
168
+ currentPosition = endTag + 1;
169
+ continue;
170
+ }
171
+
172
+ // It's text content
173
+ let endText = html.indexOf('<', currentPosition);
174
+ if (endText === -1) endText = html.length;
175
+
176
+ const textContent = html.substring(currentPosition, endText);
177
+ if (textContent.trim()) {
178
+ if (currentParent === result) {
179
+ currentParent.body = currentParent.body || [];
180
+ currentParent.body.push({
181
+ type: 'text',
182
+ content: textContent
183
+ });
184
+ } else {
185
+ currentParent.children = currentParent.children || [];
186
+ currentParent.children.push({
187
+ type: 'text',
188
+ content: textContent
189
+ });
190
+ }
191
+ }
192
+
193
+ currentPosition = endText;
194
+ }
195
+
196
+ return result;
197
+ }
198
+
199
+ // Read from stdin
200
+ let html = '';
201
+ process.stdin.setEncoding('utf8');
202
+ process.stdin.on('data', (chunk) => {
203
+ html += chunk;
204
+ });
205
+ process.stdin.on('end', () => {
206
+ // Convert HTML to Aberdeen code
207
+ const aberdeenCode = convertHTMLToAberdeen(html);
208
+
209
+ // Output to stdout
210
+ process.stdout.write(aberdeenCode);
211
+ });
212
+
213
+ // Main conversion function
214
+ function convertHTMLToAberdeen(html) {
215
+ // Parse HTML into a simple AST
216
+ const ast = parseHTML(html);
217
+
218
+ // Generate the Aberdeen code
219
+ let aberdeenCode = ``;
220
+
221
+ // Process the body's children
222
+ for (const node of ast.body) {
223
+ aberdeenCode += processNode(node);
224
+ }
225
+
226
+ return aberdeenCode;
227
+ }
228
+
229
+ // Process a node and return Aberdeen code
230
+ function processNode(node, indentLevel = 0) {
231
+ const indent = ' '.repeat(indentLevel);
232
+
233
+ // Handle text nodes
234
+ if (node.type === 'text') {
235
+ const text = node.content.trim();
236
+ return text ? `${indent}$(':${escapeString(text)}');\n` : ``;
237
+ }
238
+
239
+ // Handle comments
240
+ if (node.type === 'comment') {
241
+ return `${indent}// ${node.content.trim()}\n`;
242
+ }
243
+
244
+ // Handle elements
245
+ if (node.type === 'element') {
246
+ return processElement(node, indentLevel);
247
+ }
248
+
249
+ return '';
250
+ }
251
+
252
+ // Process an element node and build the chain of single children
253
+ function processElement(node, indentLevel) {
254
+ const indent = ' '.repeat(indentLevel);
255
+ const chain = getSingleChildChain(node);
256
+
257
+ // Build tag string for each element in the chain
258
+ let result = `${indent}$('`;
259
+ const allSeparateArgs = [];
260
+
261
+ for (let i = 0; i < chain.length; i++) {
262
+ const element = chain[i];
263
+
264
+ // Add space separator between chained elements
265
+ if (i > 0) result += ' ';
266
+
267
+ // Build tag name with classes
268
+ const tagName = element.tagName.toLowerCase();
269
+ const classAttr = element.attributes.find(attr => attr.name === 'class');
270
+ const classes = classAttr
271
+ ? classAttr.value.split(/\s+/).filter(Boolean).map(c => `.${c}`).join('')
272
+ : '';
273
+
274
+ result += tagName + classes;
275
+
276
+ // Add attributes (excluding class)
277
+ const attributes = element.attributes.filter(attr => attr.name !== 'class');
278
+ const { attrString, separateArgs } = buildAttributeString(attributes);
279
+ result += attrString;
280
+
281
+ // Check if this element has only text content
282
+ const hasOnlyText = element.children.length === 1
283
+ && element.children[0].type === 'text'
284
+ && element.children[0].content.trim();
285
+
286
+ if (hasOnlyText) {
287
+ const textContent = element.children[0].content.trim();
288
+ if (i === chain.length - 1) {
289
+ // If it's the last element in the chain and there are no other children,
290
+ // use the ':' syntax for text
291
+ result += ':' + escapeString(textContent);
292
+ } else {
293
+ // Treat text like any other attribute
294
+ const textAttr = { name: 'text', value: textContent };
295
+ const { attrString: textAttrString, separateArgs: textSeparateArgs } = buildAttributeString([textAttr]);
296
+ result += textAttrString;
297
+ separateArgs.push(...textSeparateArgs);
298
+ }
299
+ }
300
+
301
+ allSeparateArgs.push(...separateArgs);
302
+ }
303
+
304
+ result += `'`;
305
+
306
+ // Add all separate arguments
307
+ for (const value of allSeparateArgs) {
308
+ result += `, '${escapeString(value)}'`;
309
+ }
310
+
311
+ // Check if the last element in the chain has multiple children
312
+ const lastElement = chain[chain.length - 1];
313
+ const children = lastElement.children.filter(child =>
314
+ child.type === 'element' || (child.type === 'text' && child.content.trim())
315
+ );
316
+
317
+ // Exclude the case where there's only text (already handled above)
318
+ const hasOnlyText = children.length === 1 && children[0].type === 'text';
319
+
320
+ if (children.length > 0 && !hasOnlyText) {
321
+ // Add children as callback function
322
+ result += `, () => {\n`;
323
+ for (const child of children) {
324
+ result += processNode(child, indentLevel + 1);
325
+ }
326
+ result += `${indent}}`;
327
+ }
328
+
329
+ result += `);\n`;
330
+ return result;
331
+ }
332
+
333
+ // Get a chain of nodes where each node has exactly one element child
334
+ function getSingleChildChain(node) {
335
+ const chain = [node];
336
+ let current = node;
337
+
338
+ while (true) {
339
+ const elementChildren = current.children.filter(child => child.type === 'element');
340
+
341
+ if (elementChildren.length === 1) {
342
+ current = elementChildren[0];
343
+ chain.push(current);
344
+ } else {
345
+ break;
346
+ }
347
+ }
348
+
349
+ return chain;
350
+ }
351
+
352
+ // Escape special characters for single-quoted strings
353
+ function escapeString(str) {
354
+ return str
355
+ .replace(/\\/g, '\\\\')
356
+ .replace(/'/g, "\\'")
357
+ .replace(/\n/g, '\\n')
358
+ .replace(/\r/g, '\\r')
359
+ .replace(/\t/g, '\\t');
360
+ }
361
+
362
+ // Escape special characters for double-quoted strings within tag strings
363
+ function escapeDoubleQuoted(str) {
364
+ return str
365
+ .replace(/\\/g, '\\\\')
366
+ .replace(/"/g, '\\"')
367
+ .replace(/\n/g, '\\n')
368
+ .replace(/\r/g, '\\r')
369
+ .replace(/\t/g, '\\t');
370
+ }
371
+
372
+ // Build attribute string and collect values that need separate arguments
373
+ function buildAttributeString(attributes) {
374
+ let attrString = '';
375
+ const separateArgs = [];
376
+
377
+ for (const attr of attributes) {
378
+ const value = attr.value;
379
+
380
+ if (value === '') {
381
+ // Boolean attribute
382
+ attrString += ` ${attr.name}`;
383
+ } else if (value.includes('"')) {
384
+ // Contains double quotes - must use separate argument
385
+ attrString += ` ${attr.name}=`;
386
+ separateArgs.push(value);
387
+ } else if (value.includes(' ') || value.includes('\n') || value.includes('\r') || value.includes('\t')) {
388
+ // Contains spaces/whitespace - use double quotes with escaping
389
+ attrString += ` ${attr.name}="${escapeDoubleQuoted(value)}"`;
390
+ } else {
391
+ // Simple value - no quotes needed
392
+ attrString += ` ${attr.name}=${value}`;
393
+ }
394
+ }
395
+
396
+ return { attrString, separateArgs };
397
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aberdeen",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "author": "Frank van Viegen",
5
5
  "main": "dist-min/aberdeen.js",
6
6
  "devDependencies": {