Use FEP-c16b: Formatting MFM functions #2
11 changed files with 363 additions and 1398 deletions
88
README.md
88
README.md
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
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 Reader
|
||||
* a Lexer (aka Tokeniser)
|
||||
* the Parser
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
|
210
lib/encoder.ex
210
lib/encoder.ex
|
@ -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}
|
||||
%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)
|
||||
|
||||
%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}
|
||||
"<span class=\"mfm-#{name}\"#{attributes_string}>#{to_html(content)}</span>"
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
66
lib/node.ex
66
lib/node.ex
|
@ -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
|
||||
|
|
214
lib/parser.ex
214
lib/parser.ex
|
@ -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.
|
||||
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
|
||||
{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 ->
|
||||
tree
|
||||
end
|
||||
end
|
||||
|
||||
{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}
|
||||
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
|
||||
|
||||
_ ->
|
||||
{[], rest}
|
||||
end
|
||||
%Node.MFM{name: name, attributes: attributes, content: []}
|
||||
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
|
||||
)
|
||||
|
||||
%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
|
||||
defp build_attributes_list(attributes_string) do
|
||||
attributes_string
|
||||
|> String.split(",")
|
||||
|> Enum.reduce([], fn attribute_string, acc ->
|
||||
attribute =
|
||||
case attribute_string |> String.split("=") do
|
||||
[name] -> {name}
|
||||
[name, value] -> {name, value}
|
||||
end
|
||||
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, "")
|
||||
|> 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
|
||||
acc ++ [attribute]
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
9
mix.exs
9
mix.exs
|
@ -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
|
||||
|
|
4
mix.lock
4
mix.lock
|
@ -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"]},
|
||||
}
|
|
@ -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"
|
||||
expected = "<span class=\"mfm-flip\"></span>"
|
||||
|
||||
assert Encoder.to_html(input_tree) == expected
|
||||
assert Encoder.to_html(input) == expected
|
||||
end
|
||||
|
||||
test "it handles flip" do
|
||||
input_tree = [
|
||||
%Node.MFM.Flip{
|
||||
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}]
|
||||
}
|
||||
]
|
||||
test "it handles a node with a non-value attribute" do
|
||||
input = [%Node.MFM{name: "font", attributes: [{"cursive"}], content: []}]
|
||||
|
||||
input_tree_v = [
|
||||
%Node.MFM.Flip{
|
||||
children: [%Node.Text{props: %{text: "Misskey expands the world of the Fediverse"}}],
|
||||
props: %{v: true}
|
||||
}
|
||||
]
|
||||
expected = "<span class=\"mfm-font\" data-mfm-cursive></span>"
|
||||
|
||||
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
|
||||
assert Encoder.to_html(input) == expected
|
||||
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"}
|
||||
}
|
||||
]
|
||||
test "it handles a node with one value attribute" do
|
||||
input = [%Node.MFM{name: "jelly", attributes: [{"speed", "2s"}], content: []}]
|
||||
|
||||
expected =
|
||||
~s[<span class="mfm" style="display: inline-block; font-family: fantasy;">Misskey expands the world of the Fediverse</span>]
|
||||
expected = "<span class=\"mfm-jelly\" data-mfm-speed=\"2s\"></span>"
|
||||
|
||||
assert Encoder.to_html(input_tree) == expected
|
||||
assert Encoder.to_html(input) == expected
|
||||
end
|
||||
|
||||
test "it handles x" do
|
||||
input_tree = [
|
||||
%Node.MFM.X{
|
||||
children: [%Node.Text{props: %{text: "🍮"}}],
|
||||
props: %{size: "400%"}
|
||||
test "it handles a node with multiple attributes" do
|
||||
input = [
|
||||
%Node.MFM{
|
||||
name: "spin",
|
||||
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
|
||||
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
|
||||
assert Encoder.to_html(input) == 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"}
|
||||
}
|
||||
input = [
|
||||
%Node.MFM{name: "twitch", attributes: [], content: []},
|
||||
%Node.Text{content: "chocolatine"},
|
||||
%Node.MFM{name: "blabla", attributes: [], content: []}
|
||||
]
|
||||
|
||||
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>]
|
||||
expected = "<span class=\"mfm-twitch\"></span>chocolatine<span class=\"mfm-blabla\"></span>"
|
||||
|
||||
assert Encoder.to_html(input_tree) == expected
|
||||
assert Encoder.to_html(input) == expected
|
||||
end
|
||||
|
||||
test "it handles nesting" do
|
||||
input_tree = [
|
||||
%Node.MFM.Rotate{
|
||||
children: [
|
||||
%Node.MFM.Font{
|
||||
children: [%Node.Text{props: %{text: "🍮"}}],
|
||||
props: %{font: "fantasy"}
|
||||
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 =
|
||||
~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>"
|
||||
"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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"}
|
||||
},
|
||||
%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{props: %{text: " "}},
|
||||
%MfmParser.Node.MFM.Rainbow{
|
||||
children: [%MfmParser.Node.Text{props: %{text: "au"}}],
|
||||
props: %{speed: "1s"}
|
||||
},
|
||||
%MfmParser.Node.Text{props: %{text: " "}},
|
||||
%MfmParser.Node.MFM.Jump{
|
||||
children: [%MfmParser.Node.Text{props: %{text: "chocolat"}}],
|
||||
props: %{speed: "0.75s"}
|
||||
}
|
||||
],
|
||||
props: %{size: "600%"}
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
output = [
|
||||
%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"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue