@opentui/solid 0.1.8 → 0.1.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.
- package/README.md +1 -1
- package/index.js +111 -91
- package/package.json +2 -2
- package/scripts/solid-plugin.ts +9 -18
- package/src/elements/index.d.ts +3 -3
- package/src/elements/text-node.d.ts +47 -0
- package/src/reconciler.d.ts +3 -10
- package/src/reconciler.js +113 -93
- package/src/utils/log.d.ts +1 -0
package/README.md
CHANGED
package/index.js
CHANGED
|
@@ -108,16 +108,19 @@ var elements = {
|
|
|
108
108
|
import {
|
|
109
109
|
InputRenderable as InputRenderable2,
|
|
110
110
|
InputRenderableEvents,
|
|
111
|
-
Renderable,
|
|
111
|
+
Renderable as Renderable2,
|
|
112
112
|
SelectRenderable as SelectRenderable2,
|
|
113
113
|
SelectRenderableEvents,
|
|
114
114
|
StyledText,
|
|
115
115
|
TabSelectRenderable as TabSelectRenderable2,
|
|
116
116
|
TabSelectRenderableEvents,
|
|
117
|
-
TextRenderable as
|
|
117
|
+
TextRenderable as TextRenderable3
|
|
118
118
|
} from "@opentui/core";
|
|
119
119
|
import { createRenderer } from "solid-js/universal";
|
|
120
120
|
|
|
121
|
+
// src/elements/text-node.ts
|
|
122
|
+
import { Renderable, TextRenderable as TextRenderable2 } from "@opentui/core";
|
|
123
|
+
|
|
121
124
|
// src/utils/id-counter.ts
|
|
122
125
|
var idCounter = new Map;
|
|
123
126
|
function getNextId(elementType) {
|
|
@@ -129,8 +132,16 @@ function getNextId(elementType) {
|
|
|
129
132
|
return `${elementType}-${value}`;
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
// src/
|
|
135
|
+
// src/utils/log.ts
|
|
136
|
+
var log = (...args) => {
|
|
137
|
+
if (process.env.DEBUG) {
|
|
138
|
+
console.log("[Reconciler]", ...args);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/elements/text-node.ts
|
|
133
143
|
var GHOST_NODE_TAG = "text-ghost";
|
|
144
|
+
var ChunkToTextNodeMap = new WeakMap;
|
|
134
145
|
|
|
135
146
|
class TextNode {
|
|
136
147
|
id;
|
|
@@ -140,91 +151,112 @@ class TextNode {
|
|
|
140
151
|
constructor(chunk) {
|
|
141
152
|
this.id = getNextId("text-node");
|
|
142
153
|
this.chunk = chunk;
|
|
154
|
+
ChunkToTextNodeMap.set(chunk, this);
|
|
143
155
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
function getOrCreateTextGhostNode(parent, anchor) {
|
|
150
|
-
if (anchor instanceof TextNode && anchor.textParent) {
|
|
151
|
-
return anchor.textParent;
|
|
152
|
-
}
|
|
153
|
-
const children = parent.getChildren();
|
|
154
|
-
if (anchor instanceof Renderable) {
|
|
155
|
-
const anchorIndex = children.findIndex((el) => el.id === anchor.id);
|
|
156
|
-
const beforeAnchor = children[anchorIndex - 1];
|
|
157
|
-
if (beforeAnchor instanceof TextRenderable2 && beforeAnchor.id.startsWith(GHOST_NODE_TAG)) {
|
|
158
|
-
return beforeAnchor;
|
|
156
|
+
replaceText(newChunk) {
|
|
157
|
+
const textParent = this.textParent;
|
|
158
|
+
if (!textParent) {
|
|
159
|
+
log("No parent found for text node:", this.id);
|
|
160
|
+
return;
|
|
159
161
|
}
|
|
162
|
+
textParent.content = textParent.content.replace(newChunk, this.chunk);
|
|
163
|
+
this.chunk = newChunk;
|
|
164
|
+
ChunkToTextNodeMap.set(newChunk, this);
|
|
160
165
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return lastChild;
|
|
164
|
-
}
|
|
165
|
-
const ghostNode = new TextRenderable2(getNextId(GHOST_NODE_TAG), {});
|
|
166
|
-
_insertNode(parent, ghostNode, anchor);
|
|
167
|
-
return ghostNode;
|
|
168
|
-
}
|
|
169
|
-
function insertTextNode(parent, node, anchor) {
|
|
170
|
-
if (!(parent instanceof Renderable)) {
|
|
171
|
-
console.warn("Attaching text node to parent text node, impossible");
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
log("Inserting text node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
|
|
175
|
-
let textParent;
|
|
176
|
-
if (!(parent instanceof TextRenderable2)) {
|
|
177
|
-
textParent = getOrCreateTextGhostNode(parent, anchor);
|
|
178
|
-
} else {
|
|
179
|
-
textParent = parent;
|
|
166
|
+
static getTextNodeFromChunk(chunk) {
|
|
167
|
+
return ChunkToTextNodeMap.get(chunk);
|
|
180
168
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
|
|
185
|
-
if (anchorIndex == -1) {
|
|
186
|
-
console.log("anchor not found");
|
|
169
|
+
insert(parent, anchor) {
|
|
170
|
+
if (!(parent instanceof Renderable)) {
|
|
171
|
+
log("Attaching text node to parent text node, impossible");
|
|
187
172
|
return;
|
|
188
173
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
|
|
193
|
-
styledText = styledText.replace(node.chunk, firstChunk);
|
|
174
|
+
let textParent;
|
|
175
|
+
if (!(parent instanceof TextRenderable2)) {
|
|
176
|
+
textParent = this.getOrCreateTextGhostNode(parent, anchor);
|
|
194
177
|
} else {
|
|
195
|
-
|
|
178
|
+
textParent = parent;
|
|
196
179
|
}
|
|
180
|
+
this.textParent = textParent;
|
|
181
|
+
let styledText = textParent.content;
|
|
182
|
+
if (anchor && anchor instanceof TextNode) {
|
|
183
|
+
const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
|
|
184
|
+
if (anchorIndex === -1) {
|
|
185
|
+
log("anchor not found");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
styledText = styledText.insert(this.chunk, anchorIndex);
|
|
189
|
+
} else {
|
|
190
|
+
const firstChunk = textParent.content.chunks[0];
|
|
191
|
+
if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
|
|
192
|
+
styledText = styledText.replace(this.chunk, firstChunk);
|
|
193
|
+
} else {
|
|
194
|
+
styledText = styledText.insert(this.chunk);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
textParent.content = styledText;
|
|
198
|
+
this.parent = parent;
|
|
197
199
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
200
|
+
remove(parent) {
|
|
201
|
+
if (!(parent instanceof Renderable)) {
|
|
202
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (parent === this.textParent && parent instanceof TextRenderable2) {
|
|
206
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
207
|
+
parent.content = parent.content.remove(this.chunk);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (this.textParent) {
|
|
211
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
212
|
+
let styledText = this.textParent.content;
|
|
213
|
+
styledText = styledText.remove(this.chunk);
|
|
214
|
+
if (styledText.chunks.length > 0) {
|
|
215
|
+
this.textParent.content = styledText;
|
|
216
|
+
} else {
|
|
217
|
+
this.parent?.remove(this.textParent.id);
|
|
218
|
+
this.textParent.destroyRecursively();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
206
221
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
222
|
+
getOrCreateTextGhostNode(parent, anchor) {
|
|
223
|
+
if (anchor instanceof TextNode && anchor.textParent) {
|
|
224
|
+
return anchor.textParent;
|
|
225
|
+
}
|
|
226
|
+
const children = parent.getChildren();
|
|
227
|
+
if (anchor instanceof Renderable) {
|
|
228
|
+
const anchorIndex = children.findIndex((el) => el.id === anchor.id);
|
|
229
|
+
const beforeAnchor = children[anchorIndex - 1];
|
|
230
|
+
if (beforeAnchor instanceof GhostTextRenderable) {
|
|
231
|
+
return beforeAnchor;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const lastChild = children.at(-1);
|
|
235
|
+
if (lastChild instanceof GhostTextRenderable) {
|
|
236
|
+
return lastChild;
|
|
219
237
|
}
|
|
238
|
+
const ghostNode = new GhostTextRenderable(getNextId(GHOST_NODE_TAG), {});
|
|
239
|
+
insertNode(parent, ghostNode, anchor);
|
|
240
|
+
return ghostNode;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
class GhostTextRenderable extends TextRenderable2 {
|
|
245
|
+
constructor(id, options) {
|
|
246
|
+
super(id, options);
|
|
247
|
+
}
|
|
248
|
+
static isGhostNode(node) {
|
|
249
|
+
return node instanceof GhostTextRenderable;
|
|
220
250
|
}
|
|
221
251
|
}
|
|
252
|
+
|
|
253
|
+
// src/reconciler.ts
|
|
222
254
|
function _insertNode(parent, node, anchor) {
|
|
223
255
|
log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
|
|
224
256
|
if (node instanceof TextNode) {
|
|
225
|
-
return
|
|
257
|
+
return node.insert(parent, anchor);
|
|
226
258
|
}
|
|
227
|
-
if (!(parent instanceof
|
|
259
|
+
if (!(parent instanceof Renderable2)) {
|
|
228
260
|
return;
|
|
229
261
|
}
|
|
230
262
|
if (anchor) {
|
|
@@ -242,9 +274,9 @@ function _insertNode(parent, node, anchor) {
|
|
|
242
274
|
function _removeNode(parent, node) {
|
|
243
275
|
log("Removing node:", node.id, "from parent:", parent.id);
|
|
244
276
|
if (node instanceof TextNode) {
|
|
245
|
-
return
|
|
277
|
+
return node.remove(parent);
|
|
246
278
|
}
|
|
247
|
-
if (parent instanceof
|
|
279
|
+
if (parent instanceof Renderable2 && node instanceof Renderable2) {
|
|
248
280
|
parent.remove(node.id);
|
|
249
281
|
node.destroyRecursively();
|
|
250
282
|
}
|
|
@@ -278,30 +310,18 @@ var {
|
|
|
278
310
|
plainText: `${value}`
|
|
279
311
|
};
|
|
280
312
|
const textNode = new TextNode(chunk);
|
|
281
|
-
ChunkToTextNodeMap.set(chunk, textNode);
|
|
282
313
|
return textNode;
|
|
283
314
|
},
|
|
284
315
|
replaceText(textNode, value) {
|
|
285
316
|
log("Replacing text:", value, "in node:", textNode.id);
|
|
286
|
-
if (textNode instanceof
|
|
317
|
+
if (textNode instanceof Renderable2)
|
|
287
318
|
return;
|
|
288
319
|
const newChunk = {
|
|
289
320
|
__isChunk: true,
|
|
290
321
|
text: new TextEncoder().encode(value),
|
|
291
322
|
plainText: value
|
|
292
323
|
};
|
|
293
|
-
|
|
294
|
-
if (!textParent) {
|
|
295
|
-
log("No parent found for text node:", textNode.id);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
if (textParent instanceof TextRenderable2) {
|
|
299
|
-
const styledText = textParent.content;
|
|
300
|
-
styledText.replace(newChunk, textNode.chunk);
|
|
301
|
-
textParent.content = styledText;
|
|
302
|
-
textNode.chunk = newChunk;
|
|
303
|
-
ChunkToTextNodeMap.set(newChunk, textNode);
|
|
304
|
-
}
|
|
324
|
+
textNode.replaceText(newChunk);
|
|
305
325
|
},
|
|
306
326
|
setProperty(node, name, value, prev) {
|
|
307
327
|
if (node instanceof TextNode) {
|
|
@@ -414,10 +434,10 @@ var {
|
|
|
414
434
|
},
|
|
415
435
|
getFirstChild(node) {
|
|
416
436
|
log("Getting first child of node:", node.id);
|
|
417
|
-
if (node instanceof
|
|
437
|
+
if (node instanceof TextRenderable3) {
|
|
418
438
|
const chunk = node.content.chunks[0];
|
|
419
439
|
if (chunk) {
|
|
420
|
-
return
|
|
440
|
+
return TextNode.getTextNodeFromChunk(chunk);
|
|
421
441
|
} else {
|
|
422
442
|
return;
|
|
423
443
|
}
|
|
@@ -441,7 +461,7 @@ var {
|
|
|
441
461
|
return;
|
|
442
462
|
}
|
|
443
463
|
if (node instanceof TextNode) {
|
|
444
|
-
if (parent instanceof
|
|
464
|
+
if (parent instanceof TextRenderable3) {
|
|
445
465
|
const siblings2 = parent.content.chunks;
|
|
446
466
|
const index2 = siblings2.indexOf(node.chunk);
|
|
447
467
|
if (index2 === -1 || index2 === siblings2.length - 1) {
|
|
@@ -453,7 +473,7 @@ var {
|
|
|
453
473
|
log("Next sibling is null for node:", node.id);
|
|
454
474
|
return;
|
|
455
475
|
}
|
|
456
|
-
return
|
|
476
|
+
return TextNode.getTextNodeFromChunk(nextSibling2);
|
|
457
477
|
}
|
|
458
478
|
console.warn("Text parent is not a text node:", node.id);
|
|
459
479
|
return;
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"types": "index.d.ts",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"version": "0.1.
|
|
7
|
+
"version": "0.1.10",
|
|
8
8
|
"description": "SolidJS renderer for OpenTUI",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"./jsx-dev-runtime": "./jsx-runtime.d.ts"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@opentui/core": "0.1.
|
|
33
|
+
"@opentui/core": "0.1.10"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/babel__core": "7.20.5",
|
package/scripts/solid-plugin.ts
CHANGED
|
@@ -8,26 +8,17 @@ import { type BunPlugin } from "bun"
|
|
|
8
8
|
const solidTransformPlugin: BunPlugin = {
|
|
9
9
|
name: "bun-plugin-solid",
|
|
10
10
|
setup: (build) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// // plugins: ["solid-refresh/babel"],
|
|
19
|
-
// // },
|
|
20
|
-
// // },
|
|
21
|
-
// presets: [[ts, {}]],
|
|
22
|
-
// });
|
|
23
|
-
// return {
|
|
24
|
-
// contents: transforms.code,
|
|
25
|
-
// loader: "js",
|
|
26
|
-
// };
|
|
27
|
-
// });
|
|
11
|
+
build.onLoad({ filter: /\/node_modules\/solid-js\/dist\/server\.js$/ }, async (args) => {
|
|
12
|
+
const { readFile } = await import("node:fs/promises")
|
|
13
|
+
const path = args.path.replace("server.js", "solid.js")
|
|
14
|
+
const file = Bun.file(path)
|
|
15
|
+
const code = await file.text()
|
|
16
|
+
return { contents: code, loader: "js" }
|
|
17
|
+
})
|
|
28
18
|
build.onLoad({ filter: /\.(js|ts)x$/ }, async (args) => {
|
|
29
19
|
const { readFile } = await import("node:fs/promises")
|
|
30
|
-
const
|
|
20
|
+
const file = Bun.file(args.path)
|
|
21
|
+
const code = await file.text()
|
|
31
22
|
const transforms = await transformAsync(code, {
|
|
32
23
|
filename: args.path,
|
|
33
24
|
// env: {
|
package/src/elements/index.d.ts
CHANGED
|
@@ -17,12 +17,12 @@ type ElementProps<T extends RenderableOptions, K extends Renderable = Renderable
|
|
|
17
17
|
style?: Omit<T, NonStyleKeys | RenderableNonStyleKeys>;
|
|
18
18
|
ref?: Ref<K>;
|
|
19
19
|
} & T;
|
|
20
|
-
type
|
|
20
|
+
type ContainerProps = {
|
|
21
21
|
children?: JSX.Element;
|
|
22
22
|
};
|
|
23
|
-
export type BoxElementProps = ElementProps<BoxOptions, BoxRenderable, "title"> &
|
|
23
|
+
export type BoxElementProps = ElementProps<BoxOptions, BoxRenderable, "title"> & ContainerProps;
|
|
24
24
|
export type BoxStyle = BoxElementProps["style"];
|
|
25
|
-
export type GroupElementProps = ElementProps<RenderableOptions, GroupRenderable> &
|
|
25
|
+
export type GroupElementProps = ElementProps<RenderableOptions, GroupRenderable> & ContainerProps;
|
|
26
26
|
export type GroupStyle = GroupElementProps["style"];
|
|
27
27
|
export type InputElementProps = ElementProps<InputRenderableOptions, InputRenderable, "value" | "maxLength" | "placeholder"> & {
|
|
28
28
|
onInput?: (value: string) => void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Renderable, TextRenderable, type TextChunk, type TextOptions } from "@opentui/core";
|
|
2
|
+
import { type DomNode } from "../reconciler";
|
|
3
|
+
/**
|
|
4
|
+
* Represents a text node in the SolidJS reconciler.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TextNode {
|
|
7
|
+
id: string;
|
|
8
|
+
chunk: TextChunk;
|
|
9
|
+
parent?: Renderable;
|
|
10
|
+
textParent?: TextRenderable | GhostTextRenderable;
|
|
11
|
+
constructor(chunk: TextChunk);
|
|
12
|
+
/**
|
|
13
|
+
* Replaces the current text chunk with a new one.
|
|
14
|
+
* @param newChunk The new text chunk to replace with.
|
|
15
|
+
*/
|
|
16
|
+
replaceText(newChunk: TextChunk): void;
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves the TextNode associated with a given TextChunk.
|
|
19
|
+
* @param chunk The text chunk to look up.
|
|
20
|
+
* @returns The associated TextNode or undefined if not found.
|
|
21
|
+
*/
|
|
22
|
+
static getTextNodeFromChunk(chunk: TextChunk): TextNode | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Inserts this text node into the DOM structure.
|
|
25
|
+
* @param parent The parent DOM node.
|
|
26
|
+
* @param anchor The anchor node for positioning.
|
|
27
|
+
*/
|
|
28
|
+
insert(parent: DomNode, anchor?: DomNode): void;
|
|
29
|
+
/**
|
|
30
|
+
* Removes this text node from the DOM structure.
|
|
31
|
+
* @param parent The parent DOM node.
|
|
32
|
+
*/
|
|
33
|
+
remove(parent: DomNode): void;
|
|
34
|
+
/**
|
|
35
|
+
* Gets or creates a ghost text node for rendering text content.
|
|
36
|
+
* @param parent The parent renderable.
|
|
37
|
+
* @param anchor The anchor node for positioning.
|
|
38
|
+
* @returns The text renderable ghost node.
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
private getOrCreateTextGhostNode;
|
|
42
|
+
}
|
|
43
|
+
declare class GhostTextRenderable extends TextRenderable {
|
|
44
|
+
constructor(id: string, options: TextOptions);
|
|
45
|
+
static isGhostNode(node: DomNode): node is GhostTextRenderable;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
package/src/reconciler.d.ts
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import { Renderable
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
chunk: TextChunk;
|
|
5
|
-
parent?: Renderable;
|
|
6
|
-
textParent?: TextRenderable;
|
|
7
|
-
constructor(chunk: TextChunk);
|
|
8
|
-
}
|
|
9
|
-
type DomNode = Renderable | TextNode;
|
|
1
|
+
import { Renderable } from "@opentui/core";
|
|
2
|
+
import { TextNode } from "./elements/text-node";
|
|
3
|
+
export type DomNode = Renderable | TextNode;
|
|
10
4
|
export declare const _render: (code: () => DomNode, node: DomNode) => () => void, effect: <T>(fn: (prev?: T) => T, init?: T) => void, memo: <T>(fn: () => T, equal: boolean) => () => T, createComponent: <T>(Comp: (props: T) => DomNode, props: T) => DomNode, createElement: (tag: string) => DomNode, createTextNode: (value: string) => DomNode, insertNode: (parent: DomNode, node: DomNode, anchor?: DomNode | undefined) => void, solidUniversalInsert: <T>(parent: any, accessor: T | (() => T), marker?: any | null, initial?: any) => DomNode, spread: <T>(node: any, accessor: (() => T) | T, skipChildren?: boolean) => void, setProp: <T>(node: DomNode, name: string, value: T, prev?: T | undefined) => T, mergeProps: (...sources: unknown[]) => unknown, use: <A, T>(fn: (element: DomNode, arg: A) => T, element: DomNode, arg: A) => T;
|
|
11
5
|
export declare const insert: typeof solidUniversalInsert;
|
|
12
|
-
export {};
|
package/src/reconciler.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import {
|
|
4
4
|
InputRenderable as InputRenderable2,
|
|
5
5
|
InputRenderableEvents,
|
|
6
|
-
Renderable,
|
|
6
|
+
Renderable as Renderable2,
|
|
7
7
|
SelectRenderable as SelectRenderable2,
|
|
8
8
|
SelectRenderableEvents,
|
|
9
9
|
StyledText,
|
|
10
10
|
TabSelectRenderable as TabSelectRenderable2,
|
|
11
11
|
TabSelectRenderableEvents,
|
|
12
|
-
TextRenderable as
|
|
12
|
+
TextRenderable as TextRenderable3
|
|
13
13
|
} from "@opentui/core";
|
|
14
14
|
import { createRenderer } from "solid-js/universal";
|
|
15
15
|
|
|
@@ -43,6 +43,9 @@ var elements = {
|
|
|
43
43
|
text: TextRenderable
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
// src/elements/text-node.ts
|
|
47
|
+
import { Renderable, TextRenderable as TextRenderable2 } from "@opentui/core";
|
|
48
|
+
|
|
46
49
|
// src/utils/id-counter.ts
|
|
47
50
|
var idCounter = new Map;
|
|
48
51
|
function getNextId(elementType) {
|
|
@@ -54,8 +57,16 @@ function getNextId(elementType) {
|
|
|
54
57
|
return `${elementType}-${value}`;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
// src/
|
|
60
|
+
// src/utils/log.ts
|
|
61
|
+
var log = (...args) => {
|
|
62
|
+
if (process.env.DEBUG) {
|
|
63
|
+
console.log("[Reconciler]", ...args);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/elements/text-node.ts
|
|
58
68
|
var GHOST_NODE_TAG = "text-ghost";
|
|
69
|
+
var ChunkToTextNodeMap = new WeakMap;
|
|
59
70
|
|
|
60
71
|
class TextNode {
|
|
61
72
|
id;
|
|
@@ -65,91 +76,112 @@ class TextNode {
|
|
|
65
76
|
constructor(chunk) {
|
|
66
77
|
this.id = getNextId("text-node");
|
|
67
78
|
this.chunk = chunk;
|
|
79
|
+
ChunkToTextNodeMap.set(chunk, this);
|
|
68
80
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
function getOrCreateTextGhostNode(parent, anchor) {
|
|
75
|
-
if (anchor instanceof TextNode && anchor.textParent) {
|
|
76
|
-
return anchor.textParent;
|
|
77
|
-
}
|
|
78
|
-
const children = parent.getChildren();
|
|
79
|
-
if (anchor instanceof Renderable) {
|
|
80
|
-
const anchorIndex = children.findIndex((el) => el.id === anchor.id);
|
|
81
|
-
const beforeAnchor = children[anchorIndex - 1];
|
|
82
|
-
if (beforeAnchor instanceof TextRenderable2 && beforeAnchor.id.startsWith(GHOST_NODE_TAG)) {
|
|
83
|
-
return beforeAnchor;
|
|
81
|
+
replaceText(newChunk) {
|
|
82
|
+
const textParent = this.textParent;
|
|
83
|
+
if (!textParent) {
|
|
84
|
+
log("No parent found for text node:", this.id);
|
|
85
|
+
return;
|
|
84
86
|
}
|
|
87
|
+
textParent.content = textParent.content.replace(newChunk, this.chunk);
|
|
88
|
+
this.chunk = newChunk;
|
|
89
|
+
ChunkToTextNodeMap.set(newChunk, this);
|
|
85
90
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return lastChild;
|
|
89
|
-
}
|
|
90
|
-
const ghostNode = new TextRenderable2(getNextId(GHOST_NODE_TAG), {});
|
|
91
|
-
_insertNode(parent, ghostNode, anchor);
|
|
92
|
-
return ghostNode;
|
|
93
|
-
}
|
|
94
|
-
function insertTextNode(parent, node, anchor) {
|
|
95
|
-
if (!(parent instanceof Renderable)) {
|
|
96
|
-
console.warn("Attaching text node to parent text node, impossible");
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
log("Inserting text node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
|
|
100
|
-
let textParent;
|
|
101
|
-
if (!(parent instanceof TextRenderable2)) {
|
|
102
|
-
textParent = getOrCreateTextGhostNode(parent, anchor);
|
|
103
|
-
} else {
|
|
104
|
-
textParent = parent;
|
|
91
|
+
static getTextNodeFromChunk(chunk) {
|
|
92
|
+
return ChunkToTextNodeMap.get(chunk);
|
|
105
93
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
|
|
110
|
-
if (anchorIndex == -1) {
|
|
111
|
-
console.log("anchor not found");
|
|
94
|
+
insert(parent, anchor) {
|
|
95
|
+
if (!(parent instanceof Renderable)) {
|
|
96
|
+
log("Attaching text node to parent text node, impossible");
|
|
112
97
|
return;
|
|
113
98
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
|
|
118
|
-
styledText = styledText.replace(node.chunk, firstChunk);
|
|
99
|
+
let textParent;
|
|
100
|
+
if (!(parent instanceof TextRenderable2)) {
|
|
101
|
+
textParent = this.getOrCreateTextGhostNode(parent, anchor);
|
|
119
102
|
} else {
|
|
120
|
-
|
|
103
|
+
textParent = parent;
|
|
121
104
|
}
|
|
105
|
+
this.textParent = textParent;
|
|
106
|
+
let styledText = textParent.content;
|
|
107
|
+
if (anchor && anchor instanceof TextNode) {
|
|
108
|
+
const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
|
|
109
|
+
if (anchorIndex === -1) {
|
|
110
|
+
log("anchor not found");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
styledText = styledText.insert(this.chunk, anchorIndex);
|
|
114
|
+
} else {
|
|
115
|
+
const firstChunk = textParent.content.chunks[0];
|
|
116
|
+
if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
|
|
117
|
+
styledText = styledText.replace(this.chunk, firstChunk);
|
|
118
|
+
} else {
|
|
119
|
+
styledText = styledText.insert(this.chunk);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
textParent.content = styledText;
|
|
123
|
+
this.parent = parent;
|
|
122
124
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
remove(parent) {
|
|
126
|
+
if (!(parent instanceof Renderable)) {
|
|
127
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (parent === this.textParent && parent instanceof TextRenderable2) {
|
|
131
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
132
|
+
parent.content = parent.content.remove(this.chunk);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (this.textParent) {
|
|
136
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
137
|
+
let styledText = this.textParent.content;
|
|
138
|
+
styledText = styledText.remove(this.chunk);
|
|
139
|
+
if (styledText.chunks.length > 0) {
|
|
140
|
+
this.textParent.content = styledText;
|
|
141
|
+
} else {
|
|
142
|
+
this.parent?.remove(this.textParent.id);
|
|
143
|
+
this.textParent.destroyRecursively();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
131
146
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
getOrCreateTextGhostNode(parent, anchor) {
|
|
148
|
+
if (anchor instanceof TextNode && anchor.textParent) {
|
|
149
|
+
return anchor.textParent;
|
|
150
|
+
}
|
|
151
|
+
const children = parent.getChildren();
|
|
152
|
+
if (anchor instanceof Renderable) {
|
|
153
|
+
const anchorIndex = children.findIndex((el) => el.id === anchor.id);
|
|
154
|
+
const beforeAnchor = children[anchorIndex - 1];
|
|
155
|
+
if (beforeAnchor instanceof GhostTextRenderable) {
|
|
156
|
+
return beforeAnchor;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const lastChild = children.at(-1);
|
|
160
|
+
if (lastChild instanceof GhostTextRenderable) {
|
|
161
|
+
return lastChild;
|
|
144
162
|
}
|
|
163
|
+
const ghostNode = new GhostTextRenderable(getNextId(GHOST_NODE_TAG), {});
|
|
164
|
+
insertNode(parent, ghostNode, anchor);
|
|
165
|
+
return ghostNode;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
class GhostTextRenderable extends TextRenderable2 {
|
|
170
|
+
constructor(id, options) {
|
|
171
|
+
super(id, options);
|
|
172
|
+
}
|
|
173
|
+
static isGhostNode(node) {
|
|
174
|
+
return node instanceof GhostTextRenderable;
|
|
145
175
|
}
|
|
146
176
|
}
|
|
177
|
+
|
|
178
|
+
// src/reconciler.ts
|
|
147
179
|
function _insertNode(parent, node, anchor) {
|
|
148
180
|
log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
|
|
149
181
|
if (node instanceof TextNode) {
|
|
150
|
-
return
|
|
182
|
+
return node.insert(parent, anchor);
|
|
151
183
|
}
|
|
152
|
-
if (!(parent instanceof
|
|
184
|
+
if (!(parent instanceof Renderable2)) {
|
|
153
185
|
return;
|
|
154
186
|
}
|
|
155
187
|
if (anchor) {
|
|
@@ -167,9 +199,9 @@ function _insertNode(parent, node, anchor) {
|
|
|
167
199
|
function _removeNode(parent, node) {
|
|
168
200
|
log("Removing node:", node.id, "from parent:", parent.id);
|
|
169
201
|
if (node instanceof TextNode) {
|
|
170
|
-
return
|
|
202
|
+
return node.remove(parent);
|
|
171
203
|
}
|
|
172
|
-
if (parent instanceof
|
|
204
|
+
if (parent instanceof Renderable2 && node instanceof Renderable2) {
|
|
173
205
|
parent.remove(node.id);
|
|
174
206
|
node.destroyRecursively();
|
|
175
207
|
}
|
|
@@ -203,30 +235,18 @@ var {
|
|
|
203
235
|
plainText: `${value}`
|
|
204
236
|
};
|
|
205
237
|
const textNode = new TextNode(chunk);
|
|
206
|
-
ChunkToTextNodeMap.set(chunk, textNode);
|
|
207
238
|
return textNode;
|
|
208
239
|
},
|
|
209
240
|
replaceText(textNode, value) {
|
|
210
241
|
log("Replacing text:", value, "in node:", textNode.id);
|
|
211
|
-
if (textNode instanceof
|
|
242
|
+
if (textNode instanceof Renderable2)
|
|
212
243
|
return;
|
|
213
244
|
const newChunk = {
|
|
214
245
|
__isChunk: true,
|
|
215
246
|
text: new TextEncoder().encode(value),
|
|
216
247
|
plainText: value
|
|
217
248
|
};
|
|
218
|
-
|
|
219
|
-
if (!textParent) {
|
|
220
|
-
log("No parent found for text node:", textNode.id);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (textParent instanceof TextRenderable2) {
|
|
224
|
-
const styledText = textParent.content;
|
|
225
|
-
styledText.replace(newChunk, textNode.chunk);
|
|
226
|
-
textParent.content = styledText;
|
|
227
|
-
textNode.chunk = newChunk;
|
|
228
|
-
ChunkToTextNodeMap.set(newChunk, textNode);
|
|
229
|
-
}
|
|
249
|
+
textNode.replaceText(newChunk);
|
|
230
250
|
},
|
|
231
251
|
setProperty(node, name, value, prev) {
|
|
232
252
|
if (node instanceof TextNode) {
|
|
@@ -339,10 +359,10 @@ var {
|
|
|
339
359
|
},
|
|
340
360
|
getFirstChild(node) {
|
|
341
361
|
log("Getting first child of node:", node.id);
|
|
342
|
-
if (node instanceof
|
|
362
|
+
if (node instanceof TextRenderable3) {
|
|
343
363
|
const chunk = node.content.chunks[0];
|
|
344
364
|
if (chunk) {
|
|
345
|
-
return
|
|
365
|
+
return TextNode.getTextNodeFromChunk(chunk);
|
|
346
366
|
} else {
|
|
347
367
|
return;
|
|
348
368
|
}
|
|
@@ -366,7 +386,7 @@ var {
|
|
|
366
386
|
return;
|
|
367
387
|
}
|
|
368
388
|
if (node instanceof TextNode) {
|
|
369
|
-
if (parent instanceof
|
|
389
|
+
if (parent instanceof TextRenderable3) {
|
|
370
390
|
const siblings2 = parent.content.chunks;
|
|
371
391
|
const index2 = siblings2.indexOf(node.chunk);
|
|
372
392
|
if (index2 === -1 || index2 === siblings2.length - 1) {
|
|
@@ -378,7 +398,7 @@ var {
|
|
|
378
398
|
log("Next sibling is null for node:", node.id);
|
|
379
399
|
return;
|
|
380
400
|
}
|
|
381
|
-
return
|
|
401
|
+
return TextNode.getTextNodeFromChunk(nextSibling2);
|
|
382
402
|
}
|
|
383
403
|
console.warn("Text parent is not a text node:", node.id);
|
|
384
404
|
return;
|
|
@@ -405,13 +425,13 @@ var insertStyledText = (parent, value, current, marker) => {
|
|
|
405
425
|
return current;
|
|
406
426
|
if (current) {
|
|
407
427
|
if (typeof current === "object" && "__isChunk" in current) {
|
|
408
|
-
const node =
|
|
428
|
+
const node = TextNode.getTextNodeFromChunk(current);
|
|
409
429
|
if (node) {
|
|
410
430
|
_removeNode(parent, node);
|
|
411
431
|
}
|
|
412
432
|
} else if (current instanceof StyledText) {
|
|
413
433
|
for (const chunk of current.chunks) {
|
|
414
|
-
const chunkNode =
|
|
434
|
+
const chunkNode = TextNode.getTextNodeFromChunk(chunk);
|
|
415
435
|
if (!chunkNode)
|
|
416
436
|
continue;
|
|
417
437
|
_removeNode(parent, chunkNode);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const log: (...args: any[]) => void;
|