fix: handle render error in code block
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
c41756d2b9
commit
8511a79092
|
|
@ -55,9 +55,9 @@ export function ErrorElement() {
|
|||
</div>
|
||||
<h3 className="text-xl">{message}</h3>
|
||||
{import.meta.env.DEV && stack ? (
|
||||
<div className="mt-4 cursor-text overflow-auto whitespace-pre rounded-md bg-red-50 p-4 text-left font-mono text-sm text-red-600">
|
||||
<pre className="mt-4 max-h-48 cursor-text overflow-auto whitespace-pre-line rounded-md bg-red-50 p-4 text-left font-mono text-sm text-red-600">
|
||||
{attachOpenInEditor(stack)}
|
||||
</div>
|
||||
</pre>
|
||||
) : null}
|
||||
|
||||
<p className="my-8">
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ export const ModalErrorFallback: FC<AppErrorFallbackProps> = (props) => {
|
|||
</div>
|
||||
<div className="text-lg font-bold">{message}</div>
|
||||
{import.meta.env.DEV && stack ? (
|
||||
<div className="mt-4 cursor-text overflow-auto whitespace-pre rounded-md bg-red-50 p-4 text-left font-mono text-sm text-red-600">
|
||||
<pre className="mt-4 max-h-48 cursor-text overflow-auto whitespace-pre-line rounded-md bg-red-50 p-4 text-left font-mono text-sm text-red-600">
|
||||
{attachOpenInEditor(stack)}
|
||||
</div>
|
||||
</pre>
|
||||
) : null}
|
||||
|
||||
<p className="my-8">
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ export const PageErrorFallback: FC<AppErrorFallbackProps> = (props) => {
|
|||
</div>
|
||||
<div className="text-lg font-bold">{message}</div>
|
||||
{import.meta.env.DEV && stack ? (
|
||||
<div className="mt-4 cursor-text overflow-auto whitespace-pre rounded-md bg-red-50 p-4 text-left font-mono text-sm text-red-600">
|
||||
<pre className="mt-4 max-h-48 cursor-text overflow-auto whitespace-pre-line rounded-md bg-red-50 p-4 text-left font-mono text-sm text-red-600">
|
||||
{attachOpenInEditor(stack)}
|
||||
</div>
|
||||
</pre>
|
||||
) : null}
|
||||
|
||||
<p className="my-8">
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import {
|
|||
import { isElectronBuild } from "@renderer/constants"
|
||||
import { tipcClient } from "@renderer/lib/client"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { useIsomorphicLayoutEffect } from "foxact/use-isomorphic-layout-effect"
|
||||
import type { FC } from "react"
|
||||
import {
|
||||
useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
|
|
@ -95,7 +95,7 @@ export const ShikiHighLighter: FC<ShikiProps> = (props) => {
|
|||
const codeTheme = useUISettingSelector(
|
||||
(s) => overrideTheme || s.codeHighlightTheme,
|
||||
)
|
||||
useLayoutEffect(() => {
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
let isMounted = true
|
||||
setLoaded(false)
|
||||
|
||||
|
|
@ -214,7 +214,10 @@ const ShikiCode: FC<
|
|||
className,
|
||||
)}
|
||||
>
|
||||
<div dangerouslySetInnerHTML={{ __html: rendered }} />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: rendered }}
|
||||
data-language={language}
|
||||
/>
|
||||
<CopyButton
|
||||
value={code}
|
||||
className="absolute right-1 top-1 opacity-0 duration-200 group-hover:opacity-100"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
pre {
|
||||
@apply !m-0 overflow-auto p-4;
|
||||
|
||||
font-size: min(1em, 16px);
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
pre code {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { parseHtml } from "@renderer/lib/parse-html"
|
||||
import type { RemarkOptions } from "@renderer/lib/parse-markdown"
|
||||
import { parseMarkdown } from "@renderer/lib/parse-markdown"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
|
|
@ -26,3 +27,20 @@ export const Markdown: Component<
|
|||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export const HTML: Component<
|
||||
{
|
||||
children: string | null | undefined
|
||||
} & Partial<{
|
||||
renderInlineStyle: boolean
|
||||
}>
|
||||
> = ({ children, renderInlineStyle }) => {
|
||||
const stableRemarkOptions = useState({ renderInlineStyle })[0]
|
||||
|
||||
const markdownElement = useMemo(
|
||||
() => children && parseHtml(children, { ...stableRemarkOptions }).content,
|
||||
[children, stableRemarkOptions],
|
||||
)
|
||||
|
||||
return markdownElement
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import { captureException } from "@sentry/react"
|
||||
import { useEffect } from "react"
|
||||
|
||||
export const BlockError = (props: { error: any, message: string }) => {
|
||||
useEffect(() => {
|
||||
captureException(props.error)
|
||||
}, [])
|
||||
return (
|
||||
<div className="center flex min-h-12 flex-col rounded bg-red-400 py-4 text-sm text-white dark:bg-red-800">
|
||||
{props.message}
|
||||
|
||||
<pre className="m-0 bg-transparent">{props.error?.message}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ export const MarkdownLink = (props: LinkProps) => {
|
|||
}, [feedSiteUrl, props])
|
||||
const entryId = isBizId(props.href) ? props.href : null
|
||||
const entry = useEntry(entryId)
|
||||
|
||||
useAuthQuery(Queries.entries.byId(entryId!), {
|
||||
enabled: !!entryId && !entry,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
MarkdownLink,
|
||||
MarkdownP,
|
||||
} from "@renderer/components/ui/markdown/renderers"
|
||||
import { BlockError } from "@renderer/components/ui/markdown/renderers/BlockErrorBoundary"
|
||||
import { Media } from "@renderer/components/ui/media"
|
||||
import type { Components } from "hast-util-to-jsx-runtime"
|
||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
|
||||
|
|
@ -18,11 +19,11 @@ import rehypeStringify from "rehype-stringify"
|
|||
import { unified } from "unified"
|
||||
import { VFile } from "vfile"
|
||||
|
||||
export const parseHtml = async (
|
||||
export const parseHtml = (
|
||||
content: string,
|
||||
options?: {
|
||||
options?: Partial<{
|
||||
renderInlineStyle: boolean
|
||||
},
|
||||
}>,
|
||||
) => {
|
||||
const file = new VFile(content)
|
||||
const { renderInlineStyle = false } = options || {}
|
||||
|
|
@ -94,6 +95,7 @@ export const parseHtml = async (
|
|||
if (!props.children) return null
|
||||
|
||||
let language = ""
|
||||
|
||||
let codeString = null as string | null
|
||||
if (props.className?.includes("language-")) {
|
||||
language = props.className.replace("language-", "")
|
||||
|
|
@ -116,7 +118,14 @@ export const parseHtml = async (
|
|||
"props" in props.children && props.children.props.children
|
||||
if (!code) return null
|
||||
|
||||
codeString = extractCodeFromHtml(renderToString(code))
|
||||
try {
|
||||
codeString = extractCodeFromHtml(renderToString(code))
|
||||
} catch (error) {
|
||||
return createElement(BlockError, {
|
||||
error,
|
||||
message: "Code Block Render Error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!codeString) return null
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useUISettingKey } from "@renderer/atoms/settings/ui"
|
|||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import { m } from "@renderer/components/common/Motion"
|
||||
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
|
||||
import { HTML } from "@renderer/components/ui/markdown"
|
||||
import { ScrollArea } from "@renderer/components/ui/scroll-area"
|
||||
import { isWebBuild, ROUTE_FEED_PENDING } from "@renderer/constants"
|
||||
import { useEntryReadabilityToggle } from "@renderer/hooks/biz/useEntryActions"
|
||||
|
|
@ -19,7 +20,6 @@ import {
|
|||
import { useAuthQuery, useTitle } from "@renderer/hooks/common"
|
||||
import { stopPropagation } from "@renderer/lib/dom"
|
||||
import { FeedViewType } from "@renderer/lib/enum"
|
||||
import { parseHtml } from "@renderer/lib/parse-html"
|
||||
import type { ActiveEntryId } from "@renderer/models"
|
||||
import {
|
||||
useIsSoFWrappedElement,
|
||||
|
|
@ -28,8 +28,8 @@ import {
|
|||
import { Queries } from "@renderer/queries"
|
||||
import { useEntry, useEntryReadHistory } from "@renderer/store/entry"
|
||||
import { useFeedById, useFeedHeaderTitle } from "@renderer/store/feed"
|
||||
import type { FC, ReactNode } from "react"
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||
import type { FC } from "react"
|
||||
import { useEffect, useLayoutEffect, useRef } from "react"
|
||||
|
||||
import { LoadingCircle } from "../../components/ui/loading"
|
||||
import { EntryPlaceholderDaily } from "../ai/ai-daily/EntryPlaceholderDaily"
|
||||
|
|
@ -79,27 +79,7 @@ function EntryContentRender({ entryId }: { entryId: string }) {
|
|||
|
||||
const entryHistory = useEntryReadHistory(entryId)
|
||||
|
||||
const [content, setContent] = useState<JSX.Element>()
|
||||
const readerRenderInlineStyle = useUISettingKey("readerRenderInlineStyle")
|
||||
useLayoutEffect(() => {
|
||||
// Fallback data, if local data is broken should fallback to cached query data.
|
||||
const processContent = entry?.entries.content ?? data?.entries.content
|
||||
if (processContent) {
|
||||
parseHtml(processContent, {
|
||||
renderInlineStyle: readerRenderInlineStyle,
|
||||
}).then((parsed) => {
|
||||
setContent(parsed.content)
|
||||
})
|
||||
} else {
|
||||
setContent(undefined)
|
||||
}
|
||||
}, [
|
||||
data?.entries.content,
|
||||
entry?.entries.content,
|
||||
readerRenderInlineStyle,
|
||||
// Only for dx, hmr
|
||||
parseHtml,
|
||||
])
|
||||
|
||||
const translation = useAuthQuery(
|
||||
Queries.ai.translation({
|
||||
|
|
@ -137,12 +117,13 @@ function EntryContentRender({ entryId }: { entryId: string }) {
|
|||
|
||||
const isInReadabilityMode = useEntryIsInReadability(entryId)
|
||||
const scrollerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
scrollerRef.current?.scrollTo(0, 0)
|
||||
}, [entryId])
|
||||
if (!entry) return null
|
||||
|
||||
const content = entry?.entries.content ?? data?.entries.content
|
||||
|
||||
return (
|
||||
<EntryContentProvider
|
||||
entryId={entry.entries.id}
|
||||
|
|
@ -232,11 +213,13 @@ function EntryContentRender({ entryId }: { entryId: string }) {
|
|||
</AutoResizeHeight>
|
||||
</div>
|
||||
)}
|
||||
{!isInReadabilityMode ? (
|
||||
content
|
||||
) : (
|
||||
<ReadabilityContent entryId={entryId} />
|
||||
)}
|
||||
<article>
|
||||
{!isInReadabilityMode ? (
|
||||
<HTML renderInlineStyle={readerRenderInlineStyle}>{content}</HTML>
|
||||
) : (
|
||||
<ReadabilityContent entryId={entryId} />
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
</WrappedElementProvider>
|
||||
{!content && (
|
||||
|
|
@ -305,22 +288,6 @@ const TitleMetaHandler: Component<{
|
|||
const ReadabilityContent = ({ entryId }: { entryId: string }) => {
|
||||
const result = useEntryReadabilityContent(entryId)
|
||||
|
||||
const [renderer, setRenderer] = useState<ReactNode | null>(null)
|
||||
useLayoutEffect(() => {
|
||||
if (!result) return
|
||||
const { content: processContent } = result
|
||||
|
||||
if (processContent) {
|
||||
parseHtml(processContent, {
|
||||
renderInlineStyle: true,
|
||||
}).then((parsed) => {
|
||||
setRenderer(parsed.content)
|
||||
})
|
||||
} else {
|
||||
setRenderer(null)
|
||||
}
|
||||
}, [result, parseHtml])
|
||||
|
||||
return (
|
||||
<div className="grow">
|
||||
{result ? (
|
||||
|
|
@ -337,7 +304,12 @@ const ReadabilityContent = ({ entryId }: { entryId: string }) => {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
{renderer}
|
||||
<article>
|
||||
<HTML>
|
||||
{result?.content ?? ""}
|
||||
</HTML>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue