Skip to content

Commit 42e85ae

Browse files
authored
VDom 10 (#1206)
* get RefOperations and RefUpdates working. * implement a <canvas> API that can be called using RefOperations * prop to disable rehype/markdown (memory leak)
1 parent 61d6b4d commit 42e85ae

File tree

11 files changed

+196
-49
lines changed

11 files changed

+196
-49
lines changed

cmd/wsh/cmd/wshcmd-html.go

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ var App = vdomclient.DefineComponent[struct{}](HtmlVDomClient, "App",
130130
vdom.E("div", nil,
131131
vdom.E("wave:markdown",
132132
vdom.P("text", "*quick vdom application to set background colors*"),
133+
vdom.P("scrollable", false),
134+
vdom.P("rehype", false),
133135
),
134136
),
135137
vdom.E("div", nil,

frontend/app/element/markdown.tsx

+27-40
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ type MarkdownProps = {
180180
onClickExecute?: (cmd: string) => void;
181181
resolveOpts?: MarkdownResolveOpts;
182182
scrollable?: boolean;
183+
rehype?: boolean;
183184
};
184185

185186
const Markdown = ({
@@ -190,6 +191,7 @@ const Markdown = ({
190191
className,
191192
resolveOpts,
192193
scrollable = true,
194+
rehype = true,
193195
onClickExecute,
194196
}: MarkdownProps) => {
195197
const textAtomValue = useAtomValueSafe<string>(textAtom);
@@ -250,6 +252,29 @@ const Markdown = ({
250252
}, [showToc, tocRef]);
251253

252254
text = textAtomValue ?? text;
255+
let rehypePlugins = null;
256+
if (rehype) {
257+
rehypePlugins = [
258+
rehypeRaw,
259+
rehypeHighlight,
260+
() =>
261+
rehypeSanitize({
262+
...defaultSchema,
263+
attributes: {
264+
...defaultSchema.attributes,
265+
span: [
266+
...(defaultSchema.attributes?.span || []),
267+
// Allow all class names starting with `hljs-`.
268+
["className", /^hljs-./],
269+
// Alternatively, to allow only certain class names:
270+
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
271+
],
272+
},
273+
tagNames: [...(defaultSchema.tagNames || []), "span"],
274+
}),
275+
() => rehypeSlug({ prefix: idPrefix }),
276+
];
277+
}
253278

254279
const ScrollableMarkdown = () => {
255280
return (
@@ -260,26 +285,7 @@ const Markdown = ({
260285
>
261286
<ReactMarkdown
262287
remarkPlugins={[remarkGfm, remarkAlert, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
263-
rehypePlugins={[
264-
rehypeRaw,
265-
rehypeHighlight,
266-
() =>
267-
rehypeSanitize({
268-
...defaultSchema,
269-
attributes: {
270-
...defaultSchema.attributes,
271-
span: [
272-
...(defaultSchema.attributes?.span || []),
273-
// Allow all class names starting with `hljs-`.
274-
["className", /^hljs-./],
275-
// Alternatively, to allow only certain class names:
276-
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
277-
],
278-
},
279-
tagNames: [...(defaultSchema.tagNames || []), "span"],
280-
}),
281-
() => rehypeSlug({ prefix: idPrefix }),
282-
]}
288+
rehypePlugins={rehypePlugins}
283289
components={markdownComponents}
284290
>
285291
{text}
@@ -293,26 +299,7 @@ const Markdown = ({
293299
<div className="content non-scrollable">
294300
<ReactMarkdown
295301
remarkPlugins={[remarkGfm, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
296-
rehypePlugins={[
297-
rehypeRaw,
298-
rehypeHighlight,
299-
() =>
300-
rehypeSanitize({
301-
...defaultSchema,
302-
attributes: {
303-
...defaultSchema.attributes,
304-
span: [
305-
...(defaultSchema.attributes?.span || []),
306-
// Allow all class names starting with `hljs-`.
307-
["className", /^hljs-./],
308-
// Alternatively, to allow only certain class names:
309-
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
310-
],
311-
},
312-
tagNames: [...(defaultSchema.tagNames || []), "span"],
313-
}),
314-
() => rehypeSlug({ prefix: idPrefix }),
315-
]}
302+
rehypePlugins={rehypePlugins}
316303
components={markdownComponents}
317304
>
318305
{text}

frontend/app/view/vdom/vdom-model.tsx

+22-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
99
import { RpcApi } from "@/app/store/wshclientapi";
1010
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
1111
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
12-
import { mergeBackendUpdates, restoreVDomElems } from "@/app/view/vdom/vdom-utils";
12+
import { applyCanvasOp, mergeBackendUpdates, restoreVDomElems } from "@/app/view/vdom/vdom-utils";
1313
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
1414
import debug from "debug";
1515
import * as jotai from "jotai";
@@ -94,7 +94,7 @@ class VDomWshClient extends WshClient {
9494
}
9595

9696
handle_vdomasyncinitiation(rh: RpcResponseHelper, data: VDomAsyncInitiationRequest) {
97-
console.log("async-initiation", rh.getSource(), data);
97+
dlog("async-initiation", rh.getSource(), data);
9898
this.model.queueUpdate(true);
9999
}
100100
}
@@ -130,6 +130,9 @@ export class VDomModel {
130130
persist: jotai.Atom<boolean>;
131131
routeGoneUnsub: () => void;
132132
routeConfirmed: boolean = false;
133+
refOutputStore: Map<string, any> = new Map();
134+
globalVersion: jotai.PrimitiveAtom<number> = jotai.atom(0);
135+
hasBackendWork: boolean = false;
133136

134137
constructor(blockId: string, nodeModel: BlockNodeModel) {
135138
this.viewType = "vdom";
@@ -201,6 +204,9 @@ export class VDomModel {
201204
this.needsImmediateUpdate = false;
202205
this.lastUpdateTs = 0;
203206
this.queuedUpdate = null;
207+
this.refOutputStore.clear();
208+
this.globalVersion = jotai.atom(0);
209+
this.hasBackendWork = false;
204210
globalStore.set(this.contextActive, false);
205211
}
206212

@@ -537,6 +543,10 @@ export class VDomModel {
537543
this.addErrorMessage(`Could not find ref with id ${refOp.refid}`);
538544
continue;
539545
}
546+
if (elem instanceof HTMLCanvasElement) {
547+
applyCanvasOp(elem, refOp, this.refOutputStore);
548+
continue;
549+
}
540550
if (refOp.op == "focus") {
541551
if (elem == null) {
542552
this.addErrorMessage(`Could not focus ref with id ${refOp.refid}: elem is null`);
@@ -575,7 +585,17 @@ export class VDomModel {
575585
}
576586
}
577587
}
588+
globalStore.set(this.globalVersion, globalStore.get(this.globalVersion) + 1);
578589
if (update.haswork) {
590+
this.hasBackendWork = true;
591+
}
592+
}
593+
594+
renderDone(version: number) {
595+
// called when the render is done
596+
dlog("renderDone", version);
597+
if (this.hasRefUpdates() || this.hasBackendWork) {
598+
this.hasBackendWork = false;
579599
this.queueUpdate(true);
580600
}
581601
}

frontend/app/view/vdom/vdom-utils.tsx

+47
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,50 @@ export function mergeBackendUpdates(baseUpdate: VDomBackendUpdate, nextUpdate: V
197197
baseUpdate.statesync.push(...nextUpdate.statesync);
198198
}
199199
}
200+
201+
export function applyCanvasOp(canvas: HTMLCanvasElement, canvasOp: VDomRefOperation, refStore: Map<string, any>) {
202+
const ctx = canvas.getContext("2d");
203+
if (!ctx) {
204+
console.error("Canvas 2D context not available.");
205+
return;
206+
}
207+
208+
let { op, params, outputref } = canvasOp;
209+
if (params == null) {
210+
params = [];
211+
}
212+
if (op == null || op == "") {
213+
return;
214+
}
215+
// Resolve any reference parameters in params
216+
const resolvedParams: any[] = [];
217+
params.forEach((param) => {
218+
if (typeof param === "string" && param.startsWith("#ref:")) {
219+
const refId = param.slice(5); // Remove "#ref:" prefix
220+
resolvedParams.push(refStore.get(refId));
221+
} else if (typeof param === "string" && param.startsWith("#spreadRef:")) {
222+
const refId = param.slice(11); // Remove "#spreadRef:" prefix
223+
const arrayRef = refStore.get(refId);
224+
if (Array.isArray(arrayRef)) {
225+
resolvedParams.push(...arrayRef); // Spread array elements
226+
} else {
227+
console.error(`Reference ${refId} is not an array and cannot be spread.`);
228+
}
229+
} else {
230+
resolvedParams.push(param);
231+
}
232+
});
233+
234+
// Apply the operation on the canvas context
235+
if (op === "dropRef" && params.length > 0 && typeof params[0] === "string") {
236+
refStore.delete(params[0]);
237+
} else if (op === "addRef" && outputref) {
238+
refStore.set(outputref, resolvedParams[0]);
239+
} else if (typeof ctx[op as keyof CanvasRenderingContext2D] === "function") {
240+
(ctx[op as keyof CanvasRenderingContext2D] as Function).apply(ctx, resolvedParams);
241+
} else if (op in ctx) {
242+
(ctx as any)[op] = resolvedParams[0];
243+
} else {
244+
console.error(`Unsupported canvas operation: ${op}`);
245+
}
246+
}

frontend/app/view/vdom/vdom.tsx

+13-2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const AllowedSimpleTags: { [tagName: string]: boolean } = {
7272
br: true,
7373
pre: true,
7474
code: true,
75+
canvas: true,
7576
};
7677

7778
const AllowedSvgTags = {
@@ -350,7 +351,13 @@ function useVDom(model: VDomModel, elem: VDomElem): GenericPropsType {
350351
function WaveMarkdown({ elem, model }: { elem: VDomElem; model: VDomModel }) {
351352
const props = useVDom(model, elem);
352353
return (
353-
<Markdown text={props?.text} style={props?.style} className={props?.className} scrollable={props?.scrollable} />
354+
<Markdown
355+
text={props?.text}
356+
style={props?.style}
357+
className={props?.className}
358+
scrollable={props?.scrollable}
359+
rehype={props?.rehype}
360+
/>
354361
);
355362
}
356363

@@ -452,11 +459,15 @@ const testVDom: VDomElem = {
452459
};
453460

454461
function VDomRoot({ model }: { model: VDomModel }) {
462+
let version = jotai.useAtomValue(model.globalVersion);
455463
let rootNode = jotai.useAtomValue(model.vdomRoot);
464+
React.useEffect(() => {
465+
model.renderDone(version);
466+
}, [version]);
456467
if (model.viewRef.current == null || rootNode == null) {
457468
return null;
458469
}
459-
dlog("render", rootNode);
470+
dlog("render", version, rootNode);
460471
let rtn = convertElemToTag(rootNode, model);
461472
return <div className="vdom">{rtn}</div>;
462473
}

frontend/types/gotypes.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,7 @@ declare global {
741741
refid: string;
742742
op: string;
743743
params?: any[];
744+
outputref?: string;
744745
};
745746

746747
// vdom.VDomRefPosition

pkg/vdom/vdom.go

+22
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,28 @@ func UseId(ctx context.Context) string {
346346
return vc.Comp.WaveId
347347
}
348348

349+
func UseRenderTs(ctx context.Context) int64 {
350+
vc := getRenderContext(ctx)
351+
if vc == nil {
352+
panic("UseRenderTs must be called within a component (no context)")
353+
}
354+
return vc.Root.RenderTs
355+
}
356+
357+
func QueueRefOp(ctx context.Context, ref *VDomRef, op VDomRefOperation) {
358+
if ref == nil || !ref.HasCurrent {
359+
return
360+
}
361+
vc := getRenderContext(ctx)
362+
if vc == nil {
363+
panic("QueueRefOp must be called within a component (no context)")
364+
}
365+
if op.RefId == "" {
366+
op.RefId = ref.RefId
367+
}
368+
vc.Root.QueueRefOp(op)
369+
}
370+
349371
func depsEqual(deps1 []any, deps2 []any) bool {
350372
if len(deps1) != len(deps2) {
351373
return false

pkg/vdom/vdom_root.go

+48
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ package vdom
66
import (
77
"context"
88
"fmt"
9+
"log"
910
"reflect"
11+
"strconv"
12+
"strings"
1013

1114
"github.com/google/uuid"
1215
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
@@ -36,11 +39,13 @@ type Atom struct {
3639
type RootElem struct {
3740
OuterCtx context.Context
3841
Root *ComponentImpl
42+
RenderTs int64
3943
CFuncs map[string]any
4044
CompMap map[string]*ComponentImpl // component waveid -> component
4145
EffectWorkQueue []*EffectWorkElem
4246
NeedsRenderMap map[string]bool
4347
Atoms map[string]*Atom
48+
RefOperations []VDomRefOperation
4449
}
4550

4651
const (
@@ -414,6 +419,49 @@ func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **ComponentIm
414419
r.render(rtnElem, &(*comp).Comp)
415420
}
416421

422+
func (r *RootElem) UpdateRef(updateRef VDomRefUpdate) {
423+
refId := updateRef.RefId
424+
split := strings.SplitN(refId, ":", 2)
425+
if len(split) != 2 {
426+
log.Printf("invalid ref id: %s\n", refId)
427+
return
428+
}
429+
waveId := split[0]
430+
hookIdx, err := strconv.Atoi(split[1])
431+
if err != nil {
432+
log.Printf("invalid ref id (bad hook idx): %s\n", refId)
433+
return
434+
}
435+
comp := r.CompMap[waveId]
436+
if comp == nil {
437+
return
438+
}
439+
if hookIdx < 0 || hookIdx >= len(comp.Hooks) {
440+
return
441+
}
442+
hook := comp.Hooks[hookIdx]
443+
if hook == nil {
444+
return
445+
}
446+
ref, ok := hook.Val.(*VDomRef)
447+
if !ok {
448+
return
449+
}
450+
ref.HasCurrent = updateRef.HasCurrent
451+
ref.Position = updateRef.Position
452+
r.AddRenderWork(waveId)
453+
}
454+
455+
func (r *RootElem) QueueRefOp(op VDomRefOperation) {
456+
r.RefOperations = append(r.RefOperations, op)
457+
}
458+
459+
func (r *RootElem) GetRefOperations() []VDomRefOperation {
460+
ops := r.RefOperations
461+
r.RefOperations = nil
462+
return ops
463+
}
464+
417465
func convertPropsToVDom(props map[string]any) map[string]any {
418466
if len(props) == 0 {
419467
return nil

pkg/vdom/vdom_types.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,10 @@ type VDomRenderUpdate struct {
188188
}
189189

190190
type VDomRefOperation struct {
191-
RefId string `json:"refid"`
192-
Op string `json:"op" tsype:"\"focus\""`
193-
Params []any `json:"params,omitempty"`
191+
RefId string `json:"refid"`
192+
Op string `json:"op"`
193+
Params []any `json:"params,omitempty"`
194+
OutputRef string `json:"outputref,omitempty"`
194195
}
195196

196197
type VDomMessage struct {

0 commit comments

Comments
 (0)