forked from AkkomaGang/mfm-parser
Make parser FEP-c16b compliant
This is basically a rewrite of big parts of the parser, introducing a lot of breaking changes. The parser was originally written mostly as an exercise for myself and not really aimed as-is for practical usage. An adapted version has been used in Akkoma, however, and this pointed out serious flaws in how MFM was done in general on the fediverse. This was discussed [on the Foundkey issue tracker](FoundKeyGang/FoundKey#343) and a better way was decided. At the time of writing, this is being formalised into [FEP-c16b](https://codeberg.org/fediverse/fep/src/branch/main/fep/c16b/fep-c16b.md). This commit rewrites this parser to be FEP-c16b compliant. Previously, the parser had knowledge of the specific MFM functions. This was useful for setting default attribute values and adding specific CSS. This is not the case any more. The parser has no knowledge of specific MFM functions any more. It also had an understanding of the concept of newlines, this isn't the case any more either. It only does a "simple" translation from MFM function notation to FEP-c16b compliant HTML. Because of this, we also don't add CSS any more. It's up to the software who uses this HTML to decide what functions they want to provide and use the correct CSS. In practice the CSS from this parser was never used in Akkoma, so it's not really a loss.
This commit is contained in:
parent
a5faf98ecd
commit
418068793f
10 changed files with 355 additions and 1415 deletions
84
README.md
84
README.md
|
@ -1,81 +1,91 @@
|
|||
# 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
|
||||
|
|
231
lib/encoder.ex
231
lib/encoder.ex
|
@ -1,227 +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 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>"
|
||||
~S[<span class="mfm-twitch" data-mfm-speed="5s">🍮</span>]
|
||||
|
||||
iex> MfmParser.Parser.parse("$[twitch.speed=5s 🍮]") |> MfmParser.Encoder.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>"
|
||||
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 |> append_styles_when_not_empty(styles)
|
||||
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 style=\"display: inline-block; transform: scale(-1);\">#{html_child}</span>",
|
||||
styles}
|
||||
|
||||
%{v: true} ->
|
||||
{html <>
|
||||
"<span style=\"display: inline-block; transform: scaleY(-1);\">#{html_child}</span>",
|
||||
styles}
|
||||
|
||||
_ ->
|
||||
{html <>
|
||||
"<span 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 style=\"display: inline-block; font-family: #{node.props.font};\">#{html_child}</span>",
|
||||
styles ++ styles_child}
|
||||
|
||||
%Node.MFM.X{} ->
|
||||
{html_child, styles_child} = to_html_styles(node.children)
|
||||
|
||||
{html <>
|
||||
"<span font-size: \"#{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 style=\"display: inline-block; animation: #{node.props.speed} linear 0s infinite normal both running mfm-rubberBand;\">#{html_child}</span>",
|
||||
styles ++
|
||||
[
|
||||
"@keyframes mfm-rubberBand { 0% { transform:scaleZ(1) } 30% { transform:scale3d(1.25,.75,1) } 40% { transform:scale3d(.75,1.25,1) } 50% { transform:scale3d(1.15,.85,1) } 65% { transform:scale3d(.95,1.05,1) } 75% { transform:scale3d(1.05,.95,1) } to { transform:scaleZ(1) }}"
|
||||
] ++ styles_child}
|
||||
|
||||
%Node.MFM.Tada{} ->
|
||||
{html_child, styles_child} = to_html_styles(node.children)
|
||||
|
||||
{html <>
|
||||
"<span style=\"display: inline-block; font-size: 150%; animation: #{node.props.speed} linear 0s infinite normal both running tada;\">#{html_child}</span>",
|
||||
styles ++
|
||||
[
|
||||
"@keyframes tada { 0% { transform: scaleZ(1); } 10%, 20% { transform: scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg); } 30%, 50%, 70%, 90% { transform: scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg); } 40%, 60%, 80% { transform: scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg); } 100% { transform: scaleZ(1); }}"
|
||||
] ++ styles_child}
|
||||
|
||||
%Node.MFM.Jump{} ->
|
||||
{html_child, styles_child} = to_html_styles(node.children)
|
||||
|
||||
{html <>
|
||||
"<span style=\"display: inline-block; animation: #{node.props.speed} linear 0s infinite normal none running mfm-jump;\">#{html_child}</span>",
|
||||
styles ++
|
||||
[
|
||||
"@keyframes mfm-jump { 0% { transform:translateY(0) } 25% { transform:translateY(-16px) } 50% { transform:translateY(0) } 75% { transform:translateY(-8px) } to { transform:translateY(0) }}"
|
||||
] ++ styles_child}
|
||||
|
||||
%Node.MFM.Bounce{} ->
|
||||
{html_child, styles_child} = to_html_styles(node.children)
|
||||
|
||||
{html <>
|
||||
"<span 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 ++
|
||||
[
|
||||
"@keyframes mfm-bounce { 0% { transform:translateY(0) scale(1) } 25% { transform:translateY(-16px) scale(1) } 50% { transform:translateY(0) scale(1) } 75% { transform:translateY(0) scale(1.5,.75) } to { transform:translateY(0) scale(1) }}"
|
||||
] ++ 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 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 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 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 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 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 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
|
||||
|
||||
defp append_styles_when_not_empty(html, []) do
|
||||
html
|
||||
def to_html([]) do
|
||||
""
|
||||
end
|
||||
|
||||
defp append_styles_when_not_empty(html, styles) do
|
||||
styles = styles |> Enum.uniq() |> Enum.reduce("", fn style, acc -> style <> acc end)
|
||||
|
||||
html <> "<style>" <> styles <> "</style>"
|
||||
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
|
||||
|
|
4
mix.exs
4
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,7 +14,7 @@ defmodule MfmParser.MixProject do
|
|||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger]
|
||||
# extra_applications: [:logger]
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -8,426 +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 style="display: inline-block; transform: scaleX(-1);">Misskey expands the world of the Fediverse</span>]
|
||||
|
||||
expected_v =
|
||||
~s[<span style="display: inline-block; transform: scaleY(-1);">Misskey expands the world of the Fediverse</span>]
|
||||
|
||||
expected_h_v =
|
||||
~s[<span 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 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 font-size: "400%">🍮</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><style>._mfm_blur_ { filter: blur(6px); transition: filter .3s; } ._mfm_blur_:hover { filter: blur(0px); }</style>]
|
||||
|
||||
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 style="display: inline-block; animation: 1s linear 0s infinite normal both running mfm-rubberBand;">🍮</span><style>@keyframes mfm-rubberBand { 0% { transform:scaleZ(1) } 30% { transform:scale3d(1.25,.75,1) } 40% { transform:scale3d(.75,1.25,1) } 50% { transform:scale3d(1.15,.85,1) } 65% { transform:scale3d(.95,1.05,1) } 75% { transform:scale3d(1.05,.95,1) } to { transform:scaleZ(1) }}</style>]
|
||||
|
||||
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 style="display: inline-block; font-size: 150%; animation: 1s linear 0s infinite normal both running tada;">🍮</span><style>@keyframes tada { 0% { transform: scaleZ(1); } 10%, 20% { transform: scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg); } 30%, 50%, 70%, 90% { transform: scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg); } 40%, 60%, 80% { transform: scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg); } 100% { transform: scaleZ(1); }}</style>]
|
||||
|
||||
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 style="display: inline-block; animation: 0.75s linear 0s infinite normal none running mfm-jump;">🍮</span><style>@keyframes mfm-jump { 0% { transform:translateY(0) } 25% { transform:translateY(-16px) } 50% { transform:translateY(0) } 75% { transform:translateY(-8px) } to { transform:translateY(0) }}</style>]
|
||||
|
||||
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 style="display: inline-block; animation: 0.75s linear 0s infinite normal none running mfm-bounce; transform-origin: center bottom 0px;">🍮</span><style>@keyframes mfm-bounce { 0% { transform:translateY(0) scale(1) } 25% { transform:translateY(-16px) scale(1) } 50% { transform:translateY(0) scale(1) } 75% { transform:translateY(0) scale(1.5,.75) } to { transform:translateY(0) scale(1) }}</style>]
|
||||
|
||||
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 style="display: inline-block; animation: 1.5s linear 0s infinite reverse none running mfm-spin;">🍮</span><style>@keyframes mfm-spin { 0% { transform:rotate(0) } to { transform:rotate(360deg) }}</style>]
|
||||
|
||||
expected_tree_x_left =
|
||||
~s[<span style="display: inline-block; animation: 1.5s linear 0s infinite reverse none running mfm-spinX;">🍮</span><style>@keyframes mfm-spinX { 0% { transform:perspective(128px) rotateX(0) } to { transform:perspective(128px) rotateX(360deg) }}</style>]
|
||||
|
||||
expected_tree_y_left =
|
||||
~s[<span style="display: inline-block; animation: 1.5s linear 0s infinite reverse none running mfm-spinY;">🍮</span><style>@keyframes mfm-spinY { 0% { transform:perspective(128px) rotateY(0) } to { transform:perspective(128px) rotateY(360deg) }}</style>]
|
||||
|
||||
expected_tree_z_alternate =
|
||||
~s[<span style="display: inline-block; animation: 1.5s linear 0s infinite alternate none running mfm-spin;">🍮</span><style>@keyframes mfm-spin { 0% { transform:rotate(0) } to { transform:rotate(360deg) }}</style>]
|
||||
|
||||
expected_tree_x_alternate =
|
||||
~s[<span style="display: inline-block; animation: 1.5s linear 0s infinite alternate none running mfm-spinX;">🍮</span><style>@keyframes mfm-spinX { 0% { transform:perspective(128px) rotateX(0) } to { transform:perspective(128px) rotateX(360deg) }}</style>]
|
||||
|
||||
expected_tree_y_alternate =
|
||||
~s[<span style="display: inline-block; animation: 1.5s linear 0s infinite alternate none running mfm-spinY;">🍮</span><style>@keyframes mfm-spinY { 0% { transform:perspective(128px) rotateY(0) } to { transform:perspective(128px) rotateY(360deg) }}</style>]
|
||||
|
||||
expected_tree_z_normal =
|
||||
~s[<span style="display: inline-block; animation: 1.5s linear 0s infinite normal none running mfm-spin;">🍮</span><style>@keyframes mfm-spin { 0% { transform:rotate(0) } to { transform:rotate(360deg) }}</style>]
|
||||
|
||||
expected_tree_x_normal =
|
||||
~s[<span style="display: inline-block; animation: 1.5s linear 0s infinite normal none running mfm-spinX;">🍮</span><style>@keyframes mfm-spinX { 0% { transform:perspective(128px) rotateX(0) } to { transform:perspective(128px) rotateX(360deg) }}</style>]
|
||||
|
||||
expected_tree_y_normal =
|
||||
~s[<span style="display: inline-block; animation: 1.5s linear 0s infinite normal none running mfm-spinY;">🍮</span><style>@keyframes mfm-spinY { 0% { transform:perspective(128px) rotateY(0) } to { transform:perspective(128px) rotateY(360deg) }}</style>]
|
||||
|
||||
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 style="display: inline-block; animation: 0.5s ease 0s infinite normal none running mfm-shake;">🍮</span><style>@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) }}</style>]
|
||||
|
||||
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 style="display: inline-block; animation: 0.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>]
|
||||
|
||||
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 style="display: inline-block; animation: 1s linear 0s infinite normal none running mfm-rainbow;">🍮</span><style>@keyframes mfm-rainbow { 0% { filter:hue-rotate(0deg) contrast(150%) saturate(150%) } to { filter:hue-rotate(360deg) contrast(150%) saturate(150%) }}</style>]
|
||||
|
||||
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 style="display: inline-block; animation: 1s linear 0s infinite normal none running mfm-sparkle;">🍮</span><style>@keyframes mfm-sparkle { 0% { filter: brightness(100%) } to { filter: brightness(300%) }}</style>]
|
||||
|
||||
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 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 style="display: inline-block; transform: rotate(90deg); transform-origin: center center 0px;">🍮</span>pain au chocolat<span 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 style="display: inline-block; transform: rotate(90deg); transform-origin: center center 0px;"><span 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 style="display: inline-block; animation: 1s linear 0s infinite normal none running mfm-sparkle;">🍮</span><span style="display: inline-block; animation: 1s linear 0s infinite normal none running mfm-sparkle;">🍮</span><style>@keyframes mfm-sparkle { 0% { filter: brightness(100%) } to { filter: brightness(300%) }}</style>]
|
||||
|
||||
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 style=\"display: inline-block; animation: 0.2s ease 0s infinite normal none running mfm-twitch;\">chocolatine</span>\nit's <span font-size: \"600%\"><span style=\"display: inline-block; animation: 1s linear 0s infinite normal none running mfm-spin;\">pain</span> <span style=\"display: inline-block; animation: 2s linear 0s infinite normal none running mfm-rainbow;\">au</span> <span style=\"display: inline-block; animation: 0.5s linear 0s infinite normal none running mfm-jump;\">chocolat</span></span><style>@keyframes mfm-jump { 0% { transform:translateY(0) } 25% { transform:translateY(-16px) } 50% { transform:translateY(0) } 75% { transform:translateY(-8px) } to { transform:translateY(0) }}@keyframes mfm-rainbow { 0% { filter:hue-rotate(0deg) contrast(150%) saturate(150%) } to { filter:hue-rotate(360deg) contrast(150%) saturate(150%) }}@keyframes mfm-spin { 0% { transform:rotate(0) } to { transform:rotate(360deg) }}@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>"
|
||||
|
||||
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 style=\"display: inline-block; animation: 0.2s ease 0s infinite normal none running mfm-twitch;\">chocolatine</span>\nit's <span font-size: \"600%\"><span style=\"display: inline-block; animation: 1s linear 0s infinite normal none running mfm-spin;\">pain</span> <span style=\"display: inline-block; animation: 2s linear 0s infinite normal none running mfm-rainbow;\">au</span> <span style=\"display: inline-block; animation: 0.5s linear 0s infinite normal none running mfm-jump;\">chocolat</span></span><style>@keyframes mfm-jump { 0% { transform:translateY(0) } 25% { transform:translateY(-16px) } 50% { transform:translateY(0) } 75% { transform:translateY(-8px) } to { transform:translateY(0) }}@keyframes mfm-rainbow { 0% { filter:hue-rotate(0deg) contrast(150%) saturate(150%) } to { filter:hue-rotate(360deg) contrast(150%) saturate(150%) }}@keyframes mfm-spin { 0% { transform:rotate(0) } to { transform:rotate(360deg) }}@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>"
|
||||
"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