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