mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Mustache Template Renderer (#23)
* experimental version of the mustache renderer * mustache error message / loading cleanup, add refresh button
This commit is contained in:
parent
4953550425
commit
e30128d182
@ -13,6 +13,7 @@
|
|||||||
"mobx": "^6.6.0",
|
"mobx": "^6.6.0",
|
||||||
"mobx-react": "^7.5.0",
|
"mobx-react": "^7.5.0",
|
||||||
"monaco-editor": "^0.41.0",
|
"monaco-editor": "^0.41.0",
|
||||||
|
"mustache": "^4.2.0",
|
||||||
"node-fetch": "^3.2.10",
|
"node-fetch": "^3.2.10",
|
||||||
"react": "^18.1.0",
|
"react": "^18.1.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
|
14
src/emain.ts
14
src/emain.ts
@ -169,8 +169,13 @@ function getMods(input: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shNavHandler(event: any, url: any) {
|
function shNavHandler(event: any, url: any) {
|
||||||
console.log("navigation canceled", url);
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://")) {
|
||||||
|
console.log("open external, shNav", url);
|
||||||
|
electron.shell.openExternal(url);
|
||||||
|
} else {
|
||||||
|
console.log("navigation canceled", url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shFrameNavHandler(event: any, url: any) {
|
function shFrameNavHandler(event: any, url: any) {
|
||||||
@ -183,6 +188,7 @@ function shFrameNavHandler(event: any, url: any) {
|
|||||||
if (event.frame.name == "webview") {
|
if (event.frame.name == "webview") {
|
||||||
// "webview" links always open in new window
|
// "webview" links always open in new window
|
||||||
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
|
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
|
||||||
|
console.log("open external, frameNav", url);
|
||||||
electron.shell.openExternal(url);
|
electron.shell.openExternal(url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -301,13 +307,19 @@ function createMainWindow(clientData) {
|
|||||||
});
|
});
|
||||||
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
|
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
|
||||||
if (url.startsWith("https://docs.getprompt.dev/")) {
|
if (url.startsWith("https://docs.getprompt.dev/")) {
|
||||||
|
console.log("openExternal docs", url);
|
||||||
electron.shell.openExternal(url);
|
electron.shell.openExternal(url);
|
||||||
} else if (url.startsWith("https://discord.gg/")) {
|
} else if (url.startsWith("https://discord.gg/")) {
|
||||||
|
console.log("openExternal discord", url);
|
||||||
electron.shell.openExternal(url);
|
electron.shell.openExternal(url);
|
||||||
} else if (url.startsWith("https://extern/?")) {
|
} else if (url.startsWith("https://extern/?")) {
|
||||||
let qmark = url.indexOf("?");
|
let qmark = url.indexOf("?");
|
||||||
let param = url.substr(qmark + 1);
|
let param = url.substr(qmark + 1);
|
||||||
let newUrl = decodeURIComponent(param);
|
let newUrl = decodeURIComponent(param);
|
||||||
|
console.log("openExternal extern", newUrl);
|
||||||
|
electron.shell.openExternal(newUrl);
|
||||||
|
} else if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) {
|
||||||
|
console.log("openExternal fallback", url);
|
||||||
electron.shell.openExternal(newUrl);
|
electron.shell.openExternal(newUrl);
|
||||||
}
|
}
|
||||||
console.log("window-open denied", url);
|
console.log("window-open denied", url);
|
||||||
|
@ -2,6 +2,7 @@ import { RendererPluginType } from "./types";
|
|||||||
import { SimpleImageRenderer } from "./view/image";
|
import { SimpleImageRenderer } from "./view/image";
|
||||||
import { SimpleMarkdownRenderer } from "./view/markdown";
|
import { SimpleMarkdownRenderer } from "./view/markdown";
|
||||||
import { SourceCodeRenderer } from "./view/code";
|
import { SourceCodeRenderer } from "./view/code";
|
||||||
|
import { SimpleMustacheRenderer } from "./view/mustache";
|
||||||
import { OpenAIRenderer, OpenAIRendererModel } from "./view/openai";
|
import { OpenAIRenderer, OpenAIRendererModel } from "./view/openai";
|
||||||
import { isBlank } from "./util";
|
import { isBlank } from "./util";
|
||||||
import { sprintf } from "sprintf-js";
|
import { sprintf } from "sprintf-js";
|
||||||
@ -28,6 +29,17 @@ const MarkdownPlugin: RendererPluginType = {
|
|||||||
simpleComponent: SimpleMarkdownRenderer,
|
simpleComponent: SimpleMarkdownRenderer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MustachePlugin: RendererPluginType = {
|
||||||
|
name: "mustache",
|
||||||
|
rendererType: "simple",
|
||||||
|
heightType: "pixels",
|
||||||
|
dataType: "blob",
|
||||||
|
collapseType: "hide",
|
||||||
|
globalCss: null,
|
||||||
|
mimeTypes: ["text/plain"],
|
||||||
|
simpleComponent: SimpleMustacheRenderer,
|
||||||
|
};
|
||||||
|
|
||||||
const CodePlugin: RendererPluginType = {
|
const CodePlugin: RendererPluginType = {
|
||||||
name: "code",
|
name: "code",
|
||||||
rendererType: "simple",
|
rendererType: "simple",
|
||||||
@ -87,6 +99,7 @@ if ((window as any).PluginModel == null) {
|
|||||||
PluginModel.registerRendererPlugin(MarkdownPlugin);
|
PluginModel.registerRendererPlugin(MarkdownPlugin);
|
||||||
PluginModel.registerRendererPlugin(CodePlugin);
|
PluginModel.registerRendererPlugin(CodePlugin);
|
||||||
PluginModel.registerRendererPlugin(OpenAIPlugin);
|
PluginModel.registerRendererPlugin(OpenAIPlugin);
|
||||||
|
PluginModel.registerRendererPlugin(MustachePlugin);
|
||||||
(window as any).PluginModel = PluginModel;
|
(window as any).PluginModel = PluginModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +254,7 @@ input[type="checkbox"] {
|
|||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monaco-editor .monaco-editor-background {
|
.monaco-editor .monaco-editor-background {
|
||||||
background-color: rgba(255, 255, 255, 0.075) !important;
|
background-color: rgba(255, 255, 255, 0.075) !important;
|
||||||
}
|
}
|
||||||
@ -331,7 +331,26 @@ input[type="checkbox"] {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.renderer-container .markdown {
|
.renderer-container.mustache-renderer {
|
||||||
|
color: @term-white;
|
||||||
|
.cmd-hints {
|
||||||
|
display: inline-block !important;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 26px;
|
||||||
|
}
|
||||||
|
.hint-item {
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
padding: 3px 9px 2px 8px;
|
||||||
|
line-height: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.refresh-button {
|
||||||
|
color: rgb(52, 52, 52);
|
||||||
|
background-color: @term-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderer-container .content {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
@ -350,6 +369,15 @@ input[type="checkbox"] {
|
|||||||
margin: 2px 10px 6px 10px;
|
margin: 2px 10px 6px 10px;
|
||||||
padding: 4px 4px 4px 6px;
|
padding: 4px 4px 4px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: @term-white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.openai-renderer {
|
.openai-renderer {
|
||||||
|
@ -7,6 +7,7 @@ import { Main } from "./main";
|
|||||||
import { GlobalModel } from "./model";
|
import { GlobalModel } from "./model";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { loadFonts } from "./util";
|
import { loadFonts } from "./util";
|
||||||
|
import * as DOMPurify from "dompurify";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let VERSION = __PROMPT_VERSION__;
|
let VERSION = __PROMPT_VERSION__;
|
||||||
@ -31,5 +32,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
(window as any).mobx = mobx;
|
(window as any).mobx = mobx;
|
||||||
(window as any).sprintf = sprintf;
|
(window as any).sprintf = sprintf;
|
||||||
|
(window as any).DOMPurify = DOMPurify;
|
||||||
|
|
||||||
console.log("PROMPT", VERSION, BUILD);
|
console.log("PROMPT", VERSION, BUILD);
|
||||||
|
218
src/view/mustache.tsx
Normal file
218
src/view/mustache.tsx
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components";
|
||||||
|
import * as T from "../types";
|
||||||
|
import { sprintf } from "sprintf-js";
|
||||||
|
import { isBlank } from "../util";
|
||||||
|
import mustache from "mustache";
|
||||||
|
import * as DOMPurify from "dompurify";
|
||||||
|
import { GlobalModel } from "../model";
|
||||||
|
|
||||||
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
|
||||||
|
const MaxMustacheSize = 200000;
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class SimpleMustacheRenderer extends React.Component<
|
||||||
|
{ data: Blob; context: T.RendererContext; opts: T.RendererOpts; savedHeight: number; lineState: T.LineStateType },
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
templateLoading: OV<boolean> = mobx.observable.box(true, { name: "templateLoading" });
|
||||||
|
templateLoadError: OV<string> = mobx.observable.box(null, { name: "templateLoadError" });
|
||||||
|
dataLoading: OV<boolean> = mobx.observable.box(true, { name: "dataLoading" });
|
||||||
|
dataLoadError: OV<string> = mobx.observable.box(null, { name: "dataLoadError" });
|
||||||
|
mustacheTemplateText: OV<string> = mobx.observable.box(null, { name: "mustacheTemplateText" });
|
||||||
|
parsedData: OV<any> = mobx.observable.box(null, { name: "parsedData" });
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.reloadTemplate();
|
||||||
|
this.reloadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadTemplate() {
|
||||||
|
if (isBlank(this.props.lineState.template)) {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.templateLoading.set(false);
|
||||||
|
this.templateLoadError.set(`no 'template' specified`);
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.templateLoading.set(true);
|
||||||
|
this.templateLoadError.set(null);
|
||||||
|
})();
|
||||||
|
let context = this.props.context;
|
||||||
|
let lineState = this.props.lineState;
|
||||||
|
let quotedTemplateName = JSON.stringify(lineState.template);
|
||||||
|
let rtnp = GlobalModel.readRemoteFile(context.screenId, context.lineId, lineState.template);
|
||||||
|
rtnp.then((file) => {
|
||||||
|
if (file.notFound) {
|
||||||
|
this.trySetTemplateLoadError(`mustache template ${quotedTemplateName} not found`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return file.text();
|
||||||
|
})
|
||||||
|
.then((text) => {
|
||||||
|
if (isBlank(text)) {
|
||||||
|
this.trySetTemplateLoadError(`blank mustache template ${quotedTemplateName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.mustacheTemplateText.set(text);
|
||||||
|
this.templateLoading.set(false);
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.trySetTemplateLoadError(`loading mustache template ${quotedTemplateName}: ${e}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadData() {
|
||||||
|
// load json content
|
||||||
|
let dataBlob = this.props.data;
|
||||||
|
if (dataBlob == null || dataBlob.notFound) {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.dataLoading.set(false);
|
||||||
|
this.dataLoadError.set(
|
||||||
|
`file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found`
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.dataLoading.set(true);
|
||||||
|
this.dataLoadError.set(null);
|
||||||
|
})();
|
||||||
|
let rtnp = dataBlob.text();
|
||||||
|
let quotedDataName = dataBlob.name || '"terminal output"';
|
||||||
|
rtnp.then((text) => {
|
||||||
|
mobx.action(() => {
|
||||||
|
try {
|
||||||
|
this.parsedData.set(JSON.parse(text));
|
||||||
|
this.dataLoading.set(false);
|
||||||
|
} catch (e) {
|
||||||
|
this.trySetDataLoadError(`parsing json data from ${quotedDataName}: ${e}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}).catch((e) => {
|
||||||
|
this.trySetDataLoadError(`loading json data ${quotedDataName}: ${e}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
trySetTemplateLoadError(msg: string) {
|
||||||
|
if (this.templateLoadError.get() != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.templateLoadError.set(msg);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
trySetDataLoadError(msg: string) {
|
||||||
|
if (this.dataLoadError.get() != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.dataLoadError.set(msg);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
doRefresh() {
|
||||||
|
this.reloadTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCmdHints() {
|
||||||
|
return (
|
||||||
|
<div style={{ position: "absolute", bottom: "-3px", right: 0 }}>
|
||||||
|
<div className="cmd-hints" style={{ minWidth: "6rem", maxWidth: "6rem", marginLeft: "-18px" }}>
|
||||||
|
<div
|
||||||
|
onClick={this.doRefresh}
|
||||||
|
className={`hint-item refresh-button`}
|
||||||
|
title="reload template and re-render content"
|
||||||
|
>
|
||||||
|
refresh
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let errorMessage = this.dataLoadError.get() ?? this.templateLoadError.get();
|
||||||
|
if (errorMessage != null) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="renderer-container mustache-renderer"
|
||||||
|
style={{ fontSize: this.props.opts.termFontSize }}
|
||||||
|
>
|
||||||
|
<div className="load-error-text">ERROR: {errorMessage}</div>
|
||||||
|
{this.renderCmdHints()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.templateLoading.get() || this.dataLoading.get()) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="renderer-container mustache-renderer"
|
||||||
|
style={{ fontSize: this.props.opts.termFontSize, height: this.props.savedHeight }}
|
||||||
|
>
|
||||||
|
<div className="renderer-loading">
|
||||||
|
loading content <i className="fa fa-ellipsis fa-fade" />
|
||||||
|
</div>
|
||||||
|
{this.renderCmdHints()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let opts = this.props.opts;
|
||||||
|
let maxWidth = opts.maxSize.width;
|
||||||
|
let minWidth = opts.maxSize.width;
|
||||||
|
if (minWidth > 1000) {
|
||||||
|
minWidth = 1000;
|
||||||
|
}
|
||||||
|
let templateText = this.mustacheTemplateText.get();
|
||||||
|
let templateData = this.parsedData.get() || {};
|
||||||
|
let renderedText = null;
|
||||||
|
try {
|
||||||
|
renderedText = mustache.render(templateText, templateData);
|
||||||
|
renderedText = DOMPurify.sanitize(renderedText);
|
||||||
|
} catch (e) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="renderer-container mustache-renderer"
|
||||||
|
style={{ fontSize: this.props.opts.termFontSize }}
|
||||||
|
>
|
||||||
|
<div className="load-error-text">ERROR running template: {e.message}</div>
|
||||||
|
{this.renderCmdHints()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// TODO non-term content font-size (default to 16)
|
||||||
|
return (
|
||||||
|
<div className="renderer-container mustache-renderer" style={{ fontSize: 16 }}>
|
||||||
|
<div
|
||||||
|
className="scroller"
|
||||||
|
style={{
|
||||||
|
maxHeight: opts.maxSize.height,
|
||||||
|
minWidth: minWidth,
|
||||||
|
width: "min-content",
|
||||||
|
maxWidth: maxWidth,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mustache content"
|
||||||
|
style={{ maxHeight: opts.maxSize.height }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: renderedText }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{this.renderCmdHints()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SimpleMustacheRenderer };
|
@ -5628,6 +5628,11 @@ multicast-dns@^7.2.5:
|
|||||||
dns-packet "^5.2.2"
|
dns-packet "^5.2.2"
|
||||||
thunky "^1.0.2"
|
thunky "^1.0.2"
|
||||||
|
|
||||||
|
mustache@^4.2.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
|
||||||
|
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
|
||||||
|
|
||||||
nanoid@^3.3.6:
|
nanoid@^3.3.6:
|
||||||
version "3.3.6"
|
version "3.3.6"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
||||||
|
Loading…
Reference in New Issue
Block a user