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
35 changes: 35 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"react-dropzone": "^14.4.1",
"react-easy-crop": "^6.2.2",
"react-markdown": "^10.1.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1"
},
"peerDependencies": {
Expand Down
11 changes: 10 additions & 1 deletion src/components/announcements/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext, useContext } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkBreaks from 'remark-breaks';
import remarkGfm from 'remark-gfm';
import type { ComponentPropsWithoutRef } from 'react';

Expand Down Expand Up @@ -41,7 +42,15 @@ export function Markdown({ children }: MarkdownProps) {
return (
<div className="text-sm leading-relaxed" style={{ color: 'var(--text-secondary)' }}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
// `remark-breaks` makes a single newline a line break. Markdown's own
// rule is that it is a space, and only a blank line or two trailing
// spaces break the line — which is a reasonable rule for a document
// format and the wrong one for the field this text is typed into: the
// composer's Body is a plain textarea, and an author who pressed
// Enter once watched their two lines silently run together. Comment
// fields everywhere (GitHub's included) settle this the same way.
// It affects text only, so a code block's newlines stay untouched.
remarkPlugins={[remarkGfm, remarkBreaks]}
components={{
h1: props => <h1 className="text-lg font-semibold mt-4 mb-2" style={{ color: 'var(--text-primary)' }} {...props} />,
h2: props => <h2 className="text-base font-semibold mt-4 mb-2" style={{ color: 'var(--text-primary)' }} {...props} />,
Expand Down
24 changes: 24 additions & 0 deletions src/components/announcements/__tests__/Markdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,30 @@ describe('Markdown', () => {
expect(code.getAttribute('style') ?? '').toContain('--bg-hover');
});

// The composer's Body is a plain textarea, so an author presses Enter and
// expects a line break. Markdown's own rule — a single newline is a space,
// only a blank line or two trailing spaces break the line — silently ran
// their lines together, which is exactly what happened to a moderation
// note typed on two lines. Chat and comment fields everywhere (GitHub's
// included) resolve this the same way: honour the newline that was typed.
it('turns a single newline into a line break, as the textarea it was typed in shows it', () => {
const { container } = render(<Markdown>{'Test Test Test Test Test\nTestTestTestTestTest'}</Markdown>);
expect(container.querySelector('br')).toBeTruthy();
});

it('still starts a new paragraph on a blank line', () => {
const { container } = render(<Markdown>{'one\n\ntwo'}</Markdown>);
expect(container.querySelectorAll('p')).toHaveLength(2);
});

it('leaves the newlines inside a code block exactly as written', () => {
// A <br> injected into a code block would change the code itself.
const { container } = render(<Markdown>{'```\nnpm install\nnpm run build\n```'}</Markdown>);
const pre = container.querySelector('pre')!;
expect(pre.querySelector('br')).toBeNull();
expect(pre.textContent).toContain('npm install\nnpm run build');
});

it('gives a gfm table its own rules rather than leaving it borderless', () => {
const { container } = render(<Markdown>{'| Version | Status |\n| --- | --- |\n| 0.14.3 | current |'}</Markdown>);

Expand Down
Loading