markdown-run 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -11
- data/README.md +91 -2
- data/lib/code_block_helper.rb +62 -0
- data/lib/code_block_parser.rb +29 -8
- data/lib/code_executor.rb +90 -15
- data/lib/dalibo_helper.rb +39 -0
- data/lib/execution_decider.rb +131 -3
- data/lib/frontmatter_parser.rb +48 -6
- data/lib/language_configs.rb +21 -11
- data/lib/markdown/run/version.rb +1 -1
- data/lib/markdown_processor.rb +9 -179
- data/lib/result_helper.rb +141 -0
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7e078472cf91253d16b3341865954ead4ce1d6539d509c5ed6d54302c5a3147d
|
4
|
+
data.tar.gz: 76f40572409ed01a1797c4902896b783d3a0403783031fca8c8dff04a1499acf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea3ecacb94cbe3e52dacb9fdb4ea447790990cee631fd890f5389d483157d7e16621eda6dba68dddf13d1e1f5f499b8bc2ac3952e2ca1752f133f5aa496403b1
|
7
|
+
data.tar.gz: 8ae64565dc336f5158a877c54a02520bc6a0e6923c6bbdd705c2cff8c848ff832f0a0683f30c2e86df883a356d89e60269cebb219d9a9732212fc5edfd27fea6
|
data/CHANGELOG.md
CHANGED
@@ -1,42 +1,53 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.1.11] - 2025-06-04
|
4
|
+
|
5
|
+
- options are customizable with the yaml frontmatter
|
6
|
+
|
7
|
+
## [0.1.10] - 2025-06-03
|
8
|
+
|
9
|
+
- standalone options for codeblocks (run instead of run=true)
|
10
|
+
- explain option for psql code blocks with Dalibo visualization links
|
11
|
+
- Fixed Dalibo URL generation to properly submit plans via HTTP POST
|
12
|
+
- Added result option to control result block visibility (result=false hides result blocks while still executing code)
|
13
|
+
|
3
14
|
## [0.1.9] - 2025-06-02
|
4
15
|
|
5
|
-
-
|
16
|
+
- mermaid codeblocks
|
6
17
|
|
7
18
|
## [0.1.8] - 2025-06-01
|
8
19
|
|
9
|
-
-
|
20
|
+
- Added run option
|
10
21
|
|
11
22
|
## [0.1.7] - 2025-06-01
|
12
23
|
|
13
|
-
-
|
24
|
+
- Added rerun functionality
|
14
25
|
|
15
26
|
## [0.1.6] - 2025-06-01
|
16
27
|
|
17
|
-
-
|
18
|
-
-
|
28
|
+
- Refactor code to state pattern
|
29
|
+
- Add yaml frontmatter to support aliases for code blocks
|
19
30
|
|
20
31
|
## [0.1.5] - 2025-05-19
|
21
32
|
|
22
|
-
-
|
33
|
+
- Remove gif files from release
|
23
34
|
|
24
35
|
## [0.1.4] - 2025-05-18
|
25
36
|
|
26
|
-
-
|
37
|
+
- Add support for zsh, bash, sh
|
27
38
|
|
28
39
|
## [0.1.3] - 2025-05-14
|
29
40
|
|
30
|
-
-
|
41
|
+
- Fix missing minitest dep
|
31
42
|
|
32
43
|
## [0.1.2] - 2025-05-14
|
33
44
|
|
34
|
-
-
|
45
|
+
- Gemfile update
|
35
46
|
|
36
47
|
## [0.1.1] - 2025-05-14
|
37
48
|
|
38
|
-
-
|
49
|
+
- Added checks for missing dependencies
|
39
50
|
|
40
51
|
## [0.1.0] - 2025-05-13
|
41
52
|
|
42
|
-
-
|
53
|
+
- Initial release
|
data/README.md
CHANGED
@@ -57,8 +57,21 @@ example vscode keybinding
|
|
57
57
|
|
58
58
|
- `run=true` or `run=false` to control whether a code block should be executed at all. `run=true` is the default if not specified
|
59
59
|
- `rerun=true` or `rerun=false` to control whether a code block should be re-executed if a result block already exists. `rerun=false` is the default if not specified
|
60
|
+
- `result=true` or `result=false` to control whether the result block should be displayed after execution. `result=true` is the default if not specified. When `result=false`, the code still executes but the result block is hidden
|
61
|
+
- `explain=true` or `explain=false` for psql code blocks to generate query execution plans with Dalibo visualization links. `explain=false` is the default if not specified
|
60
62
|
|
61
|
-
Options can be combined. If `run=false` is specified, the code block will not execute regardless of the `rerun` setting.
|
63
|
+
Options can be combined. If `run=false` is specified, the code block will not execute regardless of the `rerun` setting. The `result` option only affects display of the result block, not code execution.
|
64
|
+
|
65
|
+
### Standalone Option Syntax
|
66
|
+
|
67
|
+
Options can also be specified using standalone syntax without explicit `=true`:
|
68
|
+
|
69
|
+
- `run` is equivalent to `run=true`
|
70
|
+
- `rerun` is equivalent to `rerun=true`
|
71
|
+
- `result` is equivalent to `result=true`
|
72
|
+
- `explain` is equivalent to `explain=true`
|
73
|
+
|
74
|
+
Explicit assignments (e.g., `run=false`) take precedence over standalone options.
|
62
75
|
|
63
76
|
Examples:
|
64
77
|
|
@@ -66,7 +79,7 @@ Examples:
|
|
66
79
|
console.log("This will not execute at all");
|
67
80
|
```
|
68
81
|
|
69
|
-
```js rerun
|
82
|
+
```js rerun
|
70
83
|
console.log("This will re-execute even if result exists");
|
71
84
|
```
|
72
85
|
|
@@ -74,6 +87,23 @@ console.log("This will re-execute even if result exists");
|
|
74
87
|
console.log("This will execute only if no result exists");
|
75
88
|
```
|
76
89
|
|
90
|
+
```ruby result=false run
|
91
|
+
puts "This executes but the result block is hidden"
|
92
|
+
```
|
93
|
+
|
94
|
+
```psql explain
|
95
|
+
SELECT * FROM users WHERE id = 1;
|
96
|
+
```
|
97
|
+
|
98
|
+
```psql explain=true
|
99
|
+
EXPLAIN (ANALYZE) SELECT * FROM large_table;
|
100
|
+
```
|
101
|
+
|
102
|
+
```psql result=false explain
|
103
|
+
SELECT * FROM large_table;
|
104
|
+
-- This will execute the explain query and show the Dalibo link but hide the result block
|
105
|
+
```
|
106
|
+
|
77
107
|
### Mermaid diagrams
|
78
108
|
|
79
109
|
Mermaid blocks generate SVG files and insert markdown image tags:
|
@@ -105,6 +135,65 @@ markdown-run:
|
|
105
135
|
- sql: psql
|
106
136
|
```
|
107
137
|
|
138
|
+
### Setting Defaults
|
139
|
+
|
140
|
+
You can override the default behavior for code block options using frontmatter:
|
141
|
+
|
142
|
+
```yaml
|
143
|
+
markdown-run:
|
144
|
+
defaults:
|
145
|
+
rerun: true
|
146
|
+
result: false
|
147
|
+
psql:
|
148
|
+
explain: true
|
149
|
+
ruby:
|
150
|
+
rerun: false
|
151
|
+
```
|
152
|
+
|
153
|
+
**Priority order (highest to lowest):**
|
154
|
+
|
155
|
+
1. Explicit options in code blocks (e.g., `rerun=true`)
|
156
|
+
2. Language-specific defaults (e.g., `psql: { explain: true }`)
|
157
|
+
3. Global defaults (e.g., `defaults: { rerun: true }`)
|
158
|
+
4. Built-in application defaults
|
159
|
+
|
160
|
+
**Available options for defaults:**
|
161
|
+
|
162
|
+
- `run`: Control whether code blocks execute (default: `true`)
|
163
|
+
- `rerun`: Control whether to re-execute if result exists (default: `false`)
|
164
|
+
- `result`: Control whether to show result blocks (default: `true`)
|
165
|
+
- `explain`: For psql blocks, generate explain plans (default: `false`)
|
166
|
+
|
167
|
+
**Examples:**
|
168
|
+
|
169
|
+
Make all code blocks rerun by default:
|
170
|
+
|
171
|
+
```yaml
|
172
|
+
markdown-run:
|
173
|
+
defaults:
|
174
|
+
rerun: true
|
175
|
+
```
|
176
|
+
|
177
|
+
Hide result blocks by default but enable explain for psql:
|
178
|
+
|
179
|
+
```yaml
|
180
|
+
markdown-run:
|
181
|
+
defaults:
|
182
|
+
result: false
|
183
|
+
psql:
|
184
|
+
explain: true
|
185
|
+
```
|
186
|
+
|
187
|
+
Language-specific settings override global defaults:
|
188
|
+
|
189
|
+
```yaml
|
190
|
+
markdown-run:
|
191
|
+
defaults:
|
192
|
+
rerun: false # Global default
|
193
|
+
ruby:
|
194
|
+
rerun: true # Ruby blocks will rerun, others won't
|
195
|
+
```
|
196
|
+
|
108
197
|
## Demo
|
109
198
|
|
110
199
|

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