1use std::sync::LazyLock;
2
3use log::info;
4use mdbook::book::Book;
5use mdbook::book::SectionNumber;
6use mdbook::errors::Error;
7use mdbook::preprocess::{Preprocessor, PreprocessorContext};
8use mdbook::BookItem;
9
10use regex::{Captures, Regex};
11pub struct Private;
12
13const STYLE_CONTENT: &str = "position: relative; padding: 20px 20px;";
14const STYLE_NOTICE: &str = "position: absolute; top: 0; right: 5px; font-size: 80%; opacity: 0.4;";
15
16impl Private {
17 pub fn new() -> Private {
18 Private
19 }
20}
21
22impl Default for Private {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl Preprocessor for Private {
29 fn name(&self) -> &str {
30 "private"
31 }
32
33 fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
34 info!("Running mdbook-private preprocessor");
35
36 let mut remove = false;
38 let mut style = true;
39 let mut notice = "CONFIDENTIAL";
40 let mut prefix = "_";
41 if let Some(private_cfg) = ctx.config.get_preprocessor(self.name()) {
42 if private_cfg.contains_key("remove") {
43 let cfg_remove = private_cfg.get("remove").unwrap();
44 remove = cfg_remove.as_bool().unwrap();
45 }
46 if private_cfg.contains_key("style") {
47 let cfg_style = private_cfg.get("style").unwrap();
48 style = cfg_style.as_bool().unwrap();
49
50 if private_cfg.contains_key("notice") {
51 let cfg_notice = private_cfg.get("notice").unwrap();
52 notice = cfg_notice.as_str().unwrap();
53 }
54 }
55 if private_cfg.contains_key("chapter-prefix") {
56 let cfg_prefix = private_cfg.get("chapter-prefix").unwrap();
57 prefix = cfg_prefix.as_str().unwrap();
58 }
59 }
60
61 static RE: LazyLock<Regex> = LazyLock::new(|| {
62 Regex::new(r"<!--\s*private\b\s*[\r?\n]?((?s).*?)[\r?\n]?\s*-->[\r?\n]?").unwrap()
63 });
64
65 book.for_each_mut(|item: &mut BookItem| {
67 if let BookItem::Chapter(ref mut chapter) = *item {
68 info!("Processing chapter '{}'", &chapter.name);
69 let result = if remove {
70 RE.replace_all(chapter.content.as_str(), "")
71 } else {
72 RE.replace_all(chapter.content.as_str(), |caps: &Captures| {
73 if style {
74 format!(
75 "<blockquote style='{}'><span style='{}'>{}</span>{}</blockquote>\n",
76 &STYLE_CONTENT, STYLE_NOTICE, ¬ice, &caps[1]
77 )
78 } else {
79 caps[1].to_string() + "\n"
80 }
81 })
82 };
83
84 chapter.content = result.to_string();
85 }
86 });
87
88 if remove {
90 let mut private_book = Book::new();
91 book.sections
92 .iter()
93 .filter_map(|section| process_item(section.clone(), prefix))
94 .for_each(|item| {
95 private_book.push_item(item);
96 });
97
98 update_section_numbers(&mut private_book);
99
100 return Ok(private_book);
101 }
102
103 Ok(book)
104 }
105
106 fn supports_renderer(&self, renderer: &str) -> bool {
107 renderer != "not-supported"
108 }
109}
110
111fn update_section_numbers(book: &mut Book) {
113 let mut current_number: Vec<u32> = Vec::new();
114
115 fn update_chapter_numbers(chapters: &mut [BookItem], current_number: &mut Vec<u32>) {
116 let mut section_counter = 1;
117
118 for item in chapters.iter_mut() {
119 if let BookItem::Chapter(ref mut chapter) = item {
120 if chapter.number.is_some() {
121 current_number.push(section_counter);
123 chapter.number = Some(SectionNumber(current_number.clone()));
124 update_chapter_numbers(&mut chapter.sub_items, current_number);
125 current_number.pop();
126 section_counter += 1;
127 }
128 }
129 }
130 }
131
132 update_chapter_numbers(&mut book.sections, &mut current_number);
133}
134
135fn process_item(item: BookItem, prefix: &str) -> Option<BookItem> {
136 match item {
137 BookItem::Chapter(ch) => {
138 if ch
139 .source_path
140 .as_ref()?
141 .file_name()?
142 .to_str()?
143 .starts_with(prefix)
144 {
145 info!("Deleting chapter {}", ch.source_path.as_ref()?.display());
146 return None;
147 }
148
149 let mut private_ch = ch.clone();
150 private_ch.sub_items.clear();
151
152 for sub in &ch.sub_items {
153 if let Some(processed_sub) = process_item(sub.clone(), prefix) {
154 private_ch.sub_items.push(processed_sub);
155 }
156 }
157
158 Some(BookItem::Chapter(private_ch))
159 }
160 _ => Some(item),
161 }
162}
163
164#[cfg(test)]
165mod test {
166 use super::*;
167
168 #[test]
169 fn private_remove_preprocessor_run() {
170 let input_json = r##"[
171 {
172 "root": "/path/to/book",
173 "config": {
174 "book": {
175 "authors": ["AUTHOR"],
176 "language": "en",
177 "multilingual": false,
178 "src": "src",
179 "title": "TITLE"
180 },
181 "preprocessor": {
182 "private": {
183 "remove": true
184 }
185 }
186 },
187 "renderer": "html",
188 "mdbook_version": "0.4.21"
189 },
190 {
191 "sections": [
192 {
193 "Chapter": {
194 "name": "Chapter 1",
195 "content": "# Chapter 1\n<!--private\nHello world!\n\nSome more text\n123!@#\n-->\nThe End",
196 "number": [1],
197 "sub_items": [],
198 "path": "chapter_1.md",
199 "source_path": "chapter_1.md",
200 "parent_names": []
201 }
202 }
203 ],
204 "__non_exhaustive": null
205 }
206 ]"##;
207 let output_json = r##"[
208 {
209 "root": "/path/to/book",
210 "config": {
211 "book": {
212 "authors": ["AUTHOR"],
213 "language": "en",
214 "multilingual": false,
215 "src": "src",
216 "title": "TITLE"
217 },
218 "preprocessor": {
219 "private": {
220 "remove": true
221 }
222 }
223 },
224 "renderer": "html",
225 "mdbook_version": "0.4.21"
226 },
227 {
228 "sections": [
229 {
230 "Chapter": {
231 "name": "Chapter 1",
232 "content": "# Chapter 1\nThe End",
233 "number": [1],
234 "sub_items": [],
235 "path": "chapter_1.md",
236 "source_path": "chapter_1.md",
237 "parent_names": []
238 }
239 }
240 ],
241 "__non_exhaustive": null
242 }
243 ]"##;
244 let input_json = input_json.as_bytes();
245 let output_json = output_json.as_bytes();
246
247 let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
248 let (_, expected_book) =
249 mdbook::preprocess::CmdPreprocessor::parse_input(output_json).unwrap();
250
251 let result = Private::new().run(&ctx, book);
252 assert!(result.is_ok());
253
254 let actual_book = result.unwrap();
255 assert_eq!(actual_book, expected_book);
256 }
257
258 #[test]
259 fn private_keep_preprocessor_run() {
260 let input_json = r##"[
261 {
262 "root": "/path/to/book",
263 "config": {
264 "book": {
265 "authors": ["AUTHOR"],
266 "language": "en",
267 "multilingual": false,
268 "src": "src",
269 "title": "TITLE"
270 },
271 "preprocessor": {
272 "private": {}
273 }
274 },
275 "renderer": "html",
276 "mdbook_version": "0.4.21"
277 },
278 {
279 "sections": [
280 {
281 "Chapter": {
282 "name": "Chapter 1",
283 "content": "# Chapter 1\n<!--private\nHello world!\n\nSome more text\n123!@#\n-->\nThe End",
284 "number": [1],
285 "sub_items": [],
286 "path": "chapter_1.md",
287 "source_path": "chapter_1.md",
288 "parent_names": []
289 }
290 }
291 ],
292 "__non_exhaustive": null
293 }
294 ]"##;
295 let output_json = r##"[
296 {
297 "root": "/path/to/book",
298 "config": {
299 "book": {
300 "authors": ["AUTHOR"],
301 "language": "en",
302 "multilingual": false,
303 "src": "src",
304 "title": "TITLE"
305 },
306 "preprocessor": {
307 "private": {}
308 }
309 },
310 "renderer": "html",
311 "mdbook_version": "0.4.21"
312 },
313 {
314 "sections": [
315 {
316 "Chapter": {
317 "name": "Chapter 1",
318 "content": "# Chapter 1\n<blockquote style='position: relative; padding: 20px 20px;'><span style='position: absolute; top: 0; right: 5px; font-size: 80%; opacity: 0.4;'>CONFIDENTIAL</span>Hello world!\n\nSome more text\n123!@#</blockquote>\nThe End",
319 "number": [1],
320 "sub_items": [],
321 "path": "chapter_1.md",
322 "source_path": "chapter_1.md",
323 "parent_names": []
324 }
325 }
326 ],
327 "__non_exhaustive": null
328 }
329 ]"##;
330 let input_json = input_json.as_bytes();
331 let output_json = output_json.as_bytes();
332
333 let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
334 let (_, expected_book) =
335 mdbook::preprocess::CmdPreprocessor::parse_input(output_json).unwrap();
336
337 let result = Private::new().run(&ctx, book);
338 assert!(result.is_ok());
339
340 let actual_book = result.unwrap();
341 assert_eq!(actual_book, expected_book);
342 }
343
344 #[test]
345 fn private_remove_robustly_run() {
346 let input_json = r##"[
347 {
348 "root": "/path/to/book",
349 "config": {
350 "book": {
351 "authors": ["AUTHOR"],
352 "language": "en",
353 "multilingual": false,
354 "src": "src",
355 "title": "TITLE"
356 },
357 "preprocessor": {
358 "private": {
359 "remove": true
360 }
361 }
362 },
363 "renderer": "html",
364 "mdbook_version": "0.4.21"
365 },
366 {
367 "sections": [
368 {
369 "Chapter": {
370 "name": "Chapter 1",
371 "content": "# Chapter 1\n<!--private Hello world! -->\nThe End",
372 "number": [1],
373 "sub_items": [],
374 "path": "chapter_1.md",
375 "source_path": "chapter_1.md",
376 "parent_names": []
377 }
378 }
379 ],
380 "__non_exhaustive": null
381 }
382 ]"##;
383 let output_json = r##"[
384 {
385 "root": "/path/to/book",
386 "config": {
387 "book": {
388 "authors": ["AUTHOR"],
389 "language": "en",
390 "multilingual": false,
391 "src": "src",
392 "title": "TITLE"
393 },
394 "preprocessor": {
395 "private": {
396 "remove": true
397 }
398 }
399 },
400 "renderer": "html",
401 "mdbook_version": "0.4.21"
402 },
403 {
404 "sections": [
405 {
406 "Chapter": {
407 "name": "Chapter 1",
408 "content": "# Chapter 1\nThe End",
409 "number": [1],
410 "sub_items": [],
411 "path": "chapter_1.md",
412 "source_path": "chapter_1.md",
413 "parent_names": []
414 }
415 }
416 ],
417 "__non_exhaustive": null
418 }
419 ]"##;
420
421 let input_json = input_json.as_bytes();
422 let output_json = output_json.as_bytes();
423
424 let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
425 let (_, expected_book) =
426 mdbook::preprocess::CmdPreprocessor::parse_input(output_json).unwrap();
427
428 let result = Private::new().run(&ctx, book);
429 assert!(result.is_ok());
430
431 let actual_book = result.unwrap();
432 assert_eq!(actual_book, expected_book);
433 }
434
435 #[test]
436 fn private_keep_robustly_run() {
437 let input_json = r##"[
438 {
439 "root": "/path/to/book",
440 "config": {
441 "book": {
442 "authors": ["AUTHOR"],
443 "language": "en",
444 "multilingual": false,
445 "src": "src",
446 "title": "TITLE"
447 },
448 "preprocessor": {
449 "private": {}
450 }
451 },
452 "renderer": "html",
453 "mdbook_version": "0.4.21"
454 },
455 {
456 "sections": [
457 {
458 "Chapter": {
459 "name": "Chapter 1",
460 "content": "# Chapter 1\n<!--private Hello world! -->\nThe End",
461 "number": [1],
462 "sub_items": [],
463 "path": "chapter_1.md",
464 "source_path": "chapter_1.md",
465 "parent_names": []
466 }
467 }
468 ],
469 "__non_exhaustive": null
470 }
471 ]"##;
472 let output_json = r##"[
473 {
474 "root": "/path/to/book",
475 "config": {
476 "book": {
477 "authors": ["AUTHOR"],
478 "language": "en",
479 "multilingual": false,
480 "src": "src",
481 "title": "TITLE"
482 },
483 "preprocessor": {
484 "private": {}
485 }
486 },
487 "renderer": "html",
488 "mdbook_version": "0.4.21"
489 },
490 {
491 "sections": [
492 {
493 "Chapter": {
494 "name": "Chapter 1",
495 "content": "# Chapter 1\n<blockquote style='position: relative; padding: 20px 20px;'><span style='position: absolute; top: 0; right: 5px; font-size: 80%; opacity: 0.4;'>CONFIDENTIAL</span>Hello world!</blockquote>\nThe End",
496 "number": [1],
497 "sub_items": [],
498 "path": "chapter_1.md",
499 "source_path": "chapter_1.md",
500 "parent_names": []
501 }
502 }
503 ],
504 "__non_exhaustive": null
505 }
506 ]"##;
507 let input_json = input_json.as_bytes();
508 let output_json = output_json.as_bytes();
509
510 let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
511 let (_, expected_book) =
512 mdbook::preprocess::CmdPreprocessor::parse_input(output_json).unwrap();
513
514 let result = Private::new().run(&ctx, book);
515 assert!(result.is_ok());
516
517 let actual_book = result.unwrap();
518 assert_eq!(actual_book, expected_book);
519 }
520
521 #[test]
522 fn private_keep_chapters_run() {
523 let input_json = r##"[
524 {
525 "root": "/path/to/book",
526 "config": {
527 "book": {
528 "authors": ["AUTHOR"],
529 "language": "en",
530 "multilingual": false,
531 "src": "src",
532 "title": "TITLE"
533 },
534 "preprocessor": {
535 "private": {}
536 }
537 },
538 "renderer": "html",
539 "mdbook_version": "0.4.32"
540 },
541 {
542 "sections": [
543 {
544 "Chapter": {
545 "name": "Chapter 1",
546 "content": "# Chapter 1\n\nThis chapter will always be present\n\n<!--private\nThis is some highly confidential material which we want to remove when sharing with external parties.\n\nAnother *line*.\n\n# A title that should remain a title \nYet another **line**.\n-->\n",
547 "number": [1],
548 "sub_items": [
549 {
550 "Chapter": {
551 "name": "Sub chapter",
552 "content": "# Subchapter\n\nThis chapter will be removed if private is enabled\n",
553 "number": [1, 1],
554 "sub_items": [],
555 "path": "_chapter_1_sub.md",
556 "source_path": "_chapter_1_sub.md",
557 "parent_names": ["Chapter 1"]
558 }
559 }
560 ],
561 "path": "chapter_1.md",
562 "source_path": "chapter_1.md",
563 "parent_names": []
564 }
565 },
566 {
567 "Chapter": {
568 "name": "Chapter 2",
569 "content": "# Chapter 2\n\nThis chapter and it's subchapters will be removed if private is enabled\n",
570 "number": [2],
571 "sub_items": [
572 {
573 "Chapter": {
574 "name": "Sub chapter",
575 "content": "# Subchapter\n\nThis will be removed if private is enabled because it's parent chapter is set to be removed.\n",
576 "number": [2, 1],
577 "sub_items": [],
578 "path": "chapter_2_sub.md",
579 "source_path": "chapter_2_sub.md",
580 "parent_names": ["Chapter 2"]
581 }
582 }
583 ],
584 "path": "_chapter_2.md",
585 "source_path": "_chapter_2.md",
586 "parent_names": []
587 }
588 }
589 ],
590 "__non_exhaustive": null
591 }
592 ]"##;
593 let output_json = r##"[
594 {
595 "root": "/path/to/book",
596 "config": {
597 "book": {
598 "authors": ["AUTHOR"],
599 "language": "en",
600 "multilingual": false,
601 "src": "src",
602 "title": "TITLE"
603 },
604 "preprocessor": {
605 "private": {}
606 }
607 },
608 "renderer": "html",
609 "mdbook_version": "0.4.32"
610 },
611 {
612 "sections": [
613 {
614 "Chapter": {
615 "name": "Chapter 1",
616 "content": "# Chapter 1\n\nThis chapter will always be present\n\n<blockquote style='position: relative; padding: 20px 20px;'><span style='position: absolute; top: 0; right: 5px; font-size: 80%; opacity: 0.4;'>CONFIDENTIAL</span>This is some highly confidential material which we want to remove when sharing with external parties.\n\nAnother *line*.\n\n# A title that should remain a title \nYet another **line**.</blockquote>\n",
617 "number": [1],
618 "sub_items": [
619 {
620 "Chapter": {
621 "name": "Sub chapter",
622 "content": "# Subchapter\n\nThis chapter will be removed if private is enabled\n",
623 "number": [1, 1],
624 "sub_items": [],
625 "path": "_chapter_1_sub.md",
626 "source_path": "_chapter_1_sub.md",
627 "parent_names": ["Chapter 1"]
628 }
629 }
630 ],
631 "path": "chapter_1.md",
632 "source_path": "chapter_1.md",
633 "parent_names": []
634 }
635 },
636 {
637 "Chapter": {
638 "name": "Chapter 2",
639 "content": "# Chapter 2\n\nThis chapter and it's subchapters will be removed if private is enabled\n",
640 "number": [2],
641 "sub_items": [
642 {
643 "Chapter": {
644 "name": "Sub chapter",
645 "content": "# Subchapter\n\nThis will be removed if private is enabled because it's parent chapter is set to be removed.\n",
646 "number": [2, 1],
647 "sub_items": [],
648 "path": "chapter_2_sub.md",
649 "source_path": "chapter_2_sub.md",
650 "parent_names": ["Chapter 2"]
651 }
652 }
653 ],
654 "path": "_chapter_2.md",
655 "source_path": "_chapter_2.md",
656 "parent_names": []
657 }
658 }
659 ],
660 "__non_exhaustive": null
661 }
662 ]"##;
663
664 let input_json = input_json.as_bytes();
665 let output_json = output_json.as_bytes();
666
667 let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
668 let (_, expected_book) =
669 mdbook::preprocess::CmdPreprocessor::parse_input(output_json).unwrap();
670
671 let result = Private::new().run(&ctx, book);
672 assert!(result.is_ok());
673
674 let actual_book = result.unwrap();
675 assert_eq!(actual_book, expected_book);
676 }
677
678 #[test]
679 fn private_remove_chapters_run() {
680 let input_json = r##"[
681 {
682 "root": "/path/to/book",
683 "config": {
684 "book": {
685 "authors": ["AUTHOR"],
686 "language": "en",
687 "multilingual": false,
688 "src": "src",
689 "title": "TITLE"
690 },
691 "preprocessor": {
692 "private": {
693 "remove": true
694 }
695 }
696 },
697 "renderer": "html",
698 "mdbook_version": "0.4.32"
699 },
700 {
701 "sections": [
702 {
703 "Chapter": {
704 "name": "Chapter 1",
705 "content": "# Chapter 1\n\nThis chapter will always be present\n\n<!--private\nThis is some highly confidential material which we want to remove when sharing with external parties.\n\nAnother *line*.\n\n# A title that should remain a title \nYet another **line**.\n-->\n",
706 "number": [1],
707 "sub_items": [
708 {
709 "Chapter": {
710 "name": "Sub chapter",
711 "content": "# Subchapter\n\nThis chapter will be removed if private is enabled\n",
712 "number": [1, 1],
713 "sub_items": [],
714 "path": "_chapter_1_sub.md",
715 "source_path": "_chapter_1_sub.md",
716 "parent_names": ["Chapter 1"]
717 }
718 }
719 ],
720 "path": "chapter_1.md",
721 "source_path": "chapter_1.md",
722 "parent_names": []
723 }
724 },
725 {
726 "Chapter": {
727 "name": "Chapter 2",
728 "content": "# Chapter 2\n\nThis chapter and it's subchapters will be removed if private is enabled\n",
729 "number": [2],
730 "sub_items": [
731 {
732 "Chapter": {
733 "name": "Sub chapter",
734 "content": "# Subchapter\n\nThis will be removed if private is enabled because it's parent chapter is set to be removed.\n",
735 "number": [2, 1],
736 "sub_items": [],
737 "path": "chapter_2_sub.md",
738 "source_path": "chapter_2_sub.md",
739 "parent_names": ["Chapter 2"]
740 }
741 }
742 ],
743 "path": "_chapter_2.md",
744 "source_path": "_chapter_2.md",
745 "parent_names": []
746 }
747 }
748 ],
749 "__non_exhaustive": null
750 }
751 ]"##;
752 let output_json = r##"[
753 {
754 "root": "/path/to/book",
755 "config": {
756 "book": {
757 "authors": ["AUTHOR"],
758 "language": "en",
759 "multilingual": false,
760 "src": "src",
761 "title": "TITLE"
762 },
763 "preprocessor": {
764 "private": {
765 "remove": true
766 }
767 }
768 },
769 "renderer": "html",
770 "mdbook_version": "0.4.32"
771 },
772 {
773 "sections": [
774 {
775 "Chapter": {
776 "name": "Chapter 1",
777 "content": "# Chapter 1\n\nThis chapter will always be present\n\n",
778 "number": [1],
779 "sub_items": [],
780 "path": "chapter_1.md",
781 "source_path": "chapter_1.md",
782 "parent_names": []
783 }
784 }
785 ],
786 "__non_exhaustive": null
787 }
788 ]"##;
789
790 let input_json = input_json.as_bytes();
791 let output_json = output_json.as_bytes();
792
793 let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
794 let (_, expected_book) =
795 mdbook::preprocess::CmdPreprocessor::parse_input(output_json).unwrap();
796
797 let result = Private::new().run(&ctx, book);
798 assert!(result.is_ok());
799
800 let actual_book = result.unwrap();
801 assert_eq!(actual_book, expected_book);
802 }
803
804 #[test]
805 fn private_remove_chapters_section_numbers_run() {
806 let input_json = r##"[
807 {
808 "root": "/path/to/book",
809 "config": {
810 "book": {
811 "authors": ["AUTHOR"],
812 "language": "en",
813 "multilingual": false,
814 "src": "src",
815 "title": "TITLE"
816 },
817 "preprocessor": {
818 "private": {
819 "remove": true
820 }
821 }
822 },
823 "renderer": "html",
824 "mdbook_version": "0.4.32"
825 },
826 {
827 "sections": [
828 {
829 "Chapter": {
830 "name": "Intro",
831 "content": "# Intro\n\nIntroduction prefix chapter\n\n<!--private\nSecret stuff\n-->\n",
832 "number": null,
833 "sub_items": [],
834 "path": "intro.md",
835 "source_path": "intro.md",
836 "parent_names": []
837 }
838 },
839 {
840 "Chapter": {
841 "name": "Chapter 1",
842 "content": "# Chapter 1\n\nThis chapter will always be present\n\n<!--private\nThis is some highly confidential material which we want to remove when sharing with external parties.\n\nAnother *line*.\n\n# A title that should remain a title \nYet another **line**.\n-->\n",
843 "number": [1],
844 "sub_items": [
845 {
846 "Chapter": {
847 "name": "Sub chapter",
848 "content": "# Subchapter\n\nThis chapter will be removed if private is enabled\n",
849 "number": [1, 1],
850 "sub_items": [],
851 "path": "_chapter_1_sub_1.md",
852 "source_path": "_chapter_1_sub.md",
853 "parent_names": ["Chapter 1"]
854 }
855 },
856 {
857 "Chapter": {
858 "name": "Sub chapter",
859 "content": "",
860 "number": [1, 2],
861 "sub_items": [],
862 "path": "chapter_1_sub_2.md",
863 "source_path": "chapter_1_sub_2.md",
864 "parent_names": ["Chapter 1"]
865 }
866 }
867 ],
868 "path": "chapter_1.md",
869 "source_path": "chapter_1.md",
870 "parent_names": []
871 }
872 },
873 {
874 "Chapter": {
875 "name": "Chapter 2",
876 "content": "# Chapter 2\n\nThis chapter and it's subchapters will be removed if private is enabled\n",
877 "number": [2],
878 "sub_items": [
879 {
880 "Chapter": {
881 "name": "Sub chapter",
882 "content": "# Subchapter\n\nThis will be removed if private is enabled because it's parent chapter is set to be removed.\n",
883 "number": [2, 1],
884 "sub_items": [],
885 "path": "chapter_2_sub.md",
886 "source_path": "chapter_2_sub.md",
887 "parent_names": ["Chapter 2"]
888 }
889 }
890 ],
891 "path": "_chapter_2.md",
892 "source_path": "_chapter_2.md",
893 "parent_names": []
894 }
895 },
896 {
897 "Chapter": {
898 "name": "Chapter 3",
899 "content": "# Chapter 1\n\nThis chapter will always be present\n\n\n",
900 "number": [3],
901 "sub_items": [],
902 "path": "chapter_3.md",
903 "source_path": "chapter_3.md",
904 "parent_names": []
905 }
906 }
907 ],
908 "__non_exhaustive": null
909 }
910 ]"##;
911 let output_json = r##"[
912 {
913 "root": "/path/to/book",
914 "config": {
915 "book": {
916 "authors": ["AUTHOR"],
917 "language": "en",
918 "multilingual": false,
919 "src": "src",
920 "title": "TITLE"
921 },
922 "preprocessor": {
923 "private": {
924 "remove": true
925 }
926 }
927 },
928 "renderer": "html",
929 "mdbook_version": "0.4.32"
930 },
931 {
932 "sections": [
933 {
934 "Chapter": {
935 "name": "Intro",
936 "content": "# Intro\n\nIntroduction prefix chapter\n\n",
937 "number": null,
938 "sub_items": [],
939 "path": "intro.md",
940 "source_path": "intro.md",
941 "parent_names": []
942 }
943 },
944 {
945 "Chapter": {
946 "name": "Chapter 1",
947 "content": "# Chapter 1\n\nThis chapter will always be present\n\n",
948 "number": [1],
949 "sub_items": [
950 {
951 "Chapter": {
952 "name": "Sub chapter",
953 "content": "",
954 "number": [1, 1],
955 "sub_items": [],
956 "path": "chapter_1_sub_2.md",
957 "source_path": "chapter_1_sub_2.md",
958 "parent_names": ["Chapter 1"]
959 }
960 }
961 ],
962 "path": "chapter_1.md",
963 "source_path": "chapter_1.md",
964 "parent_names": []
965 }
966 },
967 {
968 "Chapter": {
969 "name": "Chapter 3",
970 "content": "# Chapter 1\n\nThis chapter will always be present\n\n\n",
971 "number": [2],
972 "sub_items": [],
973 "path": "chapter_3.md",
974 "source_path": "chapter_3.md",
975 "parent_names": []
976 }
977 }
978 ],
979 "__non_exhaustive": null
980 }
981 ]"##;
982
983 let input_json = input_json.as_bytes();
984 let output_json = output_json.as_bytes();
985
986 let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
987 let (_, expected_book) =
988 mdbook::preprocess::CmdPreprocessor::parse_input(output_json).unwrap();
989
990 let result = Private::new().run(&ctx, book);
991 assert!(result.is_ok());
992
993 let actual_book = result.unwrap();
994 assert_eq!(actual_book, expected_book);
995 }
996}