diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index abac577c5..e6a2f3980 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,10 @@ # Changelog +## [Unreleased] + +### Changed +* Avoid recomputing `List.length` on the shrinking remainder for every scanned character when splitting a Markdown pipe-table row into cells (`pipeTableFindSplits` in `MarkdownTableParser.fs`). The scan now tracks the number of consumed characters incrementally, turning an O(n²) split into an O(n) one for rows with many delimiters, with no behavior change. + ## [23.0.0-alpha.2] - 2026-09-10 ### Fixed diff --git a/src/FSharp.Formatting.Markdown/MarkdownTableParser.fs b/src/FSharp.Formatting.Markdown/MarkdownTableParser.fs index 0ea689154..c0a9c5d50 100644 --- a/src/FSharp.Formatting.Markdown/MarkdownTableParser.fs +++ b/src/FSharp.Formatting.Markdown/MarkdownTableParser.fs @@ -20,26 +20,34 @@ let rec pipeTableFindSplits (delim: char array) (line: char list) = let cLstToStr (x: char list) = x |> Array.ofList |> System.String.Concat + // Scans the line and, on success, returns the number of characters consumed up to and + // including the found delimiter, together with the remaining list. Tracking the consumed + // count incrementally (rather than recomputing `List.length` over the shrinking remainder + // on every character) keeps this scan linear instead of quadratic in the row length. let rec ptfs delim line = match line with - | DelimitedLatexDisplayMath [ '$'; '$' ] (_body, rest) -> ptfs delim rest - | DelimitedLatexInlineMath [ '$' ] (_body, rest) -> ptfs delim rest - | List.DelimitedWith [ '`'; ' ' ] [ ' '; '`' ] (_body, rest, _s, _e) -> ptfs delim rest - | List.DelimitedNTimes '`' (_body, rest, _s, _e) -> ptfs delim rest - | x :: rest when Array.exists ((=) x) delim -> Some rest - | '\\' :: _ :: rest - | _ :: rest -> ptfs delim rest + | DelimitedLatexDisplayMath [ '$'; '$' ] (_body, rest) + | DelimitedLatexInlineMath [ '$' ] (_body, rest) + | List.DelimitedWith [ '`'; ' ' ] [ ' '; '`' ] (_body, rest, _, _) + | List.DelimitedNTimes '`' (_body, rest, _, _) -> + let consumedHere = List.length line - List.length rest + + ptfs delim rest + |> Option.map (fun (count, remainder) -> (count + consumedHere, remainder)) + | x :: rest when Array.exists ((=) x) delim -> Some(1, rest) + | '\\' :: _ :: rest -> ptfs delim rest |> Option.map (fun (count, remainder) -> (count + 2, remainder)) + | _ :: rest -> ptfs delim rest |> Option.map (fun (count, remainder) -> (count + 1, remainder)) | [] -> None - let rest = ptfs delim line + let result = ptfs delim line - match rest with + match result with | None -> [ cLstToStr line ] - | Some _x when List.isEmpty line -> [ "" ] - | Some x -> - let chunkSize = List.length line - List.length x - 1 + | Some _ when List.isEmpty line -> [ "" ] + | Some(count, x) -> + let chunkSize = count - 1 - cLstToStr (Seq.take chunkSize line |> Seq.toList) :: pipeTableFindSplits delim x + cLstToStr (List.truncate chunkSize line) :: pipeTableFindSplits delim x diff --git a/tests/FSharp.Markdown.Tests/Markdown.fs b/tests/FSharp.Markdown.Tests/Markdown.fs index bc25ea0ef..2d7ead07f 100644 --- a/tests/FSharp.Markdown.Tests/Markdown.fs +++ b/tests/FSharp.Markdown.Tests/Markdown.fs @@ -818,6 +818,35 @@ let ``Transform tables with delimiters in code or math correctly`` () = Markdown.ToHtml doc |> shouldEqualNoWhiteSpace expected +[] +let ``Transform tables with escaped pipe characters correctly`` () = + let doc = + """| a | b | +|---|---| +| 1\|2 | 3 | +""" + + let expected = + """ + + + + + + + + + + + + +

a

b

1\|2

3

+ +""" + |> properNewLines + + Markdown.ToHtml doc |> shouldEqualNoWhiteSpace expected + [] let ``Parse empty blockquote followed by content`` () = let doc =