Merge pull request #236 from taskchampion/issue140

Generate usage documentation
This commit is contained in:
Dustin J. Mitchell 2021-05-26 11:03:49 -04:00 committed by GitHub
commit adfde8be15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1177 additions and 55 deletions

View file

@ -23,6 +23,10 @@ atty = "^0.2.14"
toml = "^0.5.8"
toml_edit = "^0.2.0"
# only needed for usage-docs
mdbook = { version = "0.4", optional = true }
serde_json = { version = "*", optional = true }
[dependencies.taskchampion]
path = "../taskchampion"
@ -30,3 +34,14 @@ path = "../taskchampion"
assert_cmd = "^1.0.3"
predicates = "^1.0.7"
tempfile = "3"
[features]
usage-docs = [ "mdbook", "serde_json" ]
[[bin]]
name = "ta"
[[bin]]
# this is an mdbook plugin and only needed when running `mdbook`
name = "usage-docs"
required-features = [ "usage-docs" ]

View file

@ -115,7 +115,9 @@ impl Modification {
summary: "Set description",
description: "
Set the task description. Multiple arguments are combined into a single
space-separated description.",
space-separated description. To avoid surprises from shell quoting, prefer
to use a single quoted argument, for example `ta 19 modify \"return library
books\"`",
});
u.modifications.push(usage::Modification {
syntax: "+TAG",

50
cli/src/bin/usage-docs.rs Normal file
View file

@ -0,0 +1,50 @@
use mdbook::book::{Book, BookItem};
use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, PreprocessorContext};
use std::io;
use std::process;
use taskchampion_cli::Usage;
/// This is a simple mdbook preprocessor designed to substitute information from the usage
/// into the documentation.
fn main() -> anyhow::Result<()> {
// cheap way to detect the "supports" arg
if std::env::args().len() > 1 {
// sure, whatever, we support it all
process::exit(0);
}
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
eprintln!(
"Warning: This mdbook preprocessor was built against version {} of mdbook, \
but we're being called from version {}",
mdbook::MDBOOK_VERSION,
ctx.mdbook_version
);
}
let processed_book = process(&ctx, book)?;
serde_json::to_writer(io::stdout(), &processed_book)?;
Ok(())
}
fn process(_ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
let usage = Usage::new();
book.for_each_mut(|sect| {
if let BookItem::Chapter(ref mut chapter) = sect {
let new_content = usage.substitute_docs(&chapter.content).unwrap();
if new_content != chapter.content {
eprintln!(
"Substituting usage in {:?}",
chapter.source_path.as_ref().unwrap()
);
}
chapter.content = new_content;
}
});
Ok(book)
}

View file

@ -47,6 +47,9 @@ mod usage;
pub(crate) use errors::Error;
use settings::Settings;
// used by the `generate` command
pub use usage::Usage;
/// The main entry point for the command-line interface. This builds an Invocation
/// from the particulars of the operating-system interface, and then executes it.
pub fn main() -> Result<(), Error> {

View file

@ -7,5 +7,5 @@ mod report;
mod settings;
mod util;
pub(crate) use report::{Column, Property, Report, Sort, SortBy};
pub(crate) use report::{get_usage, Column, Property, Report, Sort, SortBy};
pub(crate) use settings::Settings;

View file

@ -2,6 +2,7 @@
use crate::argparse::{Condition, Filter};
use crate::settings::util::table_with_keys;
use crate::usage::{self, Usage};
use anyhow::{anyhow, bail, Result};
use std::convert::{TryFrom, TryInto};
@ -30,6 +31,7 @@ pub(crate) struct Column {
/// Task property to display in a report
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Property {
// NOTE: when adding a property here, add it to get_usage, below, as well.
/// The task's ID, either working-set index or Uuid if not in the working set
Id,
@ -59,6 +61,7 @@ pub(crate) struct Sort {
/// Task property to sort by
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum SortBy {
// NOTE: when adding a property here, add it to get_usage, below, as well.
/// The task's ID, either working-set index or a UUID prefix; working
/// set tasks sort before others.
Id,
@ -212,6 +215,34 @@ impl TryFrom<&toml::Value> for SortBy {
}
}
pub(crate) fn get_usage(u: &mut Usage) {
u.report_properties.push(usage::ReportProperty {
name: "id",
as_sort_by: Some("Sort by the task's shorthand ID"),
as_column: Some("The task's shorthand ID"),
});
u.report_properties.push(usage::ReportProperty {
name: "uuid",
as_sort_by: Some("Sort by the task's full UUID"),
as_column: Some("The task's full UUID"),
});
u.report_properties.push(usage::ReportProperty {
name: "active",
as_sort_by: None,
as_column: Some("`*` if the task is active (started)"),
});
u.report_properties.push(usage::ReportProperty {
name: "description",
as_sort_by: Some("Sort by the task's description"),
as_column: Some("The task's description"),
});
u.report_properties.push(usage::ReportProperty {
name: "tags",
as_sort_by: None,
as_column: Some("The task's tags"),
});
}
#[cfg(test)]
mod test {
use super::*;

View file

@ -2,26 +2,31 @@
//! a way that puts the source of that documentation near its implementation.
use crate::argparse;
use std::io::{Result, Write};
use crate::settings;
use anyhow::Result;
use std::io::Write;
#[cfg(feature = "usage-docs")]
use std::fmt::Write as FmtWrite;
/// A top-level structure containing usage/help information for the entire CLI.
#[derive(Debug, Default)]
pub(crate) struct Usage {
pub struct Usage {
pub(crate) subcommands: Vec<Subcommand>,
pub(crate) filters: Vec<Filter>,
pub(crate) modifications: Vec<Modification>,
pub(crate) report_properties: Vec<ReportProperty>,
}
impl Usage {
/// Get a new, completely-filled-out usage object
pub(crate) fn new() -> Self {
pub fn new() -> Self {
let mut rv = Self {
..Default::default()
};
argparse::get_usage(&mut rv);
// TODO: sort subcommands
settings::get_usage(&mut rv);
rv
}
@ -77,6 +82,62 @@ impl Usage {
}
Ok(())
}
#[cfg(feature = "usage-docs")]
/// Substitute strings matching
///
/// ```text
/// <!-- INSERT GENERATED DOCUMENTATION - $type -->
/// ```
///
/// With the appropriate documentation.
pub fn substitute_docs(&self, content: &str) -> Result<String> {
// this is not efficient, but it doesn't need to be
let mut lines = content.lines();
let mut w = String::new();
const DOC_HEADER_PREFIX: &str = "<!-- INSERT GENERATED DOCUMENTATION - ";
const DOC_HEADER_SUFFIX: &str = " -->";
for line in lines {
if line.starts_with(DOC_HEADER_PREFIX) && line.ends_with(DOC_HEADER_SUFFIX) {
let doc_type = &line[DOC_HEADER_PREFIX.len()..line.len() - DOC_HEADER_SUFFIX.len()];
match doc_type {
"subcommands" => {
for subcommand in self.subcommands.iter() {
subcommand.write_markdown(&mut w)?;
}
}
"filters" => {
for filter in self.filters.iter() {
filter.write_markdown(&mut w)?;
}
}
"modifications" => {
for modification in self.modifications.iter() {
modification.write_markdown(&mut w)?;
}
}
"report-columns" => {
for prop in self.report_properties.iter() {
prop.write_column_markdown(&mut w)?;
}
}
"report-sort-by" => {
for prop in self.report_properties.iter() {
prop.write_sort_by_markdown(&mut w)?;
}
}
_ => anyhow::bail!("Unkonwn doc type {}", doc_type),
}
} else {
writeln!(w, "{}", line)?;
}
}
Ok(w)
}
}
/// wrap an indented string
@ -122,6 +183,15 @@ impl Subcommand {
}
Ok(())
}
#[cfg(feature = "usage-docs")]
fn write_markdown<W: FmtWrite>(&self, mut w: W) -> Result<()> {
writeln!(w, "### `ta {}` - {}", self.name, self.summary)?;
writeln!(w, "```shell\nta {}\n```", self.syntax)?;
writeln!(w, "{}", indented(self.description, ""))?;
writeln!(w)?;
Ok(())
}
}
/// Usage documentation for a filter argument
@ -152,6 +222,15 @@ impl Filter {
}
Ok(())
}
#[cfg(feature = "usage-docs")]
fn write_markdown<W: FmtWrite>(&self, mut w: W) -> Result<()> {
writeln!(w, "* `{}` - {}", self.syntax, self.summary)?;
writeln!(w)?;
writeln!(w, "{}", indented(self.description, " "))?;
writeln!(w)?;
Ok(())
}
}
/// Usage documentation for a modification argument
@ -182,4 +261,51 @@ impl Modification {
}
Ok(())
}
#[cfg(feature = "usage-docs")]
fn write_markdown<W: FmtWrite>(&self, mut w: W) -> Result<()> {
writeln!(w, "* `{}` - {}", self.syntax, self.summary)?;
writeln!(w)?;
writeln!(w, "{}", indented(self.description, " "))?;
writeln!(w)?;
Ok(())
}
}
/// Usage documentation for a report property (which may be used for sorting, as a column, or
/// both).
#[derive(Debug, Default)]
pub(crate) struct ReportProperty {
/// Name of the property
pub(crate) name: &'static str,
/// Usage description for sorting, if any
pub(crate) as_sort_by: Option<&'static str>,
/// Usage description as a column, if any
pub(crate) as_column: Option<&'static str>,
}
impl ReportProperty {
#[cfg(feature = "usage-docs")]
fn write_sort_by_markdown<W: FmtWrite>(&self, mut w: W) -> Result<()> {
if let Some(as_sort_by) = self.as_sort_by {
writeln!(w, "* `{}`", self.name)?;
writeln!(w)?;
writeln!(w, "{}", indented(as_sort_by, " "))?;
writeln!(w)?;
}
Ok(())
}
#[cfg(feature = "usage-docs")]
fn write_column_markdown<W: FmtWrite>(&self, mut w: W) -> Result<()> {
if let Some(as_column) = self.as_column {
writeln!(w, "* `{}`", self.name)?;
writeln!(w)?;
writeln!(w, "{}", indented(as_column, " "))?;
writeln!(w)?;
}
Ok(())
}
}