mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
implement img streaming (local and remote) for markdown (#348)
This commit is contained in:
parent
f51678415c
commit
566bf461ff
@ -37,10 +37,13 @@
|
||||
color: #32afff;
|
||||
}
|
||||
|
||||
table {
|
||||
tr th {
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
dl,
|
||||
table,
|
||||
details table {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
|
@ -2,16 +2,18 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { CopyButton } from "@/app/element/copybutton";
|
||||
import { WshServer } from "@/app/store/wshserver";
|
||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||
import * as util from "@/util/util";
|
||||
import { useAtomValueSafe } from "@/util/util";
|
||||
import { clsx } from "clsx";
|
||||
import { Atom } from "jotai";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
import React, { CSSProperties, useCallback, useMemo, useRef } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
import { useAtomValueSafe } from "@/util/util";
|
||||
import { Atom } from "jotai";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
import RemarkFlexibleToc, { TocItem } from "remark-flexible-toc";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { useHeight } from "../hook/useHeight";
|
||||
import "./markdown.less";
|
||||
|
||||
@ -75,6 +77,63 @@ const CodeBlock = ({ children, onClickExecute }: CodeBlockProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const MarkdownSource = (props: any) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const MarkdownImg = (props: any) => {
|
||||
const [resolvedSrc, setResolvedSrc] = React.useState<string>(props.src);
|
||||
const [resolvedStr, setResolvedStr] = React.useState<string>(null);
|
||||
const [resolving, setResolving] = React.useState<boolean>(true);
|
||||
const resolveOpts: MarkdownResolveOpts = props.resolveOpts;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.src.startsWith("http://") || props.src.startsWith("https://")) {
|
||||
setResolving(false);
|
||||
setResolvedSrc(props.src);
|
||||
setResolvedStr(null);
|
||||
return;
|
||||
}
|
||||
if (props.src.startsWith("data:image/")) {
|
||||
setResolving(false);
|
||||
setResolvedSrc(props.src);
|
||||
setResolvedStr(null);
|
||||
return;
|
||||
}
|
||||
if (resolveOpts == null) {
|
||||
setResolving(false);
|
||||
setResolvedSrc(null);
|
||||
setResolvedStr(`[img:${props.src}]`);
|
||||
return;
|
||||
}
|
||||
const resolveFn = async () => {
|
||||
const route = util.makeConnRoute(resolveOpts.connName);
|
||||
const fileInfo = await WshServer.RemoteFileJoinCommand([resolveOpts.baseDir, props.src], { route: route });
|
||||
const usp = new URLSearchParams();
|
||||
usp.set("path", fileInfo.path);
|
||||
if (!util.isBlank(resolveOpts.connName)) {
|
||||
usp.set("connection", resolveOpts.connName);
|
||||
}
|
||||
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?" + usp.toString();
|
||||
setResolvedSrc(streamingUrl);
|
||||
setResolvedStr(null);
|
||||
setResolving(false);
|
||||
};
|
||||
resolveFn();
|
||||
}, [props.src]);
|
||||
|
||||
if (resolving) {
|
||||
return null;
|
||||
}
|
||||
if (resolvedStr != null) {
|
||||
return <span>{resolvedStr}</span>;
|
||||
}
|
||||
if (resolvedSrc != null) {
|
||||
return <img {...props} src={resolvedSrc} />;
|
||||
}
|
||||
return <span>[img]</span>;
|
||||
};
|
||||
|
||||
type MarkdownProps = {
|
||||
text?: string;
|
||||
textAtom?: Atom<string> | Atom<Promise<string>>;
|
||||
@ -82,9 +141,10 @@ type MarkdownProps = {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
onClickExecute?: (cmd: string) => void;
|
||||
resolveOpts?: MarkdownResolveOpts;
|
||||
};
|
||||
|
||||
const Markdown = ({ text, textAtom, showTocAtom, style, className, onClickExecute }: MarkdownProps) => {
|
||||
const Markdown = ({ text, textAtom, showTocAtom, style, className, resolveOpts, onClickExecute }: MarkdownProps) => {
|
||||
const textAtomValue = useAtomValueSafe(textAtom);
|
||||
const tocRef = useRef<TocItem[]>([]);
|
||||
const showToc = useAtomValueSafe(showTocAtom) ?? false;
|
||||
@ -114,6 +174,8 @@ const Markdown = ({ text, textAtom, showTocAtom, style, className, onClickExecut
|
||||
h4: (props: any) => <Heading {...props} hnum={4} />,
|
||||
h5: (props: any) => <Heading {...props} hnum={5} />,
|
||||
h6: (props: any) => <Heading {...props} hnum={6} />,
|
||||
img: (props: any) => <MarkdownImg {...props} resolveOpts={resolveOpts} />,
|
||||
source: (props: any) => <MarkdownSource {...props} resolveOpts={resolveOpts} />,
|
||||
code: Code,
|
||||
pre: (props: any) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
|
||||
};
|
||||
|
@ -43,13 +43,6 @@ const SpecializedViewMap: { [view: string]: ({ model }: SpecializedViewProps) =>
|
||||
directory: DirectoryPreview,
|
||||
};
|
||||
|
||||
function makeConnRoute(conn: string): string {
|
||||
if (util.isBlank(conn)) {
|
||||
return "conn:local";
|
||||
}
|
||||
return "conn:" + conn;
|
||||
}
|
||||
|
||||
function isTextFile(mimeType: string): boolean {
|
||||
if (mimeType == null) {
|
||||
return false;
|
||||
@ -686,9 +679,17 @@ function makePreviewModel(blockId: string, nodeModel: NodeModel): PreviewModel {
|
||||
}
|
||||
|
||||
function MarkdownPreview({ model }: SpecializedViewProps) {
|
||||
const connName = jotai.useAtomValue(model.connection);
|
||||
const fileInfo = jotai.useAtomValue(model.statFile);
|
||||
const resolveOpts: MarkdownResolveOpts = React.useMemo<MarkdownResolveOpts>(() => {
|
||||
return {
|
||||
connName: connName,
|
||||
baseDir: fileInfo.dir,
|
||||
};
|
||||
}, [connName, fileInfo.dir]);
|
||||
return (
|
||||
<div className="view-preview view-preview-markdown">
|
||||
<Markdown textAtom={model.fileContent} showTocAtom={model.markdownShowToc} />
|
||||
<Markdown textAtom={model.fileContent} showTocAtom={model.markdownShowToc} resolveOpts={resolveOpts} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
5
frontend/types/custom.d.ts
vendored
5
frontend/types/custom.d.ts
vendored
@ -268,6 +268,11 @@ declare global {
|
||||
}
|
||||
|
||||
type SuggestionsType = SuggestionConnectionItem | SuggestionConnectionScope;
|
||||
|
||||
type MarkdownResolveOpts = {
|
||||
connName: string;
|
||||
baseDir: string;
|
||||
};
|
||||
}
|
||||
|
||||
export {};
|
||||
|
@ -264,6 +264,13 @@ function countGraphemes(str: string): number {
|
||||
return Array.from(seg.segment(str)).length;
|
||||
}
|
||||
|
||||
function makeConnRoute(conn: string): string {
|
||||
if (isBlank(conn)) {
|
||||
return "conn:local";
|
||||
}
|
||||
return "conn:" + conn;
|
||||
}
|
||||
|
||||
export {
|
||||
atomWithDebounce,
|
||||
atomWithThrottle,
|
||||
@ -279,6 +286,7 @@ export {
|
||||
jotaiLoadableValue,
|
||||
jsonDeepEqual,
|
||||
lazy,
|
||||
makeConnRoute,
|
||||
makeExternLink,
|
||||
makeIconClass,
|
||||
stringToBase64,
|
||||
|
@ -310,6 +310,7 @@ func handleStreamFile(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
err := handleRemoteStreamFile(w, r, conn, fileName, no404 != "")
|
||||
if err != nil {
|
||||
log.Printf("error streaming remote file %q %q: %v\n", conn, fileName, err)
|
||||
http.Error(w, fmt.Sprintf("error streaming file: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user