#[cfg(test)]
mod tests;
use std::{collections::HashSet, fmt::Write};
use color_eyre::eyre::{Result, bail};
pub fn extract(toml: &str, feature_label: &str, hidden_features: &HashSet<&str>) -> Result<String> {
let mut docs = parse(toml)?;
docs.retain(|entry| match entry {
FeatureDocEntry::InBetween { .. } => true,
FeatureDocEntry::Feature { name, .. } => !hidden_features.contains(name.as_str()),
});
Ok(format(&docs, feature_label))
}
type FeatureDocs = Vec<FeatureDocEntry>;
#[derive(Debug)]
enum FeatureDocEntry {
InBetween { docs: String },
Feature { name: String, docs: String, is_default: bool },
}
fn parse(toml: &str) -> Result<FeatureDocs> {
let doc = toml_edit::Document::parse(toml)?;
let Some(features) = doc.get("features") else {
return Ok(vec![]);
};
let Some(features) = features.as_table_like() else {
return Ok(vec![]);
};
let mut defaults = HashSet::new();
if let Some(item) = features.get("default")
&& let Some(array) = item.as_array()
{
for value in array.iter() {
if let Some(str) = value.as_str() {
defaults.insert(str);
}
}
}
let mut vec = vec![];
for (key, _) in features.get_values() {
let key = key[0];
let name = key.get();
if name == "default" {
continue;
}
let decor = key.leaf_decor();
let prefix = match decor.prefix() {
Some(raw_string) => match (raw_string.as_str(), raw_string.span()) {
(Some(string), _) => string,
(None, Some(span)) => &doc.raw()[span],
(None, None) => "",
},
None => "",
};
let mut in_between_docs = String::new();
let mut feature_docs = String::new();
for line in prefix.lines() {
if let Some(in_between_comment) = comment_line(line, "#!")? {
in_between_docs.push_str(in_between_comment);
in_between_docs.push('\n');
}
if let Some(feature_comment) = comment_line(line, "##")? {
feature_docs.push_str(feature_comment);
feature_docs.push('\n');
}
}
if !in_between_docs.is_empty() {
vec.push(FeatureDocEntry::InBetween { docs: in_between_docs });
}
vec.push(FeatureDocEntry::Feature {
name: name.to_string(),
docs: feature_docs,
is_default: defaults.contains(name),
});
}
Ok(vec)
}
fn comment_line<'a>(line: &'a str, prefix: &str) -> Result<Option<&'a str>> {
let Some(comment) = line.strip_prefix(prefix) else {
return Ok(None);
};
comment_line_unprefixed(comment).map(Some)
}
fn comment_line_unprefixed(mut line: &str) -> Result<&str> {
if !line.chars().all(char::is_whitespace) {
line = match line.strip_prefix(' ') {
Some(line) => line,
None => {
bail!("a non-empty feature docs comment line must start with a space")
}
}
}
line = line.trim_end();
Ok(line)
}
fn format(docs: &FeatureDocs, feature_label: &str) -> String {
let mut out = String::new();
for doc in docs {
match doc {
FeatureDocEntry::InBetween { docs } => {
let start_pad = if out.is_empty() { "" } else { "\n" };
writeln!(out, "{start_pad}{docs}").unwrap();
}
FeatureDocEntry::Feature { name, docs, is_default } => {
let label = feature_label.replace("{feature}", name);
let default = if *is_default { " *(enabled by default)*" } else { "" };
write!(out, "- {label}{default}").unwrap();
if docs.is_empty() {
out.push('\n');
} else {
for (i, line) in docs.lines().enumerate() {
out.push_str(if i == 0 { " — " } else { " " });
out.push_str(line);
out.push('\n');
}
};
}
}
}
out
}