mdbook-angular 0.5.0

mdbook renderer to run angular code samples
Documentation
use std::path::{Path, PathBuf};

use anyhow::{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]
	Foreground,
	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, Default)]
#[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>,
	zoneless: Option<bool>,
	polyfills: Option<Vec<String>>,
	workdir: Option<String>,

	html: Option<Table>,
}

/// Configuration for mdbook-angular
#[allow(clippy::struct_excessive_bools)]
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,
	/// Whether to enable Angular Zoneless
	///
	/// Requires Angular 20 or later.
	///
	/// Default value: `false`
	pub zoneless: bool,
	/// Polyfills to import, if any
	///
	/// Note: zone.js is always included as polyfill, unless zoneless is set.
	///
	/// 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_renderer::config::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_renderer::config::Config,
		root: &Path,
		destination: PathBuf,
	) -> Result<Self> {
		let angular_renderer_config = config
			.get::<DeConfig>("output.angular")
			.context("Failed to parse mdbook-angular configuration")?
			.unwrap_or_default();

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

		let angular_root_folder = PathBuf::from(
			angular_renderer_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;

		let zoneless = angular_renderer_config.zoneless.unwrap_or(false);
		let mut polyfills = angular_renderer_config.polyfills.unwrap_or_default();

		let zone_polyfill = "zone.js".to_owned();
		let has_zone_polyfill = polyfills.contains(&zone_polyfill);
		if zoneless && has_zone_polyfill {
			return Err(anyhow!(
				"The zone.js polyfill cannot be included if zoneless is enabled"
			));
		}
		if !zoneless && !has_zone_polyfill {
			polyfills.push(zone_polyfill);
		}

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

			html: angular_renderer_config.html,

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