markdown-run 0.1.12 → 0.2.1
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 +4 -4
- data/.claude/settings.local.json +19 -0
- data/.tool-versions +1 -1
- data/CHANGELOG.md +8 -0
- data/README.md +20 -10
- data/Rakefile +54 -0
- data/lib/code_block_helper.rb +12 -6
- data/lib/code_block_parser.rb +6 -4
- data/lib/code_executor.rb +26 -17
- data/lib/execution_decider.rb +25 -1
- data/lib/frontmatter_parser.rb +14 -6
- data/lib/language_configs.rb +28 -20
- data/lib/language_resolver.rb +17 -0
- data/lib/markdown/run/version.rb +1 -1
- data/lib/markdown_file_writer.rb +5 -2
- data/lib/markdown_processor.rb +5 -5
- data/lib/postgres_helper.rb +94 -0
- data/lib/result_helper.rb +50 -17
- data/lib/test_silencer.rb +41 -0
- data/markdown-run-sample/markdown-run-sample-flamegraph-20250604-233020-29c2a34ec32b.svg +29 -0
- data/markdown-run-sample.md +8 -51
- metadata +22 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9118fa7e0d2838fe0201d314199cd8df7c1be394a962c0f15971a4cf8489875b
|
4
|
+
data.tar.gz: 8cb4c27814f03fb2afaba2b52c148df7c917e7764ed6de82ab8d9c63744c55f5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a8221dae4563d208936a0b0f3783f40ec83a08ff5daf49159106af90f38130790031e692ecdea812685ee51bd17c67639d8c584a2c28e2ab5975359fdfea94db
|
7
|
+
data.tar.gz: eb2874f8cb94acc7cee3a735119bfe85163e2f378aea120353597393566bf4038100f71e3fec0bd93f08c09bac85db79299c398b8e713286fe4ff3853546638b
|
@@ -0,0 +1,19 @@
|
|
1
|
+
{
|
2
|
+
"permissions": {
|
3
|
+
"allow": [
|
4
|
+
"Bash(ruby -e:*)",
|
5
|
+
"Bash(ruby -I lib -r markdown_run -e:*)",
|
6
|
+
"Bash(flog:*)",
|
7
|
+
"Bash(rake test:*)",
|
8
|
+
"Bash(bin/markdown-run:*)",
|
9
|
+
"Bash(exe/markdown-run:*)",
|
10
|
+
"Bash(cat:*)",
|
11
|
+
"Bash(for i in {1..3})",
|
12
|
+
"Bash(do echo \"Run $i:\" exe/markdown-run markdown-run-sample.md)",
|
13
|
+
"Bash(gem list:*)",
|
14
|
+
"Bash(rake:*)"
|
15
|
+
],
|
16
|
+
"deny": [],
|
17
|
+
"ask": []
|
18
|
+
}
|
19
|
+
}
|
data/.tool-versions
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby 3.
|
1
|
+
ruby 3.4.6
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.2.1]
|
4
|
+
|
5
|
+
- fix extra blank line with ruby blocks
|
6
|
+
|
7
|
+
## [0.2.0]
|
8
|
+
|
9
|
+
- Refactor block options to be proper markdown
|
10
|
+
|
3
11
|
## [0.1.12] - 2025-06-04
|
4
12
|
|
5
13
|
- Added `flamegraph` option for psql code blocks to generate PostgreSQL query execution plan flamegraphs as SVG images
|
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
|
data/lib/code_block_helper.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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]
|
data/lib/code_block_parser.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
require_relative "language_configs"
|
2
2
|
|
3
3
|
class CodeBlockParser
|
4
|
-
# Code block header pattern: ```language options
|
5
|
-
|
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
|
-
@
|
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
|
-
|
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
|
-
|
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(
|
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(
|
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
|
-
|
108
|
-
|
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
|
-
|
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
|
-
|
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
|
""
|
@@ -225,7 +234,7 @@ class CodeExecutor
|
|
225
234
|
relative_path = File.basename(output_path)
|
226
235
|
end
|
227
236
|
|
228
|
-
|
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
|
-
|
251
|
+
TestSilencer.warn_unless_testing "Error parsing EXPLAIN JSON: #{e.message}"
|
243
252
|
result_output
|
244
253
|
rescue => e
|
245
|
-
|
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
|
-
|
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
|
-
|
295
|
+
TestSilencer.warn_unless_testing "Error submitting plan to Dalibo: #{e.message}"
|
287
296
|
nil
|
288
297
|
end
|
289
298
|
end
|
data/lib/execution_decider.rb
CHANGED
@@ -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
|
data/lib/frontmatter_parser.rb
CHANGED
@@ -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
|
-
@
|
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 :
|
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
|
-
@
|
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
|
-
|
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
|
-
|
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
|
data/lib/language_configs.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
require
|
1
|
+
require "securerandom"
|
2
|
+
require_relative "test_silencer"
|
3
|
+
require_relative "postgres_helper"
|
2
4
|
|
3
5
|
JS_CONFIG = {
|
4
|
-
command:
|
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:
|
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:
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
[ "
|
45
|
+
[ "#{psql_cmd} -A -t -X", { stdin_data: explained_query } ]
|
38
46
|
else
|
39
|
-
[ "
|
47
|
+
[ "#{psql_cmd} -A -t -X", { stdin_data: code_content } ]
|
40
48
|
end
|
41
49
|
}
|
42
50
|
},
|
43
51
|
"ruby" => {
|
44
|
-
command:
|
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
|
-
|
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:
|
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
|
-
|
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:
|
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
|
-
|
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:
|
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
|
-
|
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:
|
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
|
-
|
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
|
data/lib/markdown/run/version.rb
CHANGED
data/lib/markdown_file_writer.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
data/lib/markdown_processor.rb
CHANGED
@@ -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
|
-
@
|
24
|
-
@
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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,21 @@ 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
|
-
|
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
|
+
end
|
107
126
|
return
|
108
127
|
end
|
109
128
|
|
110
129
|
# Check if this is Dalibo content
|
111
130
|
if decision && decision[:dalibo_content]
|
112
|
-
|
131
|
+
TestSilencer.warn_unless_testing("Found existing Dalibo link for current #{@current_block_lang} block, skipping execution.")
|
113
132
|
@output_lines.concat(lines_to_pass_through)
|
114
133
|
# No additional consumption needed for Dalibo links
|
115
134
|
return
|
@@ -117,19 +136,27 @@ module ResultHelper
|
|
117
136
|
|
118
137
|
# Check if this is flamegraph content
|
119
138
|
if decision && decision[:flamegraph_content]
|
120
|
-
|
139
|
+
TestSilencer.warn_unless_testing("Found existing flamegraph for current #{@current_block_lang} block, skipping execution.")
|
121
140
|
@output_lines.concat(lines_to_pass_through)
|
122
141
|
# No additional consumption needed for flamegraph links
|
123
142
|
return
|
124
143
|
end
|
125
144
|
|
126
145
|
if mermaid_style_result?(@current_block_lang)
|
127
|
-
|
146
|
+
TestSilencer.warn_unless_testing("Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution.")
|
128
147
|
@output_lines.concat(lines_to_pass_through)
|
129
148
|
# For mermaid, no additional consumption needed since it's just an image line
|
149
|
+
elsif ruby_style_result?(@current_block_lang)
|
150
|
+
# For ruby blocks with existing old-style RESULT blocks, discard them and output inline results
|
151
|
+
TestSilencer.warn_unless_testing("Found existing ruby result for current #{@current_block_lang} block, skipping execution.")
|
152
|
+
@output_lines << @current_code_content
|
153
|
+
@output_lines << (closing_line || "```\n")
|
154
|
+
@output_lines << "\n"
|
155
|
+
# Always consume and discard the old-style RESULT block
|
156
|
+
consume_and_discard_result_block(file_enum)
|
130
157
|
else
|
131
|
-
lang_specific_result_type =
|
132
|
-
|
158
|
+
lang_specific_result_type = "``` {result}"
|
159
|
+
TestSilencer.warn_unless_testing("Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution.")
|
133
160
|
@output_lines.concat(lines_to_pass_through)
|
134
161
|
consume_result_block_content(file_enum)
|
135
162
|
end
|
@@ -141,6 +168,12 @@ module ResultHelper
|
|
141
168
|
end
|
142
169
|
end
|
143
170
|
|
171
|
+
def consume_and_discard_result_block(file_enum)
|
172
|
+
consume_block_lines(file_enum) do |line|
|
173
|
+
# Discard the line - don't add to output
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
144
177
|
def consume_existing_result_block(file_enum, consumed_lines)
|
145
178
|
return if mermaid_style_result?(@current_block_lang)
|
146
179
|
|
@@ -161,7 +194,7 @@ module ResultHelper
|
|
161
194
|
break if is_block_end?(result_block_line)
|
162
195
|
end
|
163
196
|
rescue StopIteration
|
164
|
-
|
197
|
+
TestSilencer.warn_unless_testing "Warning: End of file reached while consuming result block."
|
165
198
|
end
|
166
199
|
end
|
167
200
|
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>
|
data/markdown-run-sample.md
CHANGED
@@ -5,35 +5,32 @@ And the result should be shown in a following codeblock
|
|
5
5
|
|
6
6
|
```ruby
|
7
7
|
p "foo"
|
8
|
-
```
|
9
|
-
|
10
|
-
```ruby RESULT
|
11
|
-
p "foo"
|
12
8
|
# >> "foo"
|
13
9
|
```
|
14
10
|
|
11
|
+
|
15
12
|
```bash
|
16
13
|
date
|
17
14
|
```
|
18
15
|
|
19
|
-
```
|
20
|
-
|
16
|
+
``` {result}
|
17
|
+
Fri Oct 17 02:39:33 PM CEST 2025
|
21
18
|
```
|
22
19
|
|
23
20
|
```zsh
|
24
21
|
date
|
25
22
|
```
|
26
23
|
|
27
|
-
```
|
28
|
-
|
24
|
+
``` {result}
|
25
|
+
Fri Oct 17 02:39:33 PM CEST 2025
|
29
26
|
```
|
30
27
|
|
31
28
|
```sh
|
32
29
|
date
|
33
30
|
```
|
34
31
|
|
35
|
-
```
|
36
|
-
|
32
|
+
``` {result}
|
33
|
+
Fri Oct 17 02:39:33 PM CEST 2025
|
37
34
|
```
|
38
35
|
|
39
36
|
```js
|
@@ -41,47 +38,7 @@ Sun May 18 15:07:05 CEST 2025
|
|
41
38
|
console.log(3);
|
42
39
|
```
|
43
40
|
|
44
|
-
```
|
41
|
+
``` {result}
|
45
42
|
3
|
46
43
|
```
|
47
44
|
|
48
|
-
```psql
|
49
|
-
\d
|
50
|
-
```
|
51
|
-
|
52
|
-
```RESULT
|
53
|
-
public|ar_internal_metadata|table|aurelienbottazini
|
54
|
-
public|schema_migrations|table|aurelienbottazini
|
55
|
-
```
|
56
|
-
|
57
|
-
```sqlite3
|
58
|
-
.stats
|
59
|
-
```
|
60
|
-
|
61
|
-
```RESULT
|
62
|
-
Memory Used: 147824 (max 147888) bytes
|
63
|
-
Number of Outstanding Allocations: 169 (max 170)
|
64
|
-
Number of Pcache Overflow Bytes: 4608 (max 4608) bytes
|
65
|
-
Largest Allocation: 122400 bytes
|
66
|
-
Largest Pcache Allocation: 4104 bytes
|
67
|
-
Lookaside Slots Used: 34 (max 34)
|
68
|
-
Successful lookaside attempts: 34
|
69
|
-
Lookaside failures due to size: 0
|
70
|
-
Lookaside failures due to OOM: 0
|
71
|
-
Pager Heap Usage: 5632 bytes
|
72
|
-
Page cache hits: 0
|
73
|
-
Page cache misses: 0
|
74
|
-
Page cache writes: 0
|
75
|
-
Page cache spills: 0
|
76
|
-
Schema Heap Usage: 0 bytes
|
77
|
-
Statement Heap/Lookaside Usage: 0 bytes
|
78
|
-
```
|
79
|
-
|
80
|
-
```psql rerun flamegraph
|
81
|
-
select 42 as answer;
|
82
|
-
```
|
83
|
-
|
84
|
-
```RESULT
|
85
|
-

|
86
|
-
```
|
87
|
-
|
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
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aurélien Bottazini
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
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.
|
@@ -90,6 +103,7 @@ executables:
|
|
90
103
|
extensions: []
|
91
104
|
extra_rdoc_files: []
|
92
105
|
files:
|
106
|
+
- ".claude/settings.local.json"
|
93
107
|
- ".rubocop.yml"
|
94
108
|
- ".tool-versions"
|
95
109
|
- CHANGELOG.md
|
@@ -107,13 +121,17 @@ files:
|
|
107
121
|
- lib/flamegraph_helper.rb
|
108
122
|
- lib/frontmatter_parser.rb
|
109
123
|
- lib/language_configs.rb
|
124
|
+
- lib/language_resolver.rb
|
110
125
|
- lib/markdown/run/version.rb
|
111
126
|
- lib/markdown_file_writer.rb
|
112
127
|
- lib/markdown_processor.rb
|
113
128
|
- lib/markdown_run.rb
|
114
129
|
- lib/pg_flamegraph_svg.rb
|
130
|
+
- lib/postgres_helper.rb
|
115
131
|
- lib/result_helper.rb
|
132
|
+
- lib/test_silencer.rb
|
116
133
|
- markdown-run-sample.md
|
134
|
+
- markdown-run-sample/markdown-run-sample-flamegraph-20250604-233020-29c2a34ec32b.svg
|
117
135
|
homepage: https://github.com/aurelienbottazini/markdown-run
|
118
136
|
licenses:
|
119
137
|
- MIT
|
@@ -121,7 +139,6 @@ metadata:
|
|
121
139
|
homepage_uri: https://github.com/aurelienbottazini/markdown-run
|
122
140
|
source_code_uri: https://github.com/aurelienbottazini/markdown-run
|
123
141
|
changelog_uri: https://github.com/aurelienbottazini/markdown-run/blob/main/CHANGELOG.md
|
124
|
-
post_install_message:
|
125
142
|
rdoc_options: []
|
126
143
|
require_paths:
|
127
144
|
- lib
|
@@ -136,8 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
153
|
- !ruby/object:Gem::Version
|
137
154
|
version: '0'
|
138
155
|
requirements: []
|
139
|
-
rubygems_version: 3.
|
140
|
-
signing_key:
|
156
|
+
rubygems_version: 3.6.9
|
141
157
|
specification_version: 4
|
142
158
|
summary: Run code blocks in Markdown files
|
143
159
|
test_files: []
|