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