diff options
Diffstat (limited to 'webui/src')
-rw-r--r-- | webui/src/App.tsx | 21 | ||||
-rw-r--r-- | webui/src/components/Content/index.tsx | 98 | ||||
-rw-r--r-- | webui/src/components/Label.tsx | 4 | ||||
-rw-r--r-- | webui/src/components/Themer.tsx | 2 | ||||
-rw-r--r-- | webui/src/index.tsx | 1 | ||||
-rw-r--r-- | webui/src/pages/bug/LabelChange.tsx | 4 | ||||
-rw-r--r-- | webui/src/themes/highlight-theme.scss | 13 |
7 files changed, 107 insertions, 36 deletions
diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 173f35ff7..a8712a945 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import { Route, Routes } from 'react-router'; import Layout from './components/Header'; @@ -9,14 +10,16 @@ import NotFoundPage from './pages/notfound/NotFoundPage'; export default function App() { return ( - <Layout> - <Routes> - <Route path="/" element={<ListPage />} /> - <Route path="/new" element={<NewBugPage />} /> - <Route path="/bug/:id" element={<BugPage />} /> - <Route path="/user/:id" element={<IdentityPage />} /> - <Route element={<NotFoundPage />} /> - </Routes> - </Layout> + <React.StrictMode> + <Layout> + <Routes> + <Route path="/" element={<ListPage />} /> + <Route path="/new" element={<NewBugPage />} /> + <Route path="/bug/:id" element={<BugPage />} /> + <Route path="/user/:id" element={<IdentityPage />} /> + <Route element={<NotFoundPage />} /> + </Routes> + </Layout> + </React.StrictMode> ); } diff --git a/webui/src/components/Content/index.tsx b/webui/src/components/Content/index.tsx index 9bf9ff7a6..3cfa9eb89 100644 --- a/webui/src/components/Content/index.tsx +++ b/webui/src/components/Content/index.tsx @@ -1,11 +1,24 @@ -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'; @@ -13,32 +26,71 @@ 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'], + }) + .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; diff --git a/webui/src/components/Label.tsx b/webui/src/components/Label.tsx index 709aceff9..a63398487 100644 --- a/webui/src/components/Label.tsx +++ b/webui/src/components/Label.tsx @@ -26,14 +26,16 @@ const createStyle = (color: Color, maxWidth?: string) => ({ type Props = { label: LabelFragment; + inline?: boolean; maxWidth?: string; className?: string; }; -function Label({ label, maxWidth, className }: Props) { +function Label({ label, inline, maxWidth, className }: Props) { return ( <Chip size={'small'} label={label.name} + component={inline ? 'span' : 'div'} className={className} style={createStyle(label.color, maxWidth)} /> diff --git a/webui/src/components/Themer.tsx b/webui/src/components/Themer.tsx index 9934888d1..f03881b4d 100644 --- a/webui/src/components/Themer.tsx +++ b/webui/src/components/Themer.tsx @@ -11,7 +11,7 @@ declare module '@mui/styles/defaultTheme' { interface DefaultTheme extends Theme {} } -const ThemeContext = createContext({ +export const ThemeContext = createContext({ toggleMode: () => {}, mode: '', }); diff --git a/webui/src/index.tsx b/webui/src/index.tsx index a7cbe559b..7667c7068 100644 --- a/webui/src/index.tsx +++ b/webui/src/index.tsx @@ -7,6 +7,7 @@ import App from './App'; import apolloClient from './apollo'; import Themer from './components/Themer'; import { defaultLightTheme, defaultDarkTheme } from './themes/index'; +import './themes/highlight-theme.scss'; const root = createRoot(document.getElementById('root') as HTMLElement); root.render( diff --git a/webui/src/pages/bug/LabelChange.tsx b/webui/src/pages/bug/LabelChange.tsx index 6b356d14a..28fc47433 100644 --- a/webui/src/pages/bug/LabelChange.tsx +++ b/webui/src/pages/bug/LabelChange.tsx @@ -35,12 +35,12 @@ function LabelChange({ op }: Props) { <Author author={op.author} className={classes.author} /> {added.length > 0 && <span> added the </span>} {added.map((label, index) => ( - <Label key={index} label={label} className={classes.label} /> + <Label inline key={index} label={label} className={classes.label} /> ))} {added.length > 0 && removed.length > 0 && <span> and</span>} {removed.length > 0 && <span> removed the </span>} {removed.map((label, index) => ( - <Label key={index} label={label} className={classes.label} /> + <Label inline key={index} label={label} className={classes.label} /> ))} <span> {' '} diff --git a/webui/src/themes/highlight-theme.scss b/webui/src/themes/highlight-theme.scss new file mode 100644 index 000000000..e6f145e9b --- /dev/null +++ b/webui/src/themes/highlight-theme.scss @@ -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'); +} |