Compare commits

...

5 Commits

Author SHA1 Message Date
sawka f1a6dfb249 testing frame component 2024-04-15 22:59:13 -07:00
sawka 6319b26924 basic waveapp json -> react/html functionality sorta working 2024-04-12 16:25:52 -07:00
sawka 41fe49a54c implement ijson commands 2024-04-12 13:16:26 -07:00
sawka e044487489 tested ts ijson 2024-04-12 12:58:53 -07:00
sawka 0fdcd27fee go ijson implementation (tested), ts ijson implemented (untested) 2024-04-12 01:18:05 -07:00
12 changed files with 1264 additions and 7 deletions
+1
View File
@@ -43,6 +43,7 @@
"papaparse": "^5.4.1",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-frame-component": "^5.2.6",
"react-markdown": "^9.0.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
+3 -4
View File
@@ -226,13 +226,12 @@
}
.ts,
.termopts,
.renderer {
.termopts {
display: flex;
}
.renderer .renderer-icon {
margin-right: 0.5em;
.renderer-icon {
margin-right: 0.2em;
}
.metapart-mono {
+2 -2
View File
@@ -291,10 +291,10 @@ class LineHeader extends React.Component<{ screen: LineContainerType; line: Line
</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="meta-divider">|</div>
<div className="renderer">
<div className="renderer-icon">
<i className="fa-sharp fa-solid fa-fill renderer-icon" />
{renderer}
</div>
<div className="renderer-name">{renderer}</div>
</If>
</div>
);
+53 -1
View File
@@ -129,4 +129,56 @@ class PacketDataBuffer extends PtyDataBuffer {
}
}
export { PtyDataBuffer, PacketDataBuffer };
class JsonLinesDataBuffer extends PtyDataBuffer {
parsePos: number;
callback: (any) => void;
constructor(callback: (any) => void) {
super();
this.parsePos = 0;
this.callback = callback;
}
reset(): void {
super.reset();
this.parsePos = 0;
}
processLine(line: string) {
if (line.length == 0) {
return;
}
let jsonVal: any = null;
try {
jsonVal = JSON.parse(line.trim());
} catch (e) {
console.log("invalid json", line, e);
return;
}
if (jsonVal != null) {
this.callback(jsonVal);
}
}
parseData() {
for (let i = this.parsePos; i < this.dataSize; i++) {
let ch = this.rawData[i];
if (ch == NewLineCharCode) {
// line does *not* include the newline
let line = new TextDecoder().decode(
new Uint8Array(this.rawData.buffer, this.parsePos, i - this.parsePos)
);
this.parsePos = i + 1;
this.processLine(line);
}
}
return;
}
receiveData(pos: number, data: Uint8Array, reason?: string): void {
super.receiveData(pos, data, reason);
this.parseData();
}
}
export { PtyDataBuffer, PacketDataBuffer, JsonLinesDataBuffer };
+13
View File
@@ -9,6 +9,7 @@ import { CSVRenderer } from "./csv/csv";
import { OpenAIRenderer, OpenAIRendererModel } from "./openai/openai";
import { SimplePdfRenderer } from "./pdf/pdf";
import { SimpleMediaRenderer } from "./media/media";
import { WaveAppRenderer, WaveAppRendererModel } from "./waveapp/waveapp";
import { isBlank } from "@/util/util";
import { sprintf } from "sprintf-js";
@@ -100,6 +101,18 @@ const PluginConfigs: RendererPluginType[] = [
mimeTypes: ["video/*", "audio/*"],
simpleComponent: SimpleMediaRenderer,
},
{
name: "waveapp",
rendererType: "full",
heightType: "pixels",
dataType: "model",
collapseType: "remove",
hidePrompt: true,
globalCss: null,
mimeTypes: ["application/x-waveapp"],
fullComponent: WaveAppRenderer,
modelCtor: () => new WaveAppRendererModel(),
},
];
class PluginModelClass {
+1
View File
@@ -0,0 +1 @@
# WaveApp Plugin
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.waveapp-renderer {
line-height: normal;
}
+248
View File
@@ -0,0 +1,248 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import { debounce } from "throttle-debounce";
import { boundMethod } from "autobind-decorator";
import { JsonLinesDataBuffer } from "../core/ptydata";
import { Markdown } from "@/elements";
import * as ijson from "@/util/ijson";
import Frame from "react-frame-component";
import "./waveapp.less";
type WaveAppProps = {
lineId: string;
isSelected: boolean;
isFocused: boolean;
savedHeight: number;
initialData: any;
onPacket: (packetFn: (packet: any) => void) => void;
};
type WaveAppNode = {
tag: string;
props?: Record<string, any>;
children?: (WaveAppNode | string)[];
};
const TagMap: Record<string, React.ComponentType<{ node: WaveAppNode }>> = {};
function convertNodeToTag(node: WaveAppNode | string, idx?: number): JSX.Element | string {
if (node == null) {
return null;
}
if (idx == null) {
idx = 0;
}
if (typeof node === "string") {
return node;
}
let key = node.props?.key ?? "child-" + idx;
let TagComp = TagMap[node.tag];
if (!TagComp) {
return (
<div key={key} s>
Unknown tag:{node.tag}
</div>
);
}
return <TagComp key={key} node={node} />;
}
@mobxReact.observer
class WaveAppHtmlTag extends React.Component<{ node: WaveAppNode }, {}> {
render() {
let { tag, props, children } = this.props.node;
let divProps = {};
if (props != null) {
for (let [key, val] of Object.entries(props)) {
if (key.startsWith("on")) {
divProps[key] = (e: any) => {
console.log("handler", key, val);
};
} else {
divProps[key] = mobx.toJS(val);
}
}
}
let childrenComps: (string | JSX.Element)[] = [];
if (children != null) {
for (let idx = 0; idx < children.length; idx++) {
let comp = convertNodeToTag(children[idx], idx);
if (comp != null) {
childrenComps.push(comp);
}
}
}
return React.createElement(tag, divProps, childrenComps);
}
}
TagMap["div"] = WaveAppHtmlTag;
TagMap["b"] = WaveAppHtmlTag;
TagMap["i"] = WaveAppHtmlTag;
TagMap["p"] = WaveAppHtmlTag;
TagMap["span"] = WaveAppHtmlTag;
TagMap["a"] = WaveAppHtmlTag;
TagMap["h1"] = WaveAppHtmlTag;
TagMap["h2"] = WaveAppHtmlTag;
TagMap["h3"] = WaveAppHtmlTag;
TagMap["h4"] = WaveAppHtmlTag;
TagMap["h5"] = WaveAppHtmlTag;
TagMap["h6"] = WaveAppHtmlTag;
TagMap["ul"] = WaveAppHtmlTag;
TagMap["ol"] = WaveAppHtmlTag;
TagMap["li"] = WaveAppHtmlTag;
class WaveAppRendererModel {
context: RendererContext;
opts: RendererOpts;
isDone: OV<boolean>;
api: RendererModelContainerApi;
savedHeight: number;
loading: OV<boolean>;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
packetData: JsonLinesDataBuffer;
rawCmd: WebCmd;
version: OV<number>;
loadError: OV<string> = mobx.observable.box(null, { name: "renderer-loadError" });
data: OV<any> = mobx.observable.box(null, { name: "renderer-data" });
constructor() {
this.packetData = new JsonLinesDataBuffer(this.packetCallback.bind(this));
this.version = mobx.observable.box(0);
}
initialize(params: RendererModelInitializeParams): void {
this.loading = mobx.observable.box(true, { name: "renderer-loading" });
this.isDone = mobx.observable.box(params.isDone, { name: "renderer-isDone" });
this.context = params.context;
this.opts = params.opts;
this.api = params.api;
this.savedHeight = params.savedHeight;
this.ptyDataSource = params.ptyDataSource;
this.rawCmd = params.rawCmd;
setTimeout(() => this.reload(0), 10);
}
packetCallback(jsonVal: any) {
console.log("packet-callback", jsonVal);
try {
let data = this.data.get();
let newData = ijson.applyCommand(data, jsonVal);
console.log("got newdata", newData);
if (newData != data) {
mobx.action(() => {
this.data.set(newData);
})();
}
} catch (e) {
console.log("error adding data", e);
}
return;
}
dispose(): void {
return;
}
reload(delayMs: number): void {
mobx.action(() => {
this.loading.set(true);
this.loadError.set(null);
})();
let rtnp = this.ptyDataSource(this.context);
if (rtnp == null) {
console.log("no promise returned from ptyDataSource (waveapp renderer)", this.context);
return;
}
rtnp.then((ptydata) => {
setTimeout(() => {
this.packetData.reset();
this.receiveData(ptydata.pos, ptydata.data, "reload");
mobx.action(() => {
this.loading.set(false);
})();
}, delayMs);
}).catch((e) => {
console.log("error loading data", e);
mobx.action(() => {
this.loadError.set("error loading data: " + e);
})();
});
}
giveFocus(): void {
return;
}
updateOpts(opts: RendererOptsUpdate): void {
Object.assign(this.opts, opts);
}
setIsDone(): void {
if (this.isDone.get()) {
return;
}
mobx.action(() => {
this.isDone.set(true);
})();
}
receiveData(pos: number, data: Uint8Array, reason?: string): void {
this.packetData.receiveData(pos, data, reason);
}
updateHeight(newHeight: number): void {
if (this.savedHeight != newHeight) {
this.savedHeight = newHeight;
this.api.saveHeight(newHeight);
}
}
}
@mobxReact.observer
class WaveAppRenderer extends React.Component<{ model: WaveAppRendererModel }, {}> {
renderError() {
const model = this.props.model;
return <div className="load-error-text">{model.loadError.get()}</div>;
}
render() {
let model = this.props.model;
let styleVal = null;
if (model.loading.get() && model.savedHeight >= 0 && model.isDone) {
styleVal = {
height: model.savedHeight,
maxHeight: model.opts.maxSize.height,
};
} else {
styleVal = {
maxHeight: model.opts.maxSize.height,
};
}
let version = model.version.get();
let loadError = model.loadError.get();
if (loadError != null) {
return (
<div className="waveapp-renderer" style={styleVal}>
{this.renderError()}
</div>
);
}
let node = model.data.get();
if (node == null) {
return <div className="waveapp-renderer" style={styleVal} />;
}
return (
<div className="waveapp-renderer" style={styleVal}>
<Frame>{convertNodeToTag(node)}</Frame>
</div>
);
}
}
export { WaveAppRendererModel, WaveAppRenderer };
+255
View File
@@ -0,0 +1,255 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// ijson values are regular JSON values: string, number, boolean, null, object, array
// path is an array of strings and numbers
import * as mobx from "mobx";
type PathType = (string | number)[];
var simplePathStrRe = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
function formatPath(path: PathType): string {
if (path.length == 0) {
return "$";
}
let pathStr = "$";
for (let pathPart of path) {
if (typeof pathPart === "string") {
if (simplePathStrRe.test(pathPart)) {
pathStr += "." + pathPart;
} else {
pathStr += "[" + JSON.stringify(pathPart) + "]";
}
} else if (typeof pathPart === "number") {
pathStr += "[" + pathPart + "]";
} else {
pathStr += ".*";
}
}
return pathStr;
}
function isArray(obj: any): boolean {
return obj != null && (Array.isArray(obj) || mobx.isObservableArray(obj));
}
function isObject(obj: any): boolean {
return obj != null && obj instanceof Object && !isArray(obj);
}
function getPath(obj: any, path: PathType): any {
let cur = obj;
for (let pathPart of path) {
if (cur == null) {
return null;
}
if (typeof pathPart === "string") {
if (isObject(cur)) {
cur = cur[pathPart];
} else {
return null;
}
} else if (typeof pathPart === "number") {
if (isArray(cur)) {
cur = cur[pathPart];
} else {
return null;
}
} else {
throw new Error("Invalid path part: " + pathPart);
}
}
return cur;
}
type SetPathOpts = {
force?: boolean;
remove?: boolean;
combinefn?: (oldVal: any, newVal: any, opts: SetPathOpts) => any;
};
function combineFn_arrayAppend(oldVal: any, newVal: any, opts: SetPathOpts): any {
if (oldVal == null) {
return [newVal];
}
if (!isArray(oldVal) && !opts.force) {
throw new Error("Cannot append to non-array: " + oldVal);
}
if (!isArray(oldVal)) {
return [newVal];
}
oldVal.push(newVal);
return oldVal;
}
function checkPath(path: PathType): boolean {
if (!isArray(path)) {
return false;
}
for (let pathPart of path) {
if (typeof pathPart !== "string" && typeof pathPart !== "number") {
return false;
}
}
return true;
}
function setPath(obj: any, path: PathType, value: any, opts: SetPathOpts) {
if (opts == null) {
opts = {};
}
if (opts.remove && value != null) {
throw new Error("Cannot set value and remove at the same time");
}
if (path == null) {
path = [];
}
if (!checkPath(path)) {
throw new Error("Invalid path: " + formatPath(path));
}
return setPathInternal(obj, path, value, opts);
}
function isEmpty(obj: any): boolean {
if (obj == null) {
return true;
}
if (isArray(obj)) {
return obj.length == 0;
}
if (isObject(obj)) {
for (let key in obj) {
return false;
}
return true;
}
return false;
}
function removeFromArr(arr: any[], idx: number): any[] {
console.log("removefromarray", arr, idx);
if (idx >= arr.length) {
return arr;
}
if (idx == arr.length - 1) {
arr.pop();
if (arr.length == 0) {
return null;
}
return arr;
}
arr[idx] = null;
return arr;
}
function setPathInternal(obj: any, path: PathType, value: any, opts: SetPathOpts): any {
if (path.length == 0) {
if (opts.combinefn != null) {
return opts.combinefn(obj, value, opts);
}
return value;
}
const pathPart = path[0];
if (typeof pathPart === "string") {
if (obj == null) {
if (opts.remove) {
return null;
}
obj = {};
}
if (!isObject(obj)) {
if (opts.force) {
obj = {};
} else {
throw new Error("Cannot set path on non-object: " + obj);
}
}
if (opts.remove && path.length == 1) {
delete obj[pathPart];
if (isEmpty(obj)) {
return null;
}
return obj;
}
const newVal = setPathInternal(obj[pathPart], path.slice(1), value, opts);
if (opts.remove && newVal == null) {
delete obj[pathPart];
if (isEmpty(obj)) {
return null;
}
return obj;
}
obj[pathPart] = newVal;
return obj;
} else if (typeof pathPart === "number") {
if (pathPart < 0 || !Number.isInteger(pathPart)) {
throw new Error("Invalid path part: " + pathPart);
}
if (obj == null) {
if (opts.remove) {
return null;
}
obj = [];
}
if (!isArray(obj)) {
if (opts.force) {
obj = [];
} else {
throw new Error("Cannot set path on non-array: " + obj);
}
}
if (opts.remove && path.length == 1) {
return removeFromArr(obj, pathPart);
}
const newVal = setPathInternal(obj[pathPart], path.slice(1), value, opts);
if (opts.remove && newVal == null) {
return removeFromArr(obj, pathPart);
}
obj[pathPart] = newVal;
return obj;
} else {
throw new Error("Invalid path part: " + pathPart);
}
}
function getCommandPath(command: object): PathType {
if (command["path"] == null) {
return [];
}
return command["path"];
}
function applyCommand(data: any, command: any): any {
if (command == null) {
throw new Error("Invalid command (null)");
}
if (!isObject(command)) {
throw new Error("Invalid command (not an object): " + command);
}
const commandType = command.type;
if (commandType == null) {
throw new Error("Invalid command (no type): " + command);
}
const path = getCommandPath(command);
if (!checkPath(path)) {
throw new Error("Invalid command path: " + formatPath(path));
}
switch (commandType) {
case "set":
return setPath(data, path, command.value, null);
case "del":
return setPath(data, path, null, { remove: true });
case "append":
return setPath(data, path, command.value, { combinefn: combineFn_arrayAppend });
default:
throw new Error("Invalid command type: " + commandType);
}
}
export type { PathType, SetPathOpts };
export { getPath, setPath, applyCommand, combineFn_arrayAppend };
File diff suppressed because it is too large Load Diff
+164
View File
@@ -0,0 +1,164 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package ijson
import "testing"
func TestDeepEqual(t *testing.T) {
if !DeepEqual(float64(1), float64(1)) {
t.Errorf("DeepEqual(1, 1) should be true")
}
if DeepEqual(float64(1), float64(2)) {
t.Errorf("DeepEqual(1, 2) should be false")
}
if !DeepEqual([]any{"a", 2.8, true, map[string]any{"c": 1.1}}, []any{"a", 2.8, true, map[string]any{"c": 1.1}}) {
t.Errorf("DeepEqual complex should be true")
}
}
func TestGetPath(t *testing.T) {
data := []any{"a", 2.8, true, map[string]any{"c": 1.1}}
rtn, err := GetPath(data, []any{0})
if err != nil {
t.Errorf("GetPath failed: %v", err)
}
if rtn != "a" {
t.Errorf("GetPath failed: %v", rtn)
}
rtn, err = GetPath(data, []any{50})
if err != nil {
t.Errorf("GetPath failed: %v", err)
}
if rtn != nil {
t.Errorf("GetPath failed: %v", rtn)
}
rtn, err = GetPath(data, []any{3, "c"})
if err != nil {
t.Errorf("GetPath failed: %v", err)
}
if rtn != 1.1 {
t.Errorf("GetPath failed: %v", rtn)
}
}
func makeValue() any {
return []any{"a", 2.8, true, map[string]any{"c": 1.1}}
}
func TestSetPath(t *testing.T) {
rtn, err := SetPath(makeValue(), []any{0}, "b", nil)
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if rtn.([]any)[0] != "b" {
t.Errorf("SetPath failed: %v", rtn)
}
rtn, err = SetPath(makeValue(), []any{10}, "b", nil)
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if len(rtn.([]any)) != 11 {
t.Errorf("SetPath failed: %v", rtn)
}
rtn, _ = GetPath(rtn, []any{10})
if rtn != "b" {
t.Errorf("SetPath failed: %v", rtn)
}
_, err = SetPath(makeValue(), []any{"a"}, "b", nil)
if err == nil {
t.Errorf("SetPath should have failed")
}
rtn, err = SetPath(makeValue(), []any{"a"}, "b", &SetPathOpts{Force: true})
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if !DeepEqual(rtn, map[string]any{"a": "b"}) {
t.Errorf("SetPath failed: %v", rtn)
}
rtn, err = SetPath(makeValue(), nil, "c", &SetPathOpts{CombineFn: CombineFn_ArrayAppend})
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if !DeepEqual(rtn, []any{"a", 2.8, true, map[string]any{"c": 1.1}, "c"}) {
t.Errorf("SetPath failed: %v", rtn)
}
_, err = SetPath(makeValue(), nil, "c", &SetPathOpts{CombineFn: CombineFn_ArrayAppend, Budget: -1})
if err == nil {
t.Errorf("SetPath should have failed")
}
rtn, err = SetPath(makeValue(), []any{5000}, "c", nil)
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if len(rtn.([]any)) != 5001 {
t.Errorf("SetPath failed: %v", rtn)
}
_, err = SetPath(makeValue(), []any{5000}, "c", &SetPathOpts{Budget: 1000})
if err == nil {
t.Errorf("SetPath should have failed")
}
rtn, err = SetPath(makeValue(), []any{3, "c"}, nil, &SetPathOpts{Remove: true})
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if !DeepEqual(rtn, []any{"a", 2.8, true}) {
t.Errorf("SetPath failed: %v", rtn)
}
rtn, _ = SetPath(makeValue(), []any{3}, nil, &SetPathOpts{Remove: true})
rtn, _ = SetPath(rtn, []any{2}, nil, &SetPathOpts{Remove: true})
rtn, _ = SetPath(rtn, []any{1}, nil, &SetPathOpts{Remove: true})
rtn, _ = SetPath(rtn, []any{0}, nil, &SetPathOpts{Remove: true})
if rtn != nil {
t.Errorf("SetPath failed: %v", rtn)
}
rtn, err = SetPath(makeValue(), []any{3, "d"}, 2.2, nil)
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if !DeepEqual(rtn, []any{"a", 2.8, true, map[string]any{"c": 1.1, "d": 2.2}}) {
t.Errorf("SetPath failed: %v", rtn)
}
rtn, err = SetPath(makeValue(), []any{1}, 2.2, &SetPathOpts{CombineFn: CombineFn_Inc})
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if !DeepEqual(rtn, []any{"a", 5.0, true, map[string]any{"c": 1.1}}) {
t.Errorf("SetPath failed: %v", rtn)
}
rtn, err = SetPath(makeValue(), []any{1}, 500.0, &SetPathOpts{CombineFn: CombineFn_Min})
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if rtn.([]any)[1] != 2.8 {
t.Errorf("SetPath failed: %v", rtn)
}
rtn, err = SetPath(makeValue(), []any{1}, 500.0, &SetPathOpts{CombineFn: CombineFn_Max})
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if rtn.([]any)[1] != 500.0 {
t.Errorf("SetPath failed: %v", rtn)
}
rtn, err = SetPath(makeValue(), []any{1}, 500.0, &SetPathOpts{CombineFn: CombineFn_SetUnless})
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if rtn.([]any)[1] != 2.8 {
t.Errorf("SetPath failed: %v", rtn)
}
rtn, err = SetPath(makeValue(), []any{8}, 500.0, &SetPathOpts{CombineFn: CombineFn_SetUnless})
if err != nil {
t.Errorf("SetPath failed: %v", err)
}
if rtn.([]any)[8] != 500.0 {
t.Errorf("SetPath failed: %v", rtn)
}
}
+5
View File
@@ -6901,6 +6901,11 @@ react-error-boundary@^3.1.4:
dependencies:
"@babel/runtime" "^7.12.5"
react-frame-component@^5.2.6:
version "5.2.6"
resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-5.2.6.tgz#0d9991d251ff1f7177479d8f370deea06b824b79"
integrity sha512-CwkEM5VSt6nFwZ1Op8hi3JB5rPseZlmnp5CGiismVTauE6S4Jsc4TNMlT0O7Cts4WgIC3ZBAQ2p1Mm9XgLbj+w==
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"