This commit is contained in:
Mike Sawka 2024-10-25 13:45:00 -07:00 committed by GitHub
parent ac6f9a05d4
commit 416c26c1cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 284 additions and 46 deletions

View File

@ -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:

View File

@ -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(`
<style>
.root {
padding: 10px;
}
.background {
display: flex;
align-items: center;
width: 100%;
.background-inner {
max-width: 300px;
.bg-item {
cursor: pointer;
padding: 8px 12px;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: flex-start;
&:hover {
background-color: var(--button-grey-hover-bg);
}
.bg-preview {
width: 20px;
height: 20px;
margin-right: 10px;
border-radius: 50%;
border: 1px solid #777;
}
.bg-label {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
</style>
`, 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(`
<div className="bg-item" onClick="#param:clickHandler">
<div className="bg-preview" style="background: #param:bg"></div>
<div className="bg-label"><bindparam key="label"/></div>
</div>`, 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(`
<div className="background">
<div className="background-inner">
<bindparam key="bgElems"/>
</div>
</div>
`, map[string]any{"bgElems": bgElems})
}
func MakeVDom() *vdom.VDomElem {
vdomStr := `
<div>
<h1 style="color:red; background-color: #bind:$.bgcolor; border-radius: 4px; padding: 5px;">hello vdom world</h1>
<div><bind key="$.text"/> | num[<bind key="$.num"/>]</div>
<div className="root">
<StyleTag/>
<h1>Set Background</h1>
<div>
<button data-text="hello" onClick='#globalevent:clickinc'>increment</button>
<wave:markdown text="*quick vdom application to set background colors*"/>
</div>
<div>
<wave:markdown text="*hello from markdown*"/>
<AllBgItemsTag/>
</div>
</div>
`
@ -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 {

View File

@ -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";

View File

@ -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,

View File

@ -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<CssNode>, list: List<CssNode>) {
// 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;
}
}

View File

@ -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 (
<div className="vdom-view" ref={viewRef}>
<VDomRoot model={model} />
</div>
);
}
export { makeVDomModel, VDomView };

View File

@ -0,0 +1,5 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.view-vdom {
}

View File

@ -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<string, VDomReactTagType> = {
"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 <style>{sanitizedCss}</style>;
}
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 <StyleTag elem={elem} model={model} />;
}
if (!AllowedSimpleTags[elem.tag]) {
return <div>{"Invalid Tag <" + elem.tag + ">"}</div>;
}
let childrenComps = convertChildren(elem, model);
@ -280,4 +331,24 @@ function VDomRoot({ model }: { model: VDomModel }) {
return <div className="vdom">{rtn}</div>;
}
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 (
<div className={clsx("vdom-view", vdomClass)} ref={viewRef}>
<VDomRoot model={model} />
</div>
);
}
export { makeVDomModel, VDomView };

View File

@ -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)

View File

@ -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)
}
})
}