import React, { useState, useMemo, useEffect, useCallback } from 'react' import download from 'downloadjs' import classNames from 'classnames' import { toast as notify } from 'react-hot-toast' import type Hljs from 'highlight.js/lib/common' import useCopyToClipboard from '~/utils/useCopyToClipboard' import { atom, useRecoilState } from 'recoil' import { localStorageRecoilEffect } from '~/utils/localStorage' import CheckIcon from '~/icons/compiled/Check' import CopyIcon from '~/icons/compiled/Copy' import DownloadIcon from '~/icons/compiled/DownloadsFolder' const packageManagerState = atom<'npm' | 'yarn'>({ key: 'packageManager', default: 'npm', effects: [localStorageRecoilEffect('iv_packageManager')], }) export interface HighlightedCodeBlockProps { code: string language?: string canCopy?: boolean canDownload?: boolean fileName?: string extension?: string className?: string shouldReplacePackageManager?: boolean shouldDisplayFileName?: boolean theme?: 'dark' | 'light' } function guessExtension(language?: string) { // based on documented languages accepted for io.display.code switch (language) { case undefined: return 'txt' case 'csharp': return 'cs' case 'javascript': return 'js' case 'typescript': return 'ts' case 'rust': return 'rs' case 'python': case 'python-repl': return 'py' case 'markdown': return 'md' case 'kotlin': return 'kt' case 'plaintext': return 'txt' case 'ruby': return 'rb' case 'shell': return 'sh' case 'swift': return 'swe' case 'php-template': return 'php' case 'makefile': return 'Makefile' default: return language } } export default function HighlightedCodeBlock({ code, language, shouldReplacePackageManager = true, shouldDisplayFileName = true, canCopy = true, canDownload = true, fileName, extension, className, theme = 'dark', }: HighlightedCodeBlockProps) { const [packageManager, setPackageManager] = useRecoilState(packageManagerState) const doesIncludePackageManager = useMemo( () => new RegExp(/(npm |npx )/).test(code), [code] ) const formattedCode = useMemo(() => { let formatted = code if (shouldReplacePackageManager && packageManager === 'yarn') { // order is important! // global install: formatted = formatted.replace(/npm install -g/, 'yarn global add') formatted = formatted.replace(/npm i -g/, 'yarn global add') // install all: formatted = formatted.replace(/npm install &&/, 'yarn &&') formatted = formatted.replace(/npm i &&/, 'yarn &&') formatted = formatted.replace(/^npm install$/, 'yarn') // install specific: formatted = formatted.replace(/npm install /, 'yarn add ') formatted = formatted.replace(/npm i /, 'yarn add ') // yarn implied run: formatted = formatted.replace(/npm run/, 'yarn') // create formatted = formatted.replace(/npx create-/, 'yarn create ') } return formatted }, [code, shouldReplacePackageManager, packageManager]) const { onCopyClick, isCopied } = useCopyToClipboard() const handleDownload = useCallback(() => { try { download( formattedCode, `${fileName ?? 'code'}.${extension ?? guessExtension(language)}`, 'text/plain' ) } catch (err) { console.error('Failed generating download', err) notify.error('Failed generating the download.') } }, [formattedCode, fileName, extension, language]) return (
)
}