Room input improvements (#1502)

* prevent context menu when editing message

* send sticker body (#1479)

* update emojiboard search text reaction input label

* stop generating upload image thumbnail (#1475)

* maintain upload order

* Fix message options spinner variant

* add markdown toggle in editor toolbar

* fix heading toggle icon update with cursor move

* add hotkeys for heading

* change editor markdown btn style

* use Ctrl + Enter to send message (#1470)

* fix reaction tooltip word-break

* add shift in editor hokeys with number

* stop parsing markdown in link
This commit is contained in:
Ajay Bura 2023-10-25 16:50:38 +11:00 committed by GitHub
parent c7e5c1fce8
commit 2957a45c4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 162 additions and 71 deletions

View file

@ -66,3 +66,7 @@ export const EditorToolbarBase = style({
export const EditorToolbar = style({ export const EditorToolbar = style({
padding: config.space.S100, padding: config.space.S100,
}); });
export const MarkdownBtnBox = style({
paddingRight: config.space.S100,
});

View file

@ -19,6 +19,7 @@ import {
import React, { ReactNode, useState } from 'react'; import React, { ReactNode, useState } from 'react';
import { ReactEditor, useSlate } from 'slate-react'; import { ReactEditor, useSlate } from 'slate-react';
import { import {
headingLevel,
isAnyMarkActive, isAnyMarkActive,
isBlockActive, isBlockActive,
isMarkActive, isMarkActive,
@ -31,6 +32,8 @@ import { BlockType, MarkType } from './types';
import { HeadingLevel } from './slate'; import { HeadingLevel } from './slate';
import { isMacOS } from '../../utils/user-agent'; import { isMacOS } from '../../utils/user-agent';
import { KeySymbol } from '../../utils/key-symbol'; import { KeySymbol } from '../../utils/key-symbol';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) { function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) {
return ( return (
@ -115,13 +118,13 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
export function HeadingBlockButton() { export function HeadingBlockButton() {
const editor = useSlate(); const editor = useSlate();
const [level, setLevel] = useState<HeadingLevel>(1); const level = headingLevel(editor);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const isActive = isBlockActive(editor, BlockType.Heading); const isActive = isBlockActive(editor, BlockType.Heading);
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
const handleMenuSelect = (selectedLevel: HeadingLevel) => { const handleMenuSelect = (selectedLevel: HeadingLevel) => {
setOpen(false); setOpen(false);
setLevel(selectedLevel);
toggleBlock(editor, BlockType.Heading, { level: selectedLevel }); toggleBlock(editor, BlockType.Heading, { level: selectedLevel });
ReactEditor.focus(editor); ReactEditor.focus(editor);
}; };
@ -130,7 +133,6 @@ export function HeadingBlockButton() {
<PopOut <PopOut
open={open} open={open}
offset={5} offset={5}
align="Start"
position="Top" position="Top"
content={ content={
<FocusTrap <FocusTrap
@ -145,15 +147,51 @@ export function HeadingBlockButton() {
> >
<Menu style={{ padding: config.space.S100 }}> <Menu style={{ padding: config.space.S100 }}>
<Box gap="100"> <Box gap="100">
<IconButton onClick={() => handleMenuSelect(1)} size="400" radii="300"> <TooltipProvider
tooltip={<BtnTooltip text="Heading 1" shortCode={`${modKey} + Shift + 1`} />}
delay={500}
>
{(triggerRef) => (
<IconButton
ref={triggerRef}
onClick={() => handleMenuSelect(1)}
size="400"
radii="300"
>
<Icon size="200" src={Icons.Heading1} /> <Icon size="200" src={Icons.Heading1} />
</IconButton> </IconButton>
<IconButton onClick={() => handleMenuSelect(2)} size="400" radii="300"> )}
</TooltipProvider>
<TooltipProvider
tooltip={<BtnTooltip text="Heading 2" shortCode={`${modKey} + Shift + 2`} />}
delay={500}
>
{(triggerRef) => (
<IconButton
ref={triggerRef}
onClick={() => handleMenuSelect(2)}
size="400"
radii="300"
>
<Icon size="200" src={Icons.Heading2} /> <Icon size="200" src={Icons.Heading2} />
</IconButton> </IconButton>
<IconButton onClick={() => handleMenuSelect(3)} size="400" radii="300"> )}
</TooltipProvider>
<TooltipProvider
tooltip={<BtnTooltip text="Heading 3" shortCode={`${modKey} + Shift + 3`} />}
delay={500}
>
{(triggerRef) => (
<IconButton
ref={triggerRef}
onClick={() => handleMenuSelect(3)}
size="400"
radii="300"
>
<Icon size="200" src={Icons.Heading3} /> <Icon size="200" src={Icons.Heading3} />
</IconButton> </IconButton>
)}
</TooltipProvider>
</Box> </Box>
</Menu> </Menu>
</FocusTrap> </FocusTrap>
@ -169,7 +207,7 @@ export function HeadingBlockButton() {
size="400" size="400"
radii="300" radii="300"
> >
<Icon size="200" src={Icons[`Heading${level}`]} /> <Icon size="200" src={level ? Icons[`Heading${level}`] : Icons.Heading1} />
<Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} /> <Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
</IconButton> </IconButton>
)} )}
@ -210,8 +248,10 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) {
export function Toolbar() { export function Toolbar() {
const editor = useSlate(); const editor = useSlate();
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl'; const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph); const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
return ( return (
<Box className={css.EditorToolbarBase}> <Box className={css.EditorToolbarBase}>
@ -271,12 +311,12 @@ export function Toolbar() {
<BlockButton <BlockButton
format={BlockType.OrderedList} format={BlockType.OrderedList}
icon={Icons.OrderList} icon={Icons.OrderList}
tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} />} tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + Shift + 7`} />}
/> />
<BlockButton <BlockButton
format={BlockType.UnorderedList} format={BlockType.UnorderedList}
icon={Icons.UnorderList} icon={Icons.UnorderList}
tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`} />} tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + Shift + 8`} />}
/> />
<HeadingBlockButton /> <HeadingBlockButton />
</Box> </Box>
@ -292,6 +332,28 @@ export function Toolbar() {
</Box> </Box>
</> </>
)} )}
<Box className={css.MarkdownBtnBox} shrink="No" grow="Yes" justifyContent="End">
<TooltipProvider
align="End"
tooltip={<BtnTooltip text="Inline Markdown" />}
delay={500}
>
{(triggerRef) => (
<IconButton
ref={triggerRef}
variant="SurfaceVariant"
onClick={() => setIsMarkdown(!isMarkdown)}
aria-pressed={isMarkdown}
size="300"
radii="300"
disabled={disableInline || !!isAnyMarkActive(editor)}
>
<Icon size="200" src={Icons.Markdown} filled={isMarkdown} />
</IconButton>
)}
</TooltipProvider>
<span />
</Box>
</Box> </Box>
</Scroll> </Scroll>
</Box> </Box>

View file

@ -15,12 +15,15 @@ export const INLINE_HOTKEYS: Record<string, MarkType> = {
const INLINE_KEYS = Object.keys(INLINE_HOTKEYS); const INLINE_KEYS = Object.keys(INLINE_HOTKEYS);
export const BLOCK_HOTKEYS: Record<string, BlockType> = { export const BLOCK_HOTKEYS: Record<string, BlockType> = {
'mod+7': BlockType.OrderedList, 'mod+shift+7': BlockType.OrderedList,
'mod+8': BlockType.UnorderedList, 'mod+shift+8': BlockType.UnorderedList,
"mod+'": BlockType.BlockQuote, "mod+'": BlockType.BlockQuote,
'mod+;': BlockType.CodeBlock, 'mod+;': BlockType.CodeBlock,
}; };
const BLOCK_KEYS = Object.keys(BLOCK_HOTKEYS); const BLOCK_KEYS = Object.keys(BLOCK_HOTKEYS);
const isHeading1 = isKeyHotkey('mod+shift+1');
const isHeading2 = isKeyHotkey('mod+shift+2');
const isHeading3 = isKeyHotkey('mod+shift+3');
/** /**
* @return boolean true if shortcut is toggled. * @return boolean true if shortcut is toggled.
@ -86,6 +89,18 @@ export const toggleKeyboardShortcut = (editor: Editor, event: KeyboardEvent<Elem
return false; return false;
}); });
if (blockToggled) return true; if (blockToggled) return true;
if (isHeading1(event)) {
toggleBlock(editor, BlockType.Heading, { level: 1 });
return true;
}
if (isHeading2(event)) {
toggleBlock(editor, BlockType.Heading, { level: 2 });
return true;
}
if (isHeading3(event)) {
toggleBlock(editor, BlockType.Heading, { level: 3 });
return true;
}
const inlineToggled = isBlockActive(editor, BlockType.CodeBlock) const inlineToggled = isBlockActive(editor, BlockType.CodeBlock)
? false ? false

View file

@ -52,6 +52,16 @@ export const isBlockActive = (editor: Editor, format: BlockType) => {
return !!match; return !!match;
}; };
export const headingLevel = (editor: Editor): HeadingLevel | undefined => {
const [nodeEntry] = Editor.nodes(editor, {
match: (node) => Element.isElement(node) && node.type === BlockType.Heading,
});
const [node] = nodeEntry ?? [];
if (!node) return undefined;
if ('level' in node) return node.level;
return undefined;
};
type BlockOption = { level: HeadingLevel }; type BlockOption = { level: HeadingLevel };
const NESTED_BLOCK = [ const NESTED_BLOCK = [
BlockType.OrderedList, BlockType.OrderedList,

View file

@ -68,6 +68,7 @@ export type EmojiItemInfo = {
type: EmojiType; type: EmojiType;
data: string; data: string;
shortcode: string; shortcode: string;
label: string;
}; };
const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`; const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`;
@ -75,13 +76,15 @@ const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`;
const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => { const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => {
const type = element.getAttribute('data-emoji-type') as EmojiType | undefined; const type = element.getAttribute('data-emoji-type') as EmojiType | undefined;
const data = element.getAttribute('data-emoji-data'); const data = element.getAttribute('data-emoji-data');
const label = element.getAttribute('title');
const shortcode = element.getAttribute('data-emoji-shortcode'); const shortcode = element.getAttribute('data-emoji-shortcode');
if (type && data && shortcode) if (type && data && shortcode && label)
return { return {
type, type,
data, data,
shortcode, shortcode,
label,
}; };
return undefined; return undefined;
}; };
@ -633,7 +636,7 @@ export function EmojiBoard({
returnFocusOnDeactivate?: boolean; returnFocusOnDeactivate?: boolean;
onEmojiSelect?: (unicode: string, shortcode: string) => void; onEmojiSelect?: (unicode: string, shortcode: string) => void;
onCustomEmojiSelect?: (mxc: string, shortcode: string) => void; onCustomEmojiSelect?: (mxc: string, shortcode: string) => void;
onStickerSelect?: (mxc: string, shortcode: string) => void; onStickerSelect?: (mxc: string, shortcode: string, label: string) => void;
allowTextCustomEmoji?: boolean; allowTextCustomEmoji?: boolean;
}) { }) {
const emojiTab = tab === EmojiBoardTab.Emoji; const emojiTab = tab === EmojiBoardTab.Emoji;
@ -712,7 +715,7 @@ export function EmojiBoard({
if (!evt.altKey && !evt.shiftKey) requestClose(); if (!evt.altKey && !evt.shiftKey) requestClose();
} }
if (emojiInfo.type === EmojiType.Sticker) { if (emojiInfo.type === EmojiType.Sticker) {
onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode); onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode, emojiInfo.label);
if (!evt.altKey && !evt.shiftKey) requestClose(); if (!evt.altKey && !evt.shiftKey) requestClose();
} }
}; };
@ -783,7 +786,7 @@ export function EmojiBoard({
data-emoji-board-search data-emoji-board-search
variant="SurfaceVariant" variant="SurfaceVariant"
size="400" size="400"
placeholder="Search" placeholder={allowTextCustomEmoji ? 'Search or Text Reaction ' : 'Search'}
maxLength={50} maxLength={50}
after={ after={
allowTextCustomEmoji && result?.query ? ( allowTextCustomEmoji && result?.query ? (
@ -791,6 +794,7 @@ export function EmojiBoard({
variant="Primary" variant="Primary"
radii="Pill" radii="Pill"
after={<Icon src={Icons.ArrowRight} size="50" />} after={<Icon src={Icons.ArrowRight} size="50" />}
outlined
onClick={() => { onClick={() => {
const searchInput = document.querySelector<HTMLInputElement>( const searchInput = document.querySelector<HTMLInputElement>(
'[data-emoji-board-search="true"]' '[data-emoji-board-search="true"]'

View file

@ -29,7 +29,6 @@ import {
config, config,
toRem, toRem,
} from 'folds'; } from 'folds';
import to from 'await-to-js';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { import {
@ -216,30 +215,24 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
}; };
const handleSendUpload = async (uploads: UploadSuccess[]) => { const handleSendUpload = async (uploads: UploadSuccess[]) => {
const sendPromises = uploads.map(async (upload) => { const contentsPromises = uploads.map(async (upload) => {
const fileItem = selectedFiles.find((f) => f.file === upload.file); const fileItem = selectedFiles.find((f) => f.file === upload.file);
if (fileItem && fileItem.file.type.startsWith('image')) { if (!fileItem) throw new Error('Broken upload');
const [imgError, imgContent] = await to(getImageMsgContent(mx, fileItem, upload.mxc));
if (imgError) console.warn(imgError); if (fileItem.file.type.startsWith('image')) {
if (imgContent) mx.sendMessage(roomId, imgContent); return getImageMsgContent(mx, fileItem, upload.mxc);
return;
} }
if (fileItem && fileItem.file.type.startsWith('video')) { if (fileItem.file.type.startsWith('video')) {
const [videoError, videoContent] = await to(getVideoMsgContent(mx, fileItem, upload.mxc)); return getVideoMsgContent(mx, fileItem, upload.mxc);
if (videoError) console.warn(videoError);
if (videoContent) mx.sendMessage(roomId, videoContent);
return;
} }
if (fileItem && fileItem.file.type.startsWith('audio')) { if (fileItem.file.type.startsWith('audio')) {
mx.sendMessage(roomId, getAudioMsgContent(fileItem, upload.mxc)); return getAudioMsgContent(fileItem, upload.mxc);
return;
}
if (fileItem) {
mx.sendMessage(roomId, getFileMsgContent(fileItem, upload.mxc));
} }
return getFileMsgContent(fileItem, upload.mxc);
}); });
handleCancelUpload(uploads); handleCancelUpload(uploads);
await Promise.allSettled(sendPromises); const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises));
contents.forEach((content) => mx.sendMessage(roomId, content));
}; };
const submit = useCallback(() => { const submit = useCallback(() => {
@ -319,7 +312,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const handleKeyDown: KeyboardEventHandler = useCallback( const handleKeyDown: KeyboardEventHandler = useCallback(
(evt) => { (evt) => {
if (enterForNewline ? isKeyHotkey('shift+enter', evt) : isKeyHotkey('enter', evt)) { if (isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) {
evt.preventDefault(); evt.preventDefault();
submit(); submit();
} }
@ -359,7 +352,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
moveCursor(editor); moveCursor(editor);
}; };
const handleStickerSelect = async (mxc: string, shortcode: string) => { const handleStickerSelect = async (mxc: string, shortcode: string, label: string) => {
const stickerUrl = mx.mxcUrlToHttp(mxc); const stickerUrl = mx.mxcUrlToHttp(mxc);
if (!stickerUrl) return; if (!stickerUrl) return;
@ -369,7 +362,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
); );
mx.sendEvent(roomId, EventType.Sticker, { mx.sendEvent(roomId, EventType.Sticker, {
body: shortcode, body: label,
url: mxc, url: mxc,
info, info,
}); });

View file

@ -392,7 +392,7 @@ export const MessageDeleteItem = as<
variant="Critical" variant="Critical"
before={ before={
deleteState.status === AsyncStatus.Loading ? ( deleteState.status === AsyncStatus.Loading ? (
<Spinner fill="Soft" variant="Critical" size="200" /> <Spinner fill="Solid" variant="Critical" size="200" />
) : undefined ) : undefined
} }
aria-disabled={deleteState.status === AsyncStatus.Loading} aria-disabled={deleteState.status === AsyncStatus.Loading}
@ -522,7 +522,7 @@ export const MessageReportItem = as<
variant="Critical" variant="Critical"
before={ before={
reportState.status === AsyncStatus.Loading ? ( reportState.status === AsyncStatus.Loading ? (
<Spinner fill="Soft" variant="Critical" size="200" /> <Spinner fill="Solid" variant="Critical" size="200" />
) : undefined ) : undefined
} }
aria-disabled={ aria-disabled={
@ -702,7 +702,7 @@ export const Message = as<'div', MessageProps>(
); );
const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => { const handleContextMenu: MouseEventHandler<HTMLDivElement> = (evt) => {
if (evt.altKey || !window.getSelection()?.isCollapsed) return; if (evt.altKey || !window.getSelection()?.isCollapsed || edit) return;
const tag = (evt.target as any).tagName; const tag = (evt.target as any).tagName;
if (typeof tag === 'string' && tag.toLowerCase() === 'a') return; if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
evt.preventDefault(); evt.preventDefault();

View file

@ -129,7 +129,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
const handleKeyDown: KeyboardEventHandler = useCallback( const handleKeyDown: KeyboardEventHandler = useCallback(
(evt) => { (evt) => {
if (enterForNewline ? isKeyHotkey('shift+enter', evt) : isKeyHotkey('enter', evt)) { if (isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) {
evt.preventDefault(); evt.preventDefault();
handleSave(); handleSave();
} }

View file

@ -68,7 +68,7 @@ export const Reactions = as<'div', ReactionsProps>(
position="Top" position="Top"
tooltip={ tooltip={
<Tooltip style={{ maxWidth: toRem(200) }}> <Tooltip style={{ maxWidth: toRem(200) }}>
<Text size="T300"> <Text className={css.ReactionsTooltipText} size="T300">
<ReactionTooltipMsg room={room} reaction={key} events={rEvents} /> <ReactionTooltipMsg room={room} reaction={key} events={rEvents} />
</Text> </Text>
</Tooltip> </Tooltip>

View file

@ -79,3 +79,7 @@ export const ReactionsContainer = style({
}, },
}, },
}); });
export const ReactionsTooltipText = style({
wordBreak: 'break-all',
});

View file

@ -54,23 +54,10 @@ export const getImageMsgContent = async (
}; };
if (imgEl) { if (imgEl) {
const blurHash = encodeBlurHash(imgEl, 512, scaleYDimension(imgEl.width, 512, imgEl.height)); const blurHash = encodeBlurHash(imgEl, 512, scaleYDimension(imgEl.width, 512, imgEl.height));
const [thumbError, thumbContent] = await to(
generateThumbnailContent(
mx,
imgEl,
getThumbnailDimensions(imgEl.width, imgEl.height),
!!encInfo
)
);
if (thumbContent && thumbContent.thumbnail_info) {
thumbContent.thumbnail_info[MATRIX_BLUR_HASH_PROPERTY_NAME] = blurHash;
}
if (thumbError) console.warn(thumbError);
content.info = { content.info = {
...getImageInfo(imgEl, file), ...getImageInfo(imgEl, file),
[MATRIX_BLUR_HASH_PROPERTY_NAME]: blurHash, [MATRIX_BLUR_HASH_PROPERTY_NAME]: blurHash,
...thumbContent,
}; };
} }
if (encInfo) { if (encInfo) {

View file

@ -45,6 +45,8 @@ import CinnySVG from '../../../../public/res/svg/cinny.svg';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useSetting } from '../../state/hooks/settings'; import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings'; import { settingsAtom } from '../../state/settings';
import { isMacOS } from '../../utils/user-agent';
import { KeySymbol } from '../../utils/key-symbol';
function AppearanceSection() { function AppearanceSection() {
const [, updateState] = useState({}); const [, updateState] = useState({});
@ -147,7 +149,7 @@ function AppearanceSection() {
onToggle={() => setEnterForNewline(!enterForNewline) } onToggle={() => setEnterForNewline(!enterForNewline) }
/> />
)} )}
content={<Text variant="b3">Use SHIFT + ENTER to send message and ENTER for newline.</Text>} content={<Text variant="b3">{`Use ${isMacOS() ? KeySymbol.Command : 'Ctrl'} + ENTER to send message and ENTER for newline.`}</Text>}
/> />
<SettingTile <SettingTile
title="Inline Markdown formatting" title="Inline Markdown formatting"

View file

@ -23,24 +23,28 @@ export type RulesRunner = (
) => string | undefined; ) => string | undefined;
const MIN_ANY = '(.+?)'; const MIN_ANY = '(.+?)';
const URL_NEG_LB = '(?<!(https?|ftp|mailto|magnet):\\/\\/\\S*)';
const BOLD_MD_1 = '**'; const BOLD_MD_1 = '**';
const BOLD_PREFIX_1 = '\\*{2}'; const BOLD_PREFIX_1 = '\\*{2}';
const BOLD_NEG_LA_1 = '(?!\\*)'; const BOLD_NEG_LA_1 = '(?!\\*)';
const BOLD_REG_1 = new RegExp(`${BOLD_PREFIX_1}${MIN_ANY}${BOLD_PREFIX_1}${BOLD_NEG_LA_1}`); const BOLD_REG_1 = new RegExp(
`${URL_NEG_LB}${BOLD_PREFIX_1}${MIN_ANY}${BOLD_PREFIX_1}${BOLD_NEG_LA_1}`
);
const BoldRule: MDRule = { const BoldRule: MDRule = {
match: (text) => text.match(BOLD_REG_1), match: (text) => text.match(BOLD_REG_1),
html: (parse, match) => { html: (parse, match) => {
const [, g1] = match; const [, g1] = match;
const child = parse(g1); return `<strong data-md="${BOLD_MD_1}">${parse(g1)}</strong>`;
return `<strong data-md="${BOLD_MD_1}">${child}</strong>`;
}, },
}; };
const ITALIC_MD_1 = '*'; const ITALIC_MD_1 = '*';
const ITALIC_PREFIX_1 = '\\*'; const ITALIC_PREFIX_1 = '\\*';
const ITALIC_NEG_LA_1 = '(?!\\*)'; const ITALIC_NEG_LA_1 = '(?!\\*)';
const ITALIC_REG_1 = new RegExp(`${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}`); const ITALIC_REG_1 = new RegExp(
`${URL_NEG_LB}${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}`
);
const ItalicRule1: MDRule = { const ItalicRule1: MDRule = {
match: (text) => text.match(ITALIC_REG_1), match: (text) => text.match(ITALIC_REG_1),
html: (parse, match) => { html: (parse, match) => {
@ -52,7 +56,9 @@ const ItalicRule1: MDRule = {
const ITALIC_MD_2 = '_'; const ITALIC_MD_2 = '_';
const ITALIC_PREFIX_2 = '_'; const ITALIC_PREFIX_2 = '_';
const ITALIC_NEG_LA_2 = '(?!_)'; const ITALIC_NEG_LA_2 = '(?!_)';
const ITALIC_REG_2 = new RegExp(`${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}`); const ITALIC_REG_2 = new RegExp(
`${URL_NEG_LB}${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}`
);
const ItalicRule2: MDRule = { const ItalicRule2: MDRule = {
match: (text) => text.match(ITALIC_REG_2), match: (text) => text.match(ITALIC_REG_2),
html: (parse, match) => { html: (parse, match) => {
@ -65,7 +71,7 @@ const UNDERLINE_MD_1 = '__';
const UNDERLINE_PREFIX_1 = '_{2}'; const UNDERLINE_PREFIX_1 = '_{2}';
const UNDERLINE_NEG_LA_1 = '(?!_)'; const UNDERLINE_NEG_LA_1 = '(?!_)';
const UNDERLINE_REG_1 = new RegExp( const UNDERLINE_REG_1 = new RegExp(
`${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}` `${URL_NEG_LB}${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}`
); );
const UnderlineRule: MDRule = { const UnderlineRule: MDRule = {
match: (text) => text.match(UNDERLINE_REG_1), match: (text) => text.match(UNDERLINE_REG_1),
@ -78,7 +84,9 @@ const UnderlineRule: MDRule = {
const STRIKE_MD_1 = '~~'; const STRIKE_MD_1 = '~~';
const STRIKE_PREFIX_1 = '~{2}'; const STRIKE_PREFIX_1 = '~{2}';
const STRIKE_NEG_LA_1 = '(?!~)'; const STRIKE_NEG_LA_1 = '(?!~)';
const STRIKE_REG_1 = new RegExp(`${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}`); const STRIKE_REG_1 = new RegExp(
`${URL_NEG_LB}${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}`
);
const StrikeRule: MDRule = { const StrikeRule: MDRule = {
match: (text) => text.match(STRIKE_REG_1), match: (text) => text.match(STRIKE_REG_1),
html: (parse, match) => { html: (parse, match) => {
@ -90,7 +98,9 @@ const StrikeRule: MDRule = {
const CODE_MD_1 = '`'; const CODE_MD_1 = '`';
const CODE_PREFIX_1 = '`'; const CODE_PREFIX_1 = '`';
const CODE_NEG_LA_1 = '(?!`)'; const CODE_NEG_LA_1 = '(?!`)';
const CODE_REG_1 = new RegExp(`${CODE_PREFIX_1}${MIN_ANY}${CODE_PREFIX_1}${CODE_NEG_LA_1}`); const CODE_REG_1 = new RegExp(
`${URL_NEG_LB}${CODE_PREFIX_1}${MIN_ANY}${CODE_PREFIX_1}${CODE_NEG_LA_1}`
);
const CodeRule: MDRule = { const CodeRule: MDRule = {
match: (text) => text.match(CODE_REG_1), match: (text) => text.match(CODE_REG_1),
html: (parse, match) => { html: (parse, match) => {
@ -103,7 +113,7 @@ const SPOILER_MD_1 = '||';
const SPOILER_PREFIX_1 = '\\|{2}'; const SPOILER_PREFIX_1 = '\\|{2}';
const SPOILER_NEG_LA_1 = '(?!\\|)'; const SPOILER_NEG_LA_1 = '(?!\\|)';
const SPOILER_REG_1 = new RegExp( const SPOILER_REG_1 = new RegExp(
`${SPOILER_PREFIX_1}${MIN_ANY}${SPOILER_PREFIX_1}${SPOILER_NEG_LA_1}` `${URL_NEG_LB}${SPOILER_PREFIX_1}${MIN_ANY}${SPOILER_PREFIX_1}${SPOILER_NEG_LA_1}`
); );
const SpoilerRule: MDRule = { const SpoilerRule: MDRule = {
match: (text) => text.match(SPOILER_REG_1), match: (text) => text.match(SPOILER_REG_1),