Repository navigation
feat(webui): remark upgrade + gfm + syntax highlighting #1444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
da6d367
4aac48e
c2ff902
e5a21d3
f4c5170
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,44 +1,96 @@ | ||
| import { createElement, Fragment, useEffect, useState } from 'react'; | ||
| import type { Root as HastRoot } from 'hast'; | ||
| import type { Root as MdRoot } from 'mdast'; | ||
| import { useEffect, useState } from 'react'; | ||
| import * as React from 'react'; | ||
| import * as production from 'react/jsx-runtime'; | ||
| import rehypeHighlight, { | ||
| Options as RehypeHighlightOpts, | ||
| } from 'rehype-highlight'; | ||
| import rehypeReact from 'rehype-react'; | ||
| import gemoji from 'remark-gemoji'; | ||
| import html from 'remark-html'; | ||
| import parse from 'remark-parse'; | ||
| import rehypeSanitize from 'rehype-sanitize'; | ||
| import remarkBreaks from 'remark-breaks'; | ||
| import remarkGemoji from 'remark-gemoji'; | ||
| import remarkGfm from 'remark-gfm'; | ||
| import remarkParse from 'remark-parse'; | ||
| import remarkRehype from 'remark-rehype'; | ||
| import type { Options as RemarkRehypeOptions } from 'remark-rehype'; | ||
| import { unified } from 'unified'; | ||
| import type { Plugin, Processor } from 'unified'; | ||
| import { Node as UnistNode } from 'unified/lib'; | ||
|
|
||
| import { ThemeContext } from '../Themer'; | ||
|
|
||
| import AnchorTag from './AnchorTag'; | ||
| import BlockQuoteTag from './BlockQuoteTag'; | ||
| import ImageTag from './ImageTag'; | ||
| import PreTag from './PreTag'; | ||
|
|
||
| type Props = { markdown: string }; | ||
|
|
||
| // @lygaret 2025/05/16 | ||
| // type inference for some of this doesn't work, but the pipeline is fine | ||
| // this might get better when we upgrade typescript | ||
|
|
||
| type RemarkPlugin = Plugin<[], MdRoot, HastRoot>; | ||
| type RemarkRehypePlugin = Plugin<RemarkRehypeOptions[], MdRoot, HastRoot>; | ||
| type RehypePlugin<Options extends unknown[] = []> = Plugin< | ||
| Options, | ||
| HastRoot, | ||
| HastRoot | ||
| >; | ||
|
|
||
| const markdownPipeline: Processor< | ||
| UnistNode, | ||
| undefined, | ||
| undefined, | ||
| HastRoot, | ||
| React.JSX.Element | ||
| > = unified() | ||
| .use(remarkParse) | ||
| .use(remarkGemoji as unknown as RemarkPlugin) | ||
| .use(remarkBreaks as unknown as RemarkPlugin) | ||
| .use(remarkGfm) | ||
| .use(remarkRehype as unknown as RemarkRehypePlugin, { | ||
| allowDangerousHtml: true, | ||
| }) | ||
| .use(rehypeSanitize as unknown as RehypePlugin) | ||
| .use(rehypeHighlight as unknown as RehypePlugin<RehypeHighlightOpts[]>, { | ||
| detect: true, | ||
| subset: ['text'], | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the subset of languages syntax highlighting will try if no language is provided
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
that's a weird design choice. good catch.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could have hacked together css to do it, but this was cleaner imo
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, i see no issue with it. that's the behavior i would expect, tbh -- plain text unless i specify a language. |
||
| }) | ||
| .use(rehypeReact, { | ||
| ...production, | ||
| components: { | ||
| a: AnchorTag, | ||
| blockquote: BlockQuoteTag, | ||
| img: ImageTag, | ||
| pre: PreTag, | ||
| }, | ||
| }); | ||
|
|
||
| const Content: React.FC<Props> = ({ markdown }: Props) => { | ||
| const [Content, setContent] = useState(<></>); | ||
| const theme = React.useContext(ThemeContext); | ||
| const [content, setContent] = useState(<></>); | ||
|
|
||
| useEffect(() => { | ||
| unified() | ||
| .use(parse) | ||
| .use(gemoji) | ||
| .use(html) | ||
| .use(remarkRehype) | ||
| .use(rehypeReact, { | ||
| createElement, | ||
| Fragment, | ||
| components: { | ||
| img: ImageTag, | ||
| pre: PreTag, | ||
| a: AnchorTag, | ||
| blockquote: BlockQuoteTag, | ||
| }, | ||
| }) | ||
| markdownPipeline | ||
| .process(markdown) | ||
| .then((file) => { | ||
| setContent(file.result); | ||
| .then((file) => setContent(file.result)) | ||
| .catch((err: any) => { | ||
| setContent( | ||
| <> | ||
| <span className="error">{err}</span> | ||
| <pre>{markdown}</pre> | ||
| </> | ||
| ); | ||
| }); | ||
| }, [markdown]); | ||
|
|
||
| return Content; | ||
| return ( | ||
| <div className={'highlight-theme'} data-theme={theme.mode}> | ||
| {content} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default Content; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| @use 'sass:meta'; | ||
|
|
||
| // if highlight.js gets updated to support proper mixins, | ||
| // meta.load-css won't be necessary; see https://sass-lang.com/documentation/breaking-changes/import/#nested-imports | ||
|
|
||
| .highlight-theme[data-theme='light'], | ||
| .highlight-theme:not([data-theme='dark']) { | ||
| @include meta.load-css('~highlight.js/scss/base16/classic-light.scss'); | ||
| } | ||
|
|
||
| .highlight-theme[data-theme='dark'] { | ||
| @include meta.load-css('~highlight.js/scss/base16/classic-dark.scss'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gets us warnings for strict-mode violations in dev-mode, and nop in prod