@opentui/solid 0.1.8 → 0.1.9
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 +109 -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 +111 -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,14 @@ function getNextId(elementType) {
|
|
|
129
132
|
return `${elementType}-${value}`;
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
// src/
|
|
135
|
+
// src/utils/log.ts
|
|
136
|
+
var log = (...args) => {
|
|
137
|
+
console.log("[Reconciler]", ...args);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/elements/text-node.ts
|
|
133
141
|
var GHOST_NODE_TAG = "text-ghost";
|
|
142
|
+
var ChunkToTextNodeMap = new WeakMap;
|
|
134
143
|
|
|
135
144
|
class TextNode {
|
|
136
145
|
id;
|
|
@@ -140,91 +149,112 @@ class TextNode {
|
|
|
140
149
|
constructor(chunk) {
|
|
141
150
|
this.id = getNextId("text-node");
|
|
142
151
|
this.chunk = chunk;
|
|
152
|
+
ChunkToTextNodeMap.set(chunk, this);
|
|
143
153
|
}
|
|
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;
|
|
154
|
+
replaceText(newChunk) {
|
|
155
|
+
const textParent = this.textParent;
|
|
156
|
+
if (!textParent) {
|
|
157
|
+
log("No parent found for text node:", this.id);
|
|
158
|
+
return;
|
|
159
159
|
}
|
|
160
|
+
textParent.content = textParent.content.replace(newChunk, this.chunk);
|
|
161
|
+
this.chunk = newChunk;
|
|
162
|
+
ChunkToTextNodeMap.set(newChunk, this);
|
|
160
163
|
}
|
|
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;
|
|
164
|
+
static getTextNodeFromChunk(chunk) {
|
|
165
|
+
return ChunkToTextNodeMap.get(chunk);
|
|
180
166
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
|
|
185
|
-
if (anchorIndex == -1) {
|
|
186
|
-
console.log("anchor not found");
|
|
167
|
+
insert(parent, anchor) {
|
|
168
|
+
if (!(parent instanceof Renderable)) {
|
|
169
|
+
log("Attaching text node to parent text node, impossible");
|
|
187
170
|
return;
|
|
188
171
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
172
|
+
let textParent;
|
|
173
|
+
if (!(parent instanceof TextRenderable2)) {
|
|
174
|
+
textParent = this.getOrCreateTextGhostNode(parent, anchor);
|
|
175
|
+
} else {
|
|
176
|
+
textParent = parent;
|
|
177
|
+
}
|
|
178
|
+
this.textParent = textParent;
|
|
179
|
+
let styledText = textParent.content;
|
|
180
|
+
if (anchor && anchor instanceof TextNode) {
|
|
181
|
+
const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
|
|
182
|
+
if (anchorIndex === -1) {
|
|
183
|
+
log("anchor not found");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
styledText = styledText.insert(this.chunk, anchorIndex);
|
|
194
187
|
} else {
|
|
195
|
-
|
|
188
|
+
const firstChunk = textParent.content.chunks[0];
|
|
189
|
+
if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
|
|
190
|
+
styledText = styledText.replace(this.chunk, firstChunk);
|
|
191
|
+
} else {
|
|
192
|
+
styledText = styledText.insert(this.chunk);
|
|
193
|
+
}
|
|
196
194
|
}
|
|
195
|
+
textParent.content = styledText;
|
|
196
|
+
this.parent = parent;
|
|
197
197
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
198
|
+
remove(parent) {
|
|
199
|
+
if (!(parent instanceof Renderable)) {
|
|
200
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (parent === this.textParent && parent instanceof TextRenderable2) {
|
|
204
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
205
|
+
parent.content = parent.content.remove(this.chunk);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (this.textParent) {
|
|
209
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
210
|
+
let styledText = this.textParent.content;
|
|
211
|
+
styledText = styledText.remove(this.chunk);
|
|
212
|
+
if (styledText.chunks.length > 0) {
|
|
213
|
+
this.textParent.content = styledText;
|
|
214
|
+
} else {
|
|
215
|
+
this.parent?.remove(this.textParent.id);
|
|
216
|
+
this.textParent.destroyRecursively();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
206
219
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
} else if (node.textParent) {
|
|
211
|
-
ChunkToTextNodeMap.delete(node.chunk);
|
|
212
|
-
let styledText = node.textParent.content;
|
|
213
|
-
styledText = styledText.remove(node.chunk);
|
|
214
|
-
if (styledText.chunks.length > 0) {
|
|
215
|
-
node.textParent.content = styledText;
|
|
216
|
-
} else {
|
|
217
|
-
node.parent?.remove(node.textParent.id);
|
|
218
|
-
node.textParent.destroyRecursively();
|
|
220
|
+
getOrCreateTextGhostNode(parent, anchor) {
|
|
221
|
+
if (anchor instanceof TextNode && anchor.textParent) {
|
|
222
|
+
return anchor.textParent;
|
|
219
223
|
}
|
|
224
|
+
const children = parent.getChildren();
|
|
225
|
+
if (anchor instanceof Renderable) {
|
|
226
|
+
const anchorIndex = children.findIndex((el) => el.id === anchor.id);
|
|
227
|
+
const beforeAnchor = children[anchorIndex - 1];
|
|
228
|
+
if (beforeAnchor instanceof GhostTextRenderable) {
|
|
229
|
+
return beforeAnchor;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const lastChild = children.at(-1);
|
|
233
|
+
if (lastChild instanceof GhostTextRenderable) {
|
|
234
|
+
return lastChild;
|
|
235
|
+
}
|
|
236
|
+
const ghostNode = new GhostTextRenderable(getNextId(GHOST_NODE_TAG), {});
|
|
237
|
+
insertNode(parent, ghostNode, anchor);
|
|
238
|
+
return ghostNode;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
class GhostTextRenderable extends TextRenderable2 {
|
|
243
|
+
constructor(id, options) {
|
|
244
|
+
super(id, options);
|
|
245
|
+
}
|
|
246
|
+
static isGhostNode(node) {
|
|
247
|
+
return node instanceof GhostTextRenderable;
|
|
220
248
|
}
|
|
221
249
|
}
|
|
250
|
+
|
|
251
|
+
// src/reconciler.ts
|
|
222
252
|
function _insertNode(parent, node, anchor) {
|
|
223
253
|
log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
|
|
224
254
|
if (node instanceof TextNode) {
|
|
225
|
-
return
|
|
255
|
+
return node.insert(parent, anchor);
|
|
226
256
|
}
|
|
227
|
-
if (!(parent instanceof
|
|
257
|
+
if (!(parent instanceof Renderable2)) {
|
|
228
258
|
return;
|
|
229
259
|
}
|
|
230
260
|
if (anchor) {
|
|
@@ -242,9 +272,9 @@ function _insertNode(parent, node, anchor) {
|
|
|
242
272
|
function _removeNode(parent, node) {
|
|
243
273
|
log("Removing node:", node.id, "from parent:", parent.id);
|
|
244
274
|
if (node instanceof TextNode) {
|
|
245
|
-
return
|
|
275
|
+
return node.remove(parent);
|
|
246
276
|
}
|
|
247
|
-
if (parent instanceof
|
|
277
|
+
if (parent instanceof Renderable2 && node instanceof Renderable2) {
|
|
248
278
|
parent.remove(node.id);
|
|
249
279
|
node.destroyRecursively();
|
|
250
280
|
}
|
|
@@ -278,30 +308,18 @@ var {
|
|
|
278
308
|
plainText: `${value}`
|
|
279
309
|
};
|
|
280
310
|
const textNode = new TextNode(chunk);
|
|
281
|
-
ChunkToTextNodeMap.set(chunk, textNode);
|
|
282
311
|
return textNode;
|
|
283
312
|
},
|
|
284
313
|
replaceText(textNode, value) {
|
|
285
314
|
log("Replacing text:", value, "in node:", textNode.id);
|
|
286
|
-
if (textNode instanceof
|
|
315
|
+
if (textNode instanceof Renderable2)
|
|
287
316
|
return;
|
|
288
317
|
const newChunk = {
|
|
289
318
|
__isChunk: true,
|
|
290
319
|
text: new TextEncoder().encode(value),
|
|
291
320
|
plainText: value
|
|
292
321
|
};
|
|
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
|
-
}
|
|
322
|
+
textNode.replaceText(newChunk);
|
|
305
323
|
},
|
|
306
324
|
setProperty(node, name, value, prev) {
|
|
307
325
|
if (node instanceof TextNode) {
|
|
@@ -414,10 +432,10 @@ var {
|
|
|
414
432
|
},
|
|
415
433
|
getFirstChild(node) {
|
|
416
434
|
log("Getting first child of node:", node.id);
|
|
417
|
-
if (node instanceof
|
|
435
|
+
if (node instanceof TextRenderable3) {
|
|
418
436
|
const chunk = node.content.chunks[0];
|
|
419
437
|
if (chunk) {
|
|
420
|
-
return
|
|
438
|
+
return TextNode.getTextNodeFromChunk(chunk);
|
|
421
439
|
} else {
|
|
422
440
|
return;
|
|
423
441
|
}
|
|
@@ -441,7 +459,7 @@ var {
|
|
|
441
459
|
return;
|
|
442
460
|
}
|
|
443
461
|
if (node instanceof TextNode) {
|
|
444
|
-
if (parent instanceof
|
|
462
|
+
if (parent instanceof TextRenderable3) {
|
|
445
463
|
const siblings2 = parent.content.chunks;
|
|
446
464
|
const index2 = siblings2.indexOf(node.chunk);
|
|
447
465
|
if (index2 === -1 || index2 === siblings2.length - 1) {
|
|
@@ -453,7 +471,7 @@ var {
|
|
|
453
471
|
log("Next sibling is null for node:", node.id);
|
|
454
472
|
return;
|
|
455
473
|
}
|
|
456
|
-
return
|
|
474
|
+
return TextNode.getTextNodeFromChunk(nextSibling2);
|
|
457
475
|
}
|
|
458
476
|
console.warn("Text parent is not a text node:", node.id);
|
|
459
477
|
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.9",
|
|
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.9"
|
|
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,14 @@ function getNextId(elementType) {
|
|
|
54
57
|
return `${elementType}-${value}`;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
// src/
|
|
60
|
+
// src/utils/log.ts
|
|
61
|
+
var log = (...args) => {
|
|
62
|
+
console.log("[Reconciler]", ...args);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/elements/text-node.ts
|
|
58
66
|
var GHOST_NODE_TAG = "text-ghost";
|
|
67
|
+
var ChunkToTextNodeMap = new WeakMap;
|
|
59
68
|
|
|
60
69
|
class TextNode {
|
|
61
70
|
id;
|
|
@@ -65,91 +74,112 @@ class TextNode {
|
|
|
65
74
|
constructor(chunk) {
|
|
66
75
|
this.id = getNextId("text-node");
|
|
67
76
|
this.chunk = chunk;
|
|
77
|
+
ChunkToTextNodeMap.set(chunk, this);
|
|
68
78
|
}
|
|
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;
|
|
79
|
+
replaceText(newChunk) {
|
|
80
|
+
const textParent = this.textParent;
|
|
81
|
+
if (!textParent) {
|
|
82
|
+
log("No parent found for text node:", this.id);
|
|
83
|
+
return;
|
|
84
84
|
}
|
|
85
|
+
textParent.content = textParent.content.replace(newChunk, this.chunk);
|
|
86
|
+
this.chunk = newChunk;
|
|
87
|
+
ChunkToTextNodeMap.set(newChunk, this);
|
|
85
88
|
}
|
|
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;
|
|
89
|
+
static getTextNodeFromChunk(chunk) {
|
|
90
|
+
return ChunkToTextNodeMap.get(chunk);
|
|
105
91
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
|
|
110
|
-
if (anchorIndex == -1) {
|
|
111
|
-
console.log("anchor not found");
|
|
92
|
+
insert(parent, anchor) {
|
|
93
|
+
if (!(parent instanceof Renderable)) {
|
|
94
|
+
log("Attaching text node to parent text node, impossible");
|
|
112
95
|
return;
|
|
113
96
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
97
|
+
let textParent;
|
|
98
|
+
if (!(parent instanceof TextRenderable2)) {
|
|
99
|
+
textParent = this.getOrCreateTextGhostNode(parent, anchor);
|
|
100
|
+
} else {
|
|
101
|
+
textParent = parent;
|
|
102
|
+
}
|
|
103
|
+
this.textParent = textParent;
|
|
104
|
+
let styledText = textParent.content;
|
|
105
|
+
if (anchor && anchor instanceof TextNode) {
|
|
106
|
+
const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
|
|
107
|
+
if (anchorIndex === -1) {
|
|
108
|
+
log("anchor not found");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
styledText = styledText.insert(this.chunk, anchorIndex);
|
|
119
112
|
} else {
|
|
120
|
-
|
|
113
|
+
const firstChunk = textParent.content.chunks[0];
|
|
114
|
+
if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
|
|
115
|
+
styledText = styledText.replace(this.chunk, firstChunk);
|
|
116
|
+
} else {
|
|
117
|
+
styledText = styledText.insert(this.chunk);
|
|
118
|
+
}
|
|
121
119
|
}
|
|
120
|
+
textParent.content = styledText;
|
|
121
|
+
this.parent = parent;
|
|
122
122
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
remove(parent) {
|
|
124
|
+
if (!(parent instanceof Renderable)) {
|
|
125
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (parent === this.textParent && parent instanceof TextRenderable2) {
|
|
129
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
130
|
+
parent.content = parent.content.remove(this.chunk);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (this.textParent) {
|
|
134
|
+
ChunkToTextNodeMap.delete(this.chunk);
|
|
135
|
+
let styledText = this.textParent.content;
|
|
136
|
+
styledText = styledText.remove(this.chunk);
|
|
137
|
+
if (styledText.chunks.length > 0) {
|
|
138
|
+
this.textParent.content = styledText;
|
|
139
|
+
} else {
|
|
140
|
+
this.parent?.remove(this.textParent.id);
|
|
141
|
+
this.textParent.destroyRecursively();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
131
144
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
} else if (node.textParent) {
|
|
136
|
-
ChunkToTextNodeMap.delete(node.chunk);
|
|
137
|
-
let styledText = node.textParent.content;
|
|
138
|
-
styledText = styledText.remove(node.chunk);
|
|
139
|
-
if (styledText.chunks.length > 0) {
|
|
140
|
-
node.textParent.content = styledText;
|
|
141
|
-
} else {
|
|
142
|
-
node.parent?.remove(node.textParent.id);
|
|
143
|
-
node.textParent.destroyRecursively();
|
|
145
|
+
getOrCreateTextGhostNode(parent, anchor) {
|
|
146
|
+
if (anchor instanceof TextNode && anchor.textParent) {
|
|
147
|
+
return anchor.textParent;
|
|
144
148
|
}
|
|
149
|
+
const children = parent.getChildren();
|
|
150
|
+
if (anchor instanceof Renderable) {
|
|
151
|
+
const anchorIndex = children.findIndex((el) => el.id === anchor.id);
|
|
152
|
+
const beforeAnchor = children[anchorIndex - 1];
|
|
153
|
+
if (beforeAnchor instanceof GhostTextRenderable) {
|
|
154
|
+
return beforeAnchor;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const lastChild = children.at(-1);
|
|
158
|
+
if (lastChild instanceof GhostTextRenderable) {
|
|
159
|
+
return lastChild;
|
|
160
|
+
}
|
|
161
|
+
const ghostNode = new GhostTextRenderable(getNextId(GHOST_NODE_TAG), {});
|
|
162
|
+
insertNode(parent, ghostNode, anchor);
|
|
163
|
+
return ghostNode;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class GhostTextRenderable extends TextRenderable2 {
|
|
168
|
+
constructor(id, options) {
|
|
169
|
+
super(id, options);
|
|
170
|
+
}
|
|
171
|
+
static isGhostNode(node) {
|
|
172
|
+
return node instanceof GhostTextRenderable;
|
|
145
173
|
}
|
|
146
174
|
}
|
|
175
|
+
|
|
176
|
+
// src/reconciler.ts
|
|
147
177
|
function _insertNode(parent, node, anchor) {
|
|
148
178
|
log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
|
|
149
179
|
if (node instanceof TextNode) {
|
|
150
|
-
return
|
|
180
|
+
return node.insert(parent, anchor);
|
|
151
181
|
}
|
|
152
|
-
if (!(parent instanceof
|
|
182
|
+
if (!(parent instanceof Renderable2)) {
|
|
153
183
|
return;
|
|
154
184
|
}
|
|
155
185
|
if (anchor) {
|
|
@@ -167,9 +197,9 @@ function _insertNode(parent, node, anchor) {
|
|
|
167
197
|
function _removeNode(parent, node) {
|
|
168
198
|
log("Removing node:", node.id, "from parent:", parent.id);
|
|
169
199
|
if (node instanceof TextNode) {
|
|
170
|
-
return
|
|
200
|
+
return node.remove(parent);
|
|
171
201
|
}
|
|
172
|
-
if (parent instanceof
|
|
202
|
+
if (parent instanceof Renderable2 && node instanceof Renderable2) {
|
|
173
203
|
parent.remove(node.id);
|
|
174
204
|
node.destroyRecursively();
|
|
175
205
|
}
|
|
@@ -203,30 +233,18 @@ var {
|
|
|
203
233
|
plainText: `${value}`
|
|
204
234
|
};
|
|
205
235
|
const textNode = new TextNode(chunk);
|
|
206
|
-
ChunkToTextNodeMap.set(chunk, textNode);
|
|
207
236
|
return textNode;
|
|
208
237
|
},
|
|
209
238
|
replaceText(textNode, value) {
|
|
210
239
|
log("Replacing text:", value, "in node:", textNode.id);
|
|
211
|
-
if (textNode instanceof
|
|
240
|
+
if (textNode instanceof Renderable2)
|
|
212
241
|
return;
|
|
213
242
|
const newChunk = {
|
|
214
243
|
__isChunk: true,
|
|
215
244
|
text: new TextEncoder().encode(value),
|
|
216
245
|
plainText: value
|
|
217
246
|
};
|
|
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
|
-
}
|
|
247
|
+
textNode.replaceText(newChunk);
|
|
230
248
|
},
|
|
231
249
|
setProperty(node, name, value, prev) {
|
|
232
250
|
if (node instanceof TextNode) {
|
|
@@ -339,10 +357,10 @@ var {
|
|
|
339
357
|
},
|
|
340
358
|
getFirstChild(node) {
|
|
341
359
|
log("Getting first child of node:", node.id);
|
|
342
|
-
if (node instanceof
|
|
360
|
+
if (node instanceof TextRenderable3) {
|
|
343
361
|
const chunk = node.content.chunks[0];
|
|
344
362
|
if (chunk) {
|
|
345
|
-
return
|
|
363
|
+
return TextNode.getTextNodeFromChunk(chunk);
|
|
346
364
|
} else {
|
|
347
365
|
return;
|
|
348
366
|
}
|
|
@@ -366,7 +384,7 @@ var {
|
|
|
366
384
|
return;
|
|
367
385
|
}
|
|
368
386
|
if (node instanceof TextNode) {
|
|
369
|
-
if (parent instanceof
|
|
387
|
+
if (parent instanceof TextRenderable3) {
|
|
370
388
|
const siblings2 = parent.content.chunks;
|
|
371
389
|
const index2 = siblings2.indexOf(node.chunk);
|
|
372
390
|
if (index2 === -1 || index2 === siblings2.length - 1) {
|
|
@@ -378,7 +396,7 @@ var {
|
|
|
378
396
|
log("Next sibling is null for node:", node.id);
|
|
379
397
|
return;
|
|
380
398
|
}
|
|
381
|
-
return
|
|
399
|
+
return TextNode.getTextNodeFromChunk(nextSibling2);
|
|
382
400
|
}
|
|
383
401
|
console.warn("Text parent is not a text node:", node.id);
|
|
384
402
|
return;
|
|
@@ -405,13 +423,13 @@ var insertStyledText = (parent, value, current, marker) => {
|
|
|
405
423
|
return current;
|
|
406
424
|
if (current) {
|
|
407
425
|
if (typeof current === "object" && "__isChunk" in current) {
|
|
408
|
-
const node =
|
|
426
|
+
const node = TextNode.getTextNodeFromChunk(current);
|
|
409
427
|
if (node) {
|
|
410
428
|
_removeNode(parent, node);
|
|
411
429
|
}
|
|
412
430
|
} else if (current instanceof StyledText) {
|
|
413
431
|
for (const chunk of current.chunks) {
|
|
414
|
-
const chunkNode =
|
|
432
|
+
const chunkNode = TextNode.getTextNodeFromChunk(chunk);
|
|
415
433
|
if (!chunkNode)
|
|
416
434
|
continue;
|
|
417
435
|
_removeNode(parent, chunkNode);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const log: (...args: any[]) => void;
|