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.
This commit is contained in:
Terts Diepraam
2023-06-04 13:54:02 +02:00
parent d98f3caa7a
commit 0d7c1b747f
13 changed files with 398 additions and 655 deletions
-2
View File
@@ -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",
]
+1
View File
@@ -1,2 +1,3 @@
# uutils-args
An experimental derive-based argument parser specifically for uutils
+28 -42
View File
@@ -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(),
)
}
+236
View File
@@ -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::<Vec<_>>()
.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::<Vec<_>>()
.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<String> {
fn is_section_header(line: &str, section: &str) -> bool {
line.strip_prefix("##")
.map_or(false, |l| l.trim().to_lowercase() == section)
}
let section = &section.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::<Vec<_>>()
.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");
}
}
+2 -2
View File
@@ -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
}
-128
View File
@@ -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))
}
+6 -4
View File
@@ -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!
Wow!
+16 -2
View File
@@ -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<String> },
MissingValue {
option: Option<String>,
},
/// Some positional arguments were not given.
MissingPositionalArguments(Vec<String>),
@@ -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<std::io::Error> 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),
}
}
}
+4 -6
View File
@@ -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<T: Arguments> ArgumentIter<T> {
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<T: Arguments> ArgumentIter<T> {
}
}
fn help(&self) -> String {
fn help(&self) -> std::io::Result<()> {
T::help(self.parser.bin_name().unwrap())
}
-11
View File
@@ -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"
-131
View File
@@ -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<u64>),
// Item,
// FootnoteDefinition(CowStr<'a>),
// Table(Vec<Alignment>),
// 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<pulldown_cmark::HeadingLevel> 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<pulldown_cmark::CodeBlockKind<'a>> 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<pulldown_cmark::Alignment> 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<pulldown_cmark::Event<'a>> 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<pulldown_cmark::Tag<'a>> 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!(),
}
}
}
-327
View File
@@ -1,327 +0,0 @@
mod event;
pub use event::*;
use nu_ansi_term::{Color, Style};
use unicode_width::UnicodeWidthStr;
pub struct Renderer<T: Iterator<Item = Event>> {
// 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<T: Iterator<Item = Event>> Renderer<T> {
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"
);
}
}
+105
View File
@@ -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<usize>,
slice_start: Option<usize>,
slice_stop: Option<usize>,
ignore_case: bool,
zero_terminated: bool,
}
impl Options<Arg> 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;
},
}
}
}