Skip to content
Snippets Groups Projects
Unverified Commit d8cf20c6 authored by Noah Moss's avatar Noah Moss Committed by GitHub
Browse files

Markdown->Slack refactor and fix for nested bold/italic rendering (#21905)

* refactor markdown->slack code to use a multimethod

* solve nested bold & italic issue

* fix docstring position

* fix tests

* restore a couple of comments

* replace mrkdwn with slack in tests
parent acdf2204
No related branches found
No related tags found
No related merge requests found
......@@ -132,7 +132,7 @@
:attachment-name "image.png"
:channel-id channel-id
:fallback card-name}
(let [mrkdwn (markdown/process-markdown (:text card-result) :mrkdwn)]
(let [mrkdwn (markdown/process-markdown (:text card-result) :slack)]
(when (not (str/blank? mrkdwn))
{:blocks [{:type "section"
:text {:type "mrkdwn"
......
......@@ -2,6 +2,7 @@
(:require [clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.walk :as walk]
[metabase.public-settings :as public-settings])
(:import [com.vladsch.flexmark.ast AutoLink BlockQuote BulletList BulletListItem Code Emphasis FencedCodeBlock
HardLineBreak Heading HtmlBlock HtmlCommentBlock HtmlEntity HtmlInline HtmlInlineBase HtmlInlineComment
......@@ -37,8 +38,8 @@
Emphasis :italic
OrderedList :ordered-list
BulletList :unordered-list
OrderedListItem :ordered-list-item
BulletListItem :unordered-list-item
OrderedListItem :list-item
BulletListItem :list-item
Code :code
FencedCodeBlock :codeblock
IndentedCodeBlock :codeblock
......@@ -203,113 +204,174 @@
(.. (URI. site-url) (resolve uri) toString)
uri))))
(defn- ast->mrkdwn
"Takes an AST representing Markdown input, and converts it to a mrkdwn string that will render nicely in Slack.
(defn- ^:private strip-tag
"Given the value from the :content field of a Markdown AST node, and a keyword representing a tag type, converts all
instances of the tag in the content to `:default` tags. This is used to suppress rendering of nested bold and italic
tags, which Slack doesn't support."
[content tag]
(walk/postwalk
(fn [node]
(if (and (map? node) (= (:tag node) tag))
(assoc node :tag :default)
node))
content))
(defmulti ast->slack
"Takes an AST representing Markdown input, and converts it to a string that will render nicely in Slack.
Some of the differences to Markdown include:
* All headers are just rendered as bold text.
* Ordered and unordered lists are printed in plain text.
* Inline images are rendered as text that links to the image source, e.g. <image.png|[Image: alt-text]>."
[{:keys [tag attrs content]}]
(let [resolved-content (if (string? content)
(escape-text content)
(map #(if (string? %)
(escape-text %)
(ast->mrkdwn %))
content))
joined-content (str/join resolved-content)]
(case tag
:document
joined-content
:paragraph
(str joined-content "\n")
:soft-line-break
" "
:hard-line-break
"\n"
:horizontal-line
"\n───────────────────\n"
:heading
(str "*" joined-content "*\n")
:bold
(str "*" joined-content "*")
:italic
(str "_" joined-content "_")
:code
(str "`" joined-content "`")
:codeblock
(str "```\n" joined-content "```")
:blockquote
(let [lines (str/split-lines joined-content)]
(str/join "\n" (map #(str ">" %) lines)))
:link
(let [resolved-uri (resolve-uri (:href attrs))]
(if (contains? #{:image :image-ref} (:tag (first content)))
;; If this is a linked image, add link target on separate line after image placeholder
(str joined-content "\n(" resolved-uri ")")
(str "<" resolved-uri "|" joined-content ">")))
:link-ref
(if-let [resolved-uri (resolve-uri (-> attrs :reference :attrs :url))]
(str "<" resolved-uri "|" joined-content ">")
;; If this was parsed as a link-ref but has no reference, assume it was just a pair of square brackets and
;; restore them. This is a known discrepency between flexmark-java and Markdown rendering on the frontend.
(str "[" joined-content "]"))
:auto-link
(str "<" (:href attrs) ">")
:mail-link
(str "<mailto:" (:address attrs) "|" (:address attrs) ">")
;; list items might have nested lists or other elements, which should have their indentation level increased
(:unordered-list-item :ordered-list-item)
(let [indented-content (->> (rest resolved-content)
str/join
str/split-lines
(map #(str " " %))
(str/join "\n"))]
(if-not (str/blank? indented-content)
(str (first resolved-content) indented-content "\n")
joined-content))
:unordered-list
(str/join (map #(str "• " %) resolved-content))
:ordered-list
(str/join (map-indexed #(str (inc %1) ". " %2) resolved-content))
;; Replace images with text that links to source, including alt text if available
:image
(let [{:keys [src alt]} attrs]
(if (str/blank? alt)
(str "<" src "|[Image]>")
(str "<" src "|[Image: " alt "]>")))
:image-ref
(let [src (-> attrs :reference :attrs :url)
alt joined-content]
(if (str/blank? alt)
(str "<" src "|[Image]>")
(str "<" src "|[Image: " alt "]>")))
:html-entity
(some->> content
(get @html-entities)
(:characters))
joined-content)))
* All headers are just rendered as bold text.
* Ordered and unordered lists are printed in plain text.
* Inline images are rendered as text that links to the image source, e.g. <image.png|[Image: alt-text]>."
:tag)
(defn ^:private resolved-content
"Given the value from the :content field of a Markdown AST node, recursively resolves subnodes into a nested list of
strings."
[content]
(if (string? content)
(escape-text content)
(map #(if (string? %)
(escape-text %)
(ast->slack %))
content)))
(defn ^:private resolved-content-string
"Given the resolved content of a Markdown AST node, converts it into a single flattened string. This is used for
rendering a couple specific types of nodes, such as list items."
[resolved-content]
(-> resolved-content
flatten
str/join))
(defn ^:private resolved-lines
"Given the value from the :content field of a Markdown AST node, recursively resolves it and returns a list of
strings corresponding to individual lines in the result."
[content]
(-> content
resolved-content
resolved-content-string
str/split-lines))
(defmethod ast->slack :default
[{content :content}]
(resolved-content content))
(defmethod ast->slack :document
[{content :content}]
(resolved-content content))
(defmethod ast->slack :paragraph
[{content :content}]
[(resolved-content content) "\n"])
(defmethod ast->slack :soft-line-break
[_]
" ")
(defmethod ast->slack :hard-line-break
[_]
"\n")
(defmethod ast->slack :horizontal-line
[_]
"\n───────────────────\n")
(defmethod ast->slack :heading
[{content :content}]
["*" (resolved-content content) "*\n"])
(defmethod ast->slack :bold
[{content :content}]
["*" (resolved-content (strip-tag content :bold)) "*"])
(defmethod ast->slack :italic
[{content :content}]
["_" (resolved-content (strip-tag content :italic)) "_"])
(defmethod ast->slack :code
[{content :content}]
["`" (resolved-content content) "`"])
(defmethod ast->slack :codeblock
[{content :content}]
["```\n" (resolved-content content) "```"])
(defmethod ast->slack :blockquote
[{content :content}]
(let [lines (resolved-lines content)]
(interpose "\n" (map (fn [line] [">" line]) lines))))
(defmethod ast->slack :link
[{:keys [content attrs]}]
(let [resolved-uri (resolve-uri (:href attrs))
resolved-content (resolved-content content)]
(if (contains? #{:image :image-ref} (:tag (first content)))
;; If this is a linked image, add link target on separate line after image placeholder
[resolved-content "\n(" resolved-uri ")"]
["<" resolved-uri "|" resolved-content ">"])))
(defmethod ast->slack :link-ref
[{:keys [content attrs]}]
(let [resolved-uri (resolve-uri (-> attrs :reference :attrs :url))
resolved-content (resolved-content content)]
(if resolved-uri
["<" resolved-uri "|" resolved-content ">"]
;; If this was parsed as a link-ref but has no reference, assume it was just a pair of square brackets and
;; restore them. This is a known discrepency between flexmark-java and Markdown rendering on the frontend.
["[" resolved-content "]"])))
(defmethod ast->slack :auto-link
[{{href :href} :attrs}]
["<" href ">"])
(defmethod ast->slack :mail-link
[{{address :address} :attrs}]
["<mailto:" address "|" address ">"])
(defmethod ast->slack :list-item
[{content :content}]
(let [resolved-content (resolved-content content)
;; list items might have nested lists or other elements, which should have their indentation level increased
indented-content (->> (rest resolved-content)
resolved-content-string
str/split-lines
(map #(str " " %))
(str/join "\n"))]
(if-not (str/blank? indented-content)
[(first resolved-content) indented-content "\n"]
resolved-content)))
(defmethod ast->slack :unordered-list
[{content :content}]
(map (fn [list-item] ["• " list-item])
(resolved-content content)))
(defmethod ast->slack :ordered-list
[{content :content}]
(map-indexed (fn [idx list-item] [(inc idx) ". " list-item])
(resolved-content content)))
(defmethod ast->slack :image
[{{:keys [src alt]} :attrs}]
;; Replace images with text that links to source, including alt text if available
(if (str/blank? alt)
["<" src "|[Image]>"]
["<" src "|[Image: " alt "]>"]))
(defmethod ast->slack :image-ref
[{:keys [content attrs]}]
(let [src (-> attrs :reference :attrs :url)
alt (-> content resolved-content resolved-content-string)]
(if (str/blank? alt)
["<" src "|[Image]>"]
["<" src "|[Image: " alt "]>"])))
(defmethod ast->slack :html-entity
[{content :content}]
(some->> content
(get @html-entities)
(:characters)))
(defn- empty-link-ref?
"Returns true if this node was parsed as a link ref, but has no references. This probably means the original text
......@@ -342,14 +404,16 @@
(defmulti process-markdown
"Converts a markdown string from a virtual card into a form that can be sent to a channel
(mrkdwn for Slack; HTML for email)."
(Slack's markup language, or HTML for email)."
(fn [_markdown channel-type] channel-type))
(defmethod process-markdown :mrkdwn
(defmethod process-markdown :slack
[markdown _]
(-> (.parse ^Parser parser ^String markdown)
to-clojure
ast->mrkdwn
ast->slack
flatten
str/join
str/trim))
(defmethod process-markdown :html
......
......@@ -4,9 +4,9 @@
[metabase.pulse.markdown :as markdown]
[metabase.test.util :as tu]))
(defn- mrkdwn
(defn- slack
[markdown]
(markdown/process-markdown markdown :mrkdwn))
(markdown/process-markdown markdown :slack))
(defn- escape
[text]
......@@ -14,162 +14,172 @@
(deftest process-markdown-slack-test
(testing "Headers are converted to bold text"
(is (= "*header*" (mrkdwn "# header")))
(is (= "*header*" (mrkdwn "## header")))
(is (= "*header*" (mrkdwn "### header")))
(is (= "*header*" (mrkdwn "#### header")))
(is (= "*header*" (mrkdwn "##### header")))
(is (= "*header*" (mrkdwn "###### header")))
(is (= "*header*" (mrkdwn "header\n=========")))
(is (= "*header*" (mrkdwn "header\n---------")))
(is (= "*header*\ncontent" (mrkdwn "# header\ncontent"))))
(is (= "*header*" (slack "# header")))
(is (= "*header*" (slack "## header")))
(is (= "*header*" (slack "### header")))
(is (= "*header*" (slack "#### header")))
(is (= "*header*" (slack "##### header")))
(is (= "*header*" (slack "###### header")))
(is (= "*header*" (slack "header\n=========")))
(is (= "*header*" (slack "header\n---------")))
(is (= "*header*\ncontent" (slack "# header\ncontent"))))
(testing "Bold and italic text uses Slack's syntax"
(is (= "*bold*" (mrkdwn "**bold**")))
(is (= "*bold*" (mrkdwn "__bold__")))
(is (= "_italic_" (mrkdwn "*italic*")))
(is (= "_italic_" (mrkdwn "_italic_")))
(is (= "_*both*_" (mrkdwn "***both***")))
(is (= "*_both_*" (mrkdwn "__*both*__")))
(is (= "*_both_*" (mrkdwn "**_both_**")))
(is (= "_*both*_" (mrkdwn "___both___"))))
(is (= "*bold*" (slack "**bold**")))
(is (= "*bold*" (slack "__bold__")))
(is (= "_italic_" (slack "*italic*")))
(is (= "_italic_" (slack "_italic_")))
(is (= "_*both*_" (slack "***both***")))
(is (= "*_both_*" (slack "__*both*__")))
(is (= "*_both_*" (slack "**_both_**")))
(is (= "_*both*_" (slack "___both___"))))
(testing "Nested bold or italic only render the top-level syntax"
(is (= "*bold extra bold*" (slack "**bold **extra bold****")))
(is (= "*bold extra extra bold*" (slack "**bold **extra **extra bold******")))
(is (= "*bold extra bold*" (slack "__bold __extra bold____")))
(is (= "*bold extra extra bold*" (slack "__bold __extra __extra bold______")))
(is (= "_italic extra italic_" (slack "*italic *extra italic**")))
(is (= "_italic extra extra italic_" (slack "*italic *extra *extra italic***")))
(is (= "_italic extra italic_" (slack "_italic _extra italic__")))
(is (= "_italic extra extra italic_" (slack "_italic _extra _extra italic___"))))
(testing "Lines are correctly split or joined"
(is (= "foo bar" (mrkdwn "foo\nbar")))
(is (= "foo\nbar" (mrkdwn "foo\n\nbar")))
(is (= "foo\nbar" (mrkdwn "foo \nbar")))
(is (= "foo\nbar" (mrkdwn "foo\\\nbar"))))
(is (= "foo bar" (slack "foo\nbar")))
(is (= "foo\nbar" (slack "foo\n\nbar")))
(is (= "foo\nbar" (slack "foo \nbar")))
(is (= "foo\nbar" (slack "foo\\\nbar"))))
(testing "Horizontal lines are created using box drawing characters"
(is (= "───────────────────" (mrkdwn "----")))
(is (= "text\n\n───────────────────\ntext" (mrkdwn "text\n\n----\ntext"))))
(is (= "───────────────────" (slack "----")))
(is (= "text\n\n───────────────────\ntext" (slack "text\n\n----\ntext"))))
(testing "Code blocks are preserved"
(is (= "`code`" (mrkdwn "`code`")))
(is (= "```\ncode\nblock```" (mrkdwn " code\n block")))
(is (= "```\ncode\nblock\n```" (mrkdwn "```\ncode\nblock\n```")))
(is (= "```\ncode\nblock\n```" (mrkdwn "```lang\ncode\nblock\n```")))
(is (= "```\ncode\nblock\n```" (mrkdwn "~~~\ncode\nblock\n~~~"))))
(is (= "`code`" (slack "`code`")))
(is (= "```\ncode\nblock```" (slack " code\n block")))
(is (= "```\ncode\nblock\n```" (slack "```\ncode\nblock\n```")))
(is (= "```\ncode\nblock\n```" (slack "```lang\ncode\nblock\n```")))
(is (= "```\ncode\nblock\n```" (slack "~~~\ncode\nblock\n~~~"))))
(testing "Blockquotes are preserved"
(is (= ">block" (mrkdwn ">block")))
(is (= ">block" (mrkdwn "> block")))
(is (= ">block quote" (mrkdwn ">block\n>quote")))
(is (= ">block quote" (mrkdwn ">block\n>quote")))
(is (= ">• quoted\n>• list" (mrkdwn "> * quoted\n> * list")))
(is (= ">1. quoted\n>2. list" (mrkdwn "> 1. quoted\n> 1. list")))
(is (= ">quote\n>• footer" (mrkdwn ">quote\n>- footer"))))
(is (= ">block" (slack ">block")))
(is (= ">block" (slack "> block")))
(is (= ">block quote" (slack ">block\n>quote")))
(is (= ">block quote" (slack ">block\n>quote")))
(is (= ">• quoted\n>• list" (slack "> * quoted\n> * list")))
(is (= ">1. quoted\n>2. list" (slack "> 1. quoted\n> 1. list")))
(is (= ">quote\n>• footer" (slack ">quote\n>- footer"))))
(testing "Links use Slack's syntax, tooltips are dropped, link formatting is preserved"
(is (= "<https://metabase.com|Metabase>" (mrkdwn "[Metabase](https://metabase.com)")))
(is (= "<https://metabase.com|Metabase>" (mrkdwn "[Metabase](https://metabase.com \"tooltip\")")))
(is (= "<https://metabase.com|_Metabase_>" (mrkdwn "[*Metabase*](https://metabase.com)")))
(is (= "<https://metabase.com|_Metabase_>" (mrkdwn "[_Metabase_](https://metabase.com)")))
(is (= "<https://metabase.com|*Metabase*>" (mrkdwn "[**Metabase**](https://metabase.com)")))
(is (= "<https://metabase.com|*Metabase*>" (mrkdwn "[__Metabase__](https://metabase.com)")))
(is (= "<https://metabase.com|`Metabase`>" (mrkdwn "[`Metabase`](https://metabase.com)"))))
(is (= "<https://metabase.com|Metabase>" (slack "[Metabase](https://metabase.com)")))
(is (= "<https://metabase.com|Metabase>" (slack "[Metabase](https://metabase.com \"tooltip\")")))
(is (= "<https://metabase.com|_Metabase_>" (slack "[*Metabase*](https://metabase.com)")))
(is (= "<https://metabase.com|_Metabase_>" (slack "[_Metabase_](https://metabase.com)")))
(is (= "<https://metabase.com|*Metabase*>" (slack "[**Metabase**](https://metabase.com)")))
(is (= "<https://metabase.com|*Metabase*>" (slack "[__Metabase__](https://metabase.com)")))
(is (= "<https://metabase.com|`Metabase`>" (slack "[`Metabase`](https://metabase.com)"))))
(testing "Relative links are resolved to the current site URL"
(tu/with-temporary-setting-values [site-url "https://example.com"]
(is (= "<https://example.com/foo|Metabase>" (mrkdwn "[Metabase](/foo)")))))
(is (= "<https://example.com/foo|Metabase>" (slack "[Metabase](/foo)")))))
(testing "Auto-links are preserved"
(is (= "<http://metabase.com>" (mrkdwn "<http://metabase.com>")))
(is (= "<mailto:test@metabase.com>" (mrkdwn "<mailto:test@metabase.com>")))
(is (= "<mailto:test@metabase.com|test@metabase.com>" (mrkdwn "<test@metabase.com>"))))
(is (= "<http://metabase.com>" (slack "<http://metabase.com>")))
(is (= "<mailto:test@metabase.com>" (slack "<mailto:test@metabase.com>")))
(is (= "<mailto:test@metabase.com|test@metabase.com>" (slack "<test@metabase.com>"))))
(testing "Bare URLs and email addresses are parsed as links"
(is (= "<https://metabase.com>" (mrkdwn "https://metabase.com")))
(is (= "<mailto:test@metabase.com|test@metabase.com>" (mrkdwn "test@metabase.com"))))
(is (= "<https://metabase.com>" (slack "https://metabase.com")))
(is (= "<mailto:test@metabase.com|test@metabase.com>" (slack "test@metabase.com"))))
(testing "Link references render as normal links"
(is (= "<https://metabase.com|metabase>" (mrkdwn "[metabase]: https://metabase.com\n[metabase]")))
(is (= "<https://metabase.com|Metabase>" (mrkdwn "[Metabase]: https://metabase.com\n[Metabase]")))
(is (= "<https://metabase.com|Metabase>" (mrkdwn "[METABASE]: https://metabase.com\n[Metabase]")))
(is (= "<https://metabase.com|Metabase>" (mrkdwn "[Metabase]: https://metabase.com \"tooltip\"\n[Metabase]"))))
(is (= "<https://metabase.com|metabase>" (slack "[metabase]: https://metabase.com\n[metabase]")))
(is (= "<https://metabase.com|Metabase>" (slack "[Metabase]: https://metabase.com\n[Metabase]")))
(is (= "<https://metabase.com|Metabase>" (slack "[METABASE]: https://metabase.com\n[Metabase]")))
(is (= "<https://metabase.com|Metabase>" (slack "[Metabase]: https://metabase.com \"tooltip\"\n[Metabase]"))))
(testing "Lists are rendered correctly using raw text"
(is (= "• foo\n• bar" (mrkdwn "* foo\n* bar")))
(is (= "• foo\n• bar" (mrkdwn "- foo\n- bar")))
(is (= "• foo\n• bar" (mrkdwn "+ foo\n+ bar")))
(is (= "1. foo\n2. bar" (mrkdwn "1. foo\n2. bar")))
(is (= "1. foo\n2. bar" (mrkdwn "1. foo\n1. bar"))))
(is (= "• foo\n• bar" (slack "* foo\n* bar")))
(is (= "• foo\n• bar" (slack "- foo\n- bar")))
(is (= "• foo\n• bar" (slack "+ foo\n+ bar")))
(is (= "1. foo\n2. bar" (slack "1. foo\n2. bar")))
(is (= "1. foo\n2. bar" (slack "1. foo\n1. bar"))))
(testing "Nested lists are rendered correctly"
(is (= "1. foo\n 1. bar" (mrkdwn "1. foo\n 1. bar")))
(is (= "• foo\n • bar" (mrkdwn "* foo\n * bar")))
(is (= "1. foo\n • bar" (mrkdwn "1. foo\n * bar")))
(is (= "• foo\n 1. bar" (mrkdwn "* foo\n 1. bar")))
(is (= "• foo\n 1. bar\n 2. baz" (mrkdwn "* foo\n 1. bar\n 2. baz")))
(is (= "• foo\n 1. bar\n• baz" (mrkdwn "* foo\n 1. bar\n* baz")))
(is (= "• foo\n >quote" (mrkdwn "* foo\n >quote")))
(is (= "• foo\n ```\n codeblock\n ```" (mrkdwn "* foo\n ```\n codeblock\n ```"))))
(is (= "1. foo\n 1. bar" (slack "1. foo\n 1. bar")))
(is (= "• foo\n • bar" (slack "* foo\n * bar")))
(is (= "1. foo\n • bar" (slack "1. foo\n * bar")))
(is (= "• foo\n 1. bar" (slack "* foo\n 1. bar")))
(is (= "• foo\n 1. bar\n 2. baz" (slack "* foo\n 1. bar\n 2. baz")))
(is (= "• foo\n 1. bar\n• baz" (slack "* foo\n 1. bar\n* baz")))
(is (= "• foo\n >quote" (slack "* foo\n >quote")))
(is (= "• foo\n ```\n codeblock\n ```" (slack "* foo\n ```\n codeblock\n ```"))))
(testing "Characters that are escaped in Markdown are preserved as plain text"
(is (= "\\" (mrkdwn "\\\\")))
(is (= "/" (mrkdwn "\\/")))
(is (= "'" (mrkdwn "\\'")))
(is (= "[" (mrkdwn "\\[")))
(is (= "]" (mrkdwn "\\]")))
(is (= "(" (mrkdwn "\\(")))
(is (= ")" (mrkdwn "\\)")))
(is (= "{" (mrkdwn "\\{")))
(is (= "}" (mrkdwn "\\}")))
(is (= "#" (mrkdwn "\\#")))
(is (= "+" (mrkdwn "\\+")))
(is (= "-" (mrkdwn "\\-")))
(is (= "." (mrkdwn "\\.")))
(is (= "!" (mrkdwn "\\!")))
(is (= "$" (mrkdwn "\\$")))
(is (= "%" (mrkdwn "\\%")))
(is (= "^" (mrkdwn "\\^")))
(is (= "=" (mrkdwn "\\=")))
(is (= "|" (mrkdwn "\\|")))
(is (= "?" (mrkdwn "\\?"))))
(is (= "\\" (slack "\\\\")))
(is (= "/" (slack "\\/")))
(is (= "'" (slack "\\'")))
(is (= "[" (slack "\\[")))
(is (= "]" (slack "\\]")))
(is (= "(" (slack "\\(")))
(is (= ")" (slack "\\)")))
(is (= "{" (slack "\\{")))
(is (= "}" (slack "\\}")))
(is (= "#" (slack "\\#")))
(is (= "+" (slack "\\+")))
(is (= "-" (slack "\\-")))
(is (= "." (slack "\\.")))
(is (= "!" (slack "\\!")))
(is (= "$" (slack "\\$")))
(is (= "%" (slack "\\%")))
(is (= "^" (slack "\\^")))
(is (= "=" (slack "\\=")))
(is (= "|" (slack "\\|")))
(is (= "?" (slack "\\?"))))
(testing "Certain characters that are escaped in Markdown are surrounded by zero-width characters for Slack"
(is (= "\u00ad&\u00ad" (mrkdwn "\\&")))
(is (= "\u00ad>\u00ad" (mrkdwn "\\>")))
(is (= "\u00ad<\u00ad" (mrkdwn "\\<")))
(is (= "\u00ad*\u00ad" (mrkdwn "\\*")))
(is (= "\u00ad_\u00ad" (mrkdwn "\\_")))
(is (= "\u00ad`\u00ad" (mrkdwn "\\`"))))
(is (= "\u00ad&\u00ad" (slack "\\&")))
(is (= "\u00ad>\u00ad" (slack "\\>")))
(is (= "\u00ad<\u00ad" (slack "\\<")))
(is (= "\u00ad*\u00ad" (slack "\\*")))
(is (= "\u00ad_\u00ad" (slack "\\_")))
(is (= "\u00ad`\u00ad" (slack "\\`"))))
(testing "Images in Markdown are converted to links, with alt text preserved"
(is (= "<image.png|[Image]>" (mrkdwn "![](image.png)")))
(is (= "<image.png|[Image: alt-text]>" (mrkdwn "![alt-text](image.png)"))))
(is (= "<image.png|[Image]>" (slack "![](image.png)")))
(is (= "<image.png|[Image: alt-text]>" (slack "![alt-text](image.png)"))))
(testing "Image references are treated the same as normal images"
(is (= "<image.png|[Image]>" (mrkdwn "![][ref]\n\n[ref]: image.png")))
(is (= "<image.png|[Image: alt-text]>" (mrkdwn "![alt-text][ref]\n\n[ref]: image.png")))
(is (= "<image.png|[Image]>" (mrkdwn "![][Ref]\n\n[REF]: image.png"))))
(is (= "<image.png|[Image]>" (slack "![][ref]\n\n[ref]: image.png")))
(is (= "<image.png|[Image: alt-text]>" (slack "![alt-text][ref]\n\n[ref]: image.png")))
(is (= "<image.png|[Image]>" (slack "![][Ref]\n\n[REF]: image.png"))))
(testing "Linked images include link target in parentheses"
(is (= "<image.png|[Image]>\n(https://metabase.com)" (mrkdwn "[![](image.png)](https://metabase.com)")))
(is (= "<image.png|[Image]>\n(https://metabase.com)" (mrkdwn "[![][ref]](https://metabase.com)\n\n[ref]: image.png"))))
(is (= "<image.png|[Image]>\n(https://metabase.com)" (slack "[![](image.png)](https://metabase.com)")))
(is (= "<image.png|[Image]>\n(https://metabase.com)" (slack "[![][ref]](https://metabase.com)\n\n[ref]: image.png"))))
(testing "Raw HTML in Markdown is passed through unmodified, aside from angle brackets being
escaped with zero-width characters"
(is (= (escape "<h1>header</h1>") (mrkdwn "<h1>header</h1>")))
(is (= (escape "<em>bold</em>") (mrkdwn "<em>bold</em>")))
(is (= (escape "<body><h1>header</h1></body>") (mrkdwn "<body><h1>header</h1></body>")))
(is (= (escape "<p>&gt;</p>") (mrkdwn "<p>&gt;</p>")))
(is (= (escape "<img src=\"img.png\" />") (mrkdwn "<img src=\"img.png\" />")))
(is (= (escape "<script>alert(1)</script>") (mrkdwn "<script>alert(1)</script>")))
(is (= (escape "<h1><!-- comment --></h1>") (mrkdwn "<h1><!-- comment --></h1>")))
(is (= (escape "<em><!-- comment --></em>") (mrkdwn "<em><!-- comment --></em>")))
(is (= (escape "<!-- <p>comm\nent</p> -->") (mrkdwn "<!-- <p>comm\nent</p> -->")))
(is (= (escape "<!-- <p>comm\nent</p> -->") (mrkdwn "<!-- <p>comm\nent</p> -->")))
(is (= (escape "<!DOCTYPE html>") (mrkdwn "<!DOCTYPE html>"))))
(is (= (escape "<h1>header</h1>") (slack "<h1>header</h1>")))
(is (= (escape "<em>bold</em>") (slack "<em>bold</em>")))
(is (= (escape "<body><h1>header</h1></body>") (slack "<body><h1>header</h1></body>")))
(is (= (escape "<p>&gt;</p>") (slack "<p>&gt;</p>")))
(is (= (escape "<img src=\"img.png\" />") (slack "<img src=\"img.png\" />")))
(is (= (escape "<script>alert(1)</script>") (slack "<script>alert(1)</script>")))
(is (= (escape "<h1><!-- comment --></h1>") (slack "<h1><!-- comment --></h1>")))
(is (= (escape "<em><!-- comment --></em>") (slack "<em><!-- comment --></em>")))
(is (= (escape "<!-- <p>comm\nent</p> -->") (slack "<!-- <p>comm\nent</p> -->")))
(is (= (escape "<!-- <p>comm\nent</p> -->") (slack "<!-- <p>comm\nent</p> -->")))
(is (= (escape "<!DOCTYPE html>") (slack "<!DOCTYPE html>"))))
(testing "HTML entities (outside of HTML tags) are converted to Unicode"
(is (= "&" (mrkdwn "&amp;")))
(is (= ">" (mrkdwn "&gt;")))
(is (= "ℋ" (mrkdwn "&HilbertSpace;"))))
(is (= "&" (slack "&amp;")))
(is (= ">" (slack "&gt;")))
(is (= "ℋ" (slack "&HilbertSpace;"))))
(testing "Square brackets that aren't used for a link are left as-is (#20993)"
(is (= "[]" (mrkdwn "[]")))
(is (= "[test]" (mrkdwn "[test]")))))
(is (= "[]" (slack "[]")))
(is (= "[test]" (slack "[test]")))))
(defn- html
[markdown]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment