Repository: speee/jsx-slack (ts)

Issue:
<Escape> component will over-escape emoji shorthand
Now emoji shorthand such as `:+1:` is not included the escape target of `<Escape>` component. jsx-slack will render emoji even if wrapped in `<Escape>`.

Putting the discussion about whether escaping emoji shorthand aside, `<Escape>` component in the current specification is over-escaping emoji shorthand that has included underscore (`_`). Slack will render emoji consisted of a simple word such as `:cat:`, but emoji shorthand that has 2 and more words like `:arrow_down:` won't be rendered in `<Escape>` component because of over-escaped underscore.

If jsx-slack detected known emoji shorthand in `<Escape>` (with decision of keeping current specification), we have to disable underscore escaping only in emoji.

Pull request description:
Prevent over-escaping in valid emoji shorthand
Resolves #98.

`<Escape>` component and `<i>` intrinsic HTML tag will detect valid emoji shorthands (includes custom emoji) and leave underscore to render emoji correctly.

```jsx
<Blocks>
  <Section>
    <p>_foo:arrow_down:bar_</p>
    <p><i>foo:arrow_down:bar</i></p>
    <p><Escape>_foo:arrow_down:bar_</Escape></p>
  </Section>
</Blocks>
```

<table>
<tr>
<td>

![](https://user-images.githubusercontent.com/3993388/71466407-ca760800-2802-11ea-924d-95f6b6eee3d5.png)

</td>
<td>:arrow_right:</td>
<td>

![](https://user-images.githubusercontent.com/3993388/71466414-cfd35280-2802-11ea-971f-c761ed08ba30.png)

</td>
</tr>
</table>


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 300f4bb..82381e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@
 - Components for [the outdated dialog](https://api.slack.com/dialogs) provided in `@speee-js/jsx-slack/dialog` can no longer use ([#84](https://github.com/speee/jsx-slack/pull/84))
 - Drop Node 8 support ([#100](https://github.com/speee/jsx-slack/pull/100))
 
+### Fixed
+
+- Prevent over-escaping in valid emoji shorthand ([#98](https://github.com/speee/jsx-slack/issues/98), [#101](https://github.com/speee/jsx-slack/pull/101))
+
 ### Changed
 
 - Upgrade dependent packages to the latest version ([#92](https://github.com/speee/jsx-slack/pull/92))
diff --git a/docs/about-escape-and-exact-mode.md b/docs/about-escape-and-exact-mode.md
index 922bc22..18ea3c5 100644
--- a/docs/about-escape-and-exact-mode.md
+++ b/docs/about-escape-and-exact-mode.md
@@ -43,6 +43,8 @@ Other special chars will replace to another Unicode character whose similar shap
 
 These replacements also will trigger by using corresponded HTML tag. (e.g. `*` and `＊` in the contents of `<b>` tag)
 
+> ℹ️ Special characters in valid emoji shorthand won't be escaped. For example, we will leave underscore(s) of the shorthand such as `:white_check_mark:`, `:marca_de_verificación_blanca:` and `:チェックマーク_緑:`.
+
 ## Exact mode
 
 Some special characters will work only in breaks of words. Take a look this example:
diff --git a/src/html.tsx b/src/html.tsx
index a14a00b..99191f0 100644
--- a/src/html.tsx
+++ b/src/html.tsx
@@ -4,6 +4,8 @@ import formatDate from './date'
 import { JSXSlack, ParseContext } from './jsx'
 import { Html, detectSpecialLink } from './utils'
 
+const emojiShorthandRegex = /(:[-a-z0-9ÀÁÂÃÄÇÈÉÊËÍÎÏÑÓÔÕÖŒœÙÚÛÜŸßàáâãäçèéêëíîïñóôõöùúûüÿ_＿+＋'\u2e80-\u2fd5\u3005\u3041-\u3096\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcb\uff10-\uff19\uff41-\uff5a\uff61-\uff9f]+:)/
+
 export const escapeEntity = (str: string) =>
   str
     .replace(/&/g, '&amp;')
@@ -23,6 +25,28 @@ const buildAttr = (props: { [key: string]: any }) => {
   return attr
 }
 
+const escapeCharsDefaultReplacer = (partial: string) =>
+  partial
+    .replace(/^(&gt;|＞)/gm, (_, c) => `\u00ad${c}`)
+    .replace(/\*/g, '\u2217')
+    .replace(/＊/g, '\ufe61')
+    .replace(/_/g, '\u02cd')
+    .replace(/＿/g, '\u2e0f')
+    .replace(/[`｀]/g, '\u02cb')
+    .replace(/~/g, '\u223c')
+
+export const escapeChars = (
+  mrkdwn: string,
+  replacer: (partial: string) => string = escapeCharsDefaultReplacer
+) =>
+  mrkdwn
+    .split(emojiShorthandRegex)
+    .reduce(
+      (acc, str, i) => [...acc, i % 2 === 1 ? str : replacer(str)],
+      [] as string[]
+    )
+    .join('')
+
 export const parse = (
   name: string,
   props: Record<string, any>,
@@ -54,9 +78,10 @@ export const parse = (
     case 'em':
       if (isDescendant('i', 'em', 'time')) return text()
 
-      return `<i>${text()
-        .replace(/_/g, '\u02cd')
-        .replace(/＿/g, '\u2e0f')}</i>`
+      // Underscores have to avoid escaping in emoji shorthand
+      return `<i>${escapeChars(text(), t =>
+        t.replace(/_/g, '\u02cd').replace(/＿/g, '\u2e0f')
+      )}</i>`
     case 's':
     case 'strike':
     case 'del':
@@ -118,16 +143,6 @@ export const parse = (
 export const Escape: JSXSlack.FC<{ children: JSXSlack.Children<{}> }> = props =>
   JSXSlack.h(JSXSlack.NodeType.escapeInHtml, props)
 
-export const escapeChars = (mrkdwn: string) =>
-  mrkdwn
-    .replace(/^(&gt;|＞)/gm, (_, c) => `\u00ad${c}`)
-    .replace(/\*/g, '\u2217')
-    .replace(/＊/g, '\ufe61')
-    .replace(/_/g, '\u02cd')
-    .replace(/＿/g, '\u2e0f')
-    .replace(/[`｀]/g, '\u02cb')
-    .replace(/~/g, '\u223c')
-
 export default function html(children: JSXSlack.Children<{}>) {
   return JSXSlack(<Html>{children}</Html>)
 }


Test patch:
diff --git a/test/block-kit/builtin-components.tsx b/test/block-kit/builtin-components.tsx
index 94cbad2..866eaa9 100644
--- a/test/block-kit/builtin-components.tsx
+++ b/test/block-kit/builtin-components.tsx
@@ -42,6 +42,23 @@ describe('Built-in components', () => {
           }),
         }),
       ]))
+
+    it('ignores escaping underscore in valid emoji shorthand', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Section>
+              <Escape>_:arrow_down: :custom_emoji: :カスタム＿絵文字:_</Escape>
+            </Section>
+          </Blocks>
+        )
+      ).toStrictEqual([
+        expect.objectContaining({
+          text: expect.objectContaining({
+            text: '\u02cd:arrow_down: :custom_emoji: :カスタム＿絵文字:\u02cd',
+          }),
+        }),
+      ]))
   })
 
   describe('<Fragment> component', () => {
diff --git a/test/html.tsx b/test/html.tsx
index 240e0cd..ec833a9 100644
--- a/test/html.tsx
+++ b/test/html.tsx
@@ -72,6 +72,11 @@ describe('HTML parser for mrkdwn', () => {
       expect(html(<i>Hello, ＿World＿!</i>)).toBe('_Hello, \u2e0fWorld\u2e0f!_')
     })
 
+    it('does not escape underscore contained in valid emoji shorthand', () => {
+      expect(html(<i>:arrow_down:</i>)).toBe('_:arrow_down:_')
+      expect(html(<i>:絵＿文字:</i>)).toBe('_:絵＿文字:_')
+    })
+
     it('applies markup per each lines when text has multiline', () => {
       expect(
         html(