aboutsummaryrefslogtreecommitdiffstats
path: root/src
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
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')
-rw-r--r--src/components/atoms/figure/figure.module.scss4
-rw-r--r--src/components/molecules/code/code.module.scss13
-rw-r--r--src/components/molecules/code/code.stories.tsx192
-rw-r--r--src/components/molecules/code/code.test.tsx (renamed from src/components/molecules/layout/code.test.tsx)15
-rw-r--r--src/components/molecules/code/code.tsx183
-rw-r--r--src/components/molecules/code/index.ts1
-rw-r--r--src/components/molecules/index.ts1
-rw-r--r--src/components/molecules/layout/code.module.scss305
-rw-r--r--src/components/molecules/layout/code.stories.tsx121
-rw-r--r--src/components/molecules/layout/code.tsx69
-rw-r--r--src/components/molecules/layout/index.ts1
m---------src/content0
-rw-r--r--src/pages/article/[slug].tsx28
-rw-r--r--src/styles/abstracts/_placeholders.scss1
-rw-r--r--src/styles/abstracts/placeholders/_prism.scss (renamed from src/styles/pages/partials/_article-prism.scss)25
-rw-r--r--src/styles/pages/article.module.scss3
-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
21 files changed, 744 insertions, 710 deletions
diff --git a/src/components/atoms/figure/figure.module.scss b/src/components/atoms/figure/figure.module.scss
index e7ba5c2..7722e59 100644
--- a/src/components/atoms/figure/figure.module.scss
+++ b/src/components/atoms/figure/figure.module.scss
@@ -7,15 +7,13 @@
border: fun.convert-px(1) solid var(--color-border-light);
font-size: var(--font-size-sm);
font-weight: 500;
+ text-align: center;
}
.wrapper {
- display: flex;
- flex-flow: column;
width: fit-content;
margin: 0 auto;
position: relative;
- text-align: center;
&--has-borders {
padding: fun.convert-px(4);
diff --git a/src/components/molecules/code/code.module.scss b/src/components/molecules/code/code.module.scss
new file mode 100644
index 0000000..b551040
--- /dev/null
+++ b/src/components/molecules/code/code.module.scss
@@ -0,0 +1,13 @@
+@use "../../../styles/abstracts/placeholders";
+
+.wrapper {
+ width: 100%;
+
+ :global {
+ @extend %prism;
+ }
+
+ figcaption {
+ margin-top: calc(var(--spacing-sm) * -1);
+ }
+}
diff --git a/src/components/molecules/code/code.stories.tsx b/src/components/molecules/code/code.stories.tsx
new file mode 100644
index 0000000..1127839
--- /dev/null
+++ b/src/components/molecules/code/code.stories.tsx
@@ -0,0 +1,192 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Code } from './code';
+
+/**
+ * Code - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Code',
+ component: Code,
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The code sample.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ filterPattern: {
+ control: {
+ type: 'text',
+ },
+ description: 'Define a pattern to filter the command line output.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ language: {
+ control: {
+ type: 'text',
+ },
+ description: 'The code sample language.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ plugins: {
+ description: 'An array of Prism plugins to activate.',
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ },
+} as ComponentMeta<typeof Code>;
+
+const Template: ComponentStory<typeof Code> = (args) => <Code {...args} />;
+
+const javascriptCodeSample = `
+const foo = () => {
+ return 'bar';
+}
+`;
+
+/**
+ * Code Stories - Code sample
+ */
+export const CodeSample = Template.bind({});
+CodeSample.args = {
+ children: javascriptCodeSample,
+ language: 'javascript',
+};
+
+/**
+ * Code Stories - Highlighting
+ *
+ * @todo Find a way to make it working: line-highlight plugin is not loaded.
+ */
+export const Highlighting = Template.bind({});
+Highlighting.args = {
+ children: javascriptCodeSample,
+ highlight: '3',
+ language: 'javascript',
+};
+
+// cSpell:ignore xinitrc
+const commandLineCode = `
+ls -lah
+#output#drwxr-x---+ 42 armand armand 4,0K 17 april 11:15 .
+#output#drwxr-xr-x 4 root root 4,0K 30 mai 2021 ..
+#output#-rw-r--r-- 1 armand armand 2,0K 21 jul. 2021 .xinitrc
+`;
+
+/**
+ * Code Stories - Command Line
+ */
+export const CommandLine = Template.bind({});
+CommandLine.args = {
+ children: commandLineCode,
+ cmdOutputFilter: '#output#',
+ isCmd: true,
+ language: 'bash',
+};
+
+// cSpell:ignore lcov
+const treeSample = `
+.
+├── bin
+│ └── deploy.sh
+├── CHANGELOG.md
+├── commitlint.config.cjs
+├── coverage
+│ ├── clover.xml
+│ ├── coverage-final.json
+│ ├── lcov-report
+│ └── lcov.info
+├── cspell.json
+├── cypress.config.js
+├── docker-compose.yml
+├── Dockerfile
+├── jest.config.js
+├── jest.setup.js
+├── lang
+│ ├── en.json
+│ └── fr.json
+├── LICENSE
+├── lint-staged.config.js
+├── mdx.d.ts
+├── next-env.d.ts
+├── next-sitemap.config.cjs
+├── next.config.js
+├── package.json
+├── public
+│ ├── apple-touch-icon.png
+│ ├── armand-philippot.jpg
+│ ├── favicon.ico
+│ ├── icon-192.png
+│ ├── icon-512.png
+│ ├── icon.svg
+│ ├── manifest.webmanifest
+│ ├── prism
+│ ├── projects
+│ ├── robots.txt
+│ ├── sitemap-0.xml
+│ ├── sitemap.xml
+│ └── vercel.svg
+├── README.md
+├── src
+│ ├── assets
+│ ├── components
+│ ├── content
+│ ├── i18n
+│ ├── pages
+│ ├── services
+│ ├── styles
+│ ├── types
+│ └── utils
+├── tests
+│ ├── cypress
+│ ├── jest
+│ └── utils
+├── tsconfig.eslint.json
+├── tsconfig.json
+├── tsconfig.tsbuildinfo
+└── yarn.lock`;
+
+/**
+ * Code Stories - Tree view
+ *
+ * @todo Find a way to make it working: treeview plugin is not loaded.
+ */
+export const TreeView = Template.bind({});
+TreeView.args = {
+ children: treeSample,
+ language: 'treeview',
+};
+
+const diffSample = `
+--- file1.js 2023-10-13 19:17:05.540644084 +0200
++++ file2.js 2023-10-13 19:17:15.310564281 +0200
+@@ -1,2 +1 @@
+-let foo = bar.baz([1, 2, 3]);
+-foo = foo + 1;
++const foo = bar.baz([1, 2, 3]) + 1;`;
+
+/**
+ * Code Stories - Diff
+ */
+export const Diff = Template.bind({});
+Diff.args = {
+ children: diffSample,
+ isDiff: true,
+ language: 'diff',
+};
diff --git a/src/components/molecules/layout/code.test.tsx b/src/components/molecules/code/code.test.tsx
index a0e4143..5b946b3 100644
--- a/src/components/molecules/layout/code.test.tsx
+++ b/src/components/molecules/code/code.test.tsx
@@ -1,17 +1,14 @@
import { describe, expect, it } from '@jest/globals';
-import { render } from '../../../../tests/utils';
+import { render, screen as rtlScreen } from '../../../../tests/utils';
import { Code } from './code';
-const code = `
-function foo() {
- return 'bar';
-}
-`;
-
-const language = 'javascript';
-
describe('Code', () => {
it('renders a code block', () => {
+ const language = 'javascript';
+ const code = 'nam';
+
render(<Code language={language}>{code}</Code>);
+
+ expect(rtlScreen.getByText(code)).toBeInTheDocument();
});
});
diff --git a/src/components/molecules/code/code.tsx b/src/components/molecules/code/code.tsx
new file mode 100644
index 0000000..0168fd6
--- /dev/null
+++ b/src/components/molecules/code/code.tsx
@@ -0,0 +1,183 @@
+import { forwardRef, type ForwardRefRenderFunction } from 'react';
+import {
+ usePrism,
+ type PrismLanguage,
+ type PrismAvailablePlugin,
+} from '../../../utils/hooks';
+import { Figure, type FigureProps } from '../../atoms';
+import styles from './code.module.scss';
+
+export type CodeProps = Omit<FigureProps, 'children'> & {
+ /**
+ * The code to highlight.
+ */
+ children: string;
+ /**
+ * Define a pattern to automatically present some lines as continuation lines
+ * when using command line.
+ *
+ * @default undefined
+ */
+ cmdContinuationFilter?: string;
+ /**
+ * Define the prompt to be displayed when the command has continued beyond
+ * the first line. Only used with command line.
+ *
+ * @default '>'
+ */
+ cmdContinuationPrompt?: string;
+ /**
+ * Define the line continuation string or character when using command line.
+ */
+ cmdContinuationStr?: string;
+ /**
+ * Define the host when using command line.
+ */
+ cmdHost?: string;
+ /**
+ * Define a custom prompt when using command line. By default, `#` will be
+ * used for the root user and `$` for all other users.
+ */
+ cmdPrompt?: string;
+ /**
+ * Define the line(s) that must be presented as output when using command
+ * line.
+ *
+ * @example '6' // a single line
+ * @example '2-7' // a range
+ * @example '3,9-11' // multiple lines with a range
+ *
+ * @default undefined
+ */
+ cmdOutput?: string;
+ /**
+ * Define a pattern to automatically present some lines as output when using
+ * command line.
+ *
+ * @default undefined
+ */
+ cmdOutputFilter?: string;
+ /**
+ * Specify the user when using command line.
+ */
+ cmdUser?: string;
+ /**
+ * Define the line(s) that must be highlighted.
+ *
+ * DON'T USE: it seems the plugin is not correctly loaded.
+ *
+ * @example '6' // a single line
+ * @example '2-7' // a range
+ * @example '3,9-11' // multiple lines with a range
+ *
+ * @default undefined
+ */
+ highlight?: string;
+ /**
+ * Should the code be treated as command lines?
+ *
+ * @default false
+ */
+ isCmd?: boolean;
+ /**
+ * Should the code be treated as a diff block?
+ *
+ * @default false
+ */
+ isDiff?: boolean;
+ /**
+ * The code language.
+ */
+ language: PrismLanguage;
+ /**
+ * Define the starting line number. It will be ignored with command lines.
+ *
+ * @default undefined // Starts with 1.
+ */
+ start?: string;
+};
+
+const CodeWithRef: ForwardRefRenderFunction<HTMLElement, CodeProps> = (
+ {
+ children,
+ className = '',
+ cmdContinuationFilter,
+ cmdContinuationPrompt,
+ cmdContinuationStr,
+ cmdHost,
+ cmdOutput,
+ cmdOutputFilter,
+ cmdPrompt,
+ cmdUser,
+ highlight,
+ isCmd = false,
+ isDiff = false,
+ language,
+ start,
+ ...props
+ },
+ ref
+) => {
+ const wrapperClass = `${styles.wrapper} ${className}`;
+ const codeClass = isDiff
+ ? `language-diff-${language}`
+ : `language-${language}`;
+ const plugins: PrismAvailablePlugin[] = [
+ 'toolbar',
+ 'autoloader',
+ 'show-language',
+ 'color-scheme',
+ 'copy-to-clipboard',
+ 'inline-color',
+ 'match-braces',
+ 'normalize-whitespace',
+ ];
+
+ if (isDiff || language === 'diff') plugins.push('diff-highlight');
+
+ if (language.endsWith('treeview')) plugins.push('treeview');
+ else plugins.push(isCmd ? 'command-line' : 'line-numbers');
+
+ const { attributes: prismAttributes, className: prismClass } = usePrism({
+ attributes: {
+ 'data-continuation-prompt': cmdContinuationPrompt,
+ 'data-continuation-str': cmdContinuationStr,
+ 'data-filter-continuation': cmdContinuationFilter,
+ 'data-filter-output': cmdOutputFilter,
+ 'data-host': cmdHost,
+ 'data-line': highlight,
+ 'data-output': cmdOutput,
+ 'data-prompt': cmdPrompt,
+ 'data-start': start,
+ 'data-toolbar-order': 'show-language,copy-to-clipboard,color-scheme',
+ 'data-user': cmdUser,
+ },
+ language,
+ plugins,
+ });
+
+ return (
+ <Figure {...props} className={wrapperClass} ref={ref}>
+ <pre
+ {...prismAttributes}
+ className={prismClass}
+ // cSpell:ignore noninteractive
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
+ tabIndex={0}
+ >
+ <code className={codeClass}>{children}</code>
+ </pre>
+ </Figure>
+ );
+};
+
+/**
+ * Code component
+ *
+ * Render a code block with syntax highlighting.
+ *
+ * @todo Find a way to load Prism plugins without Babel (Next uses SWC). It
+ * seems some plugins are not loaded correctly (`line-highlight` or `treeview`
+ * for example).
+ */
+export const Code = forwardRef(CodeWithRef);
diff --git a/src/components/molecules/code/index.ts b/src/components/molecules/code/index.ts
new file mode 100644
index 0000000..d18a4e0
--- /dev/null
+++ b/src/components/molecules/code/index.ts
@@ -0,0 +1 @@
+export * from './code';
diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts
index cb0b7eb..a1e2c7a 100644
--- a/src/components/molecules/index.ts
+++ b/src/components/molecules/index.ts
@@ -1,5 +1,6 @@
export * from './branding';
export * from './buttons';
+export * from './code';
export * from './collapsible';
export * from './forms';
export * from './images';
diff --git a/src/components/molecules/layout/code.module.scss b/src/components/molecules/layout/code.module.scss
deleted file mode 100644
index 2eaf9a2..0000000
--- a/src/components/molecules/layout/code.module.scss
+++ /dev/null
@@ -1,305 +0,0 @@
-@use "../../../styles/abstracts/functions" as fun;
-@use "../../../styles/abstracts/mixins" as mix;
-
-.wrapper {
- :global {
- .code-toolbar {
- --toolbar-height: #{fun.convert-px(100)};
-
- position: relative;
- margin-top: calc(var(--toolbar-height) + var(--spacing-sm));
-
- @include mix.media("screen") {
- @include mix.dimensions("2xs") {
- --toolbar-height: #{fun.convert-px(60)};
- }
- }
-
- .toolbar {
- display: flex;
- flex-flow: row wrap;
- justify-content: center;
- width: 100%;
- height: var(--toolbar-height);
- position: absolute;
- top: calc(var(--toolbar-height) * -1);
- left: 0;
- right: 0;
- background: var(--color-bg-tertiary);
- border: fun.convert-px(1) solid var(--color-border);
- }
-
- .toolbar-item {
- display: flex;
- align-items: center;
- margin: 0 var(--spacing-2xs);
- }
-
- .toolbar-item:nth-child(1) {
- flex: 0 0 100%;
- justify-content: center;
- margin: 0 auto 0 0;
- padding: 0 var(--spacing-sm);
- background: var(--color-bg-code);
- border-bottom: fun.convert-px(1) solid var(--color-border);
- color: var(--color-primary-darker);
- font-size: var(--font-size-sm);
- font-weight: 600;
-
- @include mix.media("screen") {
- @include mix.dimensions("2xs") {
- flex: 0 0 auto;
- justify-content: left;
- border-bottom: none;
- border-right: fun.convert-px(1) solid var(--color-border);
- }
- }
- }
- }
-
- .copy-to-clipboard-button,
- .prism-color-scheme-button {
- display: block;
- padding: fun.convert-px(3) var(--spacing-xs);
- background: var(--color-bg);
- border: 0.4ex solid var(--color-primary);
- border-radius: fun.convert-px(30);
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
- var(--color-shadow),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
- var(--color-shadow);
- color: var(--color-primary);
- font-size: var(--font-size-sm);
- font-weight: 600;
- transition: all 0.35s ease-in-out 0s;
-
- &:hover,
- &:focus {
- transform: translateX(#{fun.convert-px(-2)})
- translateY(#{fun.convert-px(-2)});
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow-light),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2)
- fun.convert-px(-2) var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5)
- fun.convert-px(-4) var(--color-shadow-light),
- fun.convert-px(4) fun.convert-px(7) fun.convert-px(8)
- fun.convert-px(-3) var(--color-shadow-light);
- }
-
- &:focus {
- text-decoration: underline var(--color-primary) fun.convert-px(3);
- }
-
- &:active {
- text-decoration: none;
- transform: translateY(#{fun.convert-px(2)});
- box-shadow: 0 0 0 0 var(--color-shadow);
- }
- }
-
- pre[class*="language-"] {
- --gutter-size-with-spacing: calc(var(--gutter-size) + var(--spacing-xs));
-
- position: relative;
- overflow: auto;
- background: var(--color-bg-secondary);
- border: fun.convert-px(1) solid var(--color-border-light);
- color: var(--color-fg);
- hyphens: none;
- tab-size: 4;
- text-align: left;
- white-space: pre;
- word-spacing: normal;
- word-break: normal;
- word-wrap: normal;
-
- &.command-line {
- --gutter-size: 19ch;
- padding-left: var(--gutter-size-with-spacing);
- }
-
- &.line-numbers {
- --gutter-size: 6ch;
-
- counter-reset: lineNumber;
- padding-left: var(--gutter-size-with-spacing);
- }
-
- code {
- display: block;
- padding: var(--spacing-xs) 0;
- position: relative;
- }
-
- .line-numbers-rows,
- .command-line-prompt {
- display: block;
- width: var(--gutter-size);
- padding: var(--spacing-xs) 0;
- position: absolute;
- top: 0;
- left: calc(var(--gutter-size-with-spacing) * -1);
- background: var(--color-bg);
- border-right: fun.convert-px(1) solid var(--color-border);
- font-size: 100%;
- letter-spacing: -1px;
- text-align: right;
- pointer-events: none;
- user-select: none;
-
- > span {
- &::before {
- display: block;
- padding-right: var(--spacing-xs);
- color: var(--color-fg-light);
- }
- }
- }
-
- .command-line-prompt {
- > span {
- &::before {
- content: " ";
- }
-
- &[data-user]::before {
- content: "[" attr(data-user) "@" attr(data-host) "] $";
- }
-
- &[data-user="root"]::before {
- content: "[" attr(data-user) "@" attr(data-host) "] #";
- }
-
- &[data-prompt]::before {
- content: attr(data-prompt);
- }
-
- &[data-continuation-prompt]::before {
- content: attr(data-continuation-prompt);
- }
- }
- }
-
- .line-numbers-rows {
- > span {
- counter-increment: lineNumber;
-
- &::before {
- content: counter(lineNumber);
- }
- }
- }
-
- .token {
- &.comment,
- &.doc-comment {
- color: var(--color-fg-light);
- }
-
- &.punctuation {
- color: var(--color-fg);
- }
-
- &.attr-name,
- &.hexcode,
- &.inserted,
- &.string {
- color: var(--color-token-green);
- }
-
- &.class,
- &.coord,
- &.id,
- &.function {
- color: var(--color-token-purple);
- }
-
- &.builtin,
- &.builtin.class-name,
- &.property-access,
- &.regex,
- &.scope {
- color: var(--color-token-magenta);
- }
-
- &.class-name,
- &.constant,
- &.global,
- &.interpolation,
- &.key,
- &.package,
- &.this,
- &.title,
- &.variable {
- color: var(--color-token-blue);
- }
-
- &.combinator,
- &.keyword,
- &.operator,
- &.pseudo-class,
- &.pseudo-element,
- &.rule,
- &.selector,
- &.unit {
- color: var(--color-token-orange);
- }
-
- &.attr-value,
- &.boolean,
- &.number {
- color: var(--color-token-yellow);
- }
-
- &.delimiter,
- &.doctype,
- &.parameter,
- &.parent,
- &.property,
- &.shebang,
- &.tag {
- color: var(--color-token-cyan);
- }
-
- &.deleted {
- color: var(--color-token-red);
- }
-
- &.punctuation.brace-hover,
- &.punctuation.brace-selected {
- background: var(--color-bg);
- outline: solid fun.convert-px(1) var(--color-primary-light);
- }
- }
-
- span.inline-color-wrapper {
- background: url(fun.encode-svg(
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="gray" d="M0 0h2v2H0z"/><path fill="white" d="M0 0h1v1H0zM1 1h1v1H1z"/></svg>'
- ));
-
- // Prevent repeating pattern to be seen.
- background-position: center;
- background-size: 110%;
-
- display: inline-block;
- height: 1.1ch;
- width: 1.1ch;
- margin: 0 0.5ch 0 0;
- border: fun.convert-px(1) solid var(--color-bg);
- outline: fun.convert-px(1) solid var(--color-border-dark);
- overflow: hidden;
- }
-
- span.inline-color {
- display: block;
-
- /* To prevent visual glitches again */
- height: 120%;
- width: 120%;
- }
- }
- }
-}
diff --git a/src/components/molecules/layout/code.stories.tsx b/src/components/molecules/layout/code.stories.tsx
deleted file mode 100644
index d20cdbe..0000000
--- a/src/components/molecules/layout/code.stories.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { Code } from './code';
-
-/**
- * Code - Storybook Meta
- */
-export default {
- title: 'Molecules/Layout/Code',
- component: Code,
- args: {
- filterOutput: false,
- outputPattern: '#output#',
- },
- argTypes: {
- 'aria-label': {
- control: {
- type: 'text',
- },
- description: 'An accessible name for the code sample.',
- table: {
- category: 'Accessibility',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- children: {
- control: {
- type: 'text',
- },
- description: 'The code sample.',
- type: {
- name: 'string',
- required: true,
- },
- },
- filterOutput: {
- control: {
- type: 'boolean',
- },
- description: 'Filter the command line output.',
- table: {
- category: 'Options',
- defaultValue: { summary: false },
- },
- type: {
- name: 'boolean',
- required: false,
- },
- },
- language: {
- control: {
- type: 'text',
- },
- description: 'The code sample language.',
- type: {
- name: 'string',
- required: true,
- },
- },
- plugins: {
- description: 'An array of Prism plugins to activate.',
- type: {
- name: 'object',
- required: false,
- value: {},
- },
- },
- outputPattern: {
- control: {
- type: 'text',
- },
- description: 'The command line output pattern.',
- table: {
- category: 'Options',
- defaultValue: { summary: '#output#' },
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- },
-} as ComponentMeta<typeof Code>;
-
-const Template: ComponentStory<typeof Code> = (args) => <Code {...args} />;
-
-const javascriptCodeSample = `
-const foo = () => {
- return 'bar';
-}
-`;
-
-/**
- * Code Stories - Code sample
- */
-export const CodeSample = Template.bind({});
-CodeSample.args = {
- children: javascriptCodeSample,
- language: 'javascript',
- plugins: ['line-numbers'],
-};
-
-const commandLineCode = `
-ls -lah
-#output#drwxr-x---+ 42 armand armand 4,0K 17 avril 11:15 .
-#output#drwxr-xr-x 4 root root 4,0K 30 mai 2021 ..
-#output#-rw-r--r-- 1 armand armand 2,0K 21 juil. 2021 .xinitrc
-`;
-
-/**
- * Code Stories - Command Line
- */
-export const CommandLine = Template.bind({});
-CommandLine.args = {
- children: commandLineCode,
- filterOutput: true,
- language: 'bash',
- plugins: ['command-line'],
-};
diff --git a/src/components/molecules/layout/code.tsx b/src/components/molecules/layout/code.tsx
deleted file mode 100644
index a1aadd8..0000000
--- a/src/components/molecules/layout/code.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { FC, useRef } from 'react';
-import {
- type OptionalPrismPlugin,
- type PrismLanguage,
- usePrism,
-} from '../../../utils/hooks';
-import styles from './code.module.scss';
-
-export type CodeProps = {
- /**
- * An accessible name.
- */
- 'aria-label'?: string;
- /**
- * The code to highlight.
- */
- children: string;
- /**
- * Filter command line output. Default: false.
- */
- filterOutput?: boolean;
- /**
- * The code language.
- */
- language: PrismLanguage;
- /**
- * The optional Prism plugins.
- */
- plugins?: OptionalPrismPlugin[];
- /**
- * Filter command line output using the given string. Default: #output#
- */
- outputPattern?: string;
-};
-
-/**
- * Code component
- *
- * Render a code block with syntax highlighting.
- */
-export const Code: FC<CodeProps> = ({
- children,
- filterOutput = false,
- language,
- plugins = [],
- outputPattern = '#output#',
- ...props
-}) => {
- const wrapperRef = useRef<HTMLDivElement>(null);
- const { attributes, className } = usePrism({ language, plugins });
-
- const outputAttribute = filterOutput
- ? { 'data-filter-output': outputPattern }
- : {};
-
- return (
- <div className={styles.wrapper} ref={wrapperRef}>
- <pre
- {...props}
- {...attributes}
- {...outputAttribute}
- className={className}
- tabIndex={0}
- >
- <code className={`language-${language}`}>{children}</code>
- </pre>
- </div>
- );
-};
diff --git a/src/components/molecules/layout/index.ts b/src/components/molecules/layout/index.ts
index 58d5442..80db10a 100644
--- a/src/components/molecules/layout/index.ts
+++ b/src/components/molecules/layout/index.ts
@@ -1,5 +1,4 @@
export * from './card';
-export * from './code';
export * from './columns';
export * from './page-footer';
export * from './page-header';
diff --git a/src/content b/src/content
-Subproject 0a5267ca7df1b6600741aa172ffdfe7b4f762d9
+Subproject c6be8a1c511e5848a0317f825a29d07d09c4731
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index dea240f..d1e680c 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -33,7 +33,6 @@ import {
} from '../../utils/helpers';
import { loadTranslation, type Messages } from '../../utils/helpers/server';
import {
- type OptionalPrismPlugin,
useArticle,
useBreadcrumb,
useComments,
@@ -70,8 +69,23 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
});
const readingTime = useReadingTime(article?.meta.wordsCount ?? 0, true);
const { website } = useSettings();
- const prismPlugins: OptionalPrismPlugin[] = ['command-line', 'line-numbers'];
- const { attributes, className } = usePrism({ plugins: prismPlugins });
+ const { attributes, className } = usePrism({
+ attributes: {
+ 'data-toolbar-order': 'show-language,copy-to-clipboard,color-scheme',
+ },
+ plugins: [
+ 'toolbar',
+ 'autoloader',
+ 'show-language',
+ 'color-scheme',
+ 'copy-to-clipboard',
+ 'inline-color',
+ 'match-braces',
+ 'normalize-whitespace',
+ 'command-line',
+ 'line-numbers',
+ ],
+ });
const loadingArticle = intl.formatMessage({
defaultMessage: 'Loading the requested article...',
description: 'ArticlePage: loading article message',
@@ -231,10 +245,10 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
str.includes('command-line') ||
(!str.includes('command-line') && str.includes('language-bash'))
) {
- return `class="${wpBlockClassName} ${commandLineClassName}${languageClassName}" tabindex="0" data-filter-output="#output#`;
+ return `class="${wpBlockClassName} ${commandLineClassName} ${languageClassName}" tabindex="0" data-filter-output="#output#`;
}
- return `class="${wpBlockClassName} ${lineNumbersClassName}${languageClassName}" tabindex="0`;
+ return `class="${wpBlockClassName} ${lineNumbersClassName} ${languageClassName}" tabindex="0`;
};
const contentWithPrismClasses = content.replaceAll(
@@ -265,9 +279,7 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
/>
<PageLayout
allowComments={true}
- bodyAttributes={{
- ...(attributes as HTMLAttributes<HTMLDivElement>),
- }}
+ bodyAttributes={attributes as HTMLAttributes<HTMLDivElement>}
bodyClassName={styles.body}
breadcrumb={breadcrumbItems}
breadcrumbSchema={breadcrumbSchema}
diff --git a/src/styles/abstracts/_placeholders.scss b/src/styles/abstracts/_placeholders.scss
index 04522d7..4070730 100644
--- a/src/styles/abstracts/_placeholders.scss
+++ b/src/styles/abstracts/_placeholders.scss
@@ -5,3 +5,4 @@
@forward "./placeholders/layout";
@forward "./placeholders/links";
@forward "./placeholders/lists";
+@forward "./placeholders/prism";
diff --git a/src/styles/pages/partials/_article-prism.scss b/src/styles/abstracts/placeholders/_prism.scss
index 7d23e38..97f28b6 100644
--- a/src/styles/pages/partials/_article-prism.scss
+++ b/src/styles/abstracts/placeholders/_prism.scss
@@ -1,7 +1,7 @@
-@use "../../abstracts/functions" as fun;
-@use "../../abstracts/mixins" as mix;
+@use "../functions" as fun;
+@use "../mixins" as mix;
-@mixin styles {
+%prism {
.code-toolbar {
--toolbar-height: #{fun.convert-px(100)};
@@ -63,8 +63,8 @@
background: var(--color-bg);
border: 0.4ex solid var(--color-primary);
border-radius: fun.convert-px(30);
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow),
+ box-shadow:
+ fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) var(--color-shadow),
fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
var(--color-shadow),
fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
@@ -78,7 +78,8 @@
&:focus {
transform: translateX(#{fun.convert-px(-2)})
translateY(#{fun.convert-px(-2)});
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+ box-shadow:
+ fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
var(--color-shadow-light),
fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
var(--color-shadow-light),
@@ -105,7 +106,9 @@
padding: 0;
position: relative;
overflow: auto;
+ background: var(--color-bg-secondary);
border: fun.convert-px(1) solid var(--color-border-light);
+ color: var(--color-fg);
hyphens: none;
tab-size: 4;
text-align: left;
@@ -122,7 +125,7 @@
&.line-numbers {
--gutter-size: 6ch;
- counter-reset: lineNumber;
+ counter-reset: linenumber;
padding-left: var(--gutter-size-with-spacing);
}
@@ -183,10 +186,10 @@
.line-numbers-rows {
> span {
- counter-increment: lineNumber;
+ counter-increment: linenumber;
&::before {
- content: counter(lineNumber);
+ content: counter(linenumber);
}
}
}
@@ -271,6 +274,10 @@
background: var(--color-bg);
outline: solid fun.convert-px(1) var(--color-primary-light);
}
+
+ &.output {
+ user-select: none;
+ }
}
span.inline-color-wrapper {
diff --git a/src/styles/pages/article.module.scss b/src/styles/pages/article.module.scss
index d2e7822..5e7d520 100644
--- a/src/styles/pages/article.module.scss
+++ b/src/styles/pages/article.module.scss
@@ -5,7 +5,6 @@
@use "partials/article-links";
@use "partials/article-lists";
@use "partials/article-media";
-@use "partials/article-prism";
@use "partials/article-wp-blocks";
.btn {
@@ -23,8 +22,8 @@
@include article-links.styles;
@include article-lists.styles;
@include article-media.styles;
- @include article-prism.styles;
@include article-wp-blocks.styles;
+ @extend %prism;
}
}
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