Skip to content

Commit

Permalink
feat(CodeBlock): allow var, mark, and anchor tags within code blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
jerelmiller committed Feb 2, 2021
1 parent e25190f commit a916fbb
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 3 deletions.
17 changes: 16 additions & 1 deletion packages/gatsby-theme-newrelic/src/components/CodeBlock.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { css } from '@emotion/core';
Expand All @@ -7,6 +8,7 @@ import CodeEditor from './CodeEditor';
import Icon from './Icon';
import CodeHighlight from './CodeHighlight';
import MiddleEllipsis from 'react-middle-ellipsis';
import RawCode from './RawCode';
import useClipboard from '../hooks/useClipboard';
import useFormattedCode from '../hooks/useFormattedCode';
import useThemeTranslation from '../hooks/useThemeTranslation';
Expand All @@ -21,10 +23,19 @@ const AUTO_FORMATTED_LANGUAGES = [
'scss',
];

const CONTAINS_VAR = /<var>(.*?)<\/var>/gs;
const CONTAINS_MARK = /<mark>(.*?)<\/mark>/gs;
const CONTAINS_LINK = /<a href=.*?>(.*?)<\/a>/gs;

const defaultComponents = {
Preview: LivePreview,
};

const containsEmbeddedHTML = (code) =>
[CONTAINS_VAR, CONTAINS_MARK, CONTAINS_LINK].some((regex) =>
regex.test(code)
);

const CodeBlock = ({
autoFormat,
children,
Expand All @@ -40,6 +51,8 @@ const CodeBlock = ({
preview,
scope,
}) => {
children = children.trim();

if (isJSLang()) {
language = 'jsx';
}
Expand Down Expand Up @@ -98,7 +111,9 @@ const CodeBlock = ({
overflow: auto;
`}
>
{live ? (
{language !== 'html' && containsEmbeddedHTML(children) ? (
<RawCode code={children} language={language} />
) : live ? (
<CodeEditor
value={code}
language={language}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const CodeHighlight = ({
var,
mark {
font-size: 1em;
font-size: inherit;
}
var {
Expand Down
93 changes: 93 additions & 0 deletions packages/gatsby-theme-newrelic/src/components/RawCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import PropTypes from 'prop-types';
import { css } from '@emotion/core';
import { Link } from '@newrelic/gatsby-theme-newrelic';
import parse, { domToReact } from 'html-react-parser';

const RawCode = ({ code, language }) => {
return (
<pre
css={css`
color: var(--color-nord-6);
font-family: var(--code-font);
font-size: 0.75rem;
display: block;
overflow: auto;
white-space: pre;
word-spacing: normal;
word-break: normal;
tab-size: 2;
hyphens: none;
text-shadow: none;
padding: 1rem;
.light-mode & {
color: var(--color-nord-0);
}
`}
data-language={language}
>
<code
css={css`
display: table;
width: 100%;
padding: 0;
background: none;
var,
mark {
font-size: inherit;
}
var {
background: var(--color-nord-2);
color: inherit;
.light-mode & {
background: var(--color-nord-4);
}
}
a:hover var {
background: var(--color-nord-3);
.light-mode & {
background: var(--color-nord-5);
}
}
mark {
color: var(--color-neutrals-900) !important;
var {
color: var(--color-neutrals-100);
.light-mode & {
color: var(--color-neutrals-900);
}
}
}
`}
>
{parse(code, {
replace: ({ name, attribs, children }) => {
if (name === 'a') {
return <Link to={attribs.href}>{domToReact(children)}</Link>;
}

if (name && name !== 'var' && name !== 'mark') {
return domToReact(children);
}
},
})}
</code>
</pre>
);
};

RawCode.propTypes = {
code: PropTypes.string.isRequired,
language: PropTypes.string,
};

export default RawCode;
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React from 'react';
import CodeBlock from '../CodeBlock';
import { render, fireEvent, screen } from '@testing-library/react';
import { renderWithTranslation } from '../../test-utils/renderHelpers';

jest.mock('gatsby', () => ({
__esModule: true,
graphql: () => {},
Link: ({ to, ...props }) => <a href={to} {...props} />,
useStaticQuery: () => ({
allLocale: {
nodes: [
{
name: 'English',
locale: 'en',
localizedPath: '/en',
isDefault: true,
},
],
},
site: {
siteMetadata: {
siteUrl: 'https://github.com/foo/bar',
repository: 'https://foobar.net',
},
},
}),
}));

test('renders embedded var tag', () => {
const { container } = renderWithTranslation(
<CodeBlock language="graphql">{`
query MyQuery(<var>$accountId</var>: ID!) {
account(accountId: <var>$accountId</var>) {
name
}
}
`}</CodeBlock>
);

const vars = container.querySelectorAll('var');

expect(vars.length).toEqual(2);
expect(vars[0].textContent).toEqual('$accountId');
expect(vars[1].textContent).toEqual('$accountId');
});

test('renders embedded mark tags', () => {
const { container } = renderWithTranslation(
<CodeBlock language="graphql">{`
query <mark>MyQuery</mark>($accountId: ID!) {
<mark>account(accountId: $accountId) {
name
}</mark>
}
`}</CodeBlock>
);

const marks = container.querySelectorAll('mark');

expect(marks.length).toEqual(2);
expect(marks[0].textContent).toEqual('MyQuery');
expect(marks[1].textContent).toEqual(`account(accountId: $accountId) {
name
}`);
});

test('renders embedded anchor tags', () => {
const { container } = renderWithTranslation(
<CodeBlock language="graphql">{`
query MyQuery($accountId: ID!) {
<a href="/docs/nerd-graph">account</a>(accountId: $accountId) {
name
}
}
`}</CodeBlock>
);

const anchors = container.querySelectorAll('a');

expect(anchors.length).toEqual(1);
expect(anchors[0].textContent).toEqual('account');
});

test('handles combinations of tags', () => {
const { container } = renderWithTranslation(
<CodeBlock language="graphql">{`
query MyQuery($accountId: ID!) {
<a href="/docs/nerd-graph"><var>account</var></a>(accountId: $accountId) {
name
}
}
`}</CodeBlock>
);

const anchors = container.querySelectorAll('a');
const vars = container.querySelectorAll('var');

expect(anchors.length).toEqual(1);
expect(vars.length).toEqual(1);
});

test('leaves text as-is if other HTML tags are used', () => {
const { container, debug } = renderWithTranslation(
<CodeBlock language="graphql">{`
query <span>MyQuery</span>($accountId: ID!) {
account(<strong>accountId</strong>: $accountId) {
name
}
}
`}</CodeBlock>
);

const code = container.querySelector('code');

expect(code.textContent).toEqual(
`query <span>MyQuery</span>($accountId: ID!) { account(<strong>accountId</strong>: $accountId) { name }}`
);
});

test('leaves var/mark/a tags as raw text when language is html', () => {
const { container } = renderWithTranslation(
<CodeBlock language="html">{`
<!DOCTYPE html>
<html>
<body>
<div>
<var>$accountId</var>
<mark>Highlight it up!</mark>
<a href="/docs/nrql">A link</a>
</div>
</body>
</html>
`}</CodeBlock>
);

const vars = container.querySelectorAll('var');

expect(container.querySelectorAll('var').length).toEqual(0);
expect(container.querySelectorAll('mark').length).toEqual(0);
expect(container.querySelectorAll('a').length).toEqual(0);
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import { themeNamespace } from '../utils/defaultOptions';
import { I18nextProvider } from 'react-i18next';
import LocaleProvider from '../components/LocaleProvider';
import translations from '../i18n/translations/en.json';
import i18n from 'i18next';

Expand All @@ -26,7 +27,9 @@ export const renderWithTranslation = (component, options) => {
});

return render(
<I18nextProvider i18n={i18n}>{component}</I18nextProvider>,
<I18nextProvider i18n={i18n}>
<LocaleProvider i18n={i18n}>{component}</LocaleProvider>
</I18nextProvider>,
options
);
};

0 comments on commit a916fbb

Please sign in to comment.