markdown-run 0.1.12 → 0.2.0

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: 2764390f72c90c38bd4862f78db857bd5585d13e59f1a2c87a3a9c67c2f66529
4
- data.tar.gz: 95caace78ee0db7d12ac3a6a7a5a9abcddf32c1e946ff3d9cf71beed12c89a91
3
+ metadata.gz: 72c9c060b9169413ee686c9a4958b36b11d5271a4222d598bfe0c847364488e0
4
+ data.tar.gz: 56bfea5eb072bfbf7db692a6df64c08e901f7a77551f2b17491a4a84201220fb
5
5
  SHA512:
6
- metadata.gz: 90aea8dd4f03bc75969643bedd914ed2a8a8a1b45adf777d51fdfabb60deefd5e36cd1b73ac37225848b4663e12872a69c2ba4c3624029ac4c81c333a8eb6a8c
7
- data.tar.gz: 7dfc12cda3abca94774e013904e2d9853713dce9c8b4059d6ca6bd90319ddff4f9f0d826c230d508f2438fdce57626f7868c1b94c9756ec3a122e0645be2121c
6
+ metadata.gz: f4b2c6aa7b3fec529749f56e4a64c186060097b94f64678c96742759d51f4b7dbb1d2014c98abb4ff047f8ba62ef15d1f32612e2b19cbdafa480664f3556a420
7
+ data.tar.gz: a7bb6b114beaea5f0857534cee766f9dcb157ab39949f433a5965acb558996f30a3fcbd6a13a3df3a264ddf64b1115dab089af7b674b68195467b7a2e482ef5f
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.3.5
1
+ ruby 3.4.6
data/README.md CHANGED
@@ -55,6 +55,16 @@ example vscode keybinding
55
55
 
56
56
  ### Code block options
57
57
 
58
+ Options are specified using Pandoc-style curly braces after the language identifier:
59
+
60
+ ```markdown
61
+ ```ruby {rerun=true}
62
+ puts "Hello World"
63
+ ```
64
+ ```
65
+
66
+ **Available options:**
67
+
58
68
  - `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
69
  - `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
70
  - `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
@@ -77,31 +87,31 @@ Explicit assignments (e.g., `run=false`) take precedence over standalone options
77
87
 
78
88
  Examples:
79
89
 
80
- ```js run=false
90
+ ```js {run=false}
81
91
  console.log("This will not execute at all");
82
92
  ```
83
93
 
84
- ```js rerun
94
+ ```js {rerun}
85
95
  console.log("This will re-execute even if result exists");
86
96
  ```
87
97
 
88
- ```js run=true rerun=false
98
+ ```js {run=true rerun=false}
89
99
  console.log("This will execute only if no result exists");
90
100
  ```
91
101
 
92
- ```ruby result=false run
102
+ ```ruby {result=false run}
93
103
  puts "This executes but the result block is hidden"
94
104
  ```
95
105
 
96
- ```psql explain
106
+ ```psql {explain}
97
107
  SELECT * FROM users WHERE id = 1;
98
108
  ```
99
109
 
100
- ```psql explain=true
110
+ ```psql {explain=true}
101
111
  EXPLAIN (ANALYZE) SELECT * FROM large_table;
102
112
  ```
103
113
 
104
- ```psql flamegraph
114
+ ```psql {flamegraph}
105
115
  SELECT u.name, COUNT(o.id) as order_count
106
116
  FROM users u
107
117
  LEFT JOIN orders o ON u.id = o.user_id
@@ -111,12 +121,12 @@ ORDER BY order_count DESC
111
121
  LIMIT 10;
112
122
  ```
113
123
 
114
- ```psql flamegraph=true result=false
124
+ ```psql {flamegraph=true result=false}
115
125
  -- This will generate a flamegraph but hide the JSON result block
116
126
  SELECT * FROM complex_query_with_joins;
117
127
  ```
118
128
 
119
- ```psql result=false explain
129
+ ```psql {result=false explain}
120
130
  SELECT * FROM large_table;
121
131
  -- This will execute the explain query and show the Dalibo link but hide the result block
122
132
  ```
@@ -125,7 +135,7 @@ SELECT * FROM large_table;
125
135
 
126
136
  PostgreSQL flamegraph blocks generate interactive SVG flamegraphs from query execution plans:
127
137
 
128
- ```psql flamegraph
138
+ ```psql {flamegraph}
129
139
  SELECT users.*, orders.total
130
140
  FROM users
131
141
  JOIN orders ON users.id = orders.user_id
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ require "bundler/gem_tasks"
1
2
  require "rake/testtask"
2
3
 
3
4
  Rake::TestTask.new(:test) do |t|
@@ -19,6 +20,44 @@ task :flog_detailed do
19
20
  system("flog -d lib/ exe/ test/")
20
21
  end
21
22
 
23
+ desc "Run tests with detailed timing information"
24
+ task :test_profile do
25
+ require 'benchmark'
26
+
27
+ puts "Running tests with detailed profiling..."
28
+
29
+ # Run tests with verbose output and capture timing
30
+ output = `bundle exec rake test TESTOPTS="-v" 2>&1`
31
+
32
+ # Extract test timings
33
+ timings = []
34
+ output.scan(/^(.+) = ([0-9]+\.[0-9]+) s = \.$/) do |test_name, time|
35
+ timings << [test_name, time.to_f]
36
+ end
37
+
38
+ # Sort by time (descending)
39
+ timings.sort_by! { |_, time| -time }
40
+
41
+ puts "\n" + "="*80
42
+ puts "TOP 15 SLOWEST TESTS"
43
+ puts "="*80
44
+
45
+ timings.first(15).each_with_index do |(test_name, time), index|
46
+ test_display = test_name.length > 60 ? test_name[0...60] + "..." : test_name
47
+ printf "%2d. %-63s %6.2f s\n", index + 1, test_display, time
48
+ end
49
+
50
+ puts "\n" + "="*80
51
+ puts "SUMMARY"
52
+ puts "="*80
53
+ total_time = timings.sum { |_, time| time }
54
+ slow_tests = timings.select { |_, time| time > 0.1 }
55
+
56
+ puts "Total test time: #{total_time.round(2)} seconds"
57
+ puts "Tests slower than 0.1s: #{slow_tests.count}"
58
+ puts "Time spent in slow tests: #{slow_tests.sum { |_, time| time }.round(2)} seconds (#{((slow_tests.sum { |_, time| time } / total_time) * 100).round(1)}%)"
59
+ end
60
+
22
61
  desc "Release"
23
62
  task :release do
24
63
  `gem bump`
@@ -29,4 +68,19 @@ task :release do
29
68
  `gem release`
30
69
  end
31
70
 
71
+ # Coverage task
72
+ desc "Run tests with SimpleCov coverage report"
73
+ task :coverage do
74
+ ENV['COVERAGE'] = 'true'
75
+ Rake::Task[:test].invoke
76
+
77
+ puts "\n🎯 Coverage Report Generated!"
78
+ puts "📊 Open coverage/index.html to view detailed coverage report"
79
+
80
+ # Try to open coverage report automatically (works on macOS)
81
+ if RUBY_PLATFORM.include?('darwin')
82
+ system('open coverage/index.html')
83
+ end
84
+ end
85
+
32
86
  task default: :test
@@ -17,7 +17,7 @@ module CodeBlockHelper
17
17
 
18
18
  def start_code_block(current_line, lang, options_string = nil)
19
19
  @output_lines << current_line
20
- @current_block_lang = resolve_language(lang)
20
+ @current_block_lang = @language_resolver.resolve_language(lang)
21
21
  @current_block_rerun = @code_block_parser.parse_rerun_option(options_string, @current_block_lang)
22
22
  @current_block_run = @code_block_parser.parse_run_option(options_string, @current_block_lang)
23
23
  @current_block_explain = @code_block_parser.parse_explain_option(options_string, @current_block_lang)
@@ -29,19 +29,25 @@ module CodeBlockHelper
29
29
 
30
30
  def accumulate_code_content(current_line)
31
31
  @current_code_content += current_line
32
- @output_lines << current_line
32
+ # For ruby blocks, don't output code content yet - we'll replace it with xmpfilter output
33
+ unless ruby_style_result?(@current_block_lang)
34
+ @output_lines << current_line
35
+ end
33
36
  end
34
37
 
35
38
  def end_code_block(current_line, file_enum)
36
- @output_lines << current_line
39
+ # For ruby blocks, don't output closing ``` yet - we'll add it after the xmpfilter output
40
+ unless ruby_style_result?(@current_block_lang)
41
+ @output_lines << current_line
42
+ end
37
43
 
38
44
  decision = decide_execution(file_enum)
39
45
 
40
46
  if decision[:execute]
41
47
  # If we consumed lines for rerun, don't add them to output (they'll be replaced)
42
- execute_and_add_result(decision[:blank_line])
48
+ execute_and_add_result(decision[:blank_line], current_line)
43
49
  else
44
- skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision)
50
+ skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision, current_line)
45
51
  end
46
52
 
47
53
  reset_code_block_state
@@ -49,7 +55,7 @@ module CodeBlockHelper
49
55
 
50
56
  def decide_execution(file_enum)
51
57
  decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_flamegraph, @current_block_result)
52
- decision = decider.decide(file_enum, method(:result_block_regex))
58
+ decision = decider.decide(file_enum, method(:result_block_regex), @current_code_content)
53
59
 
54
60
  # Handle the consume_existing flag for rerun scenarios
55
61
  if decision[:consume_existing]
@@ -1,13 +1,15 @@
1
1
  require_relative "language_configs"
2
2
 
3
3
  class CodeBlockParser
4
- # Code block header pattern: ```language options
5
- CODE_BLOCK_START_PATTERN = /^```(\w+)(?:\s+(.*))?$/i
4
+ # Code block header pattern: ```language {options}
5
+ # Supports both ```language and ```language {options} formats
6
+ CODE_BLOCK_START_PATTERN = /^```(\w+)(?:\s*\{(.*)\})?$/i
6
7
  RUBY_RESULT_BLOCK_PATTERN = /^```ruby\s+RESULT$/i
7
8
  BLOCK_END_PATTERN = "```"
8
9
 
9
- def initialize(frontmatter_parser)
10
+ def initialize(frontmatter_parser, language_resolver)
10
11
  @frontmatter_parser = frontmatter_parser
12
+ @language_resolver = language_resolver
11
13
  end
12
14
 
13
15
  def parse_code_block_header(line)
@@ -62,7 +64,7 @@ class CodeBlockParser
62
64
  private
63
65
 
64
66
  def resolve_language(lang)
65
- @frontmatter_parser.resolve_language(lang)
67
+ @language_resolver.resolve_language(lang)
66
68
  end
67
69
 
68
70
  def parse_boolean_option(options_string, option_name, default_value)
data/lib/code_executor.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "tempfile"
2
2
  require "open3"
3
3
  require_relative "language_configs"
4
+ require_relative "test_silencer"
4
5
 
5
6
  class CodeExecutor
6
7
  def self.execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
@@ -13,7 +14,7 @@ class CodeExecutor
13
14
 
14
15
  return handle_unsupported_language(lang) unless lang_config
15
16
 
16
- warn "Executing #{lang_key} code block..."
17
+ TestSilencer.warn_unless_testing("Executing #{lang_key} code block...")
17
18
 
18
19
  result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path, explain, flamegraph)
19
20
  process_execution_result(result, lang_config, lang_key, explain, flamegraph)
@@ -26,7 +27,7 @@ class CodeExecutor
26
27
  end
27
28
 
28
29
  def handle_unsupported_language(lang)
29
- warn "Unsupported language: #{lang}"
30
+ TestSilencer.warn_unless_testing "Unsupported language: #{lang}"
30
31
  "ERROR: Unsupported language: #{lang}"
31
32
  end
32
33
 
@@ -46,7 +47,13 @@ class CodeExecutor
46
47
  Tempfile.create([lang_key, temp_file_suffix], temp_dir) do |temp_file|
47
48
  temp_file.write(code_content)
48
49
  temp_file.close
49
- command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path, input_file_path, explain, flamegraph)
50
+ command_to_run, exec_options = cmd_lambda.call(**{
51
+ code_content: code_content,
52
+ temp_file_path: temp_file.path,
53
+ input_file_path: input_file_path,
54
+ explain: explain,
55
+ flamegraph: flamegraph
56
+ })
50
57
 
51
58
  # Extract output_path if present (for mermaid)
52
59
  output_path = exec_options.delete(:output_path) if exec_options.is_a?(Hash)
@@ -64,7 +71,13 @@ class CodeExecutor
64
71
  end
65
72
 
66
73
  def execute_direct_command(code_content, cmd_lambda, input_file_path = nil, explain = false, flamegraph = false)
67
- command_to_run, exec_options = cmd_lambda.call(code_content, nil, input_file_path, explain, flamegraph)
74
+ command_to_run, exec_options = cmd_lambda.call(**{
75
+ code_content: code_content,
76
+ temp_file_path: nil,
77
+ input_file_path: input_file_path,
78
+ explain: explain,
79
+ flamegraph: flamegraph
80
+ })
68
81
  captured_stdout, captured_stderr, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
69
82
  { stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj, input_file_path: input_file_path }
70
83
  end
@@ -104,8 +117,8 @@ class CodeExecutor
104
117
  end
105
118
 
106
119
  def add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
107
- warn "Code execution failed for language '#{lang_key}' with status #{exit_status}."
108
- warn "Stderr:\n#{stderr_output}" if stderr_has_content?(stderr_output)
120
+ TestSilencer.warn_unless_testing "Code execution failed for language '#{lang_key}' with status #{exit_status}."
121
+ TestSilencer.warn_unless_testing "Stderr:\n#{stderr_output}" if stderr_has_content?(stderr_output)
109
122
 
110
123
  is_js_error_already_formatted = lang_config && lang_config[:error_handling] == :js_specific && result_output.include?("Stderr:")
111
124
  unless result_output.downcase.include?("error:") || is_js_error_already_formatted
@@ -116,15 +129,11 @@ class CodeExecutor
116
129
  result_output
117
130
  end
118
131
 
119
- def stderr_has_content?(stderr_output)
120
- stderr_output && !stderr_output.strip.empty?
121
- end
122
-
123
132
  def handle_mermaid_svg_result(result, lang_key)
124
133
  output_path = result[:output_path]
125
134
 
126
135
  unless output_path && File.exist?(output_path)
127
- warn "Warning: Mermaid SVG file not generated at expected path: #{output_path}"
136
+ TestSilencer.warn_unless_testing "Warning: Mermaid SVG file not generated at expected path: #{output_path}"
128
137
  return "Error: SVG file not generated"
129
138
  end
130
139
 
@@ -143,7 +152,7 @@ class CodeExecutor
143
152
  relative_path = svg_filename
144
153
  end
145
154
 
146
- warn "Generated Mermaid SVG: #{relative_path}"
155
+ TestSilencer.warn_unless_testing("Generated Mermaid SVG: #{relative_path}")
147
156
 
148
157
  # Return markdown image tag instead of typical result content
149
158
  "![Mermaid Diagram](#{relative_path})"
@@ -225,7 +234,7 @@ class CodeExecutor
225
234
  relative_path = File.basename(output_path)
226
235
  end
227
236
 
228
- warn "Generated PostgreSQL flamegraph: #{relative_path}"
237
+ TestSilencer.warn_unless_testing("Generated PostgreSQL flamegraph: #{relative_path}")
229
238
 
230
239
  # Return a special format that the markdown processor can parse
231
240
  # Preserve any existing Dalibo link prefix
@@ -239,10 +248,10 @@ class CodeExecutor
239
248
  end
240
249
 
241
250
  rescue JSON::ParserError => e
242
- warn "Error parsing EXPLAIN JSON: #{e.message}"
251
+ TestSilencer.warn_unless_testing "Error parsing EXPLAIN JSON: #{e.message}"
243
252
  result_output
244
253
  rescue => e
245
- warn "Error generating flamegraph: #{e.message}"
254
+ TestSilencer.warn_unless_testing "Error generating flamegraph: #{e.message}"
246
255
  result_output
247
256
  end
248
257
  end
@@ -279,11 +288,11 @@ class CodeExecutor
279
288
  nil
280
289
  end
281
290
  else
282
- warn "Failed to submit plan to Dalibo: #{response.code} #{response.message}"
291
+ TestSilencer.warn_unless_testing "Failed to submit plan to Dalibo: #{response.code} #{response.message}"
283
292
  nil
284
293
  end
285
294
  rescue => e
286
- warn "Error submitting plan to Dalibo: #{e.message}"
295
+ TestSilencer.warn_unless_testing "Error submitting plan to Dalibo: #{e.message}"
287
296
  nil
288
297
  end
289
298
  end
@@ -12,9 +12,14 @@ class ExecutionDecider
12
12
  @current_block_result = current_block_result
13
13
  end
14
14
 
15
- def decide(file_enum, result_block_regex_method)
15
+ def decide(file_enum, result_block_regex_method, code_content = nil)
16
16
  return skip_execution_run_false if run_disabled?
17
17
 
18
+ # For ruby blocks, check if code content contains xmpfilter results (# >>)
19
+ if is_ruby_block? && code_content && has_xmpfilter_results?(code_content)
20
+ return handle_inline_ruby_results
21
+ end
22
+
18
23
  expected_header_regex = result_block_regex_method.call(@current_block_lang)
19
24
  peek1 = peek_next_line(file_enum)
20
25
 
@@ -296,4 +301,23 @@ class ExecutionDecider
296
301
  # so it should be updated on each run
297
302
  @current_block_flamegraph && !@current_block_result
298
303
  end
304
+
305
+ def is_ruby_block?
306
+ @current_block_lang == "ruby"
307
+ end
308
+
309
+ def has_xmpfilter_results?(code_content)
310
+ # Check if code contains xmpfilter comment markers (# >>)
311
+ code_content.include?("# >>")
312
+ end
313
+
314
+ def handle_inline_ruby_results
315
+ if @current_block_rerun
316
+ # Rerun requested, so execute and replace inline results
317
+ { execute: true }
318
+ else
319
+ # Has inline results and rerun not requested, skip execution
320
+ { execute: false, lines_to_pass_through: [] }
321
+ end
322
+ end
299
323
  end
@@ -1,16 +1,22 @@
1
1
  require "yaml"
2
2
  require_relative "enum_helper"
3
+ require_relative "language_resolver"
4
+ require_relative "test_silencer"
3
5
 
4
6
  class FrontmatterParser
5
7
  include EnumHelper
6
8
 
7
- def initialize
8
- @aliases = {}
9
+ def initialize(language_resolver = nil)
10
+ @language_resolver = language_resolver || LanguageResolver.new
9
11
  @defaults = {}
10
12
  @language_defaults = {}
11
13
  end
12
14
 
13
- attr_reader :aliases, :defaults, :language_defaults
15
+ attr_reader :defaults, :language_defaults
16
+
17
+ def language_resolver
18
+ @language_resolver
19
+ end
14
20
 
15
21
  def parse_frontmatter(file_enum, output_lines)
16
22
  first_line = peek_next_line(file_enum)
@@ -21,7 +27,7 @@ class FrontmatterParser
21
27
  end
22
28
 
23
29
  def resolve_language(lang)
24
- @aliases[lang] || lang
30
+ @language_resolver.resolve_language(lang)
25
31
  end
26
32
 
27
33
  def get_default_value(option_name, language, fallback_default)
@@ -72,7 +78,7 @@ class FrontmatterParser
72
78
  frontmatter = YAML.safe_load(frontmatter_lines.join)
73
79
  extract_aliases(frontmatter) if frontmatter.is_a?(Hash)
74
80
  rescue YAML::SyntaxError => e
75
- warn "Warning: Invalid YAML frontmatter: #{e.message}"
81
+ TestSilencer.warn_unless_testing "Warning: Invalid YAML frontmatter: #{e.message}"
76
82
  end
77
83
  end
78
84
 
@@ -83,13 +89,15 @@ class FrontmatterParser
83
89
  # Extract aliases
84
90
  aliases = markdown_run_config["alias"]
85
91
  if aliases.is_a?(Array)
92
+ new_aliases = {}
86
93
  aliases.each do |alias_config|
87
94
  next unless alias_config.is_a?(Hash)
88
95
 
89
96
  alias_config.each do |alias_name, target_lang|
90
- @aliases[alias_name.to_s] = target_lang.to_s
97
+ new_aliases[alias_name.to_s] = target_lang.to_s
91
98
  end
92
99
  end
100
+ @language_resolver.update_aliases(new_aliases)
93
101
  end
94
102
 
95
103
  # Extract defaults
@@ -1,7 +1,9 @@
1
- require 'securerandom'
1
+ require "securerandom"
2
+ require_relative "test_silencer"
3
+ require_relative "postgres_helper"
2
4
 
3
5
  JS_CONFIG = {
4
- command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
6
+ command: proc { |temp_file_path: nil, **|
5
7
  # Check if bun is available
6
8
  bun_exists = system("command -v bun > /dev/null 2>&1")
7
9
  if bun_exists
@@ -16,35 +18,41 @@ JS_CONFIG = {
16
18
  }.freeze
17
19
 
18
20
  SQLITE_CONFIG = {
19
- command: ->(code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) { [ "sqlite3 #{temp_file_path}", { stdin_data: code_content } ] },
21
+ command: proc { |code_content: nil, temp_file_path: nil, **| [ "sqlite3 #{temp_file_path}", { stdin_data: code_content } ] },
20
22
  temp_file_suffix: ".db" # Temp file is the database
21
23
  }.freeze
22
24
 
23
25
  SUPPORTED_LANGUAGES = {
24
26
  "psql" => {
25
- command: ->(code_content, _temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
26
- psql_exists = system("command -v psql > /dev/null 2>&1")
27
- unless psql_exists
28
- abort "Error: psql command not found. Please install PostgreSQL or ensure psql is in your PATH."
27
+ command: proc { |code_content: nil, explain: false, flamegraph: false, **|
28
+ unless PostgresHelper.available?
29
+ if PostgresHelper.psql_command && PostgresHelper.using_docker?
30
+ TestSilencer.abort_unless_testing "Error: PostgreSQL is running in Docker but required environment variables (PGUSER, PGDATABASE) are not set. Please set these variables before running psql commands."
31
+ else
32
+ TestSilencer.abort_unless_testing "Error: psql command not found. Please install PostgreSQL locally, ensure psql is in your PATH, or run PostgreSQL in a Docker container."
33
+ end
29
34
  end
30
35
 
36
+ PostgresHelper.validate_env_vars!
37
+ psql_cmd = PostgresHelper.psql_command
38
+
31
39
  # Modify the SQL query if explain or flamegraph option is enabled
32
40
  if explain || flamegraph
33
41
  # Wrap the query with EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
34
42
  # Remove any trailing semicolons and whitespace, then add our EXPLAIN wrapper
35
- clean_query = code_content.strip.gsub(/;\s*$/, '')
43
+ clean_query = code_content.strip.gsub(/;\s*$/, "")
36
44
  explained_query = "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) #{clean_query};"
37
- [ "psql -A -t -X", { stdin_data: explained_query } ]
45
+ [ "#{psql_cmd} -A -t -X", { stdin_data: explained_query } ]
38
46
  else
39
- [ "psql -A -t -X", { stdin_data: code_content } ]
47
+ [ "#{psql_cmd} -A -t -X", { stdin_data: code_content } ]
40
48
  end
41
49
  }
42
50
  },
43
51
  "ruby" => {
44
- command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
52
+ command: proc { |temp_file_path: nil, **|
45
53
  xmpfilter_exists = system("command -v xmpfilter > /dev/null 2>&1")
46
54
  unless xmpfilter_exists
47
- abort "Error: xmpfilter command not found. Please install xmpfilter or ensure it is in your PATH."
55
+ TestSilencer.abort_unless_testing "Error: xmpfilter command not found. Please install xmpfilter or ensure it is in your PATH."
48
56
  end
49
57
  [ "xmpfilter #{temp_file_path}", {} ]
50
58
  },
@@ -57,40 +65,40 @@ SUPPORTED_LANGUAGES = {
57
65
  "sqlite" => SQLITE_CONFIG,
58
66
  "sqlite3" => SQLITE_CONFIG, # Alias for sqlite
59
67
  "bash" => {
60
- command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
68
+ command: proc { |temp_file_path: nil, **|
61
69
  bash_exists = system("command -v bash > /dev/null 2>&1")
62
70
  unless bash_exists
63
- abort "Error: bash command not found. Please ensure bash is in your PATH."
71
+ TestSilencer.abort_unless_testing "Error: bash command not found. Please ensure bash is in your PATH."
64
72
  end
65
73
  [ "bash #{temp_file_path}", {} ]
66
74
  },
67
75
  temp_file_suffix: ".sh"
68
76
  },
69
77
  "zsh" => {
70
- command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
78
+ command: proc { |temp_file_path: nil, **|
71
79
  zsh_exists = system("command -v zsh > /dev/null 2>&1")
72
80
  unless zsh_exists
73
- abort "Error: zsh command not found. Please ensure zsh is in your PATH."
81
+ TestSilencer.abort_unless_testing "Error: zsh command not found. Please ensure zsh is in your PATH."
74
82
  end
75
83
  [ "zsh #{temp_file_path}", {} ]
76
84
  },
77
85
  temp_file_suffix: ".zsh"
78
86
  },
79
87
  "sh" => {
80
- command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
88
+ command: proc { |temp_file_path: nil, **|
81
89
  sh_exists = system("command -v sh > /dev/null 2>&1")
82
90
  unless sh_exists
83
- abort "Error: sh command not found. Please ensure sh is in your PATH."
91
+ TestSilencer.abort_unless_testing "Error: sh command not found. Please ensure sh is in your PATH."
84
92
  end
85
93
  [ "sh #{temp_file_path}", {} ]
86
94
  },
87
95
  temp_file_suffix: ".sh"
88
96
  },
89
97
  "mermaid" => {
90
- command: ->(code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
98
+ command: proc { |temp_file_path: nil, input_file_path: nil, **|
91
99
  mmdc_exists = system("command -v mmdc > /dev/null 2>&1")
92
100
  unless mmdc_exists
93
- abort "Error: mmdc command not found. Please install @mermaid-js/mermaid-cli: npm install -g @mermaid-js/mermaid-cli"
101
+ TestSilencer.abort_unless_testing "Error: mmdc command not found. Please install @mermaid-js/mermaid-cli: npm install -g @mermaid-js/mermaid-cli"
94
102
  end
95
103
 
96
104
  # Generate SVG output file path with directory structure based on markdown file
@@ -0,0 +1,17 @@
1
+ class LanguageResolver
2
+ def initialize(aliases = {})
3
+ @aliases = aliases
4
+ end
5
+
6
+ def resolve_language(lang)
7
+ @aliases[lang] || lang
8
+ end
9
+
10
+ def update_aliases(new_aliases)
11
+ @aliases.merge!(new_aliases)
12
+ end
13
+
14
+ def get_aliases
15
+ @aliases.dup
16
+ end
17
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Markdown
4
4
  module Run
5
- VERSION = "0.1.12"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -1,7 +1,9 @@
1
1
  require "tempfile"
2
2
  require "fileutils"
3
+ require_relative "test_silencer"
3
4
 
4
5
  class MarkdownFileWriter
6
+
5
7
  def self.write_output_to_file(output_lines, input_file_path)
6
8
  temp_dir = File.dirname(File.expand_path(input_file_path))
7
9
 
@@ -13,13 +15,14 @@ class MarkdownFileWriter
13
15
  begin
14
16
  FileUtils.mv(temp_output_file.path, input_file_path)
15
17
  rescue Errno::EACCES, Errno::EXDEV
16
- warn "Atomic move failed. Falling back to copy and delete."
18
+ TestSilencer.warn_unless_testing "Atomic move failed. Falling back to copy and delete."
17
19
  FileUtils.cp(temp_output_file.path, input_file_path)
18
20
  FileUtils.rm_f(temp_output_file.path)
19
21
  end
20
22
  end
21
23
 
22
- warn "Markdown processing complete. Output written to #{input_file_path}"
24
+ # Only show the warning message if we're not running tests
25
+ TestSilencer.warn_unless_testing("Markdown processing complete. Output written to #{input_file_path}")
23
26
  true # Indicate success
24
27
  end
25
28
  end
@@ -1,4 +1,5 @@
1
1
  require_relative "language_configs"
2
+ require_relative "language_resolver"
2
3
  require_relative "frontmatter_parser"
3
4
  require_relative "code_block_parser"
4
5
  require_relative "code_executor"
@@ -20,8 +21,9 @@ class MarkdownProcessor
20
21
  @output_lines = []
21
22
  reset_code_block_state
22
23
 
23
- @frontmatter_parser = FrontmatterParser.new
24
- @code_block_parser = CodeBlockParser.new(@frontmatter_parser)
24
+ @language_resolver = LanguageResolver.new
25
+ @frontmatter_parser = FrontmatterParser.new(@language_resolver)
26
+ @code_block_parser = CodeBlockParser.new(@frontmatter_parser, @language_resolver)
25
27
  end
26
28
 
27
29
  def process_file(file_enum)
@@ -38,9 +40,7 @@ class MarkdownProcessor
38
40
 
39
41
  private
40
42
 
41
- def resolve_language(lang)
42
- @frontmatter_parser.resolve_language(lang)
43
- end
43
+
44
44
 
45
45
  def is_block_end?(line)
46
46
  @code_block_parser.is_block_end?(line)
@@ -0,0 +1,94 @@
1
+ require_relative "test_silencer"
2
+
3
+ module PostgresHelper
4
+ class << self
5
+ def psql_command
6
+ @psql_command ||= detect_psql_command
7
+ end
8
+
9
+ def available?
10
+ return false if psql_command.nil?
11
+
12
+ # If using Docker, check required env vars are present
13
+ if using_docker?
14
+ has_required_env_vars?
15
+ else
16
+ true
17
+ end
18
+ end
19
+
20
+ def using_docker?
21
+ cmd = psql_command
22
+ cmd && cmd.start_with?("docker exec")
23
+ end
24
+
25
+ # Reset cached command detection (for tests)
26
+ def reset_cache!
27
+ @psql_command = nil
28
+ end
29
+
30
+ def validate_env_vars!
31
+ return unless using_docker?
32
+
33
+ required_vars = ["PGUSER", "PGDATABASE"]
34
+ missing_vars = required_vars.reject { |var| ENV[var] }
35
+
36
+ if missing_vars.any?
37
+ TestSilencer.abort_unless_testing "Error: PostgreSQL is running in Docker but required environment variables are missing: #{missing_vars.join(", ")}. Please set these variables before running psql commands."
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def has_required_env_vars?
44
+ required_vars = ["PGUSER", "PGDATABASE"]
45
+ required_vars.all? { |var| ENV[var] }
46
+ end
47
+
48
+ def detect_psql_command
49
+ # Check if psql is available locally
50
+ return "psql" if system("command -v psql > /dev/null 2>&1")
51
+
52
+ # Check if docker is available
53
+ return nil unless system("command -v docker > /dev/null 2>&1")
54
+
55
+ # Find running postgres container
56
+ container_id = find_postgres_container
57
+ return nil if container_id.nil? || container_id.empty?
58
+
59
+ # Build docker exec command with environment variables passed through
60
+ env_vars = build_env_vars
61
+ "docker exec -i -u postgres #{env_vars}#{container_id} psql"
62
+ end
63
+
64
+ def build_env_vars
65
+ # Pass through PostgreSQL environment variables if they exist
66
+ pg_env_vars = [ "PGUSER", "PGPASSWORD", "PGDATABASE", "PGHOST", "PGPORT" ]
67
+ env_flags = pg_env_vars.map do |var|
68
+ value = ENV[var]
69
+ "-e #{var}=\"#{value}\" " if value
70
+ end.compact.join
71
+ env_flags.empty? ? "" : "#{env_flags} "
72
+ end
73
+
74
+ def find_postgres_container
75
+ # Try to find a running postgres container
76
+ # First, check for container with postgres in the name
77
+ output = `docker ps --filter "ancestor=postgres" --format "{{.ID}}" 2>/dev/null`.strip
78
+ return output.split("\n").first unless output.empty?
79
+
80
+ # Alternative: check for containers with "postgres" in name
81
+ output = `docker ps --filter "name=postgres" --format "{{.ID}}" 2>/dev/null`.strip
82
+ return output.split("\n").first unless output.empty?
83
+
84
+ # Last resort: check if any running container has psql command
85
+ containers = `docker ps --format "{{.ID}}" 2>/dev/null`.strip.split("\n")
86
+ containers.each do |container_id|
87
+ has_psql = system("docker exec #{container_id} which psql > /dev/null 2>&1")
88
+ return container_id if has_psql
89
+ end
90
+
91
+ nil
92
+ end
93
+ end
94
+ end
data/lib/result_helper.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative "flamegraph_helper"
2
+ require_relative "test_silencer"
2
3
 
3
4
  module ResultHelper
4
5
  include FlamegraphHelper
@@ -16,7 +17,7 @@ module ResultHelper
16
17
  end
17
18
 
18
19
  def result_block_header(lang)
19
- ruby_style_result?(lang) ? "```ruby RESULT\n" : "```RESULT\n"
20
+ ruby_style_result?(lang) ? "```ruby\n" : "``` {result}\n"
20
21
  end
21
22
 
22
23
  def result_block_regex(lang)
@@ -24,14 +25,21 @@ module ResultHelper
24
25
  # For mermaid, look for existing image tags with .svg extension
25
26
  /^!\[.*\]\(.*\.svg\)$/i
26
27
  elsif ruby_style_result?(lang)
28
+ # For ruby, check for old-style ```ruby RESULT blocks (for backward compatibility during migration)
27
29
  /^```ruby\s+RESULT$/i
28
30
  else
29
- /^```RESULT$/i
31
+ /^```\s*\{result\}$/i
30
32
  end
31
33
  end
32
34
 
33
- def add_result_block(result_output, blank_line_before_new_result)
34
- if mermaid_style_result?(@current_block_lang)
35
+ def add_result_block(result_output, blank_line_before_new_result, closing_line = nil)
36
+ if ruby_style_result?(@current_block_lang)
37
+ # For ruby, replace the code block content with xmpfilter output
38
+ @output_lines << result_output
39
+ @output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
40
+ @output_lines << (closing_line || "```\n")
41
+ @output_lines << "\n"
42
+ elsif mermaid_style_result?(@current_block_lang)
35
43
  # For mermaid, add the image tag directly without a result block
36
44
  @output_lines << "\n" if blank_line_before_new_result.nil?
37
45
  @output_lines << result_output
@@ -55,16 +63,17 @@ module ResultHelper
55
63
  end
56
64
 
57
65
  def handle_existing_ruby_result_block(current_line, file_enum)
58
- warn "Found existing '```ruby RESULT' block, passing through."
66
+ TestSilencer.warn_unless_testing("Found existing '```ruby RESULT' or '```ruby' result block, passing through.")
59
67
  @output_lines << current_line
60
68
  @state = :inside_result_block
61
69
  end
62
70
 
63
71
 
64
- def execute_and_add_result(blank_line_before_new_result)
65
- warn "Skipping empty code block for language '#{@current_block_lang}'." && return unless has_content?(@current_code_content)
72
+ def execute_and_add_result(blank_line_before_new_result, closing_line = nil)
73
+ TestSilencer.warn_unless_testing("Skipping empty code block for language '#{@current_block_lang}'.") && return unless has_content?(@current_code_content)
66
74
 
67
- @output_lines << blank_line_before_new_result if blank_line_before_new_result
75
+ # For ruby blocks, don't add blank line since we're replacing the code block inline, not adding a RESULT block
76
+ @output_lines << blank_line_before_new_result if blank_line_before_new_result && !ruby_style_result?(@current_block_lang)
68
77
 
69
78
  result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path, @current_block_explain, @current_block_flamegraph)
70
79
 
@@ -76,7 +85,12 @@ module ResultHelper
76
85
 
77
86
  # Add the result block only if result=true (default)
78
87
  if @current_block_result
79
- add_result_block(clean_result || result_after_dalibo, blank_line_before_new_result)
88
+ add_result_block(clean_result || result_after_dalibo, blank_line_before_new_result, closing_line)
89
+ elsif ruby_style_result?(@current_block_lang)
90
+ # For ruby blocks with result=false, output the original code content without xmpfilter results
91
+ @output_lines << @current_code_content
92
+ @output_lines << (closing_line || "```\n")
93
+ @output_lines << "\n"
80
94
  end
81
95
 
82
96
  # Always add Dalibo link if it exists, even when result=false
@@ -100,16 +114,22 @@ module ResultHelper
100
114
  end
101
115
  end
102
116
 
103
- def skip_and_pass_through_result(lines_to_pass_through, file_enum, decision = nil)
117
+ def skip_and_pass_through_result(lines_to_pass_through, file_enum, decision = nil, closing_line = nil)
104
118
  # Handle run=false case where there are no lines to pass through
105
119
  if lines_to_pass_through.empty?
106
- warn "Skipping execution due to run=false option."
120
+ TestSilencer.warn_unless_testing("Skipping execution due to run=false option.")
121
+ # For ruby blocks, we still need to output the code content and closing line
122
+ if ruby_style_result?(@current_block_lang)
123
+ @output_lines << @current_code_content
124
+ @output_lines << (closing_line || "```\n")
125
+ @output_lines << "\n"
126
+ end
107
127
  return
108
128
  end
109
129
 
110
130
  # Check if this is Dalibo content
111
131
  if decision && decision[:dalibo_content]
112
- warn "Found existing Dalibo link for current #{@current_block_lang} block, skipping execution."
132
+ TestSilencer.warn_unless_testing("Found existing Dalibo link for current #{@current_block_lang} block, skipping execution.")
113
133
  @output_lines.concat(lines_to_pass_through)
114
134
  # No additional consumption needed for Dalibo links
115
135
  return
@@ -117,19 +137,27 @@ module ResultHelper
117
137
 
118
138
  # Check if this is flamegraph content
119
139
  if decision && decision[:flamegraph_content]
120
- warn "Found existing flamegraph for current #{@current_block_lang} block, skipping execution."
140
+ TestSilencer.warn_unless_testing("Found existing flamegraph for current #{@current_block_lang} block, skipping execution.")
121
141
  @output_lines.concat(lines_to_pass_through)
122
142
  # No additional consumption needed for flamegraph links
123
143
  return
124
144
  end
125
145
 
126
146
  if mermaid_style_result?(@current_block_lang)
127
- warn "Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution."
147
+ TestSilencer.warn_unless_testing("Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution.")
128
148
  @output_lines.concat(lines_to_pass_through)
129
149
  # For mermaid, no additional consumption needed since it's just an image line
150
+ elsif ruby_style_result?(@current_block_lang)
151
+ # For ruby blocks with existing old-style RESULT blocks, discard them and output inline results
152
+ TestSilencer.warn_unless_testing("Found existing ruby result for current #{@current_block_lang} block, skipping execution.")
153
+ @output_lines << @current_code_content
154
+ @output_lines << (closing_line || "```\n")
155
+ @output_lines << "\n"
156
+ # Always consume and discard the old-style RESULT block
157
+ consume_and_discard_result_block(file_enum)
130
158
  else
131
- lang_specific_result_type = ruby_style_result?(@current_block_lang) ? "```ruby RESULT" : "```RESULT"
132
- warn "Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution."
159
+ lang_specific_result_type = "``` {result}"
160
+ TestSilencer.warn_unless_testing("Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution.")
133
161
  @output_lines.concat(lines_to_pass_through)
134
162
  consume_result_block_content(file_enum)
135
163
  end
@@ -141,6 +169,12 @@ module ResultHelper
141
169
  end
142
170
  end
143
171
 
172
+ def consume_and_discard_result_block(file_enum)
173
+ consume_block_lines(file_enum) do |line|
174
+ # Discard the line - don't add to output
175
+ end
176
+ end
177
+
144
178
  def consume_existing_result_block(file_enum, consumed_lines)
145
179
  return if mermaid_style_result?(@current_block_lang)
146
180
 
@@ -161,7 +195,7 @@ module ResultHelper
161
195
  break if is_block_end?(result_block_line)
162
196
  end
163
197
  rescue StopIteration
164
- warn "Warning: End of file reached while consuming result block."
198
+ TestSilencer.warn_unless_testing "Warning: End of file reached while consuming result block."
165
199
  end
166
200
  end
167
201
  end
@@ -0,0 +1,41 @@
1
+ # Module to provide test-aware warning functionality
2
+ module TestSilencer
3
+ # Helper method to detect if we're running in a test environment
4
+ def self.running_tests?
5
+ # Check for common test environment indicators
6
+ @running_tests ||= begin
7
+ caller.any? { |line| line.include?('/test/') || line.include?('minitest') || line.include?('rspec') } ||
8
+ defined?(Minitest) ||
9
+ ENV['RAILS_ENV'] == 'test' ||
10
+ ENV['RACK_ENV'] == 'test' ||
11
+ ($PROGRAM_NAME.include?('rake') && ARGV.include?('test')) ||
12
+ $PROGRAM_NAME.include?('rake_test_loader')
13
+ end
14
+ end
15
+
16
+ # Generic warn method that silences output during tests
17
+ def self.warn_unless_testing(message)
18
+ warn message unless running_tests?
19
+ end
20
+
21
+ # Silently abort during tests by raising SystemExit without printing the message
22
+ def self.abort_unless_testing(message)
23
+ if running_tests?
24
+ # During tests, raise SystemExit without printing the error message
25
+ raise SystemExit.new(1)
26
+ else
27
+ # In production, use normal abort which prints the message and exits
28
+ abort message
29
+ end
30
+ end
31
+
32
+ # Suppress Ruby warnings during tests
33
+ def self.setup_warning_suppression
34
+ if running_tests?
35
+ # Temporarily reduce verbosity during tests
36
+ original_verbose = $VERBOSE
37
+ $VERBOSE = nil
38
+ at_exit { $VERBOSE = original_verbose }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="1200" height="104" xmlns="http://www.w3.org/2000/svg">
3
+ <style>
4
+ .frame { stroke: white; stroke-width: 1; cursor: pointer; }
5
+ .frame:hover { stroke: black; stroke-width: 2; }
6
+ .frame-text { font-family: monospace; font-size: 20px; fill: white; pointer-events: none; }
7
+ .title { font-family: Arial; font-size: 16px; font-weight: bold; fill: #333; }
8
+ .subtitle { font-family: Arial; font-size: 12px; fill: #666; }
9
+ </style>
10
+
11
+ <!-- Title -->
12
+ <text x="600" y="20" class="title" text-anchor="middle">PostgreSQL Query Execution Plan Flamegraph</text>
13
+ <text x="600" y="35" class="subtitle" text-anchor="middle">Total Execution Time: 0.0ms</text>
14
+
15
+ <!-- Flamegraph -->
16
+ <g transform="translate(0, 45)">
17
+ <rect class="frame"
18
+ x="0.0"
19
+ y="0"
20
+ width="1200.0"
21
+ height="24"
22
+ fill="#95a5a6">
23
+ <title>Result [0.0ms]
24
+ Time: 0.0ms
25
+ Percentage: 100.0%</title>
26
+ </rect>
27
+ <text class="frame-text" x="4.0" y="21">Result [0.0ms]</text>
28
+ </g>
29
+ </svg>
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdown-run
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.12
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aurélien Bottazini
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-06-04 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rcodetools
@@ -80,6 +79,20 @@ dependencies:
80
79
  - - ">="
81
80
  - !ruby/object:Gem::Version
82
81
  version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: simplecov
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.22'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.22'
83
96
  description: Run code blocks in Markdown files for Ruby, JavaScript, sqlite, psql,
84
97
  bash, zsh, and mermaid. Insert execution results next to the original code blocks.
85
98
  Generate SVG diagrams from mermaid blocks.
@@ -107,13 +120,17 @@ files:
107
120
  - lib/flamegraph_helper.rb
108
121
  - lib/frontmatter_parser.rb
109
122
  - lib/language_configs.rb
123
+ - lib/language_resolver.rb
110
124
  - lib/markdown/run/version.rb
111
125
  - lib/markdown_file_writer.rb
112
126
  - lib/markdown_processor.rb
113
127
  - lib/markdown_run.rb
114
128
  - lib/pg_flamegraph_svg.rb
129
+ - lib/postgres_helper.rb
115
130
  - lib/result_helper.rb
131
+ - lib/test_silencer.rb
116
132
  - markdown-run-sample.md
133
+ - markdown-run-sample/markdown-run-sample-flamegraph-20250604-233020-29c2a34ec32b.svg
117
134
  homepage: https://github.com/aurelienbottazini/markdown-run
118
135
  licenses:
119
136
  - MIT
@@ -121,7 +138,6 @@ metadata:
121
138
  homepage_uri: https://github.com/aurelienbottazini/markdown-run
122
139
  source_code_uri: https://github.com/aurelienbottazini/markdown-run
123
140
  changelog_uri: https://github.com/aurelienbottazini/markdown-run/blob/main/CHANGELOG.md
124
- post_install_message:
125
141
  rdoc_options: []
126
142
  require_paths:
127
143
  - lib
@@ -136,8 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
152
  - !ruby/object:Gem::Version
137
153
  version: '0'
138
154
  requirements: []
139
- rubygems_version: 3.5.16
140
- signing_key:
155
+ rubygems_version: 3.6.9
141
156
  specification_version: 4
142
157
  summary: Run code blocks in Markdown files
143
158
  test_files: []