markdown-run 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cb82e5ea1c357a231ca30c1802c103456733e8a1d76256008dbf8d8af34f1703
4
+ data.tar.gz: c25a53287f14c41a184f46a436e1f08266c184c834c7272c6b936f75652a59b3
5
+ SHA512:
6
+ metadata.gz: 153076de8074dfb3a5407380eeb0419980528f46714375ccca7aa05ed26d57fc10da029b790cd193c3076263bbf28ce8484833c7faeb7591c26c14b1923a499b
7
+ data.tar.gz: cd8ef15078084c0994f68face1f523328afed9557c9e5add38375a1c1a60e72ff8279803b63e6560b5a1748ef38e5128e01adf8909613d4d9fe446fd68fc6ae5
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-05-13
4
+
5
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Aurélien Bottazini
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Markdown::Run
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/markdown/run`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/aurelienbottazini/markdown-run. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/aurelienbottazini/markdown-run/blob/main/CODE_OF_CONDUCT.md).
32
+
33
+ ## License
34
+
35
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
36
+
37
+ ## Code of Conduct
38
+
39
+ Everyone interacting in the Markdown::Run project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/aurelienbottazini/markdown-run/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
data/exe/markdown-run ADDED
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'tempfile'
4
+ require 'open3'
5
+ require 'fileutils'
6
+
7
+ # --- Language Execution Configuration ---
8
+ JS_CONFIG = {
9
+ command: ->(_code_content, temp_file_path) { [ "bun #{temp_file_path}", {} ] },
10
+ temp_file_suffix: '.js',
11
+ error_handling: :js_specific # For specific stderr appending on error
12
+ }.freeze
13
+
14
+ SQLITE_CONFIG = {
15
+ command: ->(code_content, temp_file_path) { [ "sqlite3 #{temp_file_path}", { stdin_data: code_content } ] },
16
+ temp_file_suffix: '.db' # Temp file is the database
17
+ }.freeze
18
+
19
+ SUPPORTED_LANGUAGES = {
20
+ 'psql' => {
21
+ command: ->(code_content, _temp_file_path) { [ "psql -A -t -X", { stdin_data: code_content } ] }
22
+ },
23
+ 'ruby' => {
24
+ command: ->(_code_content, temp_file_path) { [ "xmpfilter #{temp_file_path}", {} ] },
25
+ temp_file_suffix: '.rb',
26
+ result_block_type: 'ruby' # For special '```ruby RESULT' blocks
27
+ },
28
+ 'js' => JS_CONFIG,
29
+ 'javascript' => JS_CONFIG, # Alias for js
30
+ 'sqlite' => SQLITE_CONFIG,
31
+ 'sqlite3' => SQLITE_CONFIG # Alias for sqlite
32
+ }.freeze
33
+
34
+ LANGUAGE_REGEX_PART = SUPPORTED_LANGUAGES.keys.map { |lang| Regexp.escape(lang) }.join('|').freeze
35
+ CODE_BLOCK_START_REGEX = /^```(#{LANGUAGE_REGEX_PART})$/i.freeze
36
+ # --- End Language Execution Configuration ---
37
+
38
+ if ARGV.empty?
39
+ require 'bundler/inline'
40
+ gemfile(true) do
41
+ source 'https://rubygems.org'
42
+ gem 'minitest' # You can specify a version like '~> 5.10' if needed
43
+ end
44
+
45
+ puts "Running tests..."
46
+ require 'minitest/spec'
47
+ require 'minitest/autorun'
48
+ end
49
+
50
+ # Script to process markdown files, execute code blocks based on language,
51
+ # and insert their results back into the markdown.
52
+
53
+ def execute_code_block(code_content, lang, temp_dir)
54
+ result_output = ""
55
+ stderr_output = ""
56
+ exit_status = 0
57
+ captured_stdout, captured_stderr, captured_status_obj = nil, nil, nil
58
+
59
+ lang_key = lang.downcase # Normalize lang input for lookup
60
+ lang_config = SUPPORTED_LANGUAGES[lang_key]
61
+
62
+ if lang_config
63
+ warn "Executing #{lang_key} code block..." # Generic description
64
+ cmd_lambda = lang_config[:command]
65
+ temp_file_suffix = lang_config[:temp_file_suffix]
66
+
67
+ # Determine command and options using the lambda
68
+ # The lambda receives code_content and a potential temp_file_path
69
+ # It returns [command_string, options_hash_for_open3]
70
+
71
+ if temp_file_suffix # Needs a temporary file. Use lang_key as prefix.
72
+ Tempfile.create([lang_key, temp_file_suffix], temp_dir) do |temp_file|
73
+ temp_file.write(code_content)
74
+ temp_file.close
75
+ # Pass temp_file.path. Lambda decides if it needs code_content directly.
76
+ command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path)
77
+ captured_stdout, captured_stderr, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
78
+ end
79
+ else # Direct command execution (e.g., psql that takes stdin)
80
+ # Pass nil for temp_file_path. Lambda decides if it needs code_content.
81
+ command_to_run, exec_options = cmd_lambda.call(code_content, nil)
82
+ captured_stdout, captured_stderr, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
83
+ end
84
+ else
85
+ warn "Unsupported language: #{lang}"
86
+ result_output = "ERROR: Unsupported language: #{lang}"
87
+ exit_status = 1 # Indicate an error
88
+ # captured_status_obj remains nil, so common assignments below won't run
89
+ end
90
+
91
+ # Common assignment logic for cases that used Open3.capture3
92
+ if captured_status_obj
93
+ result_output = captured_stdout
94
+ stderr_output = captured_stderr
95
+ exit_status = captured_status_obj.exitstatus
96
+
97
+ # JS-specific: Append stderr to result if execution failed and stderr has content
98
+ if lang_config && lang_config[:error_handling] == :js_specific && exit_status != 0 && stderr_output && !stderr_output.strip.empty?
99
+ result_output += "\nStderr:\n#{stderr_output.strip}" # Ensure stripping
100
+ end
101
+ end
102
+
103
+ # Common error message enhancement
104
+ if exit_status != 0
105
+ warn "Code execution failed for language '#{lang_key}' with status #{exit_status}."
106
+ warn "Stderr:\n#{stderr_output}" if stderr_output && !stderr_output.strip.empty?
107
+
108
+ is_js_error_already_formatted = lang_config && lang_config[:error_handling] == :js_specific && result_output.include?("Stderr:")
109
+ unless result_output.downcase.include?("error:") || is_js_error_already_formatted
110
+ error_prefix = "Execution failed (status: #{exit_status})."
111
+ if stderr_output && !stderr_output.strip.empty?
112
+ error_prefix += " Stderr: #{stderr_output.strip}"
113
+ end
114
+ result_output = "#{error_prefix}\n#{result_output}"
115
+ end
116
+ end
117
+ result_output
118
+ end
119
+
120
+ def process_markdown_file_main(input_file_path)
121
+ unless File.exist?(input_file_path) && File.readable?(input_file_path)
122
+ warn "Error: Input file '#{input_file_path}' not found or not readable."
123
+ return false # Indicate failure
124
+ end
125
+
126
+ temp_dir = File.dirname(File.expand_path(input_file_path))
127
+ output_lines = []
128
+ file_enum = File.foreach(input_file_path, chomp: false).to_enum
129
+
130
+ in_code_block = false
131
+ current_block_lang = ""
132
+ current_code_content = ""
133
+
134
+ loop do
135
+ current_line = nil
136
+ begin
137
+ current_line = file_enum.next
138
+ rescue StopIteration
139
+ break # End of file
140
+ end
141
+
142
+ if !in_code_block
143
+ # Case 1: A ```ruby RESULT block encountered directly
144
+ if current_line.match?(/^```ruby\s+RESULT$/i)
145
+ warn "Found existing '```ruby RESULT' block, passing through."
146
+ output_lines << current_line # The ```ruby RESULT line
147
+ begin
148
+ loop do
149
+ block_line = file_enum.next
150
+ output_lines << block_line
151
+ break if block_line.strip == '```'
152
+ end
153
+ rescue StopIteration
154
+ warn "Warning: End of file reached inside a '```ruby RESULT' block."
155
+ break # Exit main loop, as the file is malformed or ended abruptly
156
+ end
157
+ next # Continue to the next line from the file
158
+ # Case 2: Start of a new executable code block ```lang
159
+ elsif (match_data = current_line.match(CODE_BLOCK_START_REGEX))
160
+ output_lines << current_line # The opening ```lang line
161
+ current_block_lang = match_data[1].downcase
162
+ in_code_block = true
163
+ current_code_content = "" # Reset for the new block
164
+ next # Continue to the next line from the file
165
+ # Case 3: Any other line (normal text, non-executable ```foo, or generic ```RESULT not after an exec block)
166
+ else
167
+ output_lines << current_line
168
+ next # Continue to the next line from the file
169
+ end
170
+ else # We are in_code_block (current_block_lang is set, current_code_content is accumulating)
171
+ # Case 4: End of the current code block (```)
172
+ if current_line.strip == '```'
173
+ output_lines << current_line # The closing ``` of the code block
174
+
175
+ execute_this_block = true # Assume execution by default
176
+ lines_to_pass_through_if_skipped = [] # For blank line + RESULT header if skipping
177
+ blank_line_before_new_result = nil # If a blank line is consumed but block is still executed
178
+
179
+ # Peek ahead logic
180
+ peek1 = nil
181
+ begin; peek1 = file_enum.peek; rescue StopIteration; end # If EOF, execute_this_block remains true
182
+
183
+ current_lang_config = SUPPORTED_LANGUAGES[current_block_lang] # Get config for the current block
184
+ is_ruby_style_result = current_lang_config && current_lang_config[:result_block_type] == 'ruby'
185
+
186
+ expected_header_regex = is_ruby_style_result ? /^```ruby\s+RESULT$/i : /^```RESULT$/i
187
+
188
+ if peek1 && peek1.match?(expected_header_regex)
189
+ execute_this_block = false
190
+ lines_to_pass_through_if_skipped << file_enum.next # Consume RESULT header
191
+ elsif peek1 && peek1.strip == "" # Blank line detected
192
+ consumed_blank_line = file_enum.next # Consume the blank line from enum
193
+
194
+ peek2 = nil
195
+ begin; peek2 = file_enum.peek; rescue StopIteration; end # EOF after blank line
196
+
197
+ if peek2 && peek2.match?(expected_header_regex)
198
+ execute_this_block = false
199
+ lines_to_pass_through_if_skipped << consumed_blank_line # Add consumed blank line
200
+ lines_to_pass_through_if_skipped << file_enum.next # Consume RESULT header
201
+ else
202
+ # Blank line, but not followed by a RESULT header (or EOF after blank).
203
+ # We will execute the block. The consumed_blank_line should be outputted.
204
+ blank_line_before_new_result = consumed_blank_line
205
+ # execute_this_block remains true
206
+ end
207
+ end
208
+ # If peek1 was nil (EOF) or something else not matching, execute_this_block remains true.
209
+
210
+ if execute_this_block
211
+ output_lines << blank_line_before_new_result if blank_line_before_new_result
212
+ if !current_code_content.strip.empty?
213
+ warn "Executing #{current_block_lang} code block..."
214
+ result_output = execute_code_block(current_code_content, current_block_lang, temp_dir)
215
+
216
+ output_lines << "\n" if blank_line_before_new_result.nil?
217
+ output_lines << (is_ruby_style_result ? "```ruby RESULT\n" : "```RESULT\n")
218
+ output_lines << result_output
219
+ output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
220
+ output_lines << "```\n\n" # Ensures a blank line after the RESULT block
221
+ else
222
+ warn "Skipping empty code block for language '#{current_block_lang}'."
223
+ end
224
+ else # Do not execute; a RESULT block follows and was identified
225
+ lang_specific_result_type = is_ruby_style_result ? "```ruby RESULT" : "```RESULT"
226
+ warn "Found existing '#{lang_specific_result_type}' block for current #{current_block_lang} block, skipping execution."
227
+
228
+ output_lines.concat(lines_to_pass_through_if_skipped) # Add blank line (if any) and RESULT header
229
+
230
+ # Consume the rest of the RESULT block content until its closing ```
231
+ begin
232
+ loop do
233
+ result_block_line = file_enum.next
234
+ output_lines << result_block_line
235
+ break if result_block_line.strip == '```'
236
+ end
237
+ rescue StopIteration
238
+ warn "Warning: End of file reached inside a skipped 'RESULT' block."
239
+ break # Exit main loop
240
+ end
241
+ end
242
+
243
+ in_code_block = false
244
+ current_code_content = "" # Reset for next block
245
+ next # Continue to the next line from the file
246
+ # Case 5: Line inside an active code block
247
+ else
248
+ current_code_content += current_line
249
+ output_lines << current_line # This line is part of the original code block being recorded
250
+ next # Continue to the next line from the file
251
+ end
252
+ end
253
+ end # loop
254
+
255
+ # Write the modified content back to the input file
256
+ Tempfile.create([ 'md_exec_out_', File.extname(input_file_path) ], temp_dir) do |temp_output_file|
257
+ temp_output_file.write(output_lines.join(''))
258
+ temp_output_file.close
259
+ begin
260
+ FileUtils.mv(temp_output_file.path, input_file_path)
261
+ rescue Errno::EACCES, Errno::EXDEV
262
+ warn "Atomic move failed. Falling back to copy and delete."
263
+ FileUtils.cp(temp_output_file.path, input_file_path)
264
+ FileUtils.rm_f(temp_output_file.path)
265
+ end
266
+ end
267
+ warn "Markdown processing complete. Output written to #{input_file_path}"
268
+ true # Indicate success
269
+ end
270
+
271
+ # --- Minitest Test Class Definition ---
272
+ class TestMarkdownExec < Minitest::Test
273
+ def setup
274
+ @temp_dir = Dir.mktmpdir("markdown_exec_tests")
275
+ @test_md_file_path = File.join(@temp_dir, "test.md")
276
+ end
277
+
278
+ def teardown
279
+ FileUtils.remove_entry @temp_dir if @temp_dir && Dir.exist?(@temp_dir)
280
+ end
281
+
282
+ def create_md_file(content)
283
+ File.write(@test_md_file_path, content)
284
+ @test_md_file_path
285
+ end
286
+
287
+ def read_md_file
288
+ File.read(@test_md_file_path)
289
+ end
290
+
291
+ def test_script_runs_without_error_on_empty_file
292
+ create_md_file("")
293
+ assert process_markdown_file_main(@test_md_file_path), "Processing empty file should succeed"
294
+ assert_equal "", read_md_file.strip, "Empty file should remain empty after processing"
295
+ end
296
+
297
+ def test_psql_block_execution
298
+ md_content = <<~MARKDOWN
299
+ ```psql
300
+ SELECT 'hello psql test';
301
+ ```
302
+ MARKDOWN
303
+ create_md_file(md_content)
304
+ process_markdown_file_main(@test_md_file_path)
305
+
306
+ expected_output = <<~MARKDOWN.strip
307
+ ```psql
308
+ SELECT 'hello psql test';
309
+ ```
310
+
311
+ ```RESULT
312
+ hello psql test
313
+ ```
314
+ MARKDOWN
315
+ assert_equal expected_output, read_md_file.strip
316
+ end
317
+
318
+ def test_ruby_block_execution_and_result_generation
319
+ md_content = <<~MARKDOWN
320
+ ```ruby
321
+ puts "Hello from Ruby"
322
+ p 1 + 2
323
+ ```
324
+ MARKDOWN
325
+ create_md_file(md_content)
326
+ process_markdown_file_main(@test_md_file_path)
327
+
328
+ file_content = read_md_file
329
+ assert file_content.include?("```ruby\nputs \"Hello from Ruby\""), "Original Ruby code should be present"
330
+ assert file_content.include?("```ruby RESULT\n"), "Ruby RESULT block should be created"
331
+ assert file_content.include?("3"), "Output from p 1 + 2 should be in the result"
332
+ end
333
+
334
+ def test_skip_execution_if_result_block_exists
335
+ original_content = <<~MARKDOWN
336
+ ```psql
337
+ SELECT 'this should not run';
338
+ ```
339
+
340
+ ```RESULT
341
+ pre-existing result
342
+ ```
343
+ MARKDOWN
344
+ create_md_file(original_content)
345
+ process_markdown_file_main(@test_md_file_path)
346
+
347
+ assert_equal original_content.strip, read_md_file.strip, "Should not execute if RESULT block exists"
348
+ end
349
+
350
+ def test_skip_execution_if_ruby_result_block_exists
351
+ original_content = <<~MARKDOWN
352
+ ```ruby
353
+ puts "this should not run either"
354
+ ```
355
+
356
+ ```ruby RESULT
357
+ this is a pre-existing ruby result
358
+ ```
359
+ MARKDOWN
360
+ create_md_file(original_content)
361
+ process_markdown_file_main(@test_md_file_path)
362
+
363
+ assert_equal original_content.strip, read_md_file.strip, "Should not execute if ```ruby RESULT block exists"
364
+ end
365
+ end
366
+
367
+ unless ARGV.empty?
368
+ process_markdown_file_main(ARGV[0])
369
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markdown
4
+ module Run
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "run/version"
4
+
5
+ module Markdown
6
+ module Run
7
+ class Error < StandardError; end
8
+ # Your code goes here...
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ module Markdown
2
+ module Run
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: markdown-run
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Aurélien Bottazini
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-05-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Run code blocks in Markdown files. Store output in a code block following
14
+ the original code block.
15
+ email:
16
+ - 32635+aurelienbottazini@users.noreply.github.com
17
+ executables:
18
+ - markdown-run
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".rubocop.yml"
23
+ - CHANGELOG.md
24
+ - CODE_OF_CONDUCT.md
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - exe/markdown-run
29
+ - lib/markdown/run.rb
30
+ - lib/markdown/run/version.rb
31
+ - sig/markdown/run.rbs
32
+ homepage: https://github.com/aurelienbottazini/markdown-run
33
+ licenses:
34
+ - MIT
35
+ metadata:
36
+ homepage_uri: https://github.com/aurelienbottazini/markdown-run
37
+ source_code_uri: https://github.com/aurelienbottazini/markdown-run
38
+ changelog_uri: https://github.com/aurelienbottazini/markdown-run/blob/main/CHANGELOG.md
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.0.0
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.4.10
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: Run code blocks in Markdown files
58
+ test_files: []