[parsing] Add support for advanced MFM (ISH-257)
This commit is contained in:
parent
dc2d65d799
commit
fa81be967a
3 changed files with 88 additions and 12 deletions
|
@ -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(']');
|
||||||
|
|
|
@ -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 ]
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue