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,85 +1,91 @@
# Akkoma-MFMParser
extremely simple modification to [the original parser](https://codeberg.org/ilja/mfm_parser) that just doesn't include the CSS
# MfmParser # 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. It only parses the MFM specific syntax of the form `$[name.attributes content]`.
That means that it doesn't parse links, usernames, HTML, Markdown or Katex. 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{ %MfmParser.Node.Text{
props: %{ content: "it's not chocolatine, it's "
text: "it's not chocolatine, it's "
}
}, },
%MfmParser.MFM.Twitch{ %MfmParser.Node.MFM{
props: %{ name: "twitch",
speed: "0.2s" attributes: [
}, [{"alternate"}, {"speed", "0.5s"}]
children: [ ],
%MfmParser.Text{ content: [
props: %{ %MfmParser.Node.Text{
text: "pain au chocolat" 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 ## 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{ %MfmParser.Node.MFM{
children: [%MfmParser.Node.Text{props: %{text: "🍮"}}], name: "twitch",
props: %{speed: "5s"} 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 ## Reading
### The Parser ### The Parser
A [parser](https://en.wikipedia.org/wiki/Parsing#Parser) takes in structured text and outputs a so called "tree". A tree is a data structure which can be more easily worked with. A [parser](https://en.wikipedia.org/wiki/Parsing#Parser) takes in structured text and outputs a so called "tree". A tree is a data structure which can be more easily worked with.
A parser typically consists of three parts A parser typically consists of three parts
* a Reader * a Reader
* a Lexer (aka Tokeniser) * a Lexer (aka Tokeniser)
* the Parser * the Parser
A Reader typically has a `next` function which takes the next character out of the input and returns it. A Reader typically has a `next` function which takes the next character out of the input and returns it.
A `peek` function allows it to peek at the next character without changing the input. A `peek` function allows it to peek at the next character without changing the input.
There's also some way of detecting if the eof (End Of File) is reached. There's also some way of detecting if the eof (End Of File) is reached.
Depending on the needs of the parser, it may be implemented to allow asking for the nth character instead of just the next. Depending on the needs of the parser, it may be implemented to allow asking for the nth character instead of just the next.
A Lexer uses the Reader. It also has a `peek` and `next` function, but instead of returning the next (or nth) character, it returns the next (or nth) token. A Lexer uses the Reader. It also has a `peek` and `next` function, but instead of returning the next (or nth) character, it returns the next (or nth) token.
E.g. if you have the MFM `$[spin some text]`, then `$[spin`, `some text`, and `]` can be considered three different tokens. E.g. if you have the MFM `$[spin some text]`, then `$[spin`, `some text`, and `]` can be considered three different tokens.
The parser takes in the tokens and forms the tree. This is typically a data structure the programming language understands and can more easily work with. The parser takes in the tokens and forms the tree. This is typically a data structure the programming language understands and can more easily work with.
### The Encoder ### The Encoder
Once we have a good data structure, we can process this and do things with it. Once we have a good data structure, we can process this and do things with it.
E.g. an Encoder encodes the tree into a different format. E.g. an Encoder encodes the tree into a different format.
### The code ### 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 ## License
A parser/encoder for Misskey Flavoured Markdown. 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 This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as it under the terms of the GNU Affero General Public License as

View file

@ -1,198 +1,50 @@
defmodule MfmParser.Encoder do defmodule MfmParser.Encoder do
alias MfmParser.Parser
alias MfmParser.Node
@moduledoc """ @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]. 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.
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`.
## Examples ## Examples
iex> [ iex> [
...> %MfmParser.Node.MFM.Twitch{ ...> %MfmParser.Node.MFM{
...> children: [%MfmParser.Node.Text{props: %{text: "🍮"}}], ...> name: "twitch",
...> props: %{speed: "5s"} ...> content: [%MfmParser.Node.Text{content: "🍮"}],
...> attributes: [{"speed", "5s"}]
...> } ...> }
...> ] ...> ]
...> |> MfmParser.Encoder.to_html() ...> |> 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() iex> "$[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>" ~S[<span class="mfm-twitch" data-mfm-speed="5s">🍮</span>]
""" """
def to_html(tree) when is_list(tree) do def to_html([node | rest]) do
{html, _styles} = to_html_styles(tree) node_html =
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} ->
case node do case node do
%Node.Text{} -> %MfmParser.Node.Text{content: content} ->
{html <> node.props.text, styles} content
%Node.Newline{} -> %MfmParser.Node.MFM{name: name, attributes: attributes, content: content} ->
{html <> node.props.text, styles} attributes_string =
attributes
|> Enum.reduce("", fn
{name}, acc -> acc <> " data-mfm-#{name}"
{name, value}, acc -> acc <> " data-mfm-#{name}=\"#{value}\""
end)
%Node.MFM.Flip{} -> "<span class=\"mfm-#{name}\"#{attributes_string}>#{to_html(content)}</span>"
{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 end
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
end end

View file

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

View file

@ -1,67 +1,7 @@
defmodule MfmParser.Node.Text do defmodule MfmParser.Node.Text do
defstruct props: %{text: ""} defstruct content: ""
end end
defmodule MfmParser.Node.Newline do defmodule MfmParser.Node.MFM do
defstruct props: %{text: "\n"} defstruct name: "", attributes: %{}, content: []
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: []
end end

View file

@ -4,164 +4,98 @@ defmodule MfmParser.Parser do
alias MfmParser.Lexer alias MfmParser.Lexer
@moduledoc """ @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. Other parts of MFM (html, Markdown and [KaTeX](https://katex.org/)) are out of scope for this project.
## Examples ## Examples
iex> MfmParser.Parser.parse("$[twitch.speed=5s 🍮]") iex> MfmParser.Parser.parse("$[twitch.speed=0.5s 🍮]")
[ [
%MfmParser.Node.MFM.Twitch{ %MfmParser.Node.MFM{
children: [%MfmParser.Node.Text{props: %{text: "🍮"}}], name: "twitch",
props: %{speed: "5s"} 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 case Lexer.next(input) do
{token, rest} ->
case token do
%Token.Text{} ->
parse(
rest,
tree ++ [%Node.Text{content: token.content}],
is_open
)
%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 ++ [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{content: token.content}]
)
end
end
:eof -> :eof ->
tree tree
end
end
{token, rest} -> defp get_mfm_node(token) do
if is_end_token.(token) do {name, attributes} =
{tree, rest} case token.content
else |> String.trim()
case token do |> String.replace("$[", "")
%Token.MFM.Open{} -> |> String.split(".", parts: 2) do
{children, rest} = [name] -> {name, []}
case parse(rest, [], &is_mfm_close_token?/1) do [name, attributes_string] -> {name, build_attributes_list(attributes_string)}
{children, rest} -> end
{children, rest}
_ -> %Node.MFM{name: name, attributes: attributes, content: []}
{[], rest} end
end
parse( defp build_attributes_list(attributes_string) do
rest, attributes_string
tree ++ [token |> get_node() |> Map.put(:children, children)], |> String.split(",")
is_end_token |> Enum.reduce([], fn attribute_string, acc ->
) attribute =
case attribute_string |> String.split("=") do
%Token.Text{} -> [name] -> {name}
parse( [name, value] -> {name, value}
rest,
tree ++ [%Node.Text{props: %{text: token.content}}],
is_end_token
)
%Token.Newline{} ->
parse(
rest,
tree ++ [%Node.Newline{props: %{text: token.content}}],
is_end_token
)
%Token.MFM.Close{} ->
parse(
rest,
tree ++ [%Node.Text{props: %{text: token.content}}],
is_end_token
)
end
end end
end
end
defp is_mfm_close_token?(token) do acc ++ [attribute]
case token do end)
%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, "")
|> String.trim()
|> 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
%{}
end
end
)
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
end end

View file

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

View file

@ -4,7 +4,7 @@ defmodule MfmParser.MixProject do
def project do def project do
[ [
app: :mfm_parser, app: :mfm_parser,
version: "0.1.0", version: "0.2.0",
elixir: "~> 1.13", elixir: "~> 1.13",
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
deps: deps() deps: deps()
@ -14,12 +14,15 @@ defmodule MfmParser.MixProject do
# Run "mix help compile.app" to learn about applications. # Run "mix help compile.app" to learn about applications.
def application do def application do
[ [
extra_applications: [:logger] # extra_applications: [:logger]
] ]
end end
# Run "mix help deps" to learn about dependencies. # Run "mix help deps" to learn about dependencies.
defp deps do 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
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 describe "to_html" do
test "it handles text" do test "it handles text" do
input_tree = [%Node.Text{props: %{text: "chocolatine"}}] input_tree = [%Node.Text{content: "chocolatine"}]
expected = "chocolatine" expected = "chocolatine"
assert Encoder.to_html(input_tree) == expected assert Encoder.to_html(input_tree) == expected
end end
test "it handles newlines" do test "it handles a node without attributes" do
input_tree = [%Node.Newline{props: %{text: "\n"}}] input = [%Node.MFM{name: "flip", attributes: %{}, content: []}]
expected = "\n" expected = "<span class=\"mfm-flip\"></span>"
assert Encoder.to_html(input_tree) == expected assert Encoder.to_html(input) == expected
end end
test "it handles flip" do test "it handles a node with a non-value attribute" do
input_tree = [ input = [%Node.MFM{name: "font", attributes: [{"cursive"}], content: []}]
%Node.MFM.Flip{
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}]
}
]
input_tree_v = [ expected = "<span class=\"mfm-font\" data-mfm-cursive></span>"
%Node.MFM.Flip{
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}],
props: %{v: true}
}
]
input_tree_h_v = [ assert Encoder.to_html(input) == expected
%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 end
test "it handles font" do test "it handles a node with one value attribute" do
input_tree = [ input = [%Node.MFM{name: "jelly", attributes: [{"speed", "2s"}], content: []}]
%Node.MFM.Font{
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}],
props: %{font: "fantasy"}
}
]
expected = expected = "<span class=\"mfm-jelly\" data-mfm-speed=\"2s\"></span>"
~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 assert Encoder.to_html(input) == expected
end end
test "it handles x" do test "it handles a node with multiple attributes" do
input_tree = [ input = [
%Node.MFM.X{ %Node.MFM{
children: [%Node.Text{props: %{text: "🍮"}}], name: "spin",
props: %{size: "400%"} attributes: [{"alternate"}, {"speed", "0.5s"}],
content: []
} }
] ]
expected = ~s[<span style="font-size: 400%" class="mfm _mfm_x3_">🍮</span>] expected = "<span class=\"mfm-spin\" data-mfm-alternate data-mfm-speed=\"0.5s\"></span>"
assert Encoder.to_html(input_tree) == expected assert Encoder.to_html(input) == 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 end
test "it handles multpile nodes on the same level" do test "it handles multpile nodes on the same level" do
input_tree = [ input = [
%Node.MFM.Rotate{ %Node.MFM{name: "twitch", attributes: [], content: []},
children: [%Node.Text{props: %{text: "🍮"}}] %Node.Text{content: "chocolatine"},
}, %Node.MFM{name: "blabla", attributes: [], content: []}
%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 = expected = "<span class=\"mfm-twitch\"></span>chocolatine<span class=\"mfm-blabla\"></span>"
~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 assert Encoder.to_html(input) == expected
end end
test "it handles nesting" do test "it handles nesting" do
input_tree = [ input = [
%Node.MFM.Rotate{ %Node.MFM{
children: [ name: "twitch",
%Node.MFM.Font{ attributes: [],
children: [%Node.Text{props: %{text: "🍮"}}], content: [%Node.Text{content: "chocolatine"}]
props: %{font: "fantasy"} }
]
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 = 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>] "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_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>"
assert Encoder.to_html(input) == expected assert Encoder.to_html(input) == expected
end 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
end end

View file

@ -4,7 +4,6 @@ defmodule MfmParser.LexerTest do
alias MfmParser.Lexer alias MfmParser.Lexer
alias MfmParser.Token.MFM alias MfmParser.Token.MFM
alias MfmParser.Token.Newline
alias MfmParser.Token.Text alias MfmParser.Token.Text
describe "eof" do describe "eof" do
@ -82,16 +81,4 @@ defmodule MfmParser.LexerTest do
{%Text{content: "Tu abuela ve anime y no se lava el culo"}, ""} {%Text{content: "Tu abuela ve anime y no se lava el culo"}, ""}
end end
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 end

View file

@ -12,611 +12,139 @@ defmodule MfmParser.ParserTest do
end end
test "it can handle text as input" do 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 assert Parser.parse(input) == output
end end
test "it can handle a newline as input" do test "it can handle an element without attributes" do
input = "\n" input = "$[flip ]"
output = [%MfmParser.Node.Newline{props: %{text: "\n"}}] output = [%MfmParser.Node.MFM{name: "flip", attributes: [], content: []}]
assert Parser.parse(input) == output assert Parser.parse(input) == output
end end
test "it can handle a flip element" do test "it can handle an element with one non-value attribute" do
input_default = "$[flip ]" input = "$[font.cursive ]"
input_v = "$[flip.v ]"
input_hv = "$[flip.h,v ]"
output_default = [ output = [%MfmParser.Node.MFM{name: "font", attributes: [{"cursive"}], content: []}]
%MfmParser.Node.MFM.Flip{
props: %{
v: false,
h: false
},
children: []
}
]
output_v = [ assert Parser.parse(input) == output
%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
end end
test "it can handle a font element" do test "it can handle an element with one value attribute" do
input = "$[font.serif ]" 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 = [ output = [
%MfmParser.Node.MFM.Font{ %MfmParser.Node.MFM{
props: %{ name: "spin",
font: "serif" attributes: [{"alternate"}, {"speed", "0.5s"}],
}, content: []
children: []
} }
] ]
assert Parser.parse(input) == output assert Parser.parse(input) == output
end 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 end
describe "multiple element input" do describe "multiple element input" do
test "it can handle multiple elements as input" do test "it can handle multiple elements as input" do
input = "$[twitch ]chocolatine$[blabla ]\n$[jump ]" input = "$[twitch ]chocolatine$[blabla ]\n$[jump ]"
assert Parser.parse(input) == [ output = [
%MfmParser.Node.MFM.Twitch{children: [], props: %{speed: "0.5s"}}, %MfmParser.Node.MFM{name: "twitch", attributes: [], content: []},
%MfmParser.Node.Text{props: %{text: "chocolatine"}}, %MfmParser.Node.Text{content: "chocolatine"},
%MfmParser.Node.MFM.Undefined{children: [], props: %{}}, %MfmParser.Node.MFM{name: "blabla", attributes: [], content: []},
%MfmParser.Node.Newline{props: %{text: "\n"}}, %MfmParser.Node.Text{content: "\n"},
%MfmParser.Node.MFM.Jump{children: [], props: %{speed: "0.75s"}} %MfmParser.Node.MFM{name: "jump", attributes: [], content: []}
] ]
assert Parser.parse(input) == output
end end
test "it can handle nesting" do test "it can handle nesting" do
input = "$[twitch chocolatine]" input = "$[twitch chocolatine]"
assert Parser.parse(input) == [ output = [
%MfmParser.Node.MFM.Twitch{ %MfmParser.Node.MFM{
children: [%MfmParser.Node.Text{props: %{text: "chocolatine"}}], name: "twitch",
props: %{speed: "0.5s"} attributes: [],
} content: [%MfmParser.Node.Text{content: "chocolatine"}]
] }
]
assert Parser.parse(input) == output
end end
test "it can handle multiple nesting" do test "it can handle multiple nestings" do
input = "$[twitch $[spin chocolatine]]" input = "$[twitch $[spin chocolatine]]"
assert Parser.parse(input) == [ output = [
%MfmParser.Node.MFM.Twitch{ %MfmParser.Node.MFM{
children: [ name: "twitch",
%MfmParser.Node.MFM.Spin{ attributes: [],
children: [%MfmParser.Node.Text{props: %{text: "chocolatine"}}], content: [
props: %{direction: "normal", axis: "z", speed: "1.5s"} %MfmParser.Node.MFM{
} name: "spin",
], attributes: [],
props: %{speed: "0.5s"} content: [%MfmParser.Node.Text{content: "chocolatine"}]
} }
] ]
}
]
assert Parser.parse(input) == output
end end
test "it can handle a complex structure of multiple elements and nesting" do test "it can handle a complex structure of multiple elements and nesting" do
input = 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) == [ output = [
%MfmParser.Node.Text{props: %{text: "It's not "}}, %MfmParser.Node.Text{content: "It's not "},
%MfmParser.Node.MFM.Twitch{ %MfmParser.Node.MFM{
children: [%MfmParser.Node.Text{props: %{text: "chocolatine"}}], name: "twitch",
props: %{speed: "0.5s"} attributes: [],
}, content: [%MfmParser.Node.Text{content: "chocolatine"}]
%MfmParser.Node.Newline{props: %{text: "\n"}}, },
%MfmParser.Node.Text{props: %{text: "it's "}}, %MfmParser.Node.Text{content: "\nit's "},
%MfmParser.Node.MFM.X{ %MfmParser.Node.MFM{
children: [ name: "x4",
%MfmParser.Node.MFM.Spin{ attributes: [],
children: [%MfmParser.Node.Text{props: %{text: "pain"}}], content: [
props: %{direction: "normal", axis: "z", speed: "1.5s"} %MfmParser.Node.MFM{
}, name: "spin",
%MfmParser.Node.Text{props: %{text: " "}}, attributes: [{"alternate"}, {"speed", "0.2s"}],
%MfmParser.Node.MFM.Rainbow{ content: [%MfmParser.Node.Text{content: "pain"}]
children: [%MfmParser.Node.Text{props: %{text: "au"}}], },
props: %{speed: "1s"} %MfmParser.Node.Text{content: " "},
}, %MfmParser.Node.MFM{
%MfmParser.Node.Text{props: %{text: " "}}, name: "rainbow",
%MfmParser.Node.MFM.Jump{ attributes: [],
children: [%MfmParser.Node.Text{props: %{text: "chocolat"}}], content: [%MfmParser.Node.Text{content: "au"}]
props: %{speed: "0.75s"} },
} %MfmParser.Node.Text{content: " "},
], %MfmParser.Node.MFM{
props: %{size: "600%"} name: "jump",
} attributes: [{"speed", "0.5s"}],
] content: [%MfmParser.Node.Text{content: "chocolat"}]
end }
end ]
}
]
describe "to_props/1" do assert Parser.parse(input) == output
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")
end end
end end
end end