aberdeen 1.0.6 → 1.0.8

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
@@ -1,4 +1,5 @@
1
1
  import { ReverseSortedSet } from "./helpers/reverseSortedSet.js";
2
+ import type { ReverseSortedSetPointer } from "./helpers/reverseSortedSet.js";
2
3
 
3
4
  /*
4
5
  * QueueRunner
@@ -9,9 +10,11 @@ import { ReverseSortedSet } from "./helpers/reverseSortedSet.js";
9
10
  interface QueueRunner {
10
11
  prio: number; // Higher values have higher priority
11
12
  queueRun(): void;
13
+
14
+ [ptr: ReverseSortedSetPointer]: QueueRunner;
12
15
  }
13
16
 
14
- let sortedQueue: ReverseSortedSet<QueueRunner> | undefined; // When set, a runQueue is scheduled or currently running.
17
+ let sortedQueue: ReverseSortedSet<QueueRunner, "prio"> | undefined; // When set, a runQueue is scheduled or currently running.
15
18
  let runQueueDepth = 0; // Incremented when a queue event causes another queue event to be added. Reset when queue is empty. Throw when >= 42 to break (infinite) recursion.
16
19
  let topRedrawScope: Scope | undefined; // The scope that triggered the current redraw. Elements drawn at this scope level may trigger 'create' animations.
17
20
 
@@ -28,7 +31,7 @@ export type DatumType =
28
31
 
29
32
  function queue(runner: QueueRunner) {
30
33
  if (!sortedQueue) {
31
- sortedQueue = new ReverseSortedSet<QueueRunner>("prio");
34
+ sortedQueue = new ReverseSortedSet<QueueRunner, "prio">("prio");
32
35
  setTimeout(runQueue, 0);
33
36
  } else if (!(runQueueDepth & 1)) {
34
37
  runQueueDepth++; // Make it uneven
@@ -81,7 +84,7 @@ export function runQueue(): void {
81
84
  sortedQueue = undefined;
82
85
  runQueueDepth = 0;
83
86
  time = Date.now() - time;
84
- if (time > 1) console.debug(`Aberdeen queue took ${time}ms`);
87
+ if (time > 9) console.debug(`Aberdeen queue took ${time}ms`);
85
88
  }
86
89
 
87
90
  /**
@@ -168,6 +171,8 @@ abstract class Scope implements QueueRunner {
168
171
  // order of the source code.
169
172
  prio: number = --lastPrio;
170
173
 
174
+ [ptr: ReverseSortedSetPointer]: this;
175
+
171
176
  abstract onChange(index: any, newData: DatumType, oldData: DatumType): void;
172
177
  abstract queueRun(): void;
173
178
 
@@ -198,6 +203,9 @@ abstract class ContentScope extends Scope {
198
203
  // be for child scopes, subscriptions as well as `clean(..)` hooks.
199
204
  cleaners: Array<{ delete: (scope: Scope) => void } | (() => void)>;
200
205
 
206
+ // Whether this scope is within an SVG namespace context
207
+ inSvgNamespace: boolean = false;
208
+
201
209
  constructor(
202
210
  cleaners: Array<{ delete: (scope: Scope) => void } | (() => void)> = [],
203
211
  ) {
@@ -264,6 +272,10 @@ class ChainedScope extends ContentScope {
264
272
  useParentCleaners = false,
265
273
  ) {
266
274
  super(useParentCleaners ? currentScope.cleaners : []);
275
+
276
+ // Inherit SVG namespace state from current scope
277
+ this.inSvgNamespace = currentScope.inSvgNamespace;
278
+
267
279
  if (parentElement === currentScope.parentElement) {
268
280
  // If `currentScope` is not actually a ChainedScope, prevSibling will be undefined, as intended
269
281
  this.prevSibling = currentScope.getChildPrevSibling();
@@ -334,6 +346,10 @@ class MountScope extends ContentScope {
334
346
  public renderer: () => any,
335
347
  ) {
336
348
  super();
349
+
350
+ // Inherit SVG namespace state from current scope
351
+ this.inSvgNamespace = currentScope.inSvgNamespace;
352
+
337
353
  this.redraw();
338
354
  currentScope.cleaners.push(this);
339
355
  }
@@ -442,7 +458,9 @@ class SetArgScope extends ChainedScope {
442
458
  }
443
459
  }
444
460
 
445
- let immediateQueue: ReverseSortedSet<Scope> = new ReverseSortedSet("prio");
461
+ let immediateQueue: ReverseSortedSet<Scope, "prio"> = new ReverseSortedSet(
462
+ "prio",
463
+ );
446
464
 
447
465
  class ImmediateScope extends RegularScope {
448
466
  onChange(index: any, newData: DatumType, oldData: DatumType) {
@@ -491,9 +509,8 @@ class OnEachScope extends Scope {
491
509
  byIndex: Map<any, OnEachItemScope> = new Map();
492
510
 
493
511
  /** The reverse-ordered list of item scopes, not including those for which makeSortKey returned undefined. */
494
- sortedSet: ReverseSortedSet<OnEachItemScope> = new ReverseSortedSet(
495
- "sortKey",
496
- );
512
+ sortedSet: ReverseSortedSet<OnEachItemScope, "sortKey"> =
513
+ new ReverseSortedSet("sortKey");
497
514
 
498
515
  /** Indexes that have been created/removed and need to be handled in the next `queueRun`. */
499
516
  changedIndexes: Set<any> = new Set();
@@ -563,6 +580,8 @@ class OnEachScope extends Scope {
563
580
  scope.delete();
564
581
  }
565
582
 
583
+ sortedQueue?.remove(this); // This is very fast and O(1) when not queued
584
+
566
585
  // Help garbage collection:
567
586
  this.byIndex.clear();
568
587
  setTimeout(() => {
@@ -592,6 +611,9 @@ class OnEachItemScope extends ContentScope {
592
611
  ) {
593
612
  super();
594
613
  this.parentElement = parent.parentElement;
614
+
615
+ // Inherit SVG namespace state from current scope
616
+ this.inSvgNamespace = currentScope.inSvgNamespace;
595
617
 
596
618
  this.parent.byIndex.set(this.itemIndex, this);
597
619
 
@@ -1697,7 +1719,14 @@ export function $(
1697
1719
  err = `Tag '${arg}' cannot contain space`;
1698
1720
  break;
1699
1721
  } else {
1700
- result = document.createElement(arg);
1722
+ // Determine which namespace to use for element creation
1723
+ const useNamespace = currentScope.inSvgNamespace || arg === 'svg';
1724
+ if (useNamespace) {
1725
+ result = document.createElementNS('http://www.w3.org/2000/svg', arg);
1726
+ } else {
1727
+ result = document.createElement(arg);
1728
+ }
1729
+
1701
1730
  if (classes) result.className = classes.replaceAll(".", " ");
1702
1731
  if (text) result.textContent = text;
1703
1732
  addNode(result);
@@ -1705,6 +1734,12 @@ export function $(
1705
1734
  savedCurrentScope = currentScope;
1706
1735
  }
1707
1736
  const newScope = new ChainedScope(result, true);
1737
+
1738
+ // If we're creating an SVG element, set the SVG namespace flag for child scopes
1739
+ if (arg === 'svg') {
1740
+ newScope.inSvgNamespace = true;
1741
+ }
1742
+
1708
1743
  newScope.lastChild = result.lastChild || undefined;
1709
1744
  if (topRedrawScope === currentScope) topRedrawScope = newScope;
1710
1745
  currentScope = newScope;
@@ -1,4 +1,9 @@
1
- type Item<T> = T & { [idx: symbol]: Item<T> };
1
+ // Meta-data is saved in-place on ReverseSorted items.
2
+ // - Items in the set should indicate they support {[ptr: ReverseSortedSetPointer]: SELF}
3
+ export type ReverseSortedSetPointer = symbol;
4
+
5
+ // ReverseSortedSet saves the skip links for all required levels on the object itself
6
+ type SkipItem<T> = { [idx: ReverseSortedSetPointer]: T };
2
7
 
3
8
  /**
4
9
  * A set-like collection of objects that can do iteration sorted by a specified index property.
@@ -8,11 +13,11 @@ type Item<T> = T & { [idx: symbol]: Item<T> };
8
13
  * It's implemented as a skiplist, maintaining all meta-data as part of the objects that it
9
14
  * is tracking, for performance.
10
15
  */
11
- export class ReverseSortedSet<T extends object> {
16
+ export class ReverseSortedSet<T extends SkipItem<T>, KeyPropT extends keyof T> {
12
17
  // A fake item, that is not actually T, but *does* contain symbols pointing at the first item for each level.
13
- private tail: Item<T>;
18
+ private tail: SkipItem<T>;
14
19
  // As every SkipList instance has its own symbols, an object can be included in more than one SkipList.
15
- private symbols: symbol[];
20
+ private symbols: ReverseSortedSetPointer[];
16
21
 
17
22
  /**
18
23
  * Create an empty SortedSet.
@@ -21,8 +26,8 @@ export class ReverseSortedSet<T extends object> {
21
26
  * using `<` will be done on this property, so it should probably be a number or a string (or something that
22
27
  * has a useful toString-conversion).
23
28
  */
24
- constructor(private keyProp: keyof T) {
25
- this.tail = {} as Item<T>;
29
+ constructor(private keyProp: KeyPropT) {
30
+ this.tail = {} as SkipItem<T>;
26
31
  this.symbols = [Symbol(0)];
27
32
  }
28
33
 
@@ -54,15 +59,15 @@ export class ReverseSortedSet<T extends object> {
54
59
  const keyProp = this.keyProp;
55
60
  const key = item[keyProp];
56
61
 
57
- let prev: Item<T> | undefined;
58
- let current: Item<T> = this.tail;
62
+ // prev is always a complete T, current might be tail only contain pointers
63
+ let prev: T | undefined;
64
+ let current: SkipItem<T> = this.tail;
59
65
  for (let l = this.symbols.length - 1; l >= 0; l--) {
60
66
  const symbol = this.symbols[l];
61
- while ((prev = current[symbol] as Item<T>) && prev[keyProp] > key)
62
- current = prev;
67
+ while ((prev = current[symbol]) && prev[keyProp] > key) current = prev;
63
68
  if (l < level) {
64
- (item as any)[symbol] = current[symbol];
65
- (current as any)[symbol] = item;
69
+ (item as SkipItem<T>)[symbol] = current[symbol];
70
+ current[symbol] = item;
66
71
  }
67
72
  }
68
73
 
@@ -106,13 +111,15 @@ export class ReverseSortedSet<T extends object> {
106
111
  *
107
112
  * Time complexity: O(log n)
108
113
  */
109
- get(indexValue: string | number): T | undefined {
114
+ get(indexValue: T[KeyPropT]): T | undefined {
110
115
  const keyProp = this.keyProp;
111
- let current = this.tail;
112
- let prev: Item<T> | undefined;
116
+
117
+ // prev is always a complete T, current might be tail only contain pointers
118
+ let prev: T | undefined;
119
+ let current: SkipItem<T> = this.tail;
113
120
  for (let l = this.symbols.length - 1; l >= 0; l--) {
114
121
  const symbol = this.symbols[l];
115
- while ((prev = current[symbol] as Item<T>) && prev[keyProp] > indexValue)
122
+ while ((prev = current[symbol]) && prev[keyProp] > indexValue)
116
123
  current = prev;
117
124
  }
118
125
  return current[this.symbols[0]]?.[keyProp] === indexValue
@@ -125,10 +132,10 @@ export class ReverseSortedSet<T extends object> {
125
132
  */
126
133
  *[Symbol.iterator](): IterableIterator<T> {
127
134
  const symbol = this.symbols[0];
128
- let node: Item<T> | undefined = this.tail[symbol] as Item<T>;
135
+ let node = this.tail[symbol];
129
136
  while (node) {
130
137
  yield node;
131
- node = node[symbol] as Item<T> | undefined;
138
+ node = node[symbol];
132
139
  }
133
140
  }
134
141
 
@@ -140,7 +147,7 @@ export class ReverseSortedSet<T extends object> {
140
147
  * Time complexity: O(1)
141
148
  */
142
149
  prev(item: T): T | undefined {
143
- return (item as Item<T>)[this.symbols[0]];
150
+ return item[this.symbols[0]];
144
151
  }
145
152
 
146
153
  /**
@@ -156,19 +163,15 @@ export class ReverseSortedSet<T extends object> {
156
163
  const keyProp = this.keyProp;
157
164
  const prop = item[keyProp];
158
165
 
159
- let prev: Item<T> | undefined;
160
- let current: Item<T> = this.tail;
161
-
166
+ // prev is always a complete T, current might be tail only contain pointers
167
+ let prev: T | undefined;
168
+ let current: SkipItem<T> = this.tail;
162
169
  for (let l = this.symbols.length - 1; l >= 0; l--) {
163
170
  const symbol = this.symbols[l];
164
- while (
165
- (prev = current[symbol] as Item<T>) &&
166
- prev[keyProp] >= prop &&
167
- prev !== item
168
- )
171
+ while ((prev = current[symbol]) && prev[keyProp] >= prop && prev !== item)
169
172
  current = prev;
170
173
  if (prev === item) {
171
- (current as any)[symbol] = prev[symbol];
174
+ current[symbol] = prev[symbol];
172
175
  delete prev[symbol];
173
176
  }
174
177
  }
@@ -183,15 +186,15 @@ export class ReverseSortedSet<T extends object> {
183
186
  */
184
187
  clear(): void {
185
188
  const symbol = this.symbols[0];
186
- let current: Item<T> | undefined = this.tail;
189
+ let current = this.tail;
187
190
  while (current) {
188
- const prev = current[symbol] as Item<T> | undefined;
191
+ const prev = current[symbol];
189
192
  for (const symbol of this.symbols) {
190
193
  if (!(symbol in current)) break;
191
194
  delete current[symbol];
192
195
  }
193
196
  current = prev;
194
197
  }
195
- this.tail = {} as Item<T>;
198
+ this.tail = {};
196
199
  }
197
200
  }
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
- }