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 verbatim(self, value: impl Into<String>) -> Self {
41 self.block(Block::Verbatim(value.into()))
42 }
43
44 #[must_use]
46 pub fn fields(self, fields: Fields) -> Self {
47 self.block(fields)
48 }
49
50 #[must_use]
52 pub fn table(self, table: Table) -> Self {
53 self.block(table)
54 }
55
56 #[must_use]
58 pub fn section(self, section: Section) -> Self {
59 self.block(section)
60 }
61
62 #[must_use]
64 pub fn notice(self, notice: Notice) -> Self {
65 self.block(notice)
66 }
67
68 #[must_use]
70 pub fn rule(self, title: Option<Text>) -> Self {
71 self.block(Rule { title })
72 }
73
74 #[must_use]
76 pub fn is_empty(&self) -> bool {
77 self.blocks.is_empty()
78 }
79
80 #[must_use]
82 pub fn blocks(&self) -> &[Block] {
83 &self.blocks
84 }
85}
86
87#[derive(Clone, Debug, Eq, PartialEq)]
89pub enum Block {
90 Heading(Text),
92 Paragraph(Text),
94 Verbatim(String),
98 Fields(Fields),
100 Table(Table),
102 Section(Section),
104 Notice(Notice),
106 Rule(Rule),
108}
109
110impl From<Fields> for Block {
111 fn from(value: Fields) -> Self {
112 Self::Fields(value)
113 }
114}
115
116impl From<Table> for Block {
117 fn from(value: Table) -> Self {
118 Self::Table(value)
119 }
120}
121
122impl From<Section> for Block {
123 fn from(value: Section) -> Self {
124 Self::Section(value)
125 }
126}
127
128impl From<Notice> for Block {
129 fn from(value: Notice) -> Self {
130 Self::Notice(value)
131 }
132}
133
134impl From<Rule> for Block {
135 fn from(value: Rule) -> Self {
136 Self::Rule(value)
137 }
138}
139
140#[derive(Clone, Debug, Default, Eq, PartialEq)]
142pub struct Text {
143 spans: Vec<Span>,
144}
145
146impl Text {
147 #[must_use]
149 pub const fn new() -> Self {
150 Self { spans: Vec::new() }
151 }
152
153 #[must_use]
155 pub fn plain(value: impl Into<String>) -> Self {
156 Self::new().span(Role::Plain, value)
157 }
158
159 #[must_use]
161 pub fn span(mut self, role: Role, value: impl Into<String>) -> Self {
162 self.spans.push(Span {
163 role,
164 value: value.into(),
165 });
166 self
167 }
168
169 #[must_use]
171 pub fn then(self, value: impl Into<String>) -> Self {
172 self.span(Role::Plain, value)
173 }
174
175 #[must_use]
177 pub fn token(self, value: impl Into<String>) -> Self {
178 self.span(Role::Token, value)
179 }
180
181 #[must_use]
183 pub fn value(self, value: impl Into<String>) -> Self {
184 self.span(Role::Value, value)
185 }
186
187 #[must_use]
189 pub fn muted(self, value: impl Into<String>) -> Self {
190 self.span(Role::Muted, value)
191 }
192
193 #[must_use]
195 pub fn success(self, value: impl Into<String>) -> Self {
196 self.span(Role::Success, value)
197 }
198
199 #[must_use]
201 pub fn warning(self, value: impl Into<String>) -> Self {
202 self.span(Role::Warning, value)
203 }
204
205 #[must_use]
207 pub fn error(self, value: impl Into<String>) -> Self {
208 self.span(Role::Error, value)
209 }
210
211 #[must_use]
213 pub fn spans(&self) -> &[Span] {
214 &self.spans
215 }
216
217 #[must_use]
219 pub fn is_empty(&self) -> bool {
220 self.spans.iter().all(|span| span.value.is_empty())
221 }
222}
223
224impl From<&str> for Text {
225 fn from(value: &str) -> Self {
226 Self::plain(value)
227 }
228}
229
230impl From<String> for Text {
231 fn from(value: String) -> Self {
232 Self::plain(value)
233 }
234}
235
236#[derive(Clone, Debug, Eq, PartialEq)]
238pub struct Span {
239 role: Role,
240 value: String,
241}
242
243impl Span {
244 #[must_use]
246 pub const fn role(&self) -> Role {
247 self.role
248 }
249
250 #[must_use]
252 pub fn value(&self) -> &str {
253 &self.value
254 }
255}
256
257#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
259pub enum Role {
260 #[default]
262 Plain,
263 Heading,
265 Success,
267 Warning,
269 Error,
271 Value,
273 Muted,
275 Token,
277}
278
279#[derive(Clone, Debug, Default, Eq, PartialEq)]
281pub struct Fields {
282 rows: Vec<(Text, Text)>,
283}
284
285impl Fields {
286 #[must_use]
288 pub const fn new() -> Self {
289 Self { rows: Vec::new() }
290 }
291
292 #[must_use]
294 pub fn row(mut self, label: impl Into<String>, value: impl Into<Text>) -> Self {
295 self.rows.push((Text::new().token(label), value.into()));
296 self
297 }
298
299 #[must_use]
301 pub fn text_row(mut self, label: impl Into<Text>, value: impl Into<Text>) -> Self {
302 self.rows.push((label.into(), value.into()));
303 self
304 }
305
306 #[must_use]
308 pub fn rows(&self) -> &[(Text, Text)] {
309 &self.rows
310 }
311
312 #[must_use]
314 pub fn is_empty(&self) -> bool {
315 self.rows.is_empty()
316 }
317}
318
319#[derive(Clone, Debug, Default, Eq, PartialEq)]
321pub struct Table {
322 headers: Vec<Text>,
323 rows: Vec<Vec<Text>>,
324 token_column: Option<usize>,
325 stacked_below: Option<Stacked>,
326}
327
328impl Table {
329 #[must_use]
331 pub fn plain() -> Self {
332 Self::default()
333 }
334
335 #[must_use]
337 pub fn new(headers: impl IntoIterator<Item = impl Into<Text>>) -> Self {
338 Self {
339 headers: headers.into_iter().map(Into::into).collect(),
340 ..Self::default()
341 }
342 }
343
344 #[must_use]
346 pub const fn token_column(mut self, index: usize) -> Self {
347 self.token_column = Some(index);
348 self
349 }
350
351 #[must_use]
356 pub const fn stacked_below(mut self, width: u16, label_columns: usize) -> Self {
357 self.stacked_below = Some(Stacked {
358 width,
359 label_columns,
360 });
361 self
362 }
363
364 #[must_use]
366 pub fn row(mut self, cells: impl IntoIterator<Item = impl Into<Text>>) -> Self {
367 self.rows.push(cells.into_iter().map(Into::into).collect());
368 self
369 }
370
371 #[must_use]
373 pub fn headers(&self) -> &[Text] {
374 &self.headers
375 }
376
377 #[must_use]
379 pub fn rows(&self) -> &[Vec<Text>] {
380 &self.rows
381 }
382
383 #[must_use]
385 pub const fn token_column_index(&self) -> Option<usize> {
386 self.token_column
387 }
388
389 #[must_use]
391 pub const fn stacked(&self) -> Option<Stacked> {
392 self.stacked_below
393 }
394
395 #[must_use]
397 pub fn is_empty(&self) -> bool {
398 self.rows.is_empty()
399 }
400}
401
402#[derive(Clone, Copy, Debug, Eq, PartialEq)]
404pub struct Stacked {
405 width: u16,
406 label_columns: usize,
407}
408
409impl Stacked {
410 #[must_use]
412 pub const fn width(self) -> u16 {
413 self.width
414 }
415
416 #[must_use]
418 pub const fn label_columns(self) -> usize {
419 self.label_columns
420 }
421}
422
423#[derive(Clone, Debug, Eq, PartialEq)]
425pub struct Section {
426 title: Text,
427 body: Document,
428}
429
430impl Section {
431 #[must_use]
433 pub fn new(title: impl Into<Text>, body: Document) -> Self {
434 Self {
435 title: title.into(),
436 body,
437 }
438 }
439
440 #[must_use]
442 pub const fn title(&self) -> &Text {
443 &self.title
444 }
445
446 #[must_use]
448 pub const fn body(&self) -> &Document {
449 &self.body
450 }
451}
452
453#[derive(Clone, Debug, Eq, PartialEq)]
455pub struct Notice {
456 level: NoticeLevel,
457 code: Option<String>,
458 message: Text,
459}
460
461impl Notice {
462 #[must_use]
464 pub fn new(level: NoticeLevel, message: impl Into<Text>) -> Self {
465 Self {
466 level,
467 code: None,
468 message: message.into(),
469 }
470 }
471
472 #[must_use]
474 pub fn code(mut self, code: impl Into<String>) -> Self {
475 self.code = Some(code.into());
476 self
477 }
478
479 #[must_use]
481 pub const fn level(&self) -> NoticeLevel {
482 self.level
483 }
484
485 #[must_use]
487 pub fn code_value(&self) -> Option<&str> {
488 self.code.as_deref()
489 }
490
491 #[must_use]
493 pub const fn message(&self) -> &Text {
494 &self.message
495 }
496}
497
498#[derive(Clone, Copy, Debug, Eq, PartialEq)]
500pub enum NoticeLevel {
501 Success,
503 Warning,
505 Error,
507}
508
509#[derive(Clone, Debug, Eq, PartialEq)]
511pub struct Rule {
512 title: Option<Text>,
513}
514
515impl Rule {
516 #[must_use]
518 pub const fn title(&self) -> Option<&Text> {
519 self.title.as_ref()
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::{Document, Fields, Notice, NoticeLevel, Role, Table, Text};
526
527 #[test]
528 fn fluent_document_preserves_semantics() {
529 let document = Document::new()
530 .heading("status")
531 .fields(Fields::new().row("pending", "2"))
532 .table(
533 Table::new(["id", "title"])
534 .token_column(0)
535 .row(["A-1", "Ship"]),
536 )
537 .notice(Notice::new(NoticeLevel::Warning, "stale"));
538
539 assert_eq!(document.blocks().len(), 4);
540 let token = Text::new().token("--force").then(" writes");
541 assert_eq!(token.spans()[0].role(), Role::Token);
542 assert_eq!(token.spans()[1].role(), Role::Plain);
543 }
544}