Skip to content

Commit

Permalink
Initial implementation of a quote command.
Browse files Browse the repository at this point in the history
  • Loading branch information
misalcedo committed Jan 4, 2024
1 parent 24147a0 commit 82f5d0b
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 14 deletions.
66 changes: 60 additions & 6 deletions bin/arguments.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use clap::{Args, Parser, Subcommand};
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use regex::Regex;

#[derive(Clone, Debug, Eq, Parser, PartialEq)]
#[derive(Clone, Debug, Parser)]
#[clap(author, version, about)]
#[clap(args_conflicts_with_subcommands = true, infer_subcommands = true)]
pub struct Arguments {
Expand All @@ -21,21 +22,22 @@ impl Arguments {
}

/// Set the logging verbosity or level.
#[derive(Args, Copy, Clone, Debug, Eq, PartialEq)]
#[derive(Args, Copy, Clone, Debug)]
pub struct Verbosity {
#[clap(
short,
long,
action = clap::ArgAction::Count,
global = true,
help_heading("VERBOSITY"),
conflicts_with_all(&["debug", "trace"])
)]
/// Make the program more talkative.
pub verbose: u8,
#[clap(short, long, help_heading("VERBOSITY"), conflicts_with_all(&["verbose", "trace"]))]
#[clap(short, long, global = true, help_heading("VERBOSITY"), conflicts_with_all(&["verbose", "trace"]))]
/// Print debug messages.
pub debug: bool,
#[clap(short, long, help_heading("VERBOSITY"), conflicts_with_all(&["verbose", "debug"]))]
#[clap(short, long, global = true, help_heading("VERBOSITY"), conflicts_with_all(&["verbose", "debug"]))]
/// Print trace messages.
pub trace: bool,
}
Expand All @@ -51,6 +53,40 @@ pub struct LanguageArguments {
pub required: bool,
}

/// Defines which heading will be included in the output.
#[derive(Args, Clone, Debug)]
pub struct HeadingArguments {
#[clap(short, long, help_heading("HEADING"), value_enum)]
/// The level of the heading to quote.
pub level: Option<HeadingLevel>,
#[clap(short, long, help_heading("HEADING"))]
/// A regular expression to match the heading content with.
pub pattern: Option<Regex>,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
pub enum HeadingLevel {
H1,
H2,
H3,
H4,
H5,
H6,
}

impl From<HeadingLevel> for pulldown_cmark::HeadingLevel {
fn from(level: HeadingLevel) -> Self {
match level {
HeadingLevel::H1 => pulldown_cmark::HeadingLevel::H1,
HeadingLevel::H2 => pulldown_cmark::HeadingLevel::H2,
HeadingLevel::H3 => pulldown_cmark::HeadingLevel::H3,
HeadingLevel::H4 => pulldown_cmark::HeadingLevel::H4,
HeadingLevel::H5 => pulldown_cmark::HeadingLevel::H5,
HeadingLevel::H6 => pulldown_cmark::HeadingLevel::H6,
}
}
}

/// The input and output stream arguments for extracting a single file.
#[derive(Args, Clone, Debug, Eq, PartialEq)]
pub struct ExtractCommand {
Expand All @@ -68,6 +104,23 @@ pub struct ExtractCommand {
pub matcher: LanguageArguments,
}

/// The input and output stream arguments for extracting a section of markdown from a single file.
#[derive(Args, Clone, Debug)]
pub struct QuoteCommand {
/// The input stream to read Markdown from. Defaults to STDIN.
#[clap(short, long, help_heading("IO"))]
pub input: Option<PathBuf>,
/// The output stream to write matching fenced code block contents to. Defaults to STDOUT.
/// The directory path to the file must already exist.
#[clap(short, long, help_heading("IO"))]
pub output: Option<PathBuf>,
/// Overwrite the existing contents in the output stream.
#[clap(short, long, help_heading("IO"), requires("output"))]
pub force: bool,
#[clap(flatten)]
pub matcher: HeadingArguments,
}

#[derive(Clone, Debug, Eq, Parser, PartialEq)]
/// Walks a directory tree, extracting each matching file found during the walk and outputting the contents to the output directory with the `.md` extension removed.
pub struct WalkCommand {
Expand All @@ -90,7 +143,8 @@ pub struct WalkCommand {
}

/// The sub-command to execute.
#[derive(Clone, Debug, Eq, PartialEq, Subcommand)]
#[derive(Clone, Debug, Subcommand)]
pub enum Commands {
Quote(QuoteCommand),
Walk(WalkCommand),
}
37 changes: 35 additions & 2 deletions bin/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::arguments::{Commands, ExtractCommand, LanguageArguments, Verbosity, WalkCommand};
use crate::arguments::{Commands, ExtractCommand, HeadingArguments, LanguageArguments, QuoteCommand, Verbosity, WalkCommand};
use anyhow::Result;
use arguments::Arguments;
use literate::{CodeMatcher, LanguageMatcher, LiterateError};
use literate::{CodeMatcher, HeadingMatcher, LanguageMatcher, LiterateError, PatternMatcher};
use std::fs::File;
use std::io::ErrorKind::BrokenPipe;
use std::io::{stdin, stdout, Read, Write};
Expand Down Expand Up @@ -39,6 +39,7 @@ fn set_verbosity(verbosity: Verbosity) -> Result<()> {
fn run_subcommand(arguments: Arguments) -> Result<()> {
match arguments.command {
None => run_extraction(arguments.extract),
Some(Commands::Quote(command)) => run_quote(command),
Some(Commands::Walk(command)) => run_walk(command),
}
}
Expand Down Expand Up @@ -69,6 +70,32 @@ fn run_extraction(arguments: ExtractCommand) -> Result<()> {
}
}

fn run_quote(arguments: QuoteCommand) -> Result<()> {
let input: Box<dyn Read> = match arguments.input {
None => Box::new(stdin()),
Some(path) => Box::new(File::open(path)?),
};

let output: Box<dyn Write> = match arguments.output {
None => Box::new(stdout()),
Some(path) => Box::new(
File::options()
.write(true)
.create(true)
.truncate(true)
.create_new(!arguments.force)
.open(path)?,
),
};

let matcher: Box<dyn HeadingMatcher> = arguments.matcher.into();
match literate::quote(input, output, matcher) {
Ok(bytes) => Ok(info!("Extracted {bytes} bytes into the output directory.")),
Err(LiterateError::IO(error)) if error.kind() == BrokenPipe => Ok(()),
Err(error) => Ok(eprintln!("{error}")),
}
}

fn run_walk(command: WalkCommand) -> Result<()> {
let matcher: Box<dyn CodeMatcher> = command.matcher.into();

Expand All @@ -85,6 +112,12 @@ fn run_walk(command: WalkCommand) -> Result<()> {
Ok(())
}

impl From<HeadingArguments> for Box<dyn HeadingMatcher> {
fn from(arguments: HeadingArguments) -> Self {
Box::new(PatternMatcher::new(arguments.level.map(pulldown_cmark::HeadingLevel::from), arguments.pattern))
}
}

impl From<LanguageArguments> for Box<dyn CodeMatcher> {
fn from(arguments: LanguageArguments) -> Self {
match arguments.language {
Expand Down
7 changes: 6 additions & 1 deletion examples/tortuga.ta.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ f((x, y)) = y - x
```

## Factorial
Calculate the factorial of an integer recursively:

```
factorial(n = 0) = 1
factorial(n.0 > 0) = n * factorial(n - 1)
Expand All @@ -47,4 +49,7 @@ fibonacci(n <= 1) = n
fibonacci(n) = [
fibonacci(n - 2) + fibonacci(n - 1)
]
```
```

##
Empty heading
2 changes: 1 addition & 1 deletion src/matcher.rs → src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ impl Display for LanguageMatcher {
impl LanguageMatcher {
/// Creates a new [`LanguageMatcher`].
pub fn new(language: String, required: bool) -> Self {
LanguageMatcher { language, required }
Self { language, required }
}
}

Expand Down
99 changes: 99 additions & 0 deletions src/heading.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use std::fmt::{Display, Formatter};
use pulldown_cmark::HeadingLevel;
use regex::Regex;

/// Determines whether a heading should be included in the output.
pub trait HeadingMatcher {
/// Tests whether this heading should be included in the output.
fn matches(&self, level: HeadingLevel, contents: Option<&str>) -> bool;
}

impl<Matcher: HeadingMatcher + ?Sized> HeadingMatcher for Box<Matcher> {
fn matches(&self, level: HeadingLevel, contents: Option<&str>) -> bool {
(**self).matches(level, contents)
}
}

impl<Matcher: HeadingMatcher + ?Sized> HeadingMatcher for &Matcher {
fn matches(&self, level: HeadingLevel, contents: Option<&str>) -> bool {
(*self).matches(level, contents)
}
}

impl HeadingMatcher for bool {
fn matches(&self, _: HeadingLevel, _: Option<&str>) -> bool {
*self
}
}

impl HeadingMatcher for HeadingLevel {
fn matches(&self, level: HeadingLevel, _: Option<&str>) -> bool {
*self == level
}
}

impl HeadingMatcher for Option<HeadingLevel> {
fn matches(&self, level: HeadingLevel, _: Option<&str>) -> bool {
self.map(|expected| expected == level).unwrap_or(true)
}
}

impl HeadingMatcher for Option<Regex> {
fn matches(&self, _: HeadingLevel, contents: Option<&str>) -> bool {
match (self.as_ref(), contents) {
(Some(regex), Some(contents)) => regex.is_match(contents),
(Some(_), None) => false,
(None, _) => true,
}
}
}

impl HeadingMatcher for str {
fn matches(&self, _: HeadingLevel, contents: Option<&str>) -> bool {
contents.map(|c| self == c).unwrap_or(false)
}
}

impl HeadingMatcher for Option<&str> {
fn matches(&self, _: HeadingLevel, contents: Option<&str>) -> bool {
match self {
None => true,
_ => *self == contents
}
}
}

/// Matches the header against an regular expression for the contents and an optional level.
/// Exposes control over whether to include fenced code blocks without a language in the output.
#[derive(Clone, Debug)]
pub struct PatternMatcher {
level: Option<HeadingLevel>,
pattern: Option<Regex>,
}

impl Display for PatternMatcher {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.level.as_ref() {
None => f.write_str("h*"),
Some(level) => level.fmt(f)
}?;

match self.pattern.as_ref() {
None => f.write_str(" *"),
Some(pattern) => write!(f, " {}", pattern)
}
}
}

impl PatternMatcher {
/// Creates a new [`PatternMatcher`].
pub fn new(level: Option<HeadingLevel>, pattern: Option<Regex>) -> Self {
Self { level, pattern }
}
}

impl HeadingMatcher for PatternMatcher {
fn matches(&self, level: HeadingLevel, contents: Option<&str>) -> bool {
HeadingMatcher::matches(&self.level, level, contents) && HeadingMatcher::matches(&self.pattern, level, contents)
}
}
Loading

0 comments on commit 82f5d0b

Please sign in to comment.