1use 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 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 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 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}