Skip to main content

mdbook_angular/
config.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{anyhow, Context};
4use mdbook_renderer::RenderContext;
5use serde::Deserialize;
6use toml::value::Table;
7
8use crate::Result;
9
10#[derive(Deserialize, PartialEq, Eq, Debug, Default, Clone, Copy)]
11#[serde(rename_all = "lowercase")]
12pub enum Builder {
13	/// Build all chapters in a single angular build.
14	///
15	/// This is fast, but uses internal Angular APIs to use the currently
16	/// experimental "application" builder Angular provides as of 16.2.0.
17	#[default]
18	Foreground,
19	Experimental,
20	/// Build via [`Builder::Experimental`] in a background process.
21	///
22	/// This allows the angular process to keep running after the renderer exits.
23	/// This builder option enables watching, which significantly speeds up
24	/// rebuilds.
25	///
26	/// This option is not supported on Windows, where this option is considered
27	/// an alias to [`Builder::Experimental`].
28	Background,
29	/// Build every chapter as a separate angular application.
30	///
31	/// This uses stable Angular APIs and should work for Angular 14.0.0 and up.
32	Slow,
33}
34
35#[derive(Deserialize, Default)]
36#[serde(rename_all = "kebab-case", deny_unknown_fields)]
37struct DeConfig {
38	#[allow(unused)] // the command option is defined by mdbook
39	command: Option<String>,
40
41	#[serde(default)]
42	builder: Builder,
43	collapsed: Option<bool>,
44	playgrounds: Option<bool>,
45	tsconfig: Option<PathBuf>,
46	inline_style_language: Option<String>,
47	optimize: Option<bool>,
48	zoneless: Option<bool>,
49	polyfills: Option<Vec<String>>,
50	workdir: Option<String>,
51
52	html: Option<Table>,
53}
54
55/// Configuration for mdbook-angular
56#[allow(clippy::struct_excessive_bools)]
57pub struct Config {
58	/// Builder to use to compile the angular code
59	///
60	/// Default value: [`Builder::Experimental`]
61	pub builder: Builder,
62	/// Whether code blocks should be collapsed by default
63	///
64	/// This can be overridden via `collapsed` or `uncollapsed` tag on every
65	/// individual code block or `{{#angular}}` tag
66	///
67	/// Note this only takes effect on code blocks tagged with "angular", it
68	/// doesn't affect other code blocks.
69	///
70	/// Default value: `false`
71	pub collapsed: bool,
72	/// Whether playgrounds are enabled by default
73	///
74	/// This can be overridden via `playground` or `no-playground` tag on every
75	/// individual code block or `{{#angular}}` tag.
76	///
77	/// Default value: `true`
78	pub playgrounds: bool,
79	/// Path to a tsconfig to use for building, relative to the `book.toml` file
80	pub tsconfig: Option<PathBuf>,
81	/// The inline style language the angular compiler should use
82	///
83	/// Default value: `"css"`
84	pub inline_style_language: String,
85	/// Whether to optimize the angular applications
86	///
87	/// This option is ignored if background is active
88	///
89	/// Default value: `false`
90	pub optimize: bool,
91	/// Whether to enable Angular Zoneless
92	///
93	/// Requires Angular 20 or later.
94	///
95	/// Default value: `false`
96	pub zoneless: bool,
97	/// Polyfills to import, if any
98	///
99	/// Note: zone.js is always included as polyfill, unless zoneless is set.
100	///
101	/// This only supports bare specifiers, you can't add relative imports here.
102	pub polyfills: Vec<String>,
103
104	/// Configuration to pass to the HTML renderer
105	///
106	/// Use this intead of the `output.html` table itself to configure the HTML
107	/// renderer without having mdbook run the HTML renderer standalone.
108	pub html: Option<Table>,
109
110	pub(crate) book_source_folder: PathBuf,
111	pub(crate) book_theme_folder: PathBuf,
112	pub(crate) angular_root_folder: PathBuf,
113	pub(crate) target_folder: PathBuf,
114}
115
116impl Config {
117	/// Read mdbook-angular [`Config`] from the `book.toml` file inside the given folder.
118	///
119	/// # Errors
120	///
121	/// This function will return an error if reading the `book.toml` fails or if
122	/// the book contains an invalid configuration.
123	pub fn read<P: AsRef<Path>>(root: P) -> Result<Self> {
124		let root = root.as_ref();
125		let mut cfg = mdbook_renderer::config::Config::from_disk(root.join("book.toml"))
126			.context("Error reading book.toml")?;
127		cfg.update_from_env()?;
128
129		Self::from_config(
130			&cfg,
131			root,
132			// Incorrect if there are multiple backends, but... good enough?
133			root.join(&cfg.build.build_dir),
134		)
135	}
136
137	/// Create mdbook-angular configuration [`Config`] from the given render context.
138	///
139	/// # Errors
140	///
141	/// This function fails if the context contains an invalid configuration.
142	pub fn new(ctx: &RenderContext) -> Result<Self> {
143		Self::from_config(&ctx.config, &ctx.root, ctx.destination.clone())
144	}
145
146	fn from_config(
147		config: &mdbook_renderer::config::Config,
148		root: &Path,
149		destination: PathBuf,
150	) -> Result<Self> {
151		let angular_renderer_config = config
152			.get::<DeConfig>("output.angular")
153			.context("Failed to parse mdbook-angular configuration")?
154			.unwrap_or_default();
155
156		let book_source_folder = root.join(&config.book.src);
157		let book_theme_folder = book_source_folder.join("../theme");
158
159		let angular_root_folder = PathBuf::from(
160			angular_renderer_config
161				.workdir
162				.unwrap_or("mdbook_angular".to_owned()),
163		);
164		let angular_root_folder = if angular_root_folder.is_absolute() {
165			angular_root_folder
166		} else {
167			root.join(angular_root_folder)
168		};
169
170		let target_folder = destination;
171
172		let zoneless = angular_renderer_config.zoneless.unwrap_or(false);
173		let mut polyfills = angular_renderer_config.polyfills.unwrap_or_default();
174
175		let zone_polyfill = "zone.js".to_owned();
176		let has_zone_polyfill = polyfills.contains(&zone_polyfill);
177		if zoneless && has_zone_polyfill {
178			return Err(anyhow!(
179				"The zone.js polyfill cannot be included if zoneless is enabled"
180			));
181		}
182		if !zoneless && !has_zone_polyfill {
183			polyfills.push(zone_polyfill);
184		}
185
186		Ok(Config {
187			builder: angular_renderer_config.builder,
188			collapsed: angular_renderer_config.collapsed.unwrap_or(false),
189			playgrounds: angular_renderer_config.playgrounds.unwrap_or(true),
190			tsconfig: angular_renderer_config
191				.tsconfig
192				.map(|tsconfig| root.join(tsconfig)),
193			inline_style_language: angular_renderer_config
194				.inline_style_language
195				.unwrap_or("css".to_owned()),
196			optimize: angular_renderer_config.optimize.unwrap_or(false),
197			zoneless,
198			polyfills,
199
200			html: angular_renderer_config.html,
201
202			book_source_folder,
203			book_theme_folder,
204			angular_root_folder,
205			target_folder,
206		})
207	}
208}