// ============================================================================ // Markdown Content - Shared component for rendering markdown // ============================================================================ import { type FunctionComponent, ReactNode } from 'react'; import Markdown, { Options } from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { ContentVariants } from '@patternfly/react-core'; import CodeBlockMessage, { CodeBlockMessageProps } from '../Message/CodeBlockMessage/CodeBlockMessage'; import TextMessage from '../Message/TextMessage/TextMessage'; import ListItemMessage from '../Message/ListMessage/ListItemMessage'; import UnorderedListMessage from '../Message/ListMessage/UnorderedListMessage'; import OrderedListMessage from '../Message/ListMessage/OrderedListMessage'; import TableMessage from '../Message/TableMessage/TableMessage'; import TrMessage from '../Message/TableMessage/TrMessage'; import TdMessage from '../Message/TableMessage/TdMessage'; import TbodyMessage from '../Message/TableMessage/TbodyMessage'; import TheadMessage from '../Message/TableMessage/TheadMessage'; import ThMessage from '../Message/TableMessage/ThMessage'; import { TableProps } from '@patternfly/react-table'; import ImageMessage from '../Message/ImageMessage/ImageMessage'; import rehypeUnwrapImages from 'rehype-unwrap-images'; import rehypeExternalLinks from 'rehype-external-links'; import rehypeSanitize from 'rehype-sanitize'; import rehypeHighlight from 'rehype-highlight'; import 'highlight.js/styles/vs2015.css'; import { PluggableList } from 'unified'; import LinkMessage from '../Message/LinkMessage/LinkMessage'; import { rehypeMoveImagesOutOfParagraphs } from '../Message/Plugins/rehypeMoveImagesOutOfParagraphs'; import SuperscriptMessage from '../Message/SuperscriptMessage/SuperscriptMessage'; import { ButtonProps } from '@patternfly/react-core'; import { css } from '@patternfly/react-styles'; /** * MarkdownContent renders content either as plain text or with content with markdown support. * * Use this component when passing children to Message to customize its structure. */ export interface MarkdownContentProps { /** The content to render. Supports markdown formatting by default, or plain text when isMarkdownDisabled is true. */ content?: string; /** Disables markdown parsing, allowing only plain text input */ isMarkdownDisabled?: boolean; /** Props for code blocks */ codeBlockProps?: CodeBlockMessageProps; /** Props for table message. It is important to include a detailed aria-label that describes the purpose of the table. */ tableProps?: Required> & TableProps; /** Additional rehype plugins passed from the consumer */ additionalRehypePlugins?: PluggableList; /** Additional remark plugins passed from the consumer */ additionalRemarkPlugins?: PluggableList; /** Whether to open links in message in new tab. */ openLinkInNewTab?: boolean; /** Props for links */ linkProps?: ButtonProps; /** 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; /** Allows passing additional props down to remark-gfm. See https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options for options */ remarkGfmProps?: Options; /** Whether to strip out images in markdown */ hasNoImages?: boolean; /** Sets background colors to be appropriate on primary chatbot background */ isPrimary?: boolean; /** Custom component to render when markdown is disabled */ textComponent?: ReactNode; /** Flag indicating whether content should retain various styles of its context (typically font-size and text color). */ shouldRetainStyles?: boolean; } export const MarkdownContent: FunctionComponent = ({ content, isMarkdownDisabled, codeBlockProps, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, reactMarkdownProps, remarkGfmProps, hasNoImages = false, isPrimary, textComponent, shouldRetainStyles }: MarkdownContentProps) => { let rehypePlugins: PluggableList = [rehypeUnwrapImages, rehypeMoveImagesOutOfParagraphs, rehypeHighlight]; if (openLinkInNewTab) { rehypePlugins = rehypePlugins.concat([[rehypeExternalLinks, { target: '_blank' }, rehypeSanitize]]); } if (additionalRehypePlugins) { rehypePlugins.push(...additionalRehypePlugins); } const disallowedElements = hasNoImages ? ['img'] : []; if (reactMarkdownProps && reactMarkdownProps.disallowedElements) { disallowedElements.push(...reactMarkdownProps.disallowedElements); } if (isMarkdownDisabled) { if (textComponent) { return <>{textComponent}; } return ( {content} ); } return ( { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return (
); }, p: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ( ); }, code: ({ children, ...props }) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...codeProps } = props; return ( {children} ); }, h1: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, h2: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, h3: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, h4: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, h5: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, h6: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, blockquote: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ( ); }, ul: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, ol: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, li: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, // table requires node attribute for calculating headers for mobile breakpoint table: (props) => ( ), tbody: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, thead: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, tr: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, td: (props) => { // Conflicts with Td type // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, width, ...rest } = props; return ; }, th: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, img: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; }, a: (props) => { // node is just the details of the document structure - not needed // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ( // some a types conflict with ButtonProps, but it's ok because we are using an a tag // there are too many to handle manually {props.children} ); }, // used for footnotes sup: (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { node, ...rest } = props; return ; } }} remarkPlugins={[[remarkGfm, { ...remarkGfmProps }], ...additionalRemarkPlugins]} rehypePlugins={rehypePlugins} {...reactMarkdownProps} remarkRehypeOptions={{ // removes sr-only class from footnote labels applied by default footnoteLabelProperties: { className: [''] }, ...reactMarkdownProps?.remarkRehypeOptions }} disallowedElements={disallowedElements} > {content} ); }; export default MarkdownContent;