1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
use std::path::{Path, PathBuf};

use anyhow::Context;
use mdbook::renderer::RenderContext;
use serde::Deserialize;
use toml::value::Table;

use crate::Result;

#[derive(Deserialize, PartialEq, Eq, Debug, Default, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum Builder {
	/// Build all chapters in a single angular build.
	///
	/// This is fast, but uses internal Angular APIs to use the currently
	/// experimental "application" builder Angular provides as of 16.2.0.
	#[default]
	Experimental,
	/// Build via [`Builder::Experimental`] in a background process.
	///
	/// This allows the angular process to keep running after the renderer exits.
	/// This builder option enables watching, which significantly speeds up
	/// rebuilds.
	///
	/// This option is not supported on Windows, where this option is considered
	/// an alias to [`Builder::Experimental`].
	Background,
	/// Build every chapter as a separate angular application.
	///
	/// This uses stable Angular APIs and should work for Angular 14.0.0 and up.
	Slow,
}

#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct DeConfig {
	#[allow(unused)] // the command option is defined by mdbook
	command: Option<String>,

	#[serde(default)]
	builder: Builder,
	collapsed: Option<bool>,
	playgrounds: Option<bool>,
	tsconfig: Option<PathBuf>,
	inline_style_language: Option<String>,
	optimize: Option<bool>,
	polyfills: Option<Vec<String>>,
	workdir: Option<String>,

	html: Option<Table>,
}

/// Configuration for mdbook-angular
pub struct Config {
	/// Builder to use to compile the angular code
	///
	/// Default value: [`Builder::Experimental`]
	pub builder: Builder,
	/// Whether code blocks should be collapsed by default
	///
	/// This can be overridden via `collapsed` or `uncollapsed` tag on every
	/// individual code block or `{{#angular}}` tag
	///
	/// Note this only takes effect on code blocks tagged with "angular", it
	/// doesn't affect other code blocks.
	///
	/// Default value: `false`
	pub collapsed: bool,
	/// Whether playgrounds are enabled by default
	///
	/// This can be overridden via `playground` or `no-playground` tag on every
	/// individual code block or `{{#angular}}` tag.
	///
	/// Default value: `true`
	pub playgrounds: bool,
	/// Path to a tsconfig to use for building, relative to the `book.toml` file
	pub tsconfig: Option<PathBuf>,
	/// The inline style language the angular compiler should use
	///
	/// Default value: `"css"`
	pub inline_style_language: String,
	/// Whether to optimize the angular applications
	///
	/// This option is ignored if background is active
	///
	/// Default value: `false`
	pub optimize: bool,
	/// Polyfills to import, if any
	///
	/// Note: zone.js is always included as polyfill.
	///
	/// This only supports bare specifiers, you can't add relative imports here.
	pub polyfills: Vec<String>,

	/// Configuration to pass to the HTML renderer
	///
	/// Use this intead of the `output.html` table itself to configure the HTML
	/// renderer without having mdbook run the HTML renderer standalone.
	pub html: Option<Table>,

	pub(crate) book_source_folder: PathBuf,
	pub(crate) book_theme_folder: PathBuf,
	pub(crate) angular_root_folder: PathBuf,
	pub(crate) target_folder: PathBuf,
}

impl Config {
	/// Read mdbook-angular [`Config`] from the `book.toml` file inside the given folder.
	///
	/// # Errors
	///
	/// This function will return an error if reading the `book.toml` fails or if
	/// the book contains an invalid configuration.
	pub fn read<P: AsRef<Path>>(root: P) -> Result<Self> {
		let root = root.as_ref();
		let mut cfg =
			mdbook::Config::from_disk(root.join("book.toml")).context("Error reading book.toml")?;
		cfg.update_from_env();

		Self::from_config(
			&cfg,
			root,
			// Incorrect if there are multiple backends, but... good enough?
			root.join(&cfg.build.build_dir),
		)
	}

	/// Create mdbook-angular configuration [`Config`] from the given render context.
	///
	/// # Errors
	///
	/// This function fails if the context contains an invalid configuration.
	pub fn new(ctx: &RenderContext) -> Result<Self> {
		Self::from_config(&ctx.config, &ctx.root, ctx.destination.clone())
	}

	fn from_config(config: &mdbook::Config, root: &Path, destination: PathBuf) -> Result<Self> {
		let angular_renderer_config = config
			.get_renderer("angular")
			.map_or_else(Default::default, ToOwned::to_owned);
		let de_config: DeConfig = toml::Value::from(angular_renderer_config)
			.try_into()
			.context("Failed to parse mdbook-angular configuration")?;

		let book_source_folder = root.join(&config.book.src);
		let book_theme_folder = book_source_folder.join("../theme");

		let angular_root_folder =
			PathBuf::from(de_config.workdir.unwrap_or("mdbook_angular".to_owned()));
		let angular_root_folder = if angular_root_folder.is_absolute() {
			angular_root_folder
		} else {
			root.join(angular_root_folder)
		};

		let target_folder = destination;

		Ok(Config {
			builder: de_config.builder,
			collapsed: de_config.collapsed.unwrap_or(false),
			playgrounds: de_config.playgrounds.unwrap_or(true),
			tsconfig: de_config.tsconfig.map(|tsconfig| root.join(tsconfig)),
			inline_style_language: de_config.inline_style_language.unwrap_or("css".to_owned()),
			optimize: de_config.optimize.unwrap_or(false),
			polyfills: de_config.polyfills.unwrap_or_default(),

			html: de_config.html,

			book_source_folder,
			book_theme_folder,
			angular_root_folder,
			target_folder,
		})
	}
}