Skip to main content

rfham_markdown/
lib.rs

1//!
2//! One-line description.
3//!
4//! More detailed description.
5//!
6//! # Examples
7//!
8//! ```rust
9//! ```
10//!
11//! # Features
12//!
13//! - **feature-name**; Feature description
14//!
15
16use colored::Colorize as _;
17use rfham_core::error::CoreError;
18use std::{fmt::Display, io::Write};
19
20// ------------------------------------------------------------------------------------------------
21// Public Macros
22// ------------------------------------------------------------------------------------------------
23
24// ------------------------------------------------------------------------------------------------
25// Public Types
26// ------------------------------------------------------------------------------------------------
27
28pub 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
79// ------------------------------------------------------------------------------------------------
80// Public Functions
81// ------------------------------------------------------------------------------------------------
82
83const 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
309// ------------------------------------------------------------------------------------------------
310// Private Types
311// ------------------------------------------------------------------------------------------------
312
313// ------------------------------------------------------------------------------------------------
314// Implementations
315// ------------------------------------------------------------------------------------------------
316
317impl 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
415// ------------------------------------------------------------------------------------------------
416
417impl 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// ------------------------------------------------------------------------------------------------
509// Private Functions
510// ------------------------------------------------------------------------------------------------
511
512// ------------------------------------------------------------------------------------------------
513// Modules
514// ------------------------------------------------------------------------------------------------