1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! Typed debian-control-style AST + pretty-printer.
//!
//! Per the ★★★ NO-format!()-for-code prime directive. Used by the
//! control-style ecosystems: R's DESCRIPTION + Haskell's .cabal file
//! + Debian control + RPM spec headers + any future "Key: value with
//! indented continuation lines and optional indented sub-stanzas"
//! grammar.
//!
//! The shape:
//! Key: value
//! Stanza
//! sub-key: sub-value
//!
//! No quoting (values are bare strings); colon-separator; indented
//! continuation is a single-space prefix in debian-control proper +
//! 2-space prefix in Cabal — the renderer takes the indent width as
//! a parameter.
use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Line {
/// A bare comment — emitted with leading `-- ` (Haskell-flavor).
Comment(String),
/// A blank line — separates stanza groups.
Blank,
/// `Key: value` field. Value is rendered as-is (no quoting; control
/// format treats the entire RHS as a string).
Field { key: String, value: String },
/// `Header\n body…` — a named stanza with indented child lines.
/// Body lines are indented `indent_width` spaces per the format.
Stanza { header: String, body: Vec<Line> },
}
pub fn render(lines: &[Line], indent_width: usize) -> String {
let mut out = String::new();
render_at(lines, 0, indent_width, &mut out);
out
}
/// Typed wrapper bundling the lines with the format's indent-width
/// (1 for debian-control, 2 for Cabal). Implements `ast::Render` so
/// the unified surface works without per-call indent arguments.
#[derive(Debug, Clone)]
pub struct Document {
pub lines: Vec<Line>,
pub indent_width: usize,
}
impl Document {
pub fn new(indent_width: usize) -> Self {
Self { lines: Vec::new(), indent_width }
}
pub fn from(indent_width: usize, lines: impl IntoIterator<Item = Line>) -> Self {
Self { lines: lines.into_iter().collect(), indent_width }
}
pub fn push(&mut self, line: Line) -> &mut Self { self.lines.push(line); self }
}
fn render_at(lines: &[Line], depth: usize, iw: usize, out: &mut String) {
let pad = " ".repeat(depth * iw);
for line in lines {
match line {
Line::Comment(c) => {
let _ = writeln!(out, "{pad}-- {c}");
}
Line::Blank => out.push('\n'),
Line::Field { key, value } => {
let _ = writeln!(out, "{pad}{key}: {value}");
}
Line::Stanza { header, body } => {
let _ = writeln!(out, "{pad}{header}");
render_at(body, depth + 1, iw, out);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn field_emits_key_colon_value() {
let out = render(
&[Line::Field { key: "Package".into(), value: "demo".into() }],
2,
);
assert_eq!(out, "Package: demo\n");
}
#[test]
fn stanza_indents_body_by_iw() {
let out = render(
&[Line::Stanza {
header: "library".into(),
body: vec![
Line::Field { key: "exposed-modules".into(), value: "Lib".into() },
Line::Field { key: "build-depends".into(), value: "base".into() },
],
}],
2,
);
assert!(out.starts_with("library\n"));
assert!(out.contains(" exposed-modules: Lib\n"));
assert!(out.contains(" build-depends: base\n"));
}
#[test]
fn blank_line_separates() {
let out = render(
&[
Line::Field { key: "Name".into(), value: "x".into() },
Line::Blank,
Line::Field { key: "Version".into(), value: "1".into() },
],
1,
);
assert_eq!(out, "Name: x\n\nVersion: 1\n");
}
}