1use serde::{Deserialize, Serialize};
7
8use crate::style::{Style, Stylize};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum Alignment {
13 #[default]
15 Left,
16 Center,
18 Right,
20}
21
22#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
24#[serde(untagged)]
25pub enum Span {
26 Str(String),
28 Full {
30 content: String,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 style: Option<Style>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 link: Option<String>,
38 },
39}
40
41impl Default for Span {
42 fn default() -> Self {
43 Self::Str(String::new())
44 }
45}
46
47impl Span {
48 #[must_use]
50 pub fn from_string(content: impl Into<String>) -> Self {
51 Self::Full {
52 content: content.into(),
53 style: None,
54 link: None,
55 }
56 }
57
58 #[must_use]
60 pub fn linked(content: impl Into<String>, link: impl Into<String>) -> Self {
61 Self::Full {
62 content: content.into(),
63 style: None,
64 link: Some(link.into()),
65 }
66 }
67
68 #[must_use]
70 pub fn with_link(self, link: impl Into<String>) -> Self {
71 match self {
72 Self::Str(content) => Self::Full {
73 content,
74 style: None,
75 link: Some(link.into()),
76 },
77 Self::Full {
78 content,
79 style,
80 link: _,
81 } => Self::Full {
82 content,
83 style,
84 link: Some(link.into()),
85 },
86 }
87 }
88
89 #[must_use]
91 pub fn content(&self) -> &str {
92 match self {
93 Self::Str(content) | Self::Full { content, .. } => content,
94 }
95 }
96
97 #[must_use]
99 pub fn style(&self) -> Option<&Style> {
100 match self {
101 Self::Str(_) => None,
102 Self::Full { style, .. } => style.as_ref(),
103 }
104 }
105
106 #[must_use]
108 pub fn link(&self) -> Option<&str> {
109 match self {
110 Self::Str(_) => None,
111 Self::Full { link, .. } => link.as_deref(),
112 }
113 }
114
115 #[must_use]
117 pub fn effective_style(&self) -> Style {
118 self.style().cloned().unwrap_or_default()
119 }
120}
121
122impl From<&str> for Span {
123 fn from(s: &str) -> Self {
124 Self::Str(s.to_owned())
125 }
126}
127
128impl From<String> for Span {
129 fn from(s: String) -> Self {
130 Self::Str(s)
131 }
132}
133
134impl Stylize<Span> for Span {
135 fn style(self, style: Style) -> Span {
136 match self {
137 Self::Str(content) => Self::Full {
138 content,
139 style: Some(style),
140 link: None,
141 },
142 Self::Full {
143 content,
144 style: existing,
145 link,
146 } => Self::Full {
147 content,
148 style: Some(existing.unwrap_or_default().patch(style)),
149 link,
150 },
151 }
152 }
153}
154
155impl Stylize<Span> for &str {
156 fn style(self, style: Style) -> Span {
157 Span::Full {
158 content: self.to_owned(),
159 style: Some(style),
160 link: None,
161 }
162 }
163}
164
165impl Stylize<Span> for String {
166 fn style(self, style: Style) -> Span {
167 Span::Full {
168 content: self,
169 style: Some(style),
170 link: None,
171 }
172 }
173}
174
175#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
177#[serde(untagged)]
178pub enum Line {
179 Str(String),
181 Spans(Vec<Span>),
183 Full {
185 spans: Vec<Span>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 style: Option<Style>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 alignment: Option<Alignment>,
193 },
194}
195
196impl Default for Line {
197 fn default() -> Self {
198 Self::Str(String::new())
199 }
200}
201
202impl Line {
203 #[must_use]
205 pub fn from_spans(spans: Vec<Span>) -> Self {
206 Self::Full {
207 spans,
208 style: None,
209 alignment: None,
210 }
211 }
212
213 #[must_use]
215 pub fn to_text(&self) -> String {
216 let spans: &[Span] = match self {
217 Self::Str(_) => return String::new(),
218 Self::Spans(spans) | Self::Full { spans, .. } => spans,
219 };
220 let mut out = String::new();
221 for span in spans {
222 out.push_str(span.content());
223 }
224 out
225 }
226
227 #[must_use]
229 pub fn as_span(&self) -> Option<&Span> {
230 match self {
231 Self::Str(_) => None,
232 Self::Spans(spans) | Self::Full { spans, .. } => spans.first(),
233 }
234 }
235
236 #[must_use]
238 pub fn spans(&self) -> Vec<Span> {
239 match self {
240 Self::Str(text) => vec![Span::Str(text.clone())],
241 Self::Spans(spans) | Self::Full { spans, .. } => spans.clone(),
242 }
243 }
244
245 #[must_use]
247 pub fn style(&self) -> Option<&Style> {
248 match self {
249 Self::Str(_) | Self::Spans(_) => None,
250 Self::Full { style, .. } => style.as_ref(),
251 }
252 }
253
254 #[must_use]
256 pub fn alignment(&self) -> Option<Alignment> {
257 match self {
258 Self::Str(_) | Self::Spans(_) => None,
259 Self::Full { alignment, .. } => *alignment,
260 }
261 }
262}
263
264impl From<&str> for Line {
265 fn from(s: &str) -> Self {
266 Self::Str(s.to_owned())
267 }
268}
269
270impl From<String> for Line {
271 fn from(s: String) -> Self {
272 Self::Str(s)
273 }
274}
275
276impl From<Span> for Line {
277 fn from(span: Span) -> Self {
278 Self::Spans(vec![span])
279 }
280}
281
282impl From<Vec<Span>> for Line {
283 fn from(spans: Vec<Span>) -> Self {
284 Self::Spans(spans)
285 }
286}
287
288impl Stylize<Line> for Line {
289 fn style(self, style: Style) -> Line {
290 let spans = match self {
291 Self::Str(text) => vec![Span::Str(text)],
292 Self::Spans(spans) => spans,
293 Self::Full {
294 spans,
295 style: existing,
296 alignment,
297 } => {
298 return Self::Full {
299 spans,
300 style: Some(existing.unwrap_or_default().patch(style)),
301 alignment,
302 };
303 }
304 };
305 Self::Full {
306 spans,
307 style: Some(style),
308 alignment: None,
309 }
310 }
311}
312
313impl Stylize<Line> for Vec<Span> {
314 fn style(self, style: Style) -> Line {
315 Line::Full {
316 spans: self,
317 style: Some(style),
318 alignment: None,
319 }
320 }
321}
322
323#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
325#[serde(untagged)]
326pub enum Text {
327 Str(String),
329 Lines(Vec<Line>),
331 Full {
333 lines: Vec<Line>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
337 style: Option<Style>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
340 alignment: Option<Alignment>,
341 },
342}
343
344impl Default for Text {
345 fn default() -> Self {
346 Self::Str(String::new())
347 }
348}
349
350impl Text {
351 #[must_use]
353 pub fn from_lines(lines: Vec<Line>) -> Self {
354 Self::Full {
355 lines,
356 style: None,
357 alignment: None,
358 }
359 }
360
361 #[must_use]
363 pub fn lines(&self) -> Vec<Line> {
364 match self {
365 Self::Str(text) => text
366 .split('\n')
367 .map(|line| Line::Str(line.to_owned()))
368 .collect(),
369 Self::Lines(lines) | Self::Full { lines, .. } => lines.clone(),
370 }
371 }
372
373 #[must_use]
375 pub fn style(&self) -> Option<&Style> {
376 match self {
377 Self::Str(_) | Self::Lines(_) => None,
378 Self::Full { style, .. } => style.as_ref(),
379 }
380 }
381
382 #[must_use]
384 pub fn alignment(&self) -> Option<Alignment> {
385 match self {
386 Self::Str(_) | Self::Lines(_) => None,
387 Self::Full { alignment, .. } => *alignment,
388 }
389 }
390
391 #[must_use]
393 pub fn to_text(&self) -> String {
394 match self {
395 Self::Str(text) => text.clone(),
396 Self::Lines(lines) | Self::Full { lines, .. } => lines
397 .iter()
398 .map(|line| match line {
399 Line::Str(text) => text.clone(),
400 _ => line.to_text(),
401 })
402 .collect::<Vec<_>>()
403 .join("\n"),
404 }
405 }
406}
407
408impl From<&str> for Text {
409 fn from(s: &str) -> Self {
410 Self::Str(s.to_owned())
411 }
412}
413
414impl From<String> for Text {
415 fn from(s: String) -> Self {
416 Self::Str(s)
417 }
418}
419
420impl From<Line> for Text {
421 fn from(line: Line) -> Self {
422 Self::Lines(vec![line])
423 }
424}
425
426impl From<Span> for Text {
427 fn from(span: Span) -> Self {
428 Self::Lines(vec![Line::Spans(vec![span])])
429 }
430}
431
432impl From<Vec<Line>> for Text {
433 fn from(lines: Vec<Line>) -> Self {
434 Self::Lines(lines)
435 }
436}
437
438impl Stylize<Text> for Text {
439 fn style(self, style: Style) -> Text {
440 let lines = match self {
441 Self::Str(text) => text
442 .split('\n')
443 .map(|line| Line::Str(line.to_owned()))
444 .collect(),
445 Self::Lines(lines) => lines,
446 Self::Full {
447 lines,
448 style: existing,
449 alignment,
450 } => {
451 return Self::Full {
452 lines,
453 style: Some(existing.unwrap_or_default().patch(style)),
454 alignment,
455 };
456 }
457 };
458 Self::Full {
459 lines,
460 style: Some(style),
461 alignment: None,
462 }
463 }
464}
465
466impl Stylize<Text> for Vec<Line> {
467 fn style(self, style: Style) -> Text {
468 Text::Full {
469 lines: self,
470 style: Some(style),
471 alignment: None,
472 }
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 #![allow(
479 clippy::unwrap_used,
480 clippy::expect_used,
481 clippy::panic,
482 reason = "tests assert by unwrapping"
483 )]
484 use super::*;
485 use crate::style::Modifier;
486
487 #[test]
488 fn shorthand_forms_parse() {
489 let span: Span = serde_json::from_str("\"hi\"").unwrap_or_default();
490 assert_eq!(span.content(), "hi");
491 let line: Line = serde_json::from_str("[\"a\",\"b\"]").unwrap_or_default();
492 assert_eq!(line.to_text(), "ab");
493 let text: Text = serde_json::from_str("\"x\\ny\"").unwrap_or_default();
494 assert_eq!(text.lines().len(), 2);
495 }
496
497 #[test]
498 fn stylize_chain_matches_ratatui_shape() {
499 let span = "Board ".bold().fg(crate::style::Color::Accent);
500 assert_eq!(span.content(), "Board ");
501 assert!(span.effective_style().add_modifier.contains(Modifier::BOLD));
502 }
503}