a fork of iceshrimp.net but a tweaked frontend to my personal liking. waow
fediverse social-media social iceshrimp fedi
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[parsing] Add parenthesis tracking to MfmUrlNode and MfmLinkNode parsers (ISH-608)

+98 -12
+45 -9
Iceshrimp.Parsing/Mfm.fs
··· 101 101 inherit MfmInlineNode([]) 102 102 member val Char = v 103 103 104 + type internal UserState = 105 + { ParenthesisStack: char list 106 + LastLine: int64 } 107 + 108 + static member Default = { ParenthesisStack = []; LastLine = 0 } 109 + 104 110 open MfmNodeTypes 105 111 106 112 module private MfmParser = ··· 114 120 let isLetterOrNumber c = Char.IsLetterOrDigit c 115 121 let isNewline c = '\n'.Equals(c) 116 122 let isNotNewline c = not (isNewline c) 123 + 124 + let followedByChar c = nextCharSatisfies <| fun ch -> c = ch 117 125 118 126 let (|CharNode|MfmNode|) (x: MfmNode) = 119 127 if x :? MfmCharNode then ··· 174 182 | None -> None 175 183 | Some items -> items |> dict |> Some 176 184 177 - let pushLine: Parser<unit, int64> = 185 + let pushLine: Parser<unit, UserState> = 178 186 fun stream -> 179 - stream.UserState <- stream.Line 187 + stream.UserState <- 188 + { stream.UserState with 189 + LastLine = stream.Line } 190 + 180 191 Reply(()) 181 192 182 - let assertLine: Parser<unit, int64> = 193 + let assertLine: Parser<unit, UserState> = 183 194 fun stream -> 184 - match stream.UserState = stream.Line with 195 + match stream.UserState.LastLine = stream.Line with 185 196 | true -> Reply(()) 186 197 | false -> Reply(Error, messageError "Line changed") 187 198 199 + let assertParen = userStateSatisfies <| fun u -> u.ParenthesisStack.Length > 0 200 + let assertNoParen = userStateSatisfies <| fun u -> u.ParenthesisStack.Length = 0 201 + 202 + let pushParen = 203 + updateUserState 204 + <| fun u -> 205 + { u with 206 + ParenthesisStack = '(' :: u.ParenthesisStack } 207 + 208 + let popParen = 209 + assertParen 210 + >>. updateUserState (fun u -> 211 + { u with 212 + ParenthesisStack = List.tail u.ParenthesisStack }) 213 + 214 + let clearParen = updateUserState <| fun u -> { u with ParenthesisStack = [] } 215 + 188 216 // References 189 217 let node, nodeRef = createParserForwardedToRef () 190 218 let inlineNode, inlineNodeRef = createParserForwardedToRef () ··· 328 356 329 357 let urlNode = 330 358 lookAhead (skipString "https://" <|> skipString "http://") 331 - >>. manyCharsTill anyChar (nextCharSatisfies isWhitespace <|> nextCharSatisfies (isAnyOf "()") <|> eof) //FIXME: this needs significant improvements 359 + >>. manyCharsTill 360 + ((pchar '(' .>> pushParen) <|> (pchar ')' .>> popParen) <|> anyChar) 361 + (nextCharSatisfies isWhitespace 362 + <|> (assertNoParen >>. followedByChar ')') 363 + <|> eof) 364 + .>> clearParen 332 365 >>= fun uri -> 333 366 match Uri.TryCreate(uri, UriKind.Absolute) with 334 367 | true, finalUri -> ··· 356 389 .>>. (pchar '[' >>. manyCharsTill anyChar (pchar ']')) 357 390 .>>. (pchar '(' 358 391 >>. lookAhead (skipString "https://" <|> skipString "http://") 359 - >>. manyCharsTill anyChar (pchar ')')) 392 + >>. manyCharsTill 393 + ((pchar '(' .>> pushParen) <|> (pchar ')' .>> popParen) <|> anyChar) 394 + (assertNoParen >>. skipChar ')')) 395 + .>> clearParen 360 396 >>= fun ((silent, text), uri) -> 361 397 match Uri.TryCreate(uri, UriKind.Absolute) with 362 398 | true, finalUri -> ··· 394 430 | Simple 395 431 396 432 let parseNode (m: ParseMode) = 397 - let prefixedNode (m: ParseMode) : Parser<MfmNode, int64> = 433 + let prefixedNode (m: ParseMode) : Parser<MfmNode, UserState> = 398 434 fun (stream: CharStream<_>) -> 399 435 match (stream.Peek(), m) with 400 436 // Block nodes, ordered by expected frequency ··· 439 475 440 476 module Mfm = 441 477 let parse str = 442 - match runParserOnString parse 0 "" str with 478 + match runParserOnString parse UserState.Default "" str with 443 479 | Success(result, _, _) -> aggregateText result 444 480 | Failure(s, _, _) -> failwith $"Failed to parse MFM: {s}" 445 481 446 482 let parseSimple str = 447 - match runParserOnString parseSimple 0 "" str with 483 + match runParserOnString parseSimple UserState.Default "" str with 448 484 | Success(result, _, _) -> aggregateText result 449 485 | Failure(s, _, _) -> failwith $"Failed to parse MFM: {s}"
+53 -3
Iceshrimp.Tests/Parsing/MfmTests.cs
··· 262 262 [TestMethod] 263 263 public void TestUrl() 264 264 { 265 - const string input = "https://example.org/path/Name_(test)_asdf"; 266 - //TODO: List<MfmNode> expected = [new MfmUrlNode(input, false),]; 267 - List<MfmNode> expected = [new MfmUrlNode(input[..30], false), new MfmTextNode(input[30..])]; 265 + const string input = "https://example.org/path/Name_(test)_asdf"; 266 + List<MfmNode> expected = [new MfmUrlNode(input, false)]; 268 267 var res = Mfm.parse(input); 268 + 269 + AssertionOptions.FormattingOptions.MaxDepth = 100; 270 + res.ToList().Should().Equal(expected, MfmNodeEqual); 271 + MfmSerializer.Serialize(res).Should().BeEquivalentTo(input); 272 + } 273 + 274 + [TestMethod] 275 + public void TestUrlAlt() 276 + { 277 + const string input = "https://example.org/path/Name_(test"; 278 + List<MfmNode> expected = [new MfmUrlNode(input, false)]; 279 + var res = Mfm.parse(input); 280 + 281 + AssertionOptions.FormattingOptions.MaxDepth = 100; 282 + res.ToList().Should().Equal(expected, MfmNodeEqual); 283 + MfmSerializer.Serialize(res).Should().BeEquivalentTo(input); 284 + } 285 + 286 + [TestMethod] 287 + public void TestUrlNeg() 288 + { 289 + const string input = "https://example.org/path/Name_test)_asdf"; 290 + List<MfmNode> expected = [new MfmUrlNode(input[..34], false), new MfmTextNode(input[34..])]; 291 + var res = Mfm.parse(input); 292 + 293 + AssertionOptions.FormattingOptions.MaxDepth = 100; 294 + res.ToList().Should().Equal(expected, MfmNodeEqual); 295 + MfmSerializer.Serialize(res).Should().BeEquivalentTo(input); 296 + } 297 + 298 + [TestMethod] 299 + public void TestLink() 300 + { 301 + const string input = "[test](https://example.org/path/Name_(test)_asdf)"; 302 + List<MfmNode> expected = [new MfmLinkNode("https://example.org/path/Name_(test)_asdf", "test", false)]; 303 + var res = Mfm.parse(input); 304 + 305 + AssertionOptions.FormattingOptions.MaxDepth = 100; 306 + res.ToList().Should().Equal(expected, MfmNodeEqual); 307 + MfmSerializer.Serialize(res).Should().BeEquivalentTo(input); 308 + } 309 + 310 + [TestMethod] 311 + public void TestLinkNeg() 312 + { 313 + const string input = "[test](https://example.org/path/Name_(test_asdf)"; 314 + List<MfmNode> expected = 315 + [ 316 + new MfmTextNode("[test]("), new MfmUrlNode("https://example.org/path/Name_(test_asdf)", false) 317 + ]; 318 + var res = Mfm.parse(input); 269 319 270 320 AssertionOptions.FormattingOptions.MaxDepth = 100; 271 321 res.ToList().Should().Equal(expected, MfmNodeEqual);