diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8a318b6a..35e55f01 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Changed +* Refactor `Markdown.ToMd` list-block formatting: extract a shared `formatListBlock` helper to eliminate the near-identical code paths for ordered and unordered lists in `MarkdownUtils.fs`. + ### Changed * Compile `Regex` instances to module-level singletons (with `RegexOptions.Compiled`) in `PageContentList`, `HtmlFormatting`, `Formatting`, `Menu`, and `LlmsTxt`. Previously a new, uncompiled `Regex` was constructed on every call (once per page heading, once per HTML page, once per menu item, once per llms.txt entry), incurring repeated JIT overhead. The patterns are now compiled once at module load and reused across all calls. * Replace deprecated `System.Net.WebClient` with `System.Net.Http.HttpClient` in the image downloader used by `--saveimages`. Removes the `#nowarn "44"` suppression. diff --git a/src/FSharp.Formatting.Markdown/MarkdownUtils.fs b/src/FSharp.Formatting.Markdown/MarkdownUtils.fs index 6b784184..ec9e7514 100644 --- a/src/FSharp.Formatting.Markdown/MarkdownUtils.fs +++ b/src/FSharp.Formatting.Markdown/MarkdownUtils.fs @@ -157,6 +157,32 @@ module internal MarkdownUtils = /// Format a MarkdownParagraph let rec formatParagraph (ctx: FormattingContext) paragraph = + // Shared helper for both ordered and unordered list blocks. + // getPrefix receives the 0-based item index and returns the leading string (e.g. "* " or "1. "). + let formatListBlock paragraphsl (getPrefix: int -> string) = + let isTight = + paragraphsl + |> List.forall (function + | [ Span _ ] -> true + | _ -> false) + + [ for (n, paragraphs) in List.indexed paragraphsl do + for (i, paragraph) in List.indexed paragraphs do + let lines: string list = formatParagraph ctx paragraph + let lines = if lines.IsEmpty then [ "" ] else lines + + for (j, line) in List.indexed lines do + if i = 0 && j = 0 then + yield getPrefix n + line + else + yield " " + line + + if not isTight then + yield "" + + if isTight then + yield "" ] + [ match paragraph with | LatexBlock(env, lines, _) -> // Single-line equation blocks are rendered with the compact $$...$$ notation @@ -197,54 +223,8 @@ module internal MarkdownUtils = yield f yield "" - | ListBlock(Unordered, paragraphsl, _) -> - // A tight list has exactly one Span per item (no blank lines between items). - let isTight = - paragraphsl - |> List.forall (function - | [ Span _ ] -> true - | _ -> false) - - for paragraphs in paragraphsl do - for (i, paragraph) in List.indexed paragraphs do - let lines = formatParagraph ctx paragraph - let lines = if lines.IsEmpty then [ "" ] else lines - - for (j, line) in List.indexed lines do - if i = 0 && j = 0 then - yield "* " + line - else - yield " " + line - - if not isTight then - yield "" - - if isTight then - yield "" - | ListBlock(Ordered, paragraphsl, _) -> - // A tight list has exactly one Span per item (no blank lines between items). - let isTight = - paragraphsl - |> List.forall (function - | [ Span _ ] -> true - | _ -> false) - - for (n, paragraphs) in List.indexed paragraphsl do - for (i, paragraph) in List.indexed paragraphs do - let lines = formatParagraph ctx paragraph - let lines = if lines.IsEmpty then [ "" ] else lines - - for (j, line) in List.indexed lines do - if i = 0 && j = 0 then - yield $"%i{n + 1}. " + line - else - yield " " + line - - if not isTight then - yield "" - - if isTight then - yield "" + | ListBlock(Unordered, paragraphsl, _) -> yield! formatListBlock paragraphsl (fun _ -> "* ") + | ListBlock(Ordered, paragraphsl, _) -> yield! formatListBlock paragraphsl (fun n -> $"{n + 1}. ") | TableBlock(headers, alignments, rows, _) -> match headers with