diff --git a/frontend/src/v2/__tests__/V2MessageRow.realMarkdown.test.tsx b/frontend/src/v2/__tests__/V2MessageRow.realMarkdown.test.tsx index 8f2e68faa..e8a952529 100644 --- a/frontend/src/v2/__tests__/V2MessageRow.realMarkdown.test.tsx +++ b/frontend/src/v2/__tests__/V2MessageRow.realMarkdown.test.tsx @@ -5,7 +5,7 @@ // asserted here is the one a reader would click. import React from 'react'; import { render, screen } from '@testing-library/react'; -import { linkifyBareUrls, prUrlsToRefLinks } from '../components/V2MessageRow'; +import { linkifyBareUrls, messageMarkdownComponents, prUrlsToRefLinks } from '../components/V2MessageRow'; import V2GithubPrCard from '../components/V2GithubPrCard'; const RealReactMarkdown = jest.requireActual('../../../node_modules/react-markdown/index.js').default; @@ -72,6 +72,19 @@ describe('bare URL linkification through the real react-markdown', () => { expect(text).toBe('see the PR now'); }); + // TASK-306. react-markdown hands every component its mdast `node`; the + // renderers must not spread it onto the DOM as node="[object Object]". + it('rendered links and fenced code carry no node attribute', () => { + const { container, unmount } = render( + + {'see [docs](https://example.com/docs)\n\n```\nconst x = 1;\n```'} + , + ); + expect(container.querySelector('a')).not.toHaveAttribute('node'); + expect(container.querySelector('pre')).not.toHaveAttribute('node'); + unmount(); + }); + it('a PR URL in a sentence reads as a short ref, with its punctuation intact', () => { const { container, unmount } = render( {prUrlsToRefLinks('Review: https://github.com/Team-Commonly/commonly/pull/12. Then merge.')}, diff --git a/frontend/src/v2/components/V2MessageRow.tsx b/frontend/src/v2/components/V2MessageRow.tsx index 0c97f7f7d..11069309e 100644 --- a/frontend/src/v2/components/V2MessageRow.tsx +++ b/frontend/src/v2/components/V2MessageRow.tsx @@ -2,7 +2,7 @@ import React, { lazy, Suspense, useEffect, useRef, useState } from 'react'; import V2Lightbox, { type V2LightboxImage } from './V2Lightbox'; import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import ReactMarkdown from 'react-markdown'; +import ReactMarkdown, { type ExtraProps } from 'react-markdown'; import V2Avatar from './V2Avatar'; import V2GithubPrCard, { parseGithubPrUrls } from './V2GithubPrCard'; import V2ApprovalCard from './V2ApprovalCard'; @@ -75,9 +75,11 @@ export const MessageMarkdownImage = ({ src, alt }: React.ImgHTMLAttributes{alt || src}; }; -const messageMarkdownComponents = { +export const messageMarkdownComponents = { img: MessageMarkdownImage, - a: ({ children, ...props }: React.AnchorHTMLAttributes) => ( + // react-markdown passes its mdast `node` to every component; spreading it + // onto the DOM element renders node="[object Object]". + a: ({ children, node: _node, ...props }: React.AnchorHTMLAttributes & ExtraProps) => ( {renderWithMentions(children)} ), p: ({ children }: React.HTMLAttributes) => ( @@ -90,7 +92,7 @@ const messageMarkdownComponents = { // `
`. Both fall through to v2.css selectors `.v2-msg__content code`
   // and `.v2-msg__content pre`. Mentions inside code are NOT transformed.
   // Fenced code past six lines collapses (direction C).
-  pre: (props: React.HTMLAttributes) => ,
+  pre: ({ node: _node, ...props }: React.HTMLAttributes & ExtraProps) => ,
 };
 
 interface V2MessageRowProps {