* get RefOperations and RefUpdates working. 
* implement a <canvas> API that can be called using RefOperations
* prop to disable rehype/markdown (memory leak)
This commit is contained in:
Mike Sawka 2024-11-05 23:07:45 -08:00 committed by GitHub
parent 61d6b4d8eb
commit 42e85aea6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 196 additions and 49 deletions

View File

@ -130,6 +130,8 @@ var App = vdomclient.DefineComponent[struct{}](HtmlVDomClient, "App",
vdom.E("div", nil,
vdom.E("wave:markdown",
vdom.P("text", "*quick vdom application to set background colors*"),
vdom.P("scrollable", false),
vdom.P("rehype", false),
),
),
vdom.E("div", nil,

View File

@ -180,6 +180,7 @@ type MarkdownProps = {
onClickExecute?: (cmd: string) => void;
resolveOpts?: MarkdownResolveOpts;
scrollable?: boolean;
rehype?: boolean;
};
const Markdown = ({
@ -190,6 +191,7 @@ const Markdown = ({
className,
resolveOpts,
scrollable = true,
rehype = true,
onClickExecute,
}: MarkdownProps) => {
const textAtomValue = useAtomValueSafe<string>(textAtom);
@ -250,6 +252,29 @@ const Markdown = ({
}, [showToc, tocRef]);
text = textAtomValue ?? text;
let rehypePlugins = null;
if (rehype) {
rehypePlugins = [
rehypeRaw,
rehypeHighlight,
() =>
rehypeSanitize({
...defaultSchema,
attributes: {
...defaultSchema.attributes,
span: [
...(defaultSchema.attributes?.span || []),
// Allow all class names starting with `hljs-`.
["className", /^hljs-./],
// Alternatively, to allow only certain class names:
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
],
},
tagNames: [...(defaultSchema.tagNames || []), "span"],
}),
() => rehypeSlug({ prefix: idPrefix }),
];
}
const ScrollableMarkdown = () => {
return (
@ -260,26 +285,7 @@ const Markdown = ({
>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkAlert, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
rehypePlugins={[
rehypeRaw,
rehypeHighlight,
() =>
rehypeSanitize({
...defaultSchema,
attributes: {
...defaultSchema.attributes,
span: [
...(defaultSchema.attributes?.span || []),
// Allow all class names starting with `hljs-`.
["className", /^hljs-./],
// Alternatively, to allow only certain class names:
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
],
},
tagNames: [...(defaultSchema.tagNames || []), "span"],
}),
() => rehypeSlug({ prefix: idPrefix }),
]}
rehypePlugins={rehypePlugins}
components={markdownComponents}
>
{text}
@ -293,26 +299,7 @@ const Markdown = ({
<div className="content non-scrollable">
<ReactMarkdown
remarkPlugins={[remarkGfm, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
rehypePlugins={[
rehypeRaw,
rehypeHighlight,
() =>
rehypeSanitize({
...defaultSchema,
attributes: {
...defaultSchema.attributes,
span: [
...(defaultSchema.attributes?.span || []),
// Allow all class names starting with `hljs-`.
["className", /^hljs-./],
// Alternatively, to allow only certain class names:
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
],
},
tagNames: [...(defaultSchema.tagNames || []), "span"],
}),
() => rehypeSlug({ prefix: idPrefix }),
]}
rehypePlugins={rehypePlugins}
components={markdownComponents}
>
{text}

View File

@ -9,7 +9,7 @@ import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
import { RpcApi } from "@/app/store/wshclientapi";
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
import { mergeBackendUpdates, restoreVDomElems } from "@/app/view/vdom/vdom-utils";
import { applyCanvasOp, mergeBackendUpdates, restoreVDomElems } from "@/app/view/vdom/vdom-utils";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import debug from "debug";
import * as jotai from "jotai";
@ -94,7 +94,7 @@ class VDomWshClient extends WshClient {
}
handle_vdomasyncinitiation(rh: RpcResponseHelper, data: VDomAsyncInitiationRequest) {
console.log("async-initiation", rh.getSource(), data);
dlog("async-initiation", rh.getSource(), data);
this.model.queueUpdate(true);
}
}
@ -130,6 +130,9 @@ export class VDomModel {
persist: jotai.Atom<boolean>;
routeGoneUnsub: () => void;
routeConfirmed: boolean = false;
refOutputStore: Map<string, any> = new Map();
globalVersion: jotai.PrimitiveAtom<number> = jotai.atom(0);
hasBackendWork: boolean = false;
constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "vdom";
@ -201,6 +204,9 @@ export class VDomModel {
this.needsImmediateUpdate = false;
this.lastUpdateTs = 0;
this.queuedUpdate = null;
this.refOutputStore.clear();
this.globalVersion = jotai.atom(0);
this.hasBackendWork = false;
globalStore.set(this.contextActive, false);
}
@ -537,6 +543,10 @@ export class VDomModel {
this.addErrorMessage(`Could not find ref with id ${refOp.refid}`);
continue;
}
if (elem instanceof HTMLCanvasElement) {
applyCanvasOp(elem, refOp, this.refOutputStore);
continue;
}
if (refOp.op == "focus") {
if (elem == null) {
this.addErrorMessage(`Could not focus ref with id ${refOp.refid}: elem is null`);
@ -575,7 +585,17 @@ export class VDomModel {
}
}
}
globalStore.set(this.globalVersion, globalStore.get(this.globalVersion) + 1);
if (update.haswork) {
this.hasBackendWork = true;
}
}
renderDone(version: number) {
// called when the render is done
dlog("renderDone", version);
if (this.hasRefUpdates() || this.hasBackendWork) {
this.hasBackendWork = false;
this.queueUpdate(true);
}
}

View File

@ -197,3 +197,50 @@ export function mergeBackendUpdates(baseUpdate: VDomBackendUpdate, nextUpdate: V
baseUpdate.statesync.push(...nextUpdate.statesync);
}
}
export function applyCanvasOp(canvas: HTMLCanvasElement, canvasOp: VDomRefOperation, refStore: Map<string, any>) {
const ctx = canvas.getContext("2d");
if (!ctx) {
console.error("Canvas 2D context not available.");
return;
}
let { op, params, outputref } = canvasOp;
if (params == null) {
params = [];
}
if (op == null || op == "") {
return;
}
// Resolve any reference parameters in params
const resolvedParams: any[] = [];
params.forEach((param) => {
if (typeof param === "string" && param.startsWith("#ref:")) {
const refId = param.slice(5); // Remove "#ref:" prefix
resolvedParams.push(refStore.get(refId));
} else if (typeof param === "string" && param.startsWith("#spreadRef:")) {
const refId = param.slice(11); // Remove "#spreadRef:" prefix
const arrayRef = refStore.get(refId);
if (Array.isArray(arrayRef)) {
resolvedParams.push(...arrayRef); // Spread array elements
} else {
console.error(`Reference ${refId} is not an array and cannot be spread.`);
}
} else {
resolvedParams.push(param);
}
});
// Apply the operation on the canvas context
if (op === "dropRef" && params.length > 0 && typeof params[0] === "string") {
refStore.delete(params[0]);
} else if (op === "addRef" && outputref) {
refStore.set(outputref, resolvedParams[0]);
} else if (typeof ctx[op as keyof CanvasRenderingContext2D] === "function") {
(ctx[op as keyof CanvasRenderingContext2D] as Function).apply(ctx, resolvedParams);
} else if (op in ctx) {
(ctx as any)[op] = resolvedParams[0];
} else {
console.error(`Unsupported canvas operation: ${op}`);
}
}

View File

@ -72,6 +72,7 @@ const AllowedSimpleTags: { [tagName: string]: boolean } = {
br: true,
pre: true,
code: true,
canvas: true,
};
const AllowedSvgTags = {
@ -350,7 +351,13 @@ function useVDom(model: VDomModel, elem: VDomElem): GenericPropsType {
function WaveMarkdown({ elem, model }: { elem: VDomElem; model: VDomModel }) {
const props = useVDom(model, elem);
return (
<Markdown text={props?.text} style={props?.style} className={props?.className} scrollable={props?.scrollable} />
<Markdown
text={props?.text}
style={props?.style}
className={props?.className}
scrollable={props?.scrollable}
rehype={props?.rehype}
/>
);
}
@ -452,11 +459,15 @@ const testVDom: VDomElem = {
};
function VDomRoot({ model }: { model: VDomModel }) {
let version = jotai.useAtomValue(model.globalVersion);
let rootNode = jotai.useAtomValue(model.vdomRoot);
React.useEffect(() => {
model.renderDone(version);
}, [version]);
if (model.viewRef.current == null || rootNode == null) {
return null;
}
dlog("render", rootNode);
dlog("render", version, rootNode);
let rtn = convertElemToTag(rootNode, model);
return <div className="vdom">{rtn}</div>;
}

View File

@ -741,6 +741,7 @@ declare global {
refid: string;
op: string;
params?: any[];
outputref?: string;
};
// vdom.VDomRefPosition

View File

@ -346,6 +346,28 @@ func UseId(ctx context.Context) string {
return vc.Comp.WaveId
}
func UseRenderTs(ctx context.Context) int64 {
vc := getRenderContext(ctx)
if vc == nil {
panic("UseRenderTs must be called within a component (no context)")
}
return vc.Root.RenderTs
}
func QueueRefOp(ctx context.Context, ref *VDomRef, op VDomRefOperation) {
if ref == nil || !ref.HasCurrent {
return
}
vc := getRenderContext(ctx)
if vc == nil {
panic("QueueRefOp must be called within a component (no context)")
}
if op.RefId == "" {
op.RefId = ref.RefId
}
vc.Root.QueueRefOp(op)
}
func depsEqual(deps1 []any, deps2 []any) bool {
if len(deps1) != len(deps2) {
return false

View File

@ -6,7 +6,10 @@ package vdom
import (
"context"
"fmt"
"log"
"reflect"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
@ -36,11 +39,13 @@ type Atom struct {
type RootElem struct {
OuterCtx context.Context
Root *ComponentImpl
RenderTs int64
CFuncs map[string]any
CompMap map[string]*ComponentImpl // component waveid -> component
EffectWorkQueue []*EffectWorkElem
NeedsRenderMap map[string]bool
Atoms map[string]*Atom
RefOperations []VDomRefOperation
}
const (
@ -414,6 +419,49 @@ func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **ComponentIm
r.render(rtnElem, &(*comp).Comp)
}
func (r *RootElem) UpdateRef(updateRef VDomRefUpdate) {
refId := updateRef.RefId
split := strings.SplitN(refId, ":", 2)
if len(split) != 2 {
log.Printf("invalid ref id: %s\n", refId)
return
}
waveId := split[0]
hookIdx, err := strconv.Atoi(split[1])
if err != nil {
log.Printf("invalid ref id (bad hook idx): %s\n", refId)
return
}
comp := r.CompMap[waveId]
if comp == nil {
return
}
if hookIdx < 0 || hookIdx >= len(comp.Hooks) {
return
}
hook := comp.Hooks[hookIdx]
if hook == nil {
return
}
ref, ok := hook.Val.(*VDomRef)
if !ok {
return
}
ref.HasCurrent = updateRef.HasCurrent
ref.Position = updateRef.Position
r.AddRenderWork(waveId)
}
func (r *RootElem) QueueRefOp(op VDomRefOperation) {
r.RefOperations = append(r.RefOperations, op)
}
func (r *RootElem) GetRefOperations() []VDomRefOperation {
ops := r.RefOperations
r.RefOperations = nil
return ops
}
func convertPropsToVDom(props map[string]any) map[string]any {
if len(props) == 0 {
return nil

View File

@ -188,9 +188,10 @@ type VDomRenderUpdate struct {
}
type VDomRefOperation struct {
RefId string `json:"refid"`
Op string `json:"op" tsype:"\"focus\""`
Params []any `json:"params,omitempty"`
RefId string `json:"refid"`
Op string `json:"op"`
Params []any `json:"params,omitempty"`
OutputRef string `json:"outputref,omitempty"`
}
type VDomMessage struct {

View File

@ -211,7 +211,8 @@ func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
RenderUpdates: []vdom.VDomRenderUpdate{
{UpdateType: "root", VDom: renderedVDom},
},
StateSync: c.Root.GetStateSync(true),
RefOperations: c.Root.GetRefOperations(),
StateSync: c.Root.GetStateSync(true),
}, nil
}
@ -228,7 +229,8 @@ func (c *Client) incrementalRender() (*vdom.VDomBackendUpdate, error) {
RenderUpdates: []vdom.VDomRenderUpdate{
{UpdateType: "root", VDom: renderedVDom},
},
StateSync: c.Root.GetStateSync(false),
RefOperations: c.Root.GetRefOperations(),
StateSync: c.Root.GetStateSync(false),
}, nil
}

View File

@ -46,6 +46,8 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
return respChan
}
impl.Client.Root.RenderTs = feUpdate.Ts
// set atoms
for _, ss := range feUpdate.StateSync {
impl.Client.Root.SetAtomVal(ss.Atom, ss.Value, false)
@ -60,6 +62,10 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
impl.Client.Root.Event(event.WaveId, event.EventType, event)
}
}
// update refs
for _, ref := range feUpdate.RefUpdates {
impl.Client.Root.UpdateRef(ref)
}
var update *vdom.VDomBackendUpdate
var err error