Skip to content
This repository was archived by the owner on Feb 17, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agent/workflows/tdd.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ description: Test-Driven Development workflow with high safety and auditable res
## 4. Verification & Coverage
- Ensure code coverage is >80% for new logic.
- Run linters: `cargo clippy`.
- Run formatters: `cargo fmt`.

## 5. Commit & Audit
- **Commit Message**: Use conventional commits (e.g., `feat:`, `fix:`).
Expand Down
66 changes: 66 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Contributing

I would _love_ to have help on this, both because I'm an amateur programmer and a Rust newbie, and because the vision I am sketching out here will take a lot of work to realize.

## Getting Started

To build and test the project:

```bash
# Clone the repository
git clone https://github.com/bdarcus/csln.git
cd csln

# Build the project
cargo build

# Run tests
cargo test

# Run clippy for code quality checks
cargo clippy --all-targets --all-features

# Format code
cargo fmt

# Generate JSON schemas
cargo run --bin csln-schemas
```

## Development Standards

To maintain code quality and a clean history, please follow these guidelines:

1. **Commit Messages**: Use [Conventional Commits](https://www.conventionalcommits.org/).
- Subject: Max 50 characters.
- Body: Wrapped at 72 characters.
- DCO: All commits must be signed-off (`git commit -s`).
- Format: Use plain text only (no backsticks or Markdown) in commit bodies.
2. **Code Quality**: Ensure `cargo fmt` and `cargo clippy --workspace` pass before submitting.
3. **Performance**: For performance-related changes, provide before/after benchmark results from `cargo bench -p csln-processor`.
4. **Licensing**: New files must include the SPDX license header (MPL-2.0).

## Project Structure

- `csln/` - Core library with data models for styles, bibliography, and citations
- `cli/` - Command-line interface for processing citations
- `processor/` - Citation and bibliography processing engine
- `src/lib.rs` - Library entry point
- `src/processor.rs` - Main processor logic
- `src/types.rs` - Core data types
- `src/values.rs` - Value extraction logic
- `src/render.rs` - Rendering logic

## How to Help

Please contact me via discussions or the issue tracker, or by email, if you'd like to contribute.

I licensed the code here under the same terms as [citeproc-rs][CSLRS], in case code might be shared between them.
I also understand the Mozilla 2.0 license is compatible with Apache.

A note on citeproc-rs:

In reviewing the code, it strikes me pieces of it obviously complement this code base.
In particular, it has been optimized for the Zotero use-case, where it provides real-time formatting, while I have focused of the batch-processing case.

[CSLRS]: https://github.com/zotero/citeproc-rs
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ unsafe_code = "forbid"

[workspace.lints.clippy]
# not sure on what to turn on and off
complexity = { level = "allow", priority = -1 }
complexity = { level = "warn", priority = -1 }
expect_used = "warn"
large_enum_variant = "allow"
needless_borrow = "warn"
needless_question_mark = "warn"
needless_return = "warn"
style = { level = "allow", priority = -1 }
style = { level = "warn", priority = -1 }
unwrap_used = "warn"

[profile.release]
Expand Down
50 changes: 1 addition & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,56 +75,8 @@ Doing so, however, will require sorting out details of how that process is manag

## Contributing

I would _love_ to have help on this, both because I'm an amateur programmer and a Rust newbie, and because the vision I am sketching out here will take a lot of work to realize.
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to build, test, and help with the project.

### Getting Started

To build and test the project:

```bash
# Clone the repository
git clone https://github.com/bdarcus/csln.git
cd csln

# Build the project
cargo build

# Run tests
cargo test

# Run clippy for code quality checks
cargo clippy --all-targets --all-features

# Format code
cargo fmt

# Generate JSON schemas
cargo run --bin csln-schemas
```

### Development Standards

To maintain code quality and a clean history, please follow these guidelines:

1. **Commit Messages**: Use [Conventional Commits](https://www.conventionalcommits.org/).
- Subject: Max 50 characters.
- Body: Wrapped at 72 characters.
- DCO: All commits must be signed-off (`git commit -s`).
- Format: Use plain text only (no backsticks or Markdown) in commit bodies.
2. **Code Quality**: Ensure `cargo fmt` and `cargo clippy --workspace` pass before submitting.
3. **Performance**: For performance-related changes, provide before/after benchmark results from `cargo bench -p csln-processor`.
4. **Licensing**: New files must include the SPDX license header (MPL-2.0).

### Project Structure

- `csln/` - Core library with data models for styles, bibliography, and citations
- `cli/` - Command-line interface for processing citations
- `processor/` - Citation and bibliography processing engine
- `src/lib.rs` - Library entry point
- `src/processor.rs` - Main processor logic
- `src/types.rs` - Core data types
- `src/values.rs` - Value extraction logic
- `src/render.rs` - Rendering logic

### How to Help

Expand Down
52 changes: 37 additions & 15 deletions cli/src/makeschemas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,51 @@ use csln::citation::CitationList;
use csln::style::locale::Locale;
use csln::style::Style;

fn main() {
fs::create_dir_all("schemas").expect("Failed to create directory 'schemas'");
use anyhow::Context;

fn main() -> anyhow::Result<()> {
fs::create_dir_all("schemas").context("Failed to create directory 'schemas'")?;

let style_schema = schema_for!(Style);
let citation_schema = schema_for!(CitationList);
let bib_schema = schema_for!(InputBibliography);
let locale_schema = schema_for!(Locale);

let style_json_output = serde_json::to_string_pretty(&style_schema).unwrap();
let citation_json_output = serde_json::to_string_pretty(&citation_schema).unwrap();
let bib_json_output = serde_json::to_string_pretty(&bib_schema).unwrap();
let locale_json_output = serde_json::to_string_pretty(&locale_schema).unwrap();

let mut citation_file = File::create("schemas/citation.json").unwrap();
let mut style_file = File::create("schemas/style.json").unwrap();
let mut bib_file = File::create("schemas/bibliography.json").unwrap();
let mut locale_file = File::create("schemas/locale.json").unwrap();
style_file.write_all(style_json_output.as_bytes()).unwrap();
citation_file.write_all(citation_json_output.as_bytes()).unwrap();
bib_file.write_all(bib_json_output.as_bytes()).unwrap();
locale_file.write_all(locale_json_output.as_bytes()).unwrap();
let style_json_output = serde_json::to_string_pretty(&style_schema)
.context("Failed to serialize style schema")?;
let citation_json_output = serde_json::to_string_pretty(&citation_schema)
.context("Failed to serialize citation schema")?;
let bib_json_output = serde_json::to_string_pretty(&bib_schema)
.context("Failed to serialize bibliography schema")?;
let locale_json_output = serde_json::to_string_pretty(&locale_schema)
.context("Failed to serialize locale schema")?;

let mut citation_file = File::create("schemas/citation.json")
.context("Failed to create schemas/citation.json")?;
let mut style_file = File::create("schemas/style.json")
.context("Failed to create schemas/style.json")?;
let mut bib_file = File::create("schemas/bibliography.json")
.context("Failed to create schemas/bibliography.json")?;
let mut locale_file = File::create("schemas/locale.json")
.context("Failed to create schemas/locale.json")?;

style_file
.write_all(style_json_output.as_bytes())
.context("Failed to write style schema")?;
citation_file
.write_all(citation_json_output.as_bytes())
.context("Failed to write citation schema")?;
bib_file
.write_all(bib_json_output.as_bytes())
.context("Failed to write bibliography schema")?;
locale_file
.write_all(locale_json_output.as_bytes())
.context("Failed to write locale schema")?;

println!("Wrote bibliography schema to schemas/bibliography.json");
println!("Wrote citation schema to schemas/citation.json");
println!("Wrote style schema to schemas/style.json");
println!("Wrote locale schema to schemas/locale.json");

Ok(())
}
27 changes: 27 additions & 0 deletions csln/src/bibliography/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,30 @@ pub use reference::InputReference;

/// A bibliography is a collection of references.
pub type InputBibliography = HashMap<String, InputReference>;

#[cfg(test)]
mod tests {
use super::reference::Title;
use super::*;

#[test]
fn test_input_bibliography_deserialization() {
let json = r#"
{
"ITEM-1": {
"id": "ITEM-1",
"type": "book",
"title": "Book Title",
"issued": "2020"
}
}
"#;
let bib: InputBibliography = serde_json::from_str(json).unwrap();
assert_eq!(bib.len(), 1);
// Correct comparison with Title enum
assert_eq!(
bib.get("ITEM-1").unwrap().title().as_ref().unwrap(),
&Title::Single("Book Title".to_string())
);
}
}
27 changes: 27 additions & 0 deletions csln/src/citation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,30 @@ pub enum LocatorTerm {
Verse,
Volume,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_citation_deserialization() {
let json = r#"
{
"citation_items": [
{
"refId": "ITEM-1"
}
],
"mode": "integral"
}
"#;
let citation: Citation = serde_json::from_str(json).unwrap();
assert_eq!(citation.citation_items.len(), 1);
assert_eq!(citation.citation_items[0].ref_id, "ITEM-1");
// Check enum matches integral
match citation.mode {
CitationModeType::Integral => (),
_ => panic!("Expected Integral mode"),
}
}
}
21 changes: 21 additions & 0 deletions csln/src/style/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,24 @@ pub enum Category {
#[serde(rename = "social science")]
SocialScience,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_style_deserialization_minimal() {
let json = r#"
{
"info": {
"title": "Minimal Style",
"id": "http://example.com/styles/minimal"
}
}
"#;
let style: Style = serde_json::from_str(json).unwrap();
assert_eq!(style.info.title.as_ref().unwrap(), "Minimal Style");
assert!(style.bibliography.is_none());
assert!(style.citation.is_none());
}
}
2 changes: 1 addition & 1 deletion processor/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ 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
Expand Down