diff --git a/src/app/components/editor/Elements.tsx b/src/app/components/editor/Elements.tsx index 6a6659b9..675c4542 100644 --- a/src/app/components/editor/Elements.tsx +++ b/src/app/components/editor/Elements.tsx @@ -157,12 +157,12 @@ export function RenderElement({ attributes, element, children }: RenderElementPr -
{children}
+
{children}
); diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx index 04ebacd4..ba40c978 100644 --- a/src/app/plugins/react-custom-html-parser.tsx +++ b/src/app/plugins/react-custom-html-parser.tsx @@ -4,7 +4,6 @@ import React, { ReactEventHandler, Suspense, lazy, - useCallback, useMemo, useState, } from 'react'; @@ -17,7 +16,7 @@ import { } from 'html-react-parser'; import { MatrixClient } from 'matrix-js-sdk'; import classNames from 'classnames'; -import { Icon, IconButton, Icons, Scroll, Text } from 'folds'; +import { Box, Chip, config, Header, Icon, IconButton, Icons, Scroll, Text, toRem } from 'folds'; import { IntermediateRepresentation, Opts as LinkifyOpts, OptFn } from 'linkifyjs'; import Linkify from 'linkify-react'; import { ErrorBoundary } from 'react-error-boundary'; @@ -205,79 +204,108 @@ export const highlightText = ( ); }); -export function CodeBlock(children: ChildNode[], opts: HTMLReactParserOptions) { +/** + * Recursively extracts and concatenates all text content from an array of ChildNode objects. + * + * @param {ChildNode[]} nodes - An array of ChildNode objects to extract text from. + * @returns {string} The concatenated plain text content of all descendant text nodes. + */ +const extractTextFromChildren = (nodes: ChildNode[]): string => { + let text = ''; + + nodes.forEach((node) => { + if (node.type === 'text') { + text += node.data; + } else if (node instanceof Element && node.children) { + text += extractTextFromChildren(node.children); + } + }); + + return text; +}; + +export function CodeBlock({ + children, + opts, +}: { + children: ChildNode[]; + opts: HTMLReactParserOptions; +}) { + const code = children[0]; + const languageClass = + code instanceof Element && code.name === 'code' ? code.attribs.class : undefined; + const language = + languageClass && languageClass.startsWith('language-') + ? languageClass.replace('language-', '') + : languageClass; + const LINE_LIMIT = 14; - - /** - * Recursively extracts and concatenates all text content from an array of ChildNode objects. - * - * @param {ChildNode[]} nodes - An array of ChildNode objects to extract text from. - * @returns {string} The concatenated plain text content of all descendant text nodes. - */ - const extractTextFromChildren = useCallback((nodes: ChildNode[]): string => { - let text = ''; - - nodes.forEach((node) => { - if (node.type === 'text') { - text += node.data; - } else if (node instanceof Element && node.children) { - text += extractTextFromChildren(node.children); - } - }); - - return text; - }, []); - - const [copied, setCopied] = useTimeoutToggle(); - const collapsible = useMemo( + const largeCodeBlock = useMemo( () => extractTextFromChildren(children).split('\n').length > LINE_LIMIT, - [children, extractTextFromChildren] + [children] ); - const [collapsed, setCollapsed] = useState(collapsible); - const handleCopy = useCallback(() => { + const [expanded, setExpand] = useState(false); + const [copied, setCopied] = useTimeoutToggle(); + + const handleCopy = () => { copyToClipboard(extractTextFromChildren(children)); setCopied(); - }, [children, extractTextFromChildren, setCopied]); + }; - const toggleCollapse = useCallback(() => { - setCollapsed((prev) => !prev); - }, []); + const toggleExpand = () => { + setExpand(!expanded); + }; return ( - <> -
- - - - {collapsible && ( - +
+ + + {language ?? 'Code'} + + + + } > - - - )} -
- -
+ {copied ? 'Copied' : 'Copy'} + + {largeCodeBlock && ( + + + + )} + + + +
{domToReact(children, opts)}
- + {largeCodeBlock && !expanded && } + ); } @@ -355,11 +383,7 @@ export const getReactCustomHtmlParser = ( } if (name === 'pre') { - return ( - - {CodeBlock(children, opts)} - - ); + return {children}; } if (name === 'blockquote') { @@ -409,9 +433,9 @@ export const getReactCustomHtmlParser = ( } } else { return ( - + {domToReact(children, opts)} - + ); } } diff --git a/src/app/styles/CustomHtml.css.ts b/src/app/styles/CustomHtml.css.ts index ecbdbeee..f717669c 100644 --- a/src/app/styles/CustomHtml.css.ts +++ b/src/app/styles/CustomHtml.css.ts @@ -41,16 +41,19 @@ export const BlockQuote = style([ ]); const BaseCode = style({ - fontFamily: 'monospace', - color: color.Secondary.OnContainer, - background: color.Secondary.Container, - border: `${config.borderWidth.B300} solid ${color.Secondary.ContainerLine}`, + color: color.SurfaceVariant.OnContainer, + background: color.SurfaceVariant.Container, + border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`, borderRadius: config.radii.R300, }); +const CodeFont = style({ + fontFamily: 'monospace', +}); export const Code = style([ DefaultReset, BaseCode, + CodeFont, { padding: `0 ${config.space.S100}`, }, @@ -86,34 +89,31 @@ export const CodeBlock = style([ { fontStyle: 'normal', position: 'relative', + overflow: 'hidden', }, ]); -export const CodeBlockInternal = recipe({ - base: { - padding: `${config.space.S200} ${config.space.S200} 0`, - minWidth: toRem(100), - }, - variants: { - collapsed: { - true: { - maxHeight: `calc(${config.lineHeight.T400} * 9.6)`, - }, - }, - }, +export const CodeBlockHeader = style({ + padding: `0 ${config.space.S200} 0 ${config.space.S300}`, + borderBottomWidth: config.borderWidth.B300, + gap: config.space.S200, }); -export const CodeBlockControls = style({ - position: 'absolute', - top: config.space.S200, - right: config.space.S200, - visibility: 'hidden', - selectors: { - [`${CodeBlock}:hover &`]: { - visibility: 'visible', - }, - [`${CodeBlock}:focus-within &`]: { - visibility: 'visible', - }, +export const CodeBlockInternal = style([ + CodeFont, + { + padding: `${config.space.S200} ${config.space.S200} 0`, + minWidth: toRem(200), }, +]); + +export const CodeBlockBottomShadow = style({ + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + pointerEvents: 'none', + + height: config.space.S400, + background: `linear-gradient(to top, #00000022, #00000000)`, }); export const List = style([