fix: handle render error in code block

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-08-15 15:19:23 +08:00
parent c41756d2b9
commit 8511a79092
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
10 changed files with 78 additions and 60 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@
pre {
@apply !m-0 overflow-auto p-4;
font-size: min(1em, 16px);
font-size: 0.875em;
}
pre code {

View File

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

View File

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

View File

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

View File

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

View File

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