# Parser API Redesign

This document proposes a new `Parser` struct for reference detection and a
separate `Formatter` struct for rendering, sorting, deduping, and merging parsed
references.

## Goals

- Replace the current sprawl of `parse_reference*`, `parse_reference_with_options`,
  and `Text::parse_with_options` free functions with a reusable parser handle.
- Keep parsing separate from formatting. Parsing should find references and
  report diagnostics; formatting should decide how references are displayed or
  normalized for output.
- Provide builder-style configuration for parsing behavior such as fuzzy
  matching, bare references, relaxed boundaries, contextual resolution, and
  versification.
- Return diagnostics alongside successful references so callers can respond to
  invalid candidates, fuzzy matches, contextual resolution, and other parse
  events without failing the whole parse.
- Make `Parser` cheap to clone and reusable across many `parse(text)` calls.
- Add `Reference::format()` and `Reference::format_with(...)` convenience APIs
  for single-reference formatting.
- Add a broader `Formatter` type that can format one reference or prepare and
  format a collection of references.

## Non-Goals

- Do not add a `parse_one` API yet.
- Do not make formatting options part of `Parser`.
- Do not make normal invalid input a fatal parser error. Document scanning
  should still return valid references when some candidates fail.

## Parser Construction

```rust
let parser = Parser::new()
    .with_options(ParseOptions::default());

// Equivalent:
let parser = Parser::default();

// Custom options:
let parser = Parser::new().with_options(
    ParseOptions::new()
        .contextual_resolution(true)
        .fuzzy_matching(true)
        .with_versification(Versification::Masoretic),
);
```

## Parsing Options

The existing `ParseOptions` should remain the main configuration surface:

```rust
pub struct ParseOptions {
    pub fuzzy_matching: bool,
    pub bare_references: bool,
    pub relaxed_boundaries: bool,
    pub contextual_resolution: bool,
    pub versification: Versification,
}
```

Open design questions:

- Whether `ParseOptions::optimistic()` should remain as-is or be renamed to
  describe the use case more directly, such as `ParseOptions::search()`.
- Whether diagnostics should be independently configurable, for example
  `diagnostics: DiagnosticOptions`, or always emitted.

## Parse Output

Parsing should return a structured output that includes references and
diagnostics:

```rust
pub struct Text {
    pub source: String,
    pub references: Vec<LocatedReference>,
    pub diagnostics: Vec<ParseDiagnostic>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
    pub start: usize,
    pub end: usize,
}

pub struct LocatedReference {
    pub reference: Reference,
    pub start: usize,
    pub end: usize,
    pub context: Option<ContextualResolution>,
}
```

Keep `Text` as the primary parse result. It already represents parsed source
text plus located references, so adding diagnostics is the least disruptive
migration path.

## Diagnostics

Diagnostics should describe parse events and rejected candidates without making
the entire parse fail.

```rust
pub struct ParseDiagnostic {
    pub span: Span,
    pub severity: DiagnosticSeverity,
    pub kind: ParseDiagnosticKind,
}

pub enum DiagnosticSeverity {
    Info,
    Warning,
    Error,
}

pub enum ParseDiagnosticKind {
    ContextResolved {
        resolved_from: String,
    },
    FuzzyBookMatch {
        matched: String,
        resolved_to: &'static str,
        distance: usize,
    },
    InvalidChapter {
        book: &'static str,
        chapter: usize,
    },
    InvalidVerse {
        book: &'static str,
        chapter: usize,
        verse: usize,
    },
    InvalidRange {
        reason: String,
    },
    AmbiguousBareReference {
        chapter: usize,
        verse: usize,
    },
    ContextResolutionFailed,
}
```

Initially implemented diagnostics:

- `ContextResolved`
- `FuzzyBookMatch`

Future diagnostics requiring parser-internal extraction changes:

- `InvalidChapter`
- `InvalidVerse`
- `InvalidRange`
- `AmbiguousBareReference`
- `ContextResolutionFailed`

Matching diagnostics to references:

- `span` is always present because UI consumers usually need to highlight the
- `span` can be matched against `LocatedReference { start, end, .. }` when a
  diagnostic describes an accepted reference.
- Diagnostics for rejected or unresolved candidates still carry a source span
  even though they do not have a matching `LocatedReference`.

Examples:

```rust
// Accepted with info diagnostic:
// "Genisis 1:1" -> Reference::Single { book: "Genesis", ... }
// diagnostic: FuzzyBookMatch { matched: "Genisis", resolved_to: "Genesis", ... }
// diagnostic.span overlaps the accepted LocatedReference

// Rejected candidate:
// "John 3:99" -> no reference
// diagnostic: InvalidVerse { book: "John", chapter: 3, verse: 99 }
// diagnostic.span points to "John 3:99"
```

`DiagnosticSeverity::Error` should mean "this candidate failed", not "the whole
parse failed".

## Reference Provenance

The current `ContextualContext` name is not descriptive. Rename it to something
that describes why the reference exists.

Use the narrower contextual name first:

```rust
pub struct ContextualResolution {
    pub original_text: String,
    pub resolved_from: String,
}
```

Fuzzy and bare reference provenance can be added later if callers need accepted
reference metadata beyond diagnostics.

## Parser Usage

```rust
let parser = Parser::default();
let parsed = parser.parse("Compare Genisis 1:1 with John 3:99");

for reference in &parsed.references {
    println!("{}", reference.reference);
}

for diagnostic in &parsed.diagnostics {
    eprintln!("{:?}: {:?}", diagnostic.severity, diagnostic.kind);
}
```

Convenience methods can preserve the current plain-reference use case:

```rust
impl ParsedText {
    pub fn references(&self) -> impl Iterator<Item = &Reference> {
        self.references.iter().map(|located| &located.reference)
    }

    pub fn text_without_references(&self) -> String {
        // Same behavior as current Text::text_without_references()
    }
}
```

## Formatting Options

Formatting options should be owned by reference formatting APIs, not by
`Parser`.

```rust
pub struct FormatOptions {
    pub abbreviation_style: AbbreviationStyle,
    pub separator_style: SeparatorStyle,
    pub range_style: RangeStyle,
    pub versification: Versification,
}

pub enum AbbreviationStyle {
    Full,
    Standard,
    Osis,
}

pub enum SeparatorStyle {
    EnDash,
    Hyphen,
}

pub enum RangeStyle {
    Preserve,
    CollapseFullChapters,
    CollapseFullBooks,
}
```

`versification` is part of formatting because display decisions like "is this a
full chapter?" depend on verse counts.

## Reference Formatting

Add single-reference convenience methods:

```rust
impl Reference {
    pub fn format(&self) -> String {
        self.to_string()
    }

    pub fn format_with(&self, options: &FormatOptions) -> String {
        Formatter::new(options.clone()).format(self)
    }
}
```

`Display for Reference` should remain the default, stable display form.
`format_with` is where abbreviation, separator, and versification-sensitive
display rules should live.

## Formatter

Use the broad name `Formatter`, not `ReferenceFormatter`, because the type is
expected to handle single references and reference collections.

```rust
pub struct Formatter {
    format: FormatOptions,
    list: ListFormatOptions,
}

pub struct ListFormatOptions {
    pub sort: bool,
    pub dedupe: bool,
    pub orderings: Vec<&'static str>,
    pub list_separator: String,
}
```

Suggested methods:

```rust
impl Formatter {
    pub fn new(format: FormatOptions) -> Self;

    pub fn with_list_options(mut self, options: ListFormatOptions) -> Self;

    pub fn format(&self, reference: &Reference) -> String;

    pub fn format_many<'a>(&self, references: impl IntoIterator<Item = &'a Reference>) -> String;

    pub fn prepare_many(&self, references: &[Reference]) -> Vec<Reference>;
}
```

`prepare_many` should perform transformations such as sorting, deduping, and
eventually merging without rendering. `format_many` can call `prepare_many` and
then join the rendered references.

Keep transformation behavior explicit:

- Sorting should use canonical sort order options.
- Deduping should compare normalized reference identity, not formatted strings.
- Merging is deferred until the canonical rules are explicit enough to avoid
  changing reference meaning.
- Formatting should not mutate the caller's input.

## Migration / Compatibility

Existing free functions should remain as thin wrappers during the migration:

```rust
pub fn parse_reference(input: &str) -> Vec<Reference> {
    Parser::default()
        .parse(input)
        .references
        .into_iter()
        .map(|located| located.reference)
        .collect()
}

pub fn parse_reference_with_options(input: &str, options: &ParseOptions) -> Vec<Reference> {
    Parser::new()
        .with_options(options.clone())
        .parse(input)
        .references
        .into_iter()
        .map(|located| located.reference)
        .collect()
}

pub fn parse_reference_with_offsets(input: &str) -> Vec<LocatedReference> {
    Parser::default().parse(input).references
}
```

Migration steps:

1. Add `Span`, `ParseDiagnostic`, `Text::diagnostics`, and `Parser`.
2. Rename `ContextualContext` to `ContextualResolution`, with a temporary
   deprecated alias for compatibility.
3. Keep `Text` as the parse result.
4. Add `Reference::format()` and `Reference::format_with(...)`.
5. Add `Formatter` for single-reference and collection formatting.
6. Convert free functions to wrappers around `Parser`.
7. Deprecate old entry points only after the new API is documented and covered
   by tests.

## Edge Cases To Cover

- Invalid chapter and invalid verse candidates should produce diagnostics after
  the extraction layer can return rejected candidates with spans.
- Fuzzy matches should produce accepted references plus `FuzzyBookMatch`
  diagnostics.
- Contextual references should preserve provenance and emit contextual
  diagnostics.
- Bare references should be distinguishable from normal references.
- Overlapping references should have deterministic precedence.
- Duplicate references should not accidentally discard diagnostics from rejected
  or lower-priority candidates.
- Cross-chapter ranges such as `John 3:16-4:2` should either be supported or
  produce a clear diagnostic documenting current limitations.
- Same-verse ranges such as `John 3:16-16` should have explicit formatting
  behavior.
- Formatting full chapters/books must use the formatter's versification, not
  implicit English defaults.
