mdbook_private/
lib.rs

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        // Handle preprocessor configuration
37        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        // Handle private content blocks
66        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, &notice, &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        // Handle private chapters
89        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
111/// Align section numbers with visible sections
112fn 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                    // Only renumber numbered chapters
122                    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}