@alloy-js/core 0.22.0-dev.1 → 0.22.0-dev.4
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/dist/src/components/Block.d.ts.map +1 -1
- package/dist/src/components/Block.js +24 -7
- package/dist/src/components/Block.js.map +1 -1
- package/dist/src/components/Indent.d.ts.map +1 -1
- package/dist/src/components/Indent.js +2 -1
- package/dist/src/components/Indent.js.map +1 -1
- package/dist/src/components/Prose.d.ts.map +1 -1
- package/dist/src/components/Prose.js +2 -1
- package/dist/src/components/Prose.js.map +1 -1
- package/dist/src/content-slot.d.ts +51 -0
- package/dist/src/content-slot.d.ts.map +1 -0
- package/dist/src/content-slot.js +69 -0
- package/dist/src/content-slot.js.map +1 -0
- package/dist/src/content-slot.test.d.ts +2 -0
- package/dist/src/content-slot.test.d.ts.map +1 -0
- package/dist/src/content-slot.test.js +57 -0
- package/dist/src/content-slot.test.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/reactivity.d.ts +15 -1
- package/dist/src/reactivity.d.ts.map +1 -1
- package/dist/src/reactivity.js +20 -8
- package/dist/src/reactivity.js.map +1 -1
- package/dist/src/render.d.ts +24 -0
- package/dist/src/render.d.ts.map +1 -1
- package/dist/src/render.js +98 -2
- package/dist/src/render.js.map +1 -1
- package/dist/src/symbols/decl.d.ts +8 -0
- package/dist/src/symbols/decl.d.ts.map +1 -0
- package/dist/src/symbols/decl.js +22 -0
- package/dist/src/symbols/decl.js.map +1 -0
- package/dist/src/symbols/index.d.ts +1 -0
- package/dist/src/symbols/index.d.ts.map +1 -1
- package/dist/src/symbols/index.js +1 -0
- package/dist/src/symbols/index.js.map +1 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +212 -15
- package/dist/src/utils.js.map +1 -1
- package/dist/test/components/block.test.d.ts.map +1 -1
- package/dist/test/components/block.test.js +18 -1
- package/dist/test/components/block.test.js.map +1 -1
- package/dist/test/components/list.test.d.ts.map +1 -1
- package/dist/test/components/list.test.js +80 -1
- package/dist/test/components/list.test.js.map +1 -1
- package/dist/test/control-flow/for.test.js +32 -2
- package/dist/test/control-flow/for.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/components/Block.tsx +18 -6
- package/src/components/Indent.tsx +4 -2
- package/src/components/Prose.tsx +2 -1
- package/src/content-slot.test.tsx +65 -0
- package/src/content-slot.tsx +91 -0
- package/src/index.ts +1 -0
- package/src/reactivity.ts +38 -5
- package/src/render.ts +112 -3
- package/src/symbols/decl.ts +25 -0
- package/src/symbols/index.ts +1 -0
- package/src/utils.tsx +240 -16
- package/temp/api.json +550 -4
- package/test/components/block.test.tsx +21 -1
- package/test/components/list.test.tsx +76 -1
- package/test/control-flow/for.test.tsx +43 -2
package/src/components/Block.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed } from "@vue/reactivity";
|
|
2
|
+
import { createContentSlot } from "../content-slot.jsx";
|
|
2
3
|
import type { Children } from "../runtime/component.js";
|
|
3
|
-
import { childrenArray } from "../utils.jsx";
|
|
4
4
|
import { Indent } from "./Indent.jsx";
|
|
5
5
|
|
|
6
6
|
export interface BlockProps {
|
|
@@ -33,17 +33,29 @@ export interface BlockProps {
|
|
|
33
33
|
* added after the block, which defaults to `"}"`.
|
|
34
34
|
*/
|
|
35
35
|
export function Block(props: BlockProps) {
|
|
36
|
-
const
|
|
36
|
+
const ContentSlot = createContentSlot();
|
|
37
|
+
const leadingNewline = computed(() => {
|
|
38
|
+
if (!props.newline) return false;
|
|
39
|
+
|
|
40
|
+
// When inline, we want a newline only when content breaks, otherwise a nothing..
|
|
41
|
+
if (props.inline) return <sbr />;
|
|
42
|
+
|
|
43
|
+
// When not inline, we want a hardline when the slot has contents otherwise a space.
|
|
44
|
+
if (ContentSlot.hasContent) return <hbr />;
|
|
45
|
+
|
|
46
|
+
return " ";
|
|
47
|
+
});
|
|
37
48
|
return (
|
|
38
49
|
<group>
|
|
39
|
-
{
|
|
50
|
+
{leadingNewline}
|
|
40
51
|
{props.opener ?? "{"}
|
|
41
52
|
<Indent
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
hardline={!props.inline && ContentSlot.hasContent}
|
|
54
|
+
line={props.inline && ContentSlot.hasContent}
|
|
55
|
+
softline={!props.inline || ContentSlot.isEmpty}
|
|
44
56
|
trailingBreak
|
|
45
57
|
>
|
|
46
|
-
{props.children}
|
|
58
|
+
<ContentSlot>{props.children}</ContentSlot>
|
|
47
59
|
</Indent>
|
|
48
60
|
{props.closer ?? "}"}
|
|
49
61
|
</group>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { computed } from "@vue/reactivity";
|
|
1
2
|
import type { Children } from "../runtime/component.js";
|
|
2
3
|
|
|
3
4
|
export interface IndentProps {
|
|
@@ -40,12 +41,13 @@ export interface IndentProps {
|
|
|
40
41
|
* break suitable for typical blocks of statements but can be configured.
|
|
41
42
|
*/
|
|
42
43
|
export function Indent(props: IndentProps) {
|
|
43
|
-
const breakElem =
|
|
44
|
+
const breakElem = computed(() =>
|
|
44
45
|
props.nobreak ? ""
|
|
45
46
|
: props.hardline ? <hbr />
|
|
46
47
|
: props.softline ? <sbr />
|
|
47
48
|
: props.line ? <br />
|
|
48
|
-
: <hbr
|
|
49
|
+
: <hbr />,
|
|
50
|
+
);
|
|
49
51
|
|
|
50
52
|
return (
|
|
51
53
|
<>
|
package/src/components/Prose.tsx
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ref } from "@vue/reactivity";
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import "../testing/extend-expect.js";
|
|
4
|
+
import { Show } from "./components/Show.jsx";
|
|
5
|
+
import { createContentSlot } from "./content-slot.jsx";
|
|
6
|
+
import { printTree, renderTree } from "./render.js";
|
|
7
|
+
|
|
8
|
+
it("knows when its empty", () => {
|
|
9
|
+
const ContentSlot = createContentSlot();
|
|
10
|
+
|
|
11
|
+
expect(
|
|
12
|
+
<>
|
|
13
|
+
{ContentSlot.hasContent && "{"}
|
|
14
|
+
<ContentSlot>hi</ContentSlot>
|
|
15
|
+
{ContentSlot.hasContent && "}"}
|
|
16
|
+
</>,
|
|
17
|
+
).toRenderTo(`
|
|
18
|
+
{hi}
|
|
19
|
+
`);
|
|
20
|
+
|
|
21
|
+
expect(
|
|
22
|
+
<>
|
|
23
|
+
{ContentSlot.hasContent && "{"}
|
|
24
|
+
<ContentSlot>{false}</ContentSlot>
|
|
25
|
+
{ContentSlot.hasContent && "}"}
|
|
26
|
+
</>,
|
|
27
|
+
).toRenderTo(``);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("is reactive", () => {
|
|
31
|
+
const ContentSlot = createContentSlot();
|
|
32
|
+
const showContent = ref(false);
|
|
33
|
+
|
|
34
|
+
const tree = renderTree(
|
|
35
|
+
<>
|
|
36
|
+
{ContentSlot.isEmpty && "It's empty!"}
|
|
37
|
+
<ContentSlot>
|
|
38
|
+
<Show when={showContent.value}>Content!</Show>
|
|
39
|
+
</ContentSlot>
|
|
40
|
+
</>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(printTree(tree)).toBe(`It's empty!`);
|
|
44
|
+
showContent.value = true;
|
|
45
|
+
expect(printTree(tree)).toBe(`Content!`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("works with WhenEmpty and WhenHasContent", () => {
|
|
49
|
+
const ContentSlot = createContentSlot();
|
|
50
|
+
const showContent = ref(false);
|
|
51
|
+
|
|
52
|
+
const tree = renderTree(
|
|
53
|
+
<>
|
|
54
|
+
<ContentSlot.WhenEmpty>It's empty!</ContentSlot.WhenEmpty>
|
|
55
|
+
<ContentSlot.WhenHasContent>Has content!</ContentSlot.WhenHasContent>
|
|
56
|
+
<ContentSlot>
|
|
57
|
+
<Show when={showContent.value}>Content!</Show>
|
|
58
|
+
</ContentSlot>
|
|
59
|
+
</>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(printTree(tree)).toBe(`It's empty!`);
|
|
63
|
+
showContent.value = true;
|
|
64
|
+
expect(printTree(tree)).toBe(`Has content!Content!`);
|
|
65
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { effect, Ref, shallowRef } from "@vue/reactivity";
|
|
2
|
+
import { Show } from "./components/Show.jsx";
|
|
3
|
+
import { getContext } from "./reactivity.js";
|
|
4
|
+
import { Children, Component } from "./runtime/component.js";
|
|
5
|
+
|
|
6
|
+
export interface ContentSlot {
|
|
7
|
+
(props: { children: Children }): Children;
|
|
8
|
+
/**
|
|
9
|
+
* A ref indicating whether the slot is currently empty.
|
|
10
|
+
*/
|
|
11
|
+
ref: Ref<boolean>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether the slot is currently empty.
|
|
15
|
+
*/
|
|
16
|
+
isEmpty: boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether the slot has any content.
|
|
20
|
+
*/
|
|
21
|
+
hasContent: boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A component that will render its contents when the content slot is empty.
|
|
25
|
+
*/
|
|
26
|
+
WhenEmpty: Component<{}>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A component that will render its contents when the content slot has content.
|
|
30
|
+
*/
|
|
31
|
+
WhenHasContent: Component<{}>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a component which tracks whether any content is placed inside of it.
|
|
36
|
+
* The component exposes a ref `isEmpty` which indicates whether the slot is
|
|
37
|
+
* empty, as well as convenience accessors `isEmpty` and `hasContent`.
|
|
38
|
+
* Additionally, it provides two sub-components, `WhenEmpty` and
|
|
39
|
+
* `WhenHasContent`, which render their contents conditionally based on whether
|
|
40
|
+
* the slot is empty.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
*
|
|
44
|
+
* ```tsx
|
|
45
|
+
* const ContentSlot = createContentSlot();
|
|
46
|
+
*
|
|
47
|
+
* <>
|
|
48
|
+
* <ContentSlot.WhenEmpty>The slot is empty!</ContentSlot.WhenEmpty>
|
|
49
|
+
* <ContentSlot.WhenHasContent>The slot has content!</ContentSlot.WhenHasContent>
|
|
50
|
+
* <ContentSlot>
|
|
51
|
+
* {someCondition && "Here is some content!"}
|
|
52
|
+
* </ContentSlot>
|
|
53
|
+
* </>
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function createContentSlot(): ContentSlot {
|
|
57
|
+
const isEmpty = shallowRef<boolean>(false);
|
|
58
|
+
|
|
59
|
+
function ContentSlot(props: { children: Children }) {
|
|
60
|
+
const context = getContext()!;
|
|
61
|
+
effect(() => {
|
|
62
|
+
isEmpty.value = context.isEmpty!.value;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return props.children;
|
|
66
|
+
}
|
|
67
|
+
ContentSlot.ref = isEmpty;
|
|
68
|
+
ContentSlot.WhenEmpty = function WhenEmpty(props: { children: Children }) {
|
|
69
|
+
return <Show when={isEmpty.value}>{props.children}</Show>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
ContentSlot.WhenHasContent = function WhenHasContent(props: {
|
|
73
|
+
children: Children;
|
|
74
|
+
}) {
|
|
75
|
+
return <Show when={!isEmpty.value}>{props.children}</Show>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
Object.defineProperty(ContentSlot, "isEmpty", {
|
|
79
|
+
get() {
|
|
80
|
+
return isEmpty.value;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
Object.defineProperty(ContentSlot, "hasContent", {
|
|
85
|
+
get() {
|
|
86
|
+
return !isEmpty.value;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return ContentSlot as any;
|
|
91
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
export * from "./binder.js";
|
|
26
26
|
export * from "./code.js";
|
|
27
27
|
export * from "./components/index.js";
|
|
28
|
+
export * from "./content-slot.js";
|
|
28
29
|
export * from "./context.js";
|
|
29
30
|
export * from "./context/index.js";
|
|
30
31
|
export * from "./library-symbol-reference.js";
|
package/src/reactivity.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
isRef,
|
|
2
3
|
pauseTracking,
|
|
3
4
|
ReactiveEffectRunner,
|
|
5
|
+
ref,
|
|
6
|
+
Ref,
|
|
4
7
|
resetTracking,
|
|
5
8
|
ShallowReactive,
|
|
6
9
|
shallowRef,
|
|
@@ -66,6 +69,22 @@ export interface Context {
|
|
|
66
69
|
* The symbol that this component has taken.
|
|
67
70
|
*/
|
|
68
71
|
takenSymbols?: ShallowReactive<Set<OutputSymbol>>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The number of child nodes that have content. When zero, this component is
|
|
75
|
+
* semantically empty.
|
|
76
|
+
*/
|
|
77
|
+
childrenWithContent: number;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A ref that indicates whether the component is empty.
|
|
81
|
+
*/
|
|
82
|
+
isEmpty?: Ref<boolean>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether this context is a root context
|
|
86
|
+
*/
|
|
87
|
+
isRoot: boolean;
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
let globalContext: Context | null = null;
|
|
@@ -86,6 +105,9 @@ export function root<T>(fn: (d: Disposable) => T, options?: RootOptions): T {
|
|
|
86
105
|
elementCache: new Map(),
|
|
87
106
|
takesSymbols: false,
|
|
88
107
|
takenSymbols: undefined,
|
|
108
|
+
childrenWithContent: 0,
|
|
109
|
+
isEmpty: ref(true),
|
|
110
|
+
isRoot: true,
|
|
89
111
|
};
|
|
90
112
|
|
|
91
113
|
globalContext = context;
|
|
@@ -131,6 +153,8 @@ export function effect<T>(fn: (prev?: T) => T, current?: T) {
|
|
|
131
153
|
elementCache: new Map(),
|
|
132
154
|
takesSymbols: false,
|
|
133
155
|
takenSymbols: undefined,
|
|
156
|
+
childrenWithContent: 0,
|
|
157
|
+
isRoot: false,
|
|
134
158
|
};
|
|
135
159
|
|
|
136
160
|
const cleanupFn = (final: boolean) => {
|
|
@@ -162,19 +186,16 @@ export function effect<T>(fn: (prev?: T) => T, current?: T) {
|
|
|
162
186
|
scheduler: scheduler(),
|
|
163
187
|
onTrack(event) {
|
|
164
188
|
trace(TracePhase.effect.track, () => {
|
|
165
|
-
return `tracking ${event.target}, ${String(event.key)}`;
|
|
189
|
+
return `tracking ${isRef(event.target) ? `Ref:${refId(event.target)}` : event.target}, ${String(event.key)}`;
|
|
166
190
|
});
|
|
167
191
|
},
|
|
168
192
|
onTrigger(event) {
|
|
169
193
|
trace(TracePhase.effect.trigger, () => {
|
|
170
|
-
return `triggering ${event.target}, ${String(event.key)}`;
|
|
194
|
+
return `triggering ${isRef(event.target) ? `Ref:${refId(event.target)}` : event.target}, ${String(event.key)}`;
|
|
171
195
|
});
|
|
172
196
|
},
|
|
173
197
|
},
|
|
174
198
|
);
|
|
175
|
-
|
|
176
|
-
// allow recursive effects (recursive option does nothing, possible bug)
|
|
177
|
-
(runner as any).effect.flags |= 1 << 5;
|
|
178
199
|
}
|
|
179
200
|
|
|
180
201
|
/**
|
|
@@ -231,3 +252,15 @@ export function isCustomContext(child: Children): child is CustomContext {
|
|
|
231
252
|
Object.hasOwn(child, CUSTOM_CONTEXT_SYM)
|
|
232
253
|
);
|
|
233
254
|
}
|
|
255
|
+
|
|
256
|
+
const seenRefs = new WeakMap<Ref<unknown>, number>();
|
|
257
|
+
let refIdCounter = 1;
|
|
258
|
+
|
|
259
|
+
export function refId(ref: Ref<unknown>): number {
|
|
260
|
+
let id = seenRefs.get(ref);
|
|
261
|
+
if (id === undefined) {
|
|
262
|
+
id = refIdCounter++;
|
|
263
|
+
seenRefs.set(ref, id);
|
|
264
|
+
}
|
|
265
|
+
return id;
|
|
266
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isRef } from "@vue/reactivity";
|
|
1
|
+
import { isRef, ref } from "@vue/reactivity";
|
|
2
2
|
import { Doc, doc } from "prettier";
|
|
3
3
|
import prettier from "prettier/doc.js";
|
|
4
4
|
import { useContext } from "./context.js";
|
|
@@ -186,6 +186,12 @@ export function isPrintHook(type: unknown): type is PrintHook {
|
|
|
186
186
|
|
|
187
187
|
export type RenderedTextTree = (string | RenderedTextTree | PrintHook)[];
|
|
188
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Render a component tree to source directories and files. Will ensure that
|
|
191
|
+
* all non-async scheduled jobs are completed before returning. If async jobs
|
|
192
|
+
* are found, an error will be thrown. Use `renderAsync` when asynchronous
|
|
193
|
+
* jobs are expected.
|
|
194
|
+
*/
|
|
189
195
|
export function render(
|
|
190
196
|
children: Children,
|
|
191
197
|
options?: PrintTreeOptions,
|
|
@@ -195,15 +201,37 @@ export function render(
|
|
|
195
201
|
return sourceFilesForTree(tree, options);
|
|
196
202
|
}
|
|
197
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Render a component tree to source directories and files. Will ensure that all
|
|
206
|
+
* scheduled jobs are completed before returning.
|
|
207
|
+
*/
|
|
198
208
|
export async function renderAsync(
|
|
199
209
|
children: Children,
|
|
200
210
|
options?: PrintTreeOptions,
|
|
201
211
|
): Promise<OutputDirectory> {
|
|
202
212
|
const tree = renderTree(children);
|
|
213
|
+
return sourceFilesForTreeAsync(tree, options);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Convert a rendered text tree to source directories and files. Will ensure that
|
|
218
|
+
* all scheduled jobs are completed, including async ones.
|
|
219
|
+
*/
|
|
220
|
+
export async function sourceFilesForTreeAsync(
|
|
221
|
+
tree: RenderedTextTree,
|
|
222
|
+
options?: PrintTreeOptions,
|
|
223
|
+
) {
|
|
224
|
+
// if we await here, we ensure all reactive updates are flushed.
|
|
225
|
+
// sourceFilesForTree will flush again, but won't find anything, because tree
|
|
226
|
+
// printing won't schedule anything.
|
|
203
227
|
await flushJobsAsync();
|
|
204
228
|
return sourceFilesForTree(tree, options);
|
|
205
229
|
}
|
|
206
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Convert a rendered text tree to source directories and files. Will ensure
|
|
233
|
+
* that all scheduled jobs are completed before returning.
|
|
234
|
+
*/
|
|
207
235
|
export function sourceFilesForTree(
|
|
208
236
|
tree: RenderedTextTree,
|
|
209
237
|
options?: PrintTreeOptions,
|
|
@@ -332,11 +360,74 @@ function renderWorker(node: RenderedTextTree, children: Children) {
|
|
|
332
360
|
}
|
|
333
361
|
}
|
|
334
362
|
|
|
363
|
+
function contentAdded() {
|
|
364
|
+
const context: Context = getContext()!;
|
|
365
|
+
context.childrenWithContent++;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function notifyContentState() {
|
|
369
|
+
untrack(() => {
|
|
370
|
+
const startContext = getContext()!;
|
|
371
|
+
|
|
372
|
+
if (startContext.childrenWithContent === 0) {
|
|
373
|
+
if (startContext.isEmpty!.value === true) {
|
|
374
|
+
// it was already empty, no work to do.
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (startContext.isEmpty) {
|
|
379
|
+
startContext.isEmpty.value = true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// otherwise we need to decrement the content counts up the tree.
|
|
383
|
+
let current = startContext.owner;
|
|
384
|
+
while (current) {
|
|
385
|
+
if (current.childrenWithContent === 0) {
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
current.childrenWithContent--;
|
|
389
|
+
if (current.isEmpty) {
|
|
390
|
+
current.isEmpty.value = true;
|
|
391
|
+
}
|
|
392
|
+
current = current.owner;
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
if (startContext.isEmpty!.value === false) {
|
|
396
|
+
// it was already non-empty, no work to do.
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (startContext.isEmpty && startContext.isEmpty.value) {
|
|
401
|
+
startContext.isEmpty.value = false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// otherwise we need to increment the content counts up the tree.
|
|
405
|
+
let current = startContext.owner;
|
|
406
|
+
while (current) {
|
|
407
|
+
current.childrenWithContent++;
|
|
408
|
+
if (current.childrenWithContent > 1) {
|
|
409
|
+
// This isn't the first content so we have no work to do
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (current.isEmpty && current.isEmpty.value) {
|
|
414
|
+
current.isEmpty.value = false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
current = current.owner;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
335
423
|
function appendChild(node: RenderedTextTree, rawChild: Child) {
|
|
336
424
|
trace(TracePhase.render.appendChild, () => debugPrintChild(rawChild));
|
|
337
425
|
const child = normalizeChild(rawChild);
|
|
338
426
|
|
|
339
427
|
if (typeof child === "string") {
|
|
428
|
+
if (child !== "") {
|
|
429
|
+
contentAdded();
|
|
430
|
+
}
|
|
340
431
|
node.push(child);
|
|
341
432
|
} else {
|
|
342
433
|
const cache = getElementCache();
|
|
@@ -358,6 +449,7 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
|
|
|
358
449
|
renderWorker(newNode, children);
|
|
359
450
|
node.push(newNode);
|
|
360
451
|
cache.set(child, newNode);
|
|
452
|
+
notifyContentState();
|
|
361
453
|
});
|
|
362
454
|
} else if (isIntrinsicElement(child)) {
|
|
363
455
|
trace(
|
|
@@ -488,6 +580,9 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
|
|
|
488
580
|
() => "Component: " + debugPrintChild(child),
|
|
489
581
|
);
|
|
490
582
|
const context = getContext();
|
|
583
|
+
context!.childrenWithContent = 0;
|
|
584
|
+
context!.isEmpty ??= ref(true);
|
|
585
|
+
|
|
491
586
|
if (context) context.componentOwner = child;
|
|
492
587
|
const componentRoot: RenderedTextTree = [];
|
|
493
588
|
pushStack(child.component, child.props);
|
|
@@ -495,10 +590,14 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
|
|
|
495
590
|
popStack();
|
|
496
591
|
node.push(componentRoot);
|
|
497
592
|
cache.set(child, componentRoot);
|
|
498
|
-
|
|
593
|
+
notifyContentState();
|
|
499
594
|
trace(
|
|
500
595
|
TracePhase.render.appendChild,
|
|
501
|
-
() =>
|
|
596
|
+
() =>
|
|
597
|
+
"Component done: " +
|
|
598
|
+
debugPrintChild(child) +
|
|
599
|
+
", empty: " +
|
|
600
|
+
context!.isEmpty!.value,
|
|
502
601
|
);
|
|
503
602
|
});
|
|
504
603
|
} else if (typeof child === "function") {
|
|
@@ -510,10 +609,16 @@ function appendChild(node: RenderedTextTree, rawChild: Child) {
|
|
|
510
609
|
while (typeof res === "function" && !isComponentCreator(res)) {
|
|
511
610
|
res = res();
|
|
512
611
|
}
|
|
612
|
+
const context = getContext();
|
|
613
|
+
context!.childrenWithContent = 0;
|
|
614
|
+
context!.isEmpty ??= ref(true);
|
|
615
|
+
|
|
513
616
|
const newNodes: RenderedTextTree = [];
|
|
514
617
|
renderWorker(newNodes, res);
|
|
515
618
|
node[index] = newNodes;
|
|
516
619
|
cache.set(child, newNodes);
|
|
620
|
+
|
|
621
|
+
notifyContentState();
|
|
517
622
|
return newNodes;
|
|
518
623
|
});
|
|
519
624
|
} else {
|
|
@@ -618,6 +723,10 @@ const defaultPrintTreeOptions: PrintTreeOptions = {
|
|
|
618
723
|
tabWidth: 2,
|
|
619
724
|
};
|
|
620
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Convert a rendered text tree to a string. Will ensure that the scheduler is
|
|
728
|
+
* empty before printing.
|
|
729
|
+
*/
|
|
621
730
|
export function printTree(tree: RenderedTextTree, options?: PrintTreeOptions) {
|
|
622
731
|
options = {
|
|
623
732
|
...defaultPrintTreeOptions,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { toRef } from "@vue/reactivity";
|
|
2
|
+
import { useScope } from "../context/scope.js";
|
|
3
|
+
import { Namekey } from "../refkey.js";
|
|
4
|
+
import { createComponent } from "../runtime/component.js";
|
|
5
|
+
import { BasicScope } from "./basic-scope.js";
|
|
6
|
+
import { BasicSymbol } from "./basic-symbol.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a declaration in the current scope with the given namekey. Only works
|
|
10
|
+
* with basic scopes. Import `decl` from a specific language library for
|
|
11
|
+
* declaring language-specific symbols.
|
|
12
|
+
*/
|
|
13
|
+
export function decl(namekey: Namekey) {
|
|
14
|
+
return createComponent(() => {
|
|
15
|
+
const currentScope = useScope();
|
|
16
|
+
if (!(currentScope instanceof BasicScope)) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Cannot declare symbol in non-basic scope: ${currentScope.constructor.name}. Use a language-specific 'decl' function instead.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
const symbol = new BasicSymbol(namekey, currentScope.symbols);
|
|
22
|
+
|
|
23
|
+
return toRef(symbol, "name");
|
|
24
|
+
}, {});
|
|
25
|
+
}
|