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)]
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 =
126			mdbook::Config::from_disk(root.join("book.toml")).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(config: &mdbook::Config, root: &Path, destination: PathBuf) -> Result<Self> {
147		let angular_renderer_config = config
148			.get_renderer("angular")
149			.map_or_else(Default::default, ToOwned::to_owned);
150		let de_config: DeConfig = toml::Value::from(angular_renderer_config)
151			.try_into()
152			.context("Failed to parse mdbook-angular configuration")?;
153
154		let book_source_folder = root.join(&config.book.src);
155		let book_theme_folder = book_source_folder.join("../theme");
156
157		let angular_root_folder =
158			PathBuf::from(de_config.workdir.unwrap_or("mdbook_angular".to_owned()));
159		let angular_root_folder = if angular_root_folder.is_absolute() {
160			angular_root_folder
161		} else {
162			root.join(angular_root_folder)
163		};
164
165		let target_folder = destination;
166
167		let zoneless = de_config.zoneless.unwrap_or(false);
168		let mut polyfills = de_config.polyfills.unwrap_or_default();
169
170		let zone_polyfill = "zone.js".to_owned();
171		let has_zone_polyfill = polyfills.contains(&zone_polyfill);
172		if zoneless && has_zone_polyfill {
173			return Err(anyhow!(
174				"The zone.js polyfill cannot be included if zoneless is enabled"
175			));
176		}
177		if !zoneless && !has_zone_polyfill {
178			polyfills.push(zone_polyfill);
179		}
180
181		Ok(Config {
182			builder: de_config.builder,
183			collapsed: de_config.collapsed.unwrap_or(false),
184			playgrounds: de_config.playgrounds.unwrap_or(true),
185			tsconfig: de_config.tsconfig.map(|tsconfig| root.join(tsconfig)),
186			inline_style_language: de_config.inline_style_language.unwrap_or("css".to_owned()),
187			optimize: de_config.optimize.unwrap_or(false),
188			zoneless,
189			polyfills,
190
191			html: de_config.html,
192
193			book_source_folder,
194			book_theme_folder,
195			angular_root_folder,
196			target_folder,
197		})
198	}
199}