Skip to main content

sigil_stitch/
code_block.rs

1use crate::code_node::{CodeNode, parts_args_to_nodes};
2use crate::import::ImportRef;
3use crate::lang::CodeLang;
4use crate::type_name::TypeName;
5
6/// Argument-consuming format specifier kinds.
7///
8/// This is the single source of truth for what interpolation specifiers exist.
9/// Both the library's `parse_format()` and the `sigil_quote!` macro's codegen
10/// map to these same logical kinds. The macro crate cannot import this type
11/// (proc-macro dependency cycle), but the format characters are shared: the
12/// macro emits `%T`/`%N`/`%S`/`%L` strings that `parse_format` then parses
13/// via [`Specifier::from_format_char`].
14///
15/// Adding a variant here without handling it in `parse_format` and
16/// `parts_args_to_nodes` will cause exhaustiveness errors.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
18pub enum Specifier {
19    /// `%T` / `$T` — type reference (consumes `Arg::TypeName`).
20    Type,
21    /// `%N` / `$N` — name identifier (consumes `Arg::Name`).
22    Name,
23    /// `%S` / `$S` — string literal (consumes `Arg::StringLit`).
24    StringLit,
25    /// `%V` / `$V` — verbatim string literal (consumes `Arg::VerbatimStr`).
26    /// Escapes only structural delimiters, preserving interpolation sigils.
27    VerbatimStr,
28    /// `%L` / `$L` / `$C` — literal value or nested code block (consumes `Arg::Literal` or `Arg::Code`).
29    Literal,
30    /// `%R` / `$comment` — inline comment (consumes `Arg::Comment`).
31    Comment,
32}
33
34impl Specifier {
35    /// Map a format-string character to a specifier.
36    ///
37    /// Returns `None` for characters that are not argument-consuming specifiers
38    /// (e.g. `W`, `>`, `<`, `[`, `]`, `%`).
39    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    /// The format-string character for this specifier.
52    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    /// All defined specifier variants.
64    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/// A parsed format specifier from a format string.
77#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
78pub(crate) enum FormatPart {
79    /// Literal text (no interpolation).
80    Literal(String),
81    /// An argument-consuming specifier (`%T`, `%N`, `%S`, `%L`).
82    Arg(Specifier),
83    /// `%W` - soft line break point (no argument consumed).
84    Wrap,
85    /// `%>` - increase indent (no argument consumed).
86    Indent,
87    /// `%<` - decrease indent (no argument consumed).
88    Dedent,
89    /// `%[` - statement begin (no argument consumed).
90    StatementBegin,
91    /// `%]` - statement end (no argument consumed).
92    StatementEnd,
93    /// Newline.
94    Newline,
95    /// Block open delimiter — resolved at render time via `lang.block_open_for(condition)`
96    /// falling back to `lang.block_syntax().block_open`. Carries the condition text
97    /// from `begin_control_flow` (e.g., `"if x > 0"`, `"for i in range(10)"`).
98    /// Empty string means no condition (e.g., a bare `{ }` block).
99    BlockOpen(String),
100    /// Terminal block close delimiter — resolved at render time via
101    /// `lang.block_close_for(condition)` falling back to `lang.block_syntax().block_close`.
102    /// Carries the condition from the matching `begin_control_flow`.
103    /// Emits: closer only.
104    BlockClose(String),
105    /// Non-terminal block close before a branch keyword (`else`, `elif`, `catch`).
106    /// Like `BlockClose` but emits closer + space (not newline) so the branch
107    /// keyword continues on the same line (e.g., `} else {`).
108    /// Suppressed when `block_syntax().close_on_transition` is `false`.
109    BranchClose(String),
110}
111
112/// An argument to a CodeBlock format string.
113#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
114pub enum Arg {
115    /// A type name reference (used by `%T`).
116    TypeName(TypeName),
117    /// A name string (used by `%N`).
118    Name(String),
119    /// A string literal value (used by `%S`).
120    StringLit(String),
121    /// A verbatim string literal value (used by `%V`).
122    VerbatimStr(String),
123    /// A literal string value or nested code block (used by `%L`).
124    Literal(String),
125    /// A nested code block (used by `%L`).
126    Code(CodeBlock),
127    /// An inline comment (used by `%R` / `$comment`).
128    Comment(String),
129}
130
131/// An immutable code fragment with embedded type references.
132///
133/// `CodeBlock` is the core composition primitive in sigil-stitch. It stores a tree
134/// of [`CodeNode`] nodes — self-contained IR nodes produced from format strings
135/// (`%T`, `%N`, `%S`, `%L`, etc.). CodeBlocks are produced by [`CodeBlockBuilder`]
136/// and consumed by [`FileSpec`](crate::spec::file_spec::FileSpec) during rendering.
137/// Type references embedded via `%T` are automatically tracked for import resolution.
138///
139/// Use [`CodeBlock::builder()`] to construct a block incrementally, or
140/// [`CodeBlock::of()`] for simple one-liners.
141///
142/// # Examples
143///
144/// ```
145/// use sigil_stitch::code_block::CodeBlock;
146/// use sigil_stitch::lang::typescript::TypeScript;
147/// use sigil_stitch::type_name::TypeName;
148///
149/// // One-liner with a type reference:
150/// let user = TypeName::importable("./models", "User");
151/// let block = CodeBlock::of("const u: %T = getUser()", (user,)).unwrap();
152///
153/// // Multi-statement block via builder:
154/// let mut cb = CodeBlock::builder();
155/// cb.add_statement("const x = 1", ());
156/// cb.add_statement("const y = 2", ());
157/// let block = cb.build().unwrap();
158/// ```
159#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
160pub struct CodeBlock {
161    pub(crate) nodes: Vec<CodeNode>,
162}
163
164/// A parsed, composable code fragment.
165///
166/// `CodeFragment` is for snippets that should preserve sigil-stitch structure
167/// such as `%>` / `%<` indentation markers. Raw `&str` / `String` values passed
168/// through `%L` remain literal text; use `CodeFragment::of(...)` when a fragment
169/// contains format markers that must compose structurally inside another block.
170#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
171pub struct CodeFragment {
172    block: CodeBlock,
173}
174
175impl CodeFragment {
176    /// Parse a format string and arguments into a composable fragment.
177    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    /// Convert this fragment into a `CodeBlock`.
186    pub fn into_code_block(self) -> CodeBlock {
187        self.block
188    }
189}
190
191impl CodeBlock {
192    /// Create a new CodeBlockBuilder.
193    pub fn builder() -> CodeBlockBuilder {
194        CodeBlockBuilder::new()
195    }
196
197    /// Access the node tree for rewriting. Used by language rewrite passes.
198    pub fn nodes_mut(&mut self) -> &mut Vec<CodeNode> {
199        &mut self.nodes
200    }
201
202    /// Create a CodeBlock from a single format string and arguments.
203    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    /// Check if this code block is empty.
210    pub fn is_empty(&self) -> bool {
211        self.nodes.is_empty()
212    }
213
214    /// Create a parsed fragment from a single format string and arguments.
215    pub fn fragment(
216        format: &str,
217        args: impl IntoArgs,
218    ) -> Result<CodeFragment, crate::error::SigilStitchError> {
219        CodeFragment::of(format, args)
220    }
221
222    /// Check if this code block ends with a newline or block close.
223    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    /// Remove one trailing newline from this block.
236    ///
237    /// This is used by `sigil_quote!` for inline meta-splices (`$for`/`$if`
238    /// inside expressions). Statement bodies naturally emit a final newline,
239    /// but expression splices must not leak that newline before `]`, `)`, `}`,
240    /// or `;` in the surrounding expression.
241    #[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    /// Collect all import references from this code block.
260    pub fn collect_imports(&self, out: &mut Vec<ImportRef>) {
261        crate::import_collector::walk_nodes(&self.nodes, out);
262    }
263
264    /// Render this code block to a string without import resolution.
265    ///
266    /// Creates a temporary empty import group and renders using the given
267    /// language and target line width. Useful for quick one-off rendering
268    /// in tests or when import management is not needed.
269    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/// Builder for constructing [`CodeBlock`] instances.
281///
282/// Provides methods for adding formatted code fragments, statements, control
283/// flow blocks, and nested code blocks. Format strings use `%T`, `%N`, `%S`,
284/// `%L` for type/name/string/literal substitution, and `%W`, `%>`, `%<` for
285/// soft line breaks and indentation.
286///
287/// # Examples
288///
289/// ```
290/// use sigil_stitch::code_block::CodeBlock;
291/// use sigil_stitch::lang::typescript::TypeScript;
292///
293/// let mut cb = CodeBlock::builder();
294/// cb.begin_control_flow("if (x > 0)", ());
295/// cb.add_statement("return x", ());
296/// cb.next_control_flow("else", ());
297/// cb.add_statement("return -x", ());
298/// cb.end_control_flow();
299/// let block = cb.build().unwrap();
300/// ```
301#[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    /// Create a new empty code block builder.
311    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    /// Add a formatted code fragment.
321    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    /// Add a statement (wraps in %[...%] and appends language semicolon).
334    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    /// Begin a control flow block (e.g., "if foo" -> "if foo {\n" + indent).
343    ///
344    /// The **raw format string** (not the interpolated result) is stored as
345    /// the condition text and passed to `block_open_for` / `block_close_for`
346    /// at render time, enabling language backends to emit context-aware
347    /// delimiters (e.g., Bash `then`/`fi` for `if`, `do`/`done` for `for`).
348    ///
349    /// Because backends pattern-match on the stored condition (e.g.,
350    /// `condition.starts_with("if ")`), avoid interpolating into the keyword
351    /// prefix — `begin_control_flow("if %L", expr)` works, but
352    /// `begin_control_flow("%L x", some_keyword)` would not be recognized.
353    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    /// Add an else/else-if clause (e.g., "} else {" or "elif ...:" for Python).
365    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    /// End a control flow block (emits language-specific closer + newline,
380    /// decreases indent).
381    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    /// End a control flow block without a trailing newline.
391    ///
392    /// Used when the block is nested inside a `Statement::Statement` via
393    /// `%L` (e.g., expression braces in format strings). The outer
394    /// `add_statement` provides both `;` via `StatementEnd` and `\n` via
395    /// `Newline`.
396    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    /// End a control flow block with a trailing semicolon (for expression-level
405    /// control flow like `match` in PHP/Rust).
406    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    /// Add a blank line.
417    pub fn add_line(&mut self) -> &mut Self {
418        self.nodes.push(CodeNode::Newline);
419        self
420    }
421
422    /// Add an inline comment.
423    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    /// Add a language-aware attribute / annotation.
430    ///
431    /// Rendered with the language's annotation prefix and suffix
432    /// (Rust: `#[text]`, Java/Python: `@text`, C++: `[[text]]`).
433    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    /// Add a nested CodeBlock inline.
440    pub fn add_code(&mut self, block: CodeBlock) -> &mut Self {
441        self.nodes.push(CodeNode::Nested(block));
442        self
443    }
444
445    /// Add a parsed fragment inline.
446    pub fn add_fragment(&mut self, fragment: CodeFragment) -> &mut Self {
447        self.add_code(fragment.into_code_block())
448    }
449
450    /// Build the immutable CodeBlock.
451    ///
452    /// Returns an error if any format string had an argument count mismatch,
453    /// or if indent depth is not balanced (unmatched
454    /// begin_control_flow / end_control_flow).
455    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    /// Build the CodeBlock, panicking on error.
470    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
575/// Parse a format string into FormatParts.
576fn 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
632// === IntoArgs trait and implementations ===
633
634/// Trait for converting various types into a `Vec<Arg>` for format strings.
635///
636/// Implemented for `()` (no args), `TypeName`, `&str`, `String`, `CodeBlock`,
637/// `NameArg`, `StringLitArg`, `Vec<Arg>`, and tuples up to 8 elements.
638/// Bare strings convert to `Arg::Literal`; use [`NameArg`] or [`StringLitArg`]
639/// wrappers to target `%N` or `%S` specifiers instead.
640pub trait IntoArgs {
641    /// Convert into a vector of format arguments.
642    fn into_args(self) -> Vec<Arg>;
643}
644
645/// Empty args (for format strings with no specifiers).
646impl IntoArgs for () {
647    fn into_args(self) -> Vec<Arg> {
648        Vec::new()
649    }
650}
651
652/// Single TypeName arg.
653impl IntoArgs for TypeName {
654    fn into_args(self) -> Vec<Arg> {
655        vec![Arg::TypeName(self)]
656    }
657}
658
659/// Single string arg (as literal).
660impl 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
672/// Single CodeBlock arg.
673impl IntoArgs for CodeBlock {
674    fn into_args(self) -> Vec<Arg> {
675        vec![Arg::Code(self)]
676    }
677}
678
679/// Single parsed fragment arg.
680impl IntoArgs for CodeFragment {
681    fn into_args(self) -> Vec<Arg> {
682        vec![Arg::Code(self.into_code_block())]
683    }
684}
685
686/// Pre-built args vector (used by specs that dynamically build format strings).
687impl IntoArgs for Vec<Arg> {
688    fn into_args(self) -> Vec<Arg> {
689        self
690    }
691}
692
693/// A wrapper to explicitly mark a string as a Name arg (for `%N`).
694///
695/// By default, bare strings convert to `Arg::Literal` (for `%L`). Wrap with
696/// `NameArg` when your format string uses `%N`.
697///
698/// # Examples
699///
700/// ```
701/// use sigil_stitch::code_block::{CodeBlock, NameArg};
702/// use sigil_stitch::lang::typescript::TypeScript;
703///
704/// let mut cb = CodeBlock::builder();
705/// cb.add("this.%N()", (NameArg("getData".to_string()),));
706/// let block = cb.build().unwrap();
707/// ```
708pub 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
716/// A wrapper to explicitly mark a string as a StringLit arg (for `%S`).
717///
718/// By default, bare strings convert to `Arg::Literal` (for `%L`). Wrap with
719/// `StringLitArg` when your format string uses `%S` to emit a quoted string.
720///
721/// # Examples
722///
723/// ```
724/// use sigil_stitch::code_block::{CodeBlock, StringLitArg};
725/// use sigil_stitch::lang::typescript::TypeScript;
726///
727/// let mut cb = CodeBlock::builder();
728/// cb.add_statement("const msg = %S", (StringLitArg("hello".to_string()),));
729/// let block = cb.build().unwrap();
730/// ```
731pub 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
739/// Wrapper for verbatim string literal arguments — preserves interpolation sigils.
740///
741/// Use `VerbatimStrArg` when your format string uses `%V` to emit a string with
742/// minimal escaping (only structural delimiters escaped, interpolation preserved).
743///
744/// ```ignore
745/// use sigil_stitch::code_block::{CodeBlock, VerbatimStrArg};
746///
747/// let mut cb = CodeBlock::builder();
748/// cb.add_statement("echo %V", (VerbatimStrArg("$HOME/.config".to_string()),));
749/// let block = cb.build().unwrap();
750/// ```
751pub 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
759/// A wrapper to mark a string as an inline comment arg (for `%R` / `$comment`).
760///
761/// Use `CommentArg` when your format string uses `%R` to emit a language-specific
762/// comment at the current position.
763///
764/// # Examples
765///
766/// ```
767/// use sigil_stitch::code_block::{CodeBlock, CommentArg};
768///
769/// let mut cb = CodeBlock::builder();
770/// cb.add_statement("const x = 42; %R", (CommentArg("TODO: validate".to_string()),));
771/// let block = cb.build().unwrap();
772/// ```
773pub 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
781// Individual Arg conversions.
782impl 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
836// Tuple implementations for IntoArgs.
837// Each element must implement Into<Arg>.
838
839macro_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        // missing end_control_flow
939        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}