From 0d7c1b747f8729d82c00362241e1f56ce3ae6727 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sun, 4 Jun 2023 13:54:02 +0200 Subject: [PATCH] remove markdown rendering and make help write to stdout The markdown rendering is too complicated at the moment and slows the rest of development down too much. We can add it back in later. The generated code for the help string now writes directly to stdout, instead of building up a String, this leads to nicer code and is probably faster. --- Cargo.toml | 2 - README.md | 1 + derive/src/help.rs | 70 +++----- derive/src/help_parser.rs | 236 +++++++++++++++++++++++++ derive/src/lib.rs | 4 +- derive/src/markdown.rs | 128 -------------- examples/hello_world_help.md | 10 +- src/error.rs | 18 +- src/lib.rs | 10 +- term_md/Cargo.toml | 11 -- term_md/src/event.rs | 131 -------------- term_md/src/lib.rs | 327 ----------------------------------- tests/coreutils/uniq.rs | 105 +++++++++++ 13 files changed, 398 insertions(+), 655 deletions(-) create mode 100644 derive/src/help_parser.rs delete mode 100644 derive/src/markdown.rs delete mode 100644 term_md/Cargo.toml delete mode 100644 term_md/src/event.rs delete mode 100644 term_md/src/lib.rs create mode 100644 tests/coreutils/uniq.rs diff --git a/Cargo.toml b/Cargo.toml index ab0e543..4a5b26f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,8 @@ readme = "README.md" [dependencies] derive = { version = "0.1.0", path = "derive" } lexopt = "0.3.0" -term_md = { version = "0.1.0", path = "term_md" } [workspace] members = [ - "term_md", "derive", ] diff --git a/README.md b/README.md index 6985e83..2f71c24 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # uutils-args + An experimental derive-based argument parser specifically for uutils diff --git a/derive/src/help.rs b/derive/src/help.rs index 1dc1b8e..50cb0aa 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -6,7 +6,7 @@ use std::{ use crate::{ argument::{ArgType, Argument}, flags::Flags, - markdown::{get_after_event, get_h2, str_to_renderer}, + help_parser::{parse_about, parse_section, parse_usage}, }; use proc_macro2::TokenStream; use quote::quote; @@ -44,8 +44,7 @@ pub(crate) fn help_string( .. } => { let flags = flags.format(); - let renderer = str_to_renderer(help); - options.push(quote!((#flags, #renderer))); + options.push(quote!((#flags, #help))); } // Hidden arguments should not show up in --help ArgType::Option { hidden: true, .. } => {} @@ -53,64 +52,51 @@ pub(crate) fn help_string( } } - let (summary, after_options) = if let Some(file) = &file { - let (summary, after_options) = read_help_file(file); - ( - quote!(s.push_str(&#summary.render());), - quote!( - s.push('\n'); - s.push_str(&#after_options.render()); - ), - ) + // FIXME: We need to get an option per item and provide proper defaults + let (summary, usage, after_options) = if let Some(file) = file { + read_help_file(file) } else { - (quote!(), quote!()) + ("".into(), "{} [OPTIONS] [ARGUMENTS]".into(), "".into()) }; if !help_flags.is_empty() { let flags = help_flags.format(); - let renderer = str_to_renderer("Display this help message"); - options.push(quote!((#flags, #renderer))); + options.push(quote!((#flags, "Display this help message"))); } if !version_flags.is_empty() { let flags = version_flags.format(); - let renderer = str_to_renderer("Display version information"); - options.push(quote!((#flags, #renderer))); + options.push(quote!((#flags, "Display version information"))); } let options = if !options.is_empty() { let options = quote!([#(#options),*]); quote!( - s.push_str("\nOptions:\n"); - for (flags, renderer) in #options { + writeln!(w, "\nOptions:")?; + for (flags, help_string) in #options { let indent = " ".repeat(#indent); - let help_string = renderer.render(); let mut help_lines = help_string.lines(); - s.push_str(&indent); - s.push_str(&flags); + write!(w, "{}", &indent)?; + write!(w, "{}", &flags)?; if flags.len() <= #width { let line = match help_lines.next() { Some(line) => line, None => { - s.push('\n'); + writeln!(w)?; continue; }, }; let help_indent = " ".repeat(#width-flags.len()+2); - s.push_str(&help_indent); - s.push_str(line); - s.push('\n'); + writeln!(w, "{}{}", help_indent, line)?; } else { - s.push('\n'); + writeln!(w, "\n")?; } let help_indent = " ".repeat(#width+#indent+2); for line in help_lines { - s.push_str(&help_indent); - s.push_str(line); - s.push('\n'); + writeln!(w, "{}{}", help_indent, line)?; } } ) @@ -119,26 +105,25 @@ pub(crate) fn help_string( }; quote!( - let mut s = String::new(); - - s.push_str(&format!("{} {}\n", + let mut w = ::std::io::stdout(); + use ::std::io::Write; + writeln!(w, "{} {}", option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME")), env!("CARGO_PKG_VERSION"), - )); + )?; - #summary + writeln!(w, "{}", #summary)?; - s.push_str(&format!("\nUsage:\n {} [OPTIONS] [ARGS]\n", bin_name)); + writeln!(w, "\nUsage:\n {}", format!(#usage, bin_name))?; #options - #after_options - - s + writeln!(w, "{}", #after_options)?; + Ok(()) ) } -fn read_help_file(file: &str) -> (TokenStream, TokenStream) { +fn read_help_file(file: &str) -> (String, String, String) { let path = Path::new(file); let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); let mut location = PathBuf::from(manifest_dir); @@ -148,8 +133,9 @@ fn read_help_file(file: &str) -> (TokenStream, TokenStream) { f.read_to_string(&mut contents).unwrap(); ( - get_h2("summary", &contents), - get_after_event(pulldown_cmark::Event::Rule, &contents), + parse_about(&contents), + parse_usage(&contents), + parse_section("after help", &contents).unwrap_or_default(), ) } diff --git a/derive/src/help_parser.rs b/derive/src/help_parser.rs new file mode 100644 index 0000000..8faa4e6 --- /dev/null +++ b/derive/src/help_parser.rs @@ -0,0 +1,236 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! A collection of functions to parse the markdown code of help files. +//! +//! The structure of the markdown code is assumed to be: +//! +//! # util name +//! +//! ```text +//! usage info +//! ``` +//! +//! About text +//! +//! ## Section 1 +//! +//! Some content +//! +//! ## Section 2 +//! +//! Some content + +const MARKDOWN_CODE_FENCES: &str = "```"; + +/// Parses the text between the first markdown code block and the next header, if any, +/// into an about string. +pub fn parse_about(content: &str) -> String { + content + .lines() + .skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) + .skip(1) + .skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) + .skip(1) + .take_while(|l| !l.starts_with('#')) + .collect::>() + .join("\n") + .trim() + .to_string() +} + +/// Parses the first markdown code block into a usage string +/// +/// The code fences are removed and the name of the util is replaced +/// with `{}` so that it can be replaced with the appropriate name +/// at runtime. +pub fn parse_usage(content: &str) -> String { + content + .lines() + .skip_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) + .skip(1) + .take_while(|l| !l.starts_with(MARKDOWN_CODE_FENCES)) + .map(|l| { + // Replace the util name (assumed to be the first word) with "{}" + // to be replaced with the runtime value later. + if let Some((_util, args)) = l.split_once(' ') { + format!("{{}} {args}\n") + } else { + "{}\n".to_string() + } + }) + .collect::>() + .join("") + .trim() + .to_string() +} + +/// Get a single section from content +/// +/// The section must be a second level section (i.e. start with `##`). +pub fn parse_section(section: &str, content: &str) -> Option { + fn is_section_header(line: &str, section: &str) -> bool { + line.strip_prefix("##") + .map_or(false, |l| l.trim().to_lowercase() == section) + } + + let section = §ion.to_lowercase(); + + // We cannot distinguish between an empty or non-existing section below, + // so we do a quick test to check whether the section exists + if content.lines().all(|l| !is_section_header(l, section)) { + return None; + } + + // Prefix includes space to allow processing of section with level 3-6 headers + let section_header_prefix = "## "; + + Some( + content + .lines() + .skip_while(|&l| !is_section_header(l, section)) + .skip(1) + .take_while(|l| !l.starts_with(section_header_prefix)) + .collect::>() + .join("\n") + .trim() + .to_string(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_section() { + let input = "\ + # ls\n\ + ## some section\n\ + This is some section\n\ + \n\ + ## ANOTHER SECTION + This is the other section\n\ + with multiple lines\n"; + + assert_eq!( + parse_section("some section", input).unwrap(), + "This is some section" + ); + assert_eq!( + parse_section("SOME SECTION", input).unwrap(), + "This is some section" + ); + assert_eq!( + parse_section("another section", input).unwrap(), + "This is the other section\nwith multiple lines" + ); + } + + #[test] + fn test_parse_section_with_sub_headers() { + let input = "\ + # ls\n\ + ## after section\n\ + This is some section\n\ + \n\ + ### level 3 header\n\ + \n\ + Additional text under the section.\n\ + \n\ + #### level 4 header\n\ + \n\ + Yet another paragraph\n"; + + assert_eq!( + parse_section("after section", input).unwrap(), + "This is some section\n\n\ + ### level 3 header\n\n\ + Additional text under the section.\n\n\ + #### level 4 header\n\n\ + Yet another paragraph" + ); + } + + #[test] + fn test_parse_non_existing_section() { + let input = "\ + # ls\n\ + ## some section\n\ + This is some section\n\ + \n\ + ## ANOTHER SECTION + This is the other section\n\ + with multiple lines\n"; + + assert!(parse_section("non-existing section", input).is_none()); + } + + #[test] + fn test_parse_usage() { + let input = "\ + # ls\n\ + ```\n\ + ls -l\n\ + ```\n\ + ## some section\n\ + This is some section\n\ + \n\ + ## ANOTHER SECTION + This is the other section\n\ + with multiple lines\n"; + + assert_eq!(parse_usage(input), "{} -l"); + } + + #[test] + fn test_parse_multi_line_usage() { + let input = "\ + # ls\n\ + ```\n\ + ls -a\n\ + ls -b\n\ + ls -c\n\ + ```\n\ + ## some section\n\ + This is some section\n"; + + assert_eq!(parse_usage(input), "{} -a\n{} -b\n{} -c"); + } + + #[test] + fn test_parse_about() { + let input = "\ + # ls\n\ + ```\n\ + ls -l\n\ + ```\n\ + \n\ + This is the about section\n\ + \n\ + ## some section\n\ + This is some section\n"; + + assert_eq!(parse_about(input), "This is the about section"); + } + + #[test] + fn test_parse_multi_line_about() { + let input = "\ + # ls\n\ + ```\n\ + ls -l\n\ + ```\n\ + \n\ + about a\n\ + \n\ + about b\n\ + \n\ + ## some section\n\ + This is some section\n"; + + assert_eq!(parse_about(input), "about a\n\nabout b"); + } +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index c8c25fe..0f03c4f 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -2,8 +2,8 @@ mod argument; mod attributes; mod flags; mod help; +mod help_parser; mod initial; -mod markdown; use argument::{ long_handling, number_handling, parse_argument, parse_arguments_attr, positional_handling, @@ -98,7 +98,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { #missing_argument_checks } - fn help(bin_name: &str) -> String { + fn help(bin_name: &str) -> ::std::io::Result<()> { #help_string } diff --git a/derive/src/markdown.rs b/derive/src/markdown.rs deleted file mode 100644 index eb4b9a4..0000000 --- a/derive/src/markdown.rs +++ /dev/null @@ -1,128 +0,0 @@ -use proc_macro2::TokenStream; -use pulldown_cmark::{Event, HeadingLevel, Parser, Tag}; -use quote::quote; - -fn prefix(t: TokenStream) -> TokenStream { - quote!(uutils_args::term_md::#t) -} - -fn md_to_quote(event: Event) -> TokenStream { - let tokens = match event { - Event::Start(tag) => { - let tag = quote_tag(tag); - quote!(Event::Start(#tag)) - } - Event::End(tag) => { - let tag = quote_tag(tag); - quote!(Event::End(#tag)) - } - Event::Text(t) => { - let t = t.to_string(); - let text = quote!(String::from(#t)); - quote!(Event::Text(#text)) - } - Event::Code(t) => { - let t = t.to_string(); - let text = quote!(String::from(#t)); - quote!(Event::Code(#text)) - } - Event::SoftBreak => quote!(Event::SoftBreak), - Event::HardBreak => quote!(Event::HardBreak), - Event::Rule => quote!(Event::Rule), - - // Below are unsupported in term_md - Event::Html(_) => todo!(), - Event::FootnoteReference(_) => todo!(), - Event::TaskListMarker(_) => todo!(), - }; - prefix(tokens) -} - -pub(crate) fn str_to_renderer(s: &str) -> TokenStream { - let events = Parser::new(s); - let parsed_events = events.map(md_to_quote); - - prefix(quote!(Renderer::new( - 60, - vec![#(#parsed_events),*].into_iter() - ))) -} - -pub(crate) fn get_h2(heading_name: &str, s: &str) -> TokenStream { - let mut events = Parser::new(s); - let mut selected_events = Vec::new(); - while let Some(event) = events.next() { - if let Event::Start(Tag::Heading(HeadingLevel::H2, _, _)) = event { - if let Some(Event::Text(s)) = events.next() { - if s.to_lowercase() == heading_name.to_lowercase() { - selected_events.extend( - (&mut events) - .skip_while(|e| { - !matches!(e, Event::End(Tag::Heading(HeadingLevel::H2, _, _))) - }) - .skip(1) - .take_while(|e| { - !matches!( - e, - Event::Start(Tag::Heading(HeadingLevel::H2, _, _)) - | Event::Rule - ) - }), - ) - } - } - } - } - - let parsed_events = selected_events.into_iter().map(md_to_quote); - prefix(quote!(Renderer::new( - 80, - vec![#(#parsed_events),*].into_iter() - ))) -} - -pub(crate) fn get_after_event(event: Event, s: &str) -> TokenStream { - let events = Parser::new(s); - - let parsed_events = events.skip_while(|e| e != &event).skip(1).map(md_to_quote); - prefix(quote!(Renderer::new( - 80, - vec![#(#parsed_events),*].into_iter() - ))) -} - -fn quote_tag(tag: Tag) -> TokenStream { - let tokens = match tag { - Tag::Paragraph => quote!(Paragraph), - Tag::Heading(level, _, _) => { - let level = match level { - HeadingLevel::H1 => quote!(H1), - HeadingLevel::H2 => quote!(H2), - HeadingLevel::H3 => quote!(H3), - HeadingLevel::H4 => quote!(H4), - HeadingLevel::H5 => quote!(H5), - HeadingLevel::H6 => quote!(H6), - }; - let level = prefix(quote!(HeadingLevel::#level)); - quote!(Heading(#level)) - } - Tag::Emphasis => quote!(Emphasis), - Tag::Strong => quote!(Strong), - Tag::Strikethrough => quote!(Strikethrough), - - // Below are unsupported in term_md - Tag::BlockQuote => todo!(), - Tag::CodeBlock(_) => todo!(), - Tag::List(_) => todo!(), - Tag::Item => todo!(), - Tag::FootnoteDefinition(_) => todo!(), - Tag::Table(_) => todo!(), - Tag::TableHead => todo!(), - Tag::TableRow => todo!(), - Tag::TableCell => todo!(), - Tag::Link(_, _, _) => todo!(), - Tag::Image(_, _, _) => todo!(), - }; - - prefix(quote!(Tag::#tokens)) -} diff --git a/examples/hello_world_help.md b/examples/hello_world_help.md index 442a1e0..1b4a84d 100644 --- a/examples/hello_world_help.md +++ b/examples/hello_world_help.md @@ -1,13 +1,15 @@ # Helloworld -## Summary +``` +hello_world [-n NAME] [-c COUNT] [msg] +``` Hello this is the summary. ---- +## After help This is after the options! -## Values +### Values -Wow! \ No newline at end of file +Wow! diff --git a/src/error.rs b/src/error.rs index 72fc477..67593fc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,7 +7,9 @@ use std::{ /// Errors that can occur while parsing arguments. pub enum Error { /// There was an option that required an option, but none was given. - MissingValue { option: Option }, + MissingValue { + option: Option, + }, /// Some positional arguments were not given. MissingPositionalArguments(Vec), @@ -19,7 +21,10 @@ pub enum Error { UnexpectedArgument(OsString), /// A value was passed to an option that didn't expect a value. - UnexpectedValue { option: String, value: OsString }, + UnexpectedValue { + option: String, + value: OsString, + }, /// Parsing of a value failed. ParsingFailed { @@ -37,6 +42,14 @@ pub enum Error { /// The value was required to be valid UTF-8, but it wasn't. NonUnicodeValue(OsString), + + IoError(std::io::Error), +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Error::IoError(value) + } } impl StdError for Error {} @@ -101,6 +114,7 @@ impl Display for Error { Error::NonUnicodeValue(x) => { write!(f, "Invalid unicode value found: {}", x.to_string_lossy()) } + Error::IoError(x) => std::fmt::Display::fmt(x, f), } } } diff --git a/src/lib.rs b/src/lib.rs index 3d75556..d5495c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ //! //! - A derive macro for declarative argument definition. //! - Automatic help generation. -//! - (Limited) markdown support in help text. //! - Positional and optional arguments. //! - Automatically parsing values into Rust types. //! - Define a custom exit code on errors. @@ -144,7 +143,6 @@ mod value; pub use derive::*; pub use lexopt; -pub use term_md; pub use error::Error; pub use value::{Value, ValueError, ValueResult}; @@ -211,10 +209,10 @@ pub trait Arguments: Sized { /// [`Options::parse`] and [`Options::try_parse`]. fn check_missing(positional_idx: usize) -> Result<(), Error>; - /// Get the help string for this command. + /// Print the help string for this command. /// /// The `bin_name` specifies the name that executable was called with. - fn help(bin_name: &str) -> String; + fn help(bin_name: &str) -> std::io::Result<()>; /// Get the version string for this command. fn version() -> String; @@ -274,7 +272,7 @@ impl ArgumentIter { if let Some(arg) = T::next_arg(&mut self.parser, &mut self.positional_idx)? { match arg { Argument::Help => { - print!("{}", self.help()); + self.help()?; std::process::exit(0); } Argument::Version => { @@ -288,7 +286,7 @@ impl ArgumentIter { } } - fn help(&self) -> String { + fn help(&self) -> std::io::Result<()> { T::help(self.parser.bin_name().unwrap()) } diff --git a/term_md/Cargo.toml b/term_md/Cargo.toml deleted file mode 100644 index fba6c90..0000000 --- a/term_md/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "term_md" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -nu-ansi-term = "0.47.0" -pulldown-cmark = "0.9.2" -unicode-width = "0.1.10" diff --git a/term_md/src/event.rs b/term_md/src/event.rs deleted file mode 100644 index 812c3e1..0000000 --- a/term_md/src/event.rs +++ /dev/null @@ -1,131 +0,0 @@ -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum HeadingLevel { - H1, - H2, - H3, - H4, - H5, - H6, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CodeBlockKind { - Indented, - Fenced(String), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Alignment { - None, - Left, - Center, - Right, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Tag { - Paragraph, - Heading(HeadingLevel), - // BlockQuote, - // CodeBlock(CodeBlockKind), - // List(Option), - // Item, - // FootnoteDefinition(CowStr<'a>), - // Table(Vec), - // TableHead, - // TableRow, - // TableCell, - Emphasis, - Strong, - Strikethrough, - // Link(LinkType, CowStr<'a>, CowStr<'a>), - // Image(LinkType, CowStr<'a>, CowStr<'a>), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Event { - Start(Tag), - End(Tag), - Text(String), - Code(String), - // FootnoteReference(CowStr(<'a>)), - SoftBreak, - HardBreak, - Rule, - // TaskListMarker(bool), -} - -impl From for HeadingLevel { - fn from(heading_level: pulldown_cmark::HeadingLevel) -> Self { - match heading_level { - pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1, - pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2, - pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3, - pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4, - pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5, - pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6, - } - } -} - -impl<'a> From> for CodeBlockKind { - fn from(code_block_kind: pulldown_cmark::CodeBlockKind) -> Self { - match code_block_kind { - pulldown_cmark::CodeBlockKind::Indented => CodeBlockKind::Indented, - pulldown_cmark::CodeBlockKind::Fenced(x) => CodeBlockKind::Fenced(x.to_string()), - } - } -} - -impl From for Alignment { - fn from(alignment: pulldown_cmark::Alignment) -> Self { - match alignment { - pulldown_cmark::Alignment::None => Alignment::None, - pulldown_cmark::Alignment::Left => Alignment::Left, - pulldown_cmark::Alignment::Center => Alignment::Center, - pulldown_cmark::Alignment::Right => Alignment::Right, - } - } -} - -impl<'a> From> for Event { - fn from(event: pulldown_cmark::Event) -> Self { - match event { - pulldown_cmark::Event::Start(tag) => Event::Start(tag.into()), - pulldown_cmark::Event::End(tag) => Event::End(tag.into()), - pulldown_cmark::Event::Text(text) => Event::Text(text.to_string()), - pulldown_cmark::Event::Code(text) => Event::Code(text.to_string()), - pulldown_cmark::Event::FootnoteReference(_) => todo!(), - pulldown_cmark::Event::SoftBreak => Event::SoftBreak, - pulldown_cmark::Event::HardBreak => Event::HardBreak, - pulldown_cmark::Event::Rule => Event::Rule, - pulldown_cmark::Event::TaskListMarker(_) => todo!(), - - // We're never going to be able to support the events below - pulldown_cmark::Event::Html(_) => panic!("HTML is unsupported"), - } - } -} - -impl<'a> From> for Tag { - fn from(tag: pulldown_cmark::Tag) -> Self { - match tag { - pulldown_cmark::Tag::Paragraph => Tag::Paragraph, - pulldown_cmark::Tag::Heading(level, _, _) => Tag::Heading(level.into()), - pulldown_cmark::Tag::BlockQuote => todo!(), - pulldown_cmark::Tag::CodeBlock(_) => todo!(), - pulldown_cmark::Tag::List(_) => todo!(), - pulldown_cmark::Tag::Item => todo!(), - pulldown_cmark::Tag::FootnoteDefinition(_) => todo!(), - pulldown_cmark::Tag::Table(_) => todo!(), - pulldown_cmark::Tag::TableHead => todo!(), - pulldown_cmark::Tag::TableRow => todo!(), - pulldown_cmark::Tag::TableCell => todo!(), - pulldown_cmark::Tag::Emphasis => Tag::Emphasis, - pulldown_cmark::Tag::Strong => Tag::Strong, - pulldown_cmark::Tag::Strikethrough => Tag::Strikethrough, - pulldown_cmark::Tag::Link(_, _, _) => todo!(), - pulldown_cmark::Tag::Image(_, _, _) => todo!(), - } - } -} diff --git a/term_md/src/lib.rs b/term_md/src/lib.rs deleted file mode 100644 index 2c125d6..0000000 --- a/term_md/src/lib.rs +++ /dev/null @@ -1,327 +0,0 @@ -mod event; -pub use event::*; - -use nu_ansi_term::{Color, Style}; -use unicode_width::UnicodeWidthStr; - -pub struct Renderer> { - // The output string, which will be returned by `render` - output: String, - - // Terminal width to render into - width: usize, - - // Column in which the next character will be written - // - // Must always be smaller than `width` - current_column: usize, - - // Iterator of Markdown events to render - events: T, -} - -impl> Renderer { - pub fn new(width: usize, events: T) -> Self { - Self { - output: String::new(), - current_column: 0, - width, - events, - } - } - - pub fn render(mut self) -> String { - while let Some(ev) = self.events.next() { - match ev { - Event::Start(x) => match x { - Tag::Paragraph => self.render_paragraph(), - Tag::Heading(level) => self.render_heading(level), - Tag::Emphasis | Tag::Strong | Tag::Strikethrough => { - unreachable!("Can't be the opening tag") - } - }, - Event::Rule => { - if self.current_column > 0 { - self.newline(); - } - self.output.push_str(&"─".repeat(self.width)); - self.newline(); - } - _ => { - panic!( - "Internal error: we assume that the markdown always \ - starts with a start tag. If you hit this, that assumption \ - is wrong." - ) - } - } - } - - self.output - } - - fn render_paragraph(&mut self) { - self.render_inline(&Tag::Paragraph, Style::new()); - self.newline(); - } - - fn render_heading(&mut self, level: HeadingLevel) { - let style = match level { - HeadingLevel::H1 => Style::new().bold().underline(), - HeadingLevel::H2 => Style::new().bold(), - _ => panic!(), - }; - self.output.push_str(&style.prefix().to_string()); - self.render_inline(&Tag::Heading(level), style); - self.output.push_str(&style.suffix().to_string()); - self.newline(); - } - - fn render_inline(&mut self, until: &Tag, base_style: Style) { - let mut style = base_style; - while let Some(ev) = self.events.next() { - match ev { - Event::Text(x) => self.wrap_words(&x), - Event::Code(x) => { - let mut code_style = style; - // A grayish color. The range is 232 (black) to 255 (white). - // This might have to depend on the terminal colors. - code_style.foreground = Some(Color::Fixed(250)); - - // Change to the code style, push the string and change back. - self.output.push_str(&style.infix(code_style).to_string()); - self.wrap_words(&x); - self.output.push_str(&code_style.infix(style).to_string()); - } - Event::SoftBreak => { - if self.current_column >= self.width { - self.newline(); - } else { - self.current_column += 1; - self.output.push(' '); - } - } - Event::HardBreak => { - self.newline(); - } - Event::Rule => { - self.newline(); - self.output.push_str(&"─".repeat(self.width)); - self.newline(); - } - Event::Start(tag @ (Tag::Emphasis | Tag::Strong | Tag::Strikethrough)) => { - self.change_style(&mut style, tag, true); - } - Event::End(tag @ (Tag::Emphasis | Tag::Strong | Tag::Strikethrough)) => { - self.change_style(&mut style, tag, false); - } - Event::End(tag) if &tag == until => return, - Event::Start(Tag::Paragraph | Tag::Heading(_)) => { - panic!("We're already in a paragraph or heading.") - } - Event::End(Tag::Paragraph | Tag::Heading(_)) => { - unreachable!("Should have been caught above.") - } - } - } - } - - fn wrap_words(&mut self, s: &str) { - let mut words = s.split_whitespace(); - let first = words.next(); - - // The first word needs special treatment, because we only want to - // print a space in front of it if the string actually starts with a - // space. - let word = match first { - Some(word) => word, - None => return, - }; - - let width = word.width(); - - if self.current_column + width >= self.width { - self.newline(); - } else if s.starts_with(' ') { - self.output.push(' '); - self.current_column += 1; - } - - self.current_column += width; - self.output.push_str(word); - - for word in words { - let width = word.width(); - - // The +1 comes from the additional space we need in front of this - // word. - if self.current_column + width + 1 >= self.width { - self.newline(); - } else { - self.output.push(' '); - self.current_column += 1; - } - - self.current_column += width; - self.output.push_str(word); - } - - if s.ends_with(' ') { - self.output.push(' ') - } - } - - fn newline(&mut self) { - self.current_column = 0; - self.output.push('\n'); - } - - fn change_style(&mut self, style: &mut Style, tag: Tag, enable: bool) { - // Important for understanding this function: Style implements Copy - let old_style = *style; - - let setting = match tag { - Tag::Emphasis => &mut style.is_italic, - Tag::Strong => &mut style.is_bold, - Tag::Strikethrough => &mut style.is_strikethrough, - Tag::Paragraph => panic!("Paragraph is not a style"), - Tag::Heading(_) => panic!("Heading is not a style"), - }; - - *setting = enable; - - // Add the ansi code to mode between the styles to the output - self.output.push_str(&old_style.infix(*style).to_string()); - } -} - -#[cfg(test)] -mod tests { - use super::Renderer; - use pulldown_cmark::{Options, Parser}; - - #[test] - fn it_works() { - let events = Parser::new("This is *some* markdown **paragraph**!").map(|e| e.into()); - - let output = Renderer::new(40, events).render(); - - assert_eq!( - output, - "This is \u{1b}[3msome\u{1b}[0m markdown \u{1b}[1mparagraph\u{1b}[0m!\n" - ); - } - - #[test] - fn styles() { - let events = Parser::new_ext( - "We have *emphasis*, **bold**, and ~~strikethrough~~.", - Options::ENABLE_STRIKETHROUGH, - ) - .map(|e| e.into()); - - let output = Renderer::new(40, events).render(); - - println!("{output}"); - assert_eq!( - output, - "We have \u{1b}[3memphasis\u{1b}[0m, \u{1b}[1mbold\u{1b}[0m, and \u{1b}[9mstrikethrough\u{1b}[0m.\n" - ); - } - - #[test] - fn code_style() { - let events = Parser::new("To render, call the `render` method.").map(Into::into); - - let output = Renderer::new(40, events).render(); - println!("{output}"); - assert_eq!( - output, - "To render, call the \u{1b}[38;5;250mrender\u{1b}[0m method.\n" - ); - } - - #[test] - fn heading() { - let text = "\ - # Heading 1\n\ - Some text\n\ - ## Heading 2\n\ - Some more text\ - "; - let events = Parser::new(text).map(Into::into); - let output = Renderer::new(40, events).render(); - println!("{output}"); - assert_eq!( - output, - "\u{1b}[1;4mHeading 1\u{1b}[0m\n\ - Some text\n\ - \u{1b}[1mHeading 2\u{1b}[0m\n\ - Some more text\n" - ) - } - - #[test] - fn wrapping() { - let text = "This is some very long text that will definitely need to get wrapped, so we better do that **right**!"; - let events = Parser::new(text).map(Into::into); - let output = Renderer::new(10, events).render(); - println!("{output}"); - - // The lone `!` at the end is technically a bug, because words across - // styles need be preserved, but that's a can of worms I don't want to - // open right now. You'd need to keep track of the last word and its - // width and render it either at the end of the block or at the start - // of the next inline Text or Code event. It could also happen that - // there are more than 2 styles per word. - assert_eq!( - output, - "This is\n\ - some very\n\ - long text\n\ - that will\n\ - definitely\n\ - need to\n\ - get\n\ - wrapped,\n\ - so we\n\ - better do\n\ - that \u{1b}[1mright\u{1b}[0m\n\ - !\n" - ) - } - - #[test] - fn soft_break() { - let text = "This is text\nwith a soft break."; - let events = Parser::new(text).map(Into::into); - let output = Renderer::new(40, events).render(); - println!("{output}"); - - assert_eq!(output, "This is text with a soft break.\n"); - } - - #[test] - fn hard_break() { - let text = "This is text\\\nwith a hard break."; - let events = Parser::new(text).map(Into::into); - let output = Renderer::new(40, events).render(); - println!("{output}"); - - assert_eq!(output, "This is text\nwith a hard break.\n"); - } - - #[test] - fn rule() { - let text = "This text has\n\n---\n\na rule!."; - - let events = Parser::new(text).map(Into::into); - let output = Renderer::new(40, events).render(); - println!("{output}"); - - assert_eq!( - output, - "This text has\n────────────────────────────────────────\na rule!.\n" - ); - } -} diff --git a/tests/coreutils/uniq.rs b/tests/coreutils/uniq.rs new file mode 100644 index 0000000..bf94e76 --- /dev/null +++ b/tests/coreutils/uniq.rs @@ -0,0 +1,105 @@ +use uutils_args::{Arguments, Initial, Options, Value}; + +// TODO: Deprecated syntax +#[derive(Arguments)] +enum Arg { + #[option("-f N", "--skip-fields=n")] + SkipFields(usize), + + #[option("-s N", "--skip-chars=N")] + SkipChars(usize), + + #[option("-c", "--count")] + Count, + + #[option("-i", "--ignore-case")] + IgnoreCase, + + #[option("-d", "--repeated")] + Repeated, + + #[option("-D", "--all-repeated[=delimit-method]")] + AllRepeated(Delimiters), + + #[option("--group[=delimit-method]", default=Delimiters::Separate)] + Group(Delimiters), + + #[option("-u", "--unique")] + Unique, + + #[option("-w N", "--check-chars=N")] + CheckChars(usize), + + #[option("-z", "--zero-terminated")] + ZeroTerminated, +} + +#[derive(Value, Default)] +enum Delimiters { + #[default] + #[value("none")] + None, + #[value("prepend")] + Prepend, + #[value("append")] + Append, + #[value("separate")] + Separate, + // Note: both is not an accepted argument of -D/--all-repeated + #[value("both")] + Both, +} + +#[derive(Initial)] +struct Settings { + repeats_only: bool, + uniques_only: bool, + all_repeated: bool, + delimiters: Delimiters, + show_counts: bool, + skip_fields: Option, + slice_start: Option, + slice_stop: Option, + ignore_case: bool, + zero_terminated: bool, +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::SkipFields(n) => { + self.skip_fields = Some(n); + }, + Arg::SkipChars(n) => { + self.slice_start = Some(n); + }, + Arg::Count => { + self.show_counts = true; + }, + Arg::IgnoreCase => { + self.ignore_case = true; + }, + Arg::Repeated => { + self.repeats_only = true; + } + Arg::AllRepeated(d) => { + self.repeats_only = true; + self.all_repeated = true; + self.delimiters = d; + }, + Arg::Group(d) => { + self.all_repeated = true; + self.delimiters = d; + }, + Arg::Unique => { + self.uniques_only = true; + }, + Arg::CheckChars(n) => { + self.slice_stop = Some(n); + } + Arg::ZeroTerminated => { + self.zero_terminated = true; + }, + } + } +} \ No newline at end of file