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