Skip to content

Commit

Permalink
feat: init markdown component
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Jun 16, 2023
1 parent e062711 commit fcbbb86
Show file tree
Hide file tree
Showing 34 changed files with 1,554 additions and 12 deletions.
4 changes: 2 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ClerkProvider } from '@clerk/nextjs'
import { Root } from '~/components/layout/root/Root'
import { ClerkZhCN } from '~/i18n/cherk-cn'
import { defineMetadata } from '~/lib/define-metadata'
import { sansFont } from '~/lib/fonts'
import { sansFont, serifFont } from '~/lib/fonts'
import { getQueryClient } from '~/utils/query-client.server'

import { Providers } from '../providers/root'
Expand Down Expand Up @@ -80,7 +80,7 @@ export default async function RootLayout(props: Props) {
<ClerkProvider localization={ClerkZhCN}>
<html lang="zh-CN" className="noise" suppressHydrationWarning>
<body
className={`${sansFont.variable} m-0 h-full p-0 font-sans antialiased`}
className={`${sansFont.variable} ${serifFont.variable} m-0 h-full p-0 font-sans antialiased`}
>
<Providers>
<Hydrate state={dehydratedState}>
Expand Down
6 changes: 3 additions & 3 deletions src/app/notes/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { useParams } from 'next/navigation'
import { PageDataHolder } from '~/components/common/PageHolder'
import { useSetHeaderMetaInfo } from '~/components/layout/header/internal/hooks'
import { Loading } from '~/components/ui/loading'
import { Markdown } from '~/components/ui/markdown'
import { Toc, TocAutoScroll } from '~/components/widgets/toc'
import { useBeforeMounted } from '~/hooks/common/use-before-mounted'
import { useNoteByNidQuery } from '~/hooks/data/use-note'
import { ArticleElementProvider } from '~/providers/article/article-element-provider'
import { useSetCurrentNoteId } from '~/providers/note/current-note-id-provider'
import { NoteLayoutRightSidePortal } from '~/providers/note/right-side-provider'
import { parseMarkdown } from '~/remark'

const PageImpl = () => {
const { id } = useParams() as { id: string }
Expand All @@ -39,7 +39,7 @@ const PageImpl = () => {
return <Loading className="mt-12" />
}

const mardownResult = parseMarkdown(data?.data?.text ?? '')
// const mardownResult = parseMarkdown(note.text ?? '')

// Why do this, I mean why do set NoteId to context, don't use `useParams().id` for children components.
// Because any router params or query changes, will cause components that use `useParams()` hook, this hook is a context hook,
Expand Down Expand Up @@ -67,7 +67,7 @@ const PageImpl = () => {
</header>

<ArticleElementProvider>
{mardownResult.jsx}
<Markdown value={note.text} />

<NoteLayoutRightSidePortal>
<Toc className="sticky top-[120px] ml-4 mt-[120px]" />
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const MemoedHeader = memo(() => {

<HeaderLogoArea>
<AnimatedLogo />
<SiteOwnerAvatar className="hidden lg:inline-block" />
<SiteOwnerAvatar className="absolute bottom-[10px] right-[-3px] hidden lg:inline-block" />
<OnlyMobile>
<HeaderMeta />
</OnlyMobile>
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/header/internal/SiteOwnerAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const SiteOwnerAvatar: Component = ({ className }) => {
return (
<div
className={clsxm(
'absolute bottom-[8px] right-[-5px] overflow-hidden rounded-full border-[1.5px] border-accent/50',
'overflow-hidden rounded-full border-[1.5px] border-accent/50',
className,
)}
>
Expand Down
76 changes: 76 additions & 0 deletions src/components/ui/banner/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react'
import { clsx } from 'clsx'
import type { FC } from 'react'

import {
ClaritySuccessLine,
FluentShieldError20Regular,
FluentWarning28Regular,
IonInformation,
} from '../../icons/status'

const IconMap = {
warning: FluentWarning28Regular,
info: IonInformation,
error: FluentShieldError20Regular,
success: ClaritySuccessLine,
}

const bgColorMap = {
warning: 'bg-amber-50 dark:bg-amber-300',
info: 'bg-always-blue-50 dark:bg-always-blue-300',
success: 'bg-always-green-50 dark:bg-always-green-300',
error: 'bg-always-red-50 dark:bg-always-red-300',
}

const borderColorMap = {
warning: 'border-amber-300',
info: 'border-always-blue-300',

success: 'border-always-green-300',
error: 'border-always-red-300',
}

const iconColorMap = {
warning: 'text-amber-500',
info: 'text-always-blue-500',
success: 'text-always-green-500',
error: 'text-always-red-500',
}

export const Banner: FC<{
type: 'warning' | 'error' | 'success' | 'info'
message?: string | React.ReactNode
className?: string
children?: React.ReactNode
placement?: 'center' | 'left'
showIcon?: boolean
}> = (props) => {
const Icon = IconMap[props.type] || IconMap.info
const { placement = 'center', showIcon = true } = props
return (
<div
className={clsx(
'block items-center space-x-4 rounded-md border p-6 text-neutral-900 dark:bg-opacity-10 dark:text-[#c4c4c4] md:flex ' +
`${bgColorMap[props.type] || bgColorMap.info} ${
borderColorMap[props.type] || borderColorMap.info
}`,
placement == 'center' ? 'justify-center' : 'justify-start',
props.className,
)}
>
{showIcon && (
<Icon
className={`flex-shrink-0 self-start text-3xl ${
iconColorMap[props.type] || iconColorMap.info
} float-left -mr-2 md:float-none md:mr-2`}
/>
)}
{props.message ? (
<span className="leading-[1.8]">{props.message}</span>
) : (
props.children
)}
</div>
)
}
1 change: 1 addition & 0 deletions src/components/ui/banner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Banner'
44 changes: 44 additions & 0 deletions src/components/ui/collapse/Collapse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client'

import { AnimatePresence, motion } from 'framer-motion'
import * as React from 'react'

import { microReboundPreset } from '~/constants/spring'

export const Collapse = ({
isOpened,
className,
children,
}: React.PropsWithChildren<{ isOpened: boolean } & { className?: string }>) => {
// By using `AnimatePresence` to mount and unmount the contents, we can animate
// them in and out while also only rendering the contents of open accordions
return (
<>
<AnimatePresence initial={false}>
{isOpened && (
<motion.div
key="content"
initial="collapsed"
animate="open"
exit="collapsed"
variants={{
open: {
opacity: 1,
height: 'auto',
transition: microReboundPreset,
},
collapsed: {
opacity: 0,
height: 0,
overflow: 'hidden',
},
}}
className={className}
>
{children}
</motion.div>
)}
</AnimatePresence>
</>
)
}
1 change: 1 addition & 0 deletions src/components/ui/collapse/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Collapse'
1 change: 1 addition & 0 deletions src/components/ui/image/ZoomedImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type TImageProps = {
'original-src'?: string
imageRef?: React.MutableRefObject<HTMLImageElement>
zoom?: boolean
accentColor?: string
} & React.HTMLAttributes<HTMLImageElement> &
ImageProps

Expand Down
182 changes: 182 additions & 0 deletions src/components/ui/markdown/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/* eslint-disable react-hooks/rules-of-hooks */
import React, {
createElement,
memo,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { clsx } from 'clsx'
import { compiler } from 'markdown-to-jsx'
import type { MarkdownToJSX } from 'markdown-to-jsx'
import type { FC, PropsWithChildren } from 'react'

import { range } from '~/lib/_'

import styles from './index.module.css'
import { CommentAtRule } from './parsers/comment-at'
import { ContainerRule } from './parsers/container'
import { InsertRule } from './parsers/ins'
import { KateXRule } from './parsers/katex'
import { MarkRule } from './parsers/mark'
import { MentionRule } from './parsers/mention'
import { SpoilderRule } from './parsers/spoiler'
import { MParagraph, MTableBody, MTableHead, MTableRow } from './renderers'
import { MDetails } from './renderers/collapse'
import { MFootNote } from './renderers/footnotes'

export interface MdProps {
value?: string

style?: React.CSSProperties
readonly renderers?: { [key: string]: Partial<MarkdownToJSX.Rule> }
wrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>
codeBlockFully?: boolean
className?: string
tocSlot?: (props: { headings: HTMLElement[] }) => JSX.Element | null
}

export const Markdown: FC<MdProps & MarkdownToJSX.Options & PropsWithChildren> =
memo((props) => {
const {
value,
renderers,
style,
wrapperProps = {},
codeBlockFully = false,
className,
overrides,
extendsRules,
additionalParserRules,

...rest
} = props

const ref = useRef<HTMLDivElement>(null)
const [headings, setHeadings] = useState<HTMLElement[]>([])

useEffect(() => {
if (!ref.current) {
return
}

const $headings = ref.current.querySelectorAll(
range(1, 6)
.map((i) => `h${i}`)
.join(','),
) as NodeListOf<HTMLHeadingElement>

setHeadings(Array.from($headings))

return () => {
setHeadings([])
}
}, [value, props.children])

const node = useMemo(() => {
if (!value && typeof props.children != 'string') return null

const mdElement = compiler(`${value || props.children}`, {
wrapper: null,
// @ts-ignore
overrides: {
p: MParagraph,

thead: MTableHead,
tr: MTableRow,
tbody: MTableBody,
// FIXME: footer tag in raw html will renders not as expected, but footer tag in this markdown lib will wrapper as linkReferer footnotes
footer: MFootNote,
details: MDetails,

// for custom react component
// LinkCard,
...overrides,
},

extendsRules: {
gfmTask: {
react(node, _, state) {
return (
<label
className="mr-2 inline-flex items-center"
key={state?.key}
>
<input type="checkbox" checked={node.completed} readOnly />
</label>
)
},
},

list: {
react(node, output, state) {
const Tag = node.ordered ? 'ol' : 'ul'

return (
<Tag key={state?.key} start={node.start}>
{node.items.map((item: any, i: number) => {
let className = ''
if (item[0]?.type == 'gfmTask') {
className = 'list-none flex items-center'
}

return (
<li className={className} key={i}>
{output(item, state!)}
</li>
)
})}
</Tag>
)
},
},

...extendsRules,
...renderers,
},
additionalParserRules: {
spoilder: SpoilderRule,
mention: MentionRule,
commentAt: CommentAtRule,
mark: MarkRule,
ins: InsertRule,
kateX: KateXRule,
container: ContainerRule,
...additionalParserRules,
},
...rest,
})

return mdElement
}, [
value,
props.children,
overrides,
extendsRules,
renderers,
additionalParserRules,
rest,
])

return (
<div
id="write"
style={style}
{...wrapperProps}
ref={ref}
className={clsx(
styles['md'],
codeBlockFully ? styles['code-fully'] : undefined,
wrapperProps.className,
)}
>
{className ? <div className={className}>{node}</div> : node}

{props.tocSlot ? createElement(props.tocSlot, { headings }) : null}
</div>
)
})
Loading

1 comment on commit fcbbb86

@vercel
Copy link

@vercel vercel bot commented on fcbbb86 Jun 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

springtide – ./

springtide.vercel.app
springtide-innei.vercel.app
springtide-git-main-innei.vercel.app

Please sign in to comment.