1import React from 'react';
2import styled, { css } from 'styled-components';
3import { MDXProvider } from '@mdx-js/react';
4import { Link } from 'react-router-dom';
5import Highlight, { Prism } from 'prism-react-renderer';
6import nightOwlLight from 'prism-react-renderer/themes/nightOwlLight';
7
8import AnchorSvg from '../assets/anchor';
9
10const getLanguage = className => {
11 const res = className.match(/language-(\w+)/);
12 return res ? res[1] : null;
13};
14
15const Pre = styled.pre`
16 background: ${p => p.theme.colors.codeBg};
17 border: 1px solid ${p => p.theme.colors.border};
18 border-radius: ${p => p.theme.spacing.xs};
19
20 font-size: ${p => p.theme.fontSizes.code};
21 line-height: ${p => p.theme.lineHeights.code};
22
23 max-width: 100%;
24 overflow-x: auto;
25 -webkit-overflow-scrolling: touch;
26 padding: ${p => p.theme.spacing.sm};
27 position: relative;
28 white-space: pre;
29`;
30
31const Code = styled.code`
32 display: block;
33 font-family: ${p => p.theme.fonts.code};
34 color: ${p => p.theme.colors.code};
35 font-variant-ligatures: none;
36 font-feature-settings: normal;
37 white-space: pre;
38 hyphens: initial;
39`;
40
41const InlineCode = styled(props => {
42 const children = props.children.replace(/\\\|/g, '|');
43 return <code {...props}>{children}</code>;
44})`
45 background: ${p => p.theme.colors.codeBg};
46 color: ${p => p.theme.colors.code};
47 font-family: ${p => p.theme.fonts.code};
48 font-size: ${p => p.theme.fontSizes.small};
49 border-radius: ${p => p.theme.spacing.xs};
50
51 display: inline-block;
52 vertical-align: baseline;
53 font-variant-ligatures: none;
54 font-feature-settings: normal;
55 padding: 0 0.2em;
56 margin: 0;
57
58 a > & {
59 text-decoration: underline;
60 }
61`;
62
63const InlineImage = styled.img`
64 display: inline-block;
65 margin: 0 ${p => p.theme.spacing.sm} ${p => p.theme.spacing.md} 0;
66 padding: ${p => p.theme.spacing.xs} ${p => p.theme.spacing.sm};
67 border: 1px solid ${p => p.theme.colors.border};
68 border-radius: ${p => p.theme.spacing.xs};
69`;
70
71const ImageWrapper = styled.div`
72 margin: ${p => p.theme.spacing.md} 0;
73 border: 1px solid ${p => p.theme.colors.border};
74 border-radius: ${p => p.theme.spacing.xs};
75 background: ${p => p.theme.colors.bg};
76
77 display: flex;
78 flex-direction: column;
79
80 & > img {
81 padding: ${p => p.theme.spacing.md};
82 align-self: center;
83 max-height: 40vh;
84 }
85`;
86
87const ImageAlt = styled.span.attrs(() => ({
88 'aria-hidden': true, // This is just duplicating alt
89}))`
90 display: block;
91 padding: ${p => p.theme.spacing.xs} ${p => p.theme.spacing.sm};
92 border-top: 1px solid ${p => p.theme.colors.border};
93 background: ${p => p.theme.colors.codeBg};
94 font-size: ${p => p.theme.fontSizes.small};
95`;
96
97const Image = props => {
98 const { height, width, alt, src } = props;
99 if (height || width) return <InlineImage {...props} />;
100
101 return (
102 <ImageWrapper>
103 <img alt={alt} src={src} />
104 <ImageAlt>{alt}</ImageAlt>
105 </ImageWrapper>
106 );
107};
108
109const HighlightCode = ({ className = '', children }) => {
110 const language = getLanguage(className);
111
112 return (
113 <Highlight
114 Prism={Prism}
115 theme={nightOwlLight}
116 code={children.trim()}
117 language={language}
118 >
119 {({ className, style, tokens, getLineProps, getTokenProps }) => (
120 <Code
121 style={{ ...style, backgroundColor: 'none' }}
122 className={className}
123 >
124 {tokens.map((line, i) => (
125 <div {...getLineProps({ line, key: i })}>
126 {line.map((token, key) => (
127 <span {...getTokenProps({ token, key })} />
128 ))}
129 </div>
130 ))}
131 </Code>
132 )}
133 </Highlight>
134 );
135};
136
137const Blockquote = styled.blockquote`
138 margin: ${p => p.theme.spacing.md} 0;
139 padding: 0 0 0 ${p => p.theme.spacing.md};
140 border-left: 0.5rem solid ${p => p.theme.colors.border};
141 font-size: ${p => p.theme.fontSizes.small};
142
143 & > * {
144 margin: ${p => p.theme.spacing.sm} 0;
145 }
146`;
147
148const sharedTableCellStyling = css`
149 padding: ${p => p.theme.spacing.xs} ${p => p.theme.spacing.sm};
150 border-left: 1px solid ${p => p.theme.colors.passiveBg};
151 border-bottom: 1px solid ${p => p.theme.colors.passiveBg};
152
153 & > ${InlineCode} {
154 white-space: pre-wrap;
155 display: inline;
156 }
157`;
158
159const TableHeader = styled.th`
160 text-align: left;
161 white-space: nowrap;
162 ${sharedTableCellStyling}
163`;
164
165const TableCell = styled.td`
166 ${sharedTableCellStyling}
167
168 ${p => {
169 const isCodeOnly = React.Children.toArray(p.children).every(
170 x => x.props && x.props.mdxType === 'inlineCode'
171 );
172 return (
173 isCodeOnly &&
174 css`
175 background-color: ${p.theme.colors.codeBg};
176
177 && > ${InlineCode} {
178 background: none;
179 padding: 0;
180 margin: 0;
181 white-space: pre;
182 display: block;
183 }
184 `
185 );
186 }}
187
188 &:first-child {
189 width: min-content;
190 min-width: 25rem;
191 }
192
193 @media ${p => p.theme.media.md} {
194 &:not(:first-child) {
195 overflow-wrap: break-word;
196 }
197 }
198`;
199
200const TableScrollContainer = styled.div`
201 overflow-x: auto;
202
203 @media ${p => p.theme.media.maxmd} {
204 overflow-x: scroll;
205 -webkit-overflow-scrolling: touch;
206 }
207`;
208
209const Table = styled.table`
210 border: 1px solid ${p => p.theme.colors.passiveBg};
211 border-collapse: collapse;
212 overflow-x: auto;
213
214 @media ${p => p.theme.media.maxmd} {
215 overflow-x: scroll;
216 overflow-wrap: initial;
217 word-wrap: initial;
218 word-break: initial;
219 hyphens: initial;
220 }
221`;
222
223const TableScroll = props => (
224 <TableScrollContainer>
225 <Table {...props} />
226 </TableScrollContainer>
227);
228
229const MdLink = ({ href, children }) => {
230 if (!/^\w+:/.test(href) && !href.startsWith('#')) {
231 return <Link to={href}>{children}</Link>;
232 }
233
234 return (
235 <a rel="external" href={href}>
236 {children}
237 </a>
238 );
239};
240
241const HeadingText = styled.h1`
242 &:target:before {
243 content: '';
244 display: block;
245 height: 1.5em;
246 margin: -1.5em 0 0;
247 }
248`;
249
250const AnchorLink = styled.a`
251 display: inline-block;
252 color: ${p => p.theme.colors.accent};
253 padding-right: 0.5rem;
254 width: 2rem;
255
256 @media ${({ theme }) => theme.media.sm} {
257 margin-left: -2rem;
258 display: none;
259
260 ${HeadingText}:hover > & {
261 display: inline-block;
262 }
263 }
264`;
265
266const AnchorIcon = styled(AnchorSvg)`
267 height: 100%;
268`;
269
270const Header = tag => {
271 const HeaderComponent = ({ id, children }) => (
272 <HeadingText as={tag} id={id}>
273 <AnchorLink href={`#${id}`}>
274 <AnchorIcon />
275 </AnchorLink>
276 {children}
277 </HeadingText>
278 );
279
280 HeaderComponent.displayName = `Header(${tag})`;
281 return HeaderComponent;
282};
283
284const components = {
285 pre: Pre,
286 img: Image,
287 blockquote: Blockquote,
288 inlineCode: InlineCode,
289 code: HighlightCode,
290 table: TableScroll,
291 th: TableHeader,
292 td: TableCell,
293 a: MdLink,
294 h1: HeadingText,
295 h2: Header('h2'),
296 h3: Header('h3'),
297};
298
299export const MDXComponents = ({ children }) => (
300 <MDXProvider components={components}>{children}</MDXProvider>
301);