Skip to content

Commit 8511a79

Browse files
committed
fix: handle render error in code block
Signed-off-by: Innei <i@innei.in>
1 parent c41756d commit 8511a79

File tree

10 files changed

+78
-60
lines changed

10 files changed

+78
-60
lines changed

src/renderer/src/components/common/ErrorElement.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ export function ErrorElement() {
5555
</div>
5656
<h3 className="text-xl">{message}</h3>
5757
{import.meta.env.DEV && stack ? (
58-
<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">
58+
<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">
5959
{attachOpenInEditor(stack)}
60-
</div>
60+
</pre>
6161
) : null}
6262

6363
<p className="my-8">

src/renderer/src/components/errors/ModalError.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ export const ModalErrorFallback: FC<AppErrorFallbackProps> = (props) => {
2525
</div>
2626
<div className="text-lg font-bold">{message}</div>
2727
{import.meta.env.DEV && stack ? (
28-
<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">
28+
<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">
2929
{attachOpenInEditor(stack)}
30-
</div>
30+
</pre>
3131
) : null}
3232

3333
<p className="my-8">

src/renderer/src/components/errors/PageError.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export const PageErrorFallback: FC<AppErrorFallbackProps> = (props) => {
1717
</div>
1818
<div className="text-lg font-bold">{message}</div>
1919
{import.meta.env.DEV && stack ? (
20-
<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">
20+
<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">
2121
{attachOpenInEditor(stack)}
22-
</div>
22+
</pre>
2323
) : null}
2424

2525
<p className="my-8">

src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import {
55
import { isElectronBuild } from "@renderer/constants"
66
import { tipcClient } from "@renderer/lib/client"
77
import { cn } from "@renderer/lib/utils"
8+
import { useIsomorphicLayoutEffect } from "foxact/use-isomorphic-layout-effect"
89
import type { FC } from "react"
910
import {
1011
useInsertionEffect,
11-
useLayoutEffect,
1212
useMemo,
1313
useRef,
1414
useState,
@@ -95,7 +95,7 @@ export const ShikiHighLighter: FC<ShikiProps> = (props) => {
9595
const codeTheme = useUISettingSelector(
9696
(s) => overrideTheme || s.codeHighlightTheme,
9797
)
98-
useLayoutEffect(() => {
98+
useIsomorphicLayoutEffect(() => {
9999
let isMounted = true
100100
setLoaded(false)
101101

@@ -214,7 +214,10 @@ const ShikiCode: FC<
214214
className,
215215
)}
216216
>
217-
<div dangerouslySetInnerHTML={{ __html: rendered }} />
217+
<div
218+
dangerouslySetInnerHTML={{ __html: rendered }}
219+
data-language={language}
220+
/>
218221
<CopyButton
219222
value={code}
220223
className="absolute right-1 top-1 opacity-0 duration-200 group-hover:opacity-100"

src/renderer/src/components/ui/code-highlighter/shiki/shiki.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
pre {
1515
@apply !m-0 overflow-auto p-4;
1616

17-
font-size: min(1em, 16px);
17+
font-size: 0.875em;
1818
}
1919

2020
pre code {

src/renderer/src/components/ui/markdown/Markdown.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { parseHtml } from "@renderer/lib/parse-html"
12
import type { RemarkOptions } from "@renderer/lib/parse-markdown"
23
import { parseMarkdown } from "@renderer/lib/parse-markdown"
34
import { cn } from "@renderer/lib/utils"
@@ -26,3 +27,20 @@ export const Markdown: Component<
2627
</article>
2728
)
2829
}
30+
31+
export const HTML: Component<
32+
{
33+
children: string | null | undefined
34+
} & Partial<{
35+
renderInlineStyle: boolean
36+
}>
37+
> = ({ children, renderInlineStyle }) => {
38+
const stableRemarkOptions = useState({ renderInlineStyle })[0]
39+
40+
const markdownElement = useMemo(
41+
() => children && parseHtml(children, { ...stableRemarkOptions }).content,
42+
[children, stableRemarkOptions],
43+
)
44+
45+
return markdownElement
46+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { captureException } from "@sentry/react"
2+
import { useEffect } from "react"
3+
4+
export const BlockError = (props: { error: any, message: string }) => {
5+
useEffect(() => {
6+
captureException(props.error)
7+
}, [])
8+
return (
9+
<div className="center flex min-h-12 flex-col rounded bg-red-400 py-4 text-sm text-white dark:bg-red-800">
10+
{props.message}
11+
12+
<pre className="m-0 bg-transparent">{props.error?.message}</pre>
13+
</div>
14+
)
15+
}

src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const MarkdownLink = (props: LinkProps) => {
3939
}, [feedSiteUrl, props])
4040
const entryId = isBizId(props.href) ? props.href : null
4141
const entry = useEntry(entryId)
42+
4243
useAuthQuery(Queries.entries.byId(entryId!), {
4344
enabled: !!entryId && !entry,
4445
staleTime: 1000 * 60 * 5,

src/renderer/src/lib/parse-html.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MarkdownLink,
66
MarkdownP,
77
} from "@renderer/components/ui/markdown/renderers"
8+
import { BlockError } from "@renderer/components/ui/markdown/renderers/BlockErrorBoundary"
89
import { Media } from "@renderer/components/ui/media"
910
import type { Components } from "hast-util-to-jsx-runtime"
1011
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
@@ -18,11 +19,11 @@ import rehypeStringify from "rehype-stringify"
1819
import { unified } from "unified"
1920
import { VFile } from "vfile"
2021

21-
export const parseHtml = async (
22+
export const parseHtml = (
2223
content: string,
23-
options?: {
24+
options?: Partial<{
2425
renderInlineStyle: boolean
25-
},
26+
}>,
2627
) => {
2728
const file = new VFile(content)
2829
const { renderInlineStyle = false } = options || {}
@@ -94,6 +95,7 @@ export const parseHtml = async (
9495
if (!props.children) return null
9596

9697
let language = ""
98+
9799
let codeString = null as string | null
98100
if (props.className?.includes("language-")) {
99101
language = props.className.replace("language-", "")
@@ -116,7 +118,14 @@ export const parseHtml = async (
116118
"props" in props.children && props.children.props.children
117119
if (!code) return null
118120

119-
codeString = extractCodeFromHtml(renderToString(code))
121+
try {
122+
codeString = extractCodeFromHtml(renderToString(code))
123+
} catch (error) {
124+
return createElement(BlockError, {
125+
error,
126+
message: "Code Block Render Error",
127+
})
128+
}
120129
}
121130

122131
if (!codeString) return null

src/renderer/src/modules/entry-content/index.tsx

Lines changed: 18 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useUISettingKey } from "@renderer/atoms/settings/ui"
99
import { useWhoami } from "@renderer/atoms/user"
1010
import { m } from "@renderer/components/common/Motion"
1111
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
12+
import { HTML } from "@renderer/components/ui/markdown"
1213
import { ScrollArea } from "@renderer/components/ui/scroll-area"
1314
import { isWebBuild, ROUTE_FEED_PENDING } from "@renderer/constants"
1415
import { useEntryReadabilityToggle } from "@renderer/hooks/biz/useEntryActions"
@@ -19,7 +20,6 @@ import {
1920
import { useAuthQuery, useTitle } from "@renderer/hooks/common"
2021
import { stopPropagation } from "@renderer/lib/dom"
2122
import { FeedViewType } from "@renderer/lib/enum"
22-
import { parseHtml } from "@renderer/lib/parse-html"
2323
import type { ActiveEntryId } from "@renderer/models"
2424
import {
2525
useIsSoFWrappedElement,
@@ -28,8 +28,8 @@ import {
2828
import { Queries } from "@renderer/queries"
2929
import { useEntry, useEntryReadHistory } from "@renderer/store/entry"
3030
import { useFeedById, useFeedHeaderTitle } from "@renderer/store/feed"
31-
import type { FC, ReactNode } from "react"
32-
import { useEffect, useLayoutEffect, useRef, useState } from "react"
31+
import type { FC } from "react"
32+
import { useEffect, useLayoutEffect, useRef } from "react"
3333

3434
import { LoadingCircle } from "../../components/ui/loading"
3535
import { EntryPlaceholderDaily } from "../ai/ai-daily/EntryPlaceholderDaily"
@@ -79,27 +79,7 @@ function EntryContentRender({ entryId }: { entryId: string }) {
7979

8080
const entryHistory = useEntryReadHistory(entryId)
8181

82-
const [content, setContent] = useState<JSX.Element>()
8382
const readerRenderInlineStyle = useUISettingKey("readerRenderInlineStyle")
84-
useLayoutEffect(() => {
85-
// Fallback data, if local data is broken should fallback to cached query data.
86-
const processContent = entry?.entries.content ?? data?.entries.content
87-
if (processContent) {
88-
parseHtml(processContent, {
89-
renderInlineStyle: readerRenderInlineStyle,
90-
}).then((parsed) => {
91-
setContent(parsed.content)
92-
})
93-
} else {
94-
setContent(undefined)
95-
}
96-
}, [
97-
data?.entries.content,
98-
entry?.entries.content,
99-
readerRenderInlineStyle,
100-
// Only for dx, hmr
101-
parseHtml,
102-
])
10383

10484
const translation = useAuthQuery(
10585
Queries.ai.translation({
@@ -137,12 +117,13 @@ function EntryContentRender({ entryId }: { entryId: string }) {
137117

138118
const isInReadabilityMode = useEntryIsInReadability(entryId)
139119
const scrollerRef = useRef<HTMLDivElement>(null)
140-
141120
useEffect(() => {
142121
scrollerRef.current?.scrollTo(0, 0)
143122
}, [entryId])
144123
if (!entry) return null
145124

125+
const content = entry?.entries.content ?? data?.entries.content
126+
146127
return (
147128
<EntryContentProvider
148129
entryId={entry.entries.id}
@@ -232,11 +213,13 @@ function EntryContentRender({ entryId }: { entryId: string }) {
232213
</AutoResizeHeight>
233214
</div>
234215
)}
235-
{!isInReadabilityMode ? (
236-
content
237-
) : (
238-
<ReadabilityContent entryId={entryId} />
239-
)}
216+
<article>
217+
{!isInReadabilityMode ? (
218+
<HTML renderInlineStyle={readerRenderInlineStyle}>{content}</HTML>
219+
) : (
220+
<ReadabilityContent entryId={entryId} />
221+
)}
222+
</article>
240223
</div>
241224
</WrappedElementProvider>
242225
{!content && (
@@ -305,22 +288,6 @@ const TitleMetaHandler: Component<{
305288
const ReadabilityContent = ({ entryId }: { entryId: string }) => {
306289
const result = useEntryReadabilityContent(entryId)
307290

308-
const [renderer, setRenderer] = useState<ReactNode | null>(null)
309-
useLayoutEffect(() => {
310-
if (!result) return
311-
const { content: processContent } = result
312-
313-
if (processContent) {
314-
parseHtml(processContent, {
315-
renderInlineStyle: true,
316-
}).then((parsed) => {
317-
setRenderer(parsed.content)
318-
})
319-
} else {
320-
setRenderer(null)
321-
}
322-
}, [result, parseHtml])
323-
324291
return (
325292
<div className="grow">
326293
{result ? (
@@ -337,7 +304,12 @@ const ReadabilityContent = ({ entryId }: { entryId: string }) => {
337304
</span>
338305
</div>
339306
)}
340-
{renderer}
307+
<article>
308+
<HTML>
309+
{result?.content ?? ""}
310+
</HTML>
311+
</article>
312+
341313
</div>
342314
)
343315
}

0 commit comments

Comments
 (0)