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.
- package/README.md +24 -12
- package/README.md.bak +212 -0
- package/dist/aberdeen.d.ts +48 -31
- package/dist/aberdeen.js +142 -160
- package/dist/aberdeen.js.map +3 -3
- package/dist-min/aberdeen.js +5 -5
- package/dist-min/aberdeen.js.map +3 -3
- package/html-to-aberdeen +397 -0
- package/package.json +1 -1
- package/src/aberdeen.ts +197 -207
package/html-to-aberdeen
ADDED
|
@@ -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
|
+
}
|