Repository: speee/jsx-slack (ts)

Issue:
Allow `verbatim={boolean}` parameter 
For text objects, Slack allows a parameter `verbatim` to be passed, allowing some elements to be parsed by slack, without needing specific mrkdwn syntax (e.g. channels, users, urls). From Slack's doc on text object: https://api.slack.com/reference/block-kit/composition-objects#text


When set to false (as is default) URLs will be auto-converted into links, conversation names will be link-ified, and certain mentions will be automatically parsed. Using a value of true will skip any preprocessing of this nature, although you can still include manual parsing strings. This field is only usable when typeis mrkdwn.


It would be ideal to be able to use this attribute for components that support a text object (e.g. Section, Context, Fields (and pass them down to child elements). A specific use case where this would be ideal is rendering dynamic input in slack. If the dynamic content contains a URL, passing the text into a `jsx-slack` component such as a `<Button>` means you would first have to search for url's and wrap them with `<a>...</a>` to get them to render as links in Slack. Ideally you could opt into the `verbatim` parameter to simply pass the raw text to Slack to parse



Pull request description:
Add MrkDwn component
Resolves #73 

Added `<MrkDwn>` component and added logic to `<Section>` and `<Context>` blocks to handle `<MrkDwn>` component.

You can now use `<MrkDwn>` in a `<Section>` block
```
<Block>
  <Section>
    <MrkDwn verbatim={false}>
      Hello @user!
    </MrkDwn>
  </Section>
</Block>
```
which converts to
```
[{
  type: 'section',
  text: { type: 'mrkdwn', text: 'Hello!', verbatim: false }
}]
```


or a `<Context>` block
```
<Blocks>
  <Context>
    <MrkDwn verbatim={false}>
      Hello @user
    </MrkDwn>
    Welcome
  </Context>
</Blocks>
```
which converts to
```
[{
  type: 'context',
  elements: [
    {type: 'mrkdwn', text: 'Hello', verbatim: false},
    {type: 'mrkdwn', text: 'World', verbatim: true}
  ]
}]
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e17bef6..bcf5c7d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## [Unreleased]
 
+### Added
+
+- <Mrkdwn> component for use with <Section> and <Context> ([#73](https://github.com/speee/jsx-slack/issues/73), [#97](https://github.com/speee/jsx-slack/pull/97))
+
 ### Breaking
 
 - 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))
diff --git a/docs/block-elements.md b/docs/block-elements.md
index e5d76fc..23c3dc8 100644
--- a/docs/block-elements.md
+++ b/docs/block-elements.md
@@ -501,6 +501,24 @@ You can use [HTML-like formatting](./html-like-formatting.md) to the content of
 - `confirm` (**required**): A text content of the button to confirm.
 - `deny` (**required**): A text content of the button to cancel.
 
+### <a name="mrkdwn_component" id="mrkdwn_component"></a> [`<Mrkdwn>`: Markdown Composition Component](https://api.slack.com/reference/block-kit/composition-objects#text)
+
+Mrkdwn is a text composition component is used when its needed to have a Text object and explicitly set the `verbatim` property. Setting `verbatim` to false will tell Slack to auto-convert links, conversaion names, and certain mentions to be linkified and automatically parsed. If `verbatim` set to true Slack will skip any prepreprocessing.
+
+_Note: Slack recommends disabling automatic parsing on Text composition components and they have made it clear that they might deprecate this feature in the future. More information can be found <a href="https://api.slack.com/reference/surfaces/formatting#why_you_should_consider_disabling_automatic_parsing">here</a>_.
+
+```jsx
+<Blocks>
+  <Section>
+    <Mrkdwn verbatim={false}>Hello @user!</Mrkdwn>
+  </Section>
+</Blocks>
+```
+
+### Props
+
+- verbatim: (optional): A boolean prop to specify whether or not Slack auto-convert links, conversaion names, and mentions.
+
 ## Input components for modal
 
 **Input components** are available only for [`<Modal>`](block-containers.md#modal). These include a part of [interactive components](#interactive-components) and dedicated components such as [`<Input>`](#input) and [`<Textarea>`](#textarea).
diff --git a/src/block-kit/Context.tsx b/src/block-kit/Context.tsx
index 9dd7a33..f8cc755 100644
--- a/src/block-kit/Context.tsx
+++ b/src/block-kit/Context.tsx
@@ -41,6 +41,17 @@ export const Context: JSXSlack.FC<BlockComponentProps & {
           alt_text: props.alt_text,
         }
 
+      // A converted <MrkDwn> component
+      if (
+        child.type === JSXSlack.NodeType.object &&
+        props.type === 'mrkdwn_component'
+      )
+        return {
+          type: 'mrkdwn' as const,
+          text: props.text,
+          verbatim: props.verbatim,
+        }
+
       return undefined
     })()
 
diff --git a/src/block-kit/Section.tsx b/src/block-kit/Section.tsx
index 5a8d4d6..e22018e 100644
--- a/src/block-kit/Section.tsx
+++ b/src/block-kit/Section.tsx
@@ -34,18 +34,24 @@ export const Section: JSXSlack.FC<BlockComponentProps & {
 
   let accessory: SectionBlock['accessory']
   let fields: SectionBlock['fields']
+  let text: MrkdwnElement | string | undefined
 
   for (const child of JSXSlack.normalizeChildren(children)) {
     let eaten = false
-
     if (typeof child === 'object') {
-      // Accessory and fields
+      // Accessory, fields, and Mrkdwn
       if (child.type === JSXSlack.NodeType.object) {
         if (sectionAccessoryTypes.includes(child.props.type)) {
           accessory = JSXSlack(child)
         } else if (child.props.type === 'mrkdwn') {
           if (!fields) fields = []
           fields.push(child.props)
+        } else if (child.props.type === 'mrkdwn_component') {
+          text = {
+            type: 'mrkdwn',
+            text: child.props.text,
+            verbatim: child.props.verbatim,
+          }
         } else {
           throw new Error('<Section> has unexpected component as an accessory.')
         }
@@ -67,13 +73,20 @@ export const Section: JSXSlack.FC<BlockComponentProps & {
     if (!eaten) normalized.push(child)
   }
 
-  const text = html(normalized)
+  if (!text) text = html(normalized)
+
+  let sectionText
+  if (typeof text === 'object') {
+    sectionText = text
+  } else if (text && typeof text === 'string') {
+    sectionText = { text, type: 'mrkdwn', verbatim: true }
+  }
 
   return (
     <ObjectOutput<SectionBlock>
       type="section"
       block_id={id || blockId}
-      text={text ? { text, type: 'mrkdwn', verbatim: true } : undefined}
+      text={sectionText}
       accessory={accessory}
       fields={fields}
     />
diff --git a/src/block-kit/composition/Confirm.tsx b/src/block-kit/composition/Confirm.tsx
index af30ee6..394d28a 100644
--- a/src/block-kit/composition/Confirm.tsx
+++ b/src/block-kit/composition/Confirm.tsx
@@ -1,5 +1,5 @@
 /** @jsx JSXSlack.h */
-import { Confirm as SlackConfirm } from '@slack/types'
+import { Confirm as SlackConfirm, MrkdwnElement } from '@slack/types'
 import html from '../../html'
 import { JSXSlack } from '../../jsx'
 import { ObjectOutput } from '../../utils'
@@ -12,11 +12,25 @@ export interface ConfirmProps {
   title: string
 }
 
-export const Confirm: JSXSlack.FC<ConfirmProps> = props => (
-  <ObjectOutput<SlackConfirm>
-    title={plainText(props.title)}
-    text={mrkdwn(html(props.children))}
-    confirm={plainText(props.confirm)}
-    deny={plainText(props.deny)}
-  />
-)
+export const Confirm: JSXSlack.FC<ConfirmProps> = props => {
+  let confirmTextObject: string | MrkdwnElement = mrkdwn(html(props.children))
+
+  for (const child of JSXSlack.normalizeChildren(props.children)) {
+    if (typeof child === 'object' && child.props.type === 'mrkdwn_component') {
+      confirmTextObject = {
+        type: 'mrkdwn',
+        text: child.props.text,
+        verbatim: child.props.verbatim,
+      }
+    }
+  }
+
+  return (
+    <ObjectOutput<SlackConfirm>
+      title={plainText(props.title)}
+      text={confirmTextObject}
+      confirm={plainText(props.confirm)}
+      deny={plainText(props.deny)}
+    />
+  )
+}
diff --git a/src/block-kit/composition/Mrkdwn.tsx b/src/block-kit/composition/Mrkdwn.tsx
new file mode 100644
index 0000000..b379592
--- /dev/null
+++ b/src/block-kit/composition/Mrkdwn.tsx
@@ -0,0 +1,25 @@
+/** @jsx JSXSlack.h */
+import { JSXSlack } from '../../jsx'
+import { ObjectOutput } from '../../utils'
+import html from '../../html'
+
+interface MrkdwnProps {
+  children: JSXSlack.Children<{}>
+  verbatim?: boolean
+}
+
+interface MrkdwnComponentProps {
+  type: 'mrkdwn_component'
+  text: string
+  verbatim?: boolean
+}
+
+export const Mrkdwn: JSXSlack.FC<MrkdwnProps> = ({ children, verbatim }) => {
+  return (
+    <ObjectOutput<MrkdwnComponentProps>
+      type="mrkdwn_component"
+      text={html(children)}
+      verbatim={verbatim}
+    />
+  )
+}
diff --git a/src/block-kit/index.ts b/src/block-kit/index.ts
index 1176425..ecf77e2 100644
--- a/src/block-kit/index.ts
+++ b/src/block-kit/index.ts
@@ -11,6 +11,7 @@ export { File } from './File'
 export { Image } from './Image'
 export { Input, Textarea } from './Input'
 export { Section, Field } from './Section'
+export { Mrkdwn } from './composition/Mrkdwn'
 
 // Block elements
 export { Button } from './elements/Button'


Test patch:
diff --git a/test/block-kit/block-elements/composition-objects.tsx b/test/block-kit/block-elements/composition-objects.tsx
index d40a48c..295385f 100644
--- a/test/block-kit/block-elements/composition-objects.tsx
+++ b/test/block-kit/block-elements/composition-objects.tsx
@@ -1,5 +1,14 @@
 /** @jsx JSXSlack.h */
-import JSXSlack, { Actions, Blocks, Button, Confirm } from '../../../src/index'
+import JSXSlack, {
+  Actions,
+  Blocks,
+  Button,
+  Confirm,
+  Image,
+  Mrkdwn,
+  Section,
+  Context,
+} from '../../../src/index'
 
 beforeEach(() => JSXSlack.exactMode(false))
 
@@ -66,5 +75,144 @@ describe('Composition objects', () => {
           },
         ]
       `))
+    it('outputs action included <Confirm> object with a <Mrkdwn> child', () => {
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <Button
+                confirm={
+                  <Confirm
+                    title="Share to SNS"
+                    confirm="Yes, please"
+                    deny="Cancel"
+                  >
+                    <Mrkdwn verbatim={false}>submit @user</Mrkdwn>
+                  </Confirm>
+                }
+              >
+                Share
+              </Button>
+            </Actions>
+          </Blocks>
+        )
+      ).toEqual([
+        {
+          block_id: 'actions',
+          elements: [
+            {
+              confirm: {
+                confirm: {
+                  emoji: true,
+                  text: 'Yes, please',
+                  type: 'plain_text',
+                },
+                deny: {
+                  emoji: true,
+                  text: 'Cancel',
+                  type: 'plain_text',
+                },
+                text: {
+                  text: 'submit @user',
+                  type: 'mrkdwn',
+                  verbatim: false,
+                },
+                title: {
+                  emoji: true,
+                  text: 'Share to SNS',
+                  type: 'plain_text',
+                },
+              },
+              text: {
+                emoji: true,
+                text: 'Share',
+                type: 'plain_text',
+              },
+              type: 'button',
+            },
+          ],
+          type: 'actions',
+        },
+      ])
+    })
+    describe('<Mrkdwn>', () => {
+      it('outputs a <Section> block with a <Mrkdwn> component and preserves the verbatim prop', () => {
+        expect(
+          JSXSlack(
+            <Blocks>
+              <Section>
+                <Mrkdwn verbatim={false}>Hello!</Mrkdwn>
+              </Section>
+            </Blocks>
+          )
+        ).toEqual([
+          {
+            type: 'section',
+            text: { type: 'mrkdwn', text: 'Hello!', verbatim: false },
+          },
+        ])
+      })
+      it('outputs a <Context> block with an image and multiple mrkdwn elements', () => {
+        expect(
+          JSXSlack(
+            <Blocks>
+              <Context>
+                <Image src="https://example.com/test.jpg" alt="Test Image" />
+                Supporting <code>context</code> block would be hard!
+                <Mrkdwn verbatim={false}>
+                  <i>@here</i>
+                </Mrkdwn>
+                :dizzy_face:
+              </Context>
+            </Blocks>
+          )
+        ).toEqual([
+          {
+            type: 'context',
+            elements: [
+              {
+                type: 'image',
+                image_url: 'https://example.com/test.jpg',
+                alt_text: 'Test Image',
+              },
+              {
+                type: 'mrkdwn',
+                text: 'Supporting `context` block would be hard!',
+                verbatim: true,
+              },
+              {
+                type: 'mrkdwn',
+                text: '_@here_',
+                verbatim: false,
+              },
+              {
+                type: 'mrkdwn',
+                text: ':dizzy_face:',
+                verbatim: true,
+              },
+            ],
+          },
+        ])
+      })
+      it('converts <Mrkdwn> elements into mrkdwn elements and preserves the verbatim prop', () =>
+        expect(
+          JSXSlack(
+            <Blocks>
+              <Context>
+                <Mrkdwn verbatim={false}>Hello</Mrkdwn>
+                World
+              </Context>
+            </Blocks>
+          )
+        ).toEqual([
+          {
+            type: 'context',
+            elements: [
+              { type: 'mrkdwn', text: 'Hello', verbatim: false },
+              { type: 'mrkdwn', text: 'World', verbatim: true },
+            ],
+          },
+        ]))
+    })
   })
 })
diff --git a/test/block-kit/layout-blocks.tsx b/test/block-kit/layout-blocks.tsx
index 8cd3e9a..0d114e5 100644
--- a/test/block-kit/layout-blocks.tsx
+++ b/test/block-kit/layout-blocks.tsx
@@ -394,7 +394,6 @@ describe('Layout blocks', () => {
           ],
         },
       ]))
-
     it('throws error when the number of elements is 11', () =>
       expect(() =>
         JSXSlack(