implement img streaming (local and remote) for markdown (#348)

This commit is contained in:
Mike Sawka 2024-09-06 12:59:28 -07:00 committed by GitHub
parent f51678415c
commit 566bf461ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 98 additions and 18 deletions

View File

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

View File

@ -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} />,
};

View File

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

View File

@ -268,6 +268,11 @@ declare global {
}
type SuggestionsType = SuggestionConnectionItem | SuggestionConnectionScope;
type MarkdownResolveOpts = {
connName: string;
baseDir: string;
};
}
export {};

View File

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

View File

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