Room input improvements (#1502)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Wed, 25 Oct 2023 05:50:38 +0000 (16:50 +1100)
committerGitHub <noreply@github.com>
Wed, 25 Oct 2023 05:50:38 +0000 (16:50 +1100)
* 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

13 files changed:
src/app/components/editor/Editor.css.ts
src/app/components/editor/Toolbar.tsx
src/app/components/editor/keyboard.ts
src/app/components/editor/utils.ts
src/app/components/emoji-board/EmojiBoard.tsx
src/app/organisms/room/RoomInput.tsx
src/app/organisms/room/message/Message.tsx
src/app/organisms/room/message/MessageEditor.tsx
src/app/organisms/room/message/Reactions.tsx
src/app/organisms/room/message/styles.css.ts
src/app/organisms/room/msgContent.ts
src/app/organisms/settings/Settings.jsx
src/app/utils/markdown.ts

index edce743f2aca18ed385216af21e5b3245dc0f58c..09a444ec182b645602683a79891dca57cd6102c8 100644 (file)
@@ -66,3 +66,7 @@ export const EditorToolbarBase = style({
 export const EditorToolbar = style({
   padding: config.space.S100,
 });
+
+export const MarkdownBtnBox = style({
+  paddingRight: config.space.S100,
+});
index 766a1d8394a49b7aaa27847307271b16e33abe0f..6feae0095f3ab446a6ef9efe8dd9aafd028e776c 100644 (file)
@@ -19,6 +19,7 @@ import {
 import React, { ReactNode, useState } from 'react';
 import { ReactEditor, useSlate } from 'slate-react';
 import {
+  headingLevel,
   isAnyMarkActive,
   isBlockActive,
   isMarkActive,
@@ -31,6 +32,8 @@ import { BlockType, MarkType } from './types';
 import { HeadingLevel } from './slate';
 import { isMacOS } from '../../utils/user-agent';
 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 }) {
   return (
@@ -115,13 +118,13 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) {
 
 export function HeadingBlockButton() {
   const editor = useSlate();
-  const [level, setLevel] = useState<HeadingLevel>(1);
+  const level = headingLevel(editor);
   const [open, setOpen] = useState(false);
   const isActive = isBlockActive(editor, BlockType.Heading);
+  const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
 
   const handleMenuSelect = (selectedLevel: HeadingLevel) => {
     setOpen(false);
-    setLevel(selectedLevel);
     toggleBlock(editor, BlockType.Heading, { level: selectedLevel });
     ReactEditor.focus(editor);
   };
@@ -130,7 +133,6 @@ export function HeadingBlockButton() {
     <PopOut
       open={open}
       offset={5}
-      align="Start"
       position="Top"
       content={
         <FocusTrap
@@ -145,15 +147,51 @@ export function HeadingBlockButton() {
         >
           <Menu style={{ padding: config.space.S100 }}>
             <Box gap="100">
-              <IconButton onClick={() => handleMenuSelect(1)} size="400" radii="300">
-                <Icon size="200" src={Icons.Heading1} />
-              </IconButton>
-              <IconButton onClick={() => handleMenuSelect(2)} size="400" radii="300">
-                <Icon size="200" src={Icons.Heading2} />
-              </IconButton>
-              <IconButton onClick={() => handleMenuSelect(3)} size="400" radii="300">
-                <Icon size="200" src={Icons.Heading3} />
-              </IconButton>
+              <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} />
+                  </IconButton>
+                )}
+              </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} />
+                  </IconButton>
+                )}
+              </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} />
+                  </IconButton>
+                )}
+              </TooltipProvider>
             </Box>
           </Menu>
         </FocusTrap>
@@ -169,7 +207,7 @@ export function HeadingBlockButton() {
           size="400"
           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} />
         </IconButton>
       )}
@@ -210,8 +248,10 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) {
 export function Toolbar() {
   const editor = useSlate();
   const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
+  const disableInline = isBlockActive(editor, BlockType.CodeBlock);
 
   const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);
+  const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
 
   return (
     <Box className={css.EditorToolbarBase}>
@@ -271,12 +311,12 @@ export function Toolbar() {
             <BlockButton
               format={BlockType.OrderedList}
               icon={Icons.OrderList}
-              tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + 7`} />}
+              tooltip={<BtnTooltip text="Ordered List" shortCode={`${modKey} + Shift + 7`} />}
             />
             <BlockButton
               format={BlockType.UnorderedList}
               icon={Icons.UnorderList}
-              tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + 8`} />}
+              tooltip={<BtnTooltip text="Unordered List" shortCode={`${modKey} + Shift + 8`} />}
             />
             <HeadingBlockButton />
           </Box>
@@ -292,6 +332,28 @@ export function Toolbar() {
               </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>
       </Scroll>
     </Box>
index 2ea2dbe1e31803d6247290f3cf6accacddb13f35..370f3e8276db28cad2b09546d1f817cf9440c45a 100644 (file)
@@ -15,12 +15,15 @@ export const INLINE_HOTKEYS: Record<string, MarkType> = {
 const INLINE_KEYS = Object.keys(INLINE_HOTKEYS);
 
 export const BLOCK_HOTKEYS: Record<string, BlockType> = {
-  'mod+7': BlockType.OrderedList,
-  'mod+8': BlockType.UnorderedList,
+  'mod+shift+7': BlockType.OrderedList,
+  'mod+shift+8': BlockType.UnorderedList,
   "mod+'": BlockType.BlockQuote,
   'mod+;': BlockType.CodeBlock,
 };
 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.
@@ -86,6 +89,18 @@ export const toggleKeyboardShortcut = (editor: Editor, event: KeyboardEvent<Elem
     return false;
   });
   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)
     ? false
index 9bdfde1853d34757244cc983cf64a633504a4652..3f4f9547badddc3a9c9b6cff7b04e3422d92392f 100644 (file)
@@ -52,6 +52,16 @@ export const isBlockActive = (editor: Editor, format: BlockType) => {
   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 };
 const NESTED_BLOCK = [
   BlockType.OrderedList,
index 52df9258216c63b14b7a3ad98709d562ef3b6f8b..5452722fe213bbc3350403c9de093df3cd6c96b5 100644 (file)
@@ -68,6 +68,7 @@ export type EmojiItemInfo = {
   type: EmojiType;
   data: string;
   shortcode: string;
+  label: string;
 };
 
 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 type = element.getAttribute('data-emoji-type') as EmojiType | undefined;
   const data = element.getAttribute('data-emoji-data');
+  const label = element.getAttribute('title');
   const shortcode = element.getAttribute('data-emoji-shortcode');
 
-  if (type && data && shortcode)
+  if (type && data && shortcode && label)
     return {
       type,
       data,
       shortcode,
+      label,
     };
   return undefined;
 };
@@ -633,7 +636,7 @@ export function EmojiBoard({
   returnFocusOnDeactivate?: boolean;
   onEmojiSelect?: (unicode: 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;
 }) {
   const emojiTab = tab === EmojiBoardTab.Emoji;
@@ -712,7 +715,7 @@ export function EmojiBoard({
       if (!evt.altKey && !evt.shiftKey) requestClose();
     }
     if (emojiInfo.type === EmojiType.Sticker) {
-      onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode);
+      onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode, emojiInfo.label);
       if (!evt.altKey && !evt.shiftKey) requestClose();
     }
   };
@@ -783,7 +786,7 @@ export function EmojiBoard({
                 data-emoji-board-search
                 variant="SurfaceVariant"
                 size="400"
-                placeholder="Search"
+                placeholder={allowTextCustomEmoji ? 'Search or Text Reaction ' : 'Search'}
                 maxLength={50}
                 after={
                   allowTextCustomEmoji && result?.query ? (
@@ -791,6 +794,7 @@ export function EmojiBoard({
                       variant="Primary"
                       radii="Pill"
                       after={<Icon src={Icons.ArrowRight} size="50" />}
+                      outlined
                       onClick={() => {
                         const searchInput = document.querySelector<HTMLInputElement>(
                           '[data-emoji-board-search="true"]'
index f7b219be11be1921d6924bbb5ec714f3c4f96bd4..8dc4e6447adecccfd15b5f8785dd7cdebfd1d326 100644 (file)
@@ -29,7 +29,6 @@ import {
   config,
   toRem,
 } from 'folds';
-import to from 'await-to-js';
 
 import { useMatrixClient } from '../../hooks/useMatrixClient';
 import {
@@ -216,30 +215,24 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
     };
 
     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);
-        if (fileItem && fileItem.file.type.startsWith('image')) {
-          const [imgError, imgContent] = await to(getImageMsgContent(mx, fileItem, upload.mxc));
-          if (imgError) console.warn(imgError);
-          if (imgContent) mx.sendMessage(roomId, imgContent);
-          return;
-        }
-        if (fileItem && fileItem.file.type.startsWith('video')) {
-          const [videoError, videoContent] = await to(getVideoMsgContent(mx, fileItem, upload.mxc));
-          if (videoError) console.warn(videoError);
-          if (videoContent) mx.sendMessage(roomId, videoContent);
-          return;
+        if (!fileItem) throw new Error('Broken upload');
+
+        if (fileItem.file.type.startsWith('image')) {
+          return getImageMsgContent(mx, fileItem, upload.mxc);
         }
-        if (fileItem && fileItem.file.type.startsWith('audio')) {
-          mx.sendMessage(roomId, getAudioMsgContent(fileItem, upload.mxc));
-          return;
+        if (fileItem.file.type.startsWith('video')) {
+          return getVideoMsgContent(mx, fileItem, upload.mxc);
         }
-        if (fileItem) {
-          mx.sendMessage(roomId, getFileMsgContent(fileItem, upload.mxc));
+        if (fileItem.file.type.startsWith('audio')) {
+          return getAudioMsgContent(fileItem, upload.mxc);
         }
+        return getFileMsgContent(fileItem, upload.mxc);
       });
       handleCancelUpload(uploads);
-      await Promise.allSettled(sendPromises);
+      const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises));
+      contents.forEach((content) => mx.sendMessage(roomId, content));
     };
 
     const submit = useCallback(() => {
@@ -319,7 +312,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
 
     const handleKeyDown: KeyboardEventHandler = useCallback(
       (evt) => {
-        if (enterForNewline ? isKeyHotkey('shift+enter', evt) : isKeyHotkey('enter', evt)) {
+        if (isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) {
           evt.preventDefault();
           submit();
         }
@@ -359,7 +352,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
       moveCursor(editor);
     };
 
-    const handleStickerSelect = async (mxc: string, shortcode: string) => {
+    const handleStickerSelect = async (mxc: string, shortcode: string, label: string) => {
       const stickerUrl = mx.mxcUrlToHttp(mxc);
       if (!stickerUrl) return;
 
@@ -369,7 +362,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
       );
 
       mx.sendEvent(roomId, EventType.Sticker, {
-        body: shortcode,
+        body: label,
         url: mxc,
         info,
       });
index 14996a9a300f21db31fc1004a616a6e34b6490d4..25d894f312a21ebe1832fbf17460d04d7d1cc0f2 100644 (file)
@@ -392,7 +392,7 @@ export const MessageDeleteItem = as<
                   variant="Critical"
                   before={
                     deleteState.status === AsyncStatus.Loading ? (
-                      <Spinner fill="Soft" variant="Critical" size="200" />
+                      <Spinner fill="Solid" variant="Critical" size="200" />
                     ) : undefined
                   }
                   aria-disabled={deleteState.status === AsyncStatus.Loading}
@@ -522,7 +522,7 @@ export const MessageReportItem = as<
                   variant="Critical"
                   before={
                     reportState.status === AsyncStatus.Loading ? (
-                      <Spinner fill="Soft" variant="Critical" size="200" />
+                      <Spinner fill="Solid" variant="Critical" size="200" />
                     ) : undefined
                   }
                   aria-disabled={
@@ -702,7 +702,7 @@ export const Message = as<'div', MessageProps>(
     );
 
     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;
       if (typeof tag === 'string' && tag.toLowerCase() === 'a') return;
       evt.preventDefault();
index 776e1d4d8ce428302b867aa46acec12b4ba090e9..0756c38ed30fe1d651c5646435b5391e64ca601c 100644 (file)
@@ -129,7 +129,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
 
     const handleKeyDown: KeyboardEventHandler = useCallback(
       (evt) => {
-        if (enterForNewline ? isKeyHotkey('shift+enter', evt) : isKeyHotkey('enter', evt)) {
+        if (isKeyHotkey('mod+enter', evt) || (!enterForNewline && isKeyHotkey('enter', evt))) {
           evt.preventDefault();
           handleSave();
         }
index bc32c1a34f049466435e25c39100c2cf9b11f6a0..17b914e5fd8e9e7de12cc95b71f94daeff9c309c 100644 (file)
@@ -68,7 +68,7 @@ export const Reactions = as<'div', ReactionsProps>(
               position="Top"
               tooltip={
                 <Tooltip style={{ maxWidth: toRem(200) }}>
-                  <Text size="T300">
+                  <Text className={css.ReactionsTooltipText} size="T300">
                     <ReactionTooltipMsg room={room} reaction={key} events={rEvents} />
                   </Text>
                 </Tooltip>
index 9cb0f2efa758dc68cd4f48b45094006024eb6410..a5f2f6b5efbcb8fb63f5f297c05644703049b6d7 100644 (file)
@@ -79,3 +79,7 @@ export const ReactionsContainer = style({
     },
   },
 });
+
+export const ReactionsTooltipText = style({
+  wordBreak: 'break-all',
+});
index e4cf1cbc085ac8bfcd9b47a1feaf13ded4599e9b..0760ec9e9748c20411f3643d0c2c507913733f6f 100644 (file)
@@ -54,23 +54,10 @@ export const getImageMsgContent = async (
   };
   if (imgEl) {
     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 = {
       ...getImageInfo(imgEl, file),
       [MATRIX_BLUR_HASH_PROPERTY_NAME]: blurHash,
-      ...thumbContent,
     };
   }
   if (encInfo) {
index 962a80b6635c64089890461f7cba0212c17545e5..ae094efff1bfe0abe422660ed4ec167f948a7511 100644 (file)
@@ -45,6 +45,8 @@ import CinnySVG from '../../../../public/res/svg/cinny.svg';
 import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
 import { useSetting } from '../../state/hooks/settings';
 import { settingsAtom } from '../../state/settings';
+import { isMacOS } from '../../utils/user-agent';
+import { KeySymbol } from '../../utils/key-symbol';
 
 function AppearanceSection() {
   const [, updateState] = useState({});
@@ -147,7 +149,7 @@ function AppearanceSection() {
               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
           title="Inline Markdown formatting"
index 6db7a3495f5156c1e405b2f18088ea5563a78dfe..9fda6794db3f61d9566d7a395f4ab15441414aa8 100644 (file)
@@ -23,24 +23,28 @@ export type RulesRunner = (
 ) => string | undefined;
 
 const MIN_ANY = '(.+?)';
+const URL_NEG_LB = '(?<!(https?|ftp|mailto|magnet):\\/\\/\\S*)';
 
 const BOLD_MD_1 = '**';
 const BOLD_PREFIX_1 = '\\*{2}';
 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 = {
   match: (text) => text.match(BOLD_REG_1),
   html: (parse, match) => {
     const [, g1] = match;
-    const child = parse(g1);
-    return `<strong data-md="${BOLD_MD_1}">${child}</strong>`;
+    return `<strong data-md="${BOLD_MD_1}">${parse(g1)}</strong>`;
   },
 };
 
 const ITALIC_MD_1 = '*';
 const ITALIC_PREFIX_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 = {
   match: (text) => text.match(ITALIC_REG_1),
   html: (parse, match) => {
@@ -52,7 +56,9 @@ const ItalicRule1: MDRule = {
 const ITALIC_MD_2 = '_';
 const ITALIC_PREFIX_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 = {
   match: (text) => text.match(ITALIC_REG_2),
   html: (parse, match) => {
@@ -65,7 +71,7 @@ const UNDERLINE_MD_1 = '__';
 const UNDERLINE_PREFIX_1 = '_{2}';
 const UNDERLINE_NEG_LA_1 = '(?!_)';
 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 = {
   match: (text) => text.match(UNDERLINE_REG_1),
@@ -78,7 +84,9 @@ const UnderlineRule: MDRule = {
 const STRIKE_MD_1 = '~~';
 const STRIKE_PREFIX_1 = '~{2}';
 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 = {
   match: (text) => text.match(STRIKE_REG_1),
   html: (parse, match) => {
@@ -90,7 +98,9 @@ const StrikeRule: MDRule = {
 const CODE_MD_1 = '`';
 const CODE_PREFIX_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 = {
   match: (text) => text.match(CODE_REG_1),
   html: (parse, match) => {
@@ -103,7 +113,7 @@ const SPOILER_MD_1 = '||';
 const SPOILER_PREFIX_1 = '\\|{2}';
 const SPOILER_NEG_LA_1 = '(?!\\|)';
 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 = {
   match: (text) => text.match(SPOILER_REG_1),