markdown-run 0.1.8 → 0.1.10

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.
@@ -0,0 +1,342 @@
1
+ require_relative "language_configs"
2
+ require_relative "frontmatter_parser"
3
+ require_relative "code_block_parser"
4
+ require_relative "code_executor"
5
+ require_relative "execution_decider"
6
+ require_relative "enum_helper"
7
+
8
+ class MarkdownProcessor
9
+ include EnumHelper
10
+ def initialize(temp_dir, input_file_path = nil)
11
+ @temp_dir = temp_dir
12
+ @input_file_path = input_file_path
13
+ @output_lines = []
14
+ @state = :outside_code_block
15
+ @current_block_lang = ""
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
+ @frontmatter_parser = FrontmatterParser.new
22
+ @code_block_parser = CodeBlockParser.new(@frontmatter_parser)
23
+ end
24
+
25
+ def process_file(file_enum)
26
+ @frontmatter_parser.parse_frontmatter(file_enum, @output_lines)
27
+
28
+ loop do
29
+ current_line = get_next_line(file_enum)
30
+ break unless current_line
31
+
32
+ handle_line(current_line, file_enum)
33
+ end
34
+ @output_lines
35
+ end
36
+
37
+ private
38
+
39
+ def resolve_language(lang)
40
+ @frontmatter_parser.resolve_language(lang)
41
+ end
42
+
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
+ def is_block_end?(line)
69
+ @code_block_parser.is_block_end?(line)
70
+ end
71
+
72
+ def has_content?(content)
73
+ !content.strip.empty?
74
+ end
75
+
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
+ def line_matches_pattern?(line, pattern)
93
+ line && line.match?(pattern)
94
+ end
95
+
96
+ def is_blank_line?(line)
97
+ line && line.strip == ""
98
+ end
99
+
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
+ def handle_line(current_line, file_enum)
117
+ case @state
118
+ when :outside_code_block
119
+ handle_outside_code_block(current_line, file_enum)
120
+ when :inside_code_block
121
+ handle_inside_code_block(current_line, file_enum)
122
+ when :inside_result_block
123
+ handle_inside_result_block(current_line, file_enum)
124
+ end
125
+ end
126
+
127
+ def handle_outside_code_block(current_line, file_enum)
128
+ if @code_block_parser.is_ruby_result_block?(current_line)
129
+ handle_existing_ruby_result_block(current_line, file_enum)
130
+ else
131
+ parsed_header = @code_block_parser.parse_code_block_header(current_line)
132
+ if parsed_header && parsed_header[:is_supported]
133
+ start_code_block(current_line, parsed_header[:original_lang], parsed_header[:options_string])
134
+ else
135
+ @output_lines << current_line
136
+ end
137
+ end
138
+ end
139
+
140
+ def handle_inside_code_block(current_line, file_enum)
141
+ if is_block_end?(current_line)
142
+ end_code_block(current_line, file_enum)
143
+ else
144
+ accumulate_code_content(current_line)
145
+ end
146
+ 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
+ end
@@ -0,0 +1,20 @@
1
+ require_relative "language_configs"
2
+ require_relative "markdown_processor"
3
+ require_relative "markdown_file_writer"
4
+
5
+ module MarkdownRun
6
+ def self.run_code_blocks(input_file_path)
7
+ unless File.exist?(input_file_path) && File.readable?(input_file_path)
8
+ abort "Error: Input file '#{input_file_path}' not found or not readable."
9
+ end
10
+
11
+ temp_dir = File.dirname(File.expand_path(input_file_path))
12
+ file_enum = File.foreach(input_file_path, chomp: false).to_enum
13
+
14
+ processor = MarkdownProcessor.new(temp_dir, input_file_path)
15
+ output_lines = processor.process_file(file_enum)
16
+
17
+ # Write the modified content back to the input file
18
+ MarkdownFileWriter.write_output_to_file(output_lines, input_file_path)
19
+ end
20
+ end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdown-run
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.10
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-01 00:00:00.000000000 Z
11
+ date: 2025-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rcodetools
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.8.5
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.8.5
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: minitest
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -31,15 +31,44 @@ dependencies:
31
31
  - - '='
32
32
  - !ruby/object:Gem::Version
33
33
  version: 5.25.5
34
- type: :runtime
34
+ type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - '='
39
39
  - !ruby/object:Gem::Version
40
40
  version: 5.25.5
41
- description: Run code blocks in Markdown files for Ruby, JavaScript, sqlite and psql.
42
- Insert execution results next to the original code blocks.
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: flog
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Run code blocks in Markdown files for Ruby, JavaScript, sqlite, psql,
70
+ bash, zsh, and mermaid. Insert execution results next to the original code blocks.
71
+ Generate SVG diagrams from mermaid blocks.
43
72
  email:
44
73
  - 32635+aurelienbottazini@users.noreply.github.com
45
74
  executables:
@@ -48,15 +77,24 @@ extensions: []
48
77
  extra_rdoc_files: []
49
78
  files:
50
79
  - ".rubocop.yml"
80
+ - ".tool-versions"
51
81
  - CHANGELOG.md
52
82
  - CODE_OF_CONDUCT.md
53
83
  - LICENSE.txt
54
84
  - README.md
85
+ - Rakefile
55
86
  - exe/markdown-run
87
+ - lib/code_block_parser.rb
88
+ - lib/code_executor.rb
89
+ - lib/enum_helper.rb
90
+ - lib/execution_decider.rb
91
+ - lib/frontmatter_parser.rb
56
92
  - lib/language_configs.rb
57
93
  - lib/markdown/run/version.rb
94
+ - lib/markdown_file_writer.rb
95
+ - lib/markdown_processor.rb
96
+ - lib/markdown_run.rb
58
97
  - markdown-run-sample.md
59
- - test_markdown_exec.rb
60
98
  homepage: https://github.com/aurelienbottazini/markdown-run
61
99
  licenses:
62
100
  - MIT
@@ -79,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
117
  - !ruby/object:Gem::Version
80
118
  version: '0'
81
119
  requirements: []
82
- rubygems_version: 3.4.10
120
+ rubygems_version: 3.5.16
83
121
  signing_key:
84
122
  specification_version: 4
85
123
  summary: Run code blocks in Markdown files