Skip to content

Commit 4e2a8cc

Browse files
authored
Merge branch 'main' into repo-assist/perf-compile-regex-2026-04-10-f88f43f8587c44e1
2 parents 65aebd6 + 06d7467 commit 4e2a8cc

7 files changed

Lines changed: 660 additions & 462 deletions

File tree

.github/aw/actions-lock.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,20 @@
55
"version": "v6.0.2",
66
"sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd"
77
},
8-
"actions/github-script@v8": {
8+
"actions/github-script@v9.0.0": {
99
"repo": "actions/github-script",
10-
"version": "v8",
11-
"sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd"
10+
"version": "v9.0.0",
11+
"sha": "d746ffe35508b1917358783b479e04febd2b8f71"
1212
},
13-
"github/gh-aw-actions/setup@v0.65.4": {
13+
"github/gh-aw-actions/setup@v0.68.1": {
1414
"repo": "github/gh-aw-actions/setup",
15-
"version": "v0.65.4",
16-
"sha": "934698b44320d87a7a9196339f90293f10bd2247"
15+
"version": "v0.68.1",
16+
"sha": "2fe53acc038ba01c3bbdc767d4b25df31ca5bdfc"
1717
},
18-
"github/gh-aw/actions/setup@v0.65.4": {
18+
"github/gh-aw/actions/setup@v0.68.1": {
1919
"repo": "github/gh-aw/actions/setup",
20-
"version": "v0.65.4",
21-
"sha": "b5a9fb08751b738a86b3b605ff1fdf34956adbf4"
20+
"version": "v0.68.1",
21+
"sha": "5a06d310cf45161bde77d070065a1e1489fc411c"
2222
}
2323
}
2424
}

.github/workflows/repo-assist.lock.yml

Lines changed: 535 additions & 439 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/repo-assist.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ safe-outputs:
6565
target: "*"
6666
title-prefix: "[Repo Assist] "
6767
max: 4
68+
protected-files: fallback-to-issue
6869
create-issue:
6970
title-prefix: "[Repo Assist] "
7071
labels: [automation, repo-assist]
@@ -175,7 +176,7 @@ steps:
175176
json.dump(result, f, indent=2)
176177
EOF
177178
178-
source: githubnext/agentics/workflows/repo-assist.md@e1ecf341a90b7bc2021e77c58685d7e269e20b99
179+
source: githubnext/agentics/workflows/repo-assist.md@97143ac59cb3a13ef2a77581f929f06719c7402a
179180
---
180181

181182
# Repo Assist

RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
### Changed
66
* 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.
7+
### Fixed
8+
* Fix `Markdown.ToMd` dropping link titles in `DirectLink` and `DirectImage` spans. Links with a title attribute (e.g. `[text](url "title")`) now round-trip correctly; without this fix the title was silently discarded on serialisation.
9+
* Fix `Markdown.ToMd` serialising inline code spans that contain backtick characters. Previously, `InlineCode` was always wrapped in single backticks, producing syntactically incorrect Markdown when the code body contained backticks. Now the serialiser selects the shortest backtick fence that does not collide with the body content (e.g. a double-backtick fence for bodies containing single backticks, triple for double, etc.), matching the CommonMark spec.
710

811
## [22.0.0] - 2026-04-03
912

@@ -22,6 +25,8 @@
2225
## [22.0.0-alpha.2] - 2026-03-13
2326

2427
### Added
28+
* Add `--root` option to `fsdocs watch` to override the root URL for generated pages. Useful for serving docs via GitHub Codespaces, reverse proxies, or other remote hosting where `localhost` URLs are inaccessible. E.g. `fsdocs watch --root /` or `fsdocs watch --root https://example.com/docs/`. When not set, defaults to `http://localhost:<port>/` as before. [#924](https://github.com/fsprojects/FSharp.Formatting/issues/924)
29+
* Fix `fsdocs watch` hot-reload WebSocket to connect using the page's actual host (`window.location.host`) instead of a hardcoded `localhost:<port>`, so hot-reload works correctly in GitHub Codespaces, behind reverse proxies, and over HTTPS. [#924](https://github.com/fsprojects/FSharp.Formatting/issues/924)
2530
* Search dialog now auto-focuses the search input when opened, clears on close, and can be triggered with `Ctrl+K` / `Cmd+K` in addition to `/`.
2631
* Add `dotnet fsdocs convert` command to convert a single `.md`, `.fsx`, or `.ipynb` file to HTML (or another output format) without building a full documentation site. [#811](https://github.com/fsprojects/FSharp.Formatting/issues/811)
2732
* `fsdocs convert` now accepts the input file as a positional argument (e.g. `fsdocs convert notebook.ipynb -o notebook.html`). [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)

src/FSharp.Formatting.Markdown/MarkdownUtils.fs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,50 @@ module internal MarkdownUtils =
106106
| HardLineBreak(_) -> "\n"
107107

108108
| AnchorLink _ -> ""
109+
| DirectLink(body, link, title, _) ->
110+
let t =
111+
title
112+
|> Option.map (fun t -> sprintf " \"%s\"" (t.Replace("\"", "\\\"")))
113+
|> Option.defaultValue ""
114+
115+
"[" + formatSpans ctx body + "](" + link + t + ")"
116+
109117
| IndirectLink(body, _, LookupKey ctx.Links (link, _), _)
110-
| DirectLink(body, link, _, _)
111118
| IndirectLink(body, link, _, _) -> "[" + formatSpans ctx body + "](" + link + ")"
112119

113120
| IndirectImage(body, _, LookupKey ctx.Links (link, _), _) -> sprintf "![%s](%s)" body link
114121
| IndirectImage(body, _, key, _) -> sprintf "![%s][%s]" body key
115-
| DirectImage(body, link, _, _) -> sprintf "![%s](%s)" body link
122+
123+
| DirectImage(body, link, title, _) ->
124+
let t =
125+
title
126+
|> Option.map (fun t -> sprintf " \"%s\"" (t.Replace("\"", "\\\"")))
127+
|> Option.defaultValue ""
128+
129+
sprintf "![%s](%s)" body (link + t)
116130
| Strong(body, _) -> "**" + formatSpans ctx body + "**"
117-
| InlineCode(body, _) -> "`" + body + "`"
131+
| InlineCode(body, _) ->
132+
// Pick the shortest backtick fence that does not appear in the body.
133+
// E.g. body "``h``" needs a triple-backtick fence; body "a`b" needs double.
134+
let maxConsecutiveBackticks =
135+
body
136+
|> Seq.fold
137+
(fun (maxR, run) c ->
138+
if c = '`' then
139+
let run' = run + 1
140+
(max maxR run'), run'
141+
else
142+
maxR, 0)
143+
(0, 0)
144+
|> fst
145+
146+
let fence = String.replicate (maxConsecutiveBackticks + 1) "`"
147+
// Surround with spaces when the body starts or ends with a backtick so the
148+
// fence and content do not merge (e.g. `` ``h`` `` would look like 4-backtick).
149+
if body.Length > 0 && (body.[0] = '`' || body.[body.Length - 1] = '`') then
150+
fence + " " + body + " " + fence
151+
else
152+
fence + body + fence
118153
| Emphasis(body, _) -> "*" + formatSpans ctx body + "*"
119154

120155
/// Format a list of MarkdownSpan

src/fsdocs-tool/BuildCommand.fs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -827,11 +827,10 @@ module Serve =
827827
let refreshEvent = FSharp.Control.Event<string>()
828828

829829
/// generate the script to inject into html to enable hot reload during development
830-
let generateWatchScript (port: int) =
831-
let tag =
832-
"""
830+
let generateWatchScript () =
831+
"""
833832
<script type="text/javascript">
834-
var wsUri = "ws://localhost:{{PORT}}/websocket";
833+
var wsUri = "ws://" + window.location.host + "/websocket";
835834
function init()
836835
{
837836
websocket = new WebSocket(wsUri);
@@ -858,8 +857,6 @@ module Serve =
858857
</script>
859858
"""
860859

861-
tag.Replace("{{PORT}}", string<int> port)
862-
863860
let connectedClients = ConcurrentDictionary<WebSocket, unit>()
864861

865862
let socketHandler (webSocket: WebSocket) (context: HttpContext) =
@@ -1575,9 +1572,15 @@ type CoreBuildOptions(watch) =
15751572
// Adjust the user substitutions for 'watch' mode root
15761573
let userRoot, userParameters =
15771574
if watch then
1578-
let userRoot = sprintf "http://localhost:%d/" this.port_option
1579-
1580-
if userParametersDict.ContainsKey(ParamKeys.root) then
1575+
let userRoot =
1576+
match this.root_override_option with
1577+
| Some r -> r
1578+
| None -> sprintf "http://localhost:%d/" this.port_option
1579+
1580+
if
1581+
userParametersDict.ContainsKey(ParamKeys.root)
1582+
&& this.root_override_option.IsNone
1583+
then
15811584
printfn "ignoring user-specified root since in watch mode, root = %s" userRoot
15821585

15831586
let userParameters =
@@ -1888,7 +1891,7 @@ type CoreBuildOptions(watch) =
18881891
let getLatestWatchScript () =
18891892
if watch then
18901893
// if running in watch mode, inject hot reload script
1891-
[ ParamKeys.``fsdocs-watch-script``, Serve.generateWatchScript this.port_option ]
1894+
[ ParamKeys.``fsdocs-watch-script``, Serve.generateWatchScript () ]
18921895
else
18931896
// otherwise, inject empty replacement string
18941897
[ ParamKeys.``fsdocs-watch-script``, "" ]
@@ -2337,6 +2340,9 @@ type CoreBuildOptions(watch) =
23372340
abstract port_option: int
23382341
default x.port_option = 0
23392342

2343+
abstract root_override_option: string option
2344+
default x.root_override_option = None
2345+
23402346
/// Helpers for the <c>fsdocs convert</c> command.
23412347
module private ConvertHelpers =
23422348

@@ -2753,3 +2759,12 @@ type WatchCommand() =
27532759

27542760
[<Option("port", Required = false, Default = 8901, HelpText = "Port to serve content for http://localhost serving.")>]
27552761
member val port = 8901 with get, set
2762+
2763+
override x.root_override_option = if x.root = "" then None else Some x.root
2764+
2765+
[<Option("root",
2766+
Required = false,
2767+
Default = "",
2768+
HelpText =
2769+
"Override the root URL for generated pages. Useful for reverse proxies or GitHub Codespaces. E.g. --root / or --root https://example.com/docs/. When not set, defaults to http://localhost:<port>/.")>]
2770+
member val root = "" with get, set

tests/FSharp.Markdown.Tests/Markdown.fs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,16 +1239,62 @@ let ``ToMd preserves strong (bold) text`` () =
12391239
let ``ToMd preserves inline code`` () =
12401240
"Use `printf` here." |> toMd |> should contain "`printf`"
12411241

1242+
[<Test>]
1243+
let ``ToMd round-trips inline code containing a single backtick`` () =
1244+
// "a`b" must be serialised with a double-backtick fence so it re-parses correctly.
1245+
let original = "`` a`b ``"
1246+
let md = Markdown.Parse original
1247+
let result = Markdown.ToMd md
1248+
// The serialised form must round-trip: re-parsing must yield the same InlineCode body.
1249+
let reparsed = Markdown.Parse result
1250+
1251+
match reparsed.Paragraphs with
1252+
| [ Paragraph([ InlineCode("a`b", _) ], _) ] -> ()
1253+
| _ -> Assert.Fail(sprintf "Expected InlineCode(\"a`b\") after round-trip, got: %A" reparsed.Paragraphs)
1254+
1255+
[<Test>]
1256+
let ``ToMd round-trips inline code containing multiple backticks`` () =
1257+
// Body "``h``" contains double backticks — needs a triple-backtick fence.
1258+
let original = "` ``h`` `"
1259+
let md = Markdown.Parse original
1260+
let result = Markdown.ToMd md
1261+
1262+
match (Markdown.Parse result).Paragraphs with
1263+
| [ Paragraph([ InlineCode("``h``", _) ], _) ] -> ()
1264+
| _ -> Assert.Fail(sprintf "Expected InlineCode(\"``h``\") after round-trip, got: %A" result)
1265+
12421266
[<Test>]
12431267
let ``ToMd preserves a direct link`` () =
12441268
"[FSharp](https://fsharp.org)"
12451269
|> toMd
12461270
|> should contain "[FSharp](https://fsharp.org)"
12471271

1272+
[<Test>]
1273+
let ``ToMd preserves a direct link with title`` () =
1274+
let md = "[FSharp](https://fsharp.org \"F# language\")"
1275+
let result = toMd md
1276+
result |> should contain "[FSharp]("
1277+
result |> should contain "https://fsharp.org"
1278+
result |> should contain "\"F# language\""
1279+
1280+
[<Test>]
1281+
let ``ToMd preserves a direct link without title unchanged`` () =
1282+
let result = "[link](http://example.com)" |> toMd
1283+
result |> should contain "[link](http://example.com)"
1284+
result |> should not' (contain "\"")
1285+
12481286
[<Test>]
12491287
let ``ToMd preserves a direct image`` () =
12501288
"![alt text](image.png)" |> toMd |> should contain "![alt text](image.png)"
12511289

1290+
[<Test>]
1291+
let ``ToMd preserves a direct image with title`` () =
1292+
let md = "![photo](image.png \"My Photo\")"
1293+
let result = toMd md
1294+
result |> should contain "![photo]("
1295+
result |> should contain "image.png"
1296+
result |> should contain "\"My Photo\""
1297+
12521298
[<Test>]
12531299
let ``ToMd preserves an unordered list`` () =
12541300
let md = "* apple\n* banana\n* cherry"

0 commit comments

Comments
 (0)