Use FEP-c16b: Formatting MFM functions #2

Open
ilja wants to merge 9 commits from ilja/mfm-parser:akkoma_use_fep-c16b_formatting_mfm_functions into akkoma
11 changed files with 363 additions and 1398 deletions

View file

@ -1,49 +1,55 @@
# Akkoma-MFMParser
extremely simple modification to [the original parser](https://codeberg.org/ilja/mfm_parser) that just doesn't include the CSS
# MfmParser
A simple parser for [Misskey Flavoured Markdown](https://github.com/misskey-dev/mfm.js/).
A simple [FEP-c16b](https://codeberg.org/fediverse/fep/src/branch/main/fep/c16b/fep-c16b.md) compliant parser for Misskey's [Markup language For Misskey](https://misskey-hub.net/en/docs/for-users/features/mfm/) MFM functions.
It only parses the MFM specific syntax of the form $[name.params content] and newlines.
That means that it doesn't parse links, usernames, HTML, Markdown or Katex.
It only parses the MFM specific syntax of the form `$[name.attributes content]`.
That means that it doesn't parse e.g. links, usernames, HTML, Markdown or Katex.
The Parser returns a tree, which looks like
The Parser returns a tree. For example, `it's not chocolatine, it's $[spin.alternate,speed=0.5s pain au chocolat]` will look like
[
%MfmParser.Text{
props: %{
text: "it's not chocolatine, it's "
}
%MfmParser.Node.Text{
content: "it's not chocolatine, it's "
},
%MfmParser.MFM.Twitch{
props: %{
speed: "0.2s"
},
children: [
%MfmParser.Text{
props: %{
text: "pain au chocolat"
}
%MfmParser.Node.MFM{
name: "twitch",
attributes: [
[{"alternate"}, {"speed", "0.5s"}]
],
content: [
%MfmParser.Node.Text{
content: "pain au chocolat"
}
]
}
]
You can also convert the tree into HTML.
You can also convert the tree into FEP-c16b compatible HTML.
it's not chocolatine, it's <span class=\"mfm-spin\" data-mfm-alternate data-mfm-speed=\"0.5s\">pain au chocolat</span>
## Examples
iex> MfmParser.Parser.parse("$[twitch.speed=5s 🍮]")
Here we turn our input into a tree
iex> "$[twitch.speed=0.5s 🍮]" |> MfmParser.Parser.parse()
[
%MfmParser.Node.MFM.Twitch{
children: [%MfmParser.Node.Text{props: %{text: "🍮"}}],
props: %{speed: "5s"}
%MfmParser.Node.MFM{
name: "twitch",
attributes: [{"speed", "0.5s"}],
content: [%MfmParser.Node.Text{content: "pain au chocolat"}]
}
]
iex> MfmParser.Parser.parse("$[twitch.speed=5s 🍮]") |> MfmParser.to_html()
"<span style=\\"display: inline-block; animation: 5s ease 0s infinite normal none running mfm-twitch;\\">🍮</span><style>@keyframes mfm-twitch { 0% { transform:translate(7px,-2px) } 5% { transform:translate(-3px,1px) } 10% { transform:translate(-7px,-1px) } 15% { transform:translateY(-1px) } 20% { transform:translate(-8px,6px) } 25% { transform:translate(-4px,-3px) } 30% { transform:translate(-4px,-6px) } 35% { transform:translate(-8px,-8px) } 40% { transform:translate(4px,6px) } 45% { transform:translate(-3px,1px) } 50% { transform:translate(2px,-10px) } 55% { transform:translate(-7px) } 60% { transform:translate(-2px,4px) } 65% { transform:translate(3px,-8px) } 70% { transform:translate(6px,7px) } 75% { transform:translate(-7px,-2px) } 80% { transform:translate(-7px,-8px) } 85% { transform:translate(9px,3px) } 90% { transform:translate(-3px,-2px) } 95% { transform:translate(-10px,2px) } to { transform:translate(-2px,-6px) }}</style>"
Here we pipe the MFM notation through the encoder and then the parser, turning the MFM into FEP-c16b compatible HTML.
iex> "$[twitch.speed=0.5s 🍮]" |> MfmParser.Parser.parse() |> MfmParser.Encoder.to_html()
"<span class="mfm-twitch" data-mfm-speed="0.5s">🍮</span>"
Or we can use `MfmParser.Encoder.to_html/1` directly without having to call the parser ourselves.
iex> "$[twitch.speed=0.5s 🍮]" |> MfmParser.Encoder.to_html()
"<span class="mfm-twitch" data-mfm-speed="0.5s">🍮</span>"
## Reading
### The Parser
@ -72,14 +78,14 @@ E.g. an Encoder encodes the tree into a different format.
### The code
The code can be found in the *lib* folder. It contains, among other things, the Reader, Lexer, Parse, and Encoder modules.
The code can be found in the *lib* folder. It contains, among other things, the Reader, Lexer, Parser, and Encoder modules.
The *test* folder contains the unit tests.
The *test* folder contains the tests.
## License
A parser/encoder for Misskey Flavoured Markdown.
Copyright (C) 2022 Ilja
Copyright (C) 2024 ilja.space
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as

View file

@ -1,198 +1,50 @@
defmodule MfmParser.Encoder do
alias MfmParser.Parser
alias MfmParser.Node
@moduledoc """
An encoder who can turn a tree into HTML.
An encoder who can turn a String with MFM functions, or an MFM tree from `MfmParser.Parser.parse/1`, into FEP-c16b compliant HTML.
It only works for the MFM specific tags of the form $[name.opts content].
Other parts of MFM (html, Markdown and [KaTeX](https://katex.org/)) are out of scope for this project.
It can directly take input from function `MfmParser.Parser.parse`.
It only works for the MFM specific tags of the form `$[name.attributes content]`. Other parts of MFM (e.g. html, Markdown and [KaTeX](https://katex.org/)) are out of scope for this project.
## Examples
iex> [
...> %MfmParser.Node.MFM.Twitch{
...> children: [%MfmParser.Node.Text{props: %{text: "🍮"}}],
...> props: %{speed: "5s"}
...> %MfmParser.Node.MFM{
...> name: "twitch",
...> content: [%MfmParser.Node.Text{content: "🍮"}],
...> attributes: [{"speed", "5s"}]
...> }
...> ]
...> |> MfmParser.Encoder.to_html()
"<span class=\\"mfm\\" style=\\"display: inline-block; animation: 5s ease 0s infinite normal none running mfm-twitch;\\">🍮</span>"
~S[<span class="mfm-twitch" data-mfm-speed="5s">🍮</span>]
iex> MfmParser.Parser.parse("$[twitch.speed=5s 🍮]") |> MfmParser.Encoder.to_html()
"<span class=\\"mfm\\" style=\\"display: inline-block; animation: 5s ease 0s infinite normal none running mfm-twitch;\\">🍮</span>"
iex> "$[twitch.speed=5s 🍮]" |> MfmParser.Encoder.to_html()
~S[<span class="mfm-twitch" data-mfm-speed="5s">🍮</span>]
"""
def to_html(tree) when is_list(tree) do
{html, _styles} = to_html_styles(tree)
html
end
def to_html(input) when is_binary(input) do
Parser.parse(input) |> to_html()
end
defp to_html_styles(tree, _style \\ []) do
tree
|> Enum.reduce({"", []}, fn node, {html, styles} ->
def to_html([node | rest]) do
node_html =
case node do
%Node.Text{} ->
{html <> node.props.text, styles}
%MfmParser.Node.Text{content: content} ->
content
%Node.Newline{} ->
{html <> node.props.text, styles}
%Node.MFM.Flip{} ->
{html_child, styles_child} = to_html_styles(node.children)
case node.props do
%{v: true, h: true} ->
{html <>
"<span class=\"mfm\" style=\"display: inline-block; transform: scale(-1);\">#{html_child}</span>",
styles}
%{v: true} ->
{html <>
"<span class=\"mfm\" style=\"display: inline-block; transform: scaleY(-1);\">#{html_child}</span>",
styles}
_ ->
{html <>
"<span class=\"mfm\" style=\"display: inline-block; transform: scaleX(-1);\">#{html_child}</span>",
styles ++ styles_child}
end
%Node.MFM.Font{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <>
"<span class=\"mfm\" style=\"display: inline-block; font-family: #{node.props.font};\">#{html_child}</span>",
styles ++ styles_child}
%Node.MFM.X{} ->
prop_map = %{"200%" => "2", "400%" => "3", "600%" => "4"}
{html_child, styles_child} = to_html_styles(node.children)
{html <>
"<span style=\"font-size: #{node.props.size}\" class=\"mfm _mfm_x#{prop_map[node.props.size]}_\">#{html_child}</span>",
styles ++ styles_child}
%Node.MFM.Blur{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <> "<span class=\"_mfm_blur_\">#{html_child}</span>",
styles ++
[
"._mfm_blur_ { filter: blur(6px); transition: filter .3s; } ._mfm_blur_:hover { filter: blur(0px); }"
] ++ styles_child}
%Node.MFM.Jelly{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <> "<span class=\"mfm _mfm_jelly_\" style=\"display: inline-block; animation: #{node.props.speed} linear 0s infinite normal both running mfm-rubberBand;\">#{html_child}</span>", styles_child}
%Node.MFM.Tada{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <>
"<span class=\"mfm _mfm_tada_\" style=\"display: inline-block; font-size: 150%; animation: #{node.props.speed} linear 0s infinite normal both running mfm-tada;\">#{html_child}</span>", styles_child}
%Node.MFM.Jump{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <> "<span class=\"mfm _mfm_jump_\" style=\"display: inline-block; animation: #{node.props.speed} linear 0s infinite normal none running mfm-jump;\">#{html_child}</span>", styles_child}
%Node.MFM.Bounce{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <> "<span class=\"mfm _mfm_bounce_\" style=\"display: inline-block; animation: #{node.props.speed} linear 0s infinite normal none running mfm-bounce; transform-origin: center bottom 0px;\">#{html_child}</span>", styles_child}
%Node.MFM.Spin{} ->
{html_child, styles_child} = to_html_styles(node.children)
styles_map = %{
"x" =>
"@keyframes mfm-spinX { 0% { transform:perspective(128px) rotateX(0) } to { transform:perspective(128px) rotateX(360deg) }}",
"y" =>
"@keyframes mfm-spinY { 0% { transform:perspective(128px) rotateY(0) } to { transform:perspective(128px) rotateY(360deg) }}",
"z" =>
"@keyframes mfm-spin { 0% { transform:rotate(0) } to { transform:rotate(360deg) }}"
}
keyframe_names_map = %{
"x" => "mfm-spinX",
"y" => "mfm-spinY",
"z" => "mfm-spin"
}
directions_map = %{
"left" => "reverse"
}
{html <>
"<span class=\"mfm _mfm_spin_\" style=\"display: inline-block; animation: #{node.props.speed} linear 0s infinite #{Map.get(directions_map, node.props.direction, node.props.direction)} none running #{Map.get(keyframe_names_map, node.props.axis, "")};\">#{html_child}</span>",
styles ++ [Map.get(styles_map, node.props.axis, "")] ++ styles_child}
%Node.MFM.Shake{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <>
"<span class=\"mfm\" style=\"display: inline-block; animation: #{node.props.speed} ease 0s infinite normal none running mfm-shake;\">#{html_child}</span>",
styles ++
[
"@keyframes mfm-shake { 0% { transform:translate(-3px,-1px) rotate(-8deg) } 5% { transform:translateY(-1px) rotate(-10deg) } 10% { transform:translate(1px,-3px) rotate(0) } 15% { transform:translate(1px,1px) rotate(11deg) } 20% { transform:translate(-2px,1px) rotate(1deg) } 25% { transform:translate(-1px,-2px) rotate(-2deg) } 30% { transform:translate(-1px,2px) rotate(-3deg) } 35% { transform:translate(2px,1px) rotate(6deg) } 40% { transform:translate(-2px,-3px) rotate(-9deg) } 45% { transform:translateY(-1px) rotate(-12deg) } 50% { transform:translate(1px,2px) rotate(10deg) } 55% { transform:translateY(-3px) rotate(8deg) } 60% { transform:translate(1px,-1px) rotate(8deg) } 65% { transform:translateY(-1px) rotate(-7deg) } 70% { transform:translate(-1px,-3px) rotate(6deg) } 75% { transform:translateY(-2px) rotate(4deg) } 80% { transform:translate(-2px,-1px) rotate(3deg) } 85% { transform:translate(1px,-3px) rotate(-10deg) } 90% { transform:translate(1px) rotate(3deg) } 95% { transform:translate(-2px) rotate(-3deg) } to { transform:translate(2px,1px) rotate(2deg) }}"
] ++ styles_child}
%Node.MFM.Twitch{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <>
"<span class=\"mfm\" style=\"display: inline-block; animation: #{node.props.speed} ease 0s infinite normal none running mfm-twitch;\">#{html_child}</span>",
styles ++
[
"@keyframes mfm-twitch { 0% { transform:translate(7px,-2px) } 5% { transform:translate(-3px,1px) } 10% { transform:translate(-7px,-1px) } 15% { transform:translateY(-1px) } 20% { transform:translate(-8px,6px) } 25% { transform:translate(-4px,-3px) } 30% { transform:translate(-4px,-6px) } 35% { transform:translate(-8px,-8px) } 40% { transform:translate(4px,6px) } 45% { transform:translate(-3px,1px) } 50% { transform:translate(2px,-10px) } 55% { transform:translate(-7px) } 60% { transform:translate(-2px,4px) } 65% { transform:translate(3px,-8px) } 70% { transform:translate(6px,7px) } 75% { transform:translate(-7px,-2px) } 80% { transform:translate(-7px,-8px) } 85% { transform:translate(9px,3px) } 90% { transform:translate(-3px,-2px) } 95% { transform:translate(-10px,2px) } to { transform:translate(-2px,-6px) }}"
] ++ styles_child}
%Node.MFM.Rainbow{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <>
"<span class=\"mfm\" style=\"display: inline-block; animation: #{node.props.speed} linear 0s infinite normal none running mfm-rainbow;\">#{html_child}</span>",
styles ++
[
"@keyframes mfm-rainbow { 0% { filter:hue-rotate(0deg) contrast(150%) saturate(150%) } to { filter:hue-rotate(360deg) contrast(150%) saturate(150%) }}"
] ++ styles_child}
%Node.MFM.Sparkle{} ->
# TODO: This is not how Misskey does it and should be changed to make it work like Misskey.
{html_child, styles_child} = to_html_styles(node.children)
{html <>
"<span class=\"mfm\" style=\"display: inline-block; animation: 1s linear 0s infinite normal none running mfm-sparkle;\">#{html_child}</span>",
styles ++
[
"@keyframes mfm-sparkle { 0% { filter: brightness(100%) } to { filter: brightness(300%) }}"
] ++ styles_child}
%Node.MFM.Rotate{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <>
"<span class=\"mfm\" style=\"display: inline-block; transform: rotate(90deg); transform-origin: center center 0px;\">#{html_child}</span>",
styles ++ styles_child}
%Node.MFM.Undefined{} ->
{html_child, styles_child} = to_html_styles(node.children)
{html <>
"<span>#{html_child}</span>", styles ++ styles_child}
_ ->
{html, styles}
end
%MfmParser.Node.MFM{name: name, attributes: attributes, content: content} ->
attributes_string =
attributes
|> Enum.reduce("", fn
{name}, acc -> acc <> " data-mfm-#{name}"
{name, value}, acc -> acc <> " data-mfm-#{name}=\"#{value}\""
end)
"<span class=\"mfm-#{name}\"#{attributes_string}>#{to_html(content)}</span>"
end
node_html <> to_html(rest)
end
def to_html([]) do
""
end
def to_html(mfm_string) when is_binary(mfm_string) do
MfmParser.Parser.parse(mfm_string) |> to_html()
end
end

View file

@ -3,7 +3,6 @@ defmodule MfmParser.Lexer do
alias MfmParser.Token
alias MfmParser.Token.MFM
alias MfmParser.Token.Newline
alias MfmParser.Token.Text
def peek(input) do
@ -44,9 +43,6 @@ defmodule MfmParser.Lexer do
"]" ->
%MFM.Close{}
"\n" ->
%Newline{}
_ ->
%Text{}
end
@ -60,10 +56,6 @@ defmodule MfmParser.Lexer do
true
end
defp is_last_char_of_token?(_, _, %Newline{}) do
true
end
defp is_last_char_of_token?(_, rest, %Text{}) do
case Reader.next(rest) do
:eof -> true

View file

@ -1,67 +1,7 @@
defmodule MfmParser.Node.Text do
defstruct props: %{text: ""}
defstruct content: ""
end
defmodule MfmParser.Node.Newline do
defstruct props: %{text: "\n"}
end
defmodule MfmParser.Node.MFM.Blur do
defstruct props: %{}, children: []
end
defmodule MfmParser.Node.MFM.Bounce do
defstruct props: %{speed: "0.75s"}, children: []
end
defmodule MfmParser.Node.MFM.Flip do
defstruct props: %{v: false, h: false}, children: []
end
defmodule MfmParser.Node.MFM.Font do
defstruct props: %{font: nil}, children: []
end
defmodule MfmParser.Node.MFM.Jelly do
defstruct props: %{speed: "1s"}, children: []
end
defmodule MfmParser.Node.MFM.Jump do
defstruct props: %{speed: "0.75s"}, children: []
end
defmodule MfmParser.Node.MFM.Rainbow do
defstruct props: %{speed: "1s"}, children: []
end
defmodule MfmParser.Node.MFM.Rotate do
defstruct props: %{}, children: []
end
defmodule MfmParser.Node.MFM.Shake do
defstruct props: %{speed: "0.5s"}, children: []
end
defmodule MfmParser.Node.MFM.Sparkle do
defstruct props: %{}, children: []
end
defmodule MfmParser.Node.MFM.Spin do
defstruct props: %{axis: "z", direction: "normal", speed: "1.5s"}, children: []
end
defmodule MfmParser.Node.MFM.Tada do
defstruct props: %{speed: "1s"}, children: []
end
defmodule MfmParser.Node.MFM.Twitch do
defstruct props: %{speed: "0.5s"}, children: []
end
defmodule MfmParser.Node.MFM.Undefined do
defstruct props: %{}, children: []
end
defmodule MfmParser.Node.MFM.X do
defstruct props: %{size: nil}, children: []
defmodule MfmParser.Node.MFM do
defstruct name: "", attributes: %{}, content: []
end

View file

@ -4,164 +4,98 @@ defmodule MfmParser.Parser do
alias MfmParser.Lexer
@moduledoc """
`MfmParser` is a parser for [Misskey Flavoured Markdown](https://mk.nixnet.social/mfm-cheat-sheet).
`MfmParser` is a [FEP-c16b](https://codeberg.org/fediverse/fep/src/branch/main/fep/c16b/fep-c16b.md) compatible parser for Misskey's [Markup language For Misskey](https://misskey-hub.net/en/docs/for-users/features/mfm/) MFM functions.
It can parse MFM and return a tree. It also has an encoder who can turn a tree into HTML.
It can parse a string representing text containing MFM functions and return a tree. There's also has an encoder who can turn a tree into HTML.
It only works for the MFM specific tags of the form $[name.opts content].
It only parses the MFM specific tags of the form $[name.opts content].
Other parts of MFM (html, Markdown and [KaTeX](https://katex.org/)) are out of scope for this project.
## Examples
iex> MfmParser.Parser.parse("$[twitch.speed=5s 🍮]")
iex> MfmParser.Parser.parse("$[twitch.speed=0.5s 🍮]")
[
%MfmParser.Node.MFM.Twitch{
children: [%MfmParser.Node.Text{props: %{text: "🍮"}}],
props: %{speed: "5s"}
%MfmParser.Node.MFM{
name: "twitch",
attributes: [{"speed", "0.5s"}],
content: [%MfmParser.Node.Text{content: "🍮"}]
}
]
"""
def parse(input, tree \\ [], is_end_token \\ fn _ -> false end) do
def parse(input, tree \\ [], is_open \\ false) do
case Lexer.next(input) do
:eof ->
tree
{token, rest} ->
if is_end_token.(token) do
{tree, rest}
else
case token do
%Token.MFM.Open{} ->
{children, rest} =
case parse(rest, [], &is_mfm_close_token?/1) do
{children, rest} ->
{children, rest}
_ ->
{[], rest}
end
parse(
rest,
tree ++ [token |> get_node() |> Map.put(:children, children)],
is_end_token
)
%Token.Text{} ->
parse(
rest,
tree ++ [%Node.Text{props: %{text: token.content}}],
is_end_token
tree ++ [%Node.Text{content: token.content}],
is_open
)
%Token.Newline{} ->
%Token.MFM.Open{} ->
# Here we go deeper in the structure
{children, rest} =
case parse(rest, [], true) do
{children, child_rest} -> {children, child_rest}
# Here we capture an edge case where an unclosed tag makes us hit :eof
# this causes the tree to be returned directly instead of part of a tuple
children -> {children, ""}
end
# Here we went dept already, so now we are parsing the next Open token on the same level
parse(
rest,
tree ++ [%Node.Newline{props: %{text: token.content}}],
is_end_token
tree ++ [token |> get_mfm_node() |> Map.put(:content, children)],
is_open
)
# We can either have a Close token who properly closes an Open token
# Or we can have a stray Close token, while currently not processing an Open token
# In the first case, we return what we have bc parsing of this Node is finished
# In the second case, we add it as text
%Token.MFM.Close{} ->
if is_open do
{tree, rest}
else
parse(
rest,
tree ++ [%Node.Text{props: %{text: token.content}}],
is_end_token
tree ++ [%Node.Text{content: token.content}]
)
end
end
:eof ->
tree
end
end
defp is_mfm_close_token?(token) do
case token do
%Token.MFM.Close{} -> true
_ -> false
end
end
defp get_node(token = %{content: content}) do
cond do
content =~ "$[flip" -> %Node.MFM.Flip{}
content =~ "$[font" -> %Node.MFM.Font{}
content =~ "$[x" -> %Node.MFM.X{}
content =~ "$[blur" -> %Node.MFM.Blur{}
content =~ "$[jelly" -> %Node.MFM.Jelly{}
content =~ "$[tada" -> %Node.MFM.Tada{}
content =~ "$[jump" -> %Node.MFM.Jump{}
content =~ "$[bounce" -> %Node.MFM.Bounce{}
content =~ "$[spin" -> %Node.MFM.Spin{}
content =~ "$[shake" -> %Node.MFM.Shake{}
content =~ "$[twitch" -> %Node.MFM.Twitch{}
content =~ "$[rainbow" -> %Node.MFM.Rainbow{}
content =~ "$[sparkle" -> %Node.MFM.Sparkle{}
content =~ "$[rotate" -> %Node.MFM.Rotate{}
true -> %Node.MFM.Undefined{}
end
|> fill_props(token)
end
defp fill_props(node = %{props: props}, %{content: content}) do
new_props = props |> Map.merge(to_props(content))
node |> Map.merge(%{props: new_props})
end
def to_props(opts_string) when is_binary(opts_string) do
cond do
opts_string =~ "." ->
Regex.replace(~r/^.*?\./u, opts_string, "")
defp get_mfm_node(token) do
{name, attributes} =
case token.content
|> String.trim()
|> String.replace("$[", "")
|> String.split(".", parts: 2) do
[name] -> {name, []}
[name, attributes_string] -> {name, build_attributes_list(attributes_string)}
end
%Node.MFM{name: name, attributes: attributes, content: []}
end
defp build_attributes_list(attributes_string) do
attributes_string
|> String.split(",")
|> Enum.reduce(%{}, fn opt, acc ->
acc
|> Map.merge(
cond do
opt =~ "speed" ->
%{speed: String.replace(opt, "speed=", "")}
opt =~ "v" ->
%{v: true}
opt =~ "h" ->
%{h: true}
opt =~ "x" ->
%{axis: "x"}
opt =~ "y" ->
%{axis: "y"}
opt =~ "left" ->
%{direction: "left"}
opt =~ "alternate" ->
%{direction: "alternate"}
true ->
if Regex.match?(~r/^\$\[font/, opts_string) do
%{font: opt}
else
%{}
|> Enum.reduce([], fn attribute_string, acc ->
attribute =
case attribute_string |> String.split("=") do
[name] -> {name}
[name, value] -> {name, value}
end
end
)
acc ++ [attribute]
end)
opts_string =~ "$[x" ->
%{
size:
case opts_string |> String.replace("$[x", "") |> String.trim() do
"2" -> "200%"
"3" -> "400%"
"4" -> "600%"
_ -> "100%"
end
}
true ->
%{}
end
end
end

View file

@ -8,10 +8,6 @@ defmodule MfmParser.Token.Text do
defstruct content: ""
end
defmodule MfmParser.Token.Newline do
defstruct content: ""
end
defmodule MfmParser.Token.MFM.Open do
defstruct content: ""
end

View file

@ -4,7 +4,7 @@ defmodule MfmParser.MixProject do
def project do
[
app: :mfm_parser,
version: "0.1.0",
version: "0.2.0",
elixir: "~> 1.13",
start_permanent: Mix.env() == :prod,
deps: deps()
@ -14,12 +14,15 @@ defmodule MfmParser.MixProject do
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
# extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[]
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View file

@ -1,4 +0,0 @@
%{
"phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"},
"temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
}

View file

@ -8,425 +8,156 @@ defmodule MfmParser.EncoderTest do
describe "to_html" do
test "it handles text" do
input_tree = [%Node.Text{props: %{text: "chocolatine"}}]
input_tree = [%Node.Text{content: "chocolatine"}]
expected = "chocolatine"
assert Encoder.to_html(input_tree) == expected
end
test "it handles newlines" do
input_tree = [%Node.Newline{props: %{text: "\n"}}]
test "it handles a node without attributes" do
input = [%Node.MFM{name: "flip", attributes: %{}, content: []}]
expected = "\n"
assert Encoder.to_html(input_tree) == expected
end
test "it handles flip" do
input_tree = [
%Node.MFM.Flip{
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}]
}
]
input_tree_v = [
%Node.MFM.Flip{
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}],
props: %{v: true}
}
]
input_tree_h_v = [
%Node.MFM.Flip{
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}],
props: %{v: true, h: true}
}
]
expected =
~s[<span class="mfm" style="display: inline-block; transform: scaleX(-1);">Misskey expands the world of the Fediverse</span>]
expected_v =
~s[<span class="mfm" style="display: inline-block; transform: scaleY(-1);">Misskey expands the world of the Fediverse</span>]
expected_h_v =
~s[<span class="mfm" style="display: inline-block; transform: scale(-1);">Misskey expands the world of the Fediverse</span>]
assert Encoder.to_html(input_tree) == expected
assert Encoder.to_html(input_tree_v) == expected_v
assert Encoder.to_html(input_tree_h_v) == expected_h_v
end
test "it handles font" do
input_tree = [
%Node.MFM.Font{
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}],
props: %{font: "fantasy"}
}
]
expected =
~s[<span class="mfm" style="display: inline-block; font-family: fantasy;">Misskey expands the world of the Fediverse</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles x" do
input_tree = [
%Node.MFM.X{
children: [%Node.Text{props: %{text: "🍮"}}],
props: %{size: "400%"}
}
]
expected = ~s[<span style="font-size: 400%" class="mfm _mfm_x3_">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles blur" do
input_tree = [
%Node.MFM.Blur{
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}]
}
]
expected = ~s[<span class="_mfm_blur_">Misskey expands the world of the Fediverse</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles jelly" do
input_tree = [
%Node.MFM.Jelly{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected =
~s[<span class="mfm _mfm_jelly_" style="display: inline-block; animation: 1s linear 0s infinite normal both running mfm-rubberBand;">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles tada" do
input_tree = [
%Node.MFM.Tada{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected =
~s[<span class="mfm _mfm_tada_" style="display: inline-block; font-size: 150%; animation: 1s linear 0s infinite normal both running mfm-tada;">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles jump" do
input_tree = [
%Node.MFM.Jump{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected =
~s[<span class="mfm _mfm_jump_" style="display: inline-block; animation: 0.75s linear 0s infinite normal none running mfm-jump;">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles bounce" do
input_tree = [
%Node.MFM.Bounce{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected =
~s[<span class="mfm _mfm_bounce_" style="display: inline-block; animation: 0.75s linear 0s infinite normal none running mfm-bounce; transform-origin: center bottom 0px;">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles spin" do
input_tree_z_left = [
%Node.MFM.Spin{
props: %{axis: "z", direction: "left", speed: "1.5s"},
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
input_tree_x_left = [
%Node.MFM.Spin{
props: %{axis: "x", direction: "left", speed: "1.5s"},
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
input_tree_y_left = [
%Node.MFM.Spin{
props: %{axis: "y", direction: "left", speed: "1.5s"},
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
input_tree_z_alternate = [
%Node.MFM.Spin{
props: %{axis: "z", direction: "alternate", speed: "1.5s"},
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
input_tree_x_alternate = [
%Node.MFM.Spin{
props: %{axis: "x", direction: "alternate", speed: "1.5s"},
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
input_tree_y_alternate = [
%Node.MFM.Spin{
props: %{axis: "y", direction: "alternate", speed: "1.5s"},
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
input_tree_z_normal = [
%Node.MFM.Spin{
props: %{axis: "z", direction: "normal", speed: "1.5s"},
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
input_tree_x_normal = [
%Node.MFM.Spin{
props: %{axis: "x", direction: "normal", speed: "1.5s"},
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
input_tree_y_normal = [
%Node.MFM.Spin{
props: %{axis: "y", direction: "normal", speed: "1.5s"},
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected_tree_z_left =
~s[<span class="mfm _mfm_spin_" style="display: inline-block; animation: 1.5s linear 0s infinite reverse none running mfm-spin;">🍮</span>]
expected_tree_x_left =
~s[<span class="mfm _mfm_spin_" style="display: inline-block; animation: 1.5s linear 0s infinite reverse none running mfm-spinX;">🍮</span>]
expected_tree_y_left =
~s[<span class="mfm _mfm_spin_" style="display: inline-block; animation: 1.5s linear 0s infinite reverse none running mfm-spinY;">🍮</span>]
expected_tree_z_alternate =
~s[<span class="mfm _mfm_spin_" style="display: inline-block; animation: 1.5s linear 0s infinite alternate none running mfm-spin;">🍮</span>]
expected_tree_x_alternate =
~s[<span class="mfm _mfm_spin_" style="display: inline-block; animation: 1.5s linear 0s infinite alternate none running mfm-spinX;">🍮</span>]
expected_tree_y_alternate =
~s[<span class="mfm _mfm_spin_" style="display: inline-block; animation: 1.5s linear 0s infinite alternate none running mfm-spinY;">🍮</span>]
expected_tree_z_normal =
~s[<span class="mfm _mfm_spin_" style="display: inline-block; animation: 1.5s linear 0s infinite normal none running mfm-spin;">🍮</span>]
expected_tree_x_normal =
~s[<span class="mfm _mfm_spin_" style="display: inline-block; animation: 1.5s linear 0s infinite normal none running mfm-spinX;">🍮</span>]
expected_tree_y_normal =
~s[<span class="mfm _mfm_spin_" style="display: inline-block; animation: 1.5s linear 0s infinite normal none running mfm-spinY;">🍮</span>]
assert Encoder.to_html(input_tree_z_left) == expected_tree_z_left
assert Encoder.to_html(input_tree_x_left) == expected_tree_x_left
assert Encoder.to_html(input_tree_y_left) == expected_tree_y_left
assert Encoder.to_html(input_tree_z_alternate) == expected_tree_z_alternate
assert Encoder.to_html(input_tree_x_alternate) == expected_tree_x_alternate
assert Encoder.to_html(input_tree_y_alternate) == expected_tree_y_alternate
assert Encoder.to_html(input_tree_z_normal) == expected_tree_z_normal
assert Encoder.to_html(input_tree_x_normal) == expected_tree_x_normal
assert Encoder.to_html(input_tree_y_normal) == expected_tree_y_normal
end
test "it handles shake" do
input_tree = [
%Node.MFM.Shake{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected =
~s[<span class="mfm" style="display: inline-block; animation: 0.5s ease 0s infinite normal none running mfm-shake;">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles twitch" do
input_tree = [
%Node.MFM.Twitch{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected =
~s[<span class="mfm" style="display: inline-block; animation: 0.5s ease 0s infinite normal none running mfm-twitch;">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles rainbow" do
input_tree = [
%Node.MFM.Rainbow{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected =
~s[<span class="mfm" style="display: inline-block; animation: 1s linear 0s infinite normal none running mfm-rainbow;">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles sparkle" do
# TODO: This is not how Misskey does it and should be changed to make it work like Misskey.
input_tree = [
%Node.MFM.Sparkle{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected =
~s[<span class="mfm" style="display: inline-block; animation: 1s linear 0s infinite normal none running mfm-sparkle;">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles rotate" do
input_tree = [
%Node.MFM.Rotate{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected =
~s[<span class="mfm" style="display: inline-block; transform: rotate(90deg); transform-origin: center center 0px;">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles unsuported formats" do
input_tree = [
%Node.MFM.Undefined{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected = ~s[<span>🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles multpile nodes on the same level" do
input_tree = [
%Node.MFM.Rotate{
children: [%Node.Text{props: %{text: "🍮"}}]
},
%Node.Text{props: %{text: "pain au chocolat"}},
%Node.MFM.Font{
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}],
props: %{font: "fantasy"}
}
]
expected =
~s[<span class="mfm" style="display: inline-block; transform: rotate(90deg); transform-origin: center center 0px;">🍮</span>pain au chocolat<span class="mfm" style="display: inline-block; font-family: fantasy;">Misskey expands the world of the Fediverse</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles nesting" do
input_tree = [
%Node.MFM.Rotate{
children: [
%Node.MFM.Font{
children: [%Node.Text{props: %{text: "🍮"}}],
props: %{font: "fantasy"}
}
]
}
]
expected =
~s[<span class="mfm" style="display: inline-block; transform: rotate(90deg); transform-origin: center center 0px;"><span class="mfm" style="display: inline-block; font-family: fantasy;">🍮</span></span>]
assert Encoder.to_html(input_tree) == expected
end
test "it shouldn't have duplicate styles" do
input_tree = [
%Node.MFM.Sparkle{
children: [%Node.Text{props: %{text: "🍮"}}]
},
%Node.MFM.Sparkle{
children: [%Node.Text{props: %{text: "🍮"}}]
}
]
expected =
~s[<span class="mfm" style="display: inline-block; animation: 1s linear 0s infinite normal none running mfm-sparkle;">🍮</span><span class="mfm" style="display: inline-block; animation: 1s linear 0s infinite normal none running mfm-sparkle;">🍮</span>]
assert Encoder.to_html(input_tree) == expected
end
test "it handles complex nesting of nodes" do
input_tree = [
%MfmParser.Node.Text{props: %{text: "It's not "}},
%MfmParser.Node.MFM.Twitch{
children: [%MfmParser.Node.Text{props: %{text: "chocolatine"}}],
props: %{speed: "0.2s"}
},
%MfmParser.Node.Newline{props: %{text: "\n"}},
%MfmParser.Node.Text{props: %{text: "it's "}},
%MfmParser.Node.MFM.X{
children: [
%MfmParser.Node.MFM.Spin{
children: [%MfmParser.Node.Text{props: %{text: "pain"}}],
props: %{direction: "normal", axis: "z", speed: "1s"}
},
%MfmParser.Node.Text{props: %{text: " "}},
%MfmParser.Node.MFM.Rainbow{
children: [%MfmParser.Node.Text{props: %{text: "au"}}],
props: %{speed: "2s"}
},
%MfmParser.Node.Text{props: %{text: " "}},
%MfmParser.Node.MFM.Jump{
children: [%MfmParser.Node.Text{props: %{text: "chocolat"}}],
props: %{speed: "0.5s"}
}
],
props: %{size: "600%"}
}
]
expected =
"It's not <span class=\"mfm\" style=\"display: inline-block; animation: 0.2s ease 0s infinite normal none running mfm-twitch;\">chocolatine</span>\nit's <span style=\"font-size: 600%\" class=\"mfm _mfm_x4_\"><span class=\"mfm _mfm_spin_\" style=\"display: inline-block; animation: 1s linear 0s infinite normal none running mfm-spin;\">pain</span> <span class=\"mfm\" style=\"display: inline-block; animation: 2s linear 0s infinite normal none running mfm-rainbow;\">au</span> <span class=\"mfm _mfm_jump_\" style=\"display: inline-block; animation: 0.5s linear 0s infinite normal none running mfm-jump;\">chocolat</span></span>"
assert Encoder.to_html(input_tree) == expected
end
test "it should be able to go from mfm-text input to html output" do
input =
"It's not $[twitch.speed=0.2s chocolatine]\nit's $[x4 $[spin.speed=1s pain] $[rainbow.speed=2s au] $[jump.speed=0.5s chocolat]]"
expected =
"It's not <span class=\"mfm\" style=\"display: inline-block; animation: 0.2s ease 0s infinite normal none running mfm-twitch;\">chocolatine</span>\nit's <span style=\"font-size: 600%\" class=\"mfm _mfm_x4_\"><span class=\"mfm _mfm_spin_\" style=\"display: inline-block; animation: 1s linear 0s infinite normal none running mfm-spin;\">pain</span> <span class=\"mfm\" style=\"display: inline-block; animation: 2s linear 0s infinite normal none running mfm-rainbow;\">au</span> <span class=\"mfm _mfm_jump_\" style=\"display: inline-block; animation: 0.5s linear 0s infinite normal none running mfm-jump;\">chocolat</span></span>"
expected = "<span class=\"mfm-flip\"></span>"
assert Encoder.to_html(input) == expected
end
test "it handles a node with a non-value attribute" do
input = [%Node.MFM{name: "font", attributes: [{"cursive"}], content: []}]
expected = "<span class=\"mfm-font\" data-mfm-cursive></span>"
assert Encoder.to_html(input) == expected
end
test "it handles a node with one value attribute" do
input = [%Node.MFM{name: "jelly", attributes: [{"speed", "2s"}], content: []}]
expected = "<span class=\"mfm-jelly\" data-mfm-speed=\"2s\"></span>"
assert Encoder.to_html(input) == expected
end
test "it handles a node with multiple attributes" do
input = [
%Node.MFM{
name: "spin",
attributes: [{"alternate"}, {"speed", "0.5s"}],
content: []
}
]
expected = "<span class=\"mfm-spin\" data-mfm-alternate data-mfm-speed=\"0.5s\"></span>"
assert Encoder.to_html(input) == expected
end
test "it handles multpile nodes on the same level" do
input = [
%Node.MFM{name: "twitch", attributes: [], content: []},
%Node.Text{content: "chocolatine"},
%Node.MFM{name: "blabla", attributes: [], content: []}
]
expected = "<span class=\"mfm-twitch\"></span>chocolatine<span class=\"mfm-blabla\"></span>"
assert Encoder.to_html(input) == expected
end
test "it handles nesting" do
input = [
%Node.MFM{
name: "twitch",
attributes: [],
content: [%Node.Text{content: "chocolatine"}]
}
]
expected = "<span class=\"mfm-twitch\">chocolatine</span>"
assert Encoder.to_html(input) == expected
end
test "it handles complex nesting of nodes" do
input = [
%MfmParser.Node.Text{content: "It's not "},
%MfmParser.Node.MFM{
name: "twitch",
attributes: [],
content: [%MfmParser.Node.Text{content: "chocolatine"}]
},
%MfmParser.Node.Text{content: "\nit's "},
%MfmParser.Node.MFM{
name: "x4",
attributes: [],
content: [
%MfmParser.Node.MFM{
name: "spin",
attributes: [{"alternate"}, {"speed", "0.2s"}],
content: [%MfmParser.Node.Text{content: "pain"}]
},
%MfmParser.Node.Text{content: " "},
%MfmParser.Node.MFM{
name: "rainbow",
attributes: [],
content: [%MfmParser.Node.Text{content: "au"}]
},
%MfmParser.Node.Text{content: " "},
%MfmParser.Node.MFM{
name: "jump",
attributes: [{"speed", "0.5s"}],
content: [%MfmParser.Node.Text{content: "chocolat"}]
}
]
}
]
expected =
"It's not <span class=\"mfm-twitch\">chocolatine</span>\nit's <span class=\"mfm-x4\"><span class=\"mfm-spin\" data-mfm-alternate data-mfm-speed=\"0.2s\">pain</span> <span class=\"mfm-rainbow\">au</span> <span class=\"mfm-jump\" data-mfm-speed=\"0.5s\">chocolat</span></span>"
assert Encoder.to_html(input) == expected
end
test "it should be able to go from mfm text input to html output" do
input =
"It's not $[twitch chocolatine]\nit's $[x4 $[spin.alternate,speed=0.2s pain] $[rainbow au] $[jump.speed=0.5s chocolat]]"
expected =
"It's not <span class=\"mfm-twitch\">chocolatine</span>\nit's <span class=\"mfm-x4\"><span class=\"mfm-spin\" data-mfm-alternate data-mfm-speed=\"0.2s\">pain</span> <span class=\"mfm-rainbow\">au</span> <span class=\"mfm-jump\" data-mfm-speed=\"0.5s\">chocolat</span></span>"
assert input |> MfmParser.Parser.parse() |> Encoder.to_html() == expected
assert input |> Encoder.to_html() == expected
end
test "it handles possible edge cases" do
assert MfmParser.Parser.parse("") |> Encoder.to_html() == ""
assert MfmParser.Parser.parse("]") |> Encoder.to_html() == "]"
assert MfmParser.Parser.parse("[") |> Encoder.to_html() == "["
assert MfmParser.Parser.parse("$") |> Encoder.to_html() == "$"
assert MfmParser.Parser.parse("[1]") |> Encoder.to_html() == "[1]"
assert MfmParser.Parser.parse("$$[spin beep]$") |> Encoder.to_html() ==
"$<span class=\"mfm-spin\">beep</span>$"
assert MfmParser.Parser.parse("$[spin boop]]") |> Encoder.to_html() ==
"<span class=\"mfm-spin\">boop</span>]"
# Behaviour of these is currently undefined
# The important part is that they do not crash the whole thing
MfmParser.Parser.parse("$[") |> Encoder.to_html()
MfmParser.Parser.parse("$[]") |> Encoder.to_html()
MfmParser.Parser.parse("$[spin ]") |> Encoder.to_html()
MfmParser.Parser.parse("$[spin beep]$[") |> Encoder.to_html()
MfmParser.Parser.parse("$[spin ") |> Encoder.to_html()
MfmParser.Parser.parse("$[spin chocoretto") |> Encoder.to_html()
MfmParser.Parser.parse("$[spin. ") |> Encoder.to_html()
MfmParser.Parser.parse("$[spin. chocoretto") |> Encoder.to_html()
MfmParser.Parser.parse("$[spin.x= ") |> Encoder.to_html()
MfmParser.Parser.parse("$[spin.x= chocoretto") |> Encoder.to_html()
MfmParser.Parser.parse("$[spin. chocoretto]") |> Encoder.to_html()
MfmParser.Parser.parse("$[spin.x= chocoretto]") |> Encoder.to_html()
MfmParser.Parser.parse("$[sp") |> Encoder.to_html()
end
end
end

View file

@ -4,7 +4,6 @@ defmodule MfmParser.LexerTest do
alias MfmParser.Lexer
alias MfmParser.Token.MFM
alias MfmParser.Token.Newline
alias MfmParser.Token.Text
describe "eof" do
@ -82,16 +81,4 @@ defmodule MfmParser.LexerTest do
{%Text{content: "Tu abuela ve anime y no se lava el culo"}, ""}
end
end
describe "newline token" do
test "it handles \n as a token" do
assert Lexer.peek("\nchocolat") == %Newline{content: "\n"}
assert Lexer.next("\nchocolat") == {%Newline{content: "\n"}, "chocolat"}
end
test "it works at the eof" do
assert Lexer.peek("\n") == %Newline{content: "\n"}
assert Lexer.next("\n") == {%Newline{content: "\n"}, ""}
end
end
end

View file

@ -12,611 +12,139 @@ defmodule MfmParser.ParserTest do
end
test "it can handle text as input" do
input = "pain au chocolat"
input = "pain\nau\nchocolat"
output = [%MfmParser.Node.Text{props: %{text: "pain au chocolat"}}]
output = [%MfmParser.Node.Text{content: "pain\nau\nchocolat"}]
assert Parser.parse(input) == output
end
test "it can handle a newline as input" do
input = "\n"
test "it can handle an element without attributes" do
input = "$[flip ]"
output = [%MfmParser.Node.Newline{props: %{text: "\n"}}]
output = [%MfmParser.Node.MFM{name: "flip", attributes: [], content: []}]
assert Parser.parse(input) == output
end
test "it can handle a flip element" do
input_default = "$[flip ]"
input_v = "$[flip.v ]"
input_hv = "$[flip.h,v ]"
test "it can handle an element with one non-value attribute" do
input = "$[font.cursive ]"
output_default = [
%MfmParser.Node.MFM.Flip{
props: %{
v: false,
h: false
},
children: []
}
]
output = [%MfmParser.Node.MFM{name: "font", attributes: [{"cursive"}], content: []}]
output_v = [
%MfmParser.Node.MFM.Flip{
props: %{
v: true,
h: false
},
children: []
}
]
output_hv = [
%MfmParser.Node.MFM.Flip{
props: %{
v: true,
h: true
},
children: []
}
]
assert Parser.parse(input_default) == output_default
assert Parser.parse(input_v) == output_v
assert Parser.parse(input_hv) == output_hv
assert Parser.parse(input) == output
end
test "it can handle a font element" do
input = "$[font.serif ]"
test "it can handle an element with one value attribute" do
input = "$[jelly.speed=2s ]"
output = [%MfmParser.Node.MFM{name: "jelly", attributes: [{"speed", "2s"}], content: []}]
assert Parser.parse(input) == output
end
test "it can handle an element with multiple attributes" do
input = "$[spin.alternate,speed=0.5s ]"
output = [
%MfmParser.Node.MFM.Font{
props: %{
font: "serif"
},
children: []
%MfmParser.Node.MFM{
name: "spin",
attributes: [{"alternate"}, {"speed", "0.5s"}],
content: []
}
]
assert Parser.parse(input) == output
end
test "it can handle an x element" do
input2 = "$[x2 ]"
input3 = "$[x3 ]"
input4 = "$[x4 ]"
output2 = [
%MfmParser.Node.MFM.X{
props: %{
size: "200%"
},
children: []
}
]
output3 = [
%MfmParser.Node.MFM.X{
props: %{
size: "400%"
},
children: []
}
]
output4 = [
%MfmParser.Node.MFM.X{
props: %{
size: "600%"
},
children: []
}
]
assert Parser.parse(input2) == output2
assert Parser.parse(input3) == output3
assert Parser.parse(input4) == output4
end
test "it can handle a blur element" do
input = "$[blur ]"
output = [
%MfmParser.Node.MFM.Blur{
props: %{},
children: []
}
]
assert Parser.parse(input) == output
end
test "it can handle a jelly element" do
input_default = "$[jelly ]"
output_default = [
%MfmParser.Node.MFM.Jelly{
props: %{
speed: "1s"
},
children: []
}
]
input_speed = "$[jelly.speed=20s ]"
output_speed = [
%MfmParser.Node.MFM.Jelly{
props: %{
speed: "20s"
},
children: []
}
]
assert Parser.parse(input_default) == output_default
assert Parser.parse(input_speed) == output_speed
end
test "it can handle a tada element" do
input_default = "$[tada ]"
output_default = [
%MfmParser.Node.MFM.Tada{
props: %{
speed: "1s"
},
children: []
}
]
input_speed = "$[tada.speed=20s ]"
output_speed = [
%MfmParser.Node.MFM.Tada{
props: %{
speed: "20s"
},
children: []
}
]
assert Parser.parse(input_default) == output_default
assert Parser.parse(input_speed) == output_speed
end
test "it can handle a jump element" do
input_default = "$[jump ]"
output_default = [
%MfmParser.Node.MFM.Jump{
props: %{
speed: "0.75s"
},
children: []
}
]
input_speed = "$[jump.speed=20s ]"
output_speed = [
%MfmParser.Node.MFM.Jump{
props: %{
speed: "20s"
},
children: []
}
]
assert Parser.parse(input_default) == output_default
assert Parser.parse(input_speed) == output_speed
end
test "it can handle a bounce element" do
input_default = "$[bounce ]"
output_default = [
%MfmParser.Node.MFM.Bounce{
props: %{
speed: "0.75s"
},
children: []
}
]
input_speed = "$[bounce.speed=20s ]"
output_speed = [
%MfmParser.Node.MFM.Bounce{
props: %{
speed: "20s"
},
children: []
}
]
assert Parser.parse(input_default) == output_default
assert Parser.parse(input_speed) == output_speed
end
test "it can handle a spin element" do
input_default = "$[spin ]"
output_default = [
%MfmParser.Node.MFM.Spin{
props: %{
axis: "z",
direction: "normal",
speed: "1.5s"
},
children: []
}
]
input_left = "$[spin.left ]"
output_left = [
%MfmParser.Node.MFM.Spin{
props: %{
axis: "z",
direction: "left",
speed: "1.5s"
},
children: []
}
]
input_alternate = "$[spin.alternate ]"
output_alternate = [
%MfmParser.Node.MFM.Spin{
props: %{
axis: "z",
direction: "alternate",
speed: "1.5s"
},
children: []
}
]
input_x = "$[spin.x ]"
output_x = [
%MfmParser.Node.MFM.Spin{
props: %{
axis: "x",
direction: "normal",
speed: "1.5s"
},
children: []
}
]
input_x_left = "$[spin.x,left ]"
output_x_left = [
%MfmParser.Node.MFM.Spin{
props: %{
axis: "x",
direction: "left",
speed: "1.5s"
},
children: []
}
]
input_x_alternate = "$[spin.x,alternate ]"
output_x_alternate = [
%MfmParser.Node.MFM.Spin{
props: %{
axis: "x",
direction: "alternate",
speed: "1.5s"
},
children: []
}
]
input_y = "$[spin.y ]"
output_y = [
%MfmParser.Node.MFM.Spin{
props: %{
axis: "y",
direction: "normal",
speed: "1.5s"
},
children: []
}
]
input_y_left = "$[spin.y,left ]"
output_y_left = [
%MfmParser.Node.MFM.Spin{
props: %{
axis: "y",
direction: "left",
speed: "1.5s"
},
children: []
}
]
input_y_alternate = "$[spin.y,alternate ]"
output_y_alternate = [
%MfmParser.Node.MFM.Spin{
props: %{
axis: "y",
direction: "alternate",
speed: "1.5s"
},
children: []
}
]
input_speed = "$[spin.speed=20s ]"
output_speed = [
%MfmParser.Node.MFM.Spin{
props: %{
axis: "z",
direction: "normal",
speed: "20s"
},
children: []
}
]
assert Parser.parse(input_default) == output_default
assert Parser.parse(input_left) == output_left
assert Parser.parse(input_alternate) == output_alternate
assert Parser.parse(input_x) == output_x
assert Parser.parse(input_x_left) == output_x_left
assert Parser.parse(input_x_alternate) == output_x_alternate
assert Parser.parse(input_y) == output_y
assert Parser.parse(input_y_left) == output_y_left
assert Parser.parse(input_y_alternate) == output_y_alternate
assert Parser.parse(input_speed) == output_speed
end
test "it can handle a shake element" do
input_default = "$[shake ]"
output_default = [
%MfmParser.Node.MFM.Shake{
props: %{
speed: "0.5s"
},
children: []
}
]
input_speed = "$[shake.speed=20s ]"
output_speed = [
%MfmParser.Node.MFM.Shake{
props: %{
speed: "20s"
},
children: []
}
]
assert Parser.parse(input_default) == output_default
assert Parser.parse(input_speed) == output_speed
end
test "it can handle a twitch element" do
input_default = "$[twitch ]"
output_default = [
%MfmParser.Node.MFM.Twitch{
props: %{
speed: "0.5s"
},
children: []
}
]
input_speed = "$[twitch.speed=0.2s ]"
output_speed = [
%MfmParser.Node.MFM.Twitch{
props: %{
speed: "0.2s"
},
children: []
}
]
assert Parser.parse(input_default) == output_default
assert Parser.parse(input_speed) == output_speed
end
test "it can handle a rainbow element" do
input_default = "$[rainbow ]"
output_default = [
%MfmParser.Node.MFM.Rainbow{
props: %{
speed: "1s"
},
children: []
}
]
input_speed = "$[rainbow.speed=20s ]"
output_speed = [
%MfmParser.Node.MFM.Rainbow{
props: %{
speed: "20s"
},
children: []
}
]
assert Parser.parse(input_default) == output_default
assert Parser.parse(input_speed) == output_speed
end
test "it can handle a sparkle element" do
input = "$[sparkle ]"
output = [
%MfmParser.Node.MFM.Sparkle{
props: %{},
children: []
}
]
assert Parser.parse(input) == output
end
test "it can handle a rotate element" do
input = "$[rotate ]"
output = [
%MfmParser.Node.MFM.Rotate{
props: %{},
children: []
}
]
assert Parser.parse(input) == output
end
test "it can handle an undefined element" do
input = "$[blabla ]"
output = [
%MfmParser.Node.MFM.Undefined{
props: %{},
children: []
}
]
assert Parser.parse(input) == output
end
test "it doesn't crash on a lost end token" do
Parser.parse("]")
end
test "it doesn't crash on a non-closed token" do
Parser.parse("$[spi")
Parser.parse("$[spin ")
Parser.parse("$[spin chocolatine")
end
end
describe "multiple element input" do
test "it can handle multiple elements as input" do
input = "$[twitch ]chocolatine$[blabla ]\n$[jump ]"
assert Parser.parse(input) == [
%MfmParser.Node.MFM.Twitch{children: [], props: %{speed: "0.5s"}},
%MfmParser.Node.Text{props: %{text: "chocolatine"}},
%MfmParser.Node.MFM.Undefined{children: [], props: %{}},
%MfmParser.Node.Newline{props: %{text: "\n"}},
%MfmParser.Node.MFM.Jump{children: [], props: %{speed: "0.75s"}}
output = [
%MfmParser.Node.MFM{name: "twitch", attributes: [], content: []},
%MfmParser.Node.Text{content: "chocolatine"},
%MfmParser.Node.MFM{name: "blabla", attributes: [], content: []},
%MfmParser.Node.Text{content: "\n"},
%MfmParser.Node.MFM{name: "jump", attributes: [], content: []}
]
assert Parser.parse(input) == output
end
test "it can handle nesting" do
input = "$[twitch chocolatine]"
assert Parser.parse(input) == [
%MfmParser.Node.MFM.Twitch{
children: [%MfmParser.Node.Text{props: %{text: "chocolatine"}}],
props: %{speed: "0.5s"}
output = [
%MfmParser.Node.MFM{
name: "twitch",
attributes: [],
content: [%MfmParser.Node.Text{content: "chocolatine"}]
}
]
assert Parser.parse(input) == output
end
test "it can handle multiple nesting" do
test "it can handle multiple nestings" do
input = "$[twitch $[spin chocolatine]]"
assert Parser.parse(input) == [
%MfmParser.Node.MFM.Twitch{
children: [
%MfmParser.Node.MFM.Spin{
children: [%MfmParser.Node.Text{props: %{text: "chocolatine"}}],
props: %{direction: "normal", axis: "z", speed: "1.5s"}
}
],
props: %{speed: "0.5s"}
output = [
%MfmParser.Node.MFM{
name: "twitch",
attributes: [],
content: [
%MfmParser.Node.MFM{
name: "spin",
attributes: [],
content: [%MfmParser.Node.Text{content: "chocolatine"}]
}
]
}
]
assert Parser.parse(input) == output
end
test "it can handle a complex structure of multiple elements and nesting" do
input =
"It's not $[twitch chocolatine]\nit's $[x4 $[spin pain] $[rainbow au] $[jump chocolat]]"
"It's not $[twitch chocolatine]\nit's $[x4 $[spin.alternate,speed=0.2s pain] $[rainbow au] $[jump.speed=0.5s chocolat]]"
assert Parser.parse(input) == [
%MfmParser.Node.Text{props: %{text: "It's not "}},
%MfmParser.Node.MFM.Twitch{
children: [%MfmParser.Node.Text{props: %{text: "chocolatine"}}],
props: %{speed: "0.5s"}
output = [
%MfmParser.Node.Text{content: "It's not "},
%MfmParser.Node.MFM{
name: "twitch",
attributes: [],
content: [%MfmParser.Node.Text{content: "chocolatine"}]
},
%MfmParser.Node.Newline{props: %{text: "\n"}},
%MfmParser.Node.Text{props: %{text: "it's "}},
%MfmParser.Node.MFM.X{
children: [
%MfmParser.Node.MFM.Spin{
children: [%MfmParser.Node.Text{props: %{text: "pain"}}],
props: %{direction: "normal", axis: "z", speed: "1.5s"}
%MfmParser.Node.Text{content: "\nit's "},
%MfmParser.Node.MFM{
name: "x4",
attributes: [],
content: [
%MfmParser.Node.MFM{
name: "spin",
attributes: [{"alternate"}, {"speed", "0.2s"}],
content: [%MfmParser.Node.Text{content: "pain"}]
},
%MfmParser.Node.Text{props: %{text: " "}},
%MfmParser.Node.MFM.Rainbow{
children: [%MfmParser.Node.Text{props: %{text: "au"}}],
props: %{speed: "1s"}
%MfmParser.Node.Text{content: " "},
%MfmParser.Node.MFM{
name: "rainbow",
attributes: [],
content: [%MfmParser.Node.Text{content: "au"}]
},
%MfmParser.Node.Text{props: %{text: " "}},
%MfmParser.Node.MFM.Jump{
children: [%MfmParser.Node.Text{props: %{text: "chocolat"}}],
props: %{speed: "0.75s"}
}
],
props: %{size: "600%"}
%MfmParser.Node.Text{content: " "},
%MfmParser.Node.MFM{
name: "jump",
attributes: [{"speed", "0.5s"}],
content: [%MfmParser.Node.Text{content: "chocolat"}]
}
]
}
]
end
end
describe "to_props/1" do
test "it returns speed in the list of parameters" do
assert %{speed: "5s"} = Parser.to_props("$[blabla.speed=5s")
assert %{speed: "0.5s"} = Parser.to_props("$[blabla.speed=0.5s")
end
test "it returns v and h in the list of parameters" do
assert %{v: true} = Parser.to_props("$[blabla.v")
assert %{v: true, h: true} = Parser.to_props("$[blabla.h,v")
end
test "it returns fonts" do
assert %{font: "some_font"} = Parser.to_props("$[font.some_font")
end
test "it returns a size for an x element" do
assert %{size: "200%"} = Parser.to_props("$[x2")
assert %{size: "400%"} = Parser.to_props("$[x3")
assert %{size: "600%"} = Parser.to_props("$[x4")
assert %{size: "100%"} = Parser.to_props("$[xqsdfqsf")
end
test "it returns an empty list when there are no parameters" do
assert %{} = Parser.to_props("$[blabla")
end
test "it ignores unknown parameters" do
assert %{} = Parser.to_props("$[blabla.idk")
assert Parser.parse(input) == output
end
end
end