1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum PhpTokenCategory {
10 OpeningTag,
11 ClosingTag,
12 Keyword,
13 Identifier,
14 Variable,
15 Literal,
16 Operator,
17 Delimiter,
18 Comment,
19 Whitespace,
20 Unknown,
21}
22
23impl PhpTokenCategory {
24 pub const fn as_str(self) -> &'static str {
25 match self {
26 Self::OpeningTag => "opening-tag",
27 Self::ClosingTag => "closing-tag",
28 Self::Keyword => "keyword",
29 Self::Identifier => "identifier",
30 Self::Variable => "variable",
31 Self::Literal => "literal",
32 Self::Operator => "operator",
33 Self::Delimiter => "delimiter",
34 Self::Comment => "comment",
35 Self::Whitespace => "whitespace",
36 Self::Unknown => "unknown",
37 }
38 }
39}
40
41impl fmt::Display for PhpTokenCategory {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 formatter.write_str(self.as_str())
44 }
45}
46
47impl FromStr for PhpTokenCategory {
48 type Err = PhpTokenError;
49
50 fn from_str(input: &str) -> Result<Self, Self::Err> {
51 match normalized_label(input)?.as_str() {
52 "openingtag" => Ok(Self::OpeningTag),
53 "closingtag" => Ok(Self::ClosingTag),
54 "keyword" => Ok(Self::Keyword),
55 "identifier" => Ok(Self::Identifier),
56 "variable" => Ok(Self::Variable),
57 "literal" => Ok(Self::Literal),
58 "operator" => Ok(Self::Operator),
59 "delimiter" => Ok(Self::Delimiter),
60 "comment" => Ok(Self::Comment),
61 "whitespace" => Ok(Self::Whitespace),
62 "unknown" => Ok(Self::Unknown),
63 _ => Err(PhpTokenError::UnknownLabel),
64 }
65 }
66}
67
68#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
70pub enum PhpDelimiter {
71 OpenParen,
72 CloseParen,
73 OpenBrace,
74 CloseBrace,
75 OpenBracket,
76 CloseBracket,
77 Semicolon,
78 Comma,
79 Colon,
80 DoubleColon,
81 Arrow,
82 NamespaceSeparator,
83}
84
85impl PhpDelimiter {
86 pub const fn as_str(self) -> &'static str {
87 match self {
88 Self::OpenParen => "(",
89 Self::CloseParen => ")",
90 Self::OpenBrace => "{",
91 Self::CloseBrace => "}",
92 Self::OpenBracket => "[",
93 Self::CloseBracket => "]",
94 Self::Semicolon => ";",
95 Self::Comma => ",",
96 Self::Colon => ":",
97 Self::DoubleColon => "::",
98 Self::Arrow => "->",
99 Self::NamespaceSeparator => "\\",
100 }
101 }
102}
103
104impl fmt::Display for PhpDelimiter {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 formatter.write_str(self.as_str())
107 }
108}
109
110#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
112pub enum PhpOperator {
113 Assign,
114 Plus,
115 Minus,
116 Multiply,
117 Divide,
118 Modulo,
119 Equal,
120 Identical,
121 NotEqual,
122 NotIdentical,
123 Spaceship,
124 NullCoalesce,
125 Elvis,
126}
127
128impl PhpOperator {
129 pub const fn as_str(self) -> &'static str {
130 match self {
131 Self::Assign => "=",
132 Self::Plus => "+",
133 Self::Minus => "-",
134 Self::Multiply => "*",
135 Self::Divide => "/",
136 Self::Modulo => "%",
137 Self::Equal => "==",
138 Self::Identical => "===",
139 Self::NotEqual => "!=",
140 Self::NotIdentical => "!==",
141 Self::Spaceship => "<=>",
142 Self::NullCoalesce => "??",
143 Self::Elvis => "?:",
144 }
145 }
146}
147
148impl fmt::Display for PhpOperator {
149 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150 formatter.write_str(self.as_str())
151 }
152}
153
154#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
156pub enum PhpLiteralKind {
157 String,
158 Integer,
159 Float,
160 Boolean,
161 Null,
162 Array,
163 Heredoc,
164 Nowdoc,
165}
166
167impl PhpLiteralKind {
168 pub const fn as_str(self) -> &'static str {
169 match self {
170 Self::String => "string",
171 Self::Integer => "integer",
172 Self::Float => "float",
173 Self::Boolean => "boolean",
174 Self::Null => "null",
175 Self::Array => "array",
176 Self::Heredoc => "heredoc",
177 Self::Nowdoc => "nowdoc",
178 }
179 }
180}
181
182#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
184pub enum PhpCommentKind {
185 Line,
186 Block,
187 Docblock,
188 HashLine,
189}
190
191impl PhpCommentKind {
192 pub const fn as_str(self) -> &'static str {
193 match self {
194 Self::Line => "line",
195 Self::Block => "block",
196 Self::Docblock => "docblock",
197 Self::HashLine => "hash-line",
198 }
199 }
200}
201
202#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
204pub struct PhpTokenText(String);
205
206impl PhpTokenText {
207 pub fn new(input: &str) -> Result<Self, PhpTokenError> {
208 let trimmed = input.trim();
209 if trimmed.is_empty() {
210 Err(PhpTokenError::Empty)
211 } else {
212 Ok(Self(trimmed.to_string()))
213 }
214 }
215
216 pub fn as_str(&self) -> &str {
217 &self.0
218 }
219}
220
221impl fmt::Display for PhpTokenText {
222 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
223 formatter.write_str(self.as_str())
224 }
225}
226
227impl FromStr for PhpTokenText {
228 type Err = PhpTokenError;
229
230 fn from_str(input: &str) -> Result<Self, Self::Err> {
231 Self::new(input)
232 }
233}
234
235#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
237pub struct PhpTokenSpan {
238 start: usize,
239 end: usize,
240}
241
242impl PhpTokenSpan {
243 pub const fn new(start: usize, end: usize) -> Result<Self, PhpTokenError> {
244 if end < start {
245 Err(PhpTokenError::InvalidSpan)
246 } else {
247 Ok(Self { start, end })
248 }
249 }
250
251 pub const fn start(self) -> usize {
252 self.start
253 }
254
255 pub const fn end(self) -> usize {
256 self.end
257 }
258
259 pub const fn len(self) -> usize {
260 self.end - self.start
261 }
262
263 pub const fn is_empty(self) -> bool {
264 self.start == self.end
265 }
266}
267
268#[derive(Clone, Debug, Eq, PartialEq)]
270pub struct PhpToken {
271 category: PhpTokenCategory,
272 text: PhpTokenText,
273 span: PhpTokenSpan,
274}
275
276impl PhpToken {
277 pub const fn new(category: PhpTokenCategory, text: PhpTokenText, span: PhpTokenSpan) -> Self {
278 Self {
279 category,
280 text,
281 span,
282 }
283 }
284
285 pub const fn category(&self) -> PhpTokenCategory {
286 self.category
287 }
288
289 pub const fn text(&self) -> &PhpTokenText {
290 &self.text
291 }
292
293 pub const fn span(&self) -> PhpTokenSpan {
294 self.span
295 }
296}
297
298#[derive(Clone, Copy, Debug, Eq, PartialEq)]
300pub enum PhpTokenError {
301 Empty,
302 InvalidSpan,
303 UnknownLabel,
304}
305
306impl fmt::Display for PhpTokenError {
307 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
308 match self {
309 Self::Empty => formatter.write_str("PHP token metadata cannot be empty"),
310 Self::InvalidSpan => formatter.write_str("PHP token span end cannot precede start"),
311 Self::UnknownLabel => formatter.write_str("unknown PHP token metadata label"),
312 }
313 }
314}
315
316impl Error for PhpTokenError {}
317
318fn normalized_label(input: &str) -> Result<String, PhpTokenError> {
319 let trimmed = input.trim();
320 if trimmed.is_empty() {
321 Err(PhpTokenError::Empty)
322 } else {
323 Ok(trimmed.to_ascii_lowercase().replace(['-', '_', ' '], ""))
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::{
330 PhpDelimiter, PhpOperator, PhpToken, PhpTokenCategory, PhpTokenError, PhpTokenSpan,
331 PhpTokenText,
332 };
333
334 #[test]
335 fn builds_token_metadata() -> Result<(), PhpTokenError> {
336 let token = PhpToken::new(
337 PhpTokenCategory::Variable,
338 PhpTokenText::new(" $value ")?,
339 PhpTokenSpan::new(4, 10)?,
340 );
341
342 assert_eq!(token.category(), PhpTokenCategory::Variable);
343 assert_eq!(token.text().as_str(), "$value");
344 assert_eq!(token.span().len(), 6);
345 Ok(())
346 }
347
348 #[test]
349 fn exposes_operator_and_delimiter_labels() {
350 assert_eq!(PhpDelimiter::DoubleColon.to_string(), "::");
351 assert_eq!(PhpOperator::Spaceship.to_string(), "<=>");
352 }
353}