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;
|
color: #32afff;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
p,
|
||||||
tr th {
|
ul,
|
||||||
color: var(--app-text-color);
|
ol,
|
||||||
}
|
dl,
|
||||||
|
table,
|
||||||
|
details table {
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
@ -2,16 +2,18 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { CopyButton } from "@/app/element/copybutton";
|
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 { clsx } from "clsx";
|
||||||
|
import { Atom } from "jotai";
|
||||||
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||||
import React, { CSSProperties, useCallback, useMemo, useRef } from "react";
|
import React, { CSSProperties, useCallback, useMemo, useRef } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw";
|
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 RemarkFlexibleToc, { TocItem } from "remark-flexible-toc";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
import { useHeight } from "../hook/useHeight";
|
import { useHeight } from "../hook/useHeight";
|
||||||
import "./markdown.less";
|
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 = {
|
type MarkdownProps = {
|
||||||
text?: string;
|
text?: string;
|
||||||
textAtom?: Atom<string> | Atom<Promise<string>>;
|
textAtom?: Atom<string> | Atom<Promise<string>>;
|
||||||
@ -82,9 +141,10 @@ type MarkdownProps = {
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClickExecute?: (cmd: string) => void;
|
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 textAtomValue = useAtomValueSafe(textAtom);
|
||||||
const tocRef = useRef<TocItem[]>([]);
|
const tocRef = useRef<TocItem[]>([]);
|
||||||
const showToc = useAtomValueSafe(showTocAtom) ?? false;
|
const showToc = useAtomValueSafe(showTocAtom) ?? false;
|
||||||
@ -114,6 +174,8 @@ const Markdown = ({ text, textAtom, showTocAtom, style, className, onClickExecut
|
|||||||
h4: (props: any) => <Heading {...props} hnum={4} />,
|
h4: (props: any) => <Heading {...props} hnum={4} />,
|
||||||
h5: (props: any) => <Heading {...props} hnum={5} />,
|
h5: (props: any) => <Heading {...props} hnum={5} />,
|
||||||
h6: (props: any) => <Heading {...props} hnum={6} />,
|
h6: (props: any) => <Heading {...props} hnum={6} />,
|
||||||
|
img: (props: any) => <MarkdownImg {...props} resolveOpts={resolveOpts} />,
|
||||||
|
source: (props: any) => <MarkdownSource {...props} resolveOpts={resolveOpts} />,
|
||||||
code: Code,
|
code: Code,
|
||||||
pre: (props: any) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
|
pre: (props: any) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
|
||||||
};
|
};
|
||||||
|
@ -43,13 +43,6 @@ const SpecializedViewMap: { [view: string]: ({ model }: SpecializedViewProps) =>
|
|||||||
directory: DirectoryPreview,
|
directory: DirectoryPreview,
|
||||||
};
|
};
|
||||||
|
|
||||||
function makeConnRoute(conn: string): string {
|
|
||||||
if (util.isBlank(conn)) {
|
|
||||||
return "conn:local";
|
|
||||||
}
|
|
||||||
return "conn:" + conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTextFile(mimeType: string): boolean {
|
function isTextFile(mimeType: string): boolean {
|
||||||
if (mimeType == null) {
|
if (mimeType == null) {
|
||||||
return false;
|
return false;
|
||||||
@ -686,9 +679,17 @@ function makePreviewModel(blockId: string, nodeModel: NodeModel): PreviewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MarkdownPreview({ model }: SpecializedViewProps) {
|
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 (
|
return (
|
||||||
<div className="view-preview view-preview-markdown">
|
<div className="view-preview view-preview-markdown">
|
||||||
<Markdown textAtom={model.fileContent} showTocAtom={model.markdownShowToc} />
|
<Markdown textAtom={model.fileContent} showTocAtom={model.markdownShowToc} resolveOpts={resolveOpts} />
|
||||||
</div>
|
</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 SuggestionsType = SuggestionConnectionItem | SuggestionConnectionScope;
|
||||||
|
|
||||||
|
type MarkdownResolveOpts = {
|
||||||
|
connName: string;
|
||||||
|
baseDir: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
@ -264,6 +264,13 @@ function countGraphemes(str: string): number {
|
|||||||
return Array.from(seg.segment(str)).length;
|
return Array.from(seg.segment(str)).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeConnRoute(conn: string): string {
|
||||||
|
if (isBlank(conn)) {
|
||||||
|
return "conn:local";
|
||||||
|
}
|
||||||
|
return "conn:" + conn;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
atomWithDebounce,
|
atomWithDebounce,
|
||||||
atomWithThrottle,
|
atomWithThrottle,
|
||||||
@ -279,6 +286,7 @@ export {
|
|||||||
jotaiLoadableValue,
|
jotaiLoadableValue,
|
||||||
jsonDeepEqual,
|
jsonDeepEqual,
|
||||||
lazy,
|
lazy,
|
||||||
|
makeConnRoute,
|
||||||
makeExternLink,
|
makeExternLink,
|
||||||
makeIconClass,
|
makeIconClass,
|
||||||
stringToBase64,
|
stringToBase64,
|
||||||
|
@ -310,6 +310,7 @@ func handleStreamFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
err := handleRemoteStreamFile(w, r, conn, fileName, no404 != "")
|
err := handleRemoteStreamFile(w, r, conn, fileName, no404 != "")
|
||||||
if err != nil {
|
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)
|
http.Error(w, fmt.Sprintf("error streaming file: %v", err), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user