Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,835 changes: 2,770 additions & 1,065 deletions webui/package-lock.json

Large diffs are not rendered by default.

19 changes: 12 additions & 7 deletions webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0",
"rehype-react": "^7.1.1",
"remark-gemoji": "^7.0.1",
"remark-html": "^15.0.1",
"remark-parse": "^10.0.1",
"remark-react": "^9.0.1",
"remark-rehype": "^10.1.0",
"unified": "^10.1.2"
"rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0",
"rehype-react": "^8.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-breaks": "^4.0.0",
"remark-gemoji": "^8.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"unified": "^11.0.5"
},
"devDependencies": {
"@babel/runtime": "^7.27.1",
Expand All @@ -40,6 +44,7 @@
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"react-scripts": "^5.0.1",
"sass": "^1.89.0",
"typescript": "^4.9.5"
},
"scripts": {
Expand Down
65 changes: 40 additions & 25 deletions webui/packed_assets.go

Large diffs are not rendered by default.

21 changes: 12 additions & 9 deletions webui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react';
import { Route, Routes } from 'react-router';

import Layout from './components/Header';
Expand All @@ -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>

Copy link
Copy Markdown
Contributor Author

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

);
}
98 changes: 75 additions & 23 deletions webui/src/components/Content/index.tsx
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'],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

  • without detect:true, code blocks without a given language have bad styling (no margin/padding)
  • ['text'] as a subset means we only bother trying plain text as the default, which does no additional highlighting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

without detect:true, code blocks without a given language have bad styling (no margin/padding)

that's a weird design choice. good catch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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;
4 changes: 3 additions & 1 deletion webui/src/components/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
/>
Expand Down
2 changes: 1 addition & 1 deletion webui/src/components/Themer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ declare module '@mui/styles/defaultTheme' {
interface DefaultTheme extends Theme {}
}

const ThemeContext = createContext({
export const ThemeContext = createContext({
toggleMode: () => {},
mode: '',
});
Expand Down
1 change: 1 addition & 0 deletions webui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions webui/src/pages/bug/LabelChange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
{' '}
Expand Down
13 changes: 13 additions & 0 deletions webui/src/themes/highlight-theme.scss
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');
}