aimx/inference/
item.rs

1//! Inference item parsing for Agentic Inference Markup (AIM) format.
2//!
3//! This module provides functionality for parsing inference data items, which represent
4//! individual elements in AIM files that can be either simple values or tasks with status.
5//! Items support various prefix styles including ordered lists, unordered lists, and tasks
6//! with checkbox status.
7//!
8//! # Item Grammar
9//! The item grammar supports several patterns:
10//! - **Value items**: `(ordered|unordered)? value` (e.g., `1. First item`, `- Bullet point`)
11//! - **Task items**: `(ordered|unordered)? [status] value` (e.g., `[x] Completed task`, `[ ] Pending task`)
12//! - **Inline items**: Simplified parsing without prefixes for inline contexts
13//!
14//! # Status Indicators
15//! Task items support status indicators:
16//! - `[x]` or `[X]` or `[+]` - Completed task
17//! - `[-]` - Failed task  
18//! - `[ ]` - Pending task (no status character)
19//!
20//! # Examples
21//! ```text
22//! // Value items
23//! 1. First ordered item
24//! - Unordered bullet point
25//! Simple value without prefix
26//!
27//! // Task items
28//! [x] Completed task
29//! [-] Failed task
30//! [ ] Pending task
31//! 1. [x] Ordered completed task
32//! - [ ] Unordered pending task
33//! ```
34//!
35//! The parser recognizes these patterns to properly structure inference data and task tracking.
36
37use nom::{
38    error::{Error, ErrorKind},
39     branch::alt,
40    character::complete::{char, digit1, multispace0, one_of},
41    combinator::opt,
42    sequence::{delimited, preceded},
43    IResult, Parser,
44};
45use crate::Prefix;
46
47/// Represents an inference data item parsed from AIM files.
48///
49/// Items can be either simple values or tasks with status indicators.
50/// Each item may have an optional prefix style (ordered, unordered, or none)
51/// and for tasks, an optional status indicating completion state.
52#[derive(Debug, PartialEq, Clone)]
53pub enum Item {
54    /// A simple value item without task status.
55    ///
56    /// Contains a prefix style and the item's text content.
57    ///
58    /// # Examples
59    /// ```text
60    /// 1. Ordered value
61    /// - Unordered value
62    /// Simple value
63    /// ```
64    Value(Prefix, String),
65    
66    /// A task item with optional completion status.
67    ///
68    /// Contains a prefix style, optional status (Some(true) = completed,
69    /// Some(false) = failed, None = pending), and the task text.
70    ///
71    /// # Examples
72    /// ```text
73    /// [x] Completed task
74    /// [-] Failed task
75    /// [ ] Pending task
76    /// 1. [x] Ordered completed task
77    /// ```
78    Task(Prefix, Option<bool>, String),
79}
80
81/// Parse non-empty content from the input.
82///
83/// This helper function ensures that there is actual content to parse.
84/// It consumes optional whitespace and fails if the input is empty.
85///
86/// # Grammar
87/// ```text
88/// contents = WS? <any>+ EOL
89/// ```
90///
91/// # Arguments
92/// * `input` - The input string to parse
93///
94/// # Returns
95/// Returns `Ok((remaining, ()))` if content exists, or `Err` if input is empty
96fn parse_contents(input: &str) -> IResult<&str, ()> {
97    let (input, _) = multispace0(input)?;
98    if input.is_empty() {
99        Err(nom::Err::Error(Error::new(input, ErrorKind::Fail)))
100    } else {
101        Ok((input, ()))
102    }
103}
104
105/// Parse an ordered list marker (e.g., "1.", "2.", etc.).
106///
107/// # Grammar
108/// ```text
109/// ordered = WS? digit+ '.'
110/// ```
111///
112/// # Examples
113/// ```text
114/// 1. First item
115/// 42. Another item
116/// ```
117///
118/// # Arguments
119/// * `input` - The input string to parse
120///
121/// # Returns
122/// Returns `IResult` containing remaining input and `Prefix::Ordered`
123fn ordered(input: &str) -> IResult<&str, Prefix> {
124    let (input, _) = multispace0(input)?;
125    let (input, _) = digit1(input)?;
126    let (input, _) = char('.')(input)?;
127    Ok((input, Prefix::Ordered))
128}
129
130/// Parse an unordered list marker (e.g., "-").
131///
132/// # Grammar
133/// ```text
134/// unordered = WS? '-'
135/// ```
136///
137/// # Examples
138/// ```text
139/// - Bullet point
140/// - Another item
141/// ```
142///
143/// # Arguments
144/// * `input` - The input string to parse
145///
146/// # Returns
147/// Returns `IResult` containing remaining input and `Prefix::Unordered`
148fn unordered(input: &str) -> IResult<&str, Prefix> {
149    let (input, _) = multispace0(input)?;
150    let (input, _) = char('-')(input)?;
151    Ok((input, Prefix::Unordered))
152}
153
154/// Parse a checkbox status indicator.
155///
156/// # Grammar
157/// ```text
158/// checkbox = WS? '[' (WS? ('X'|'x'|'+'|'-')? WS? ']'
159/// ```
160///
161/// # Status Mapping
162/// - `[x]`, `[X]`, `[+]` → `Some(true)` (completed)
163/// - `[-]` → `Some(false)` (failed)
164/// - `[ ]` → `None` (pending)
165///
166/// # Arguments
167/// * `input` - The input string to parse
168///
169/// # Returns
170/// Returns `IResult` containing remaining input and optional boolean status
171fn checkbox(input: &str) -> IResult<&str, Option<bool>> {
172    let (input, _) = multispace0(input)?;
173    let (input, status_char) = delimited(
174        char('['),
175        opt(preceded(multispace0, one_of("Xx+-"))),
176        preceded(multispace0, char(']')),
177    ).parse(input)?;
178    let status: Option<bool> = match status_char {
179        Some('X') | Some('x') | Some('+') => Some(true),
180        Some('-') => Some(false),
181        _ => None,
182    };
183    Ok((input, status))
184}
185
186/// Parse an inline task (task without prefix markers).
187///
188/// This function parses tasks that appear inline, typically without
189/// ordered/unordered list prefixes.
190///
191/// # Grammar
192/// ```text
193/// inline_task = checkbox WS? value
194/// ```
195///
196/// # Arguments
197/// * `input` - The input string to parse
198///
199/// # Returns
200/// Returns `IResult` containing remaining input and tuple of (status, value)
201fn parse_inline_task(input: &str) -> IResult<&str, (Option<bool>, String)> {
202    let (input, status) = checkbox(input)?;
203    let (input, _) = multispace0(input)?;
204    let content = input.trim_end().to_string();
205    if content.is_empty() {
206        return Err(nom::Err::Error(Error::new(input, ErrorKind::Fail)));
207    }
208    Ok(("", (status, content)))
209}
210
211/// Parse an inline inference item.
212///
213/// This function is designed for parsing items in inline contexts where
214/// list prefixes (ordered/unordered) are not expected. It first attempts
215/// to parse as a task (looking for checkbox syntax), and falls back to
216/// parsing as a simple value.
217///
218/// # Grammar
219/// ```text
220/// inline_item = inline_task | value
221/// ```
222///
223/// # Examples
224/// ```rust
225/// use aimx::{inference::{parse_inline_item, Item}, writer::Prefix};
226///
227/// // Parse inline task
228/// assert_eq!(
229///     parse_inline_item("[x] Complete task"),
230///     Ok(("", Item::Task(Prefix::None, Some(true), "Complete task".to_string())))
231/// );
232///
233/// // Parse inline value
234/// assert_eq!(
235///     parse_inline_item("Simple value"),
236///     Ok(("Simple value", Item::Value(Prefix::None, "Simple value".to_string())))
237/// );
238/// ```
239///
240/// # Arguments
241/// * `input` - The input string to parse
242///
243/// # Returns
244/// Returns `IResult` containing remaining input and parsed `Item`
245pub fn parse_inline_item(input: &str) -> IResult<&str, Item> {
246    // Try to parse inline task first (look for box)
247    if let Ok((remaining, (status, value))) = parse_inline_task(input) {
248        return Ok((remaining, Item::Task(Prefix::None, status, value)));
249    }
250    let (input, _) = parse_contents(input)?;
251    Ok((input, Item::Value(Prefix::None, input.trim_end().to_string())))
252}
253
254/// Parse a task item with optional prefix and status.
255///
256/// # Grammar
257/// ```text
258/// task = (ordered|unordered)? WS? checkbox WS? value
259/// ```
260///
261/// # Arguments
262/// * `input` - The input string to parse
263///
264/// # Returns
265/// Returns `IResult` containing remaining input and tuple of (prefix, status, value)
266fn parse_task(input: &str) -> IResult<&str, (Prefix, Option<bool>, String)> {
267    let (input, prefix) = opt(alt((ordered, unordered))).parse(input)?;
268    let prefix = prefix.unwrap_or(Prefix::None);
269    let (input, _) = multispace0(input)?;
270    let (input, status) = checkbox(input)?;
271    let (input, _) = parse_contents(input)?;
272
273    Ok((input, (prefix, status, input.trim_end().to_string())))
274}
275
276/// Parse a value item with optional prefix.
277///
278/// # Grammar
279/// ```text
280/// value = (ordered|unordered)? WS? value
281/// ```
282///
283/// # Arguments
284/// * `input` - The input string to parse
285///
286/// # Returns
287/// Returns `IResult` containing remaining input and tuple of (prefix, value)
288fn parse_value(input: &str) -> IResult<&str, (Prefix, String)> {
289    let (input, prefix) = opt(alt((ordered, unordered))).parse(input)?;
290    let prefix = prefix.unwrap_or(Prefix::None);
291    let (input, _) = parse_contents(input)?;
292
293    Ok((input, (prefix, input.trim_end().to_string())))
294}
295
296/// Parse a complete inference item.
297///
298/// This is the main parsing function for inference items. It first attempts
299/// to parse as a task (looking for checkbox syntax), and falls back to
300/// parsing as a simple value if no checkbox is found.
301///
302/// # Grammar
303/// ```text
304/// item = task | value
305/// ```
306///
307/// # Examples
308/// ```rust
309/// use aimx::{inference::{parse_item, Item}, writer::Prefix};
310///
311/// // Parse task with ordered prefix
312/// assert_eq!(
313///     parse_item("1. [x] Completed task"),
314///     Ok(("Completed task", Item::Task(Prefix::Ordered, Some(true), "Completed task".to_string())))
315/// );
316///
317/// // Parse task with unordered prefix
318/// assert_eq!(
319///     parse_item("- [ ] Pending task"),
320///     Ok(("Pending task", Item::Task(Prefix::Unordered, None, "Pending task".to_string())))
321/// );
322///
323/// // Parse value with ordered prefix
324/// assert_eq!(
325///     parse_item("1. First item"),
326///     Ok(("First item", Item::Value(Prefix::Ordered, "First item".to_string())))
327/// );
328///
329/// // Parse simple value without prefix
330/// assert_eq!(
331///     parse_item("Simple value"),
332///     Ok(("Simple value", Item::Value(Prefix::None, "Simple value".to_string())))
333/// );
334/// ```
335///
336/// # Arguments
337/// * `input` - The input string to parse
338///
339/// # Returns
340/// Returns `IResult` containing remaining input and parsed `Item`
341pub fn parse_item(input: &str) -> IResult<&str, Item> {
342    // Try to parse as task first (look for box)
343    if let Ok((remaining, (prefix, status, value))) = parse_task(input) {
344        return Ok((remaining, Item::Task(prefix, status, value)));
345    }
346    
347    // Otherwise parse as value
348    let (input, (prefix, value)) = parse_value(input)?;
349    Ok((input, Item::Value(prefix, value)))
350}