Skip to content

Commit

Permalink
refactor(markdown): separate the markdown ctx and decouple it from th…
Browse files Browse the repository at this point in the history
…e biz

Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Oct 21, 2024
1 parent e875edf commit 4fbe873
Show file tree
Hide file tree
Showing 24 changed files with 367 additions and 268 deletions.
90 changes: 90 additions & 0 deletions apps/renderer/src/components/ui/markdown/HTML.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import katexStyle from "katex/dist/katex.min.css?raw"
import { createElement, Fragment, memo, useEffect, useMemo, useRef, useState } from "react"

import { MemoedDangerousHTMLStyle } from "~/components/common/MemoedDangerousHTMLStyle"
import { parseHtml } from "~/lib/parse-html"
import { useWrappedElementSize } from "~/providers/wrapped-element-provider"

import type { MediaInfoRecord } from "../media"
import { MediaContainerWidthProvider, MediaInfoRecordProvider } from "../media"
import { MarkdownRenderContainerRefContext } from "./context"

export type HTMLProps<A extends keyof JSX.IntrinsicElements = "div"> = {
children: string | null | undefined
as: A

accessory?: React.ReactNode
noMedia?: boolean
mediaInfo?: MediaInfoRecord

handleTranslate?: (html: HTMLElement | null) => void
} & JSX.IntrinsicElements[A] &
Partial<{
renderInlineStyle: boolean
}>
const HTMLImpl = <A extends keyof JSX.IntrinsicElements = "div">(props: HTMLProps<A>) => {
const {
children,
renderInlineStyle,
as = "div",
accessory,
noMedia,
mediaInfo,
handleTranslate: translate,
...rest
} = props
const [remarkOptions, setRemarkOptions] = useState({
renderInlineStyle,
noMedia,
})
const [shouldForceReMountKey, setShouldForceReMountKey] = useState(0)

useEffect(() => {
setRemarkOptions((options) => {
if (JSON.stringify(options) === JSON.stringify({ renderInlineStyle, noMedia })) {
return options
}

setShouldForceReMountKey((key) => key + 1)
return { ...options, renderInlineStyle, noMedia }
})
}, [renderInlineStyle, noMedia])

const [refElement, setRefElement] = useState<HTMLElement | null>(null)

const onceRef = useRef(false)
useEffect(() => {
if (onceRef.current || !refElement) {
return
}

translate?.(refElement)
onceRef.current = true
}, [translate, refElement])

const markdownElement = useMemo(
() =>
children &&
parseHtml(children, {
...remarkOptions,
}).toContent(),
[children, remarkOptions],
)

const { w: containerWidth } = useWrappedElementSize()

if (!markdownElement) return null
return (
<MarkdownRenderContainerRefContext.Provider value={refElement}>
<MediaContainerWidthProvider width={containerWidth}>
<MediaInfoRecordProvider mediaInfo={mediaInfo}>
<MemoedDangerousHTMLStyle>{katexStyle}</MemoedDangerousHTMLStyle>
{createElement(as, { ...rest, ref: setRefElement }, markdownElement)}
</MediaInfoRecordProvider>
</MediaContainerWidthProvider>
{!!accessory && <Fragment key={shouldForceReMountKey}>{accessory}</Fragment>}
</MarkdownRenderContainerRefContext.Provider>
)
}

export const HTML = memo(HTMLImpl)
89 changes: 1 addition & 88 deletions apps/renderer/src/components/ui/markdown/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import katexStyle from "katex/dist/katex.min.css?raw"
import { createElement, Fragment, memo, useEffect, useMemo, useRef, useState } from "react"
import { useMemo, useState } from "react"

import { MemoedDangerousHTMLStyle } from "~/components/common/MemoedDangerousHTMLStyle"
import { parseHtml } from "~/lib/parse-html"
import type { RemarkOptions } from "~/lib/parse-markdown"
import { parseMarkdown } from "~/lib/parse-markdown"
import { cn } from "~/lib/utils"
import { useWrappedElementSize } from "~/providers/wrapped-element-provider"

import type { MediaInfoRecord } from "../media"
import { MediaContainerWidthProvider, MediaInfoRecordProvider } from "../media"
import { MarkdownRenderContainerRefContext } from "./context"

export const Markdown: Component<
Expand Down Expand Up @@ -39,84 +33,3 @@ export const Markdown: Component<
</MarkdownRenderContainerRefContext.Provider>
)
}

const HTMLImpl = <A extends keyof JSX.IntrinsicElements = "div">(
props: {
children: string | null | undefined
as: A

accessory?: React.ReactNode
noMedia?: boolean
mediaInfo?: MediaInfoRecord

translate?: (html: HTMLElement | null) => void
} & JSX.IntrinsicElements[A] &
Partial<{
renderInlineStyle: boolean
}>,
) => {
const {
children,
renderInlineStyle,
as = "div",
accessory,
noMedia,
mediaInfo,
translate,
...rest
} = props
const [remarkOptions, setRemarkOptions] = useState({
renderInlineStyle,
noMedia,
})
const [shouldForceReMountKey, setShouldForceReMountKey] = useState(0)

useEffect(() => {
setRemarkOptions((options) => {
if (JSON.stringify(options) === JSON.stringify({ renderInlineStyle, noMedia })) {
return options
}

setShouldForceReMountKey((key) => key + 1)
return { ...options, renderInlineStyle, noMedia }
})
}, [renderInlineStyle, noMedia])

const [refElement, setRefElement] = useState<HTMLElement | null>(null)

const onceRef = useRef(false)
useEffect(() => {
if (onceRef.current || !refElement) {
return
}

translate?.(refElement)
onceRef.current = true
}, [translate, refElement])

const markdownElement = useMemo(
() =>
children &&
parseHtml(children, {
...remarkOptions,
}).toContent(),
[children, remarkOptions],
)

const { w: containerWidth } = useWrappedElementSize()

if (!markdownElement) return null
return (
<MarkdownRenderContainerRefContext.Provider value={refElement}>
<MediaContainerWidthProvider width={containerWidth}>
<MediaInfoRecordProvider mediaInfo={mediaInfo}>
<MemoedDangerousHTMLStyle>{katexStyle}</MemoedDangerousHTMLStyle>
{createElement(as, { ...rest, ref: setRefElement }, markdownElement)}
</MediaInfoRecordProvider>
</MediaContainerWidthProvider>
{!!accessory && <Fragment key={shouldForceReMountKey}>{accessory}</Fragment>}
</MarkdownRenderContainerRefContext.Provider>
)
}

export const HTML = memo(HTMLImpl)
15 changes: 13 additions & 2 deletions apps/renderer/src/components/ui/markdown/context.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import { createContext } from "react"
import { createContext as reactCreateContext } from "react"
import { createContext } from "use-context-selector"

export const MarkdownRenderContainerRefContext = createContext<HTMLElement | null>(null)
import type { MarkdownImage, MarkdownRenderActions } from "./types"

export const MarkdownRenderContainerRefContext = reactCreateContext<HTMLElement | null>(null)

export const MarkdownImageRecordContext = createContext<Record<string, MarkdownImage>>({})

export const MarkdownRenderActionContext = reactCreateContext<MarkdownRenderActions>({
transformUrl: (url) => url ?? "",
isAudio: () => false,
ensureAndRenderTimeStamp: () => false,
})
1 change: 0 additions & 1 deletion apps/renderer/src/components/ui/markdown/index.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useContext } from "react"
import { useContextSelector } from "use-context-selector"

import { cn } from "~/lib/utils"
import { useEntryContentContext } from "~/modules/entry-content/hooks"
import { useWrappedElementSize } from "~/providers/wrapped-element-provider"
import { useEntry } from "~/store/entry"

import { Media } from "../../media"
import { usePopulatedFullUrl } from "../utils"
import { MarkdownImageRecordContext, MarkdownRenderActionContext } from "../context"

export const MarkdownBlockImage = (
props: React.ImgHTMLAttributes<HTMLImageElement> & {
Expand All @@ -15,12 +16,13 @@ export const MarkdownBlockImage = (
},
) => {
const size = useWrappedElementSize()
const { feedId } = useEntryContentContext()

const src = usePopulatedFullUrl(feedId, props.src)
const { transformUrl } = useContext(MarkdownRenderActionContext)
const src = transformUrl(props.src)

const { entryId } = useEntryContentContext()
const media = useEntry(entryId, (entry) => entry?.entries.media?.find((m) => m.url === props.src))
const media = useContextSelector(MarkdownImageRecordContext, (record) =>
props.src ? record[props.src] : null,
)

return (
<Media
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useContext } from "react"

import { cn } from "~/lib/utils"
import { useEntryContentContext } from "~/modules/entry-content/hooks"

import { Media } from "../../media"
import { usePopulatedFullUrl } from "../utils"
import { MarkdownRenderActionContext } from "../context"

export const MarkdownInlineImage = (
props: React.ImgHTMLAttributes<HTMLImageElement> & {
Expand All @@ -12,8 +13,8 @@ export const MarkdownInlineImage = (
}
},
) => {
const { feedId } = useEntryContentContext()
const populatedUrl = usePopulatedFullUrl(feedId, props.src)
const { transformUrl } = useContext(MarkdownRenderActionContext)
const populatedUrl = transformUrl(props.src)
return (
<Media
type="photo"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { FeedViewType } from "~/lib/enum"
import { useEntryContentContext } from "~/modules/entry-content/hooks"
import { useContext } from "react"

import type { LinkProps } from "../../link"
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "../../tooltip"
import { ensureAndRenderTimeStamp, usePopulatedFullUrl } from "../utils"
import { MarkdownRenderActionContext } from "../context"

export const MarkdownLink = (props: LinkProps) => {
const { view, feedId } = useEntryContentContext()
const { transformUrl, isAudio, ensureAndRenderTimeStamp } = useContext(
MarkdownRenderActionContext,
)

const populatedFullHref = usePopulatedFullUrl(feedId, props.href)
const populatedFullHref = transformUrl(props.href)

const parseTimeStamp = view === FeedViewType.Audios
const parseTimeStamp = isAudio(populatedFullHref)
if (parseTimeStamp) {
const childrenText = props.children

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import * as React from "react"

import { FeedViewType } from "~/lib/enum"
import { useEntryContentContextSelector } from "~/modules/entry-content/hooks"

import { ensureAndRenderTimeStamp } from "../utils"
import { MarkdownRenderActionContext } from "../context"
import { IsInParagraphContext } from "./ctx"

export const MarkdownP: Component<
React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
> = ({ children, ...props }) => {
const view = useEntryContentContextSelector((s) => s.view)
const parseTimeline = view === FeedViewType.Audios
const { isAudio, ensureAndRenderTimeStamp } = React.useContext(MarkdownRenderActionContext)
const parseTimeline = isAudio()
if (parseTimeline && typeof children === "string") {
const renderer = ensureAndRenderTimeStamp(children)
if (renderer) return <p>{renderer}</p>
Expand Down
12 changes: 12 additions & 0 deletions apps/renderer/src/components/ui/markdown/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type MarkdownImage = {
url: string
blurhash?: string
width: number
height: number
}

export interface MarkdownRenderActions {
transformUrl: (url?: string) => string
isAudio: (url?: string) => boolean
ensureAndRenderTimeStamp: (children: string) => React.ReactNode
}
64 changes: 0 additions & 64 deletions apps/renderer/src/components/ui/markdown/utils/index.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion apps/renderer/src/lib/error-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ExternalToast } from "sonner"
import { toast } from "sonner"

import { CopyButton } from "~/components/ui/code-highlighter"
import { Markdown } from "~/components/ui/markdown"
import { Markdown } from "~/components/ui/markdown/Markdown"
import { cn } from "~/lib/utils"

export const getFetchErrorMessage = (error: Error) => {
Expand Down
Loading

0 comments on commit 4fbe873

Please sign in to comment.