diff --git a/.github/scripts/validate-csharp-docs.sh b/.github/scripts/validate-csharp-docs.sh index 1043036e..abe10ef7 100755 --- a/.github/scripts/validate-csharp-docs.sh +++ b/.github/scripts/validate-csharp-docs.sh @@ -8,6 +8,7 @@ for file in \ index.html \ api/Platform.Interfaces.html \ api/Platform.Interfaces.IFactory-1.html \ + Platform.Interfaces.Documentation.pdf \ xrefmap.yml; do if [[ ! -s "$site/$file" ]]; then echo "Documentation site is missing $site/$file." >&2 @@ -15,4 +16,14 @@ for file in \ fi done -echo "Validated DocFX home page, API pages, and cross-reference map." +if [[ $(head -c 5 "$site/Platform.Interfaces.Documentation.pdf") != '%PDF-' ]]; then + echo "Documentation PDF is invalid: $site/Platform.Interfaces.Documentation.pdf." >&2 + exit 1 +fi + +if ! grep -Fq 'Platform.Interfaces.Documentation.pdf' "$site/index.html"; then + echo "Documentation home page is missing its PDF link: $site/index.html." >&2 + exit 1 +fi + +echo "Validated DocFX home page, API pages, documentation PDF, and cross-reference map." diff --git a/.github/scripts/validate-csharp-docs.test.mjs b/.github/scripts/validate-csharp-docs.test.mjs index c8b54e54..2826c15a 100644 --- a/.github/scripts/validate-csharp-docs.test.mjs +++ b/.github/scripts/validate-csharp-docs.test.mjs @@ -9,13 +9,16 @@ import test from "node:test"; const validator = new URL("./validate-csharp-docs.sh", import.meta.url).pathname; -const withSite = (files, check) => { +const withSite = (files, check, contents = {}) => { const site = mkdtempSync(join(tmpdir(), "csharp-docs-")); try { for (const file of files) { const path = join(site, file); mkdirSync(join(path, ".."), { recursive: true }); - writeFileSync(path, "generated content"); + const content = file === "index.html" + ? 'Documentation PDF' + : file.endsWith(".pdf") ? "%PDF-1.7\n" : "generated content"; + writeFileSync(path, contents[file] ?? content); } check(spawnSync("bash", [validator, site], { encoding: "utf8" })); } finally { @@ -28,6 +31,7 @@ const requiredFiles = [ "api/Platform.Interfaces.html", "api/Platform.Interfaces.IFactory-1.html", "xrefmap.yml", + "Platform.Interfaces.Documentation.pdf", ]; test("accepts a complete DocFX site", () => { @@ -44,3 +48,17 @@ for (const missingFile of requiredFiles) { }); }); } + +test("rejects an invalid documentation PDF", () => { + withSite(requiredFiles, ({ status, stderr }) => { + assert.notEqual(status, 0); + assert.match(stderr, /Documentation PDF is invalid/); + }, { "Platform.Interfaces.Documentation.pdf": "not a PDF" }); +}); + +test("rejects a site with no documentation PDF link", () => { + withSite(requiredFiles, ({ status, stderr }) => { + assert.notEqual(status, 0); + assert.match(stderr, /home page is missing its PDF link/); + }, { "index.html": "generated content" }); +}); diff --git a/.gitignore b/.gitignore index 5b14641a..c489eb5b 100644 --- a/.gitignore +++ b/.gitignore @@ -323,6 +323,9 @@ ASALocalRun/ # MSBuild Binary and Structured Log *.binlog +# Generated DocFX site +csharp/_site/ + # NVidia Nsight GPU debugger configuration file *.nvuser @@ -343,4 +346,4 @@ ASALocalRun/ *.[0-9].rule.txt *.[0-9][0-9].rule.txt *.[0-9][0-9][0-9].rule.txt -*.[0-9][0-9][0-9][0-9].rule.txt \ No newline at end of file +*.[0-9][0-9][0-9][0-9].rule.txt diff --git a/README.md b/README.md index 029c0694..066ef74b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ NuGet package: [Platform.Interfaces](https://www.nuget.org/packages/Platform.Int [PDF file](https://linksplatform.github.io/Interfaces/csharp/Platform.Interfaces.pdf) with code for e-readers. +[API documentation PDF](https://linksplatform.github.io/Interfaces/csharp/Platform.Interfaces.Documentation.pdf) generated by DocFX. + ## Dependent libraries * [Platform.Collections](https://github.com/linksplatform/Collections) * [Platform.Setters](https://github.com/linksplatform/Setters) diff --git a/csharp/Platform.Interfaces/Platform.Interfaces.csproj b/csharp/Platform.Interfaces/Platform.Interfaces.csproj index 375a9b54..733ee7d9 100644 --- a/csharp/Platform.Interfaces/Platform.Interfaces.csproj +++ b/csharp/Platform.Interfaces/Platform.Interfaces.csproj @@ -4,7 +4,7 @@ LinksPlatform's Platform.Interfaces Class Library Konstantin Diachenko Platform.Interfaces - 0.5.2 + 0.5.3 Konstantin Diachenko net8 Platform.Interfaces @@ -23,7 +23,7 @@ true embedded latest - Fix package publishing while preserving embedded symbols, eliminate C# workflow warnings, and validate NuGet artifacts before release. + Fix package publishing while preserving embedded symbols, eliminate C# workflow warnings, validate NuGet artifacts before release, and add downloadable DocFX API documentation in PDF format. enable diff --git a/csharp/docfx.json b/csharp/docfx.json index f016de80..b63f6cf7 100644 --- a/csharp/docfx.json +++ b/csharp/docfx.json @@ -28,12 +28,16 @@ "globalMetadata": { "_appTitle": "LinksPlatform's Platform.Interfaces Library", "_enableSearch": true, + "pdf": true, + "pdfFileName": "Platform.Interfaces.Documentation.pdf", + "pdfTocPage": true, "_gitContribute": { "branch": "main" }, "_gitUrlPattern": "github" }, "markdownEngineName": "markdig", + "template": ["default", "modern"], "dest": "_site" } } diff --git a/docs/screenshots/docfx-pdf-home.png b/docs/screenshots/docfx-pdf-home.png new file mode 100644 index 00000000..694069c7 Binary files /dev/null and b/docs/screenshots/docfx-pdf-home.png differ diff --git a/experiments/inspect-docfx-pdf.py b/experiments/inspect-docfx-pdf.py new file mode 100644 index 00000000..c12cd5e7 --- /dev/null +++ b/experiments/inspect-docfx-pdf.py @@ -0,0 +1,16 @@ +"""Inspect a generated DocFX PDF with: pip install pypdf.""" + +import sys + +from pypdf import PdfReader + + +reader = PdfReader(sys.argv[1]) +text = "\n".join(page.extract_text() or "" for page in reader.pages) + +print(f"Pages: {len(reader.pages)}") +for topic in ("Interfaces", "Platform.Interfaces", "IFactory", "IProvider"): + print(f"{topic}: {topic in text}") + +if len(reader.pages) < 2 or any(topic not in text for topic in ("IFactory", "IProvider")): + raise SystemExit("The PDF is missing expected API content.")