|
| 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