Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/module/src/Message/Message.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -962,4 +962,23 @@ describe('Message', () => {
const form = container.querySelector('form');
expect(form).toHaveClass('test');
});
it('should be able to disable markdown parsing', () => {
render(<Message avatar="./img" role="user" name="User" content={CODE_MESSAGE} isMarkdownDisabled />);
// this is looking for markdown syntax that is ordinarily stripped
expect(screen.getByText(/~~~yaml/i)).toBeTruthy();
});
it('should be able to pass props to react-markdown, such as disabling tags', () => {
render(
<Message
avatar="./img"
role="user"
name="User"
content={CODE_MESSAGE}
reactMarkdownProps={{ disallowedElements: ['code'] }}
/>
);
expect(screen.getByText('Here is some YAML code:')).toBeTruthy();
// code block isn't rendering
expect(screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy();
});
});
108 changes: 62 additions & 46 deletions packages/module/src/Message/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// ============================================================================
import { forwardRef, ReactNode, useEffect, useState } from 'react';
import type { FunctionComponent, HTMLProps, MouseEvent as ReactMouseEvent, Ref } from 'react';
import Markdown from 'react-markdown';
import Markdown, { Options } from 'react-markdown';
import remarkGfm from 'remark-gfm';
import {
AlertProps,
Expand Down Expand Up @@ -185,6 +185,10 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
editFormProps?: FormProps;
/** Sets message to compact styling. */
isCompact?: boolean;
/** Disables markdown parsing for message, allowing only text input */
isMarkdownDisabled?: boolean;
/** Allows passing additional props down to markdown parser react-markdown, such as allowedElements and disallowedElements. See https://github.com/remarkjs/react-markdown?tab=readme-ov-file#options for options */
reactMarkdownProps?: Options;
}

export const MessageBase: FunctionComponent<MessageProps> = ({
Expand Down Expand Up @@ -224,6 +228,8 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
inputRef,
editFormProps,
isCompact,
isMarkdownDisabled,
reactMarkdownProps,
...props
}: MessageProps) => {
const [messageText, setMessageText] = useState(content);
Expand All @@ -250,6 +256,60 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
const date = new Date();
const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;

const handleMarkdown = () => {
if (isMarkdownDisabled) {
return (
<TextMessage component={ContentVariants.p} {...props}>
{messageText}
</TextMessage>
);
}
return (
<Markdown
components={{
p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
code: ({ children, ...props }) => (
<CodeBlockMessage {...props} {...codeBlockProps}>
{children}
</CodeBlockMessage>
),
h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
ul: (props) => <UnorderedListMessage {...props} />,
ol: (props) => <OrderedListMessage {...props} />,
li: (props) => <ListItemMessage {...props} />,
table: (props) => <TableMessage {...props} {...tableProps} />,
tbody: (props) => <TbodyMessage {...props} />,
thead: (props) => <TheadMessage {...props} />,
tr: (props) => <TrMessage {...props} />,
td: (props) => {
// Conflicts with Td type
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { width, ...rest } = props;
return <TdMessage {...rest} />;
},
th: (props) => <ThMessage {...props} />,
img: (props) => <ImageMessage {...props} />,
a: (props) => (
<LinkMessage href={props.href} rel={props.rel} target={props.target} {...linkProps}>
{props.children}
</LinkMessage>
)
}}
remarkPlugins={[remarkGfm]}
rehypePlugins={rehypePlugins}
{...reactMarkdownProps}
>
{messageText}
</Markdown>
);
};

const renderMessage = () => {
if (isLoading) {
return <MessageLoading loadingWord={loadingWord} />;
Expand Down Expand Up @@ -277,51 +337,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
return (
<>
{beforeMainContent && <>{beforeMainContent}</>}
{error ? (
<ErrorMessage {...error} />
) : (
<Markdown
components={{
p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
code: ({ children, ...props }) => (
<CodeBlockMessage {...props} {...codeBlockProps}>
{children}
</CodeBlockMessage>
),
h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
ul: (props) => <UnorderedListMessage {...props} />,
ol: (props) => <OrderedListMessage {...props} />,
li: (props) => <ListItemMessage {...props} />,
table: (props) => <TableMessage {...props} {...tableProps} />,
tbody: (props) => <TbodyMessage {...props} />,
thead: (props) => <TheadMessage {...props} />,
tr: (props) => <TrMessage {...props} />,
td: (props) => {
// Conflicts with Td type
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { width, ...rest } = props;
return <TdMessage {...rest} />;
},
th: (props) => <ThMessage {...props} />,
img: (props) => <ImageMessage {...props} />,
a: (props) => (
<LinkMessage href={props.href} rel={props.rel} target={props.target} {...linkProps}>
{props.children}
</LinkMessage>
)
}}
remarkPlugins={[remarkGfm]}
rehypePlugins={rehypePlugins}
>
{messageText}
</Markdown>
)}
{error ? <ErrorMessage {...error} /> : handleMarkdown()}
</>
);
};
Expand Down
Loading