@apify/ui-library 0.71.1-featcolortokens-178953.56 → 0.71.1-featcolortokens-178953.58
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/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +7 -5
- package/.stylelintrc +0 -12
- package/CHANGELOG.md +0 -3334
- package/CODEOWNERS +0 -7
- package/eslint.config.mjs +0 -44
- package/src/codemods/generate_typograpy_tokens_files.mjs +0 -137
- package/src/components/action_link.tsx +0 -60
- package/src/components/actor_template_card.tsx +0 -116
- package/src/components/badge.tsx +0 -148
- package/src/components/banner.tsx +0 -94
- package/src/components/blog_article.tsx +0 -85
- package/src/components/box.tsx +0 -127
- package/src/components/button.tsx +0 -305
- package/src/components/chip.tsx +0 -128
- package/src/components/code/action_button.tsx +0 -96
- package/src/components/code/code_block/code_block.styled.tsx +0 -180
- package/src/components/code/code_block/code_block.tsx +0 -224
- package/src/components/code/code_block/code_block_with_tabs.tsx +0 -257
- package/src/components/code/code_block/utils.tsx +0 -67
- package/src/components/code/index.ts +0 -5
- package/src/components/code/inline_code/inline_code.tsx +0 -62
- package/src/components/code/one_line_code/one_line_code.tsx +0 -228
- package/src/components/code/prism_highlighter.tsx +0 -180
- package/src/components/color_wheel_gradient.tsx +0 -31
- package/src/components/floating/index.ts +0 -3
- package/src/components/floating/menu.tsx +0 -189
- package/src/components/floating/menu_common.tsx +0 -31
- package/src/components/floating/menu_components.tsx +0 -99
- package/src/components/image.tsx +0 -24
- package/src/components/index.ts +0 -22
- package/src/components/link.tsx +0 -114
- package/src/components/message.tsx +0 -153
- package/src/components/rating.tsx +0 -106
- package/src/components/readme_renderer/index.ts +0 -3
- package/src/components/readme_renderer/pythonize_value.ts +0 -76
- package/src/components/readme_renderer/table_of_contents.tsx +0 -272
- package/src/components/readme_renderer/utils.tsx +0 -46
- package/src/components/simple_markdown/index.ts +0 -2
- package/src/components/simple_markdown/simple_markdown.tsx +0 -214
- package/src/components/simple_markdown/simple_markdown_components.tsx +0 -293
- package/src/components/tabs/index.ts +0 -2
- package/src/components/tabs/tab.tsx +0 -217
- package/src/components/tabs/tabs.tsx +0 -169
- package/src/components/tag.tsx +0 -196
- package/src/components/text/heading_content.tsx +0 -56
- package/src/components/text/heading_marketing.tsx +0 -55
- package/src/components/text/heading_shared.tsx +0 -55
- package/src/components/text/index.ts +0 -19
- package/src/components/text/text_base.tsx +0 -52
- package/src/components/text/text_content.tsx +0 -104
- package/src/components/text/text_marketing.tsx +0 -152
- package/src/components/text/text_shared.tsx +0 -95
- package/src/components/tile/horizontal_tile.tsx +0 -77
- package/src/components/tile/index.ts +0 -2
- package/src/components/tile/shared.ts +0 -27
- package/src/components/tile/vertical_tile.tsx +0 -59
- package/src/components/to_consolidate/card.tsx +0 -141
- package/src/components/to_consolidate/index.ts +0 -4
- package/src/components/to_consolidate/markdown.tsx +0 -609
- package/src/components/to_consolidate/pagination.tsx +0 -136
- package/src/components/to_consolidate/tab_number_chip.tsx +0 -31
- package/src/design_system/colors/build_color_tokens.js +0 -175
- package/src/design_system/colors/figma_color_tokens.dark.json +0 -886
- package/src/design_system/colors/figma_color_tokens.light.json +0 -886
- package/src/design_system/colors/generated/colors_theme.dark.ts +0 -110
- package/src/design_system/colors/generated/colors_theme.light.ts +0 -110
- package/src/design_system/colors/generated/dark.ts +0 -147
- package/src/design_system/colors/generated/light.ts +0 -147
- package/src/design_system/colors/generated/palette.dark.ts +0 -74
- package/src/design_system/colors/generated/palette.light.ts +0 -74
- package/src/design_system/colors/generated/properties_theme.ts +0 -179
- package/src/design_system/colors/index.ts +0 -7
- package/src/design_system/colors_theme.ts +0 -213
- package/src/design_system/properties_theme.ts +0 -453
- package/src/design_system/supernova_typography_tokens.json +0 -657
- package/src/design_system/theme.ts +0 -25
- package/src/design_system/tokens/index.ts +0 -5
- package/src/design_system/tokens/layouts.ts +0 -29
- package/src/design_system/tokens/radiuses.ts +0 -22
- package/src/design_system/tokens/shadows.ts +0 -22
- package/src/design_system/tokens/spaces.ts +0 -15
- package/src/design_system/tokens/transitions.ts +0 -19
- package/src/design_system/typography_theme.ts +0 -197
- package/src/index.ts +0 -8
- package/src/type_utils.ts +0 -7
- package/src/ui_dependency_provider.tsx +0 -58
- package/src/utils/copy_to_clipboard.ts +0 -24
- package/src/utils/image_color.ts +0 -42
- package/src/utils/index.ts +0 -4
- package/src/utils/resize_observer.ts +0 -18
- package/src/utils/sanitization.ts +0 -14
- package/tsconfig.build.json +0 -17
- package/tsconfig.json +0 -10
- /package/{src/design_system/colors/generated → style/colors}/dark.scss +0 -0
- /package/{src/design_system/colors/generated → style/colors}/light.scss +0 -0
- /package/{src/design_system/colors/generated → style/colors}/palette.dark.scss +0 -0
- /package/{src/design_system/colors/generated → style/colors}/palette.light.scss +0 -0
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import type { FC } from 'react';
|
|
2
|
-
import { Fragment, useMemo } from 'react';
|
|
3
|
-
import styled from 'styled-components';
|
|
4
|
-
|
|
5
|
-
import { StarEmptyIcon, StarFullIcon, StarHalfIcon } from '@apify/ui-icons';
|
|
6
|
-
|
|
7
|
-
import type { ReviewRating } from '@apify-packages/types';
|
|
8
|
-
|
|
9
|
-
import { theme } from '../design_system/theme.js';
|
|
10
|
-
import type { BoxProps } from './box.js';
|
|
11
|
-
import { Box } from './box.js';
|
|
12
|
-
import { Text } from './text/index.js';
|
|
13
|
-
|
|
14
|
-
type RatingStatsProps = {
|
|
15
|
-
ratingStats: Record<ReviewRating, number>
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const StyledRating = styled(Box)`
|
|
19
|
-
display: flex;
|
|
20
|
-
`;
|
|
21
|
-
|
|
22
|
-
const StyledRatingStats = styled(Box)`
|
|
23
|
-
display: grid;
|
|
24
|
-
grid-template-columns: auto minmax(0, 1fr);
|
|
25
|
-
align-items: center;
|
|
26
|
-
gap: 1px ${theme.space.space8};
|
|
27
|
-
align-items: center;
|
|
28
|
-
|
|
29
|
-
p {
|
|
30
|
-
/* TODO: This font is not defined! */
|
|
31
|
-
line-height: 10px;
|
|
32
|
-
font-size: 8px;
|
|
33
|
-
}
|
|
34
|
-
`;
|
|
35
|
-
|
|
36
|
-
const StyledRatingBar = styled(Box)<{ $widthPercent: number }>`
|
|
37
|
-
height: 5px;
|
|
38
|
-
border-radius: 10px;
|
|
39
|
-
background: ${theme.color.neutral.overflow};
|
|
40
|
-
position: relative;
|
|
41
|
-
overflow: hidden;
|
|
42
|
-
|
|
43
|
-
&::after {
|
|
44
|
-
content: "";
|
|
45
|
-
position: absolute;
|
|
46
|
-
display: block;
|
|
47
|
-
height: 100%;
|
|
48
|
-
left: 0;
|
|
49
|
-
top: 0;
|
|
50
|
-
border-radius: 10px;
|
|
51
|
-
background: ${theme.color.neutral.icon};
|
|
52
|
-
width: ${({ $widthPercent }) => $widthPercent}%;
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
`;
|
|
56
|
-
|
|
57
|
-
// 0 is only for empty rating - we don't display any stars filled
|
|
58
|
-
type RatingProps = BoxProps & {
|
|
59
|
-
rating: number | undefined;
|
|
60
|
-
color?: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export const Rating: FC<RatingProps> = ({
|
|
64
|
-
rating = 0,
|
|
65
|
-
color = theme.color.neutral.icon,
|
|
66
|
-
...rest
|
|
67
|
-
}) => {
|
|
68
|
-
const ratingStatsContent = useMemo(() => [1, 2, 3, 4, 5].map((rate) => {
|
|
69
|
-
const ratingFloor = Math.floor(rating);
|
|
70
|
-
const ratingDecimals = rating % 1;
|
|
71
|
-
|
|
72
|
-
if (ratingFloor >= rate || (ratingFloor === rate - 1 && ratingDecimals > 0.75)) return <StarFullIcon size="12" color={color} key={rate} />;
|
|
73
|
-
if (ratingFloor === rate - 1 && ratingDecimals > 0.25) return <StarHalfIcon size="12" color={color} key={rate} />;
|
|
74
|
-
return <StarEmptyIcon size="12" color={color} key={rate} />;
|
|
75
|
-
}), [rating, color]);
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<StyledRating {...rest}>
|
|
79
|
-
{ratingStatsContent}
|
|
80
|
-
</StyledRating>
|
|
81
|
-
);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export const RatingStats: FC<RatingStatsProps & BoxProps> = ({
|
|
85
|
-
ratingStats,
|
|
86
|
-
...rest
|
|
87
|
-
}) => {
|
|
88
|
-
const totalRates = ratingStats[1] + ratingStats[2] + ratingStats[3] + ratingStats[4] + ratingStats[5];
|
|
89
|
-
|
|
90
|
-
const ratingStatsContent = useMemo(() => ([5, 4, 3, 2, 1] as const).map((rate) => {
|
|
91
|
-
const widthPercent = totalRates > 0 ? ((ratingStats[rate] / totalRates) * 100) : 0;
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<Fragment key={rate}>
|
|
95
|
-
<Text align='center' color={theme.color.neutral.textSubtle}>{rate}</Text>
|
|
96
|
-
<StyledRatingBar $widthPercent={widthPercent}/>
|
|
97
|
-
</Fragment>
|
|
98
|
-
);
|
|
99
|
-
}), [totalRates, ratingStats]);
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<StyledRatingStats {...rest}>
|
|
103
|
-
{ratingStatsContent}
|
|
104
|
-
</StyledRatingStats>
|
|
105
|
-
);
|
|
106
|
-
};
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/* eslint-disable prefer-template, no-control-regex */
|
|
2
|
-
// Escaped versions of control characters
|
|
3
|
-
const escapes = {
|
|
4
|
-
92: '\\\\',
|
|
5
|
-
34: '\\"',
|
|
6
|
-
8: '\\b',
|
|
7
|
-
12: '\\f',
|
|
8
|
-
13: '\\r',
|
|
9
|
-
9: '\\t',
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// Replaces a control character with its escaped value
|
|
13
|
-
const escapeChar = (character: string) => {
|
|
14
|
-
const charCode = character.charCodeAt(0);
|
|
15
|
-
const escaped = escapes[charCode as keyof typeof escapes];
|
|
16
|
-
if (escaped) {
|
|
17
|
-
return escaped;
|
|
18
|
-
}
|
|
19
|
-
return '\\u00' + charCode.toString(16).padStart(2, '0');
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// Escapes special characters in a string and surrounds it with proper quotes
|
|
23
|
-
const pythonQuoteString = (str: string, escapeNewlines = true) => {
|
|
24
|
-
const escapedValue = str.replace(/[\x00-\x09\x0b-\x1f\x22\x5c]/g, escapeChar);
|
|
25
|
-
if (!escapeNewlines && escapedValue.includes('\n')) {
|
|
26
|
-
return '"""' + escapedValue + '"""';
|
|
27
|
-
}
|
|
28
|
-
return '"' + escapedValue.replace(/\n/g, '\\n') + '"';
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const INDENT = ' ';
|
|
32
|
-
|
|
33
|
-
// Converts a Javascript value to its equivalent Python representation
|
|
34
|
-
// Doesn't support everything, because that would be impossible,
|
|
35
|
-
// only the values which are supported in an input schema prefill (booleans, numbers, strings, arrays, objects)
|
|
36
|
-
// Tries to keep the representation short, but readable.
|
|
37
|
-
export const pythonizeValue = <T>(value: T, depth = 0): string | T => {
|
|
38
|
-
// None-like values
|
|
39
|
-
if (value === undefined) return 'None';
|
|
40
|
-
if (value === null) return 'None';
|
|
41
|
-
|
|
42
|
-
// Boolean values
|
|
43
|
-
if (value === true) return 'True';
|
|
44
|
-
if (value === false) return 'False';
|
|
45
|
-
|
|
46
|
-
// Number values (sorry, JSON doesn't support infinities)
|
|
47
|
-
if (value === Infinity) return 'None';
|
|
48
|
-
if (value === -Infinity) return 'None';
|
|
49
|
-
if (Number.isNaN(value)) return 'None';
|
|
50
|
-
if (typeof value === 'number') return value;
|
|
51
|
-
|
|
52
|
-
// String values
|
|
53
|
-
if (typeof value === 'string') return pythonQuoteString(value, false);
|
|
54
|
-
|
|
55
|
-
// Arrays of values
|
|
56
|
-
if (Array.isArray(value)) {
|
|
57
|
-
if (value.length === 0) return '[]';
|
|
58
|
-
if (value.length === 1) return '[' + pythonizeValue(value[0], depth + 1) + ']';
|
|
59
|
-
return '[\n'
|
|
60
|
-
+ value.map((v) => INDENT.repeat(depth + 1) + pythonizeValue(v, depth + 1) + ',\n').join('')
|
|
61
|
-
+ INDENT.repeat(depth) + ']';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Object values
|
|
65
|
-
if (typeof value === 'object') {
|
|
66
|
-
const entries = Object.entries(value);
|
|
67
|
-
if (entries.length === 0) return '{}';
|
|
68
|
-
if (entries.length === 1) return '{ ' + pythonQuoteString(entries[0][0]) + ': ' + pythonizeValue(entries[0][1], depth + 1) + ' }';
|
|
69
|
-
return '{\n'
|
|
70
|
-
+ entries.map(([k, v]) => INDENT.repeat(depth + 1) + pythonQuoteString(k) + ': ' + pythonizeValue(v, depth + 1) + ',\n').join('')
|
|
71
|
-
+ INDENT.repeat(depth) + '}';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// This should never happen, if it did, something went wrong
|
|
75
|
-
return 'UNSUPPORTED VALUE';
|
|
76
|
-
};
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import _ from 'lodash';
|
|
2
|
-
import React, {
|
|
3
|
-
useCallback,
|
|
4
|
-
useMemo,
|
|
5
|
-
} from 'react';
|
|
6
|
-
import ReactMarkdown from 'react-markdown';
|
|
7
|
-
import type { AllowElement } from 'react-markdown/lib/rehype-filter';
|
|
8
|
-
import remarkToc from 'remark-toc';
|
|
9
|
-
import styled from 'styled-components';
|
|
10
|
-
|
|
11
|
-
import { theme } from '../../design_system/theme.js';
|
|
12
|
-
import { inlineCodeStyles } from '../code/index.js';
|
|
13
|
-
import { Link } from '../link.js';
|
|
14
|
-
import { Text } from '../text/index.js';
|
|
15
|
-
import { cleanMarkdown, slugifyHeadingChildren } from './utils.js';
|
|
16
|
-
|
|
17
|
-
const TOC_HEADING_ID = 'Contents';
|
|
18
|
-
|
|
19
|
-
const StyledTOCLink = styled(Text)`
|
|
20
|
-
display: inline-block;
|
|
21
|
-
color: ${theme.color.neutral.textMuted};
|
|
22
|
-
text-decoration: none;
|
|
23
|
-
|
|
24
|
-
/* Do no change the font style if the heading is defined as bold in the markdown and is wrapped in a <strong> tag */
|
|
25
|
-
strong {
|
|
26
|
-
font-size: inherit !important;
|
|
27
|
-
line-height: inherit !important;
|
|
28
|
-
font-weight: inherit !important;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
&:hover, &.selected {
|
|
32
|
-
color: ${theme.color.primary.text} !important;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
&:before {
|
|
36
|
-
content: " ";
|
|
37
|
-
display: inline-block;
|
|
38
|
-
height: 100%;
|
|
39
|
-
left: 0;
|
|
40
|
-
margin-top: -1px;
|
|
41
|
-
position: absolute;
|
|
42
|
-
width: 1px;
|
|
43
|
-
background-color: ${theme.color.neutral.border};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
&.selected:before {
|
|
47
|
-
background-color: ${theme.color.primary.text};
|
|
48
|
-
}
|
|
49
|
-
`;
|
|
50
|
-
|
|
51
|
-
const StyledTableOfContents = styled.div`
|
|
52
|
-
position: relative;
|
|
53
|
-
overflow: hidden;
|
|
54
|
-
|
|
55
|
-
p {
|
|
56
|
-
margin: 0;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.level-3 a {
|
|
60
|
-
color: ${theme.color.neutral.textSubtle};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
ul {
|
|
64
|
-
padding-left: ${theme.space.space8} !important;
|
|
65
|
-
list-style: none;
|
|
66
|
-
margin: 0;
|
|
67
|
-
overflow-y: auto;
|
|
68
|
-
|
|
69
|
-
&.level-3 {
|
|
70
|
-
padding-left: ${theme.space.space16} !important;
|
|
71
|
-
max-height: 0;
|
|
72
|
-
overflow: hidden;
|
|
73
|
-
transition: all .3s ease-in-out;
|
|
74
|
-
|
|
75
|
-
&.expanded {
|
|
76
|
-
max-height: 1000px;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.inline-code {
|
|
82
|
-
${inlineCodeStyles}
|
|
83
|
-
}
|
|
84
|
-
`;
|
|
85
|
-
|
|
86
|
-
interface CustomHTMLAnchorElement extends Omit<HTMLAnchorElement, 'children' | 'parentNode'> {
|
|
87
|
-
hash: string;
|
|
88
|
-
tagName: string;
|
|
89
|
-
type: string;
|
|
90
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
-
parentNode: any;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const handleScroll = (anchors: CustomHTMLAnchorElement[], headlines: HTMLElement[], anchorLists: HTMLUListElement[] | null, headingOffsetPx: number) => {
|
|
95
|
-
if (!anchors || !headlines) return;
|
|
96
|
-
|
|
97
|
-
// Items that are past scroll
|
|
98
|
-
const headlinesPastScrollPosition = headlines.filter((headline) => {
|
|
99
|
-
return headline.getBoundingClientRect().top - headingOffsetPx < 0;
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Last item that is past the scroll is current in view. If no headline is past the scroll, let's select the first one
|
|
103
|
-
const currentHeadlineId = headlinesPastScrollPosition[headlinesPastScrollPosition.length - 1]?.id || headlines[0]?.id;
|
|
104
|
-
|
|
105
|
-
// Highlighting anchor of current headline
|
|
106
|
-
if (currentHeadlineId) {
|
|
107
|
-
const currentHash = `#${currentHeadlineId}`;
|
|
108
|
-
|
|
109
|
-
anchorLists?.forEach((anchorList) => anchorList.classList.remove('expanded'));
|
|
110
|
-
|
|
111
|
-
// Remove selected class from all anchors and only add it to the current one
|
|
112
|
-
anchors.forEach((anchor) => {
|
|
113
|
-
anchor.classList.remove('selected');
|
|
114
|
-
if (anchor.hash === currentHash) {
|
|
115
|
-
anchor.classList.add('selected');
|
|
116
|
-
|
|
117
|
-
const grandparent = anchor.parentNode?.parentNode;
|
|
118
|
-
|
|
119
|
-
// Structure of the table of content is normalized so we can figure out if section should be expanded
|
|
120
|
-
// by tag and className of neighboring nodes. There are two cases:
|
|
121
|
-
|
|
122
|
-
// 1) If h2 is selected, we want to expand its section with h3 headings
|
|
123
|
-
if (grandparent?.tagName === 'LI' && grandparent.children[1]?.tagName === 'UL' && grandparent.children[1]?.classList.contains('level-3')) {
|
|
124
|
-
grandparent.children[1].classList.add('expanded');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 2) We also want to expand section of h3 headings that follow h1 right away
|
|
128
|
-
if (grandparent?.tagName === 'UL' && grandparent.classList.contains('level-3')) {
|
|
129
|
-
grandparent.classList.add('expanded');
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const assignHeadingLevelsRecursively = (
|
|
137
|
-
{
|
|
138
|
-
children,
|
|
139
|
-
tagName,
|
|
140
|
-
properties,
|
|
141
|
-
}: {
|
|
142
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
143
|
-
children: any[],
|
|
144
|
-
tagName: string,
|
|
145
|
-
properties: { className: string }
|
|
146
|
-
},
|
|
147
|
-
level: number,
|
|
148
|
-
) => {
|
|
149
|
-
if (level > 3) return;
|
|
150
|
-
let currentLevel = level;
|
|
151
|
-
if (tagName === 'ul') {
|
|
152
|
-
// eslint-disable-next-line no-param-reassign
|
|
153
|
-
properties.className = `level-${currentLevel}`; // assign level className
|
|
154
|
-
currentLevel += 1; // increase current heading level;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
children?.forEach((child) => {
|
|
158
|
-
// We only care about ul and li elements - we don't need to traverse any other
|
|
159
|
-
if (child.type === 'element' && (tagName === 'ul' || tagName === 'li')) {
|
|
160
|
-
assignHeadingLevelsRecursively(child, currentLevel);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
export interface TableOfContentsProps {
|
|
166
|
-
markdown: string;
|
|
167
|
-
headingOffsetPx?: number; // How far from the top should the toc detect the current heading
|
|
168
|
-
// Function where we can define which elements are allowed in the markdown. See https://github.com/remarkjs/react-markdown#props for more info
|
|
169
|
-
allowElement?: AllowElement;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const TableOfContentsComponent = ({
|
|
173
|
-
markdown,
|
|
174
|
-
headingOffsetPx = 10,
|
|
175
|
-
allowElement,
|
|
176
|
-
...rest
|
|
177
|
-
}: TableOfContentsProps) => {
|
|
178
|
-
const cleanedMarkdown = useMemo(() => {
|
|
179
|
-
const cleanedMarkdownString = cleanMarkdown(markdown);
|
|
180
|
-
return `### ${TOC_HEADING_ID}\n${cleanedMarkdownString}`;
|
|
181
|
-
}, [markdown]);
|
|
182
|
-
|
|
183
|
-
const hasTopLevelHeading = useMemo(() => !!cleanedMarkdown.match(/^#\s.+$/m), [cleanedMarkdown]);
|
|
184
|
-
|
|
185
|
-
const tocRef = useCallback((node: HTMLElement | null) => {
|
|
186
|
-
const anchors = node ? Array.from(node.querySelectorAll('a')) : [];
|
|
187
|
-
const anchorLists = node ? Array.from(node.querySelectorAll('ul')) : null;
|
|
188
|
-
const headlines = (anchors)
|
|
189
|
-
.map((anchor) => document.getElementById(anchor.hash.substr(1)))
|
|
190
|
-
.filter((headline) => headline !== undefined && headline !== null) as HTMLElement[];
|
|
191
|
-
|
|
192
|
-
const scrollHandler = () => handleScroll(anchors, headlines, anchorLists, headingOffsetPx);
|
|
193
|
-
|
|
194
|
-
// Callback refs cannot return cleanup functions - but they are called again with null on unmount
|
|
195
|
-
if (node) {
|
|
196
|
-
window.addEventListener('scroll', scrollHandler);
|
|
197
|
-
} else {
|
|
198
|
-
window.removeEventListener('scroll', scrollHandler);
|
|
199
|
-
}
|
|
200
|
-
scrollHandler(); // call for the first time so we select the first heading without scrolling
|
|
201
|
-
}, [headingOffsetPx]);
|
|
202
|
-
|
|
203
|
-
return (
|
|
204
|
-
<StyledTableOfContents ref={tocRef} {...rest}>
|
|
205
|
-
<ReactMarkdown
|
|
206
|
-
allowElement={allowElement}
|
|
207
|
-
remarkPlugins={[
|
|
208
|
-
[remarkToc, { heading: TOC_HEADING_ID, maxDepth: 3 }],
|
|
209
|
-
() => ({ children, ...nodeRest }) => {
|
|
210
|
-
// TOC plug-ins only inject content table to existing markdown documents - only generating it is not a common use-case
|
|
211
|
-
// To make is happen, we can just take the generated node where toc is located and throw away the rest.
|
|
212
|
-
// children[0] is the heading where toc is placed (that's plug-in's requirement)
|
|
213
|
-
// children[1] is the generated table of contents we can simply pick
|
|
214
|
-
|
|
215
|
-
let tocContentNode = children[1];
|
|
216
|
-
|
|
217
|
-
// If there is no H1 heading in the readme then assignHeadingLevelsRecursively does not work correctly.
|
|
218
|
-
// We need to 'normalize' the final node to have all the levels we expect
|
|
219
|
-
// - solution is to wrap the content in extra node that mimic this H1 group that remarkToc would normally create
|
|
220
|
-
if (!hasTopLevelHeading) {
|
|
221
|
-
tocContentNode = {
|
|
222
|
-
type: 'list',
|
|
223
|
-
ordered: false,
|
|
224
|
-
spread: false,
|
|
225
|
-
children: [
|
|
226
|
-
{
|
|
227
|
-
type: 'listItem',
|
|
228
|
-
spread: true,
|
|
229
|
-
children: [
|
|
230
|
-
tocContentNode,
|
|
231
|
-
],
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
...nodeRest,
|
|
239
|
-
children: [
|
|
240
|
-
tocContentNode,
|
|
241
|
-
],
|
|
242
|
-
};
|
|
243
|
-
},
|
|
244
|
-
]}
|
|
245
|
-
rehypePlugins={[() => (input) => {
|
|
246
|
-
// this plug-in already works with html node representation so we can assign classNames that we need
|
|
247
|
-
// in order to allow hiding blocks with h3 headings that are out of the viewport
|
|
248
|
-
assignHeadingLevelsRecursively(input.children[0], 1);
|
|
249
|
-
return input;
|
|
250
|
-
}]}
|
|
251
|
-
components={{
|
|
252
|
-
a: ({ children }) => (
|
|
253
|
-
<StyledTOCLink
|
|
254
|
-
forwardedAs={Link}
|
|
255
|
-
to={`#${slugifyHeadingChildren(children)}`}
|
|
256
|
-
py={'space4'}
|
|
257
|
-
>
|
|
258
|
-
{children}
|
|
259
|
-
</StyledTOCLink>
|
|
260
|
-
),
|
|
261
|
-
code: ({ children }) => (
|
|
262
|
-
<code className="inline-code">{children}</code>
|
|
263
|
-
),
|
|
264
|
-
}}
|
|
265
|
-
>
|
|
266
|
-
{cleanedMarkdown}
|
|
267
|
-
</ReactMarkdown>
|
|
268
|
-
</StyledTableOfContents>
|
|
269
|
-
);
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
export const TableOfContents = React.memo(TableOfContentsComponent, (prevProps, nextProps) => _.isEqual(prevProps, nextProps));
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import React, { useCallback } from 'react';
|
|
2
|
-
import type { AllowElement } from 'react-markdown/lib/rehype-filter';
|
|
3
|
-
import slugify from 'slugify';
|
|
4
|
-
|
|
5
|
-
export const slugifyHeadingChildren = (
|
|
6
|
-
headingChildren: React.ReactNode,
|
|
7
|
-
): string | undefined => {
|
|
8
|
-
if (!headingChildren) return undefined;
|
|
9
|
-
|
|
10
|
-
const slugs: string[] = [];
|
|
11
|
-
React.Children.forEach(headingChildren, (child) => {
|
|
12
|
-
if (typeof child === 'string') {
|
|
13
|
-
slugs.push(slugify(child, { lower: true, strict: true }));
|
|
14
|
-
} else if (React.isValidElement(child) && child.props.children) {
|
|
15
|
-
const nestedSlugs = slugifyHeadingChildren(child.props.children);
|
|
16
|
-
if (nestedSlugs) slugs.push(nestedSlugs);
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
return slugs.join('-');
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const cleanMarkdown = (markdown: string, removeFirstH1?: boolean): string => {
|
|
24
|
-
// Remove the table of contents as we are generating our own
|
|
25
|
-
let clean = markdown
|
|
26
|
-
.replace(/<!-- toc start -->.*?<!-- toc end -->/s, '')
|
|
27
|
-
.replace(/##.*content.*?\n*<!-- toc -->(.|[\r\n])*<!-- tocstop -->\n*/, '');
|
|
28
|
-
|
|
29
|
-
// Remove first h1 if removeFirstH1 is true
|
|
30
|
-
if (removeFirstH1) clean = clean.replace(removeFirstH1 ? /^#\s.+$/m : '', '');
|
|
31
|
-
|
|
32
|
-
return clean.trim();
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// This removes the first element if it's a `h1` containing exactly the Actor title
|
|
36
|
-
export const useActorTitleHeadingFilter = (actorTitle: string): AllowElement => {
|
|
37
|
-
return useCallback((element, index, parent) => {
|
|
38
|
-
if (parent.type === 'root'
|
|
39
|
-
&& index === 0
|
|
40
|
-
&& element.tagName === 'h1'
|
|
41
|
-
&& element.children.length === 1
|
|
42
|
-
&& element.children[0].type === 'text'
|
|
43
|
-
&& element.children[0].value?.toLowerCase() === actorTitle.toLowerCase()) return false;
|
|
44
|
-
return true;
|
|
45
|
-
}, [actorTitle]);
|
|
46
|
-
};
|