mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
relative markdown text (#1489)
This commit is contained in:
parent
51bd45bd2b
commit
f1cd6b933d
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -20,6 +20,9 @@
|
|||||||
"[less]": {
|
"[less]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
|
"[scss]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
"[css]": {
|
"[css]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { RpcApi } from "@/app/store/wshclientapi";
|
||||||
|
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||||
|
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||||
|
import { isBlank, makeConnRoute } from "@/util/util";
|
||||||
|
import parseSrcSet from "parse-srcset";
|
||||||
|
|
||||||
export type MarkdownContentBlockType = {
|
export type MarkdownContentBlockType = {
|
||||||
type: string;
|
type: string;
|
||||||
id: string;
|
id: string;
|
||||||
@ -147,3 +153,56 @@ export function transformBlocks(content: string): { content: string; blocks: Map
|
|||||||
blocks: blocks,
|
blocks: blocks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const resolveRemoteFile = async (filepath: string, resolveOpts: MarkdownResolveOpts): Promise<string | null> => {
|
||||||
|
if (!filepath || filepath.startsWith("http://") || filepath.startsWith("https://")) {
|
||||||
|
return filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const route = makeConnRoute(resolveOpts.connName);
|
||||||
|
const fileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [resolveOpts.baseDir, filepath], {
|
||||||
|
route: route,
|
||||||
|
});
|
||||||
|
|
||||||
|
const usp = new URLSearchParams();
|
||||||
|
usp.set("path", fileInfo.path);
|
||||||
|
if (!isBlank(resolveOpts.connName)) {
|
||||||
|
usp.set("connection", resolveOpts.connName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getWebServerEndpoint() + "/wave/stream-file?" + usp.toString();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to resolve remote file:", filepath, err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveSrcSet = async (srcSet: string, resolveOpts: MarkdownResolveOpts): Promise<string> => {
|
||||||
|
if (!srcSet) return null;
|
||||||
|
|
||||||
|
// Parse the srcset
|
||||||
|
const candidates = parseSrcSet(srcSet);
|
||||||
|
|
||||||
|
// Resolve each URL in the array of candidates
|
||||||
|
const resolvedCandidates = await Promise.all(
|
||||||
|
candidates.map(async (candidate) => {
|
||||||
|
const resolvedUrl = await resolveRemoteFile(candidate.url, resolveOpts);
|
||||||
|
return {
|
||||||
|
...candidate,
|
||||||
|
url: resolvedUrl,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reconstruct the srcset string
|
||||||
|
return resolvedCandidates
|
||||||
|
.map((candidate) => {
|
||||||
|
let part = candidate.url;
|
||||||
|
if (candidate.w) part += ` ${candidate.w}w`;
|
||||||
|
if (candidate.h) part += ` ${candidate.h}h`;
|
||||||
|
if (candidate.d) part += ` ${candidate.d}x`;
|
||||||
|
return part;
|
||||||
|
})
|
||||||
|
.join(", ");
|
||||||
|
};
|
||||||
|
@ -16,21 +16,66 @@
|
|||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
font-family: var(--markdown-font);
|
font-family: var(--markdown-font-family);
|
||||||
font-size: 14px;
|
font-size: var(--markdown-font-size);
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
&.non-scrollable {
|
&.non-scrollable {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading:not(.heading ~ .heading) {
|
||||||
&:first-of-type {
|
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
margin-top: 16px;
|
margin-top: 1.143em;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 0.571em;
|
||||||
|
font-weight: semibold;
|
||||||
|
padding-top: 0.429em;
|
||||||
|
|
||||||
|
&.is-1 {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 0.429em;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
&.is-2 {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 0.429em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
&.is-3 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
&.is-4 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
&.is-5 {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
&.is-6 {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paragraph {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
&[align="right"] {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[align="left"] {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
@ -44,24 +89,24 @@
|
|||||||
ul {
|
ul {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
list-style-position: outside;
|
list-style-position: outside;
|
||||||
margin-left: 16px;
|
margin-left: 1.143em;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
list-style-position: outside;
|
list-style-position: outside;
|
||||||
margin-left: 19px;
|
margin-left: 1.357em;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 4px 10px 4px 10px;
|
margin: 0.286em 0.714em;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
background-color: var(--panel-bg-color);
|
background-color: var(--panel-bg-color);
|
||||||
padding: 2px 4px 2px 6px;
|
padding: 0.143em 0.286em 0.143em 0.429em;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre.codeblock {
|
pre.codeblock {
|
||||||
background-color: var(--panel-bg-color);
|
background-color: var(--panel-bg-color);
|
||||||
margin: 4px 10px;
|
margin: 0.286em 0.714em;
|
||||||
padding: 0.4em 0.7em;
|
padding: 0.4em 0.7em;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -83,11 +128,11 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
margin: 2px 2px;
|
margin: 0.143em;
|
||||||
padding: 4px 4px;
|
padding: 0.286em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 4px;
|
gap: 0.286em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .codeblock-actions {
|
&:hover .codeblock-actions {
|
||||||
@ -98,6 +143,7 @@
|
|||||||
code {
|
code {
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
font: var(--fixed-font);
|
font: var(--fixed-font);
|
||||||
|
font-size: var(--markdown-fixed-font-size);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,41 +151,13 @@
|
|||||||
outline: 2px solid var(--accent-color);
|
outline: 2px solid var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
|
||||||
font-weight: semibold;
|
|
||||||
padding-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading.is-1 {
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
padding-bottom: 6px;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
.heading.is-2 {
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
padding-bottom: 6px;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
.heading.is-3 {
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
.heading.is-4 {
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
.heading.is-5 {
|
|
||||||
font-size: 0.875em;
|
|
||||||
}
|
|
||||||
.heading.is-6 {
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waveblock {
|
.waveblock {
|
||||||
margin: 16px 0;
|
margin: 1.143em 0;
|
||||||
|
|
||||||
.wave-block-content {
|
.wave-block-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px;
|
padding: 0.857em;
|
||||||
background-color: var(--highlight-bg-color);
|
background-color: var(--highlight-bg-color);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -150,15 +168,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 40px;
|
width: 2.857em;
|
||||||
height: 40px;
|
height: 2.857em;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-right: 12px;
|
margin-right: 0.857em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wave-block-icon i {
|
.wave-block-icon i {
|
||||||
font-size: 18px;
|
font-size: 1.125em;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,19 +186,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wave-block-filename {
|
.wave-block-filename {
|
||||||
font-size: 14px;
|
font-size: 1em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wave-block-size {
|
.wave-block-size {
|
||||||
font-size: 12px;
|
font-size: 0.857em;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The TOC view should scroll independently of the contents view.
|
|
||||||
.toc {
|
.toc {
|
||||||
max-width: 40%;
|
max-width: 40%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -192,21 +209,20 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 0.357em;
|
||||||
text-wrap: wrap;
|
text-wrap: wrap;
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
padding-left: 5px;
|
padding-left: 0.357em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc-item {
|
.toc-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
--indent-factor: 1;
|
--indent-factor: 1;
|
||||||
|
// The offset in the padding will ensure that when the text in the item wraps, it indents slightly.
|
||||||
// The 5px offset in the padding will ensure that when the text in the item wraps, it indents slightly.
|
|
||||||
// The indent factor is set in the React code and denotes the depth of the item in the TOC tree.
|
// The indent factor is set in the React code and denotes the depth of the item in the TOC tree.
|
||||||
padding-left: calc((var(--indent-factor) - 1) * 10px + 5px);
|
padding-left: calc((var(--indent-factor) - 1) * 0.714em + 0.357em);
|
||||||
text-indent: -5px;
|
text-indent: -0.357em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,13 @@
|
|||||||
|
|
||||||
import { CopyButton } from "@/app/element/copybutton";
|
import { CopyButton } from "@/app/element/copybutton";
|
||||||
import { createContentBlockPlugin } from "@/app/element/markdown-contentblock-plugin";
|
import { createContentBlockPlugin } from "@/app/element/markdown-contentblock-plugin";
|
||||||
import { MarkdownContentBlockType, transformBlocks } from "@/app/element/markdown-util";
|
import {
|
||||||
import { RpcApi } from "@/app/store/wshclientapi";
|
MarkdownContentBlockType,
|
||||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
resolveRemoteFile,
|
||||||
import { getWebServerEndpoint } from "@/util/endpoints";
|
resolveSrcSet,
|
||||||
import { isBlank, makeConnRoute, useAtomValueSafe } from "@/util/util";
|
transformBlocks,
|
||||||
|
} from "@/app/element/markdown-util";
|
||||||
|
import { useAtomValueSafe } from "@/util/util";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { Atom } from "jotai";
|
import { Atom } from "jotai";
|
||||||
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
|
||||||
@ -108,8 +110,34 @@ const CodeBlock = ({ children, onClickExecute }: CodeBlockProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MarkdownSource = (props: React.HTMLAttributes<HTMLSourceElement>) => {
|
const MarkdownSource = ({
|
||||||
|
props,
|
||||||
|
resolveOpts,
|
||||||
|
}: {
|
||||||
|
props: React.HTMLAttributes<HTMLSourceElement> & {
|
||||||
|
srcSet?: string;
|
||||||
|
media?: string;
|
||||||
|
};
|
||||||
|
resolveOpts: MarkdownResolveOpts;
|
||||||
|
}) => {
|
||||||
|
const [resolvedSrcSet, setResolvedSrcSet] = useState<string>(props.srcSet);
|
||||||
|
const [resolving, setResolving] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const resolvePath = async () => {
|
||||||
|
const resolved = await resolveSrcSet(props.srcSet, resolveOpts);
|
||||||
|
setResolvedSrcSet(resolved);
|
||||||
|
setResolving(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
resolvePath();
|
||||||
|
}, [props.srcSet]);
|
||||||
|
|
||||||
|
if (resolving) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <source srcSet={resolvedSrcSet} media={props.media} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface WaveBlockProps {
|
interface WaveBlockProps {
|
||||||
@ -148,16 +176,11 @@ const MarkdownImg = ({
|
|||||||
resolveOpts: MarkdownResolveOpts;
|
resolveOpts: MarkdownResolveOpts;
|
||||||
}) => {
|
}) => {
|
||||||
const [resolvedSrc, setResolvedSrc] = useState<string>(props.src);
|
const [resolvedSrc, setResolvedSrc] = useState<string>(props.src);
|
||||||
|
const [resolvedSrcSet, setResolvedSrcSet] = useState<string>(props.srcSet);
|
||||||
const [resolvedStr, setResolvedStr] = useState<string>(null);
|
const [resolvedStr, setResolvedStr] = useState<string>(null);
|
||||||
const [resolving, setResolving] = useState<boolean>(true);
|
const [resolving, setResolving] = useState<boolean>(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.src.startsWith("http://") || props.src.startsWith("https://")) {
|
|
||||||
setResolving(false);
|
|
||||||
setResolvedSrc(props.src);
|
|
||||||
setResolvedStr(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (props.src.startsWith("data:image/")) {
|
if (props.src.startsWith("data:image/")) {
|
||||||
setResolving(false);
|
setResolving(false);
|
||||||
setResolvedSrc(props.src);
|
setResolvedSrc(props.src);
|
||||||
@ -170,23 +193,20 @@ const MarkdownImg = ({
|
|||||||
setResolvedStr(`[img:${props.src}]`);
|
setResolvedStr(`[img:${props.src}]`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveFn = async () => {
|
const resolveFn = async () => {
|
||||||
const route = makeConnRoute(resolveOpts.connName);
|
const [resolvedSrc, resolvedSrcSet] = await Promise.all([
|
||||||
const fileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [resolveOpts.baseDir, props.src], {
|
resolveRemoteFile(props.src, resolveOpts),
|
||||||
route: route,
|
resolveSrcSet(props.srcSet, resolveOpts),
|
||||||
});
|
]);
|
||||||
const usp = new URLSearchParams();
|
|
||||||
usp.set("path", fileInfo.path);
|
setResolvedSrc(resolvedSrc);
|
||||||
if (!isBlank(resolveOpts.connName)) {
|
setResolvedSrcSet(resolvedSrcSet);
|
||||||
usp.set("connection", resolveOpts.connName);
|
|
||||||
}
|
|
||||||
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?" + usp.toString();
|
|
||||||
setResolvedSrc(streamingUrl);
|
|
||||||
setResolvedStr(null);
|
setResolvedStr(null);
|
||||||
setResolving(false);
|
setResolving(false);
|
||||||
};
|
};
|
||||||
resolveFn();
|
resolveFn();
|
||||||
}, [props.src]);
|
}, [props.src, props.srcSet]);
|
||||||
|
|
||||||
if (resolving) {
|
if (resolving) {
|
||||||
return null;
|
return null;
|
||||||
@ -195,7 +215,7 @@ const MarkdownImg = ({
|
|||||||
return <span>{resolvedStr}</span>;
|
return <span>{resolvedStr}</span>;
|
||||||
}
|
}
|
||||||
if (resolvedSrc != null) {
|
if (resolvedSrc != null) {
|
||||||
return <img {...props} src={resolvedSrc} />;
|
return <img {...props} src={resolvedSrc} srcSet={resolvedSrcSet} />;
|
||||||
}
|
}
|
||||||
return <span>[img]</span>;
|
return <span>[img]</span>;
|
||||||
};
|
};
|
||||||
@ -210,6 +230,8 @@ type MarkdownProps = {
|
|||||||
resolveOpts?: MarkdownResolveOpts;
|
resolveOpts?: MarkdownResolveOpts;
|
||||||
scrollable?: boolean;
|
scrollable?: boolean;
|
||||||
rehype?: boolean;
|
rehype?: boolean;
|
||||||
|
fontSizeOverride?: number;
|
||||||
|
fixedFontSizeOverride?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Markdown = ({
|
const Markdown = ({
|
||||||
@ -219,6 +241,8 @@ const Markdown = ({
|
|||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
resolveOpts,
|
resolveOpts,
|
||||||
|
fontSizeOverride,
|
||||||
|
fixedFontSizeOverride,
|
||||||
scrollable = true,
|
scrollable = true,
|
||||||
rehype = true,
|
rehype = true,
|
||||||
onClickExecute,
|
onClickExecute,
|
||||||
@ -262,7 +286,9 @@ const Markdown = ({
|
|||||||
h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => <Heading props={props} hnum={5} />,
|
h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => <Heading props={props} hnum={5} />,
|
||||||
h6: (props: React.HTMLAttributes<HTMLHeadingElement>) => <Heading props={props} hnum={6} />,
|
h6: (props: React.HTMLAttributes<HTMLHeadingElement>) => <Heading props={props} hnum={6} />,
|
||||||
img: (props: React.HTMLAttributes<HTMLImageElement>) => <MarkdownImg props={props} resolveOpts={resolveOpts} />,
|
img: (props: React.HTMLAttributes<HTMLImageElement>) => <MarkdownImg props={props} resolveOpts={resolveOpts} />,
|
||||||
source: (props: React.HTMLAttributes<HTMLSourceElement>) => <MarkdownSource {...props} />,
|
source: (props: React.HTMLAttributes<HTMLSourceElement>) => (
|
||||||
|
<MarkdownSource props={props} resolveOpts={resolveOpts} />
|
||||||
|
),
|
||||||
code: Code,
|
code: Code,
|
||||||
pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
|
pre: (props: React.HTMLAttributes<HTMLPreElement>) => (
|
||||||
<CodeBlock children={props.children} onClickExecute={onClickExecute} />
|
<CodeBlock children={props.children} onClickExecute={onClickExecute} />
|
||||||
@ -301,12 +327,15 @@ const Markdown = ({
|
|||||||
...(defaultSchema.attributes?.span || []),
|
...(defaultSchema.attributes?.span || []),
|
||||||
// Allow all class names starting with `hljs-`.
|
// Allow all class names starting with `hljs-`.
|
||||||
["className", /^hljs-./],
|
["className", /^hljs-./],
|
||||||
|
["srcset"],
|
||||||
|
["media"],
|
||||||
|
["type"],
|
||||||
// Alternatively, to allow only certain class names:
|
// Alternatively, to allow only certain class names:
|
||||||
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
|
// ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
|
||||||
],
|
],
|
||||||
waveblock: [["blockkey"]],
|
waveblock: [["blockkey"]],
|
||||||
},
|
},
|
||||||
tagNames: [...(defaultSchema.tagNames || []), "span", "waveblock"],
|
tagNames: [...(defaultSchema.tagNames || []), "span", "waveblock", "picture", "source"],
|
||||||
}),
|
}),
|
||||||
() => rehypeSlug({ prefix: idPrefix }),
|
() => rehypeSlug({ prefix: idPrefix }),
|
||||||
];
|
];
|
||||||
@ -349,8 +378,15 @@ const Markdown = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mergedStyle = { ...style };
|
||||||
|
if (fontSizeOverride != null) {
|
||||||
|
mergedStyle["--markdown-font-size"] = `${fontSizeOverride}px`;
|
||||||
|
}
|
||||||
|
if (fixedFontSizeOverride != null) {
|
||||||
|
mergedStyle["--markdown-fixed-font-size"] = `${fixedFontSizeOverride}px`;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className={clsx("markdown", className)} style={style}>
|
<div className={clsx("markdown", className)} style={mergedStyle}>
|
||||||
{scrollable ? <ScrollableMarkdown /> : <NonScrollableMarkdown />}
|
{scrollable ? <ScrollableMarkdown /> : <NonScrollableMarkdown />}
|
||||||
{toc && (
|
{toc && (
|
||||||
<OverlayScrollbarsComponent className="toc" options={{ scrollbars: { autoHide: "leave" } }}>
|
<OverlayScrollbarsComponent className="toc" options={{ scrollbars: { autoHide: "leave" } }}>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Used for syntax highlighting in markdown
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--main-text-color: #f7f7f7;
|
--main-text-color: #f7f7f7;
|
||||||
--title-font-size: 18px;
|
--title-font-size: 18px;
|
||||||
@ -16,8 +14,10 @@
|
|||||||
--accent-color: rgb(88, 193, 66);
|
--accent-color: rgb(88, 193, 66);
|
||||||
--panel-bg-color: rgba(31, 33, 31, 0.5);
|
--panel-bg-color: rgba(31, 33, 31, 0.5);
|
||||||
--highlight-bg-color: rgba(255, 255, 255, 0.2);
|
--highlight-bg-color: rgba(255, 255, 255, 0.2);
|
||||||
--markdown-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif,
|
--markdown-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif,
|
||||||
"Apple Color Emoji", "Segoe UI Emoji";
|
"Apple Color Emoji", "Segoe UI Emoji";
|
||||||
|
--markdown-font-size: 14px;
|
||||||
|
--markdown-fixed-font-size: 12px;
|
||||||
--error-color: rgb(229, 77, 46);
|
--error-color: rgb(229, 77, 46);
|
||||||
--warning-color: rgb(224, 185, 86);
|
--warning-color: rgb(224, 185, 86);
|
||||||
--success-color: rgb(78, 154, 6);
|
--success-color: rgb(78, 154, 6);
|
||||||
|
@ -783,6 +783,8 @@ function makePreviewModel(blockId: string, nodeModel: BlockNodeModel): PreviewMo
|
|||||||
function MarkdownPreview({ model }: SpecializedViewProps) {
|
function MarkdownPreview({ model }: SpecializedViewProps) {
|
||||||
const connName = useAtomValue(model.connection);
|
const connName = useAtomValue(model.connection);
|
||||||
const fileInfo = useAtomValue(model.statFile);
|
const fileInfo = useAtomValue(model.statFile);
|
||||||
|
const fontSizeOverride = useAtomValue(getOverrideConfigAtom(model.blockId, "markdown:fontsize"));
|
||||||
|
const fixedFontSizeOverride = useAtomValue(getOverrideConfigAtom(model.blockId, "markdown:fixedfontsize"));
|
||||||
const resolveOpts: MarkdownResolveOpts = useMemo<MarkdownResolveOpts>(() => {
|
const resolveOpts: MarkdownResolveOpts = useMemo<MarkdownResolveOpts>(() => {
|
||||||
return {
|
return {
|
||||||
connName: connName,
|
connName: connName,
|
||||||
@ -791,7 +793,13 @@ function MarkdownPreview({ model }: SpecializedViewProps) {
|
|||||||
}, [connName, 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} resolveOpts={resolveOpts} />
|
<Markdown
|
||||||
|
textAtom={model.fileContent}
|
||||||
|
showTocAtom={model.markdownShowToc}
|
||||||
|
resolveOpts={resolveOpts}
|
||||||
|
fontSizeOverride={fontSizeOverride}
|
||||||
|
fixedFontSizeOverride={fixedFontSizeOverride}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -131,9 +131,6 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
font-family: var(--termfontfamily);
|
|
||||||
font-weight: normal;
|
|
||||||
line-height: var(--termlineheight);
|
|
||||||
height: 21px;
|
height: 21px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -569,7 +569,7 @@ const ChatWindow = memo(
|
|||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
value: string;
|
value: string;
|
||||||
termFontSize: number;
|
baseFontSize: number;
|
||||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
onMouseDown: (e: React.MouseEvent<HTMLTextAreaElement>) => void;
|
onMouseDown: (e: React.MouseEvent<HTMLTextAreaElement>) => void;
|
||||||
@ -577,7 +577,7 @@ interface ChatInputProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
||||||
({ value, onChange, onKeyDown, onMouseDown, termFontSize, model }, ref) => {
|
({ value, onChange, onKeyDown, onMouseDown, baseFontSize, model }, ref) => {
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => textAreaRef.current as HTMLTextAreaElement);
|
useImperativeHandle(ref, () => textAreaRef.current as HTMLTextAreaElement);
|
||||||
@ -594,7 +594,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
|||||||
|
|
||||||
// Adjust the height of the textarea to fit the text
|
// Adjust the height of the textarea to fit the text
|
||||||
const textAreaMaxLines = 5;
|
const textAreaMaxLines = 5;
|
||||||
const textAreaLineHeight = termFontSize * 1.5;
|
const textAreaLineHeight = baseFontSize * 1.5;
|
||||||
const textAreaMinHeight = textAreaLineHeight;
|
const textAreaMinHeight = textAreaLineHeight;
|
||||||
const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines;
|
const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines;
|
||||||
|
|
||||||
@ -608,7 +608,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
|||||||
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
|
const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight);
|
||||||
textAreaRef.current.style.height = newHeight + "px";
|
textAreaRef.current.style.height = newHeight + "px";
|
||||||
},
|
},
|
||||||
[termFontSize]
|
[baseFontSize]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -624,7 +624,7 @@ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
|||||||
onMouseDown={onMouseDown} // When the user clicks on the textarea
|
onMouseDown={onMouseDown} // When the user clicks on the textarea
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
style={{ fontSize: termFontSize }}
|
style={{ fontSize: baseFontSize }}
|
||||||
placeholder="Ask anything..."
|
placeholder="Ask anything..."
|
||||||
value={value}
|
value={value}
|
||||||
></textarea>
|
></textarea>
|
||||||
@ -642,7 +642,7 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
|
|||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
const [selectedBlockIdx, setSelectedBlockIdx] = useState<number | null>(null);
|
const [selectedBlockIdx, setSelectedBlockIdx] = useState<number | null>(null);
|
||||||
|
|
||||||
const termFontSize: number = 14;
|
const baseFontSize: number = 14;
|
||||||
const msgWidths = {};
|
const msgWidths = {};
|
||||||
const locked = useAtomValue(model.locked);
|
const locked = useAtomValue(model.locked);
|
||||||
|
|
||||||
@ -815,7 +815,7 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
|
|||||||
onChange={handleTextAreaChange}
|
onChange={handleTextAreaChange}
|
||||||
onKeyDown={handleTextAreaKeyDown}
|
onKeyDown={handleTextAreaKeyDown}
|
||||||
onMouseDown={handleTextAreaMouseDown}
|
onMouseDown={handleTextAreaMouseDown}
|
||||||
termFontSize={termFontSize}
|
baseFontSize={baseFontSize}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button className={buttonClass} onClick={handleButtonPress}>
|
<Button className={buttonClass} onClick={handleButtonPress}>
|
||||||
|
4
frontend/types/gotypes.d.ts
vendored
4
frontend/types/gotypes.d.ts
vendored
@ -490,6 +490,8 @@ declare global {
|
|||||||
"term:vdomblockid"?: string;
|
"term:vdomblockid"?: string;
|
||||||
"term:vdomtoolbarblockid"?: string;
|
"term:vdomtoolbarblockid"?: string;
|
||||||
"web:zoom"?: number;
|
"web:zoom"?: number;
|
||||||
|
"markdown:fontsize"?: number;
|
||||||
|
"markdown:fixedfontsize"?: number;
|
||||||
"vdom:*"?: boolean;
|
"vdom:*"?: boolean;
|
||||||
"vdom:initialized"?: boolean;
|
"vdom:initialized"?: boolean;
|
||||||
"vdom:correlationid"?: string;
|
"vdom:correlationid"?: string;
|
||||||
@ -638,6 +640,8 @@ declare global {
|
|||||||
"autoupdate:intervalms"?: number;
|
"autoupdate:intervalms"?: number;
|
||||||
"autoupdate:installonquit"?: boolean;
|
"autoupdate:installonquit"?: boolean;
|
||||||
"autoupdate:channel"?: string;
|
"autoupdate:channel"?: string;
|
||||||
|
"markdown:fontsize"?: number;
|
||||||
|
"markdown:fixedfontsize"?: number;
|
||||||
"preview:showhiddenfiles"?: boolean;
|
"preview:showhiddenfiles"?: boolean;
|
||||||
"tab:preset"?: string;
|
"tab:preset"?: string;
|
||||||
"widget:*"?: boolean;
|
"widget:*"?: boolean;
|
||||||
|
@ -116,6 +116,7 @@
|
|||||||
"overlayscrollbars": "^2.10.1",
|
"overlayscrollbars": "^2.10.1",
|
||||||
"overlayscrollbars-react": "^0.5.6",
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
|
"parse-srcset": "^1.0.2",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
@ -96,6 +96,9 @@ const (
|
|||||||
|
|
||||||
MetaKey_WebZoom = "web:zoom"
|
MetaKey_WebZoom = "web:zoom"
|
||||||
|
|
||||||
|
MetaKey_MarkdownFontSize = "markdown:fontsize"
|
||||||
|
MetaKey_MarkdownFixedFontSize = "markdown:fixedfontsize"
|
||||||
|
|
||||||
MetaKey_VDomClear = "vdom:*"
|
MetaKey_VDomClear = "vdom:*"
|
||||||
MetaKey_VDomInitialized = "vdom:initialized"
|
MetaKey_VDomInitialized = "vdom:initialized"
|
||||||
MetaKey_VDomCorrelationId = "vdom:correlationid"
|
MetaKey_VDomCorrelationId = "vdom:correlationid"
|
||||||
|
@ -97,6 +97,9 @@ type MetaTSType struct {
|
|||||||
|
|
||||||
WebZoom float64 `json:"web:zoom,omitempty"`
|
WebZoom float64 `json:"web:zoom,omitempty"`
|
||||||
|
|
||||||
|
MarkdownFontSize float64 `json:"markdown:fontsize,omitempty"`
|
||||||
|
MarkdownFixedFontSize float64 `json:"markdown:fixedfontsize,omitempty"`
|
||||||
|
|
||||||
VDomClear bool `json:"vdom:*,omitempty"`
|
VDomClear bool `json:"vdom:*,omitempty"`
|
||||||
VDomInitialized bool `json:"vdom:initialized,omitempty"`
|
VDomInitialized bool `json:"vdom:initialized,omitempty"`
|
||||||
VDomCorrelationId string `json:"vdom:correlationid,omitempty"`
|
VDomCorrelationId string `json:"vdom:correlationid,omitempty"`
|
||||||
|
@ -46,6 +46,9 @@ const (
|
|||||||
ConfigKey_AutoUpdateInstallOnQuit = "autoupdate:installonquit"
|
ConfigKey_AutoUpdateInstallOnQuit = "autoupdate:installonquit"
|
||||||
ConfigKey_AutoUpdateChannel = "autoupdate:channel"
|
ConfigKey_AutoUpdateChannel = "autoupdate:channel"
|
||||||
|
|
||||||
|
ConfigKey_MarkdownFontSize = "markdown:fontsize"
|
||||||
|
ConfigKey_MarkdownFixedFontSize = "markdown:fixedfontsize"
|
||||||
|
|
||||||
ConfigKey_PreviewShowHiddenFiles = "preview:showhiddenfiles"
|
ConfigKey_PreviewShowHiddenFiles = "preview:showhiddenfiles"
|
||||||
|
|
||||||
ConfigKey_TabPreset = "tab:preset"
|
ConfigKey_TabPreset = "tab:preset"
|
||||||
|
@ -73,6 +73,9 @@ type SettingsType struct {
|
|||||||
AutoUpdateInstallOnQuit bool `json:"autoupdate:installonquit,omitempty"`
|
AutoUpdateInstallOnQuit bool `json:"autoupdate:installonquit,omitempty"`
|
||||||
AutoUpdateChannel string `json:"autoupdate:channel,omitempty"`
|
AutoUpdateChannel string `json:"autoupdate:channel,omitempty"`
|
||||||
|
|
||||||
|
MarkdownFontSize float64 `json:"markdown:fontsize,omitempty"`
|
||||||
|
MarkdownFixedFontSize float64 `json:"markdown:fixedfontsize,omitempty"`
|
||||||
|
|
||||||
PreviewShowHiddenFiles *bool `json:"preview:showhiddenfiles,omitempty"`
|
PreviewShowHiddenFiles *bool `json:"preview:showhiddenfiles,omitempty"`
|
||||||
|
|
||||||
TabPreset string `json:"tab:preset,omitempty"`
|
TabPreset string `json:"tab:preset,omitempty"`
|
||||||
|
@ -16290,6 +16290,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"parse-srcset@npm:^1.0.2":
|
||||||
|
version: 1.0.2
|
||||||
|
resolution: "parse-srcset@npm:1.0.2"
|
||||||
|
checksum: 10c0/2f268e3d110d4c53d06ed2a8e8ee61a7da0cee13bf150819a6da066a8ca9b8d15b5600d6e6cae8be940e2edc50ee7c1e1052934d6ec858324065ecef848f0497
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"parse5-htmlparser2-tree-adapter@npm:^7.0.0":
|
"parse5-htmlparser2-tree-adapter@npm:^7.0.0":
|
||||||
version: 7.1.0
|
version: 7.1.0
|
||||||
resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0"
|
resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0"
|
||||||
@ -22003,6 +22010,7 @@ __metadata:
|
|||||||
overlayscrollbars: "npm:^2.10.1"
|
overlayscrollbars: "npm:^2.10.1"
|
||||||
overlayscrollbars-react: "npm:^0.5.6"
|
overlayscrollbars-react: "npm:^0.5.6"
|
||||||
papaparse: "npm:^5.4.1"
|
papaparse: "npm:^5.4.1"
|
||||||
|
parse-srcset: "npm:^1.0.2"
|
||||||
pngjs: "npm:^7.0.0"
|
pngjs: "npm:^7.0.0"
|
||||||
prettier: "npm:^3.4.2"
|
prettier: "npm:^3.4.2"
|
||||||
prettier-plugin-jsdoc: "npm:^1.3.0"
|
prettier-plugin-jsdoc: "npm:^1.3.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user