1use crate::code_node::{CodeNode, parts_args_to_nodes};
2use crate::import::ImportRef;
3use crate::lang::CodeLang;
4use crate::type_name::TypeName;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
18pub enum Specifier {
19 Type,
21 Name,
23 StringLit,
25 VerbatimStr,
28 Literal,
30 Comment,
32}
33
34impl Specifier {
35 pub fn from_format_char(ch: char) -> Option<Self> {
40 match ch {
41 'T' => Some(Self::Type),
42 'N' => Some(Self::Name),
43 'S' => Some(Self::StringLit),
44 'V' => Some(Self::VerbatimStr),
45 'L' => Some(Self::Literal),
46 'R' => Some(Self::Comment),
47 _ => None,
48 }
49 }
50
51 pub fn format_char(self) -> char {
53 match self {
54 Self::Type => 'T',
55 Self::Name => 'N',
56 Self::StringLit => 'S',
57 Self::VerbatimStr => 'V',
58 Self::Literal => 'L',
59 Self::Comment => 'R',
60 }
61 }
62
63 pub fn all() -> &'static [Self] {
65 &[
66 Self::Type,
67 Self::Name,
68 Self::StringLit,
69 Self::VerbatimStr,
70 Self::Literal,
71 Self::Comment,
72 ]
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
78pub(crate) enum FormatPart {
79 Literal(String),
81 Arg(Specifier),
83 Wrap,
85 Indent,
87 Dedent,
89 StatementBegin,
91 StatementEnd,
93 Newline,
95 BlockOpen(String),
100 BlockClose(String),
105 BranchClose(String),
110}
111
112#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
114pub enum Arg {
115 TypeName(TypeName),
117 Name(String),
119 StringLit(String),
121 VerbatimStr(String),
123 Literal(String),
125 Code(CodeBlock),
127 Comment(String),
129}
130
131#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
160pub struct CodeBlock {
161 pub(crate) nodes: Vec<CodeNode>,
162}
163
164#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
171pub struct CodeFragment {
172 block: CodeBlock,
173}
174
175impl CodeFragment {
176 pub fn of(format: &str, args: impl IntoArgs) -> Result<Self, crate::error::SigilStitchError> {
178 let nodes = format_to_nodes(format, args.into_args())?;
179 validate_balanced_indent_markers(&nodes)?;
180 Ok(Self {
181 block: CodeBlock { nodes },
182 })
183 }
184
185 pub fn into_code_block(self) -> CodeBlock {
187 self.block
188 }
189}
190
191impl CodeBlock {
192 pub fn builder() -> CodeBlockBuilder {
194 CodeBlockBuilder::new()
195 }
196
197 pub fn nodes_mut(&mut self) -> &mut Vec<CodeNode> {
199 &mut self.nodes
200 }
201
202 pub fn of(format: &str, args: impl IntoArgs) -> Result<Self, crate::error::SigilStitchError> {
204 let mut builder = CodeBlockBuilder::new();
205 builder.add(format, args);
206 builder.build()
207 }
208
209 pub fn is_empty(&self) -> bool {
211 self.nodes.is_empty()
212 }
213
214 pub fn fragment(
216 format: &str,
217 args: impl IntoArgs,
218 ) -> Result<CodeFragment, crate::error::SigilStitchError> {
219 CodeFragment::of(format, args)
220 }
221
222 pub fn ends_with_newline_or_block_close(&self) -> bool {
224 fn check_last(nodes: &[CodeNode]) -> bool {
225 match nodes.last() {
226 Some(CodeNode::Newline | CodeNode::BlockClose(_)) => true,
227 Some(CodeNode::Sequence(children)) => check_last(children),
228 Some(CodeNode::Nested(inner)) => check_last(&inner.nodes),
229 _ => false,
230 }
231 }
232 check_last(&self.nodes)
233 }
234
235 #[doc(hidden)]
242 pub fn __sigil_trim_trailing_newline(mut self) -> Self {
243 fn trim(nodes: &mut Vec<CodeNode>) -> bool {
244 match nodes.last_mut() {
245 Some(CodeNode::Newline) => {
246 nodes.pop();
247 true
248 }
249 Some(CodeNode::Sequence(children)) => trim(children),
250 Some(CodeNode::Nested(inner)) => trim(&mut inner.nodes),
251 _ => false,
252 }
253 }
254
255 trim(&mut self.nodes);
256 self
257 }
258
259 pub fn collect_imports(&self, out: &mut Vec<ImportRef>) {
261 crate::import_collector::walk_nodes(&self.nodes, out);
262 }
263
264 pub fn render_standalone(
270 &self,
271 lang: &dyn CodeLang,
272 width: usize,
273 ) -> Result<String, crate::error::SigilStitchError> {
274 let imports = crate::import::ImportGroup::new();
275 let mut renderer = crate::code_renderer::CodeRenderer::new(lang, &imports, width);
276 renderer.render(self)
277 }
278}
279
280#[derive(Debug)]
302pub struct CodeBlockBuilder {
303 nodes: Vec<CodeNode>,
304 indent_depth: i32,
305 block_stack: Vec<String>,
306 errors: Vec<crate::error::SigilStitchError>,
307}
308
309impl CodeBlockBuilder {
310 pub fn new() -> Self {
312 Self {
313 nodes: Vec::new(),
314 indent_depth: 0,
315 block_stack: Vec::new(),
316 errors: Vec::new(),
317 }
318 }
319
320 pub fn add(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
322 let new_nodes = match format_to_nodes(format, args.into_args()) {
323 Ok(nodes) => nodes,
324 Err(err) => {
325 self.errors.push(err);
326 return self;
327 }
328 };
329 self.nodes.extend(new_nodes);
330 self
331 }
332
333 pub fn add_statement(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
335 self.nodes.push(CodeNode::StatementBegin);
336 self.add(format, args);
337 self.nodes.push(CodeNode::StatementEnd);
338 self.nodes.push(CodeNode::Newline);
339 self
340 }
341
342 pub fn begin_control_flow(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
354 let condition = format.to_string();
355 self.block_stack.push(condition.clone());
356 self.add(format, args);
357 self.nodes.push(CodeNode::BlockOpen(condition));
358 self.nodes.push(CodeNode::Newline);
359 self.nodes.push(CodeNode::Indent);
360 self.indent_depth += 1;
361 self
362 }
363
364 pub fn next_control_flow(&mut self, format: &str, args: impl IntoArgs) -> &mut Self {
366 let condition = self.block_stack.last().cloned().unwrap_or_default();
367 self.nodes.push(CodeNode::Dedent);
368 self.indent_depth -= 1;
369 self.nodes.push(CodeNode::BranchClose(condition));
370 self.add(format, args);
371 let new_condition = format.to_string();
372 self.nodes.push(CodeNode::BlockOpen(new_condition));
373 self.nodes.push(CodeNode::Newline);
374 self.nodes.push(CodeNode::Indent);
375 self.indent_depth += 1;
376 self
377 }
378
379 pub fn end_control_flow(&mut self) -> &mut Self {
382 let condition = self.block_stack.pop().unwrap_or_default();
383 self.nodes.push(CodeNode::Dedent);
384 self.indent_depth -= 1;
385 self.nodes.push(CodeNode::BlockClose(condition));
386 self.nodes.push(CodeNode::Newline);
387 self
388 }
389
390 pub fn end_control_flow_no_newline(&mut self) -> &mut Self {
397 let condition = self.block_stack.pop().unwrap_or_default();
398 self.nodes.push(CodeNode::Dedent);
399 self.indent_depth -= 1;
400 self.nodes.push(CodeNode::BlockClose(condition));
401 self
402 }
403
404 pub fn end_control_flow_with_semicolon(&mut self) -> &mut Self {
407 let condition = self.block_stack.pop().unwrap_or_default();
408 self.nodes.push(CodeNode::Dedent);
409 self.indent_depth -= 1;
410 self.nodes.push(CodeNode::BlockClose(condition));
411 self.nodes.push(CodeNode::StatementEnd);
412 self.nodes.push(CodeNode::Newline);
413 self
414 }
415
416 pub fn add_line(&mut self) -> &mut Self {
418 self.nodes.push(CodeNode::Newline);
419 self
420 }
421
422 pub fn add_comment(&mut self, text: &str) -> &mut Self {
424 self.nodes.push(CodeNode::Comment(text.to_string()));
425 self.nodes.push(CodeNode::Newline);
426 self
427 }
428
429 pub fn add_attribute(&mut self, text: &str) -> &mut Self {
434 self.nodes.push(CodeNode::Attribute(text.to_string()));
435 self.nodes.push(CodeNode::Newline);
436 self
437 }
438
439 pub fn add_code(&mut self, block: CodeBlock) -> &mut Self {
441 self.nodes.push(CodeNode::Nested(block));
442 self
443 }
444
445 pub fn add_fragment(&mut self, fragment: CodeFragment) -> &mut Self {
447 self.add_code(fragment.into_code_block())
448 }
449
450 pub fn build(self) -> Result<CodeBlock, crate::error::SigilStitchError> {
456 if let Some(err) = self.errors.into_iter().next() {
457 return Err(err);
458 }
459 if self.indent_depth != 0 {
460 return Err(crate::error::SigilStitchError::UnbalancedIndent {
461 depth: self.indent_depth,
462 });
463 }
464 validate_balanced_indent_markers(&self.nodes)?;
465 validate_no_unresolved_indent_markers(&self.nodes)?;
466 Ok(CodeBlock { nodes: self.nodes })
467 }
468
469 pub fn build_unwrap(self) -> CodeBlock {
471 self.build().unwrap()
472 }
473}
474
475impl Default for CodeBlockBuilder {
476 fn default() -> Self {
477 Self::new()
478 }
479}
480
481fn format_to_nodes(
482 format: &str,
483 args: Vec<Arg>,
484) -> Result<Vec<CodeNode>, crate::error::SigilStitchError> {
485 let parsed = parse_format(format)?;
486 let consuming_specifiers: Vec<String> = parsed
487 .iter()
488 .filter_map(|p| match p {
489 FormatPart::Arg(s) => Some(format!("%{}", s.format_char())),
490 _ => None,
491 })
492 .collect();
493
494 let expected_args = consuming_specifiers.len();
495
496 if expected_args != args.len() {
497 let actual_arg_kinds: Vec<String> = args.iter().map(arg_kind_name).collect();
498 return Err(crate::error::SigilStitchError::FormatArgCount {
499 format: format.to_string(),
500 expected: expected_args,
501 actual: args.len(),
502 expected_specifiers: consuming_specifiers,
503 actual_arg_kinds,
504 });
505 }
506
507 let nodes = parts_args_to_nodes(&parsed, &args);
508 validate_no_unresolved_indent_markers(&nodes)?;
509 Ok(nodes)
510}
511
512pub(crate) fn validate_balanced_indent_markers(
513 nodes: &[CodeNode],
514) -> Result<(), crate::error::SigilStitchError> {
515 fn walk(nodes: &[CodeNode], depth: &mut i32) -> Result<(), crate::error::SigilStitchError> {
516 for node in nodes {
517 match node {
518 CodeNode::Indent => *depth += 1,
519 CodeNode::Dedent => *depth -= 1,
520 CodeNode::Nested(block) => walk(&block.nodes, depth)?,
521 CodeNode::Sequence(children) => walk(children, depth)?,
522 _ => {}
523 }
524 }
525 Ok(())
526 }
527
528 let mut depth = 0;
529 walk(nodes, &mut depth)?;
530 if depth != 0 {
531 return Err(crate::error::SigilStitchError::UnbalancedIndent { depth });
532 }
533 Ok(())
534}
535
536pub(crate) fn validate_no_unresolved_indent_markers(
537 nodes: &[CodeNode],
538) -> Result<(), crate::error::SigilStitchError> {
539 fn check_text(text: &str, context: &str) -> Result<(), crate::error::SigilStitchError> {
540 for marker in ["%>", "%<"] {
541 if text.contains(marker) {
542 return Err(crate::error::SigilStitchError::UnresolvedIndentMarker {
543 marker: marker.to_string(),
544 context: context.to_string(),
545 });
546 }
547 }
548 Ok(())
549 }
550
551 for node in nodes {
552 match node {
553 CodeNode::Literal(text) => check_text(text, "format literal")?,
554 CodeNode::InlineLiteral(text) => check_text(text, "%L literal")?,
555 CodeNode::Nested(block) => validate_no_unresolved_indent_markers(&block.nodes)?,
556 CodeNode::Sequence(children) => validate_no_unresolved_indent_markers(children)?,
557 _ => {}
558 }
559 }
560 Ok(())
561}
562
563fn arg_kind_name(arg: &Arg) -> String {
564 match arg {
565 Arg::TypeName(_) => "TypeName".to_string(),
566 Arg::Name(_) => "Name".to_string(),
567 Arg::StringLit(_) => "StringLit".to_string(),
568 Arg::VerbatimStr(_) => "VerbatimStr".to_string(),
569 Arg::Literal(_) => "Literal".to_string(),
570 Arg::Code(_) => "Code".to_string(),
571 Arg::Comment(_) => "Comment".to_string(),
572 }
573}
574
575fn parse_format(format: &str) -> Result<Vec<FormatPart>, crate::error::SigilStitchError> {
577 let mut parts = Vec::new();
578 let mut current_literal = String::new();
579 let mut chars = format.char_indices().peekable();
580
581 while let Some(&(_, ch)) = chars.peek() {
582 if ch == '%' {
583 chars.next();
584 if let Some(&(_, spec)) = chars.peek() {
585 chars.next();
586 let part = match spec {
587 'W' => Some(FormatPart::Wrap),
588 '>' => Some(FormatPart::Indent),
589 '<' => Some(FormatPart::Dedent),
590 '[' => Some(FormatPart::StatementBegin),
591 ']' => Some(FormatPart::StatementEnd),
592 '%' => {
593 current_literal.push('%');
594 continue;
595 }
596 _ => match Specifier::from_format_char(spec) {
597 Some(s) => Some(FormatPart::Arg(s)),
598 None => {
599 return Err(crate::error::SigilStitchError::InvalidFormatSpecifier {
600 format: format.to_string(),
601 specifier: spec,
602 });
603 }
604 },
605 };
606 if let Some(part) = part {
607 if !current_literal.is_empty() {
608 parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
609 }
610 parts.push(part);
611 }
612 }
613 } else if ch == '\n' {
614 chars.next();
615 if !current_literal.is_empty() {
616 parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
617 }
618 parts.push(FormatPart::Newline);
619 } else {
620 chars.next();
621 current_literal.push(ch);
622 }
623 }
624
625 if !current_literal.is_empty() {
626 parts.push(FormatPart::Literal(current_literal));
627 }
628
629 Ok(parts)
630}
631
632pub trait IntoArgs {
641 fn into_args(self) -> Vec<Arg>;
643}
644
645impl IntoArgs for () {
647 fn into_args(self) -> Vec<Arg> {
648 Vec::new()
649 }
650}
651
652impl IntoArgs for TypeName {
654 fn into_args(self) -> Vec<Arg> {
655 vec![Arg::TypeName(self)]
656 }
657}
658
659impl IntoArgs for &str {
661 fn into_args(self) -> Vec<Arg> {
662 vec![Arg::Literal(self.to_string())]
663 }
664}
665
666impl IntoArgs for String {
667 fn into_args(self) -> Vec<Arg> {
668 vec![Arg::Literal(self)]
669 }
670}
671
672impl IntoArgs for CodeBlock {
674 fn into_args(self) -> Vec<Arg> {
675 vec![Arg::Code(self)]
676 }
677}
678
679impl IntoArgs for CodeFragment {
681 fn into_args(self) -> Vec<Arg> {
682 vec![Arg::Code(self.into_code_block())]
683 }
684}
685
686impl IntoArgs for Vec<Arg> {
688 fn into_args(self) -> Vec<Arg> {
689 self
690 }
691}
692
693pub struct NameArg(pub String);
709
710impl IntoArgs for NameArg {
711 fn into_args(self) -> Vec<Arg> {
712 vec![Arg::Name(self.0)]
713 }
714}
715
716pub struct StringLitArg(pub String);
732
733impl IntoArgs for StringLitArg {
734 fn into_args(self) -> Vec<Arg> {
735 vec![Arg::StringLit(self.0)]
736 }
737}
738
739pub struct VerbatimStrArg(pub String);
752
753impl IntoArgs for VerbatimStrArg {
754 fn into_args(self) -> Vec<Arg> {
755 vec![Arg::VerbatimStr(self.0)]
756 }
757}
758
759pub struct CommentArg(pub String);
774
775impl IntoArgs for CommentArg {
776 fn into_args(self) -> Vec<Arg> {
777 vec![Arg::Comment(self.0)]
778 }
779}
780
781impl From<TypeName> for Arg {
783 fn from(tn: TypeName) -> Self {
784 Arg::TypeName(tn)
785 }
786}
787
788impl From<&str> for Arg {
789 fn from(s: &str) -> Self {
790 Arg::Literal(s.to_string())
791 }
792}
793
794impl From<String> for Arg {
795 fn from(s: String) -> Self {
796 Arg::Literal(s)
797 }
798}
799
800impl From<CodeBlock> for Arg {
801 fn from(cb: CodeBlock) -> Self {
802 Arg::Code(cb)
803 }
804}
805
806impl From<CodeFragment> for Arg {
807 fn from(fragment: CodeFragment) -> Self {
808 Arg::Code(fragment.into_code_block())
809 }
810}
811
812impl From<NameArg> for Arg {
813 fn from(n: NameArg) -> Self {
814 Arg::Name(n.0)
815 }
816}
817
818impl From<StringLitArg> for Arg {
819 fn from(s: StringLitArg) -> Self {
820 Arg::StringLit(s.0)
821 }
822}
823
824impl From<VerbatimStrArg> for Arg {
825 fn from(s: VerbatimStrArg) -> Self {
826 Arg::VerbatimStr(s.0)
827 }
828}
829
830impl From<CommentArg> for Arg {
831 fn from(s: CommentArg) -> Self {
832 Arg::Comment(s.0)
833 }
834}
835
836macro_rules! impl_into_args_tuple {
840 ($($idx:tt $T:ident),+) => {
841 impl<$($T: Into<Arg>),+> IntoArgs for ($($T,)+) {
842 fn into_args(self) -> Vec<Arg> {
843 vec![$(self.$idx.into()),+]
844 }
845 }
846 };
847}
848
849impl_into_args_tuple!(0 A);
850impl_into_args_tuple!(0 A, 1 B);
851impl_into_args_tuple!(0 A, 1 B, 2 C);
852impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D);
853impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
854impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
855impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
856impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
857
858#[cfg(test)]
859mod tests {
860 use super::*;
861 use crate::code_node::CodeNode;
862 use crate::lang::typescript::TypeScript;
863
864 #[test]
865 fn test_parse_all_specifiers() {
866 let parts = parse_format("hello %T world %N %S %L %W %> %< %[ %]").unwrap();
867 assert!(parts.contains(&FormatPart::Arg(Specifier::Type)));
868 assert!(parts.contains(&FormatPart::Arg(Specifier::Name)));
869 assert!(parts.contains(&FormatPart::Arg(Specifier::StringLit)));
870 assert!(parts.contains(&FormatPart::Arg(Specifier::Literal)));
871 assert!(parts.contains(&FormatPart::Wrap));
872 assert!(parts.contains(&FormatPart::Indent));
873 assert!(parts.contains(&FormatPart::Dedent));
874 assert!(parts.contains(&FormatPart::StatementBegin));
875 assert!(parts.contains(&FormatPart::StatementEnd));
876 }
877
878 #[test]
879 fn test_parse_literal_percent() {
880 let parts = parse_format("100%%").unwrap();
881 assert_eq!(parts, vec![FormatPart::Literal("100%".to_string())]);
882 }
883
884 #[test]
885 fn test_parse_empty() {
886 let parts = parse_format("").unwrap();
887 assert!(parts.is_empty());
888 }
889
890 #[test]
891 fn test_parse_newlines() {
892 let parts = parse_format("line1\nline2").unwrap();
893 assert_eq!(
894 parts,
895 vec![
896 FormatPart::Literal("line1".to_string()),
897 FormatPart::Newline,
898 FormatPart::Literal("line2".to_string()),
899 ]
900 );
901 }
902
903 #[test]
904 fn test_builder_add_statement() {
905 let mut b = CodeBlock::builder();
906 b.add_statement("const x = %L", "42");
907 let block = b.build().unwrap();
908
909 assert!(!block.is_empty());
910 let has_stmt_begin = block
911 .nodes
912 .iter()
913 .any(|n| matches!(n, CodeNode::StatementBegin));
914 let has_stmt_end = block
915 .nodes
916 .iter()
917 .any(|n| matches!(n, CodeNode::StatementEnd));
918 assert!(has_stmt_begin);
919 assert!(has_stmt_end);
920 }
921
922 #[test]
923 fn test_builder_control_flow() {
924 let mut b = CodeBlock::builder();
925 b.begin_control_flow("if (x > 0)", ());
926 b.add_statement("return x", ());
927 b.end_control_flow();
928 let block = b.build().unwrap();
929
930 assert!(!block.is_empty());
931 }
932
933 #[test]
934 fn test_builder_unbalanced_control_flow() {
935 let mut b = CodeBlock::builder();
936 b.begin_control_flow("if (x)", ());
937 b.add_statement("y()", ());
938 let result = b.build();
940 assert!(result.is_err());
941 assert!(result.unwrap_err().to_string().contains("unbalanced"));
942 }
943
944 #[test]
945 fn test_mismatched_arg_count() {
946 let mut b = CodeBlock::builder();
947 b.add("%T", ());
948 let result = b.build();
949 assert!(result.is_err());
950 assert!(
951 result
952 .unwrap_err()
953 .to_string()
954 .contains("expects 1 args but got 0")
955 );
956 }
957
958 #[test]
959 fn test_into_args_tuple() {
960 let user = TypeName::importable("./models", "User");
961 let args: Vec<Arg> = (user, "hello").into_args();
962 assert_eq!(args.len(), 2);
963 assert!(matches!(&args[0], Arg::TypeName(_)));
964 assert!(matches!(&args[1], Arg::Literal(s) if s == "hello"));
965 }
966
967 #[test]
968 fn test_into_args_single_typename() {
969 let user = TypeName::importable("./models", "User");
970 let args: Vec<Arg> = user.into_args();
971 assert_eq!(args.len(), 1);
972 }
973
974 #[test]
975 fn test_into_args_single_str() {
976 let args: Vec<Arg> = "hello".into_args();
977 assert_eq!(args.len(), 1);
978 assert!(matches!(&args[0], Arg::Literal(s) if s == "hello"));
979 }
980
981 #[test]
982 fn test_raw_literal_rejects_unresolved_indent_marker() {
983 let result = CodeBlock::of("%L", "%>");
984
985 assert!(result.is_err());
986 let err_msg = result.unwrap_err().to_string();
987 assert!(err_msg.contains("unresolved indentation marker '%>'"));
988 assert!(err_msg.contains("CodeBlock/CodeFragment"));
989 }
990
991 #[test]
992 fn test_raw_literal_rejects_unresolved_dedent_marker() {
993 let result = CodeBlock::of("%L", "%<");
994
995 assert!(result.is_err());
996 let err_msg = result.unwrap_err().to_string();
997 assert!(err_msg.contains("unresolved indentation marker '%<'"));
998 }
999
1000 #[test]
1001 fn test_fragment_composes_indent_markers_structurally() {
1002 let fragment = CodeFragment::of("%>nested%<", ()).unwrap();
1003 let mut b = CodeBlock::builder();
1004 b.add("outer\n", ());
1005 b.add_fragment(fragment);
1006 let block = b.build().unwrap();
1007
1008 let output = block.render_standalone(&TypeScript::new(), 80).unwrap();
1009 assert_eq!(output, "outer\n nested");
1010 }
1011
1012 #[test]
1013 fn test_fragment_rejects_unbalanced_indent_marker() {
1014 let result = CodeFragment::of("%>nested", ());
1015
1016 assert!(result.is_err());
1017 let err_msg = result.unwrap_err().to_string();
1018 assert!(err_msg.contains("unbalanced control flow"));
1019 assert!(err_msg.contains("indent depth is 1"));
1020 }
1021
1022 #[test]
1023 fn test_fragment_rejects_unmatched_dedent_marker() {
1024 let result = CodeFragment::of("%<nested", ());
1025
1026 assert!(result.is_err());
1027 let err_msg = result.unwrap_err().to_string();
1028 assert!(err_msg.contains("unbalanced control flow"));
1029 assert!(err_msg.contains("indent depth is -1"));
1030 }
1031
1032 #[test]
1033 fn test_builder_allows_incremental_balanced_indent_markers() {
1034 let mut b = CodeBlock::builder();
1035 b.add("outer\n", ());
1036 b.add("%>", ());
1037 b.add("nested", ());
1038 b.add("%<", ());
1039 let block = b.build().unwrap();
1040
1041 let output = block.render_standalone(&TypeScript::new(), 80).unwrap();
1042 assert_eq!(output, "outer\n nested");
1043 }
1044
1045 #[test]
1046 fn test_builder_rejects_unbalanced_parsed_indent_marker_at_build() {
1047 let mut b = CodeBlock::builder();
1048 b.add("%>", ());
1049 let result = b.build();
1050
1051 assert!(result.is_err());
1052 let err_msg = result.unwrap_err().to_string();
1053 assert!(err_msg.contains("unbalanced control flow"));
1054 assert!(err_msg.contains("indent depth is 1"));
1055 }
1056
1057 #[test]
1058 fn test_fragment_can_be_passed_to_percent_l() {
1059 let fragment = CodeFragment::of("%>nested%<", ()).unwrap();
1060 let block = CodeBlock::of("outer\n%L", fragment).unwrap();
1061
1062 let output = block.render_standalone(&TypeScript::new(), 80).unwrap();
1063 assert_eq!(output, "outer\n nested");
1064 }
1065
1066 #[test]
1067 fn test_fragment_preserves_imports_when_passed_to_percent_l() {
1068 let user = TypeName::importable_type("./models", "User");
1069 let fragment = CodeFragment::of("const user: %T = loadUser()", (user,)).unwrap();
1070 let block = CodeBlock::of("%L", fragment).unwrap();
1071
1072 let imports = crate::import_collector::collect_imports(&block);
1073 assert_eq!(imports.len(), 1);
1074 assert_eq!(imports[0].module, "./models");
1075 assert_eq!(imports[0].name, "User");
1076 assert!(imports[0].is_type_only);
1077 }
1078
1079 #[test]
1080 fn test_fragment_accepts_nested_codeblock_arguments() {
1081 let inner = CodeBlock::of("compute()", ()).unwrap();
1082 let fragment = CodeFragment::of("return %L", inner).unwrap();
1083 let block = CodeBlock::of("%L", fragment).unwrap();
1084
1085 let output = block.render_standalone(&TypeScript::new(), 80).unwrap();
1086 assert_eq!(output, "return compute()");
1087 }
1088
1089 #[test]
1090 fn test_ordinary_percent_text_stays_raw() {
1091 let block = CodeBlock::of("progress = %L", "100%").unwrap();
1092
1093 let output = block.render_standalone(&TypeScript::new(), 80).unwrap();
1094 assert_eq!(output, "progress = 100%");
1095 }
1096
1097 #[test]
1098 fn test_collect_imports_from_codeblock() {
1099 let user = TypeName::importable("./models", "User");
1100 let tag = TypeName::importable("./models", "Tag");
1101 let mut b = CodeBlock::builder();
1102 b.add_statement("const u: %T = getUser()", (user,));
1103 b.add_statement("const t: %T = getTag()", (tag,));
1104 let block = b.build().unwrap();
1105
1106 let mut imports = Vec::new();
1107 block.collect_imports(&mut imports);
1108 assert_eq!(imports.len(), 2);
1109 assert_eq!(imports[0].name, "User");
1110 assert_eq!(imports[1].name, "Tag");
1111 }
1112
1113 #[test]
1114 fn test_nested_codeblock_imports() {
1115 let user = TypeName::importable("./models", "User");
1116 let mut ib = CodeBlock::builder();
1117 ib.add_statement("return new %T()", (user,));
1118 let inner = ib.build().unwrap();
1119
1120 let mut ob = CodeBlock::builder();
1121 ob.add_code(inner);
1122 let outer = ob.build().unwrap();
1123
1124 let mut imports = Vec::new();
1125 outer.collect_imports(&mut imports);
1126 assert_eq!(imports.len(), 1);
1127 assert_eq!(imports[0].name, "User");
1128 }
1129
1130 #[test]
1131 fn test_name_arg() {
1132 let mut b = CodeBlock::builder();
1133 b.add("this.%N()", (NameArg("getUser".to_string()),));
1134 let block = b.build().unwrap();
1135 let has_name = block
1136 .nodes
1137 .iter()
1138 .any(|n| matches!(n, CodeNode::NameRef(s) if s == "getUser"));
1139 assert!(has_name);
1140 }
1141
1142 #[test]
1143 fn test_string_lit_arg() {
1144 let mut b = CodeBlock::builder();
1145 b.add("const x = %S", (StringLitArg("hello".to_string()),));
1146 let block = b.build().unwrap();
1147 let has_str_lit = block
1148 .nodes
1149 .iter()
1150 .any(|n| matches!(n, CodeNode::StringLit(s) if s == "hello"));
1151 assert!(has_str_lit);
1152 }
1153
1154 #[test]
1155 fn test_invalid_format_specifier() {
1156 let mut b = CodeBlock::builder();
1157 b.add("hello %X world", ());
1158 let result = b.build();
1159 assert!(result.is_err());
1160 let err_msg = result.unwrap_err().to_string();
1161 assert!(err_msg.contains("invalid format specifier"));
1162 assert!(err_msg.contains("%X"));
1163 }
1164
1165 #[test]
1166 fn test_parse_format_invalid_specifier_returns_error() {
1167 let result = parse_format("foo %Z bar");
1168 assert!(result.is_err());
1169 let err_msg = result.unwrap_err().to_string();
1170 assert!(err_msg.contains("invalid format specifier"));
1171 assert!(err_msg.contains("%Z"));
1172 }
1173
1174 #[test]
1175 fn test_mismatched_arg_count_includes_specifiers_and_kinds() {
1176 let user = TypeName::importable("./models", "User");
1177 let mut b = CodeBlock::builder();
1178 b.add("%T %S %L", (user,));
1179 let result = b.build();
1180 assert!(result.is_err());
1181 let err_msg = result.unwrap_err().to_string();
1182 assert!(err_msg.contains("expects 3 args but got 1"));
1183 assert!(err_msg.contains("%T"));
1184 assert!(err_msg.contains("%S"));
1185 assert!(err_msg.contains("%L"));
1186 assert!(err_msg.contains("TypeName"));
1187 }
1188
1189 #[test]
1190 fn test_begin_control_flow_stores_condition() {
1191 let mut b = CodeBlock::builder();
1192 b.begin_control_flow("class Functor f", ());
1193 b.add_statement("fmap :: (a -> b) -> f a -> f b", ());
1194 b.end_control_flow();
1195 let block = b.build().unwrap();
1196 let has_open = block
1197 .nodes
1198 .iter()
1199 .any(|n| matches!(n, CodeNode::BlockOpen(s) if s == "class Functor f"));
1200 assert!(has_open, "should contain BlockOpen with condition text");
1201 let has_close = block
1202 .nodes
1203 .iter()
1204 .any(|n| matches!(n, CodeNode::BlockClose(s) if s == "class Functor f"));
1205 assert!(has_close, "should contain BlockClose with condition text");
1206 }
1207
1208 #[test]
1209 fn test_begin_control_flow_match_empty_open() {
1210 let mut b = CodeBlock::builder();
1211 b.begin_control_flow("match x with", ());
1212 b.add("| Red -> red", ());
1213 b.add_line();
1214 b.end_control_flow();
1215 let block = b.build().unwrap();
1216 let has_open = block
1217 .nodes
1218 .iter()
1219 .any(|n| matches!(n, CodeNode::BlockOpen(s) if s == "match x with"));
1220 assert!(has_open, "should contain BlockOpen(\"match x with\")");
1221 }
1222}