1 import React from 'react'
2 import {Box, Heading, themeGet, Octicon} from '@primer/react'
3 import styled from 'styled-components'
4 import {variant as styledVariant} from 'styled-system'
5 import {LinkIcon} from '@primer/octicons-react'
6 import textContent from 'react-addons-text-content'
7 import {SCROLL_MARGIN_TOP} from '../constants'
8 import usePage from '../hooks/use-page'
9 import SiteLink, {LinkNoUnderline} from '../components/link'
10 import Code from './code'
11
12 export {Code}
13 export {default as Index} from './nav-hierarchy'
14
15 const required = (prop, name) => {
16 if (!prop) {
17 throw new Error(`${name} prop is required`)
18 }
19 return prop
20 }
21
22 export const Link = props => <SiteLink underline {...props} />
23
24 const StyledHeading = styled(Heading)`
25 margin-top: ${themeGet('space.4')};
26 margin-bottom: ${themeGet('space.3')};
27 scroll-margin-top: ${SCROLL_MARGIN_TOP}px;
28 line-height: ${themeGet('lineHeights.condensed')};
29
30 @media (hover: hover) {
31 & .octicon-link {
32 visibility: hidden;
33 }
34
35 &:hover .octicon-link,
36 &:focus-within .octicon-link {
37 visibility: visible;
38 }
39 }
40 `
41
42 const HeaderLink = ({autolink, children, ...props}) =>
43 autolink ? (
44 <LinkNoUnderline {...props} sx={{color: 'inherit'}}>
45 {children}
46 <Octicon
47 icon={LinkIcon}
48 className="octicon-link"
49 sx={{
50 ml: 2,
51 color: 'fg.muted',
52 // !important is needed here to override default icon styles
53 verticalAlign: 'middle !important',
54 }}
55 />
56 </LinkNoUnderline>
57 ) : (
58 children
59 )
60
61 const Headings = {
62 Markdown: ({children, autolink = true, ...props}) => {
63 const childArray = React.Children.toArray(children)
64 const childLink =
65 React.Children.count(children) > 1 && childArray[0].type?.name === 'Link' ? childArray.shift() : null
66
67 const {slugger} = usePage()
68 const text = children ? textContent(children) : ''
69 const id = text ? slugger.slug(text) : ''
70 const linkProps = {
71 autolink,
72 'aria-label': `${text} permalink`.trim(),
73 ...(id ? {href: `#${id}`} : {}),
74 }
75
76 return (
77 <StyledHeading {...(autolink && id ? {id} : {})} {...props}>
78 {childLink ? (
79 <React.Fragment>
80 {childLink}
81 <HeaderLink {...linkProps}>
82 {childArray.map((child, index) => (
83 <React.Fragment key={index}>{child}</React.Fragment>
84 ))}
85 </HeaderLink>
86 </React.Fragment>
87 ) : (
88 <HeaderLink {...linkProps}>{children}</HeaderLink>
89 )}
90 </StyledHeading>
91 )
92 },
93 h1: styled(StyledHeading).attrs({as: 'h1'})`
94 padding-bottom: ${themeGet('space.2')};
95 font-size: ${themeGet('fontSizes.6')};
96 border-bottom: 1px solid ${themeGet('colors.border.default')};
97 margin-top: 0;
98 `,
99 h2: styled(StyledHeading).attrs({as: 'h2'})`
100 padding-bottom: ${themeGet('space.2')};
101 font-size: ${themeGet('fontSizes.4')};
102 border-bottom: 1px solid ${themeGet('colors.border.default')};
103 font-weight: ${themeGet('fontWeights.semibold')};
104 `,
105 h3: styled(StyledHeading).attrs({as: 'h3'})`
106 font-size: ${themeGet('fontSizes.3')};
107 font-weight: ${themeGet('fontWeights.semibold')};
108 `,
109 h4: styled(StyledHeading).attrs({as: 'h4'})`
110 font-size: ${themeGet('fontSizes.2')};
111 font-weight: ${themeGet('fontWeights.semibold')};
112 `,
113 h5: styled(StyledHeading).attrs({as: 'h5'})`
114 font-size: ${themeGet('fontSizes.1')};
115 `,
116 h6: styled(StyledHeading).attrs({as: 'h6'})`
117 font-size: ${themeGet('fontSizes.1')};
118 color: ${themeGet('colors.fg.muted')};
119 `,
120 wrap(as) {
121 return props => <Headings.Markdown as={Headings[as]} {...props} />
122 },
123 }
124
125 export const H1 = Headings.wrap('h1')
126 export const H2 = Headings.wrap('h2')
127 export const H3 = Headings.wrap('h3')
128 export const H4 = Headings.wrap('h4')
129 export const H5 = Headings.wrap('h5')
130 export const H6 = Headings.wrap('h6')
131
132 export const Blockquote = styled.blockquote`
133 margin: 0 0 ${themeGet('space.3')};
134 padding: 0 ${themeGet('space.3')};
135 color: ${themeGet('colors.fg.muted')};
136 border-left: 0.25em solid ${themeGet('colors.border.default')};
137
138 > :first-child {
139 margin-top: 0;
140 }
141
142 > :last-child {
143 margin-bottom: 0;
144 }
145 `
146
147 export const DescriptionList = styled.dl`
148 padding: 0;
149
150 dt {
151 padding: 0;
152 margin-top: ${themeGet('space.3')};
153 font-size: 1em;
154 font-style: italic;
155 font-weight: ${themeGet('fontWeights.bold')};
156 }
157
158 dd {
159 padding: 0 ${themeGet('space.3')};
160 margin: 0 0 ${themeGet('space.3')};
161 }
162 `
163
164 export const HorizontalRule = styled.hr`
165 height: ${themeGet('borderWidths.1')};
166 padding: 0;
167 margin: ${themeGet('space.4')} 0;
168 background-color: ${themeGet('colors.border.default')};
169 border: 0;
170 `
171
172 export const UnorderedList = styled.ul`
173 padding-left: 2em;
174
175 ul,
176 ol {
177 margin-top: 0;
178 margin-bottom: 0;
179 }
180
181 li {
182 word-break: break-all;
183 }
184
185 li > p {
186 margin-top: ${themeGet('space.3')};
187 }
188
189 li + li {
190 margin-top: ${themeGet('space.1')};
191 }
192 `
193
194 export const OrderedList = UnorderedList.withComponent('ol')
195
196 export const Paragraph = styled.p`
197 margin: 0 0 ${themeGet('space.3')};
198 `
199
200 export const Table = styled.table`
201 display: block;
202 width: 100%;
203 margin: 0 0 ${themeGet('space.3')};
204 overflow: auto;
205 border-collapse: separate;
206 border-spacing: 0px;
207
208 th {
209 font-weight: ${themeGet('fontWeights.bold')};
210 background-color: ${themeGet('colors.neutral.subtle')};
211 }
212
213 th,
214 td {
215 padding: ${themeGet('space.2')} ${themeGet('space.3')};
216 border-color: ${themeGet('colors.border.muted')};
217 border-style: solid;
218 border-width: 0;
219 border-left-width: ${themeGet('borderWidths.1')};
220 border-top-width: ${themeGet('borderWidths.1')};
221 }
222
223 tr:last-child td {
224 border-bottom-width: ${themeGet('borderWidths.1')};
225 }
226
227 tr td:last-child,
228 tr th:last-child {
229 border-right-width: ${themeGet('borderWidths.1')};
230 }
231
232 thead th:first-child {
233 border-top-left-radius: ${themeGet('radii.2')};
234 }
235
236 thead th:last-child {
237 border-top-right-radius: ${themeGet('radii.2')};
238 }
239
240 tbody tr:last-child td:last-child {
241 border-bottom-right-radius: ${themeGet('radii.2')};
242 }
243
244 tbody tr:last-child td:first-child {
245 border-bottom-left-radius: ${themeGet('radii.2')};
246 }
247
248 img {
249 background-color: transparent;
250 vertical-align: middle;
251 }
252 `
253
254 const StyledNote = styled.div`
255 padding: ${themeGet('space.3')};
256 margin-bottom: ${themeGet('space.3')};
257 border-radius: ${themeGet('radii.2')};
258 border-left: ${themeGet('radii.2')} solid;
259
260 & *:last-child {
261 margin-bottom: 0;
262 }
263
264 ${styledVariant({
265 variants: {
266 info: {
267 borderColor: 'accent.muted',
268 bg: 'accent.subtle',
269 },
270 warning: {
271 borderColor: 'attention.muted',
272 bg: 'attention.subtle',
273 },
274 danger: {
275 borderColor: 'danger.muted',
276 bg: 'danger.subtle',
277 },
278 },
279 })}
280 `
281
282 export const Note = ({variant = 'info', ...props}) => <StyledNote variant={variant} {...props} />
283
284 export const Prompt = props => <Code prompt={true} {...props} />
285
286 const RequiredImage = ({src, alt, ...props}) => <img src={required(src, 'src')} alt={required(alt, 'alt')} {...props} />
287
288 export const Image = styled(RequiredImage)`
289 max-width: 100%;
290 box-sizing: content-box;
291 `
292
293 export const Screenshot = styled(RequiredImage)`
294 margin-top: ${themeGet('space.3')};
295 margin-bottom: ${themeGet('space.3')};
296 max-width: min(100%, 525px);
297 max-height: 300px;
298 border: 1px solid ${themeGet('colors.border.default')};
299 display: block;
300 `
301
302 export const YouTube = ({id}) => (
303 <Box
304 as="iframe"
305 sx={{aspectRatio: '16 / 9', width: '100%'}}
306 title="YouTube video"
307 src={`https://www.youtube.com/embed/${id}`}
308 frameBorder="0"
309 allowFullScreen
310 />
311 )