diff --git a/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs b/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs index 22b37ad7..63b3a1b2 100644 --- a/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs +++ b/Iceshrimp.Backend/Core/Helpers/LibMfm/Serialization/MfmSerializer.cs @@ -35,11 +35,13 @@ public static class MfmSerializer result.Append(" [search]"); break; } - case MfmBoldNode: + case MfmBoldNode mfmBoldNode: { - result.Append("**"); + var start = mfmBoldNode.Type.IsSymbol ? "**" : ""; + var end = mfmBoldNode.Type.IsSymbol ? "**" : ""; + result.Append(start); result.Append(SerializeInternal(node.Children)); - result.Append("**"); + result.Append(end); break; } case MfmCenterNode: @@ -80,11 +82,13 @@ public static class MfmSerializer result.Append($"`{mfmInlineCodeNode.Code}`"); break; } - case MfmItalicNode: + case MfmItalicNode mfmItalicNode: { - result.Append('*'); + var start = mfmItalicNode.Type.IsSymbol ? "*" : ""; + var end = mfmItalicNode.Type.IsSymbol ? "*" : ""; + result.Append(start); result.Append(SerializeInternal(node.Children)); - result.Append('*'); + result.Append(end); break; } case MfmLinkNode mfmLinkNode: @@ -125,11 +129,13 @@ public static class MfmSerializer result.Append(""); break; } - case MfmStrikeNode: + case MfmStrikeNode mfmStrikeNode: { - result.Append("~~"); + var start = mfmStrikeNode.Type.IsSymbol ? "~~" : ""; + var end = mfmStrikeNode.Type.IsSymbol ? "~~" : ""; + result.Append(start); result.Append(SerializeInternal(node.Children)); - result.Append("~~"); + result.Append(end); break; } case MfmTextNode mfmTextNode: diff --git a/Iceshrimp.Parsing/Mfm.fs b/Iceshrimp.Parsing/Mfm.fs index d3426610..30cacfb3 100644 --- a/Iceshrimp.Parsing/Mfm.fs +++ b/Iceshrimp.Parsing/Mfm.fs @@ -19,18 +19,25 @@ module MfmNodeTypes = inherit MfmNode() do base.Children <- c |> List.map (fun x -> x :> MfmNode) + type InlineNodeType = + | Symbol + | HtmlTag + type MfmTextNode(v: string) = inherit MfmInlineNode([]) member val Text = v - type MfmItalicNode(c) = + type MfmItalicNode(c, t) = inherit MfmInlineNode(c) + member val Type: InlineNodeType = t - type MfmBoldNode(c) = + type MfmBoldNode(c, t) = inherit MfmInlineNode(c) + member val Type: InlineNodeType = t - type MfmStrikeNode(c) = + type MfmStrikeNode(c, t) = inherit MfmInlineNode(c) + member val Type: InlineNodeType = t type MfmInlineCodeNode(v: string) = inherit MfmInlineNode([]) @@ -240,7 +247,7 @@ module private MfmParser = >>. pushLine >>. manyTill inlineNode italicPatternAsterisk .>> assertLine - |>> fun c -> MfmItalicNode(aggregateTextInline c) :> MfmNode + |>> fun c -> MfmItalicNode(aggregateTextInline c, Symbol) :> MfmNode let italicUnderscoreNode = previousCharSatisfiesNot isNotWhitespace @@ -248,11 +255,11 @@ module private MfmParser = >>. pushLine >>. manyTill inlineNode italicPatternUnderscore .>> assertLine - |>> fun c -> MfmItalicNode(aggregateTextInline c) :> MfmNode + |>> fun c -> MfmItalicNode(aggregateTextInline c, Symbol) :> MfmNode let italicTagNode = skipString "" >>. manyTill inlineNode (skipString "") - |>> fun c -> MfmItalicNode(aggregateTextInline c) :> MfmNode + |>> fun c -> MfmItalicNode(aggregateTextInline c, HtmlTag) :> MfmNode let boldAsteriskNode = previousCharSatisfiesNot isNotWhitespace @@ -260,7 +267,7 @@ module private MfmParser = >>. pushLine >>. manyTill inlineNode (skipString "**") .>> assertLine - |>> fun c -> MfmBoldNode(aggregateTextInline c) :> MfmNode + |>> fun c -> MfmBoldNode(aggregateTextInline c, Symbol) :> MfmNode let boldUnderscoreNode = previousCharSatisfiesNot isNotWhitespace @@ -268,16 +275,20 @@ module private MfmParser = >>. pushLine >>. manyTill inlineNode (skipString "__") .>> assertLine - |>> fun c -> MfmBoldNode(aggregateTextInline c) :> MfmNode + |>> fun c -> MfmBoldNode(aggregateTextInline c, Symbol) :> MfmNode let boldTagNode = skipString "" >>. manyTill inlineNode (skipString "") - |>> fun c -> MfmBoldNode(aggregateTextInline c) :> MfmNode + |>> fun c -> MfmBoldNode(aggregateTextInline c, HtmlTag) :> MfmNode let strikeNode = skipString "~~" >>. pushLine >>. manyTill inlineNode (skipString "~~") .>> assertLine - |>> fun c -> MfmStrikeNode(aggregateTextInline c) :> MfmNode + |>> fun c -> MfmStrikeNode(aggregateTextInline c, Symbol) :> MfmNode + + let strikeTagNode = + skipString "" >>. manyTill inlineNode (skipString "") + |>> fun c -> MfmStrikeNode(aggregateTextInline c, HtmlTag) :> MfmNode let codeNode = codePattern >>. pushLine >>. manyCharsTill anyChar codePattern .>> assertLine @@ -430,6 +441,14 @@ module private MfmParser = | Simple let parseNode (m: ParseMode) = + let inlineTagNodes = + [ plainNode + smallNode + italicTagNode + boldTagNode + strikeTagNode + urlNodeBrackets ] + let prefixedNode (m: ParseMode) : Parser = fun (stream: CharStream<_>) -> match (stream.Peek(), m) with @@ -450,7 +469,7 @@ module private MfmParser = | ':', (Full | Inline | Simple) -> emojiCodeNode | '~', (Full | Inline) when stream.Match "~~" -> strikeNode | '[', (Full | Inline) -> linkNode - | '<', (Full | Inline) -> choice [ plainNode; smallNode; italicTagNode; boldTagNode; urlNodeBrackets ] + | '<', (Full | Inline) -> choice inlineTagNodes | '<', Simple when stream.Match "" -> plainNode | '\\', (Full | Inline) when stream.Match "\\(" -> mathNode | '$', (Full | Inline) when stream.Match "$[" -> fnNode diff --git a/Iceshrimp.Tests/Parsing/MfmTests.cs b/Iceshrimp.Tests/Parsing/MfmTests.cs index 34f9821a..685f5a4c 100644 --- a/Iceshrimp.Tests/Parsing/MfmTests.cs +++ b/Iceshrimp.Tests/Parsing/MfmTests.cs @@ -14,30 +14,73 @@ public class MfmTests [TestMethod] public void TestParseBoldItalic() { - List expected = + // @formatter:off + List expected123 = [ new MfmItalicNode(ListModule.OfSeq([ new MfmTextNode("italic "), - new MfmBoldNode(ListModule.OfSeq([new MfmTextNode("bold")])), + new MfmBoldNode(ListModule.OfSeq([new MfmTextNode("bold")]), InlineNodeType.Symbol), new MfmTextNode(" italic") - ])) + ]), InlineNodeType.Symbol) ]; + + List expected4 = + [ + new MfmItalicNode(ListModule.OfSeq([ + new MfmTextNode("italic "), + new MfmBoldNode(ListModule.OfSeq([new MfmTextNode("bold")]), InlineNodeType.HtmlTag), + new MfmTextNode(" italic") + ]), InlineNodeType.HtmlTag) + ]; + + List expected5 = + [ + new MfmItalicNode(ListModule.OfSeq([ + new MfmTextNode("italic "), + new MfmBoldNode(ListModule.OfSeq([new MfmTextNode("bold")]), InlineNodeType.Symbol), + new MfmTextNode(" italic") + ]), InlineNodeType.HtmlTag) + ]; + + List expected6 = + [ + new MfmItalicNode(ListModule.OfSeq([ + new MfmTextNode("italic "), + new MfmBoldNode(ListModule.OfSeq([new MfmTextNode("bold")]), InlineNodeType.HtmlTag), + new MfmTextNode(" italic") + ]), InlineNodeType.Symbol) + ]; + // @formatter:on - var res = Mfm.parse("*italic **bold** italic*").ToList(); - var resAlt = Mfm.parse("_italic **bold** italic_").ToList(); - var resAlt2 = Mfm.parse("_italic __bold__ italic_").ToList(); - var resAlt3 = Mfm.parse("italic bold italic").ToList(); - var resMixed = Mfm.parse("italic **bold** italic").ToList(); - var resMixedAlt = Mfm.parse("*italic bold italic*").ToList(); + const string input = "*italic **bold** italic*"; + const string input2 = "_italic **bold** italic_"; + const string input3 = "_italic __bold__ italic_"; + const string input4 = "italic bold italic"; + const string input5 = "italic **bold** italic"; + const string input6 = "*italic bold italic*"; + + var res = Mfm.parse(input).ToList(); + var res2 = Mfm.parse(input2).ToList(); + var res3 = Mfm.parse(input3).ToList(); + var res4 = Mfm.parse(input4).ToList(); + var res5 = Mfm.parse(input5).ToList(); + var res6 = Mfm.parse(input6).ToList(); AssertionOptions.FormattingOptions.MaxDepth = 100; - res.Should().Equal(expected, MfmNodeEqual); - resAlt.Should().Equal(expected, MfmNodeEqual); - resAlt2.Should().Equal(expected, MfmNodeEqual); - resAlt3.Should().Equal(expected, MfmNodeEqual); - resMixed.Should().Equal(expected, MfmNodeEqual); - resMixedAlt.Should().Equal(expected, MfmNodeEqual); + res.Should().Equal(expected123, MfmNodeEqual); + res2.Should().Equal(expected123, MfmNodeEqual); + res3.Should().Equal(expected123, MfmNodeEqual); + res4.Should().Equal(expected4, MfmNodeEqual); + res5.Should().Equal(expected5, MfmNodeEqual); + res6.Should().Equal(expected6, MfmNodeEqual); + + MfmSerializer.Serialize(res).Should().BeEquivalentTo(input); + MfmSerializer.Serialize(res2).Should().BeEquivalentTo(input); + MfmSerializer.Serialize(res3).Should().BeEquivalentTo(input); + MfmSerializer.Serialize(res4).Should().BeEquivalentTo(input4); + MfmSerializer.Serialize(res5).Should().BeEquivalentTo(input5); + MfmSerializer.Serialize(res6).Should().BeEquivalentTo(input6); } [TestMethod] @@ -58,7 +101,7 @@ public class MfmTests expected = [ new MfmTextNode("test "), - new MfmItalicNode(ListModule.OfSeq([new MfmTextNode("test")])), + new MfmItalicNode(ListModule.OfSeq([new MfmTextNode("test")]), InlineNodeType.Symbol), new MfmTextNode("test") ]; @@ -66,6 +109,31 @@ public class MfmTests Mfm.parse("test _test_test").ToList().Should().Equal(expected, MfmNodeEqual); } + [TestMethod] + public void TestStrike() + { + const string input = "~~test~~"; + const string input2 = "test"; + List expected = + [ + new MfmStrikeNode(ListModule.OfSeq([new MfmTextNode("test")]), InlineNodeType.Symbol) + ]; + + List expected2 = + [ + new MfmStrikeNode(ListModule.OfSeq([new MfmTextNode("test")]), InlineNodeType.HtmlTag) + ]; + + var res = Mfm.parse(input).ToList(); + var res2 = Mfm.parse(input2).ToList(); + + res.Should().Equal(expected, MfmNodeEqual); + res2.Should().Equal(expected2, MfmNodeEqual); + + MfmSerializer.Serialize(res).Should().BeEquivalentTo(input); + MfmSerializer.Serialize(res2).Should().BeEquivalentTo(input2); + } + [TestMethod] public void TestParseList() { @@ -511,6 +579,24 @@ public class MfmTests { case MfmTextNode textNode when ((MfmTextNode)b).Text != textNode.Text: return false; + case MfmItalicNode ax: + { + var bx = (MfmItalicNode)b; + if (!bx.Type.Equals(ax.Type)) return false; + break; + } + case MfmBoldNode ax: + { + var bx = (MfmBoldNode)b; + if (!bx.Type.Equals(ax.Type)) return false; + break; + } + case MfmStrikeNode ax: + { + var bx = (MfmStrikeNode)b; + if (!bx.Type.Equals(ax.Type)) return false; + break; + } case MfmMentionNode ax: { var bx = (MfmMentionNode)b;