Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion src/components/ChannelPreview/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import type { Channel, PollVote, TranslationLanguages, UserResponse } from 'stre
import type { TranslationContextValue } from '../../context/TranslationContext';
import type { ChatContextValue } from '../../context';
import type { PluggableList } from 'unified';
import { htmlToTextPlugin } from '../Message';
import { htmlToTextPlugin, plusPlusToEmphasis } from '../Message';
import remarkGfm from 'remark-gfm';

const remarkPlugins: PluggableList = [
htmlToTextPlugin,
[remarkGfm, { singleTilde: false }],
plusPlusToEmphasis,
];

export const renderPreviewText = (text: string) => (
Expand Down
35 changes: 33 additions & 2 deletions src/components/Message/renderText/__tests__/renderText.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import React from 'react';
import { findAndReplace } from 'hast-util-find-and-replace';
import { u } from 'unist-builder';
import { render } from '@testing-library/react';
import { htmlToTextPlugin, keepLineBreaksPlugin } from '../remarkPlugins';
import { render, screen } from '@testing-library/react';
import {
htmlToTextPlugin,
keepLineBreaksPlugin,
plusPlusToEmphasis,
} from '../remarkPlugins';
import { defaultAllowedTagNames, renderText } from '../renderText';
import '@testing-library/jest-dom';

Expand Down Expand Up @@ -433,3 +437,30 @@ describe('htmlToTextPlugin', () => {
expect(container).toMatchSnapshot();
});
});

describe('plusPlusToEmphasis', () => {
const renderTextPlusPlus = (text, withPlugin = true) => {
const Markdown = renderText(
text,
{},
{ getRemarkPlugins: () => (withPlugin ? [plusPlusToEmphasis] : []) },
);
return render(Markdown).container;
};

it('++…++ renders as <ins> and ignores code/links', () => {
renderTextPlusPlus('This is ++inserted++ and `++not++` and [x](y) ++also++');
expect(screen.getByText('inserted', { selector: 'ins' })).toBeInTheDocument();
expect(screen.getByText('++not++', { selector: 'code' })).toBeInTheDocument();
// link text exists; its inner text shouldn't be transformed
// expect(screen.getByRole('link', { name: 'x' })).toBeInTheDocument();
});

it('does not render ++…++ as <ins> if not present', () => {
renderTextPlusPlus('This is ++inserted++ and `++not++` and [x](y) ++also++', false);
expect(screen.queryByText('inserted', { selector: 'ins' })).not.toBeInTheDocument();
expect(screen.getByText('++not++', { selector: 'code' })).toBeInTheDocument();
// link text exists; its inner text shouldn't be transformed
// expect(screen.getByRole('link', { name: 'x' })).toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions src/components/Message/renderText/remarkPlugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './htmlToTextPlugin';
export * from './keepLineBreaksPlugin';
export * from './plusPlusToEmphasis';
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// remark-plusplus-ins.ts
import type { Plugin } from 'unified';
import { SKIP, visit } from 'unist-util-visit';
import type { Visitor } from 'unist-util-visit';
import type { Parent, PhrasingContent, Text } from 'mdast';

/**
* \S → first char must be non-whitespace
* (?:...)?→ optional middle+closing when length > 1
* [\s\S]*?→ anything (including newlines), lazy
* final \S→ last char non-whitespace (only required when there’s more than 1)
*
* Matches:
* ++a++
* Does not match:
* ++++
* ++ ++
*/
const INS_REGEX = /\+\+(\S(?:[\s\S]*?\S)?)\+\+/g;
const IGNORE_NODE_TYPES = new Set([
'code',
'inlineCode',
'link',
'linkReference',
'definition',
'math',
'inlineMath',
]);

/**
* Converts MD "++Some text++" to inserted text element rendered in HTML as <ins>Some text</ins>
* https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ins
*/
export const plusPlusToEmphasis: Plugin<[]> = () => {
const visitor: Visitor = (node, index, parent) => {
// 1) Don’t traverse inside ignored nodes
if (IGNORE_NODE_TYPES.has(node.type)) return SKIP;

// 2) Only transform text nodes with a valid parent + index
if (node.type !== 'text' || parent == null || typeof index !== 'number') return;

const value = (node as Text).value;

// Reset lastIndex to 0 per node so each node is scanned from the beginning
INS_REGEX.lastIndex = 0;

let match: RegExpExecArray | null;
let last = 0;
const out: PhrasingContent[] = [];

while ((match = INS_REGEX.exec(value))) {
const [full, inner] = match;
const start = match.index;

if (start > last) out.push({ type: 'text', value: value.slice(last, start) });

// Render as <ins>…</ins> (remark-rehype respects data.hName)
out.push({
children: [{ type: 'text', value: inner }],
data: { hName: 'ins' },
type: 'emphasis',
});

last = start + full.length;
}

if (out.length === 0) return; // nothing to change
if (last < value.length) out.push({ type: 'text', value: value.slice(last) });

(parent as Parent).children.splice(index, 1, ...out);

// Skip re-visiting the replaced range; continue after inserted nodes
return [SKIP, index + out.length];
};

return (tree) => visit(tree, visitor);
};
8 changes: 7 additions & 1 deletion src/components/Message/renderText/renderText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import type { PluggableList } from 'unified'; // A sub-dependency of react-markd
import { Anchor, Emoji, Mention } from './componentRenderers';
import { detectHttp, matchMarkdownLinks, messageCodeBlocks } from './regex';
import { emojiMarkdownPlugin, mentionsMarkdownPlugin } from './rehypePlugins';
import { htmlToTextPlugin, keepLineBreaksPlugin } from './remarkPlugins';
import {
htmlToTextPlugin,
keepLineBreaksPlugin,
plusPlusToEmphasis,
} from './remarkPlugins';
import { ErrorBoundary } from '../../UtilityComponents';
import type { MentionProps } from './componentRenderers';

Expand Down Expand Up @@ -51,6 +55,7 @@ export const defaultAllowedTagNames: Array<
'h4',
'h5',
'h6',
'ins',
];

function formatUrlForDisplay(url: string) {
Expand Down Expand Up @@ -169,6 +174,7 @@ export const renderText = (
htmlToTextPlugin,
keepLineBreaksPlugin,
[remarkGfm, { singleTilde: false }],
plusPlusToEmphasis,
];
const rehypePlugins: PluggableList = [emojiMarkdownPlugin];

Expand Down
Loading