aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules/code
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/molecules/code')
-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.tsx14
-rw-r--r--src/components/molecules/code/code.tsx183
-rw-r--r--src/components/molecules/code/index.ts1
5 files changed, 403 insertions, 0 deletions
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/code/code.test.tsx b/src/components/molecules/code/code.test.tsx
new file mode 100644
index 0000000..5b946b3
--- /dev/null
+++ b/src/components/molecules/code/code.test.tsx
@@ -0,0 +1,14 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '../../../../tests/utils';
+import { Code } from './code';
+
+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';