1#[derive(Clone, Debug, Default, Eq, PartialEq)]
8pub struct Document {
9 blocks: Vec<Block>,
10}
11
12impl Document {
13 #[must_use]
15 pub const fn new() -> Self {
16 Self { blocks: Vec::new() }
17 }
18
19 #[must_use]
21 pub fn block(mut self, block: impl Into<Block>) -> Self {
22 self.blocks.push(block.into());
23 self
24 }
25
26 #[must_use]
28 pub fn heading(self, value: impl Into<Text>) -> Self {
29 self.block(Block::Heading(value.into()))
30 }
31
32 #[must_use]
34 pub fn paragraph(self, value: impl Into<Text>) -> Self {
35 self.block(Block::Paragraph(value.into()))
36 }
37
38 #[must_use]
40 pub fn fields(self, fields: Fields) -> Self {
41 self.block(fields)
42 }
43
44 #[must_use]
46 pub fn table(self, table: Table) -> Self {
47 self.block(table)
48 }
49
50 #[must_use]
52 pub fn section(self, section: Section) -> Self {
53 self.block(section)
54 }
55
56 #[must_use]
58 pub fn notice(self, notice: Notice) -> Self {
59 self.block(notice)
60 }
61
62 #[must_use]
64 pub fn rule(self, title: Option<Text>) -> Self {
65 self.block(Rule { title })
66 }
67
68 #[must_use]
70 pub fn is_empty(&self) -> bool {
71 self.blocks.is_empty()
72 }
73
74 #[must_use]
76 pub fn blocks(&self) -> &[Block] {
77 &self.blocks
78 }
79}
80
81#[derive(Clone, Debug, Eq, PartialEq)]
83pub enum Block {
84 Heading(Text),
86 Paragraph(Text),
88 Fields(Fields),
90 Table(Table),
92 Section(Section),
94 Notice(Notice),
96 Rule(Rule),
98}
99
100impl From<Fields> for Block {
101 fn from(value: Fields) -> Self {
102 Self::Fields(value)
103 }
104}
105
106impl From<Table> for Block {
107 fn from(value: Table) -> Self {
108 Self::Table(value)
109 }
110}
111
112impl From<Section> for Block {
113 fn from(value: Section) -> Self {
114 Self::Section(value)
115 }
116}
117
118impl From<Notice> for Block {
119 fn from(value: Notice) -> Self {
120 Self::Notice(value)
121 }
122}
123
124impl From<Rule> for Block {
125 fn from(value: Rule) -> Self {
126 Self::Rule(value)
127 }
128}
129
130#[derive(Clone, Debug, Default, Eq, PartialEq)]
132pub struct Text {
133 spans: Vec<Span>,
134}
135
136impl Text {
137 #[must_use]
139 pub const fn new() -> Self {
140 Self { spans: Vec::new() }
141 }
142
143 #[must_use]
145 pub fn plain(value: impl Into<String>) -> Self {
146 Self::new().span(Role::Plain, value)
147 }
148
149 #[must_use]
151 pub fn span(mut self, role: Role, value: impl Into<String>) -> Self {
152 self.spans.push(Span {
153 role,
154 value: value.into(),
155 });
156 self
157 }
158
159 #[must_use]
161 pub fn then(self, value: impl Into<String>) -> Self {
162 self.span(Role::Plain, value)
163 }
164
165 #[must_use]
167 pub fn token(self, value: impl Into<String>) -> Self {
168 self.span(Role::Token, value)
169 }
170
171 #[must_use]
173 pub fn value(self, value: impl Into<String>) -> Self {
174 self.span(Role::Value, value)
175 }
176
177 #[must_use]
179 pub fn muted(self, value: impl Into<String>) -> Self {
180 self.span(Role::Muted, value)
181 }
182
183 #[must_use]
185 pub fn success(self, value: impl Into<String>) -> Self {
186 self.span(Role::Success, value)
187 }
188
189 #[must_use]
191 pub fn warning(self, value: impl Into<String>) -> Self {
192 self.span(Role::Warning, value)
193 }
194
195 #[must_use]
197 pub fn error(self, value: impl Into<String>) -> Self {
198 self.span(Role::Error, value)
199 }
200
201 #[must_use]
203 pub fn spans(&self) -> &[Span] {
204 &self.spans
205 }
206
207 #[must_use]
209 pub fn is_empty(&self) -> bool {
210 self.spans.iter().all(|span| span.value.is_empty())
211 }
212}
213
214impl From<&str> for Text {
215 fn from(value: &str) -> Self {
216 Self::plain(value)
217 }
218}
219
220impl From<String> for Text {
221 fn from(value: String) -> Self {
222 Self::plain(value)
223 }
224}
225
226#[derive(Clone, Debug, Eq, PartialEq)]
228pub struct Span {
229 role: Role,
230 value: String,
231}
232
233impl Span {
234 #[must_use]
236 pub const fn role(&self) -> Role {
237 self.role
238 }
239
240 #[must_use]
242 pub fn value(&self) -> &str {
243 &self.value
244 }
245}
246
247#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
249pub enum Role {
250 #[default]
252 Plain,
253 Heading,
255 Success,
257 Warning,
259 Error,
261 Value,
263 Muted,
265 Token,
267}
268
269#[derive(Clone, Debug, Default, Eq, PartialEq)]
271pub struct Fields {
272 rows: Vec<(Text, Text)>,
273}
274
275impl Fields {
276 #[must_use]
278 pub const fn new() -> Self {
279 Self { rows: Vec::new() }
280 }
281
282 #[must_use]
284 pub fn row(mut self, label: impl Into<String>, value: impl Into<Text>) -> Self {
285 self.rows.push((Text::new().token(label), value.into()));
286 self
287 }
288
289 #[must_use]
291 pub fn text_row(mut self, label: impl Into<Text>, value: impl Into<Text>) -> Self {
292 self.rows.push((label.into(), value.into()));
293 self
294 }
295
296 #[must_use]
298 pub fn rows(&self) -> &[(Text, Text)] {
299 &self.rows
300 }
301
302 #[must_use]
304 pub fn is_empty(&self) -> bool {
305 self.rows.is_empty()
306 }
307}
308
309#[derive(Clone, Debug, Default, Eq, PartialEq)]
311pub struct Table {
312 headers: Vec<Text>,
313 rows: Vec<Vec<Text>>,
314 token_column: Option<usize>,
315 stacked_below: Option<Stacked>,
316}
317
318impl Table {
319 #[must_use]
321 pub fn plain() -> Self {
322 Self::default()
323 }
324
325 #[must_use]
327 pub fn new(headers: impl IntoIterator<Item = impl Into<Text>>) -> Self {
328 Self {
329 headers: headers.into_iter().map(Into::into).collect(),
330 ..Self::default()
331 }
332 }
333
334 #[must_use]
336 pub const fn token_column(mut self, index: usize) -> Self {
337 self.token_column = Some(index);
338 self
339 }
340
341 #[must_use]
346 pub const fn stacked_below(mut self, width: u16, label_columns: usize) -> Self {
347 self.stacked_below = Some(Stacked {
348 width,
349 label_columns,
350 });
351 self
352 }
353
354 #[must_use]
356 pub fn row(mut self, cells: impl IntoIterator<Item = impl Into<Text>>) -> Self {
357 self.rows.push(cells.into_iter().map(Into::into).collect());
358 self
359 }
360
361 #[must_use]
363 pub fn headers(&self) -> &[Text] {
364 &self.headers
365 }
366
367 #[must_use]
369 pub fn rows(&self) -> &[Vec<Text>] {
370 &self.rows
371 }
372
373 #[must_use]
375 pub const fn token_column_index(&self) -> Option<usize> {
376 self.token_column
377 }
378
379 #[must_use]
381 pub const fn stacked(&self) -> Option<Stacked> {
382 self.stacked_below
383 }
384
385 #[must_use]
387 pub fn is_empty(&self) -> bool {
388 self.rows.is_empty()
389 }
390}
391
392#[derive(Clone, Copy, Debug, Eq, PartialEq)]
394pub struct Stacked {
395 width: u16,
396 label_columns: usize,
397}
398
399impl Stacked {
400 #[must_use]
402 pub const fn width(self) -> u16 {
403 self.width
404 }
405
406 #[must_use]
408 pub const fn label_columns(self) -> usize {
409 self.label_columns
410 }
411}
412
413#[derive(Clone, Debug, Eq, PartialEq)]
415pub struct Section {
416 title: Text,
417 body: Document,
418}
419
420impl Section {
421 #[must_use]
423 pub fn new(title: impl Into<Text>, body: Document) -> Self {
424 Self {
425 title: title.into(),
426 body,
427 }
428 }
429
430 #[must_use]
432 pub const fn title(&self) -> &Text {
433 &self.title
434 }
435
436 #[must_use]
438 pub const fn body(&self) -> &Document {
439 &self.body
440 }
441}
442
443#[derive(Clone, Debug, Eq, PartialEq)]
445pub struct Notice {
446 level: NoticeLevel,
447 code: Option<String>,
448 message: Text,
449}
450
451impl Notice {
452 #[must_use]
454 pub fn new(level: NoticeLevel, message: impl Into<Text>) -> Self {
455 Self {
456 level,
457 code: None,
458 message: message.into(),
459 }
460 }
461
462 #[must_use]
464 pub fn code(mut self, code: impl Into<String>) -> Self {
465 self.code = Some(code.into());
466 self
467 }
468
469 #[must_use]
471 pub const fn level(&self) -> NoticeLevel {
472 self.level
473 }
474
475 #[must_use]
477 pub fn code_value(&self) -> Option<&str> {
478 self.code.as_deref()
479 }
480
481 #[must_use]
483 pub const fn message(&self) -> &Text {
484 &self.message
485 }
486}
487
488#[derive(Clone, Copy, Debug, Eq, PartialEq)]
490pub enum NoticeLevel {
491 Success,
493 Warning,
495 Error,
497}
498
499#[derive(Clone, Debug, Eq, PartialEq)]
501pub struct Rule {
502 title: Option<Text>,
503}
504
505impl Rule {
506 #[must_use]
508 pub const fn title(&self) -> Option<&Text> {
509 self.title.as_ref()
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::{Document, Fields, Notice, NoticeLevel, Role, Table, Text};
516
517 #[test]
518 fn fluent_document_preserves_semantics() {
519 let document = Document::new()
520 .heading("status")
521 .fields(Fields::new().row("pending", "2"))
522 .table(
523 Table::new(["id", "title"])
524 .token_column(0)
525 .row(["A-1", "Ship"]),
526 )
527 .notice(Notice::new(NoticeLevel::Warning, "stale"));
528
529 assert_eq!(document.blocks().len(), 4);
530 let token = Text::new().token("--force").then(" writes");
531 assert_eq!(token.spans()[0].role(), Role::Token);
532 assert_eq!(token.spans()[1].role(), Role::Plain);
533 }
534}