diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-13 19:32:56 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:14:41 +0100 |
| commit | 006b15b467a5cd835a6eab1b49023100bdc8f2e6 (patch) | |
| tree | 949c7295c2e206f42357f135bab4696ddf6576ec /src/utils | |
| parent | 00f147a7a687d5772bcc538bc606cfff972178cd (diff) | |
refactor(components): rewrite Code component and usePrism hook
* move Prism styles to Sass placeholders to avoid repeats
* let usePrism consumer define its plugins (remove default ones)
* remove `plugins` prop from Code component
* add new props to Code component to let consumer configure plugins
(and handle plugin list from the given options)
However there are some problems with Prism plugins: line-highlight and
treeview does not seems to be loaded. I don't want to use Babel instead
of SWC so I have no solution for now.
Diffstat (limited to 'src/utils')
| -rw-r--r-- | src/utils/hooks/use-prism.tsx | 183 | ||||
| -rw-r--r-- | src/utils/hooks/use-prism/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-prism/use-prism.test.tsx | 91 | ||||
| -rw-r--r-- | src/utils/hooks/use-prism/use-prism.ts | 217 | ||||
| -rw-r--r-- | src/utils/plugins/prism-color-scheme.cjs (renamed from src/utils/plugins/prism-color-scheme.js) | 0 |
5 files changed, 309 insertions, 183 deletions
diff --git a/src/utils/hooks/use-prism.tsx b/src/utils/hooks/use-prism.tsx deleted file mode 100644 index 429808f..0000000 --- a/src/utils/hooks/use-prism.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import Prism from 'prismjs'; -import { useEffect, useMemo } from 'react'; -import { useIntl } from 'react-intl'; - -const PRISM_PLUGINS = [ - 'autoloader', - 'color-scheme', - 'command-line', - 'copy-to-clipboard', - 'diff-highlight', - 'inline-color', - 'line-highlight', - 'line-numbers', - 'match-braces', - 'normalize-whitespace', - 'show-language', - 'toolbar', -] as const; - -export type PrismPlugin = (typeof PRISM_PLUGINS)[number]; - -export type DefaultPrismPlugin = Extract< - PrismPlugin, - | 'autoloader' - | 'color-scheme' - | 'copy-to-clipboard' - | 'match-braces' - | 'normalize-whitespace' - | 'show-language' - | 'toolbar' ->; - -export type OptionalPrismPlugin = Exclude<PrismPlugin, DefaultPrismPlugin>; - -export type PrismLanguage = - | 'apacheconf' - | 'bash' - | 'css' - | 'diff' - | 'docker' - | 'editorconfig' - | 'ejs' - | 'git' - | 'graphql' - | 'html' - | 'ignore' - | 'ini' - | 'javascript' - | 'jsdoc' - | 'json' - | 'jsx' - | 'makefile' - | 'markup' - | 'php' - | 'phpdoc' - | 'regex' - | 'scss' - | 'shell-session' - | 'smarty' - | 'tcl' - | 'toml' - | 'tsx' - | 'twig' - | 'yaml'; - -export type PrismAttributes = { - 'data-prismjs-copy': string; - 'data-prismjs-copy-success': string; - 'data-prismjs-copy-error': string; - 'data-prismjs-color-scheme-dark': string; - 'data-prismjs-color-scheme-light': string; -}; - -export type UsePrismProps = { - language?: PrismLanguage; - plugins: OptionalPrismPlugin[]; -}; - -export type UsePrismReturn = { - attributes: PrismAttributes; - className: string; -}; - -/** - * Import and configure all given Prism plugins. - * - * @param {PrismPlugin[]} plugins - The Prism plugins to activate. - */ -const loadPrismPlugins = async (plugins: PrismPlugin[]) => { - for (const plugin of plugins) { - try { - if (plugin === 'color-scheme') { - await import(`../plugins/prism-${plugin}`); - } else { - await import(`prismjs/plugins/${plugin}/prism-${plugin}.min.js`); - } - - if (plugin === 'autoloader') { - Prism.plugins.autoloader.languages_path = '/prism/'; - } - } catch (error) { - console.error('usePrism: an error occurred while loading Prism plugins.'); - console.error(error); - } - } -}; - -/** - * Use Prism and its plugins. - * - * @param {UsePrismProps} props - An object of options. - * @returns {UsePrismReturn} An object of data. - */ -export const usePrism = ({ - language, - plugins, -}: UsePrismProps): UsePrismReturn => { - /** - * The order matter. Toolbar must be loaded before some other plugins. - */ - const defaultPlugins: DefaultPrismPlugin[] = useMemo( - () => [ - 'toolbar', - 'autoloader', - 'show-language', - 'copy-to-clipboard', - 'color-scheme', - 'match-braces', - 'normalize-whitespace', - ], - [] - ); - - useEffect(() => { - loadPrismPlugins([...defaultPlugins, ...plugins]).then(() => { - Prism.highlightAll(); - }); - }, [defaultPlugins, plugins]); - - const defaultClassName = 'match-braces'; - const languageClassName = language ? `language-${language}` : ''; - const pluginsClassName = plugins.join(' '); - const className = `${defaultClassName} ${pluginsClassName} ${languageClassName}`; - - const intl = useIntl(); - const copyText = intl.formatMessage({ - defaultMessage: 'Copy', - description: 'usePrism: copy button text (not clicked)', - id: '6GySNl', - }); - const copiedText = intl.formatMessage({ - defaultMessage: 'Copied!', - description: 'usePrism: copy button text (clicked)', - id: 'nsw6Th', - }); - const errorText = intl.formatMessage({ - defaultMessage: 'Use Ctrl+c to copy', - description: 'usePrism: copy button error text', - id: 'lKhTGM', - }); - const darkTheme = intl.formatMessage({ - defaultMessage: 'Dark Theme 🌙', - description: 'usePrism: toggle dark theme button text', - id: 'QLisK6', - }); - const lightTheme = intl.formatMessage({ - defaultMessage: 'Light Theme 🌞', - description: 'usePrism: toggle light theme button text', - id: 'hHVgW3', - }); - const attributes = { - 'data-prismjs-copy': copyText, - 'data-prismjs-copy-success': copiedText, - 'data-prismjs-copy-error': errorText, - 'data-prismjs-color-scheme-dark': darkTheme, - 'data-prismjs-color-scheme-light': lightTheme, - }; - - return { - attributes, - className, - }; -}; diff --git a/src/utils/hooks/use-prism/index.ts b/src/utils/hooks/use-prism/index.ts new file mode 100644 index 0000000..751cde7 --- /dev/null +++ b/src/utils/hooks/use-prism/index.ts @@ -0,0 +1 @@ +export * from './use-prism'; diff --git a/src/utils/hooks/use-prism/use-prism.test.tsx b/src/utils/hooks/use-prism/use-prism.test.tsx new file mode 100644 index 0000000..f7e83a2 --- /dev/null +++ b/src/utils/hooks/use-prism/use-prism.test.tsx @@ -0,0 +1,91 @@ +import { describe, expect, it } from '@jest/globals'; +import { renderHook } from '@testing-library/react'; +import type { PropsWithChildren, ReactElement } from 'react'; +import { type IntlConfig, IntlProvider } from 'react-intl'; +import { + type PrismLanguage, + usePrism, + type PrismAvailablePlugin, + type PrismAttributes, + type PrismToolbarAttributes, +} from './use-prism'; + +type WrapperProps = { + children: ReactElement<unknown>; +}; + +const createWrapper = ( + Wrapper: typeof IntlProvider, + props: PropsWithChildren<IntlConfig> +) => + function CreatedWrapper({ children }: WrapperProps) { + return <Wrapper {...props}>{children}</Wrapper>; + }; + +const toolbarAttributes: PrismToolbarAttributes = { + 'data-prismjs-color-scheme-dark': 'Dark Theme 🌙', + 'data-prismjs-color-scheme-light': 'Light Theme 🌞', + 'data-prismjs-copy': 'Copy', + 'data-prismjs-copy-error': 'Use Ctrl+c to copy', + 'data-prismjs-copy-success': 'Copied!', +}; + +describe('usePrism', () => { + it('returns the className and the attributes', () => { + const { result } = renderHook(() => usePrism({}), { + wrapper: createWrapper(IntlProvider, { locale: 'en' }), + }); + + expect(result.current.className).toStrictEqual(''); + expect(result.current.attributes).toStrictEqual(toolbarAttributes); + }); + + it('can return a className based on the given language', () => { + const language: PrismLanguage = 'docker'; + const { result } = renderHook(() => usePrism({ language }), { + wrapper: createWrapper(IntlProvider, { locale: 'en' }), + }); + + expect(result.current.className).toStrictEqual(`language-${language}`); + }); + + it('can return a className based on the given plugins', () => { + const pluginWithClass: PrismAvailablePlugin = 'diff-highlight'; + const { result } = renderHook( + () => usePrism({ plugins: [pluginWithClass] }), + { + wrapper: createWrapper(IntlProvider, { locale: 'en' }), + } + ); + + expect(result.current.className).toMatch(pluginWithClass); + }); + + it('can return a className based on the given language and plugins', () => { + const language: PrismLanguage = 'javascript'; + const pluginWithClass: PrismAvailablePlugin = 'diff-highlight'; + const { result } = renderHook( + () => usePrism({ language, plugins: [pluginWithClass] }), + { + wrapper: createWrapper(IntlProvider, { locale: 'en' }), + } + ); + + expect(result.current.className).toMatch(`language-diff-${language}`); + expect(result.current.className).toMatch(pluginWithClass); + }); + + it('can return the default attributes with the given owns', () => { + const attributes: Partial<PrismAttributes> = { + 'data-filter-output': '(out)', + }; + const { result } = renderHook(() => usePrism({ attributes }), { + wrapper: createWrapper(IntlProvider, { locale: 'en' }), + }); + + expect(result.current.attributes).toStrictEqual({ + ...toolbarAttributes, + ...attributes, + }); + }); +}); diff --git a/src/utils/hooks/use-prism/use-prism.ts b/src/utils/hooks/use-prism/use-prism.ts new file mode 100644 index 0000000..7f8330b --- /dev/null +++ b/src/utils/hooks/use-prism/use-prism.ts @@ -0,0 +1,217 @@ +import Prism from 'prismjs'; +import { useEffect } from 'react'; +import { useIntl } from 'react-intl'; + +export type PrismToolbarAttributes = { + 'data-prismjs-copy': string; + 'data-prismjs-copy-success': string; + 'data-prismjs-copy-error': string; + 'data-prismjs-color-scheme-dark': string; + 'data-prismjs-color-scheme-light': string; +}; + +export type PrismAttributes = PrismToolbarAttributes & { + 'data-continuation-prompt'?: string; + 'data-continuation-str'?: string; + 'data-filter-output'?: string; + 'data-filter-continuation'?: string; + 'data-host'?: string; + 'data-line'?: string; + 'data-prompt'?: string; + 'data-output'?: string; + 'data-start'?: string; + 'data-toolbar-order'?: string; + 'data-user'?: string; +}; + +export type PrismLanguage = + | 'apacheconf' + | 'bash' + | 'css' + | 'diff' + | 'docker' + | 'editorconfig' + | 'ejs' + | 'git' + | 'graphql' + | 'html' + | 'ignore' + | 'ini' + | 'javascript' + | 'jsdoc' + | 'json' + | 'jsx' + | 'makefile' + | 'markup' + | 'php' + | 'phpdoc' + | 'regex' + | 'scss' + | 'shell-session' + | 'smarty' + | 'tcl' + | 'toml' + | 'treeview' + | 'tsx' + | 'twig' + | 'yaml'; + +export type PrismAvailablePlugin = + | 'autoloader' + | 'color-scheme' + | 'command-line' + | 'copy-to-clipboard' + | 'diff-highlight' + | 'inline-color' + | 'line-highlight' + | 'line-numbers' + | 'match-braces' + | 'normalize-whitespace' + | 'show-language' + | 'toolbar' + | 'treeview'; + +type PrismPlugin = { + name: PrismAvailablePlugin; + hasClassName: boolean; +}; + +const prismPlugins: PrismPlugin[] = [ + { name: 'toolbar', hasClassName: false }, + { name: 'autoloader', hasClassName: false }, + { name: 'show-language', hasClassName: false }, + { name: 'color-scheme', hasClassName: false }, + { name: 'copy-to-clipboard', hasClassName: false }, + { name: 'command-line', hasClassName: true }, + { name: 'diff-highlight', hasClassName: true }, + { name: 'inline-color', hasClassName: false }, + { name: 'line-highlight', hasClassName: false }, + { name: 'line-numbers', hasClassName: true }, + { name: 'match-braces', hasClassName: true }, + { name: 'normalize-whitespace', hasClassName: false }, + { name: 'treeview', hasClassName: false }, +]; + +/** + * Reorder the given plugins. + * + * The toolbar plugin must be loaded before some other plugins, so we need to + * ensure it is at the beginning of the array. + * + * @param {PrismAvailablePlugin[]} plugins - An array of Prism plugins. + * @returns {PrismAvailablePlugin[]} The sorted plugins. + */ +const sortPlugins = ( + plugins: PrismAvailablePlugin[] +): PrismAvailablePlugin[] => { + if (!plugins.includes('toolbar')) return plugins; + + const remainingPlugins = plugins.filter((plugin) => plugin !== 'toolbar'); + + return ['toolbar', ...remainingPlugins]; +}; + +/** + * Import and configure all given Prism plugins. + * + * @param {PrismAvailablePlugin[]} plugins - The plugins to activate. + */ +const loadPrismPlugins = async (plugins: PrismAvailablePlugin[]) => { + if (!plugins.length) return; + + const orderedPlugins = sortPlugins(plugins); + + try { + const importPromises = orderedPlugins.map(async (plugin) => { + if (plugin === 'color-scheme') { + return import('../../plugins/prism-color-scheme.cjs'); + } + + return import(`prismjs/plugins/${plugin}/prism-${plugin}.min.js`); + }); + + await importPromises.reduce(async (currImport, nextImport) => + currImport.then(await nextImport) + ); + + if (orderedPlugins.includes('autoloader')) + // cSpell:ignore camelcase + // eslint-disable-next-line camelcase -- Case is coming from Prism + Prism.plugins.autoloader.languages_path = '/prism/'; + } catch (error) { + console.error('usePrism: an error occurred while loading Prism plugins.'); + console.error(error); + } +}; + +export type UsePrismProps = { + attributes?: Omit<PrismAttributes, keyof PrismToolbarAttributes>; + language?: PrismLanguage; + plugins?: PrismAvailablePlugin[]; +}; + +/** + * Use Prism and its plugins. + * + * @param {UsePrismProps} props - An object of options. + * @returns An object with attributes and className. + */ +export const usePrism = ({ attributes, language, plugins }: UsePrismProps) => { + const intl = useIntl(); + const pluginsToLoad = prismPlugins.filter( + (plugin) => plugins?.includes(plugin.name) + ); + + const pluginClasses = pluginsToLoad + .map((plugin) => { + if (plugin.hasClassName) return plugin.name; + return undefined; + }) + .filter((maybeStr): maybeStr is PrismAvailablePlugin => !!maybeStr); + + const diffClass = language ? `language-diff-${language}` : 'language-diff'; + const languageClass = plugins?.includes('diff-highlight') + ? diffClass + : `language-${language}`; + + const className = [language ? languageClass : '', ...pluginClasses].join(' '); + + const toolbarAttributes: PrismToolbarAttributes = { + 'data-prismjs-color-scheme-dark': intl.formatMessage({ + defaultMessage: 'Dark Theme 🌙', + description: 'usePrism: toggle dark theme button text', + id: 'QLisK6', + }), + 'data-prismjs-color-scheme-light': intl.formatMessage({ + defaultMessage: 'Light Theme 🌞', + description: 'usePrism: toggle light theme button text', + id: 'hHVgW3', + }), + 'data-prismjs-copy': intl.formatMessage({ + defaultMessage: 'Copy', + description: 'usePrism: copy button text (not clicked)', + id: '6GySNl', + }), + 'data-prismjs-copy-error': intl.formatMessage({ + defaultMessage: 'Use Ctrl+c to copy', + description: 'usePrism: copy button error text', + id: 'lKhTGM', + }), + 'data-prismjs-copy-success': intl.formatMessage({ + defaultMessage: 'Copied!', + description: 'usePrism: copy button text (clicked)', + id: 'nsw6Th', + }), + }; + + useEffect(() => { + loadPrismPlugins(pluginsToLoad.map((plugin) => plugin.name)).then(() => { + Prism.highlightAll(); + }); + }, [pluginsToLoad]); + + return { + attributes: { ...toolbarAttributes, ...attributes }, + className, + }; +}; diff --git a/src/utils/plugins/prism-color-scheme.js b/src/utils/plugins/prism-color-scheme.cjs index 2632dd3..2632dd3 100644 --- a/src/utils/plugins/prism-color-scheme.js +++ b/src/utils/plugins/prism-color-scheme.cjs |
