• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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&nbsp;\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