mod changelog;
mod cond;
mod deps;
mod expr;
mod files;
mod macros;
mod preamble;
mod scriptlet;
mod section;
mod text;
mod util;
use crate::ast::{Section, SpecFile, SpecItem};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TokenKind {
Plain,
TagName,
TagQualifier,
SectionKeyword,
ConditionalKeyword,
MacroDefKeyword,
MacroRef,
ShellMacro,
ExprMacro,
String,
Number,
Operator,
Comment,
ChangelogHeader,
ShellBody,
TextBody,
Flag,
}
pub trait PrintWriter {
fn emit(&mut self, kind: TokenKind, text: &str);
}
impl PrintWriter for String {
fn emit(&mut self, _kind: TokenKind, text: &str) {
self.push_str(text);
}
}
pub const FEDORA_PREAMBLE_VALUE_COLUMN: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrinterConfig {
pub indent: usize,
pub preamble_value_column: Option<usize>,
}
impl Default for PrinterConfig {
fn default() -> Self {
Self {
indent: 0,
preamble_value_column: Some(FEDORA_PREAMBLE_VALUE_COLUMN),
}
}
}
impl PrinterConfig {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_indent(mut self, spaces: usize) -> Self {
self.indent = spaces;
self
}
#[must_use]
pub fn with_preamble_value_column(mut self, col: Option<usize>) -> Self {
self.preamble_value_column = col;
self
}
}
pub fn print<T>(spec: &SpecFile<T>) -> String {
print_with(spec, &PrinterConfig::default())
}
pub fn print_with<T>(spec: &SpecFile<T>, cfg: &PrinterConfig) -> String {
let mut out = String::new();
print_to(spec, cfg, &mut out);
out
}
pub fn print_to<T>(spec: &SpecFile<T>, cfg: &PrinterConfig, w: &mut dyn PrintWriter) {
let mut p = Printer::new(w, cfg);
print_spec(&mut p, spec);
}
const BLANK_LINE_NEWLINES: u8 = 2;
pub(crate) struct Printer<'a> {
out: &'a mut dyn PrintWriter,
cfg: &'a PrinterConfig,
indent_level: usize,
trailing_newlines: u8,
emitted: bool,
}
impl<'a> Printer<'a> {
pub(crate) fn new(out: &'a mut dyn PrintWriter, cfg: &'a PrinterConfig) -> Self {
Self {
out,
cfg,
indent_level: 0,
trailing_newlines: 0,
emitted: false,
}
}
pub(crate) fn cfg(&self) -> &PrinterConfig {
self.cfg
}
pub(crate) fn emit(&mut self, kind: TokenKind, text: &str) {
if text.is_empty() {
return;
}
self.emitted = true;
let new_trailing = text.bytes().rev().take_while(|&b| b == b'\n').count();
let added = new_trailing.min(BLANK_LINE_NEWLINES as usize) as u8;
self.trailing_newlines = if new_trailing == text.len() {
self.trailing_newlines
.saturating_add(added)
.min(BLANK_LINE_NEWLINES)
} else {
added
};
self.out.emit(kind, text);
}
pub(crate) fn raw(&mut self, s: &str) {
self.emit(TokenKind::Plain, s);
}
pub(crate) fn raw_char(&mut self, c: char) {
let mut buf = [0u8; 4];
self.emit(TokenKind::Plain, c.encode_utf8(&mut buf));
}
pub(crate) fn write_indent(&mut self) {
let n = self.cfg.indent.saturating_mul(self.indent_level);
if n == 0 {
return;
}
const STACK: &str = " ";
if n <= STACK.len() {
self.raw(&STACK[..n]);
} else {
self.raw(&" ".repeat(n));
}
}
pub(crate) fn newline(&mut self) {
self.raw("\n");
}
pub(crate) fn nested<F: FnOnce(&mut Self)>(&mut self, body: F) {
self.indent_level += 1;
body(self);
self.indent_level -= 1;
}
pub(crate) fn ends_with_blank_line(&self) -> bool {
!self.emitted || self.trailing_newlines >= BLANK_LINE_NEWLINES
}
}
fn print_spec<T>(p: &mut Printer<'_>, spec: &SpecFile<T>) {
for (idx, item) in spec.items.iter().enumerate() {
let needs_blank_before = matches!(item, SpecItem::Section(_)) && idx > 0;
if needs_blank_before && !p.ends_with_blank_line() {
p.newline();
}
print_spec_item(p, item);
}
}
pub(crate) fn print_spec_item<T>(p: &mut Printer<'_>, item: &SpecItem<T>) {
match item {
SpecItem::Preamble(pi) => preamble::print_preamble_item(p, pi),
SpecItem::Section(sec) => print_section(p, sec.as_ref()),
SpecItem::Conditional(c) => {
cond::print_conditional(p, c, |p, it| print_spec_item(p, it));
}
SpecItem::MacroDef(m) => macros::print_macro_def(p, m),
SpecItem::BuildCondition(b) => macros::print_build_condition(p, b),
SpecItem::Include(i) => macros::print_include(p, i),
SpecItem::Statement(m) => {
p.write_indent();
let mut buf = String::new();
{
let mut tmp = Printer::new(&mut buf, p.cfg());
tmp.raw_char('%');
text::print_macro_ref_no_percent(&mut tmp, m);
}
p.emit(TokenKind::MacroRef, &buf);
p.newline();
}
SpecItem::Comment(c) => macros::print_comment(p, c),
SpecItem::Blank => {
p.newline();
}
}
}
fn print_section<T>(p: &mut Printer<'_>, section: &Section<T>) {
section::print_section(p, section);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{PreambleItem, Tag, TagValue, Text};
#[test]
fn default_config_is_no_indent_col16() {
let cfg = PrinterConfig::default();
assert_eq!(cfg.indent, 0);
assert_eq!(cfg.preamble_value_column, Some(16));
}
#[test]
fn builders_compose() {
let cfg = PrinterConfig::new()
.with_indent(4)
.with_preamble_value_column(None);
assert_eq!(cfg.indent, 4);
assert!(cfg.preamble_value_column.is_none());
}
#[test]
fn empty_spec_yields_empty_string() {
let spec: SpecFile<()> = SpecFile::default();
assert_eq!(print(&spec), "");
}
#[test]
fn single_preamble_item() {
let mut spec: SpecFile<()> = SpecFile::default();
spec.items.push(SpecItem::Preamble(PreambleItem {
tag: Tag::Name,
qualifiers: vec![],
lang: None,
value: TagValue::Text(Text::from("hello")),
data: (),
}));
let out = print(&spec);
assert!(out.starts_with("Name:"));
assert!(out.ends_with("hello\n"));
}
}