blob: 937f0d594ac07b7fd46c1e06236981e274ec41c5 [file] [log] [blame]
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
borrow::Cow,
fmt::{Result, Write},
};
/// Number of space used to indent lines when no alignement is required.
pub(crate) const INDENTATION_SIZE: usize = 2;
/// A list of [`Block`] possibly rendered with a [`Decoration`].
///
/// This is the top-level renderable component, corresponding to the description
/// or match explanation of a single matcher.
///
/// The constituent [`Block`] of a `List` can be decorated with either bullets
/// (`* `) or enumeration (`0. `, `1. `, ...). This is controlled via the
/// methods [`List::bullet_list`] and [`List::enumerate`]. By default, there is
/// no decoration.
///
/// A `List` can be constructed as follows:
///
/// * [`Default::default()`] constructs an empty `List`.
/// * [`Iterator::collect()`] on an [`Iterator`] of [`Block`].
/// * [`Iterator::collect()`] on an [`Iterator`] of `String`, which produces a
/// [`Block::Literal`] for each `String`.
/// * [`Iterator::collect()`] on an [`Iterator`] of `List`, which produces a
/// [`Block::Nested`] for each `List`.
#[derive(Debug, Default)]
pub(crate) struct List(Vec<Block>, Decoration);
impl List {
/// Render this instance using the formatter `f`.
///
/// Indent each line of output by `indentation` spaces.
pub(crate) fn render(&self, f: &mut dyn Write, indentation: usize) -> Result {
self.render_with_prefix(f, indentation, "".into())
}
/// Append a new [`Block`] containing `literal`.
///
/// The input `literal` is split into lines so that each line will be
/// indented correctly.
pub(crate) fn push_literal(&mut self, literal: Cow<'static, str>) {
self.0.push(literal.into());
}
/// Append a new [`Block`] containing `inner` as a nested [`List`].
pub(crate) fn push_nested(&mut self, inner: List) {
self.0.push(Block::Nested(inner));
}
/// Render each [`Block`] of this instance preceded with a bullet "* ".
pub(crate) fn bullet_list(self) -> Self {
Self(self.0, Decoration::Bullet)
}
/// Render each [`Block`] of this instance preceded with its 0-based index.
pub(crate) fn enumerate(self) -> Self {
Self(self.0, Decoration::Enumerate)
}
/// Return the number of [`Block`] in this instance.
pub(crate) fn len(&self) -> usize {
self.0.len()
}
/// Return `true` if there are no [`Block`] in this instance, `false`
/// otherwise.
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
fn render_with_prefix(
&self,
f: &mut dyn Write,
indentation: usize,
prefix: Cow<'static, str>,
) -> Result {
if self.0.is_empty() {
return Ok(());
}
let enumeration_padding = self.enumeration_padding();
self.0[0].render(
f,
indentation,
self.full_prefix(0, enumeration_padding, &prefix).into(),
)?;
for (index, block) in self.0[1..].iter().enumerate() {
writeln!(f)?;
block.render(
f,
indentation + prefix.len(),
self.prefix(index + 1, enumeration_padding),
)?;
}
Ok(())
}
fn full_prefix(&self, index: usize, enumeration_padding: usize, prior_prefix: &str) -> String {
format!("{prior_prefix}{}", self.prefix(index, enumeration_padding))
}
fn prefix(&self, index: usize, enumeration_padding: usize) -> Cow<'static, str> {
match self.1 {
Decoration::None => "".into(),
Decoration::Bullet => "* ".into(),
Decoration::Enumerate => format!("{:>enumeration_padding$}. ", index).into(),
}
}
fn enumeration_padding(&self) -> usize {
match self.1 {
Decoration::None => 0,
Decoration::Bullet => 0,
Decoration::Enumerate => {
if self.0.len() > 1 {
((self.0.len() - 1) as f64).log10().floor() as usize + 1
} else {
// Avoid negative logarithm when there is only 0 or 1 element.
1
}
}
}
}
}
impl FromIterator<Block> for List {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = Block>,
{
Self(iter.into_iter().collect(), Decoration::None)
}
}
impl<ElementT: Into<Cow<'static, str>>> FromIterator<ElementT> for List {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = ElementT>,
{
Self(iter.into_iter().map(|b| b.into().into()).collect(), Decoration::None)
}
}
impl FromIterator<List> for List {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = List>,
{
Self(iter.into_iter().map(Block::nested).collect(), Decoration::None)
}
}
/// A sequence of [`Fragment`] or a nested [`List`].
///
/// This may be rendered with a prefix specified by the [`Decoration`] of the
/// containing [`List`]. In this case, all lines are indented to align with the
/// first character of the first line of the block.
#[derive(Debug)]
enum Block {
/// A block of text.
///
/// Each constituent [`Fragment`] contains one line of text. The lines are
/// indented uniformly to the current indentation of this block when
/// rendered.
Literal(Vec<Fragment>),
/// A nested [`List`].
///
/// The [`List`] is rendered recursively at the next level of indentation.
Nested(List),
}
impl Block {
fn nested(inner: List) -> Self {
Self::Nested(inner)
}
fn render(&self, f: &mut dyn Write, indentation: usize, prefix: Cow<'static, str>) -> Result {
match self {
Self::Literal(fragments) => {
if fragments.is_empty() {
return Ok(());
}
write!(f, "{:indentation$}{prefix}", "")?;
fragments[0].render(f)?;
let block_indentation = indentation + prefix.as_ref().len();
for fragment in &fragments[1..] {
writeln!(f)?;
write!(f, "{:block_indentation$}", "")?;
fragment.render(f)?;
}
Ok(())
}
Self::Nested(inner) => inner.render_with_prefix(
f,
indentation + INDENTATION_SIZE.saturating_sub(prefix.len()),
prefix,
),
}
}
}
impl From<String> for Block {
fn from(value: String) -> Self {
Block::Literal(value.lines().map(|v| Fragment(v.to_string().into())).collect())
}
}
impl From<&'static str> for Block {
fn from(value: &'static str) -> Self {
Block::Literal(value.lines().map(|v| Fragment(v.into())).collect())
}
}
impl From<Cow<'static, str>> for Block {
fn from(value: Cow<'static, str>) -> Self {
match value {
Cow::Borrowed(value) => value.into(),
Cow::Owned(value) => value.into(),
}
}
}
/// A string representing one line of a description or match explanation.
#[derive(Debug)]
struct Fragment(Cow<'static, str>);
impl Fragment {
fn render(&self, f: &mut dyn Write) -> Result {
write!(f, "{}", self.0)
}
}
/// The decoration which appears on [`Block`] of a [`List`] when rendered.
#[derive(Debug, Default)]
enum Decoration {
/// No decoration on each [`Block`]. The default.
#[default]
None,
/// Each [`Block`] is preceded by a bullet (`* `).
Bullet,
/// Each [`Block`] is preceded by its index in the [`List`] (`0. `, `1. `,
/// ...).
Enumerate,
}
#[cfg(test)]
mod tests {
use super::{Block, Fragment, List};
use crate::prelude::*;
use indoc::indoc;
#[test]
fn renders_fragment() -> Result<()> {
let fragment = Fragment("A fragment".into());
let mut result = String::new();
fragment.render(&mut result)?;
verify_that!(result, eq("A fragment"))
}
#[test]
fn renders_empty_block() -> Result<()> {
let block = Block::Literal(vec![]);
let mut result = String::new();
block.render(&mut result, 0, "".into())?;
verify_that!(result, eq(""))
}
#[test]
fn renders_block_with_one_fragment() -> Result<()> {
let block: Block = "A fragment".into();
let mut result = String::new();
block.render(&mut result, 0, "".into())?;
verify_that!(result, eq("A fragment"))
}
#[test]
fn renders_block_with_two_fragments() -> Result<()> {
let block: Block = "A fragment\nAnother fragment".into();
let mut result = String::new();
block.render(&mut result, 0, "".into())?;
verify_that!(result, eq("A fragment\nAnother fragment"))
}
#[test]
fn renders_indented_block() -> Result<()> {
let block: Block = "A fragment\nAnother fragment".into();
let mut result = String::new();
block.render(&mut result, 2, "".into())?;
verify_that!(result, eq(" A fragment\n Another fragment"))
}
#[test]
fn renders_block_with_prefix() -> Result<()> {
let block: Block = "A fragment\nAnother fragment".into();
let mut result = String::new();
block.render(&mut result, 0, "* ".into())?;
verify_that!(result, eq("* A fragment\n Another fragment"))
}
#[test]
fn renders_indented_block_with_prefix() -> Result<()> {
let block: Block = "A fragment\nAnother fragment".into();
let mut result = String::new();
block.render(&mut result, 2, "* ".into())?;
verify_that!(result, eq(" * A fragment\n Another fragment"))
}
#[test]
fn renders_empty_list() -> Result<()> {
let list = list(vec![]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(""))
}
#[test]
fn renders_plain_list_with_one_block() -> Result<()> {
let list = list(vec!["A fragment".into()]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("A fragment"))
}
#[test]
fn renders_plain_list_with_two_blocks() -> Result<()> {
let list = list(vec!["A fragment".into(), "A fragment in a second block".into()]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("A fragment\nA fragment in a second block"))
}
#[test]
fn renders_plain_list_with_one_block_with_two_fragments() -> Result<()> {
let list = list(vec!["A fragment\nA second fragment".into()]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("A fragment\nA second fragment"))
}
#[test]
fn renders_nested_plain_list_with_one_block() -> Result<()> {
let list = list(vec![Block::nested(list(vec!["A fragment".into()]))]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" A fragment"))
}
#[test]
fn renders_nested_plain_list_with_two_blocks() -> Result<()> {
let list = list(vec![Block::nested(list(vec![
"A fragment".into(),
"A fragment in a second block".into(),
]))]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" A fragment\n A fragment in a second block"))
}
#[test]
fn renders_nested_plain_list_with_one_block_with_two_fragments() -> Result<()> {
let list = list(vec![Block::nested(list(vec!["A fragment\nA second fragment".into()]))]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" A fragment\n A second fragment"))
}
#[test]
fn renders_bulleted_list_with_one_block() -> Result<()> {
let list = list(vec!["A fragment".into()]).bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* A fragment"))
}
#[test]
fn renders_bulleted_list_with_two_blocks() -> Result<()> {
let list =
list(vec!["A fragment".into(), "A fragment in a second block".into()]).bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* A fragment\n* A fragment in a second block"))
}
#[test]
fn renders_bulleted_list_with_one_block_with_two_fragments() -> Result<()> {
let list = list(vec!["A fragment\nA second fragment".into()]).bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* A fragment\n A second fragment"))
}
#[test]
fn renders_nested_bulleted_list_with_one_block() -> Result<()> {
let list = list(vec![Block::nested(list(vec!["A fragment".into()]).bullet_list())]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" * A fragment"))
}
#[test]
fn renders_nested_bulleted_list_with_two_blocks() -> Result<()> {
let list = list(vec![Block::nested(
list(vec!["A fragment".into(), "A fragment in a second block".into()]).bullet_list(),
)]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" * A fragment\n * A fragment in a second block"))
}
#[test]
fn renders_nested_bulleted_list_with_one_block_with_two_fragments() -> Result<()> {
let list = list(vec![Block::nested(
list(vec!["A fragment\nA second fragment".into()]).bullet_list(),
)]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" * A fragment\n A second fragment"))
}
#[test]
fn renders_enumerated_list_with_one_block() -> Result<()> {
let list = list(vec!["A fragment".into()]).enumerate();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("0. A fragment"))
}
#[test]
fn renders_enumerated_list_with_two_blocks() -> Result<()> {
let list =
list(vec!["A fragment".into(), "A fragment in a second block".into()]).enumerate();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("0. A fragment\n1. A fragment in a second block"))
}
#[test]
fn renders_enumerated_list_with_one_block_with_two_fragments() -> Result<()> {
let list = list(vec!["A fragment\nA second fragment".into()]).enumerate();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("0. A fragment\n A second fragment"))
}
#[test]
fn aligns_renders_enumerated_list_with_more_than_ten_blocks() -> Result<()> {
let list =
(0..11).map(|i| Block::from(format!("Fragment {i}"))).collect::<List>().enumerate();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(
result,
eq(indoc! {"
0. Fragment 0
1. Fragment 1
2. Fragment 2
3. Fragment 3
4. Fragment 4
5. Fragment 5
6. Fragment 6
7. Fragment 7
8. Fragment 8
9. Fragment 9
10. Fragment 10"})
)
}
#[test]
fn renders_fragment_plus_nested_plain_list_with_one_block() -> Result<()> {
let list =
list(vec!["A fragment".into(), Block::nested(list(vec!["Another fragment".into()]))]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("A fragment\n Another fragment"))
}
#[test]
fn renders_double_nested_plain_list_with_one_block() -> Result<()> {
let list =
list(vec![Block::nested(list(vec![Block::nested(list(vec!["A fragment".into()]))]))]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq(" A fragment"))
}
#[test]
fn renders_headers_plus_double_nested_plain_list() -> Result<()> {
let list = list(vec![
"First header".into(),
Block::nested(list(vec![
"Second header".into(),
Block::nested(list(vec!["A fragment".into()])),
])),
]);
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("First header\n Second header\n A fragment"))
}
#[test]
fn renders_double_nested_bulleted_list() -> Result<()> {
let list =
list(vec![Block::nested(list(vec!["A fragment".into()]).bullet_list())]).bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* * A fragment"))
}
#[test]
fn renders_nested_enumeration_with_two_blocks_inside_bulleted_list() -> Result<()> {
let list =
list(vec![Block::nested(list(vec!["Block 1".into(), "Block 2".into()]).enumerate())])
.bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* 0. Block 1\n 1. Block 2"))
}
#[test]
fn renders_nested_enumeration_with_block_with_two_fragments_inside_bulleted_list() -> Result<()>
{
let list = list(vec![Block::nested(
list(vec!["A fragment\nAnother fragment".into()]).enumerate(),
)])
.bullet_list();
let mut result = String::new();
list.render(&mut result, 0)?;
verify_that!(result, eq("* 0. A fragment\n Another fragment"))
}
fn list(blocks: Vec<Block>) -> List {
List(blocks, super::Decoration::None)
}
}