markdown-run 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -15
- data/README.md +59 -0
- data/lib/code_block_helper.rb +62 -0
- data/lib/code_block_parser.rb +12 -8
- data/lib/code_executor.rb +7 -7
- data/lib/dalibo_helper.rb +39 -0
- data/lib/frontmatter_parser.rb +48 -6
- data/lib/markdown/run/version.rb +1 -1
- data/lib/markdown_processor.rb +9 -259
- 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,49 +1,53 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.1.11] - 2025-06-04
|
4
|
+
|
5
|
+
- options are customizable with the yaml frontmatter
|
6
|
+
|
3
7
|
## [0.1.10] - 2025-06-03
|
4
8
|
|
5
|
-
-
|
6
|
-
-
|
7
|
-
-
|
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)
|
9
13
|
|
10
14
|
## [0.1.9] - 2025-06-02
|
11
15
|
|
12
|
-
-
|
16
|
+
- mermaid codeblocks
|
13
17
|
|
14
18
|
## [0.1.8] - 2025-06-01
|
15
19
|
|
16
|
-
-
|
20
|
+
- Added run option
|
17
21
|
|
18
22
|
## [0.1.7] - 2025-06-01
|
19
23
|
|
20
|
-
-
|
24
|
+
- Added rerun functionality
|
21
25
|
|
22
26
|
## [0.1.6] - 2025-06-01
|
23
27
|
|
24
|
-
-
|
25
|
-
-
|
28
|
+
- Refactor code to state pattern
|
29
|
+
- Add yaml frontmatter to support aliases for code blocks
|
26
30
|
|
27
31
|
## [0.1.5] - 2025-05-19
|
28
32
|
|
29
|
-
-
|
33
|
+
- Remove gif files from release
|
30
34
|
|
31
35
|
## [0.1.4] - 2025-05-18
|
32
36
|
|
33
|
-
-
|
37
|
+
- Add support for zsh, bash, sh
|
34
38
|
|
35
39
|
## [0.1.3] - 2025-05-14
|
36
40
|
|
37
|
-
-
|
41
|
+
- Fix missing minitest dep
|
38
42
|
|
39
43
|
## [0.1.2] - 2025-05-14
|
40
44
|
|
41
|
-
-
|
45
|
+
- Gemfile update
|
42
46
|
|
43
47
|
## [0.1.1] - 2025-05-14
|
44
48
|
|
45
|
-
-
|
49
|
+
- Added checks for missing dependencies
|
46
50
|
|
47
51
|
## [0.1.0] - 2025-05-13
|
48
52
|
|
49
|
-
-
|
53
|
+
- Initial release
|
data/README.md
CHANGED
@@ -135,6 +135,65 @@ markdown-run:
|
|
135
135
|
- sql: psql
|
136
136
|
```
|
137
137
|
|
138
|
+
### Setting Defaults
|
139
|
+
|
140
|
+
You can override the default behavior for code block options using frontmatter:
|
141
|
+
|
142
|
+
```yaml
|
143
|
+
markdown-run:
|
144
|
+
defaults:
|
145
|
+
rerun: true
|
146
|
+
result: false
|
147
|
+
psql:
|
148
|
+
explain: true
|
149
|
+
ruby:
|
150
|
+
rerun: false
|
151
|
+
```
|
152
|
+
|
153
|
+
**Priority order (highest to lowest):**
|
154
|
+
|
155
|
+
1. Explicit options in code blocks (e.g., `rerun=true`)
|
156
|
+
2. Language-specific defaults (e.g., `psql: { explain: true }`)
|
157
|
+
3. Global defaults (e.g., `defaults: { rerun: true }`)
|
158
|
+
4. Built-in application defaults
|
159
|
+
|
160
|
+
**Available options for defaults:**
|
161
|
+
|
162
|
+
- `run`: Control whether code blocks execute (default: `true`)
|
163
|
+
- `rerun`: Control whether to re-execute if result exists (default: `false`)
|
164
|
+
- `result`: Control whether to show result blocks (default: `true`)
|
165
|
+
- `explain`: For psql blocks, generate explain plans (default: `false`)
|
166
|
+
|
167
|
+
**Examples:**
|
168
|
+
|
169
|
+
Make all code blocks rerun by default:
|
170
|
+
|
171
|
+
```yaml
|
172
|
+
markdown-run:
|
173
|
+
defaults:
|
174
|
+
rerun: true
|
175
|
+
```
|
176
|
+
|
177
|
+
Hide result blocks by default but enable explain for psql:
|
178
|
+
|
179
|
+
```yaml
|
180
|
+
markdown-run:
|
181
|
+
defaults:
|
182
|
+
result: false
|
183
|
+
psql:
|
184
|
+
explain: true
|
185
|
+
```
|
186
|
+
|
187
|
+
Language-specific settings override global defaults:
|
188
|
+
|
189
|
+
```yaml
|
190
|
+
markdown-run:
|
191
|
+
defaults:
|
192
|
+
rerun: false # Global default
|
193
|
+
ruby:
|
194
|
+
rerun: true # Ruby blocks will rerun, others won't
|
195
|
+
```
|
196
|
+
|
138
197
|
## Demo
|
139
198
|
|
140
199
|

|
@@ -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,20 +34,24 @@ class CodeBlockParser
|
|
34
34
|
line.strip == BLOCK_END_PATTERN
|
35
35
|
end
|
36
36
|
|
37
|
-
def parse_run_option(options_string)
|
38
|
-
|
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)
|
43
45
|
end
|
44
46
|
|
45
|
-
def parse_explain_option(options_string)
|
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)
|
47
50
|
end
|
48
51
|
|
49
|
-
def parse_result_option(options_string)
|
50
|
-
|
52
|
+
def parse_result_option(options_string, language = nil)
|
53
|
+
default_value = @frontmatter_parser.get_default_value("result", language, true)
|
54
|
+
parse_boolean_option(options_string, "result", default_value)
|
51
55
|
end
|
52
56
|
|
53
57
|
private
|
data/lib/code_executor.rb
CHANGED
@@ -21,6 +21,12 @@ class CodeExecutor
|
|
21
21
|
|
22
22
|
private
|
23
23
|
|
24
|
+
|
25
|
+
|
26
|
+
def stderr_has_content?(stderr_output)
|
27
|
+
stderr_output && !stderr_output.strip.empty?
|
28
|
+
end
|
29
|
+
|
24
30
|
def handle_unsupported_language(lang)
|
25
31
|
warn "Unsupported language: #{lang}"
|
26
32
|
"ERROR: Unsupported language: #{lang}"
|
@@ -167,31 +173,25 @@ class CodeExecutor
|
|
167
173
|
|
168
174
|
def submit_plan_to_dalibo(plan_json)
|
169
175
|
begin
|
170
|
-
# Start with HTTPS directly to avoid the HTTP->HTTPS redirect
|
171
176
|
uri = URI('https://explain.dalibo.com/new')
|
172
177
|
http = Net::HTTP.new(uri.host, uri.port)
|
173
178
|
http.use_ssl = true
|
174
|
-
http.read_timeout = 10
|
179
|
+
http.read_timeout = 10
|
175
180
|
|
176
|
-
# Prepare the JSON payload
|
177
181
|
payload = {
|
178
182
|
'plan' => plan_json,
|
179
183
|
'title' => "Query Plan - #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}",
|
180
184
|
'query' => ''
|
181
185
|
}
|
182
186
|
|
183
|
-
# Create the POST request
|
184
187
|
request = Net::HTTP::Post.new(uri)
|
185
188
|
request['Content-Type'] = 'application/json'
|
186
189
|
request.body = JSON.generate(payload)
|
187
190
|
|
188
|
-
# Send the request and follow redirects to get the final URL
|
189
191
|
response = http.request(request)
|
190
192
|
|
191
|
-
# Dalibo returns a redirect to the plan URL
|
192
193
|
if response.is_a?(Net::HTTPRedirection)
|
193
194
|
location = response['location']
|
194
|
-
# Make sure it's a full URL
|
195
195
|
if location
|
196
196
|
if location.start_with?('/')
|
197
197
|
location = "https://explain.dalibo.com#{location}"
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module DaliboHelper
|
2
|
+
private
|
3
|
+
|
4
|
+
DALIBO_LINK_PREFIX = "DALIBO_LINK:"
|
5
|
+
|
6
|
+
def extract_dalibo_link(result_output)
|
7
|
+
# Check if the result contains a Dalibo link marker
|
8
|
+
if result_output.start_with?(DALIBO_LINK_PREFIX)
|
9
|
+
lines = result_output.split("\n", 2)
|
10
|
+
dalibo_url = lines[0].sub(DALIBO_LINK_PREFIX, "")
|
11
|
+
clean_result = lines[1] || ""
|
12
|
+
dalibo_link = "**Dalibo Visualization:** [View Query Plan](#{dalibo_url})"
|
13
|
+
[dalibo_link, clean_result]
|
14
|
+
else
|
15
|
+
[nil, result_output]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def consume_dalibo_link_if_present(file_enum, consumed_lines)
|
20
|
+
# Look ahead to see if there are Dalibo links after the result block
|
21
|
+
begin
|
22
|
+
# Keep consuming blank lines and Dalibo links until we hit something else
|
23
|
+
loop do
|
24
|
+
next_line = peek_next_line(file_enum)
|
25
|
+
|
26
|
+
if is_blank_line?(next_line)
|
27
|
+
consumed_lines << file_enum.next
|
28
|
+
elsif next_line&.start_with?("**Dalibo Visualization:**")
|
29
|
+
consumed_lines << file_enum.next
|
30
|
+
else
|
31
|
+
# Hit something that's not a blank line or Dalibo link, stop consuming
|
32
|
+
break
|
33
|
+
end
|
34
|
+
end
|
35
|
+
rescue StopIteration
|
36
|
+
# End of file reached, nothing more to consume
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
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/markdown/run/version.rb
CHANGED
data/lib/markdown_processor.rb
CHANGED
@@ -4,20 +4,22 @@ require_relative "code_block_parser"
|
|
4
4
|
require_relative "code_executor"
|
5
5
|
require_relative "execution_decider"
|
6
6
|
require_relative "enum_helper"
|
7
|
+
require_relative "dalibo_helper"
|
8
|
+
require_relative "code_block_helper"
|
9
|
+
require_relative "result_helper"
|
7
10
|
|
8
11
|
class MarkdownProcessor
|
9
12
|
include EnumHelper
|
13
|
+
include DaliboHelper
|
14
|
+
include CodeBlockHelper
|
15
|
+
include ResultHelper
|
16
|
+
|
10
17
|
def initialize(temp_dir, input_file_path = nil)
|
11
18
|
@temp_dir = temp_dir
|
12
19
|
@input_file_path = input_file_path
|
13
20
|
@output_lines = []
|
14
|
-
|
15
|
-
|
16
|
-
@current_code_content = ""
|
17
|
-
@current_block_rerun = false
|
18
|
-
@current_block_run = true
|
19
|
-
@current_block_explain = false
|
20
|
-
@current_block_result = true
|
21
|
+
reset_code_block_state
|
22
|
+
|
21
23
|
@frontmatter_parser = FrontmatterParser.new
|
22
24
|
@code_block_parser = CodeBlockParser.new(@frontmatter_parser)
|
23
25
|
end
|
@@ -40,31 +42,6 @@ class MarkdownProcessor
|
|
40
42
|
@frontmatter_parser.resolve_language(lang)
|
41
43
|
end
|
42
44
|
|
43
|
-
def ruby_style_result?(lang)
|
44
|
-
lang_config = SUPPORTED_LANGUAGES[lang]
|
45
|
-
lang_config && lang_config[:result_block_type] == "ruby"
|
46
|
-
end
|
47
|
-
|
48
|
-
def mermaid_style_result?(lang)
|
49
|
-
lang_config = SUPPORTED_LANGUAGES[lang]
|
50
|
-
lang_config && lang_config[:result_handling] == :mermaid_svg
|
51
|
-
end
|
52
|
-
|
53
|
-
def result_block_header(lang)
|
54
|
-
ruby_style_result?(lang) ? "```ruby RESULT\n" : "```RESULT\n"
|
55
|
-
end
|
56
|
-
|
57
|
-
def result_block_regex(lang)
|
58
|
-
if mermaid_style_result?(lang)
|
59
|
-
# For mermaid, look for existing image tags with .svg extension
|
60
|
-
/^!\[.*\]\(.*\.svg\)$/i
|
61
|
-
elsif ruby_style_result?(lang)
|
62
|
-
/^```ruby\s+RESULT$/i
|
63
|
-
else
|
64
|
-
/^```RESULT$/i
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
45
|
def is_block_end?(line)
|
69
46
|
@code_block_parser.is_block_end?(line)
|
70
47
|
end
|
@@ -73,22 +50,6 @@ class MarkdownProcessor
|
|
73
50
|
!content.strip.empty?
|
74
51
|
end
|
75
52
|
|
76
|
-
def add_result_block(result_output, blank_line_before_new_result)
|
77
|
-
if mermaid_style_result?(@current_block_lang)
|
78
|
-
# For mermaid, add the image tag directly without a result block
|
79
|
-
@output_lines << "\n" if blank_line_before_new_result.nil?
|
80
|
-
@output_lines << result_output
|
81
|
-
@output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
|
82
|
-
@output_lines << "\n"
|
83
|
-
else
|
84
|
-
@output_lines << "\n" if blank_line_before_new_result.nil?
|
85
|
-
@output_lines << result_block_header(@current_block_lang)
|
86
|
-
@output_lines << result_output
|
87
|
-
@output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
|
88
|
-
@output_lines << "```\n\n"
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
53
|
def line_matches_pattern?(line, pattern)
|
93
54
|
line && line.match?(pattern)
|
94
55
|
end
|
@@ -97,22 +58,6 @@ class MarkdownProcessor
|
|
97
58
|
line && line.strip == ""
|
98
59
|
end
|
99
60
|
|
100
|
-
def parse_rerun_option(options_string)
|
101
|
-
@code_block_parser.parse_rerun_option(options_string)
|
102
|
-
end
|
103
|
-
|
104
|
-
def parse_run_option(options_string)
|
105
|
-
@code_block_parser.parse_run_option(options_string)
|
106
|
-
end
|
107
|
-
|
108
|
-
def parse_explain_option(options_string)
|
109
|
-
@code_block_parser.parse_explain_option(options_string)
|
110
|
-
end
|
111
|
-
|
112
|
-
def parse_result_option(options_string)
|
113
|
-
@code_block_parser.parse_result_option(options_string)
|
114
|
-
end
|
115
|
-
|
116
61
|
def handle_line(current_line, file_enum)
|
117
62
|
case @state
|
118
63
|
when :outside_code_block
|
@@ -144,199 +89,4 @@ class MarkdownProcessor
|
|
144
89
|
accumulate_code_content(current_line)
|
145
90
|
end
|
146
91
|
end
|
147
|
-
|
148
|
-
def handle_inside_result_block(current_line, file_enum)
|
149
|
-
@output_lines << current_line
|
150
|
-
if is_block_end?(current_line)
|
151
|
-
@state = :outside_code_block
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
def handle_existing_ruby_result_block(current_line, file_enum)
|
156
|
-
warn "Found existing '```ruby RESULT' block, passing through."
|
157
|
-
@output_lines << current_line
|
158
|
-
@state = :inside_result_block
|
159
|
-
end
|
160
|
-
|
161
|
-
def start_code_block(current_line, lang, options_string = nil)
|
162
|
-
@output_lines << current_line
|
163
|
-
@current_block_lang = resolve_language(lang)
|
164
|
-
@current_block_rerun = parse_rerun_option(options_string)
|
165
|
-
@current_block_run = parse_run_option(options_string)
|
166
|
-
@current_block_explain = parse_explain_option(options_string)
|
167
|
-
@current_block_result = parse_result_option(options_string)
|
168
|
-
@state = :inside_code_block
|
169
|
-
@current_code_content = ""
|
170
|
-
end
|
171
|
-
|
172
|
-
def accumulate_code_content(current_line)
|
173
|
-
@current_code_content += current_line
|
174
|
-
@output_lines << current_line
|
175
|
-
end
|
176
|
-
|
177
|
-
def end_code_block(current_line, file_enum)
|
178
|
-
@output_lines << current_line
|
179
|
-
|
180
|
-
decision = decide_execution(file_enum)
|
181
|
-
|
182
|
-
if decision[:execute]
|
183
|
-
# If we consumed lines for rerun, don't add them to output (they'll be replaced)
|
184
|
-
execute_and_add_result(decision[:blank_line])
|
185
|
-
else
|
186
|
-
skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision)
|
187
|
-
end
|
188
|
-
|
189
|
-
reset_code_block_state
|
190
|
-
end
|
191
|
-
|
192
|
-
def decide_execution(file_enum)
|
193
|
-
decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_result)
|
194
|
-
decision = decider.decide(file_enum, method(:result_block_regex))
|
195
|
-
|
196
|
-
# Handle the consume_existing flag for rerun scenarios
|
197
|
-
if decision[:consume_existing]
|
198
|
-
consume_existing_result_block(file_enum, decision[:consumed_lines])
|
199
|
-
elsif decision[:consume_existing_dalibo]
|
200
|
-
# Dalibo links are already consumed in the decision process
|
201
|
-
# Just acknowledge they were consumed
|
202
|
-
end
|
203
|
-
|
204
|
-
decision
|
205
|
-
end
|
206
|
-
|
207
|
-
def execute_and_add_result(blank_line_before_new_result)
|
208
|
-
@output_lines << blank_line_before_new_result if blank_line_before_new_result
|
209
|
-
|
210
|
-
if has_content?(@current_code_content)
|
211
|
-
result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path, @current_block_explain)
|
212
|
-
|
213
|
-
# Check if result contains a Dalibo link for psql explain queries
|
214
|
-
dalibo_link, clean_result = extract_dalibo_link(result_output)
|
215
|
-
|
216
|
-
# Add the result block only if result=true (default)
|
217
|
-
if @current_block_result
|
218
|
-
add_result_block(clean_result || result_output, blank_line_before_new_result)
|
219
|
-
end
|
220
|
-
|
221
|
-
# Always add Dalibo link if it exists, even when result=false
|
222
|
-
if dalibo_link
|
223
|
-
# Add appropriate spacing based on whether result block was shown
|
224
|
-
if @current_block_result
|
225
|
-
@output_lines << "#{dalibo_link}\n\n"
|
226
|
-
else
|
227
|
-
@output_lines << "\n#{dalibo_link}\n\n"
|
228
|
-
end
|
229
|
-
end
|
230
|
-
else
|
231
|
-
warn "Skipping empty code block for language '#{@current_block_lang}'."
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
def skip_and_pass_through_result(lines_to_pass_through, file_enum, decision = nil)
|
236
|
-
# Handle run=false case where there are no lines to pass through
|
237
|
-
if lines_to_pass_through.empty?
|
238
|
-
warn "Skipping execution due to run=false option."
|
239
|
-
return
|
240
|
-
end
|
241
|
-
|
242
|
-
# Check if this is Dalibo content
|
243
|
-
if decision && decision[:dalibo_content]
|
244
|
-
warn "Found existing Dalibo link for current #{@current_block_lang} block, skipping execution."
|
245
|
-
@output_lines.concat(lines_to_pass_through)
|
246
|
-
# No additional consumption needed for Dalibo links
|
247
|
-
return
|
248
|
-
end
|
249
|
-
|
250
|
-
if mermaid_style_result?(@current_block_lang)
|
251
|
-
warn "Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution."
|
252
|
-
@output_lines.concat(lines_to_pass_through)
|
253
|
-
# For mermaid, no additional consumption needed since it's just an image line
|
254
|
-
else
|
255
|
-
lang_specific_result_type = ruby_style_result?(@current_block_lang) ? "```ruby RESULT" : "```RESULT"
|
256
|
-
warn "Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution."
|
257
|
-
@output_lines.concat(lines_to_pass_through)
|
258
|
-
consume_result_block_content(file_enum)
|
259
|
-
end
|
260
|
-
end
|
261
|
-
|
262
|
-
def consume_result_block_content(file_enum)
|
263
|
-
consume_block_lines(file_enum) do |line|
|
264
|
-
@output_lines << line
|
265
|
-
end
|
266
|
-
end
|
267
|
-
|
268
|
-
def consume_existing_result_block(file_enum, consumed_lines)
|
269
|
-
if mermaid_style_result?(@current_block_lang)
|
270
|
-
# For mermaid, there's no result block to consume, just the image line
|
271
|
-
# The image line should already be in consumed_lines from ExecutionDecider
|
272
|
-
return
|
273
|
-
end
|
274
|
-
|
275
|
-
consume_block_lines(file_enum) do |line|
|
276
|
-
consumed_lines << line
|
277
|
-
end
|
278
|
-
|
279
|
-
# After consuming the result block, check if there's a Dalibo link to consume as well
|
280
|
-
consume_dalibo_link_if_present(file_enum, consumed_lines)
|
281
|
-
end
|
282
|
-
|
283
|
-
def consume_block_lines(file_enum)
|
284
|
-
begin
|
285
|
-
loop do
|
286
|
-
result_block_line = file_enum.next
|
287
|
-
yield result_block_line
|
288
|
-
break if is_block_end?(result_block_line)
|
289
|
-
end
|
290
|
-
rescue StopIteration
|
291
|
-
warn "Warning: End of file reached while consuming result block."
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
def reset_code_block_state
|
296
|
-
@state = :outside_code_block
|
297
|
-
@current_code_content = ""
|
298
|
-
@current_block_rerun = false
|
299
|
-
@current_block_run = true
|
300
|
-
@current_block_explain = false
|
301
|
-
@current_block_result = true
|
302
|
-
end
|
303
|
-
|
304
|
-
def stderr_has_content?(stderr_output)
|
305
|
-
stderr_output && !stderr_output.strip.empty?
|
306
|
-
end
|
307
|
-
|
308
|
-
def extract_dalibo_link(result_output)
|
309
|
-
# Check if the result contains a Dalibo link marker
|
310
|
-
if result_output.start_with?("DALIBO_LINK:")
|
311
|
-
lines = result_output.split("\n", 2)
|
312
|
-
dalibo_url = lines[0].sub("DALIBO_LINK:", "")
|
313
|
-
clean_result = lines[1] || ""
|
314
|
-
dalibo_link = "**Dalibo Visualization:** [View Query Plan](#{dalibo_url})"
|
315
|
-
[dalibo_link, clean_result]
|
316
|
-
else
|
317
|
-
[nil, result_output]
|
318
|
-
end
|
319
|
-
end
|
320
|
-
|
321
|
-
def consume_dalibo_link_if_present(file_enum, consumed_lines)
|
322
|
-
# Look ahead to see if there are Dalibo links after the result block
|
323
|
-
begin
|
324
|
-
# Keep consuming blank lines and Dalibo links until we hit something else
|
325
|
-
loop do
|
326
|
-
next_line = peek_next_line(file_enum)
|
327
|
-
|
328
|
-
if is_blank_line?(next_line)
|
329
|
-
consumed_lines << file_enum.next
|
330
|
-
elsif next_line&.start_with?("**Dalibo Visualization:**")
|
331
|
-
consumed_lines << file_enum.next
|
332
|
-
else
|
333
|
-
# Hit something that's not a blank line or Dalibo link, stop consuming
|
334
|
-
break
|
335
|
-
end
|
336
|
-
end
|
337
|
-
rescue StopIteration
|
338
|
-
# End of file reached, nothing more to consume
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
92
|
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module ResultHelper
|
2
|
+
private
|
3
|
+
|
4
|
+
def ruby_style_result?(lang)
|
5
|
+
lang_config = SUPPORTED_LANGUAGES[lang]
|
6
|
+
lang_config && lang_config[:result_block_type] == "ruby"
|
7
|
+
end
|
8
|
+
|
9
|
+
def mermaid_style_result?(lang)
|
10
|
+
lang_config = SUPPORTED_LANGUAGES[lang]
|
11
|
+
lang_config && lang_config[:result_handling] == :mermaid_svg
|
12
|
+
end
|
13
|
+
|
14
|
+
def result_block_header(lang)
|
15
|
+
ruby_style_result?(lang) ? "```ruby RESULT\n" : "```RESULT\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
def result_block_regex(lang)
|
19
|
+
if mermaid_style_result?(lang)
|
20
|
+
# For mermaid, look for existing image tags with .svg extension
|
21
|
+
/^!\[.*\]\(.*\.svg\)$/i
|
22
|
+
elsif ruby_style_result?(lang)
|
23
|
+
/^```ruby\s+RESULT$/i
|
24
|
+
else
|
25
|
+
/^```RESULT$/i
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_result_block(result_output, blank_line_before_new_result)
|
30
|
+
if mermaid_style_result?(@current_block_lang)
|
31
|
+
# For mermaid, add the image tag directly without a result block
|
32
|
+
@output_lines << "\n" if blank_line_before_new_result.nil?
|
33
|
+
@output_lines << result_output
|
34
|
+
@output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
|
35
|
+
@output_lines << "\n"
|
36
|
+
else
|
37
|
+
@output_lines << "\n" if blank_line_before_new_result.nil?
|
38
|
+
@output_lines << result_block_header(@current_block_lang)
|
39
|
+
@output_lines << result_output
|
40
|
+
@output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
|
41
|
+
@output_lines << "```\n\n"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def handle_inside_result_block(current_line, file_enum)
|
47
|
+
@output_lines << current_line
|
48
|
+
if is_block_end?(current_line)
|
49
|
+
@state = :outside_code_block
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def handle_existing_ruby_result_block(current_line, file_enum)
|
54
|
+
warn "Found existing '```ruby RESULT' block, passing through."
|
55
|
+
@output_lines << current_line
|
56
|
+
@state = :inside_result_block
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
def execute_and_add_result(blank_line_before_new_result)
|
61
|
+
warn "Skipping empty code block for language '#{@current_block_lang}'." && return unless has_content?(@current_code_content)
|
62
|
+
|
63
|
+
@output_lines << blank_line_before_new_result if blank_line_before_new_result
|
64
|
+
|
65
|
+
result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path, @current_block_explain)
|
66
|
+
|
67
|
+
# Check if result contains a Dalibo link for psql explain queries
|
68
|
+
dalibo_link, clean_result = extract_dalibo_link(result_output)
|
69
|
+
|
70
|
+
# Add the result block only if result=true (default)
|
71
|
+
if @current_block_result
|
72
|
+
add_result_block(clean_result || result_output, blank_line_before_new_result)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Always add Dalibo link if it exists, even when result=false
|
76
|
+
if dalibo_link
|
77
|
+
# Add appropriate spacing based on whether result block was shown
|
78
|
+
if @current_block_result
|
79
|
+
@output_lines << "#{dalibo_link}\n\n"
|
80
|
+
else
|
81
|
+
@output_lines << "\n#{dalibo_link}\n\n"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def skip_and_pass_through_result(lines_to_pass_through, file_enum, decision = nil)
|
87
|
+
# Handle run=false case where there are no lines to pass through
|
88
|
+
if lines_to_pass_through.empty?
|
89
|
+
warn "Skipping execution due to run=false option."
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
# Check if this is Dalibo content
|
94
|
+
if decision && decision[:dalibo_content]
|
95
|
+
warn "Found existing Dalibo link for current #{@current_block_lang} block, skipping execution."
|
96
|
+
@output_lines.concat(lines_to_pass_through)
|
97
|
+
# No additional consumption needed for Dalibo links
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
if mermaid_style_result?(@current_block_lang)
|
102
|
+
warn "Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution."
|
103
|
+
@output_lines.concat(lines_to_pass_through)
|
104
|
+
# For mermaid, no additional consumption needed since it's just an image line
|
105
|
+
else
|
106
|
+
lang_specific_result_type = ruby_style_result?(@current_block_lang) ? "```ruby RESULT" : "```RESULT"
|
107
|
+
warn "Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution."
|
108
|
+
@output_lines.concat(lines_to_pass_through)
|
109
|
+
consume_result_block_content(file_enum)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def consume_result_block_content(file_enum)
|
114
|
+
consume_block_lines(file_enum) do |line|
|
115
|
+
@output_lines << line
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def consume_existing_result_block(file_enum, consumed_lines)
|
120
|
+
return if mermaid_style_result?(@current_block_lang)
|
121
|
+
|
122
|
+
consume_block_lines(file_enum) do |line|
|
123
|
+
consumed_lines << line
|
124
|
+
end
|
125
|
+
|
126
|
+
consume_dalibo_link_if_present(file_enum, consumed_lines)
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
def consume_block_lines(file_enum)
|
131
|
+
begin
|
132
|
+
loop do
|
133
|
+
result_block_line = file_enum.next
|
134
|
+
yield result_block_line
|
135
|
+
break if is_block_end?(result_block_line)
|
136
|
+
end
|
137
|
+
rescue StopIteration
|
138
|
+
warn "Warning: End of file reached while consuming result block."
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: markdown-run
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
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:
|