use super::{
escape::{Apostrophes, Escape},
monoid::FreeMonoid,
};
use std::ops::{Add, AddAssign};
#[derive(Debug, Default, Clone)]
pub(crate) struct Roff {
payload: FreeMonoid<Escape>,
pub strip_newlines: bool,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum Font {
Roman,
Bold,
Italic,
}
pub(crate) const RESTORE_FONT: &str = "\\fP";
impl Font {
pub(crate) fn escape(self) -> &'static str {
match self {
Font::Bold => "\\fB",
Font::Italic => "\\fI",
Font::Roman => "\\fR",
}
}
}
impl Roff {
#[must_use]
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn strip_newlines(&mut self, state: bool) -> &mut Self {
self.strip_newlines = state;
self
}
pub(crate) fn control<S, I>(&mut self, name: &str, args: I) -> &mut Self
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
self.payload.push_str(Escape::UnescapedAtNewline, ".");
self.payload.push_str(Escape::Unescaped, name);
for arg in args {
let mut s = arg.as_ref();
if s.is_empty() {
s = "\"\"";
}
self.payload
.push_str(Escape::Unescaped, " ")
.push_str(Escape::Spaces, s);
}
self.payload.push_str(Escape::UnescapedAtNewline, "");
self
}
pub(crate) fn control0(&mut self, name: &str) -> &mut Self {
self.payload.push_str(Escape::UnescapedAtNewline, ".");
self.payload.push_str(Escape::Unescaped, name);
self.payload.push_str(Escape::UnescapedAtNewline, "");
self
}
pub(crate) fn roff_linebreak(&mut self) -> &mut Self {
self.payload.push_str(Escape::UnescapedAtNewline, "");
self
}
pub(crate) fn escape(&mut self, arg: &str) -> &mut Self {
self.payload.push_str(Escape::Unescaped, arg);
self
}
pub(crate) fn plaintext(&mut self, text: &str) -> &mut Self {
if self.strip_newlines {
self.payload.push_str(Escape::SpecialNoNewline, text);
} else {
self.payload.push_str(Escape::Special, text);
}
self
}
pub(crate) fn text(&mut self, text: &[(Font, &str)]) -> &mut Self {
let mut prev_font = None;
for (font, item) in text {
if prev_font == Some(font) {
self.plaintext(item.as_ref());
} else {
let escape = font.escape();
self.escape(escape).plaintext(item.as_ref());
prev_font = Some(font);
}
}
if prev_font.is_some() {
self.escape(RESTORE_FONT);
}
self
}
#[must_use]
pub(crate) fn render(&self, ap: Apostrophes) -> String {
let mut res = Vec::with_capacity(self.payload.payload_size() * 2);
if ap == Apostrophes::Handle {
res.extend(super::escape::APOSTROPHE_PREABMLE.as_bytes());
}
super::escape::escape(&self.payload, &mut res, ap);
String::from_utf8(res).expect("Should be valid utf8 by construction")
}
}
impl AddAssign<&Roff> for Roff {
fn add_assign(&mut self, rhs: &Roff) {
self.payload += &rhs.payload;
self.strip_newlines = rhs.strip_newlines;
}
}
impl Add<&Roff> for Roff {
type Output = Self;
fn add(mut self, rhs: &Roff) -> Self::Output {
self += rhs;
self
}
}
impl<'a> Extend<&'a Roff> for Roff {
fn extend<I: IntoIterator<Item = &'a Roff>>(&mut self, iter: I) {
for i in iter {
*self += i;
}
}
}
#[cfg(test)]
mod test {
use super::{Apostrophes, Font, Roff};
const NO_AP: Apostrophes = Apostrophes::DontHandle;
#[test]
fn escape_dash_in_plaintext() {
let text = Roff::default().plaintext("-").render(NO_AP);
assert_eq!(r"\-", text);
}
#[test]
fn escape_backslash_in_plaintext() {
let text = Roff::default().plaintext(r"\x").render(NO_AP);
assert_eq!(r"\\x", text);
}
#[test]
fn escape_backslash_and_dash_in_plaintext() {
let text = Roff::default().plaintext(r"\-").render(NO_AP);
assert_eq!(r"\\\-", text);
}
#[test]
fn escapes_leading_control_chars_and_space_in_plaintext() {
let text = Roff::default()
.plaintext("foo\n.bar\n'yo\n hmm")
.render(NO_AP);
assert_eq!("foo\n\\&.bar\n\\&'yo\n hmm", text);
}
#[test]
fn escape_plain_in_plaintext() {
let text = Roff::default().plaintext("abc").render(NO_AP);
assert_eq!("abc", text);
}
#[test]
fn render_dash_in_plaintext() {
let text = Roff::default().plaintext("foo-bar").render(NO_AP);
assert_eq!("foo\\-bar", text);
}
#[test]
fn render_dash_in_font() {
let text = Roff::default()
.text(&[(Font::Roman, "foo-bar")])
.render(NO_AP);
assert_eq!(text, "\\fRfoo\\-bar\\fP");
}
#[test]
fn render_roman() {
let text = Roff::default().text(&[(Font::Roman, "foo")]).render(NO_AP);
assert_eq!("\\fRfoo\\fP", text);
}
#[test]
fn render_italic() {
let text = Roff::default().text(&[(Font::Italic, "foo")]).render(NO_AP);
assert_eq!("\\fIfoo\\fP", text);
}
#[test]
fn render_bold() {
let text = Roff::default().text(&[(Font::Bold, "foo")]).render(NO_AP);
assert_eq!("\\fBfoo\\fP", text);
}
#[test]
fn render_text_roman() {
let text = Roff::default()
.text(&[(Font::Roman, "roman")])
.render(NO_AP);
assert_eq!("\\fRroman\\fP", text);
}
#[test]
fn render_text_with_leading_period() {
let text = Roff::default()
.text(&[(Font::Roman, ".roman")])
.render(NO_AP);
assert_eq!("\\fR.roman\\fP", text);
}
#[test]
fn render_text_with_newline_period() {
let text = Roff::default()
.text(&[(Font::Roman, "foo\n.roman")])
.render(NO_AP);
assert_eq!(text, "\\fRfoo\n\\&.roman\\fP");
}
#[test]
fn render_line_break() {
let text = Roff::default()
.text(&[(Font::Roman, "roman\n")])
.control("br", None::<&str>)
.text(&[(Font::Roman, "more\n")])
.render(NO_AP);
assert_eq!(text, "\\fRroman\n\\fP\n.br\n\\fRmore\n\\fP");
}
#[test]
fn render_control() {
let text = Roff::default()
.control("foo", ["bar", "foo and bar"])
.render(NO_AP);
assert_eq!(".foo bar foo\\ and\\ bar\n", text);
}
#[test]
fn twice_bold() {
let text = Roff::default()
.text(&[
(Font::Bold, "bold,"),
(Font::Roman, " more bold"),
(Font::Bold, " and more bold"),
])
.render(NO_AP);
assert_eq!(text, "\\fBbold,\\fR more bold\\fB and more bold\\fP");
}
#[test]
fn multiple_controls() {
let text = Roff::default()
.control("br", None::<&str>)
.control0("br")
.control("br", None::<&str>)
.render(NO_AP);
assert_eq!(".br\n.br\n.br\n", text);
}
}