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