Packages
mdex
0.13.1
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.5
retired
0.12.4
0.12.3
0.12.2
0.12.1
0.12.0
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.13
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Fast and extensible Markdown for Elixir
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/mdex/slack_converter.ex
defmodule MDEx.SlackConverter do
@moduledoc false
alias MDEx.Document
@typedoc "Conversion options"
@type options :: keyword()
@spec convert(Document.t(), options()) :: {:ok, String.t()} | {:error, term()}
def convert(%Document{nodes: nodes}, options) do
context = %{list_depth: 0, link_label: false, unsafe: Keyword.get(options, :unsafe, false), styles: MapSet.new()}
{:ok, nodes |> render_nodes(context) |> IO.iodata_to_binary()}
end
defp render_nodes(nodes, context), do: Enum.map(nodes, &render_node(&1, context))
defp render_node(node, context)
defp render_node(%MDEx.Document{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.Paragraph{nodes: nodes}, context), do: [render_nodes(nodes, context), "\n"]
defp render_node(%MDEx.Text{literal: text}, _context), do: escape_text(text)
defp render_node(%MDEx.Strong{nodes: nodes}, context), do: render_style(:strong, "*", nodes, context)
defp render_node(%MDEx.Emph{nodes: nodes}, context), do: render_style(:emph, "_", nodes, context)
defp render_node(%MDEx.Code{literal: code}, _context), do: ["`", escape_text(code), "`"]
defp render_node(%MDEx.CodeBlock{literal: code}, _context) do
["```\n", String.trim_trailing(code, "\n"), "\n```\n"]
end
defp render_node(%MDEx.Heading{nodes: nodes}, context), do: [render_style(:strong, "*", nodes, context), "\n"]
defp render_node(%MDEx.Link{url: url, nodes: nodes}, context) do
label = render_nodes(nodes, %{context | link_label: true})
case render_url(url, context) do
nil -> label
url -> ["<", escape_link_part(url), "|", label, ">"]
end
end
defp render_node(%MDEx.WikiLink{url: url, nodes: nodes}, context) do
label = render_nodes(nodes, %{context | link_label: true})
case render_url(url, context) do
nil -> label
url -> ["<", escape_link_part(url), "|", label, ">"]
end
end
defp render_node(%MDEx.Strikethrough{nodes: nodes}, context), do: render_style(:strike, "~", nodes, context)
defp render_node(%MDEx.Underline{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.Highlight{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.Insert{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.Subscript{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.Superscript{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.Subtext{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.SpoileredText{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.BlockQuote{nodes: nodes}, context), do: render_quote(nodes, context)
defp render_node(%MDEx.MultilineBlockQuote{nodes: nodes}, context), do: render_quote(nodes, context)
defp render_node(%MDEx.List{nodes: items}, context), do: render_list(items, context)
defp render_node(%MDEx.ListItem{} = item, context), do: render_list_item(item, context, "- ")
defp render_node(%MDEx.TaskItem{checked: checked, nodes: nodes}, context) do
marker = if checked, do: "[x] ", else: "[ ] "
render_list_item(%MDEx.ListItem{nodes: nodes}, context, "- " <> marker)
end
defp render_node(%MDEx.SoftBreak{}, _context), do: " "
defp render_node(%MDEx.LineBreak{}, _context), do: "\n"
defp render_node(%MDEx.ThematicBreak{}, _context), do: "---\n"
defp render_node(%MDEx.Image{url: url, title: title, nodes: nodes}, context) do
label = if title in [nil, ""], do: render_nodes(nodes, %{context | link_label: true}), else: escape_text(title)
case render_url(url, context) do
nil -> label
url -> ["<", escape_link_part(url), "|", label, ">"]
end
end
defp render_node(%MDEx.HtmlBlock{literal: html}, _context), do: [escape_text(html), "\n"]
defp render_node(%MDEx.HtmlInline{literal: html}, _context), do: escape_text(html)
defp render_node(%MDEx.HeexBlock{literal: heex}, _context), do: [escape_text(heex), "\n"]
defp render_node(%MDEx.HeexInline{literal: heex}, _context), do: escape_text(heex)
defp render_node(%MDEx.Raw{literal: content}, _context), do: escape_text(content)
defp render_node(%MDEx.ShortCode{emoji: emoji}, _context), do: emoji
defp render_node(%MDEx.Math{literal: math}, _context), do: escape_text(math)
defp render_node(%MDEx.FrontMatter{}, _context), do: ""
defp render_node(%MDEx.FootnoteReference{name: name}, _context), do: ["[^", escape_text(name), "]"]
defp render_node(%MDEx.FootnoteDefinition{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.Alert{title: title, nodes: nodes}, context), do: render_alert(title, nodes, context)
defp render_node(%MDEx.Table{nodes: rows}, context), do: render_nodes(rows, context)
defp render_node(%MDEx.TableRow{nodes: cells}, context), do: [cells |> Enum.map(&render_node(&1, context)) |> Enum.intersperse(" | "), "\n"]
defp render_node(%MDEx.TableCell{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.DescriptionList{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.DescriptionItem{nodes: nodes}, context), do: render_nodes(nodes, context)
defp render_node(%MDEx.DescriptionTerm{nodes: nodes}, context), do: [render_nodes(nodes, context), "\n"]
defp render_node(%MDEx.DescriptionDetails{nodes: nodes}, context), do: [": ", render_nodes(nodes, context), "\n"]
defp render_node(%MDEx.Escaped{}, _context), do: ""
defp render_node(%MDEx.EscapedTag{literal: literal}, _context), do: escape_text(literal)
defp render_node(%MDEx.BlockDirective{info: info, nodes: nodes}, context), do: ["*", escape_text(info), "*\n", render_nodes(nodes, context)]
defp render_node(%{literal: literal}, _context) when is_binary(literal), do: escape_text(literal)
defp render_node(%{nodes: nodes}, context) when is_list(nodes), do: render_nodes(nodes, context)
defp render_node(_node, _context), do: ""
defp render_quote(nodes, context) do
nodes
|> render_nodes(context)
|> IO.iodata_to_binary()
|> String.split("\n", trim: false)
|> Enum.map_join("\n", fn
"" -> ""
line -> "> " <> line
end)
end
defp render_alert(title, nodes, context) do
title = if title in [nil, ""], do: [], else: ["*", escape_text(title), "*\n"]
[title, render_quote(nodes, context)]
end
defp render_list(items, context) do
items
|> Enum.with_index(1)
|> Enum.map(fn
{%MDEx.ListItem{list_type: :ordered} = item, index} -> render_list_item(item, context, "#{index}. ")
{item, _index} -> render_node(item, context)
end)
end
defp render_list_item(%{nodes: children}, context, marker) do
{content, nested_lists} = Enum.split_with(children, &(not match?(%MDEx.List{}, &1)))
indent = String.duplicate(" ", context.list_depth)
text =
content
|> Enum.map(fn
%MDEx.Paragraph{nodes: paragraph_nodes} -> render_nodes(paragraph_nodes, context)
node -> render_node(node, context)
end)
nested_context = %{context | list_depth: context.list_depth + 1}
nested = render_nodes(nested_lists, nested_context)
[indent, marker, text, "\n", nested]
end
defp render_style(style, marker, nodes, context) do
if context.link_label or MapSet.member?(context.styles, style) do
render_nodes(nodes, context)
else
context = %{context | styles: MapSet.put(context.styles, style)}
[marker, render_nodes(nodes, context), marker]
end
end
defp escape_text(text), do: escape_text(text, [])
defp escape_text(<<>>, acc), do: Enum.reverse(acc)
defp escape_text(<<"&", rest::binary>>, acc), do: escape_text(rest, ["&" | acc])
defp escape_text(<<"<", rest::binary>>, acc), do: escape_text(rest, ["<" | acc])
defp escape_text(<<">", rest::binary>>, acc), do: escape_text(rest, [">" | acc])
defp escape_text(<<char, rest::binary>>, acc), do: escape_text(rest, [<<char>> | acc])
defp escape_link_part(text), do: text |> to_string() |> escape_link_part([])
defp escape_link_part(<<>>, acc), do: Enum.reverse(acc)
defp escape_link_part(<<"&", rest::binary>>, acc), do: escape_link_part(rest, ["&" | acc])
defp escape_link_part(<<"<", rest::binary>>, acc), do: escape_link_part(rest, ["<" | acc])
defp escape_link_part(<<">", rest::binary>>, acc), do: escape_link_part(rest, [">" | acc])
defp escape_link_part(<<"|", rest::binary>>, acc), do: escape_link_part(rest, ["%7C" | acc])
defp escape_link_part(<<char, rest::binary>>, acc), do: escape_link_part(rest, [<<char>> | acc])
defp render_url(url, %{unsafe: true}) when is_binary(url), do: url
defp render_url(url, _context) when is_binary(url) do
if MDExNative.Comrak.dangerous_url?(url), do: nil, else: url
end
defp render_url(url, _context), do: url
end