1pub(crate) use std::collections::HashMap;
2use std::sync::OnceLock;
3
4pub(crate) use crate::core::signatures::ExpectedDomain;
5pub(crate) use crate::core::source::{Position, SourceFile, Span};
6use crate::program::Program;
7pub(crate) use crate::settings::table::KeyKind;
8pub(crate) use crate::settings::{PathPart, table};
9pub(crate) use crate::settings::{Settings, SettingsListElement, SettingsNode};
10pub(crate) use crate::wir::{
11 self, Action, Event, EventTarget, EventTeam, ModifyOp, PlayerEventKind, Value, ValueNode,
12};
13
14pub(crate) use super::lexer::{Token, TokenKind, tokenize};
15pub(crate) use crate::catalog::{Catalog, Kind, Locale};
16pub(crate) use crate::core::error::{Result, WorkshopError};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub(crate) enum Stop {
20 SectionClosed,
21 End,
22 ElseIf,
23 Else,
24}
25
26pub(crate) enum AssignmentOperator {
27 Set,
28 Modify(ModifyOp),
29}
30
31pub fn parse(input: &str, catalog: &Catalog, locale: &Locale) -> Result<Program> {
37 parse_with_context(input, catalog, locale, catalog)
38}
39
40#[cfg(test)]
41pub(crate) fn parse_wir(input: &str, catalog: &Catalog, locale: &Locale) -> Result<wir::Program> {
42 parse_wir_with_context(input, catalog, locale, catalog)
43}
44
45pub fn parse_with_context(
56 input: &str,
57 catalog: &Catalog,
58 locale: &Locale,
59 context: &dyn ExpectedDomain,
60) -> Result<Program> {
61 parse_wir_with_context(input, catalog, locale, context).and_then(Program::from_wir)
62}
63
64pub(crate) fn parse_wir_with_context(
65 input: &str,
66 catalog: &Catalog,
67 locale: &Locale,
68 context: &dyn ExpectedDomain,
69) -> Result<wir::Program> {
70 let tokens = tokenize(input).map_err(|error| {
71 WorkshopError::malformed(error.message, Some(synthetic_span(error.position)))
72 })?;
73 ParseContext {
74 tokens,
75 pos: 0,
76 source: input,
77 catalog,
78 locale: locale.clone(),
79 context,
80 expected_domain: None,
81 condition_pending: false,
82 in_condition: false,
83 call_stack: Vec::new(),
84 target: wir::Program::default(),
85 globals: HashMap::new(),
86 players: HashMap::new(),
87 subroutines: HashMap::new(),
88 next_global_index: 0,
89 next_player_index: 0,
90 }
91 .program()
92}
93
94pub(crate) fn synthetic_span(position: Position) -> Span {
95 Span::new(crate::core::ids::Id::from_index(0), position, position)
96}
97
98pub(crate) struct ParseContext<'a> {
99 pub(crate) tokens: Vec<Token>,
100 pub(crate) pos: usize,
101 pub(crate) source: &'a str,
102 pub(crate) catalog: &'a Catalog,
103 pub(crate) locale: Locale,
104 pub(crate) context: &'a dyn ExpectedDomain,
105 pub(crate) expected_domain: Option<&'a str>,
106 pub(crate) condition_pending: bool,
107 pub(crate) in_condition: bool,
108 pub(crate) call_stack: Vec<String>,
109 pub(crate) target: wir::Program,
110 pub(crate) globals: HashMap<String, wir::GlobalVarId>,
111 pub(crate) players: HashMap<String, wir::PlayerVarId>,
112 pub(crate) subroutines: HashMap<String, wir::SubroutineId>,
113 pub(crate) next_global_index: u32,
114 pub(crate) next_player_index: u32,
115}
116
117impl<'a> ParseContext<'a> {
118 pub(crate) fn with_domain<T>(
119 &mut self,
120 domain: Option<&'a str>,
121 f: impl FnOnce(&mut Self) -> Result<T>,
122 ) -> Result<T> {
123 let saved = self.expected_domain;
124 self.expected_domain = domain;
125 let result = f(self);
126 self.expected_domain = saved;
127 result
128 }
129
130 pub(crate) fn value_in_domain(&mut self, domain: Option<&'a str>) -> Result<wir::ValueId> {
131 self.with_domain(domain, |p| p.value())
132 }
133
134 pub(crate) fn resolve_entry(
135 &self,
136 kind: Kind,
137 spelling: &str,
138 ) -> Option<&'a crate::catalog::CatalogEntry> {
139 self.catalog
140 .resolve(kind, &self.locale, spelling)
141 .or_else(|| {
142 if self.locale != *self.catalog.primary_locale() {
143 self.catalog
144 .resolve(kind, self.catalog.primary_locale(), spelling)
145 } else {
146 None
147 }
148 })
149 }
150
151 pub(crate) fn canonical_keyword(&self, spelling: &str) -> String {
152 self.catalog
153 .resolve(Kind::Structural, &self.locale, spelling)
154 .or_else(|| {
155 (self.locale != *self.catalog.primary_locale()).then(|| {
156 self.catalog
157 .resolve(Kind::Structural, self.catalog.primary_locale(), spelling)
158 })?
159 })
160 .map(|entry| entry.id.clone())
161 .unwrap_or_else(|| canonical_keyword(spelling).to_string())
162 }
163
164 pub(crate) fn line_has_assignment(&self) -> bool {
165 let tokens: Vec<_> = self.tokens[self.pos..]
166 .iter()
167 .take_while(|token| !matches!(token.kind, TokenKind::Semi | TokenKind::RBrace))
168 .collect();
169 tokens.iter().any(|token| {
170 matches!(&token.kind, TokenKind::Op(op) if matches!(op.as_str(), "=" | "+=" | "-=" | "*=" | "/=" | "%="))
171 }) || tokens.windows(2).any(|window| {
172 matches!(&window[0].kind, TokenKind::Word(_))
173 && matches!(&window[1].kind, TokenKind::Op(op) if op == "=")
174 })
175 }
176
177 pub(crate) fn phrase(&mut self) -> Result<(String, Position, Position)> {
181 let mut parts: Vec<(String, Position, Position)> = Vec::new();
182 let (start, mut end) = match self.peek() {
183 Some(Token {
184 kind: TokenKind::Word(word),
185 start,
186 end,
187 }) => {
188 parts.push((word.clone(), start, end));
189 (start, end)
190 }
191 Some(Token {
192 kind: TokenKind::Number { text, .. },
193 start,
194 end,
195 }) => {
196 parts.push((text.clone(), start, end));
197 (start, end)
198 }
199 Some(token) => return Err(self.malformed("expected an identifier", &token)),
200 None => return Err(self.malformed("expected an identifier", self.eof())),
201 };
202 self.pos += 1;
203 while let Some(token) = self.peek() {
204 match token {
205 Token {
206 kind: TokenKind::Word(word),
207 start: word_start,
208 end: word_end,
209 } => {
210 if matches!(
211 self.peek_at(1).map(|token| token.kind),
212 Some(TokenKind::Op(equal)) if equal == "="
213 ) {
214 break;
215 }
216 parts.push((word.clone(), word_start, word_end));
217 end = word_end;
218 self.pos += 1;
219 }
220 Token {
224 kind: TokenKind::Number { text, .. },
225 start: number_start,
226 end: number_end,
227 } => {
228 parts.push((text.clone(), number_start, number_end));
229 end = number_end;
230 self.pos += 1;
231 }
232 _ => break,
233 }
234 }
235 Ok((join_as_written(&parts), start, end))
236 }
237
238 pub(crate) fn phrase_on_line(&mut self) -> Result<(String, Position, Position)> {
239 self.phrase_on_line_impl(false)
240 }
241
242 pub(crate) fn settings_list_name(&mut self) -> Result<(String, Position, Position)> {
246 self.phrase_on_line_impl(true)
247 }
248
249 fn phrase_on_line_impl(&mut self, list_name: bool) -> Result<(String, Position, Position)> {
250 let mut parts: Vec<(String, Position, Position)> = Vec::new();
251 let (start, mut end, line) = match self.peek() {
252 Some(Token {
253 kind: TokenKind::Word(word),
254 start,
255 end,
256 }) => {
257 parts.push((word.clone(), start, end));
258 (start, end, start.line)
259 }
260 Some(Token {
261 kind: TokenKind::Number { text, .. },
262 start,
263 end,
264 }) => {
265 parts.push((text.clone(), start, end));
266 (start, end, start.line)
267 }
268 Some(token) => return Err(self.malformed("expected an identifier", &token)),
269 None => return Err(self.malformed("expected an identifier", self.eof())),
270 };
271 self.pos += 1;
272 while let Some(token) = self.peek() {
273 let (text, word_start, word_end) = match token {
274 Token {
275 kind: TokenKind::Word(word),
276 start,
277 end,
278 } => (word.clone(), start, end),
279 Token {
280 kind: TokenKind::Number { text, .. },
281 start,
282 end,
283 } => (text.clone(), start, end),
284 Token {
285 kind: TokenKind::Dot,
286 start,
287 end,
288 } => (".".to_string(), start, end),
289 Token {
290 kind: TokenKind::Op(op),
291 start,
292 end,
293 } if matches!(op.as_str(), "-" | "%") => (op.clone(), start, end),
294 Token {
295 kind: TokenKind::Colon,
296 start,
297 end,
298 } if list_name => (":".to_string(), start, end),
299 _ => break,
300 };
301 if word_start.line != line {
302 break;
303 }
304 parts.push((text, word_start, word_end));
305 end = word_end;
306 self.pos += 1;
307 }
308 Ok((join_as_written(&parts), start, end))
313 }
314
315 pub(crate) fn phrase_on_line_with_colon(&mut self) -> Result<(String, Position, Position)> {
316 let (mut phrase, start, mut end) = self.phrase_on_line()?;
317 if matches!(
318 self.peek(),
319 Some(Token {
320 kind: TokenKind::Colon,
321 ..
322 })
323 ) {
324 let colon_pos = self.pos;
325 self.next();
326 let (rest, _, rest_end) = self.phrase_on_line()?;
327 if matches!(
328 self.peek(),
329 Some(Token {
330 kind: TokenKind::LBrace,
331 ..
332 })
333 ) {
334 phrase.push(':');
335 phrase.push(' ');
336 phrase.push_str(&rest);
337 end = rest_end;
338 } else {
339 self.pos = colon_pos;
340 }
341 }
342 Ok((phrase, start, end))
343 }
344
345 pub(crate) fn enum_member_phrase(&mut self) -> Result<(String, Position, Position)> {
350 let first = self
351 .peek()
352 .ok_or_else(|| self.malformed("expected an enum member", self.eof()))?;
353 let start = first.start;
354 let line = first.start.line;
355 let mut end = first.end;
356 let mut parts: Vec<(String, Position, Position)> = Vec::new();
357 while let Some(token) = self.peek() {
358 if token.start.line != line
363 || matches!(
364 token.kind,
365 TokenKind::RParen | TokenKind::Comma | TokenKind::LParen
366 )
367 {
368 break;
369 }
370 self.pos += 1;
371 end = token.end;
372 parts.push((raw_token_text(&token.kind), token.start, token.end));
373 }
374 if parts.is_empty() {
375 return Err(self.malformed("expected an enum member", &first));
376 }
377 Ok((join_as_written(&parts), start, end))
378 }
379
380 pub(crate) fn line_text(&mut self) -> Result<(String, Option<Span>)> {
386 let mut parts: Vec<(String, Position, Position)> = Vec::new();
387 let mut start = None;
388 let mut end = None;
389 loop {
390 match self.peek() {
391 Some(Token {
392 kind: TokenKind::Semi,
393 ..
394 }) => {
395 self.pos += 1;
396 break;
397 }
398 Some(token) => {
399 start.get_or_insert(token.start);
400 end = Some(token.end);
401 match &token.kind {
402 TokenKind::Word(_)
403 | TokenKind::Number { .. }
404 | TokenKind::Dot
405 | TokenKind::Colon
406 | TokenKind::Op(_) => {}
407 _ => return Err(self.malformed("expected a text line", &token)),
408 }
409 parts.push((raw_token_text(&token.kind), token.start, token.end));
410 self.pos += 1;
411 }
412 None => {
413 return Err(self.malformed("unexpected end of input in line", self.eof()));
414 }
415 }
416 }
417 Ok((
418 join_as_written(&parts),
419 start
420 .zip(end)
421 .map(|(start, end)| Span::new(self.file(), start, end)),
422 ))
423 }
424
425 pub(crate) fn spelling_run(
434 &self,
435 pos: usize,
436 attaches_to: Option<Position>,
437 ) -> Vec<(usize, String, Position)> {
438 let mut candidates = Vec::new();
439 let mut text = String::new();
440 let mut index = pos;
441 let mut prev_end = attaches_to;
442 while let Some(token) = self.tokens.get(index) {
443 let fragment = match &token.kind {
444 TokenKind::Word(word) => word.as_str(),
445 TokenKind::Number { text, .. } => text.as_str(),
446 TokenKind::Dot => ".",
447 TokenKind::Colon => ":",
448 TokenKind::Op(op) if op == "not" => "!",
449 TokenKind::Op(op) if op == "-" => "-",
453 _ => break,
454 };
455 let needs_adjacent = matches!(token.kind, TokenKind::Dot | TokenKind::Colon)
459 || matches!(&token.kind, TokenKind::Op(op) if op == "not");
460 if needs_adjacent && prev_end.is_none_or(|end| token.start != end) {
461 break;
462 }
463 if prev_end.is_some_and(|end| token.start != end) {
464 text.push(' ');
465 }
466 text.push_str(fragment);
467 prev_end = Some(token.end);
468 index += 1;
469 candidates.push((index - pos, text.clone(), token.end));
470 }
471 candidates.reverse();
472 candidates
473 }
474
475 pub(crate) fn catalog_spelling(
482 &mut self,
483 accept: impl Fn(&str) -> bool,
484 ) -> Option<(String, Position, Position)> {
485 let start = self.tokens.get(self.pos)?.start;
486 for (count, text, end) in self.spelling_run(self.pos, None) {
487 if count < 2 {
488 break;
489 }
490 if accept(&text) {
491 self.pos += count;
492 return Some((text, start, end));
493 }
494 }
495 None
496 }
497
498 pub(crate) fn extend_catalog_spelling(
504 &mut self,
505 phrase: &str,
506 phrase_end: Position,
507 accept: impl Fn(&str) -> bool,
508 ) -> Option<(String, Position)> {
509 for (count, tail, end) in self.spelling_run(self.pos, Some(phrase_end)) {
510 let joined = format!("{phrase}{tail}");
511 if accept(&joined) {
512 self.pos += count;
513 return Some((joined, end));
514 }
515 }
516 None
517 }
518
519 pub(crate) fn extend_entry_spelling(
520 &mut self,
521 phrase: &str,
522 phrase_end: Position,
523 kinds: &[Kind],
524 ) -> Option<(String, Position)> {
525 let catalog = self.catalog;
526 let locale = self.locale.clone();
527 let primary = catalog.primary_locale();
528 let kinds = kinds.to_vec();
529 self.extend_catalog_spelling(phrase, phrase_end, |spelling| {
530 kinds.iter().any(|kind| {
531 catalog.resolve(*kind, &locale, spelling).is_some()
532 || (locale != *primary && catalog.resolve(*kind, primary, spelling).is_some())
533 })
534 })
535 }
536
537 pub(crate) fn consume_phrase(&mut self, expected: &str) -> Result<()> {
538 let (phrase, _, _) = self.phrase()?;
539 if phrase != expected {
540 return Err(self.malformed(&format!("expected '{expected}'"), self.previous()));
541 }
542 Ok(())
543 }
544
545 fn structural_spellings(&self, id: &str) -> Vec<String> {
546 let mut spellings = Vec::new();
547 if let Some(entry) = self.catalog.entry(Kind::Structural, id) {
548 spellings.extend(entry.spellings(&self.locale).iter().cloned());
549 let primary = self.catalog.primary_locale();
550 if self.locale != *primary {
551 spellings.extend(entry.spellings(primary).iter().cloned());
552 }
553 }
554 for (spelling, english) in structural_keyword_map() {
555 if self
556 .catalog
557 .resolve(Kind::Structural, self.catalog.primary_locale(), english)
558 .is_some_and(|entry| entry.id == id)
559 {
560 spellings.push(spelling.clone());
561 }
562 }
563 spellings
564 }
565
566 fn match_spelling_tokens(&self, spelling: &str) -> Option<usize> {
567 let mut offset = 0;
568 for piece in spelling.split(' ') {
569 let (word, dotted) = match piece.strip_suffix('.') {
570 Some(word) => (word, true),
571 None => (piece, false),
572 };
573 if !word.is_empty() {
574 match self.peek_at(offset).map(|token| token.kind) {
575 Some(TokenKind::Word(ref candidate)) if table::names_eq(candidate, word) => {}
576 Some(TokenKind::Number { ref text, .. }) if text == word => {}
577 _ => return None,
578 }
579 offset += 1;
580 }
581 if dotted {
582 match self.peek_at(offset).map(|token| token.kind) {
583 Some(TokenKind::Dot) => offset += 1,
584 _ => return None,
585 }
586 }
587 }
588 (offset > 0).then_some(offset)
589 }
590
591 pub(crate) fn keyword_prefix(&mut self, ids: &[&str]) -> Option<String> {
596 let mut best: Option<(String, usize)> = None;
597 for id in ids {
598 for spelling in self.structural_spellings(id) {
599 if let Some(len) = self.match_spelling_tokens(&spelling) {
600 if best.as_ref().is_none_or(|(_, length)| len > *length) {
601 best = Some(((*id).to_string(), len));
602 }
603 }
604 }
605 }
606 if let Some((id, len)) = best {
607 self.pos += len;
608 Some(id)
609 } else {
610 None
611 }
612 }
613
614 pub(crate) fn consume_keyword(&mut self, id: &str) -> Result<()> {
615 if self.keyword_prefix(&[id]).is_some() {
616 return Ok(());
617 }
618 match self.peek() {
619 Some(token) => Err(self.malformed(&format!("expected '{id}'"), &token)),
620 None => Err(self.malformed(&format!("expected '{id}'"), self.eof())),
621 }
622 }
623
624 pub(crate) fn expect_keyword(&mut self, expected: &str) -> Result<Position> {
625 let start = self.peek().map(|token| token.start);
626 if self.keyword_prefix(&[expected]).is_some() {
627 return Ok(start.unwrap_or_else(|| self.previous().start));
628 }
629 match self.next() {
630 Some(token) => Err(self.malformed(&format!("expected '{expected}'"), &token)),
631 None => Err(self.malformed(&format!("expected '{expected}'"), self.eof())),
632 }
633 }
634
635 pub(crate) fn expect(&mut self, kind: TokenKind, message: &str) -> Result<()> {
636 match self.next() {
637 Some(token) if token.kind == kind => Ok(()),
638 Some(token) => Err(self.malformed(message, &token)),
639 None => Err(self.malformed(message, self.eof())),
640 }
641 }
642
643 pub(crate) fn expect_semi(&mut self, message: &str) -> Result<()> {
644 match self.peek() {
645 Some(Token {
646 kind: TokenKind::Semi,
647 ..
648 }) => {
649 self.pos += 1;
650 Ok(())
651 }
652 Some(token) if token.start.line > self.previous().end.line => {
653 let end = self.previous().end;
654 Err(WorkshopError::malformed(
655 message.to_string(),
656 Some(Span::new(self.file(), end, end)),
657 ))
658 }
659 Some(token) => Err(self.malformed(message, &token)),
660 None => Err(self.malformed(message, self.eof())),
661 }
662 }
663
664 pub(crate) fn expect_string(&mut self, message: &str) -> Result<String> {
665 match self.next() {
666 Some(Token {
667 kind: TokenKind::String(content),
668 ..
669 }) => Ok(content),
670 Some(token) => Err(self.malformed(message, &token)),
671 None => Err(self.malformed(message, self.eof())),
672 }
673 }
674
675 pub(crate) fn malformed(&self, message: &str, token: &Token) -> WorkshopError {
676 WorkshopError::malformed(
677 message.to_string(),
678 Some(Span::new(self.file(), token.start, token.end)),
679 )
680 }
681
682 pub(crate) fn unknown(&self, kind: &'static str, spelling: &str) -> WorkshopError {
683 WorkshopError::unknown(kind, spelling.to_string(), self.locale.clone(), None)
684 }
685
686 pub(crate) fn unknown_at(
687 &self,
688 kind: &'static str,
689 spelling: &str,
690 start: Position,
691 end: Position,
692 ) -> WorkshopError {
693 WorkshopError::unknown(
694 kind,
695 spelling.to_string(),
696 self.locale.clone(),
697 Some(Span::new(self.file(), start, end)),
698 )
699 }
700
701 pub(crate) fn accepted_spellings(
702 &self,
703 kind: Kind,
704 include_primary: bool,
705 ) -> impl Iterator<Item = &str> {
706 let catalog = self.catalog;
707 let primary = catalog.primary_locale();
708 let include_primary = include_primary && self.locale != *primary;
709 catalog.entries_of(kind).flat_map(move |entry| {
710 let local: &[String] = entry.spellings(&self.locale);
711 let fallback: &[String] = if include_primary {
712 entry.spellings(primary)
713 } else {
714 &[]
715 };
716 local.iter().chain(fallback).map(String::as_str)
717 })
718 }
719
720 pub(crate) fn accepted_member_spellings<'b>(
721 &'b self,
722 domain: &'b str,
723 include_primary: bool,
724 ) -> impl Iterator<Item = &'b str> {
725 let catalog = self.catalog;
726 let primary = catalog.primary_locale();
727 let include_primary = include_primary && self.locale != *primary;
728 catalog
729 .enum_domain(domain)
730 .into_iter()
731 .flat_map(move |domain| {
732 domain.members.iter().flat_map(move |member| {
733 let local: &[String] = member.spellings(&self.locale);
734 let fallback: &[String] = if include_primary {
735 member.spellings(primary)
736 } else {
737 &[]
738 };
739 local.iter().chain(fallback).map(String::as_str)
740 })
741 })
742 }
743
744 pub(crate) fn spelling_candidates(
745 &self,
746 kind: Kind,
747 spelling: &str,
748 include_primary: bool,
749 ) -> Vec<String> {
750 crate::core::suggest::nearest(
751 spelling,
752 self.accepted_spellings(kind, include_primary),
753 crate::core::suggest::CANDIDATE_LIMIT,
754 )
755 }
756
757 pub(crate) fn member_candidates(
758 &self,
759 domain: &str,
760 spelling: &str,
761 include_primary: bool,
762 ) -> Vec<String> {
763 crate::core::suggest::nearest(
764 spelling,
765 self.accepted_member_spellings(domain, include_primary),
766 crate::core::suggest::CANDIDATE_LIMIT,
767 )
768 }
769
770 pub(crate) fn peek(&self) -> Option<Token> {
771 self.tokens.get(self.pos).cloned()
772 }
773
774 pub(crate) fn peek_at(&self, offset: usize) -> Option<Token> {
775 self.tokens.get(self.pos + offset).cloned()
776 }
777
778 pub(crate) fn next(&mut self) -> Option<Token> {
779 let token = self.tokens.get(self.pos).cloned();
780 if token.is_some() {
781 self.pos += 1;
782 }
783 token
784 }
785
786 pub(crate) fn previous(&self) -> &Token {
787 self.tokens
788 .get(self.pos.saturating_sub(1))
789 .unwrap_or_else(|| self.tokens.last().unwrap())
790 }
791
792 pub(crate) fn previous_span(&self) -> (Position, Position) {
793 let token = self.previous();
794 (token.start, token.end)
795 }
796
797 pub(crate) fn span_here(&self) -> (Position, Position) {
798 let token = self
799 .peek()
800 .unwrap_or_else(|| self.tokens.last().unwrap().clone());
801 (token.start, token.end)
802 }
803
804 pub(crate) fn eof(&self) -> &Token {
805 self.tokens.last().unwrap()
806 }
807
808 pub(crate) fn file(&self) -> crate::core::ids::Id<SourceFile> {
809 crate::core::ids::Id::from_index(0)
810 }
811}
812
813fn structural_keyword_map() -> &'static HashMap<String, String> {
814 static KEYWORDS: OnceLock<HashMap<String, String>> = OnceLock::new();
815 KEYWORDS.get_or_init(|| {
816 serde_json::from_str(include_str!("structural_keywords.json"))
817 .expect("structural keyword data is valid JSON")
818 })
819}
820
821pub(crate) fn canonical_keyword(keyword: &str) -> &str {
822 structural_keyword_map()
823 .get(keyword)
824 .map(String::as_str)
825 .unwrap_or(keyword)
826}
827
828pub(crate) fn join_as_written(parts: &[(String, Position, Position)]) -> String {
832 let mut text = String::new();
833 let mut prev_end: Option<Position> = None;
834 for (fragment, start, end) in parts {
835 if prev_end.is_some_and(|prev| prev != *start) {
836 text.push(' ');
837 }
838 text.push_str(fragment);
839 prev_end = Some(*end);
840 }
841 text
842}
843
844pub(crate) fn raw_token_text(kind: &TokenKind) -> String {
845 match kind {
846 TokenKind::Word(value) => value.clone(),
847 TokenKind::Number { text, .. } => text.clone(),
848 TokenKind::String(value) => format!(
849 "\"{}\"",
850 crate::output::emitter::escape_settings_string(value)
851 ),
852 TokenKind::Op(value) => match value.as_str() {
853 "not" => "!".to_string(),
854 "and" => "&&".to_string(),
855 "or" => "||".to_string(),
856 _ => value.clone(),
857 },
858 TokenKind::LParen => "(".to_string(),
859 TokenKind::RParen => ")".to_string(),
860 TokenKind::Comma => ",".to_string(),
861 TokenKind::Semi => ";".to_string(),
862 TokenKind::LBrace => "{".to_string(),
863 TokenKind::RBrace => "}".to_string(),
864 TokenKind::Colon => ":".to_string(),
865 TokenKind::Dot => ".".to_string(),
866 TokenKind::LBracket => "[".to_string(),
867 TokenKind::RBracket => "]".to_string(),
868 TokenKind::Eof => String::new(),
869 }
870}