Skip to content

Commit be17647

Browse files
Improve fsdocs convert CLI UX and add fsdocs-tool.Tests project
- Make input file positional (Value(0)) instead of --input flag - Add -o shorthand for --output - Infer output format from output file extension when --outputformat not specified - Move ConvertCommand integration tests to new fsdocs-tool.Tests project - Add new test for format inference from extension - Add fsdocs-tool.Tests to solution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 91b3dbf commit be17647

6 files changed

Lines changed: 176 additions & 99 deletions

File tree

FSharp.Formatting.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "content", "content", "{FAD5
128128
docs\content\fsdocs-theme-set-dark.js = docs\content\fsdocs-theme-set-dark.js
129129
EndProjectSection
130130
EndProject
131+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "fsdocs-tool.Tests", "tests\fsdocs-tool.Tests\fsdocs-tool.Tests.fsproj", "{F748A965-C949-4FE7-BFE9-40449F3C58B8}"
132+
EndProject
131133
Global
132134
GlobalSection(SolutionConfigurationPlatforms) = preSolution
133135
Debug|Any CPU = Debug|Any CPU
@@ -230,6 +232,10 @@ Global
230232
{CB78F0EA-8005-4735-A02C-B86CEDC29D85}.Debug|Any CPU.Build.0 = Debug|Any CPU
231233
{CB78F0EA-8005-4735-A02C-B86CEDC29D85}.Release|Any CPU.ActiveCfg = Release|Any CPU
232234
{CB78F0EA-8005-4735-A02C-B86CEDC29D85}.Release|Any CPU.Build.0 = Release|Any CPU
235+
{F748A965-C949-4FE7-BFE9-40449F3C58B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
236+
{F748A965-C949-4FE7-BFE9-40449F3C58B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
237+
{F748A965-C949-4FE7-BFE9-40449F3C58B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
238+
{F748A965-C949-4FE7-BFE9-40449F3C58B8}.Release|Any CPU.Build.0 = Release|Any CPU
233239
EndGlobalSection
234240
GlobalSection(SolutionProperties) = preSolution
235241
HideSolutionNode = FALSE
@@ -265,6 +271,7 @@ Global
265271
{188DC91F-2202-4495-ACD2-542D7C30364E} = {C7804F57-7FC6-4CF6-BDF6-127D6F9EBEA6}
266272
{FAD5C374-4748-4A3D-A435-FFA425916F3A} = {312E452A-1068-4804-89E7-0AFBAD5F885F}
267273
{52B949AA-A3F7-4894-B713-804BAEB71118} = {4AE0198D-EDE5-40B0-A5CD-FC7B6F891D94}
274+
{F748A965-C949-4FE7-BFE9-40449F3C58B8} = {8D44B659-E9F7-4CE4-B5DA-D37CDDCD2525}
268275
EndGlobalSection
269276
GlobalSection(ExtensibilityGlobals) = postSolution
270277
SolutionGuid = {76F121F8-70E0-49FB-9ADF-C7B660C0EB67}

RELEASE_NOTES.md

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

55
### Added
66
* 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)
7+
* `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)
8+
* `fsdocs convert` infers the output format from the output file extension when `--outputformat` is not specified (e.g. `-o out.md` implies `--outputformat markdown`). [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
9+
* `fsdocs convert` now accepts `-o` as a shorthand for `--output`. [#1019](https://github.com/fsprojects/FSharp.Formatting/pull/1019)
710
* Add `<FsDocsAllowExecutableProject>true</FsDocsAllowExecutableProject>` project file setting to include executable projects (OutputType=Exe/WinExe) in API documentation generation. [#918](https://github.com/fsprojects/FSharp.Formatting/issues/918)
811
* Add `{{fsdocs-logo-alt}}` substitution (configurable via `<FsDocsLogoAlt>` MSBuild property, defaults to `Logo`) for accessible alt text on the header logo image. [#626](https://github.com/fsprojects/FSharp.Formatting/issues/626)
912

src/fsdocs-tool/BuildCommand.fs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2113,10 +2113,11 @@ type CoreBuildOptions(watch) =
21132113
"convert a single document (.md, .fsx, .ipynb) to HTML or another output format without building a full documentation site")>]
21142114
type ConvertCommand() =
21152115

2116-
[<Option("input", Required = true, HelpText = "Input file to convert (.md, .fsx or .ipynb).")>]
2116+
[<Value(0, MetaName = "input", Required = true, HelpText = "Input file to convert (.md, .fsx or .ipynb).")>]
21172117
member val input = "" with get, set
21182118

2119-
[<Option("output",
2119+
[<Option('o',
2120+
"output",
21202121
Required = false,
21212122
HelpText =
21222123
"Output file path. Defaults to the input filename with the output format extension in the current directory.")>]
@@ -2129,9 +2130,10 @@ type ConvertCommand() =
21292130

21302131
[<Option("outputformat",
21312132
Required = false,
2132-
Default = "html",
2133-
HelpText = "Output format: html (default), ipynb, latex, fsx, markdown.")>]
2134-
member val outputFormat = "html" with get, set
2133+
Default = "",
2134+
HelpText =
2135+
"Output format: html (default), ipynb, latex, fsx, markdown. When not specified, inferred from the output file extension.")>]
2136+
member val outputFormat = "" with get, set
21352137

21362138
[<Option("eval", Default = false, Required = false, HelpText = "Evaluate F# fragments in scripts.")>]
21372139
member val eval = false with get, set
@@ -2152,8 +2154,24 @@ type ConvertCommand() =
21522154
1
21532155
else
21542156

2157+
// Infer output format: explicit flag > extension of -o > default html
2158+
let resolvedFormat =
2159+
if not (String.IsNullOrWhiteSpace this.outputFormat) then
2160+
this.outputFormat.ToLowerInvariant()
2161+
elif not (String.IsNullOrWhiteSpace this.output) then
2162+
let ext = Path.GetExtension(this.output).TrimStart('.').ToLowerInvariant()
2163+
2164+
match ext with
2165+
| "md" -> "markdown"
2166+
| "ipynb" -> "ipynb"
2167+
| "tex" -> "latex"
2168+
| "fsx" -> "fsx"
2169+
| _ -> "html"
2170+
else
2171+
"html"
2172+
21552173
let outputKind =
2156-
match this.outputFormat.ToLowerInvariant() with
2174+
match resolvedFormat with
21572175
| "ipynb" -> OutputKind.Pynb
21582176
| "latex" -> OutputKind.Latex
21592177
| "fsx" -> OutputKind.Fsx

tests/FSharp.Literate.Tests/DocContentTests.fs

Lines changed: 0 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -337,96 +337,3 @@ let ``ipynb notebook evaluates`` () =
337337
338338
ipynbOut |> shouldContainText "10007"
339339
*)
340-
341-
// --------------------------------------------------------------------------------------
342-
// Integration tests for ConvertCommand
343-
// --------------------------------------------------------------------------------------
344-
345-
[<Test>]
346-
let ``ConvertCommand converts .md file to HTML`` () =
347-
let inputFile = __SOURCE_DIRECTORY__ </> "files" </> "simple2.md"
348-
let outputFile = __SOURCE_DIRECTORY__ </> "convert-output" </> "simple2-convert.html"
349-
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
350-
351-
let cmd = ConvertCommand()
352-
cmd.input <- inputFile
353-
cmd.output <- outputFile
354-
cmd.outputFormat <- "html"
355-
let result = cmd.Execute()
356-
357-
result |> shouldEqual 0
358-
File.Exists(outputFile) |> shouldEqual true
359-
let html = File.ReadAllText(outputFile)
360-
html |> shouldContainText "Heading"
361-
html |> shouldContainText "Code sample"
362-
363-
[<Test>]
364-
let ``ConvertCommand converts .fsx file to HTML`` () =
365-
let inputFile = __SOURCE_DIRECTORY__ </> "files" </> "simple1.fsx"
366-
let outputFile = __SOURCE_DIRECTORY__ </> "convert-output" </> "simple1-convert.html"
367-
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
368-
369-
let cmd = ConvertCommand()
370-
cmd.input <- inputFile
371-
cmd.output <- outputFile
372-
cmd.outputFormat <- "html"
373-
let result = cmd.Execute()
374-
375-
result |> shouldEqual 0
376-
File.Exists(outputFile) |> shouldEqual true
377-
let html = File.ReadAllText(outputFile)
378-
html |> shouldContainText "Heading"
379-
html |> shouldContainText "Code sample"
380-
381-
[<Test>]
382-
let ``ConvertCommand converts .ipynb file to HTML`` () =
383-
let inputFile = __SOURCE_DIRECTORY__ </> "files" </> "simple3.ipynb"
384-
let outputFile = __SOURCE_DIRECTORY__ </> "convert-output" </> "simple3-convert.html"
385-
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
386-
387-
let cmd = ConvertCommand()
388-
cmd.input <- inputFile
389-
cmd.output <- outputFile
390-
cmd.outputFormat <- "html"
391-
let result = cmd.Execute()
392-
393-
result |> shouldEqual 0
394-
File.Exists(outputFile) |> shouldEqual true
395-
let html = File.ReadAllText(outputFile)
396-
html |> shouldContainText "Heading"
397-
html |> shouldContainText "Code sample"
398-
399-
[<Test>]
400-
let ``ConvertCommand converts .md file to Markdown output format`` () =
401-
let inputFile = __SOURCE_DIRECTORY__ </> "files" </> "simple2.md"
402-
let outputFile = __SOURCE_DIRECTORY__ </> "convert-output" </> "simple2-convert.md"
403-
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
404-
405-
let cmd = ConvertCommand()
406-
cmd.input <- inputFile
407-
cmd.output <- outputFile
408-
cmd.outputFormat <- "markdown"
409-
let result = cmd.Execute()
410-
411-
result |> shouldEqual 0
412-
File.Exists(outputFile) |> shouldEqual true
413-
414-
[<Test>]
415-
let ``ConvertCommand returns error code for non-existent input file`` () =
416-
let cmd = ConvertCommand()
417-
cmd.input <- __SOURCE_DIRECTORY__ </> "files" </> "does-not-exist.md"
418-
cmd.output <- __SOURCE_DIRECTORY__ </> "convert-output" </> "out.html"
419-
let result = cmd.Execute()
420-
result |> shouldEqual 1
421-
422-
[<Test>]
423-
let ``ConvertCommand returns error code for unsupported file extension`` () =
424-
let inputFile = __SOURCE_DIRECTORY__ </> "files" </> "template.html"
425-
let outputFile = __SOURCE_DIRECTORY__ </> "convert-output" </> "out.html"
426-
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
427-
428-
let cmd = ConvertCommand()
429-
cmd.input <- inputFile
430-
cmd.output <- outputFile
431-
let result = cmd.Execute()
432-
result |> shouldEqual 1
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
module fsdocs.Tests.ConvertCommand
2+
3+
open System.IO
4+
open fsdocs
5+
open NUnit.Framework
6+
open FsUnitTyped
7+
8+
do FSharp.Formatting.TestHelpers.enableLogging ()
9+
10+
let (</>) a b = Path.Combine(a, b)
11+
12+
let literateTestFiles = Path.GetFullPath(Path.Combine(__SOURCE_DIRECTORY__, "..", "FSharp.Literate.Tests", "files"))
13+
14+
// --------------------------------------------------------------------------------------
15+
// Integration tests for ConvertCommand
16+
// --------------------------------------------------------------------------------------
17+
18+
[<Test>]
19+
let ``ConvertCommand converts .md file to HTML`` () =
20+
let inputFile = literateTestFiles </> "simple2.md"
21+
let outputFile = Path.GetTempPath() </> "fsdocs-tool-tests" </> "simple2-convert.html"
22+
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
23+
24+
let cmd = ConvertCommand()
25+
cmd.input <- inputFile
26+
cmd.output <- outputFile
27+
cmd.outputFormat <- "html"
28+
let result = cmd.Execute()
29+
30+
result |> shouldEqual 0
31+
File.Exists(outputFile) |> shouldEqual true
32+
let html = File.ReadAllText(outputFile)
33+
html |> shouldContainText "Heading"
34+
html |> shouldContainText "Code sample"
35+
36+
[<Test>]
37+
let ``ConvertCommand converts .fsx file to HTML`` () =
38+
let inputFile = literateTestFiles </> "simple1.fsx"
39+
let outputFile = Path.GetTempPath() </> "fsdocs-tool-tests" </> "simple1-convert.html"
40+
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
41+
42+
let cmd = ConvertCommand()
43+
cmd.input <- inputFile
44+
cmd.output <- outputFile
45+
cmd.outputFormat <- "html"
46+
let result = cmd.Execute()
47+
48+
result |> shouldEqual 0
49+
File.Exists(outputFile) |> shouldEqual true
50+
let html = File.ReadAllText(outputFile)
51+
html |> shouldContainText "Heading"
52+
html |> shouldContainText "Code sample"
53+
54+
[<Test>]
55+
let ``ConvertCommand converts .ipynb file to HTML`` () =
56+
let inputFile = literateTestFiles </> "simple3.ipynb"
57+
let outputFile = Path.GetTempPath() </> "fsdocs-tool-tests" </> "simple3-convert.html"
58+
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
59+
60+
let cmd = ConvertCommand()
61+
cmd.input <- inputFile
62+
cmd.output <- outputFile
63+
cmd.outputFormat <- "html"
64+
let result = cmd.Execute()
65+
66+
result |> shouldEqual 0
67+
File.Exists(outputFile) |> shouldEqual true
68+
let html = File.ReadAllText(outputFile)
69+
html |> shouldContainText "Heading"
70+
html |> shouldContainText "Code sample"
71+
72+
[<Test>]
73+
let ``ConvertCommand converts .md file to Markdown output format`` () =
74+
let inputFile = literateTestFiles </> "simple2.md"
75+
let outputFile = Path.GetTempPath() </> "fsdocs-tool-tests" </> "simple2-convert.md"
76+
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
77+
78+
let cmd = ConvertCommand()
79+
cmd.input <- inputFile
80+
cmd.output <- outputFile
81+
cmd.outputFormat <- "markdown"
82+
let result = cmd.Execute()
83+
84+
result |> shouldEqual 0
85+
File.Exists(outputFile) |> shouldEqual true
86+
87+
[<Test>]
88+
let ``ConvertCommand infers output format from output file extension`` () =
89+
let inputFile = literateTestFiles </> "simple2.md"
90+
let outputFile = Path.GetTempPath() </> "fsdocs-tool-tests" </> "simple2-inferred.md"
91+
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
92+
93+
let cmd = ConvertCommand()
94+
cmd.input <- inputFile
95+
cmd.output <- outputFile
96+
// no outputFormat set — should infer "markdown" from .md extension
97+
let result = cmd.Execute()
98+
99+
result |> shouldEqual 0
100+
File.Exists(outputFile) |> shouldEqual true
101+
102+
[<Test>]
103+
let ``ConvertCommand returns error code for non-existent input file`` () =
104+
let cmd = ConvertCommand()
105+
cmd.input <- literateTestFiles </> "does-not-exist.md"
106+
cmd.output <- Path.GetTempPath() </> "fsdocs-tool-tests" </> "out.html"
107+
let result = cmd.Execute()
108+
result |> shouldEqual 1
109+
110+
[<Test>]
111+
let ``ConvertCommand returns error code for unsupported file extension`` () =
112+
let inputFile = literateTestFiles </> "template.html"
113+
let outputFile = Path.GetTempPath() </> "fsdocs-tool-tests" </> "out.html"
114+
Directory.CreateDirectory(Path.GetDirectoryName(outputFile)) |> ignore
115+
116+
let cmd = ConvertCommand()
117+
cmd.input <- inputFile
118+
cmd.output <- outputFile
119+
let result = cmd.Execute()
120+
result |> shouldEqual 1
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<RollForward>LatestMajor</RollForward>
6+
<DisableMSBuildAssemblyCopyCheck>true</DisableMSBuildAssemblyCopyCheck>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<Compile Include="ConvertCommandTests.fs" />
10+
</ItemGroup>
11+
<ItemGroup>
12+
<ProjectReference Include="..\..\src\fsdocs-tool\fsdocs-tool.fsproj" />
13+
<ProjectReference Include="..\FSharp.Formatting.TestHelpers\FSharp.Formatting.TestHelpers.fsproj" />
14+
</ItemGroup>
15+
<ItemGroup>
16+
<PackageReference Include="FSharp.Core" />
17+
<PackageReference Include="FsUnit" />
18+
<PackageReference Include="NUnit" />
19+
<PackageReference Include="NUnit3TestAdapter" />
20+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
21+
</ItemGroup>
22+
</Project>

0 commit comments

Comments
 (0)