Skip to content

Commit 3d1c4f3

Browse files
Filmbostock
andauthored
add docs-test (#1688)
* add docs-test from d3/d3#3673 fix a few malformed links; the automatic renumbering of duplicate links with -1 seems fragile, so we make them explicit rather than making the link checker smarter. * ignore links to plot.js * apply changes from d3/d3#3673 * ignore .js files (a link to plot.js) --------- Co-authored-by: Mike Bostock <[email protected]>
1 parent 2d4a2f6 commit 3d1c4f3

File tree

9 files changed

+87
-13
lines changed

9 files changed

+87
-13
lines changed

docs/features/scales.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1013,7 +1013,7 @@ If the input channel is *data*, then the reducer is passed groups of the mark’
10131013

10141014
Note: when the value of the sort option is a string or a function, it is interpreted as a mark [sort transform](../transforms/sort.md). To use both sort options and a mark sort transform, use [Plot.sort](../transforms/sort.md#sort-order-options).
10151015

1016-
## scale(*options*)
1016+
## scale(*options*) {#scale-options-1}
10171017

10181018
You can also create a standalone scale with Plot.**scale**(*options*). The *options* object must define at least one scale; see [Scale options](#scale-options) for how to define a scale. For example, here is a linear color scale with the default domain of [0, 1] and default scheme *turbo*:
10191019

docs/interactions/pointer.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ To resolve the horizontal target position, the pointer transform applies the fol
180180

181181
The same precedence applies to the **py**, **y**, **y1**, and **y2** channels.
182182

183-
## pointer(*options*)
183+
## pointer(*options*) {#pointer-options-1}
184184

185185
```js
186186
Plot.tip(penguins, Plot.pointer({x: "culmen_length_mm", y: "culmen_depth_mm"}))

docs/marks/bar.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ The following optional channels are supported:
232232

233233
If neither the **x1** nor **x2** option is specified, the **x** option may be specified as shorthand to apply an implicit [stackX transform](../transforms/stack.md); this is the typical configuration for a horizontal bar chart with bars aligned at *x* = 0. If the **x** option is not specified, it defaults to [identity](../features/transforms.md#identity). If *options* is undefined, then it defaults to **x2** as identity and **y** as the zero-based index [0, 1, 2, …]; this allows an array of numbers to be passed to barX to make a quick sequential bar chart. If the **y** channel is not specified, the bar will span the full vertical extent of the plot (or facet).
234234

235-
If an **interval** is specified, such as d3.utcDay, **x1** and **x2** can be derived from **x**: *interval*.floor(*x*) is invoked for each *x* to produce *x1*, and *interval*.offset(*x1*) is invoked for each *x1* to produce *x2*. If the interval is specified as a number *n*, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
235+
If an **interval** is specified, such as d3.utcDay, **x1** and **x2** can be derived from **x**: *interval*.floor(*x*) is invoked for each *x* to produce *x1*, and *interval*.offset(*x1*) is invoked for each *x1* to produce *x2*. If the interval is specified as a number *n*, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
236236

237237
## barY(*data*, *options*)
238238

@@ -251,4 +251,4 @@ The following optional channels are supported:
251251

252252
If neither the **y1** nor **y2** option is specified, the **y** option may be specified as shorthand to apply an implicit [stackY transform](../transforms/stack.md); this is the typical configuration for a vertical bar chart with bars aligned at *y* = 0. If the **y** option is not specified, it defaults to [identity](../features/transforms.md#identity). If *options* is undefined, then it defaults to **y2** as identity and **x** as the zero-based index [0, 1, 2, …]; this allows an array of numbers to be passed to barY to make a quick sequential bar chart. If the **x** channel is not specified, the bar will span the full horizontal extent of the plot (or facet).
253253

254-
If an **interval** is specified, such as d3.utcDay, **y1** and **y2** can be derived from **y**: *interval*.floor(*y*) is invoked for each *y* to produce *y1*, and *interval*.offset(*y1*) is invoked for each *y1* to produce *y2*. If the interval is specified as a number *n*, *y1* and *y2* are taken as the two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
254+
If an **interval** is specified, such as d3.utcDay, **y1** and **y2** can be derived from **y**: *interval*.floor(*y*) is invoked for each *y* to produce *y1*, and *interval*.offset(*y1*) is invoked for each *y1* to produce *y2*. If the interval is specified as a number *n*, *y1* and *y2* are taken as the two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).

docs/marks/dot.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ Plot.dotX(cars.map((d) => d["economy (mpg)"]))
349349

350350
Equivalent to [dot](#dot-data-options) except that if the **x** option is not specified, it defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
351351

352-
If an **interval** is specified, such as d3.utcDay, **y** is transformed to (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If the interval is specified as a number *n*, *y* will be the midpoint of two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
352+
If an **interval** is specified, such as d3.utcDay, **y** is transformed to (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If the interval is specified as a number *n*, *y* will be the midpoint of two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
353353

354354
## dotY(*data*, *options*)
355355

@@ -359,7 +359,7 @@ Plot.dotY(cars.map((d) => d["economy (mpg)"]))
359359

360360
Equivalent to [dot](#dot-data-options) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
361361

362-
If an **interval** is specified, such as d3.utcDay, **x** is transformed to (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If the interval is specified as a number *n*, *x* will be the midpoint of two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
362+
If an **interval** is specified, such as d3.utcDay, **x** is transformed to (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If the interval is specified as a number *n*, *x* will be the midpoint of two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
363363

364364
## circle(*data*, *options*)
365365

docs/marks/frame.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ The frame mark supports the [standard mark options](../features/marks.md#mark-op
115115

116116
If the **anchor** option is specified as one of *left*, *right*, *top*, or *bottom*, that side is rendered as a single line (and the **fill**, **fillOpacity**, **rx**, and **ry** options are ignored).
117117

118-
## frame(*options*)
118+
## frame(*options*) {#frame-options-1}
119119

120120
```js
121121
Plot.frame({stroke: "red"})

docs/marks/rect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ The following channels are optional:
201201

202202
Typically either **x1** and **x2** are specified, or **y1** and **y2**, or both.
203203

204-
If an **interval** is specified, such as d3.utcDay, **x1** and **x2** can be derived from **x**: *interval*.floor(*x*) is invoked for each **x** to produce **x1**, and *interval*.offset(*x1*) is invoked for each **x1** to produce **x2**. The same is true for **y**, **y1**, and **y2**, respectively. If the interval is specified as a number *n*, **x1** and **x2** are taken as the two consecutive multiples of *n* that bracket **x**. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
204+
If an **interval** is specified, such as d3.utcDay, **x1** and **x2** can be derived from **x**: *interval*.floor(*x*) is invoked for each **x** to produce **x1**, and *interval*.offset(*x1*) is invoked for each **x1** to produce **x2**. The same is true for **y**, **y1**, and **y2**, respectively. If the interval is specified as a number *n*, **x1** and **x2** are taken as the two consecutive multiples of *n* that bracket **x**. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
205205

206206
The rect mark supports the [standard mark options](../features/marks.md#mark-options), including insets and rounded corners. The **stroke** defaults to *none*. The **fill** defaults to *currentColor* if the stroke is *none*, and to *none* otherwise.
207207

docs/marks/rule.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ Plot.plot({
135135
```
136136
:::
137137

138-
Rules are also used by the [grid mark](./grid) to draw grid lines.
138+
Rules are also used by the [grid mark](./grid.md) to draw grid lines.
139139

140140
## Rule options
141141

@@ -160,7 +160,7 @@ If **x** is not specified, it defaults to [identity](../features/transforms.md#i
160160

161161
If **y** is specified, it is shorthand for **y2** with **y1** equal to zero; this is the typical configuration for a vertical lollipop chart with rules aligned at *y* = 0. If **y1** is not specified, the rule will start at the top of the plot (or facet). If **y2** is not specified, the rule will end at the bottom of the plot (or facet).
162162

163-
If an **interval** is specified, such as d3.utcDay, **y1** and **y2** can be derived from **y**: *interval*.floor(*y*) is invoked for each *y* to produce *y1*, and *interval*.offset(*y1*) is invoked for each *y1* to produce *y2*. If the interval is specified as a number *n*, *y1* and *y2* are taken as the two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
163+
If an **interval** is specified, such as d3.utcDay, **y1** and **y2** can be derived from **y**: *interval*.floor(*y*) is invoked for each *y* to produce *y1*, and *interval*.offset(*y1*) is invoked for each *y1* to produce *y2*. If the interval is specified as a number *n*, *y1* and *y2* are taken as the two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
164164

165165
## ruleY(*data*, *options*)
166166

@@ -181,4 +181,4 @@ If **y** is not specified, it defaults to [identity](../features/transforms.md#i
181181

182182
If **x** is specified, it is shorthand for **x2** with **x1** equal to zero; this is the typical configuration for a horizontal lollipop chart with rules aligned at *x* = 0. If **x1** is not specified, the rule will start at the left edge of the plot (or facet). If **x2** is not specified, the rule will end at the right edge of the plot (or facet).
183183

184-
If an **interval** is specified, such as d3.utcDay, **x1** and **x2** can be derived from **x**: *interval*.floor(*x*) is invoked for each *x* to produce *x1*, and *interval*.offset(*x1*) is invoked for each *x1* to produce *x2*. If the interval is specified as a number *n*, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
184+
If an **interval** is specified, such as d3.utcDay, **x1** and **x2** can be derived from **x**: *interval*.floor(*x*) is invoked for each *x* to produce *x1*, and *interval*.offset(*x1*) is invoked for each *x1* to produce *x2*. If the interval is specified as a number *n*, *x1* and *x2* are taken as the two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).

docs/marks/text.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ Plot.textX(alphabet.map((d) => d.frequency))
256256

257257
Equivalent to [text](#text-data-options), except **x** defaults to [identity](../features/transforms.md#identity) and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
258258

259-
If an **interval** is specified, such as d3.utcDay, **y** is transformed to (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If the interval is specified as a number *n*, *y* will be the midpoint of two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
259+
If an **interval** is specified, such as d3.utcDay, **y** is transformed to (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If the interval is specified as a number *n*, *y* will be the midpoint of two consecutive multiples of *n* that bracket *y*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).
260260

261261
## textY(*data*, *options*)
262262

@@ -266,4 +266,4 @@ Plot.textY(alphabet.map((d) => d.frequency))
266266

267267
Equivalent to [text](#text-data-options), except **y** defaults to [identity](../features/transforms.md#identity) and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
268268

269-
If an **interval** is specified, such as d3.utcDay, **x** is transformed to (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If the interval is specified as a number *n*, *x* will be the midpoint of two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales#scale-options).
269+
If an **interval** is specified, such as d3.utcDay, **x** is transformed to (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If the interval is specified as a number *n*, *x* will be the midpoint of two consecutive multiples of *n* that bracket *x*. Named UTC intervals such as *day* are also supported; see [scale options](../features/scales.md#scale-options).

test/docs-test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import assert from "assert";
2+
import {readdir, readFile, stat} from "fs/promises";
3+
4+
it("documentation links point to existing internal anchors", async () => {
5+
const root = "docs";
6+
7+
// Crawl all files, read their links and anchors.
8+
const anchors = new Map();
9+
const links = [];
10+
for await (const file of readMarkdownFiles(root)) {
11+
const text = await readMarkdownSource(root + file);
12+
anchors.set(file, getAnchors(text));
13+
for (const {pathname, hash} of getLinks(file, text)) {
14+
links.push({source: file, target: pathname, hash});
15+
}
16+
}
17+
18+
// Check for broken links.
19+
let errors = [];
20+
for (let {source, target, hash} of links) {
21+
if (!target.endsWith(".md")) {
22+
errors.push(`- ${source} points to ${target} instead of ${target}.md.`);
23+
target += ".md";
24+
}
25+
26+
if (!hash || anchors.get(target).includes(hash.slice(1))) continue;
27+
errors.push(`- ${source} points to missing ${target}${hash}.`);
28+
}
29+
assert(errors.length === 0, new Error(`${errors.length} broken links:\n${errors.join("\n")}`));
30+
});
31+
32+
// Anchors can be derived from headers, or explicitly written as {#names}.
33+
function getAnchors(text) {
34+
const anchors = [];
35+
for (const [, header] of text.matchAll(/^#+ ([*\w][*().,\w\d -]+)\n/gm)) {
36+
anchors.push(
37+
header
38+
.replaceAll(/[^\w\d\s]+/g, " ")
39+
.trim()
40+
.replaceAll(/ +/g, "-")
41+
.toLowerCase()
42+
);
43+
}
44+
for (const [, anchor] of text.matchAll(/ \{#([\w\d-]+)\}/g)) {
45+
anchors.push(anchor);
46+
}
47+
return anchors;
48+
}
49+
50+
// Internal links.
51+
function getLinks(file, text) {
52+
const links = [];
53+
for (const match of text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
54+
const [, link] = match;
55+
if (/^\w+:/.test(link)) continue; // absolute link with protocol
56+
const {pathname, hash} = new URL(link, new URL(file, "https://example.com/"));
57+
links.push({pathname, hash});
58+
}
59+
return links;
60+
}
61+
62+
// In source files, ignore comments.
63+
async function readMarkdownSource(f) {
64+
return (await readFile(f, "utf8")).replaceAll(/<!-- .*? -->/gs, "");
65+
}
66+
67+
// Recursively find all md files in the directory.
68+
async function* readMarkdownFiles(root, subpath = "/") {
69+
for (const fname of await readdir(root + subpath)) {
70+
if (fname.startsWith(".") || fname.endsWith(".js")) continue; // ignore .vitepress etc.
71+
if ((await stat(root + subpath + fname)).isDirectory()) yield* readMarkdownFiles(root, subpath + fname + "/");
72+
else if (fname.endsWith(".md")) yield subpath + fname;
73+
}
74+
}

0 commit comments

Comments
 (0)