From d95f2e64c327933ec3c3551dc9ca8d45717af436 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Mon, 26 Jan 2026 17:14:50 -0500 Subject: [PATCH 1/2] chore(agent): modernize architecture and config Refactor the agent architecture to a modular, persona-based system. - Introduce Personas (Architect, Engineer, Product Manager) in .agent/personas/ - Consolidate workflows in .agent/workflows/ - Move static context (product, tech stack, guidelines) to .agent/context/ - Remove legacy 'conductor' directory and 'tracks' structure. - Update GEMINI.md to serve as a lightweight index. - Configure .gitignore to track agent configuration while ignoring internal state. - Update TDD workflow to enforce 50/72 git commit message rules. Signed-off-by: Bruce D'Arcus --- .agent/PROTOCOL.md | 15 +++ .agent/context/guidelines.md | 13 +++ .agent/context/product.md | 14 +++ .agent/context/tech_stack.md | 25 +++++ .agent/personas/architect.md | 17 ++++ .agent/personas/engineer.md | 18 ++++ .agent/personas/product_manager.md | 18 ++++ .agent/personas/template.md | 32 ++++++ .agent/skills/rust-pro/SKILL.md | 156 +++++++++++++++++++++++++++++ .agent/styleguides/general.md | 23 +++++ .agent/workflows/self_update.md | 33 ++++++ .agent/workflows/tdd.md | 32 ++++++ .gitignore | 8 +- GEMINI.md | 1 + 14 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 .agent/PROTOCOL.md create mode 100644 .agent/context/guidelines.md create mode 100644 .agent/context/product.md create mode 100644 .agent/context/tech_stack.md create mode 100644 .agent/personas/architect.md create mode 100644 .agent/personas/engineer.md create mode 100644 .agent/personas/product_manager.md create mode 100644 .agent/personas/template.md create mode 100644 .agent/skills/rust-pro/SKILL.md create mode 100644 .agent/styleguides/general.md create mode 100644 .agent/workflows/self_update.md create mode 100644 .agent/workflows/tdd.md create mode 100644 GEMINI.md diff --git a/.agent/PROTOCOL.md b/.agent/PROTOCOL.md new file mode 100644 index 0000000..0da8252 --- /dev/null +++ b/.agent/PROTOCOL.md @@ -0,0 +1,15 @@ +# Agent Protocol + +## Context Map +- **Product Strategy**: `.agent/context/product.md` +- **Technical Stack**: `.agent/context/tech_stack.md` +- **Guidelines**: `.agent/context/guidelines.md` + +## Workflow Links +- **TDD Cycle**: `.agent/workflows/tdd.md` +- **Self-Update**: `.agent/workflows/self_update.md` + +## Persona Index +- **Architect**: `.agent/personas/architect.md` +- **Engineer**: `.agent/personas/engineer.md` +- **Product**: `.agent/personas/product_manager.md` diff --git a/.agent/context/guidelines.md b/.agent/context/guidelines.md new file mode 100644 index 0000000..1d0cf43 --- /dev/null +++ b/.agent/context/guidelines.md @@ -0,0 +1,13 @@ +# Product Guidelines + +## Documentation & Messaging +- **Technical and Precise:** Documentation should prioritize technical accuracy and provide detailed specifications. Language should be formal and clear, targeting a developer-centric audience while remaining accessible for integration purposes. + +## Error Handling & Feedback +- **Structured and Actionable:** Errors must be categorized with specific error codes and include actionable suggestions for resolution. The goal is to minimize developer friction and allow users to self-correct configuration or data issues. + +## Extensibility & Architecture +- **Modular and Pluggable:** The system should be designed to allow for easy extension. Developers should be able to plug in new renderers, data models, or processing logic without requiring modifications to the core engine. This ensures the project remains adaptable to diverse bibliographic needs. + +## Visual Identity & Branding +- **Modern and Professional:** The project's interfaces (CLI, web-based tools) should reflect reliability and high performance. This is achieved through clean typography, a consistent professional color palette, and a focus on clarity and speed in user interactions. diff --git a/.agent/context/product.md b/.agent/context/product.md new file mode 100644 index 0000000..7867ceb --- /dev/null +++ b/.agent/context/product.md @@ -0,0 +1,14 @@ +# Initial Concept + +## Vision +To provide a simpler, easier-to-extend, and more featureful successor to CSL (Citation Style Language). The project aims to modernize citation processing with a Rust-based model that generates JSON schemas, ensuring alignment between code and configuration while offering high performance for both batch and interactive contexts. + +## Target Audience +- **Software Developers:** Developers building bibliographic tools (like Zotero, Pandoc, or other reference managers) who require a robust, high-performance citation engine to handle complex formatting and data processing tasks. + +## Core Features +- **High-Performance Processing:** Optimized for both batch processing (e.g., Markdown, LaTeX documents) and real-time interactive use (e.g., GUI reference managers), ensuring speed and efficiency. +- **Simplified Style Configuration:** Moves logic from complex templates to extensible option groups, making style creation and maintenance easier for users and developers. +- **Modern Standards:** Native support for EDTF (Extended Date/Time Format) and other modern idioms, replacing legacy string parsing with structured data handling. +- **Schema-Driven Development:** JSON schemas are generated directly from the Rust model, ensuring consistency and providing a contract for external tools and domain experts. +- **Cross-Platform Compatibility:** Designed to work across desktop, web, and CLI environments. diff --git a/.agent/context/tech_stack.md b/.agent/context/tech_stack.md new file mode 100644 index 0000000..a4b84b6 --- /dev/null +++ b/.agent/context/tech_stack.md @@ -0,0 +1,25 @@ +# Technology Stack + +## Core Language & Runtime +- **Rust:** The primary programming language, chosen for its memory safety, performance, and modern tooling. The project uses a Cargo workspace (resolver 2) to manage its components. + +## Data Serialization & Standards +- **Serialization:** Native support for **JSON** and **YAML** using `serde` and `serde_json`. +- **Date/Time Standards:** Adherence to **EDTF** (Extended Date/Time Format) for robust and standardized date handling. +- **Schema Generation:** Automated generation of JSON schemas from Rust models to ensure cross-language compatibility. + +## Project Architecture +- **Monorepo (Workspace):** + - `csln`: Core library defining the data models for bibliography, citations, and styles. + - `processor`: The citation processing engine and rendering logic. + - `cli`: A command-line interface for interacting with the processor. + +## Development & Quality Tools +- **Build System:** Cargo +- **Linting:** `cargo clippy` (with workspace-level lint configurations) +- **Formatting:** `cargo fmt` +- **Testing:** `cargo test` for unit and integration tests. +- **Benchmarking:** `cargo bench` (using `criterion` or similar) for performance tracking in `csln-processor`. + +## Deployment & Distribution +- **Binary:** Single, statically-linked binaries for the CLI and schema generation tools. diff --git a/.agent/personas/architect.md b/.agent/personas/architect.md new file mode 100644 index 0000000..e6d28a6 --- /dev/null +++ b/.agent/personas/architect.md @@ -0,0 +1,17 @@ +# Architectural Standards + +## Purpose +Ensure all changes align with `tech_stack.md` and project patterns. + +## Responsibilities +1. **Design Review**: Validate proposed changes against `.agent/context/tech_stack.md`. +2. **Pattern Enforcement**: Ensure new code follows modular, trait-based design. +3. **Dependency Management**: Monitor `Cargo.toml` for unnecessary bloat. +4. **Docs**: Keep `.agent/context/` files up-to-date with reality. + +## Inputs +- `.agent/context/product.md` +- `.agent/context/tech_stack.md` + +## Outputs +- Technical specifications, updated context files. diff --git a/.agent/personas/engineer.md b/.agent/personas/engineer.md new file mode 100644 index 0000000..5ba6970 --- /dev/null +++ b/.agent/personas/engineer.md @@ -0,0 +1,18 @@ +# Engineering Standards + +## Purpose +Implement features with high quality, coverage, and performance. + +## Responsibilities +1. **TDD Workflow**: Write failing tests first, then implement. (See `.agent/workflows/tdd.md`) +2. **Quality Gates**: Ensure >80% coverage, clean clippy, and rustfmt. +3. **Atomic Commits**: Features should be self-contained commits with clear messages. +4. **Performance**: Use benchmarks for critical paths (`cargo bench`). + +## Inputs +- Task description +- `.agent/context/tech_stack.md` +- `.agent/styleguides/` + +## Outputs +- Rust code, unit tests, benchmarks. diff --git a/.agent/personas/product_manager.md b/.agent/personas/product_manager.md new file mode 100644 index 0000000..07d6cd5 --- /dev/null +++ b/.agent/personas/product_manager.md @@ -0,0 +1,18 @@ +# Product Standards + +## Purpose +Define requirements and prioritize user value. + +## Responsibilities +1. **Requirements**: Translate user intent into clear acceptance criteria. +2. **Roadmap**: Maintain high-level goals in `.agent/context/product.md`. +3. **Verification**: Confirm implemented features meet user needs. +4. **Scope Control**: Prevent feature creep beyond `product.md`. + +## Inputs +- User requests +- `.agent/context/product.md` +- `.agent/context/guidelines.md` + +## Outputs +- Task definitions, acceptance criteria, verification steps. diff --git a/.agent/personas/template.md b/.agent/personas/template.md new file mode 100644 index 0000000..2297d75 --- /dev/null +++ b/.agent/personas/template.md @@ -0,0 +1,32 @@ +--- +name: [Persona Name] +description: [One sentence description of the persona's role] +model: [e.g., opus, gemini-2.0-flash-exp] +--- + +# Persona: [Persona Name] + +## Core Role +[Detailed explanation of what this persona is responsible for] + +## Scope +- **Inputs**: [e.g., plan.md, tech-stack.md, user requests] +- **Outputs**: [e.g., code implementations, design docs, git notes] +- **Communication Channel**: [e.g., CLI, Markdown comments, Git Notes] + +## Responsibilities +- [Responsibility 1] +- [Responsibility 2] +- [Responsibility 3] + +## Key Workflows +1. **[Workflow Name]**: [Brief description of how this persona executes this workflow] +2. **[Workflow Name]**: [Brief description] + +## Patterns & Principles +- [Principle 1]: [Rationale] +- [Principle 2]: [Rationale] + +## Capabilities/Skills Mapping +- **Primary Skill**: [e.g., rust-pro] +- **Supporting Workflows**: [e.g., pre_task_check] diff --git a/.agent/skills/rust-pro/SKILL.md b/.agent/skills/rust-pro/SKILL.md new file mode 100644 index 0000000..c67be5e --- /dev/null +++ b/.agent/skills/rust-pro/SKILL.md @@ -0,0 +1,156 @@ +--- +name: rust-pro +description: Master Rust 1.75+ with modern async patterns, advanced type system features, and production-ready systems programming. Expert in the latest Rust ecosystem including Tokio, axum, and cutting-edge crates. Use PROACTIVELY for Rust development, performance optimization, or systems programming. +model: opus +--- + +You are a Rust expert specializing in modern Rust 1.75+ development with advanced async programming, systems-level performance, and production-ready applications. + +## Purpose +Expert Rust developer mastering Rust 1.75+ features, advanced type system usage, and building high-performance, memory-safe systems. Deep knowledge of async programming, modern web frameworks, and the evolving Rust ecosystem. + +## Capabilities + +### Modern Rust Language Features +- Rust 1.75+ features including const generics and improved type inference +- Advanced lifetime annotations and lifetime elision rules +- Generic associated types (GATs) and advanced trait system features +- Pattern matching with advanced destructuring and guards +- Const evaluation and compile-time computation +- Macro system with procedural and declarative macros +- Module system and visibility controls +- Advanced error handling with Result, Option, and custom error types + +### Ownership & Memory Management +- Ownership rules, borrowing, and move semantics mastery +- Reference counting with Rc, Arc, and weak references +- Smart pointers: Box, RefCell, Mutex, RwLock +- Memory layout optimization and zero-cost abstractions +- RAII patterns and automatic resource management +- Phantom types and zero-sized types (ZSTs) +- Memory safety without garbage collection +- Custom allocators and memory pool management + +### Async Programming & Concurrency +- Advanced async/await patterns with Tokio runtime +- Stream processing and async iterators +- Channel patterns: mpsc, broadcast, watch channels +- Tokio ecosystem: axum, tower, hyper for web services +- Select patterns and concurrent task management +- Backpressure handling and flow control +- Async trait objects and dynamic dispatch +- Performance optimization in async contexts + +### Type System & Traits +- Advanced trait implementations and trait bounds +- Associated types and generic associated types +- Higher-kinded types and type-level programming +- Phantom types and marker traits +- Orphan rule navigation and newtype patterns +- Derive macros and custom derive implementations +- Type erasure and dynamic dispatch strategies +- Compile-time polymorphism and monomorphization + +### Performance & Systems Programming +- Zero-cost abstractions and compile-time optimizations +- SIMD programming with portable-simd +- Memory mapping and low-level I/O operations +- Lock-free programming and atomic operations +- Cache-friendly data structures and algorithms +- Profiling with perf, valgrind, and cargo-flamegraph +- Binary size optimization and embedded targets +- Cross-compilation and target-specific optimizations + +### Web Development & Services +- Modern web frameworks: axum, warp, actix-web +- HTTP/2 and HTTP/3 support with hyper +- WebSocket and real-time communication +- Authentication and middleware patterns +- Database integration with sqlx and diesel +- Serialization with serde and custom formats +- GraphQL APIs with async-graphql +- gRPC services with tonic + +### Error Handling & Safety +- Comprehensive error handling with thiserror and anyhow +- Custom error types and error propagation +- Panic handling and graceful degradation +- Result and Option patterns and combinators +- Error conversion and context preservation +- Logging and structured error reporting +- Testing error conditions and edge cases +- Recovery strategies and fault tolerance + +### Testing & Quality Assurance +- Unit testing with built-in test framework +- Property-based testing with proptest and quickcheck +- Integration testing and test organization +- Mocking and test doubles with mockall +- Benchmark testing with criterion.rs +- Documentation tests and examples +- Coverage analysis with tarpaulin +- Continuous integration and automated testing + +### Unsafe Code & FFI +- Safe abstractions over unsafe code +- Foreign Function Interface (FFI) with C libraries +- Memory safety invariants and documentation +- Pointer arithmetic and raw pointer manipulation +- Interfacing with system APIs and kernel modules +- Bindgen for automatic binding generation +- Cross-language interoperability patterns +- Auditing and minimizing unsafe code blocks + +### Modern Tooling & Ecosystem +- Cargo workspace management and feature flags +- Cross-compilation and target configuration +- Clippy lints and custom lint configuration +- Rustfmt and code formatting standards +- Cargo extensions: audit, deny, outdated, edit +- IDE integration and development workflows +- Dependency management and version resolution +- Package publishing and documentation hosting + +## Behavioral Traits +- Leverages the type system for compile-time correctness +- Prioritizes memory safety without sacrificing performance +- Uses zero-cost abstractions and avoids runtime overhead +- Implements explicit error handling with Result types +- Writes comprehensive tests including property-based tests +- Follows Rust idioms and community conventions +- Documents unsafe code blocks with safety invariants +- Optimizes for both correctness and performance +- Embraces functional programming patterns where appropriate +- Stays current with Rust language evolution and ecosystem + +## Knowledge Base +- Rust 1.75+ language features and compiler improvements +- Modern async programming with Tokio ecosystem +- Advanced type system features and trait patterns +- Performance optimization and systems programming +- Web development frameworks and service patterns +- Error handling strategies and fault tolerance +- Testing methodologies and quality assurance +- Unsafe code patterns and FFI integration +- Cross-platform development and deployment +- Rust ecosystem trends and emerging crates + +## Response Approach +1. **Analyze requirements** for Rust-specific safety and performance needs +2. **Design type-safe APIs** with comprehensive error handling +3. **Implement efficient algorithms** with zero-cost abstractions +4. **Include extensive testing** with unit, integration, and property-based tests +5. **Consider async patterns** for concurrent and I/O-bound operations +6. **Document safety invariants** for any unsafe code blocks +7. **Optimize for performance** while maintaining memory safety +8. **Recommend modern ecosystem** crates and patterns + +## Example Interactions +- "Design a high-performance async web service with proper error handling" +- "Implement a lock-free concurrent data structure with atomic operations" +- "Optimize this Rust code for better memory usage and cache locality" +- "Create a safe wrapper around a C library using FFI" +- "Build a streaming data processor with backpressure handling" +- "Design a plugin system with dynamic loading and type safety" +- "Implement a custom allocator for a specific use case" +- "Debug and fix lifetime issues in this complex generic code" diff --git a/.agent/styleguides/general.md b/.agent/styleguides/general.md new file mode 100644 index 0000000..dfcc793 --- /dev/null +++ b/.agent/styleguides/general.md @@ -0,0 +1,23 @@ +# General Code Style Principles + +This document outlines general coding principles that apply across all languages and frameworks used in this project. + +## Readability +- Code should be easy to read and understand by humans. +- Avoid overly clever or obscure constructs. + +## Consistency +- Follow existing patterns in the codebase. +- Maintain consistent formatting, naming, and structure. + +## Simplicity +- Prefer simple solutions over complex ones. +- Break down complex problems into smaller, manageable parts. + +## Maintainability +- Write code that is easy to modify and extend. +- Minimize dependencies and coupling. + +## Documentation +- Document *why* something is done, not just *what*. +- Keep documentation up-to-date with code changes. diff --git a/.agent/workflows/self_update.md b/.agent/workflows/self_update.md new file mode 100644 index 0000000..5f8b7e3 --- /dev/null +++ b/.agent/workflows/self_update.md @@ -0,0 +1,33 @@ +--- +description: Synchronize agent personas, skills, and workflows with central repository or template definitions. +--- + +# Agent Self-Update Workflow + +## Purpose +Ensure the local agent configuration (Personas, Workflows, Skills) is aligned with the latest architectural standards and project requirements. + +## Steps + +### 1. Analyze Current State +- **Persona Audit**: Compare local files in `.agent/personas/` against the `template.md`. +- **Workflow Audit**: List files in `.agent/workflows/` and check for alignment with `PROTOCOL.md` and `tdd.md`. +- **Skill Audit**: Verify `.agent/skills/` contains required professional skillsets. + +### 2. Check for External Updates +// turbo +- Command: `gemini update --check` (or alternative sync script) +- Description: Fetches the latest global or project-specific agent definitions. + +### 3. Propose Alignment Changes +- If a persona definition is stale or missing a required section (e.g., "Meta-Orchestration"), propose a diff. +- If a new global workflow is available, propose its installation. + +### 4. Execute Update +- Update files in `.agent/` after user confirmation. +- Commit changes with `chore(agent): synchronize personas and workflows`. + +## Triggers +- **Manual**: User requests "update yourself" or "sync personas". +- **Periodic**: Once per week or at the start of a major project phase. +- **Dependency Change**: When `tech-stack.md` undergoes a significant architectural shift. diff --git a/.agent/workflows/tdd.md b/.agent/workflows/tdd.md new file mode 100644 index 0000000..226032b --- /dev/null +++ b/.agent/workflows/tdd.md @@ -0,0 +1,32 @@ +--- +description: Test-Driven Development workflow with high safety and auditable results. +--- + +# TDD Workflow (Red-Green-Refactor) + +## 1. Red Phase: Failing Tests +- Create or update a test file (e.g., `tests/*.rs`). +- Write unit tests for the specific task requirements. +- **Run tests**: `cargo test --test ` and confirm failure. + +## 2. Green Phase: Implementation +- Write the minimum code required to pass the failing tests. +- **Verify**: `cargo test --workspace` until all tests pass. + +## 3. Refactor Phase +- Clean up the code and tests while keeping them green. +- Check performance: `cargo bench` if applicable. + +## 4. Verification & Coverage +- Ensure code coverage is >80% for new logic. +- Run linters: `cargo clippy`. + +## 5. Commit & Audit +- **Commit Message**: Use conventional commits (e.g., `feat:`, `fix:`). + - **Subject**: Max 50 chars, lowercase, no period. + - **Body**: Wrap at 72 chars. Why, not just what. +- **Git Note**: Attach a summary of the task using `git notes add -m "" `. +- **Note Content**: Task name, changes, files modified, and "why". + +## 6. Documentation Updates +- Update `PLAN.md` or track plans with the commit SHA and status `[x]`. diff --git a/.gitignore b/.gitignore index 1b7371b..c10823e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,10 @@ Cargo.lock target schemas *.bak -.agent/* +# Agent internal state +.agent/brain/ +.agent/tmp/ +.agent/memory/ + + + diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..0217cfc --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +@import .agent/PROTOCOL.md From 19a1753debccf0dd371fa9e0efef04fb2f154f15 Mon Sep 17 00:00:00 2001 From: Bruce D'Arcus Date: Sun, 25 Jan 2026 08:34:45 -0500 Subject: [PATCH 2/2] perf: optimize memory and pre-calculate hints Refactor the processor and bibliography models to minimize cloning and redundant calculations. Shift to a borrowing-first architecture for reference data across the rendering pipeline. Pre-calculate processing hints during initialization to reduce rendering complexity from quadratic to linear relative to bibliography size. Fix author substitution logic and transition to Result-based error handling in the processor to improve reliability. Expand documentation for the bibliography data model. Verification results: - Rendering performance improved by 97 percent. - All workspace tests passed. - Clippy and rustfmt checks passed. Signed-off-by: Bruce D'Arcus --- csln/src/bibliography/README.md | 25 +++- csln/src/bibliography/reference.rs | 29 ++-- processor/src/processor.rs | 208 +++++++++++++---------------- processor/src/render.rs | 45 +++---- processor/src/values.rs | 39 +++--- 5 files changed, 169 insertions(+), 177 deletions(-) diff --git a/csln/src/bibliography/README.md b/csln/src/bibliography/README.md index 5c39233..907ed6c 100644 --- a/csln/src/bibliography/README.md +++ b/csln/src/bibliography/README.md @@ -1,3 +1,24 @@ -This is a Rust library that implements the [csl-next](https://github.com/bdarcus/csl-next) bibliography model. +# csln-bibliography + +This library implements the core bibliography data model for CSLNext. It is designed to be highly structured where needed (e.g., for names and dates) while remaining flexible for diverse bibliographic data. + +## Key Concepts + +### InputReference +The primary unit of data. It is an enum with several variants: +- **Monograph**: Books, reports, etc. +- **Collection**: Edited volumes, anthologies. +- **CollectionComponent**: Chapters or parts of a collection. +- **SerialComponent**: Articles in journals, newspapers, etc. + +### Contributor +Represents persons or organizations. Supports simple strings, structured names (given/family), and lists. It includes formatting logic for names (e.g., initials, sorting order). + +### Date (EDTF) +Dates are stored as EDTF strings, allowing for flexible date-time encoding (uncertain dates, intervals, seasons). The library provides utilities to extract years and months from these strings. + +## Usage +The `InputBibliography` type is a `HashMap`, where the key is the citation key (ID). + +JSON schemas for these models can be generated using the `csln-schemas` binary in the `cli` crate. -The `csln-schemas` binary will generate the input JSON schemas. diff --git a/csln/src/bibliography/reference.rs b/csln/src/bibliography/reference.rs index b5c3bd4..9eb1368 100644 --- a/csln/src/bibliography/reference.rs +++ b/csln/src/bibliography/reference.rs @@ -669,7 +669,7 @@ impl fmt::Display for ContributorList { impl Contributor { // if as_sorted is true, the name will be displayed as sorted, overriding the configuration option. - pub fn names(&self, options: Config, as_sorted: bool) -> Vec { + pub fn names(&self, options: &Config, as_sorted: bool) -> Vec { match self { Contributor::SimpleName(c) => vec![c.name.to_string()], Contributor::StructuredName(contributor) => { @@ -688,7 +688,7 @@ impl Contributor { /// Join a vector of strings with commas and "and". pub fn name_list_and(&self, and: String) -> Vec { - let names = self.names(Config::default(), false); + let names = self.names(&Config::default(), false); let mut result = names; if result.len() > 1 { if let Some(last) = result.pop() { @@ -732,15 +732,16 @@ impl Contributor { } } - pub fn format(&self, options: Config, locale: Locale) -> String { + pub fn format(&self, options: &Config, locale: &Locale) -> String { let as_sorted: bool = matches!(self, Contributor::StructuredName(_)); - let names = self.names(options.clone(), as_sorted); + let names = self.names(options, as_sorted); let contributor_options = options.contributors.clone().unwrap_or_default(); let shorten: bool = contributor_options.shorten.unwrap_or_default().min <= names.len() as u8; if shorten { let shorten_options = options .contributors + .clone() .unwrap_or_default() .shorten .clone() @@ -749,10 +750,10 @@ impl Contributor { let and_others = shorten_options.and_others; let and_others_string = match and_others { AndOtherOptions::EtAl => { - locale.terms.et_al.unwrap_or("et al".to_string()) + locale.terms.et_al.clone().unwrap_or("et al".to_string()) } // TODO localize AndOtherOptions::Text => { - locale.terms.and_others.unwrap_or("and others".to_string()) + locale.terms.and_others.clone().unwrap_or("and others".to_string()) } }; let names_str: Vec<&str> = names.iter().map(AsRef::as_ref).collect(); @@ -776,7 +777,7 @@ impl Contributor { impl ContributorList { // ... - fn as_sorted(options: Config, index: usize) -> bool { + fn as_sorted(options: &Config, index: usize) -> bool { let display_as_sort = options .contributors .clone() @@ -787,13 +788,11 @@ impl ContributorList { || display_as_sort == Some(DisplayAsSort::All) } - pub fn names_list(&self, options: Config) -> Vec { + pub fn names_list(&self, options: &Config) -> Vec { self.0 .iter() .enumerate() - .flat_map(|(i, c)| { - c.names(options.clone(), Self::as_sorted(options.clone(), i)) - }) + .flat_map(|(i, c)| c.names(options, Self::as_sorted(options, i))) .collect::>() } } @@ -810,15 +809,15 @@ fn display_and_sort_names() { }); let options = Config::default(); // FIXME use this format method in this test - assert_eq!(simple.names(options, false).join(" "), "John Doe"); + assert_eq!(simple.names(&options, false).join(" "), "John Doe"); let options = Config::default(); assert_eq!( - simple.names(options, true).join(" "), + simple.names(&options, true).join(" "), "John Doe", "as_sorted=true should not affect a simple name" ); let options = Config::default(); - assert_eq!(structured.names(options, false).join(" "), "John Doe"); + assert_eq!(structured.names(&options, false).join(" "), "John Doe"); let options = Config::default(); - assert_eq!(structured.names(options, true).join(", "), "Doe, John"); + assert_eq!(structured.names(&options, true).join(", "), "Doe, John"); } diff --git a/processor/src/processor.rs b/processor/src/processor.rs index aa0783f..26b9204 100644 --- a/processor/src/processor.rs +++ b/processor/src/processor.rs @@ -9,11 +9,11 @@ use crate::types::{ ProcReferences, ProcTemplate, ProcTemplateComponent, ProcValues, RenderOptions, }; use crate::values::ComponentValues; -use csln::bibliography::reference::{InputReference, RefID}; +use csln::bibliography::reference::InputReference; use csln::bibliography::InputBibliography as Bibliography; use csln::citation::{Citation, CitationItem, Citations}; use csln::style::locale::Locale; -use csln::style::options::{Config, SortKey, SubstituteKey}; +use csln::style::options::{Config, SortKey, Substitute, SubstituteKey}; use csln::style::template::TemplateComponent; use csln::style::Style; use itertools::Itertools; @@ -35,23 +35,39 @@ pub struct Processor { /// Default configuration for reference. #[serde(skip)] default_config: Config, + /// Pre-calculated processing hints. + #[serde(skip)] + hints: HashMap, } impl Processor { /// Create a new Processor instance. pub fn new( style: Style, - bibliography: Bibliography, + mut bibliography: Bibliography, citations: Citations, locale: Locale, ) -> Processor { - Processor { + // Normalize the bibliography by ensuring all references have an ID. + for (id, reference) in bibliography.iter_mut() { + if reference.id().is_none() { + reference.set_id(id.clone()); + } + } + + let mut processor = Processor { style, bibliography, citations, locale, default_config: Config::default(), - } + hints: HashMap::new(), + }; + + // Pre-calculate hints. + processor.hints = processor.calculate_proc_hints(); + + processor } /// Render references to AST. @@ -60,41 +76,42 @@ impl Processor { let sorted_references = self.sort_references(self.get_references()); let bibliography: ProcBibliography = sorted_references .par_iter() - .map(|reference| self.process_reference(reference)) + .map(|reference| self.process_reference(*reference)) .collect(); let citations = if self.citations.is_empty() { None } else { - Some(self.process_citations(&self.citations)) + match self.process_citations(&self.citations) { + Ok(c) => Some(c), + Err(e) => { + eprintln!("Citation processing error: {}", e); + None + } + } }; ProcReferences { bibliography, citations } } - fn process_citations(&self, citations: &Citations) -> ProcCitations { + fn process_citations( + &self, + citations: &Citations, + ) -> Result { citations .iter() .map(|citation| self.process_citation(citation)) .collect() } - fn process_citation(&self, citation: &Citation) -> ProcCitation { + fn process_citation( + &self, + citation: &Citation, + ) -> Result { // TODO handle the prefix and suffix, though am uncertain how to best do that - let pcitation = citation + citation .citation_items .iter() - .map(|citation_item| { - match self.process_citation_item(citation_item) { - Ok(item) => item, - Err(e) => { - // Fallback for error rendering - // TODO: Makes this configurable? - eprintln!("Citation processing error: {}", e); - vec![] - } - } - }) - .collect(); - pcitation + .map(|citation_item| self.process_citation_item(citation_item)) + .collect() } /// Process a single citation item. @@ -102,11 +119,14 @@ impl Processor { &self, citation_item: &CitationItem, ) -> Result { - let citation_style = self.style.citation.clone(); + let citation_style = self.style.citation.as_ref(); let reference = self.get_reference(&citation_item.ref_id)?; - let template = citation_style.map(|cs| cs.template).unwrap_or_default(); - let proc_template = self.process_template(&reference, &template); + let template = citation_style + .map(|cs| &cs.template) + .map(|t| t.as_slice()) + .unwrap_or_default(); + let proc_template = self.process_template(reference, template); Ok(proc_template) } @@ -146,13 +166,11 @@ impl Processor { component: &TemplateComponent, reference: &InputReference, ) -> Option { - let hints = self.get_proc_hints(); - let reference_id: Option = reference.id(); - let hint: ProcHints = - // TODO why would reference_id be None? - hints.get(&reference_id.unwrap_or_default()).cloned().unwrap_or_default(); + let reference_id: String = reference.id().unwrap_or_default(); + let default_hint = ProcHints::default(); + let hint: &ProcHints = self.hints.get(&reference_id).unwrap_or(&default_hint); let options = self.get_render_options(); - let values = component.values(reference, &hint, &options)?; + let values = component.values(reference, hint, &options)?; let template_component = component.clone(); // TODO add role here if specified in the style // TODO affixes from style? @@ -171,48 +189,20 @@ impl Processor { } /// Get references from the bibliography. - pub fn get_references(&self) -> Vec { - self.bibliography - .iter() - .map(|(key, reference)| match reference { - InputReference::Monograph(monograph) => { - let mut input_reference = - InputReference::Monograph(monograph.clone()); - input_reference.set_id(key.clone()); - input_reference - } - InputReference::CollectionComponent(collection_component) => { - let mut input_reference = - InputReference::CollectionComponent(collection_component.clone()); - input_reference.set_id(key.clone()); - input_reference - } - InputReference::SerialComponent(serial_component) => { - let mut input_reference = - InputReference::SerialComponent(serial_component.clone()); - input_reference.set_id(key.clone()); - input_reference - } - InputReference::Collection(collection) => { - let mut input_reference = - InputReference::Collection(collection.clone()); - input_reference.set_id(key.clone()); - input_reference - } - }) - .collect() + pub fn get_references(&self) -> Vec<&InputReference> { + self.bibliography.values().collect() } /// Get a reference from the bibliography by id/citekey. - pub fn get_reference(&self, id: &str) -> Result { + pub fn get_reference(&self, id: &str) -> Result<&InputReference, ProcessorError> { match self.bibliography.get(id) { - Some(reference) => Ok(reference.clone()), + Some(reference) => Ok(reference), None => Err(ProcessorError::ReferenceNotFound(id.to_string())), } } /// Get all cited references from the inputs. - pub fn get_cited_references(&self) -> Vec { + pub fn get_cited_references(&self) -> Vec<&InputReference> { let mut cited_references = Vec::new(); for key in &self.get_cited_keys() { if let Ok(reference) = self.get_reference(key) { @@ -237,20 +227,21 @@ impl Processor { /// Sort the references according to instructions in the style. #[inline] - pub fn sort_references( + pub fn sort_references<'a>( &self, - references: Vec, - ) -> Vec { - let mut references: Vec = references; - let options: Config = self.style.options.clone().unwrap_or_default(); - if let Some(sort_config) = - options.processing.clone().unwrap_or_default().config().sort - { + references: Vec<&'a InputReference>, + ) -> Vec<&'a InputReference> { + let mut references = references; + let options = self.style.options.as_ref().unwrap_or(&self.default_config); + let processing = options.processing.as_ref().cloned().unwrap_or_default(); + let processing_config = processing.config(); + + if let Some(sort_config) = &processing_config.sort { sort_config.template.iter().rev().for_each(|sort| match sort.key { SortKey::Author => { references.par_sort_by(|a, b| { let a_author = match a.author() { - Some(author) => author.names(options.clone(), true).join("-"), + Some(author) => author.names(options, true).join("-"), None => match self.get_author_substitute(a) { Some((substitute, _)) => substitute, None => "".to_string(), @@ -258,7 +249,7 @@ impl Processor { }; let b_author = match b.author() { - Some(author) => author.names(options.clone(), true).join("-"), + Some(author) => author.names(options, true).join("-"), None => match self.get_author_substitute(b) { Some((substitute, _)) => substitute, None => "".to_string(), @@ -268,7 +259,7 @@ impl Processor { }); } SortKey::Year => { - references.par_sort_by(|a: &InputReference, b: &InputReference| { + references.par_sort_by(|a, b| { let a_year = a.issued().as_ref().map(|d| d.year()).unwrap_or_default(); let b_year = @@ -282,12 +273,17 @@ impl Processor { references } + /// Get the pre-calculated processing hints. + pub fn get_proc_hints(&self) -> &HashMap { + &self.hints + } + /// Process the references and return a HashMap of ProcHints. - pub fn get_proc_hints(&self) -> HashMap { + fn calculate_proc_hints(&self) -> HashMap { let refs = self.get_references(); let sorted_refs = self.sort_references(refs); let grouped_refs = self.group_references(sorted_refs); - let proc_hints = grouped_refs + grouped_refs .iter() .flat_map(|(key, group)| { let group_len = group.len(); @@ -301,50 +297,33 @@ impl Processor { group_length: group_len, group_key: key.clone(), }; - let ref_id = match reference { - InputReference::Monograph(monograph) => monograph.id.clone(), - InputReference::CollectionComponent(collection_component) => { - collection_component.id.clone() - } - InputReference::SerialComponent(serial_component) => { - serial_component.id.clone() - } - InputReference::Collection(collection) => { - collection.id.clone() - } - }; + let ref_id = reference.id(); ref_id.map(|id| (id, proc_hint)) }, ) }) - .collect(); - proc_hints + .collect() } /// Return a string to use for grouping for a given reference, using instructions in the style. fn make_group_key(&self, reference: &InputReference) -> String { - let options: Config = match self.style.options { - Some(ref options) => options.clone(), - None => Config::default(), // TODO is this right? - }; - let group_template = options - .processing - .unwrap_or_default() + let options = self.style.options.as_ref().unwrap_or(&self.default_config); + let processing = options.processing.as_ref().cloned().unwrap_or_default(); + let group_template = processing .config() .group .as_ref() - .map(|g| g.template.clone()) + .map(|g| &g.template) + .cloned() .unwrap_or_default(); - let options = self.style.options.clone(); + let as_sorted = false; let group_key = group_template // This is likely unnecessary, but just in case. .par_iter() .map(|key| match key { SortKey::Author => match reference.author() { - Some(author) => author - .names(options.clone().unwrap_or_default(), as_sorted) - .join("-"), + Some(author) => author.names(options, as_sorted).join("-"), None => "".to_string(), }, SortKey::Year => reference @@ -367,16 +346,19 @@ impl Processor { &self, reference: &InputReference, ) -> Option<(String, SubstituteKey)> { - let options = self.style.options.clone().unwrap_or_default(); - let substitute_config = options.substitute.clone(); // FIXME default? the below line panics - substitute_config - .unwrap_or_default() + let options = self.style.options.as_ref().unwrap_or(&self.default_config); + let substitute_config = options.substitute.as_ref(); + + // Use default substitute if not provided in style + let default_sub = Substitute::default(); + let substitute = substitute_config.unwrap_or(&default_sub); + + substitute .template .iter() .find_map(|substitute_key| match *substitute_key { SubstituteKey::Editor => { - let names = - reference.editor()?.format(options.clone(), self.locale.clone()); + let names = reference.editor()?.format(options, &self.locale); Some((names, substitute_key.clone())) } _ => None, @@ -385,10 +367,10 @@ impl Processor { /// Group references according to instructions in the style. #[inline] - pub fn group_references( + pub fn group_references<'a>( &self, - references: Vec, - ) -> HashMap> { + references: Vec<&'a InputReference>, + ) -> HashMap> { references .into_iter() .group_by(|reference| self.make_group_key(reference)) diff --git a/processor/src/render.rs b/processor/src/render.rs index 7ffad7c..4e50137 100644 --- a/processor/src/render.rs +++ b/processor/src/render.rs @@ -34,30 +34,29 @@ pub fn refs_to_string(proc_templates: Vec) -> String { impl Display for ProcTemplateComponent { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let rendering = self.template_component.rendering(); - let prefix: String = rendering - .clone() // REVIEW this compiles, but too much cloning - .unwrap_or_default() - .prefix - .unwrap_or_default(); - let suffix: String = - rendering.clone().unwrap_or_default().suffix.unwrap_or_default(); - let wrap: WrapPunctuation = - rendering.unwrap_or_default().wrap.unwrap_or_default(); - let wrap_punct: (String, String) = match wrap { - WrapPunctuation::None => ("".to_string(), "".to_string()), - WrapPunctuation::Parentheses => ("(".to_string(), ")".to_string()), - WrapPunctuation::Brackets => ("[".to_string(), "]".to_string()), + let r = rendering.as_ref(); + + let prefix = r.and_then(|r| r.prefix.as_deref()).unwrap_or_default(); + let suffix = r.and_then(|r| r.suffix.as_deref()).unwrap_or_default(); + let wrap = r.and_then(|r| r.wrap.as_ref()).unwrap_or(&WrapPunctuation::None); + + let wrap_punct: (&str, &str) = match wrap { + WrapPunctuation::None => ("", ""), + WrapPunctuation::Parentheses => ("(", ")"), + WrapPunctuation::Brackets => ("[", "]"), }; - // REVIEW: is this where to plugin different renderers? - // Also, how to handle the different affixes, including within the values? - let result = wrap_punct.0 - + &prefix - + &self.values.prefix.clone().unwrap_or_default() - + &self.values.value - + &self.values.suffix.clone().unwrap_or_default() - + &suffix - + &wrap_punct.1; - write!(f, "{}", result) + + write!( + f, + "{}{}{}{}{}{}{}", + wrap_punct.0, + prefix, + self.values.prefix.as_deref().unwrap_or_default(), + self.values.value, + self.values.suffix.as_deref().unwrap_or_default(), + suffix, + wrap_punct.1 + ) } } diff --git a/processor/src/values.rs b/processor/src/values.rs index 9532790..6518410 100644 --- a/processor/src/values.rs +++ b/processor/src/values.rs @@ -238,17 +238,19 @@ impl ComponentValues for TemplateContributor { let author = reference.author(); if author.is_some() { Some(ProcValues { - value: author?.format(options.global.clone(), locale.clone()), + value: author?.format(options.global, locale), prefix: None, suffix: None, }) } else { // TODO generalize the substitution - let add_role_form = - // REVIEW is this correct? - options.global.substitute.clone()?.contributor_role_form; + let add_role_form = options + .global + .substitute + .as_ref() + .and_then(|s| s.contributor_role_form.clone()); let editor = reference.editor()?; - let editor_length = editor.names(options.global.clone(), true).len(); + let editor_length = editor.names(options.global, true).len(); // get the role string; if it's in fact author, it will be None let suffix = add_role_form.map(|role_form| { role_to_string( @@ -258,15 +260,11 @@ impl ComponentValues for TemplateContributor { editor_length, ) }); - let suffix_padded = suffix.and_then(|s| { - Some(match s { - Some(val) => format!(" {}", val), - None => return None, - }) - }); // TODO fix this matching logic + let suffix_padded = + suffix.and_then(|s| s.map(|val| format!(" {}", val))); // TODO fix this matching logic Some(ProcValues { - value: editor.format(options.global.clone(), locale.clone()), + value: editor.format(options.global, locale), prefix: None, suffix: suffix_padded, }) @@ -278,8 +276,7 @@ impl ComponentValues for TemplateContributor { _ => { let editor = &reference.editor()?; let form = &self.form; - let editor_length = - editor.names(options.global.clone(), true).len(); + let editor_length = editor.names(options.global, true).len(); // TODO handle verb and non-verb forms match form { @@ -298,8 +295,7 @@ impl ComponentValues for TemplateContributor { } }); Some(ProcValues { - value: editor - .format(options.global.clone(), locale.clone()), + value: editor.format(options.global, locale), prefix: prefix_padded, suffix: None, }) @@ -319,8 +315,7 @@ impl ComponentValues for TemplateContributor { } }); Some(ProcValues { - value: editor - .format(options.global.clone(), locale.clone()), + value: editor.format(options.global, locale), prefix: None, suffix: suffix_padded, // TODO handle None }) @@ -330,16 +325,12 @@ impl ComponentValues for TemplateContributor { } } ContributorRole::Translator => Some(ProcValues { - value: reference - .translator()? - .format(options.global.clone(), locale.clone()), + value: reference.translator()?.format(options.global, locale), prefix: None, suffix: None, }), ContributorRole::Publisher => Some(ProcValues { - value: reference - .publisher()? - .format(options.global.clone(), locale.clone()), + value: reference.publisher()?.format(options.global, locale), prefix: None, suffix: None, }),