627 lines
19 KiB
Rust
627 lines
19 KiB
Rust
// 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()
|
|
}
|
|
|
|
/// Append a new [`List`] in the last element which must be a
|
|
/// [`Block::Nested`]. Panic if `self` is empty or the last element is
|
|
/// not [`Block::Nested`].
|
|
pub(crate) fn push_at_end(&mut self, list: List) {
|
|
if let Some(Block::Nested(self_list)) = self.0.last_mut() {
|
|
self_list.push_nested(list);
|
|
} else {
|
|
panic!("pushing elements at the end of {self:#?} which last element is not Nested")
|
|
}
|
|
}
|
|
|
|
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!("{index:>enumeration_padding$}. ").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 crate::Result;
|
|
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)
|
|
}
|
|
}
|