Skip to main content

cargo_rdme/
inject_doc.rs

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