@opentui/solid 0.1.7 → 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 +6 -4
- package/index.js +109 -93
- package/jsx-runtime.d.ts +11 -11
- package/package.json +8 -8
- package/scripts/preload.ts +3 -3
- package/scripts/solid-plugin.ts +19 -28
- 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 -95
- package/src/utils/log.d.ts +1 -0
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ bun install solid-js @opentui/solid
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
1. Add jsx config to tsconfig.json:
|
|
14
|
+
|
|
14
15
|
```json
|
|
15
16
|
{
|
|
16
17
|
"compilerOptions": {
|
|
@@ -21,16 +22,17 @@ bun install solid-js @opentui/solid
|
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
2. Add preload script to bunfig.toml:
|
|
25
|
+
|
|
24
26
|
```toml
|
|
25
27
|
preload = ["@opentui/solid/preload"]
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
3. Add render function to index.tsx:
|
|
31
|
+
|
|
29
32
|
```tsx
|
|
30
|
-
import { render } from "@opentui/solid"
|
|
33
|
+
import { render } from "@opentui/solid"
|
|
31
34
|
|
|
32
|
-
render(() => <text>Hello, World!</text>)
|
|
35
|
+
render(() => <text>Hello, World!</text>)
|
|
33
36
|
```
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
4. Run with `bun --conditions=browser index.tsx`.
|
|
38
|
+
4. Run with `bun index.tsx`.
|
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,93 +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
|
-
styledText.remove(node.chunk);
|
|
211
|
-
parent.content = styledText;
|
|
212
|
-
} else if (node.textParent) {
|
|
213
|
-
ChunkToTextNodeMap.delete(node.chunk);
|
|
214
|
-
const styledText = node.textParent.content;
|
|
215
|
-
styledText.remove(node.chunk);
|
|
216
|
-
if (styledText.chunks.length > 0) {
|
|
217
|
-
node.textParent.content = styledText;
|
|
218
|
-
} else {
|
|
219
|
-
node.parent?.remove(node.textParent.id);
|
|
220
|
-
node.textParent.destroyRecursively();
|
|
220
|
+
getOrCreateTextGhostNode(parent, anchor) {
|
|
221
|
+
if (anchor instanceof TextNode && anchor.textParent) {
|
|
222
|
+
return anchor.textParent;
|
|
221
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;
|
|
222
248
|
}
|
|
223
249
|
}
|
|
250
|
+
|
|
251
|
+
// src/reconciler.ts
|
|
224
252
|
function _insertNode(parent, node, anchor) {
|
|
225
253
|
log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
|
|
226
254
|
if (node instanceof TextNode) {
|
|
227
|
-
return
|
|
255
|
+
return node.insert(parent, anchor);
|
|
228
256
|
}
|
|
229
|
-
if (!(parent instanceof
|
|
257
|
+
if (!(parent instanceof Renderable2)) {
|
|
230
258
|
return;
|
|
231
259
|
}
|
|
232
260
|
if (anchor) {
|
|
@@ -244,9 +272,9 @@ function _insertNode(parent, node, anchor) {
|
|
|
244
272
|
function _removeNode(parent, node) {
|
|
245
273
|
log("Removing node:", node.id, "from parent:", parent.id);
|
|
246
274
|
if (node instanceof TextNode) {
|
|
247
|
-
return
|
|
275
|
+
return node.remove(parent);
|
|
248
276
|
}
|
|
249
|
-
if (parent instanceof
|
|
277
|
+
if (parent instanceof Renderable2 && node instanceof Renderable2) {
|
|
250
278
|
parent.remove(node.id);
|
|
251
279
|
node.destroyRecursively();
|
|
252
280
|
}
|
|
@@ -280,30 +308,18 @@ var {
|
|
|
280
308
|
plainText: `${value}`
|
|
281
309
|
};
|
|
282
310
|
const textNode = new TextNode(chunk);
|
|
283
|
-
ChunkToTextNodeMap.set(chunk, textNode);
|
|
284
311
|
return textNode;
|
|
285
312
|
},
|
|
286
313
|
replaceText(textNode, value) {
|
|
287
314
|
log("Replacing text:", value, "in node:", textNode.id);
|
|
288
|
-
if (textNode instanceof
|
|
315
|
+
if (textNode instanceof Renderable2)
|
|
289
316
|
return;
|
|
290
317
|
const newChunk = {
|
|
291
318
|
__isChunk: true,
|
|
292
319
|
text: new TextEncoder().encode(value),
|
|
293
320
|
plainText: value
|
|
294
321
|
};
|
|
295
|
-
|
|
296
|
-
if (!textParent) {
|
|
297
|
-
log("No parent found for text node:", textNode.id);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
if (textParent instanceof TextRenderable2) {
|
|
301
|
-
const styledText = textParent.content;
|
|
302
|
-
styledText.replace(newChunk, textNode.chunk);
|
|
303
|
-
textParent.content = styledText;
|
|
304
|
-
textNode.chunk = newChunk;
|
|
305
|
-
ChunkToTextNodeMap.set(newChunk, textNode);
|
|
306
|
-
}
|
|
322
|
+
textNode.replaceText(newChunk);
|
|
307
323
|
},
|
|
308
324
|
setProperty(node, name, value, prev) {
|
|
309
325
|
if (node instanceof TextNode) {
|
|
@@ -416,10 +432,10 @@ var {
|
|
|
416
432
|
},
|
|
417
433
|
getFirstChild(node) {
|
|
418
434
|
log("Getting first child of node:", node.id);
|
|
419
|
-
if (node instanceof
|
|
435
|
+
if (node instanceof TextRenderable3) {
|
|
420
436
|
const chunk = node.content.chunks[0];
|
|
421
437
|
if (chunk) {
|
|
422
|
-
return
|
|
438
|
+
return TextNode.getTextNodeFromChunk(chunk);
|
|
423
439
|
} else {
|
|
424
440
|
return;
|
|
425
441
|
}
|
|
@@ -443,7 +459,7 @@ var {
|
|
|
443
459
|
return;
|
|
444
460
|
}
|
|
445
461
|
if (node instanceof TextNode) {
|
|
446
|
-
if (parent instanceof
|
|
462
|
+
if (parent instanceof TextRenderable3) {
|
|
447
463
|
const siblings2 = parent.content.chunks;
|
|
448
464
|
const index2 = siblings2.indexOf(node.chunk);
|
|
449
465
|
if (index2 === -1 || index2 === siblings2.length - 1) {
|
|
@@ -455,7 +471,7 @@ var {
|
|
|
455
471
|
log("Next sibling is null for node:", node.id);
|
|
456
472
|
return;
|
|
457
473
|
}
|
|
458
|
-
return
|
|
474
|
+
return TextNode.getTextNodeFromChunk(nextSibling2);
|
|
459
475
|
}
|
|
460
476
|
console.warn("Text parent is not a text node:", node.id);
|
|
461
477
|
return;
|
package/jsx-runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Renderable } from "@opentui/core"
|
|
1
|
+
import { Renderable } from "@opentui/core"
|
|
2
2
|
import {
|
|
3
3
|
ASCIIFontElementProps,
|
|
4
4
|
BoxElementProps,
|
|
@@ -7,25 +7,25 @@ import {
|
|
|
7
7
|
SelectElementProps,
|
|
8
8
|
TabSelectElementProps,
|
|
9
9
|
TextElementProps,
|
|
10
|
-
} from "./src/elements/index"
|
|
10
|
+
} from "./src/elements/index"
|
|
11
11
|
|
|
12
12
|
declare namespace JSX {
|
|
13
13
|
// Replace Node with Renderable
|
|
14
|
-
type Element = Renderable | ArrayElement | (string & {}) | number | boolean | null | undefined
|
|
14
|
+
type Element = Renderable | ArrayElement | (string & {}) | number | boolean | null | undefined
|
|
15
15
|
|
|
16
16
|
interface ArrayElement extends Array<Element> {}
|
|
17
17
|
|
|
18
18
|
interface IntrinsicElements {
|
|
19
|
-
ascii_font: ASCIIFontElementProps
|
|
20
|
-
box: BoxElementProps
|
|
21
|
-
group: GroupElementProps
|
|
22
|
-
input: InputElementProps
|
|
23
|
-
select: SelectElementProps
|
|
24
|
-
tab_select: TabSelectElementProps
|
|
25
|
-
text: TextElementProps
|
|
19
|
+
ascii_font: ASCIIFontElementProps
|
|
20
|
+
box: BoxElementProps
|
|
21
|
+
group: GroupElementProps
|
|
22
|
+
input: InputElementProps
|
|
23
|
+
select: SelectElementProps
|
|
24
|
+
tab_select: TabSelectElementProps
|
|
25
|
+
text: TextElementProps
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
interface ElementChildrenAttribute {
|
|
29
|
-
children: {}
|
|
29
|
+
children: {}
|
|
30
30
|
}
|
|
31
31
|
}
|
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,18 +30,18 @@
|
|
|
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
|
-
"@types/babel__core": "
|
|
37
|
-
"babel-plugin-module-resolver": "
|
|
38
|
-
"@babel/core": "
|
|
39
|
-
"@babel/preset-typescript": "
|
|
40
|
-
"babel-preset-solid": "
|
|
36
|
+
"@types/babel__core": "7.20.5",
|
|
37
|
+
"babel-plugin-module-resolver": "5.0.2",
|
|
38
|
+
"@babel/core": "7.28.0",
|
|
39
|
+
"@babel/preset-typescript": "7.27.1",
|
|
40
|
+
"babel-preset-solid": "1.9.9",
|
|
41
41
|
"@types/bun": "latest"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"solid-js": "
|
|
44
|
+
"solid-js": "1.9.9",
|
|
45
45
|
"typescript": "^5"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/scripts/preload.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import solidTransformPlugin from "./solid-plugin"
|
|
2
|
-
import { plugin, type BunPlugin } from "bun"
|
|
1
|
+
import solidTransformPlugin from "./solid-plugin"
|
|
2
|
+
import { plugin, type BunPlugin } from "bun"
|
|
3
3
|
|
|
4
|
-
plugin(solidTransformPlugin)
|
|
4
|
+
plugin(solidTransformPlugin)
|
package/scripts/solid-plugin.ts
CHANGED
|
@@ -1,33 +1,24 @@
|
|
|
1
|
-
import { transformAsync } from "@babel/core"
|
|
1
|
+
import { transformAsync } from "@babel/core"
|
|
2
2
|
// @ts-expect-error - Types not important.
|
|
3
|
-
import ts from "@babel/preset-typescript"
|
|
3
|
+
import ts from "@babel/preset-typescript"
|
|
4
4
|
// @ts-expect-error - Types not important.
|
|
5
|
-
import solid from "babel-preset-solid"
|
|
6
|
-
import { type BunPlugin } from "bun"
|
|
5
|
+
import solid from "babel-preset-solid"
|
|
6
|
+
import { type BunPlugin } from "bun"
|
|
7
7
|
|
|
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
|
-
const { readFile } = await import("node:fs/promises")
|
|
30
|
-
const
|
|
19
|
+
const { readFile } = await import("node:fs/promises")
|
|
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: {
|
|
@@ -46,13 +37,13 @@ const solidTransformPlugin: BunPlugin = {
|
|
|
46
37
|
],
|
|
47
38
|
[ts],
|
|
48
39
|
],
|
|
49
|
-
})
|
|
40
|
+
})
|
|
50
41
|
return {
|
|
51
42
|
contents: transforms?.code ?? "",
|
|
52
43
|
loader: "js",
|
|
53
|
-
}
|
|
54
|
-
})
|
|
44
|
+
}
|
|
45
|
+
})
|
|
55
46
|
},
|
|
56
|
-
}
|
|
47
|
+
}
|
|
57
48
|
|
|
58
|
-
export default solidTransformPlugin
|
|
49
|
+
export default solidTransformPlugin
|
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,93 +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
|
-
styledText.remove(node.chunk);
|
|
136
|
-
parent.content = styledText;
|
|
137
|
-
} else if (node.textParent) {
|
|
138
|
-
ChunkToTextNodeMap.delete(node.chunk);
|
|
139
|
-
const styledText = node.textParent.content;
|
|
140
|
-
styledText.remove(node.chunk);
|
|
141
|
-
if (styledText.chunks.length > 0) {
|
|
142
|
-
node.textParent.content = styledText;
|
|
143
|
-
} else {
|
|
144
|
-
node.parent?.remove(node.textParent.id);
|
|
145
|
-
node.textParent.destroyRecursively();
|
|
145
|
+
getOrCreateTextGhostNode(parent, anchor) {
|
|
146
|
+
if (anchor instanceof TextNode && anchor.textParent) {
|
|
147
|
+
return anchor.textParent;
|
|
146
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;
|
|
147
173
|
}
|
|
148
174
|
}
|
|
175
|
+
|
|
176
|
+
// src/reconciler.ts
|
|
149
177
|
function _insertNode(parent, node, anchor) {
|
|
150
178
|
log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
|
|
151
179
|
if (node instanceof TextNode) {
|
|
152
|
-
return
|
|
180
|
+
return node.insert(parent, anchor);
|
|
153
181
|
}
|
|
154
|
-
if (!(parent instanceof
|
|
182
|
+
if (!(parent instanceof Renderable2)) {
|
|
155
183
|
return;
|
|
156
184
|
}
|
|
157
185
|
if (anchor) {
|
|
@@ -169,9 +197,9 @@ function _insertNode(parent, node, anchor) {
|
|
|
169
197
|
function _removeNode(parent, node) {
|
|
170
198
|
log("Removing node:", node.id, "from parent:", parent.id);
|
|
171
199
|
if (node instanceof TextNode) {
|
|
172
|
-
return
|
|
200
|
+
return node.remove(parent);
|
|
173
201
|
}
|
|
174
|
-
if (parent instanceof
|
|
202
|
+
if (parent instanceof Renderable2 && node instanceof Renderable2) {
|
|
175
203
|
parent.remove(node.id);
|
|
176
204
|
node.destroyRecursively();
|
|
177
205
|
}
|
|
@@ -205,30 +233,18 @@ var {
|
|
|
205
233
|
plainText: `${value}`
|
|
206
234
|
};
|
|
207
235
|
const textNode = new TextNode(chunk);
|
|
208
|
-
ChunkToTextNodeMap.set(chunk, textNode);
|
|
209
236
|
return textNode;
|
|
210
237
|
},
|
|
211
238
|
replaceText(textNode, value) {
|
|
212
239
|
log("Replacing text:", value, "in node:", textNode.id);
|
|
213
|
-
if (textNode instanceof
|
|
240
|
+
if (textNode instanceof Renderable2)
|
|
214
241
|
return;
|
|
215
242
|
const newChunk = {
|
|
216
243
|
__isChunk: true,
|
|
217
244
|
text: new TextEncoder().encode(value),
|
|
218
245
|
plainText: value
|
|
219
246
|
};
|
|
220
|
-
|
|
221
|
-
if (!textParent) {
|
|
222
|
-
log("No parent found for text node:", textNode.id);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (textParent instanceof TextRenderable2) {
|
|
226
|
-
const styledText = textParent.content;
|
|
227
|
-
styledText.replace(newChunk, textNode.chunk);
|
|
228
|
-
textParent.content = styledText;
|
|
229
|
-
textNode.chunk = newChunk;
|
|
230
|
-
ChunkToTextNodeMap.set(newChunk, textNode);
|
|
231
|
-
}
|
|
247
|
+
textNode.replaceText(newChunk);
|
|
232
248
|
},
|
|
233
249
|
setProperty(node, name, value, prev) {
|
|
234
250
|
if (node instanceof TextNode) {
|
|
@@ -341,10 +357,10 @@ var {
|
|
|
341
357
|
},
|
|
342
358
|
getFirstChild(node) {
|
|
343
359
|
log("Getting first child of node:", node.id);
|
|
344
|
-
if (node instanceof
|
|
360
|
+
if (node instanceof TextRenderable3) {
|
|
345
361
|
const chunk = node.content.chunks[0];
|
|
346
362
|
if (chunk) {
|
|
347
|
-
return
|
|
363
|
+
return TextNode.getTextNodeFromChunk(chunk);
|
|
348
364
|
} else {
|
|
349
365
|
return;
|
|
350
366
|
}
|
|
@@ -368,7 +384,7 @@ var {
|
|
|
368
384
|
return;
|
|
369
385
|
}
|
|
370
386
|
if (node instanceof TextNode) {
|
|
371
|
-
if (parent instanceof
|
|
387
|
+
if (parent instanceof TextRenderable3) {
|
|
372
388
|
const siblings2 = parent.content.chunks;
|
|
373
389
|
const index2 = siblings2.indexOf(node.chunk);
|
|
374
390
|
if (index2 === -1 || index2 === siblings2.length - 1) {
|
|
@@ -380,7 +396,7 @@ var {
|
|
|
380
396
|
log("Next sibling is null for node:", node.id);
|
|
381
397
|
return;
|
|
382
398
|
}
|
|
383
|
-
return
|
|
399
|
+
return TextNode.getTextNodeFromChunk(nextSibling2);
|
|
384
400
|
}
|
|
385
401
|
console.warn("Text parent is not a text node:", node.id);
|
|
386
402
|
return;
|
|
@@ -407,13 +423,13 @@ var insertStyledText = (parent, value, current, marker) => {
|
|
|
407
423
|
return current;
|
|
408
424
|
if (current) {
|
|
409
425
|
if (typeof current === "object" && "__isChunk" in current) {
|
|
410
|
-
const node =
|
|
426
|
+
const node = TextNode.getTextNodeFromChunk(current);
|
|
411
427
|
if (node) {
|
|
412
428
|
_removeNode(parent, node);
|
|
413
429
|
}
|
|
414
430
|
} else if (current instanceof StyledText) {
|
|
415
431
|
for (const chunk of current.chunks) {
|
|
416
|
-
const chunkNode =
|
|
432
|
+
const chunkNode = TextNode.getTextNodeFromChunk(chunk);
|
|
417
433
|
if (!chunkNode)
|
|
418
434
|
continue;
|
|
419
435
|
_removeNode(parent, chunkNode);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const log: (...args: any[]) => void;
|