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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1139e0ccb6ed1b5b4ee2cef44cf067c83fc8a92ad750eae2e256725282534b68
4
- data.tar.gz: 8887f778eaec56e6bdbb84d0a48f8db82fbd52cabbcdea01c48f4cffb5ff410b
3
+ metadata.gz: 2764390f72c90c38bd4862f78db857bd5585d13e59f1a2c87a3a9c67c2f66529
4
+ data.tar.gz: 95caace78ee0db7d12ac3a6a7a5a9abcddf32c1e946ff3d9cf71beed12c89a91
5
5
  SHA512:
6
- metadata.gz: 6656d86bf179be5a044a95ccbc1a9c11e68642a35ed2ffa99138dc0de653ed4b2e176a9763f08b765d409ec232e4b9eda4779b166c97b7304ed156a48b147c93
7
- data.tar.gz: 3f8771ece0365e9a1c7073a6d749d41570776b59e9cbe0c38383516cce9cf0261a2ba287aadb30278ee0cf72e859fe4b4cb072fa89294bc3a525700fdec5ff9c
6
+ metadata.gz: 90aea8dd4f03bc75969643bedd914ed2a8a8a1b45adf777d51fdfabb60deefd5e36cd1b73ac37225848b4663e12872a69c2ba4c3624029ac4c81c333a8eb6a8c
7
+ data.tar.gz: 7dfc12cda3abca94774e013904e2d9853713dce9c8b4059d6ca6bd90319ddff4f9f0d826c230d508f2438fdce57626f7868c1b94c9756ec3a122e0645be2121c
data/CHANGELOG.md CHANGED
@@ -1,49 +1,60 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.12] - 2025-06-04
4
+
5
+ - Added `flamegraph` option for psql code blocks to generate PostgreSQL query execution plan flamegraphs as SVG images
6
+ - PostgreSQL flamegraphs provide interactive, color-coded visualization of query performance with hover tooltips
7
+ - Flamegraph SVG files follow same directory structure as mermaid diagrams (organized by markdown file basename)
8
+ - Added flamegraph option support to frontmatter defaults and language-specific configurations
9
+
10
+ ## [0.1.11] - 2025-06-04
11
+
12
+ - options are customizable with the yaml frontmatter
13
+
3
14
  ## [0.1.10] - 2025-06-03
4
15
 
5
- - standalone options for codeblocks (run instead of run=true)
6
- - explain option for psql code blocks with Dalibo visualization links
7
- - Fixed Dalibo URL generation to properly submit plans via HTTP POST
8
- - Added result option to control result block visibility (result=false hides result blocks while still executing code)
16
+ - standalone options for codeblocks (run instead of run=true)
17
+ - explain option for psql code blocks with Dalibo visualization links
18
+ - Fixed Dalibo URL generation to properly submit plans via HTTP POST
19
+ - Added result option to control result block visibility (result=false hides result blocks while still executing code)
9
20
 
10
21
  ## [0.1.9] - 2025-06-02
11
22
 
12
- - mermaid codeblocks
23
+ - mermaid codeblocks
13
24
 
14
25
  ## [0.1.8] - 2025-06-01
15
26
 
16
- - Added run option
27
+ - Added run option
17
28
 
18
29
  ## [0.1.7] - 2025-06-01
19
30
 
20
- - Added rerun functionality
31
+ - Added rerun functionality
21
32
 
22
33
  ## [0.1.6] - 2025-06-01
23
34
 
24
- - Refactor code to state pattern
25
- - Add yaml frontmatter to support aliases for code blocks
35
+ - Refactor code to state pattern
36
+ - Add yaml frontmatter to support aliases for code blocks
26
37
 
27
38
  ## [0.1.5] - 2025-05-19
28
39
 
29
- - Remove gif files from release
40
+ - Remove gif files from release
30
41
 
31
42
  ## [0.1.4] - 2025-05-18
32
43
 
33
- - Add support for zsh, bash, sh
44
+ - Add support for zsh, bash, sh
34
45
 
35
46
  ## [0.1.3] - 2025-05-14
36
47
 
37
- - Fix missing minitest dep
48
+ - Fix missing minitest dep
38
49
 
39
50
  ## [0.1.2] - 2025-05-14
40
51
 
41
- - Gemfile update
52
+ - Gemfile update
42
53
 
43
54
  ## [0.1.1] - 2025-05-14
44
55
 
45
- - Added checks for missing dependencies
56
+ - Added checks for missing dependencies
46
57
 
47
58
  ## [0.1.0] - 2025-05-13
48
59
 
49
- - Initial release
60
+ - Initial release
data/README.md CHANGED
@@ -59,6 +59,7 @@ example vscode keybinding
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
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
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
62
+ - `flamegraph=true` or `flamegraph=false` for psql code blocks to generate PostgreSQL query execution plan flamegraphs as SVG images. `flamegraph=false` is the default if not specified
62
63
 
63
64
  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
 
@@ -70,6 +71,7 @@ Options can also be specified using standalone syntax without explicit `=true`:
70
71
  - `rerun` is equivalent to `rerun=true`
71
72
  - `result` is equivalent to `result=true`
72
73
  - `explain` is equivalent to `explain=true`
74
+ - `flamegraph` is equivalent to `flamegraph=true`
73
75
 
74
76
  Explicit assignments (e.g., `run=false`) take precedence over standalone options.
75
77
 
@@ -99,11 +101,57 @@ SELECT * FROM users WHERE id = 1;
99
101
  EXPLAIN (ANALYZE) SELECT * FROM large_table;
100
102
  ```
101
103
 
104
+ ```psql flamegraph
105
+ SELECT u.name, COUNT(o.id) as order_count
106
+ FROM users u
107
+ LEFT JOIN orders o ON u.id = o.user_id
108
+ WHERE u.created_at > '2024-01-01'
109
+ GROUP BY u.id, u.name
110
+ ORDER BY order_count DESC
111
+ LIMIT 10;
112
+ ```
113
+
114
+ ```psql flamegraph=true result=false
115
+ -- This will generate a flamegraph but hide the JSON result block
116
+ SELECT * FROM complex_query_with_joins;
117
+ ```
118
+
102
119
  ```psql result=false explain
103
120
  SELECT * FROM large_table;
104
121
  -- This will execute the explain query and show the Dalibo link but hide the result block
105
122
  ```
106
123
 
124
+ ### PostgreSQL Flamegraphs
125
+
126
+ PostgreSQL flamegraph blocks generate interactive SVG flamegraphs from query execution plans:
127
+
128
+ ```psql flamegraph
129
+ SELECT users.*, orders.total
130
+ FROM users
131
+ JOIN orders ON users.id = orders.user_id
132
+ WHERE users.created_at > '2024-01-01';
133
+ ```
134
+
135
+ This generates:
136
+
137
+ - An SVG flamegraph file in a directory named after the markdown file
138
+ - Filename format: `my-document-flamegraph-20250118-143022-a1b2c3.svg`
139
+ - Embedded image tag: `![PostgreSQL Query Flamegraph](my-document/my-document-flamegraph-20250118-143022-a1b2c3.svg)`
140
+
141
+ **Flamegraph features:**
142
+
143
+ - **Interactive**: Hover over rectangles to see detailed timing information
144
+ - **Color-coded**: Different colors for operation types (red=seq scans, green=index scans, blue=joins, etc.)
145
+ - **Hierarchical**: Shows query plan structure visually
146
+ - **Performance insights**: Width represents execution time, making bottlenecks immediately visible
147
+
148
+ **What flamegraphs help identify:**
149
+
150
+ - Slow operations (widest rectangles)
151
+ - Query plan structure (nested relationships)
152
+ - Inefficient operations (color-coded by type)
153
+ - Execution time distribution across plan nodes
154
+
107
155
  ### Mermaid diagrams
108
156
 
109
157
  Mermaid blocks generate SVG files and insert markdown image tags:
@@ -135,6 +183,74 @@ markdown-run:
135
183
  - sql: psql
136
184
  ```
137
185
 
186
+ ### Setting Defaults
187
+
188
+ You can override the default behavior for code block options using frontmatter:
189
+
190
+ ```yaml
191
+ markdown-run:
192
+ defaults:
193
+ rerun: true
194
+ result: false
195
+ psql:
196
+ explain: true
197
+ ruby:
198
+ rerun: false
199
+ ```
200
+
201
+ **Priority order (highest to lowest):**
202
+
203
+ 1. Explicit options in code blocks (e.g., `rerun=true`)
204
+ 2. Language-specific defaults (e.g., `psql: { explain: true }`)
205
+ 3. Global defaults (e.g., `defaults: { rerun: true }`)
206
+ 4. Built-in application defaults
207
+
208
+ **Available options for defaults:**
209
+
210
+ - `run`: Control whether code blocks execute (default: `true`)
211
+ - `rerun`: Control whether to re-execute if result exists (default: `false`)
212
+ - `result`: Control whether to show result blocks (default: `true`)
213
+ - `explain`: For psql blocks, generate explain plans (default: `false`)
214
+ - `flamegraph`: For psql blocks, generate flamegraph SVGs (default: `false`)
215
+
216
+ **Examples:**
217
+
218
+ Make all code blocks rerun by default:
219
+
220
+ ```yaml
221
+ markdown-run:
222
+ defaults:
223
+ rerun: true
224
+ ```
225
+
226
+ Hide result blocks by default but enable explain for psql:
227
+
228
+ ```yaml
229
+ markdown-run:
230
+ defaults:
231
+ result: false
232
+ psql:
233
+ explain: true
234
+ ```
235
+
236
+ Enable flamegraphs by default for all psql blocks:
237
+
238
+ ```yaml
239
+ markdown-run:
240
+ psql:
241
+ flamegraph: true
242
+ ```
243
+
244
+ Language-specific settings override global defaults:
245
+
246
+ ```yaml
247
+ markdown-run:
248
+ defaults:
249
+ rerun: false # Global default
250
+ ruby:
251
+ rerun: true # Ruby blocks will rerun, others won't
252
+ ```
253
+
138
254
  ## Demo
139
255
 
140
256
  ![VSCode Usage](docs/markdown-run-vscode.gif)
data/Rakefile CHANGED
@@ -19,4 +19,14 @@ task :flog_detailed do
19
19
  system("flog -d lib/ exe/ test/")
20
20
  end
21
21
 
22
+ desc "Release"
23
+ task :release do
24
+ `gem bump`
25
+ `bundle`
26
+ `git commit --amend`
27
+ `git push`
28
+ `git push --tags`
29
+ `gem release`
30
+ end
31
+
22
32
  task default: :test
@@ -0,0 +1,64 @@
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_flamegraph = false
14
+ @current_block_result = true
15
+ end
16
+
17
+
18
+ def start_code_block(current_line, lang, options_string = nil)
19
+ @output_lines << current_line
20
+ @current_block_lang = resolve_language(lang)
21
+ @current_block_rerun = @code_block_parser.parse_rerun_option(options_string, @current_block_lang)
22
+ @current_block_run = @code_block_parser.parse_run_option(options_string, @current_block_lang)
23
+ @current_block_explain = @code_block_parser.parse_explain_option(options_string, @current_block_lang)
24
+ @current_block_flamegraph = @code_block_parser.parse_flamegraph_option(options_string, @current_block_lang)
25
+ @current_block_result = @code_block_parser.parse_result_option(options_string, @current_block_lang)
26
+ @state = :inside_code_block
27
+ @current_code_content = ""
28
+ end
29
+
30
+ def accumulate_code_content(current_line)
31
+ @current_code_content += current_line
32
+ @output_lines << current_line
33
+ end
34
+
35
+ def end_code_block(current_line, file_enum)
36
+ @output_lines << current_line
37
+
38
+ decision = decide_execution(file_enum)
39
+
40
+ if decision[:execute]
41
+ # If we consumed lines for rerun, don't add them to output (they'll be replaced)
42
+ execute_and_add_result(decision[:blank_line])
43
+ else
44
+ skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum, decision)
45
+ end
46
+
47
+ reset_code_block_state
48
+ end
49
+
50
+ def decide_execution(file_enum)
51
+ decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_flamegraph, @current_block_result)
52
+ decision = decider.decide(file_enum, method(:result_block_regex))
53
+
54
+ # Handle the consume_existing flag for rerun scenarios
55
+ if decision[:consume_existing]
56
+ consume_existing_result_block(file_enum, decision[:consumed_lines])
57
+ elsif decision[:consume_existing_dalibo]
58
+ # Dalibo links are already consumed in the decision process
59
+ # Just acknowledge they were consumed
60
+ end
61
+
62
+ decision
63
+ end
64
+ end
@@ -34,20 +34,29 @@ class CodeBlockParser
34
34
  line.strip == BLOCK_END_PATTERN
35
35
  end
36
36
 
37
- def parse_run_option(options_string)
38
- parse_boolean_option(options_string, "run", true)
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
- parse_boolean_option(options_string, "rerun", false)
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
- parse_boolean_option(options_string, "explain", false)
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
- parse_boolean_option(options_string, "result", true)
52
+ def parse_flamegraph_option(options_string, language = nil)
53
+ default_value = @frontmatter_parser.get_default_value("flamegraph", language, false)
54
+ parse_boolean_option(options_string, "flamegraph", default_value)
55
+ end
56
+
57
+ def parse_result_option(options_string, language = nil)
58
+ default_value = @frontmatter_parser.get_default_value("result", language, true)
59
+ parse_boolean_option(options_string, "result", default_value)
51
60
  end
52
61
 
53
62
  private
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, explain = false)
7
- new.execute(code_content, lang, temp_dir, input_file_path, explain)
6
+ def self.execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
7
+ new.execute(code_content, lang, temp_dir, input_file_path, explain, flamegraph)
8
8
  end
9
9
 
10
- def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false)
10
+ def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
11
11
  lang_key = lang.downcase
12
12
  lang_config = SUPPORTED_LANGUAGES[lang_key]
13
13
 
@@ -15,34 +15,38 @@ 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, explain)
19
- process_execution_result(result, lang_config, lang_key, explain)
18
+ result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path, explain, flamegraph)
19
+ process_execution_result(result, lang_config, lang_key, explain, flamegraph)
20
20
  end
21
21
 
22
22
  private
23
23
 
24
+ def stderr_has_content?(stderr_output)
25
+ stderr_output && !stderr_output.strip.empty?
26
+ end
27
+
24
28
  def handle_unsupported_language(lang)
25
29
  warn "Unsupported language: #{lang}"
26
30
  "ERROR: Unsupported language: #{lang}"
27
31
  end
28
32
 
29
- def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false)
33
+ def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false, flamegraph = false)
30
34
  cmd_lambda = lang_config[:command]
31
35
  temp_file_suffix = lang_config[:temp_file_suffix]
32
36
 
33
37
  if temp_file_suffix
34
- execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain)
38
+ execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain, flamegraph)
35
39
  else
36
- execute_direct_command(code_content, cmd_lambda, explain)
40
+ execute_direct_command(code_content, cmd_lambda, input_file_path, explain, flamegraph)
37
41
  end
38
42
  end
39
43
 
40
- def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false)
44
+ def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false, flamegraph = false)
41
45
  result = nil
42
46
  Tempfile.create([lang_key, temp_file_suffix], temp_dir) do |temp_file|
43
47
  temp_file.write(code_content)
44
48
  temp_file.close
45
- command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path, input_file_path, explain)
49
+ command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path, input_file_path, explain, flamegraph)
46
50
 
47
51
  # Extract output_path if present (for mermaid)
48
52
  output_path = exec_options.delete(:output_path) if exec_options.is_a?(Hash)
@@ -52,27 +56,35 @@ class CodeExecutor
52
56
  stdout: captured_stdout,
53
57
  stderr: captured_stderr,
54
58
  status: captured_status_obj,
55
- output_path: output_path # For mermaid SVG output
59
+ output_path: output_path, # For mermaid SVG output
60
+ input_file_path: input_file_path # Pass through for flamegraph generation
56
61
  }
57
62
  end
58
63
  result
59
64
  end
60
65
 
61
- def execute_direct_command(code_content, cmd_lambda, explain = false)
62
- command_to_run, exec_options = cmd_lambda.call(code_content, nil, nil, explain)
66
+ def execute_direct_command(code_content, cmd_lambda, input_file_path = nil, explain = false, flamegraph = false)
67
+ command_to_run, exec_options = cmd_lambda.call(code_content, nil, input_file_path, explain, flamegraph)
63
68
  captured_stdout, captured_stderr, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
64
- { stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj }
69
+ { stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj, input_file_path: input_file_path }
65
70
  end
66
71
 
67
- def process_execution_result(result, lang_config, lang_key, explain = false)
72
+ def process_execution_result(result, lang_config, lang_key, explain = false, flamegraph = false)
68
73
  exit_status, result_output, stderr_output = format_captured_output(result, lang_config)
69
74
 
70
75
  if exit_status != 0
71
76
  result_output = add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
72
77
  elsif lang_config && lang_config[:result_handling] == :mermaid_svg
73
78
  result_output = handle_mermaid_svg_result(result, lang_key)
74
- elsif explain && lang_key == "psql"
75
- result_output = handle_psql_explain_result(result_output)
79
+ else
80
+ # Handle psql explain and flamegraph processing (both can be enabled)
81
+ if explain && lang_key == "psql"
82
+ result_output = handle_psql_explain_result(result_output)
83
+ end
84
+
85
+ if flamegraph && lang_key == "psql"
86
+ result_output = handle_psql_flamegraph_result(result_output, result[:input_file_path])
87
+ end
76
88
  end
77
89
 
78
90
  result_output
@@ -163,35 +175,101 @@ class CodeExecutor
163
175
  end
164
176
  end
165
177
 
178
+ def handle_psql_flamegraph_result(result_output, input_file_path = nil)
179
+ require_relative 'pg_flamegraph_svg'
180
+
181
+ begin
182
+ # Extract clean JSON from result_output (might contain Dalibo link prefix)
183
+ json_text = if result_output.start_with?("DALIBO_LINK:")
184
+ # Extract the JSON part after the Dalibo link line
185
+ lines = result_output.split("\n", 2)
186
+ lines[1] || ""
187
+ else
188
+ result_output.strip
189
+ end
190
+
191
+ # Parse the EXPLAIN JSON output
192
+ json_data = JSON.parse(json_text)
193
+
194
+ # Generate SVG flamegraph
195
+ flamegraph_generator = PostgreSQLFlameGraphSVG.new(JSON.generate(json_data))
196
+ svg_content = flamegraph_generator.generate_svg
197
+
198
+ # Save SVG file following same pattern as mermaid
199
+ if input_file_path
200
+ # Extract markdown file basename without extension
201
+ md_basename = File.basename(input_file_path, ".*")
202
+
203
+ # Create directory named after the markdown file
204
+ output_dir = File.join(File.dirname(input_file_path), md_basename)
205
+ Dir.mkdir(output_dir) unless Dir.exist?(output_dir)
206
+
207
+ # Generate unique filename with markdown basename prefix
208
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
209
+ random_suffix = SecureRandom.hex(6)
210
+ svg_filename = "#{md_basename}-flamegraph-#{timestamp}-#{random_suffix}.svg"
211
+ output_path = File.join(output_dir, svg_filename)
212
+ else
213
+ # Fallback to simple naming
214
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
215
+ output_path = "pg-flamegraph-#{timestamp}.svg"
216
+ end
217
+
218
+ # Write SVG file
219
+ File.write(output_path, svg_content)
220
+
221
+ # Generate relative path for markdown
222
+ if input_file_path
223
+ relative_path = "#{File.basename(output_dir)}/#{File.basename(output_path)}"
224
+ else
225
+ relative_path = File.basename(output_path)
226
+ end
227
+
228
+ warn "Generated PostgreSQL flamegraph: #{relative_path}"
229
+
230
+ # Return a special format that the markdown processor can parse
231
+ # Preserve any existing Dalibo link prefix
232
+ if result_output.start_with?("DALIBO_LINK:")
233
+ lines = result_output.split("\n", 2)
234
+ dalibo_part = lines[0]
235
+ json_part = lines[1] || ""
236
+ "#{dalibo_part}\nFLAMEGRAPH_LINK:#{relative_path}\n#{json_part}"
237
+ else
238
+ "FLAMEGRAPH_LINK:#{relative_path}\n#{json_text}"
239
+ end
240
+
241
+ rescue JSON::ParserError => e
242
+ warn "Error parsing EXPLAIN JSON: #{e.message}"
243
+ result_output
244
+ rescue => e
245
+ warn "Error generating flamegraph: #{e.message}"
246
+ result_output
247
+ end
248
+ end
249
+
166
250
  private
167
251
 
168
252
  def submit_plan_to_dalibo(plan_json)
169
253
  begin
170
- # Start with HTTPS directly to avoid the HTTP->HTTPS redirect
171
254
  uri = URI('https://explain.dalibo.com/new')
172
255
  http = Net::HTTP.new(uri.host, uri.port)
173
256
  http.use_ssl = true
174
- http.read_timeout = 10 # 10 seconds timeout
257
+ http.read_timeout = 10
175
258
 
176
- # Prepare the JSON payload
177
259
  payload = {
178
260
  'plan' => plan_json,
179
261
  'title' => "Query Plan - #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}",
180
262
  'query' => ''
181
263
  }
182
264
 
183
- # Create the POST request
184
265
  request = Net::HTTP::Post.new(uri)
185
266
  request['Content-Type'] = 'application/json'
186
267
  request.body = JSON.generate(payload)
187
268
 
188
- # Send the request and follow redirects to get the final URL
189
269
  response = http.request(request)
190
270
 
191
- # Dalibo returns a redirect to the plan URL
192
271
  if response.is_a?(Net::HTTPRedirection)
193
272
  location = response['location']
194
- # Make sure it's a full URL
195
273
  if location
196
274
  if location.start_with?('/')
197
275
  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](#{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]")
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