From ee0e5ea51651e49c7b4aac9a7f5d4d1bf87c64d2 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Sat, 20 Dec 2025 14:10:50 -0500 Subject: [PATCH 1/2] added an option to include more than one root folder --- backend/src/app_conf.rs | 2 +- backend/src/git.rs | 84 ++++++++++++++++++++++++++++++-------- backend/src/main.rs | 8 +++- default.toml | 5 ++- frontend/src/lib/render.ts | 82 ++++++++++++++++++------------------- 5 files changed, 119 insertions(+), 62 deletions(-) diff --git a/backend/src/app_conf.rs b/backend/src/app_conf.rs index 18c4e5af..ec9e17b9 100644 --- a/backend/src/app_conf.rs +++ b/backend/src/app_conf.rs @@ -19,7 +19,7 @@ pub struct AppConf { #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct Files { pub asset_path: String, - pub docs_path: String, + pub docs_path: Vec, pub repo_path: String, pub repo_url: String, } diff --git a/backend/src/git.rs b/backend/src/git.rs index 85665059..1d5ff6cf 100644 --- a/backend/src/git.rs +++ b/backend/src/git.rs @@ -23,7 +23,7 @@ pub struct Interface { /// The path to the documents folder, relative to the server executable. /// /// EG: `./repo/docs` - doc_path: PathBuf, + doc_path: Vec, /// The path to the assets folder, relative to the server executable. /// /// EG: `./repo/assets` @@ -52,10 +52,10 @@ impl Interface { pub fn new( repo_url: String, repo_path: String, - docs_path: String, + docs_path: Vec, assets_path: String, ) -> Result { - let doc_path = PathBuf::from(docs_path); + let doc_path: Vec = docs_path.into_iter().collect(); let asset_path = PathBuf::from(assets_path); let repo = Self::load_repository(&repo_url, &repo_path)?; Ok(Self { @@ -76,10 +76,39 @@ impl Interface { /// This function will return an error if filesystem operations fail. #[tracing::instrument(skip(self))] pub fn get_doc + std::fmt::Debug>(&self, path: P) -> Result> { - let mut path_to_doc: PathBuf = PathBuf::from(&self.doc_path); - path_to_doc.push(path); - let doc = Self::get_file(&path_to_doc)?.map(|v| String::from_utf8(v).unwrap()); - Ok(doc) + let path = path.as_ref(); + + // Convert once to string + let path_str = match path.to_str() { + Some(s) => s.trim_start_matches(['/', '\\']), + None => return Ok(None), + }; + + for doc_root in &self.doc_path { + let root_str = match doc_root.to_str() { + Some(s) => s, + None => continue, + }; + + // Use map_or_else instead of if let/else + let candidate_rel = path_str + .strip_prefix(root_str) + .map_or_else( + || PathBuf::from(path_str), + |rest| PathBuf::from(rest.trim_start_matches(['/', '\\'])), + ); + + let candidate = doc_root.join(candidate_rel); + + tracing::debug!("Trying candidate path: {:?}", candidate.display()); + + if let Some(bytes) = Self::get_file(&candidate)? { + let doc = String::from_utf8(bytes).unwrap(); // assuming valid UTF-8 files + return Ok(Some(doc)); + } + } + + Ok(None) } /// Return the asset from the provided `path`, where `path` is the @@ -104,9 +133,22 @@ impl Interface { /// # Errors /// This function fails if filesystem ops fail (reading file, reading directory) #[tracing::instrument(skip(self))] - pub fn get_doc_tree(&self) -> Result { - let doc_tree = Self::get_file_tree(&self.doc_path)?; - Ok(doc_tree) + pub fn get_doc_tree(&self) -> Result { + let mut children = vec![]; + for doc_root in &self.doc_path { + if let Ok(tree) = Self::get_file_tree(doc_root) { + children.push(tree); + } + } + if children.is_empty() { + Err("No doc tree found in any doc root".to_owned()) + } else { + // Synthetic merged root node + Ok(INode { + name: "root".to_string(), + children + }) + } } /// Read the assets folder into a tree-style structure. @@ -137,10 +179,12 @@ impl Interface { path: P, new_doc: &str, ) -> Result<()> { - // TODO: refactoring hopefully means that all paths can just assume that it's relative to - // the root of the repo - let mut path_to_doc: PathBuf = PathBuf::from(&self.doc_path); + let mut path_to_doc = PathBuf::new(); + for part in &self.doc_path { + path_to_doc.push(part); + } path_to_doc.push(path.as_ref()); + Self::put_file(&path_to_doc, new_doc.as_bytes())?; Ok(()) @@ -187,11 +231,15 @@ impl Interface { /// /// # Errors /// Returns an error if the file cannot be deleted from the filesystem. - pub fn delete_doc + Copy>(&self, path: P) -> Result<()> { - let mut path_to_doc: PathBuf = PathBuf::from(&self.doc_path); - path_to_doc.push(path); - Self::delete_file(&path_to_doc)?; - Ok(()) + pub fn delete_doc + Copy>(&self, path: P) -> Result<(), String> { + for doc_root in &self.doc_path { + let mut candidate = doc_root.clone(); + candidate.push(path.as_ref()); + if Self::delete_file(&candidate).is_ok() { + return Ok(()); + } + } + Err(format!("Document {:?} not found in any doc root", path.as_ref())) } /// Deletes the asset at the specified `path` within the repository's asset directory. diff --git a/backend/src/main.rs b/backend/src/main.rs index cb743f82..8ad130c4 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -36,6 +36,7 @@ use std::env::current_exe; use std::sync::Arc; use std::sync::LazyLock; use std::time::Duration; +use std::path::PathBuf; use tracing::{Level, Span}; use tracing::{debug, info, info_span, warn}; @@ -141,7 +142,12 @@ async fn main() -> Result<()> { async fn init_state(cli_args: &Args) -> Result { let repo_url = CONFIG.files.repo_url.clone(); let repo_path = CONFIG.files.repo_path.clone(); - let docs_path = CONFIG.files.docs_path.clone(); + let docs_path: Vec = CONFIG + .files + .docs_path + .iter() + .map(PathBuf::from) + .collect(); let asset_path = CONFIG.files.asset_path.clone(); let git = diff --git a/default.toml b/default.toml index acb1af39..01dedbee 100644 --- a/default.toml +++ b/default.toml @@ -1,7 +1,10 @@ # Files is related to any URL or internal files Hyde will use [files] # The location of the markdown files relative to the root of the repo -docs_path = "docs/" +docs_path = [ + "docs/", + "_includes/" +] # The location of the assets files relative to the root of the repo asset_path = "assets/" # The path where the repository will be pulled and used diff --git a/frontend/src/lib/render.ts b/frontend/src/lib/render.ts index a056e758..06a2157c 100644 --- a/frontend/src/lib/render.ts +++ b/frontend/src/lib/render.ts @@ -5,14 +5,14 @@ import fm from 'front-matter'; import { Renderer, marked, type TokensList } from 'marked'; import DOMPurify from 'dompurify'; -import { ToastType, addToast, dismissToast } from './toast'; +//import { ToastType, addToast, dismissToast } from './toast'; import { apiAddress } from './main'; /** * When the rendered file is missing a valid frontmatter header, then an error toast is displayed. * If the toast is not displayed, this is set to zero. If it *is* displayed, this is the ID of the toast being rendered. */ -let toastId = -1; +//let toastId = -1; interface FrontMatter { title?: string; @@ -28,14 +28,14 @@ interface FrontMatter { */ export async function renderMarkdown(input: string, output: HTMLElement): Promise { // Parse front matter and get title, description, and markdown content - getFrontMatterType(input); + // getFrontMatterType(input); const parsed = fm(input); const frontMatter = parsed.attributes as FrontMatter; const title = frontMatter.title; const description = frontMatter.description; const content = parsed.body; - checkFrontMatter(title); + //checkFrontMatter(title); // Convert content to tokens and process images marked.use({ renderer: new Renderer() }); @@ -68,44 +68,44 @@ export async function renderMarkdown(input: string, output: HTMLElement): Promis output.innerHTML = outputHtml; } -export function checkFrontMatter(title?: string): void { - if (!title) { - // Display a toast notification if title is missing - if (toastId === -1) { - toastId = addToast( - 'Missing front matter: Ensure the title is defined.', - ToastType.Error, - false - ); - } - } else { - // Hide the toast if title is present - if (toastId !== -1) { - dismissToast(toastId); - toastId = -1; - } - } -} +// export function checkFrontMatter(title?: string): void { +// if (!title) { +// // Display a toast notification if title is missing +// if (toastId === -1) { +// toastId = addToast( +// 'Missing front matter: Ensure the title is defined.', +// ToastType.Error, +// false +// ); +// } +// } else { +// // Hide the toast if title is present +// if (toastId !== -1) { +// dismissToast(toastId); +// toastId = -1; +// } +// } +// } -export function getFrontMatterType(input: string): 'yaml' | 'toml' | 'json' | 'unknown' { - const trimmed = input.trim(); +// export function getFrontMatterType(input: string): 'yaml' | 'toml' | 'json' | 'unknown' { +// const trimmed = input.trim(); - if (trimmed.startsWith('---') && trimmed.endsWith('---')) { - return 'yaml'; - } else if (trimmed.startsWith('+++') && trimmed.endsWith('+++')) { - return 'toml'; - } else if (trimmed.startsWith('{') && trimmed.endsWith('}')) { - return 'json'; - } +// if (trimmed.startsWith('---') && trimmed.endsWith('---')) { +// return 'yaml'; +// } else if (trimmed.startsWith('+++') && trimmed.endsWith('+++')) { +// return 'toml'; +// } else if (trimmed.startsWith('{') && trimmed.endsWith('}')) { +// return 'json'; +// } - // Display a toast notification if front matter is not YAML - if (toastId === -1) { - toastId = addToast( - 'Warning: Front matter is not in YAML format. YAML is recommended.', - ToastType.Warning, - false - ); - } +// // Display a toast notification if front matter is not YAML +// if (toastId === -1) { +// toastId = addToast( +// 'Warning: Front matter is not in YAML format. YAML is recommended.', +// ToastType.Warning, +// false +// ); +// } - return 'unknown'; -} +// return 'unknown'; +// } From e19101b716e66800dc2f02ff56a0a331c09eecc0 Mon Sep 17 00:00:00 2001 From: TheKrol Date: Sat, 20 Dec 2025 14:10:50 -0500 Subject: [PATCH 2/2] Update hyde to include second path and comment out frontmatter code for now until updates occur. --- backend/src/app_conf.rs | 2 +- backend/src/git.rs | 84 ++++++++++++++++++++++++++------- backend/src/main.rs | 8 +++- default.toml | 5 +- frontend/src/lib/render.test.ts | 55 +++++++++++---------- frontend/src/lib/render.ts | 82 ++++++++++++++++---------------- 6 files changed, 148 insertions(+), 88 deletions(-) diff --git a/backend/src/app_conf.rs b/backend/src/app_conf.rs index 18c4e5af..ec9e17b9 100644 --- a/backend/src/app_conf.rs +++ b/backend/src/app_conf.rs @@ -19,7 +19,7 @@ pub struct AppConf { #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct Files { pub asset_path: String, - pub docs_path: String, + pub docs_path: Vec, pub repo_path: String, pub repo_url: String, } diff --git a/backend/src/git.rs b/backend/src/git.rs index 85665059..1d5ff6cf 100644 --- a/backend/src/git.rs +++ b/backend/src/git.rs @@ -23,7 +23,7 @@ pub struct Interface { /// The path to the documents folder, relative to the server executable. /// /// EG: `./repo/docs` - doc_path: PathBuf, + doc_path: Vec, /// The path to the assets folder, relative to the server executable. /// /// EG: `./repo/assets` @@ -52,10 +52,10 @@ impl Interface { pub fn new( repo_url: String, repo_path: String, - docs_path: String, + docs_path: Vec, assets_path: String, ) -> Result { - let doc_path = PathBuf::from(docs_path); + let doc_path: Vec = docs_path.into_iter().collect(); let asset_path = PathBuf::from(assets_path); let repo = Self::load_repository(&repo_url, &repo_path)?; Ok(Self { @@ -76,10 +76,39 @@ impl Interface { /// This function will return an error if filesystem operations fail. #[tracing::instrument(skip(self))] pub fn get_doc + std::fmt::Debug>(&self, path: P) -> Result> { - let mut path_to_doc: PathBuf = PathBuf::from(&self.doc_path); - path_to_doc.push(path); - let doc = Self::get_file(&path_to_doc)?.map(|v| String::from_utf8(v).unwrap()); - Ok(doc) + let path = path.as_ref(); + + // Convert once to string + let path_str = match path.to_str() { + Some(s) => s.trim_start_matches(['/', '\\']), + None => return Ok(None), + }; + + for doc_root in &self.doc_path { + let root_str = match doc_root.to_str() { + Some(s) => s, + None => continue, + }; + + // Use map_or_else instead of if let/else + let candidate_rel = path_str + .strip_prefix(root_str) + .map_or_else( + || PathBuf::from(path_str), + |rest| PathBuf::from(rest.trim_start_matches(['/', '\\'])), + ); + + let candidate = doc_root.join(candidate_rel); + + tracing::debug!("Trying candidate path: {:?}", candidate.display()); + + if let Some(bytes) = Self::get_file(&candidate)? { + let doc = String::from_utf8(bytes).unwrap(); // assuming valid UTF-8 files + return Ok(Some(doc)); + } + } + + Ok(None) } /// Return the asset from the provided `path`, where `path` is the @@ -104,9 +133,22 @@ impl Interface { /// # Errors /// This function fails if filesystem ops fail (reading file, reading directory) #[tracing::instrument(skip(self))] - pub fn get_doc_tree(&self) -> Result { - let doc_tree = Self::get_file_tree(&self.doc_path)?; - Ok(doc_tree) + pub fn get_doc_tree(&self) -> Result { + let mut children = vec![]; + for doc_root in &self.doc_path { + if let Ok(tree) = Self::get_file_tree(doc_root) { + children.push(tree); + } + } + if children.is_empty() { + Err("No doc tree found in any doc root".to_owned()) + } else { + // Synthetic merged root node + Ok(INode { + name: "root".to_string(), + children + }) + } } /// Read the assets folder into a tree-style structure. @@ -137,10 +179,12 @@ impl Interface { path: P, new_doc: &str, ) -> Result<()> { - // TODO: refactoring hopefully means that all paths can just assume that it's relative to - // the root of the repo - let mut path_to_doc: PathBuf = PathBuf::from(&self.doc_path); + let mut path_to_doc = PathBuf::new(); + for part in &self.doc_path { + path_to_doc.push(part); + } path_to_doc.push(path.as_ref()); + Self::put_file(&path_to_doc, new_doc.as_bytes())?; Ok(()) @@ -187,11 +231,15 @@ impl Interface { /// /// # Errors /// Returns an error if the file cannot be deleted from the filesystem. - pub fn delete_doc + Copy>(&self, path: P) -> Result<()> { - let mut path_to_doc: PathBuf = PathBuf::from(&self.doc_path); - path_to_doc.push(path); - Self::delete_file(&path_to_doc)?; - Ok(()) + pub fn delete_doc + Copy>(&self, path: P) -> Result<(), String> { + for doc_root in &self.doc_path { + let mut candidate = doc_root.clone(); + candidate.push(path.as_ref()); + if Self::delete_file(&candidate).is_ok() { + return Ok(()); + } + } + Err(format!("Document {:?} not found in any doc root", path.as_ref())) } /// Deletes the asset at the specified `path` within the repository's asset directory. diff --git a/backend/src/main.rs b/backend/src/main.rs index cb743f82..8ad130c4 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -36,6 +36,7 @@ use std::env::current_exe; use std::sync::Arc; use std::sync::LazyLock; use std::time::Duration; +use std::path::PathBuf; use tracing::{Level, Span}; use tracing::{debug, info, info_span, warn}; @@ -141,7 +142,12 @@ async fn main() -> Result<()> { async fn init_state(cli_args: &Args) -> Result { let repo_url = CONFIG.files.repo_url.clone(); let repo_path = CONFIG.files.repo_path.clone(); - let docs_path = CONFIG.files.docs_path.clone(); + let docs_path: Vec = CONFIG + .files + .docs_path + .iter() + .map(PathBuf::from) + .collect(); let asset_path = CONFIG.files.asset_path.clone(); let git = diff --git a/default.toml b/default.toml index acb1af39..01dedbee 100644 --- a/default.toml +++ b/default.toml @@ -1,7 +1,10 @@ # Files is related to any URL or internal files Hyde will use [files] # The location of the markdown files relative to the root of the repo -docs_path = "docs/" +docs_path = [ + "docs/", + "_includes/" +] # The location of the assets files relative to the root of the repo asset_path = "assets/" # The path where the repository will be pulled and used diff --git a/frontend/src/lib/render.test.ts b/frontend/src/lib/render.test.ts index 3e891795..e0b3b205 100644 --- a/frontend/src/lib/render.test.ts +++ b/frontend/src/lib/render.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect, vi } from 'vitest'; import { renderMarkdown } from './render'; -import { addToast } from './toast'; +//import { addToast } from './toast'; // Mock the addToast function vi.mock('./toast', async (importOriginal) => { @@ -36,36 +36,39 @@ Content here.`; expect(mockOutput.innerHTML).toContain('Content here.'); }); - test('displays error toast when frontmatter header is missing', () => { - const input = `--- -layout ---- ----`; + // Commented out until we can figure out the frontmatter with updated paths. + // Will probably need to categorize each section of the wiki with specific frontmatters - const mockOutput = { innerHTML: '' } as HTMLElement; + // test('displays error toast when frontmatter header is missing', () => { + // const input = `--- + // layout + // --- + // ---`; - renderMarkdown(input, mockOutput); + // const mockOutput = { innerHTML: '' } as HTMLElement; - // Check that addToast was called at least once with the error message - expect(addToast).toHaveBeenCalled(); - expect(addToast).toHaveBeenCalledWith( - 'Missing front matter: Ensure the title is defined.', - expect.anything(), - false - ); - }); + // renderMarkdown(input, mockOutput); - test('preserves title and description when frontmatter is malformed', async () => { - const input = `--- -title: My Title -description: My Description ----`; + // // Check that addToast was called at least once with the error message + // expect(addToast).toHaveBeenCalled(); + // expect(addToast).toHaveBeenCalledWith( + // 'Missing front matter: Ensure the title is defined.', + // expect.anything(), + // false + // ); + // }); - const mockOutput = { innerHTML: '' } as HTMLElement; + // test('preserves title and description when frontmatter is malformed', async () => { + // const input = `--- + // title: My Title + // description: My Description + // ---`; - await renderMarkdown(input, mockOutput); + // const mockOutput = { innerHTML: '' } as HTMLElement; - expect(mockOutput.innerHTML).toContain('

My Title

'); - expect(mockOutput.innerHTML).toContain('

My Description

'); - }); + // await renderMarkdown(input, mockOutput); + + // expect(mockOutput.innerHTML).toContain('

My Title

'); + // expect(mockOutput.innerHTML).toContain('

My Description

'); + // }); }); diff --git a/frontend/src/lib/render.ts b/frontend/src/lib/render.ts index a056e758..06a2157c 100644 --- a/frontend/src/lib/render.ts +++ b/frontend/src/lib/render.ts @@ -5,14 +5,14 @@ import fm from 'front-matter'; import { Renderer, marked, type TokensList } from 'marked'; import DOMPurify from 'dompurify'; -import { ToastType, addToast, dismissToast } from './toast'; +//import { ToastType, addToast, dismissToast } from './toast'; import { apiAddress } from './main'; /** * When the rendered file is missing a valid frontmatter header, then an error toast is displayed. * If the toast is not displayed, this is set to zero. If it *is* displayed, this is the ID of the toast being rendered. */ -let toastId = -1; +//let toastId = -1; interface FrontMatter { title?: string; @@ -28,14 +28,14 @@ interface FrontMatter { */ export async function renderMarkdown(input: string, output: HTMLElement): Promise { // Parse front matter and get title, description, and markdown content - getFrontMatterType(input); + // getFrontMatterType(input); const parsed = fm(input); const frontMatter = parsed.attributes as FrontMatter; const title = frontMatter.title; const description = frontMatter.description; const content = parsed.body; - checkFrontMatter(title); + //checkFrontMatter(title); // Convert content to tokens and process images marked.use({ renderer: new Renderer() }); @@ -68,44 +68,44 @@ export async function renderMarkdown(input: string, output: HTMLElement): Promis output.innerHTML = outputHtml; } -export function checkFrontMatter(title?: string): void { - if (!title) { - // Display a toast notification if title is missing - if (toastId === -1) { - toastId = addToast( - 'Missing front matter: Ensure the title is defined.', - ToastType.Error, - false - ); - } - } else { - // Hide the toast if title is present - if (toastId !== -1) { - dismissToast(toastId); - toastId = -1; - } - } -} +// export function checkFrontMatter(title?: string): void { +// if (!title) { +// // Display a toast notification if title is missing +// if (toastId === -1) { +// toastId = addToast( +// 'Missing front matter: Ensure the title is defined.', +// ToastType.Error, +// false +// ); +// } +// } else { +// // Hide the toast if title is present +// if (toastId !== -1) { +// dismissToast(toastId); +// toastId = -1; +// } +// } +// } -export function getFrontMatterType(input: string): 'yaml' | 'toml' | 'json' | 'unknown' { - const trimmed = input.trim(); +// export function getFrontMatterType(input: string): 'yaml' | 'toml' | 'json' | 'unknown' { +// const trimmed = input.trim(); - if (trimmed.startsWith('---') && trimmed.endsWith('---')) { - return 'yaml'; - } else if (trimmed.startsWith('+++') && trimmed.endsWith('+++')) { - return 'toml'; - } else if (trimmed.startsWith('{') && trimmed.endsWith('}')) { - return 'json'; - } +// if (trimmed.startsWith('---') && trimmed.endsWith('---')) { +// return 'yaml'; +// } else if (trimmed.startsWith('+++') && trimmed.endsWith('+++')) { +// return 'toml'; +// } else if (trimmed.startsWith('{') && trimmed.endsWith('}')) { +// return 'json'; +// } - // Display a toast notification if front matter is not YAML - if (toastId === -1) { - toastId = addToast( - 'Warning: Front matter is not in YAML format. YAML is recommended.', - ToastType.Warning, - false - ); - } +// // Display a toast notification if front matter is not YAML +// if (toastId === -1) { +// toastId = addToast( +// 'Warning: Front matter is not in YAML format. YAML is recommended.', +// ToastType.Warning, +// false +// ); +// } - return 'unknown'; -} +// return 'unknown'; +// }