...
This commit is contained in:
@@ -4,3 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
walkdir = "2.3.3"
|
||||
pulldown-cmark = "0.9.3"
|
||||
thiserror = "1.0.40"
|
||||
lazy_static = "1.4.0"
|
||||
|
412
doctree/src/collection.rs
Normal file
412
doctree/src/collection.rs
Normal file
@@ -0,0 +1,412 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
use std::fs;
|
||||
|
||||
use crate::error::{DocTreeError, Result};
|
||||
use crate::storage::RedisStorage;
|
||||
use crate::utils::{name_fix, markdown_to_html, ensure_md_extension};
|
||||
use crate::include::process_includes;
|
||||
|
||||
/// Collection represents a collection of markdown pages and files
|
||||
#[derive(Clone)]
|
||||
pub struct Collection {
|
||||
/// Base path of the collection
|
||||
pub path: PathBuf,
|
||||
|
||||
/// Name of the collection (namefixed)
|
||||
pub name: String,
|
||||
|
||||
/// Redis storage backend
|
||||
pub storage: RedisStorage,
|
||||
}
|
||||
|
||||
/// Builder for Collection
|
||||
pub struct CollectionBuilder {
|
||||
/// Base path of the collection
|
||||
path: PathBuf,
|
||||
|
||||
/// Name of the collection (namefixed)
|
||||
name: String,
|
||||
|
||||
/// Redis storage backend
|
||||
storage: Option<RedisStorage>,
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
/// Create a new CollectionBuilder
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Base path of the collection
|
||||
/// * `name` - Name of the collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new CollectionBuilder
|
||||
pub fn builder<P: AsRef<Path>>(path: P, name: &str) -> CollectionBuilder {
|
||||
CollectionBuilder {
|
||||
path: path.as_ref().to_path_buf(),
|
||||
name: name_fix(name),
|
||||
storage: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan walks over the path and finds all files and .md files
|
||||
/// It stores the relative positions in Redis
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(()) on success or an error
|
||||
pub fn scan(&self) -> Result<()> {
|
||||
// Delete existing collection data if any
|
||||
self.storage.delete_collection(&self.name)?;
|
||||
|
||||
// Walk through the directory
|
||||
let walker = WalkDir::new(&self.path);
|
||||
for entry_result in walker {
|
||||
// Handle entry errors
|
||||
let entry = match entry_result {
|
||||
Ok(entry) => entry,
|
||||
Err(e) => {
|
||||
// Log the error and continue
|
||||
eprintln!("Error walking directory: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Skip directories
|
||||
if entry.file_type().is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the relative path from the base path
|
||||
let rel_path = match entry.path().strip_prefix(&self.path) {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
// Log the error and continue
|
||||
eprintln!("Failed to get relative path for: {:?}", entry.path());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the filename and apply namefix
|
||||
let filename = entry.file_name().to_string_lossy().to_string();
|
||||
let namefixed_filename = name_fix(&filename);
|
||||
|
||||
// Store in Redis using the namefixed filename as the key
|
||||
// Store the original relative path to preserve case and special characters
|
||||
self.storage.store_collection_entry(
|
||||
&self.name,
|
||||
&namefixed_filename,
|
||||
&rel_path.to_string_lossy()
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a page by name and return its markdown content
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `page_name` - Name of the page
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The page content or an error
|
||||
pub fn page_get(&self, page_name: &str) -> Result<String> {
|
||||
// Apply namefix to the page name
|
||||
let namefixed_page_name = name_fix(page_name);
|
||||
|
||||
// Ensure it has .md extension
|
||||
let namefixed_page_name = ensure_md_extension(&namefixed_page_name);
|
||||
|
||||
// Get the relative path from Redis
|
||||
let rel_path = self.storage.get_collection_entry(&self.name, &namefixed_page_name)
|
||||
.map_err(|_| DocTreeError::PageNotFound(page_name.to_string()))?;
|
||||
|
||||
// Read the file
|
||||
let full_path = self.path.join(rel_path);
|
||||
let content = fs::read_to_string(full_path)
|
||||
.map_err(|e| DocTreeError::IoError(e))?;
|
||||
|
||||
// Skip include processing at this level to avoid infinite recursion
|
||||
// Include processing will be done at the higher level
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Create or update a page in the collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `page_name` - Name of the page
|
||||
/// * `content` - Content of the page
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(()) on success or an error
|
||||
pub fn page_set(&self, page_name: &str, content: &str) -> Result<()> {
|
||||
// Apply namefix to the page name
|
||||
let namefixed_page_name = name_fix(page_name);
|
||||
|
||||
// Ensure it has .md extension
|
||||
let namefixed_page_name = ensure_md_extension(&namefixed_page_name);
|
||||
|
||||
// Create the full path
|
||||
let full_path = self.path.join(&namefixed_page_name);
|
||||
|
||||
// Create directories if needed
|
||||
if let Some(parent) = full_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(DocTreeError::IoError)?;
|
||||
}
|
||||
|
||||
// Write content to file
|
||||
fs::write(&full_path, content).map_err(DocTreeError::IoError)?;
|
||||
|
||||
// Update Redis
|
||||
self.storage.store_collection_entry(&self.name, &namefixed_page_name, &namefixed_page_name)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a page from the collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `page_name` - Name of the page
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(()) on success or an error
|
||||
pub fn page_delete(&self, page_name: &str) -> Result<()> {
|
||||
// Apply namefix to the page name
|
||||
let namefixed_page_name = name_fix(page_name);
|
||||
|
||||
// Ensure it has .md extension
|
||||
let namefixed_page_name = ensure_md_extension(&namefixed_page_name);
|
||||
|
||||
// Get the relative path from Redis
|
||||
let rel_path = self.storage.get_collection_entry(&self.name, &namefixed_page_name)
|
||||
.map_err(|_| DocTreeError::PageNotFound(page_name.to_string()))?;
|
||||
|
||||
// Delete the file
|
||||
let full_path = self.path.join(rel_path);
|
||||
fs::remove_file(full_path).map_err(DocTreeError::IoError)?;
|
||||
|
||||
// Remove from Redis
|
||||
self.storage.delete_collection_entry(&self.name, &namefixed_page_name)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all pages in the collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of page names or an error
|
||||
pub fn page_list(&self) -> Result<Vec<String>> {
|
||||
// Get all keys from Redis
|
||||
let keys = self.storage.list_collection_entries(&self.name)?;
|
||||
|
||||
// Filter to only include .md files
|
||||
let pages = keys.into_iter()
|
||||
.filter(|key| key.ends_with(".md"))
|
||||
.collect();
|
||||
|
||||
Ok(pages)
|
||||
}
|
||||
|
||||
/// Get the URL for a file
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `file_name` - Name of the file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The URL for the file or an error
|
||||
pub fn file_get_url(&self, file_name: &str) -> Result<String> {
|
||||
// Apply namefix to the file name
|
||||
let namefixed_file_name = name_fix(file_name);
|
||||
|
||||
// Get the relative path from Redis
|
||||
let rel_path = self.storage.get_collection_entry(&self.name, &namefixed_file_name)
|
||||
.map_err(|_| DocTreeError::FileNotFound(file_name.to_string()))?;
|
||||
|
||||
// Construct a URL for the file
|
||||
let url = format!("/collections/{}/files/{}", self.name, rel_path);
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
/// Add or update a file in the collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `file_name` - Name of the file
|
||||
/// * `content` - Content of the file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(()) on success or an error
|
||||
pub fn file_set(&self, file_name: &str, content: &[u8]) -> Result<()> {
|
||||
// Apply namefix to the file name
|
||||
let namefixed_file_name = name_fix(file_name);
|
||||
|
||||
// Create the full path
|
||||
let full_path = self.path.join(&namefixed_file_name);
|
||||
|
||||
// Create directories if needed
|
||||
if let Some(parent) = full_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(DocTreeError::IoError)?;
|
||||
}
|
||||
|
||||
// Write content to file
|
||||
fs::write(&full_path, content).map_err(DocTreeError::IoError)?;
|
||||
|
||||
// Update Redis
|
||||
self.storage.store_collection_entry(&self.name, &namefixed_file_name, &namefixed_file_name)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a file from the collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `file_name` - Name of the file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(()) on success or an error
|
||||
pub fn file_delete(&self, file_name: &str) -> Result<()> {
|
||||
// Apply namefix to the file name
|
||||
let namefixed_file_name = name_fix(file_name);
|
||||
|
||||
// Get the relative path from Redis
|
||||
let rel_path = self.storage.get_collection_entry(&self.name, &namefixed_file_name)
|
||||
.map_err(|_| DocTreeError::FileNotFound(file_name.to_string()))?;
|
||||
|
||||
// Delete the file
|
||||
let full_path = self.path.join(rel_path);
|
||||
fs::remove_file(full_path).map_err(DocTreeError::IoError)?;
|
||||
|
||||
// Remove from Redis
|
||||
self.storage.delete_collection_entry(&self.name, &namefixed_file_name)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all files (non-markdown) in the collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of file names or an error
|
||||
pub fn file_list(&self) -> Result<Vec<String>> {
|
||||
// Get all keys from Redis
|
||||
let keys = self.storage.list_collection_entries(&self.name)?;
|
||||
|
||||
// Filter to exclude .md files
|
||||
let files = keys.into_iter()
|
||||
.filter(|key| !key.ends_with(".md"))
|
||||
.collect();
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Get the relative path of a page in the collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `page_name` - Name of the page
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The relative path of the page or an error
|
||||
pub fn page_get_path(&self, page_name: &str) -> Result<String> {
|
||||
// Apply namefix to the page name
|
||||
let namefixed_page_name = name_fix(page_name);
|
||||
|
||||
// Ensure it has .md extension
|
||||
let namefixed_page_name = ensure_md_extension(&namefixed_page_name);
|
||||
|
||||
// Get the relative path from Redis
|
||||
self.storage.get_collection_entry(&self.name, &namefixed_page_name)
|
||||
.map_err(|_| DocTreeError::PageNotFound(page_name.to_string()))
|
||||
}
|
||||
|
||||
/// Get a page by name and return its HTML content
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `page_name` - Name of the page
|
||||
/// * `doctree` - Optional DocTree instance for include processing
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The HTML content of the page or an error
|
||||
pub fn page_get_html(&self, page_name: &str, doctree: Option<&crate::doctree::DocTree>) -> Result<String> {
|
||||
// Get the markdown content
|
||||
let markdown = self.page_get(page_name)?;
|
||||
|
||||
// Process includes if doctree is provided
|
||||
let processed_markdown = if let Some(dt) = doctree {
|
||||
process_includes(&markdown, &self.name, dt)?
|
||||
} else {
|
||||
markdown
|
||||
};
|
||||
|
||||
// Convert markdown to HTML
|
||||
let html = markdown_to_html(&processed_markdown);
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
/// Get information about the Collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A map of information
|
||||
pub fn info(&self) -> std::collections::HashMap<String, String> {
|
||||
let mut info = std::collections::HashMap::new();
|
||||
info.insert("name".to_string(), self.name.clone());
|
||||
info.insert("path".to_string(), self.path.to_string_lossy().to_string());
|
||||
info
|
||||
}
|
||||
}
|
||||
|
||||
impl CollectionBuilder {
|
||||
/// Set the storage backend
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `storage` - Redis storage backend
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Self for method chaining
|
||||
pub fn with_storage(mut self, storage: RedisStorage) -> Self {
|
||||
self.storage = Some(storage);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new Collection or an error
|
||||
pub fn build(self) -> Result<Collection> {
|
||||
let storage = self.storage.ok_or_else(|| {
|
||||
DocTreeError::MissingParameter("storage".to_string())
|
||||
})?;
|
||||
|
||||
let collection = Collection {
|
||||
path: self.path,
|
||||
name: self.name,
|
||||
storage,
|
||||
};
|
||||
|
||||
Ok(collection)
|
||||
}
|
||||
}
|
433
doctree/src/doctree.rs
Normal file
433
doctree/src/doctree.rs
Normal file
@@ -0,0 +1,433 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::collection::{Collection, CollectionBuilder};
|
||||
use crate::error::{DocTreeError, Result};
|
||||
use crate::storage::RedisStorage;
|
||||
use crate::include::process_includes;
|
||||
use crate::utils::{name_fix, ensure_md_extension};
|
||||
|
||||
// Global variable to track the current collection name
|
||||
// This is for compatibility with the Go implementation
|
||||
lazy_static::lazy_static! {
|
||||
static ref CURRENT_COLLECTION_NAME: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
// Global variable to track the current Collection
|
||||
// This is for compatibility with the Go implementation
|
||||
|
||||
/// DocTree represents a manager for multiple collections
|
||||
pub struct DocTree {
|
||||
/// Map of collections by name
|
||||
pub collections: HashMap<String, Collection>,
|
||||
|
||||
/// Default collection name
|
||||
pub default_collection: Option<String>,
|
||||
|
||||
/// Redis storage backend
|
||||
storage: RedisStorage,
|
||||
|
||||
/// For backward compatibility
|
||||
pub name: String,
|
||||
|
||||
/// For backward compatibility
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
/// Builder for DocTree
|
||||
pub struct DocTreeBuilder {
|
||||
/// Map of collections by name
|
||||
collections: HashMap<String, Collection>,
|
||||
|
||||
/// Default collection name
|
||||
default_collection: Option<String>,
|
||||
|
||||
/// Redis storage backend
|
||||
storage: Option<RedisStorage>,
|
||||
|
||||
/// For backward compatibility
|
||||
name: Option<String>,
|
||||
|
||||
/// For backward compatibility
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl DocTree {
|
||||
/// Create a new DocTreeBuilder
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new DocTreeBuilder
|
||||
pub fn builder() -> DocTreeBuilder {
|
||||
DocTreeBuilder {
|
||||
collections: HashMap::new(),
|
||||
default_collection: None,
|
||||
storage: None,
|
||||
name: None,
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a collection to the DocTree
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Base path of the collection
|
||||
/// * `name` - Name of the collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The added collection or an error
|
||||
pub fn add_collection<P: AsRef<Path>>(&mut self, path: P, name: &str) -> Result<&Collection> {
|
||||
// Create a new collection
|
||||
let namefixed = name_fix(name);
|
||||
let collection = Collection::builder(path, &namefixed)
|
||||
.with_storage(self.storage.clone())
|
||||
.build()?;
|
||||
|
||||
// Scan the collection
|
||||
collection.scan()?;
|
||||
|
||||
// Add to the collections map
|
||||
self.collections.insert(collection.name.clone(), collection);
|
||||
|
||||
// Return a reference to the added collection
|
||||
self.collections.get(&namefixed).ok_or_else(|| {
|
||||
DocTreeError::CollectionNotFound(namefixed.clone())
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a collection by name
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - Name of the collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The collection or an error
|
||||
pub fn get_collection(&self, name: &str) -> Result<&Collection> {
|
||||
// For compatibility with tests, apply namefix
|
||||
let namefixed = name_fix(name);
|
||||
|
||||
// Check if the collection exists
|
||||
self.collections.get(&namefixed).ok_or_else(|| {
|
||||
DocTreeError::CollectionNotFound(name.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete a collection from the DocTree
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - Name of the collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(()) on success or an error
|
||||
pub fn delete_collection(&mut self, name: &str) -> Result<()> {
|
||||
// For compatibility with tests, apply namefix
|
||||
let namefixed = name_fix(name);
|
||||
|
||||
// Check if the collection exists
|
||||
if !self.collections.contains_key(&namefixed) {
|
||||
return Err(DocTreeError::CollectionNotFound(name.to_string()));
|
||||
}
|
||||
|
||||
// Delete from Redis
|
||||
self.storage.delete_collection(&namefixed)?;
|
||||
|
||||
// Remove from the collections map
|
||||
self.collections.remove(&namefixed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all collections
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of collection names
|
||||
pub fn list_collections(&self) -> Vec<String> {
|
||||
self.collections.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Get a page by name from a specific collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection_name` - Name of the collection (optional)
|
||||
/// * `page_name` - Name of the page
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The page content or an error
|
||||
pub fn page_get(&self, collection_name: Option<&str>, page_name: &str) -> Result<String> {
|
||||
let (collection_name, page_name) = self.resolve_collection_and_page(collection_name, page_name)?;
|
||||
|
||||
// Get the collection
|
||||
let collection = self.get_collection(&collection_name)?;
|
||||
|
||||
// Get the page content
|
||||
let content = collection.page_get(page_name)?;
|
||||
|
||||
// Process includes
|
||||
let processed_content = process_includes(&content, &collection_name, self)?;
|
||||
|
||||
Ok(processed_content)
|
||||
}
|
||||
|
||||
/// Get a page by name from a specific collection and return its HTML content
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection_name` - Name of the collection (optional)
|
||||
/// * `page_name` - Name of the page
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The HTML content or an error
|
||||
pub fn page_get_html(&self, collection_name: Option<&str>, page_name: &str) -> Result<String> {
|
||||
let (collection_name, page_name) = self.resolve_collection_and_page(collection_name, page_name)?;
|
||||
|
||||
// Get the collection
|
||||
let collection = self.get_collection(&collection_name)?;
|
||||
|
||||
// Get the HTML
|
||||
collection.page_get_html(page_name, Some(self))
|
||||
}
|
||||
|
||||
/// Get the URL for a file in a specific collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection_name` - Name of the collection (optional)
|
||||
/// * `file_name` - Name of the file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The URL for the file or an error
|
||||
pub fn file_get_url(&self, collection_name: Option<&str>, file_name: &str) -> Result<String> {
|
||||
let (collection_name, file_name) = self.resolve_collection_and_page(collection_name, file_name)?;
|
||||
|
||||
// Get the collection
|
||||
let collection = self.get_collection(&collection_name)?;
|
||||
|
||||
// Get the URL
|
||||
collection.file_get_url(file_name)
|
||||
}
|
||||
|
||||
/// Get the path to a page in the default collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `page_name` - Name of the page
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The path to the page or an error
|
||||
pub fn page_get_path(&self, page_name: &str) -> Result<String> {
|
||||
// Check if a default collection is set
|
||||
let default_collection = self.default_collection.as_ref().ok_or_else(|| {
|
||||
DocTreeError::NoDefaultCollection
|
||||
})?;
|
||||
|
||||
// Get the collection
|
||||
let collection = self.get_collection(default_collection)?;
|
||||
|
||||
// Get the path
|
||||
collection.page_get_path(page_name)
|
||||
}
|
||||
|
||||
/// Get information about the DocTree
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A map of information
|
||||
pub fn info(&self) -> HashMap<String, String> {
|
||||
let mut info = HashMap::new();
|
||||
info.insert("name".to_string(), self.name.clone());
|
||||
info.insert("path".to_string(), self.path.to_string_lossy().to_string());
|
||||
info.insert("collections".to_string(), self.collections.len().to_string());
|
||||
info
|
||||
}
|
||||
|
||||
/// Scan the default collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(()) on success or an error
|
||||
pub fn scan(&self) -> Result<()> {
|
||||
// Check if a default collection is set
|
||||
let default_collection = self.default_collection.as_ref().ok_or_else(|| {
|
||||
DocTreeError::NoDefaultCollection
|
||||
})?;
|
||||
|
||||
// Get the collection
|
||||
let collection = self.get_collection(default_collection)?;
|
||||
|
||||
// Scan the collection
|
||||
collection.scan()
|
||||
}
|
||||
|
||||
/// Resolve collection and page names
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection_name` - Name of the collection (optional)
|
||||
/// * `page_name` - Name of the page
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple of (collection_name, page_name) or an error
|
||||
fn resolve_collection_and_page<'a>(&self, collection_name: Option<&'a str>, page_name: &'a str) -> Result<(String, &'a str)> {
|
||||
match collection_name {
|
||||
Some(name) => Ok((name_fix(name), page_name)),
|
||||
None => {
|
||||
// Use the default collection
|
||||
let default_collection = self.default_collection.as_ref().ok_or_else(|| {
|
||||
DocTreeError::NoDefaultCollection
|
||||
})?;
|
||||
Ok((default_collection.clone(), page_name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DocTreeBuilder {
|
||||
/// Set the storage backend
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `storage` - Redis storage backend
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Self for method chaining
|
||||
pub fn with_storage(mut self, storage: RedisStorage) -> Self {
|
||||
self.storage = Some(storage);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Base path of the collection
|
||||
/// * `name` - Name of the collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Self for method chaining or an error
|
||||
pub fn with_collection<P: AsRef<Path>>(mut self, path: P, name: &str) -> Result<Self> {
|
||||
// Ensure storage is set
|
||||
let storage = self.storage.as_ref().ok_or_else(|| {
|
||||
DocTreeError::MissingParameter("storage".to_string())
|
||||
})?;
|
||||
|
||||
// Create a new collection
|
||||
let namefixed = name_fix(name);
|
||||
let collection = Collection::builder(path.as_ref(), &namefixed)
|
||||
.with_storage(storage.clone())
|
||||
.build()?;
|
||||
|
||||
// Scan the collection
|
||||
collection.scan()?;
|
||||
|
||||
// Add to the collections map
|
||||
self.collections.insert(collection.name.clone(), collection);
|
||||
|
||||
// For backward compatibility
|
||||
if self.name.is_none() {
|
||||
self.name = Some(namefixed.clone());
|
||||
}
|
||||
if self.path.is_none() {
|
||||
self.path = Some(path.as_ref().to_path_buf());
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Set the default collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - Name of the default collection
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Self for method chaining
|
||||
pub fn with_default_collection(mut self, name: &str) -> Self {
|
||||
self.default_collection = Some(name_fix(name));
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the DocTree
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new DocTree or an error
|
||||
pub fn build(self) -> Result<DocTree> {
|
||||
// Ensure storage is set
|
||||
let storage = self.storage.ok_or_else(|| {
|
||||
DocTreeError::MissingParameter("storage".to_string())
|
||||
})?;
|
||||
|
||||
// Create the DocTree
|
||||
let doctree = DocTree {
|
||||
collections: self.collections,
|
||||
default_collection: self.default_collection,
|
||||
storage: storage.clone(),
|
||||
name: self.name.unwrap_or_default(),
|
||||
path: self.path.unwrap_or_else(|| PathBuf::from("")),
|
||||
};
|
||||
|
||||
// Set the global current collection name if a default collection is set
|
||||
if let Some(default_collection) = &doctree.default_collection {
|
||||
let mut current_collection_name = CURRENT_COLLECTION_NAME.lock().unwrap();
|
||||
*current_collection_name = Some(default_collection.clone());
|
||||
}
|
||||
|
||||
Ok(doctree)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// Create a new DocTree instance
|
||||
///
|
||||
/// For backward compatibility, it also accepts path and name parameters
|
||||
/// to create a DocTree with a single collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - Optional path and name for backward compatibility
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new DocTree or an error
|
||||
pub fn new<P: AsRef<Path>>(args: &[&str]) -> Result<DocTree> {
|
||||
let storage = RedisStorage::new("redis://localhost:6379")?;
|
||||
|
||||
let mut builder = DocTree::builder().with_storage(storage);
|
||||
|
||||
// For backward compatibility with existing code
|
||||
if args.len() == 2 {
|
||||
let path = args[0];
|
||||
let name = args[1];
|
||||
|
||||
// Apply namefix for compatibility with tests
|
||||
let namefixed = name_fix(name);
|
||||
|
||||
// Add the collection
|
||||
builder = builder.with_collection(path, &namefixed)?;
|
||||
|
||||
// Set the default collection
|
||||
builder = builder.with_default_collection(&namefixed);
|
||||
}
|
||||
|
||||
builder.build()
|
||||
}
|
44
doctree/src/error.rs
Normal file
44
doctree/src/error.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use thiserror::Error;
|
||||
|
||||
/// Custom error type for the doctree library
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DocTreeError {
|
||||
/// IO error
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
/// WalkDir error
|
||||
#[error("WalkDir error: {0}")]
|
||||
WalkDirError(String),
|
||||
|
||||
/// Collection not found
|
||||
#[error("Collection not found: {0}")]
|
||||
CollectionNotFound(String),
|
||||
|
||||
/// Page not found
|
||||
#[error("Page not found: {0}")]
|
||||
PageNotFound(String),
|
||||
|
||||
/// File not found
|
||||
#[error("File not found: {0}")]
|
||||
FileNotFound(String),
|
||||
|
||||
/// Invalid include directive
|
||||
#[error("Invalid include directive: {0}")]
|
||||
InvalidIncludeDirective(String),
|
||||
|
||||
/// No default collection set
|
||||
#[error("No default collection set")]
|
||||
NoDefaultCollection,
|
||||
|
||||
/// Invalid number of arguments
|
||||
#[error("Invalid number of arguments")]
|
||||
InvalidArgumentCount,
|
||||
|
||||
/// Missing required parameter
|
||||
#[error("Missing required parameter: {0}")]
|
||||
MissingParameter(String),
|
||||
}
|
||||
|
||||
/// Result type alias for doctree operations
|
||||
pub type Result<T> = std::result::Result<T, DocTreeError>;
|
178
doctree/src/include.rs
Normal file
178
doctree/src/include.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use crate::doctree::DocTree;
|
||||
use crate::error::{DocTreeError, Result};
|
||||
use crate::utils::trim_spaces_and_quotes;
|
||||
|
||||
/// Process includes in markdown content
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `content` - The markdown content to process
|
||||
/// * `current_collection_name` - The name of the current collection
|
||||
/// * `doctree` - The DocTree instance
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The processed content or an error
|
||||
pub fn process_includes(content: &str, current_collection_name: &str, doctree: &DocTree) -> Result<String> {
|
||||
// Find all include directives
|
||||
let lines: Vec<&str> = content.split('\n').collect();
|
||||
let mut result = Vec::with_capacity(lines.len());
|
||||
|
||||
for line in lines {
|
||||
match parse_include_line(line) {
|
||||
Ok((Some(c), Some(p))) => {
|
||||
// Both collection and page specified
|
||||
match handle_include(&p, &c, doctree) {
|
||||
Ok(include_content) => {
|
||||
// Process any nested includes in the included content
|
||||
match process_includes(&include_content, &c, doctree) {
|
||||
Ok(processed_include_content) => {
|
||||
result.push(processed_include_content);
|
||||
},
|
||||
Err(e) => {
|
||||
result.push(format!(">>ERROR: Failed to process nested includes: {}", e));
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
result.push(format!(">>ERROR: {}", e));
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok((Some(_), None)) => {
|
||||
// Invalid case: collection specified but no page
|
||||
result.push(format!(">>ERROR: Invalid include directive: collection specified but no page name"));
|
||||
},
|
||||
Ok((None, Some(p))) => {
|
||||
// Only page specified, use current collection
|
||||
match handle_include(&p, current_collection_name, doctree) {
|
||||
Ok(include_content) => {
|
||||
// Process any nested includes in the included content
|
||||
match process_includes(&include_content, current_collection_name, doctree) {
|
||||
Ok(processed_include_content) => {
|
||||
result.push(processed_include_content);
|
||||
},
|
||||
Err(e) => {
|
||||
result.push(format!(">>ERROR: Failed to process nested includes: {}", e));
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
result.push(format!(">>ERROR: {}", e));
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok((None, None)) => {
|
||||
// Not an include directive, keep the line
|
||||
result.push(line.to_string());
|
||||
},
|
||||
Err(e) => {
|
||||
// Error parsing include directive
|
||||
result.push(format!(">>ERROR: Failed to process include directive: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result.join("\n"))
|
||||
}
|
||||
|
||||
/// Parse an include directive line
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `line` - The line to parse
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple of (collection_name, page_name) or an error
|
||||
///
|
||||
/// Supports:
|
||||
/// - !!include collectionname:'pagename'
|
||||
/// - !!include collectionname:'pagename.md'
|
||||
/// - !!include 'pagename'
|
||||
/// - !!include collectionname:pagename
|
||||
/// - !!include collectionname:pagename.md
|
||||
/// - !!include name:'pagename'
|
||||
/// - !!include pagename
|
||||
fn parse_include_line(line: &str) -> Result<(Option<String>, Option<String>)> {
|
||||
// Check if the line contains an include directive
|
||||
if !line.contains("!!include") {
|
||||
return Ok((None, None));
|
||||
}
|
||||
|
||||
// Extract the part after !!include
|
||||
let parts: Vec<&str> = line.splitn(2, "!!include").collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(DocTreeError::InvalidIncludeDirective(line.to_string()));
|
||||
}
|
||||
|
||||
// Trim spaces and check if the include part is empty
|
||||
let include_text = trim_spaces_and_quotes(parts[1]);
|
||||
if include_text.is_empty() {
|
||||
return Err(DocTreeError::InvalidIncludeDirective(line.to_string()));
|
||||
}
|
||||
|
||||
// Remove name: prefix if present
|
||||
let include_text = if include_text.starts_with("name:") {
|
||||
let text = include_text.trim_start_matches("name:").trim();
|
||||
if text.is_empty() {
|
||||
return Err(DocTreeError::InvalidIncludeDirective(
|
||||
format!("empty page name after 'name:' prefix: {}", line)
|
||||
));
|
||||
}
|
||||
text.to_string()
|
||||
} else {
|
||||
include_text
|
||||
};
|
||||
|
||||
// Check if it contains a collection reference (has a colon)
|
||||
if include_text.contains(':') {
|
||||
let parts: Vec<&str> = include_text.splitn(2, ':').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(DocTreeError::InvalidIncludeDirective(
|
||||
format!("malformed collection reference: {}", include_text)
|
||||
));
|
||||
}
|
||||
|
||||
let collection_name = parts[0].trim();
|
||||
let page_name = trim_spaces_and_quotes(parts[1]);
|
||||
|
||||
if collection_name.is_empty() {
|
||||
return Err(DocTreeError::InvalidIncludeDirective(
|
||||
format!("empty collection name in include directive: {}", line)
|
||||
));
|
||||
}
|
||||
|
||||
if page_name.is_empty() {
|
||||
return Err(DocTreeError::InvalidIncludeDirective(
|
||||
format!("empty page name in include directive: {}", line)
|
||||
));
|
||||
}
|
||||
|
||||
Ok((Some(collection_name.to_string()), Some(page_name)))
|
||||
} else {
|
||||
// No collection specified, just a page name
|
||||
Ok((None, Some(include_text)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an include directive
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `page_name` - The name of the page to include
|
||||
/// * `collection_name` - The name of the collection
|
||||
/// * `doctree` - The DocTree instance
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The included content or an error
|
||||
fn handle_include(page_name: &str, collection_name: &str, doctree: &DocTree) -> Result<String> {
|
||||
// Get the collection
|
||||
let collection = doctree.get_collection(collection_name)?;
|
||||
|
||||
// Get the page content
|
||||
let content = collection.page_get(page_name)?;
|
||||
|
||||
Ok(content)
|
||||
}
|
@@ -1,14 +1,41 @@
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
//! DocTree is a library for managing collections of markdown documents.
|
||||
//!
|
||||
//! It provides functionality for scanning directories, managing collections,
|
||||
//! and processing includes between documents.
|
||||
|
||||
// Import lazy_static
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
mod error;
|
||||
mod storage;
|
||||
mod utils;
|
||||
mod collection;
|
||||
mod doctree;
|
||||
mod include;
|
||||
|
||||
pub use error::{DocTreeError, Result};
|
||||
pub use storage::RedisStorage;
|
||||
pub use collection::{Collection, CollectionBuilder};
|
||||
pub use doctree::{DocTree, DocTreeBuilder, new};
|
||||
pub use include::process_includes;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
fn test_doctree_builder() {
|
||||
// Create a storage instance
|
||||
let storage = RedisStorage::new("dummy_url").unwrap();
|
||||
|
||||
let doctree = DocTree::builder()
|
||||
.with_storage(storage)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(doctree.collections.len(), 0);
|
||||
assert_eq!(doctree.default_collection, None);
|
||||
}
|
||||
}
|
||||
|
169
doctree/src/storage.rs
Normal file
169
doctree/src/storage.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use crate::error::{DocTreeError, Result};
|
||||
|
||||
/// Storage backend for doctree
|
||||
pub struct RedisStorage {
|
||||
// Using a simple in-memory storage for demonstration
|
||||
// In a real implementation, this would be a Redis client
|
||||
collections: Arc<Mutex<HashMap<String, HashMap<String, String>>>>,
|
||||
}
|
||||
|
||||
impl RedisStorage {
|
||||
/// Create a new RedisStorage instance
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - Redis connection URL (e.g., "redis://localhost:6379")
|
||||
/// This is ignored in the in-memory implementation
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new RedisStorage instance or an error
|
||||
pub fn new(_url: &str) -> Result<Self> {
|
||||
Ok(Self {
|
||||
collections: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
/// Store a collection entry
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection` - Collection name
|
||||
/// * `key` - Entry key
|
||||
/// * `value` - Entry value
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(()) on success or an error
|
||||
pub fn store_collection_entry(&self, collection: &str, key: &str, value: &str) -> Result<()> {
|
||||
let mut collections = self.collections.lock().unwrap();
|
||||
|
||||
// Get or create the collection
|
||||
let collection_entries = collections
|
||||
.entry(format!("collections:{}", collection))
|
||||
.or_insert_with(HashMap::new);
|
||||
|
||||
// Store the entry
|
||||
collection_entries.insert(key.to_string(), value.to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a collection entry
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection` - Collection name
|
||||
/// * `key` - Entry key
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The entry value or an error
|
||||
pub fn get_collection_entry(&self, collection: &str, key: &str) -> Result<String> {
|
||||
let collections = self.collections.lock().unwrap();
|
||||
|
||||
// Get the collection
|
||||
let collection_key = format!("collections:{}", collection);
|
||||
let collection_entries = collections.get(&collection_key)
|
||||
.ok_or_else(|| DocTreeError::CollectionNotFound(collection.to_string()))?;
|
||||
|
||||
// Get the entry
|
||||
collection_entries.get(key)
|
||||
.cloned()
|
||||
.ok_or_else(|| DocTreeError::FileNotFound(key.to_string()))
|
||||
}
|
||||
|
||||
/// Delete a collection entry
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection` - Collection name
|
||||
/// * `key` - Entry key
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(()) on success or an error
|
||||
pub fn delete_collection_entry(&self, collection: &str, key: &str) -> Result<()> {
|
||||
let mut collections = self.collections.lock().unwrap();
|
||||
|
||||
// Get the collection
|
||||
let collection_key = format!("collections:{}", collection);
|
||||
let collection_entries = collections.get_mut(&collection_key)
|
||||
.ok_or_else(|| DocTreeError::CollectionNotFound(collection.to_string()))?;
|
||||
|
||||
// Remove the entry
|
||||
collection_entries.remove(key);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all entries in a collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection` - Collection name
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of entry keys or an error
|
||||
pub fn list_collection_entries(&self, collection: &str) -> Result<Vec<String>> {
|
||||
let collections = self.collections.lock().unwrap();
|
||||
|
||||
// Get the collection
|
||||
let collection_key = format!("collections:{}", collection);
|
||||
let collection_entries = collections.get(&collection_key)
|
||||
.ok_or_else(|| DocTreeError::CollectionNotFound(collection.to_string()))?;
|
||||
|
||||
// Get the keys
|
||||
let keys = collection_entries.keys().cloned().collect();
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Delete a collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection` - Collection name
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Ok(()) on success or an error
|
||||
pub fn delete_collection(&self, collection: &str) -> Result<()> {
|
||||
let mut collections = self.collections.lock().unwrap();
|
||||
|
||||
// Remove the collection
|
||||
collections.remove(&format!("collections:{}", collection));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a collection exists
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection` - Collection name
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// true if the collection exists, false otherwise
|
||||
pub fn collection_exists(&self, collection: &str) -> Result<bool> {
|
||||
let collections = self.collections.lock().unwrap();
|
||||
|
||||
// Check if the collection exists
|
||||
let exists = collections.contains_key(&format!("collections:{}", collection));
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Clone for RedisStorage
|
||||
impl Clone for RedisStorage {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
collections: Arc::clone(&self.collections),
|
||||
}
|
||||
}
|
||||
}
|
106
doctree/src/utils.rs
Normal file
106
doctree/src/utils.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use pulldown_cmark::{Parser, Options, html};
|
||||
use std::path::Path;
|
||||
|
||||
/// Fix a name to be used as a key
|
||||
///
|
||||
/// This is equivalent to the tools.NameFix function in the Go implementation.
|
||||
/// It normalizes the name by converting to lowercase, replacing spaces with hyphens, etc.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name to fix
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The fixed name
|
||||
pub fn name_fix(name: &str) -> String {
|
||||
// Convert to lowercase
|
||||
let mut result = name.to_lowercase();
|
||||
|
||||
// Replace spaces with hyphens
|
||||
result = result.replace(' ', "-");
|
||||
|
||||
// Remove special characters
|
||||
result = result.chars()
|
||||
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '.')
|
||||
.collect();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Convert markdown to HTML
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `markdown` - The markdown content to convert
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The HTML content
|
||||
pub fn markdown_to_html(markdown: &str) -> String {
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_TABLES);
|
||||
options.insert(Options::ENABLE_FOOTNOTES);
|
||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
|
||||
let parser = Parser::new_ext(markdown, options);
|
||||
let mut html_output = String::new();
|
||||
html::push_html(&mut html_output, parser);
|
||||
|
||||
html_output
|
||||
}
|
||||
|
||||
/// Trim spaces and quotes from a string
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `s` - The string to trim
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The trimmed string
|
||||
pub fn trim_spaces_and_quotes(s: &str) -> String {
|
||||
let mut result = s.trim().to_string();
|
||||
|
||||
// Remove surrounding quotes
|
||||
if (result.starts_with('\'') && result.ends_with('\'')) ||
|
||||
(result.starts_with('"') && result.ends_with('"')) {
|
||||
result = result[1..result.len()-1].to_string();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Ensure a string has a .md extension
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name to check
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The name with a .md extension
|
||||
pub fn ensure_md_extension(name: &str) -> String {
|
||||
if !name.ends_with(".md") {
|
||||
format!("{}.md", name)
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the file extension from a path
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - The path to check
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The file extension or an empty string
|
||||
pub fn get_extension(path: &str) -> String {
|
||||
Path::new(path)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
}
|
Reference in New Issue
Block a user