1use crate::ast::{
7 Argument, ArgumentValue, BlankLine, Block, Comment, Config, ConfigItem, Directive, Span,
8};
9use crate::is_raw_block_directive;
10use crate::line_index::LineIndex;
11use crate::syntax_kind::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
12
13pub fn convert(root: &SyntaxNode, source: &str) -> Config {
15 let line_index = LineIndex::new(source);
16 let ctx = ConvertCtx {
17 line_index: &line_index,
18 };
19 let items = ctx.convert_items(root);
20 Config {
21 items,
22 include_context: Vec::new(),
23 }
24}
25
26struct ConvertCtx<'a> {
28 line_index: &'a LineIndex,
29}
30
31impl<'a> ConvertCtx<'a> {
32 fn span_of(&self, node: &SyntaxNode) -> Span {
35 self.line_index.span(node.text_range())
36 }
37
38 fn span_of_token(&self, token: &SyntaxToken) -> Span {
39 self.line_index.span(token.text_range())
40 }
41
42 fn convert_items(&self, parent: &SyntaxNode) -> Vec<ConfigItem> {
51 let mut items: Vec<ConfigItem> = Vec::new();
52 let children: Vec<SyntaxElement> = parent.children_with_tokens().collect();
53 let len = children.len();
54 let mut i = 0;
55
56 let mut consecutive_newlines: usize = 0;
60
61 while i < len {
62 let child = &children[i];
63 match child.kind() {
64 SyntaxKind::DIRECTIVE => {
65 let leading_ws = self.collect_leading_whitespace(&children, i);
67 let node = child.as_node().unwrap();
68 let directive = self.convert_directive(node, &leading_ws, &children, i);
69 items.push(ConfigItem::Directive(Box::new(directive)));
70 consecutive_newlines = 0;
71 i += 1;
72 }
73 SyntaxKind::COMMENT => {
74 let token = child.as_token().unwrap();
75 let leading_ws = self.collect_leading_whitespace(&children, i);
76 let comment = Comment {
77 text: token.text().to_string(),
78 span: self.span_of_token(token),
79 leading_whitespace: leading_ws,
80 trailing_whitespace: String::new(),
84 };
85 items.push(ConfigItem::Comment(comment));
86 consecutive_newlines = 0;
87 i += 1;
88 }
89 SyntaxKind::BLANK_LINE => {
90 let node = child.as_node().unwrap();
91 consecutive_newlines += 1;
95 if !items.is_empty() {
96 let text = node.text().to_string();
97 let content = text.strip_suffix('\n').unwrap_or(&text).to_string();
99 let span = self.span_of(node);
100 items.push(ConfigItem::BlankLine(BlankLine { span, content }));
101 }
102 i += 1;
103 }
104 SyntaxKind::NEWLINE => {
105 consecutive_newlines += 1;
106 if consecutive_newlines > 1 && !items.is_empty() {
109 let span = if let Some(tok) = child.as_token() {
110 self.span_of_token(tok)
111 } else {
112 Span::default()
113 };
114 items.push(ConfigItem::BlankLine(BlankLine {
115 span,
116 content: String::new(),
117 }));
118 }
119 i += 1;
120 }
121 _ => {
125 if child.kind() != SyntaxKind::WHITESPACE {
127 consecutive_newlines = 0;
128 }
129 i += 1;
130 }
131 }
132 }
133
134 items
135 }
136
137 fn collect_leading_whitespace(&self, children: &[SyntaxElement], i: usize) -> String {
143 if i == 0 {
144 return String::new();
145 }
146 let prev = &children[i - 1];
150 if prev.kind() == SyntaxKind::WHITESPACE
151 && let Some(tok) = prev.as_token()
152 {
153 if i < 2 {
155 return tok.text().to_string();
156 }
157 let before = &children[i - 2];
158 if before.kind() == SyntaxKind::NEWLINE {
159 return tok.text().to_string();
160 }
161 }
162 String::new()
163 }
164
165 fn convert_directive(
168 &self,
169 node: &SyntaxNode,
170 leading_ws: &str,
171 parent_children: &[SyntaxElement],
172 parent_idx: usize,
173 ) -> Directive {
174 let children: Vec<SyntaxElement> = node.children_with_tokens().collect();
175
176 let (name, name_span, name_idx) = self.find_directive_name(&children);
178
179 let args = self.collect_arguments(&children, name_idx);
181
182 let mut block: Option<Block> = None;
184 let mut trailing_comment: Option<Comment> = None;
185 let mut space_before_terminator = String::new();
186 let mut trailing_whitespace = String::new();
187 let mut dir_span_end = name_span.end;
190
191 let terminator_info = self.find_terminator(&children);
193
194 match terminator_info {
195 Terminator::Semicolon { idx } => {
196 space_before_terminator = self.whitespace_before(&children, idx);
197
198 if let Some(tok) = children[idx].as_token() {
200 dir_span_end = self.span_of_token(tok).end;
201 }
202
203 trailing_comment = self.find_trailing_comment(&children, idx);
205
206 trailing_whitespace =
209 self.collect_directive_trailing_whitespace(parent_children, parent_idx);
210 }
211 Terminator::Block { idx } => {
212 let block_node = children[idx].as_node().unwrap();
213 let is_raw = is_raw_block_directive(&name);
214
215 space_before_terminator = self.whitespace_before(&children, idx);
217
218 trailing_whitespace = self.opening_brace_trailing(block_node);
222
223 block = Some(self.convert_block(block_node, is_raw));
224
225 dir_span_end = self.span_of(block_node).end;
227
228 trailing_comment =
230 self.find_trailing_comment_after_block(parent_children, parent_idx);
231
232 if trailing_comment.is_none()
236 && let Some(ref mut b) = block
237 {
238 b.trailing_whitespace =
239 self.collect_directive_trailing_whitespace(parent_children, parent_idx);
240 }
241 }
242 Terminator::Missing => {
243 }
245 }
246
247 let dir_span = Span::new(name_span.start, dir_span_end);
248
249 Directive {
250 name,
251 name_span,
252 args,
253 block,
254 span: dir_span,
255 trailing_comment,
256 leading_whitespace: leading_ws.to_string(),
257 space_before_terminator,
258 trailing_whitespace,
259 }
260 }
261
262 fn find_directive_name(&self, children: &[SyntaxElement]) -> (String, Span, usize) {
264 for (idx, child) in children.iter().enumerate() {
265 match child.kind() {
266 SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE => continue,
267 _ => {
268 if let Some(token) = child.as_token() {
269 let raw = token.text().to_string();
270 let name = match child.kind() {
271 SyntaxKind::DOUBLE_QUOTED_STRING | SyntaxKind::SINGLE_QUOTED_STRING => {
272 strip_quotes(&raw)
274 }
275 _ => raw.clone(),
276 };
277 let span = self.span_of_token(token);
278 return (name, span, idx);
279 }
280 }
281 }
282 }
283 (String::new(), Span::default(), 0)
285 }
286
287 fn collect_arguments(&self, children: &[SyntaxElement], name_idx: usize) -> Vec<Argument> {
289 let mut args = Vec::new();
290
291 for child in children.iter().skip(name_idx + 1) {
292 match child.kind() {
293 SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE => continue,
294 SyntaxKind::SEMICOLON | SyntaxKind::COMMENT => break,
295 SyntaxKind::BLOCK => break,
296 kind if is_argument_token(kind) => {
297 if let Some(token) = child.as_token() {
298 args.push(self.token_to_argument(token));
299 }
300 }
301 _ => continue,
302 }
303 }
304
305 args
306 }
307
308 fn token_to_argument(&self, token: &SyntaxToken) -> Argument {
310 let raw = token.text().to_string();
311 let span = self.span_of_token(token);
312 let value = match token.kind() {
313 SyntaxKind::DOUBLE_QUOTED_STRING => ArgumentValue::QuotedString(strip_quotes(&raw)),
314 SyntaxKind::SINGLE_QUOTED_STRING => {
315 ArgumentValue::SingleQuotedString(strip_quotes(&raw))
316 }
317 SyntaxKind::VARIABLE => {
318 let var_name = if raw.starts_with("${") && raw.ends_with('}') {
319 raw[2..raw.len() - 1].to_string()
320 } else if let Some(stripped) = raw.strip_prefix('$') {
321 stripped.to_string()
322 } else {
323 raw.clone()
324 };
325 ArgumentValue::Variable(var_name)
326 }
327 _ => ArgumentValue::Literal(raw.clone()),
329 };
330 Argument { value, span, raw }
331 }
332
333 fn find_terminator(&self, children: &[SyntaxElement]) -> Terminator {
335 for (idx, child) in children.iter().enumerate() {
336 match child.kind() {
337 SyntaxKind::SEMICOLON => return Terminator::Semicolon { idx },
338 SyntaxKind::BLOCK => return Terminator::Block { idx },
339 _ => continue,
340 }
341 }
342 Terminator::Missing
343 }
344
345 fn whitespace_before(&self, children: &[SyntaxElement], idx: usize) -> String {
347 if idx == 0 {
348 return String::new();
349 }
350 let prev = &children[idx - 1];
351 if prev.kind() == SyntaxKind::WHITESPACE
352 && let Some(tok) = prev.as_token()
353 {
354 return tok.text().to_string();
355 }
356 String::new()
357 }
358
359 fn find_trailing_comment(
361 &self,
362 children: &[SyntaxElement],
363 semi_idx: usize,
364 ) -> Option<Comment> {
365 let mut idx = semi_idx + 1;
367 let mut comment_leading_ws = String::new();
368
369 while idx < children.len() {
370 match children[idx].kind() {
371 SyntaxKind::WHITESPACE => {
372 if let Some(tok) = children[idx].as_token() {
373 comment_leading_ws = tok.text().to_string();
374 }
375 idx += 1;
376 }
377 SyntaxKind::COMMENT => {
378 let token = children[idx].as_token().unwrap();
379 return Some(Comment {
380 text: token.text().to_string(),
381 span: self.span_of_token(token),
382 leading_whitespace: comment_leading_ws,
383 trailing_whitespace: String::new(),
384 });
385 }
386 _ => break,
387 }
388 }
389 None
390 }
391
392 fn find_trailing_comment_after_block(
395 &self,
396 parent_children: &[SyntaxElement],
397 dir_idx: usize,
398 ) -> Option<Comment> {
399 let mut idx = dir_idx + 1;
402 let mut comment_leading_ws = String::new();
403
404 while idx < parent_children.len() {
405 match parent_children[idx].kind() {
406 SyntaxKind::WHITESPACE => {
407 if let Some(tok) = parent_children[idx].as_token() {
408 comment_leading_ws = tok.text().to_string();
409 }
410 idx += 1;
411 }
412 SyntaxKind::COMMENT => {
413 let token = parent_children[idx].as_token().unwrap();
414 return Some(Comment {
415 text: token.text().to_string(),
416 span: self.span_of_token(token),
417 leading_whitespace: comment_leading_ws,
418 trailing_whitespace: String::new(),
419 });
420 }
421 SyntaxKind::NEWLINE => break,
422 _ => break,
423 }
424 }
425 None
426 }
427
428 fn collect_directive_trailing_whitespace(
435 &self,
436 parent_children: &[SyntaxElement],
437 dir_idx: usize,
438 ) -> String {
439 let idx = dir_idx + 1;
440 if idx < parent_children.len() && parent_children[idx].kind() == SyntaxKind::WHITESPACE {
441 if idx + 1 < parent_children.len() {
444 let next_kind = parent_children[idx + 1].kind();
445 if (next_kind == SyntaxKind::NEWLINE
446 || next_kind == SyntaxKind::DIRECTIVE
447 || next_kind == SyntaxKind::BLANK_LINE)
448 && let Some(tok) = parent_children[idx].as_token()
449 {
450 return tok.text().to_string();
451 }
452 } else {
454 if let Some(tok) = parent_children[idx].as_token() {
456 return tok.text().to_string();
457 }
458 }
459 }
460 String::new()
461 }
462
463 fn opening_brace_trailing(&self, block_node: &SyntaxNode) -> String {
468 let mut found_lbrace = false;
470 for child in block_node.children_with_tokens() {
471 if child.kind() == SyntaxKind::L_BRACE {
472 found_lbrace = true;
473 continue;
474 }
475 if found_lbrace {
476 if child.kind() == SyntaxKind::WHITESPACE
477 && let Some(tok) = child.as_token()
478 {
479 return tok.text().to_string();
480 }
481 return String::new();
482 }
483 }
484 String::new()
485 }
486
487 fn convert_block(&self, block_node: &SyntaxNode, is_raw: bool) -> Block {
490 let span = self.span_of(block_node);
491
492 if is_raw {
493 let raw_content = self.extract_raw_content(block_node);
494 let closing_ws = self.closing_brace_leading_whitespace(block_node);
495 return Block {
496 items: Vec::new(),
497 span,
498 raw_content: Some(raw_content),
499 closing_brace_leading_whitespace: closing_ws,
500 trailing_whitespace: String::new(),
501 };
502 }
503
504 let items = self.convert_items(block_node);
505 let closing_ws = self.closing_brace_leading_whitespace(block_node);
506
507 Block {
508 items,
509 span,
510 raw_content: None,
511 closing_brace_leading_whitespace: closing_ws,
512 trailing_whitespace: String::new(), }
514 }
515
516 fn extract_raw_content(&self, block_node: &SyntaxNode) -> String {
523 let children: Vec<SyntaxElement> = block_node.children_with_tokens().collect();
524 let mut content = String::new();
525 let mut depth: u32 = 0;
526 let mut i = 0;
527
528 while i < children.len() {
529 let kind = children[i].kind();
530 match kind {
531 SyntaxKind::L_BRACE => {
532 if depth > 0 {
533 content.push('{');
534 }
535 depth += 1;
536 i += 1;
537 }
538 SyntaxKind::R_BRACE => {
539 depth = depth.saturating_sub(1);
540 if depth > 0 {
541 content.push('}');
542 }
543 i += 1;
544 }
545 SyntaxKind::NEWLINE => {
546 content.push('\n');
550 content.push('\n');
551 i += 1;
552 }
553 SyntaxKind::WHITESPACE => {
554 i += 1;
557 }
558 _ => {
559 if let Some(tok) = children[i].as_token() {
561 content.push_str(tok.text());
562 }
563 i += 1;
564 let mut next_i = i;
568 while next_i < children.len()
569 && children[next_i].kind() == SyntaxKind::WHITESPACE
570 {
571 next_i += 1;
572 }
573 if next_i < children.len() {
574 let next_kind = children[next_i].kind();
575 if !matches!(
576 next_kind,
577 SyntaxKind::NEWLINE | SyntaxKind::R_BRACE | SyntaxKind::SEMICOLON
578 ) {
579 content.push(' ');
580 }
581 }
582 i = next_i;
584 }
585 }
586 }
587
588 content.trim().to_string()
589 }
590
591 fn closing_brace_leading_whitespace(&self, block_node: &SyntaxNode) -> String {
593 let children: Vec<SyntaxElement> = block_node.children_with_tokens().collect();
594 for (idx, child) in children.iter().enumerate().rev() {
596 if child.kind() == SyntaxKind::R_BRACE {
597 if idx > 0
598 && children[idx - 1].kind() == SyntaxKind::WHITESPACE
599 && let Some(tok) = children[idx - 1].as_token()
600 {
601 return tok.text().to_string();
602 }
603 break;
604 }
605 }
606 String::new()
607 }
608}
609
610enum Terminator {
612 Semicolon { idx: usize },
613 Block { idx: usize },
614 Missing,
615}
616
617fn strip_quotes(s: &str) -> String {
620 if s.len() < 2 {
621 return s.to_string();
622 }
623 if s.starts_with('"') && s.ends_with('"') {
624 unescape_double_quoted(&s[1..s.len() - 1])
625 } else if s.starts_with('\'') && s.ends_with('\'') {
626 unescape_single_quoted(&s[1..s.len() - 1])
627 } else {
628 s.to_string()
629 }
630}
631
632fn unescape_double_quoted(s: &str) -> String {
634 let mut result = String::with_capacity(s.len());
635 let mut chars = s.chars();
636 while let Some(ch) = chars.next() {
637 if ch == '\\' {
638 match chars.next() {
639 Some('n') => result.push('\n'),
640 Some('t') => result.push('\t'),
641 Some('r') => result.push('\r'),
642 Some('\\') => result.push('\\'),
643 Some('"') => result.push('"'),
644 Some('$') => result.push('$'),
645 Some(c) => {
646 result.push('\\');
647 result.push(c);
648 }
649 None => result.push('\\'),
650 }
651 } else {
652 result.push(ch);
653 }
654 }
655 result
656}
657
658fn unescape_single_quoted(s: &str) -> String {
660 let mut result = String::with_capacity(s.len());
661 let mut chars = s.chars();
662 while let Some(ch) = chars.next() {
663 if ch == '\\' {
664 match chars.next() {
665 Some('\\') => result.push('\\'),
666 Some('\'') => result.push('\''),
667 Some(c) => {
668 result.push('\\');
669 result.push(c);
670 }
671 None => result.push('\\'),
672 }
673 } else {
674 result.push(ch);
675 }
676 }
677 result
678}
679
680fn is_argument_token(kind: SyntaxKind) -> bool {
682 matches!(
683 kind,
684 SyntaxKind::IDENT
685 | SyntaxKind::ARGUMENT
686 | SyntaxKind::VARIABLE
687 | SyntaxKind::DOUBLE_QUOTED_STRING
688 | SyntaxKind::SINGLE_QUOTED_STRING
689 )
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695 use crate::parse_string_rowan;
696
697 fn parse_and_convert(source: &str) -> Config {
698 let (root, errors) = parse_string_rowan(source);
699 assert!(errors.is_empty(), "parse errors: {:?}", errors);
700 convert(&root, source)
701 }
702
703 #[test]
704 fn simple_directive() {
705 let config = parse_and_convert("listen 80;");
706 let dirs: Vec<_> = config.directives().collect();
707 assert_eq!(dirs.len(), 1);
708 assert_eq!(dirs[0].name, "listen");
709 assert_eq!(dirs[0].args.len(), 1);
710 assert_eq!(dirs[0].args[0].raw, "80");
711 assert!(matches!(dirs[0].args[0].value, ArgumentValue::Literal(ref s) if s == "80"));
712 }
713
714 #[test]
715 fn block_directive() {
716 let config = parse_and_convert("server {\n listen 80;\n}");
717 let dirs: Vec<_> = config.directives().collect();
718 assert_eq!(dirs.len(), 1);
719 assert_eq!(dirs[0].name, "server");
720 assert!(dirs[0].block.is_some());
721
722 let block = dirs[0].block.as_ref().unwrap();
723 let inner: Vec<_> = block.directives().collect();
724 assert_eq!(inner.len(), 1);
725 assert_eq!(inner[0].name, "listen");
726 assert_eq!(inner[0].leading_whitespace, " ");
727 }
728
729 #[test]
730 fn variable_argument() {
731 let config = parse_and_convert("set $var value;");
732 let d = config.directives().next().unwrap();
733 assert_eq!(d.args.len(), 2);
734 assert_eq!(d.args[0].raw, "$var");
735 assert!(matches!(d.args[0].value, ArgumentValue::Variable(ref s) if s == "var"));
736 assert_eq!(d.args[1].raw, "value");
737 }
738
739 #[test]
740 fn quoted_string_argument() {
741 let config = parse_and_convert(r#"return 200 "hello world";"#);
742 let d = config.directives().next().unwrap();
743 assert_eq!(d.args.len(), 2);
744 assert_eq!(d.args[1].raw, "\"hello world\"");
745 assert!(
746 matches!(d.args[1].value, ArgumentValue::QuotedString(ref s) if s == "hello world")
747 );
748 }
749
750 #[test]
751 fn trailing_comment() {
752 let config = parse_and_convert("listen 80; # port\n");
753 let d = config.directives().next().unwrap();
754 assert!(d.trailing_comment.is_some());
755 assert_eq!(d.trailing_comment.as_ref().unwrap().text, "# port");
756 }
757
758 #[test]
759 fn standalone_comment() {
760 let config = parse_and_convert("# comment\nlisten 80;");
761 assert_eq!(config.items.len(), 2);
762 assert!(matches!(config.items[0], ConfigItem::Comment(_)));
763 assert!(matches!(config.items[1], ConfigItem::Directive(_)));
764 }
765
766 #[test]
767 fn span_positions() {
768 let config = parse_and_convert("listen 80;");
769 let d = config.directives().next().unwrap();
770 assert_eq!(d.name_span.start.line, 1);
772 assert_eq!(d.name_span.start.column, 1);
773 assert_eq!(d.name_span.start.offset, 0);
774 assert_eq!(d.name_span.end.offset, 6);
775 }
776}