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