[parsing] Add strike html tag support, keep italic/bold/strike in html tag form when reserializing

This commit is contained in:
Laura Hausmann 2024-11-17 16:44:05 +01:00
parent cd84b480a6
commit 2ef78e3f41
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
3 changed files with 147 additions and 36 deletions

View file

@ -35,11 +35,13 @@ public static class MfmSerializer
result.Append(" [search]"); result.Append(" [search]");
break; break;
} }
case MfmBoldNode: case MfmBoldNode mfmBoldNode:
{ {
result.Append("**"); var start = mfmBoldNode.Type.IsSymbol ? "**" : "<b>";
var end = mfmBoldNode.Type.IsSymbol ? "**" : "</b>";
result.Append(start);
result.Append(SerializeInternal(node.Children)); result.Append(SerializeInternal(node.Children));
result.Append("**"); result.Append(end);
break; break;
} }
case MfmCenterNode: case MfmCenterNode:
@ -80,11 +82,13 @@ public static class MfmSerializer
result.Append($"`{mfmInlineCodeNode.Code}`"); result.Append($"`{mfmInlineCodeNode.Code}`");
break; break;
} }
case MfmItalicNode: case MfmItalicNode mfmItalicNode:
{ {
result.Append('*'); var start = mfmItalicNode.Type.IsSymbol ? "*" : "<i>";
var end = mfmItalicNode.Type.IsSymbol ? "*" : "</i>";
result.Append(start);
result.Append(SerializeInternal(node.Children)); result.Append(SerializeInternal(node.Children));
result.Append('*'); result.Append(end);
break; break;
} }
case MfmLinkNode mfmLinkNode: case MfmLinkNode mfmLinkNode:
@ -125,11 +129,13 @@ public static class MfmSerializer
result.Append("</small>"); result.Append("</small>");
break; break;
} }
case MfmStrikeNode: case MfmStrikeNode mfmStrikeNode:
{ {
result.Append("~~"); var start = mfmStrikeNode.Type.IsSymbol ? "~~" : "<s>";
var end = mfmStrikeNode.Type.IsSymbol ? "~~" : "</s>";
result.Append(start);
result.Append(SerializeInternal(node.Children)); result.Append(SerializeInternal(node.Children));
result.Append("~~"); result.Append(end);
break; break;
} }
case MfmTextNode mfmTextNode: case MfmTextNode mfmTextNode:

View file

@ -19,18 +19,25 @@ module MfmNodeTypes =
inherit MfmNode() inherit MfmNode()
do base.Children <- c |> List.map (fun x -> x :> MfmNode) do base.Children <- c |> List.map (fun x -> x :> MfmNode)
type InlineNodeType =
| Symbol
| HtmlTag
type MfmTextNode(v: string) = type MfmTextNode(v: string) =
inherit MfmInlineNode([]) inherit MfmInlineNode([])
member val Text = v member val Text = v
type MfmItalicNode(c) = type MfmItalicNode(c, t) =
inherit MfmInlineNode(c) inherit MfmInlineNode(c)
member val Type: InlineNodeType = t
type MfmBoldNode(c) = type MfmBoldNode(c, t) =
inherit MfmInlineNode(c) inherit MfmInlineNode(c)
member val Type: InlineNodeType = t
type MfmStrikeNode(c) = type MfmStrikeNode(c, t) =
inherit MfmInlineNode(c) inherit MfmInlineNode(c)
member val Type: InlineNodeType = t
type MfmInlineCodeNode(v: string) = type MfmInlineCodeNode(v: string) =
inherit MfmInlineNode([]) inherit MfmInlineNode([])
@ -240,7 +247,7 @@ module private MfmParser =
>>. pushLine >>. pushLine
>>. manyTill inlineNode italicPatternAsterisk >>. manyTill inlineNode italicPatternAsterisk
.>> assertLine .>> assertLine
|>> fun c -> MfmItalicNode(aggregateTextInline c) :> MfmNode |>> fun c -> MfmItalicNode(aggregateTextInline c, Symbol) :> MfmNode
let italicUnderscoreNode = let italicUnderscoreNode =
previousCharSatisfiesNot isNotWhitespace previousCharSatisfiesNot isNotWhitespace
@ -248,11 +255,11 @@ module private MfmParser =
>>. pushLine >>. pushLine
>>. manyTill inlineNode italicPatternUnderscore >>. manyTill inlineNode italicPatternUnderscore
.>> assertLine .>> assertLine
|>> fun c -> MfmItalicNode(aggregateTextInline c) :> MfmNode |>> fun c -> MfmItalicNode(aggregateTextInline c, Symbol) :> MfmNode
let italicTagNode = let italicTagNode =
skipString "<i>" >>. manyTill inlineNode (skipString "</i>") skipString "<i>" >>. manyTill inlineNode (skipString "</i>")
|>> fun c -> MfmItalicNode(aggregateTextInline c) :> MfmNode |>> fun c -> MfmItalicNode(aggregateTextInline c, HtmlTag) :> MfmNode
let boldAsteriskNode = let boldAsteriskNode =
previousCharSatisfiesNot isNotWhitespace previousCharSatisfiesNot isNotWhitespace
@ -260,7 +267,7 @@ module private MfmParser =
>>. pushLine >>. pushLine
>>. manyTill inlineNode (skipString "**") >>. manyTill inlineNode (skipString "**")
.>> assertLine .>> assertLine
|>> fun c -> MfmBoldNode(aggregateTextInline c) :> MfmNode |>> fun c -> MfmBoldNode(aggregateTextInline c, Symbol) :> MfmNode
let boldUnderscoreNode = let boldUnderscoreNode =
previousCharSatisfiesNot isNotWhitespace previousCharSatisfiesNot isNotWhitespace
@ -268,16 +275,20 @@ module private MfmParser =
>>. pushLine >>. pushLine
>>. manyTill inlineNode (skipString "__") >>. manyTill inlineNode (skipString "__")
.>> assertLine .>> assertLine
|>> fun c -> MfmBoldNode(aggregateTextInline c) :> MfmNode |>> fun c -> MfmBoldNode(aggregateTextInline c, Symbol) :> MfmNode
let boldTagNode = let boldTagNode =
skipString "<b>" >>. manyTill inlineNode (skipString "</b>") skipString "<b>" >>. manyTill inlineNode (skipString "</b>")
|>> fun c -> MfmBoldNode(aggregateTextInline c) :> MfmNode |>> fun c -> MfmBoldNode(aggregateTextInline c, HtmlTag) :> MfmNode
let strikeNode = let strikeNode =
skipString "~~" >>. pushLine >>. manyTill inlineNode (skipString "~~") skipString "~~" >>. pushLine >>. manyTill inlineNode (skipString "~~")
.>> assertLine .>> assertLine
|>> fun c -> MfmStrikeNode(aggregateTextInline c) :> MfmNode |>> fun c -> MfmStrikeNode(aggregateTextInline c, Symbol) :> MfmNode
let strikeTagNode =
skipString "<s>" >>. manyTill inlineNode (skipString "</s>")
|>> fun c -> MfmStrikeNode(aggregateTextInline c, HtmlTag) :> MfmNode
let codeNode = let codeNode =
codePattern >>. pushLine >>. manyCharsTill anyChar codePattern .>> assertLine codePattern >>. pushLine >>. manyCharsTill anyChar codePattern .>> assertLine
@ -430,6 +441,14 @@ module private MfmParser =
| Simple | Simple
let parseNode (m: ParseMode) = let parseNode (m: ParseMode) =
let inlineTagNodes =
[ plainNode
smallNode
italicTagNode
boldTagNode
strikeTagNode
urlNodeBrackets ]
let prefixedNode (m: ParseMode) : Parser<MfmNode, UserState> = let prefixedNode (m: ParseMode) : Parser<MfmNode, UserState> =
fun (stream: CharStream<_>) -> fun (stream: CharStream<_>) ->
match (stream.Peek(), m) with match (stream.Peek(), m) with
@ -450,7 +469,7 @@ module private MfmParser =
| ':', (Full | Inline | Simple) -> emojiCodeNode | ':', (Full | Inline | Simple) -> emojiCodeNode
| '~', (Full | Inline) when stream.Match "~~" -> strikeNode | '~', (Full | Inline) when stream.Match "~~" -> strikeNode
| '[', (Full | Inline) -> linkNode | '[', (Full | Inline) -> linkNode
| '<', (Full | Inline) -> choice [ plainNode; smallNode; italicTagNode; boldTagNode; urlNodeBrackets ] | '<', (Full | Inline) -> choice inlineTagNodes
| '<', Simple when stream.Match "<plain>" -> plainNode | '<', Simple when stream.Match "<plain>" -> plainNode
| '\\', (Full | Inline) when stream.Match "\\(" -> mathNode | '\\', (Full | Inline) when stream.Match "\\(" -> mathNode
| '$', (Full | Inline) when stream.Match "$[" -> fnNode | '$', (Full | Inline) when stream.Match "$[" -> fnNode

View file

@ -14,30 +14,73 @@ public class MfmTests
[TestMethod] [TestMethod]
public void TestParseBoldItalic() public void TestParseBoldItalic()
{ {
List<MfmNode> expected = // @formatter:off
List<MfmNode> expected123 =
[ [
new MfmItalicNode(ListModule.OfSeq<MfmInlineNode>([ new MfmItalicNode(ListModule.OfSeq<MfmInlineNode>([
new MfmTextNode("italic "), new MfmTextNode("italic "),
new MfmBoldNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("bold")])), new MfmBoldNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("bold")]), InlineNodeType.Symbol),
new MfmTextNode(" italic") new MfmTextNode(" italic")
])) ]), InlineNodeType.Symbol)
]; ];
var res = Mfm.parse("*italic **bold** italic*").ToList(); List<MfmNode> expected4 =
var resAlt = Mfm.parse("_italic **bold** italic_").ToList(); [
var resAlt2 = Mfm.parse("_italic __bold__ italic_").ToList(); new MfmItalicNode(ListModule.OfSeq<MfmInlineNode>([
var resAlt3 = Mfm.parse("<i>italic <b>bold</b> italic</i>").ToList(); new MfmTextNode("italic "),
var resMixed = Mfm.parse("<i>italic **bold** italic</i>").ToList(); new MfmBoldNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("bold")]), InlineNodeType.HtmlTag),
var resMixedAlt = Mfm.parse("*italic <b>bold</b> italic*").ToList(); new MfmTextNode(" italic")
]), InlineNodeType.HtmlTag)
];
List<MfmNode> expected5 =
[
new MfmItalicNode(ListModule.OfSeq<MfmInlineNode>([
new MfmTextNode("italic "),
new MfmBoldNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("bold")]), InlineNodeType.Symbol),
new MfmTextNode(" italic")
]), InlineNodeType.HtmlTag)
];
List<MfmNode> expected6 =
[
new MfmItalicNode(ListModule.OfSeq<MfmInlineNode>([
new MfmTextNode("italic "),
new MfmBoldNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("bold")]), InlineNodeType.HtmlTag),
new MfmTextNode(" italic")
]), InlineNodeType.Symbol)
];
// @formatter:on
const string input = "*italic **bold** italic*";
const string input2 = "_italic **bold** italic_";
const string input3 = "_italic __bold__ italic_";
const string input4 = "<i>italic <b>bold</b> italic</i>";
const string input5 = "<i>italic **bold** italic</i>";
const string input6 = "*italic <b>bold</b> 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; AssertionOptions.FormattingOptions.MaxDepth = 100;
res.Should().Equal(expected, MfmNodeEqual); res.Should().Equal(expected123, MfmNodeEqual);
resAlt.Should().Equal(expected, MfmNodeEqual); res2.Should().Equal(expected123, MfmNodeEqual);
resAlt2.Should().Equal(expected, MfmNodeEqual); res3.Should().Equal(expected123, MfmNodeEqual);
resAlt3.Should().Equal(expected, MfmNodeEqual); res4.Should().Equal(expected4, MfmNodeEqual);
resMixed.Should().Equal(expected, MfmNodeEqual); res5.Should().Equal(expected5, MfmNodeEqual);
resMixedAlt.Should().Equal(expected, 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] [TestMethod]
@ -58,7 +101,7 @@ public class MfmTests
expected = expected =
[ [
new MfmTextNode("test "), new MfmTextNode("test "),
new MfmItalicNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("test")])), new MfmItalicNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("test")]), InlineNodeType.Symbol),
new MfmTextNode("test") new MfmTextNode("test")
]; ];
@ -66,6 +109,31 @@ public class MfmTests
Mfm.parse("test _test_test").ToList().Should().Equal(expected, MfmNodeEqual); Mfm.parse("test _test_test").ToList().Should().Equal(expected, MfmNodeEqual);
} }
[TestMethod]
public void TestStrike()
{
const string input = "~~test~~";
const string input2 = "<s>test</s>";
List<MfmNode> expected =
[
new MfmStrikeNode(ListModule.OfSeq<MfmInlineNode>([new MfmTextNode("test")]), InlineNodeType.Symbol)
];
List<MfmNode> expected2 =
[
new MfmStrikeNode(ListModule.OfSeq<MfmInlineNode>([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] [TestMethod]
public void TestParseList() public void TestParseList()
{ {
@ -511,6 +579,24 @@ public class MfmTests
{ {
case MfmTextNode textNode when ((MfmTextNode)b).Text != textNode.Text: case MfmTextNode textNode when ((MfmTextNode)b).Text != textNode.Text:
return false; 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: case MfmMentionNode ax:
{ {
var bx = (MfmMentionNode)b; var bx = (MfmMentionNode)b;