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
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
Original file line number Diff line number Diff line change
Expand Up @@ -1176,13 +1176,13 @@ describe(`EditMessageForm`, () => {
mentioned_users: [
expect.objectContaining({
banned: false,
created_at: '2020-04-27T13:39:49.331742Z',
created_at: expect.any(String),
id: 'mention-id',
image: expect.any(String),
name: 'mention-name',
online: false,
role: 'user',
updated_at: '2020-04-27T13:39:49.332087Z',
updated_at: expect.any(String),
}),
],
parent_id: undefined,
Expand Down