Skip to main content

chordsketch_core/
token.rs

1//! Token and span types for the ChordPro lexer.
2//!
3//! This module defines the token types produced by the lexer. Tokens represent
4//! the smallest meaningful units in a ChordPro document. The lexer does not
5//! understand the structure of the document (that is the parser's job); it only
6//! identifies individual tokens and their positions.
7
8/// A position in the source text, identified by line and column numbers.
9///
10/// Both `line` and `column` are 1-based, matching the conventions used by
11/// editors and error messages.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct Position {
14    /// 1-based line number.
15    pub line: usize,
16    /// 1-based column number (in characters, not bytes).
17    pub column: usize,
18}
19
20impl Position {
21    /// Creates a new `Position` with the given line and column.
22    #[must_use]
23    pub fn new(line: usize, column: usize) -> Self {
24        Self { line, column }
25    }
26}
27
28/// A span in the source text, defined by a start and end position.
29///
30/// The start position is inclusive and the end position is exclusive, following
31/// the convention of half-open intervals.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Span {
34    /// The start position (inclusive).
35    pub start: Position,
36    /// The end position (exclusive).
37    pub end: Position,
38}
39
40impl Span {
41    /// Creates a new `Span` from the given start and end positions.
42    #[must_use]
43    pub fn new(start: Position, end: Position) -> Self {
44        Self { start, end }
45    }
46}
47
48/// The kind of a token.
49///
50/// These represent the distinct syntactic elements that the lexer recognizes
51/// in a ChordPro document.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum TokenKind {
54    /// Opening brace `{` — starts a directive.
55    DirectiveOpen,
56    /// Closing brace `}` — ends a directive.
57    DirectiveClose,
58    /// Opening bracket `[` — starts a chord annotation.
59    ChordOpen,
60    /// Closing bracket `]` — ends a chord annotation.
61    ChordClose,
62    /// Colon `:` — separates a directive name from its value.
63    ///
64    /// Only emitted when the lexer is inside a directive (between `{` and `}`).
65    Colon,
66    /// A run of text content (lyrics, directive names, directive values, chord
67    /// names, etc.).
68    ///
69    /// The lexer does not interpret text — it simply captures contiguous runs
70    /// of characters that are not special delimiters.
71    Text(String),
72    /// A newline character (`\n` or `\r\n`).
73    Newline,
74    /// End of input.
75    Eof,
76}
77
78/// A token produced by the lexer.
79///
80/// Each token carries its [`TokenKind`] and the [`Span`] that locates it in
81/// the original source text.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct Token {
84    /// The kind of this token.
85    pub kind: TokenKind,
86    /// The location of this token in the source text.
87    pub span: Span,
88}
89
90impl Token {
91    /// Creates a new `Token` with the given kind and span.
92    #[must_use]
93    pub fn new(kind: TokenKind, span: Span) -> Self {
94        Self { kind, span }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn position_new() {
104        let pos = Position::new(1, 5);
105        assert_eq!(pos.line, 1);
106        assert_eq!(pos.column, 5);
107    }
108
109    #[test]
110    fn span_new() {
111        let span = Span::new(Position::new(1, 1), Position::new(1, 5));
112        assert_eq!(span.start, Position::new(1, 1));
113        assert_eq!(span.end, Position::new(1, 5));
114    }
115
116    #[test]
117    fn token_new() {
118        let span = Span::new(Position::new(1, 1), Position::new(1, 2));
119        let token = Token::new(TokenKind::DirectiveOpen, span);
120        assert_eq!(token.kind, TokenKind::DirectiveOpen);
121        assert_eq!(token.span, span);
122    }
123
124    #[test]
125    fn token_kind_text_equality() {
126        let a = TokenKind::Text("hello".to_string());
127        let b = TokenKind::Text("hello".to_string());
128        let c = TokenKind::Text("world".to_string());
129        assert_eq!(a, b);
130        assert_ne!(a, c);
131    }
132}