#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Document {
blocks: Vec<Block>,
}
impl Document {
#[must_use]
pub const fn new() -> Self {
Self { blocks: Vec::new() }
}
#[must_use]
pub fn block(mut self, block: impl Into<Block>) -> Self {
self.blocks.push(block.into());
self
}
#[must_use]
pub fn heading(self, value: impl Into<Text>) -> Self {
self.block(Block::Heading(value.into()))
}
#[must_use]
pub fn paragraph(self, value: impl Into<Text>) -> Self {
self.block(Block::Paragraph(value.into()))
}
#[must_use]
pub fn verbatim(self, value: impl Into<String>) -> Self {
self.block(Block::Verbatim(value.into()))
}
#[must_use]
pub fn fields(self, fields: Fields) -> Self {
self.block(fields)
}
#[must_use]
pub fn table(self, table: Table) -> Self {
self.block(table)
}
#[must_use]
pub fn section(self, section: Section) -> Self {
self.block(section)
}
#[must_use]
pub fn notice(self, notice: Notice) -> Self {
self.block(notice)
}
#[must_use]
pub fn rule(self, title: Option<Text>) -> Self {
self.block(Rule { title })
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
#[must_use]
pub fn blocks(&self) -> &[Block] {
&self.blocks
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Block {
Heading(Text),
Paragraph(Text),
Verbatim(String),
Fields(Fields),
Table(Table),
Section(Section),
Notice(Notice),
Rule(Rule),
}
impl From<Fields> for Block {
fn from(value: Fields) -> Self {
Self::Fields(value)
}
}
impl From<Table> for Block {
fn from(value: Table) -> Self {
Self::Table(value)
}
}
impl From<Section> for Block {
fn from(value: Section) -> Self {
Self::Section(value)
}
}
impl From<Notice> for Block {
fn from(value: Notice) -> Self {
Self::Notice(value)
}
}
impl From<Rule> for Block {
fn from(value: Rule) -> Self {
Self::Rule(value)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Text {
spans: Vec<Span>,
}
impl Text {
#[must_use]
pub const fn new() -> Self {
Self { spans: Vec::new() }
}
#[must_use]
pub fn plain(value: impl Into<String>) -> Self {
Self::new().span(Role::Plain, value)
}
#[must_use]
pub fn span(mut self, role: Role, value: impl Into<String>) -> Self {
self.spans.push(Span {
role,
value: value.into(),
});
self
}
#[must_use]
pub fn then(self, value: impl Into<String>) -> Self {
self.span(Role::Plain, value)
}
#[must_use]
pub fn token(self, value: impl Into<String>) -> Self {
self.span(Role::Token, value)
}
#[must_use]
pub fn value(self, value: impl Into<String>) -> Self {
self.span(Role::Value, value)
}
#[must_use]
pub fn muted(self, value: impl Into<String>) -> Self {
self.span(Role::Muted, value)
}
#[must_use]
pub fn success(self, value: impl Into<String>) -> Self {
self.span(Role::Success, value)
}
#[must_use]
pub fn warning(self, value: impl Into<String>) -> Self {
self.span(Role::Warning, value)
}
#[must_use]
pub fn error(self, value: impl Into<String>) -> Self {
self.span(Role::Error, value)
}
#[must_use]
pub fn spans(&self) -> &[Span] {
&self.spans
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.spans.iter().all(|span| span.value.is_empty())
}
}
impl From<&str> for Text {
fn from(value: &str) -> Self {
Self::plain(value)
}
}
impl From<String> for Text {
fn from(value: String) -> Self {
Self::plain(value)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Span {
role: Role,
value: String,
}
impl Span {
#[must_use]
pub const fn role(&self) -> Role {
self.role
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Role {
#[default]
Plain,
Heading,
Success,
Warning,
Error,
Value,
Muted,
Token,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Fields {
rows: Vec<(Text, Text)>,
}
impl Fields {
#[must_use]
pub const fn new() -> Self {
Self { rows: Vec::new() }
}
#[must_use]
pub fn row(mut self, label: impl Into<String>, value: impl Into<Text>) -> Self {
self.rows.push((Text::new().token(label), value.into()));
self
}
#[must_use]
pub fn text_row(mut self, label: impl Into<Text>, value: impl Into<Text>) -> Self {
self.rows.push((label.into(), value.into()));
self
}
#[must_use]
pub fn rows(&self) -> &[(Text, Text)] {
&self.rows
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Table {
headers: Vec<Text>,
rows: Vec<Vec<Text>>,
token_column: Option<usize>,
stacked_below: Option<Stacked>,
}
impl Table {
#[must_use]
pub fn plain() -> Self {
Self::default()
}
#[must_use]
pub fn new(headers: impl IntoIterator<Item = impl Into<Text>>) -> Self {
Self {
headers: headers.into_iter().map(Into::into).collect(),
..Self::default()
}
}
#[must_use]
pub const fn token_column(mut self, index: usize) -> Self {
self.token_column = Some(index);
self
}
#[must_use]
pub const fn stacked_below(mut self, width: u16, label_columns: usize) -> Self {
self.stacked_below = Some(Stacked {
width,
label_columns,
});
self
}
#[must_use]
pub fn row(mut self, cells: impl IntoIterator<Item = impl Into<Text>>) -> Self {
self.rows.push(cells.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn headers(&self) -> &[Text] {
&self.headers
}
#[must_use]
pub fn rows(&self) -> &[Vec<Text>] {
&self.rows
}
#[must_use]
pub const fn token_column_index(&self) -> Option<usize> {
self.token_column
}
#[must_use]
pub const fn stacked(&self) -> Option<Stacked> {
self.stacked_below
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Stacked {
width: u16,
label_columns: usize,
}
impl Stacked {
#[must_use]
pub const fn width(self) -> u16 {
self.width
}
#[must_use]
pub const fn label_columns(self) -> usize {
self.label_columns
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Section {
title: Text,
body: Document,
}
impl Section {
#[must_use]
pub fn new(title: impl Into<Text>, body: Document) -> Self {
Self {
title: title.into(),
body,
}
}
#[must_use]
pub const fn title(&self) -> &Text {
&self.title
}
#[must_use]
pub const fn body(&self) -> &Document {
&self.body
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Notice {
level: NoticeLevel,
code: Option<String>,
message: Text,
}
impl Notice {
#[must_use]
pub fn new(level: NoticeLevel, message: impl Into<Text>) -> Self {
Self {
level,
code: None,
message: message.into(),
}
}
#[must_use]
pub fn code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
#[must_use]
pub const fn level(&self) -> NoticeLevel {
self.level
}
#[must_use]
pub fn code_value(&self) -> Option<&str> {
self.code.as_deref()
}
#[must_use]
pub const fn message(&self) -> &Text {
&self.message
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NoticeLevel {
Success,
Warning,
Error,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Rule {
title: Option<Text>,
}
impl Rule {
#[must_use]
pub const fn title(&self) -> Option<&Text> {
self.title.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::{Document, Fields, Notice, NoticeLevel, Role, Table, Text};
#[test]
fn fluent_document_preserves_semantics() {
let document = Document::new()
.heading("status")
.fields(Fields::new().row("pending", "2"))
.table(
Table::new(["id", "title"])
.token_column(0)
.row(["A-1", "Ship"]),
)
.notice(Notice::new(NoticeLevel::Warning, "stale"));
assert_eq!(document.blocks().len(), 4);
let token = Text::new().token("--force").then(" writes");
assert_eq!(token.spans()[0].role(), Role::Token);
assert_eq!(token.spans()[1].role(), Role::Plain);
}
}