ogmarkup/
lib.rs

1//  ogmarkup -- a markup language for story writers
2//  Copyright (C) <year>  <name of author>
3//
4//  This program is free software: you can redistribute it and/or modify
5//  it under the terms of the GNU General Public License as published by
6//  the Free Software Foundation, either version 3 of the License, or
7//  (at your option) any later version.
8//
9//  This program is distributed in the hope that it will be useful,
10//  but WITHOUT ANY WARRANTY; without even the implied warranty of
11//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12//  GNU General Public License for more details.
13//
14//  You should have received a copy of the GNU General Public License
15//  along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17#![cfg_attr(feature = "html", feature(proc_macro_hygiene))]
18
19#[macro_use]
20extern crate nom;
21#[cfg(feature = "html")]
22extern crate maud;
23
24pub mod ast;
25pub mod generator;
26#[cfg(feature = "html")]
27pub mod html;
28pub mod stats;
29pub mod typography;
30
31use ast::*;
32pub use generator::render;
33use generator::Output;
34use typography::Typography;
35
36use nom::character::streaming::{alphanumeric1, anychar};
37
38const BARRIER_TOKENS: &str = "!?.\"«»`+*[]<>|_'’,;-—: \n\r\t   ";
39
40macro_rules! cond_reduce (
41    ($input:expr, $cond:expr, $sub:ident!( $($args:tt)* )) => (
42        map_opt!($input, cond!($cond, $sub!($($args)*)), |x| x)
43    );
44);
45
46/// Synonym to `many1!(complete!(x))`
47macro_rules! some (
48    ($input:expr, $sub:ident!( $($args:tt)* )) => (
49        many1!($input, complete!($sub!($($args)*)))
50    );
51    ($input:expr, $f:expr) => (
52        some!($input, call!($f))
53    );
54);
55
56/// `consume_until!(x)` will consume the input according to the following rules:
57/// 1. at least one character
58/// 2.a. until it finds one character that belongs to $arr, or
59/// 2.b. until it reaches the end of the input
60macro_rules! consume_until (
61    ($input:expr, $arr:expr) => (
62        {
63            use nom::Err;
64            use nom::error::ErrorKind;
65
66            match take_till1!($input, |c| $arr.contains(c)) {
67                Err(Err::Incomplete(_)) => if $input.len() != 0 {
68                    call!($input, nom::combinator::rest)
69                } else {
70                    Err(Err::Error(error_position!($input, ErrorKind::TakeUntil)))
71                },
72                Ok((i, o)) => Ok((i, o)),
73                Err(e) => Err(e)
74            }
75        }
76    );
77);
78
79/// `recover_incomplete!(f)` will fail only if `f` fails due to something else
80/// than an incomplete stream.
81macro_rules! recover_incomplete (
82    ($i:expr, $submac:ident!( $($args:tt)* )) => (
83        {
84            use nom::lib::std::result::Result::*;
85            use nom::Err;
86
87            match $submac!($i, $($args)*) {
88                Err(Err::Incomplete(_)) =>  {
89                    Ok(("", ()))
90                },
91                Ok((rest, _)) => {
92                    Ok((rest, ()))
93                },
94                Err(rest) => Err(rest)
95            }
96        }
97    );
98);
99
100named!(white_spaces<&str, ()>,
101       recover_incomplete!(take_while!(|c| "\r\t    ".contains(c)))
102);
103
104named!(blank<&str, ()>,
105       do_parse!(
106           white_spaces >>
107           opt!(do_parse!(char!('\n') >> white_spaces >> (()))) >>
108           (())
109       )
110);
111
112#[test]
113fn test_white_spaces() {
114    assert_eq!(white_spaces(","), Ok((",", ())));
115    assert_eq!(white_spaces("     ,"), Ok((",", ())));
116    assert_eq!(white_spaces("     "), Ok(("", ())));
117}
118
119named!(atom<&str, Atom>, do_parse!(
120    opt!(do_parse!(char!('\n') >> white_spaces >> (()))) >>
121    r: alt!( map!(consume_until!(BARRIER_TOKENS), Atom::Word)
122           | do_parse!(char!(';')  >> (Atom::Punctuation(Mark::Semicolon)))
123           | do_parse!(char!(':')  >> (Atom::Punctuation(Mark::Colon)))
124           | do_parse!(char!('?')  >> (Atom::Punctuation(Mark::Question)))
125           | do_parse!(char!('!')  >> (Atom::Punctuation(Mark::Exclamation)))
126           | do_parse!(tag!("---") >> (Atom::Punctuation(Mark::LongDash)))
127           | do_parse!(char!('—')  >> (Atom::Punctuation(Mark::LongDash)))
128           | do_parse!(tag!("--")  >> (Atom::Punctuation(Mark::Dash)))
129           | do_parse!(char!('–')  >> (Atom::Punctuation(Mark::LongDash)))
130           | do_parse!(char!(',')  >> (Atom::Punctuation(Mark::Comma)))
131           | do_parse!(char!('-')  >> (Atom::Punctuation(Mark::Hyphen)))
132           | do_parse!(char!('…')  >> (Atom::Punctuation(Mark::SuspensionPoints)))
133           | do_parse!(char!('.')  >> some!(char!('.'))
134                                   >> (Atom::Punctuation(Mark::SuspensionPoints)))
135           | do_parse!(char!('.')  >> (Atom::Punctuation(Mark::Point)))
136           | do_parse!(char!('\'') >> (Atom::Punctuation(Mark::Apostrophe)))
137           | do_parse!(char!('’')  >> (Atom::Punctuation(Mark::Apostrophe)))
138           | do_parse!(char!('`')  >> lw: take_until!("`")
139                                   >> char!('`')
140                                   >> (Atom::Word(lw)))
141           )            >>
142    white_spaces >>
143    (r)
144));
145
146#[test]
147fn test_atom() {
148    assert_eq!(atom(","), Ok(("", Atom::Punctuation(Mark::Comma))));
149    assert_eq!(atom(", "), Ok(("", Atom::Punctuation(Mark::Comma))));
150    assert_eq!(
151        atom("......."),
152        Ok(("", Atom::Punctuation(Mark::SuspensionPoints)))
153    );
154    assert_eq!(atom("’"), Ok(("", Atom::Punctuation(Mark::Apostrophe))));
155    assert_eq!(atom("`@test`"), Ok(("", Atom::Word("@test"))));
156    assert_eq!(atom("test"), Ok(("", Atom::Word("test"))));
157}
158
159named_args!(format_rec(in_strong: bool, in_emph: bool, in_quote: bool)<&str, Format>,
160    alt!( map!(some!(atom), Format::Raw)
161        | cond_reduce!(!in_strong, do_parse!(
162            opt!(do_parse!(char!('\n') >> white_spaces >> (()))) >>
163            char!('+') >>
164            white_spaces >>
165            st: some!(call!(format_rec, true, in_emph, in_quote)) >>
166            blank >>
167            char!('+') >>
168            white_spaces >>
169            (Format::StrongEmph(st))
170          ))
171        | cond_reduce!(!in_emph, do_parse!(
172            opt!(do_parse!(char!('\n') >> white_spaces >> (()))) >>
173            char!('*') >>
174            white_spaces >>
175            st: some!(call!(format_rec, in_strong, true, in_quote)) >>
176            blank >>
177            char!('*') >>
178            white_spaces >>
179            (Format::Emph(st))
180          ))
181        | cond_reduce!(!in_quote, do_parse!(
182            opt!(do_parse!(char!('\n') >> white_spaces >> (()))) >>
183            alt!(char!('"') | char!('«')) >>
184            white_spaces >>
185            st: some!(call!(format_rec, in_strong, in_emph, true)) >>
186            white_spaces >>
187            alt!(char!('"') | char!('»')) >>
188            white_spaces >>
189            (Format::Quote(st))
190          ))
191    )
192);
193
194named!(format<&str, Format>, call!(format_rec, false, false, false));
195
196#[test]
197fn test_format() {
198    assert_eq!(
199        format("Hi stranger, how are you?"),
200        Ok((
201            "",
202            Format::Raw(vec![
203                Atom::Word("Hi"),
204                Atom::Word("stranger"),
205                Atom::Punctuation(Mark::Comma),
206                Atom::Word("how"),
207                Atom::Word("are"),
208                Atom::Word("you"),
209                Atom::Punctuation(Mark::Question),
210            ])
211        ))
212    );
213
214    assert_eq!(
215        format(
216            r#"Hi stranger, how
217are you?"#
218        ),
219        Ok((
220            "",
221            Format::Raw(vec![
222                Atom::Word("Hi"),
223                Atom::Word("stranger"),
224                Atom::Punctuation(Mark::Comma),
225                Atom::Word("how"),
226                Atom::Word("are"),
227                Atom::Word("you"),
228                Atom::Punctuation(Mark::Question),
229            ])
230        ))
231    );
232
233    assert_eq!(
234        format(
235            r#"Hi stranger, how
236
237are you?"#
238        ),
239        Ok((
240            "\n\nare you?",
241            Format::Raw(vec![
242                Atom::Word("Hi"),
243                Atom::Word("stranger"),
244                Atom::Punctuation(Mark::Comma),
245                Atom::Word("how")
246            ])
247        ))
248    );
249
250    assert_eq!(
251        format("+Hi stranger+, how are you?"),
252        Ok((
253            ", how are you?",
254            Format::StrongEmph(vec![Format::Raw(vec![
255                Atom::Word("Hi"),
256                Atom::Word("stranger")
257            ])])
258        ))
259    );
260
261    assert_eq!(
262        format("+Hi *stranger*+, how are you?"),
263        Ok((
264            ", how are you?",
265            Format::StrongEmph(vec![
266                Format::Raw(vec![Atom::Word("Hi")]),
267                Format::Emph(vec![Format::Raw(vec![Atom::Word("stranger")])])
268            ])
269        ))
270    );
271
272    assert_eq!(format("+Hi *+ stranger*, how are you?").is_err(), true);
273}
274
275named_args!(reply(b: char, e: char)<&str, Reply>, do_parse!(
276    char!(b)  >>
277    call!(white_spaces) >>
278    before: some!(format) >>
279    x: call!(anychar) >>
280    r: alt!( cond_reduce!(x == e, do_parse!((None)))
281           | cond_reduce!(x == '|', do_parse!(
282               call!(white_spaces) >>
283               prep: some!(format) >>
284               char!('|') >>
285               call!(white_spaces) >>
286               after: opt!(some!(format)) >>
287               char!(e) >>
288               (Some((prep, after)))
289           ))
290    ) >>
291    call!(white_spaces) >>
292    (match r {
293        None => {
294            Reply::Simple(before)
295        },
296        Some((prep, after)) => {
297            Reply::WithSay(before, prep, after)
298        }
299    })
300));
301
302#[test]
303fn test_reply() {
304    assert_eq!(
305        reply("[Hi stranger]", '[', ']'),
306        Ok((
307            "",
308            Reply::Simple(vec![Format::Raw(vec![
309                Atom::Word("Hi"),
310                Atom::Word("stranger"),
311            ])])
312        ))
313    );
314
315    assert_eq!(
316        reply("[Hi stranger,| they salute.|]", '[', ']'),
317        Ok((
318            "",
319            Reply::WithSay(
320                vec![Format::Raw(vec![
321                    Atom::Word("Hi"),
322                    Atom::Word("stranger"),
323                    Atom::Punctuation(Mark::Comma)
324                ])],
325                vec![Format::Raw(vec![
326                    Atom::Word("they"),
327                    Atom::Word("salute"),
328                    Atom::Punctuation(Mark::Point)
329                ])],
330                None
331            )
332        ))
333    );
334}
335
336// Because I have spent a fair amonut of time in a train trying to figure out
337// the best way to implement this parser, I consider it is probably a good idea
338// to explain why there is a call to `blank` for Dialogue and Thought, but not
339// teller. The reason is actually quite simple: the `atom` parser is already
340// eating the whitespaces before him, so if we do add blank to Teller as well,
341// then this means two newlines are consumed when a Teller component follows a
342// Dialogue for instance.
343named!(component<&str, Component>, alt! (
344            do_parse!(
345              tel: some!(format) >>
346              (Component::Teller(tel)))
347         |  do_parse!(
348              blank >>
349              dial: call!(reply, '[' , ']') >>
350              by: opt!(complete!(do_parse!(
351                     char!('(') >>
352                     name: call!(alphanumeric1) >>
353                     char!(')') >>
354                     white_spaces >>
355                     (name)))) >>
356              (Component::Dialogue(dial, by)))
357         |  do_parse!(
358              blank >>
359              th: call!(reply, '<' , '>') >>
360              by: opt!(complete!(do_parse!(
361                     char!('(') >>
362                     name: call!(alphanumeric1) >>
363                     char!(')') >>
364                     white_spaces >>
365                     (name)))) >>
366              (Component::Thought(th, by)))
367         | map!(consume_until!("\n"), Component::IllFormed)
368         )
369);
370
371#[test]
372fn test_component() {
373    assert_eq!(
374        component("[Hi]"),
375        Ok((
376            "",
377            Component::Dialogue(
378                Reply::Simple(vec![Format::Raw(vec![Atom::Word("Hi")])]),
379                None
380            )
381        ))
382    );
383
384    assert_eq!(
385        component("Hi stranger,\n*this* is me."),
386        Ok((
387            "",
388            Component::Teller(vec![
389                Format::Raw(vec![
390                    Atom::Word("Hi"),
391                    Atom::Word("stranger"),
392                    Atom::Punctuation(Mark::Comma),
393                ]),
394                Format::Emph(vec![Format::Raw(vec![Atom::Word("this"),])]),
395                Format::Raw(vec![
396                    Atom::Word("is"),
397                    Atom::Word("me"),
398                    Atom::Punctuation(Mark::Point)
399                ])
400            ])
401        ))
402    );
403
404    assert_eq!(
405        component("Hi stranger, this is me."),
406        Ok((
407            "",
408            Component::Teller(vec![Format::Raw(vec![
409                Atom::Word("Hi"),
410                Atom::Word("stranger"),
411                Atom::Punctuation(Mark::Comma),
412                Atom::Word("this"),
413                Atom::Word("is"),
414                Atom::Word("me"),
415                Atom::Punctuation(Mark::Point)
416            ])])
417        ))
418    );
419
420    assert_eq!(
421        component("[Hi](alice)"),
422        Ok((
423            "",
424            Component::Dialogue(
425                Reply::Simple(vec![Format::Raw(vec![Atom::Word("Hi")])]),
426                Some("alice")
427            )
428        ))
429    );
430
431    assert_eq!(
432        component("[Hi \ntest\n\n"),
433        Ok(("\ntest\n\n", Component::IllFormed("[Hi ")))
434    );
435}
436
437named!(empty_line<&str, ()>, do_parse!(
438       white_spaces >>
439       char!('\n')  >>
440       white_spaces >>
441       (())
442));
443
444named!(
445    paragraph<&str, Paragraph>,
446    do_parse!(
447        not!(peek!(one_of!("_="))) >>
448        p: some!(component) >>
449        many0!(complete!(empty_line)) >>
450        (Paragraph(p))
451    )
452);
453
454#[test]
455fn test_paragraph() {
456    assert_eq!(
457        paragraph("+Hi+"),
458        Ok((
459            "",
460            Paragraph(vec![Component::Teller(vec![Format::StrongEmph(vec![
461                Format::Raw(vec![Atom::Word("Hi")]),
462            ])])])
463        ))
464    );
465
466    assert_eq!(
467        paragraph("[Hi stranger, this is me.] Indeed.\n\n[Hi]"),
468        Ok((
469            "[Hi]",
470            Paragraph(vec![
471                Component::Dialogue(
472                    Reply::Simple(vec![Format::Raw(vec![
473                        Atom::Word("Hi"),
474                        Atom::Word("stranger"),
475                        Atom::Punctuation(Mark::Comma),
476                        Atom::Word("this"),
477                        Atom::Word("is"),
478                        Atom::Word("me"),
479                        Atom::Punctuation(Mark::Point)
480                    ])]),
481                    None
482                ),
483                Component::Teller(vec![Format::Raw(vec![
484                    Atom::Word("Indeed"),
485                    Atom::Punctuation(Mark::Point)
486                ])])
487            ])
488        ))
489    );
490}
491
492named_args!(
493    search_recovery_point_rec<'a>(acc: &mut Vec<&'a str>)<&'a str, ()>,
494    alt!(
495        map!(some!(empty_line), |_| ())
496      | do_parse!(
497          l: consume_until!("\n") >>
498          do_parse!(({ acc.push(l) })) >>
499          char!('\n') >>
500          call!(search_recovery_point_rec, acc) >>
501          (())
502      )
503    )
504);
505
506fn search_recovery_point<'input>(
507    input: &'input str,
508) -> nom::IResult<&'input str, Vec<&'input str>> {
509    let mut acc = vec![];
510    match search_recovery_point_rec(input, &mut acc) {
511        Ok((input, _)) => Ok((input, acc)),
512        Err(_) => Ok(("", acc)),
513    }
514}
515
516#[test]
517fn test_recovery() {
518    assert_eq!(
519        search_recovery_point(
520            r#"We need
521to try.
522
523Recover!"#
524        ),
525        Ok(("Recover!", vec!["We need", "to try."]))
526    );
527}
528
529named!(
530    section<&str, Section>, do_parse!(
531    res: alt!(
532        complete!(do_parse!(
533            some!(char!('_')) >>
534            cls: opt!(
535                    do_parse!(
536                        cls: call!(alphanumeric1) >>
537                        some!(char!('_')) >>
538                        (cls)
539                    )
540            ) >>
541            some!(empty_line) >>
542            sec: some!(paragraph) >>
543            some!(char!('_')) >>
544            (Section::Aside(cls, sec))
545        ))
546      | do_parse!(
547          opt!(do_parse!(some!(char!('=')) >> some!(empty_line) >> (()))) >>
548          r: map!(some!(paragraph), Section::Story) >>
549          (r)
550        )
551      | map_opt!(search_recovery_point, |x: Vec<_>| if !x.is_empty() { Some(Section::IllFormed(x)) } else { None } )
552    ) >>
553    many0!(complete!(empty_line)) >>
554    (res)
555));
556
557#[test]
558fn test_section() {
559    assert!(section("").is_err());
560
561    assert_eq!(
562        section("+\nHi  \n +"),
563        Ok((
564            "",
565            Section::Story(vec![Paragraph(vec![Component::Teller(vec![
566                Format::StrongEmph(vec![Format::Raw(vec![Atom::Word("Hi")])])
567            ])])])
568        ))
569    );
570
571    assert_eq!(
572        section("+Hi+"),
573        Ok((
574            "",
575            Section::Story(vec![Paragraph(vec![Component::Teller(vec![
576                Format::StrongEmph(vec![Format::Raw(vec![Atom::Word("Hi")])])
577            ])])])
578        ))
579    );
580
581    assert_eq!(
582        section(
583            r#"_____letter____
584Dear friend.
585
586I love you.
587_______________"#
588        ),
589        Ok((
590            "",
591            Section::Aside(
592                Some("letter"),
593                vec![
594                    Paragraph(vec![Component::Teller(vec![Format::Raw(vec![
595                        Atom::Word("Dear"),
596                        Atom::Word("friend"),
597                        Atom::Punctuation(Mark::Point)
598                    ])])]),
599                    Paragraph(vec![Component::Teller(vec![Format::Raw(vec![
600                        Atom::Word("I"),
601                        Atom::Word("love"),
602                        Atom::Word("you"),
603                        Atom::Punctuation(Mark::Point)
604                    ])])])
605                ]
606            )
607        ))
608    );
609
610    assert_eq!(
611        section(
612            r#"_____letter____
613Dear friend.
614
615I love you."#
616        ),
617        Ok((
618            "I love you.",
619            Section::IllFormed(vec!["_____letter____", "Dear friend."])
620        ))
621    );
622
623    assert_eq!(
624        section(r#"Dear friend."#),
625        Ok((
626            "",
627            Section::Story(vec![Paragraph(vec![Component::Teller(vec![Format::Raw(
628                vec![
629                    Atom::Word("Dear"),
630                    Atom::Word("friend"),
631                    Atom::Punctuation(Mark::Point)
632                ]
633            )])])])
634        ))
635    );
636}
637
638named!(
639    document<&str, Document>, do_parse!(
640      opt!(complete!(blank)) >>
641      many0!(complete!(empty_line)) >>
642      x: many0!(section) >>
643      (Document(x))
644    )
645);
646
647#[test]
648fn test_empty_document() {
649    assert_eq!(document(""), Ok(("", Document(vec![]))));
650}
651
652#[test]
653fn test_document_with_leading_ws() {
654    assert_eq!(
655        document("   \n  \n She opened the letter."),
656        Ok((
657            "",
658            Document(vec![Section::Story(vec![Paragraph(vec![
659                Component::Teller(vec![Format::Raw(vec![
660                    Atom::Word("She"),
661                    Atom::Word("opened"),
662                    Atom::Word("the"),
663                    Atom::Word("letter"),
664                    Atom::Punctuation(Mark::Point),
665                ])])
666            ])])])
667        ))
668    );
669}
670
671#[test]
672fn test_incomplete_aside() {
673    assert_eq!(
674        document("________________"),
675        Ok((
676            "",
677            Document(vec![Section::IllFormed(vec!["________________"])])
678        ))
679    );
680}
681
682#[test]
683fn test_document() {
684    assert_eq!(
685        document(
686            r#"She opened the letter.
687
688======
689
690She cry."#
691        ),
692        Ok((
693            "",
694            Document(vec![
695                Section::Story(vec![Paragraph(vec![Component::Teller(vec![Format::Raw(
696                    vec![
697                        Atom::Word("She"),
698                        Atom::Word("opened"),
699                        Atom::Word("the"),
700                        Atom::Word("letter"),
701                        Atom::Punctuation(Mark::Point),
702                    ]
703                )])])]),
704                Section::Story(vec![Paragraph(vec![Component::Teller(vec![Format::Raw(
705                    vec![
706                        Atom::Word("She"),
707                        Atom::Word("cry"),
708                        Atom::Punctuation(Mark::Point),
709                    ]
710                )])])]),
711            ])
712        ))
713    );
714    assert_eq!(
715        document(
716            r#"She opened the letter.
717
718======She cry."#
719        ),
720        Ok((
721            "",
722            Document(vec![
723                Section::Story(vec![Paragraph(vec![Component::Teller(vec![Format::Raw(
724                    vec![
725                        Atom::Word("She"),
726                        Atom::Word("opened"),
727                        Atom::Word("the"),
728                        Atom::Word("letter"),
729                        Atom::Punctuation(Mark::Point),
730                    ]
731                )])])]),
732                Section::IllFormed(vec!["======She cry."])
733            ])
734        ))
735    );
736
737    assert_eq!(
738        document(
739            r#"She opened the letter, and read it.
740
741_____letter____
742Dear friend.
743
744I love you.
745_______________"#
746        ),
747        Ok((
748            "",
749            Document(vec![
750                Section::Story(vec![Paragraph(vec![Component::Teller(vec![Format::Raw(
751                    vec![
752                        Atom::Word("She"),
753                        Atom::Word("opened"),
754                        Atom::Word("the"),
755                        Atom::Word("letter"),
756                        Atom::Punctuation(Mark::Comma),
757                        Atom::Word("and"),
758                        Atom::Word("read"),
759                        Atom::Word("it"),
760                        Atom::Punctuation(Mark::Point)
761                    ]
762                )])])]),
763                Section::Aside(
764                    Some("letter"),
765                    vec![
766                        Paragraph(vec![Component::Teller(vec![Format::Raw(vec![
767                            Atom::Word("Dear"),
768                            Atom::Word("friend"),
769                            Atom::Punctuation(Mark::Point)
770                        ])])]),
771                        Paragraph(vec![Component::Teller(vec![Format::Raw(vec![
772                            Atom::Word("I"),
773                            Atom::Word("love"),
774                            Atom::Word("you"),
775                            Atom::Punctuation(Mark::Point)
776                        ])])])
777                    ]
778                )
779            ])
780        ))
781    );
782}
783
784#[derive(PartialEq, Eq, Debug)]
785pub enum Error<'input> {
786    IncompleteParsing(Document<'input>, &'input str),
787    ParsingError,
788}
789
790pub fn parse(input: &str) -> Result<Document, Error> {
791    match document(input) {
792        Ok(("", res)) => Ok(res),
793        Ok((rest, res)) => Err(Error::IncompleteParsing(res, rest)),
794        _ => Err(Error::ParsingError),
795    }
796}
797
798pub fn compile<'input, O: Output, T: Typography + ?Sized>(
799    input: &'input str,
800    typo: &T,
801) -> Result<O, Error<'input>> {
802    let mut out = O::empty(input.len());
803
804    render(&parse(input)?, typo, &mut out);
805
806    Ok(out)
807}
808
809#[test]
810fn test_render() {
811    use stats::Digest;
812    use typography::ENGLISH;
813
814    let res: Digest = compile(r#"Hi everyone."#, &ENGLISH).unwrap();
815
816    assert_eq!(res.words_count, 2);
817
818    let res: Digest = compile(r#"Hi everyone. +My name is.. Suly+."#, &ENGLISH).unwrap();
819
820    assert_eq!(res.signs_count, 3);
821
822    let res: Digest = compile(
823        r#"Hi everyone.
824
825 +My name is.. Suly+.
826
827____test____
828
829What is your name?
830____________"#,
831        &ENGLISH,
832    )
833    .unwrap();
834
835    assert_eq!(res.spaces_count, 7);
836
837    let res: Digest = compile(
838        r#"Hi everyone.
839
840[+My name is.. Suly+.](john)
841
842[Really?](merida)
843
844[Yay](john)
845
846____test____
847
848What is your name?
849____________"#,
850        &ENGLISH,
851    )
852    .unwrap();
853
854    assert_eq!(
855        res.characters,
856        [String::from("john"), String::from("merida")]
857            .iter()
858            .cloned()
859            .collect()
860    );
861}