diff --git a/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs b/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs index c0bc7439..53900a72 100644 --- a/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs +++ b/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs @@ -56,9 +56,13 @@ public static class MfmSerializer { result.Append("$["); result.Append(mfmFnNode.Name); - result.Append('.'); - var args = mfmFnNode.Args.Select(p => p.Value != null ? $"{p.Key}={p.Value}" : $"{p.Key}"); - result.Append(string.Join(',', args)); + if (mfmFnNode.Args is { } args) + { + result.Append('.'); + var str = args.Value.Select(p => p.Value != null ? $"{p.Key}={p.Value.Value}" : $"{p.Key}"); + result.Append(string.Join(',', str)); + } + result.Append(' '); result.Append(Serialize(node.Children)); result.Append(']'); diff --git a/Iceshrimp.Parsing/Mfm.fs b/Iceshrimp.Parsing/Mfm.fs index a6e52ad4..7c19b846 100644 --- a/Iceshrimp.Parsing/Mfm.fs +++ b/Iceshrimp.Parsing/Mfm.fs @@ -92,11 +92,10 @@ module MfmNodeTypes = member val Url = url member val Silent = silent - type MfmFnNode(args: Dictionary, name, children) = + type MfmFnNode(name, args: IDictionary option, children) = inherit MfmInlineNode(children) - // (string, bool) args = (string, null as string?) - member val Args = args member val Name = name + member val Args = args type internal MfmCharNode(v: char) = inherit MfmInlineNode([]) @@ -163,6 +162,18 @@ module private MfmParser = | None -> user | Some v -> user + "@" + v + let fnArg = + many1Chars asciiLetter + .>>. opt ( + pchar '=' + >>. manyCharsTill anyChar (nextCharSatisfies <| fun p -> p = ',' || isWhitespace p) + ) + + let fnDict (input: (string * string option) list option) : IDictionary option = + match input with + | None -> None + | Some items -> items |> dict |> Some + // References let node, nodeRef = createParserForwardedToRef () let inlineNode, inlineNodeRef = createParserForwardedToRef () @@ -228,6 +239,13 @@ module private MfmParser = >>. manyCharsTill (satisfy isAsciiLetter <|> satisfy isDigit <|> anyOf "+-_") (skipChar ':') |>> fun e -> MfmEmojiCodeNode(e) :> MfmNode + let fnNode = + skipString "$[" >>. many1Chars asciiLower + .>>. opt (skipChar '.' >>. sepBy1 fnArg (skipChar ',')) + .>> skipChar ' ' + .>>. many1Till inlineNode (skipChar ']') + |>> fun ((n, o), c) -> MfmFnNode(n, fnDict o, aggregateTextInline c) :> MfmNode + let plainNode = skipString "" >>. manyCharsTill anyChar (skipString "") |>> fun v -> MfmPlainNode(v) :> MfmNode @@ -241,7 +259,8 @@ module private MfmParser = |>> fun c -> MfmCenterNode(aggregateTextInline c) :> MfmNode let mentionNode = - (previousCharSatisfiesNot isNotWhitespace <|> previousCharSatisfies (isAnyOf <| "()")) + (previousCharSatisfiesNot isNotWhitespace + <|> previousCharSatisfies (isAnyOf <| "()")) >>. skipString "@" >>. many1Chars ( satisfy isLetterOrNumber @@ -337,9 +356,10 @@ module private MfmParser = linkNode mathNode emojiCodeNode + fnNode charNode ] - //TODO: still missing: FnNode, MfmSearchNode + //TODO: still missing: FnNode let blockNodeSeq = [ plainNode; centerNode; smallNode; codeBlockNode; mathBlockNode; quoteNode ] diff --git a/Iceshrimp.Tests/Parsing/MfmTests.cs b/Iceshrimp.Tests/Parsing/MfmTests.cs index e36163e4..ff26f0c0 100644 --- a/Iceshrimp.Tests/Parsing/MfmTests.cs +++ b/Iceshrimp.Tests/Parsing/MfmTests.cs @@ -1,6 +1,7 @@ using Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization; using Iceshrimp.Parsing; using Microsoft.FSharp.Collections; +using Microsoft.FSharp.Core; using static Iceshrimp.Parsing.MfmNodeTypes; namespace Iceshrimp.Tests.Parsing; @@ -38,9 +39,7 @@ public class MfmTests { List expected = [ - new MfmInlineCodeNode("test"), - new MfmCodeBlockNode("test", null), - new MfmCodeBlockNode("test", "lang") + new MfmInlineCodeNode("test"), new MfmCodeBlockNode("test", null), new MfmCodeBlockNode("test", "lang") ]; var res = Mfm.parse(""" @@ -298,6 +297,54 @@ public class MfmTests MfmSerializer.Serialize(res).Should().BeEquivalentTo(canonical); } + [TestMethod] + public void TestFn() + { + const string input = + "test $[] $[test] $[test ] $[test test] $[test.a test] $[test.a=b test] $[test.a=b,c=e test] $[test.a,c=e test] $[test.a=b,c test]"; + + var some = FSharpOption>>.Some; + var none = FSharpOption>>.None; + var test = ListModule.OfSeq([new MfmTextNode("test")]); + + // @formatter:off + List expected = + [ + new MfmTextNode("test $[] $[test] $[test ] "), + new MfmFnNode("test", + none, + test), + new MfmTextNode(" "), + new MfmFnNode("test", + some(new Dictionary>{ {"a", FSharpOption.None} }), + test), + new MfmTextNode(" "), + new MfmFnNode("test", + some(new Dictionary>{ {"a", FSharpOption.Some("b")} }), + test), + new MfmTextNode(" "), + new MfmFnNode("test", + some(new Dictionary>{ {"a", FSharpOption.Some("b")}, {"c", FSharpOption.Some("e")} }), + test), + new MfmTextNode(" "), + new MfmFnNode("test", + some(new Dictionary>{ {"a", FSharpOption.None}, {"c", FSharpOption.Some("e")} }), + test), + new MfmTextNode(" "), + new MfmFnNode("test", + some(new Dictionary>{ {"a", FSharpOption.Some("b")}, {"c", FSharpOption.None} }), + test), + ]; + // @formatter:on + + var res = Mfm.parse(input); + + AssertionOptions.FormattingOptions.MaxDepth = 100; + res.ToList().Should().Equal(expected, MfmNodeEqual); + + MfmSerializer.Serialize(res).Should().BeEquivalentTo(input); + } + [TestMethod] public void Benchmark() { @@ -416,8 +463,13 @@ public class MfmTests case MfmFnNode ax: { var bx = (MfmFnNode)b; - if (ax.Args != bx.Args) return false; if (ax.Name != bx.Name) return false; + if ((ax.Args == null) != (bx.Args == null)) return false; + if (ax.Args == null || bx.Args == null) return true; + if (ax.Args.Value.Count != bx.Args.Value.Count) return false; + // ReSharper disable once UsageOfDefaultStructEquality + if (ax.Args.Value.Except(bx.Args.Value).Any()) return false; + break; } }