Skip to content

Commit

Permalink
Implement code blocks support
Browse files Browse the repository at this point in the history
  • Loading branch information
golozubov committed Jul 10, 2024
1 parent 4e75d41 commit cdb9876
Show file tree
Hide file tree
Showing 9 changed files with 451 additions and 68 deletions.
27 changes: 27 additions & 0 deletions src/components/code-block.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import cn from 'classnames';
import PropTypes from 'prop-types';
import styles from './code-block.module.scss';

/** @argument {{ text: string, className: [string] }} */
export default function CodeBlock({ text, className = '' }) {
const clsName = cn(styles.codeBlock, className);

return (
<>
<span className="p-break">
<br /> <br />
</span>
<pre className={clsName}>
<code>{text}</code>
</pre>
<span className="p-break">
<br /> <br />
</span>
</>
);
}

CodeBlock.propTypes = {
text: PropTypes.string.isRequired,
className: PropTypes.string,
};
11 changes: 11 additions & 0 deletions src/components/code-block.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.codeBlock {
display: inline;
padding: unset;
font-size: 90%;
word-break: auto-phrase;
white-space: pre-wrap;
background-color: transparent;
border: none;
position: relative;
margin: unset;
}
12 changes: 10 additions & 2 deletions src/components/linkify-elements.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import {
SHORT_LINK,
isShortLink,
CODE_INLINE,
trimBackticks,
CODE_BLOCK,
} from '../utils/parse-text';
import { INITIAL_CHECKBOX, isChecked } from '../utils/initial-checkbox';
import UserName from './user-name';
import { MediaOpener, getMediaType } from './media-opener';
import { InitialCheckbox } from './initial-checkbox';
import { Anchor, Link } from './linkify-links';
import CodeBlock from './code-block';

const { searchEngine } = CONFIG.search;
const MAX_URL_LENGTH = 50;
Expand Down Expand Up @@ -140,7 +141,14 @@ export function tokenToElement(token, key, params) {
return <InitialCheckbox key={key} checked={isChecked(token.text)} />;

case CODE_INLINE:
return <code key={key}>{trimBackticks(token.text)}</code>;
return (
<code key={key} className="inline-code">
{token.text}
</code>
);

case CODE_BLOCK:
return <CodeBlock key={key} text={token.text} />;
}
return token.text;
}
Expand Down
12 changes: 9 additions & 3 deletions src/utils/parse-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export const PARAGRAPH_BREAK = 'PARAGRAPH_BREAK';
export const REDDIT_LINK = 'REDDIT_LINK';
export const SHORT_LINK = 'SHORT_LINK';
export const CODE_INLINE = 'CODE_INLINE';
export const CODE_BLOCK = 'CODE_BLOCK';

const redditLinks = withFilters(
reTokenizer(/\/?r\/[A-Za-z\d]\w{1,20}/g, makeToken(REDDIT_LINK)),
Expand Down Expand Up @@ -126,10 +127,14 @@ export const lineBreaks = reTokenizer(/[^\S\n]*\n\s*/g, (offset, text) => {
return makeToken(PARAGRAPH_BREAK)(offset, text);
});

export const trimBackticks = (code) => code.replace(/^`+/, '').replace(/`+$/, '');

export const codeInline = withFilters(
reTokenizer(/``.+?``|`[^`]+`/g, makeToken(CODE_INLINE)),
reTokenizer(/``.+?``|`[^`]+`/gs, makeToken(CODE_INLINE)),
withCharsBefore(wordAdjacentChars.withoutChars('`').withChars('-')),
withCharsAfter(wordAdjacentChars.withoutChars('`').withChars('-')),
);

export const codeBlocks = withFilters(
reTokenizer(/```.+?```/gs, makeToken(CODE_BLOCK)),
withCharsBefore(wordAdjacentChars.withoutChars('`').withChars('-')),
withCharsAfter(wordAdjacentChars.withoutChars('`').withChars('-')),
);
Expand All @@ -149,6 +154,7 @@ export const parseText = withTexts(
checkboxParser,
lineBreaks,
codeInline,
codeBlocks,
),
),
);
Expand Down
7 changes: 7 additions & 0 deletions styles/shared/post.scss
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ $post-line-height: rem(20px);
}
}

// Inline code elements
code.inline-code {
color: unset;
background-color: unset;
padding: unset;
}

.text-no-breaks br {
display: none;
}
Expand Down
51 changes: 49 additions & 2 deletions test/jest/__snapshots__/piece-of-text.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,51 @@ exports[`PieceOfText > Renders an empty span if empty 1`] = `
</DocumentFragment>
`;

exports[`PieceOfText > Renders text with code block 1`] = `
<DocumentFragment>
<span
class="Linkify"
dir="auto"
role="region"
>
<span
dir="auto"
>
<span
class="p-break"
>
<br />
<br />
</span>
<pre
class="codeBlock"
>
<code>
\`\`\` 1+1=2; foo(); @mention user@example.com #hashtag ^ &lt;spoiler&gt;https://example.com \`\`\`
</code>
</pre>
<span
class="p-break"
>
<br />
<br />
</span>
</span>
<a
aria-disabled="false"
class="read-more"
role="button"
tabindex="0"
>
Expand
</a>
</span>
</DocumentFragment>
`;

exports[`PieceOfText > Renders text with inline code 1`] = `
<DocumentFragment>
<span
Expand All @@ -104,8 +149,10 @@ exports[`PieceOfText > Renders text with inline code 1`] = `
<span
dir="auto"
>
<code>
1+1=2; foo(); @mention user@example.com #hashtag ^ &lt;spoiler&gt;https://example.com
<code
class="inline-code"
>
\`1+1=2; foo(); @mention user@example.com #hashtag ^ &lt;spoiler&gt;https://example.com\`
</code>
</span>
Expand Down
8 changes: 8 additions & 0 deletions test/jest/piece-of-text.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,12 @@ describe('PieceOfText', () => {

expect(asFragment()).toMatchSnapshot();
});

it('Renders text with code block', () => {
const code =
'```\n1+1=2;\nfoo();\n@mention \nuser@example.com \n\n#hashtag \n^ \n\n<spoiler>https://example.com\n```';
const { asFragment } = render(<PieceOfText text={code} />);

expect(asFragment()).toMatchSnapshot();
});
});
34 changes: 31 additions & 3 deletions test/unit/components/piece-of-text.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Linkify from '../../../src/components/linkify';
import { ButtonLink } from '../../../src/components/button-link';
import { Anchor } from '../../../src/components/linkify-links';
import ErrorBoundary from '../../../src/components/error-boundary';
import CodeBlock from '../../../src/components/code-block';

const expect = unexpected.clone().use(unexpectedReact);

Expand Down Expand Up @@ -111,10 +112,10 @@ describe('<PieceOfText>', () => {

it('should correctly process texts with inline code', () => {
const code =
'1+1=2; foo(); @mention \n user@example.com \n\n #hashtag \n ^ \n\n <spoiler>https://example.com';
const text = `Here is the code \`${code}\`. </spoiler> Read it carefully`;
'`1+1=2; foo(); @mention \n user@example.com \n\n #hashtag \n ^ \n\n <spoiler>https://example.com`';
const text = `Here is the code ${code}. </spoiler> Read it carefully`;
expect(
<Linkify>{text}</Linkify>, //<PieceOfText text={text} readMoreStyle={READMORE_STYLE_COMFORT} />,
<Linkify>{text}</Linkify>,
'when rendered',
'to have rendered with all children',
<span>
Expand All @@ -126,4 +127,31 @@ describe('<PieceOfText>', () => {
</span>,
);
});

it('should correctly process texts with code blocks', () => {
const codeBlock =
'```1+1=2; foo(); @mention \n user@example.com \n\n #hashtag \n ^ \n\n <spoiler>https://example.com```';
const text = `Here is the code block\n ${codeBlock}.\n</spoiler> Read it carefully`;
expect(
<Linkify>{text}</Linkify>,
'when rendered',
'to have rendered with all children',
<span>
<ErrorBoundary>
{'Here is the code block'}
<no-display-name>
{' '}
<br />
</no-display-name>
<CodeBlock text={codeBlock} />
{'.'}
<no-display-name>
{' '}
<br />
</no-display-name>
{'</spoiler> Read it carefully'}
</ErrorBoundary>
</span>,
);
});
});
Loading

0 comments on commit cdb9876

Please sign in to comment.