markdown-run 0.1.10 → 0.1.12
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 +26 -15
- data/README.md +116 -0
- data/Rakefile +10 -0
- data/lib/code_block_helper.rb +64 -0
- data/lib/code_block_parser.rb +17 -8
- data/lib/code_executor.rb +102 -24
- data/lib/dalibo_helper.rb +39 -0
- data/lib/execution_decider.rb +83 -9
- data/lib/flamegraph_helper.rb +39 -0
- data/lib/frontmatter_parser.rb +48 -6
- data/lib/language_configs.rb +10 -10
- data/lib/markdown/run/version.rb +1 -1
- data/lib/markdown_processor.rb +9 -259
- data/lib/pg_flamegraph_svg.rb +221 -0
- data/lib/result_helper.rb +167 -0
- data/markdown-run-sample.md +9 -0
- metadata +21 -2
data/lib/execution_decider.rb
CHANGED
@@ -3,11 +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, current_block_explain = false, current_block_result = true)
|
6
|
+
def initialize(current_block_run, current_block_rerun, current_block_lang, current_block_explain = false, current_block_flamegraph = 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
10
|
@current_block_explain = current_block_explain
|
11
|
+
@current_block_flamegraph = current_block_flamegraph
|
11
12
|
@current_block_result = current_block_result
|
12
13
|
end
|
13
14
|
|
@@ -21,8 +22,10 @@ class ExecutionDecider
|
|
21
22
|
handle_immediate_result_block(file_enum)
|
22
23
|
elsif is_blank_line?(peek1)
|
23
24
|
handle_blank_line_scenario(file_enum, expected_header_regex)
|
24
|
-
elsif @current_block_explain && is_dalibo_link?(peek1)
|
25
|
+
elsif (@current_block_explain || @current_block_flamegraph) && is_dalibo_link?(peek1)
|
25
26
|
handle_immediate_dalibo_link(file_enum)
|
27
|
+
elsif @current_block_flamegraph && is_flamegraph_link?(peek1)
|
28
|
+
handle_immediate_flamegraph_link(file_enum)
|
26
29
|
else
|
27
30
|
execute_without_existing_result
|
28
31
|
end
|
@@ -48,11 +51,11 @@ class ExecutionDecider
|
|
48
51
|
|
49
52
|
def handle_blank_line_scenario(file_enum, expected_header_regex)
|
50
53
|
consumed_blank_line = file_enum.next
|
51
|
-
|
54
|
+
|
52
55
|
# Look ahead past multiple blank lines to find actual content
|
53
56
|
peek2 = peek_next_line(file_enum)
|
54
57
|
additional_blanks = []
|
55
|
-
|
58
|
+
|
56
59
|
# Consume consecutive blank lines
|
57
60
|
while is_blank_line?(peek2)
|
58
61
|
additional_blanks << file_enum.next
|
@@ -61,8 +64,10 @@ class ExecutionDecider
|
|
61
64
|
|
62
65
|
if line_matches_pattern?(peek2, expected_header_regex)
|
63
66
|
handle_result_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
|
64
|
-
elsif @current_block_explain && is_dalibo_link?(peek2)
|
67
|
+
elsif (@current_block_explain || @current_block_flamegraph) && is_dalibo_link?(peek2)
|
65
68
|
handle_dalibo_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
|
69
|
+
elsif @current_block_flamegraph && is_flamegraph_link?(peek2)
|
70
|
+
handle_flamegraph_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
|
66
71
|
else
|
67
72
|
execute_with_blank_lines(consumed_blank_line, additional_blanks)
|
68
73
|
end
|
@@ -194,7 +199,7 @@ class ExecutionDecider
|
|
194
199
|
# Consume all consecutive Dalibo links and blank lines
|
195
200
|
loop do
|
196
201
|
next_line = peek_next_line(file_enum)
|
197
|
-
|
202
|
+
|
198
203
|
if is_blank_line?(next_line) || is_dalibo_link?(next_line)
|
199
204
|
consumed_line = file_enum.next
|
200
205
|
consumed_lines << consumed_line
|
@@ -205,7 +210,7 @@ class ExecutionDecider
|
|
205
210
|
end
|
206
211
|
|
207
212
|
def is_dalibo_link?(line)
|
208
|
-
line&.start_with?("
|
213
|
+
line&.start_with?("[Dalibo]")
|
209
214
|
end
|
210
215
|
|
211
216
|
def line_matches_pattern?(line, pattern)
|
@@ -217,9 +222,78 @@ class ExecutionDecider
|
|
217
222
|
end
|
218
223
|
|
219
224
|
def should_auto_replace_dalibo_link?
|
220
|
-
# Auto-replace Dalibo links when using explain with result=false
|
225
|
+
# Auto-replace Dalibo links when using explain or flamegraph with result=false
|
221
226
|
# This makes sense because with result=false, there's only a Dalibo link,
|
222
227
|
# so it should be updated on each run
|
223
|
-
@current_block_explain && !@current_block_result
|
228
|
+
(@current_block_explain || @current_block_flamegraph) && !@current_block_result
|
229
|
+
end
|
230
|
+
|
231
|
+
def is_flamegraph_link?(line)
|
232
|
+
line&.start_with?("![PostgreSQL Query Flamegraph]")
|
233
|
+
end
|
234
|
+
|
235
|
+
def handle_flamegraph_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
|
236
|
+
# For flamegraph result=false, always replace existing flamegraph links
|
237
|
+
# For flamegraph result=true, follow normal rerun logic
|
238
|
+
if should_auto_replace_flamegraph_link? || @current_block_rerun
|
239
|
+
execute_with_consumed_flamegraph_and_blanks(file_enum, consumed_blank_line, additional_blanks)
|
240
|
+
else
|
241
|
+
skip_with_blanks_and_flamegraph(file_enum, consumed_blank_line, additional_blanks)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def handle_immediate_flamegraph_link(file_enum)
|
246
|
+
# For flamegraph result=false, always replace existing flamegraph links
|
247
|
+
# For flamegraph result=true, follow normal rerun logic
|
248
|
+
if should_auto_replace_flamegraph_link? || @current_block_rerun
|
249
|
+
execute_with_consumed_flamegraph(file_enum)
|
250
|
+
else
|
251
|
+
skip_with_existing_flamegraph(file_enum)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def execute_with_consumed_flamegraph(file_enum)
|
256
|
+
consumed_lines = []
|
257
|
+
consume_flamegraph_links(file_enum, consumed_lines)
|
258
|
+
{ execute: true, consumed_lines: consumed_lines, consume_existing_flamegraph: true }
|
259
|
+
end
|
260
|
+
|
261
|
+
def skip_with_existing_flamegraph(file_enum)
|
262
|
+
consumed_lines = []
|
263
|
+
consume_flamegraph_links(file_enum, consumed_lines)
|
264
|
+
{ execute: false, lines_to_pass_through: consumed_lines, flamegraph_content: true }
|
265
|
+
end
|
266
|
+
|
267
|
+
def execute_with_consumed_flamegraph_and_blanks(file_enum, consumed_blank_line, additional_blanks)
|
268
|
+
consumed_lines = [consumed_blank_line] + additional_blanks
|
269
|
+
consume_flamegraph_links(file_enum, consumed_lines)
|
270
|
+
{ execute: true, consumed_lines: consumed_lines, blank_line: consumed_blank_line, consume_existing_flamegraph: true }
|
271
|
+
end
|
272
|
+
|
273
|
+
def skip_with_blanks_and_flamegraph(file_enum, consumed_blank_line, additional_blanks)
|
274
|
+
consumed_lines = [consumed_blank_line] + additional_blanks
|
275
|
+
consume_flamegraph_links(file_enum, consumed_lines)
|
276
|
+
{ execute: false, lines_to_pass_through: consumed_lines, flamegraph_content: true }
|
277
|
+
end
|
278
|
+
|
279
|
+
def consume_flamegraph_links(file_enum, consumed_lines)
|
280
|
+
# Consume all consecutive flamegraph links and blank lines
|
281
|
+
loop do
|
282
|
+
next_line = peek_next_line(file_enum)
|
283
|
+
|
284
|
+
if is_blank_line?(next_line) || is_flamegraph_link?(next_line)
|
285
|
+
consumed_line = file_enum.next
|
286
|
+
consumed_lines << consumed_line
|
287
|
+
else
|
288
|
+
break
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def should_auto_replace_flamegraph_link?
|
294
|
+
# Auto-replace flamegraph links when using flamegraph with result=false
|
295
|
+
# This makes sense because with result=false, there's only a flamegraph link,
|
296
|
+
# so it should be updated on each run
|
297
|
+
@current_block_flamegraph && !@current_block_result
|
224
298
|
end
|
225
299
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module FlamegraphHelper
|
2
|
+
private
|
3
|
+
|
4
|
+
FLAMEGRAPH_LINK_PREFIX = "FLAMEGRAPH_LINK:"
|
5
|
+
|
6
|
+
def extract_flamegraph_link(result_output)
|
7
|
+
# Check if the result contains a flamegraph link marker
|
8
|
+
if result_output.start_with?(FLAMEGRAPH_LINK_PREFIX)
|
9
|
+
lines = result_output.split("\n", 2)
|
10
|
+
flamegraph_path = lines[0].sub(FLAMEGRAPH_LINK_PREFIX, "")
|
11
|
+
clean_result = lines[1] || ""
|
12
|
+
flamegraph_link = ""
|
13
|
+
[flamegraph_link, clean_result]
|
14
|
+
else
|
15
|
+
[nil, result_output]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def consume_flamegraph_link_if_present(file_enum, consumed_lines)
|
20
|
+
# Look ahead to see if there are flamegraph links after the result block
|
21
|
+
begin
|
22
|
+
# Keep consuming blank lines and flamegraph 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?("![PostgreSQL Query Flamegraph]")
|
29
|
+
consumed_lines << file_enum.next
|
30
|
+
else
|
31
|
+
# Hit something that's not a blank line or flamegraph 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/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, explain = false) {
|
4
|
+
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = 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,20 +16,20 @@ JS_CONFIG = {
|
|
16
16
|
}.freeze
|
17
17
|
|
18
18
|
SQLITE_CONFIG = {
|
19
|
-
command: ->(code_content, temp_file_path, input_file_path = nil, explain = false) { [ "sqlite3 #{temp_file_path}", { stdin_data: code_content } ] },
|
19
|
+
command: ->(code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = 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, explain = false) {
|
25
|
+
command: ->(code_content, _temp_file_path, input_file_path = nil, explain = false, flamegraph = 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
|
31
|
+
# Modify the SQL query if explain or flamegraph option is enabled
|
32
|
+
if explain || flamegraph
|
33
33
|
# Wrap the query with EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
|
34
34
|
# Remove any trailing semicolons and whitespace, then add our EXPLAIN wrapper
|
35
35
|
clean_query = code_content.strip.gsub(/;\s*$/, '')
|
@@ -41,7 +41,7 @@ SUPPORTED_LANGUAGES = {
|
|
41
41
|
}
|
42
42
|
},
|
43
43
|
"ruby" => {
|
44
|
-
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
|
44
|
+
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
45
45
|
xmpfilter_exists = system("command -v xmpfilter > /dev/null 2>&1")
|
46
46
|
unless xmpfilter_exists
|
47
47
|
abort "Error: xmpfilter command not found. Please install xmpfilter or ensure it is in your PATH."
|
@@ -57,7 +57,7 @@ SUPPORTED_LANGUAGES = {
|
|
57
57
|
"sqlite" => SQLITE_CONFIG,
|
58
58
|
"sqlite3" => SQLITE_CONFIG, # Alias for sqlite
|
59
59
|
"bash" => {
|
60
|
-
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
|
60
|
+
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
61
61
|
bash_exists = system("command -v bash > /dev/null 2>&1")
|
62
62
|
unless bash_exists
|
63
63
|
abort "Error: bash command not found. Please ensure bash is in your PATH."
|
@@ -67,7 +67,7 @@ SUPPORTED_LANGUAGES = {
|
|
67
67
|
temp_file_suffix: ".sh"
|
68
68
|
},
|
69
69
|
"zsh" => {
|
70
|
-
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
|
70
|
+
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
71
71
|
zsh_exists = system("command -v zsh > /dev/null 2>&1")
|
72
72
|
unless zsh_exists
|
73
73
|
abort "Error: zsh command not found. Please ensure zsh is in your PATH."
|
@@ -77,7 +77,7 @@ SUPPORTED_LANGUAGES = {
|
|
77
77
|
temp_file_suffix: ".zsh"
|
78
78
|
},
|
79
79
|
"sh" => {
|
80
|
-
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
|
80
|
+
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
81
81
|
sh_exists = system("command -v sh > /dev/null 2>&1")
|
82
82
|
unless sh_exists
|
83
83
|
abort "Error: sh command not found. Please ensure sh is in your PATH."
|
@@ -87,7 +87,7 @@ SUPPORTED_LANGUAGES = {
|
|
87
87
|
temp_file_suffix: ".sh"
|
88
88
|
},
|
89
89
|
"mermaid" => {
|
90
|
-
command: ->(code_content, temp_file_path, input_file_path = nil, explain = false) {
|
90
|
+
command: ->(code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
91
91
|
mmdc_exists = system("command -v mmdc > /dev/null 2>&1")
|
92
92
|
unless mmdc_exists
|
93
93
|
abort "Error: mmdc command not found. Please install @mermaid-js/mermaid-cli: npm install -g @mermaid-js/mermaid-cli"
|
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
|