Skip to content

Commit 894d535

Browse files
committed
chore(ci): check that common.md is included where it's needed
1 parent 35d4ef0 commit 894d535

File tree

3 files changed

+138
-6
lines changed

3 files changed

+138
-6
lines changed

.github/workflows/doc.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
run: .github/scripts/install-deno.sh
2121
- name: Set rustdoc flags
2222
run: |
23+
# [ref:doc_global_styling]
2324
echo "RUSTDOCFLAGS=--html-in-header `pwd`/src/r3/src/common.md" >> $GITHUB_ENV
2425
2526
- name: Build Documentation
@@ -32,6 +33,11 @@ jobs:
3233
- name: Replace the non-local crate documentation with redirect pages to docs.rs
3334
run: deno run -A scripts/externalize-non-local-docs.ts -y
3435

36+
- name: Check the generated documentation
37+
run: |
38+
# Don't fail the pipeline on errors because they are mostly minor
39+
deno run --allow-read scripts/check-doc.ts || true
40+
3541
- name: Generate Badge
3642
run: |
3743
rev=`git show-ref --head HEAD | cut -b 1-7`

scripts/check-doc.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// This [Deno] program scans the compiled API documentation to check for errors.
2+
//
3+
// [Deno]: https://deno.land/
4+
//
5+
// Usage: deno run --allow-read scripts/check-workspace.ts
6+
import { parse as parseFlags } from "https://deno.land/std@0.125.0/flags/mod.ts";
7+
import { walk } from "https://deno.land/std@0.125.0/fs/mod.ts";
8+
import * as path from "https://deno.land/std@0.125.0/path/mod.ts";
9+
import * as log from "https://deno.land/std@0.125.0/log/mod.ts";
10+
11+
const parsedArgs = parseFlags(Deno.args, {
12+
"alias": {
13+
h: "help",
14+
d: "rustdoc-output",
15+
},
16+
"string": [
17+
"rustdoc-output",
18+
],
19+
});
20+
21+
if (parsedArgs["help"]) {
22+
console.log("Arguments:");
23+
console.log(" -h --help Displays this message");
24+
console.log(" -d DIRECTORY --rustdoc-output=DIRECTORY");
25+
console.log(" Specifies the rustdoc output directory to scan. " +
26+
"Defaults to `./target/doc` when unspecified.");
27+
}
28+
29+
await log.setup({
30+
handlers: {
31+
console: new log.handlers.ConsoleHandler("DEBUG"),
32+
},
33+
34+
loggers: {
35+
default: {
36+
level: "INFO",
37+
handlers: ["console"],
38+
},
39+
},
40+
});
41+
42+
const logger = log.getLogger();
43+
let hasError = false;
44+
let expectedRepository: string | null = null;
45+
46+
// A code fragment indicating the presence of `common.md`
47+
const COMMON_CSS_FRAGMENT = /\.toc-header \+ ul::before {/g;
48+
// Code fragments that require the presence of `common.md`
49+
const COMMON_CSS_USES = [
50+
/class="toc-header"/,
51+
// The negative lookbehind is intended to avoid matching
52+
// the example code in `common.md`
53+
/(?<!\* *<div )class="admonition-follows"/,
54+
/class="disabled-feature-warning"/,
55+
// The negative lookbehind is intended to avoid matching
56+
// the example code in `common.md`
57+
/(?<!\* *<span )class="class"/,
58+
'<cneter>',
59+
];
60+
61+
await validateRustdocOutput(parsedArgs.d || "./target/doc");
62+
63+
if (hasError) {
64+
Deno.exit(1);
65+
}
66+
67+
async function validateRustdocOutput(docPath: string): Promise<void> {
68+
let numFilesScanned = 0;
69+
70+
logger.info(`Scanning ${docPath}`);
71+
for await (const { path } of walk(docPath, { includeDirs: false })) {
72+
if (!path.endsWith(".html") && !path.endsWith(".htm")) {
73+
continue;
74+
}
75+
76+
logger.debug(`# ${path}`);
77+
78+
const html = await Deno.readTextFile(path);
79+
numFilesScanned += 1;
80+
81+
const numCommonCssInstances =
82+
Array.from(html.matchAll(COMMON_CSS_FRAGMENT)).length;
83+
if (numCommonCssInstances === 0) {
84+
// Maybe a redirect page?
85+
logger.debug(`${path}: Doesn't contain a fragment of 'common.md' - ignoring`);
86+
continue;
87+
} else if (numCommonCssInstances >= 2) {
88+
// `#[doc = ...]` (per-file) + `$RUSTDOCFLAGS` [ref:doc_global_styling]
89+
logger.debug(`${path}: There's a per-file inclusion of 'common.md' - ignoring`);
90+
91+
if (numCommonCssInstances > 2) {
92+
logger.warning(`${path}: Includes too many instances of 'common.md'`);
93+
}
94+
95+
continue;
96+
} else {
97+
// This file lacks a per-file incluson of `common.md`.
98+
}
99+
100+
// To prevent the degradation of the appearance in the absence of the
101+
// appropriate `$RUSTDOCFLAGS`, this page must be devoid of constructs
102+
// that make use of the styling rules defined by `common.md`.
103+
for (const cssUsage of COMMON_CSS_USES) {
104+
if (html.match(cssUsage)) {
105+
logger.error(`${path}: This file lacks a per-file inclusion of 'common.md', ` +
106+
`but includes a code fragment '${cssUsage}'`);
107+
hasError = true;
108+
}
109+
}
110+
}
111+
112+
logger.info(`${numFilesScanned} file(s) have been checked`);
113+
} // validateRustdocOutput

scripts/check-workspace.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ async function validateWorkspace(workspacePath: string): Promise<void> {
156156
}
157157

158158
// We want `common.md` applied for the entire crate in order that the
159-
// upper-left logo is properly styled [tag:doc_global_styling]
159+
// upper-left custom logo is properly styled in official documentation
160+
// builds [tag:doc_global_styling]
160161
const docsMetadata = pkg?.metadata?.docs?.rs ?? {};
161162
if (publish) {
162163
const docsArgs = docsMetadata['rustdoc-args'] ?? [];
@@ -179,12 +180,24 @@ async function validateWorkspace(workspacePath: string): Promise<void> {
179180
}
180181

181182
// The custom logo needs a custom stylesheet [ref:doc_global_styling],
182-
// so it must be enabled conditionally. The `doc` Cargo feature is used
183-
// to toggle this. [tag:doc_feature]
183+
// so it must be disabled conditionally if the custom stylesheet isn't
184+
// applied globally. The `doc` Cargo feature is used to toggle this.
185+
// [tag:doc_feature] In summary, there are two cases we consider:
184186
//
185-
// This means that the appearance will be degraded when `doc` is used
186-
// alone without the appropriate stylesheet, but this can only happen
187-
// in "unofficial" documentation builds.
187+
// - In official documentation builds (docs.rs and our API
188+
// documentation website), the `doc` Cargo feature is enabled, and
189+
// the custom stylesheet is applied globally using `RUSTDOCFLAGS`.
190+
// The result is a custom logo styled consistently.
191+
//
192+
// - In unofficial documentation builds (e.g., `cargo doc` in
193+
// downstream workspaces), the `doc` Cargo feature is disabled by
194+
// default, and the custom stylesheet is not applied globally. The
195+
// custom logo is disabled by `cfg_attr` in this case.
196+
//
197+
// We don't handle the cases where `doc` is enabled in unofficial
198+
// builds. In such cases, the custom logo will be styled properly or
199+
// improperly depending on whether `common.md` is included by `#[doc =
200+
// ...]` in that file.
188201
if (publish) {
189202
if (typeof features.doc == "undefined") {
190203
logger.error(`${crateRelPath}: features.doc is not present.`);

0 commit comments

Comments
 (0)