diff --git a/frontend/app/element/markdown.less b/frontend/app/element/markdown.less index 8a266239e..6e4cbcc05 100644 --- a/frontend/app/element/markdown.less +++ b/frontend/app/element/markdown.less @@ -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 { diff --git a/frontend/app/element/markdown.tsx b/frontend/app/element/markdown.tsx index dfbc5807e..8caaf0974 100644 --- a/frontend/app/element/markdown.tsx +++ b/frontend/app/element/markdown.tsx @@ -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(props.src); + const [resolvedStr, setResolvedStr] = React.useState(null); + const [resolving, setResolving] = React.useState(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 {resolvedStr}; + } + if (resolvedSrc != null) { + return ; + } + return [img]; +}; + type MarkdownProps = { text?: string; textAtom?: Atom | Atom>; @@ -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([]); const showToc = useAtomValueSafe(showTocAtom) ?? false; @@ -114,6 +174,8 @@ const Markdown = ({ text, textAtom, showTocAtom, style, className, onClickExecut h4: (props: any) => , h5: (props: any) => , h6: (props: any) => , + img: (props: any) => , + source: (props: any) => , code: Code, pre: (props: any) => , }; diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 2b77b92a7..149288de7 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -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(() => { + return { + connName: connName, + baseDir: fileInfo.dir, + }; + }, [connName, fileInfo.dir]); return (
- +
); } diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 6bf485dd4..e645e6c40 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -268,6 +268,11 @@ declare global { } type SuggestionsType = SuggestionConnectionItem | SuggestionConnectionScope; + + type MarkdownResolveOpts = { + connName: string; + baseDir: string; + }; } export {}; diff --git a/frontend/util/util.ts b/frontend/util/util.ts index f6c37e467..72729e855 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -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, diff --git a/pkg/web/web.go b/pkg/web/web.go index ab002504a..96b1e77c6 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -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) } }