diff --git a/.circleci/config.yml b/.circleci/config.yml index a1a3866c82..9b63d1e760 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ version: 2.1 jobs: test-argos-ci: docker: - - image: cimg/node:16.19.0-browsers + - image: cimg/node:16.19.1-browsers steps: - checkout - run: diff --git a/.dumi/theme/builtins/Previewer/CodePreviewer.tsx b/.dumi/theme/builtins/Previewer/CodePreviewer.tsx new file mode 100644 index 0000000000..cf9d1516df --- /dev/null +++ b/.dumi/theme/builtins/Previewer/CodePreviewer.tsx @@ -0,0 +1,556 @@ +import { + CheckOutlined, + LinkOutlined, + SnippetsOutlined, + ThunderboltOutlined, +} from '@ant-design/icons'; +import type { Project } from '@stackblitz/sdk'; +import stackblitzSdk from '@stackblitz/sdk'; +import { Alert, Badge, Space, Tooltip } from 'antd'; +import classNames from 'classnames'; +import LZString from 'lz-string'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import type { IPreviewerProps } from 'dumi'; +import { FormattedMessage } from 'dumi'; +import Prism from 'prismjs'; +import JsonML from 'jsonml.js/lib/utils'; +import toReactElement from 'jsonml-to-react-element'; +import { ping } from '../../utils'; +import ClientOnly from '../../common/ClientOnly'; +import BrowserFrame from '../../common/BrowserFrame'; +import EditButton from '../../common/EditButton'; +import CodePenIcon from '../../common/CodePenIcon'; +import CodePreview from '../../common/CodePreview'; +import CodeSandboxIcon from '../../common/CodeSandboxIcon'; +import RiddleIcon from '../../common/RiddleIcon'; +import ExternalLinkIcon from '../../common/ExternalLinkIcon'; +import type { SiteContextProps } from '../../slots/SiteContext'; +import SiteContext from '../../slots/SiteContext'; +import useLocation from '../../../hooks/useLocation'; + +const { ErrorBoundary } = Alert; + +function toReactComponent(jsonML: any) { + return toReactElement(jsonML, [ + [ + (node: any) => JsonML.isElement(node) && JsonML.getTagName(node) === 'pre', + (node: any, index: any) => { + // ref: https://github.com/benjycui/bisheng/blob/master/packages/bisheng/src/bisheng-plugin-highlight/lib/browser.js#L7 + const attr = JsonML.getAttributes(node); + return React.createElement( + 'pre', + { + key: index, + className: `language-${attr.lang}`, + }, + React.createElement('code', { + dangerouslySetInnerHTML: { __html: attr.highlighted }, + }), + ); + }, + ], + ]); +} + +function compress(string: string): string { + return LZString.compressToBase64(string) + .replace(/\+/g, '-') // Convert '+' to '-' + .replace(/\//g, '_') // Convert '/' to '_' + .replace(/=+$/, ''); // Remove ending '=' +} + +const track = ({ type, demo }: { type: string; demo: string }) => { + if (!window.gtag) { + return; + } + window.gtag('event', 'demo', { event_category: type, event_label: demo }); +}; + +let pingDeferrer: PromiseLike; + +function useShowRiddleButton() { + const [showRiddleButton, setShowRiddleButton] = useState(false); + + useEffect(() => { + pingDeferrer ??= new Promise((resolve) => { + ping((status) => { + if (status !== 'timeout' && status !== 'error') { + return resolve(true); + } + + return resolve(false); + }); + }); + pingDeferrer.then(setShowRiddleButton); + }, []); + + return showRiddleButton; +} + +const CodePreviewer: React.FC = (props) => { + const { + asset, + expand, + iframe, + demoUrl, + children, + title, + description, + debug, + jsx, + style, + compact, + background, + filePath, + version, + } = props; + + const location = useLocation(); + + const entryCode = asset.dependencies['index.tsx'].value; + const showRiddleButton = useShowRiddleButton(); + + const liveDemo = useRef(null); + const anchorRef = useRef(null); + const codeSandboxIconRef = useRef(null); + const riddleIconRef = useRef(null); + const codepenIconRef = useRef(null); + const [codeExpand, setCodeExpand] = useState(false); + const [copyTooltipOpen, setCopyTooltipOpen] = useState(false); + const [copied, setCopied] = useState(false); + const [codeType, setCodeType] = useState('tsx'); + const { theme } = useContext(SiteContext); + + const { hash, pathname, search } = location; + const docsOnlineUrl = `https://ant.design${pathname}${search}#${asset.id}`; + + const [showOnlineUrl, setShowOnlineUrl] = useState(false); + + const highlightedCodes = { + jsx: Prism.highlight(jsx, Prism.languages.javascript, 'jsx'), + tsx: Prism.highlight(entryCode, Prism.languages.javascript, 'jsx'), + }; + + const highlightedStyle = style ? Prism.highlight(style, Prism.languages.css, 'css') : ''; + + useEffect(() => { + const regexp = /preview-(\d+)-ant-design/; // matching PR preview addresses + setShowOnlineUrl( + process.env.NODE_ENV === 'development' || regexp.test(window.location.hostname), + ); + }, []); + + const handleCodeExpand = (demo: string) => { + setCodeExpand((prev) => !prev); + track({ type: 'expand', demo }); + }; + + const handleCodeCopied = (demo: string) => { + setCopied(true); + track({ type: 'copy', demo }); + }; + + const onCopyTooltipOpenChange = (open: boolean) => { + setCopyTooltipOpen(open); + if (open) { + setCopied(false); + } + }; + + useEffect(() => { + if (asset.id === hash.slice(1)) { + anchorRef.current?.click(); + } + }, []); + + useEffect(() => { + setCodeExpand(expand); + }, [expand]); + + if (!liveDemo.current) { + liveDemo.current = iframe ? ( + +