Skip to content

Commit b8206ca

Browse files
committed
Update after feedback
- Rename the feature name - Use `merge_next`/`merge_previous` instead of documentation in documentation - Do not propagate tags anymore - Clarify the interaction with the various tags (like `compile_fail`, or `text`)
1 parent d7d6042 commit b8206ca

2 files changed

+387
-422
lines changed
Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
- Feature Name: code-block-continuation-in-documentation
2+
- Start Date: 2020-04-01
3+
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
4+
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
The goal is to improve the experience of displaying complex examples in the documentation.
10+
11+
The currently proposed solution is to add `merge_next` and `merge_previous` attributes to code-block.
12+
13+
Example:
14+
15+
```rust
16+
/// This is some documentation
17+
///
18+
/// ```merge_next
19+
/// // A comment inside a code block
20+
/// let some_code = 0;
21+
/// ```
22+
///
23+
/// A line rendered as *regular documentation*
24+
///
25+
/// ```merge_previous
26+
/// /// We can use variable declared in the first code-block
27+
/// let other_code = some_code;
28+
/// ```
29+
```
30+
31+
… would be rendered as:
32+
33+
`<<<<<<<<<<<<<<<<<<<<<<<<<<<<<`
34+
35+
This is some documentation
36+
37+
```rust
38+
// A comment inside a code block
39+
let some_code = 0;
40+
```
41+
42+
A line rendered as *regular documentation*
43+
44+
```rust
45+
/// We can use variable declared in the first code-block
46+
let other_code = some_code;
47+
```
48+
49+
`>>>>>>>>>>>>>>>>>>>>>>>>>>>>>`
50+
51+
When running `cargo test`, and if the documentation generates a link to the playground, or a `run me` button, two snippets would be generated.
52+
53+
The first contains only the code of the first one.
54+
55+
```rust
56+
// A comment inside a code block
57+
let some_code = 0;
58+
```
59+
60+
The second will contains the aggregate of both.
61+
62+
```rust
63+
// A comment inside a code block
64+
let some_code = 0;
65+
/// We can use variable declared in the first code-block
66+
let other_code = some_code;
67+
```
68+
69+
# Motivation
70+
[motivation]: #motivation
71+
72+
Currently, `cargo doc` generates a really nice documentation. However, if you have a complicated setup to explain, it can be quite difficult to express examples in a concise and maintainable way.
73+
74+
For example if you are working on a graph library, even a small example requires to create a graph. If you are documenting a function that have multiple use-cases it can become quickly an issue. Let's take a concrete example.
75+
76+
```rust
77+
fn dijkstra(
78+
graph: &Graph,
79+
start: Graph::Node,
80+
exit_condition: &dyn Fn(Graph::Node, Cost) -> bool
81+
edge_cost: &dyn Fn(Graph::Node) -> Cost
82+
) -> Hashmap<Graph::Node, Cost>;
83+
```
84+
85+
The user can change the behavior of the function in many ways. As a library writer, we would like to give examples of the major use-cases. Each of those examples will need to instantiate a graph. Since the dijkstra function doesn't modify the graph, and since if the content isn't related to the examples themselves, we may want to share the set-up between all the example.
86+
87+
The rendered documentation we may want to create could look like this:
88+
89+
`<<<<<<<<<<<<<<<<<<<<<<<<<<<<<`
90+
91+
Function
92+
93+
```rust
94+
fn dijkstra(
95+
graph: &Graph,
96+
start: Graph::Node,
97+
exit_condition: &dyn Fn(Graph::Node, Cost) -> bool
98+
edge_cost: &dyn Fn(Graph::Node) -> Cost
99+
) -> Hashmap<Graph::Node, Cost>;
100+
```
101+
102+
---
103+
104+
# Examples
105+
106+
## Set-up
107+
108+
```rust
109+
use Graph;
110+
use dijkstra;
111+
use std::collections::HashMap;
112+
113+
let mut graph = Graph::new();
114+
let a = graph.add_node();
115+
let b = graph.add_node();
116+
let c = graph.add_node();
117+
118+
// z will be in another connected component
119+
let z = graph.add_node();
120+
121+
graph.extend_with_edges(&[
122+
(a, b),
123+
(b, c),
124+
(c, d),
125+
(d, a),
126+
]);
127+
128+
// a ----> b z (not connected)
129+
// ^ |
130+
// | v
131+
// d <---- c
132+
```
133+
134+
## Basic usage
135+
136+
Compute the distances to all nodes in the graph from `a`.
137+
138+
```rust
139+
let distances = dijkstra(
140+
graph,
141+
a,
142+
&|_node, _total_distance| -> false,
143+
&|_edge| -> 1,
144+
);
145+
```
146+
147+
## Early stopping
148+
149+
Stops the algorithm if a given number of nodes have been reached.
150+
151+
```rust
152+
let distances = dijkstra(
153+
graph,
154+
a,
155+
&|_node, total_distance| -> total_distance > 3,
156+
&|_edge| -> 1,
157+
);
158+
```
159+
160+
… (more examples)
161+
162+
163+
`>>>>>>>>>>>>>>>>>>>>>>>>>>>>>`
164+
165+
As you can see, and even if the setup is quite trivial, it takes quite a lot of lines to write. And even if is trivial, it is required for each examples to compile.
166+
167+
So how can we currently create such kind of documentation?
168+
169+
- we can duplicate the set-up for each code-block, and add a `#` before each line
170+
- use a macro (declared in #[cfg(test)] mod tests { } modules in the sources, and prefixed by `#` in the documentation) at the beginning of each code-block to remove the duplication
171+
- use a single code-block, and use comments inside the block
172+
173+
All of those options are far from optimal.
174+
175+
- Duplicating the set-up code means more maintenance.
176+
- Creating a macro for such a trivial task seems strange (and the content of the macro would still need to be duplicated in the set-up part of the documentation).
177+
- By having a single code-block, the documentation (especially the titles) will not be rendered nicely (as a comment instead of markdown).
178+
179+
Having a way to way to render documentation inside code-block inside documentation solves this dilemma.
180+
181+
# Guide-level explanation
182+
[guide-level-explanation]: #guide-level-explanation
183+
184+
When documenting a complex code-block, you can merge multiple block by adding `merge_next` to the first parts, and `merge_previous` to the next one. It can be done multiple time (to split a code-block in 3 or more parts). In between those code-block, any regular items can be used, including other code-blocks.
185+
186+
As shown in the summary, this example demonstrate how this feature can be used.
187+
188+
```rust
189+
/// This is some documentation
190+
///
191+
/// ```merge_next
192+
/// // A comment inside a code block
193+
/// let some_code = 0;
194+
/// ```
195+
///
196+
/// A line rendered as *regular documentation*
197+
///
198+
/// ```merge_previous
199+
/// /// We can use variable declared in the first code-block
200+
/// let other_code = some_code;
201+
/// ```
202+
```
203+
204+
You can use this feature to add some high level explanations in the middle of an code block. If you are familiar with [jupyter notebook](https://jupyter.org/) ([link to the rust kernel](https://github.com/google/evcxr/tree/master/evcxr_jupyter)), it is similar to use markdown block in between your executable code. They provide an easy way to have a nicely rendered multi-part explanation.
205+
206+
---
207+
208+
Both `merge_next` and `merge_previous` implies `rust`. As such they are imcompatible with tags like `text`. The tag `rust` can be used explicitely but isn't required.
209+
210+
---
211+
212+
Each code-block with a `merge_next` tag must have another `merge_previous` block before the end of the current documentation block, and vice versa.
213+
214+
You can't write …
215+
216+
```rust
217+
/// ```merge_next
218+
/// let some_statement = 0;
219+
/// ```
220+
/// No `merge_next` block will follow
221+
fn foo();
222+
```
223+
224+
… nor …
225+
226+
```rust
227+
/// No `merge_previous` block before this one
228+
/// ```merge_previous
229+
/// let some_statement = 0;
230+
/// ```
231+
fn foo();
232+
```
233+
234+
---
235+
236+
All code-blocks are independents, and therefore can have different tags (like `compile_fail` or `ignore`). This also means that a tag may need to be repeated multiple times (like `edition`).
237+
238+
```rust
239+
/// ```merge_next,compile_fail
240+
/// match "some_string" {
241+
/// "some_string" => "true",
242+
/// ```
243+
///
244+
/// The statement is split between two code-blocks. The first one will fail to
245+
/// compile (but shouldn't be considered an error), so a `compile_fail` tag is
246+
/// needed.
247+
///
248+
/// The expected use-case for this construction is gives some explanations in
249+
/// the middle of a complex statement.
250+
///
251+
/// ```merge_previous
252+
/// _ => "impossible",
253+
/// ```
254+
/// The second code-block doesn't need `compile_fail` since it will contains the
255+
/// aggregate of both parts, and thus will generates a valid snippet.
256+
```
257+
258+
---
259+
260+
Code block can be split in more than two parts.
261+
262+
```rust
263+
/// ```merge_next
264+
/// let part_0 = 0;
265+
/// ```
266+
/// A first line of documentation…
267+
/// ```merge_previous,merge_next
268+
/// let part_1 = part_0;
269+
/// ```merge_previous
270+
/// … and a second
271+
/// let part_2a = part_0;
272+
/// let part_2b = part_1;
273+
/// ```
274+
```
275+
276+
---
277+
278+
It is possible to have complex use case, like displaying other code-blocks (even
279+
rust code even it shouldn't be recommended).
280+
281+
```rust
282+
/// The following code-block is the first of a 3 part snippet.
283+
///
284+
/// ```merge_next
285+
/// // A comment inside a code block
286+
/// let from_first_block = 0;
287+
/// ```
288+
///
289+
/// You can use any kind of documentation, like another code-block.
290+
///
291+
/// ```text
292+
/// Some verbatim text
293+
/// ```
294+
///
295+
/// The next code-block is the second part of the snippet. It need both to
296+
/// explicitely be merged to the previous and the next one.
297+
///
298+
/// ```merge_previous,merge_next
299+
/// /// We can use variable declared in the first code-block
300+
/// let from_second_block = from_first_block;
301+
/// ```
302+
///
303+
/// It is even possible to use regular rust code-block in between a multi-parts
304+
/// snippet.
305+
///
306+
/// ```rust
307+
/// let some_unrelated_rust_code = 1;
308+
/// // You can't use `from_first_block` or `from_second_block` here.
309+
/// ```
310+
///
311+
/// And finally the last part.
312+
///
313+
/// ```merge_previous
314+
/// /// We can use variable declared in the first or second code-block
315+
/// let in_third_block_a = from_first_block;
316+
/// let in_third_block_b = from_second_block;
317+
/// ```
318+
///
319+
/// The following multi-parts snippet isn't merged with the previous
320+
///
321+
/// ```merge_next
322+
/// let in_part_4 = 0
323+
/// // You can't use any of `from_first_block`, `from_second_block`,
324+
/// // `in_third_block_a` and `in_third_block_b` here.
325+
/// ```
326+
///
327+
/// And finally a last snippet
328+
///
329+
/// ```merge_previous
330+
/// let in_part_5 = in_part_5;
331+
/// ```
332+
```
333+
334+
# Reference-level explanation
335+
[reference-level-explanation]: #reference-level-explanation
336+
337+
The currently proposed solution is to add `merge_next` and `merge_previous` attributes to code-block.
338+
339+
A explained above, the following parts will have to be modified.
340+
341+
- When running `cargo test`, and if the documentation engine. generates a link to the playground, or a `run me` button, one snippet will be generated by code-block (like usual), but each subsequent snippet will also contains the previous ones.
342+
- When running `cargo doc` (or similar tools), the generated html should be displayed using the normal markdown engine, as if the code-blocks had the `rust` attributes.
343+
344+
# Drawbacks
345+
[drawbacks]: #drawbacks
346+
347+
It make things more complicated to parse, as explained in the section above.
348+
349+
# Rationale and alternatives
350+
[rationale-and-alternatives]: #rationale-and-alternatives
351+
352+
Here is a list of possible alternative:
353+
354+
- Have a closing tag, instead of and opening tags to merge multiple code-blocks
355+
356+
```rust
357+
/// ```
358+
/// let some_code = 0;
359+
/// ```pause
360+
/// Some documentation
361+
/// ```continue
362+
/// let other_code = some_code;
363+
/// ```
364+
```
365+
366+
It is more natural, but may need more modifications in the parser itself. It would also make it non standard (see example 15 of the commonmark [documentation](https://spec.commonmark.org/0.28/#fenced-code-blocks)).
367+
368+
- Display unit tests in the documentation (possibly with an attribute to be able to opt-in).
369+
370+
I think both approach can be implemented, and they complement each other.
371+
372+
# Prior art
373+
[prior-art]: #prior-art
374+
375+
This proposition allow to document your code a bit like what you would do with a [jupyter notebook](https://jupyter.org/) ([link to the rust kernel](https://github.com/google/evcxr/tree/master/evcxr_jupyter)). I personally think that jupyter are a nice way to illustrate how your code should be used. As far as I understand it enables [literate programming](https://en.wikipedia.org/wiki/Literate_programming).
376+
377+
# Unresolved questions
378+
[unresolved-questions]: #unresolved-questions
379+
380+
None for the moment.
381+
382+
# Future possibilities
383+
[future-possibilities]: #future-possibilities
384+
385+
- As explained above, and in addition/instead of the current proposition, I think it should be possible to render unit-tests (probably behind a `expand` button) in the documentation.
386+
387+
- If we ever support testing more languages than just `rust` (like a snippet in `C`), then `merge_next`/`merge_previous` should become compatible with more than just `rust`, and extend to any supported languages.

0 commit comments

Comments
 (0)