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.")