[parsing] Add support for advanced MFM (ISH-257)

This commit is contained in:
Laura Hausmann 2024-10-19 02:37:46 +02:00
parent dc2d65d799
commit fa81be967a
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
3 changed files with 88 additions and 12 deletions

View file

@ -56,9 +56,13 @@ public static class MfmSerializer
{ {
result.Append("$["); result.Append("$[");
result.Append(mfmFnNode.Name); result.Append(mfmFnNode.Name);
result.Append('.'); if (mfmFnNode.Args is { } args)
var args = mfmFnNode.Args.Select(p => p.Value != null ? $"{p.Key}={p.Value}" : $"{p.Key}"); {
result.Append(string.Join(',', 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(' ');
result.Append(Serialize(node.Children)); result.Append(Serialize(node.Children));
result.Append(']'); result.Append(']');

View file

@ -92,11 +92,10 @@ module MfmNodeTypes =
member val Url = url member val Url = url
member val Silent = silent member val Silent = silent
type MfmFnNode(args: Dictionary<string, string>, name, children) = type MfmFnNode(name, args: IDictionary<string, string option> option, children) =
inherit MfmInlineNode(children) inherit MfmInlineNode(children)
// (string, bool) args = (string, null as string?)
member val Args = args
member val Name = name member val Name = name
member val Args = args
type internal MfmCharNode(v: char) = type internal MfmCharNode(v: char) =
inherit MfmInlineNode([]) inherit MfmInlineNode([])
@ -163,6 +162,18 @@ module private MfmParser =
| None -> user | None -> user
| Some v -> user + "@" + v | 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<string, string option> option =
match input with
| None -> None
| Some items -> items |> dict |> Some
// References // References
let node, nodeRef = createParserForwardedToRef () let node, nodeRef = createParserForwardedToRef ()
let inlineNode, inlineNodeRef = createParserForwardedToRef () let inlineNode, inlineNodeRef = createParserForwardedToRef ()
@ -228,6 +239,13 @@ module private MfmParser =
>>. manyCharsTill (satisfy isAsciiLetter <|> satisfy isDigit <|> anyOf "+-_") (skipChar ':') >>. manyCharsTill (satisfy isAsciiLetter <|> satisfy isDigit <|> anyOf "+-_") (skipChar ':')
|>> fun e -> MfmEmojiCodeNode(e) :> MfmNode |>> 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 = let plainNode =
skipString "<plain>" >>. manyCharsTill anyChar (skipString "</plain>") skipString "<plain>" >>. manyCharsTill anyChar (skipString "</plain>")
|>> fun v -> MfmPlainNode(v) :> MfmNode |>> fun v -> MfmPlainNode(v) :> MfmNode
@ -241,7 +259,8 @@ module private MfmParser =
|>> fun c -> MfmCenterNode(aggregateTextInline c) :> MfmNode |>> fun c -> MfmCenterNode(aggregateTextInline c) :> MfmNode
let mentionNode = let mentionNode =
(previousCharSatisfiesNot isNotWhitespace <|> previousCharSatisfies (isAnyOf <| "()")) (previousCharSatisfiesNot isNotWhitespace
<|> previousCharSatisfies (isAnyOf <| "()"))
>>. skipString "@" >>. skipString "@"
>>. many1Chars ( >>. many1Chars (
satisfy isLetterOrNumber satisfy isLetterOrNumber
@ -337,9 +356,10 @@ module private MfmParser =
linkNode linkNode
mathNode mathNode
emojiCodeNode emojiCodeNode
fnNode
charNode ] charNode ]
//TODO: still missing: FnNode, MfmSearchNode //TODO: still missing: FnNode
let blockNodeSeq = let blockNodeSeq =
[ plainNode; centerNode; smallNode; codeBlockNode; mathBlockNode; quoteNode ] [ plainNode; centerNode; smallNode; codeBlockNode; mathBlockNode; quoteNode ]

View file

@ -1,6 +1,7 @@
using Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization; using Iceshrimp.Backend.Core.Helpers.LibMfm.Serialization;
using Iceshrimp.Parsing; using Iceshrimp.Parsing;
using Microsoft.FSharp.Collections; using Microsoft.FSharp.Collections;
using Microsoft.FSharp.Core;
using static Iceshrimp.Parsing.MfmNodeTypes; using static Iceshrimp.Parsing.MfmNodeTypes;
namespace Iceshrimp.Tests.Parsing; namespace Iceshrimp.Tests.Parsing;
@ -38,9 +39,7 @@ public class MfmTests
{ {
List<MfmNode> expected = List<MfmNode> expected =
[ [
new MfmInlineCodeNode("test"), new MfmInlineCodeNode("test"), new MfmCodeBlockNode("test", null), new MfmCodeBlockNode("test", "lang")
new MfmCodeBlockNode("test", null),
new MfmCodeBlockNode("test", "lang")
]; ];
var res = Mfm.parse(""" var res = Mfm.parse("""
@ -298,6 +297,54 @@ public class MfmTests
MfmSerializer.Serialize(res).Should().BeEquivalentTo(canonical); 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<IDictionary<string, FSharpOption<string>>>.Some;
var none = FSharpOption<IDictionary<string, FSharpOption<string>>>.None;
var test = ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("test")]);
// @formatter:off
List<MfmNode> expected =
[
new MfmTextNode("test $[] $[test] $[test ] "),
new MfmFnNode("test",
none,
test),
new MfmTextNode(" "),
new MfmFnNode("test",
some(new Dictionary<string, FSharpOption<string>>{ {"a", FSharpOption<string>.None} }),
test),
new MfmTextNode(" "),
new MfmFnNode("test",
some(new Dictionary<string, FSharpOption<string>>{ {"a", FSharpOption<string>.Some("b")} }),
test),
new MfmTextNode(" "),
new MfmFnNode("test",
some(new Dictionary<string, FSharpOption<string>>{ {"a", FSharpOption<string>.Some("b")}, {"c", FSharpOption<string>.Some("e")} }),
test),
new MfmTextNode(" "),
new MfmFnNode("test",
some(new Dictionary<string, FSharpOption<string>>{ {"a", FSharpOption<string>.None}, {"c", FSharpOption<string>.Some("e")} }),
test),
new MfmTextNode(" "),
new MfmFnNode("test",
some(new Dictionary<string, FSharpOption<string>>{ {"a", FSharpOption<string>.Some("b")}, {"c", FSharpOption<string>.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] [TestMethod]
public void Benchmark() public void Benchmark()
{ {
@ -416,8 +463,13 @@ public class MfmTests
case MfmFnNode ax: case MfmFnNode ax:
{ {
var bx = (MfmFnNode)b; var bx = (MfmFnNode)b;
if (ax.Args != bx.Args) return false;
if (ax.Name != bx.Name) 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; break;
} }
} }