1 // Copyright © SixtyFPS GmbH <info@sixtyfps.io>
2 // SPDX-License-Identifier: MIT OR Apache-2.0
3
4 /*!
5 Document your crate's feature flags.
6
7 This crates provides a macro that extracts "documentation" comments from Cargo.toml
8
9 To use this crate, add `#![doc = document_features::document_features!()]` in your crate documentation.
10 The `document_features!()` macro reads your `Cargo.toml` file, extracts feature comments and generates
11 a markdown string for your documentation.
12
13 Basic example:
14
15 ```rust
16 //! Normal crate documentation goes here.
17 //!
18 //! ## Feature flags
19 #![doc = document_features::document_features!()]
20
21 // rest of the crate goes here.
22 ```
23
24 ## Documentation format:
25
26 The documentation of your crate features goes into `Cargo.toml`, where they are defined.
27
28 The `document_features!()` macro analyzes the contents of `Cargo.toml`.
29 Similar to Rust's documentation comments `///` and `//!`, the macro understands
30 comments that start with `## ` and `#! `. Note the required trailing space.
31 Lines starting with `###` will not be understood as doc comment.
32
33 `## ` comments are meant to be *above* the feature they document.
34 There can be several `## ` comments, but they must always be followed by a
35 feature name or an optional dependency.
36 There should not be `#! ` comments between the comment and the feature they document.
37
38 `#! ` comments are not associated with a particular feature, and will be printed
39 in where they occur. Use them to group features, for example.
40
41 ## Examples:
42
43 */
44 // Note: because rustdoc escapes the first `#` of a line starting with `#`,
45 // these docs comments have one more `#` ,
46 #![doc = self_test!(/**
47 [package]
48 name = "..."
49 ## ...
50
51 [features]
52 default = ["foo"]
53 ##! This comments goes on top
54
55 ### The foo feature enables the `foo` functions
56 foo = []
57
58 ### The bar feature enables the bar module
59 bar = []
60
61 ##! ### Experimental features
62 ##! The following features are experimental
63
64 ### Enable the fusion reactor
65 ###
66 ### ⚠️ Can lead to explosions
67 fusion = []
68
69 [dependencies]
70 document-features = "0.2"
71
72 ##! ### Optional dependencies
73
74 ### Enable this feature to implement the trait for the types from the genial crate
75 genial = { version = "0.2", optional = true }
76
77 ### This awesome dependency is specified in its own table
78 [dependencies.awesome]
79 version = "1.3.5"
80 optional = true
81 */
82 =>
83 /**
84 This comments goes on top
85 * **`foo`** *(enabled by default)* — The foo feature enables the `foo` functions
86 * **`bar`** — The bar feature enables the bar module
87
88 #### Experimental features
89 The following features are experimental
90 * **`fusion`** — Enable the fusion reactor
91
92 ⚠️ Can lead to explosions
93
94 #### Optional dependencies
95 * **`genial`** — Enable this feature to implement the trait for the types from the genial crate
96 * **`awesome`** — This awesome dependency is specified in its own table
97 */
98 )]
99 /*!
100
101 ## Customization
102
103 You can customize the formatting of the features in the generated documentation by setting
104 the key **`feature_label=`** to a given format string. This format string must be either
105 a [string literal](https://doc.rust-lang.org/reference/tokens.html#string-literals) or
106 a [raw string literal](https://doc.rust-lang.org/reference/tokens.html#raw-string-literals).
107 Every occurrence of `{feature}` inside the format string will be substituted with the name of the feature.
108
109 For instance, to emulate the HTML formatting used by `rustdoc` one can use the following:
110
111 ```rust
112 #![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
113 ```
114
115 The default formatting is equivalent to:
116
117 ```rust
118 #![doc = document_features::document_features!(feature_label = "**`{feature}`**")]
119 ```
120
121 ## Compatibility
122
123 The minimum Rust version required to use this crate is Rust 1.54 because of the
124 feature to have macro in doc comments. You can make this crate optional and use
125 `#[cfg_attr()]` statements to enable it only when building the documentation:
126 You need to have two levels of `cfg_attr` because Rust < 1.54 doesn't parse the attribute
127 otherwise.
128
129 ```rust,ignore
130 #![cfg_attr(
131 feature = "document-features",
132 cfg_attr(doc, doc = ::document_features::document_features!())
133 )]
134 ```
135
136 In your Cargo.toml, enable this feature while generating the documentation on docs.rs:
137
138 ```toml
139 [dependencies]
140 document-features = { version = "0.2", optional = true }
141
142 [package.metadata.docs.rs]
143 features = ["document-features"]
144 ## Alternative: enable all features so they are all documented
145 ## all-features = true
146 ```
147 */
148
149 #[cfg(not(feature = "default"))]
150 compile_error!(
151 "The feature `default` must be enabled to ensure \
152 forward compatibility with future version of this crate"
153 );
154
155 extern crate proc_macro;
156
157 use proc_macro::{TokenStream, TokenTree};
158 use std::borrow::Cow;
159 use std::collections::HashSet;
160 use std::convert::TryFrom;
161 use std::fmt::Write;
162 use std::path::Path;
163 use std::str::FromStr;
164
error(e: &str) -> TokenStream165 fn error(e: &str) -> TokenStream {
166 TokenStream::from_str(&format!("::core::compile_error!{{\"{}\"}}", e.escape_default())).unwrap()
167 }
168
compile_error(msg: &str, tt: Option<TokenTree>) -> TokenStream169 fn compile_error(msg: &str, tt: Option<TokenTree>) -> TokenStream {
170 let span = tt.as_ref().map_or_else(proc_macro::Span::call_site, TokenTree::span);
171 use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing};
172 use std::iter::FromIterator;
173 TokenStream::from_iter(vec![
174 TokenTree::Ident(Ident::new("compile_error", span)),
175 TokenTree::Punct({
176 let mut punct = Punct::new('!', Spacing::Alone);
177 punct.set_span(span);
178 punct
179 }),
180 TokenTree::Group({
181 let mut group = Group::new(Delimiter::Brace, {
182 TokenStream::from_iter([TokenTree::Literal({
183 let mut string = Literal::string(msg);
184 string.set_span(span);
185 string
186 })])
187 });
188 group.set_span(span);
189 group
190 }),
191 ])
192 }
193
194 #[derive(Default)]
195 struct Args {
196 feature_label: Option<String>,
197 }
198
parse_args(input: TokenStream) -> Result<Args, TokenStream>199 fn parse_args(input: TokenStream) -> Result<Args, TokenStream> {
200 let mut token_trees = input.into_iter().fuse();
201
202 // parse the key, ensuring that it is the identifier `feature_label`
203 match token_trees.next() {
204 None => return Ok(Args::default()),
205 Some(TokenTree::Ident(ident)) if ident.to_string() == "feature_label" => (),
206 tt => return Err(compile_error("expected `feature_label`", tt)),
207 }
208
209 // parse a single equal sign `=`
210 match token_trees.next() {
211 Some(TokenTree::Punct(p)) if p.as_char() == '=' => (),
212 tt => return Err(compile_error("expected `=`", tt)),
213 }
214
215 // parse the value, ensuring that it is a string literal containing the substring `"{feature}"`
216 let feature_label;
217 if let Some(tt) = token_trees.next() {
218 match litrs::StringLit::<String>::try_from(&tt) {
219 Ok(string_lit) if string_lit.value().contains("{feature}") => {
220 feature_label = string_lit.value().to_string()
221 }
222 _ => {
223 return Err(compile_error(
224 "expected a string literal containing the substring \"{feature}\"",
225 Some(tt),
226 ))
227 }
228 }
229 } else {
230 return Err(compile_error(
231 "expected a string literal containing the substring \"{feature}\"",
232 None,
233 ));
234 }
235
236 // ensure there is nothing left after the format string
237 if let tt @ Some(_) = token_trees.next() {
238 return Err(compile_error("unexpected token after the format string", tt));
239 }
240
241 Ok(Args { feature_label: Some(feature_label) })
242 }
243
244 /// Produce a literal string containing documentation extracted from Cargo.toml
245 ///
246 /// See the [crate] documentation for details
247 #[proc_macro]
document_features(tokens: TokenStream) -> TokenStream248 pub fn document_features(tokens: TokenStream) -> TokenStream {
249 parse_args(tokens)
250 .and_then(|args| document_features_impl(&args))
251 .unwrap_or_else(std::convert::identity)
252 }
253
document_features_impl(args: &Args) -> Result<TokenStream, TokenStream>254 fn document_features_impl(args: &Args) -> Result<TokenStream, TokenStream> {
255 let path = std::env::var("CARGO_MANIFEST_DIR").unwrap();
256 let mut cargo_toml = std::fs::read_to_string(Path::new(&path).join("Cargo.toml"))
257 .map_err(|e| error(&format!("Can't open Cargo.toml: {:?}", e)))?;
258
259 if !cargo_toml.contains("\n##") && !cargo_toml.contains("\n#!") {
260 // On crates.io, Cargo.toml is usually "normalized" and stripped of all comments.
261 // The original Cargo.toml has been renamed Cargo.toml.orig
262 if let Ok(orig) = std::fs::read_to_string(Path::new(&path).join("Cargo.toml.orig")) {
263 if orig.contains("##") || orig.contains("#!") {
264 cargo_toml = orig;
265 }
266 }
267 }
268
269 let result = process_toml(&cargo_toml, args).map_err(|e| error(&e))?;
270 Ok(std::iter::once(proc_macro::TokenTree::from(proc_macro::Literal::string(&result))).collect())
271 }
272
process_toml(cargo_toml: &str, args: &Args) -> Result<String, String>273 fn process_toml(cargo_toml: &str, args: &Args) -> Result<String, String> {
274 // Get all lines between the "[features]" and the next block
275 let mut lines = cargo_toml
276 .lines()
277 .map(str::trim)
278 // and skip empty lines and comments that are not docs comments
279 .filter(|l| {
280 !l.is_empty() && (!l.starts_with('#') || l.starts_with("##") || l.starts_with("#!"))
281 });
282 let mut top_comment = String::new();
283 let mut current_comment = String::new();
284 let mut features = vec![];
285 let mut default_features = HashSet::new();
286 let mut current_table = "";
287 while let Some(line) = lines.next() {
288 if let Some(x) = line.strip_prefix("#!") {
289 if !x.is_empty() && !x.starts_with(' ') {
290 continue; // it's not a doc comment
291 }
292 if !current_comment.is_empty() {
293 return Err("Cannot mix ## and #! comments between features.".into());
294 }
295 if top_comment.is_empty() && !features.is_empty() {
296 top_comment = "\n".into();
297 }
298 writeln!(top_comment, "{}", x).unwrap();
299 } else if let Some(x) = line.strip_prefix("##") {
300 if !x.is_empty() && !x.starts_with(' ') {
301 continue; // it's not a doc comment
302 }
303 writeln!(current_comment, " {}", x).unwrap();
304 } else if let Some(table) = line.strip_prefix('[') {
305 current_table = table
306 .split_once(']')
307 .map(|(t, _)| t.trim())
308 .ok_or_else(|| format!("Parse error while parsing line: {}", line))?;
309 if !current_comment.is_empty() {
310 let dep = current_table
311 .rsplit_once('.')
312 .and_then(|(table, dep)| table.trim().ends_with("dependencies").then(|| dep))
313 .ok_or_else(|| format!("Not a feature: `{}`", line))?;
314 features.push((
315 dep.trim(),
316 std::mem::take(&mut top_comment),
317 std::mem::take(&mut current_comment),
318 ));
319 }
320 } else if let Some((dep, rest)) = line.split_once('=') {
321 let dep = dep.trim().trim_matches('"');
322 let rest = get_balanced(rest, &mut lines)
323 .map_err(|e| format!("Parse error while parsing value {}: {}", dep, e))?;
324 if current_table == "features" && dep == "default" {
325 let defaults = rest
326 .trim()
327 .strip_prefix('[')
328 .and_then(|r| r.strip_suffix(']'))
329 .ok_or_else(|| format!("Parse error while parsing dependency {}", dep))?
330 .split(',')
331 .map(|d| d.trim().trim_matches(|c| c == '"' || c == '\'').trim().to_string())
332 .filter(|d| !d.is_empty());
333 default_features.extend(defaults);
334 }
335 if !current_comment.is_empty() {
336 if current_table.ends_with("dependencies") {
337 if !rest
338 .split_once("optional")
339 .and_then(|(_, r)| r.trim().strip_prefix('='))
340 .map_or(false, |r| r.trim().starts_with("true"))
341 {
342 return Err(format!("Dependency {} is not an optional dependency", dep));
343 }
344 } else if current_table != "features" {
345 return Err(format!(
346 r#"Comment cannot be associated with a feature: "{}""#,
347 current_comment.trim()
348 ));
349 }
350 features.push((
351 dep,
352 std::mem::take(&mut top_comment),
353 std::mem::take(&mut current_comment),
354 ));
355 }
356 }
357 }
358 if !current_comment.is_empty() {
359 return Err("Found comment not associated with a feature".into());
360 }
361 if features.is_empty() {
362 return Ok("*No documented features in Cargo.toml*".into());
363 }
364 let mut result = String::new();
365 for (f, top, comment) in features {
366 let default = if default_features.contains(f) { " *(enabled by default)*" } else { "" };
367 if !comment.trim().is_empty() {
368 if let Some(feature_label) = &args.feature_label {
369 writeln!(
370 result,
371 "{}* {}{} —{}",
372 top,
373 feature_label.replace("{feature}", f),
374 default,
375 comment.trim_end(),
376 )
377 .unwrap();
378 } else {
379 writeln!(result, "{}* **`{}`**{} —{}", top, f, default, comment.trim_end())
380 .unwrap();
381 }
382 } else if let Some(feature_label) = &args.feature_label {
383 writeln!(result, "{}* {}{}", top, feature_label.replace("{feature}", f), default,)
384 .unwrap();
385 } else {
386 writeln!(result, "{}* **`{}`**{}", top, f, default).unwrap();
387 }
388 }
389 result += &top_comment;
390 Ok(result)
391 }
392
get_balanced<'a>( first_line: &'a str, lines: &mut impl Iterator<Item = &'a str>, ) -> Result<Cow<'a, str>, String>393 fn get_balanced<'a>(
394 first_line: &'a str,
395 lines: &mut impl Iterator<Item = &'a str>,
396 ) -> Result<Cow<'a, str>, String> {
397 let mut line = first_line;
398 let mut result = Cow::from("");
399
400 let mut in_quote = false;
401 let mut level = 0;
402 loop {
403 let mut last_slash = false;
404 for (idx, b) in line.as_bytes().iter().enumerate() {
405 if last_slash {
406 last_slash = false
407 } else if in_quote {
408 match b {
409 b'\\' => last_slash = true,
410 b'"' | b'\'' => in_quote = false,
411 _ => (),
412 }
413 } else {
414 match b {
415 b'\\' => last_slash = true,
416 b'"' => in_quote = true,
417 b'{' | b'[' => level += 1,
418 b'}' | b']' if level == 0 => return Err("unbalanced source".into()),
419 b'}' | b']' => level -= 1,
420 b'#' => {
421 line = &line[..idx];
422 break;
423 }
424 _ => (),
425 }
426 }
427 }
428 if result.len() == 0 {
429 result = Cow::from(line);
430 } else {
431 *result.to_mut() += line;
432 }
433 if level == 0 {
434 return Ok(result);
435 }
436 line = if let Some(l) = lines.next() {
437 l
438 } else {
439 return Err("unbalanced source".into());
440 };
441 }
442 }
443
444 #[test]
test_get_balanced()445 fn test_get_balanced() {
446 assert_eq!(
447 get_balanced(
448 "{",
449 &mut IntoIterator::into_iter(["a", "{ abc[], #ignore", " def }", "}", "xxx"])
450 ),
451 Ok("{a{ abc[], def }}".into())
452 );
453 assert_eq!(
454 get_balanced("{ foo = \"{#\" } #ignore", &mut IntoIterator::into_iter(["xxx"])),
455 Ok("{ foo = \"{#\" } ".into())
456 );
457 assert_eq!(
458 get_balanced("]", &mut IntoIterator::into_iter(["["])),
459 Err("unbalanced source".into())
460 );
461 }
462
463 #[cfg(feature = "self-test")]
464 #[proc_macro]
465 #[doc(hidden)]
466 /// Helper macro for the tests. Do not use
self_test_helper(input: TokenStream) -> TokenStream467 pub fn self_test_helper(input: TokenStream) -> TokenStream {
468 process_toml((&input).to_string().trim_matches(|c| c == '"' || c == '#'), &Args::default())
469 .map_or_else(
470 |e| error(&e),
471 |r| {
472 std::iter::once(proc_macro::TokenTree::from(proc_macro::Literal::string(&r)))
473 .collect()
474 },
475 )
476 }
477
478 #[cfg(feature = "self-test")]
479 macro_rules! self_test {
480 (#[doc = $toml:literal] => #[doc = $md:literal]) => {
481 concat!(
482 "\n`````rust\n\
483 fn normalize_md(md : &str) -> String {
484 md.lines().skip_while(|l| l.is_empty()).map(|l| l.trim())
485 .collect::<Vec<_>>().join(\"\\n\")
486 }
487 assert_eq!(normalize_md(document_features::self_test_helper!(",
488 stringify!($toml),
489 ")), normalize_md(",
490 stringify!($md),
491 "));\n`````\n\n"
492 )
493 };
494 }
495
496 #[cfg(not(feature = "self-test"))]
497 macro_rules! self_test {
498 (#[doc = $toml:literal] => #[doc = $md:literal]) => {
499 concat!(
500 "This contents in Cargo.toml:\n`````toml",
501 $toml,
502 "\n`````\n Generates the following:\n\
503 <table><tr><th>Preview</th></tr><tr><td>\n\n",
504 $md,
505 "\n</td></tr></table>\n\n \n",
506 )
507 };
508 }
509
510 // The following struct is inserted only during generation of the documentation in order to exploit doc-tests.
511 // These doc-tests are used to check that invalid arguments to the `document_features!` macro cause a compile time error.
512 // For a more principled way of testing compilation error, maybe investigate <https://docs.rs/trybuild>.
513 //
514 /// ```rust
515 /// #![doc = document_features::document_features!()]
516 /// #![doc = document_features::document_features!(feature_label = "**`{feature}`**")]
517 /// #![doc = document_features::document_features!(feature_label = r"**`{feature}`**")]
518 /// #![doc = document_features::document_features!(feature_label = r#"**`{feature}`**"#)]
519 /// #![doc = document_features::document_features!(feature_label = "<span class=\"stab portability\"><code>{feature}</code></span>")]
520 /// #![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
521 /// ```
522 /// ```compile_fail
523 /// #![doc = document_features::document_features!(feature_label > "<span>{feature}</span>")]
524 /// ```
525 /// ```compile_fail
526 /// #![doc = document_features::document_features!(label = "<span>{feature}</span>")]
527 /// ```
528 /// ```compile_fail
529 /// #![doc = document_features::document_features!(feature_label = "{feat}")]
530 /// ```
531 /// ```compile_fail
532 /// #![doc = document_features::document_features!(feature_label = 3.14)]
533 /// ```
534 /// ```compile_fail
535 /// #![doc = document_features::document_features!(feature_label = )]
536 /// ```
537 /// ```compile_fail
538 /// #![doc = document_features::document_features!(feature_label = "**`{feature}`**" extra)]
539 /// ```
540 #[cfg(doc)]
541 struct FeatureLabelCompilationTest;
542
543 #[cfg(test)]
544 mod tests {
545 use super::{process_toml, Args};
546
547 #[track_caller]
test_error(toml: &str, expected: &str)548 fn test_error(toml: &str, expected: &str) {
549 let err = process_toml(toml, &Args::default()).unwrap_err();
550 assert!(err.contains(expected), "{:?} does not contain {:?}", err, expected)
551 }
552
553 #[test]
only_get_balanced_in_correct_table()554 fn only_get_balanced_in_correct_table() {
555 process_toml(
556 r#"
557
558 [package.metadata.release]
559 pre-release-replacements = [
560 {test=\"\#\# \"},
561 ]
562 [abcd]
563 [features]#xyz
564 #! abc
565 #
566 ###
567 #! def
568 #!
569 ## 123
570 ## 456
571 feat1 = ["plop"]
572 #! ghi
573 no_doc = []
574 ##
575 feat2 = ["momo"]
576 #! klm
577 default = ["feat1", "something_else"]
578 #! end
579 "#,
580 &Args::default(),
581 )
582 .unwrap();
583 }
584
585 #[test]
no_features()586 fn no_features() {
587 let r = process_toml(
588 r#"
589 [features]
590 [dependencies]
591 foo = 4;
592 "#,
593 &Args::default(),
594 )
595 .unwrap();
596 assert_eq!(r, "*No documented features in Cargo.toml*");
597 }
598
599 #[test]
no_features2()600 fn no_features2() {
601 let r = process_toml(
602 r#"
603 [packages]
604 [dependencies]
605 "#,
606 &Args::default(),
607 )
608 .unwrap();
609 assert_eq!(r, "*No documented features in Cargo.toml*");
610 }
611
612 #[test]
parse_error3()613 fn parse_error3() {
614 test_error(
615 r#"
616 [features]
617 ff = []
618 [abcd
619 efgh
620 [dependencies]
621 "#,
622 "Parse error while parsing line: [abcd",
623 );
624 }
625
626 #[test]
parse_error4()627 fn parse_error4() {
628 test_error(
629 r#"
630 [features]
631 ## dd
632 ## ff
633 #! ee
634 ## ff
635 "#,
636 "Cannot mix",
637 );
638 }
639
640 #[test]
parse_error5()641 fn parse_error5() {
642 test_error(
643 r#"
644 [features]
645 ## dd
646 "#,
647 "not associated with a feature",
648 );
649 }
650
651 #[test]
parse_error6()652 fn parse_error6() {
653 test_error(
654 r#"
655 [features]
656 # ff
657 foo = []
658 default = [
659 #ffff
660 # ff
661 "#,
662 "Parse error while parsing value default",
663 );
664 }
665
666 #[test]
parse_error7()667 fn parse_error7() {
668 test_error(
669 r#"
670 [features]
671 # f
672 foo = [ x = { ]
673 bar = []
674 "#,
675 "Parse error while parsing value foo",
676 );
677 }
678
679 #[test]
not_a_feature1()680 fn not_a_feature1() {
681 test_error(
682 r#"
683 ## hallo
684 [features]
685 "#,
686 "Not a feature: `[features]`",
687 );
688 }
689
690 #[test]
not_a_feature2()691 fn not_a_feature2() {
692 test_error(
693 r#"
694 [package]
695 ## hallo
696 foo = []
697 "#,
698 "Comment cannot be associated with a feature: \"hallo\"",
699 );
700 }
701
702 #[test]
non_optional_dep1()703 fn non_optional_dep1() {
704 test_error(
705 r#"
706 [dev-dependencies]
707 ## Not optional
708 foo = { version = "1.2", optional = false }
709 "#,
710 "Dependency foo is not an optional dependency",
711 );
712 }
713
714 #[test]
non_optional_dep2()715 fn non_optional_dep2() {
716 test_error(
717 r#"
718 [dev-dependencies]
719 ## Not optional
720 foo = { version = "1.2" }
721 "#,
722 "Dependency foo is not an optional dependency",
723 );
724 }
725
726 #[test]
basic()727 fn basic() {
728 let toml = r#"
729 [abcd]
730 [features]#xyz
731 #! abc
732 #
733 ###
734 #! def
735 #!
736 ## 123
737 ## 456
738 feat1 = ["plop"]
739 #! ghi
740 no_doc = []
741 ##
742 feat2 = ["momo"]
743 #! klm
744 default = ["feat1", "something_else"]
745 #! end
746 "#;
747 let parsed = process_toml(toml, &Args::default()).unwrap();
748 assert_eq!(
749 parsed,
750 " abc\n def\n\n* **`feat1`** *(enabled by default)* — 123\n 456\n\n ghi\n* **`feat2`**\n\n klm\n end\n"
751 );
752 let parsed = process_toml(
753 toml,
754 &Args {
755 feature_label: Some(
756 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
757 ),
758 },
759 )
760 .unwrap();
761 assert_eq!(
762 parsed,
763 " abc\n def\n\n* <span class=\"stab portability\"><code>feat1</code></span> *(enabled by default)* — 123\n 456\n\n ghi\n* <span class=\"stab portability\"><code>feat2</code></span>\n\n klm\n end\n"
764 );
765 }
766
767 #[test]
dependencies()768 fn dependencies() {
769 let toml = r#"
770 #! top
771 [dev-dependencies] #yo
772 ## dep1
773 dep1 = { version="1.2", optional=true}
774 #! yo
775 dep2 = "1.3"
776 ## dep3
777 [target.'cfg(unix)'.build-dependencies.dep3]
778 version = "42"
779 optional = true
780 "#;
781 let parsed = process_toml(toml, &Args::default()).unwrap();
782 assert_eq!(parsed, " top\n* **`dep1`** — dep1\n\n yo\n* **`dep3`** — dep3\n");
783 let parsed = process_toml(
784 toml,
785 &Args {
786 feature_label: Some(
787 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
788 ),
789 },
790 )
791 .unwrap();
792 assert_eq!(parsed, " top\n* <span class=\"stab portability\"><code>dep1</code></span> — dep1\n\n yo\n* <span class=\"stab portability\"><code>dep3</code></span> — dep3\n");
793 }
794
795 #[test]
multi_lines()796 fn multi_lines() {
797 let toml = r#"
798 [package.metadata.foo]
799 ixyz = [
800 ["array"],
801 [
802 "of",
803 "arrays"
804 ]
805 ]
806 [dev-dependencies]
807 ## dep1
808 dep1 = {
809 version="1.2-}",
810 optional=true
811 }
812 [features]
813 default = [
814 "goo",
815 "\"]",
816 "bar",
817 ]
818 ## foo
819 foo = [
820 "bar"
821 ]
822 ## bar
823 bar = [
824
825 ]
826 "#;
827 let parsed = process_toml(toml, &Args::default()).unwrap();
828 assert_eq!(
829 parsed,
830 "* **`dep1`** — dep1\n* **`foo`** — foo\n* **`bar`** *(enabled by default)* — bar\n"
831 );
832 let parsed = process_toml(
833 toml,
834 &Args {
835 feature_label: Some(
836 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
837 ),
838 },
839 )
840 .unwrap();
841 assert_eq!(
842 parsed,
843 "* <span class=\"stab portability\"><code>dep1</code></span> — dep1\n* <span class=\"stab portability\"><code>foo</code></span> — foo\n* <span class=\"stab portability\"><code>bar</code></span> *(enabled by default)* — bar\n"
844 );
845 }
846
847 #[test]
dots_in_feature()848 fn dots_in_feature() {
849 let toml = r#"
850 [features]
851 ## This is a test
852 "teßt." = []
853 default = ["teßt."]
854 [dependencies]
855 ## A dep
856 "dep" = { version = "123", optional = true }
857 "#;
858 let parsed = process_toml(toml, &Args::default()).unwrap();
859 assert_eq!(
860 parsed,
861 "* **`teßt.`** *(enabled by default)* — This is a test\n* **`dep`** — A dep\n"
862 );
863 let parsed = process_toml(
864 toml,
865 &Args {
866 feature_label: Some(
867 "<span class=\"stab portability\"><code>{feature}</code></span>".into(),
868 ),
869 },
870 )
871 .unwrap();
872 assert_eq!(
873 parsed,
874 "* <span class=\"stab portability\"><code>teßt.</code></span> *(enabled by default)* — This is a test\n* <span class=\"stab portability\"><code>dep</code></span> — A dep\n"
875 );
876 }
877 }
878