markdown-run 0.1.10 → 0.1.12

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.
@@ -3,11 +3,12 @@ require_relative "enum_helper"
3
3
  class ExecutionDecider
4
4
  include EnumHelper
5
5
 
6
- def initialize(current_block_run, current_block_rerun, current_block_lang, current_block_explain = false, current_block_result = true)
6
+ def initialize(current_block_run, current_block_rerun, current_block_lang, current_block_explain = false, current_block_flamegraph = false, current_block_result = true)
7
7
  @current_block_run = current_block_run
8
8
  @current_block_rerun = current_block_rerun
9
9
  @current_block_lang = current_block_lang
10
10
  @current_block_explain = current_block_explain
11
+ @current_block_flamegraph = current_block_flamegraph
11
12
  @current_block_result = current_block_result
12
13
  end
13
14
 
@@ -21,8 +22,10 @@ class ExecutionDecider
21
22
  handle_immediate_result_block(file_enum)
22
23
  elsif is_blank_line?(peek1)
23
24
  handle_blank_line_scenario(file_enum, expected_header_regex)
24
- elsif @current_block_explain && is_dalibo_link?(peek1)
25
+ elsif (@current_block_explain || @current_block_flamegraph) && is_dalibo_link?(peek1)
25
26
  handle_immediate_dalibo_link(file_enum)
27
+ elsif @current_block_flamegraph && is_flamegraph_link?(peek1)
28
+ handle_immediate_flamegraph_link(file_enum)
26
29
  else
27
30
  execute_without_existing_result
28
31
  end
@@ -48,11 +51,11 @@ class ExecutionDecider
48
51
 
49
52
  def handle_blank_line_scenario(file_enum, expected_header_regex)
50
53
  consumed_blank_line = file_enum.next
51
-
54
+
52
55
  # Look ahead past multiple blank lines to find actual content
53
56
  peek2 = peek_next_line(file_enum)
54
57
  additional_blanks = []
55
-
58
+
56
59
  # Consume consecutive blank lines
57
60
  while is_blank_line?(peek2)
58
61
  additional_blanks << file_enum.next
@@ -61,8 +64,10 @@ class ExecutionDecider
61
64
 
62
65
  if line_matches_pattern?(peek2, expected_header_regex)
63
66
  handle_result_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
64
- elsif @current_block_explain && is_dalibo_link?(peek2)
67
+ elsif (@current_block_explain || @current_block_flamegraph) && is_dalibo_link?(peek2)
65
68
  handle_dalibo_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
69
+ elsif @current_block_flamegraph && is_flamegraph_link?(peek2)
70
+ handle_flamegraph_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
66
71
  else
67
72
  execute_with_blank_lines(consumed_blank_line, additional_blanks)
68
73
  end
@@ -194,7 +199,7 @@ class ExecutionDecider
194
199
  # Consume all consecutive Dalibo links and blank lines
195
200
  loop do
196
201
  next_line = peek_next_line(file_enum)
197
-
202
+
198
203
  if is_blank_line?(next_line) || is_dalibo_link?(next_line)
199
204
  consumed_line = file_enum.next
200
205
  consumed_lines << consumed_line
@@ -205,7 +210,7 @@ class ExecutionDecider
205
210
  end
206
211
 
207
212
  def is_dalibo_link?(line)
208
- line&.start_with?("**Dalibo Visualization:**")
213
+ line&.start_with?("[Dalibo]")
209
214
  end
210
215
 
211
216
  def line_matches_pattern?(line, pattern)
@@ -217,9 +222,78 @@ class ExecutionDecider
217
222
  end
218
223
 
219
224
  def should_auto_replace_dalibo_link?
220
- # Auto-replace Dalibo links when using explain with result=false
225
+ # Auto-replace Dalibo links when using explain or flamegraph with result=false
221
226
  # This makes sense because with result=false, there's only a Dalibo link,
222
227
  # so it should be updated on each run
223
- @current_block_explain && !@current_block_result
228
+ (@current_block_explain || @current_block_flamegraph) && !@current_block_result
229
+ end
230
+
231
+ def is_flamegraph_link?(line)
232
+ line&.start_with?("![PostgreSQL Query Flamegraph]")
233
+ end
234
+
235
+ def handle_flamegraph_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
236
+ # For flamegraph result=false, always replace existing flamegraph links
237
+ # For flamegraph result=true, follow normal rerun logic
238
+ if should_auto_replace_flamegraph_link? || @current_block_rerun
239
+ execute_with_consumed_flamegraph_and_blanks(file_enum, consumed_blank_line, additional_blanks)
240
+ else
241
+ skip_with_blanks_and_flamegraph(file_enum, consumed_blank_line, additional_blanks)
242
+ end
243
+ end
244
+
245
+ def handle_immediate_flamegraph_link(file_enum)
246
+ # For flamegraph result=false, always replace existing flamegraph links
247
+ # For flamegraph result=true, follow normal rerun logic
248
+ if should_auto_replace_flamegraph_link? || @current_block_rerun
249
+ execute_with_consumed_flamegraph(file_enum)
250
+ else
251
+ skip_with_existing_flamegraph(file_enum)
252
+ end
253
+ end
254
+
255
+ def execute_with_consumed_flamegraph(file_enum)
256
+ consumed_lines = []
257
+ consume_flamegraph_links(file_enum, consumed_lines)
258
+ { execute: true, consumed_lines: consumed_lines, consume_existing_flamegraph: true }
259
+ end
260
+
261
+ def skip_with_existing_flamegraph(file_enum)
262
+ consumed_lines = []
263
+ consume_flamegraph_links(file_enum, consumed_lines)
264
+ { execute: false, lines_to_pass_through: consumed_lines, flamegraph_content: true }
265
+ end
266
+
267
+ def execute_with_consumed_flamegraph_and_blanks(file_enum, consumed_blank_line, additional_blanks)
268
+ consumed_lines = [consumed_blank_line] + additional_blanks
269
+ consume_flamegraph_links(file_enum, consumed_lines)
270
+ { execute: true, consumed_lines: consumed_lines, blank_line: consumed_blank_line, consume_existing_flamegraph: true }
271
+ end
272
+
273
+ def skip_with_blanks_and_flamegraph(file_enum, consumed_blank_line, additional_blanks)
274
+ consumed_lines = [consumed_blank_line] + additional_blanks
275
+ consume_flamegraph_links(file_enum, consumed_lines)
276
+ { execute: false, lines_to_pass_through: consumed_lines, flamegraph_content: true }
277
+ end
278
+
279
+ def consume_flamegraph_links(file_enum, consumed_lines)
280
+ # Consume all consecutive flamegraph links and blank lines
281
+ loop do
282
+ next_line = peek_next_line(file_enum)
283
+
284
+ if is_blank_line?(next_line) || is_flamegraph_link?(next_line)
285
+ consumed_line = file_enum.next
286
+ consumed_lines << consumed_line
287
+ else
288
+ break
289
+ end
290
+ end
291
+ end
292
+
293
+ def should_auto_replace_flamegraph_link?
294
+ # Auto-replace flamegraph links when using flamegraph with result=false
295
+ # This makes sense because with result=false, there's only a flamegraph link,
296
+ # so it should be updated on each run
297
+ @current_block_flamegraph && !@current_block_result
224
298
  end
225
299
  end
@@ -0,0 +1,39 @@
1
+ module FlamegraphHelper
2
+ private
3
+
4
+ FLAMEGRAPH_LINK_PREFIX = "FLAMEGRAPH_LINK:"
5
+
6
+ def extract_flamegraph_link(result_output)
7
+ # Check if the result contains a flamegraph link marker
8
+ if result_output.start_with?(FLAMEGRAPH_LINK_PREFIX)
9
+ lines = result_output.split("\n", 2)
10
+ flamegraph_path = lines[0].sub(FLAMEGRAPH_LINK_PREFIX, "")
11
+ clean_result = lines[1] || ""
12
+ flamegraph_link = "![PostgreSQL Query Flamegraph](#{flamegraph_path})"
13
+ [flamegraph_link, clean_result]
14
+ else
15
+ [nil, result_output]
16
+ end
17
+ end
18
+
19
+ def consume_flamegraph_link_if_present(file_enum, consumed_lines)
20
+ # Look ahead to see if there are flamegraph links after the result block
21
+ begin
22
+ # Keep consuming blank lines and flamegraph links until we hit something else
23
+ loop do
24
+ next_line = peek_next_line(file_enum)
25
+
26
+ if is_blank_line?(next_line)
27
+ consumed_lines << file_enum.next
28
+ elsif next_line&.start_with?("![PostgreSQL Query Flamegraph]")
29
+ consumed_lines << file_enum.next
30
+ else
31
+ # Hit something that's not a blank line or flamegraph link, stop consuming
32
+ break
33
+ end
34
+ end
35
+ rescue StopIteration
36
+ # End of file reached, nothing more to consume
37
+ end
38
+ end
39
+ end
@@ -6,9 +6,11 @@ class FrontmatterParser
6
6
 
7
7
  def initialize
8
8
  @aliases = {}
9
+ @defaults = {}
10
+ @language_defaults = {}
9
11
  end
10
12
 
11
- attr_reader :aliases
13
+ attr_reader :aliases, :defaults, :language_defaults
12
14
 
13
15
  def parse_frontmatter(file_enum, output_lines)
14
16
  first_line = peek_next_line(file_enum)
@@ -22,6 +24,26 @@ class FrontmatterParser
22
24
  @aliases[lang] || lang
23
25
  end
24
26
 
27
+ def get_default_value(option_name, language, fallback_default)
28
+ # Priority order:
29
+ # 1. Language-specific defaults (e.g., psql: { explain: true })
30
+ # 2. Global defaults (e.g., defaults: { rerun: true })
31
+ # 3. Fallback default (hardcoded in the application)
32
+
33
+ # Check language-specific defaults first
34
+ if @language_defaults[language] && @language_defaults[language].key?(option_name)
35
+ return @language_defaults[language][option_name]
36
+ end
37
+
38
+ # Check global defaults
39
+ if @defaults.key?(option_name)
40
+ return @defaults[option_name]
41
+ end
42
+
43
+ # Return fallback default
44
+ fallback_default
45
+ end
46
+
25
47
  private
26
48
 
27
49
  def collect_frontmatter_lines(file_enum, output_lines)
@@ -58,14 +80,34 @@ class FrontmatterParser
58
80
  markdown_run_config = frontmatter["markdown-run"]
59
81
  return unless markdown_run_config.is_a?(Hash)
60
82
 
83
+ # Extract aliases
61
84
  aliases = markdown_run_config["alias"]
62
- return unless aliases.is_a?(Array)
85
+ if aliases.is_a?(Array)
86
+ aliases.each do |alias_config|
87
+ next unless alias_config.is_a?(Hash)
88
+
89
+ alias_config.each do |alias_name, target_lang|
90
+ @aliases[alias_name.to_s] = target_lang.to_s
91
+ end
92
+ end
93
+ end
94
+
95
+ # Extract defaults
96
+ defaults = markdown_run_config["defaults"]
97
+ if defaults.is_a?(Hash)
98
+ defaults.each do |option_name, option_value|
99
+ @defaults[option_name.to_s] = option_value
100
+ end
101
+ end
63
102
 
64
- aliases.each do |alias_config|
65
- next unless alias_config.is_a?(Hash)
103
+ # Extract language-specific defaults
104
+ markdown_run_config.each do |key, value|
105
+ next if ["alias", "defaults"].include?(key)
106
+ next unless value.is_a?(Hash)
66
107
 
67
- alias_config.each do |alias_name, target_lang|
68
- @aliases[alias_name.to_s] = target_lang.to_s
108
+ @language_defaults[key.to_s] = {}
109
+ value.each do |option_name, option_value|
110
+ @language_defaults[key.to_s][option_name.to_s] = option_value
69
111
  end
70
112
  end
71
113
  end
@@ -1,7 +1,7 @@
1
1
  require 'securerandom'
2
2
 
3
3
  JS_CONFIG = {
4
- command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
4
+ command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
5
5
  # Check if bun is available
6
6
  bun_exists = system("command -v bun > /dev/null 2>&1")
7
7
  if bun_exists
@@ -16,20 +16,20 @@ JS_CONFIG = {
16
16
  }.freeze
17
17
 
18
18
  SQLITE_CONFIG = {
19
- command: ->(code_content, temp_file_path, input_file_path = nil, explain = false) { [ "sqlite3 #{temp_file_path}", { stdin_data: code_content } ] },
19
+ command: ->(code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) { [ "sqlite3 #{temp_file_path}", { stdin_data: code_content } ] },
20
20
  temp_file_suffix: ".db" # Temp file is the database
21
21
  }.freeze
22
22
 
23
23
  SUPPORTED_LANGUAGES = {
24
24
  "psql" => {
25
- command: ->(code_content, _temp_file_path, input_file_path = nil, explain = false) {
25
+ command: ->(code_content, _temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
26
26
  psql_exists = system("command -v psql > /dev/null 2>&1")
27
27
  unless psql_exists
28
28
  abort "Error: psql command not found. Please install PostgreSQL or ensure psql is in your PATH."
29
29
  end
30
30
 
31
- # Modify the SQL query if explain option is enabled
32
- if explain
31
+ # Modify the SQL query if explain or flamegraph option is enabled
32
+ if explain || flamegraph
33
33
  # Wrap the query with EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
34
34
  # Remove any trailing semicolons and whitespace, then add our EXPLAIN wrapper
35
35
  clean_query = code_content.strip.gsub(/;\s*$/, '')
@@ -41,7 +41,7 @@ SUPPORTED_LANGUAGES = {
41
41
  }
42
42
  },
43
43
  "ruby" => {
44
- command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
44
+ command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
45
45
  xmpfilter_exists = system("command -v xmpfilter > /dev/null 2>&1")
46
46
  unless xmpfilter_exists
47
47
  abort "Error: xmpfilter command not found. Please install xmpfilter or ensure it is in your PATH."
@@ -57,7 +57,7 @@ SUPPORTED_LANGUAGES = {
57
57
  "sqlite" => SQLITE_CONFIG,
58
58
  "sqlite3" => SQLITE_CONFIG, # Alias for sqlite
59
59
  "bash" => {
60
- command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
60
+ command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
61
61
  bash_exists = system("command -v bash > /dev/null 2>&1")
62
62
  unless bash_exists
63
63
  abort "Error: bash command not found. Please ensure bash is in your PATH."
@@ -67,7 +67,7 @@ SUPPORTED_LANGUAGES = {
67
67
  temp_file_suffix: ".sh"
68
68
  },
69
69
  "zsh" => {
70
- command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
70
+ command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
71
71
  zsh_exists = system("command -v zsh > /dev/null 2>&1")
72
72
  unless zsh_exists
73
73
  abort "Error: zsh command not found. Please ensure zsh is in your PATH."
@@ -77,7 +77,7 @@ SUPPORTED_LANGUAGES = {
77
77
  temp_file_suffix: ".zsh"
78
78
  },
79
79
  "sh" => {
80
- command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
80
+ command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
81
81
  sh_exists = system("command -v sh > /dev/null 2>&1")
82
82
  unless sh_exists
83
83
  abort "Error: sh command not found. Please ensure sh is in your PATH."
@@ -87,7 +87,7 @@ SUPPORTED_LANGUAGES = {
87
87
  temp_file_suffix: ".sh"
88
88
  },
89
89
  "mermaid" => {
90
- command: ->(code_content, temp_file_path, input_file_path = nil, explain = false) {
90
+ command: ->(code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
91
91
  mmdc_exists = system("command -v mmdc > /dev/null 2>&1")
92
92
  unless mmdc_exists
93
93
  abort "Error: mmdc command not found. Please install @mermaid-js/mermaid-cli: npm install -g @mermaid-js/mermaid-cli"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Markdown
4
4
  module Run
5
- VERSION = "0.1.10"
5
+ VERSION = "0.1.12"
6
6
  end
7
7
  end
@@ -4,20 +4,22 @@ require_relative "code_block_parser"
4
4
  require_relative "code_executor"
5
5
  require_relative "execution_decider"
6
6
  require_relative "enum_helper"
7
+ require_relative "dalibo_helper"
8
+ require_relative "code_block_helper"
9
+ require_relative "result_helper"
7
10
 
8
11
  class MarkdownProcessor
9
12
  include EnumHelper
13
+ include DaliboHelper
14
+ include CodeBlockHelper
15
+ include ResultHelper
16
+
10
17
  def initialize(temp_dir, input_file_path = nil)
11
18
  @temp_dir = temp_dir
12
19
  @input_file_path = input_file_path
13
20
  @output_lines = []
14
- @state = :outside_code_block
15
- @current_block_lang = ""
16
- @current_code_content = ""
17
- @current_block_rerun = false
18
- @current_block_run = true
19
- @current_block_explain = false
20
- @current_block_result = true
21
+ reset_code_block_state
22
+
21
23
  @frontmatter_parser = FrontmatterParser.new
22
24
  @code_block_parser = CodeBlockParser.new(@frontmatter_parser)
23
25
  end
@@ -40,31 +42,6 @@ class MarkdownProcessor
40
42
  @frontmatter_parser.resolve_language(lang)
41
43
  end
42
44
 
43
- def ruby_style_result?(lang)
44
- lang_config = SUPPORTED_LANGUAGES[lang]
45
- lang_config && lang_config[:result_block_type] == "ruby"
46
- end
47
-
48
- def mermaid_style_result?(lang)
49
- lang_config = SUPPORTED_LANGUAGES[lang]
50
- lang_config && lang_config[:result_handling] == :mermaid_svg
51
- end
52
-
53
- def result_block_header(lang)
54
- ruby_style_result?(lang) ? "```ruby RESULT\n" : "```RESULT\n"
55
- end
56
-
57
- def result_block_regex(lang)
58
- if mermaid_style_result?(lang)
59
- # For mermaid, look for existing image tags with .svg extension
60
- /^!\[.*\]\(.*\.svg\)$/i
61
- elsif ruby_style_result?(lang)
62
- /^```ruby\s+RESULT$/i
63
- else
64
- /^```RESULT$/i
65
- end
66
- end
67
-
68
45
  def is_block_end?(line)
69
46
  @code_block_parser.is_block_end?(line)
70
47
  end
@@ -73,22 +50,6 @@ class MarkdownProcessor
73
50
  !content.strip.empty?
74
51
  end
75
52
 
76
- def add_result_block(result_output, blank_line_before_new_result)
77
- if mermaid_style_result?(@current_block_lang)
78
- # For mermaid, add the image tag directly without a result block
79
- @output_lines << "\n" if blank_line_before_new_result.nil?
80
- @output_lines << result_output
81
- @output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
82
- @output_lines << "\n"
83
- else
84
- @output_lines << "\n" if blank_line_before_new_result.nil?
85
- @output_lines << result_block_header(@current_block_lang)
86
- @output_lines << result_output
87
- @output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
88
- @output_lines << "```\n\n"
89
- end
90
- end
91
-
92
53
  def line_matches_pattern?(line, pattern)
93
54
  line && line.match?(pattern)
94
55
  end
@@ -97,22 +58,6 @@ class MarkdownProcessor
97
58
  line && line.strip == ""
98
59
  end
99
60
 
100
- def parse_rerun_option(options_string)
101
- @code_block_parser.parse_rerun_option(options_string)
102
- end
103
-
104
- def parse_run_option(options_string)
105
- @code_block_parser.parse_run_option(options_string)
106
- end
107
-
108
- def parse_explain_option(options_string)
109
- @code_block_parser.parse_explain_option(options_string)
110
- end
111
-
112
- def parse_result_option(options_string)
113
- @code_block_parser.parse_result_option(options_string)
114
- end
115
-
116
61
  def handle_line(current_line, file_enum)
117
62
  case @state
118
63
  when :outside_code_block
@@ -144,199 +89,4 @@ class MarkdownProcessor
144
89
  accumulate_code_content(current_line)
145
90
  end
146
91
  end
147
-
148
- def handle_inside_result_block(current_line, file_enum)
149
- @output_lines << current_line
150
- if is_block_end?(current_line)
151
- @state = :outside_code_block
152
- end
153
- end
154
-
155
- def handle_existing_ruby_result_block(current_line, file_enum)
156
- warn "Found existing '```ruby RESULT' block, passing through."
157
- @output_lines << current_line
158
- @state = :inside_result_block
159
- end
160
-
161
- def start_code_block(current_line, lang, options_string = nil)
162
- @output_lines << current_line
163
- @current_block_lang = resolve_language(lang)
164
- @current_block_rerun = parse_rerun_option(options_string)
165
- @current_block_run = parse_run_option(options_string)
166
- @current_block_explain = parse_explain_option(options_string)
167
- @current_block_result = parse_result_option(options_string)
168
- @state = :inside_code_block
169
- @current_code_content = ""
170
- end
171
-
172
- def accumulate_code_content(current_line)
173
- @current_code_content += current_line
174
- @output_lines << current_line
175
- end
176
-
177
- def end_code_block(current_line, file_enum)
178
- @output_lines << current_line
179
-
180
- decision = decide_execution(file_enum)
181
-
182
- if decision[:execute]
183
- # If we consumed lines for rerun, don't add them to output (they'll be replaced)
184
- execute_and_add_result(decision[:blank_line])
185
- else
186
- skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision)
187
- end
188
-
189
- reset_code_block_state
190
- end
191
-
192
- def decide_execution(file_enum)
193
- decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_result)
194
- decision = decider.decide(file_enum, method(:result_block_regex))
195
-
196
- # Handle the consume_existing flag for rerun scenarios
197
- if decision[:consume_existing]
198
- consume_existing_result_block(file_enum, decision[:consumed_lines])
199
- elsif decision[:consume_existing_dalibo]
200
- # Dalibo links are already consumed in the decision process
201
- # Just acknowledge they were consumed
202
- end
203
-
204
- decision
205
- end
206
-
207
- def execute_and_add_result(blank_line_before_new_result)
208
- @output_lines << blank_line_before_new_result if blank_line_before_new_result
209
-
210
- if has_content?(@current_code_content)
211
- result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path, @current_block_explain)
212
-
213
- # Check if result contains a Dalibo link for psql explain queries
214
- dalibo_link, clean_result = extract_dalibo_link(result_output)
215
-
216
- # Add the result block only if result=true (default)
217
- if @current_block_result
218
- add_result_block(clean_result || result_output, blank_line_before_new_result)
219
- end
220
-
221
- # Always add Dalibo link if it exists, even when result=false
222
- if dalibo_link
223
- # Add appropriate spacing based on whether result block was shown
224
- if @current_block_result
225
- @output_lines << "#{dalibo_link}\n\n"
226
- else
227
- @output_lines << "\n#{dalibo_link}\n\n"
228
- end
229
- end
230
- else
231
- warn "Skipping empty code block for language '#{@current_block_lang}'."
232
- end
233
- end
234
-
235
- def skip_and_pass_through_result(lines_to_pass_through, file_enum, decision = nil)
236
- # Handle run=false case where there are no lines to pass through
237
- if lines_to_pass_through.empty?
238
- warn "Skipping execution due to run=false option."
239
- return
240
- end
241
-
242
- # Check if this is Dalibo content
243
- if decision && decision[:dalibo_content]
244
- warn "Found existing Dalibo link for current #{@current_block_lang} block, skipping execution."
245
- @output_lines.concat(lines_to_pass_through)
246
- # No additional consumption needed for Dalibo links
247
- return
248
- end
249
-
250
- if mermaid_style_result?(@current_block_lang)
251
- warn "Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution."
252
- @output_lines.concat(lines_to_pass_through)
253
- # For mermaid, no additional consumption needed since it's just an image line
254
- else
255
- lang_specific_result_type = ruby_style_result?(@current_block_lang) ? "```ruby RESULT" : "```RESULT"
256
- warn "Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution."
257
- @output_lines.concat(lines_to_pass_through)
258
- consume_result_block_content(file_enum)
259
- end
260
- end
261
-
262
- def consume_result_block_content(file_enum)
263
- consume_block_lines(file_enum) do |line|
264
- @output_lines << line
265
- end
266
- end
267
-
268
- def consume_existing_result_block(file_enum, consumed_lines)
269
- if mermaid_style_result?(@current_block_lang)
270
- # For mermaid, there's no result block to consume, just the image line
271
- # The image line should already be in consumed_lines from ExecutionDecider
272
- return
273
- end
274
-
275
- consume_block_lines(file_enum) do |line|
276
- consumed_lines << line
277
- end
278
-
279
- # After consuming the result block, check if there's a Dalibo link to consume as well
280
- consume_dalibo_link_if_present(file_enum, consumed_lines)
281
- end
282
-
283
- def consume_block_lines(file_enum)
284
- begin
285
- loop do
286
- result_block_line = file_enum.next
287
- yield result_block_line
288
- break if is_block_end?(result_block_line)
289
- end
290
- rescue StopIteration
291
- warn "Warning: End of file reached while consuming result block."
292
- end
293
- end
294
-
295
- def reset_code_block_state
296
- @state = :outside_code_block
297
- @current_code_content = ""
298
- @current_block_rerun = false
299
- @current_block_run = true
300
- @current_block_explain = false
301
- @current_block_result = true
302
- end
303
-
304
- def stderr_has_content?(stderr_output)
305
- stderr_output && !stderr_output.strip.empty?
306
- end
307
-
308
- def extract_dalibo_link(result_output)
309
- # Check if the result contains a Dalibo link marker
310
- if result_output.start_with?("DALIBO_LINK:")
311
- lines = result_output.split("\n", 2)
312
- dalibo_url = lines[0].sub("DALIBO_LINK:", "")
313
- clean_result = lines[1] || ""
314
- dalibo_link = "**Dalibo Visualization:** [View Query Plan](#{dalibo_url})"
315
- [dalibo_link, clean_result]
316
- else
317
- [nil, result_output]
318
- end
319
- end
320
-
321
- def consume_dalibo_link_if_present(file_enum, consumed_lines)
322
- # Look ahead to see if there are Dalibo links after the result block
323
- begin
324
- # Keep consuming blank lines and Dalibo links until we hit something else
325
- loop do
326
- next_line = peek_next_line(file_enum)
327
-
328
- if is_blank_line?(next_line)
329
- consumed_lines << file_enum.next
330
- elsif next_line&.start_with?("**Dalibo Visualization:**")
331
- consumed_lines << file_enum.next
332
- else
333
- # Hit something that's not a blank line or Dalibo link, stop consuming
334
- break
335
- end
336
- end
337
- rescue StopIteration
338
- # End of file reached, nothing more to consume
339
- end
340
- end
341
-
342
92
  end