1use colored::Colorize as _;
17use rfham_core::error::CoreError;
18use std::{fmt::Display, io::Write};
19
20pub trait ToMarkdown {
29 fn write_markdown<W: Write>(&self, writer: &mut W) -> Result<(), CoreError>;
30 fn to_markdown_string(&self) -> Result<String, CoreError> {
31 let mut buffer = Vec::new();
32 self.write_markdown(&mut buffer)?;
33 Ok(String::from_utf8(buffer)?)
34 }
35}
36
37pub trait ToMarkdownWith {
38 type Context: Sized;
39
40 fn write_markdown_with<W: Write>(
41 &self,
42 writer: &mut W,
43 context: Self::Context,
44 ) -> Result<(), CoreError>;
45 fn to_markdown_string_with(&self, context: Self::Context) -> Result<String, CoreError> {
46 let mut buffer = Vec::new();
47 self.write_markdown_with(&mut buffer, context)?;
48 Ok(String::from_utf8(buffer)?)
49 }
50}
51
52impl<T: ToMarkdownWith<Context = C>, C: Default> ToMarkdown for T {
53 fn write_markdown<W: Write>(&self, writer: &mut W) -> Result<(), CoreError> {
54 self.write_markdown_with(writer, C::default())
55 }
56}
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub enum ColumnJustification {
60 #[default]
61 Left,
62 Right,
63 Centered,
64}
65
66#[derive(Clone, Debug, PartialEq)]
67pub struct Column {
68 label: String,
69 justification: Option<ColumnJustification>,
70 width: Option<usize>,
71}
72
73#[derive(Clone, Debug)]
74pub struct Table {
75 super_labels: Vec<Column>,
76 columns: Vec<Column>,
77}
78
79const VERTICAL_SEPARATOR_END: &str = "|";
84const VERTICAL_SEPARATOR_INNER: &str = " | ";
85const BULLET_LIST_BULLET: &str = "*";
86const NUMBER_LIST_SEPARATOR: &str = ".";
87const HEADING_PREFIX: &str = "#";
88const DEFN_LIST_TERM_PREFIX: &str = ";";
89const DEFN_LIST_DEFINITION_PREFIX: &str = ":";
90const FMT_ITALIC_DELIM: &str = "*";
91const FMT_BOLD_DELIM: &str = "**";
92const FMT_STRIKETHROUGH_DELIM: &str = "~~";
93const FMT_CODE_DELIM: &str = "`";
94const BLOCK_QUOTE_PREFIX: &str = ">";
95
96pub fn blank_line<W: Write>(w: &mut W) -> Result<(), CoreError> {
97 writeln!(w)?;
98 Ok(())
99}
100
101pub fn plain_text<W: Write, S: AsRef<str>>(w: &mut W, content: S) -> Result<(), CoreError> {
102 writeln!(w, "{}", content.as_ref().normal())?;
103 Ok(())
104}
105
106pub fn block_quote<W: Write, S: AsRef<str>>(w: &mut W, content: S) -> Result<(), CoreError> {
107 writeln!(w, "{} {}", BLOCK_QUOTE_PREFIX, content.as_ref().italic())?;
108 Ok(())
109}
110
111pub fn bold_to_string<S: AsRef<str>>(content: S) -> String {
112 format!(
113 "{}{}{}",
114 FMT_BOLD_DELIM,
115 content.as_ref().bold(),
116 FMT_BOLD_DELIM
117 )
118}
119
120pub fn bold<W: Write, S: AsRef<str>>(w: &mut W, content: S) -> Result<(), CoreError> {
121 write!(w, "{}", bold_to_string(content))?;
122 Ok(())
123}
124
125pub fn code_to_string<S: AsRef<str>>(content: S) -> String {
126 format!(
127 "{}{}{}",
128 FMT_CODE_DELIM,
129 content.as_ref().white().dimmed(),
130 FMT_CODE_DELIM
131 )
132}
133
134pub fn code<W: Write, S: AsRef<str>>(w: &mut W, content: S) -> Result<(), CoreError> {
135 write!(w, "{}", code_to_string(content))?;
136 Ok(())
137}
138
139pub fn italic_to_string<S: AsRef<str>>(content: S) -> String {
140 format!(
141 "{}{}{}",
142 FMT_ITALIC_DELIM,
143 content.as_ref().italic(),
144 FMT_ITALIC_DELIM
145 )
146}
147
148pub fn italic<W: Write, S: AsRef<str>>(w: &mut W, content: S) -> Result<(), CoreError> {
149 write!(w, "{}", italic_to_string(content))?;
150 Ok(())
151}
152
153pub fn strikethrough_to_string<S: AsRef<str>>(content: S) -> String {
154 format!(
155 "{}{}{}",
156 FMT_STRIKETHROUGH_DELIM,
157 content.as_ref().strikethrough(),
158 FMT_STRIKETHROUGH_DELIM
159 )
160}
161
162pub fn strikethrough<W: Write, S: AsRef<str>>(w: &mut W, content: S) -> Result<(), CoreError> {
163 write!(w, "{}", strikethrough_to_string(content))?;
164 Ok(())
165}
166
167pub fn link_to_string<S1: AsRef<str>, S2: AsRef<str>>(text: S1, url: S2) -> String {
168 format!("[{}]({})", text.as_ref(), url.as_ref())
169 .magenta()
170 .underline()
171 .to_string()
172}
173
174pub fn link<W: Write, S1: AsRef<str>, S2: AsRef<str>>(
175 w: &mut W,
176 text: S1,
177 url: S2,
178) -> Result<(), CoreError> {
179 write!(w, "{}", link_to_string(text, url))?;
180 Ok(())
181}
182
183pub fn header<W: Write, S: AsRef<str>>(w: &mut W, level: u16, content: S) -> Result<(), CoreError> {
184 assert!(level > 0);
185 writeln!(w, "{}", header_to_string(level, content))?;
186 Ok(())
187}
188
189pub fn header_to_string<S: AsRef<str>>(level: u16, content: S) -> String {
190 format!(
191 "{} {}",
192 HEADING_PREFIX.repeat(level as usize),
193 content.as_ref()
194 )
195 .blue()
196 .bold()
197 .to_string()
198}
199
200const CODE_FENCE_STR: &str = "```";
201
202pub fn fenced_code_block_start<W: Write>(w: &mut W) -> Result<(), CoreError> {
203 writeln!(w, "{}", format!("{CODE_FENCE_STR}text").dimmed())?;
204 Ok(())
205}
206
207pub fn fenced_code_block_start_for<W: Write, S: AsRef<str>>(
208 w: &mut W,
209 language: S,
210) -> Result<(), CoreError> {
211 writeln!(
212 w,
213 "{}",
214 format!("{CODE_FENCE_STR}{}", language.as_ref()).dimmed()
215 )?;
216 Ok(())
217}
218
219pub fn fenced_code_block_end<W: Write>(w: &mut W) -> Result<(), CoreError> {
220 writeln!(w, "{}", CODE_FENCE_STR.dimmed())?;
221 Ok(())
222}
223
224pub fn bulleted_list<W: Write, S: AsRef<str>>(
225 w: &mut W,
226 level: u16,
227 content: &[S],
228) -> Result<(), CoreError> {
229 let result: Result<Vec<()>, CoreError> = content
230 .iter()
231 .map(|content| bulleted_list_item(w, level, content))
232 .collect();
233 result.map(|_| ())
234}
235
236pub fn bulleted_list_item<W: Write, S: AsRef<str>>(
237 w: &mut W,
238 level: u16,
239 content: S,
240) -> Result<(), CoreError> {
241 assert!(level > 0);
242 writeln!(
243 w,
244 "{}",
245 format!(
246 "{}{} {}",
247 " ".repeat((level as usize - 1) * 2_usize),
248 BULLET_LIST_BULLET,
249 content.as_ref()
250 )
251 .yellow()
252 )?;
253 Ok(())
254}
255
256pub fn numbered_list<W: Write, S: AsRef<str>>(
257 w: &mut W,
258 level: u16,
259 content: &[S],
260) -> Result<(), CoreError> {
261 let result: Result<Vec<()>, CoreError> = content
262 .iter()
263 .enumerate()
264 .map(|(number, content)| numbered_list_item(w, level, number, content))
265 .collect();
266 result.map(|_| ())
267}
268
269pub fn numbered_list_item<W: Write, S: AsRef<str>>(
270 w: &mut W,
271 level: u16,
272 number: usize,
273 content: S,
274) -> Result<(), CoreError> {
275 assert!(level > 0);
276 writeln!(
277 w,
278 "{}",
279 format!(
280 "{}{}{} {}",
281 " ".repeat((level as usize - 1) * 3_usize),
282 number,
283 NUMBER_LIST_SEPARATOR,
284 content.as_ref()
285 )
286 .yellow()
287 )?;
288 Ok(())
289}
290
291pub fn definition_list_item<W: Write, S1: AsRef<str>, S2: AsRef<str>>(
292 w: &mut W,
293 term: S1,
294 definition: S2,
295) -> Result<(), CoreError> {
296 writeln!(
297 w,
298 "{}",
299 format!("{} {}", DEFN_LIST_TERM_PREFIX, term.as_ref()).yellow()
300 )?;
301 writeln!(
302 w,
303 "{}",
304 format!("{} {}", DEFN_LIST_DEFINITION_PREFIX, definition.as_ref()).yellow()
305 )?;
306 Ok(())
307}
308
309impl Display for Column {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 write!(
320 f,
321 "{}",
322 if let Some(width) = &self.width {
323 match self.justification {
324 Some(ColumnJustification::Left) => format!("{:<width$}", self.label),
325 Some(ColumnJustification::Right) => format!("{:>width$}", self.label),
326 Some(ColumnJustification::Centered) => format!("{:^width$}", self.label),
327 None => format!("{:width$}", self.label),
328 }
329 } else {
330 self.label.to_string()
331 }
332 )
333 }
334}
335
336impl From<String> for Column {
337 fn from(value: String) -> Self {
338 Self::new(value)
339 }
340}
341
342impl From<(&str, usize)> for Column {
343 fn from(value: (&str, usize)) -> Self {
344 Self::new(value.0).with_width(value.1)
345 }
346}
347
348impl From<(String, usize)> for Column {
349 fn from(value: (String, usize)) -> Self {
350 Self::new(value.0).with_width(value.1)
351 }
352}
353
354impl Column {
355 pub fn new<S: Into<String>>(content: S) -> Self {
356 Self {
357 label: content.into(),
358 justification: None,
359 width: None,
360 }
361 }
362
363 pub fn left_justified<S: Into<String>>(content: S) -> Self {
364 Self::new(content).with_justification(ColumnJustification::Left)
365 }
366
367 pub fn right_justified<S: Into<String>>(content: S) -> Self {
368 Self::new(content).with_justification(ColumnJustification::Right)
369 }
370
371 pub fn centered<S: Into<String>>(content: S) -> Self {
372 Self::new(content).with_justification(ColumnJustification::Centered)
373 }
374
375 pub fn fill(fill_char: char, width: usize) -> Self {
376 Self::new(fill_char.to_string().repeat(width)).with_width(width)
377 }
378
379 pub fn with_justification(mut self, justification: ColumnJustification) -> Self {
380 self.justification = Some(justification);
381 self
382 }
383
384 pub fn with_width(mut self, width: usize) -> Self {
385 self.width = Some(width);
386 self
387 }
388
389 pub fn row_separator(&self) -> Self {
390 match (self.justification, self.width) {
391 (Some(ColumnJustification::Left), Some(width)) if width >= 2 => Self {
392 label: format!(":{}", "-".repeat(width - 1)),
393 ..*self
394 },
395 (Some(ColumnJustification::Right), Some(width)) if width >= 2 => Self {
396 label: format!("{}:", "-".repeat(width - 1)),
397 ..*self
398 },
399 (Some(ColumnJustification::Centered), Some(width)) if width >= 3 => Self {
400 label: format!(":{}:", "-".repeat(width - 2)),
401 ..*self
402 },
403 (None, Some(width)) => Self {
404 label: "-".repeat(width),
405 ..*self
406 },
407 _ => Self {
408 label: "-".repeat(2),
409 ..*self
410 },
411 }
412 }
413}
414
415impl From<Vec<Column>> for Table {
418 fn from(value: Vec<Column>) -> Self {
419 Self::new(value)
420 }
421}
422
423impl Table {
424 pub fn new(columns: Vec<Column>) -> Self {
425 Self {
426 super_labels: Default::default(),
427 columns,
428 }
429 }
430
431 pub fn with_super_labels<S>(mut self, labels: Vec<S>) -> Self
432 where
433 S: Into<String>,
434 {
435 assert_eq!(labels.len(), self.columns.len());
436 self.super_labels = labels
437 .into_iter()
438 .zip(self.columns.iter())
439 .map(|(label, col)| Column {
440 label: label.into(),
441 ..col.clone()
442 })
443 .collect();
444 self
445 }
446
447 pub fn headers<W>(&self, w: &mut W) -> Result<(), CoreError>
448 where
449 W: Write,
450 {
451 if !self.super_labels.is_empty() {
452 self.write_row(w, &self.super_labels, true)?;
453 }
454 self.write_row(w, &self.columns, true)?;
455 self.write_row(
456 w,
457 &self
458 .columns
459 .iter()
460 .map(|c| c.row_separator())
461 .collect::<Vec<_>>(),
462 true,
463 )?;
464 Ok(())
465 }
466
467 pub fn data_row<W, S>(&self, w: &mut W, row: &[S]) -> Result<(), CoreError>
468 where
469 W: Write,
470 S: Into<String>,
471 String: for<'a> From<&'a S>,
472 {
473 let row: Vec<Column> = row
474 .iter()
475 .zip(self.columns.iter())
476 .map(|(label, col): (&S, &Column)| Column {
477 label: String::from(label),
478 ..col.clone()
479 })
480 .collect();
481 self.write_row(w, &row, false)?;
482 Ok(())
483 }
484
485 fn write_row<W>(&self, w: &mut W, row: &[Column], is_header: bool) -> Result<(), CoreError>
486 where
487 W: Write,
488 {
489 let row_string = format!(
490 "{} {} {}",
491 VERTICAL_SEPARATOR_END.bold(),
492 row.iter()
493 .map(|cell| if is_header {
494 cell.to_string().bold()
495 } else {
496 cell.to_string().normal()
497 }
498 .to_string())
499 .collect::<Vec<_>>()
500 .join(&VERTICAL_SEPARATOR_INNER.bold()),
501 VERTICAL_SEPARATOR_END.bold()
502 );
503 writeln!(w, "{}", row_string)?;
504 Ok(())
505 }
506}
507
508