aimx/inference/
item.rs

1//! Parsing of list and task items in AIM inference content.
2//!
3//! Items support ordered and unordered prefixes and optional checkbox status to
4//! represent values or tasks for inference results.
5
6use crate::aim::Prefix;
7use nom::{
8    IResult, Parser, branch::alt, character::complete::{char, digit1, multispace0, one_of}, combinator::{map, opt, value}, error::{Error, ErrorKind}, sequence::{delimited, preceded}
9};
10use std::sync::Arc;
11
12/// Inference list or task item parsed from AIM content.
13///
14/// Carries an optional list prefix and, for tasks, an optional status.
15#[derive(Debug, PartialEq, Clone)]
16pub enum Item {
17    /// Value item with prefix and text.
18    Value(Prefix, Arc<str>),
19
20    /// Task item with prefix, optional status (`Some(true)` completed, `Some(false)` failed, `None` pending), and text.
21    Task(Prefix, Option<bool>, Arc<str>),
22}
23
24fn parse_contents(input: &str) -> IResult<&str, ()> {
25    let (input, _) = multispace0(input)?;
26    if input.is_empty() {
27        Err(nom::Err::Error(Error::new(input, ErrorKind::Fail)))
28    } else {
29        Ok((input, ()))
30    }
31}
32fn parse_ordered(input: &str) -> IResult<&str, Prefix> {
33    preceded(
34        (multispace0, digit1, char('.')),
35        value(Prefix::Ordered, multispace0)
36    ).parse(input)
37}
38
39fn parse_unordered(input: &str) -> IResult<&str, Prefix> {
40    preceded(
41        (multispace0, char('-')),
42        value(Prefix::Unordered, multispace0)
43    ).parse(input)
44}
45
46fn parse_checkbox(input: &str) -> IResult<&str, Option<bool>> {    
47    map(
48        preceded(
49            multispace0, 
50            delimited(
51                char('['),
52                opt(preceded(multispace0, one_of("Xx+-"))),
53                preceded(multispace0, char(']')),
54            )
55        ),
56        |status_char| match status_char {
57            Some('X') | Some('x') | Some('+') => Some(true),
58            Some('-') => Some(false),
59            _ => None,
60        }
61    ).parse(input)
62}
63
64fn parse_inline_task(input: &str) -> IResult<&str, (Option<bool>, Arc<str>)> {
65    let (input, status) = parse_checkbox(input)?;
66    let (input, _) = multispace0(input)?;
67    let content = input.trim_end().to_string();
68    if content.is_empty() {
69        return Err(nom::Err::Error(Error::new(input, ErrorKind::Fail)));
70    }
71    Ok(("", (status, Arc::from(content))))
72}
73
74/// Parse an inline item without list prefix.
75///
76/// Interprets leading checkbox as `Task`, otherwise returns `Value` with `Prefix::None`.
77pub fn parse_inline_item(input: &str) -> IResult<&str, Item> {
78    // Try to parse inline task first (look for box)
79    if let Ok((remaining, (status, value))) = parse_inline_task(input) {
80        return Ok((remaining, Item::Task(Prefix::None, status, value)));
81    }
82    let (input, _) = parse_contents(input)?;
83    Ok((
84        input,
85        Item::Value(Prefix::None, Arc::from(input.trim_end())),
86    ))
87}
88
89fn parse_task(input: &str) -> IResult<&str, (Prefix, Option<bool>, Arc<str>)> {
90    let (input, prefix) = opt(alt((parse_ordered, parse_unordered))).parse(input)?;
91    let prefix = prefix.unwrap_or(Prefix::None);
92    let (input, _) = multispace0(input)?;
93    let (input, status) = parse_checkbox(input)?;
94    let (input, _) = parse_contents(input)?;
95
96    Ok((input, (prefix, status, Arc::from(input.trim_end()))))
97}
98
99fn parse_value(input: &str) -> IResult<&str, (Prefix, Arc<str>)> {
100    let (input, prefix) = opt(alt((parse_ordered, parse_unordered))).parse(input)?;
101    let prefix = prefix.unwrap_or(Prefix::None);
102    let (input, _) = parse_contents(input)?;
103
104    Ok((input, (prefix, Arc::from(input.trim_end()))))
105}
106
107/// Parse a full item line.
108///
109/// Prefers `Task` (checkbox after optional prefix); falls back to `Value`.
110pub fn parse_item(input: &str) -> IResult<&str, Item> {
111    // Try to parse as task first (look for box)
112    if let Ok((remaining, (prefix, status, value))) = parse_task(input) {
113        return Ok((remaining, Item::Task(prefix, status, value)));
114    }
115
116    // Otherwise parse as value
117    let (input, (prefix, value)) = parse_value(input)?;
118    Ok((input, Item::Value(prefix, value)))
119}