markdown-run 0.1.9 → 0.1.11

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d87607302aa1e1f4b95d1da2b1857f792d34b3d3d7985299db1a6730d7f49b57
4
- data.tar.gz: d10dd3193558ce3c59770bd5b9ba7f73e8a5f3459cea9482deeff7bfc21333a1
3
+ metadata.gz: 7e078472cf91253d16b3341865954ead4ce1d6539d509c5ed6d54302c5a3147d
4
+ data.tar.gz: 76f40572409ed01a1797c4902896b783d3a0403783031fca8c8dff04a1499acf
5
5
  SHA512:
6
- metadata.gz: 1bbf13073d892d85165b1396a78ec6f274cb75a941fd8dc50f21b4c9636572966c2983f001851286fcb14531eed0a3d2177df42f8d1851e98f62175c5b83c78e
7
- data.tar.gz: 97f9f35734c1440d8b21cc62176a65458bce898cb223368c65af4e0888f4bbd986dc6e403a382f982561dc6ad572f4f9c24c3f2f41d8c0aa6186cc6d5d25f656
6
+ metadata.gz: ea3ecacb94cbe3e52dacb9fdb4ea447790990cee631fd890f5389d483157d7e16621eda6dba68dddf13d1e1f5f499b8bc2ac3952e2ca1752f133f5aa496403b1
7
+ data.tar.gz: 8ae64565dc336f5158a877c54a02520bc6a0e6923c6bbdd705c2cff8c848ff832f0a0683f30c2e86df883a356d89e60269cebb219d9a9732212fc5edfd27fea6
data/CHANGELOG.md CHANGED
@@ -1,42 +1,53 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.11] - 2025-06-04
4
+
5
+ - options are customizable with the yaml frontmatter
6
+
7
+ ## [0.1.10] - 2025-06-03
8
+
9
+ - standalone options for codeblocks (run instead of run=true)
10
+ - explain option for psql code blocks with Dalibo visualization links
11
+ - Fixed Dalibo URL generation to properly submit plans via HTTP POST
12
+ - Added result option to control result block visibility (result=false hides result blocks while still executing code)
13
+
3
14
  ## [0.1.9] - 2025-06-02
4
15
 
5
- - mermaid codeblocks
16
+ - mermaid codeblocks
6
17
 
7
18
  ## [0.1.8] - 2025-06-01
8
19
 
9
- - Added run option
20
+ - Added run option
10
21
 
11
22
  ## [0.1.7] - 2025-06-01
12
23
 
13
- - Added rerun functionality
24
+ - Added rerun functionality
14
25
 
15
26
  ## [0.1.6] - 2025-06-01
16
27
 
17
- - Refactor code to state pattern
18
- - Add yaml frontmatter to support aliases for code blocks
28
+ - Refactor code to state pattern
29
+ - Add yaml frontmatter to support aliases for code blocks
19
30
 
20
31
  ## [0.1.5] - 2025-05-19
21
32
 
22
- - Remove gif files from release
33
+ - Remove gif files from release
23
34
 
24
35
  ## [0.1.4] - 2025-05-18
25
36
 
26
- - Add support for zsh, bash, sh
37
+ - Add support for zsh, bash, sh
27
38
 
28
39
  ## [0.1.3] - 2025-05-14
29
40
 
30
- - Fix missing minitest dep
41
+ - Fix missing minitest dep
31
42
 
32
43
  ## [0.1.2] - 2025-05-14
33
44
 
34
- - Gemfile update
45
+ - Gemfile update
35
46
 
36
47
  ## [0.1.1] - 2025-05-14
37
48
 
38
- - Added checks for missing dependencies
49
+ - Added checks for missing dependencies
39
50
 
40
51
  ## [0.1.0] - 2025-05-13
41
52
 
42
- - Initial release
53
+ - Initial release
data/README.md CHANGED
@@ -57,8 +57,21 @@ example vscode keybinding
57
57
 
58
58
  - `run=true` or `run=false` to control whether a code block should be executed at all. `run=true` is the default if not specified
59
59
  - `rerun=true` or `rerun=false` to control whether a code block should be re-executed if a result block already exists. `rerun=false` is the default if not specified
60
+ - `result=true` or `result=false` to control whether the result block should be displayed after execution. `result=true` is the default if not specified. When `result=false`, the code still executes but the result block is hidden
61
+ - `explain=true` or `explain=false` for psql code blocks to generate query execution plans with Dalibo visualization links. `explain=false` is the default if not specified
60
62
 
61
- Options can be combined. If `run=false` is specified, the code block will not execute regardless of the `rerun` setting.
63
+ Options can be combined. If `run=false` is specified, the code block will not execute regardless of the `rerun` setting. The `result` option only affects display of the result block, not code execution.
64
+
65
+ ### Standalone Option Syntax
66
+
67
+ Options can also be specified using standalone syntax without explicit `=true`:
68
+
69
+ - `run` is equivalent to `run=true`
70
+ - `rerun` is equivalent to `rerun=true`
71
+ - `result` is equivalent to `result=true`
72
+ - `explain` is equivalent to `explain=true`
73
+
74
+ Explicit assignments (e.g., `run=false`) take precedence over standalone options.
62
75
 
63
76
  Examples:
64
77
 
@@ -66,7 +79,7 @@ Examples:
66
79
  console.log("This will not execute at all");
67
80
  ```
68
81
 
69
- ```js rerun=true
82
+ ```js rerun
70
83
  console.log("This will re-execute even if result exists");
71
84
  ```
72
85
 
@@ -74,6 +87,23 @@ console.log("This will re-execute even if result exists");
74
87
  console.log("This will execute only if no result exists");
75
88
  ```
76
89
 
90
+ ```ruby result=false run
91
+ puts "This executes but the result block is hidden"
92
+ ```
93
+
94
+ ```psql explain
95
+ SELECT * FROM users WHERE id = 1;
96
+ ```
97
+
98
+ ```psql explain=true
99
+ EXPLAIN (ANALYZE) SELECT * FROM large_table;
100
+ ```
101
+
102
+ ```psql result=false explain
103
+ SELECT * FROM large_table;
104
+ -- This will execute the explain query and show the Dalibo link but hide the result block
105
+ ```
106
+
77
107
  ### Mermaid diagrams
78
108
 
79
109
  Mermaid blocks generate SVG files and insert markdown image tags:
@@ -105,6 +135,65 @@ markdown-run:
105
135
  - sql: psql
106
136
  ```
107
137
 
138
+ ### Setting Defaults
139
+
140
+ You can override the default behavior for code block options using frontmatter:
141
+
142
+ ```yaml
143
+ markdown-run:
144
+ defaults:
145
+ rerun: true
146
+ result: false
147
+ psql:
148
+ explain: true
149
+ ruby:
150
+ rerun: false
151
+ ```
152
+
153
+ **Priority order (highest to lowest):**
154
+
155
+ 1. Explicit options in code blocks (e.g., `rerun=true`)
156
+ 2. Language-specific defaults (e.g., `psql: { explain: true }`)
157
+ 3. Global defaults (e.g., `defaults: { rerun: true }`)
158
+ 4. Built-in application defaults
159
+
160
+ **Available options for defaults:**
161
+
162
+ - `run`: Control whether code blocks execute (default: `true`)
163
+ - `rerun`: Control whether to re-execute if result exists (default: `false`)
164
+ - `result`: Control whether to show result blocks (default: `true`)
165
+ - `explain`: For psql blocks, generate explain plans (default: `false`)
166
+
167
+ **Examples:**
168
+
169
+ Make all code blocks rerun by default:
170
+
171
+ ```yaml
172
+ markdown-run:
173
+ defaults:
174
+ rerun: true
175
+ ```
176
+
177
+ Hide result blocks by default but enable explain for psql:
178
+
179
+ ```yaml
180
+ markdown-run:
181
+ defaults:
182
+ result: false
183
+ psql:
184
+ explain: true
185
+ ```
186
+
187
+ Language-specific settings override global defaults:
188
+
189
+ ```yaml
190
+ markdown-run:
191
+ defaults:
192
+ rerun: false # Global default
193
+ ruby:
194
+ rerun: true # Ruby blocks will rerun, others won't
195
+ ```
196
+
108
197
  ## Demo
109
198
 
110
199
  ![VSCode Usage](docs/markdown-run-vscode.gif)
@@ -0,0 +1,62 @@
1
+ module CodeBlockHelper
2
+ private
3
+
4
+ def reset_code_block_state
5
+ @state = :outside_code_block
6
+ @current_code_content = ""
7
+ @current_block_lang = ""
8
+
9
+
10
+ @current_block_rerun = false
11
+ @current_block_run = true
12
+ @current_block_explain = false
13
+ @current_block_result = true
14
+ end
15
+
16
+
17
+ def start_code_block(current_line, lang, options_string = nil)
18
+ @output_lines << current_line
19
+ @current_block_lang = resolve_language(lang)
20
+ @current_block_rerun = @code_block_parser.parse_rerun_option(options_string, @current_block_lang)
21
+ @current_block_run = @code_block_parser.parse_run_option(options_string, @current_block_lang)
22
+ @current_block_explain = @code_block_parser.parse_explain_option(options_string, @current_block_lang)
23
+ @current_block_result = @code_block_parser.parse_result_option(options_string, @current_block_lang)
24
+ @state = :inside_code_block
25
+ @current_code_content = ""
26
+ end
27
+
28
+ def accumulate_code_content(current_line)
29
+ @current_code_content += current_line
30
+ @output_lines << current_line
31
+ end
32
+
33
+ def end_code_block(current_line, file_enum)
34
+ @output_lines << current_line
35
+
36
+ decision = decide_execution(file_enum)
37
+
38
+ if decision[:execute]
39
+ # If we consumed lines for rerun, don't add them to output (they'll be replaced)
40
+ execute_and_add_result(decision[:blank_line])
41
+ else
42
+ skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision)
43
+ end
44
+
45
+ reset_code_block_state
46
+ end
47
+
48
+ def decide_execution(file_enum)
49
+ decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_result)
50
+ decision = decider.decide(file_enum, method(:result_block_regex))
51
+
52
+ # Handle the consume_existing flag for rerun scenarios
53
+ if decision[:consume_existing]
54
+ consume_existing_result_block(file_enum, decision[:consumed_lines])
55
+ elsif decision[:consume_existing_dalibo]
56
+ # Dalibo links are already consumed in the decision process
57
+ # Just acknowledge they were consumed
58
+ end
59
+
60
+ decision
61
+ end
62
+ end
@@ -34,12 +34,24 @@ class CodeBlockParser
34
34
  line.strip == BLOCK_END_PATTERN
35
35
  end
36
36
 
37
- def parse_run_option(options_string)
38
- parse_boolean_option(options_string, "run", true)
37
+ def parse_run_option(options_string, language = nil)
38
+ default_value = @frontmatter_parser.get_default_value("run", language, true)
39
+ parse_boolean_option(options_string, "run", default_value)
39
40
  end
40
41
 
41
- def parse_rerun_option(options_string)
42
- parse_boolean_option(options_string, "rerun", false)
42
+ def parse_rerun_option(options_string, language = nil)
43
+ default_value = @frontmatter_parser.get_default_value("rerun", language, false)
44
+ parse_boolean_option(options_string, "rerun", default_value)
45
+ end
46
+
47
+ def parse_explain_option(options_string, language = nil)
48
+ default_value = @frontmatter_parser.get_default_value("explain", language, false)
49
+ parse_boolean_option(options_string, "explain", default_value)
50
+ end
51
+
52
+ def parse_result_option(options_string, language = nil)
53
+ default_value = @frontmatter_parser.get_default_value("result", language, true)
54
+ parse_boolean_option(options_string, "result", default_value)
43
55
  end
44
56
 
45
57
  private
@@ -51,10 +63,19 @@ class CodeBlockParser
51
63
  def parse_boolean_option(options_string, option_name, default_value)
52
64
  return default_value unless options_string
53
65
 
54
- # Match option=true or option=false
55
- match = options_string.match(/#{option_name}\s*=\s*(true|false)/i)
56
- return default_value unless match
66
+ # First, check for explicit option=true/false assignments (highest priority)
67
+ explicit_match = options_string.match(/#{option_name}\s*=\s*(true|false)/i)
68
+ if explicit_match
69
+ return explicit_match[1].downcase == "true"
70
+ end
71
+
72
+ # If no explicit assignment, check for standalone option (e.g., "rerun")
73
+ standalone_match = options_string.match(/\b#{option_name}\b(?!\s*=)/i)
74
+ if standalone_match
75
+ return true
76
+ end
57
77
 
58
- match[1].downcase == "true"
78
+ # If neither found, return default value
79
+ default_value
59
80
  end
60
81
  end
data/lib/code_executor.rb CHANGED
@@ -3,11 +3,11 @@ require "open3"
3
3
  require_relative "language_configs"
4
4
 
5
5
  class CodeExecutor
6
- def self.execute(code_content, lang, temp_dir, input_file_path = nil)
7
- new.execute(code_content, lang, temp_dir, input_file_path)
6
+ def self.execute(code_content, lang, temp_dir, input_file_path = nil, explain = false)
7
+ new.execute(code_content, lang, temp_dir, input_file_path, explain)
8
8
  end
9
9
 
10
- def execute(code_content, lang, temp_dir, input_file_path = nil)
10
+ def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false)
11
11
  lang_key = lang.downcase
12
12
  lang_config = SUPPORTED_LANGUAGES[lang_key]
13
13
 
@@ -15,34 +15,40 @@ class CodeExecutor
15
15
 
16
16
  warn "Executing #{lang_key} code block..."
17
17
 
18
- result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path)
19
- process_execution_result(result, lang_config, lang_key)
18
+ result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path, explain)
19
+ process_execution_result(result, lang_config, lang_key, explain)
20
20
  end
21
21
 
22
22
  private
23
23
 
24
+
25
+
26
+ def stderr_has_content?(stderr_output)
27
+ stderr_output && !stderr_output.strip.empty?
28
+ end
29
+
24
30
  def handle_unsupported_language(lang)
25
31
  warn "Unsupported language: #{lang}"
26
32
  "ERROR: Unsupported language: #{lang}"
27
33
  end
28
34
 
29
- def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil)
35
+ def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false)
30
36
  cmd_lambda = lang_config[:command]
31
37
  temp_file_suffix = lang_config[:temp_file_suffix]
32
38
 
33
39
  if temp_file_suffix
34
- execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path)
40
+ execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain)
35
41
  else
36
- execute_direct_command(code_content, cmd_lambda)
42
+ execute_direct_command(code_content, cmd_lambda, explain)
37
43
  end
38
44
  end
39
45
 
40
- def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil)
46
+ def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false)
41
47
  result = nil
42
48
  Tempfile.create([lang_key, temp_file_suffix], temp_dir) do |temp_file|
43
49
  temp_file.write(code_content)
44
50
  temp_file.close
45
- command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path, input_file_path)
51
+ command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path, input_file_path, explain)
46
52
 
47
53
  # Extract output_path if present (for mermaid)
48
54
  output_path = exec_options.delete(:output_path) if exec_options.is_a?(Hash)
@@ -58,19 +64,21 @@ class CodeExecutor
58
64
  result
59
65
  end
60
66
 
61
- def execute_direct_command(code_content, cmd_lambda)
62
- command_to_run, exec_options = cmd_lambda.call(code_content, nil)
67
+ def execute_direct_command(code_content, cmd_lambda, explain = false)
68
+ command_to_run, exec_options = cmd_lambda.call(code_content, nil, nil, explain)
63
69
  captured_stdout, captured_stderr, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
64
70
  { stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj }
65
71
  end
66
72
 
67
- def process_execution_result(result, lang_config, lang_key)
73
+ def process_execution_result(result, lang_config, lang_key, explain = false)
68
74
  exit_status, result_output, stderr_output = format_captured_output(result, lang_config)
69
75
 
70
76
  if exit_status != 0
71
77
  result_output = add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
72
78
  elsif lang_config && lang_config[:result_handling] == :mermaid_svg
73
79
  result_output = handle_mermaid_svg_result(result, lang_key)
80
+ elsif explain && lang_key == "psql"
81
+ result_output = handle_psql_explain_result(result_output)
74
82
  end
75
83
 
76
84
  result_output
@@ -118,7 +126,7 @@ class CodeExecutor
118
126
  # If the SVG is in a subdirectory, include the directory in the path
119
127
  output_dir = File.dirname(output_path)
120
128
  svg_filename = File.basename(output_path)
121
-
129
+
122
130
  # Check if SVG is in a subdirectory (new behavior) or same directory (fallback)
123
131
  parent_dir = File.dirname(output_dir)
124
132
  if File.basename(output_dir) != File.basename(parent_dir)
@@ -128,10 +136,77 @@ class CodeExecutor
128
136
  # SVG is in same directory (fallback behavior)
129
137
  relative_path = svg_filename
130
138
  end
131
-
139
+
132
140
  warn "Generated Mermaid SVG: #{relative_path}"
133
141
 
134
142
  # Return markdown image tag instead of typical result content
135
143
  "![Mermaid Diagram](#{relative_path})"
136
144
  end
145
+
146
+ def handle_psql_explain_result(result_output)
147
+ require 'json'
148
+ require 'net/http'
149
+ require 'uri'
150
+
151
+ # Try to parse the result as JSON (EXPLAIN output)
152
+ begin
153
+ # Clean up the result output and try to parse as JSON
154
+ json_data = JSON.parse(result_output.strip)
155
+
156
+ # Submit plan to Dalibo via POST request
157
+ dalibo_url = submit_plan_to_dalibo(JSON.generate(json_data))
158
+
159
+ if dalibo_url
160
+ # Return a special format that the markdown processor can parse
161
+ "DALIBO_LINK:#{dalibo_url}\n#{result_output.strip}"
162
+ else
163
+ # If submission failed, just return the original output
164
+ result_output
165
+ end
166
+ rescue JSON::ParserError
167
+ # If it's not valid JSON, just return the original output
168
+ result_output
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ def submit_plan_to_dalibo(plan_json)
175
+ begin
176
+ uri = URI('https://explain.dalibo.com/new')
177
+ http = Net::HTTP.new(uri.host, uri.port)
178
+ http.use_ssl = true
179
+ http.read_timeout = 10
180
+
181
+ payload = {
182
+ 'plan' => plan_json,
183
+ 'title' => "Query Plan - #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}",
184
+ 'query' => ''
185
+ }
186
+
187
+ request = Net::HTTP::Post.new(uri)
188
+ request['Content-Type'] = 'application/json'
189
+ request.body = JSON.generate(payload)
190
+
191
+ response = http.request(request)
192
+
193
+ if response.is_a?(Net::HTTPRedirection)
194
+ location = response['location']
195
+ if location
196
+ if location.start_with?('/')
197
+ location = "https://explain.dalibo.com#{location}"
198
+ end
199
+ location
200
+ else
201
+ nil
202
+ end
203
+ else
204
+ warn "Failed to submit plan to Dalibo: #{response.code} #{response.message}"
205
+ nil
206
+ end
207
+ rescue => e
208
+ warn "Error submitting plan to Dalibo: #{e.message}"
209
+ nil
210
+ end
211
+ end
137
212
  end
@@ -0,0 +1,39 @@
1
+ module DaliboHelper
2
+ private
3
+
4
+ DALIBO_LINK_PREFIX = "DALIBO_LINK:"
5
+
6
+ def extract_dalibo_link(result_output)
7
+ # Check if the result contains a Dalibo link marker
8
+ if result_output.start_with?(DALIBO_LINK_PREFIX)
9
+ lines = result_output.split("\n", 2)
10
+ dalibo_url = lines[0].sub(DALIBO_LINK_PREFIX, "")
11
+ clean_result = lines[1] || ""
12
+ dalibo_link = "**Dalibo Visualization:** [View Query Plan](#{dalibo_url})"
13
+ [dalibo_link, clean_result]
14
+ else
15
+ [nil, result_output]
16
+ end
17
+ end
18
+
19
+ def consume_dalibo_link_if_present(file_enum, consumed_lines)
20
+ # Look ahead to see if there are Dalibo links after the result block
21
+ begin
22
+ # Keep consuming blank lines and Dalibo 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?("**Dalibo Visualization:**")
29
+ consumed_lines << file_enum.next
30
+ else
31
+ # Hit something that's not a blank line or Dalibo 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
@@ -3,10 +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)
6
+ def initialize(current_block_run, current_block_rerun, current_block_lang, current_block_explain = 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
+ @current_block_explain = current_block_explain
11
+ @current_block_result = current_block_result
10
12
  end
11
13
 
12
14
  def decide(file_enum, result_block_regex_method)
@@ -19,6 +21,8 @@ class ExecutionDecider
19
21
  handle_immediate_result_block(file_enum)
20
22
  elsif is_blank_line?(peek1)
21
23
  handle_blank_line_scenario(file_enum, expected_header_regex)
24
+ elsif @current_block_explain && is_dalibo_link?(peek1)
25
+ handle_immediate_dalibo_link(file_enum)
22
26
  else
23
27
  execute_without_existing_result
24
28
  end
@@ -44,12 +48,23 @@ class ExecutionDecider
44
48
 
45
49
  def handle_blank_line_scenario(file_enum, expected_header_regex)
46
50
  consumed_blank_line = file_enum.next
51
+
52
+ # Look ahead past multiple blank lines to find actual content
47
53
  peek2 = peek_next_line(file_enum)
54
+ additional_blanks = []
55
+
56
+ # Consume consecutive blank lines
57
+ while is_blank_line?(peek2)
58
+ additional_blanks << file_enum.next
59
+ peek2 = peek_next_line(file_enum)
60
+ end
48
61
 
49
62
  if line_matches_pattern?(peek2, expected_header_regex)
50
- handle_result_after_blank_line(file_enum, consumed_blank_line)
63
+ handle_result_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
64
+ elsif @current_block_explain && is_dalibo_link?(peek2)
65
+ handle_dalibo_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
51
66
  else
52
- execute_with_blank_line(consumed_blank_line)
67
+ execute_with_blank_lines(consumed_blank_line, additional_blanks)
53
68
  end
54
69
  end
55
70
 
@@ -61,6 +76,24 @@ class ExecutionDecider
61
76
  end
62
77
  end
63
78
 
79
+ def handle_result_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
80
+ if @current_block_rerun
81
+ execute_with_consumed_result_and_blanks(file_enum, consumed_blank_line, additional_blanks)
82
+ else
83
+ skip_with_blanks_and_result(file_enum, consumed_blank_line, additional_blanks)
84
+ end
85
+ end
86
+
87
+ def handle_dalibo_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
88
+ # For explain result=false, always replace existing Dalibo links
89
+ # For explain result=true, follow normal rerun logic
90
+ if should_auto_replace_dalibo_link? || @current_block_rerun
91
+ execute_with_consumed_dalibo_and_blanks(file_enum, consumed_blank_line, additional_blanks)
92
+ else
93
+ skip_with_blanks_and_dalibo(file_enum, consumed_blank_line, additional_blanks)
94
+ end
95
+ end
96
+
64
97
  def execute_with_consumed_result(file_enum)
65
98
  consumed_lines = [file_enum.next]
66
99
  { execute: true, consumed_lines: consumed_lines, consume_existing: true }
@@ -83,10 +116,98 @@ class ExecutionDecider
83
116
  { execute: true, blank_line: consumed_blank_line }
84
117
  end
85
118
 
119
+ def execute_with_blank_lines(consumed_blank_line, additional_blanks)
120
+ { execute: true, blank_line: consumed_blank_line, additional_blanks: additional_blanks }
121
+ end
122
+
123
+ def execute_with_consumed_result_and_blanks(file_enum, consumed_blank_line, additional_blanks)
124
+ consumed_lines = [consumed_blank_line] + additional_blanks + [file_enum.next]
125
+ { execute: true, consumed_lines: consumed_lines, blank_line: consumed_blank_line, consume_existing: true }
126
+ end
127
+
128
+ def skip_with_blanks_and_result(file_enum, consumed_blank_line, additional_blanks)
129
+ lines_to_pass = [consumed_blank_line] + additional_blanks + [file_enum.next]
130
+ { execute: false, lines_to_pass_through: lines_to_pass }
131
+ end
132
+
133
+ def execute_with_consumed_dalibo_and_blanks(file_enum, consumed_blank_line, additional_blanks)
134
+ consumed_lines = [consumed_blank_line] + additional_blanks
135
+ consume_dalibo_links(file_enum, consumed_lines)
136
+ { execute: true, consumed_lines: consumed_lines, blank_line: consumed_blank_line, consume_existing_dalibo: true }
137
+ end
138
+
139
+ def skip_with_blanks_and_dalibo(file_enum, consumed_blank_line, additional_blanks)
140
+ consumed_lines = [consumed_blank_line] + additional_blanks
141
+ consume_dalibo_links(file_enum, consumed_lines)
142
+ { execute: false, lines_to_pass_through: consumed_lines, dalibo_content: true }
143
+ end
144
+
86
145
  def execute_without_existing_result
87
146
  { execute: true }
88
147
  end
89
148
 
149
+ def handle_immediate_dalibo_link(file_enum)
150
+ # For explain result=false, always replace existing Dalibo links
151
+ # For explain result=true, follow normal rerun logic
152
+ if should_auto_replace_dalibo_link? || @current_block_rerun
153
+ execute_with_consumed_dalibo(file_enum)
154
+ else
155
+ skip_with_existing_dalibo(file_enum)
156
+ end
157
+ end
158
+
159
+ def handle_dalibo_after_blank_line(file_enum, consumed_blank_line)
160
+ # For explain result=false, always replace existing Dalibo links
161
+ # For explain result=true, follow normal rerun logic
162
+ if should_auto_replace_dalibo_link? || @current_block_rerun
163
+ execute_with_consumed_dalibo_and_blank(file_enum, consumed_blank_line)
164
+ else
165
+ skip_with_blank_and_dalibo(file_enum, consumed_blank_line)
166
+ end
167
+ end
168
+
169
+ def execute_with_consumed_dalibo(file_enum)
170
+ consumed_lines = []
171
+ consume_dalibo_links(file_enum, consumed_lines)
172
+ { execute: true, consumed_lines: consumed_lines, consume_existing_dalibo: true }
173
+ end
174
+
175
+ def skip_with_existing_dalibo(file_enum)
176
+ consumed_lines = []
177
+ consume_dalibo_links(file_enum, consumed_lines)
178
+ { execute: false, lines_to_pass_through: consumed_lines, dalibo_content: true }
179
+ end
180
+
181
+ def execute_with_consumed_dalibo_and_blank(file_enum, consumed_blank_line)
182
+ consumed_lines = [consumed_blank_line]
183
+ consume_dalibo_links(file_enum, consumed_lines)
184
+ { execute: true, consumed_lines: consumed_lines, blank_line: consumed_blank_line, consume_existing_dalibo: true }
185
+ end
186
+
187
+ def skip_with_blank_and_dalibo(file_enum, consumed_blank_line)
188
+ consumed_lines = [consumed_blank_line]
189
+ consume_dalibo_links(file_enum, consumed_lines)
190
+ { execute: false, lines_to_pass_through: consumed_lines, dalibo_content: true }
191
+ end
192
+
193
+ def consume_dalibo_links(file_enum, consumed_lines)
194
+ # Consume all consecutive Dalibo links and blank lines
195
+ loop do
196
+ next_line = peek_next_line(file_enum)
197
+
198
+ if is_blank_line?(next_line) || is_dalibo_link?(next_line)
199
+ consumed_line = file_enum.next
200
+ consumed_lines << consumed_line
201
+ else
202
+ break
203
+ end
204
+ end
205
+ end
206
+
207
+ def is_dalibo_link?(line)
208
+ line&.start_with?("**Dalibo Visualization:**")
209
+ end
210
+
90
211
  def line_matches_pattern?(line, pattern)
91
212
  line && line.match?(pattern)
92
213
  end
@@ -94,4 +215,11 @@ class ExecutionDecider
94
215
  def is_blank_line?(line)
95
216
  line && line.strip == ""
96
217
  end
218
+
219
+ def should_auto_replace_dalibo_link?
220
+ # Auto-replace Dalibo links when using explain with result=false
221
+ # This makes sense because with result=false, there's only a Dalibo link,
222
+ # so it should be updated on each run
223
+ @current_block_explain && !@current_block_result
224
+ end
97
225
  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) {
4
+ command: ->(_code_content, temp_file_path, input_file_path = nil, explain = 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,22 +16,32 @@ JS_CONFIG = {
16
16
  }.freeze
17
17
 
18
18
  SQLITE_CONFIG = {
19
- command: ->(code_content, temp_file_path, input_file_path = nil) { [ "sqlite3 #{temp_file_path}", { stdin_data: code_content } ] },
19
+ command: ->(code_content, temp_file_path, input_file_path = nil, explain = 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) {
25
+ command: ->(code_content, _temp_file_path, input_file_path = nil, explain = 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
- [ "psql -A -t -X", { stdin_data: code_content } ]
30
+
31
+ # Modify the SQL query if explain option is enabled
32
+ if explain
33
+ # Wrap the query with EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
34
+ # Remove any trailing semicolons and whitespace, then add our EXPLAIN wrapper
35
+ clean_query = code_content.strip.gsub(/;\s*$/, '')
36
+ explained_query = "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) #{clean_query};"
37
+ [ "psql -A -t -X", { stdin_data: explained_query } ]
38
+ else
39
+ [ "psql -A -t -X", { stdin_data: code_content } ]
40
+ end
31
41
  }
32
42
  },
33
43
  "ruby" => {
34
- command: ->(_code_content, temp_file_path, input_file_path = nil) {
44
+ command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
35
45
  xmpfilter_exists = system("command -v xmpfilter > /dev/null 2>&1")
36
46
  unless xmpfilter_exists
37
47
  abort "Error: xmpfilter command not found. Please install xmpfilter or ensure it is in your PATH."
@@ -47,7 +57,7 @@ SUPPORTED_LANGUAGES = {
47
57
  "sqlite" => SQLITE_CONFIG,
48
58
  "sqlite3" => SQLITE_CONFIG, # Alias for sqlite
49
59
  "bash" => {
50
- command: ->(_code_content, temp_file_path, input_file_path = nil) {
60
+ command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
51
61
  bash_exists = system("command -v bash > /dev/null 2>&1")
52
62
  unless bash_exists
53
63
  abort "Error: bash command not found. Please ensure bash is in your PATH."
@@ -57,7 +67,7 @@ SUPPORTED_LANGUAGES = {
57
67
  temp_file_suffix: ".sh"
58
68
  },
59
69
  "zsh" => {
60
- command: ->(_code_content, temp_file_path, input_file_path = nil) {
70
+ command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
61
71
  zsh_exists = system("command -v zsh > /dev/null 2>&1")
62
72
  unless zsh_exists
63
73
  abort "Error: zsh command not found. Please ensure zsh is in your PATH."
@@ -67,7 +77,7 @@ SUPPORTED_LANGUAGES = {
67
77
  temp_file_suffix: ".zsh"
68
78
  },
69
79
  "sh" => {
70
- command: ->(_code_content, temp_file_path, input_file_path = nil) {
80
+ command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
71
81
  sh_exists = system("command -v sh > /dev/null 2>&1")
72
82
  unless sh_exists
73
83
  abort "Error: sh command not found. Please ensure sh is in your PATH."
@@ -77,7 +87,7 @@ SUPPORTED_LANGUAGES = {
77
87
  temp_file_suffix: ".sh"
78
88
  },
79
89
  "mermaid" => {
80
- command: ->(code_content, temp_file_path, input_file_path = nil) {
90
+ command: ->(code_content, temp_file_path, input_file_path = nil, explain = false) {
81
91
  mmdc_exists = system("command -v mmdc > /dev/null 2>&1")
82
92
  unless mmdc_exists
83
93
  abort "Error: mmdc command not found. Please install @mermaid-js/mermaid-cli: npm install -g @mermaid-js/mermaid-cli"
@@ -87,11 +97,11 @@ SUPPORTED_LANGUAGES = {
87
97
  if input_file_path
88
98
  # Extract markdown file basename without extension
89
99
  md_basename = File.basename(input_file_path, ".*")
90
-
100
+
91
101
  # Create directory named after the markdown file
92
102
  output_dir = File.join(File.dirname(input_file_path), md_basename)
93
103
  Dir.mkdir(output_dir) unless Dir.exist?(output_dir)
94
-
104
+
95
105
  # Generate unique filename with markdown basename prefix
96
106
  timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
97
107
  random_suffix = SecureRandom.hex(6)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Markdown
4
4
  module Run
5
- VERSION = "0.1.9"
5
+ VERSION = "0.1.11"
6
6
  end
7
7
  end
@@ -4,18 +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
21
+ reset_code_block_state
22
+
19
23
  @frontmatter_parser = FrontmatterParser.new
20
24
  @code_block_parser = CodeBlockParser.new(@frontmatter_parser)
21
25
  end
@@ -38,31 +42,6 @@ class MarkdownProcessor
38
42
  @frontmatter_parser.resolve_language(lang)
39
43
  end
40
44
 
41
- def ruby_style_result?(lang)
42
- lang_config = SUPPORTED_LANGUAGES[lang]
43
- lang_config && lang_config[:result_block_type] == "ruby"
44
- end
45
-
46
- def mermaid_style_result?(lang)
47
- lang_config = SUPPORTED_LANGUAGES[lang]
48
- lang_config && lang_config[:result_handling] == :mermaid_svg
49
- end
50
-
51
- def result_block_header(lang)
52
- ruby_style_result?(lang) ? "```ruby RESULT\n" : "```RESULT\n"
53
- end
54
-
55
- def result_block_regex(lang)
56
- if mermaid_style_result?(lang)
57
- # For mermaid, look for existing image tags with .svg extension
58
- /^!\[.*\]\(.*\.svg\)$/i
59
- elsif ruby_style_result?(lang)
60
- /^```ruby\s+RESULT$/i
61
- else
62
- /^```RESULT$/i
63
- end
64
- end
65
-
66
45
  def is_block_end?(line)
67
46
  @code_block_parser.is_block_end?(line)
68
47
  end
@@ -71,22 +50,6 @@ class MarkdownProcessor
71
50
  !content.strip.empty?
72
51
  end
73
52
 
74
- def add_result_block(result_output, blank_line_before_new_result)
75
- if mermaid_style_result?(@current_block_lang)
76
- # For mermaid, add the image tag directly without a result block
77
- @output_lines << "\n" if blank_line_before_new_result.nil?
78
- @output_lines << result_output
79
- @output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
80
- @output_lines << "\n"
81
- else
82
- @output_lines << "\n" if blank_line_before_new_result.nil?
83
- @output_lines << result_block_header(@current_block_lang)
84
- @output_lines << result_output
85
- @output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
86
- @output_lines << "```\n\n"
87
- end
88
- end
89
-
90
53
  def line_matches_pattern?(line, pattern)
91
54
  line && line.match?(pattern)
92
55
  end
@@ -95,14 +58,6 @@ class MarkdownProcessor
95
58
  line && line.strip == ""
96
59
  end
97
60
 
98
- def parse_rerun_option(options_string)
99
- @code_block_parser.parse_rerun_option(options_string)
100
- end
101
-
102
- def parse_run_option(options_string)
103
- @code_block_parser.parse_run_option(options_string)
104
- end
105
-
106
61
  def handle_line(current_line, file_enum)
107
62
  case @state
108
63
  when :outside_code_block
@@ -134,129 +89,4 @@ class MarkdownProcessor
134
89
  accumulate_code_content(current_line)
135
90
  end
136
91
  end
137
-
138
- def handle_inside_result_block(current_line, file_enum)
139
- @output_lines << current_line
140
- if is_block_end?(current_line)
141
- @state = :outside_code_block
142
- end
143
- end
144
-
145
- def handle_existing_ruby_result_block(current_line, file_enum)
146
- warn "Found existing '```ruby RESULT' block, passing through."
147
- @output_lines << current_line
148
- @state = :inside_result_block
149
- end
150
-
151
- def start_code_block(current_line, lang, options_string = nil)
152
- @output_lines << current_line
153
- @current_block_lang = resolve_language(lang)
154
- @current_block_rerun = parse_rerun_option(options_string)
155
- @current_block_run = parse_run_option(options_string)
156
- @state = :inside_code_block
157
- @current_code_content = ""
158
- end
159
-
160
- def accumulate_code_content(current_line)
161
- @current_code_content += current_line
162
- @output_lines << current_line
163
- end
164
-
165
- def end_code_block(current_line, file_enum)
166
- @output_lines << current_line
167
-
168
- decision = decide_execution(file_enum)
169
-
170
- if decision[:execute]
171
- # If we consumed lines for rerun, don't add them to output (they'll be replaced)
172
- execute_and_add_result(decision[:blank_line])
173
- else
174
- skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum)
175
- end
176
-
177
- reset_code_block_state
178
- end
179
-
180
- def decide_execution(file_enum)
181
- decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang)
182
- decision = decider.decide(file_enum, method(:result_block_regex))
183
-
184
- # Handle the consume_existing flag for rerun scenarios
185
- if decision[:consume_existing]
186
- consume_existing_result_block(file_enum, decision[:consumed_lines])
187
- end
188
-
189
- decision
190
- end
191
-
192
- def execute_and_add_result(blank_line_before_new_result)
193
- @output_lines << blank_line_before_new_result if blank_line_before_new_result
194
-
195
- if has_content?(@current_code_content)
196
- result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path)
197
- add_result_block(result_output, blank_line_before_new_result)
198
- else
199
- warn "Skipping empty code block for language '#{@current_block_lang}'."
200
- end
201
- end
202
-
203
- def skip_and_pass_through_result(lines_to_pass_through, file_enum)
204
- # Handle run=false case where there are no lines to pass through
205
- if lines_to_pass_through.empty?
206
- warn "Skipping execution due to run=false option."
207
- return
208
- end
209
-
210
- if mermaid_style_result?(@current_block_lang)
211
- warn "Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution."
212
- @output_lines.concat(lines_to_pass_through)
213
- # For mermaid, no additional consumption needed since it's just an image line
214
- else
215
- lang_specific_result_type = ruby_style_result?(@current_block_lang) ? "```ruby RESULT" : "```RESULT"
216
- warn "Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution."
217
- @output_lines.concat(lines_to_pass_through)
218
- consume_result_block_content(file_enum)
219
- end
220
- end
221
-
222
- def consume_result_block_content(file_enum)
223
- consume_block_lines(file_enum) do |line|
224
- @output_lines << line
225
- end
226
- end
227
-
228
- def consume_existing_result_block(file_enum, consumed_lines)
229
- if mermaid_style_result?(@current_block_lang)
230
- # For mermaid, there's no result block to consume, just the image line
231
- # The image line should already be in consumed_lines from ExecutionDecider
232
- return
233
- end
234
-
235
- consume_block_lines(file_enum) do |line|
236
- consumed_lines << line
237
- end
238
- end
239
-
240
- def consume_block_lines(file_enum)
241
- begin
242
- loop do
243
- result_block_line = file_enum.next
244
- yield result_block_line
245
- break if is_block_end?(result_block_line)
246
- end
247
- rescue StopIteration
248
- warn "Warning: End of file reached while consuming result block."
249
- end
250
- end
251
-
252
- def reset_code_block_state
253
- @state = :outside_code_block
254
- @current_code_content = ""
255
- @current_block_rerun = false
256
- @current_block_run = true
257
- end
258
-
259
- def stderr_has_content?(stderr_output)
260
- stderr_output && !stderr_output.strip.empty?
261
- end
262
92
  end
@@ -0,0 +1,141 @@
1
+ module ResultHelper
2
+ private
3
+
4
+ def ruby_style_result?(lang)
5
+ lang_config = SUPPORTED_LANGUAGES[lang]
6
+ lang_config && lang_config[:result_block_type] == "ruby"
7
+ end
8
+
9
+ def mermaid_style_result?(lang)
10
+ lang_config = SUPPORTED_LANGUAGES[lang]
11
+ lang_config && lang_config[:result_handling] == :mermaid_svg
12
+ end
13
+
14
+ def result_block_header(lang)
15
+ ruby_style_result?(lang) ? "```ruby RESULT\n" : "```RESULT\n"
16
+ end
17
+
18
+ def result_block_regex(lang)
19
+ if mermaid_style_result?(lang)
20
+ # For mermaid, look for existing image tags with .svg extension
21
+ /^!\[.*\]\(.*\.svg\)$/i
22
+ elsif ruby_style_result?(lang)
23
+ /^```ruby\s+RESULT$/i
24
+ else
25
+ /^```RESULT$/i
26
+ end
27
+ end
28
+
29
+ def add_result_block(result_output, blank_line_before_new_result)
30
+ if mermaid_style_result?(@current_block_lang)
31
+ # For mermaid, add the image tag directly without a result block
32
+ @output_lines << "\n" if blank_line_before_new_result.nil?
33
+ @output_lines << result_output
34
+ @output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
35
+ @output_lines << "\n"
36
+ else
37
+ @output_lines << "\n" if blank_line_before_new_result.nil?
38
+ @output_lines << result_block_header(@current_block_lang)
39
+ @output_lines << result_output
40
+ @output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
41
+ @output_lines << "```\n\n"
42
+ end
43
+ end
44
+
45
+
46
+ def handle_inside_result_block(current_line, file_enum)
47
+ @output_lines << current_line
48
+ if is_block_end?(current_line)
49
+ @state = :outside_code_block
50
+ end
51
+ end
52
+
53
+ def handle_existing_ruby_result_block(current_line, file_enum)
54
+ warn "Found existing '```ruby RESULT' block, passing through."
55
+ @output_lines << current_line
56
+ @state = :inside_result_block
57
+ end
58
+
59
+
60
+ def execute_and_add_result(blank_line_before_new_result)
61
+ warn "Skipping empty code block for language '#{@current_block_lang}'." && return unless has_content?(@current_code_content)
62
+
63
+ @output_lines << blank_line_before_new_result if blank_line_before_new_result
64
+
65
+ result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path, @current_block_explain)
66
+
67
+ # Check if result contains a Dalibo link for psql explain queries
68
+ dalibo_link, clean_result = extract_dalibo_link(result_output)
69
+
70
+ # Add the result block only if result=true (default)
71
+ if @current_block_result
72
+ add_result_block(clean_result || result_output, blank_line_before_new_result)
73
+ end
74
+
75
+ # Always add Dalibo link if it exists, even when result=false
76
+ if dalibo_link
77
+ # Add appropriate spacing based on whether result block was shown
78
+ if @current_block_result
79
+ @output_lines << "#{dalibo_link}\n\n"
80
+ else
81
+ @output_lines << "\n#{dalibo_link}\n\n"
82
+ end
83
+ end
84
+ end
85
+
86
+ def skip_and_pass_through_result(lines_to_pass_through, file_enum, decision = nil)
87
+ # Handle run=false case where there are no lines to pass through
88
+ if lines_to_pass_through.empty?
89
+ warn "Skipping execution due to run=false option."
90
+ return
91
+ end
92
+
93
+ # Check if this is Dalibo content
94
+ if decision && decision[:dalibo_content]
95
+ warn "Found existing Dalibo link for current #{@current_block_lang} block, skipping execution."
96
+ @output_lines.concat(lines_to_pass_through)
97
+ # No additional consumption needed for Dalibo links
98
+ return
99
+ end
100
+
101
+ if mermaid_style_result?(@current_block_lang)
102
+ warn "Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution."
103
+ @output_lines.concat(lines_to_pass_through)
104
+ # For mermaid, no additional consumption needed since it's just an image line
105
+ else
106
+ lang_specific_result_type = ruby_style_result?(@current_block_lang) ? "```ruby RESULT" : "```RESULT"
107
+ warn "Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution."
108
+ @output_lines.concat(lines_to_pass_through)
109
+ consume_result_block_content(file_enum)
110
+ end
111
+ end
112
+
113
+ def consume_result_block_content(file_enum)
114
+ consume_block_lines(file_enum) do |line|
115
+ @output_lines << line
116
+ end
117
+ end
118
+
119
+ def consume_existing_result_block(file_enum, consumed_lines)
120
+ return if mermaid_style_result?(@current_block_lang)
121
+
122
+ consume_block_lines(file_enum) do |line|
123
+ consumed_lines << line
124
+ end
125
+
126
+ consume_dalibo_link_if_present(file_enum, consumed_lines)
127
+ end
128
+
129
+
130
+ def consume_block_lines(file_enum)
131
+ begin
132
+ loop do
133
+ result_block_line = file_enum.next
134
+ yield result_block_line
135
+ break if is_block_end?(result_block_line)
136
+ end
137
+ rescue StopIteration
138
+ warn "Warning: End of file reached while consuming result block."
139
+ end
140
+ end
141
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdown-run
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aurélien Bottazini
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-06-02 00:00:00.000000000 Z
11
+ date: 2025-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rcodetools
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.8.5
27
+ - !ruby/object:Gem::Dependency
28
+ name: ostruct
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.6.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.6.1
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: minitest
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -84,8 +98,10 @@ files:
84
98
  - README.md
85
99
  - Rakefile
86
100
  - exe/markdown-run
101
+ - lib/code_block_helper.rb
87
102
  - lib/code_block_parser.rb
88
103
  - lib/code_executor.rb
104
+ - lib/dalibo_helper.rb
89
105
  - lib/enum_helper.rb
90
106
  - lib/execution_decider.rb
91
107
  - lib/frontmatter_parser.rb
@@ -94,6 +110,7 @@ files:
94
110
  - lib/markdown_file_writer.rb
95
111
  - lib/markdown_processor.rb
96
112
  - lib/markdown_run.rb
113
+ - lib/result_helper.rb
97
114
  - markdown-run-sample.md
98
115
  homepage: https://github.com/aurelienbottazini/markdown-run
99
116
  licenses: