summaryrefslogtreecommitdiffstatshomepage
path: root/webui/src
diff options
context:
space:
mode:
Diffstat (limited to 'webui/src')
-rw-r--r--webui/src/App.tsx21
-rw-r--r--webui/src/components/Content/index.tsx98
-rw-r--r--webui/src/components/Label.tsx4
-rw-r--r--webui/src/components/Themer.tsx2
-rw-r--r--webui/src/index.tsx1
-rw-r--r--webui/src/pages/bug/LabelChange.tsx4
-rw-r--r--webui/src/themes/highlight-theme.scss13
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');
+}