diff --git a/Taskfile.yml b/Taskfile.yml
index 7f6056a62..ead74e104 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -159,7 +159,7 @@ tasks:
vars:
GOOS: darwin
GOARCH: arm64
- - cp dist/bin/wsh-{{.VERSION}}-darwin.arm64 ~/.waveterm-dev/bin/wsh
+ - cp dist/bin/wsh-{{.VERSION}}-darwin.arm64 ~/Library/Application\ Support/waveterm-dev/bin/wsh
build:wsh:internal:
vars:
diff --git a/cmd/wsh/cmd/wshcmd-html.go b/cmd/wsh/cmd/wshcmd-html.go
index b30ad3603..cd48216ef 100644
--- a/cmd/wsh/cmd/wshcmd-html.go
+++ b/cmd/wsh/cmd/wshcmd-html.go
@@ -4,16 +4,21 @@
package cmd
import (
+ "context"
"log"
"time"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/vdom"
"github.com/wavetermdev/waveterm/pkg/vdom/vdomclient"
+ "github.com/wavetermdev/waveterm/pkg/waveobj"
+ "github.com/wavetermdev/waveterm/pkg/wshrpc"
+ "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
"github.com/wavetermdev/waveterm/pkg/wshutil"
)
var htmlCmdNewBlock bool
+var GlobalVDomClient *vdomclient.Client
func init() {
htmlCmd.Flags().BoolVarP(&htmlCmdNewBlock, "newblock", "n", false, "create a new block")
@@ -27,16 +32,116 @@ var htmlCmd = &cobra.Command{
RunE: htmlRun,
}
+func StyleTag(ctx context.Context, props map[string]any) any {
+ return vdom.Bind(`
+
+ `, nil)
+}
+
+func BgItemTag(ctx context.Context, props map[string]any) any {
+ clickFn := func() {
+ log.Printf("bg item clicked %q\n", props["bg"])
+ blockInfo, err := wshclient.BlockInfoCommand(GlobalVDomClient.RpcClient, GlobalVDomClient.RpcContext.BlockId, nil)
+ if err != nil {
+ log.Printf("error getting block info: %v\n", err)
+ return
+ }
+ log.Printf("block info: tabid=%q\n", blockInfo.TabId)
+ err = wshclient.SetMetaCommand(GlobalVDomClient.RpcClient, wshrpc.CommandSetMetaData{
+ ORef: waveobj.ORef{OType: "tab", OID: blockInfo.TabId},
+ Meta: map[string]any{"bg": props["bg"]},
+ }, nil)
+ if err != nil {
+ log.Printf("error setting meta: %v\n", err)
+ }
+ // wshclient.SetMetaCommand(GlobalVDomClient.RpcClient)
+ }
+ params := map[string]any{
+ "bg": props["bg"],
+ "label": props["label"],
+ "clickHandler": clickFn,
+ }
+ return vdom.Bind(`
+
`, params)
+}
+
+func AllBgItemsTag(ctx context.Context, props map[string]any) any {
+ items := []map[string]any{
+ {"bg": nil, "label": "default"},
+ {"bg": "#ff0000", "label": "red"},
+ {"bg": "#00ff00", "label": "green"},
+ {"bg": "#0000ff", "label": "blue"},
+ }
+ bgElems := make([]*vdom.VDomElem, 0)
+ for _, item := range items {
+ elem := vdom.E("BgItemTag", item)
+ bgElems = append(bgElems, elem)
+ }
+ return vdom.Bind(`
+
+ `, map[string]any{"bgElems": bgElems})
+}
+
func MakeVDom() *vdom.VDomElem {
vdomStr := `
-
-
hello vdom world
-
| num[]
+
+
+
Set Background
-
+
`
@@ -58,11 +163,15 @@ func htmlRun(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
+ GlobalVDomClient = client
client.SetGlobalEventHandler(GlobalEventHandler)
log.Printf("created client: %v\n", client)
client.SetAtomVal("bgcolor", "#0000ff77")
client.SetAtomVal("text", "initial text")
client.SetAtomVal("num", 0)
+ client.RegisterComponent("StyleTag", StyleTag)
+ client.RegisterComponent("BgItemTag", BgItemTag)
+ client.RegisterComponent("AllBgItemsTag", AllBgItemsTag)
client.SetRootElem(MakeVDom())
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: htmlCmdNewBlock})
if err != nil {
diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx
index 741fa66ef..7b70678a1 100644
--- a/frontend/app/block/block.tsx
+++ b/frontend/app/block/block.tsx
@@ -5,8 +5,8 @@ import { BlockComponentModel2, BlockProps } from "@/app/block/blocktypes";
import { PlotView } from "@/app/view/plotview/plotview";
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
import { SysinfoView, SysinfoViewModel, makeSysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
-import { VDomModel } from "@/app/view/term/vdom-model";
-import { VDomView, makeVDomModel } from "@/app/view/vdom/vdom-view";
+import { VDomView, makeVDomModel } from "@/app/view/vdom/vdom";
+import { VDomModel } from "@/app/view/vdom/vdom-model";
import { ErrorBoundary } from "@/element/errorboundary";
import { CenteredDiv } from "@/element/quickelems";
import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index";
diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx
index 2b8c6ad01..eab67e915 100644
--- a/frontend/app/view/term/term.tsx
+++ b/frontend/app/view/term/term.tsx
@@ -8,7 +8,7 @@ import { RpcApi } from "@/app/store/wshclientapi";
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
import { TermWshClient } from "@/app/view/term/term-wsh";
-import { VDomModel } from "@/app/view/term/vdom-model";
+import { VDomModel } from "@/app/view/vdom/vdom-model";
import { NodeModel } from "@/layout/index";
import {
WOS,
diff --git a/frontend/app/view/term/vdom-model.tsx b/frontend/app/view/vdom/vdom-model.tsx
similarity index 100%
rename from frontend/app/view/term/vdom-model.tsx
rename to frontend/app/view/vdom/vdom-model.tsx
diff --git a/frontend/app/view/vdom/vdom-utils.tsx b/frontend/app/view/vdom/vdom-utils.tsx
new file mode 100644
index 000000000..4df721a63
--- /dev/null
+++ b/frontend/app/view/vdom/vdom-utils.tsx
@@ -0,0 +1,58 @@
+// Copyright 2024, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+import { VDomModel } from "@/app/view/vdom/vdom-model";
+import type { CssNode, List, ListItem } from "css-tree";
+import * as csstree from "css-tree";
+
+const TextTag = "#text";
+
+// TODO support binding
+export function getTextChildren(elem: VDomElem): string {
+ if (elem.tag == TextTag) {
+ return elem.text;
+ }
+ if (!elem.children) {
+ return null;
+ }
+ const textArr = elem.children.map((child) => {
+ return getTextChildren(child);
+ });
+ return textArr.join("");
+}
+
+export function convertVDomId(model: VDomModel, id: string): string {
+ return model.blockId + "::" + id;
+}
+
+export function validateAndWrapCss(model: VDomModel, cssText: string, wrapperClassName: string) {
+ try {
+ const ast = csstree.parse(cssText);
+ csstree.walk(ast, {
+ enter(node: CssNode, item: ListItem
, list: List) {
+ // Remove disallowed @rules
+ const blockedRules = ["import", "font-face", "keyframes", "namespace", "supports"];
+ if (node.type === "Atrule" && blockedRules.includes(node.name)) {
+ list.remove(item);
+ }
+ // Remove :root selectors
+ if (
+ node.type === "Selector" &&
+ node.children.some((child) => child.type === "PseudoClassSelector" && child.name === "root")
+ ) {
+ list.remove(item);
+ }
+
+ if (node.type === "IdSelector") {
+ node.name = convertVDomId(model, node.name);
+ }
+ },
+ });
+ const sanitizedCss = csstree.generate(ast);
+ return `.${wrapperClassName} { ${sanitizedCss} }`;
+ } catch (error) {
+ // TODO better error handling
+ console.error("CSS processing error:", error);
+ return null;
+ }
+}
diff --git a/frontend/app/view/vdom/vdom-view.tsx b/frontend/app/view/vdom/vdom-view.tsx
deleted file mode 100644
index 124149078..000000000
--- a/frontend/app/view/vdom/vdom-view.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2024, Command Line Inc.
-// SPDX-License-Identifier: Apache-2.0
-
-import { VDomRoot } from "@/app/view/term/vdom";
-import { VDomModel } from "@/app/view/term/vdom-model";
-import { NodeModel } from "@/layout/index";
-import { useRef } from "react";
-
-function makeVDomModel(blockId: string, nodeModel: NodeModel): VDomModel {
- return new VDomModel(blockId, nodeModel);
-}
-
-type VDomViewProps = {
- model: VDomModel;
- blockId: string;
-};
-
-function VDomView({ blockId, model }: VDomViewProps) {
- let viewRef = useRef(null);
- model.viewRef = viewRef;
- return (
-
-
-
- );
-}
-
-export { makeVDomModel, VDomView };
diff --git a/frontend/app/view/vdom/vdom.less b/frontend/app/view/vdom/vdom.less
new file mode 100644
index 000000000..3e959889d
--- /dev/null
+++ b/frontend/app/view/vdom/vdom.less
@@ -0,0 +1,5 @@
+// Copyright 2024, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+.view-vdom {
+}
diff --git a/frontend/app/view/term/vdom.tsx b/frontend/app/view/vdom/vdom.tsx
similarity index 80%
rename from frontend/app/view/term/vdom.tsx
rename to frontend/app/view/vdom/vdom.tsx
index fcea0714e..8f8706c05 100644
--- a/frontend/app/view/term/vdom.tsx
+++ b/frontend/app/view/vdom/vdom.tsx
@@ -2,16 +2,22 @@
// SPDX-License-Identifier: Apache-2.0
import { Markdown } from "@/app/element/markdown";
-import { VDomModel } from "@/app/view/term/vdom-model";
+import { VDomModel } from "@/app/view/vdom/vdom-model";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
+import clsx from "clsx";
import debug from "debug";
import * as jotai from "jotai";
import * as React from "react";
+import { convertVDomId, getTextChildren, validateAndWrapCss } from "@/app/view/vdom/vdom-utils";
+import { NodeModel } from "@/layout/index";
+import "./vdom.less";
+
const TextTag = "#text";
const FragmentTag = "#fragment";
const WaveTextTag = "wave:text";
const WaveNullTag = "wave:null";
+const StyleTagName = "style";
const VDomObjType_Ref = "ref";
const VDomObjType_Binding = "binding";
@@ -25,7 +31,7 @@ const WaveTagMap: Record = {
"wave:markdown": WaveMarkdown,
};
-const AllowedTags: { [tagName: string]: boolean } = {
+const AllowedSimpleTags: { [tagName: string]: boolean } = {
div: true,
b: true,
i: true,
@@ -49,6 +55,30 @@ const AllowedTags: { [tagName: string]: boolean } = {
select: true,
option: true,
form: true,
+ label: true,
+ table: true,
+ thead: true,
+ tbody: true,
+ tr: true,
+ th: true,
+ td: true,
+ hr: true,
+ br: true,
+ pre: true,
+ code: true,
+};
+
+const IdAttributes = {
+ id: true,
+ for: true,
+ "aria-labelledby": true,
+ "aria-describedby": true,
+ "aria-controls": true,
+ "aria-owns": true,
+ form: true,
+ headers: true,
+ usemap: true,
+ list: true,
};
function convertVDomFunc(model: VDomModel, fnDecl: VDomFunc, compId: string, propName: string): (e: any) => void {
@@ -165,6 +195,10 @@ function convertProps(elem: VDomElem, model: VDomModel): [GenericPropsType, Set<
}
// fallthrough to set props[key] = val
}
+ if (IdAttributes[key]) {
+ props[key] = convertVDomId(model, val);
+ continue;
+ }
props[key] = val;
}
return [props, atomKeys];
@@ -223,6 +257,20 @@ function WaveMarkdown({ elem, model }: { elem: VDomElem; model: VDomModel }) {
);
}
+function StyleTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
+ const styleText = getTextChildren(elem);
+ if (styleText == null) {
+ return null;
+ }
+ const wrapperClassName = "vdom-" + model.blockId;
+ // TODO handle errors
+ const sanitizedCss = validateAndWrapCss(model, styleText, wrapperClassName);
+ if (sanitizedCss == null) {
+ return null;
+ }
+ return ;
+}
+
function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
const props = useVDom(model, elem);
if (elem.tag == WaveNullTag) {
@@ -235,7 +283,10 @@ function VDomTag({ elem, model }: { elem: VDomElem; model: VDomModel }) {
if (waveTag) {
return waveTag({ elem, model });
}
- if (!AllowedTags[elem.tag]) {
+ if (elem.tag == StyleTagName) {
+ return ;
+ }
+ if (!AllowedSimpleTags[elem.tag]) {
return {"Invalid Tag <" + elem.tag + ">"}
;
}
let childrenComps = convertChildren(elem, model);
@@ -280,4 +331,24 @@ function VDomRoot({ model }: { model: VDomModel }) {
return {rtn}
;
}
-export { VDomRoot };
+function makeVDomModel(blockId: string, nodeModel: NodeModel): VDomModel {
+ return new VDomModel(blockId, nodeModel);
+}
+
+type VDomViewProps = {
+ model: VDomModel;
+ blockId: string;
+};
+
+function VDomView({ blockId, model }: VDomViewProps) {
+ let viewRef = React.useRef(null);
+ model.viewRef = viewRef;
+ const vdomClass = "vdom-" + blockId;
+ return (
+
+
+
+ );
+}
+
+export { makeVDomModel, VDomView };
diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go
index b0a0c9761..79ee5d743 100644
--- a/pkg/vdom/vdomclient/vdomclient.go
+++ b/pkg/vdom/vdomclient/vdomclient.go
@@ -197,6 +197,10 @@ func makeNullVDom() *vdom.VDomElem {
return &vdom.VDomElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
}
+func (c *Client) RegisterComponent(name string, cfunc vdom.CFunc) {
+ c.Root.RegisterComponent(name, cfunc)
+}
+
func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
c.Root.RunWork()
c.Root.Render(c.RootElem)
diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go
index 602842320..de7aa5c59 100644
--- a/pkg/wstore/wstore_dbops.go
+++ b/pkg/wstore/wstore_dbops.go
@@ -270,11 +270,30 @@ func DBFindWindowForTabId(ctx context.Context, tabId string) (string, error) {
func DBFindTabForBlockId(ctx context.Context, blockId string) (string, error) {
return WithTxRtn(ctx, func(tx *TxWrap) (string, error) {
- query := `
- SELECT t.oid
- FROM db_tab t, json_each(data->'blockids') je
- WHERE je.value = ?;`
- return tx.GetString(query, blockId), nil
+ iterNum := 1
+ for {
+ if iterNum > 5 {
+ return "", fmt.Errorf("too many iterations looking for tab in block parents")
+ }
+ query := `
+ SELECT json_extract(b.data, '$.parentoref') AS parentoref
+ FROM db_block b
+ WHERE b.oid = ?;`
+ parentORef := tx.GetString(query, blockId)
+ oref, err := waveobj.ParseORef(parentORef)
+ if err != nil {
+ return "", fmt.Errorf("bad block parent oref: %v", err)
+ }
+ if oref.OType == "tab" {
+ return oref.OID, nil
+ }
+ if oref.OType == "block" {
+ blockId = oref.OID
+ iterNum++
+ continue
+ }
+ return "", fmt.Errorf("bad parent oref type: %v", oref.OType)
+ }
})
}