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 id(self, value: impl Into<String>) -> Self {
184 self.span(Role::Id, value)
185 }
186
187 #[must_use]
189 pub fn value(self, value: impl Into<String>) -> Self {
190 self.span(Role::Value, value)
191 }
192
193 #[must_use]
195 pub fn muted(self, value: impl Into<String>) -> Self {
196 self.span(Role::Muted, value)
197 }
198
199 #[must_use]
201 pub fn success(self, value: impl Into<String>) -> Self {
202 self.span(Role::Success, value)
203 }
204
205 #[must_use]
207 pub fn warning(self, value: impl Into<String>) -> Self {
208 self.span(Role::Warning, value)
209 }
210
211 #[must_use]
213 pub fn error(self, value: impl Into<String>) -> Self {
214 self.span(Role::Error, value)
215 }
216
217 #[must_use]
219 pub fn spans(&self) -> &[Span] {
220 &self.spans
221 }
222
223 #[must_use]
225 pub fn is_empty(&self) -> bool {
226 self.spans.iter().all(|span| span.value.is_empty())
227 }
228}
229
230impl From<&str> for Text {
231 fn from(value: &str) -> Self {
232 Self::plain(value)
233 }
234}
235
236impl From<String> for Text {
237 fn from(value: String) -> Self {
238 Self::plain(value)
239 }
240}
241
242#[derive(Clone, Debug, Eq, PartialEq)]
244pub struct Span {
245 role: Role,
246 value: String,
247}
248
249impl Span {
250 #[must_use]
252 pub const fn role(&self) -> Role {
253 self.role
254 }
255
256 #[must_use]
258 pub fn value(&self) -> &str {
259 &self.value
260 }
261}
262
263#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
265#[non_exhaustive]
266pub enum Role {
267 #[default]
269 Plain,
270 Heading,
272 Success,
274 Warning,
276 Error,
278 Value,
280 Muted,
282 Token,
284 Id,
286}
287
288#[derive(Clone, Debug, Default, Eq, PartialEq)]
290pub struct Fields {
291 rows: Vec<(Text, Text)>,
292}
293
294impl Fields {
295 #[must_use]
297 pub const fn new() -> Self {
298 Self { rows: Vec::new() }
299 }
300
301 #[must_use]
303 pub fn row(mut self, label: impl Into<String>, value: impl Into<Text>) -> Self {
304 self.rows.push((Text::new().token(label), value.into()));
305 self
306 }
307
308 #[must_use]
310 pub fn text_row(mut self, label: impl Into<Text>, value: impl Into<Text>) -> Self {
311 self.rows.push((label.into(), value.into()));
312 self
313 }
314
315 #[must_use]
317 pub fn rows(&self) -> &[(Text, Text)] {
318 &self.rows
319 }
320
321 #[must_use]
323 pub fn is_empty(&self) -> bool {
324 self.rows.is_empty()
325 }
326}
327
328#[derive(Clone, Debug, Default, Eq, PartialEq)]
330pub struct Table {
331 headers: Vec<Text>,
332 rows: Vec<Vec<Text>>,
333 token_column: Option<usize>,
334 id_column: Option<usize>,
335 stacked_below: Option<Stacked>,
336}
337
338impl Table {
339 #[must_use]
341 pub fn plain() -> Self {
342 Self::default()
343 }
344
345 #[must_use]
347 pub fn new(headers: impl IntoIterator<Item = impl Into<Text>>) -> Self {
348 Self {
349 headers: headers.into_iter().map(Into::into).collect(),
350 ..Self::default()
351 }
352 }
353
354 #[must_use]
356 pub const fn token_column(mut self, index: usize) -> Self {
357 self.token_column = Some(index);
358 self
359 }
360
361 #[must_use]
363 pub const fn id_column(mut self, index: usize) -> Self {
364 self.id_column = Some(index);
365 self
366 }
367
368 #[must_use]
373 pub const fn stacked_below(mut self, width: u16, label_columns: usize) -> Self {
374 self.stacked_below = Some(Stacked {
375 width,
376 label_columns,
377 });
378 self
379 }
380
381 #[must_use]
383 pub fn row(mut self, cells: impl IntoIterator<Item = impl Into<Text>>) -> Self {
384 self.rows.push(cells.into_iter().map(Into::into).collect());
385 self
386 }
387
388 #[must_use]
390 pub fn headers(&self) -> &[Text] {
391 &self.headers
392 }
393
394 #[must_use]
396 pub fn rows(&self) -> &[Vec<Text>] {
397 &self.rows
398 }
399
400 #[must_use]
402 pub const fn token_column_index(&self) -> Option<usize> {
403 self.token_column
404 }
405
406 #[must_use]
408 pub const fn id_column_index(&self) -> Option<usize> {
409 self.id_column
410 }
411
412 #[must_use]
414 pub const fn stacked(&self) -> Option<Stacked> {
415 self.stacked_below
416 }
417
418 #[must_use]
420 pub fn is_empty(&self) -> bool {
421 self.rows.is_empty()
422 }
423}
424
425#[derive(Clone, Copy, Debug, Eq, PartialEq)]
427pub struct Stacked {
428 width: u16,
429 label_columns: usize,
430}
431
432impl Stacked {
433 #[must_use]
435 pub const fn width(self) -> u16 {
436 self.width
437 }
438
439 #[must_use]
441 pub const fn label_columns(self) -> usize {
442 self.label_columns
443 }
444}
445
446#[derive(Clone, Debug, Eq, PartialEq)]
448pub struct Section {
449 title: Text,
450 body: Document,
451}
452
453impl Section {
454 #[must_use]
456 pub fn new(title: impl Into<Text>, body: Document) -> Self {
457 Self {
458 title: title.into(),
459 body,
460 }
461 }
462
463 #[must_use]
465 pub const fn title(&self) -> &Text {
466 &self.title
467 }
468
469 #[must_use]
471 pub const fn body(&self) -> &Document {
472 &self.body
473 }
474}
475
476#[derive(Clone, Debug, Eq, PartialEq)]
478pub struct Notice {
479 level: NoticeLevel,
480 code: Option<String>,
481 message: Text,
482}
483
484impl Notice {
485 #[must_use]
487 pub fn new(level: NoticeLevel, message: impl Into<Text>) -> Self {
488 Self {
489 level,
490 code: None,
491 message: message.into(),
492 }
493 }
494
495 #[must_use]
497 pub fn code(mut self, code: impl Into<String>) -> Self {
498 self.code = Some(code.into());
499 self
500 }
501
502 #[must_use]
504 pub const fn level(&self) -> NoticeLevel {
505 self.level
506 }
507
508 #[must_use]
510 pub fn code_value(&self) -> Option<&str> {
511 self.code.as_deref()
512 }
513
514 #[must_use]
516 pub const fn message(&self) -> &Text {
517 &self.message
518 }
519}
520
521#[derive(Clone, Copy, Debug, Eq, PartialEq)]
523pub enum NoticeLevel {
524 Success,
526 Warning,
528 Error,
530}
531
532#[derive(Clone, Debug, Eq, PartialEq)]
534pub struct Rule {
535 title: Option<Text>,
536}
537
538impl Rule {
539 #[must_use]
541 pub const fn title(&self) -> Option<&Text> {
542 self.title.as_ref()
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::{Document, Fields, Notice, NoticeLevel, Role, Table, Text};
549
550 #[test]
551 fn fluent_document_preserves_semantics() {
552 let document = Document::new()
553 .heading("status")
554 .fields(Fields::new().row("pending", "2"))
555 .table(
556 Table::new(["id", "title"])
557 .token_column(0)
558 .row(["A-1", "Ship"]),
559 )
560 .notice(Notice::new(NoticeLevel::Warning, "stale"));
561
562 assert_eq!(document.blocks().len(), 4);
563 let token = Text::new().token("--force").then(" writes");
564 assert_eq!(token.spans()[0].role(), Role::Token);
565 assert_eq!(token.spans()[1].role(), Role::Plain);
566 }
567}