osp_cli/ui/inline.rs
1use crate::ui::document::{LineBlock, LinePart};
2use crate::ui::style::{StyleOverrides, StyleToken, apply_style_with_theme_overrides};
3use crate::ui::theme::ThemeDefinition;
4
5/// Parses lightweight inline markup into styled line parts.
6///
7/// Recognizes backtick-delimited code, single-asterisk muted text, and
8/// double-asterisk emphasized text. Escaped marker characters are preserved.
9///
10/// # Examples
11///
12/// ```
13/// use osp_cli::ui::{StyleToken, parts_from_inline};
14///
15/// let parts = parts_from_inline("Use `uid` and *optional* **flags**");
16///
17/// assert_eq!(parts[0].text, "Use ");
18/// assert_eq!(parts[1].token, Some(StyleToken::Key));
19/// assert_eq!(parts[1].text, "uid");
20/// assert_eq!(parts[3].token, Some(StyleToken::Muted));
21/// assert_eq!(parts[5].token, Some(StyleToken::PanelBorder));
22/// ```
23pub fn parts_from_inline(text: &str) -> Vec<LinePart> {
24 let mut parts: Vec<LinePart> = Vec::new();
25 let mut buf = String::new();
26 let chars: Vec<char> = text.chars().collect();
27 let mut i = 0usize;
28
29 let flush = |parts: &mut Vec<LinePart>, buf: &mut String| {
30 if !buf.is_empty() {
31 parts.push(LinePart {
32 text: buf.clone(),
33 token: None,
34 });
35 buf.clear();
36 }
37 };
38
39 while i < chars.len() {
40 let ch = chars[i];
41 if ch == '\\' && i + 1 < chars.len() {
42 buf.push(chars[i + 1]);
43 i += 2;
44 continue;
45 }
46
47 if ch == '`' {
48 let fence = if i + 1 < chars.len() && chars[i + 1] == '`' {
49 2
50 } else {
51 1
52 };
53 let mut end = i + fence;
54 while end + fence - 1 < chars.len() {
55 if chars[end..end + fence].iter().all(|c| *c == '`') {
56 flush(&mut parts, &mut buf);
57 let content: String = chars[i + fence..end].iter().collect();
58 parts.push(LinePart {
59 text: content,
60 token: Some(StyleToken::Key),
61 });
62 i = end + fence;
63 break;
64 }
65 end += 1;
66 }
67 if end + fence - 1 < chars.len() {
68 continue;
69 }
70 }
71
72 if ch == '*' && i + 1 < chars.len() && chars[i + 1] == '*' {
73 let mut end = i + 2;
74 while end + 1 < chars.len() {
75 if chars[end] == '*' && chars[end + 1] == '*' {
76 flush(&mut parts, &mut buf);
77 let content: String = chars[i + 2..end].iter().collect();
78 parts.push(LinePart {
79 text: content,
80 token: Some(StyleToken::PanelBorder),
81 });
82 i = end + 2;
83 break;
84 }
85 end += 1;
86 }
87 if end + 1 < chars.len() {
88 continue;
89 }
90 }
91
92 if ch == '*' {
93 let mut end = i + 1;
94 while end < chars.len() {
95 if chars[end] == '*' {
96 flush(&mut parts, &mut buf);
97 let content: String = chars[i + 1..end].iter().collect();
98 parts.push(LinePart {
99 text: content,
100 token: Some(StyleToken::Muted),
101 });
102 i = end + 1;
103 break;
104 }
105 end += 1;
106 }
107 if end < chars.len() {
108 continue;
109 }
110 }
111
112 buf.push(ch);
113 i += 1;
114 }
115
116 flush(&mut parts, &mut buf);
117 parts
118}
119
120/// Parses inline markup and wraps the result in a single [`LineBlock`].
121///
122/// This keeps the block model aligned with the richer document renderer while
123/// still letting callers use the lightweight inline grammar.
124///
125/// # Examples
126///
127/// ```
128/// use osp_cli::ui::{StyleToken, line_from_inline};
129///
130/// let line = line_from_inline("`uid`");
131///
132/// assert_eq!(line.parts.len(), 1);
133/// assert_eq!(line.parts[0].token, Some(StyleToken::Key));
134/// assert_eq!(line.parts[0].text, "uid");
135/// ```
136pub fn line_from_inline(text: &str) -> LineBlock {
137 LineBlock {
138 parts: parts_from_inline(text),
139 }
140}
141
142/// Renders lightweight inline markup to a styled string.
143///
144/// Returns plain text when `color` is `false` so callers can reuse the same
145/// content path for terminals, copy buffers, and test assertions.
146///
147/// # Examples
148///
149/// ```
150/// use osp_cli::ui::{StyleOverrides, render_inline, resolve_theme};
151///
152/// let rendered = render_inline(
153/// "Use `uid`",
154/// false,
155/// &resolve_theme("dracula"),
156/// &StyleOverrides::default(),
157/// );
158///
159/// assert_eq!(rendered, "Use uid");
160/// ```
161pub fn render_inline(
162 text: &str,
163 color: bool,
164 theme: &ThemeDefinition,
165 overrides: &StyleOverrides,
166) -> String {
167 let mut out = String::new();
168 for part in parts_from_inline(text) {
169 if let Some(token) = part.token {
170 out.push_str(&apply_style_with_theme_overrides(
171 &part.text, token, color, theme, overrides,
172 ));
173 } else {
174 out.push_str(&part.text);
175 }
176 }
177 out
178}
179
180#[cfg(test)]
181mod tests;