diff --git a/src/components/catalog-row/__tests__/catalog-row.test.tsx b/src/components/catalog-row/__tests__/catalog-row.test.tsx index 95483eb1..7be84b53 100644 --- a/src/components/catalog-row/__tests__/catalog-row.test.tsx +++ b/src/components/catalog-row/__tests__/catalog-row.test.tsx @@ -480,6 +480,22 @@ describe('CatalogRow', () => { expect(container.textContent).toContain('Text title: Plain thread body'); }); + it('preserves literal catalog teaser markers without applying body markdown styles', async () => { + const post: TestComment = { + cid: 'literal-markers', + content: '>we got 5chan before Half Life 3\n*poisons u*', + replyCount: 2, + communityAddress: 'music-posting.eth', + }; + + await renderWithRouter(createElement(CatalogRow, { row: [post] }), '/mu/catalog'); + + expect(container.textContent).toContain('>we got 5chan before Half Life 3'); + expect(container.textContent).toContain('*poisons u*'); + expect(container.querySelector('.greentext')).toBeNull(); + expect(container.querySelector('.spoilertext')).toBeNull(); + }); + it('applies the estimated row height to the virtualization wrapper', async () => { const post: TestComment = { cid: 'estimated-post', diff --git a/src/components/catalog-row/catalog-row.tsx b/src/components/catalog-row/catalog-row.tsx index f2a13dce..60886ced 100644 --- a/src/components/catalog-row/catalog-row.tsx +++ b/src/components/catalog-row/catalog-row.tsx @@ -18,7 +18,7 @@ import useCountLinksInReplies from '../../hooks/use-count-links-in-replies'; import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame'; import useHide from '../../hooks/use-hide'; import { isCommentArchived } from '../../lib/utils/comment-moderation-utils'; -import { removeMarkdown } from '../../lib/utils/post-utils'; +import { CATALOG_PREVIEW_MARKDOWN_OPTIONS, removeMarkdown } from '../../lib/utils/post-utils'; import PostMenuDesktop from '../post-desktop/post-menu-desktop'; import styles from './catalog-row.module.css'; import capitalize from 'lodash/capitalize'; @@ -215,7 +215,7 @@ const CatalogPost = memo( {content ? ': ' : ''} )} - {content && removeMarkdown(content)} + {content && removeMarkdown(content, CATALOG_PREVIEW_MARKDOWN_OPTIONS)} )} diff --git a/src/lib/utils/__tests__/misc-utils.test.ts b/src/lib/utils/__tests__/misc-utils.test.ts index 89e5b0c9..458ef6cf 100644 --- a/src/lib/utils/__tests__/misc-utils.test.ts +++ b/src/lib/utils/__tests__/misc-utils.test.ts @@ -105,6 +105,9 @@ describe('misc utils', () => { expect(removeMarkdown('[spoiler]secret[/spoiler]\n>greentext\n**bold** [label](https://example.com) `code` ```block```  ')).toBe( 'secret\ngreentext\nbold label code block', ); + expect(removeMarkdown('>greentext\n*poisons u* _and underlines_', { preserveEmphasisMarkers: true, preserveGreentextMarkers: true })).toBe( + '>greentext\n*poisons u* _and underlines_', + ); }); it('preloads theme asset URLs through Image instances', () => { diff --git a/src/lib/utils/post-utils.ts b/src/lib/utils/post-utils.ts index d75b7c84..e0b13772 100644 --- a/src/lib/utils/post-utils.ts +++ b/src/lib/utils/post-utils.ts @@ -21,13 +21,31 @@ export function getTextColorForBackground(rgb: string): string { return brightness > 125 ? 'black' : 'white'; } -export function removeMarkdown(md: string): string { - return md +export interface RemoveMarkdownOptions { + preserveEmphasisMarkers?: boolean; + preserveGreentextMarkers?: boolean; +} + +export const CATALOG_PREVIEW_MARKDOWN_OPTIONS: RemoveMarkdownOptions = { + preserveEmphasisMarkers: true, + preserveGreentextMarkers: true, +}; + +export function removeMarkdown(md: string, options: RemoveMarkdownOptions = {}): string { + let withoutMarkdown = md .replace(/\[spoiler\](.*?)\[\/spoiler\]/gis, '$1') // spoiler tags - keep inner text .replace(/\[([^\]]*?)\]\([^)]*\)/g, '$1') // [text](url) -> text - .replace(/ /g, ' ') //   -> space - .replace(/^>\s*/gm, '') // greentext at line start - .replace(/[*_]/g, '') // bold/italic markers + .replace(/ /g, ' '); //   -> space + + if (!options.preserveGreentextMarkers) { + withoutMarkdown = withoutMarkdown.replace(/^>\s*/gm, ''); // greentext at line start + } + + if (!options.preserveEmphasisMarkers) { + withoutMarkdown = withoutMarkdown.replace(/[*_]/g, ''); // bold/italic markers + } + + return withoutMarkdown .replace(/```[\s\S]*?```/g, (m) => m.slice(3, -3)) // code blocks - keep content .replace(/`([^`]*)`/g, '$1') // inline code - keep content .trim(); diff --git a/src/lib/utils/pretext-height-estimates.ts b/src/lib/utils/pretext-height-estimates.ts index 35c1efbb..bc39e0c6 100644 --- a/src/lib/utils/pretext-height-estimates.ts +++ b/src/lib/utils/pretext-height-estimates.ts @@ -2,7 +2,7 @@ import type { Comment } from '@bitsocial/bitsocial-react-hooks'; import { layout, layoutNextLine, prepare, prepareWithSegments } from '@chenglou/pretext'; import { getCommentMediaInfo, getHasThumbnail } from './media-utils'; import { EXPANDED_MEDIA_DATA_ATTRIBUTE } from './measurement-attributes'; -import { removeMarkdown } from './post-utils'; +import { CATALOG_PREVIEW_MARKDOWN_OPTIONS, removeMarkdown, type RemoveMarkdownOptions } from './post-utils'; import { getRenderableMobileBacklinks } from './reply-backlink-utils'; export type ReplyVirtualizationMode = 'off' | 'estimates' | 'item-size'; @@ -393,8 +393,8 @@ const getCatalogPostMediaHeight = (post: Comment | undefined, imageSize: Catalog return mediaBox ? mediaBox.height + CATALOG_CARD_MEDIA_GAP_HEIGHT : 0; }; -const normalizeRenderedCommentText = (rawContent: string): string => - removeMarkdown(rawContent) +const normalizeRenderedCommentText = (rawContent: string, markdownOptions?: RemoveMarkdownOptions): string => + removeMarkdown(rawContent, markdownOptions) .replace(/\n \n/g, '\n\n') .replace(/\n{3,}/g, '\n\n') .split('\n') @@ -402,7 +402,7 @@ const normalizeRenderedCommentText = (rawContent: string): string => .join('\n') .trim(); -const getVisibleCommentBodyText = (comment: Comment | undefined, maxContentChars: number): string => { +const getVisibleCommentBodyText = (comment: Comment | undefined, maxContentChars: number, markdownOptions?: RemoveMarkdownOptions): string => { if (!comment) { return ''; } @@ -412,7 +412,7 @@ const getVisibleCommentBodyText = (comment: Comment | undefined, maxContentChars const removed = comment.removed; const reason = comment.reason?.trim(); const rawContent = comment.content || ''; - const content = normalizeRenderedCommentText(rawContent.slice(0, maxContentChars)); + const content = normalizeRenderedCommentText(rawContent.slice(0, maxContentChars), markdownOptions); if (purged) { return 'This post was purged'; @@ -433,13 +433,13 @@ const getVisibleCommentBodyText = (comment: Comment | undefined, maxContentChars return content; }; -const getVisibleCommentText = (comment: Comment | undefined, maxContentChars: number): string => { +const getVisibleCommentText = (comment: Comment | undefined, maxContentChars: number, markdownOptions?: RemoveMarkdownOptions): string => { if (!comment) { return ''; } const title = comment.title?.trim(); - const content = getVisibleCommentBodyText(comment, maxContentChars); + const content = getVisibleCommentBodyText(comment, maxContentChars, markdownOptions); if (title && content) { return `${title}: ${content}`; @@ -455,7 +455,7 @@ const getVisibleCatalogText = (post: Comment | undefined, showOPComment: boolean return ''; } - return getVisibleCommentText(post, 1600); + return getVisibleCommentText(post, 1600, CATALOG_PREVIEW_MARKDOWN_OPTIONS); }; const getMobileBacklinkCount = (reply: Comment, backlinkMaps: ReplyBacklinkMaps): number => {