howzit 2.1.28 → 2.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/howzit/topic.rb CHANGED
@@ -7,22 +7,25 @@ module Howzit
7
7
 
8
8
  attr_accessor :content
9
9
 
10
- attr_reader :title, :tasks, :prereqs, :postreqs, :results, :named_args
10
+ attr_reader :title, :tasks, :prereqs, :postreqs, :results, :named_args, :directives
11
11
 
12
12
  ##
13
13
  ## Initialize a topic object
14
14
  ##
15
15
  ## @param title [String] The topic title
16
16
  ## @param content [String] The raw topic content
17
+ ## @param metadata [Hash] Optional metadata hash
17
18
  ##
18
- def initialize(title, content)
19
+ def initialize(title, content, metadata = nil)
19
20
  @title = title
20
21
  @content = content
21
22
  @parent = nil
22
23
  @nest_level = 0
23
24
  @named_args = {}
25
+ @metadata = metadata
24
26
  arguments
25
27
 
28
+ @directives = parse_directives_with_conditionals
26
29
  @tasks = gather_tasks
27
30
  @results = { total: 0, success: 0, errors: 0, message: ''.c }
28
31
  end
@@ -79,9 +82,22 @@ module Howzit
79
82
 
80
83
  cols = check_cols
81
84
 
85
+ # Use sequential processing if we have directives with conditionals
86
+ if @directives && @directives.any?(&:conditional?)
87
+ return run_sequential(nested: nested, output: output, cols: cols)
88
+ end
89
+
90
+ # Fall back to old behavior for backward compatibility
91
+ # Note: @set_var directives are already processed in gather_tasks for non-sequential path
92
+ # This section is kept for backward compatibility but shouldn't be needed
93
+
82
94
  if @tasks.count.positive?
83
95
  unless @prereqs.empty?
84
- puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
96
+ begin
97
+ puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
98
+ rescue Errno::EPIPE
99
+ # Pipe closed, ignore
100
+ end
85
101
  res = Prompt.yn('Have the above prerequisites been met?', default: true)
86
102
  Process.exit 1 unless res
87
103
 
@@ -123,7 +139,13 @@ module Howzit
123
139
 
124
140
  output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
125
141
 
126
- puts TTY::Box.frame("{bw}#{@postreqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols) unless @postreqs.empty?
142
+ unless @postreqs.empty?
143
+ begin
144
+ puts TTY::Box.frame("{bw}#{@postreqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
145
+ rescue Errno::EPIPE
146
+ # Pipe closed, ignore
147
+ end
148
+ end
127
149
 
128
150
  output
129
151
  end
@@ -166,7 +188,8 @@ module Howzit
166
188
  output = []
167
189
 
168
190
  if keys[:action] =~ / *\[(.*?)\] *$/
169
- Howzit.named_arguments = @named_args
191
+ Howzit.named_arguments ||= {}
192
+ Howzit.named_arguments.merge!(@named_args) if @named_args
170
193
  Howzit.arguments = Regexp.last_match(1).split(/ *, */).map!(&:render_arguments)
171
194
  end
172
195
 
@@ -254,14 +277,16 @@ module Howzit
254
277
  output.push(@title.format_header)
255
278
  output.push('')
256
279
  end
257
- topic = @content.dup
280
+ # Process conditional blocks first
281
+ metadata = @metadata || Howzit.buildnote&.metadata
282
+ topic = ConditionalContent.process(@content.dup, { metadata: metadata })
258
283
  unless Howzit.options[:show_all_code]
259
284
  topic.gsub!(/(?mix)^(`{3,})run([?!]*)\s*
260
285
  ([^\n]*)[\s\S]*?\n\1\s*$/, '@@@run\2 \3')
261
286
  end
262
287
  topic.split(/\n/).each do |l|
263
288
  case l
264
- when /@(before|after|prereq|end)/
289
+ when /@(before|after|prereq|end|if|unless)/
265
290
  next
266
291
  when /@include(?<optional>[!?]{1,2})?\((?<action>[^)]+)\)/
267
292
  output.concat(process_include(Regexp.last_match.named_captures.symbolize_keys, opt))
@@ -300,11 +325,13 @@ module Howzit
300
325
  # Store the actual title (not overridden by show_all_code - that's only for display)
301
326
  task_args = { type: :include,
302
327
  arguments: nil,
303
- title: title.dup, # Make a copy to avoid reference issues
328
+ title: title.dup, # Make a copy to avoid reference issues
304
329
  action: obj,
305
330
  parent: self }
306
331
  # Set named_arguments before processing titles for variable substitution
307
- Howzit.named_arguments = @named_args
332
+ # Merge with existing named_arguments to preserve @set_var variables
333
+ Howzit.named_arguments ||= {}
334
+ Howzit.named_arguments.merge!(@named_args) if @named_args
308
335
  case cmd
309
336
  when /include/i
310
337
  if title =~ /\[(.*?)\] *$/
@@ -321,6 +348,14 @@ module Howzit
321
348
  when /run/i
322
349
  task_args[:type] = :run
323
350
  task_args[:title] = title.render_arguments
351
+ # Parse log_level from action if present (format: script, log_level=level)
352
+ if obj =~ /,\s*log_level\s*=\s*(\w+)/i
353
+ log_level = Regexp.last_match(1).downcase
354
+ task_args[:log_level] = log_level
355
+ # Remove log_level parameter from action
356
+ obj = obj.sub(/,\s*log_level\s*=\s*\w+/i, '').strip
357
+ end
358
+ task_args[:action] = obj
324
359
  when /copy/i
325
360
  task_args[:type] = :copy
326
361
  task_args[:action] = Shellwords.escape(obj)
@@ -363,18 +398,57 @@ module Howzit
363
398
 
364
399
  def gather_tasks
365
400
  runnable = []
366
- @prereqs = @content.scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
367
- @postreqs = @content.scan(/(?<=@after\n).*?(?=\n@end)/im).map(&:strip)
401
+ # Process conditional blocks first
402
+ # Set named_arguments before processing so conditions can access them
403
+ Howzit.named_arguments ||= {}
404
+ Howzit.named_arguments.merge!(@named_args) if @named_args
405
+
406
+ # Process @set_var directives before gathering tasks (for non-sequential path)
407
+ # This ensures variables are available when task actions are rendered
408
+ if @directives && !@directives.any?(&:conditional?)
409
+ @directives.each do |dir|
410
+ next unless dir.set_var?
411
+
412
+ value = dir.var_value
413
+ if value
414
+ # Check for command substitution: backticks or $()
415
+ if value =~ /^`(.+)`$/ || value =~ /^\$\((.+)\)$/
416
+ command = Regexp.last_match(1).strip
417
+ # Apply variable substitution to command before execution
418
+ command = command.render_arguments
419
+ # Execute command and capture output
420
+ begin
421
+ value = `#{command}`.strip
422
+ rescue StandardError => e
423
+ Howzit.console.warn("Error executing command in @set_var: #{e.message}")
424
+ value = ''
425
+ end
426
+ else
427
+ # Apply variable substitution to the value (in case it references other variables)
428
+ value = value.render_arguments
429
+ end
430
+ end
431
+
432
+ Howzit.named_arguments[dir.var_name] = value
433
+ end
434
+ end
435
+
436
+ metadata = @metadata || Howzit.buildnote&.metadata
437
+ processed_content = ConditionalContent.process(@content, { metadata: metadata })
438
+
439
+ @prereqs = processed_content.scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
440
+ @postreqs = processed_content.scan(/(?<=@after\n).*?(?=\n@end)/im).map(&:strip)
368
441
 
369
442
  rx = /(?mix)(?:
370
443
  @(?<cmd>include|run|copy|open|url)(?<optional>[!?]{1,2})?\((?<action>[^)]*?)\)(?<title>[^\n]+)?
371
444
  |(?<fence>`{3,})run(?<optional2>[!?]{1,2})?(?<title2>[^\n]+)?(?<block>.*?)\k<fence>
372
445
  )/
373
446
  matches = []
374
- @content.scan(rx) { matches << Regexp.last_match }
447
+ processed_content.scan(rx) { matches << Regexp.last_match }
375
448
  matches.each do |m|
376
449
  c = m.named_captures.symbolize_keys
377
- Howzit.named_arguments = @named_args
450
+ Howzit.named_arguments ||= {}
451
+ Howzit.named_arguments.merge!(@named_args) if @named_args
378
452
 
379
453
  if c[:cmd].nil?
380
454
  optional, default = define_optional(c[:optional2])
@@ -398,5 +472,494 @@ module Howzit
398
472
 
399
473
  runnable
400
474
  end
475
+
476
+ ##
477
+ ## Parse directives with conditional context for sequential evaluation
478
+ ##
479
+ ## @return [Array] Array of Directive objects
480
+ ##
481
+ def parse_directives_with_conditionals
482
+ directives = []
483
+ lines = @content.split(/\n/)
484
+ conditional_stack = [] # Array of directive indices for @if/@unless directives
485
+ current_branch_index = nil # Track current @if/@elsif/@else branch index
486
+ line_num = 0
487
+ in_code_block = false
488
+ code_block_lines = []
489
+ code_block_fence = nil
490
+ code_block_title = nil
491
+ code_block_optional = nil
492
+
493
+ # Extract prereqs and postreqs from raw content
494
+ @prereqs = @content.scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
495
+ @postreqs = @content.scan(/(?<=@after\n).*?(?=\n@end)/im).map(&:strip)
496
+
497
+ lines.each do |line|
498
+ line_num += 1
499
+
500
+ # Handle code blocks (fenced code)
501
+ if line =~ /^(`{3,})run([?!]*)\s*(.*?)$/i && !in_code_block
502
+ in_code_block = true
503
+ code_block_fence = Regexp.last_match(1)
504
+ code_block_optional = Regexp.last_match(2)
505
+ code_block_title = Regexp.last_match(3).strip
506
+ code_block_lines = []
507
+ next
508
+ elsif in_code_block
509
+ if line =~ /^#{Regexp.escape(code_block_fence)}\s*$/
510
+ # End of code block
511
+ block_content = code_block_lines.join("\n")
512
+ optional, default = define_optional(code_block_optional)
513
+ conditional_path = conditional_stack.dup
514
+ # If we're inside an @elsif/@else branch, include it in the path
515
+ conditional_path << current_branch_index if current_branch_index
516
+ directives << Howzit::Directive.new(
517
+ type: :task,
518
+ content: {
519
+ type: :block,
520
+ title: code_block_title,
521
+ action: block_content,
522
+ arguments: nil
523
+ },
524
+ optional: optional,
525
+ default: default,
526
+ line_number: line_num,
527
+ conditional_path: conditional_path
528
+ )
529
+ in_code_block = false
530
+ code_block_lines = []
531
+ code_block_fence = nil
532
+ else
533
+ code_block_lines << line
534
+ end
535
+ next
536
+ end
537
+
538
+ # Handle conditional directives
539
+ if line =~ /^@(if|unless)\s+(.+)$/i
540
+ directive_type = Regexp.last_match(1).downcase
541
+ condition = Regexp.last_match(2).strip
542
+ directive_index = directives.length
543
+ conditional_stack << directive_index
544
+ current_branch_index = directive_index
545
+ directives << Howzit::Directive.new(
546
+ type: directive_type.to_sym,
547
+ condition: condition,
548
+ directive_type: directive_type,
549
+ line_number: line_num,
550
+ conditional_path: conditional_stack[0..-2].dup
551
+ )
552
+ next
553
+ elsif line =~ /^@elsif\s+(.+)$/i
554
+ condition = Regexp.last_match(1).strip
555
+ directive_index = directives.length
556
+ current_branch_index = directive_index
557
+ directives << Howzit::Directive.new(
558
+ type: :elsif,
559
+ condition: condition,
560
+ directive_type: 'elsif',
561
+ line_number: line_num,
562
+ conditional_path: conditional_stack[0..-2].dup
563
+ )
564
+ next
565
+ elsif line =~ /^@else\s*$/i
566
+ directive_index = directives.length
567
+ current_branch_index = directive_index
568
+ directives << Howzit::Directive.new(
569
+ type: :else,
570
+ directive_type: 'else',
571
+ line_number: line_num,
572
+ conditional_path: conditional_stack[0..-2].dup
573
+ )
574
+ next
575
+ elsif line =~ /^@end\s*$/i && !conditional_stack.empty?
576
+ # Closing a conditional block
577
+ conditional_stack.pop
578
+ current_branch_index = nil
579
+ directives << Howzit::Directive.new(
580
+ type: :end,
581
+ directive_type: 'end',
582
+ line_number: line_num,
583
+ conditional_path: conditional_stack.dup
584
+ )
585
+ next
586
+ end
587
+
588
+ # Handle @log_level directive
589
+ if line =~ /^@log_level\s*\(([^)]+)\)\s*$/i
590
+ log_level = Regexp.last_match(1).strip
591
+ conditional_path = conditional_stack.dup
592
+ directives << Howzit::Directive.new(
593
+ type: :log_level,
594
+ log_level_value: log_level,
595
+ line_number: line_num,
596
+ conditional_path: conditional_path
597
+ )
598
+ next
599
+ end
600
+
601
+ # Handle @set_var directive
602
+ if line =~ /^@set_var\s*\(/i
603
+ # Extract content between parentheses, handling nested parentheses
604
+ paren_content = line.sub(/^@set_var\s*\(/i, '').sub(/\)\s*$/, '')
605
+ # Split by first comma only - everything after first comma is the value
606
+ if paren_content =~ /^([^,]+),\s*(.+)$/
607
+ var_name = Regexp.last_match(1).strip
608
+ var_value = Regexp.last_match(2).strip
609
+ # Validate variable name: alphanumeric, dashes, underscores only
610
+ if var_name =~ /^[A-Za-z0-9_-]+$/
611
+ # Remove quotes from value if present (handles both single and double quotes)
612
+ var_value = Regexp.last_match(1) if var_value =~ /^["'](.+)["']$/
613
+ conditional_path = conditional_stack.dup
614
+ directives << Howzit::Directive.new(
615
+ type: :set_var,
616
+ var_name: var_name,
617
+ var_value: var_value,
618
+ line_number: line_num,
619
+ conditional_path: conditional_path
620
+ )
621
+ end
622
+ end
623
+ next
624
+ end
625
+
626
+ # Handle task directives (@run, @include, etc.)
627
+ unless line =~ /^@(?<cmd>include|run|copy|open|url)(?<optional>[!?]{1,2})?\((?<action>[^)]*?)\)(?<title>.*?)$/
628
+ next
629
+ end
630
+
631
+ cmd = Regexp.last_match(:cmd)
632
+ optional_str = Regexp.last_match(:optional) || ''
633
+ action = Regexp.last_match(:action)
634
+ title = Regexp.last_match(:title).strip
635
+
636
+ optional, default = define_optional(optional_str)
637
+ conditional_path = conditional_stack.dup
638
+ # If we're inside an @elsif/@else branch, include it in the path
639
+ conditional_path << current_branch_index if current_branch_index
640
+ directives << Howzit::Directive.new(
641
+ type: :task,
642
+ content: {
643
+ type: cmd.downcase.to_sym,
644
+ action: action,
645
+ title: title,
646
+ arguments: nil
647
+ },
648
+ optional: optional,
649
+ default: default,
650
+ line_number: line_num,
651
+ conditional_path: conditional_path
652
+ )
653
+ end
654
+
655
+ directives
656
+ end
657
+
658
+ ##
659
+ ## Run directives sequentially with conditional re-evaluation
660
+ ##
661
+ def run_sequential(nested: false, output: [], cols: 80)
662
+ # Initialize conditional state
663
+ conditional_state = {} # { index => { evaluated: bool, result: bool, matched_chain: bool } }
664
+ directive_index = 0
665
+ current_log_level = nil # Track current log level set by @log_level directives
666
+
667
+ # Initialize named_arguments with topic's named args (don't overwrite on each iteration)
668
+ Howzit.named_arguments ||= {}
669
+ Howzit.named_arguments.merge!(@named_args) if @named_args
670
+
671
+ unless @prereqs.empty?
672
+ begin
673
+ puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
674
+ rescue Errno::EPIPE
675
+ # Pipe closed, ignore
676
+ end
677
+ res = Prompt.yn('Have the above prerequisites been met?', default: true)
678
+ Process.exit 1 unless res
679
+ end
680
+
681
+ # Process directives sequentially
682
+ while directive_index < @directives.length
683
+ directive = @directives[directive_index]
684
+ directive_index += 1
685
+
686
+ # Update context for condition evaluation
687
+ metadata = @metadata || Howzit.buildnote&.metadata
688
+ context = { metadata: metadata }
689
+
690
+ # Handle conditional directives
691
+ if directive.conditional?
692
+ case directive.type
693
+ when :if, :unless
694
+ # Evaluate condition
695
+ result = ConditionEvaluator.evaluate(directive.condition, context)
696
+ result = !result if directive.directive_type == 'unless'
697
+
698
+ conditional_state[directive_index - 1] = {
699
+ evaluated: true,
700
+ result: result,
701
+ matched_chain: result,
702
+ condition: directive.condition,
703
+ directive_type: directive.directive_type
704
+ }
705
+
706
+ when :elsif
707
+ # Find the matching @if/@unless
708
+ matching_if_index = find_matching_if_index(directive_index - 1)
709
+ if matching_if_index && conditional_state[matching_if_index]
710
+ # If previous branch matched, this is false
711
+ if conditional_state[matching_if_index][:matched_chain]
712
+ conditional_state[directive_index - 1] = {
713
+ evaluated: true,
714
+ result: false,
715
+ matched_chain: false,
716
+ condition: directive.condition,
717
+ directive_type: 'elsif',
718
+ parent_index: matching_if_index
719
+ }
720
+ else
721
+ # Evaluate condition
722
+ result = ConditionEvaluator.evaluate(directive.condition, context)
723
+ conditional_state[directive_index - 1] = {
724
+ evaluated: true,
725
+ result: result,
726
+ matched_chain: result,
727
+ condition: directive.condition,
728
+ directive_type: 'elsif',
729
+ parent_index: matching_if_index
730
+ }
731
+ conditional_state[matching_if_index][:matched_chain] = true if result
732
+ end
733
+ end
734
+
735
+ when :else
736
+ # Find the matching @if/@unless
737
+ matching_if_index = find_matching_if_index(directive_index - 1)
738
+ if matching_if_index && conditional_state[matching_if_index]
739
+ # If any previous branch matched, else is false
740
+ if conditional_state[matching_if_index][:matched_chain]
741
+ conditional_state[directive_index - 1] = {
742
+ evaluated: true,
743
+ result: false,
744
+ matched_chain: false,
745
+ directive_type: 'else',
746
+ parent_index: matching_if_index
747
+ }
748
+ else
749
+ conditional_state[directive_index - 1] = {
750
+ evaluated: true,
751
+ result: true,
752
+ matched_chain: true,
753
+ directive_type: 'else',
754
+ parent_index: matching_if_index
755
+ }
756
+ conditional_state[matching_if_index][:matched_chain] = true
757
+ end
758
+ end
759
+
760
+ when :end
761
+ # End of conditional block - no action needed, state is managed by stack
762
+ end
763
+ next
764
+ end
765
+
766
+ # Handle @log_level directive (before task check)
767
+ if directive.log_level?
768
+ current_log_level = directive.log_level_value
769
+ next
770
+ end
771
+
772
+ # Handle @set_var directive (before task check)
773
+ if directive.set_var?
774
+ # Set the variable in named_arguments
775
+ Howzit.named_arguments ||= {}
776
+ value = directive.var_value
777
+
778
+ if value
779
+ # Check for command substitution: backticks or $()
780
+ if value =~ /^`(.+)`$/ || value =~ /^\$\((.+)\)$/
781
+ command = Regexp.last_match(1).strip
782
+ # Apply variable substitution to command before execution
783
+ command = command.render_arguments
784
+ # Execute command and capture output
785
+ begin
786
+ value = `#{command}`.strip
787
+ rescue StandardError => e
788
+ Howzit.console.warn("Error executing command in @set_var: #{e.message}")
789
+ value = ''
790
+ end
791
+ else
792
+ # Apply variable substitution to the value (in case it references other variables)
793
+ value = value.render_arguments
794
+ end
795
+ end
796
+
797
+ Howzit.named_arguments[directive.var_name] = value
798
+ # Re-evaluate conditionals after setting variable
799
+ re_evaluate_conditionals(conditional_state, directive_index - 1, context)
800
+ next
801
+ end
802
+
803
+ # Handle task directives
804
+ next unless directive.task?
805
+
806
+ # Check if all parent conditionals are true
807
+ should_execute = true
808
+
809
+ # If path ends with an @elsif/@else, skip the parent @if index
810
+ # (the index right before the elsif/else in the path)
811
+ path_to_check = directive.conditional_path.dup
812
+ if path_to_check.length >= 2
813
+ last_idx = path_to_check.last
814
+ last_state = conditional_state[last_idx]
815
+ if last_state && %w[elsif else].include?(last_state[:directive_type])
816
+ # Skip the parent @if index (the one before the elsif/else)
817
+ parent_if_idx = path_to_check[path_to_check.length - 2]
818
+ parent_if_state = conditional_state[parent_if_idx]
819
+ if parent_if_state && %w[if unless].include?(parent_if_state[:directive_type])
820
+ path_to_check.delete(parent_if_idx)
821
+ end
822
+ end
823
+ end
824
+
825
+ path_to_check.each do |cond_idx|
826
+ cond_state = conditional_state[cond_idx]
827
+ if cond_state.nil? || !cond_state[:evaluated] || !cond_state[:result]
828
+ should_execute = false
829
+ break
830
+ end
831
+ end
832
+
833
+ next unless should_execute
834
+
835
+ # Convert directive to task
836
+ task = directive.to_task(self, current_log_level: current_log_level)
837
+ next unless task
838
+
839
+ next if (task.optional || Howzit.options[:ask]) && !ask_task(task)
840
+
841
+ run_output, total, success = task.run
842
+
843
+ output.concat(run_output)
844
+ @results[:total] += total
845
+
846
+ if success
847
+ @results[:success] += total
848
+ else
849
+ Howzit.console.warn %({bw}\u{2297} {br}Error running task {bw}"#{task.title}"{x}).c
850
+
851
+ @results[:errors] += total
852
+
853
+ break unless Howzit.options[:force]
854
+ end
855
+
856
+ log_task_result(task, success)
857
+
858
+ # Re-evaluate all open conditionals after task execution
859
+ re_evaluate_conditionals(conditional_state, directive_index - 1, context)
860
+ end
861
+
862
+ total = "{bw}#{@results[:total]}{by} #{@results[:total] == 1 ? 'task' : 'tasks'}".c
863
+ errors = "{bw}#{@results[:errors]}{by} #{@results[:errors] == 1 ? 'error' : 'errors'}".c
864
+ @results[:message] += if @results[:errors].zero?
865
+ "{bg}\u{2713} {by}Ran #{total}{x}".c
866
+ elsif Howzit.options[:force]
867
+ "{br}\u{2715} {by}Completed #{total} with #{errors}{x}".c
868
+ else
869
+ "{br}\u{2715} {by}Ran #{total}, terminated due to error{x}".c
870
+ end
871
+
872
+ output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
873
+
874
+ unless @postreqs.empty?
875
+ begin
876
+ puts TTY::Box.frame("{bw}#{@postreqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
877
+ rescue Errno::EPIPE
878
+ # Pipe closed, ignore
879
+ end
880
+ end
881
+
882
+ output
883
+ end
884
+
885
+ ##
886
+ ## Find the index of the matching @if/@unless for an @elsif/@else/@end
887
+ ##
888
+ def find_matching_if_index(current_index)
889
+ stack_depth = 0
890
+ (current_index - 1).downto(0) do |i|
891
+ dir = @directives[i]
892
+ next unless dir.conditional?
893
+
894
+ case dir.type
895
+ when :end
896
+ stack_depth += 1
897
+ when :if, :unless
898
+ return i if stack_depth.zero?
899
+
900
+ stack_depth -= 1
901
+
902
+ when :elsif, :else
903
+ stack_depth -= 1 if stack_depth.positive?
904
+ end
905
+ end
906
+ nil
907
+ end
908
+
909
+ ##
910
+ ## Re-evaluate conditionals after a task runs (variables may have changed)
911
+ ##
912
+ def re_evaluate_conditionals(conditional_state, current_index, context)
913
+ # Re-evaluate all conditionals that come after the current task
914
+ # and before the next task
915
+ (current_index + 1).upto(@directives.length - 1) do |i|
916
+ dir = @directives[i]
917
+ break if dir.task? # Stop at next task
918
+
919
+ next unless dir.conditional?
920
+
921
+ case dir.type
922
+ when :if, :unless
923
+ if conditional_state[i]
924
+ # Re-evaluate
925
+ result = ConditionEvaluator.evaluate(dir.condition, context)
926
+ result = !result if dir.directive_type == 'unless'
927
+ conditional_state[i][:result] = result
928
+ conditional_state[i][:matched_chain] = result
929
+ end
930
+ when :elsif
931
+ matching_if_index = find_matching_if_index(i)
932
+ if matching_if_index && conditional_state[matching_if_index]
933
+ parent_state = conditional_state[matching_if_index]
934
+ if conditional_state[i]
935
+ if parent_state[:matched_chain] && !conditional_state[i][:matched_chain]
936
+ conditional_state[i][:result] = false
937
+ else
938
+ result = ConditionEvaluator.evaluate(dir.condition, context)
939
+ conditional_state[i][:result] = result
940
+ conditional_state[i][:matched_chain] = result
941
+ parent_state[:matched_chain] = true if result
942
+ end
943
+ end
944
+ end
945
+ when :else
946
+ matching_if_index = find_matching_if_index(i)
947
+ if matching_if_index && conditional_state[matching_if_index]
948
+ parent_state = conditional_state[matching_if_index]
949
+ if conditional_state[i]
950
+ if parent_state[:matched_chain]
951
+ conditional_state[i][:result] = false
952
+ else
953
+ conditional_state[i][:result] = true
954
+ conditional_state[i][:matched_chain] = true
955
+ parent_state[:matched_chain] = true
956
+ end
957
+ end
958
+ end
959
+ when :end
960
+ # No re-evaluation needed
961
+ end
962
+ end
963
+ end
401
964
  end
402
965
  end