markdown-run 0.1.7 → 0.1.9

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,262 @@
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
+ @frontmatter_parser = FrontmatterParser.new
20
+ @code_block_parser = CodeBlockParser.new(@frontmatter_parser)
21
+ end
22
+
23
+ def process_file(file_enum)
24
+ @frontmatter_parser.parse_frontmatter(file_enum, @output_lines)
25
+
26
+ loop do
27
+ current_line = get_next_line(file_enum)
28
+ break unless current_line
29
+
30
+ handle_line(current_line, file_enum)
31
+ end
32
+ @output_lines
33
+ end
34
+
35
+ private
36
+
37
+ def resolve_language(lang)
38
+ @frontmatter_parser.resolve_language(lang)
39
+ end
40
+
41
+ def ruby_style_result?(lang)
42
+ lang_config = SUPPORTED_LANGUAGES[lang]
43
+ lang_config && lang_config[:result_block_type] == "ruby"
44
+ end
45
+
46
+ def mermaid_style_result?(lang)
47
+ lang_config = SUPPORTED_LANGUAGES[lang]
48
+ lang_config && lang_config[:result_handling] == :mermaid_svg
49
+ end
50
+
51
+ def result_block_header(lang)
52
+ ruby_style_result?(lang) ? "```ruby RESULT\n" : "```RESULT\n"
53
+ end
54
+
55
+ def result_block_regex(lang)
56
+ if mermaid_style_result?(lang)
57
+ # For mermaid, look for existing image tags with .svg extension
58
+ /^!\[.*\]\(.*\.svg\)$/i
59
+ elsif ruby_style_result?(lang)
60
+ /^```ruby\s+RESULT$/i
61
+ else
62
+ /^```RESULT$/i
63
+ end
64
+ end
65
+
66
+ def is_block_end?(line)
67
+ @code_block_parser.is_block_end?(line)
68
+ end
69
+
70
+ def has_content?(content)
71
+ !content.strip.empty?
72
+ end
73
+
74
+ def add_result_block(result_output, blank_line_before_new_result)
75
+ if mermaid_style_result?(@current_block_lang)
76
+ # For mermaid, add the image tag directly without a result block
77
+ @output_lines << "\n" if blank_line_before_new_result.nil?
78
+ @output_lines << result_output
79
+ @output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
80
+ @output_lines << "\n"
81
+ else
82
+ @output_lines << "\n" if blank_line_before_new_result.nil?
83
+ @output_lines << result_block_header(@current_block_lang)
84
+ @output_lines << result_output
85
+ @output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
86
+ @output_lines << "```\n\n"
87
+ end
88
+ end
89
+
90
+ def line_matches_pattern?(line, pattern)
91
+ line && line.match?(pattern)
92
+ end
93
+
94
+ def is_blank_line?(line)
95
+ line && line.strip == ""
96
+ end
97
+
98
+ def parse_rerun_option(options_string)
99
+ @code_block_parser.parse_rerun_option(options_string)
100
+ end
101
+
102
+ def parse_run_option(options_string)
103
+ @code_block_parser.parse_run_option(options_string)
104
+ end
105
+
106
+ def handle_line(current_line, file_enum)
107
+ case @state
108
+ when :outside_code_block
109
+ handle_outside_code_block(current_line, file_enum)
110
+ when :inside_code_block
111
+ handle_inside_code_block(current_line, file_enum)
112
+ when :inside_result_block
113
+ handle_inside_result_block(current_line, file_enum)
114
+ end
115
+ end
116
+
117
+ def handle_outside_code_block(current_line, file_enum)
118
+ if @code_block_parser.is_ruby_result_block?(current_line)
119
+ handle_existing_ruby_result_block(current_line, file_enum)
120
+ else
121
+ parsed_header = @code_block_parser.parse_code_block_header(current_line)
122
+ if parsed_header && parsed_header[:is_supported]
123
+ start_code_block(current_line, parsed_header[:original_lang], parsed_header[:options_string])
124
+ else
125
+ @output_lines << current_line
126
+ end
127
+ end
128
+ end
129
+
130
+ def handle_inside_code_block(current_line, file_enum)
131
+ if is_block_end?(current_line)
132
+ end_code_block(current_line, file_enum)
133
+ else
134
+ accumulate_code_content(current_line)
135
+ end
136
+ end
137
+
138
+ def handle_inside_result_block(current_line, file_enum)
139
+ @output_lines << current_line
140
+ if is_block_end?(current_line)
141
+ @state = :outside_code_block
142
+ end
143
+ end
144
+
145
+ def handle_existing_ruby_result_block(current_line, file_enum)
146
+ warn "Found existing '```ruby RESULT' block, passing through."
147
+ @output_lines << current_line
148
+ @state = :inside_result_block
149
+ end
150
+
151
+ def start_code_block(current_line, lang, options_string = nil)
152
+ @output_lines << current_line
153
+ @current_block_lang = resolve_language(lang)
154
+ @current_block_rerun = parse_rerun_option(options_string)
155
+ @current_block_run = parse_run_option(options_string)
156
+ @state = :inside_code_block
157
+ @current_code_content = ""
158
+ end
159
+
160
+ def accumulate_code_content(current_line)
161
+ @current_code_content += current_line
162
+ @output_lines << current_line
163
+ end
164
+
165
+ def end_code_block(current_line, file_enum)
166
+ @output_lines << current_line
167
+
168
+ decision = decide_execution(file_enum)
169
+
170
+ if decision[:execute]
171
+ # If we consumed lines for rerun, don't add them to output (they'll be replaced)
172
+ execute_and_add_result(decision[:blank_line])
173
+ else
174
+ skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum)
175
+ end
176
+
177
+ reset_code_block_state
178
+ end
179
+
180
+ def decide_execution(file_enum)
181
+ decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang)
182
+ decision = decider.decide(file_enum, method(:result_block_regex))
183
+
184
+ # Handle the consume_existing flag for rerun scenarios
185
+ if decision[:consume_existing]
186
+ consume_existing_result_block(file_enum, decision[:consumed_lines])
187
+ end
188
+
189
+ decision
190
+ end
191
+
192
+ def execute_and_add_result(blank_line_before_new_result)
193
+ @output_lines << blank_line_before_new_result if blank_line_before_new_result
194
+
195
+ if has_content?(@current_code_content)
196
+ result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path)
197
+ add_result_block(result_output, blank_line_before_new_result)
198
+ else
199
+ warn "Skipping empty code block for language '#{@current_block_lang}'."
200
+ end
201
+ end
202
+
203
+ def skip_and_pass_through_result(lines_to_pass_through, file_enum)
204
+ # Handle run=false case where there are no lines to pass through
205
+ if lines_to_pass_through.empty?
206
+ warn "Skipping execution due to run=false option."
207
+ return
208
+ end
209
+
210
+ if mermaid_style_result?(@current_block_lang)
211
+ warn "Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution."
212
+ @output_lines.concat(lines_to_pass_through)
213
+ # For mermaid, no additional consumption needed since it's just an image line
214
+ else
215
+ lang_specific_result_type = ruby_style_result?(@current_block_lang) ? "```ruby RESULT" : "```RESULT"
216
+ warn "Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution."
217
+ @output_lines.concat(lines_to_pass_through)
218
+ consume_result_block_content(file_enum)
219
+ end
220
+ end
221
+
222
+ def consume_result_block_content(file_enum)
223
+ consume_block_lines(file_enum) do |line|
224
+ @output_lines << line
225
+ end
226
+ end
227
+
228
+ def consume_existing_result_block(file_enum, consumed_lines)
229
+ if mermaid_style_result?(@current_block_lang)
230
+ # For mermaid, there's no result block to consume, just the image line
231
+ # The image line should already be in consumed_lines from ExecutionDecider
232
+ return
233
+ end
234
+
235
+ consume_block_lines(file_enum) do |line|
236
+ consumed_lines << line
237
+ end
238
+ end
239
+
240
+ def consume_block_lines(file_enum)
241
+ begin
242
+ loop do
243
+ result_block_line = file_enum.next
244
+ yield result_block_line
245
+ break if is_block_end?(result_block_line)
246
+ end
247
+ rescue StopIteration
248
+ warn "Warning: End of file reached while consuming result block."
249
+ end
250
+ end
251
+
252
+ def reset_code_block_state
253
+ @state = :outside_code_block
254
+ @current_code_content = ""
255
+ @current_block_rerun = false
256
+ @current_block_run = true
257
+ end
258
+
259
+ def stderr_has_content?(stderr_output)
260
+ stderr_output && !stderr_output.strip.empty?
261
+ end
262
+ 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.7
4
+ version: 0.1.9
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-02 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
@@ -1,204 +0,0 @@
1
- require "bundler/inline"
2
- gemfile(true) do
3
- source "https://rubygems.org"
4
- gem "minitest", "5.25.5" # Specify the required version
5
- gem "rcodetools"
6
- end
7
-
8
- require "minitest/test"
9
- require "minitest/autorun"
10
- require "fileutils"
11
- require "tmpdir"
12
-
13
- # --- Minitest Test Class Definition ---
14
- class TestMarkdownExec < Minitest::Test
15
- def setup
16
- @temp_dir = Dir.mktmpdir("markdown_exec_tests")
17
- @test_md_file_path = File.join(@temp_dir, "test.md")
18
- end
19
-
20
- def teardown
21
- FileUtils.remove_entry @temp_dir if @temp_dir && Dir.exist?(@temp_dir)
22
- end
23
-
24
- def create_md_file(content)
25
- File.write(@test_md_file_path, content)
26
- @test_md_file_path
27
- end
28
-
29
- def read_md_file
30
- File.read(@test_md_file_path)
31
- end
32
-
33
- def test_script_runs_without_error_on_empty_file
34
- create_md_file("")
35
- assert process_markdown_file_main(@test_md_file_path), "Processing empty file should succeed"
36
- assert_equal "", read_md_file.strip, "Empty file should remain empty after processing"
37
- end
38
-
39
- def test_psql_block_execution
40
- skip "Skipping test_psql_block_execution on GitHub CI" if ENV['CI']
41
-
42
- md_content = <<~MARKDOWN
43
- ```psql
44
- SELECT 'hello psql test';
45
- ```
46
- MARKDOWN
47
- create_md_file(md_content)
48
- process_markdown_file_main(@test_md_file_path)
49
-
50
- expected_output = <<~MARKDOWN.strip
51
- ```psql
52
- SELECT 'hello psql test';
53
- ```
54
-
55
- ```RESULT
56
- hello psql test
57
- ```
58
- MARKDOWN
59
- assert_equal expected_output, read_md_file.strip
60
- end
61
-
62
- def test_ruby_block_execution_and_result_generation
63
- md_content = <<~MARKDOWN
64
- ```ruby
65
- puts "Hello from Ruby"
66
- p 1 + 2
67
- ```
68
- MARKDOWN
69
- create_md_file(md_content)
70
- process_markdown_file_main(@test_md_file_path)
71
-
72
- file_content = read_md_file
73
- assert file_content.include?("```ruby\nputs \"Hello from Ruby\""), "Original Ruby code should be present"
74
- assert file_content.include?("```ruby RESULT\n"), "Ruby RESULT block should be created"
75
- assert file_content.include?("3"), "Output from p 1 + 2 should be in the result"
76
- end
77
-
78
- def test_skip_execution_if_result_block_exists
79
- original_content = <<~MARKDOWN
80
- ```psql
81
- SELECT 'this should not run';
82
- ```
83
-
84
- ```RESULT
85
- pre-existing result
86
- ```
87
- MARKDOWN
88
- create_md_file(original_content)
89
- process_markdown_file_main(@test_md_file_path)
90
-
91
- assert_equal original_content.strip, read_md_file.strip, "Should not execute if RESULT block exists"
92
- end
93
-
94
- def test_skip_execution_if_ruby_result_block_exists
95
- original_content = <<~MARKDOWN
96
- ```ruby
97
- puts "this should not run either"
98
- ```
99
-
100
- ```ruby RESULT
101
- this is a pre-existing ruby result
102
- ```
103
- MARKDOWN
104
- create_md_file(original_content)
105
- process_markdown_file_main(@test_md_file_path)
106
-
107
- assert_equal original_content.strip, read_md_file.strip, "Should not execute if ```ruby RESULT block exists"
108
- end
109
-
110
- def test_frontmatter_alias_functionality
111
- skip "Skipping test_frontmatter_alias_functionality on GitHub CI" if ENV['CI']
112
-
113
- md_content = <<~MARKDOWN
114
- ---
115
- markdown-run:
116
- alias:
117
- - sql: psql
118
- ---
119
-
120
- # Test Document
121
-
122
- ```sql
123
- SELECT 'aliased to psql' as test;
124
- ```
125
- MARKDOWN
126
- create_md_file(md_content)
127
- process_markdown_file_main(@test_md_file_path)
128
-
129
- file_content = read_md_file
130
- assert file_content.include?("```sql\nSELECT 'aliased to psql' as test;"), "Original SQL code should be present"
131
- assert file_content.include?("```RESULT\n"), "RESULT block should be created for aliased language"
132
- assert file_content.include?("aliased to psql"), "Output should contain the expected result"
133
- end
134
-
135
- def test_rerun_functionality
136
- # Test 1: Default behavior (no rerun option) should skip existing result
137
- md_content_with_result = <<~MARKDOWN
138
- ```ruby
139
- puts "Should not change: \#{Time.now.to_i}"
140
- ```
141
-
142
- ```ruby RESULT
143
- Should not change: 999999999
144
- ```
145
- MARKDOWN
146
- create_md_file(md_content_with_result)
147
- process_markdown_file_main(@test_md_file_path)
148
-
149
- file_content = read_md_file
150
- assert file_content.include?("Should not change: 999999999"), "Default behavior should preserve existing result"
151
- refute file_content.match?(/Should not change: (?!999999999)\d+/), "Default behavior should not generate new timestamp"
152
-
153
- # Test 2: rerun=false should skip existing result
154
- md_content_rerun_false = <<~MARKDOWN
155
- ```ruby rerun=false
156
- puts "Should not change either: \#{Time.now.to_i}"
157
- ```
158
-
159
- ```ruby RESULT
160
- Should not change either: 888888888
161
- ```
162
- MARKDOWN
163
- create_md_file(md_content_rerun_false)
164
- process_markdown_file_main(@test_md_file_path)
165
-
166
- file_content = read_md_file
167
- assert file_content.include?("Should not change either: 888888888"), "rerun=false should preserve existing result"
168
- refute file_content.match?(/Should not change either: (?!888888888)\d+/), "rerun=false should not generate new timestamp"
169
-
170
- # Test 3: rerun=true should replace existing result
171
- md_content_rerun_true = <<~MARKDOWN
172
- ```ruby rerun=true
173
- puts "Should change: \#{Time.now.to_i}"
174
- ```
175
-
176
- ```ruby RESULT
177
- Should change: 777777777
178
- ```
179
- MARKDOWN
180
- create_md_file(md_content_rerun_true)
181
- process_markdown_file_main(@test_md_file_path)
182
-
183
- file_content = read_md_file
184
- refute file_content.include?("Should change: 777777777"), "rerun=true should replace existing result"
185
- assert file_content.match?(/Should change: \d+/), "rerun=true should generate new result with actual timestamp"
186
-
187
- # Test 4: rerun=true with blank line before result block
188
- md_content_rerun_true_blank = <<~MARKDOWN
189
- ```ruby rerun=true
190
- puts "Should also change: \#{Time.now.to_i}"
191
- ```
192
-
193
- ```ruby RESULT
194
- Should also change: 666666666
195
- ```
196
- MARKDOWN
197
- create_md_file(md_content_rerun_true_blank)
198
- process_markdown_file_main(@test_md_file_path)
199
-
200
- file_content = read_md_file
201
- refute file_content.include?("Should also change: 666666666"), "rerun=true with blank line should replace existing result"
202
- assert file_content.match?(/Should also change: \d+/), "rerun=true with blank line should generate new result"
203
- end
204
- end