1use crate::diagnostic::{SourcePosition, Span};
4use crate::lexer;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Document {
9 tokens: Vec<Token>,
10}
11
12impl Document {
13 pub(crate) fn from_lexer_tokens(source: &str, tokens: Vec<lexer::Token>) -> Self {
14 let mut lossless_tokens = Vec::new();
15 let mut cursor = SourcePosition::start();
16
17 for token in tokens {
18 append_trivia(source, cursor, token.span.start, &mut lossless_tokens);
19
20 let span = token.span;
21 let text = source[span.start.byte_offset..span.end.byte_offset].to_owned();
22 let kind = match token.kind {
23 lexer::TokenKind::Bare(_) => TokenKind::Bare,
24 lexer::TokenKind::String(value) => TokenKind::String(value),
25 lexer::TokenKind::LeftBrace => TokenKind::LeftBrace,
26 lexer::TokenKind::RightBrace => TokenKind::RightBrace,
27 lexer::TokenKind::LeftBracket => TokenKind::LeftBracket,
28 lexer::TokenKind::RightBracket => TokenKind::RightBracket,
29 lexer::TokenKind::Comma => TokenKind::Comma,
30 lexer::TokenKind::Dot => TokenKind::Dot,
31 lexer::TokenKind::ForwardArrow => TokenKind::ForwardArrow,
32 lexer::TokenKind::BidirectionalArrow => TokenKind::BidirectionalArrow,
33 lexer::TokenKind::Association => TokenKind::Association,
34 lexer::TokenKind::End => TokenKind::End,
35 };
36 lossless_tokens.push(Token { kind, text, span });
37 cursor = span.end;
38 }
39
40 Self {
41 tokens: lossless_tokens,
42 }
43 }
44
45 pub fn tokens(&self) -> &[Token] {
47 &self.tokens
48 }
49
50 pub fn reconstruct(&self) -> String {
52 let mut source = String::new();
53 for token in &self.tokens {
54 source.push_str(&token.text);
55 }
56 source
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Token {
63 pub kind: TokenKind,
65 pub text: String,
67 pub span: Span,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum TokenKind {
74 Whitespace,
76 LineComment,
78 Bare,
80 String(String),
82 LeftBrace,
84 RightBrace,
86 LeftBracket,
88 RightBracket,
90 Comma,
92 Dot,
94 ForwardArrow,
96 BidirectionalArrow,
98 Association,
100 End,
102}
103
104fn append_trivia(
105 source: &str,
106 mut position: SourcePosition,
107 end: SourcePosition,
108 tokens: &mut Vec<Token>,
109) {
110 while position.byte_offset < end.byte_offset {
111 let start = position;
112 let is_comment = source[position.byte_offset..end.byte_offset].starts_with("//");
113
114 if is_comment {
115 while position.byte_offset < end.byte_offset
116 && next_character(source, position.byte_offset)
117 .is_some_and(|character| !matches!(character, '\n' | '\r'))
118 {
119 advance_position(source, &mut position);
120 }
121 } else {
122 while position.byte_offset < end.byte_offset
123 && !source[position.byte_offset..end.byte_offset].starts_with("//")
124 {
125 advance_position(source, &mut position);
126 }
127 }
128
129 let text = source[start.byte_offset..position.byte_offset].to_owned();
130 tokens.push(Token {
131 kind: if is_comment {
132 TokenKind::LineComment
133 } else {
134 TokenKind::Whitespace
135 },
136 text,
137 span: Span {
138 start,
139 end: position,
140 },
141 });
142 }
143}
144
145fn next_character(source: &str, offset: usize) -> Option<char> {
146 source[offset..].chars().next()
147}
148
149fn advance_position(source: &str, position: &mut SourcePosition) {
150 let Some(character) = next_character(source, position.byte_offset) else {
151 return;
152 };
153
154 if character == '\r' && source[position.byte_offset..].starts_with("\r\n") {
155 position.byte_offset += 2;
156 position.line += 1;
157 position.column = 1;
158 } else {
159 position.byte_offset += character.len_utf8();
160 if matches!(character, '\n' | '\r') {
161 position.line += 1;
162 position.column = 1;
163 } else {
164 position.column += 1;
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::{Document, TokenKind};
172 use crate::lexer::tokenize;
173
174 #[test]
175 fn preserves_every_lexeme_and_decoded_string_value() {
176 let source = " \t// lead\r\nword \"\\u0041\" {}[],. a->b a<->b a--b // tail";
177 let result = tokenize(source);
178 assert!(result.is_ok(), "{result:?}");
179 let tokens: Vec<_> = result.into_iter().flatten().collect();
180 let document = Document::from_lexer_tokens(source, tokens);
181
182 assert_eq!(document.reconstruct(), source);
183 assert_eq!(document.tokens[0].kind, TokenKind::Whitespace);
184 assert_eq!(document.tokens[0].text, " \t");
185 assert_eq!(document.tokens[1].kind, TokenKind::LineComment);
186 assert_eq!(document.tokens[1].text, "// lead");
187 assert_eq!(document.tokens[1].span.start.line, 1);
188 assert_eq!(document.tokens[2].text, "\r\n");
189 assert_eq!(document.tokens[2].span.end.line, 2);
190 assert!(document.tokens.iter().any(
191 |token| matches!(&token.kind, TokenKind::String(value) if value == "A")
192 && token.text == "\"\\u0041\""
193 ));
194
195 for expected in [
196 TokenKind::LeftBrace,
197 TokenKind::RightBrace,
198 TokenKind::LeftBracket,
199 TokenKind::RightBracket,
200 TokenKind::Comma,
201 TokenKind::Dot,
202 TokenKind::ForwardArrow,
203 TokenKind::BidirectionalArrow,
204 TokenKind::Association,
205 TokenKind::End,
206 ] {
207 assert!(document.tokens.iter().any(|token| token.kind == expected));
208 }
209
210 let mut byte_offset = 0;
211 for token in document.tokens() {
212 assert_eq!(token.span.start.byte_offset, byte_offset);
213 byte_offset = token.span.end.byte_offset;
214 }
215 assert_eq!(byte_offset, source.len());
216 }
217
218 #[test]
219 fn defensive_position_advance_stops_at_end_of_source() {
220 let mut position = crate::diagnostic::SourcePosition {
221 byte_offset: 0,
222 line: 1,
223 column: 1,
224 };
225 super::advance_position("", &mut position);
226 assert_eq!(position.byte_offset, 0);
227 }
228}