fix(markdown): enhance Markdown component with middleware support,fix ai summary entry id renderer
- Updated the Markdown component to accept an `applyMiddleware` prop for custom processing. - Modified the `parseMarkdown` function to utilize the new middleware feature, allowing for additional processing steps. - Adjusted related components to ensure compatibility with the new middleware functionality. These changes improve the flexibility and extensibility of the Markdown rendering process. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
614a0a99d6
commit
c3812be806
|
|
@ -10,8 +10,8 @@ export const Markdown: Component<
|
|||
{
|
||||
children: string
|
||||
} & Partial<RemarkOptions>
|
||||
> = ({ children, components, className }) => {
|
||||
const stableRemarkOptions = useState({ components })[0]
|
||||
> = ({ children, components, className, applyMiddleware }) => {
|
||||
const stableRemarkOptions = useState({ components, applyMiddleware })[0]
|
||||
|
||||
const markdownElement = useMemo(
|
||||
() => parseMarkdown(children, { ...stableRemarkOptions }).content,
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ const Wrapper: Component<{
|
|||
"flex justify-end gap-3 p-2 text-white/70 duration-200 [&_button]:hover:text-white",
|
||||
"hover:!transform-none hover:!opacity-100",
|
||||
|
||||
sideContent ? "rounded-bl-xl" : "rounded-xl",
|
||||
"bg-black/30",
|
||||
// sideContent ? "rounded-bl-xl" : "rounded-xl",
|
||||
"bg-black/50",
|
||||
)}
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import type { Components } from "hast-util-to-jsx-runtime"
|
|||
import type { Variant } from "motion/react"
|
||||
import { m, useAnimationControls } from "motion/react"
|
||||
import type { FC } from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
|
||||
import { MenuItemText } from "~/atoms/context-menu"
|
||||
|
|
@ -53,6 +53,7 @@ import { Queries } from "~/queries"
|
|||
import { useEntry } from "~/store/entry"
|
||||
import { useFeedById } from "~/store/feed"
|
||||
|
||||
import { remarkSnowflakeId } from "./plugins/parse-snowflake"
|
||||
import type { DailyItemProps, DailyView } from "./types"
|
||||
import { useParseDailyDate } from "./useParseDailyDate"
|
||||
|
||||
|
|
@ -176,17 +177,28 @@ export const DailyReportContent: Component<DailyReportContentProps> = ({
|
|||
const content = useQueryData({ endDate, startDate, view })
|
||||
|
||||
const RelatedEntryLink = useState(() => createRelatedEntryLink("modal"))[0]
|
||||
|
||||
return (
|
||||
<Card className="border-none bg-transparent">
|
||||
<CardContent className={cn("space-y-0 p-0", className)}>
|
||||
<ScrollArea.ScrollArea mask={false} flex viewportClassName="max-h-[calc(100vh-176px)]">
|
||||
<ScrollArea.ScrollArea mask={false} flex viewportClassName="h-[calc(100vh-176px)]">
|
||||
<AutoResizeHeight spring>
|
||||
{content.isLoading ? (
|
||||
<LoadingCircle size="large" className="mt-8 text-center" />
|
||||
<LoadingCircle
|
||||
size="large"
|
||||
className="center flex h-[calc(100vh-176px)] text-center"
|
||||
/>
|
||||
) : (
|
||||
!!content.data && (
|
||||
<Markdown
|
||||
applyMiddleware={(pipeline) => {
|
||||
pipeline.use(remarkSnowflakeId)
|
||||
|
||||
return pipeline
|
||||
}}
|
||||
components={{
|
||||
// @ts-expect-error
|
||||
"snowflake-id": SnowflakeId,
|
||||
a: RelatedEntryLink as Components["a"],
|
||||
}}
|
||||
className="prose-sm prose-p:my-1 prose-ul:my-1 prose-ul:list-outside prose-ul:list-disc prose-li:marker:text-accent mt-4 px-6"
|
||||
|
|
@ -265,8 +277,7 @@ const createRelatedEntryLink = (variant: "toast" | "modal") => (props: LinkProps
|
|||
const { href, children } = props
|
||||
const entryId = isBizId(href) ? href : null
|
||||
|
||||
const { present } = useModalStack()
|
||||
|
||||
const peekModal = usePeekModal()
|
||||
if (!entryId) {
|
||||
return <MarkdownLink {...props} />
|
||||
}
|
||||
|
|
@ -275,49 +286,7 @@ const createRelatedEntryLink = (variant: "toast" | "modal") => (props: LinkProps
|
|||
type="button"
|
||||
className="follow-link--underline text-foreground cursor-pointer font-semibold no-underline"
|
||||
onClick={() => {
|
||||
const basePresentProps = {
|
||||
clickOutsideToDismiss: true,
|
||||
title: "Entry Preview",
|
||||
}
|
||||
|
||||
if (variant === "toast") {
|
||||
present({
|
||||
...basePresentProps,
|
||||
CustomModalComponent: PlainModal,
|
||||
content: () => <EntryToastPreview entryId={entryId} />,
|
||||
overlay: false,
|
||||
modal: false,
|
||||
modalContainerClassName: "right-0 left-[auto]",
|
||||
})
|
||||
} else {
|
||||
present({
|
||||
...basePresentProps,
|
||||
autoFocus: false,
|
||||
modalClassName:
|
||||
"relative mx-auto mt-[10vh] scrollbar-none max-w-full overflow-auto px-2 lg:max-w-[65rem] lg:p-0",
|
||||
// eslint-disable-next-line @eslint-react/no-nested-component-definitions
|
||||
CustomModalComponent: ({ children }) => {
|
||||
const { feedId } = useEntry(entryId) || {}
|
||||
|
||||
return (
|
||||
<PeekModal
|
||||
rightActions={[
|
||||
{
|
||||
onClick: () => {},
|
||||
label: "More Actions",
|
||||
icon: <EntryMoreActions entryId={entryId} />,
|
||||
},
|
||||
]}
|
||||
to={`/timeline/view-${getRouteParams().view}/${feedId}/${entryId}`}
|
||||
>
|
||||
{children}
|
||||
</PeekModal>
|
||||
)
|
||||
},
|
||||
content: () => <EntryModalPreview entryId={entryId} />,
|
||||
overlay: true,
|
||||
})
|
||||
}
|
||||
peekModal(entryId, variant)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
@ -326,6 +295,57 @@ const createRelatedEntryLink = (variant: "toast" | "modal") => (props: LinkProps
|
|||
)
|
||||
}
|
||||
|
||||
const usePeekModal = () => {
|
||||
const { present } = useModalStack()
|
||||
return useCallback(
|
||||
(entryId: string, variant: "toast" | "modal") => {
|
||||
const basePresentProps = {
|
||||
clickOutsideToDismiss: true,
|
||||
title: "Entry Preview",
|
||||
}
|
||||
|
||||
if (variant === "toast") {
|
||||
present({
|
||||
...basePresentProps,
|
||||
CustomModalComponent: PlainModal,
|
||||
content: () => <EntryToastPreview entryId={entryId} />,
|
||||
overlay: false,
|
||||
modal: false,
|
||||
modalContainerClassName: "right-0 left-[auto]",
|
||||
})
|
||||
} else {
|
||||
present({
|
||||
...basePresentProps,
|
||||
autoFocus: false,
|
||||
modalClassName:
|
||||
"relative mx-auto mt-[10vh] scrollbar-none max-w-full overflow-auto px-2 lg:max-w-[65rem] lg:p-0",
|
||||
|
||||
CustomModalComponent: ({ children }) => {
|
||||
const { feedId } = useEntry(entryId) || {}
|
||||
|
||||
return (
|
||||
<PeekModal
|
||||
rightActions={[
|
||||
{
|
||||
onClick: () => {},
|
||||
label: "More Actions",
|
||||
icon: <EntryMoreActions entryId={entryId} />,
|
||||
},
|
||||
]}
|
||||
to={`/timeline/view-${getRouteParams().view}/${feedId}/${entryId}`}
|
||||
>
|
||||
{children}
|
||||
</PeekModal>
|
||||
)
|
||||
},
|
||||
content: () => <EntryModalPreview entryId={entryId} />,
|
||||
overlay: true,
|
||||
})
|
||||
}
|
||||
},
|
||||
[present],
|
||||
)
|
||||
}
|
||||
const EntryToastPreview = ({ entryId }: { entryId: string }) => {
|
||||
useAuthQuery(Queries.entries.byId(entryId))
|
||||
|
||||
|
|
@ -508,3 +528,27 @@ const EntryMoreActions: FC<{ entryId: string }> = ({ entryId }) => {
|
|||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
interface SnowflakeIdProps {
|
||||
id: string
|
||||
children?: React.ReactNode
|
||||
index: number
|
||||
}
|
||||
|
||||
const SnowflakeId: React.FC<SnowflakeIdProps> = ({ id: entryId, index }) => {
|
||||
const peekModal = usePeekModal()
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="follow-link--underline text-foreground cursor-pointer font-semibold no-underline"
|
||||
onClick={() => {
|
||||
peekModal(entryId, "modal")
|
||||
}}
|
||||
>
|
||||
<sup className="inline-flex items-center gap-0.5 font-medium">
|
||||
<span className="text-[0.65rem] opacity-70">{index}</span>
|
||||
</sup>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import type { Parent } from "mdast"
|
||||
import type { Node } from "unist"
|
||||
import { visit } from "unist-util-visit"
|
||||
|
||||
interface SnowflakeOptions {
|
||||
component?: string
|
||||
}
|
||||
|
||||
const SNOWFLAKE_PATTERN = /\[(\d{15,20})\]/g
|
||||
|
||||
function isTextNode(node: Node): node is { type: "text"; value: string } {
|
||||
return node.type === "text"
|
||||
}
|
||||
|
||||
export function remarkSnowflakeId(options: SnowflakeOptions = {}) {
|
||||
const { component = "snowflake-id" } = options
|
||||
|
||||
return (tree: Node) => {
|
||||
let idIndex = 1
|
||||
visit(tree, "text", (node, index, parent: Parent) => {
|
||||
if (!isTextNode(node) || !parent) return
|
||||
|
||||
const { value } = node as { value: string }
|
||||
const matches = Array.from(value.matchAll(SNOWFLAKE_PATTERN))
|
||||
|
||||
if (matches.length === 0) return
|
||||
|
||||
const result: (Node | string)[] = []
|
||||
let lastIndex = 0
|
||||
|
||||
for (const match of matches) {
|
||||
if (!Array.isArray(match)) continue
|
||||
|
||||
const [fullMatch, snowflakeId] = match
|
||||
const matchIndex = match.index!
|
||||
|
||||
// Add text before the match
|
||||
if (matchIndex > lastIndex) {
|
||||
result.push({
|
||||
type: "text",
|
||||
// @ts-expect-error
|
||||
value: value.slice(lastIndex, matchIndex),
|
||||
})
|
||||
}
|
||||
|
||||
// Add snowflake node
|
||||
result.push({
|
||||
type: "snowflakeId",
|
||||
data: {
|
||||
hName: component,
|
||||
hProperties: {
|
||||
id: snowflakeId,
|
||||
index: idIndex++,
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
children: [
|
||||
{
|
||||
type: "text",
|
||||
value: fullMatch,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
lastIndex = matchIndex + fullMatch.length
|
||||
}
|
||||
|
||||
// Add any remaining text after the last match
|
||||
if (lastIndex < value.length) {
|
||||
result.push({
|
||||
type: "text",
|
||||
// @ts-expect-error
|
||||
value: value.slice(lastIndex),
|
||||
})
|
||||
}
|
||||
|
||||
// Replace the current node with the new nodes
|
||||
// @ts-expect-error
|
||||
parent.children.splice(index, 1, ...result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -14,17 +14,20 @@ import remarkGfm from "remark-gfm"
|
|||
import remarkGithubAlerts from "remark-gh-alerts"
|
||||
import remarkParse from "remark-parse"
|
||||
import remarkRehype from "remark-rehype"
|
||||
import type { Processor } from "unified"
|
||||
import { unified } from "unified"
|
||||
import { VFile } from "vfile"
|
||||
|
||||
export interface RemarkOptions {
|
||||
components: Partial<Components>
|
||||
applyMiddleware?: <T extends Processor<any, any, any, any, any>>(pipeline: T) => T
|
||||
}
|
||||
|
||||
export const parseMarkdown = (content: string, options?: Partial<RemarkOptions>) => {
|
||||
const file = new VFile(content)
|
||||
const { components } = options || {}
|
||||
const { components, applyMiddleware } = options || {}
|
||||
|
||||
const pipeline = unified()
|
||||
let pipeline: Processor<any, any, any, any, any> = unified()
|
||||
.use(remarkDirective)
|
||||
.use(remarkParse)
|
||||
|
||||
|
|
@ -61,6 +64,12 @@ export const parseMarkdown = (content: string, options?: Partial<RemarkOptions>)
|
|||
},
|
||||
})
|
||||
|
||||
// Apply custom middleware if provided
|
||||
if (applyMiddleware) {
|
||||
pipeline = applyMiddleware(pipeline)
|
||||
}
|
||||
|
||||
pipeline = pipeline
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
.use(rehypeStringify, { allowDangerousHtml: true })
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue