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}