@apify/ui-library 1.98.2 → 1.98.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apify/ui-library",
3
- "version": "1.98.2",
3
+ "version": "1.98.3",
4
4
  "description": "React UI library used by apify.com",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -64,5 +64,5 @@
64
64
  "src",
65
65
  "style"
66
66
  ],
67
- "gitHead": "5c3b5f64c4675e693452f6755aaf3d8b3c4552c6"
67
+ "gitHead": "11a60e90663e75d2de31c28a6e6b01c98d0b9d57"
68
68
  }
@@ -37,6 +37,40 @@ export const isUrlEmail = (url: To) => {
37
37
  if (!url || typeof (url) !== 'string') return false;
38
38
  return url.startsWith('mailto:');
39
39
  };
40
+ /**
41
+ * Omits the domain and protocol from a given URL string.
42
+ * @param rawUrlString The input URL string which may include protocol and domain.
43
+ * @returns The URL string without the domain and protocol, or if it's invalid URL then it returns the input param — `rawUrlString`.
44
+ */
45
+ export function omitDomainAndProtocol(rawUrlString: string): string {
46
+ try {
47
+ const { pathname, search, hash } = new URL(rawUrlString);
48
+ return pathname + search + hash;
49
+ } catch {
50
+ return rawUrlString;
51
+ }
52
+ }
53
+
54
+ export function hasUrlHttpProtocol(rawUrlString: string): boolean {
55
+ const httpProtocolRegex = /^https?:\/\//i;
56
+ return httpProtocolRegex.test(rawUrlString);
57
+ }
58
+ /**
59
+ * Returns pathname if the input param `to` is string and internal link or if it's a `To` object
60
+ */
61
+ function getHref(to: To, isExternal?: boolean): string {
62
+ if (typeof to === 'string') {
63
+ // React Router ignores all urls contains origins and append it to current window url.
64
+ // This extracts the pathname from the URL if it's same as the current window url
65
+ if (!isExternal && hasUrlHttpProtocol(to)) {
66
+ return omitDomainAndProtocol(to);
67
+ }
68
+
69
+ return to;
70
+ }
71
+
72
+ return createPath(to);
73
+ }
40
74
 
41
75
  const StyledLink = styled(Box)`
42
76
  /* Basic positioning */
@@ -78,8 +112,8 @@ export const Link = forwardRef<HTMLElement, LinkProps>(({
78
112
  trackClick,
79
113
  InternalLink,
80
114
  } = useSharedUiDependencies();
81
- const href = typeof (to) === 'string' ? to : createPath(to);
82
115
  const isExternal = isUrlExternal(to, windowLocationHost);
116
+ const href = getHref(to, isExternal);
83
117
  const isEmail = isUrlEmail(to);
84
118
  const isTrusted = isHrefTrusted(href);
85
119
 
@@ -88,12 +122,13 @@ export const Link = forwardRef<HTMLElement, LinkProps>(({
88
122
  if (onClick) onClick(e);
89
123
  };
90
124
 
91
- const effectiveRel = clsx(
92
- rel,
125
+ const uniqRel = new Set([
126
+ ...(rel?.split(' ') || []),
93
127
  isExternal && 'external',
94
128
  (isExternal || target === '_blank') && 'noopener',
95
129
  (isExternal && !isTrusted) && 'nofollow',
96
- );
130
+ ]);
131
+ const effectiveRel = clsx(Array.from(uniqRel));
97
132
 
98
133
  return (
99
134
  <StyledLink
@@ -15,6 +15,7 @@ import type { UiThemeOption } from '../../design_system/theme.js';
15
15
  import { theme } from '../../design_system/theme.js';
16
16
  import { useCopyToClipboard } from '../../utils/index.js';
17
17
  import { CodeBlock, inlineCodeStyles, OneLineCode } from '../code/index.js';
18
+ import { Link as SharedLink, type LinkProps } from '../link.js';
18
19
  import { cleanMarkdown, slugifyHeadingChildren } from '../readme_renderer/utils.js';
19
20
 
20
21
  interface StyledReadmeProps {
@@ -422,13 +423,12 @@ interface LinkRendererProps extends React.AnchorHTMLAttributes<HTMLAnchorElement
422
423
 
423
424
  interface LinkRendererOptions {
424
425
  hostname?: string,
425
- Link?: React.ElementType;
426
426
  }
427
427
 
428
428
  // We want no-follow for external links
429
429
  // Also if link is a video from youtube or vimeo, we want to render it as iframe
430
430
  // Allowing to pass hostname to check if the link is an Apify link to the same hostname (is needed for SSR on the web)
431
- const DefaultLinkRenderer = ({ node, href, ...props }: LinkRendererProps, { hostname, Link }: LinkRendererOptions, isUserGeneratedContent?: boolean) => {
431
+ const DefaultLinkRenderer = ({ node, href, ...props }: LinkRendererProps, { hostname }: LinkRendererOptions, isUserGeneratedContent?: boolean) => {
432
432
  const videoSrc = node.properties.enableEmbeddedVideo && getVideoSrc(href);
433
433
  if (videoSrc) return <Video src={videoSrc} />;
434
434
 
@@ -454,12 +454,7 @@ const DefaultLinkRenderer = ({ node, href, ...props }: LinkRendererProps, { host
454
454
  && APIFY_HOSTNAMES.includes(urlParsed.hostname.toLowerCase())
455
455
  && urlParsed.protocol === 'https:'; // we want to disqualify links that have http: protocol. It's a mistake on users' side that we are penalized for.
456
456
 
457
- // Same host name, use the provided link for internal navigation
458
- if (!hasDifferentHostname && Link) {
459
- return <Link to={urlParsed} {...props} />;
460
- }
461
-
462
- let linkProps = {};
457
+ let linkProps: Pick<LinkProps, 'rel' | 'target'> = {};
463
458
 
464
459
  // If false, we want to open the link in the same tab (linkProps won't have any props)
465
460
  if (hasDifferentHostname) {
@@ -472,11 +467,11 @@ const DefaultLinkRenderer = ({ node, href, ...props }: LinkRendererProps, { host
472
467
  // Google says:
473
468
  // It’s valid to use nofollow with the new attributes — such as rel=”nofollow ugc”
474
469
  // — if you wish to be backwards-compatible with services that don’t support the new attributes.
475
- linkProps = { target: '_blank', rel: clsx('noopener nofollow', isUserGeneratedContent && 'ugc') };
470
+ linkProps = { rel: clsx(isUserGeneratedContent && 'ugc') };
476
471
  }
477
472
  }
478
473
 
479
- return <a href={href} {...props} {...linkProps} />;
474
+ return <SharedLink {...props as LinkProps} {...linkProps} to={href}/>;
480
475
  };
481
476
 
482
477
  // node is just to omit from exported props
@@ -548,7 +543,6 @@ export interface MarkdownProps {
548
543
  isUserGeneratedContent?: boolean;
549
544
  currentPathHostname?: string;
550
545
  addHeadingAnchors?: boolean;
551
- Link?: React.ElementType;
552
546
  LinkRenderer?: (props: LinkRendererProps, options: LinkRendererOptions, isUserGeneratedContent?: boolean) => React.ReactElement;
553
547
  lazyLoadImages?: boolean;
554
548
  }
@@ -563,7 +557,6 @@ const Markdown = ({
563
557
  currentPathHostname,
564
558
  addHeadingAnchors,
565
559
  isUserGeneratedContent,
566
- Link,
567
560
  LinkRenderer,
568
561
  lazyLoadImages,
569
562
  }: MarkdownProps) => {
@@ -617,8 +610,8 @@ const Markdown = ({
617
610
  h5: headingRenderer,
618
611
  a: (linkProps: LinkRendererProps) => (
619
612
  LinkRenderer
620
- ? LinkRenderer(linkProps, { hostname: currentPathHostname, Link }, isUserGeneratedContent)
621
- : DefaultLinkRenderer(linkProps, { hostname: currentPathHostname, Link }, isUserGeneratedContent)
613
+ ? LinkRenderer(linkProps, { hostname: currentPathHostname }, isUserGeneratedContent)
614
+ : DefaultLinkRenderer(linkProps, { hostname: currentPathHostname }, isUserGeneratedContent)
622
615
  ),
623
616
  code: (codeProps: CodeRendererComponentProps) => CodeRenderer(codeProps),
624
617
  p: ParagraphRenderer,