aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-13 19:32:56 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:14:41 +0100
commit006b15b467a5cd835a6eab1b49023100bdc8f2e6 (patch)
tree949c7295c2e206f42357f135bab4696ddf6576ec /src/utils
parent00f147a7a687d5772bcc538bc606cfff972178cd (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.tsx183
-rw-r--r--src/utils/hooks/use-prism/index.ts1
-rw-r--r--src/utils/hooks/use-prism/use-prism.test.tsx91
-rw-r--r--src/utils/hooks/use-prism/use-prism.ts217
-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