mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-02-22 02:41:23 +01:00
Table of contents for markdown preview (#323)
Adds a table of contents in the markdown preview, with a button in the header to toggle whether to show the TOC. When a user clicks one of the TOC elements, the preview will scroll to the corresponding heading. I've also cleaned up some MD preview styling that was inconsistent and causing the preview to overflow unnecessarily. This also fixes some terminology in the preview code. <img width="574" alt="image" src="https://github.com/user-attachments/assets/abb18ba9-21d3-4315-bdc3-e4bdcca39a4c">
This commit is contained in:
parent
0fae6981f8
commit
072730f7eb
@ -12,7 +12,7 @@ import * as WOS from "@/store/wos";
|
||||
import { getElemAsStr } from "@/util/focusutil";
|
||||
import * as util from "@/util/util";
|
||||
import { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot";
|
||||
import { HelpView } from "@/view/helpview/helpview";
|
||||
import { HelpView, HelpViewModel, makeHelpViewModel } from "@/view/helpview/helpview";
|
||||
import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term";
|
||||
import { WaveAi, WaveAiModel, makeWaveAiViewModel } from "@/view/waveai/waveai";
|
||||
import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview";
|
||||
@ -44,6 +44,9 @@ function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel)
|
||||
if (blockView === "cpuplot") {
|
||||
return makeCpuPlotViewModel(blockId);
|
||||
}
|
||||
if (blockView === "help") {
|
||||
return makeHelpViewModel();
|
||||
}
|
||||
return makeDefaultViewModel(blockId, blockView);
|
||||
}
|
||||
|
||||
@ -84,7 +87,7 @@ function getViewElem(
|
||||
return <CpuPlotView key={blockId} blockId={blockId} model={viewModel as CpuPlotViewModel} />;
|
||||
}
|
||||
if (blockView == "help") {
|
||||
return <HelpView key={blockId} blockId={blockId} />;
|
||||
return <HelpView key={blockId} model={viewModel as HelpViewModel} />;
|
||||
}
|
||||
return <CenteredDiv>Invalid View "{blockView}"</CenteredDiv>;
|
||||
}
|
||||
|
@ -2,148 +2,187 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.markdown {
|
||||
color: var(--app-text-color);
|
||||
font-family: var(--markdown-font);
|
||||
font-size: 14px;
|
||||
overflow-wrap: break-word;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
line-height: 1.5;
|
||||
color: var(--app-text-color);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
font-family: var(--markdown-font);
|
||||
font-size: 14px;
|
||||
overflow-wrap: break-word;
|
||||
margin-bottom: 10px;
|
||||
--half-contents-height: 10em;
|
||||
|
||||
strong {
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
.heading {
|
||||
&:first-of-type {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
color: var(--app-text-color);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
scroll-margin-block-end: var(--half-contents-height);
|
||||
}
|
||||
|
||||
a {
|
||||
color: #32afff;
|
||||
}
|
||||
|
||||
table {
|
||||
tr th {
|
||||
strong {
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: outside;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-position: outside;
|
||||
margin-left: 19px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 4px 10px 4px 10px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--panel-bg-color);
|
||||
padding: 2px 4px 2px 6px;
|
||||
}
|
||||
|
||||
pre.codeblock {
|
||||
background-color: var(--panel-bg-color);
|
||||
margin: 4px 10px;
|
||||
padding: 0.4em 0.7em;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
code {
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
a {
|
||||
color: #32afff;
|
||||
}
|
||||
|
||||
.codeblock-actions {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
|
||||
i {
|
||||
color: var(--line-actions-inactive-color);
|
||||
margin-left: 4px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--line-actions-active-color);
|
||||
}
|
||||
|
||||
&.fa-check {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.fa-square-terminal {
|
||||
cursor: pointer;
|
||||
}
|
||||
table {
|
||||
tr th {
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .codeblock-actions {
|
||||
visibility: visible;
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: outside;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-position: outside;
|
||||
margin-left: 19px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 4px 10px 4px 10px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--panel-bg-color);
|
||||
padding: 2px 4px 2px 6px;
|
||||
}
|
||||
|
||||
pre.codeblock {
|
||||
background-color: var(--panel-bg-color);
|
||||
margin: 4px 10px;
|
||||
padding: 0.4em 0.7em;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
code {
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.codeblock-actions {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
|
||||
i {
|
||||
color: var(--line-actions-inactive-color);
|
||||
margin-left: 4px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--line-actions-active-color);
|
||||
}
|
||||
|
||||
&.fa-check {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.fa-square-terminal {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .codeblock-actions {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--app-text-color);
|
||||
background-color: var(--panel-bg-color);
|
||||
font-family: var(--termfontfamily);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre.selected {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--app-text-color);
|
||||
background-color: var(--panel-bg-color);
|
||||
font-family: var(--termfontfamily);
|
||||
border-radius: 4px;
|
||||
}
|
||||
// The TOC view should scroll independently of the contents view.
|
||||
.toc {
|
||||
max-width: 40%;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
border-left: 1px solid var(--border-color);
|
||||
.toc-inner {
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
text-wrap: wrap;
|
||||
|
||||
pre.selected {
|
||||
outline: 2px solid var(--accent-color);
|
||||
}
|
||||
h4 {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: semibold;
|
||||
padding-top: 6px;
|
||||
}
|
||||
.toc-item {
|
||||
cursor: pointer;
|
||||
--indent-factor: 1;
|
||||
|
||||
.title.is-1 {
|
||||
border-bottom: 1px solid #777;
|
||||
padding-bottom: 6px;
|
||||
font-size: 2em;
|
||||
}
|
||||
.title.is-2 {
|
||||
border-bottom: 1px solid #777;
|
||||
padding-bottom: 6px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.title.is-3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
.title.is-4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
.title.is-5 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.title.is-6 {
|
||||
font-size: 0.85em;
|
||||
// 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.
|
||||
padding-left: calc((var(--indent-factor) - 1) * 10px + 5px);
|
||||
text-indent: -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown.content {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown > *:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
@ -3,11 +3,15 @@
|
||||
|
||||
import { CopyButton } from "@/app/element/copybutton";
|
||||
import { clsx } from "clsx";
|
||||
import React from "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 { Atom, useAtomValue } from "jotai";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
import RemarkFlexibleToc, { TocItem } from "remark-flexible-toc";
|
||||
import { useHeight } from "../hook/useHeight";
|
||||
import "./markdown.less";
|
||||
|
||||
const Link = ({ href, children }: { href: string; children: React.ReactNode }) => {
|
||||
@ -19,8 +23,8 @@ const Link = ({ href, children }: { href: string; children: React.ReactNode }) =
|
||||
);
|
||||
};
|
||||
|
||||
const Header = ({ children, hnum }: { children: React.ReactNode; hnum: number }) => {
|
||||
return <div className={clsx("title", `is-${hnum}`)}>{children}</div>;
|
||||
const Heading = ({ children, hnum }: { children: React.ReactNode; hnum: number }) => {
|
||||
return <div className={clsx("heading", `is-${hnum}`)}>{children}</div>;
|
||||
};
|
||||
|
||||
const Code = ({ children }: { children: React.ReactNode }) => {
|
||||
@ -71,30 +75,87 @@ const CodeBlock = ({ children, onClickExecute }: CodeBlockProps) => {
|
||||
};
|
||||
|
||||
type MarkdownProps = {
|
||||
text: string;
|
||||
textAtom: Atom<string> | Atom<Promise<string>>;
|
||||
showTocAtom: Atom<boolean>;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
onClickExecute?: (cmd: string) => void;
|
||||
};
|
||||
|
||||
const Markdown = ({ text, style, className, onClickExecute }: MarkdownProps) => {
|
||||
const Markdown = ({ textAtom, showTocAtom, style, className, onClickExecute }: MarkdownProps) => {
|
||||
const text = useAtomValue(textAtom);
|
||||
const tocRef = useRef<TocItem[]>([]);
|
||||
const showToc = useAtomValue(showTocAtom);
|
||||
const contentsRef = useRef<HTMLDivElement>(null);
|
||||
const contentsHeight = useHeight(contentsRef, 200);
|
||||
|
||||
const halfContentsHeight = useMemo(() => {
|
||||
return `${contentsHeight / 2}px`;
|
||||
}, [contentsHeight]);
|
||||
|
||||
const onTocClick = useCallback((data: string) => {
|
||||
if (contentsRef.current) {
|
||||
const headings = contentsRef.current.getElementsByClassName("heading");
|
||||
for (const heading of headings) {
|
||||
if (heading.textContent === data) {
|
||||
heading.scrollIntoView({ inline: "nearest", block: "end" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const markdownComponents = {
|
||||
a: Link,
|
||||
h1: (props: any) => <Header {...props} hnum={1} />,
|
||||
h2: (props: any) => <Header {...props} hnum={2} />,
|
||||
h3: (props: any) => <Header {...props} hnum={3} />,
|
||||
h4: (props: any) => <Header {...props} hnum={4} />,
|
||||
h5: (props: any) => <Header {...props} hnum={5} />,
|
||||
h6: (props: any) => <Header {...props} hnum={6} />,
|
||||
h1: (props: any) => <Heading {...props} hnum={1} />,
|
||||
h2: (props: any) => <Heading {...props} hnum={2} />,
|
||||
h3: (props: any) => <Heading {...props} hnum={3} />,
|
||||
h4: (props: any) => <Heading {...props} hnum={4} />,
|
||||
h5: (props: any) => <Heading {...props} hnum={5} />,
|
||||
h6: (props: any) => <Heading {...props} hnum={6} />,
|
||||
code: Code,
|
||||
pre: (props: any) => <CodeBlock {...props} onClickExecute={onClickExecute} />,
|
||||
};
|
||||
|
||||
const toc = useMemo(() => {
|
||||
if (showToc && tocRef.current.length > 0) {
|
||||
return tocRef.current.map((item) => {
|
||||
return (
|
||||
<a
|
||||
key={item.href}
|
||||
className="toc-item"
|
||||
style={{ "--indent-factor": item.depth } as CSSProperties}
|
||||
onClick={() => onTocClick(item.value)}
|
||||
>
|
||||
{item.value}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [showToc, tocRef]);
|
||||
|
||||
return (
|
||||
<div className={clsx("markdown content", className)} style={style}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} components={markdownComponents}>
|
||||
{text}
|
||||
</ReactMarkdown>
|
||||
<div className={clsx("markdown", className)} style={style} ref={contentsRef}>
|
||||
<OverlayScrollbarsComponent
|
||||
className="content"
|
||||
style={{ "--half-contents-height": halfContentsHeight } as CSSProperties}
|
||||
options={{ scrollbars: { autoHide: "leave" } }}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
components={markdownComponents}
|
||||
>
|
||||
{text}
|
||||
</ReactMarkdown>
|
||||
</OverlayScrollbarsComponent>
|
||||
{showToc && (
|
||||
<OverlayScrollbarsComponent className="toc" options={{ scrollbars: { autoHide: "leave" } }}>
|
||||
<div className="toc-inner">
|
||||
<h4>Table of Contents</h4>
|
||||
{toc}
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -7,7 +7,7 @@
|
||||
--secondary-text-color: rgb(195, 200, 194);
|
||||
--grey-text-color: #666;
|
||||
--main-bg-color: rgb(34, 34, 34);
|
||||
--border-color: rgba(255, 255, 255, 0.08);
|
||||
--border-color: rgba(255, 255, 255, 0.16);
|
||||
--base-font: normal 14px / normal "Inter", sans-serif;
|
||||
--fixed-font: normal 12px / normal "Hack", monospace;
|
||||
--accent-color: rgb(88, 193, 66);
|
||||
|
@ -2,8 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
.help-view {
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
|
@ -2,7 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Markdown } from "@/app/element/markdown";
|
||||
|
||||
import { globalStore } from "@/app/store/global";
|
||||
import { Atom, atom, PrimitiveAtom } from "jotai";
|
||||
import "./helpview.less";
|
||||
|
||||
const helpText = `
|
||||
@ -179,8 +180,37 @@ Other useful metadata values to override block titles, icons, colors, themes, et
|
||||
|
||||
`;
|
||||
|
||||
function HelpView({ blockId }: { blockId: string }) {
|
||||
return <Markdown text={helpText} className="help-view" />;
|
||||
const helpTextAtom = atom(helpText);
|
||||
|
||||
class HelpViewModel implements ViewModel {
|
||||
viewType: string;
|
||||
showTocAtom: PrimitiveAtom<boolean>;
|
||||
endIconButtons: Atom<HeaderIconButton[]>;
|
||||
|
||||
constructor() {
|
||||
this.viewType = "help";
|
||||
this.showTocAtom = atom(false);
|
||||
this.endIconButtons = atom([
|
||||
{
|
||||
elemtype: "iconbutton",
|
||||
icon: "book",
|
||||
title: "Table of Contents",
|
||||
click: () => this.showTocToggle(),
|
||||
},
|
||||
] as HeaderIconButton[]);
|
||||
}
|
||||
|
||||
showTocToggle() {
|
||||
globalStore.set(this.showTocAtom, !globalStore.get(this.showTocAtom));
|
||||
}
|
||||
}
|
||||
|
||||
export { HelpView };
|
||||
function makeHelpViewModel() {
|
||||
return new HelpViewModel();
|
||||
}
|
||||
|
||||
function HelpView({ model }: { model: HelpViewModel }) {
|
||||
return <Markdown textAtom={helpTextAtom} showTocAtom={model.showTocAtom} className="help-view" />;
|
||||
}
|
||||
|
||||
export { HelpView, HelpViewModel, makeHelpViewModel };
|
||||
|
@ -4,13 +4,12 @@
|
||||
.view-preview {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
padding: 0 5px;
|
||||
|
||||
&.view-preview-markdown {
|
||||
align-items: start;
|
||||
|
@ -116,6 +116,8 @@ export class PreviewModel implements ViewModel {
|
||||
openFileError: jotai.PrimitiveAtom<string>;
|
||||
openFileModalGiveFocusRef: React.MutableRefObject<() => boolean>;
|
||||
|
||||
markdownShowToc: jotai.PrimitiveAtom<boolean>;
|
||||
|
||||
monacoRef: React.MutableRefObject<MonacoTypes.editor.IStandaloneCodeEditor>;
|
||||
|
||||
showHiddenFiles: jotai.PrimitiveAtom<boolean>;
|
||||
@ -140,6 +142,7 @@ export class PreviewModel implements ViewModel {
|
||||
this.openFileModalGiveFocusRef = createRef();
|
||||
this.manageConnection = jotai.atom(true);
|
||||
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
|
||||
this.markdownShowToc = jotai.atom(false);
|
||||
this.monacoRef = createRef();
|
||||
this.viewIcon = jotai.atom((get) => {
|
||||
const blockData = get(this.blockAtom);
|
||||
@ -196,6 +199,7 @@ export class PreviewModel implements ViewModel {
|
||||
headerPath = `~ (${loadableFileInfo.data?.dir})`;
|
||||
}
|
||||
}
|
||||
|
||||
const viewTextChildren: HeaderElem[] = [
|
||||
{
|
||||
elemtype: "text",
|
||||
@ -256,6 +260,8 @@ export class PreviewModel implements ViewModel {
|
||||
});
|
||||
this.endIconButtons = jotai.atom((get) => {
|
||||
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
|
||||
const loadableSV = get(this.loadableSpecializedView);
|
||||
const isCeView = loadableSV.state == "hasData" && loadableSV.data.specializedView == "codeedit";
|
||||
if (mimeType == "directory") {
|
||||
const showHiddenFiles = get(this.showHiddenFiles);
|
||||
return [
|
||||
@ -271,7 +277,16 @@ export class PreviewModel implements ViewModel {
|
||||
icon: "arrows-rotate",
|
||||
click: () => this.refreshCallback?.(),
|
||||
},
|
||||
];
|
||||
] as HeaderIconButton[];
|
||||
} else if (!isCeView && mimeType.startsWith("text/markdown")) {
|
||||
return [
|
||||
{
|
||||
elemtype: "iconbutton",
|
||||
icon: "book",
|
||||
title: "Table of Contents",
|
||||
click: () => this.markdownShowTocToggle(),
|
||||
},
|
||||
] as HeaderIconButton[];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@ -343,6 +358,10 @@ export class PreviewModel implements ViewModel {
|
||||
this.loadableFileInfo = loadable(this.statFile);
|
||||
}
|
||||
|
||||
markdownShowTocToggle() {
|
||||
globalStore.set(this.markdownShowToc, !globalStore.get(this.markdownShowToc));
|
||||
}
|
||||
|
||||
async getSpecializedView(getFn: jotai.Getter): Promise<{ specializedView?: string; errorStr?: string }> {
|
||||
const mimeType = await getFn(this.fileMimeType);
|
||||
const fileInfo = await getFn(this.statFile);
|
||||
@ -637,10 +656,9 @@ function makePreviewModel(blockId: string, nodeModel: NodeModel): PreviewModel {
|
||||
}
|
||||
|
||||
function MarkdownPreview({ model }: SpecializedViewProps) {
|
||||
const readmeText = jotai.useAtomValue(model.fileContent);
|
||||
return (
|
||||
<div className="view-preview view-preview-markdown">
|
||||
<Markdown text={readmeText} />
|
||||
<Markdown textAtom={model.fileContent} showTocAtom={model.markdownShowToc} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -113,6 +113,7 @@
|
||||
"react-gauge-chart": "^0.5.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-flexible-toc": "^1.1.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"shell-quote": "^1.8.1",
|
||||
|
18
yarn.lock
18
yarn.lock
@ -3528,7 +3528,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mdast@npm:^4.0.0":
|
||||
"@types/mdast@npm:^4.0.0, @types/mdast@npm:^4.0.4":
|
||||
version: 4.0.4
|
||||
resolution: "@types/mdast@npm:4.0.4"
|
||||
dependencies:
|
||||
@ -10744,6 +10744,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"remark-flexible-toc@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "remark-flexible-toc@npm:1.1.1"
|
||||
dependencies:
|
||||
"@types/mdast": "npm:^4.0.4"
|
||||
github-slugger: "npm:^2.0.0"
|
||||
mdast-util-to-string: "npm:^4.0.0"
|
||||
unified: "npm:^11.0.5"
|
||||
unist-util-visit: "npm:^5.0.0"
|
||||
checksum: 10c0/e1dfaaa6635b94c23835e3f0fb2f71affda5f08bcfc4e385c2972b4fa6c5217253022d2da4de09c6df8b8a0bcb56b0b787268c0f73f714feeb69a4c685b2117c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"remark-gfm@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "remark-gfm@npm:4.0.0"
|
||||
@ -11755,6 +11768,7 @@ __metadata:
|
||||
react-gauge-chart: "npm:^0.5.1"
|
||||
react-markdown: "npm:^9.0.1"
|
||||
rehype-raw: "npm:^7.0.0"
|
||||
remark-flexible-toc: "npm:^1.1.1"
|
||||
remark-gfm: "npm:^4.0.0"
|
||||
rollup-plugin-flow: "npm:^1.1.1"
|
||||
rxjs: "npm:^7.8.1"
|
||||
@ -12167,7 +12181,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unified@npm:^11.0.0":
|
||||
"unified@npm:^11.0.0, unified@npm:^11.0.5":
|
||||
version: 11.0.5
|
||||
resolution: "unified@npm:11.0.5"
|
||||
dependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user