#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(clippy::print_stderr)]
#![warn(clippy::print_stdout)]
use std::io::Write;
use std::write;
#[derive(Debug, PartialEq, Eq, Default)]
pub struct Roff {
lines: Vec<Line>,
}
impl Roff {
pub fn new() -> Self {
Default::default()
}
pub fn control<'a>(
&mut self,
name: impl Into<String>,
args: impl IntoIterator<Item = &'a str>,
) -> &mut Self {
self.lines.push(Line::control(
name.into(),
args.into_iter().map(|s| s.to_owned()).collect(),
));
self
}
pub fn text(&mut self, inlines: impl Into<Vec<Inline>>) -> &mut Self {
self.lines.push(Line::text(inlines.into()));
self
}
pub fn render(&self) -> String {
let mut buf = vec![];
self.to_writer(&mut buf).unwrap(); String::from_utf8(buf)
.expect("output is utf8 if all input is utf8 and our API guarantees that")
}
pub fn to_writer(&self, w: &mut dyn Write) -> Result<(), std::io::Error> {
w.write_all(APOSTROPHE_PREABMLE.as_bytes())?;
for line in self.lines.iter() {
line.render(w, Apostrophes::Handle)?;
}
Ok(())
}
pub fn to_roff(&self) -> String {
let mut buf = vec![];
for line in self.lines.iter() {
line.render(&mut buf, Apostrophes::DontHandle).unwrap();
}
String::from_utf8(buf)
.expect("output is utf8 if all input is utf8 and our API guarantees that")
}
}
impl<I: Into<Inline>> From<I> for Roff {
fn from(other: I) -> Self {
let mut r = Roff::new();
r.text([other.into()]);
r
}
}
impl<R: Into<Roff>> FromIterator<R> for Roff {
fn from_iter<I: IntoIterator<Item = R>>(iter: I) -> Self {
let mut r = Roff::new();
for i in iter {
r.lines.extend(i.into().lines);
}
r
}
}
impl<R: Into<Roff>> Extend<R> for Roff {
fn extend<T: IntoIterator<Item = R>>(&mut self, iter: T) {
for i in iter {
self.lines.extend(i.into().lines);
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Inline {
Roman(String),
Italic(String),
Bold(String),
LineBreak,
}
impl<S: Into<String>> From<S> for Inline {
fn from(s: S) -> Self {
roman(s)
}
}
pub fn roman(input: impl Into<String>) -> Inline {
Inline::Roman(input.into())
}
pub fn bold(input: impl Into<String>) -> Inline {
Inline::Bold(input.into())
}
pub fn italic(input: impl Into<String>) -> Inline {
Inline::Italic(input.into())
}
pub fn line_break() -> Inline {
Inline::LineBreak
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) enum Line {
Control {
name: String,
args: Vec<String>,
},
Text(Vec<Inline>),
}
impl Line {
pub(crate) fn control(name: String, args: Vec<String>) -> Self {
Self::Control { name, args }
}
pub(crate) fn text(parts: Vec<Inline>) -> Self {
Self::Text(parts)
}
fn render(
&self,
out: &mut dyn Write,
handle_apostrophes: Apostrophes,
) -> Result<(), std::io::Error> {
match self {
Self::Control { name, args } => {
write!(out, ".{name}")?;
for arg in args {
write!(out, " {}", &escape_spaces(arg))?;
}
}
Self::Text(inlines) => {
let mut at_line_start = true;
for inline in inlines.iter() {
match inline {
Inline::LineBreak => {
if at_line_start {
writeln!(out, ".br")?;
} else {
writeln!(out, "\n.br")?;
}
}
Inline::Roman(text) | Inline::Italic(text) | Inline::Bold(text) => {
let mut text = escape_inline(text);
if handle_apostrophes == Apostrophes::Handle {
text = escape_apostrophes(&text);
};
let text = escape_leading_cc(&text);
if let Inline::Bold(_) = inline {
write!(out, r"\fB{text}\fR")?;
} else if let Inline::Italic(_) = inline {
write!(out, r"\fI{text}\fR")?;
} else {
if at_line_start && starts_with_cc(&text) {
write!(out, r"\&").unwrap();
}
write!(out, "{text}")?;
}
}
}
at_line_start = false;
}
}
};
writeln!(out)?;
Ok(())
}
}
fn starts_with_cc(line: &str) -> bool {
line.starts_with('.') || line.starts_with('\'')
}
fn escape_spaces(w: &str) -> String {
if w.contains(' ') {
format!("\"{w}\"")
} else {
w.to_owned()
}
}
fn escape_leading_cc(s: &str) -> String {
s.replace("\n.", "\n\\&.").replace("\n'", "\n\\&'")
}
fn escape_inline(text: &str) -> String {
text.replace('\\', r"\\").replace('-', r"\-")
}
fn escape_apostrophes(text: &str) -> String {
text.replace('\'', APOSTROPHE)
}
#[derive(Eq, PartialEq)]
enum Apostrophes {
Handle,
DontHandle,
}
const APOSTROPHE: &str = r"\*(Aq";
const APOSTROPHE_PREABMLE: &str = r#".ie \n(.g .ds Aq \(aq
.el .ds Aq '
"#;
#[cfg(test)]
mod test {
use super::*;
#[test]
fn escape_dash() {
assert_eq!(r"\-", escape_inline("-"));
}
#[test]
fn escape_backslash() {
assert_eq!(r"\\x", escape_inline(r"\x"));
}
#[test]
fn escape_backslash_and_dash() {
assert_eq!(r"\\\-", escape_inline(r"\-"));
}
#[test]
fn escapes_leading_control_chars() {
assert_eq!("foo\n\\&.bar\n\\&'yo", escape_leading_cc("foo\n.bar\n'yo"));
}
#[test]
fn escape_plain() {
assert_eq!("abc", escape_inline("abc"));
}
#[test]
fn render_roman() {
let text = Roff::new().text([roman("foo")]).to_roff();
assert_eq!(text, "foo\n");
}
#[test]
fn render_dash() {
let text = Roff::new().text([roman("foo-bar")]).to_roff();
assert_eq!(text, "foo\\-bar\n");
}
#[test]
fn render_italic() {
let text = Roff::new().text([italic("foo")]).to_roff();
assert_eq!(text, "\\fIfoo\\fR\n");
}
#[test]
fn render_bold() {
let text = Roff::new().text([bold("foo")]).to_roff();
assert_eq!(text, "\\fBfoo\\fR\n");
}
#[test]
fn render_text() {
let text = Roff::new().text([roman("roman")]).to_roff();
assert_eq!(text, "roman\n");
}
#[test]
fn render_text_with_leading_period() {
let text = Roff::new().text([roman(".roman")]).to_roff();
assert_eq!(text, "\\&.roman\n");
}
#[test]
fn render_text_with_newline_period() {
let text = Roff::new().text([roman("foo\n.roman")]).to_roff();
assert_eq!(text, "foo\n\\&.roman\n");
}
#[test]
fn render_line_break() {
let text = Roff::new()
.text([roman("roman"), Inline::LineBreak, roman("more")])
.to_roff();
assert_eq!(text, "roman\n.br\nmore\n");
}
#[test]
fn render_control() {
let text = Roff::new().control("foo", ["bar", "foo and bar"]).to_roff();
assert_eq!(text, ".foo bar \"foo and bar\"\n");
}
}
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;