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 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
195 #[test]
196 fn test_readme_line_iterator() {
197 let str = indoc! { "
198 marker test <!-- cargo-rdme -->.
199 Starting with whitespace.
200
201 <!-- cargo-rdme start -->
202 <!-- cargo-rdme end -->
203 <!-- cargo-rdme -->
204 <!-- cargo-rdme end --> <- Does not count.
205 <!-- cargo-rdme start -->
206 <!-- cargo-rdme end -->\r
207 <!-- cargo-rdme start -->
208 <!-- cargo-rdme end --> \r
209 <!-- cargo-rdme start -->
210 <!-- cargo-rdme end --> "
211 };
212
213 let readme = Readme::from_str(str);
214 let mut iter = readme_line_iterator(&readme).items();
215
216 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
219 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
220 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdme(_))));
221 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
222 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
223 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
224 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
225 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
226 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
227 assert_eq!(iter.next(), None);
228 }
229
230 #[test]
231 fn test_readme_line_iterator_nested() {
232 let str = indoc! { "
233 A
234 <!-- cargo-rdme start -->
235 B
236 <!-- cargo-rdme -->
237 C
238 <!-- cargo-rdme start -->
239 D
240 <!-- cargo-rdme end -->
241 E
242 <!-- cargo-rdme end -->
243 F"
244 };
245
246 let readme = Readme::from_str(str);
247 let mut iter = readme_line_iterator(&readme).items();
248
249 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeStart(_))));
252 assert!(matches!(iter.next(), Some(ReadmeLine::MarkerCargoRdmeEnd(_))));
253 assert_eq!(iter.next(), None);
254 }
255
256 #[test]
257 fn test_inject_doc_single_marker() {
258 let readme_str = indoc! { r#"
259 This is a really nice crate.
260
261 <!-- cargo-rdme -->
262
263 Hope you enjoy!
264 "#
265 };
266 let doc_str = indoc! { r#"
267 # The crate
268
269 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
270 incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
271 exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
272 "#
273 };
274
275 let expected = indoc! { r#"
276 This is a really nice crate.
277
278 <!-- cargo-rdme start -->
279
280 # The crate
281
282 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
283 incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
284 exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
285
286 <!-- cargo-rdme end -->
287
288 Hope you enjoy!
289 "#
290 };
291
292 let readme = Readme::from_str(readme_str);
293 let doc = Doc::from_str(doc_str);
294
295 let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
296
297 assert_eq!(new_readme.readme.markdown.as_string(), expected);
298 assert!(new_readme.had_marker);
299 }
300
301 #[test]
302 fn test_inject_doc_start_end_marker() {
303 let readme_str = indoc! { r#"
304 This is a really nice crate.
305
306 <!-- cargo-rdme start -->
307
308 Li Europan lingues es membres del sam familie. Lor separat existentie es un myth.
309 Por scientie, musica, sport etc, litot Europa usa li sam vocabular.
310
311 <!-- cargo-rdme end -->
312
313 Hope you enjoy!
314 "#
315 };
316 let doc_str = indoc! { r#"
317 # The crate
318
319 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
320 incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
321 exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
322 "#
323 };
324
325 let expected = indoc! { r#"
326 This is a really nice crate.
327
328 <!-- cargo-rdme start -->
329
330 # The crate
331
332 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
333 incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
334 exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
335
336 <!-- cargo-rdme end -->
337
338 Hope you enjoy!
339 "#
340 };
341
342 let readme = Readme::from_str(readme_str);
343 let doc = Doc::from_str(doc_str);
344
345 let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
346
347 assert_eq!(new_readme.readme.markdown.as_string(), expected);
348 assert!(new_readme.had_marker);
349 }
350
351 #[test]
352 fn test_inject_doc_unmatched_start_marker() {
353 let readme_str = indoc! { r#"
354 This is a really nice crate.
355
356 <!-- cargo-rdme -->
357
358 <!-- cargo-rdme start -->
359
360 Hope you enjoy!
361 "#
362 };
363 let doc_str = indoc! { r#"
364 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
365 incididunt ut labore et dolore magna aliqua.
366 "#
367 };
368
369 let readme = Readme::from_str(readme_str);
370 let doc = Doc::from_str(doc_str);
371
372 let result = inject_doc_in_readme(&readme, &doc, None);
373
374 assert_eq!(result.err(), Some(InjectDocError::UnmatchedMarkerCargoRdmeStart));
375 }
376
377 #[test]
378 fn test_inject_doc_unexpected_end_marker() {
379 let readme_str = indoc! { r#"
380 This is a really nice crate.
381
382 <!-- cargo-rdme -->
383
384 <!-- cargo-rdme end -->
385
386 Hope you enjoy!
387 "#
388 };
389 let doc_str = indoc! { r#"
390 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
391 incididunt ut labore et dolore magna aliqua.
392 "#
393 };
394
395 let readme = Readme::from_str(readme_str);
396 let doc = Doc::from_str(doc_str);
397
398 let result = inject_doc_in_readme(&readme, &doc, None);
399
400 assert_eq!(
401 result.err(),
402 Some(InjectDocError::UnexpectedMarkerCargoRdmeEnd { line_number: 5 })
403 );
404 }
405
406 #[test]
407 fn test_bump_heading_level() {
408 let doc_str = indoc! { r#"
409 # Foo
410 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
411 incididunt ut labore et dolore magna aliqua.
412
413 ## Bar
414 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
415 "#
416 };
417 let doc = Doc::from_str(doc_str);
418
419 let new_readme = bump_heading_level(&doc, 0);
420
421 assert_eq!(new_readme.markdown.as_string(), doc_str);
422
423 let expected = indoc! { r#"
424 ### Foo
425 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
426 incididunt ut labore et dolore magna aliqua.
427
428 #### Bar
429 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
430 "#
431 };
432
433 let new_readme = bump_heading_level(&doc, 2);
434
435 assert_eq!(new_readme.markdown.as_string(), expected);
436 }
437
438 #[test]
439 fn test_inject_doc_bump_heading_level() {
440 let readme_str = indoc! { r#"
441 # The crate
442
443 This is a really nice crate.
444
445 <!-- cargo-rdme -->
446
447 Hope you enjoy!
448 "#
449 };
450 let doc_str = indoc! { r#"
451 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
452 incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
453 exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
454
455 # Foo
456
457 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
458 "#
459 };
460
461 let expected = indoc! { r#"
462 # The crate
463
464 This is a really nice crate.
465
466 <!-- cargo-rdme start -->
467
468 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
469 incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
470 exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
471
472 ## Foo
473
474 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
475
476 <!-- cargo-rdme end -->
477
478 Hope you enjoy!
479 "#
480 };
481
482 let readme = Readme::from_str(readme_str);
483 let doc = Doc::from_str(doc_str);
484
485 let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
486
487 assert_eq!(new_readme.readme.markdown.as_string(), expected);
488 assert!(new_readme.had_marker);
489 }
490
491 #[test]
492 fn test_inject_doc_bump_heading_level_ignore_within_markers() {
493 let readme_str = indoc! { r#"
494 # The crate
495
496 This is a really nice crate.
497
498 <!-- cargo-rdme start -->
499
500 ### The crate
501
502 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
503 incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
504 exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
505
506 <!-- cargo-rdme end -->
507
508 Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
509
510 <!-- cargo-rdme -->
511
512 Hope you enjoy!
513 "#
514 };
515 let doc_str = indoc! { r#"
516 # Foo
517
518 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
519 "#
520 };
521
522 let expected = indoc! { r#"
523 # The crate
524
525 This is a really nice crate.
526
527 <!-- cargo-rdme start -->
528
529 ## Foo
530
531 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
532
533 <!-- cargo-rdme end -->
534
535 Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
536
537 <!-- cargo-rdme start -->
538
539 ## Foo
540
541 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
542
543 <!-- cargo-rdme end -->
544
545 Hope you enjoy!
546 "#
547 };
548
549 let readme = Readme::from_str(readme_str);
550 let doc = Doc::from_str(doc_str);
551
552 let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
553
554 assert_eq!(new_readme.readme.markdown.as_string(), expected);
555 assert!(new_readme.had_marker);
556 }
557
558 #[test]
559 fn test_inject_doc_bump_heading_level_ignore_code_blocks() {
560 let readme_str = indoc! { r#"
561 # The crate
562
563 This is a really nice crate.
564 You should try it!
565
566 ```
567 ### This is code
568 ```
569
570 <!-- cargo-rdme -->
571 "#
572 };
573 let doc_str = indoc! { r#"
574 # Foo
575
576 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
577 "#
578 };
579
580 let expected = indoc! { r#"
581 # The crate
582
583 This is a really nice crate.
584 You should try it!
585
586 ```
587 ### This is code
588 ```
589
590 <!-- cargo-rdme start -->
591
592 ## Foo
593
594 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
595
596 <!-- cargo-rdme end -->
597 "#
598 };
599
600 let readme = Readme::from_str(readme_str);
601 let doc = Doc::from_str(doc_str);
602
603 let new_readme = inject_doc_in_readme(&readme, &doc, None).unwrap();
604
605 assert_eq!(new_readme.readme.markdown.as_string(), expected);
606 assert!(new_readme.had_marker);
607 }
608
609 #[test]
610 fn test_inject_doc_with_zero_heading_base_level() {
611 let readme_str = indoc! { r#"
612 # The crate
613
614 This is a really nice crate.
615
616 <!-- cargo-rdme -->
617
618 Hope you enjoy!
619 "#
620 };
621 let doc_str = indoc! { r#"
622 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
623 incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
624 exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
625
626 # Foo
627
628 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
629 "#
630 };
631
632 let expected = indoc! { r#"
633 # The crate
634
635 This is a really nice crate.
636
637 <!-- cargo-rdme start -->
638
639 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
640 incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
641 exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
642
643 # Foo
644
645 Aenean dictum in nisi eu rutrum. Suspendisse vulputate tristique turpis eu vestibulum.
646
647 <!-- cargo-rdme end -->
648
649 Hope you enjoy!
650 "#
651 };
652
653 let readme = Readme::from_str(readme_str);
654 let doc = Doc::from_str(doc_str);
655
656 let new_readme = inject_doc_in_readme(&readme, &doc, Some(0)).unwrap();
657
658 assert_eq!(new_readme.readme.markdown.as_string(), expected);
659 assert!(new_readme.had_marker);
660 }
661}