cargo_rdme/
inject_doc.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 */
5
6use crate::utils::{ItemOrOther, MarkdownItemIterator, Span};
7use crate::{Doc, Readme};
8use thiserror::Error;
9
10pub const MARKER_RDME: &str = "<!-- cargo-rdme -->";
11const MARKER_RDME_START: &str = "<!-- cargo-rdme start -->";
12const MARKER_RDME_END: &str = "<!-- cargo-rdme end -->";
13
14#[derive(PartialEq, Eq, Clone, Debug)]
15struct Heading<'a> {
16    level: u8,
17    text: &'a str,
18}
19
20#[derive(PartialEq, Eq, Clone, Debug)]
21enum ReadmeLine<'a> {
22    Heading(Heading<'a>, Span),
23    MarkerCargoRdme(Span),
24    MarkerCargoRdmeStart(Span),
25    MarkerCargoRdmeEnd(Span),
26}
27
28fn readme_line_iterator(readme: &Readme) -> MarkdownItemIterator<ReadmeLine> {
29    use pulldown_cmark::{Event, Options, Parser, Tag};
30
31    let source = readme.as_string();
32    let parser = Parser::new_ext(source, Options::all());
33
34    let is_line_start =
35        |start| start == 0 || source[0..start].chars().rev().find(|&c| c != ' ') == Some('\n');
36    let mut depth = 0;
37
38    let iter = parser.into_offset_iter().filter_map(move |(event, range)| match event {
39        Event::Start(Tag::Heading { level, .. }) => Some((
40            range.clone().into(),
41            ReadmeLine::Heading(
42                Heading { level: level as u8, text: &source[range.start..range.end] },
43                range.into(),
44            ),
45        )),
46        Event::Html(ref html) if is_line_start(range.start) => {
47            let trimmed_line = html.strip_suffix('\r').unwrap_or_else(|| html.as_ref()).trim();
48
49            match trimmed_line {
50                MARKER_RDME if depth == 0 => {
51                    Some((range.clone().into(), ReadmeLine::MarkerCargoRdme(range.into())))
52                }
53                MARKER_RDME_START if depth == 0 => {
54                    depth += 1;
55                    Some((range.clone().into(), ReadmeLine::MarkerCargoRdmeStart(range.into())))
56                }
57                MARKER_RDME_END if depth <= 1 => {
58                    depth -= 1;
59                    Some((range.clone().into(), ReadmeLine::MarkerCargoRdmeEnd(range.into())))
60                }
61                MARKER_RDME_START => {
62                    depth += 1;
63                    None
64                }
65                MARKER_RDME_END => {
66                    depth -= 1;
67                    None
68                }
69                _ => None,
70            }
71        }
72        _ => None,
73    });
74
75    MarkdownItemIterator::new(source, iter)
76}
77
78fn doc_heading_iterator(doc: &Doc) -> MarkdownItemIterator<Heading> {
79    use pulldown_cmark::{Event, Options, Parser, Tag};
80
81    let source = doc.as_string();
82    let parser = Parser::new_ext(source, Options::all());
83
84    let iter = parser.into_offset_iter().filter_map(move |(event, range)| match event {
85        Event::Start(Tag::Heading { level, .. }) => Some((
86            range.clone().into(),
87            Heading { level: level as u8, text: &source[range.start..range.end] },
88        )),
89        _ => None,
90    });
91
92    MarkdownItemIterator::new(source, iter)
93}
94
95#[derive(Error, Eq, PartialEq, Debug)]
96pub enum InjectDocError {
97    #[error("unexpected end marker at line {line_number}")]
98    UnexpectedMarkerCargoRdmeEnd { line_number: usize },
99    #[error("unmatched start marker")]
100    UnmatchedMarkerCargoRdmeStart,
101}
102
103fn bump_heading_level(doc: &Doc, level_bump: u8) -> Doc {
104    let mut new_doc = String::with_capacity(doc.as_string().len() + 256);
105
106    for item in doc_heading_iterator(doc).complete() {
107        match item {
108            ItemOrOther::Item(Heading { text, .. }) => {
109                (0..level_bump).for_each(|_| new_doc.push('#'));
110                new_doc.push_str(text);
111            }
112            ItemOrOther::Other(other) => {
113                new_doc.push_str(other);
114            }
115        }
116    }
117
118    Doc::from_str(new_doc)
119}
120
121pub struct NewReadme {
122    pub readme: Readme,
123    /// Weather the README had a cargo-rdme marker or not.
124    pub had_marker: bool,
125}
126
127pub fn inject_doc_in_readme(
128    readme: &Readme,
129    doc: &Doc,
130    heading_base_level: Option<u8>,
131) -> Result<NewReadme, InjectDocError> {
132    fn inject(new_readme: &mut String, doc: &Doc) {
133        new_readme.push_str(MARKER_RDME_START);
134        new_readme.push_str("\n\n");
135        doc.lines().for_each(|line| {
136            new_readme.push_str(line);
137            new_readme.push('\n');
138        });
139        new_readme.push('\n');
140        new_readme.push_str(MARKER_RDME_END);
141        new_readme.push('\n');
142    }
143
144    let mut new_readme: String =
145        String::with_capacity(readme.as_string().len() + doc.as_string().len() + 1024);
146    let mut inside_markers = false;
147    let mut last_heading_level: u8 = 0;
148    let mut had_marker = false;
149
150    for item in readme_line_iterator(readme).complete() {
151        match (inside_markers, item) {
152            (true, ItemOrOther::Item(ReadmeLine::MarkerCargoRdmeEnd(_))) => {
153                inside_markers = false;
154            }
155            (true, _) => (),
156
157            (false, ItemOrOther::Item(ReadmeLine::MarkerCargoRdmeEnd(span))) => {
158                let line_number =
159                    1 + readme.as_string()[0..span.start].chars().filter(|&c| c == '\n').count();
160
161                return Err(InjectDocError::UnexpectedMarkerCargoRdmeEnd { line_number });
162            }
163            (false, ItemOrOther::Item(ReadmeLine::Heading(Heading { level, text }, _))) => {
164                new_readme.push_str(text);
165                last_heading_level = level;
166            }
167            (false, ItemOrOther::Other(other)) => new_readme.push_str(other),
168            (false, ItemOrOther::Item(ReadmeLine::MarkerCargoRdme(_))) => {
169                let level_bump = heading_base_level.unwrap_or(last_heading_level);
170                let doc = bump_heading_level(doc, level_bump);
171                inject(&mut new_readme, &doc);
172                had_marker = true;
173            }
174            (false, ItemOrOther::Item(ReadmeLine::MarkerCargoRdmeStart(_))) => {
175                let level_bump = heading_base_level.unwrap_or(last_heading_level);
176                let doc = bump_heading_level(doc, level_bump);
177                inject(&mut new_readme, &doc);
178                inside_markers = true;
179                had_marker = true;
180            }
181        }
182    }
183
184    match inside_markers {
185        true => Err(InjectDocError::UnmatchedMarkerCargoRdmeStart),
186        false => {
187            let new_readme = NewReadme { readme: Readme::from_str(new_readme), had_marker };
188
189            Ok(new_readme)
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use indoc::indoc;
198    use pretty_assertions::assert_eq;
199
200    #[test]
201    fn test_readme_line_iterator() {
202        let str = indoc! { "
203            marker test <!-- cargo-rdme -->.
204             Starting with whitespace.
205
206            <!-- cargo-rdme start -->
207            <!-- cargo-rdme end -->
208            <!-- cargo-rdme -->
209            <!-- cargo-rdme end --> <- Does not count.
210             <!-- cargo-rdme start -->
211            <!-- cargo-rdme end -->\r
212            <!-- cargo-rdme start -->
213            <!-- cargo-rdme end --> \r
214            <!-- cargo-rdme start -->
215            <!-- cargo-rdme end --> "
216        };
217
218        let readme = Readme::from_str(str);
219        let mut iter = readme_line_iterator(&readme).items();
220
221        // TODO Replace by `assert_matches!()` once https://github.com/rust-lang/rust/issues/82775
222        // stabilizes.
223        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
224        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
225        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdme(_))));
226        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
227        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
228        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
229        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
230        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
231        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
232        assert_eq!(iter.next(), None);
233    }
234
235    #[test]
236    fn test_readme_line_iterator_nested() {
237        let str = indoc! { "
238            A
239            <!-- cargo-rdme start -->
240            B
241            <!-- cargo-rdme -->
242            C
243            <!-- cargo-rdme start -->
244            D
245            <!-- cargo-rdme end -->
246            E
247            <!-- cargo-rdme end -->
248            F"
249        };
250
251        let readme = Readme::from_str(str);
252        let mut iter = readme_line_iterator(&readme).items();
253
254        // TODO Replace by `assert_matches!()` once https://github.com/rust-lang/rust/issues/82775
255        // stabilizes.
256        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
257        assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
258        assert_eq!(iter.next(), None);
259    }
260
261    #[test]
262    fn test_inject_doc_single_marker() {
263        let readme_str = indoc! { r#"
264            This is a really nice crate.
265
266            <!-- cargo-rdme -->
267
268            Hope you enjoy!
269            "#
270        };
271        let doc_str = indoc! { r#"
272            # The crate
273
274            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
275            incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
276            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
277            "#
278        };
279
280        let expected = indoc! { r#"
281            This is a really nice crate.
282
283            <!-- cargo-rdme start -->
284
285            # The crate
286
287            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
288            incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
289            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
290
291            <!-- cargo-rdme end -->
292
293            Hope you enjoy!
294            "#
295        };
296
297        let readme = Readme::from_str(readme_str);
298        let doc = Doc::from_str(doc_str);
299
300        let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
301
302        assert_eq!(new_readme.readme.markdown.as_string(), expected);
303        assert!(new_readme.had_marker);
304    }
305
306    #[test]
307    fn test_inject_doc_start_end_marker() {
308        let readme_str = indoc! { r#"
309            This is a really nice crate.
310
311            <!-- cargo-rdme start -->
312
313            Li Europan lingues es membres del sam familie. Lor separat existentie es un myth.
314            Por scientie, musica, sport etc, litot Europa usa li sam vocabular.
315
316            <!-- cargo-rdme end -->
317
318            Hope you enjoy!
319            "#
320        };
321        let doc_str = indoc! { r#"
322            # The crate
323
324            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
325            incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
326            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
327            "#
328        };
329
330        let expected = indoc! { r#"
331            This is a really nice crate.
332
333            <!-- cargo-rdme start -->
334
335            # The crate
336
337            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
338            incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
339            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
340
341            <!-- cargo-rdme end -->
342
343            Hope you enjoy!
344            "#
345        };
346
347        let readme = Readme::from_str(readme_str);
348        let doc = Doc::from_str(doc_str);
349
350        let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
351
352        assert_eq!(new_readme.readme.markdown.as_string(), expected);
353        assert!(new_readme.had_marker);
354    }
355
356    #[test]
357    fn test_inject_doc_unmatched_start_marker() {
358        let readme_str = indoc! { r#"
359            This is a really nice crate.
360
361            <!-- cargo-rdme -->
362
363            <!-- cargo-rdme start -->
364
365            Hope you enjoy!
366            "#
367        };
368        let doc_str = indoc! { r#"
369            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
370            incididunt ut labore et dolore magna aliqua.
371            "#
372        };
373
374        let readme = Readme::from_str(readme_str);
375        let doc = Doc::from_str(doc_str);
376
377        let result = inject_doc_in_readme(&readme, &doc, None);
378
379        assert_eq!(result.err(), Some(InjectDocError::UnmatchedMarkerCargoRdmeStart));
380    }
381
382    #[test]
383    fn test_inject_doc_unexpected_end_marker() {
384        let readme_str = indoc! { r#"
385            This is a really nice crate.
386
387            <!-- cargo-rdme -->
388
389            <!-- cargo-rdme end -->
390
391            Hope you enjoy!
392            "#
393        };
394        let doc_str = indoc! { r#"
395            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
396            incididunt ut labore et dolore magna aliqua.
397            "#
398        };
399
400        let readme = Readme::from_str(readme_str);
401        let doc = Doc::from_str(doc_str);
402
403        let result = inject_doc_in_readme(&readme, &doc, None);
404
405        assert_eq!(
406            result.err(),
407            Some(InjectDocError::UnexpectedMarkerCargoRdmeEnd { line_number: 5 })
408        );
409    }
410
411    #[test]
412    fn test_bump_heading_level() {
413        let doc_str = indoc! { r#"
414            # Foo
415            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
416            incididunt ut labore et dolore magna aliqua.
417
418            ## Bar
419            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
420            "#
421        };
422        let doc = Doc::from_str(doc_str);
423
424        let new_readme = bump_heading_level(&doc, 0);
425
426        assert_eq!(new_readme.markdown.as_string(), doc_str);
427
428        let expected = indoc! { r#"
429            ### Foo
430            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
431            incididunt ut labore et dolore magna aliqua.
432
433            #### Bar
434            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
435            "#
436        };
437
438        let new_readme = bump_heading_level(&doc, 2);
439
440        assert_eq!(new_readme.markdown.as_string(), expected);
441    }
442
443    #[test]
444    fn test_inject_doc_bump_heading_level() {
445        let readme_str = indoc! { r#"
446            # The crate
447
448            This is a really nice crate.
449
450            <!-- cargo-rdme -->
451
452            Hope you enjoy!
453            "#
454        };
455        let doc_str = indoc! { r#"
456            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
457            incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
458            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
459
460            # Foo
461
462            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
463            "#
464        };
465
466        let expected = indoc! { r#"
467            # The crate
468
469            This is a really nice crate.
470
471            <!-- cargo-rdme start -->
472
473            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
474            incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
475            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
476
477            ## Foo
478
479            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
480
481            <!-- cargo-rdme end -->
482
483            Hope you enjoy!
484            "#
485        };
486
487        let readme = Readme::from_str(readme_str);
488        let doc = Doc::from_str(doc_str);
489
490        let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
491
492        assert_eq!(new_readme.readme.markdown.as_string(), expected);
493        assert!(new_readme.had_marker);
494    }
495
496    #[test]
497    fn test_inject_doc_bump_heading_level_ignore_within_markers() {
498        let readme_str = indoc! { r#"
499            # The crate
500
501            This is a really nice crate.
502
503            <!-- cargo-rdme start -->
504
505            ### The crate
506
507            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
508            incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
509            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
510
511            <!-- cargo-rdme end -->
512
513            Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
514
515            <!-- cargo-rdme -->
516
517            Hope you enjoy!
518            "#
519        };
520        let doc_str = indoc! { r#"
521            # Foo
522
523            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
524            "#
525        };
526
527        let expected = indoc! { r#"
528            # The crate
529
530            This is a really nice crate.
531
532            <!-- cargo-rdme start -->
533
534            ## Foo
535
536            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
537
538            <!-- cargo-rdme end -->
539
540            Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
541
542            <!-- cargo-rdme start -->
543
544            ## Foo
545
546            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
547
548            <!-- cargo-rdme end -->
549
550            Hope you enjoy!
551            "#
552        };
553
554        let readme = Readme::from_str(readme_str);
555        let doc = Doc::from_str(doc_str);
556
557        let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
558
559        assert_eq!(new_readme.readme.markdown.as_string(), expected);
560        assert!(new_readme.had_marker);
561    }
562
563    #[test]
564    fn test_inject_doc_bump_heading_level_ignore_code_blocks() {
565        let readme_str = indoc! { r#"
566            # The crate
567
568            This is a really nice crate.
569            You should try it!
570
571            ```
572            ### This is code
573            ```
574
575            <!-- cargo-rdme -->
576            "#
577        };
578        let doc_str = indoc! { r#"
579            # Foo
580
581            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
582            "#
583        };
584
585        let expected = indoc! { r#"
586            # The crate
587
588            This is a really nice crate.
589            You should try it!
590
591            ```
592            ### This is code
593            ```
594
595            <!-- cargo-rdme start -->
596
597            ## Foo
598
599            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
600
601            <!-- cargo-rdme end -->
602            "#
603        };
604
605        let readme = Readme::from_str(readme_str);
606        let doc = Doc::from_str(doc_str);
607
608        let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
609
610        assert_eq!(new_readme.readme.markdown.as_string(), expected);
611        assert!(new_readme.had_marker);
612    }
613
614    #[test]
615    fn test_inject_doc_with_zero_heading_base_level() {
616        let readme_str = indoc! { r#"
617            # The crate
618
619            This is a really nice crate.
620
621            <!-- cargo-rdme -->
622
623            Hope you enjoy!
624            "#
625        };
626        let doc_str = indoc! { r#"
627            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
628            incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
629            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
630
631            # Foo
632
633            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
634            "#
635        };
636
637        let expected = indoc! { r#"
638            # The crate
639
640            This is a really nice crate.
641
642            <!-- cargo-rdme start -->
643
644            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
645            incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
646            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
647
648            # Foo
649
650            Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
651
652            <!-- cargo-rdme end -->
653
654            Hope you enjoy!
655            "#
656        };
657
658        let readme = Readme::from_str(readme_str);
659        let doc = Doc::from_str(doc_str);
660
661        let new_readme = inject_doc_in_readme(&readme, &doc, Some(0)).unwrap();
662
663        assert_eq!(new_readme.readme.markdown.as_string(), expected);
664        assert!(new_readme.had_marker);
665    }
666}