@apify/ui-library 0.71.1-featcolortokens-178953.58 → 0.71.1-featcolortokens-178953.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/design_system/colors/generated/{dark.d.ts → css_variables.dark.d.ts} +1 -1
- package/dist/src/design_system/colors/generated/css_variables.dark.d.ts.map +1 -0
- package/dist/src/design_system/colors/generated/css_variables.dark.js +147 -0
- package/dist/src/design_system/colors/generated/css_variables.dark.js.map +1 -0
- package/dist/src/design_system/colors/generated/{light.d.ts → css_variables.light.d.ts} +1 -1
- package/dist/src/design_system/colors/generated/css_variables.light.d.ts.map +1 -0
- package/dist/src/design_system/colors/generated/css_variables.light.js +147 -0
- package/dist/src/design_system/colors/generated/css_variables.light.js.map +1 -0
- package/dist/src/design_system/colors/generated/{palette.dark.d.ts → css_variables_palette.dark.d.ts} +1 -1
- package/dist/src/design_system/colors/generated/css_variables_palette.dark.d.ts.map +1 -0
- package/dist/src/design_system/colors/generated/css_variables_palette.dark.js +74 -0
- package/dist/src/design_system/colors/generated/css_variables_palette.dark.js.map +1 -0
- package/dist/src/design_system/colors/generated/{palette.light.d.ts → css_variables_palette.light.d.ts} +1 -1
- package/dist/src/design_system/colors/generated/css_variables_palette.light.d.ts.map +1 -0
- package/dist/src/design_system/colors/generated/css_variables_palette.light.js +74 -0
- package/dist/src/design_system/colors/generated/css_variables_palette.light.js.map +1 -0
- package/dist/src/design_system/colors/index.d.ts +4 -4
- package/dist/src/design_system/colors/index.d.ts.map +1 -1
- package/dist/src/design_system/colors/index.js +4 -4
- package/dist/src/design_system/colors/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/codemods/generate_typograpy_tokens_files.mjs +137 -0
- package/src/components/action_link.tsx +60 -0
- package/src/components/actor_template_card.tsx +116 -0
- package/src/components/badge.tsx +148 -0
- package/src/components/banner.tsx +94 -0
- package/src/components/blog_article.tsx +85 -0
- package/src/components/box.tsx +127 -0
- package/src/components/button.tsx +305 -0
- package/src/components/chip.tsx +128 -0
- package/src/components/code/action_button.tsx +96 -0
- package/src/components/code/code_block/code_block.styled.tsx +180 -0
- package/src/components/code/code_block/code_block.tsx +224 -0
- package/src/components/code/code_block/code_block_with_tabs.tsx +257 -0
- package/src/components/code/code_block/utils.tsx +67 -0
- package/src/components/code/index.ts +5 -0
- package/src/components/code/inline_code/inline_code.tsx +62 -0
- package/src/components/code/one_line_code/one_line_code.tsx +228 -0
- package/src/components/code/prism_highlighter.tsx +180 -0
- package/src/components/color_wheel_gradient.tsx +31 -0
- package/src/components/floating/index.ts +3 -0
- package/src/components/floating/menu.tsx +189 -0
- package/src/components/floating/menu_common.tsx +31 -0
- package/src/components/floating/menu_components.tsx +99 -0
- package/src/components/image.tsx +24 -0
- package/src/components/index.ts +22 -0
- package/src/components/link.tsx +114 -0
- package/src/components/message.tsx +153 -0
- package/src/components/rating.tsx +106 -0
- package/src/components/readme_renderer/index.ts +3 -0
- package/src/components/readme_renderer/pythonize_value.ts +76 -0
- package/src/components/readme_renderer/table_of_contents.tsx +272 -0
- package/src/components/readme_renderer/utils.tsx +46 -0
- package/src/components/simple_markdown/index.ts +2 -0
- package/src/components/simple_markdown/simple_markdown.tsx +214 -0
- package/src/components/simple_markdown/simple_markdown_components.tsx +293 -0
- package/src/components/tabs/index.ts +2 -0
- package/src/components/tabs/tab.tsx +217 -0
- package/src/components/tabs/tabs.tsx +169 -0
- package/src/components/tag.tsx +196 -0
- package/src/components/text/heading_content.tsx +56 -0
- package/src/components/text/heading_marketing.tsx +55 -0
- package/src/components/text/heading_shared.tsx +55 -0
- package/src/components/text/index.ts +19 -0
- package/src/components/text/text_base.tsx +52 -0
- package/src/components/text/text_content.tsx +104 -0
- package/src/components/text/text_marketing.tsx +152 -0
- package/src/components/text/text_shared.tsx +95 -0
- package/src/components/tile/horizontal_tile.tsx +77 -0
- package/src/components/tile/index.ts +2 -0
- package/src/components/tile/shared.ts +27 -0
- package/src/components/tile/vertical_tile.tsx +59 -0
- package/src/components/to_consolidate/card.tsx +141 -0
- package/src/components/to_consolidate/index.ts +4 -0
- package/src/components/to_consolidate/markdown.tsx +609 -0
- package/src/components/to_consolidate/pagination.tsx +136 -0
- package/src/components/to_consolidate/tab_number_chip.tsx +31 -0
- package/src/design_system/colors/build_color_tokens.js +183 -0
- package/src/design_system/colors/figma_color_tokens.dark.json +886 -0
- package/src/design_system/colors/figma_color_tokens.light.json +886 -0
- package/src/design_system/colors/generated/colors_theme.dark.ts +110 -0
- package/src/design_system/colors/generated/colors_theme.light.ts +110 -0
- package/{dist/src/design_system/colors/generated/dark.js → src/design_system/colors/generated/css_variables.dark.ts} +1 -1
- package/{dist/src/design_system/colors/generated/light.js → src/design_system/colors/generated/css_variables.light.ts} +1 -1
- package/{dist/src/design_system/colors/generated/palette.dark.js → src/design_system/colors/generated/css_variables_palette.dark.ts} +1 -1
- package/{dist/src/design_system/colors/generated/palette.light.js → src/design_system/colors/generated/css_variables_palette.light.ts} +1 -1
- package/{dist/src/design_system/properties_theme.js → src/design_system/colors/generated/properties_theme.ts} +20 -156
- package/src/design_system/colors/index.ts +7 -0
- package/src/design_system/supernova_typography_tokens.json +657 -0
- package/src/design_system/theme.ts +25 -0
- package/src/design_system/tokens/index.ts +5 -0
- package/src/design_system/tokens/layouts.ts +29 -0
- package/src/design_system/tokens/radiuses.ts +22 -0
- package/src/design_system/tokens/shadows.ts +22 -0
- package/src/design_system/tokens/spaces.ts +15 -0
- package/src/design_system/tokens/transitions.ts +19 -0
- package/src/design_system/typography_theme.ts +197 -0
- package/src/index.ts +8 -0
- package/src/type_utils.ts +7 -0
- package/src/ui_dependency_provider.tsx +58 -0
- package/src/utils/copy_to_clipboard.ts +24 -0
- package/src/utils/image_color.ts +42 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/resize_observer.ts +18 -0
- package/src/utils/sanitization.ts +14 -0
- package/dist/src/design_system/colors/generated/dark.d.ts.map +0 -1
- package/dist/src/design_system/colors/generated/dark.js.map +0 -1
- package/dist/src/design_system/colors/generated/light.d.ts.map +0 -1
- package/dist/src/design_system/colors/generated/light.js.map +0 -1
- package/dist/src/design_system/colors/generated/palette.dark.d.ts.map +0 -1
- package/dist/src/design_system/colors/generated/palette.dark.js.map +0 -1
- package/dist/src/design_system/colors/generated/palette.light.d.ts.map +0 -1
- package/dist/src/design_system/colors/generated/palette.light.js.map +0 -1
- package/dist/src/design_system/colors_theme.d.ts +0 -213
- package/dist/src/design_system/colors_theme.d.ts.map +0 -1
- package/dist/src/design_system/colors_theme.js +0 -213
- package/dist/src/design_system/colors_theme.js.map +0 -1
- package/dist/src/design_system/properties_theme.d.ts +0 -175
- package/dist/src/design_system/properties_theme.d.ts.map +0 -1
- package/dist/src/design_system/properties_theme.js.map +0 -1
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import qs from 'query-string';
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
|
+
import type { Components } from 'react-markdown';
|
|
6
|
+
import ReactMarkdown, { uriTransformer } from 'react-markdown';
|
|
7
|
+
import type { AllowElement, Node } from 'react-markdown/lib/rehype-filter';
|
|
8
|
+
import rehypeRaw from 'rehype-raw';
|
|
9
|
+
import remarkGfm from 'remark-gfm';
|
|
10
|
+
import styled from 'styled-components';
|
|
11
|
+
|
|
12
|
+
import { LinkIcon } from '@apify/ui-icons';
|
|
13
|
+
|
|
14
|
+
import type { UiThemeOption } from '../../design_system/theme.js';
|
|
15
|
+
import { theme } from '../../design_system/theme.js';
|
|
16
|
+
import { useCopyToClipboard } from '../../utils/index.js';
|
|
17
|
+
import { CodeBlock, inlineCodeStyles, OneLineCode } from '../code/index.js';
|
|
18
|
+
import { cleanMarkdown, slugifyHeadingChildren } from '../readme_renderer/utils.js';
|
|
19
|
+
|
|
20
|
+
interface StyledReadmeProps {
|
|
21
|
+
$scrollMarginTopPx: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const markdownClassNames = {
|
|
25
|
+
COPY_BUTTON: 'Markdown-CopyButton',
|
|
26
|
+
CODE_PRE: 'Markdown-CodePre',
|
|
27
|
+
VIDEO_WRAPPER: 'Markdown-VideoWrapper',
|
|
28
|
+
IFRAME_CONTAINER: 'Iframe-container',
|
|
29
|
+
HEADING_COPY_ICON: 'ReadmeRenderer-headingCopyIcon',
|
|
30
|
+
INLINE_CODE: 'Markdown-InlineCode',
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
const StyledMarkdown = styled(ReactMarkdown) <StyledReadmeProps>`
|
|
34
|
+
@font-face {
|
|
35
|
+
font-family: "ellipsis-font";
|
|
36
|
+
src: local("Courier");
|
|
37
|
+
unicode-range: U+2026;
|
|
38
|
+
size-adjust: 0%;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&>:first-child {
|
|
42
|
+
margin-top: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
h1, h2, h3, h4, h5 {
|
|
46
|
+
scroll-margin-top: ${({ $scrollMarginTopPx }) => `${$scrollMarginTopPx}px`};
|
|
47
|
+
margin-top: ${theme.space.space32};
|
|
48
|
+
margin-bottom: ${theme.space.space16};
|
|
49
|
+
align-items: center;
|
|
50
|
+
|
|
51
|
+
& > .${markdownClassNames.INLINE_CODE} {
|
|
52
|
+
font-size: inherit !important;
|
|
53
|
+
line-height: inherit !important;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Do no change the font style if the heading is defined as bold in the markdown and is wrapped in a <strong> tag */
|
|
57
|
+
strong {
|
|
58
|
+
font-size: inherit !important;
|
|
59
|
+
line-height: inherit !important;
|
|
60
|
+
font-weight: inherit !important;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
a {
|
|
64
|
+
text-decoration: none !important;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.${markdownClassNames.HEADING_COPY_ICON} {
|
|
68
|
+
display: none;
|
|
69
|
+
transform: translateX(${theme.space.space4});
|
|
70
|
+
color: ${theme.color.neutral.textSubtle};
|
|
71
|
+
|
|
72
|
+
svg {
|
|
73
|
+
stroke: ${theme.color.primary.text};
|
|
74
|
+
max-height: 1em !important;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&:hover {
|
|
79
|
+
.${markdownClassNames.HEADING_COPY_ICON} {
|
|
80
|
+
display: inline-block;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
p, li, strong, b, table {
|
|
86
|
+
${theme.typography.content.mobile.paragraph}
|
|
87
|
+
@media (min-width: ${theme.layout.tablet}) {
|
|
88
|
+
${theme.typography.content.tablet.paragraph}
|
|
89
|
+
}
|
|
90
|
+
@media (min-width: ${theme.layout.desktop}) {
|
|
91
|
+
${theme.typography.content.desktop.paragraph}
|
|
92
|
+
}
|
|
93
|
+
a {
|
|
94
|
+
overflow-wrap: break-word;
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
&:hover {
|
|
97
|
+
text-decoration: underline;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
p {
|
|
103
|
+
margin: ${theme.space.space16} 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
h1 {
|
|
107
|
+
${theme.typography.content.mobile.heading1}
|
|
108
|
+
@media (min-width: ${theme.layout.tablet}) {
|
|
109
|
+
${theme.typography.content.tablet.heading1}
|
|
110
|
+
}
|
|
111
|
+
@media (min-width: ${theme.layout.desktop}) {
|
|
112
|
+
${theme.typography.content.desktop.heading1}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
h2 {
|
|
117
|
+
${theme.typography.content.mobile.heading2}
|
|
118
|
+
@media (min-width: ${theme.layout.tablet}) {
|
|
119
|
+
${theme.typography.content.tablet.heading2}
|
|
120
|
+
}
|
|
121
|
+
@media (min-width: ${theme.layout.desktop}) {
|
|
122
|
+
${theme.typography.content.desktop.heading2}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
h3 {
|
|
127
|
+
${theme.typography.content.mobile.heading3}
|
|
128
|
+
@media (min-width: ${theme.layout.tablet}) {
|
|
129
|
+
${theme.typography.content.tablet.heading3}
|
|
130
|
+
}
|
|
131
|
+
@media (min-width: ${theme.layout.desktop}) {
|
|
132
|
+
${theme.typography.content.desktop.heading3}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
h4 {
|
|
137
|
+
${theme.typography.content.mobile.heading4}
|
|
138
|
+
@media (min-width: ${theme.layout.tablet}) {
|
|
139
|
+
${theme.typography.content.tablet.heading4}
|
|
140
|
+
}
|
|
141
|
+
@media (min-width: ${theme.layout.desktop}) {
|
|
142
|
+
${theme.typography.content.desktop.heading4}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
h5 {
|
|
147
|
+
${theme.typography.content.mobile.heading5}
|
|
148
|
+
@media (min-width: ${theme.layout.tablet}) {
|
|
149
|
+
${theme.typography.content.tablet.heading5}
|
|
150
|
+
}
|
|
151
|
+
@media (min-width: ${theme.layout.desktop}) {
|
|
152
|
+
${theme.typography.content.desktop.heading5}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
h6 {
|
|
157
|
+
${theme.typography.content.mobile.heading6}
|
|
158
|
+
@media (min-width: ${theme.layout.tablet}) {
|
|
159
|
+
${theme.typography.content.tablet.heading6}
|
|
160
|
+
}
|
|
161
|
+
@media (min-width: ${theme.layout.desktop}) {
|
|
162
|
+
${theme.typography.content.desktop.heading6}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
img {
|
|
167
|
+
max-width: 100%;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
ul, ol {
|
|
171
|
+
padding-left: ${theme.space.space32};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
li {
|
|
175
|
+
margin-top: ${theme.space.space4};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
hr {
|
|
179
|
+
color: ${theme.color.neutral.border};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
strong {
|
|
183
|
+
${theme.typography.content.mobile.paragraphStrong}
|
|
184
|
+
@media (min-width: ${theme.layout.tablet}) {
|
|
185
|
+
${theme.typography.content.tablet.paragraphStrong}
|
|
186
|
+
}
|
|
187
|
+
@media (min-width: ${theme.layout.desktop}) {
|
|
188
|
+
${theme.typography.content.desktop.paragraphStrong}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
blockquote {
|
|
193
|
+
border-left: 2px solid ${theme.color.neutral.separatorSubtle};
|
|
194
|
+
padding-left: ${theme.space.space16};
|
|
195
|
+
|
|
196
|
+
color: ${theme.color.neutral.textMuted};
|
|
197
|
+
|
|
198
|
+
p {
|
|
199
|
+
margin-bottom: 0;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
table {
|
|
204
|
+
display: block;
|
|
205
|
+
overflow: auto;
|
|
206
|
+
border-collapse: collapse;
|
|
207
|
+
|
|
208
|
+
td, th {
|
|
209
|
+
border: 1px solid ${theme.color.neutral.border};
|
|
210
|
+
padding: ${theme.space.space16};
|
|
211
|
+
text-align: left;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
tr:nth-child(even):not([class]) {
|
|
215
|
+
> th, > td {
|
|
216
|
+
background-color: inherit;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
th {
|
|
222
|
+
${theme.typography.content.mobile.paragraphStrong}
|
|
223
|
+
@media (min-width: ${theme.layout.tablet}) {
|
|
224
|
+
${theme.typography.content.tablet.paragraphStrong}
|
|
225
|
+
}
|
|
226
|
+
@media (min-width: ${theme.layout.desktop}) {
|
|
227
|
+
${theme.typography.content.desktop.paragraphStrong}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
a:hover {
|
|
232
|
+
text-decoration: underline;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.${markdownClassNames.VIDEO_WRAPPER} {
|
|
236
|
+
width: 65%;
|
|
237
|
+
display: block;
|
|
238
|
+
|
|
239
|
+
.${markdownClassNames.IFRAME_CONTAINER} {
|
|
240
|
+
position: relative;
|
|
241
|
+
overflow: hidden;
|
|
242
|
+
width: 100%;
|
|
243
|
+
padding-top: 56.25%;
|
|
244
|
+
display: block;
|
|
245
|
+
|
|
246
|
+
iframe {
|
|
247
|
+
position: absolute;
|
|
248
|
+
top: 0;
|
|
249
|
+
left: 0;
|
|
250
|
+
bottom: 0;
|
|
251
|
+
right: 0;
|
|
252
|
+
width: 100%;
|
|
253
|
+
height: 100%;
|
|
254
|
+
border: 0px;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.${markdownClassNames.INLINE_CODE} {
|
|
260
|
+
${inlineCodeStyles}
|
|
261
|
+
${theme.typography.content.mobile.snippet}
|
|
262
|
+
/* prevents long URLs from overflowing its container */
|
|
263
|
+
hyphens: auto;
|
|
264
|
+
overflow-wrap: break-word;
|
|
265
|
+
|
|
266
|
+
@media (min-width: ${theme.layout.tablet}) {
|
|
267
|
+
${theme.typography.content.tablet.snippet}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@media (min-width: ${theme.layout.desktop}) {
|
|
271
|
+
${theme.typography.content.desktop.snippet}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
b, strong{
|
|
275
|
+
font-size: inherit !important;
|
|
276
|
+
line-height: inherit !important;
|
|
277
|
+
font-family: inherit !important;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
export interface HeadingRendererProps {
|
|
283
|
+
node: {
|
|
284
|
+
tagName: keyof JSX.IntrinsicElements;
|
|
285
|
+
};
|
|
286
|
+
children: React.ReactNode;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Adds ids to headings
|
|
291
|
+
*/
|
|
292
|
+
const DefaultHeadingRenderer = ({ node, children }: HeadingRendererProps) => {
|
|
293
|
+
const Tag = node.tagName;
|
|
294
|
+
const id = slugifyHeadingChildren(children);
|
|
295
|
+
return <Tag id={id}>{children}</Tag>;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
interface CodeRendererComponentProps {
|
|
299
|
+
inline: boolean;
|
|
300
|
+
className: string;
|
|
301
|
+
children: React.ReactNode;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function CodeRenderer({
|
|
305
|
+
inline,
|
|
306
|
+
className,
|
|
307
|
+
children,
|
|
308
|
+
}: CodeRendererComponentProps) {
|
|
309
|
+
if (inline) {
|
|
310
|
+
return <code className={clsx(className, markdownClassNames.INLINE_CODE)}>{children}</code>;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const code = String(children).replace(/\n$/, '').trim();
|
|
314
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
315
|
+
const language = match?.[1]?.toLowerCase();
|
|
316
|
+
const isOneLineCode = code.split('\n').length <= 1;
|
|
317
|
+
|
|
318
|
+
if (isOneLineCode) {
|
|
319
|
+
return (
|
|
320
|
+
<OneLineCode
|
|
321
|
+
language={language}
|
|
322
|
+
fullWidth
|
|
323
|
+
>
|
|
324
|
+
{code}
|
|
325
|
+
</OneLineCode>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<CodeBlock
|
|
331
|
+
content={code}
|
|
332
|
+
language={language}
|
|
333
|
+
hideLineNumbers
|
|
334
|
+
fullWidth
|
|
335
|
+
hideBashHeader
|
|
336
|
+
hideBashPromptPrefixes
|
|
337
|
+
/>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const youtubeRegex = /^(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:youtube(?:-nocookie)?\.com|youtu\.be)\/(?:watch\?v=|embed\/|v\/)?([a-zA-Z0-9\-_]+)(?:\S*)?$/;
|
|
342
|
+
const vimeoRegex = /^((?:https?:\/\/)?(?:player\.)?vimeo\.com(?:\/video)?\/(\d+))$/;
|
|
343
|
+
|
|
344
|
+
const getVideoSrc = (link: string) => {
|
|
345
|
+
const youtubeLink = link.match(youtubeRegex);
|
|
346
|
+
const vimeoLink = link.match(vimeoRegex);
|
|
347
|
+
let src;
|
|
348
|
+
if (youtubeLink) {
|
|
349
|
+
// add rel=0 to disable related videos from other channels at the end of the video
|
|
350
|
+
// add enablejsapi=1 to enable tracking videos via API through Google Analytics
|
|
351
|
+
src = qs.stringifyUrl({ url: `https://www.youtube.com/embed/${youtubeLink[1]}`, query: { rel: 0, enablejsapi: 1 } });
|
|
352
|
+
}
|
|
353
|
+
if (vimeoLink) src = `https://player.vimeo.com/video/${vimeoLink[2]}`;
|
|
354
|
+
|
|
355
|
+
return src;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
interface VideoProps {
|
|
359
|
+
src: string;
|
|
360
|
+
}
|
|
361
|
+
const Video = ({ src }: VideoProps) => {
|
|
362
|
+
return (
|
|
363
|
+
<span className={markdownClassNames.VIDEO_WRAPPER}>
|
|
364
|
+
<span className="Iframe-container">
|
|
365
|
+
<iframe loading="lazy" allowFullScreen src={src} />
|
|
366
|
+
</span>
|
|
367
|
+
</span>
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
export const APIFY_HOSTNAMES = [
|
|
372
|
+
'apify.com',
|
|
373
|
+
'docs.apify.com',
|
|
374
|
+
'help.apify.com',
|
|
375
|
+
'sdk.apify.com',
|
|
376
|
+
'blog.apify.com',
|
|
377
|
+
'kb.apify.com', // old knowledge base domain (redirects to help.apify.com)
|
|
378
|
+
'my.apify.com', // old console.domain (redirects to console.apify.com)
|
|
379
|
+
'console.apify.com',
|
|
380
|
+
'crawlee.dev',
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
interface LinkRendererProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
384
|
+
node: {
|
|
385
|
+
properties: {
|
|
386
|
+
enableEmbeddedVideo: boolean;
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
href: string;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
interface LinkRendererOptions {
|
|
393
|
+
hostname?: string,
|
|
394
|
+
Link?: React.ElementType;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// We want no-follow for external links
|
|
398
|
+
// Also if link is a video from youtube or vimeo, we want to render it as iframe
|
|
399
|
+
// Allowing to pass hostname to check if the link is an Apify link to the same hostname (is needed for SSR on the web)
|
|
400
|
+
const DefaultLinkRenderer = ({ node, href, ...props }: LinkRendererProps, { hostname, Link }: LinkRendererOptions, isUserGeneratedContent?: boolean) => {
|
|
401
|
+
const videoSrc = node.properties.enableEmbeddedVideo && getVideoSrc(href);
|
|
402
|
+
if (videoSrc) return <Video src={videoSrc} />;
|
|
403
|
+
|
|
404
|
+
// check for anchor links, hash link are not parsable by URL constructor
|
|
405
|
+
const isHashLink = (href || '').startsWith('#');
|
|
406
|
+
if (isHashLink) return <a href={href} {...props} />;
|
|
407
|
+
|
|
408
|
+
let urlParsed: URL | undefined;
|
|
409
|
+
try { // TODO: replace with URL.canParse() when we have node 19+ 🥲
|
|
410
|
+
urlParsed = new URL(href);
|
|
411
|
+
} catch {
|
|
412
|
+
// Probably invalid url, go on as it doesn't make sense to track this error
|
|
413
|
+
// This component is used on all Actor pages - readmes.
|
|
414
|
+
// So it is mainly for the user-generated content - there can be many invalid links and we won't do anything about it anyway.
|
|
415
|
+
return <span>{href}</span>;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!hostname && (typeof window !== 'undefined' && !window?.location?.hostname)) return <a href={href} {...props} />;
|
|
419
|
+
|
|
420
|
+
const currentHostname = hostname || (typeof window !== 'undefined' && window?.location?.hostname.toLowerCase());
|
|
421
|
+
const hasDifferentHostname = urlParsed && (urlParsed.hostname.toLowerCase() !== currentHostname);
|
|
422
|
+
const isApifyLink = urlParsed
|
|
423
|
+
&& APIFY_HOSTNAMES.includes(urlParsed.hostname.toLowerCase())
|
|
424
|
+
&& urlParsed.protocol === 'https:'; // we want to disqualify links that have http: protocol. It's a mistake on users' side that we are penalized for.
|
|
425
|
+
|
|
426
|
+
// Same host name, use the provided link for internal navigation
|
|
427
|
+
if (!hasDifferentHostname && Link) {
|
|
428
|
+
return <Link to={urlParsed} {...props} />;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
let linkProps = {};
|
|
432
|
+
|
|
433
|
+
// If false, we want to open the link in the same tab (linkProps won't have any props)
|
|
434
|
+
if (hasDifferentHostname) {
|
|
435
|
+
// If the link is an Apify link to the different hostname, we want to open in a new tab
|
|
436
|
+
if (isApifyLink) {
|
|
437
|
+
linkProps = { target: '_blank', rel: 'noopener' };
|
|
438
|
+
} else {
|
|
439
|
+
// If an external non-Apify link, we want to always open it in a new tab and add rel="noopener nofollow"
|
|
440
|
+
// USG - User Generated Content (new rel attribute for nofollow links)
|
|
441
|
+
// Google says:
|
|
442
|
+
// It’s valid to use nofollow with the new attributes — such as rel=”nofollow ugc”
|
|
443
|
+
// — if you wish to be backwards-compatible with services that don’t support the new attributes.
|
|
444
|
+
linkProps = { target: '_blank', rel: clsx('noopener nofollow', isUserGeneratedContent && 'ugc') };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return <a href={href} {...props} {...linkProps} />;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// node is just to omit from exported props
|
|
452
|
+
interface ParagraphRendererProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
|
453
|
+
node?: {
|
|
454
|
+
children: {
|
|
455
|
+
tagName: keyof JSX.IntrinsicElements;
|
|
456
|
+
type: string;
|
|
457
|
+
value: string;
|
|
458
|
+
properties: {
|
|
459
|
+
enableEmbeddedVideo: boolean;
|
|
460
|
+
};
|
|
461
|
+
}[];
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// If the paragraph is just a link to youtube or vimeo, we want to render it as iframe
|
|
466
|
+
const ParagraphRenderer = ({ node, ...props }: ParagraphRendererProps) => {
|
|
467
|
+
// We enable embedded video only is surrounded by new lines (will be rendered as only child in paragraph)
|
|
468
|
+
node?.children.forEach((child) => {
|
|
469
|
+
// eslint-disable-next-line no-param-reassign
|
|
470
|
+
if (child.tagName === 'a') child.properties.enableEmbeddedVideo = node.children.length === 1; // This means the link is the only child
|
|
471
|
+
});
|
|
472
|
+
const child = node?.children[0];
|
|
473
|
+
const isText = child?.type === 'text';
|
|
474
|
+
|
|
475
|
+
const videoSrc = isText && getVideoSrc(child.value);
|
|
476
|
+
if (videoSrc) return <Video src={videoSrc} />;
|
|
477
|
+
|
|
478
|
+
return <p {...props} />;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const HeadingRendererWithAnchor = ({ node, children }: HeadingRendererProps) => {
|
|
482
|
+
const Tag = node.tagName;
|
|
483
|
+
const id = slugifyHeadingChildren(children);
|
|
484
|
+
|
|
485
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
486
|
+
const [_isCopied, handleClick] = useCopyToClipboard({
|
|
487
|
+
text: id ?? '',
|
|
488
|
+
// We want the whole URL to be copied, not just the ID
|
|
489
|
+
transform: (text) => {
|
|
490
|
+
const url = new URL(window.location.href);
|
|
491
|
+
url.hash = `#${text}`;
|
|
492
|
+
return url.toString();
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<Tag id={id}>
|
|
498
|
+
{children}
|
|
499
|
+
<a
|
|
500
|
+
onClick={handleClick}
|
|
501
|
+
href={`#${id}`}
|
|
502
|
+
className={`${markdownClassNames.HEADING_COPY_ICON}`}
|
|
503
|
+
>
|
|
504
|
+
<LinkIcon size="16" />
|
|
505
|
+
</a>
|
|
506
|
+
</Tag>
|
|
507
|
+
);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
export interface MarkdownProps {
|
|
511
|
+
markdown: string;
|
|
512
|
+
transformLinkUri?: (href: string) => string;
|
|
513
|
+
transformImageUri?: (src: string) => string;
|
|
514
|
+
className?: string;
|
|
515
|
+
theme?: UiThemeOption;
|
|
516
|
+
scrollMarginTopPx?: number; // Offset from top of page to account for sticky header
|
|
517
|
+
// Function where we can define which elements are allowed in the markdown. See // Function where we can define which elements are allowed in the markdown. See https://github.com/remarkjs/react-markdown#props for more info
|
|
518
|
+
allowElement?: AllowElement
|
|
519
|
+
isUserGeneratedContent?: boolean;
|
|
520
|
+
currentPathHostname?: string;
|
|
521
|
+
addHeadingAnchors?: boolean;
|
|
522
|
+
Link?: React.ElementType;
|
|
523
|
+
LinkRenderer?: (props: LinkRendererProps, options: LinkRendererOptions, isUserGeneratedContent?: boolean) => React.ReactElement;
|
|
524
|
+
lazyLoadImages?: boolean;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const Markdown = ({
|
|
528
|
+
markdown,
|
|
529
|
+
transformLinkUri,
|
|
530
|
+
transformImageUri,
|
|
531
|
+
className,
|
|
532
|
+
scrollMarginTopPx = 10,
|
|
533
|
+
allowElement,
|
|
534
|
+
currentPathHostname,
|
|
535
|
+
addHeadingAnchors,
|
|
536
|
+
isUserGeneratedContent,
|
|
537
|
+
Link,
|
|
538
|
+
LinkRenderer,
|
|
539
|
+
lazyLoadImages,
|
|
540
|
+
}: MarkdownProps) => {
|
|
541
|
+
const headingRenderer = addHeadingAnchors ? HeadingRendererWithAnchor : DefaultHeadingRenderer;
|
|
542
|
+
const cleanedMarkdown = useMemo(() => cleanMarkdown(markdown), [markdown]);
|
|
543
|
+
return (
|
|
544
|
+
<StyledMarkdown
|
|
545
|
+
$scrollMarginTopPx={scrollMarginTopPx}
|
|
546
|
+
className={className}
|
|
547
|
+
rehypePlugins={[rehypeRaw]}
|
|
548
|
+
remarkPlugins={[remarkGfm]}
|
|
549
|
+
allowedElements={[
|
|
550
|
+
'a',
|
|
551
|
+
'b',
|
|
552
|
+
'blockquote',
|
|
553
|
+
'br',
|
|
554
|
+
'center',
|
|
555
|
+
'code',
|
|
556
|
+
'del',
|
|
557
|
+
'em',
|
|
558
|
+
'h1',
|
|
559
|
+
'h2',
|
|
560
|
+
'h3',
|
|
561
|
+
'h4',
|
|
562
|
+
'h5',
|
|
563
|
+
'hr',
|
|
564
|
+
'i',
|
|
565
|
+
'img',
|
|
566
|
+
'li',
|
|
567
|
+
'ol',
|
|
568
|
+
'p',
|
|
569
|
+
'pre',
|
|
570
|
+
'span',
|
|
571
|
+
'strong',
|
|
572
|
+
'table',
|
|
573
|
+
'tbody',
|
|
574
|
+
'td',
|
|
575
|
+
'tfoot',
|
|
576
|
+
'th',
|
|
577
|
+
'thead',
|
|
578
|
+
'tr',
|
|
579
|
+
'u',
|
|
580
|
+
'ul',
|
|
581
|
+
]}
|
|
582
|
+
allowElement={allowElement}
|
|
583
|
+
components={{
|
|
584
|
+
h1: headingRenderer,
|
|
585
|
+
h2: headingRenderer,
|
|
586
|
+
h3: headingRenderer,
|
|
587
|
+
h4: headingRenderer,
|
|
588
|
+
h5: headingRenderer,
|
|
589
|
+
a: (linkProps: LinkRendererProps) => (
|
|
590
|
+
LinkRenderer
|
|
591
|
+
? LinkRenderer(linkProps, { hostname: currentPathHostname, Link }, isUserGeneratedContent)
|
|
592
|
+
: DefaultLinkRenderer(linkProps, { hostname: currentPathHostname, Link }, isUserGeneratedContent)
|
|
593
|
+
),
|
|
594
|
+
code: (codeProps: CodeRendererComponentProps) => CodeRenderer(codeProps),
|
|
595
|
+
p: ParagraphRenderer,
|
|
596
|
+
img: ({ node, ...imageProps }: React.ImgHTMLAttributes<HTMLImageElement> & { node?: Node }) => (
|
|
597
|
+
<img {...imageProps} {...(lazyLoadImages ? { loading: 'lazy' } : {})} /> // node is injected by rehype-raw plugin and causing invalid prop
|
|
598
|
+
),
|
|
599
|
+
} as unknown as Components}
|
|
600
|
+
transformLinkUri={(href) => (transformLinkUri ? uriTransformer(transformLinkUri(href)) : uriTransformer(href))}
|
|
601
|
+
transformImageUri={transformImageUri}
|
|
602
|
+
>
|
|
603
|
+
{cleanedMarkdown}
|
|
604
|
+
</StyledMarkdown>
|
|
605
|
+
);
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const MemoizedMarkdown = React.memo(Markdown, (prevProps, nextProps) => _.isEqual(prevProps, nextProps));
|
|
609
|
+
export { MemoizedMarkdown as Markdown };
|