use {
crate::*,
lazy_regex::regex_captures,
rustc_hash::FxHashMap,
std::{
borrow::Cow,
fs,
io::Write,
path::{
Path,
PathBuf,
},
},
termimad::crossterm::style::Stylize,
};
pub struct Project {
pub root: PathBuf,
pub src_path: PathBuf,
pub build_path: PathBuf,
pub config: Config,
modules: Vec<Module>,
pub pages: FxHashMap<PagePath, Page>,
}
impl Project {
pub fn load(path: &Path) -> DdResult<Self> {
let mut project = Self {
root: path.to_owned(),
config: Default::default(),
modules: Default::default(),
pages: Default::default(),
src_path: path.join("src"),
build_path: path.join("site"),
};
project.load_content()?;
Ok(project)
}
pub fn watch_targets(&self) -> Vec<WatchTarget> {
let mut targets = Vec::new();
for module in &self.modules {
module.add_watch_targets(&mut targets);
}
targets
}
pub fn plugin_names(&self) -> impl Iterator<Item = &str> {
self.modules
.iter()
.map(|m| m.name.as_str())
.filter(|name| !name.is_empty())
}
fn load_content(&mut self) -> DdResult<()> {
self.modules = Vec::new();
self.pages.clear();
let main_module = Module::load("", &self.root)?;
let mut config = main_module
.config
.clone()
.ok_or(DdError::ConfigNotFound)?
.take_entity();
let active_plugins = config.active_plugins.clone();
self.modules.push(main_module);
for name in &active_plugins {
let plugin_root = self.root.join("plugins").join(name);
if !plugin_root.exists() {
eprintln!(
"{}: plugin '{}' not found at expected path {:?}",
"error".red().bold(),
name.to_string().red(),
plugin_root,
);
if plugin_is_known(name) {
eprintln!(
" Plugin '{}' is known, but not found in the project.\n You can initialize it with {}?",
name.to_string().yellow(),
format!("ddoc --init-plugin {}", name).green().bold(),
);
}
continue;
}
let plugin_module = Module::load(name, &plugin_root)?;
if let Some(plugin_config) = &plugin_module.config {
config.merge(plugin_config.as_ref());
}
self.modules.push(plugin_module);
}
compat::fix_old_config(&mut config);
config.site_map.add_pages(self);
self.config = config;
Ok(())
}
pub fn build(&self) -> DdResult<()> {
for module in &self.modules {
module.copy_all_statics_into(&self.build_path)?;
}
before_0_16::write_special_js_files_if_needed(&self.config, self)?;
for page_path in self.pages.keys() {
self.build_page(page_path)?;
}
Ok(())
}
pub fn add_js_to_build(
&self,
filename: &str,
bytes: &[u8],
) -> DdResult<()> {
let js_path = self.build_path.join("js").join(filename);
if !js_path.exists() {
if let Some(parent) = js_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&js_path, bytes)?;
}
Ok(())
}
pub fn update(
&mut self,
change: FileChange,
base_url: &str, ) -> DdResult<bool> {
eprintln!("Received change: {:?}", change);
match change {
FileChange::Other => {
self.reload_and_rebuild(base_url)?;
return Ok(true);
}
FileChange::Removal(touched_path) => {
if let Ok(rel_path) = touched_path.strip_prefix(&self.src_path) {
if rel_path.starts_with("css/") || rel_path.starts_with("js/") {
self.reload_and_rebuild(base_url)?;
return Ok(true);
}
}
}
FileChange::Write(touched_path) => {
if let Ok(rel_path) = touched_path.strip_prefix(&self.src_path) {
let ext = rel_path.extension().and_then(|s| s.to_str());
if ext == Some("md") {
for (page_path, page) in &self.pages {
if page.md_file_path == touched_path {
info!("Modified page {:?}", page_path);
let url = page_path.to_absolute_url(base_url);
eprintln!("Modified {}", url.yellow());
self.build_page(page_path)?;
return Ok(true);
}
}
return Ok(false); }
if let Ok(rel_img) = rel_path.strip_prefix("img/") {
info!("Deployed image {rel_img:?}");
eprintln!("Deployed image {}", rel_img.to_string_lossy().yellow());
let dst_path = self.build_path.join("img").join(rel_img);
if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&touched_path, &dst_path)?;
return Ok(true);
}
}
self.reload_and_rebuild(base_url)?;
return Ok(true);
}
}
Ok(false)
}
fn reload_and_rebuild(
&mut self,
base_url: &str, ) -> DdResult<()> {
info!("full rebuild");
eprintln!("Full rebuild of {}", base_url.yellow());
match self.load_content() {
Ok(()) => {
self.build()?;
}
Err(DdError::ConfigNotFound) => eprintln!(
"{}: could not read updated config file at {:?}, keeping the old one.",
"warning".yellow().bold(),
self.root.join(CONFIG_FILE_NAME),
),
Err(e) => eprintln!(
"{}: failed to reload the project: {e}",
"error".red().bold()
),
}
Ok(())
}
pub fn clean_build_dir(&self) -> DdResult<()> {
if self.build_path.exists() {
fs::remove_dir_all(&self.build_path)?;
}
Ok(())
}
pub fn load_and_build(path: &Path) -> DdResult<()> {
let project = Self::load(path)?;
project.build()?;
Ok(())
}
pub fn page_path_of(
&self,
path: &Path,
) -> Option<&PagePath> {
for (page_path, page) in &self.pages {
if page.md_file_path == path {
return Some(page_path);
}
}
None
}
pub fn list_js(&self) -> DdResult<Vec<StaticEntry>> {
let mut entries = Vec::new();
for module in &self.modules {
module.list_js(&mut entries)?;
}
Ok(entries)
}
pub fn list_css(&self) -> DdResult<Vec<StaticEntry>> {
let mut entries = Vec::new();
for module in &self.modules {
module.list_css(&mut entries)?;
}
Ok(entries)
}
pub fn copy_static(
&self,
dir: &str,
) -> DdResult<()> {
let static_src = self.src_path.join(dir);
if !static_src.exists() {
return Ok(());
}
let static_dst = self.build_path.join(dir);
copy_normal_recursive(&static_src, &static_dst)?;
Ok(())
}
pub fn build_page(
&self,
page_path: &PagePath,
) -> DdResult<()> {
let page = self
.pages
.get(page_path)
.ok_or_else(|| DdError::internal(format!("Page not found: {:?}", page_path)))?;
let mut html = String::new();
page.write_html(&mut html, self)?;
let html_path = page_path.html_path_buf(&self.build_path);
if let Some(parent) = html_path.parent() {
std::fs::create_dir_all(parent)?;
}
if html_path.exists() {
fs::remove_file(&html_path)?;
}
let mut file = fs::File::create(&html_path)?;
file.write_all(html.as_bytes())?;
Ok(())
}
pub fn check_img_path(
&self,
mut img_path: &str, page_path: &PagePath,
) {
for _ in 0..page_path.depth() {
if img_path.starts_with("../") {
img_path = &img_path[3..];
}
}
let path = self.build_path.join(img_path);
if !path.exists() {
eprintln!(
"{}: {} contains a broken img src: {}",
"error".red().bold(),
page_path.to_string().yellow(),
img_path.to_string().red(),
);
}
}
pub fn img_url(
&self,
src: &str,
page_path: &PagePath,
) -> String {
let mut src = src;
let conf_var_value;
if let Some(var_name) = src.strip_prefix("--") {
if let Some(var_value) = self.config.var(var_name) {
conf_var_value = var_value;
src = &conf_var_value;
}
}
if let Some((_, before, path)) = regex_captures!(r"^(\.\./)*(img/.*)$", &src) {
self.check_img_path(src, page_path);
let depth = page_path.depth();
if depth == 0 && before.is_empty() {
return src.to_string(); }
let mut url = String::new();
for _ in 0..depth {
url.push_str("../");
}
url.push_str(path);
return url;
}
src.to_string()
}
pub fn load_file(
&self,
path: &str,
) -> DdResult<Option<String>> {
let file_path = self.build_path.join(path);
if !file_path.exists() {
return Ok(None);
}
let content = fs::read_to_string(file_path)?;
Ok(Some(content))
}
pub fn check_page_path(
&self,
page_path: &PagePath,
) {
if !self.pages.contains_key(page_path) {
eprintln!("Error: link to non-existing page: {:?}", page_path);
}
}
pub fn previous_page(
&self,
current_page: &PagePath,
) -> Option<&Page> {
self.config
.site_map
.previous(current_page)
.and_then(|p| self.pages.get(p))
}
pub fn next_page(
&self,
current_page: &PagePath,
) -> Option<&Page> {
self.config
.site_map
.next(current_page)
.and_then(|p| self.pages.get(p))
}
pub fn rewrite_link_url(
&self,
src: &str,
page_path: &PagePath,
) -> Option<String> {
if let Some(var_name) = src.strip_prefix("--") {
if let Some(var_value) = self.config.var(var_name) {
return Some(var_value);
}
if var_name == "previous" {
return self
.config
.site_map
.previous(page_path)
.map(|dst_page_path| page_path.link_to(dst_page_path));
}
if var_name == "next" {
return self
.config
.site_map
.next(page_path)
.map(|dst_page_path| page_path.link_to(dst_page_path));
}
if let Some(value) = before_0_16::expand_special_var(var_name, &self.config) {
return Some(value);
}
return None; }
if let Some((_, path, file, _ext, hash)) =
regex_captures!(r"^/([\w\-/]+/)*([\w\-/]*?)(?:index)?(\.md)?/?(#.*)?$", &src,)
{
let depth = page_path.depth();
let mut url = String::new();
for _ in 0..depth {
url.push_str("../");
}
url.push_str(path);
url.push_str(file);
url.push_str(hash);
let dst_page_path = PagePath::from_path_file(path, file);
if !self.pages.contains_key(&dst_page_path) {
eprintln!("path: {}, file: {}", path, file);
eprintln!("dst_page_path: {:?}", dst_page_path);
eprintln!(
"{}: {} contains a broken link: {}",
"error".red(),
page_path.to_string().yellow(),
src.to_string().red(),
);
}
return Some(url);
}
if let Some((_, path, file, _ext, hash)) =
regex_captures!(r"^(\.\./|[\w\-/]+/)*([\w\-/]+?)(\.md)?/?(#.*)?$", &src,)
{
let dst_page_path = page_path.follow_relative_link(path, file);
if !self.pages.contains_key(&dst_page_path) {
eprintln!(
"{}: {} contains a broken relative link: {}",
"error".red().bold(),
page_path.to_string().yellow(),
src.to_string().red(),
);
}
let file = if file == "index" { "" } else { file };
let url = format!("{}{}{}", path, file, hash,);
return Some(url);
}
None
}
pub fn link_url<'s>(
&self,
src: &'s str,
page_path: &PagePath,
) -> Cow<'s, str> {
match self.rewrite_link_url(src, page_path) {
Some(new_url) => Cow::Owned(new_url),
None => Cow::Borrowed(src),
}
}
pub fn static_url(
&self,
filename: &str,
page_path: &PagePath,
) -> String {
let depth = page_path.depth();
let mut url = String::new();
for _ in 0..depth {
url.push_str("../");
}
url.push_str(filename);
url
}
}