1use core::{fmt::Display, mem::take, ops::Add};
2
3use crate::{
4 font::{Font, FontWeight},
5 text,
6};
7use alloc::{string::String, vec, vec::Vec};
8use core::ops::AddAssign;
9use nami::impl_constant;
10use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
11use waterui_color::Color;
12use waterui_core::{Str, View};
13
14#[derive(Debug, Clone, Default)]
16pub struct Style {
17 pub font: Font,
19 pub foreground: Option<Color>,
21 pub background: Option<Color>,
23 pub italic: bool,
25 pub underline: bool,
27 pub strikethrough: bool,
29}
30
31impl Style {
32 #[must_use]
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 #[must_use]
40 pub fn font(mut self, font: impl Into<Font>) -> Self {
41 self.font = font.into();
42 self
43 }
44
45 #[must_use]
47 pub fn foreground(mut self, color: impl Into<Color>) -> Self {
48 self.foreground = Some(color.into());
49 self
50 }
51
52 #[must_use]
54 pub fn background(mut self, color: impl Into<Color>) -> Self {
55 self.background = Some(color.into());
56 self
57 }
58
59 #[must_use]
61 pub fn weight(mut self, weight: FontWeight) -> Self {
62 self.font = self.font.weight(weight);
63 self
64 }
65
66 #[must_use]
69 pub fn bold(mut self) -> Self {
70 self.font = self.font.bold();
71 self
72 }
73
74 #[must_use]
76 pub fn size(mut self, size: f32) -> Self {
77 self.font = self.font.size(size);
78 self
79 }
80
81 #[must_use]
83 pub const fn italic(mut self) -> Self {
84 self.italic = true;
85 self
86 }
87
88 #[must_use]
90 pub const fn not_italic(mut self) -> Self {
91 self.italic = false;
92 self
93 }
94
95 #[must_use]
97 pub const fn underline(mut self) -> Self {
98 self.underline = true;
99 self
100 }
101
102 #[must_use]
104 pub const fn not_underline(mut self) -> Self {
105 self.underline = false;
106 self
107 }
108
109 #[must_use]
111 pub const fn strikethrough(mut self) -> Self {
112 self.strikethrough = true;
113 self
114 }
115
116 #[must_use]
118 pub const fn not_strikethrough(mut self) -> Self {
119 self.strikethrough = false;
120 self
121 }
122}
123
124#[derive(Debug, Clone, Default)]
126pub struct StyledStr {
127 chunks: Vec<(Str, Style)>,
128}
129
130impl StyledStr {
131 #[must_use]
133 pub const fn empty() -> Self {
134 Self { chunks: Vec::new() }
135 }
136
137 #[must_use]
142 pub fn from_markdown(markdown: &str) -> Self {
143 let options =
144 Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES | Options::ENABLE_STRIKETHROUGH;
145 let parser = Parser::new_ext(markdown, options);
146
147 let mut builder = MarkdownInlineBuilder::new();
148 let mut pending_block_break = false;
149
150 for event in parser {
151 match event {
152 Event::Start(tag) => match tag {
153 Tag::Heading { level, .. } => {
154 if pending_block_break || !builder.is_empty() {
155 builder.push_text("\n\n");
156 }
157 pending_block_break = false;
158 builder.enter_with(move |_| heading_style(level));
159 }
160 Tag::Paragraph => {
161 if pending_block_break || !builder.is_empty() {
162 builder.push_text("\n\n");
163 }
164 pending_block_break = false;
165 }
166 Tag::Emphasis => builder.enter_emphasis(),
167 Tag::Strong => builder.enter_strong(),
168 Tag::CodeBlock(kind) => {
169 if pending_block_break || !builder.is_empty() {
170 builder.push_text("\n\n");
171 }
172 pending_block_break = false;
173 if let CodeBlockKind::Fenced(info) = kind
174 && !info.is_empty()
175 {
176 builder.push_text(info.as_ref());
177 builder.push_text(":\n");
178 }
179 }
180 Tag::List(_) | Tag::Item => {
181 if pending_block_break || !builder.is_empty() {
182 builder.push_text("\n");
183 }
184 pending_block_break = false;
185 }
186 _ => {}
187 },
188 Event::End(tag) => match tag {
189 pulldown_cmark::TagEnd::Heading(_) => {
190 builder.exit();
191 pending_block_break = true;
192 }
193 pulldown_cmark::TagEnd::Paragraph
194 | pulldown_cmark::TagEnd::CodeBlock
195 | pulldown_cmark::TagEnd::List(_) => {
196 pending_block_break = true;
197 }
198 pulldown_cmark::TagEnd::Emphasis | pulldown_cmark::TagEnd::Strong => {
199 builder.exit();
200 }
201 _ => {}
202 },
203 Event::Text(text)
204 | Event::Code(text)
205 | Event::Html(text)
206 | Event::FootnoteReference(text)
207 | Event::InlineMath(text)
208 | Event::DisplayMath(text)
209 | Event::InlineHtml(text) => {
210 if pending_block_break && !builder.is_empty() {
211 builder.push_text("\n\n");
212 pending_block_break = false;
213 }
214 builder.push_text(text.as_ref());
215 }
216 Event::SoftBreak => builder.push_soft_break(),
217 Event::HardBreak => builder.push_hard_break(),
218 Event::Rule => {
219 builder.push_text("\n\nāā\n\n");
220 pending_block_break = false;
221 }
222 Event::TaskListMarker(checked) => {
223 if pending_block_break && !builder.is_empty() {
224 builder.push_text("\n");
225 pending_block_break = false;
226 }
227 builder.push_text(if checked { "[x] " } else { "[ ] " });
228 }
229 }
230 }
231
232 builder.finish()
233 }
234
235 #[must_use]
237 pub fn plain(text: impl Into<Str>) -> Self {
238 let mut s = Self::empty();
239 s.push(text.into(), Style::default());
240 s
241 }
242
243 pub fn push(&mut self, text: impl Into<Str>, style: Style) {
245 let text = text.into();
246 self.chunks.push((text, style));
247 }
248
249 pub fn push_str(&mut self, text: impl Into<Str>) {
251 let text = text.into();
252 if let Some(last) = self.chunks.last_mut() {
253 let (last_text, _) = last;
254 last_text.add_assign(text);
255 } else {
256 self.chunks.push((text, Style::default()));
257 }
258 }
259
260 #[must_use]
262 pub fn len(&self) -> usize {
263 self.chunks.iter().map(|(text, _)| text.len()).sum()
264 }
265
266 #[must_use]
268 pub const fn is_empty(&self) -> bool {
269 self.chunks.is_empty()
270 }
271
272 #[must_use]
274 pub fn to_plain(&self) -> Str {
275 if self.chunks.len() == 1 {
276 return self.chunks[0].0.clone();
277 }
278
279 let mut result = String::new();
280 for (text, _) in &self.chunks {
281 result.push_str(text);
282 }
283 result.into()
284 }
285
286 #[must_use]
288 pub fn into_chunks(self) -> Vec<(Str, Style)> {
289 self.chunks
290 }
291
292 #[must_use]
294 pub fn set_style(self, style: &Style) -> Self {
295 self.apply_style(|s| *s = style.clone())
296 }
297
298 fn apply_style(mut self, f: impl Fn(&mut Style)) -> Self {
299 if self.chunks.is_empty() {
300 return self;
301 }
302 let old_chunks = core::mem::take(&mut self.chunks);
303 for (text, mut style) in old_chunks {
304 f(&mut style);
305 self.push(text, style);
306 }
307 self
308 }
309
310 #[must_use]
312 pub fn size(self, size: f32) -> Self {
313 self.apply_style(|s| *s = take(s).size(size))
314 }
315
316 #[must_use]
318 pub fn font(self, font: &Font) -> Self {
319 self.apply_style(|s| s.font = font.clone())
320 }
321
322 #[must_use]
324 pub fn foreground(self, color: impl Into<Color>) -> Self {
325 let color = color.into();
326 self.apply_style(|s| s.foreground = Some(color.clone()))
327 }
328
329 #[must_use]
331 pub fn background_color(self, color: impl Into<Color>) -> Self {
332 let color = color.into();
333 self.apply_style(|s| s.background = Some(color.clone()))
334 }
335
336 #[must_use]
338 pub fn weight(self, weight: FontWeight) -> Self {
339 self.apply_style(|s| {
340 *s = take(s).weight(weight);
341 })
342 }
343
344 #[must_use]
346 pub fn bold(self) -> Self {
347 self.weight(FontWeight::Bold)
348 }
349
350 #[must_use]
352 pub fn italic(self, italic: bool) -> Self {
353 self.apply_style(|s| s.italic = italic)
354 }
355
356 #[must_use]
358 pub fn underline(self, underline: bool) -> Self {
359 self.apply_style(|s| s.underline = underline)
360 }
361
362 #[must_use]
364 pub fn strikethrough(self, strikethrough: bool) -> Self {
365 self.apply_style(|s| s.strikethrough = strikethrough)
366 }
367}
368
369#[derive(Debug, Clone)]
373pub struct MarkdownInlineBuilder {
374 base_style: Style,
375 stack: Vec<Style>,
376 buffer: String,
377 result: StyledStr,
378}
379
380impl Default for MarkdownInlineBuilder {
381 fn default() -> Self {
382 Self::new()
383 }
384}
385
386impl MarkdownInlineBuilder {
387 #[must_use]
389 pub fn new() -> Self {
390 Self::with_base_style(Style::default())
391 }
392
393 #[must_use]
395 pub fn with_base_style(style: Style) -> Self {
396 Self {
397 base_style: style.clone(),
398 stack: vec![style],
399 buffer: String::new(),
400 result: StyledStr::empty(),
401 }
402 }
403
404 fn current_style(&self) -> Style {
405 self.stack.last().cloned().unwrap_or_else(Style::default)
406 }
407
408 fn flush(&mut self) {
409 if self.buffer.is_empty() {
410 return;
411 }
412
413 let text = take(&mut self.buffer);
414 self.result.push(text, self.current_style());
415 }
416
417 pub fn push_text(&mut self, text: &str) {
419 if !text.is_empty() {
420 self.buffer.push_str(text);
421 }
422 }
423
424 pub fn push_soft_break(&mut self) {
426 self.buffer.push(' ');
427 }
428
429 pub fn push_hard_break(&mut self) {
431 self.buffer.push('\n');
432 }
433
434 pub fn enter_with(&mut self, f: impl FnOnce(Style) -> Style) {
436 self.flush();
437 let style = self.current_style();
438 self.stack.push(f(style));
439 }
440
441 pub fn exit(&mut self) {
443 self.flush();
444 if self.stack.len() > 1 {
445 self.stack.pop();
446 }
447 }
448
449 pub fn enter_emphasis(&mut self) {
451 self.enter_with(Style::italic);
452 }
453
454 pub fn enter_strong(&mut self) {
456 self.enter_with(Style::bold);
457 }
458
459 #[must_use]
461 pub const fn is_empty(&self) -> bool {
462 self.result.is_empty() && self.buffer.is_empty()
463 }
464
465 #[must_use]
467 pub fn base_style(&self) -> Style {
468 self.base_style.clone()
469 }
470
471 #[must_use]
474 pub fn take(&mut self) -> Option<StyledStr> {
475 self.flush();
476
477 if self.result.is_empty() {
478 return None;
479 }
480
481 let mut output = StyledStr::empty();
482 core::mem::swap(&mut output, &mut self.result);
483 self.stack.truncate(1);
484 if let Some(first) = self.stack.first_mut() {
485 *first = self.base_style.clone();
486 }
487
488 Some(output)
489 }
490
491 #[must_use]
493 pub fn finish(mut self) -> StyledStr {
494 self.flush();
495 self.result
496 }
497}
498
499#[must_use]
501pub fn heading_style(level: HeadingLevel) -> Style {
502 use crate::font::{Body, Caption, Footnote, Headline, Subheadline, Title};
503
504 let font: Font = match level {
505 HeadingLevel::H1 => Headline.into(),
506 HeadingLevel::H2 => Title.into(),
507 HeadingLevel::H3 => Subheadline.into(),
508 HeadingLevel::H4 => Body.into(),
509 HeadingLevel::H5 => Caption.into(),
510 HeadingLevel::H6 => Footnote.into(),
511 };
512
513 Style::default().font(font).bold()
514}
515
516impl View for StyledStr {
517 fn body(self, _env: &waterui_core::Environment) -> impl waterui_core::View {
518 text(self)
519 }
520}
521
522impl Add for StyledStr {
523 type Output = Self;
524
525 fn add(mut self, rhs: Self) -> Self::Output {
526 for (text, style) in rhs.chunks {
527 self.push(text, style);
528 }
529 self
530 }
531}
532
533impl Add<&'static str> for StyledStr {
534 type Output = Self;
535
536 fn add(mut self, rhs: &'static str) -> Self::Output {
537 self.push(rhs, Style::default());
538 self
539 }
540}
541
542impl Extend<(Str, Style)> for StyledStr {
543 fn extend<T: IntoIterator<Item = (Str, Style)>>(&mut self, iter: T) {
544 for (text, style) in iter {
545 self.push(text, style);
546 }
547 }
548}
549
550impl From<Str> for StyledStr {
551 fn from(value: Str) -> Self {
552 Self::plain(value)
553 }
554}
555
556impl From<&'static str> for StyledStr {
557 fn from(value: &'static str) -> Self {
558 Self::plain(value)
559 }
560}
561
562impl From<String> for StyledStr {
563 fn from(value: String) -> Self {
564 Self::plain(value)
565 }
566}
567
568impl Display for StyledStr {
569 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
570 f.write_str(&self.to_plain())
571 }
572}
573
574impl_constant!(Style, StyledStr);
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579
580 #[test]
581 fn parses_emphasis_markdown() {
582 let styled = StyledStr::from_markdown("Hello *world*!");
583 let chunks = styled.into_chunks();
584 assert_eq!(chunks.len(), 3);
585 assert_eq!(chunks[0].0.as_str(), "Hello ");
586 assert_eq!(chunks[1].0.as_str(), "world");
587 assert!(chunks[1].1.italic);
588 assert_eq!(chunks[2].0.as_str(), "!");
589 }
590
591 #[test]
592 fn parses_heading_markdown() {
593 let styled = StyledStr::from_markdown("# Title");
594 let chunks = styled.into_chunks();
595 assert_eq!(chunks.len(), 1);
596 assert_eq!(chunks[0].0.as_str(), "Title");
597 }
598}