mdbook_angular/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(clippy::cargo, clippy::pedantic)]
3#![warn(
4	clippy::exit,
5	clippy::expect_used,
6	clippy::panic,
7	clippy::semicolon_inside_block,
8	clippy::str_to_string,
9	clippy::string_to_string,
10	clippy::unnecessary_self_imports,
11	clippy::use_debug
12)]
13#![deny(clippy::print_stderr, clippy::print_stdout)]
14#![allow(clippy::must_use_candidate)]
15
16mod angular;
17pub(crate) mod codeblock;
18pub(crate) mod config;
19mod js;
20mod markdown;
21mod utils;
22
23/// The version of mdbook-angular
24pub const MDBOOK_ANGULAR_VERSION: &str = env!("CARGO_PKG_VERSION");
25
26/// The expected version of mdbook
27///
28/// This crate can be used with any mdbook version that are semver compatible
29/// with this expected version.
30pub const EXPECTED_MDBOOK_VERSION: &str = mdbook::MDBOOK_VERSION;
31
32use std::{env, fs};
33
34pub use angular::stop_background_process;
35pub use config::{Builder, Config};
36
37use angular::build;
38use log::debug;
39use log::warn;
40use markdown::process_markdown;
41use markdown::ChapterWithCodeBlocks;
42use mdbook::{
43	renderer::{HtmlHandlebars, RenderContext},
44	BookItem, Renderer,
45};
46
47fn validate_version(ctx: &RenderContext) -> Result<()> {
48	let req = semver::VersionReq::parse(EXPECTED_MDBOOK_VERSION).unwrap();
49
50	if semver::Version::parse(&ctx.version).map_or(false, |version| req.matches(&version)) {
51		Ok(())
52	} else {
53		bail!("Invalid mdbook version {}, expected {}", &ctx.version, req);
54	}
55}
56
57pub(crate) use anyhow::{bail, Context, Error, Result};
58
59/// An mdbook [`Renderer`] for including live angular code samples
60pub struct AngularRenderer {}
61
62impl Renderer for AngularRenderer {
63	fn name(&self) -> &str {
64		"angular"
65	}
66
67	/// Prefer [`Self::render_mut`]
68	///
69	/// The [`AngularRenderer`] has to modify the book passed in with the context,
70	/// so this function has to clone the given context in order to mutate it.
71	/// Using [`Self::render_mut`] can prevent a needless copy.
72	#[inline]
73	fn render(&self, ctx: &RenderContext) -> Result<()> {
74		self.render_mut(&mut ctx.clone())
75	}
76}
77
78impl AngularRenderer {
79	pub fn new() -> Self {
80		Self {}
81	}
82
83	/// Renders the given [`RenderContext`]
84	///
85	/// This function can make changes to the context, notably to edit the markdown
86	/// files to insert angular code blocks, live angular applications, and
87	/// playground tables.
88	#[allow(clippy::missing_errors_doc)]
89	pub fn render_mut(&self, ctx: &mut RenderContext) -> Result<()> {
90		validate_version(ctx)?;
91
92		let config = Config::new(ctx)?;
93		let mut chapters_with_codeblocks = Vec::new();
94		let mut result: Result<()> = Ok(());
95
96		ctx.book.for_each_mut(|item| {
97			if result.is_err() {
98				return;
99			}
100
101			if let BookItem::Chapter(chapter) = item {
102				debug!("Processing chapter {}", &chapter.name);
103				match process_markdown(&config, chapter) {
104					Ok(processed) => {
105						debug!("Processed chapter {}", &chapter.name);
106						if let Some(processed) = processed {
107							chapters_with_codeblocks.push(processed);
108						}
109					}
110					Err(error) => result = Err(error),
111				};
112			}
113		});
114
115		debug!("Processed chapters");
116
117		if let Some(html) = &config.html {
118			ctx.config.set("output.html", html)?;
119		}
120
121		HtmlHandlebars::new().render(ctx)?;
122
123		fs::write(
124			config.target_folder.join("playground-io.min.js"),
125			crate::js::PLAYGROUND_SCRIPT,
126		)?;
127
128		debug!("Finished rendering");
129
130		#[allow(unused_mut)]
131		let mut run_build = !chapters_with_codeblocks.is_empty();
132
133		#[cfg(debug_assertions)]
134		if env::var("MDBOOK_ANGULAR_SKIP_BUILD").is_ok() {
135			run_build = false;
136		}
137
138		if run_build {
139			build(ctx, &config, chapters_with_codeblocks)?;
140		}
141
142		debug!("Finished");
143
144		Ok(())
145	}
146}
147
148impl Default for AngularRenderer {
149	fn default() -> Self {
150		Self::new()
151	}
152}