markdown-run 0.1.10 → 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: 1139e0ccb6ed1b5b4ee2cef44cf067c83fc8a92ad750eae2e256725282534b68
4
- data.tar.gz: 8887f778eaec56e6bdbb84d0a48f8db82fbd52cabbcdea01c48f4cffb5ff410b
3
+ metadata.gz: 7e078472cf91253d16b3341865954ead4ce1d6539d509c5ed6d54302c5a3147d
4
+ data.tar.gz: 76f40572409ed01a1797c4902896b783d3a0403783031fca8c8dff04a1499acf
5
5
  SHA512:
6
- metadata.gz: 6656d86bf179be5a044a95ccbc1a9c11e68642a35ed2ffa99138dc0de653ed4b2e176a9763f08b765d409ec232e4b9eda4779b166c97b7304ed156a48b147c93
7
- data.tar.gz: 3f8771ece0365e9a1c7073a6d749d41570776b59e9cbe0c38383516cce9cf0261a2ba287aadb30278ee0cf72e859fe4b4cb072fa89294bc3a525700fdec5ff9c
6
+ metadata.gz: ea3ecacb94cbe3e52dacb9fdb4ea447790990cee631fd890f5389d483157d7e16621eda6dba68dddf13d1e1f5f499b8bc2ac3952e2ca1752f133f5aa496403b1
7
+ data.tar.gz: 8ae64565dc336f5158a877c54a02520bc6a0e6923c6bbdd705c2cff8c848ff832f0a0683f30c2e86df883a356d89e60269cebb219d9a9732212fc5edfd27fea6
data/CHANGELOG.md CHANGED
@@ -1,49 +1,53 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.11] - 2025-06-04
4
+
5
+ - options are customizable with the yaml frontmatter
6
+
3
7
  ## [0.1.10] - 2025-06-03
4
8
 
5
- - standalone options for codeblocks (run instead of run=true)
6
- - explain option for psql code blocks with Dalibo visualization links
7
- - Fixed Dalibo URL generation to properly submit plans via HTTP POST
8
- - Added result option to control result block visibility (result=false hides result blocks while still executing code)
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)
9
13
 
10
14
  ## [0.1.9] - 2025-06-02
11
15
 
12
- - mermaid codeblocks
16
+ - mermaid codeblocks
13
17
 
14
18
  ## [0.1.8] - 2025-06-01
15
19
 
16
- - Added run option
20
+ - Added run option
17
21
 
18
22
  ## [0.1.7] - 2025-06-01
19
23
 
20
- - Added rerun functionality
24
+ - Added rerun functionality
21
25
 
22
26
  ## [0.1.6] - 2025-06-01
23
27
 
24
- - Refactor code to state pattern
25
- - 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
26
30
 
27
31
  ## [0.1.5] - 2025-05-19
28
32
 
29
- - Remove gif files from release
33
+ - Remove gif files from release
30
34
 
31
35
  ## [0.1.4] - 2025-05-18
32
36
 
33
- - Add support for zsh, bash, sh
37
+ - Add support for zsh, bash, sh
34
38
 
35
39
  ## [0.1.3] - 2025-05-14
36
40
 
37
- - Fix missing minitest dep
41
+ - Fix missing minitest dep
38
42
 
39
43
  ## [0.1.2] - 2025-05-14
40
44
 
41
- - Gemfile update
45
+ - Gemfile update
42
46
 
43
47
  ## [0.1.1] - 2025-05-14
44
48
 
45
- - Added checks for missing dependencies
49
+ - Added checks for missing dependencies
46
50
 
47
51
  ## [0.1.0] - 2025-05-13
48
52
 
49
- - Initial release
53
+ - Initial release
data/README.md CHANGED
@@ -135,6 +135,65 @@ markdown-run:
135
135
  - sql: psql
136
136
  ```
137
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
+
138
197
  ## Demo
139
198
 
140
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,20 +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)
43
45
  end
44
46
 
45
- def parse_explain_option(options_string)
46
- parse_boolean_option(options_string, "explain", false)
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)
47
50
  end
48
51
 
49
- def parse_result_option(options_string)
50
- parse_boolean_option(options_string, "result", true)
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)
51
55
  end
52
56
 
53
57
  private
data/lib/code_executor.rb CHANGED
@@ -21,6 +21,12 @@ class CodeExecutor
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}"
@@ -167,31 +173,25 @@ class CodeExecutor
167
173
 
168
174
  def submit_plan_to_dalibo(plan_json)
169
175
  begin
170
- # Start with HTTPS directly to avoid the HTTP->HTTPS redirect
171
176
  uri = URI('https://explain.dalibo.com/new')
172
177
  http = Net::HTTP.new(uri.host, uri.port)
173
178
  http.use_ssl = true
174
- http.read_timeout = 10 # 10 seconds timeout
179
+ http.read_timeout = 10
175
180
 
176
- # Prepare the JSON payload
177
181
  payload = {
178
182
  'plan' => plan_json,
179
183
  'title' => "Query Plan - #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}",
180
184
  'query' => ''
181
185
  }
182
186
 
183
- # Create the POST request
184
187
  request = Net::HTTP::Post.new(uri)
185
188
  request['Content-Type'] = 'application/json'
186
189
  request.body = JSON.generate(payload)
187
190
 
188
- # Send the request and follow redirects to get the final URL
189
191
  response = http.request(request)
190
192
 
191
- # Dalibo returns a redirect to the plan URL
192
193
  if response.is_a?(Net::HTTPRedirection)
193
194
  location = response['location']
194
- # Make sure it's a full URL
195
195
  if location
196
196
  if location.start_with?('/')
197
197
  location = "https://explain.dalibo.com#{location}"
@@ -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
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Markdown
4
4
  module Run
5
- VERSION = "0.1.10"
5
+ VERSION = "0.1.11"
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
@@ -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.10
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-03 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: