use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Section {
pub name: String,
pub level: u8,
pub content: String,
pub code_blocks: Vec<CodeBlock>,
#[serde(default)]
pub start_line: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeBlock {
pub language: Option<String>,
pub code: String,
}
#[derive(Debug, thiserror::Error)]
pub enum ParserError {
#[error("Failed to parse content: {0}")]
ParseError(String),
}
#[derive(Debug, thiserror::Error)]
pub enum FileSystemError {
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Path not found: {0}")]
PathNotFound(PathBuf),
}
pub trait MarkdownParser: Send + Sync {
fn parse_sections(&self, content: &str) -> Result<Vec<Section>, ParserError>;
}
pub trait PatternMatcher: Send + Sync {
fn find_matches(&self, pattern: &str, text: &str) -> Vec<PatternMatch>;
fn compile(&self, pattern: &str) -> Result<CompiledPattern, PatternError>;
fn is_match(&self, pattern: &str, text: &str) -> bool {
!self.find_matches(pattern, text).is_empty()
}
fn captures_iter(&self, pattern: &str, text: &str) -> Vec<Captures>;
}
#[derive(Debug, Clone)]
pub struct PatternMatch {
pub start: usize,
pub end: usize,
pub matched_text: String,
}
#[derive(Debug, Clone)]
pub struct Captures {
groups: Vec<Option<PatternMatch>>,
}
impl Captures {
#[must_use]
pub fn new(groups: Vec<Option<PatternMatch>>) -> Self {
Self { groups }
}
#[must_use]
pub fn get(&self, idx: usize) -> Option<&PatternMatch> {
self.groups.get(idx).and_then(Option::as_ref)
}
#[must_use]
pub fn len(&self) -> usize {
self.groups.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.groups.is_empty()
}
}
type FindFn = Box<dyn Fn(&str) -> Vec<PatternMatch> + Send + Sync>;
type IsMatchFn = Box<dyn Fn(&str) -> bool + Send + Sync>;
type CapturesFn = Box<dyn Fn(&str) -> Vec<Captures> + Send + Sync>;
pub struct CompiledPattern {
find: FindFn,
is_match: IsMatchFn,
captures: CapturesFn,
}
impl CompiledPattern {
#[must_use]
pub fn new(find: FindFn, is_match: IsMatchFn, captures: CapturesFn) -> Self {
Self {
find,
is_match,
captures,
}
}
pub fn find_matches(&self, text: &str) -> Vec<PatternMatch> {
(self.find)(text)
}
pub fn is_match(&self, text: &str) -> bool {
(self.is_match)(text)
}
pub fn captures_iter(&self, text: &str) -> Vec<Captures> {
(self.captures)(text)
}
}
#[derive(Debug, thiserror::Error)]
pub enum PatternError {
#[error("Invalid pattern: {0}")]
InvalidPattern(String),
}
#[derive(Debug, Clone)]
pub struct FileContent {
bytes: Vec<u8>,
}
impl FileContent {
#[must_use]
pub fn new(bytes: Vec<u8>) -> Self {
Self { bytes }
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[must_use]
pub fn decode_utf8_lossy(&self) -> DecodedText {
let decode_warning = std::str::from_utf8(&self.bytes).is_err();
DecodedText {
text: String::from_utf8_lossy(&self.bytes).into_owned(),
decode_warning,
}
}
}
#[derive(Debug, Clone)]
pub struct DecodedText {
pub text: String,
pub decode_warning: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct FileMeta {
pub len: u64,
}
pub trait FileSystemProvider: Send + Sync {
fn read_file_bytes(&self, path: &Path) -> Result<FileContent, FileSystemError>;
fn list_files(
&self,
path: &Path,
pattern: &str,
recursive: bool,
) -> Result<Vec<PathBuf>, FileSystemError>;
fn exists(&self, path: &Path) -> bool;
fn metadata(&self, path: &Path) -> Result<FileMeta, FileSystemError> {
let bytes = self.read_file_bytes(path)?;
Ok(FileMeta {
len: bytes.as_bytes().len() as u64,
})
}
fn is_file(&self, path: &Path) -> bool {
self.read_file_bytes(path).is_ok()
}
fn is_dir(&self, path: &Path) -> bool {
self.exists(path) && !self.is_file(path)
}
fn walk_files(
&self,
path: &Path,
_max_depth: usize,
_skip_dirs: &[&str],
) -> Result<Vec<PathBuf>, FileSystemError> {
self.list_files(path, "*", true)
}
}