blogs_md_easy/
lib.rs

1use std::{collections::HashMap, error::Error, ops::{Div, Mul}, str::FromStr};
2use nom::{branch::alt, bytes::complete::{escaped, is_not, tag, take_till, take_until, take_while, take_while_m_n}, character::complete::{alphanumeric1, anychar, multispace0, one_of, space0}, combinator::{opt, recognize, rest}, multi::{many0, many1, many_till, separated_list1}, sequence::{delimited, preceded, separated_pair, terminated, tuple}, IResult, Parser};
3use nom_locate::LocatedSpan;
4
5////////////////////////////////////////////////////////////////////////////////
6// Structs and types
7/// A [`LocatedSpan`] of a string slice, with lifetime `'a`.
8pub type Span<'a> = LocatedSpan<&'a str>;
9
10/// A list of all the available text case `Filter`s.
11#[derive(Clone, Debug, PartialEq)]
12pub enum TextCase {
13    /// Converts a string into lowercase.
14    ///
15    /// # Example
16    /// ```rust
17    /// use blogs_md_easy::{render_filter, Filter, TextCase};
18    ///
19    /// let input = "Hello, World!".to_string();
20    /// let filter = Filter::Text { case: TextCase::Lower };
21    /// let output = render_filter(input, &filter);
22    ///
23    /// assert_eq!(output, "hello, world!");
24    /// ```
25    Lower,
26    /// Converts a string into uppercase.
27    ///
28    /// # Example
29    /// ```rust
30    /// use blogs_md_easy::{render_filter, Filter, TextCase};
31    ///
32    /// let input = "Hello, World!".to_string();
33    /// let filter = Filter::Text { case: TextCase::Upper };
34    /// let output = render_filter(input, &filter);
35    ///
36    /// assert_eq!(output, "HELLO, WORLD!");
37    /// ```
38    Upper,
39    /// Converts a string into title case.
40    ///
41    /// Every character that supersedes a space or hyphen.
42    ///
43    /// # Example
44    /// ```rust
45    /// use blogs_md_easy::{render_filter, Filter, TextCase};
46    ///
47    /// let input = "john doe-bloggs".to_string();
48    /// let filter = Filter::Text { case: TextCase::Title };
49    /// let output = render_filter(input, &filter);
50    ///
51    /// assert_eq!(output, "John Doe-Bloggs");
52    /// ```
53    Title,
54    /// Converts a string into kebab case.
55    ///
56    /// # Example
57    /// ```rust
58    /// use blogs_md_easy::{render_filter, Filter, TextCase};
59    ///
60    /// let input = "kebab case".to_string();
61    /// let filter = Filter::Text { case: TextCase::Kebab };
62    /// let output = render_filter(input, &filter);
63    ///
64    /// assert_eq!(output, "kebab-case");
65    /// ```
66    /// Converts a string into kebab case.
67    ///
68    /// # Example
69    /// ```rust
70    /// use blogs_md_easy::{render_filter, Filter, TextCase};
71    ///
72    /// let input = "kebab case".to_string();
73    /// let filter = Filter::Text { case: TextCase::Kebab };
74    /// let output = render_filter(input, &filter);
75    ///
76    /// assert_eq!(output, "kebab-case");
77    /// ```
78    Kebab,
79    /// Converts a string into snake case.
80    ///
81    /// # Example
82    /// ```rust
83    /// use blogs_md_easy::{render_filter, Filter, TextCase};
84    ///
85    /// let input = "snake case".to_string();
86    /// let filter = Filter::Text { case: TextCase::Snake };
87    /// let output = render_filter(input, &filter);
88    ///
89    /// assert_eq!(output, "snake_case");
90    /// ```
91    Snake,
92    /// Converts a string into Pascal case.
93    ///
94    /// # Example
95    /// ```rust
96    /// use blogs_md_easy::{render_filter, Filter, TextCase};
97    ///
98    /// let input = "pascal case".to_string();
99    /// let filter = Filter::Text { case: TextCase::Pascal };
100    /// let output = render_filter(input, &filter);
101    ///
102    /// assert_eq!(output, "PascalCase");
103    /// ```
104    Pascal,
105    /// Converts a string into camel case.
106    ///
107    /// # Example
108    /// ```rust
109    /// use blogs_md_easy::{render_filter, Filter, TextCase};
110    ///
111    /// let input = "camel case".to_string();
112    /// let filter = Filter::Text { case: TextCase::Camel };
113    /// let output = render_filter(input, &filter);
114    ///
115    /// assert_eq!(output, "camelCase");
116    /// ```
117    Camel,
118    /// Converts a string by inverting the case.
119    ///
120    /// # Example
121    /// ```rust
122    /// use blogs_md_easy::{render_filter, Filter, TextCase};
123    ///
124    /// let input = "Hello, World!".to_string();
125    /// let filter = Filter::Text { case: TextCase::Invert };
126    /// let output = render_filter(input, &filter);
127    ///
128    /// assert_eq!(output, "hELLO, wORLD!");
129    /// ```
130    Invert,
131}
132
133impl FromStr for TextCase {
134    type Err = String;
135
136    /// Parse a string slice, into a `TextCase`.
137    ///
138    /// # Examples
139    /// ```rust
140    /// use blogs_md_easy::TextCase;
141    /// // For both lower and upper, the word "case" can be appended.
142    /// assert_eq!("lower".parse::<TextCase>(), Ok(TextCase::Lower));
143    /// assert_eq!("lowercase".parse::<TextCase>(), Ok(TextCase::Lower));
144    ///
145    /// // For programming cases, the word case can be appended in that style.
146    /// assert_eq!("snake".parse::<TextCase>(), Ok(TextCase::Snake));
147    /// assert_eq!("snake_case".parse::<TextCase>(), Ok(TextCase::Snake));
148    /// assert_eq!("title".parse::<TextCase>(), Ok(TextCase::Title));
149    /// assert_eq!("Title".parse::<TextCase>(), Ok(TextCase::Title));
150    fn from_str(s: &str) -> Result<Self, Self::Err> {
151        match s {
152            "lower" | "lowercase" => Ok(Self::Lower),
153            "upper" | "uppercase" | "UPPERCASE" => Ok(Self::Upper),
154            "title" | "Title" => Ok(Self::Title),
155            "kebab" | "kebab-case" => Ok(Self::Kebab),
156            "snake" | "snake_case" => Ok(Self::Snake),
157            "pascal" | "PascalCase" => Ok(Self::Pascal),
158            "camel" | "camelCase" => Ok(Self::Camel),
159            "invert" | "inverse" => Ok(Self::Invert),
160            _ => Err(format!("Unable to parse TextCase from '{}'", s)),
161        }
162    }
163}
164
165/// Predefined functions names that will be used within [`render_filter`] to
166/// convert a value.
167#[derive(Clone, Debug, PartialEq)]
168pub enum Filter {
169    // Maths filters
170
171    /// Rounds a numeric value up to the nearest whole number.
172    ///
173    /// # Example
174    /// ```rust
175    /// use blogs_md_easy::{render_filter, Filter};
176    ///
177    /// let input = "1.234".to_string();
178    /// let filter = Filter::Ceil;
179    /// let output = render_filter(input, &filter);
180    ///
181    /// assert_eq!(output, "2");
182    /// ```
183    Ceil,
184    /// Rounds a numeric value down to the nearest whole number.
185    ///
186    /// # Example
187    /// ```rust
188    /// use blogs_md_easy::{render_filter, Filter};
189    ///
190    /// let input = "4.567".to_string();
191    /// let filter = Filter::Floor;
192    /// let output = render_filter(input, &filter);
193    ///
194    /// assert_eq!(output, "4");
195    /// ```
196    Floor,
197    /// Round a number to a given precision.
198    ///
199    /// `Default argument: precision`
200    ///
201    /// # Examples
202    /// Precision of 0 to remove decimal place.
203    /// ```rust
204    /// use blogs_md_easy::{render_filter, Filter};
205    ///
206    /// let input = "1.234".to_string();
207    /// let filter = Filter::Round { precision: 0 };
208    /// let output = render_filter(input, &filter);
209    ///
210    /// assert_eq!(output, "1");
211    /// ```
212    ///
213    /// Precision of 3 for three decimal places.
214    /// ```rust
215    /// use blogs_md_easy::{render_filter, Filter};
216    ///
217    /// let input = "1.23456789".to_string();
218    /// let filter = Filter::Round { precision: 3 };
219    /// let output = render_filter(input, &filter);
220    ///
221    /// assert_eq!(output, "1.235");
222    /// ```
223    Round {
224        /// The number of decimal places to round to.
225        /// A half is rounded down.
226        ///
227        /// `Default: 0`
228        ///
229        /// # Examples
230        /// Providing no arguments.
231        /// ```rust
232        /// use blogs_md_easy::{parse_filter, Filter, Span};
233        ///
234        /// let input = Span::new("round");
235        /// let (_, filter) = parse_filter(input).unwrap();
236        ///
237        /// assert!(matches!(filter, Filter::Round { .. }));
238        /// assert_eq!(filter, Filter::Round { precision: 0 });
239        /// ```
240        ///
241        /// Providing the default argument.
242        /// ```rust
243        /// use blogs_md_easy::{parse_filter, Filter, Span};
244        ///
245        /// let input = Span::new("round = 3");
246        /// let (_, filter) = parse_filter(input).unwrap();
247        ///
248        /// assert!(matches!(filter, Filter::Round { .. }));
249        /// assert_eq!(filter, Filter::Round { precision: 3 });
250        /// ```
251        ///
252        /// Alternatively, it is possible to be more explicit.
253        /// ```rust
254        /// use blogs_md_easy::{parse_filter, Filter, Span};
255        ///
256        /// let input = Span::new("round = precision: 42");
257        /// let (_, filter) = parse_filter(input).unwrap();
258        ///
259        /// assert!(matches!(filter, Filter::Round { .. }));
260        /// assert_eq!(filter, Filter::Round { precision: 42 });
261        /// ```
262        precision: u8,
263    },
264
265    // String filter
266
267    /// Converts a string from Markdown into HTML.
268    ///
269    /// # Example
270    /// ```rust
271    /// use blogs_md_easy::{render_filter, Filter};
272    ///
273    /// let input = r#"# Markdown Title
274    /// First paragraph.
275    ///
276    /// [example.com](https://example.com)
277    ///
278    /// * Unordered list
279    ///
280    /// 1. Ordered list"#.to_string();
281    /// let filter = Filter::Markdown;
282    /// let output = render_filter(input, &filter);
283    ///
284    /// assert_eq!(output, r#"<h1>Markdown Title</h1>
285    /// <p>First paragraph.</p>
286    /// <p><a href="https://example.com">example.com</a></p>
287    /// <ul>
288    /// <li>Unordered list</li>
289    /// </ul>
290    /// <ol>
291    /// <li>Ordered list</li>
292    /// </ol>"#);
293    /// ```
294    Markdown,
295    /// Replace a given substring with another. Optionally, limit the number of
296    /// replacements from the start of the string.
297    ///
298    /// `Default argument: find`
299    ///
300    /// # Example
301    /// ```rust
302    /// use blogs_md_easy::{render_filter, Filter};
303    ///
304    /// let input = "Hello, World!".to_string();
305    /// let filter = Filter::Replace {
306    ///     find: "World".to_string(),
307    ///     replacement: "Rust".to_string(),
308    ///     limit: None,
309    /// };
310    /// let output = render_filter(input, &filter);
311    ///
312    /// assert_eq!(output, "Hello, Rust!");
313    /// ```
314    Replace {
315        /// The substring that we are looking for.
316        find: String,
317        /// The substring that will replace what we `find`.
318        replacement: String,
319        /// Limit the number of replacements from the start of the string.
320        ///
321        /// `Default: None`
322        ///
323        /// # Examples
324        /// Without an argument, this will default to doing nothing.
325        /// ```rust
326        /// use blogs_md_easy::{parse_placeholder, render_filter, Filter, Span};
327        ///
328        /// let input = Span::new("{{ £greeting | replace }}");
329        /// let (_, placeholder) = parse_placeholder(input).unwrap();
330        ///
331        /// assert!(matches!(placeholder.filters[0], Filter::Replace { .. }));
332        /// assert_eq!(placeholder.filters[0], Filter::Replace {
333        ///     find: "".to_string(),
334        ///     replacement: "".to_string(),
335        ///     limit: None,
336        /// });
337        ///
338        /// let greeting = "Hello, World!".to_string();
339        /// // Cloning here, only so we can reuse the `greeting` variable in
340        /// // assert, to prove that they are identical.
341        /// let output = render_filter(greeting.clone(), &placeholder.filters[0]);
342        /// assert_eq!(output, greeting);
343        /// ```
344        ///
345        /// Providing the default argument.
346        /// In this case the value will be assigned to `find`, and the
347        /// `replacement` will be an empty String, essentially removing this
348        /// phrase from the string.
349        /// ```rust
350        /// use blogs_md_easy::{parse_placeholder, render_filter, Filter, Span};
351        ///
352        /// let input = Span::new("{{ £greeting | replace = World }}");
353        /// let (_, placeholder) = parse_placeholder(input).unwrap();
354        ///
355        /// assert!(matches!(placeholder.filters[0], Filter::Replace { .. }));
356        /// assert_eq!(placeholder.filters[0], Filter::Replace {
357        ///     find: "World".to_string(),
358        ///     replacement: "".to_string(),
359        ///     limit: None,
360        /// });
361        ///
362        /// let greeting = "Hello, World!".to_string();
363        /// let output = render_filter(greeting, &placeholder.filters[0]);
364        /// assert_eq!(output, "Hello, !".to_string());
365        /// ```
366        ///
367        /// Specify the number of replacements.
368        /// ```rust
369        /// use blogs_md_easy::{parse_placeholder, render_filter, Filter, Span};
370        ///
371        /// let input = Span::new("{{ £greeting | replace = !, limit: 2 }}");
372        /// let (_, placeholder) = parse_placeholder(input).unwrap();
373        ///
374        /// assert!(matches!(placeholder.filters[0], Filter::Replace { .. }));
375        /// assert_eq!(placeholder.filters[0], Filter::Replace {
376        ///     find: "!".to_string(),
377        ///     replacement: "".to_string(),
378        ///     limit: Some(2),
379        /// });
380        ///
381        /// let greeting = "Hello, World!!!".to_string();
382        /// let output = render_filter(greeting, &placeholder.filters[0]);
383        /// assert_eq!(output, "Hello, World!".to_string());
384        /// ```
385        ///
386        /// Setting all arguments explicitly.
387        /// ```rust
388        /// use blogs_md_easy::{parse_placeholder, render_filter, Filter, Span};
389        ///
390        /// let input = Span::new("{{ £greeting | replace = find: World, replacement: Rust, limit: 1 }}");
391        /// let (_, placeholder) = parse_placeholder(input).unwrap();
392        ///
393        /// assert!(matches!(placeholder.filters[0], Filter::Replace { .. }));
394        /// assert_eq!(placeholder.filters[0], Filter::Replace {
395        ///     find: "World".to_string(),
396        ///     replacement: "Rust".to_string(),
397        ///     limit: Some(1),
398        /// });
399        ///
400        /// let greeting = "Hello, World! Hello, World!".to_string();
401        /// let output = render_filter(greeting, &placeholder.filters[0]);
402        /// assert_eq!(output, "Hello, Rust! Hello, World!".to_string());
403        /// ```
404        limit: Option<u8>,
405    },
406    /// Reverse a string, character by character.
407    ///
408    /// # Example
409    /// ```rust
410    /// use blogs_md_easy::{render_filter, Filter};
411    ///
412    /// let input = "Hello, World!".to_string();
413    /// let filter = Filter::Reverse;
414    /// let output = render_filter(input, &filter);
415    ///
416    /// assert_eq!(output, "!dlroW ,olleH");
417    /// ```
418    Reverse,
419    /// Converts text to another format.
420    ///
421    /// Currently, the only argument is `case`.
422    ///
423    /// `Default argument: case`
424    ///
425    /// # Example
426    /// ```rust
427    /// use blogs_md_easy::{render_filter, Filter, TextCase};
428    ///
429    /// let input = "Hello, World!".to_string();
430    /// let filter = Filter::Text { case: TextCase::Upper };
431    /// let output = render_filter(input, &filter);
432    ///
433    /// assert_eq!(output, "HELLO, WORLD!");
434    /// ```
435    Text {
436        /// Specifies the [`TextCase`] that the font should use.
437        ///
438        /// `Default: lower`
439        ///
440        /// # Examples
441        /// Without an argument, this will default to lowercase.
442        /// ```rust
443        /// use blogs_md_easy::{parse_filter, Filter, Span, TextCase};
444        ///
445        /// let input = Span::new("text");
446        /// let (_, filter) = parse_filter(input).unwrap();
447        ///
448        /// assert!(matches!(filter, Filter::Text { .. }));
449        /// assert_eq!(filter, Filter::Text { case: TextCase::Lower });
450        /// ```
451        ///
452        /// Passing in a case, without an argument is possible too.
453        /// ```rust
454        /// use blogs_md_easy::{parse_filter, Filter, Span, TextCase};
455        ///
456        /// let input = Span::new("text = upper");
457        /// let (_, filter) = parse_filter(input).unwrap();
458        ///
459        /// assert!(matches!(filter, Filter::Text { .. }));
460        /// assert_eq!(filter, Filter::Text { case: TextCase::Upper });
461        /// ```
462        ///
463        /// Alternatively, it is possible to be more explicit.
464        /// ```rust
465        /// use blogs_md_easy::{parse_filter, Filter, Span, TextCase};
466        ///
467        /// let input = Span::new("text = case: snake");
468        /// let (_, filter) = parse_filter(input).unwrap();
469        ///
470        /// assert!(matches!(filter, Filter::Text { .. }));
471        /// assert_eq!(filter, Filter::Text { case: TextCase::Snake });
472        /// ```
473        case: TextCase,
474    },
475    /// Truncates a string to a given length, and applies a `trail`ing string,
476    /// if the string was truncated.
477    ///
478    /// `Default argument: characters`
479    ///
480    /// # Example
481    /// ```rust
482    /// use blogs_md_easy::{render_filter, Filter};
483    ///
484    /// let input = "Hello, World!".to_string();
485    /// let filter = Filter::Truncate { characters: 5, trail: "...".to_string() };
486    /// let output = render_filter(input, &filter);
487    ///
488    /// assert_eq!(output, "Hello...");
489    /// ```
490    Truncate {
491        /// The number of characters the String will be cut to.
492        ///
493        /// If this number is greater than the String's length, then nothing
494        /// happens to the String.
495        ///
496        /// `Default: 100`
497        ///
498        /// # Example
499        /// ```rust
500        /// use blogs_md_easy::{parse_filter, Filter, Span};
501        ///
502        /// let input = Span::new("truncate = trail: --");
503        /// let (_, filter) = parse_filter(input).unwrap();
504        ///
505        /// assert!(matches!(filter, Filter::Truncate { .. }));
506        /// assert_eq!(filter, Filter::Truncate {
507        ///     characters: 100,
508        ///     trail: "--".to_string(),
509        /// });
510        /// ```
511        characters: u8,
512        /// The trailing characters to be appended to a truncated String.
513        ///
514        /// Due to this being appended, that means that your string will exceed
515        /// the characters length.  \
516        /// To counter this, you will need to reduce your `characters` value.
517        ///
518        /// `Default: "..."`
519        ///
520        /// # Example
521        /// ```rust
522        /// use blogs_md_easy::{parse_filter, Filter, Span};
523        ///
524        /// let input = Span::new("truncate = characters: 42");
525        /// let (_, filter) = parse_filter(input).unwrap();
526        ///
527        /// assert!(matches!(filter, Filter::Truncate { .. }));
528        /// assert_eq!(filter, Filter::Truncate {
529        ///     characters: 42,
530        ///     trail: "...".to_string(),
531        /// });
532        /// ```
533        trail: String,
534    }
535}
536
537/// A simple struct to store the key value pair from within the meta section of
538/// a Markdown file.
539///
540/// # Example
541/// ```rust
542/// use blogs_md_easy::{parse_meta_line, Meta, Span};
543///
544/// let input = Span::new("foo = bar");
545/// let (_, meta) = parse_meta_line(input).unwrap();
546/// // Unwrap because key-values are Some() and comments are None.
547/// let meta = meta.unwrap();
548/// assert_eq!(meta, Meta::new("foo", "bar"));
549/// ```
550#[derive(Debug, PartialEq)]
551pub struct Meta {
552    pub key: String,
553    pub value: String,
554}
555
556impl Meta {
557    /// Trims the `key` and `value` and stores them in the respective values in
558    /// this struct.
559    ///
560    /// # Example
561    /// ```rust
562    /// use blogs_md_easy::Meta;
563    ///
564    /// let meta_with_space = Meta::new("  foo  ", "  bar  ");
565    /// let meta_without_space = Meta::new("foo", "bar");
566    /// assert_eq!(meta_with_space, meta_without_space);
567    /// ```
568    pub fn new(key: &str, value: &str) -> Self {
569        Self {
570            key: key.trim().to_string(),
571            value: value.trim().to_string(),
572        }
573    }
574}
575
576/// A position for a Cursor within a [`Span`].
577#[derive(Clone, Copy, Debug, PartialEq)]
578pub struct Marker {
579    pub line: u32,
580    pub offset: usize,
581}
582
583impl Marker {
584    /// Extracts the `location_line()` and `location_offset()` from the [`Span`].
585    pub fn new(span: Span) -> Self {
586        Self {
587            line: span.location_line(),
588            offset: span.location_offset(),
589        }
590    }
591}
592
593impl Default for Marker {
594    /// Create a `Marker` with a `line` of `1` and `offset` of `1`.
595    ///
596    /// # Example
597    /// ```rust
598    /// use blogs_md_easy::Marker;
599    ///
600    /// let marker_default = Marker::default();
601    /// let marker_new = Marker { line: 1, offset: 1 };
602    /// assert_eq!(marker_default, marker_new);
603    /// ```
604    fn default() -> Self {
605        Self {
606            line: 1,
607            offset: 1,
608        }
609    }
610}
611
612/// A helper struct that contains a start and end [`Marker`] of a [`Span`].
613#[derive(Clone, Copy, Debug, Default, PartialEq)]
614pub struct Selection {
615    pub start: Marker,
616    pub end: Marker,
617}
618
619impl Selection {
620    /// Generate a new selection from two [`Span`]s.
621    ///
622    /// The `start` argument will simply extract the `location_line` and
623    /// `location_offset` from the [`Span`].
624    /// The `end` argument will use the `location_line`, but will set the offset
625    /// to the `location_offset` added to the `fragment` length to ensure we
626    /// consume the entire match.
627    pub fn from(start: Span, end: Span) -> Self {
628        Self {
629            start: Marker::new(start),
630            // We cannot use `new` because we need to account for the string
631            // fragment length.
632            end: Marker {
633                line: end.location_line(),
634                offset: end.location_offset() + end.fragment().len()
635            }
636        }
637    }
638}
639
640/// A `Placeholder` is a variable that is created within a Template file.
641///
642/// The syntax for a `Placeholder` is as below.
643///
644/// `{{ £variable_name[| filter_name[= [key: ]value]...] }}`
645///
646/// A compulsory `variable_name`, preceded by a `£`.  \
647/// Then an optional pipe (`|`) separated list of [`Filter`]s.  \
648/// Some filters are just a name, although some have additional arguments.
649///
650/// For more explanation on what a `Placeholder` looks like inside a template,
651/// see [`parse_placeholder`].
652///
653/// For more explanation on what a [`Filter`] looks like inside a `Placeholder`,
654/// see [`parse_filter`].
655#[derive(Clone, Debug, Default, PartialEq)]
656pub struct Placeholder {
657    pub name: String,
658    pub selection: Selection,
659    pub filters: Vec<Filter>,
660}
661
662
663////////////////////////////////////////////////////////////////////////////////
664// Parsers
665/// Parse any character until the end of the line.
666/// This will return all characters, except the newline which will be consumed
667/// and discarded.
668///
669/// # Examples
670/// When there is no newline.
671/// ```rust
672/// use blogs_md_easy::{parse_until_eol, Span};
673///
674/// let input = Span::new("Hello, World!");
675/// let (input, until_eol) = parse_until_eol(input).unwrap();
676/// assert_eq!(input.fragment(), &"");
677/// assert_eq!(until_eol.fragment(), &"Hello, World!");
678/// ```
679///
680/// When there is a newline, the newline is consumed.
681/// ```rust
682/// use blogs_md_easy::{parse_until_eol, Span};
683///
684/// let input = Span::new("Hello, World!\nThis is Sparta!");
685/// let (input, until_eol) = parse_until_eol(input).unwrap();
686/// assert_eq!(input.fragment(), &"This is Sparta!");
687/// assert_eq!(until_eol.fragment(), &"Hello, World!");
688/// ```
689pub fn parse_until_eol(input: Span) -> IResult<Span, Span> {
690    terminated(
691        alt((take_until("\n"), rest)),
692        alt((tag("\n"), tag(""))),
693    )(input)
694}
695
696/// Parse a comment starting with either a `#` or `//` and ending with a newline.
697///
698/// # Example
699/// ```rust
700/// use blogs_md_easy::{parse_meta_comment, Span};
701///
702/// let input = Span::new("# This is a comment");
703/// let (input, meta_comment) = parse_meta_comment(input).unwrap();
704/// assert_eq!(input.fragment(), &"");
705/// assert_eq!(meta_comment.fragment(), &"This is a comment");
706/// ```
707pub fn parse_meta_comment(input: Span) -> IResult<Span, Span> {
708    preceded(
709        // All comments start with either a `#` or `//` followed by a space(s).
710        tuple((alt((tag("#"), tag("//"))), space0)),
711        parse_until_eol,
712    )(input)
713}
714
715/// Parse a key, that starts with an optional `£`, followed by an alphabetic
716/// character, then any number of alphanumeric characters, hyphens and
717/// underscores.
718///
719/// # Examples
720/// A valid variable, consisting of letters and underscores.
721/// ```rust
722/// use blogs_md_easy::{parse_meta_key, Span};
723///
724/// let input = Span::new("£publish_date");
725/// let (_, variable) = parse_meta_key(input).unwrap();
726/// assert_eq!(variable.fragment(), &"publish_date");
727///
728/// let input = Span::new("$publish_date");
729/// let (_, variable) = parse_meta_key(input).unwrap();
730/// assert_eq!(variable.fragment(), &"publish_date");
731/// ```
732/// An invalid example, variables cannot start with a number.
733/// ```rust
734/// use blogs_md_easy::{parse_meta_key, Span};
735///
736/// let input = Span::new("£1_to_2");
737/// let variable = parse_meta_key(input);
738/// assert!(variable.is_err());
739/// ```
740pub fn parse_meta_key(input: Span) -> IResult<Span, Span> {
741    preceded(
742        opt(alt((tag("£"), tag("$")))),
743        parse_variable_name
744    )(input)
745}
746
747/// Parse any number of characters until the end of the line or string.
748///
749/// # Examples
750/// Meta values are essentially just anything that isn't a newline.
751/// ```rust
752/// use blogs_md_easy::{parse_meta_value, Span};
753///
754/// let input = Span::new("This is a value");
755/// let (_, value) = parse_meta_value(input).unwrap();
756/// assert_eq!(value.fragment(), &"This is a value");
757/// ```
758///
759/// However, if you need newlines, then wrap the string in double quotes.  \
760/// Don't forget to escape your quotes too!
761/// ```rust
762/// use blogs_md_easy::{parse_meta_value, Span};
763///
764/// let input = Span::new(r#""This \"value\" is on
765/// a new line""#);
766/// let (_, value) = parse_meta_value(input).unwrap();
767/// assert_eq!(value.fragment(), &"This \\\"value\\\" is on\na new line");
768/// ```
769pub fn parse_meta_value(input: Span) -> IResult<Span, Span> {
770    alt((
771        // Match a delimited quote string.
772        delimited(
773            tag(r#"""#),
774            // Match everything that isn't `\"`.
775            escaped(
776                is_not(r#"\""#),
777                '\\',
778                one_of(r#"nrt\""#)
779            ),
780            tag(r#"""#),
781        ),
782        // The value of the variable, everything after the equals sign.
783        // Continue to a newline or the end of the string.
784        parse_until_eol,
785    ))(input)
786}
787
788/// Parse a key-value pair of meta_key and meta_value.
789///
790/// # Example
791/// ```rust
792/// use blogs_md_easy::{parse_meta_key_value, Span};
793///
794/// let input = Span::new("£publish_date = 2021-01-01");
795/// let (_, meta) = parse_meta_key_value(input).unwrap();
796/// assert_eq!(meta.key, "publish_date");
797/// assert_eq!(meta.value, "2021-01-01");
798/// ```
799pub fn parse_meta_key_value(input: Span) -> IResult<Span, Meta> {
800    separated_pair(
801        parse_meta_key,
802        recognize(tuple((space0, tag("="), space0))),
803        parse_meta_value
804    )(input)
805    .map(|(input, (key, value))| {
806        (input, Meta::new(key.fragment(), value.fragment()))
807    })
808}
809
810/// Parse a line of meta data. This can either be a comment or a key-value pair.
811///
812/// # Examples
813/// Parsing of a comment returns None.
814/// ```rust
815/// use blogs_md_easy::{parse_meta_line, Span};
816///
817/// let input = Span::new("# This is a comment");
818/// let (_, meta) = parse_meta_line(input).unwrap();
819/// assert!(meta.is_none());
820/// ```
821/// Parsing of a key-value pair returns a Meta object.
822/// ```rust
823/// use blogs_md_easy::{parse_meta_line, Span};
824///
825/// let input = Span::new("£publish_date = 2021-01-01");
826/// let (_, meta) = parse_meta_line(input).unwrap();
827/// assert!(&meta.is_some());
828/// let meta = meta.unwrap();
829/// assert_eq!(&meta.key, "publish_date");
830/// assert_eq!(&meta.value, "2021-01-01");
831/// ```
832pub fn parse_meta_line(input: Span) -> IResult<Span, Option<Meta>> {
833    let (input, _) = space0(input)?;
834    let (input, res) = alt((
835        parse_meta_comment.map(|_| None),
836        parse_meta_key_value.map(Some),
837    ))(input)?;
838    let (input, _) = multispace0(input)?;
839    Ok((input, res))
840}
841
842/// Parse the meta section. This is either a `:meta`, `<meta>`, or `<?meta` tag
843/// surrounding a Vector of [`parse_meta_line`].
844///
845/// # Example
846/// ```rust
847/// use blogs_md_easy::{parse_meta_section, Meta, Span};
848///
849/// let input = Span::new(":meta\n// This is the published date\npublish_date = 2021-01-01\n:meta\n# Markdown title");
850/// let (input, meta) = parse_meta_section(input).unwrap();
851/// // Comments are ignored and removed from the Vector.
852/// assert_eq!(meta.len(), 1);
853/// assert_eq!(meta, vec![
854///     Meta {
855///         key: "publish_date".to_string(),
856///         value: "2021-01-01".to_string(),
857///     },
858/// ]);
859/// assert_eq!(input.fragment(), &"# Markdown title");
860/// ```
861pub fn parse_meta_section(input: Span) -> IResult<Span, Vec<Meta>> {
862    alt((
863        // I can't think of a more elegant solution for ensuring the pairs match
864        // one another. The previous solution could open with `:meta` and close
865        // with `</meta>` for example.
866        delimited(
867            tuple((multispace0, tag(":meta"), multispace0)),
868            many1(parse_meta_line),
869            tuple((multispace0, tag(":meta"), multispace0)),
870        ),
871        delimited(
872            tuple((multispace0, tag("<?"), opt(tag("meta")), multispace0)),
873            many1(parse_meta_line),
874            tuple((multispace0, tag("?>"), multispace0)),
875        ),
876        delimited(
877            tuple((multispace0, tag("<meta>"), multispace0)),
878            many1(parse_meta_line),
879            tuple((multispace0, tag("</meta>"), multispace0)),
880        ),
881    ))(input)
882    // Filter out None values, leaving only legitimate meta values.
883    .map(|(input, res)| {
884        // When calling flatten on Option<> types, None values are considered
885        // empty iterators and removed, Some values are considered iterators
886        // with a single element and are therefore unwrapped and returned.
887        (input, res.into_iter().flatten().collect())
888    })
889}
890
891/// Parse the title of the document. This is either a Markdown title or an HTML
892/// heading with the `h1` tag.
893///
894/// # Examples
895/// Using a Markdown heading.
896/// ```rust
897/// use blogs_md_easy::{parse_title, Span};
898///
899/// let input = Span::new("# This is the title");
900/// let (_, title) = parse_title(input).unwrap();
901/// assert_eq!(title.fragment(), &"This is the title");
902/// ```
903/// Using an HTML heading.
904/// ```rust
905/// use blogs_md_easy::{parse_title, Span};
906///
907/// let input = Span::new("<h1>This is the title</h1>");
908/// let (_, title) = parse_title(input).unwrap();
909/// assert_eq!(title.fragment(), &"This is the title");
910/// ```
911pub fn parse_title(input: Span) -> IResult<Span, Span> {
912    let (input, _) = multispace0(input)?;
913
914    let (input, title) = alt((
915        // Either a Markdown title...
916        preceded(tuple((tag("#"), space0)), take_till(|c| c == '\n' || c == '\r')),
917        // ... or an HTML title.
918        delimited(tag("<h1>"), take_until("</h1>"), tag("</h1>"))
919    ))(input)?;
920
921    Ok((input.to_owned(), title.to_owned()))
922}
923
924/// Rewrite of the `nom::is_alphabetic` function that takes a char instead.
925///
926/// # Example
927/// ```rust
928/// use blogs_md_easy::is_alphabetic;
929///
930/// assert!(is_alphabetic('a'));
931/// assert!(is_alphabetic('A'));
932/// assert!(!is_alphabetic('1'));
933/// assert!(!is_alphabetic('-'));
934/// ```
935pub fn is_alphabetic(input: char) -> bool {
936    vec!['a'..='z', 'A'..='Z'].into_iter().flatten().any(|c| c == input)
937}
938
939/// A function that checks if a character is valid for a filter name.
940///
941/// The filter name is the value before the `=` in a Template.
942///
943/// # Example
944/// ```rust
945/// use blogs_md_easy::is_filter_name;
946///
947/// assert!(is_filter_name('a'));
948/// assert!(is_filter_name('A'));
949/// assert!(is_filter_name('1'));
950/// assert!(!is_filter_name('-'));
951/// assert!(!is_filter_name(' '));
952/// ```
953pub fn is_filter_name(input: char) -> bool {
954    input.is_alphanumeric() || ['_'].contains(&input)
955}
956
957/// A function that checks if a character is valid for a filter argument name.
958///
959/// This is the string preceding the `=` in the `meta` section.
960///
961/// # Example
962/// ```rust
963/// use blogs_md_easy::is_filter_arg;
964///
965/// assert!(is_filter_arg('a'));
966/// assert!(is_filter_arg('A'));
967/// assert!(is_filter_arg('1'));
968/// assert!(!is_filter_arg('-'));
969/// assert!(!is_filter_arg(' '));
970/// ```
971pub fn is_filter_arg(input: char) -> bool {
972    input.is_alphanumeric() || ['_'].contains(&input)
973}
974
975/// A function that checks if a character is valid for a filter argument value.
976///
977/// This is the string following the `=` in the `meta` section.
978///
979/// # Example
980/// ```rust
981/// use blogs_md_easy::is_filter_value;
982///
983/// assert!(is_filter_value('a'));
984/// assert!(is_filter_value('A'));
985/// assert!(is_filter_value('1'));
986/// assert!(!is_filter_value('|'));
987/// assert!(!is_filter_value(','));
988/// assert!(!is_filter_value('{'));
989/// assert!(!is_filter_value('}'));
990/// ```
991pub fn is_filter_value(input: char) -> bool {
992    input.is_alphanumeric()
993    || ![' ', '|', ',', '{', '}'].contains(&input)
994}
995
996/// Variable names must start with an alphabetic character, then any number of
997/// alphanumeric characters, hyphens and underscores.
998///
999/// # Examples
1000/// Variables can consist of letters and underscores.
1001/// ```rust
1002/// use blogs_md_easy::{parse_variable_name, Span};
1003///
1004/// let input = Span::new("publish_date");
1005/// let (_, variable) = parse_variable_name(input).unwrap();
1006/// assert_eq!(variable.fragment(), &"publish_date");
1007/// ```
1008///
1009/// Variables cannot start with a number or underscore.
1010/// ```rust
1011/// use blogs_md_easy::{parse_variable_name, Span};
1012///
1013/// let input = Span::new("1_to_2");
1014/// let variable = parse_variable_name(input);
1015/// assert!(variable.is_err());
1016/// ```
1017pub fn parse_variable_name(input: Span) -> IResult<Span, Span> {
1018    recognize(tuple((
1019        take_while_m_n(1, 1, is_alphabetic),
1020        many0(alt((alphanumeric1, tag("-"), tag("_")))),
1021    )))(input)
1022}
1023
1024/// Parse a template placeholder variable. This is a `£` followed by a variable
1025/// name.
1026///
1027/// # Examples
1028/// Variables must start with a `£`.
1029/// ```rust
1030/// use blogs_md_easy::{parse_variable, Span};
1031///
1032/// let input = Span::new("£variable");
1033/// let (_, variable) = parse_variable(input).unwrap();
1034/// assert_eq!(variable.fragment(), &"variable");
1035/// ```
1036///
1037/// In keeping with what people are used to, it is also possible to use a `$`.
1038/// ```rust
1039/// use blogs_md_easy::{parse_variable, Span};
1040///
1041/// let input = Span::new("$variable");
1042/// let (_, variable) = parse_variable(input).unwrap();
1043/// assert_eq!(variable.fragment(), &"variable");
1044/// ```
1045///
1046/// Failing to start with a `£` will return an error.
1047/// ```rust
1048/// use blogs_md_easy::{parse_variable, Span};
1049///
1050/// let input = Span::new("variable");
1051/// let variable = parse_variable(input);
1052/// assert!(variable.is_err());
1053/// ```
1054pub fn parse_variable(input: Span) -> IResult<Span, Span> {
1055    preceded(
1056        alt((tag("£"), tag("$"))),
1057        parse_variable_name
1058    )(input)
1059}
1060
1061/// Parser that will parse exclusively the key-values from after a filter.  \
1062/// This will return the key (before the `:`) and the value (after the `:`). It
1063/// will also return a key of `_` if no key was provided.
1064///
1065/// # Examples
1066/// Ensure that a key-value pair, separated by a colon, can be parsed into a
1067/// tuple.
1068/// ```rust
1069/// use blogs_md_easy::{parse_filter_key_value, Span};
1070///
1071/// let input = Span::new("trail: ...");
1072/// let (_, args) = parse_filter_key_value(input).unwrap();
1073/// assert_eq!(args, ("trail", "..."));
1074/// ```
1075///
1076/// Ensure that a single value can be parsed into a tuple with a key of `_`.
1077/// ```rust
1078/// use blogs_md_easy::{parse_filter_key_value, Span};
1079///
1080/// let input = Span::new("20");
1081/// let (_, args) = parse_filter_key_value(input).unwrap();
1082/// assert_eq!(args, ("_", "20"));
1083/// ```
1084pub fn parse_filter_key_value(input: Span) -> IResult<Span, (&str, &str)> {
1085    alt((
1086        // This matches a key-value separated by a colon.
1087        // Example: `truncate = characters: 20`
1088        separated_pair(
1089            take_while(is_filter_arg).map(|arg: Span| *arg.fragment()),
1090            tuple((space0, tag(":"), space0)),
1091            take_while(is_filter_value).map(|value: Span| *value.fragment()),
1092        ),
1093        // But it's also possible to just provide a value.
1094        // Example: `truncate = 20`
1095        take_while(is_filter_value)
1096        .map(|value: Span| ("_", *value.fragment()))
1097    ))(input)
1098}
1099
1100/// Parser that will parse exclusively the key-values from after a filter.  \
1101/// The signature of a filter is `filter_name = key1: value1, key2: value2,...`,
1102/// or just `filter_name = value`.
1103///
1104/// # Examples
1105/// Ensure that a key-value pair, separated by a colon, can be parsed into a
1106/// tuple.
1107/// ```rust
1108/// use blogs_md_easy::{parse_filter_args, Span};
1109///
1110/// let input = Span::new("characters: 20, trail: ...");
1111/// let (_, args) = parse_filter_args(input).unwrap();
1112/// assert_eq!(args, vec![
1113///     ("characters", "20"),
1114///     ("trail", "..."),
1115/// ]);
1116/// ```
1117///
1118/// Ensure that a single value can be parsed into a tuple with a key of `_`.
1119/// ```rust
1120/// use blogs_md_easy::{parse_filter_args, Span};
1121///
1122/// let input = Span::new("20");
1123/// let (_, args) = parse_filter_args(input).unwrap();
1124/// assert_eq!(args, vec![
1125///     ("_", "20")
1126/// ]);
1127/// ```
1128pub fn parse_filter_args(input: Span) -> IResult<Span, Vec<(&str, &str)>> {
1129    separated_list1(
1130        tuple((space0, tag(","), space0)),
1131        parse_filter_key_value
1132    )(input)
1133}
1134
1135/// Parse a [`Filter`], and optionally its arguments if present.
1136///
1137/// # Examples
1138/// A filter with no arguments.
1139/// ```rust
1140/// use blogs_md_easy::{parse_filter, Filter, Span, TextCase};
1141///
1142/// let input = Span::new("lowercase");
1143/// let (_, filter) = parse_filter(input).unwrap();
1144/// assert!(matches!(filter, Filter::Text { case: TextCase::Lower }));
1145/// ```
1146///
1147/// A filter with just a value, but no key.  \
1148/// This will be parsed as a key of `_`, which will then be set to a key of the
1149/// given enum Struct variant that is deemed the default.  \
1150/// In the case of [`Filter::Truncate`], this will be the `characters`.
1151/// ```rust
1152/// use blogs_md_easy::{parse_filter, Filter, Span};
1153///
1154/// let input = Span::new("truncate = 20");
1155/// let (_, filter) = parse_filter(input).unwrap();
1156/// assert_eq!(filter, Filter::Truncate { characters: 20, trail: "...".to_string() });
1157/// ```
1158///
1159/// A filter with multiple arguments, and given keys.
1160/// ```rust
1161/// use blogs_md_easy::{parse_filter, Filter, Span};
1162///
1163/// let input = Span::new("truncate = characters: 15, trail:...");
1164/// let (_, filter) = parse_filter(input).unwrap();
1165/// assert!(matches!(filter, Filter::Truncate { .. }));
1166/// assert_eq!(filter, Filter::Truncate {
1167///     characters: 15,
1168///     trail: "...".to_string(),
1169/// });
1170/// ```
1171///
1172/// For some filters, default values are provided, if not present.
1173/// ```rust
1174/// use blogs_md_easy::{parse_filter, Filter, Span};
1175///
1176/// let input = Span::new("truncate = trail:...");
1177/// let (_, filter) = parse_filter(input).unwrap();
1178/// assert!(matches!(filter, Filter::Truncate { .. }));
1179/// assert_eq!(filter, Filter::Truncate {
1180///     characters: 100,
1181///     trail: "...".to_string(),
1182/// });
1183/// ```
1184pub fn parse_filter(input: Span) -> IResult<Span, Filter> {
1185    separated_pair(
1186        take_while(is_filter_name),
1187        opt(tuple((space0, tag("="), space0))),
1188        opt(parse_filter_args)
1189    )(input)
1190    .map(|(input, (name, args))| {
1191        let args: HashMap<&str, &str> = args.unwrap_or_default().into_iter().collect();
1192
1193        (input, match name.fragment().to_lowercase().trim() {
1194            // Maths filters.
1195            "ceil" => Filter::Ceil,
1196            "floor" => Filter::Floor,
1197            "round" => Filter::Round {
1198                precision: args.get("precision").unwrap_or(
1199                    args.get("_").unwrap_or(&"0")
1200                ).parse::<u8>().unwrap_or(0),
1201            },
1202
1203            // String filters.
1204            "lowercase" => Filter::Text { case: TextCase::Lower },
1205            "uppercase" => Filter::Text { case: TextCase::Upper },
1206            "markdown" => Filter::Markdown,
1207            "replace" => Filter::Replace {
1208                find: args.get("find").unwrap_or(
1209                    args.get("_").unwrap_or(&"")
1210                ).to_string(),
1211                replacement: args.get("replacement").unwrap_or(&"").to_string(),
1212                limit: args.get("limit").map(|s| s.parse::<u8>().ok()).unwrap_or(None),
1213            },
1214            "reverse" => Filter::Reverse,
1215            "truncate" => Filter::Truncate {
1216                // Attempt to get the characters, but if we can't then we use
1217                // the unnamed value, defined as "_".
1218                characters: args.get("characters").unwrap_or(
1219                    args.get("_").unwrap_or(&"100")
1220                ).parse::<u8>().unwrap_or(100),
1221                trail: args.get("trail").unwrap_or(&"...").to_string(),
1222            },
1223            "text" => Filter::Text {
1224                // Default is `case: TextCase::Lower`.
1225                case: args.get("case").unwrap_or(
1226                    args.get("_").unwrap_or(&"lower")
1227                ).parse::<TextCase>().unwrap_or(TextCase::Lower)
1228            },
1229            _ => {
1230                dbg!(name);
1231                unreachable!();
1232            }
1233        })
1234    })
1235}
1236
1237/// Parsers a pipe (`|`) separated list of [`Filter`]s.
1238///
1239/// # Examples
1240/// A single filter.
1241/// ```rust
1242/// use blogs_md_easy::{parse_filters, Filter, Span, TextCase};
1243///
1244/// // As in {{ £my_variable | lowercase }}
1245/// let input = Span::new("| lowercase");
1246/// let (_, filters) = parse_filters(input).unwrap();
1247/// assert!(matches!(filters[0], Filter::Text { case: TextCase::Lower }));
1248/// ```
1249///
1250/// Multiple filters chained together with `|`.
1251/// ```rust
1252/// use blogs_md_easy::{parse_filters, Filter, Span, TextCase};
1253///
1254/// // As in {{ £my_variable | lowercase | truncate = trail: ..! }}
1255/// let input = Span::new("| lowercase | truncate = trail: ..!");
1256/// let (_, filters) = parse_filters(input).unwrap();
1257/// assert!(matches!(filters[0], Filter::Text { case: TextCase::Lower }));
1258/// assert!(matches!(filters[1], Filter::Truncate { .. }));
1259/// assert_eq!(filters[1], Filter::Truncate {
1260///     characters: 100,
1261///     trail: "..!".to_string(),
1262/// });
1263/// ```
1264pub fn parse_filters(input: Span) -> IResult<Span, Vec<Filter>> {
1265    preceded(
1266        tuple((space0, tag("|"), space0)),
1267        separated_list1(tuple((space0, tag("|"), space0)), parse_filter)
1268    )(input)
1269}
1270
1271/// Parse a template [`Placeholder`].
1272///
1273/// This is a variable name, surrounded by `{{` and `}}`.  \
1274/// Whitespace is optional.
1275///
1276/// # Examples
1277/// A simple [`Placeholder`].
1278/// ```rust
1279/// use blogs_md_easy::{parse_placeholder, Span};
1280///
1281/// let input = Span::new("{{ £variable }}");
1282/// let (_, placeholder) = parse_placeholder(input).unwrap();
1283/// assert_eq!(placeholder.name.as_str(), "variable");
1284/// assert_eq!(placeholder.selection.start.offset, 0);
1285/// assert_eq!(placeholder.selection.end.offset, 16);
1286/// ```
1287///
1288/// A [`Placeholder`] without whitespace.
1289/// ```rust
1290/// use blogs_md_easy::{parse_placeholder, Span};
1291///
1292/// let input = Span::new("{{£variable}}");
1293/// let (_, placeholder) = parse_placeholder(input).unwrap();
1294/// assert_eq!(placeholder.name.as_str(), "variable");
1295/// assert_eq!(placeholder.selection.start.offset, 0);
1296/// assert_eq!(placeholder.selection.end.offset, 14);
1297/// ```
1298///
1299/// A [`Placeholder`] with a single [`Filter`].
1300/// ```rust
1301/// use blogs_md_easy::{parse_placeholder, Filter, Span, TextCase};
1302///
1303/// let input = Span::new("{{ £variable | uppercase }}");
1304/// let (_, placeholder) = parse_placeholder(input).unwrap();
1305/// assert_eq!(placeholder.name.as_str(), "variable");
1306/// assert_eq!(placeholder.selection.start.offset, 0);
1307/// assert_eq!(placeholder.selection.end.offset, 28);
1308/// assert!(matches!(placeholder.filters[0], Filter::Text { case: TextCase::Upper }));
1309/// ```
1310///
1311/// A [`Placeholder`] with a two [`Filter`]s.
1312/// ```rust
1313/// use blogs_md_easy::{parse_placeholder, Filter, Span, TextCase};
1314///
1315/// let input = Span::new("{{ £variable | lowercase | truncate = characters: 42 }}");
1316/// let (_, placeholder) = parse_placeholder(input).unwrap();
1317/// assert_eq!(placeholder.name.as_str(), "variable");
1318/// assert_eq!(placeholder.selection.start.offset, 0);
1319/// assert_eq!(placeholder.selection.end.offset, 56);
1320/// assert!(matches!(placeholder.filters[0], Filter::Text { case: TextCase::Lower }));
1321/// assert_eq!(placeholder.filters[1], Filter::Truncate { characters: 42, trail: "...".to_string() });
1322/// ```
1323pub fn parse_placeholder(input: Span) -> IResult<Span, Placeholder> {
1324    tuple((
1325        tuple((tag("{{"), multispace0)),
1326        parse_variable,
1327        opt(parse_filters),
1328        tuple((multispace0, tag("}}"))),
1329    ))(input)
1330    .map(|(input, (start, variable, filters, end))| {
1331        let mut filters = filters.unwrap_or_default();
1332
1333        // By default, £content will always be parsed as Markdown.
1334        if variable.to_ascii_lowercase().as_str() == "content" && !filters.contains(&Filter::Markdown) {
1335            filters.push(Filter::Markdown);
1336        }
1337
1338        (input, Placeholder {
1339            name: variable.to_string(),
1340            filters,
1341            selection: Selection::from(start.0, end.1)
1342        })
1343    })
1344}
1345
1346/// Parse a string consuming - and discarding - any character, and stopping at
1347/// the first matched placeholder, returning a [`Placeholder`] struct.
1348///
1349/// # Example
1350/// ```rust
1351/// use blogs_md_easy::{take_till_placeholder, Marker, Placeholder, Selection, Span};
1352///
1353/// let input = Span::new("Hello, {{ £name }}!");
1354/// let (input, placeholder) = take_till_placeholder(input).expect("to parse input");
1355/// assert_eq!(input.fragment(), &"!");
1356/// assert_eq!(placeholder, Placeholder {
1357///     name: "name".to_string(),
1358///     selection: Selection {
1359///         start: Marker {
1360///             line: 1,
1361///             offset: 7,
1362///         },
1363///         end: Marker {
1364///             line: 1,
1365///             offset: 19,
1366///         },
1367///     },
1368///     filters: vec![],
1369/// });
1370/// ```
1371pub fn take_till_placeholder(input: Span) -> IResult<Span, Placeholder> {
1372    many_till(anychar, parse_placeholder)(input)
1373    // Map to remove anychar's captures.
1374    .map(|(input, (_, placeholder))| (input, placeholder))
1375}
1376
1377/// Consume an entire string, and return a Vector of a tuple; where the first
1378/// element is a String of the variable name, and the second element is the
1379/// [`Placeholder`].
1380///
1381/// # Example
1382/// ```rust
1383/// use blogs_md_easy::{parse_placeholder_locations, Span};
1384///
1385/// let input = Span::new("Hello, {{ £name }}!");
1386/// let placeholders = parse_placeholder_locations(input).unwrap();
1387/// assert_eq!(placeholders.len(), 1);
1388/// assert_eq!(placeholders[0].name.as_str(), "name");
1389/// assert_eq!(placeholders[0].selection.start.offset, 7);
1390/// assert_eq!(placeholders[0].selection.end.offset, 19);
1391/// ```
1392pub fn parse_placeholder_locations(input: Span) -> Result<Vec<Placeholder>, Box<dyn Error>> {
1393    let (_, mut placeholders) = many0(take_till_placeholder)(input).unwrap_or((input, Vec::new()));
1394
1395    // Sort in reverse so that when we replace each placeholder, the offsets do
1396    // not affect offsets after this point.
1397    placeholders.sort_by(|a, b| b.selection.start.offset.cmp(&a.selection.start.offset));
1398
1399    Ok(placeholders)
1400}
1401
1402////////////////////////////////////////////////////////////////////////////////
1403// Functions
1404
1405/// Replaces a substring in the original string with a replacement string.
1406///
1407/// # Arguments
1408///
1409/// * `original` - The original string.
1410/// * `start` - The start position of the substring in the original string.
1411/// * `end` - The end position of the substring in the original string.
1412/// * `replacement` - The string to replace the substring.
1413///
1414/// # Returns
1415///
1416/// * A new string with the replacement in place of the original substring.
1417///
1418/// # Example
1419/// ```
1420/// use blogs_md_easy::replace_substring;
1421///
1422/// let original = "Hello, World!";
1423/// let start = 7;
1424/// let end = 12;
1425/// let replacement = "Rust";
1426/// let result = replace_substring(original, start, end, replacement);
1427/// println!("{}", result);  // Prints: "Hello, Rust!"
1428/// ```
1429pub fn replace_substring(original: &str, start: usize, end: usize, replacement: &str) -> String {
1430    let mut result = String::new();
1431    result.push_str(&original[..start]);
1432    result.push_str(replacement);
1433    result.push_str(&original[end..]);
1434    result
1435}
1436
1437/// Creates a HashMap of key-value pairs from meta values.
1438///
1439/// # Arguments
1440/// * `markdown` - A LocatedSpan of the markdown file.
1441/// * `meta_values` - An optional vector of Meta values.
1442///
1443/// # Returns
1444/// Convert the meta_values into a [`HashMap`], then parse the title and content
1445/// from the markdown file.
1446///
1447/// # Example
1448/// ```
1449/// use blogs_md_easy::{create_variables, parse_meta_section, Span};
1450///
1451/// let markdown = Span::new(":meta\nauthor = John Doe\n:meta\n# Markdown title\nContent paragraph");
1452/// let (markdown, meta_values) = parse_meta_section(markdown).unwrap_or((markdown, vec![]));
1453/// let variables = create_variables(markdown, meta_values).expect("to create variables");
1454/// assert_eq!(variables.get("title").unwrap(), "Markdown title");
1455/// assert_eq!(variables.get("author").unwrap(), "John Doe");
1456/// assert_eq!(variables.get("content").unwrap(), "# Markdown title\nContent paragraph");
1457/// ```
1458pub fn create_variables(markdown: Span, meta_values: Vec<Meta>) -> Result<HashMap<String, String>, Box<dyn Error>> {
1459    let mut variables: HashMap<String, String> = meta_values
1460        .into_iter()
1461        .map(|meta| (meta.key.to_owned(), meta.value.to_owned()))
1462        .collect();
1463
1464    // Make sure that we have a title and content variable.
1465    if !variables.contains_key("title") {
1466        if let Ok(title) = parse_title(markdown) {
1467            let (_, title) = title;
1468            variables.insert("title".to_string(), title.to_string());
1469        } else {
1470            return Err("Missing title".to_string())?;
1471        }
1472    }
1473    if !variables.contains_key("content") {
1474        let content = markdown.fragment().trim().to_string();
1475        variables.insert("content".to_string(), content);
1476    }
1477
1478    Ok(variables)
1479}
1480
1481/// Make the start of each word capital, splitting on `sep`.
1482///
1483/// # Examples
1484/// A simple phrase with a space.
1485/// ```rust
1486/// use blogs_md_easy::split_string;
1487///
1488/// let phrase = "Hello World";
1489/// let phrase = split_string(phrase.to_string(), &[' ', '-']);
1490/// //let phrase = phrase.iter().map(|word| word.as_str()).collect::<&str>();
1491/// assert_eq!(phrase, vec!["Hello", " ", "World"]);
1492/// ```
1493///
1494/// A name with a hyphen.
1495/// ```rust
1496/// use blogs_md_easy::split_string;
1497///
1498/// let phrase = "John Doe-Bloggs";
1499/// let phrase = split_string(phrase.to_string(), &[' ', '-']);
1500/// //let phrase = phrase.iter().map(|word| word.as_str()).collect::<&str>();
1501/// assert_eq!(phrase, vec!["John", " ", "Doe", "-", "Bloggs"]);
1502/// ```
1503///
1504/// Two separators in a row.
1505/// ```rust
1506/// use blogs_md_easy::split_string;
1507///
1508/// let phrase = "Hello, World!";
1509/// let phrase = split_string(phrase.to_string(), &[' ', ',', '!']);
1510/// //let phrase = phrase.iter().map(|word| word.as_str()).collect::<&str>()
1511/// assert_eq!(phrase, vec!["Hello", ",", " ", "World", "!"]);
1512/// ```
1513pub fn split_string(phrase: String, separators: &[char]) -> Vec<String> {
1514    let mut words = Vec::new();
1515    let mut current_word = String::new();
1516
1517    for c in phrase.chars() {
1518        // If we hit a separator; push the current word, then the separator.
1519        // Otherwise, add the character to the current word.
1520        if separators.contains(&c) {
1521            // Make sure that we aren't pushing an empty string into the Vec.
1522            // This cannot be added as an `&&` above, because otherwise it
1523            // pushes a separator onto the start of `current_word` in the event
1524            // that we have two separators in a row.
1525            if !current_word.is_empty() {
1526                words.push(current_word.clone());
1527                current_word.clear();
1528            }
1529            words.push(c.to_string());
1530        } else {
1531            current_word.push(c);
1532        }
1533    }
1534
1535    if !current_word.is_empty() {
1536        words.push(current_word);
1537    }
1538    words
1539}
1540
1541/// Take a variable, and run it through a [`Filter`] function to get the new
1542/// output.
1543///
1544/// For an example of how these [`Filter`]s work within a [`Placeholder`], see
1545/// [`parse_placeholder`].
1546///
1547/// # Examples
1548/// [`Filter`] that has no arguments.
1549/// ```rust
1550/// use blogs_md_easy::{render_filter, Filter, TextCase};
1551///
1552/// let variable = "hello, world!".to_string();
1553/// assert_eq!("HELLO, WORLD!", render_filter(variable, &Filter::Text { case: TextCase::Upper }));
1554/// ```
1555///
1556/// [`Filter`] that has arguments.
1557/// ```rust
1558/// use blogs_md_easy::{render_filter, Filter};
1559///
1560/// let variable = "hello, world!".to_string();
1561/// assert_eq!("hello...", render_filter(variable, &Filter::Truncate { characters: 5, trail: "...".to_string() }));
1562/// ```
1563pub fn render_filter(variable: String, filter: &Filter) -> String {
1564    match filter {
1565        // Maths filters.
1566        Filter::Ceil => variable.parse::<f64>().unwrap_or_default().ceil().to_string(),
1567        Filter::Floor => variable.parse::<f64>().unwrap_or_default().floor().to_string(),
1568        Filter::Round { precision } => variable
1569            .parse::<f64>()
1570            .unwrap_or_default()
1571            // Be default, Rust rounds away all decimals.
1572            // So we want to move the decimal places `precision` places to the
1573            // left.
1574            .mul(10_f64.powi((*precision as u32) as i32))
1575            // Now round, removing all decimal places.
1576            .round()
1577            // Now move the decimal place back.
1578            .div(10_f64.powi((*precision as u32) as i32))
1579            .to_string(),
1580
1581        // String filters.
1582        Filter::Markdown  => {
1583            markdown::to_html_with_options(&variable, &markdown::Options {
1584                compile: markdown::CompileOptions {
1585                    allow_dangerous_html: true,
1586                    allow_dangerous_protocol: false,
1587                    ..Default::default()
1588                },
1589                ..Default::default()
1590            }).unwrap_or_default()
1591        },
1592        Filter::Replace { find, replacement, limit } => {
1593            if limit.is_none() {
1594                variable.replace(find, replacement)
1595            } else {
1596                // Subtract 1 to account for the final iteration.
1597                let segments = variable.split(find).count() - 1;
1598
1599                variable
1600                .split(find)
1601                .enumerate()
1602                .map(|(count, part)| {
1603                    // We can safely unwrap, because `limit.is_some()`.
1604                    if (count as u8) < limit.unwrap() {
1605                        format!("{}{}", part, replacement)
1606                    } else {
1607                        format!("{}{}", part, if count < segments { find } else { "" })
1608                    }
1609                })
1610                .collect::<Vec<String>>()
1611                .join("")
1612            }
1613        },
1614        Filter::Reverse => variable.chars().rev().collect(),
1615        Filter::Truncate { characters, trail } => {
1616            let mut new_variable = variable.to_string();
1617            new_variable.truncate(*characters as usize);
1618            // Now truncate and append the trail.
1619            if (variable.len() as u8) > *characters {
1620                new_variable.push_str(trail);
1621            }
1622            new_variable
1623        },
1624        Filter::Text { case } => {
1625            let separators = &[' ', ',', '!', '-', '_'];
1626            match case {
1627                TextCase::Lower => variable.to_lowercase(),
1628                TextCase::Upper => variable.to_uppercase(),
1629                TextCase::Title => {
1630                    split_string(variable, separators)
1631                    .into_iter()
1632                    .map(|word| {
1633                        if word.len() == 1 && separators.contains(&word.chars().next().unwrap_or_default()) {
1634                            word
1635                        } else {
1636                            word[0..1].to_uppercase() + &word[1..]
1637                        }
1638                    })
1639                    .collect::<String>()
1640                },
1641                TextCase::Kebab => variable
1642                    .to_lowercase()
1643                    .split(|c| separators.contains(&c))
1644                    .filter(|s| !s.is_empty())
1645                    .collect::<Vec<&str>>()
1646                    .join("-"),
1647                TextCase::Snake => variable
1648                    .to_lowercase()
1649                    .split(|c| separators.contains(&c))
1650                    .filter(|s| !s.is_empty())
1651                    .collect::<Vec<&str>>()
1652                    .join("_"),
1653                TextCase::Pascal => variable
1654                    .split(|c| separators.contains(&c))
1655                    .filter(|s| !s.is_empty())
1656                    .map(|s| {
1657                        let mut c = s.chars();
1658                        match c.next() {
1659                            Some(first) => first.to_uppercase().collect::<String>() + c.as_str(),
1660                            None => String::new(),
1661                        }
1662                    })
1663                    .collect::<Vec<String>>()
1664                    .join(""),
1665                TextCase::Camel => variable
1666                    .split(|c| separators.contains(&c))
1667                    .filter(|s| !s.is_empty())
1668                    .enumerate()
1669                    .map(|(i, s)| {
1670                        let mut c = s.chars();
1671                        match c.next() {
1672                            Some(first) => (if i == 0 {
1673                                first.to_lowercase().collect::<String>()
1674                            } else {
1675                                first.to_uppercase().collect::<String>()
1676                            }) + c.as_str(),
1677                            None => String::new(),
1678                        }
1679                    })
1680                    .collect::<Vec<String>>()
1681                    .join(""),
1682                TextCase::Invert => variable.chars().fold(String::new(), |mut str, c| {
1683                    if c.is_lowercase() {
1684                        str.push_str(&c.to_uppercase().collect::<String>());
1685                    } else {
1686                        str.push_str(&c.to_lowercase().collect::<String>());
1687                    }
1688                    str
1689                }),
1690            }
1691        },
1692    }
1693}