diff --git a/src/renderer/src/components/common/ErrorElement.tsx b/src/renderer/src/components/common/ErrorElement.tsx
index 81411b7e90..1c9eda009e 100644
--- a/src/renderer/src/components/common/ErrorElement.tsx
+++ b/src/renderer/src/components/common/ErrorElement.tsx
@@ -55,9 +55,9 @@ export function ErrorElement() {
{message}
{import.meta.env.DEV && stack ? (
-
+
{attachOpenInEditor(stack)}
-
+
) : null}
diff --git a/src/renderer/src/components/errors/ModalError.tsx b/src/renderer/src/components/errors/ModalError.tsx
index e92c2f5235..f1b7c5c0c6 100644
--- a/src/renderer/src/components/errors/ModalError.tsx
+++ b/src/renderer/src/components/errors/ModalError.tsx
@@ -25,9 +25,9 @@ export const ModalErrorFallback: FC = (props) => {
{message}
{import.meta.env.DEV && stack ? (
-
+
{attachOpenInEditor(stack)}
-
+
) : null}
diff --git a/src/renderer/src/components/errors/PageError.tsx b/src/renderer/src/components/errors/PageError.tsx
index 1254bb6377..d0f55da0e3 100644
--- a/src/renderer/src/components/errors/PageError.tsx
+++ b/src/renderer/src/components/errors/PageError.tsx
@@ -17,9 +17,9 @@ export const PageErrorFallback: FC = (props) => {
{message}
{import.meta.env.DEV && stack ? (
-
+
{attachOpenInEditor(stack)}
-
+
) : null}
diff --git a/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx b/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx
index 66c4914d4a..907e84a099 100644
--- a/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx
+++ b/src/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx
@@ -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 = (props) => {
const codeTheme = useUISettingSelector(
(s) => overrideTheme || s.codeHighlightTheme,
)
- useLayoutEffect(() => {
+ useIsomorphicLayoutEffect(() => {
let isMounted = true
setLoaded(false)
@@ -214,7 +214,10 @@ const ShikiCode: FC<
className,
)}
>
-
+
)
}
+
+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
+}
diff --git a/src/renderer/src/components/ui/markdown/renderers/BlockErrorBoundary.tsx b/src/renderer/src/components/ui/markdown/renderers/BlockErrorBoundary.tsx
new file mode 100644
index 0000000000..06f12df4d6
--- /dev/null
+++ b/src/renderer/src/components/ui/markdown/renderers/BlockErrorBoundary.tsx
@@ -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 (
+
+ {props.message}
+
+
{props.error?.message}
+
+ )
+}
diff --git a/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx b/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx
index 8a7fd65ebd..7d2019cc68 100644
--- a/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx
+++ b/src/renderer/src/components/ui/markdown/renderers/MarkdownLink.tsx
@@ -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,
diff --git a/src/renderer/src/lib/parse-html.ts b/src/renderer/src/lib/parse-html.ts
index dac2fc7506..1400b87e96 100644
--- a/src/renderer/src/lib/parse-html.ts
+++ b/src/renderer/src/lib/parse-html.ts
@@ -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
diff --git a/src/renderer/src/modules/entry-content/index.tsx b/src/renderer/src/modules/entry-content/index.tsx
index 880c7520b0..3e7208d45a 100644
--- a/src/renderer/src/modules/entry-content/index.tsx
+++ b/src/renderer/src/modules/entry-content/index.tsx
@@ -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()
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(null)
-
useEffect(() => {
scrollerRef.current?.scrollTo(0, 0)
}, [entryId])
if (!entry) return null
+ const content = entry?.entries.content ?? data?.entries.content
+
return (
)}
- {!isInReadabilityMode ? (
- content
- ) : (
-
- )}
+
+ {!isInReadabilityMode ? (
+ {content}
+ ) : (
+
+ )}
+
{!content && (
@@ -305,22 +288,6 @@ const TitleMetaHandler: Component<{
const ReadabilityContent = ({ entryId }: { entryId: string }) => {
const result = useEntryReadabilityContent(entryId)
- const [renderer, setRenderer] = useState(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 (
{result ? (
@@ -337,7 +304,12 @@ const ReadabilityContent = ({ entryId }: { entryId: string }) => {
)}
- {renderer}
+
+
+ {result?.content ?? ""}
+
+
+
)
}