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

@ -17,6 +17,26 @@ jobs:
with:
mdbook-version: 'latest'
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Create usage-docs plugin
run: cargo build -p taskchampion-cli --features usage-docs --bin usage-docs
- run: mdbook build docs
- name: Deploy

View file

@ -75,5 +75,25 @@ jobs:
with:
mdbook-version: 'latest'
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Create usage-docs plugin
run: cargo build -p taskchampion-cli --features usage-docs --bin usage-docs
- run: mdbook test docs
- run: mdbook build docs

844
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -33,3 +33,8 @@ There are three crates here:
* [taskchampion-cli](./cli) - the command-line binary
* [taskchampion-sync-server](./sync-server) - the server against which `task sync` operates
## Documentation Generation
The `mdbook` configuration contains a "preprocessor" implemented in the `taskchampion-cli` crate in order to reflect CLI usage information into the generated book.
Tihs preprocessor is not built by default.
To (re)build it, run `cargo build -p taskchampion-cli --features usage-docs --bin usage-docs`.

View file

@ -10,7 +10,7 @@
1. Run `git tag vX.Y.Z`
1. Run `git push upstream`
1. Run `git push --tags upstream`
1. Run `( cd docs; ./build.sh )`
1. Run `( ./build-docs.sh )`
1. Run `(cd taskchampion; cargo publish)` (note that the other crates do not get published)
1. Navigate to the tag in the GitHub releases UI and create a release with general comments about the changes in the release
1. Upload `./target/release/task` and `./target/release/task-sync-server` to the release

31
build-docs.sh Executable file
View file

@ -0,0 +1,31 @@
#! /bin/bash
REMOTE=origin
set -e
if ! [ -f "docs/src/SUMMARY.md" ]; then
echo "Run this from the root of the repo"
exit 1
fi
# build the latest version of the mdbook plugin
cargo build -p taskchampion-cli --features usage-docs --bin usage-docs
# create a worktree of this repo, with the `gh-pages` branch checked out
if ! [ -d ./docs/tmp ]; then
git worktree add docs/tmp gh-pages
fi
# update the wortree
(cd docs/tmp && git pull $REMOTE gh-pages)
# remove all files in the worktree and regenerate the book there
rm -rf docs/tmp/*
mdbook build docs
cp -rp docs/book/* docs/tmp
# add everything in the worktree, commit, and push
(cd docs/tmp && git add -A)
(cd docs/tmp && git commit -am "update docs")
(cd docs/tmp && git push $REMOTE gh-pages:gh-pages)

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(())
}
}

View file

@ -1,3 +1,10 @@
This is an [mdbook](https://rust-lang.github.io/mdBook/index.html) book.
Minor modifications can be made without installing the mdbook tool, as the content is simple Markdown.
Changes are verified on pull requests.
To build the docs locally, you will need to build `usage-docs`:
```
cargo build -p taskchampion-cli --feature usage-docs --bin usage-docs
mdbook build docs/
```

View file

@ -7,3 +7,6 @@ title = "TaskChampion"
[output.html]
default-theme = "ayu"
[preprocessor.usage-docs]
command = "target/debug/usage-docs"

View file

@ -1,23 +0,0 @@
#! /bin/bash
REMOTE=origin
set -e
if ! [ -f "./src/SUMMARY.md" ]; then
echo "Run this from the docs/ dir"
exit 1
fi
if ! [ -d ./tmp ]; then
git worktree add tmp gh-pages
fi
(cd tmp && git pull $REMOTE gh-pages)
rm -rf tmp/*
mdbook build
cp -rp book/* tmp
(cd tmp && git add -A)
(cd tmp && git commit -am "update docs")
(cd tmp && git push $REMOTE gh-pages:gh-pages)

View file

@ -3,9 +3,11 @@
- [Welcome to TaskChampion](./welcome.md)
* [Installation](./installation.md)
* [Using the Task Command](./using-task-command.md)
* [Configuration](./config-file.md)
* [Reports](./reports.md)
* [Tags](./tags.md)
* [Filters](./filters.md)
* [Modifications](./modifications.md)
* [Configuration](./config-file.md)
* [Environment](./environment.md)
* [Synchronization](./task-sync.md)
* [Running the Sync Server](./running-sync-server.md)

9
docs/src/filters.md Normal file
View file

@ -0,0 +1,9 @@
# Filters
Filters are used to select specific tasks for reports or to specify tasks to be modified.
When more than one filter is given, only tasks which match all of the filters are selected.
When no filter is given, the command implicitly selects all tasks.
Filters can have the following forms:
<!-- INSERT GENERATED DOCUMENTATION - filters -->

View file

@ -0,0 +1,5 @@
# Modifications
Modifications can have the following forms:
<!-- INSERT GENERATED DOCUMENTATION - modifications-->

View file

@ -49,9 +49,8 @@ columns = [
]
```
The filter is a list of filter arguments, just like those that can be used on the command line.
See the `ta help` output for more details on this syntax.
It will be merged with any filters provided on the command line, when the report is invoked.
The `filter` property is a list of [filters](./filters.md).
It will be merged with any filters provided on the command line when the report is invoked.
The sort order is defined by an array of tables containing a `sort_by` property and an optional `ascending` property.
Tasks are compared by the first criterion, and if that is equal by the second, and so on.
@ -70,11 +69,11 @@ sort = [
The available values of `sort_by` are
(TODO: generate automatically)
<!-- INSERT GENERATED DOCUMENTATION - report-sort-by -->
Finally, the `columns` configuration specifies the list of columns to display.
Each element has a `label` and a `property`, as shown in the example above.
The avaliable properties are:
(TODO: generate automatically)
<!-- INSERT GENERATED DOCUMENTATION - report-columns -->

View file

@ -4,6 +4,13 @@ The main interface to your tasks is the `ta` command, which supports various sub
Customizable [reports](./reports.md) are also available as subcommands, such as `next`.
The command reads a [configuration file](./config-file.md) for its settings, including where to find the task database.
And the `sync` subcommand [synchronizes tasks with a sync server](./task-sync.md).
You can find a list of all subcommands, as well as the built-in reports, with `ta help`.
> NOTE: the `task` interface does not precisely match that of TaskWarrior.
## Subcommands
The sections below describe each subcommand of the `ta` command.
The syntax of `[filter]` is defined in [filters](./filters.md), and that of `[modification]` in [modifications](./modifications.md).
You can also find a summary of all subcommands, as well as filters, built-in reports, and so on, with `ta help`.
<!-- INSERT GENERATED DOCUMENTATION - subcommands -->