markdown-run 0.1.8 → 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.
- checksums.yaml +4 -4
- data/.tool-versions +1 -0
- data/CHANGELOG.md +4 -0
- data/README.md +19 -0
- data/Rakefile +22 -0
- data/exe/markdown-run +6 -433
- data/lib/code_block_parser.rb +60 -0
- data/lib/code_executor.rb +137 -0
- data/lib/enum_helper.rb +17 -0
- data/lib/execution_decider.rb +97 -0
- data/lib/frontmatter_parser.rb +72 -0
- data/lib/language_configs.rb +42 -7
- data/lib/markdown/run/version.rb +1 -1
- data/lib/markdown_file_writer.rb +25 -0
- data/lib/markdown_processor.rb +262 -0
- data/lib/markdown_run.rb +20 -0
- metadata +49 -11
- data/test_markdown_exec.rb +0 -297
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d87607302aa1e1f4b95d1da2b1857f792d34b3d3d7985299db1a6730d7f49b57
|
4
|
+
data.tar.gz: d10dd3193558ce3c59770bd5b9ba7f73e8a5f3459cea9482deeff7bfc21333a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bbf13073d892d85165b1396a78ec6f274cb75a941fd8dc50f21b4c9636572966c2983f001851286fcb14531eed0a3d2177df42f8d1851e98f62175c5b83c78e
|
7
|
+
data.tar.gz: 97f9f35734c1440d8b21cc62176a65458bce898cb223368c65af4e0888f4bbd986dc6e403a382f982561dc6ad572f4f9c24c3f2f41d8c0aa6186cc6d5d25f656
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.3.5
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -16,6 +16,7 @@ Supported languages:
|
|
16
16
|
- postgresql
|
17
17
|
- bash
|
18
18
|
- zsh
|
19
|
+
- mermaid (generates SVG diagrams)
|
19
20
|
|
20
21
|
## Installation
|
21
22
|
|
@@ -73,6 +74,24 @@ console.log("This will re-execute even if result exists");
|
|
73
74
|
console.log("This will execute only if no result exists");
|
74
75
|
```
|
75
76
|
|
77
|
+
### Mermaid diagrams
|
78
|
+
|
79
|
+
Mermaid blocks generate SVG files and insert markdown image tags:
|
80
|
+
|
81
|
+
```mermaid
|
82
|
+
graph TD
|
83
|
+
A[Start] --> B[Process]
|
84
|
+
B --> C[End]
|
85
|
+
```
|
86
|
+
|
87
|
+
This generates an SVG file in a directory named after the markdown file, with a filename that includes the markdown file's basename and a timestamp:
|
88
|
+
|
89
|
+
- Directory: `my-document/` (if the markdown file is `my-document.md`)
|
90
|
+
- Filename: `my-document-20250602-215507-a1289a799c29.svg`
|
91
|
+
- Image tag: ``
|
92
|
+
|
93
|
+
Note: Requires `@mermaid-js/mermaid-cli` to be installed: `npm install -g @mermaid-js/mermaid-cli`
|
94
|
+
|
76
95
|
## Frontmatter
|
77
96
|
|
78
97
|
You can add a yaml frontmatter to redefine code block behavior.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "rake/testtask"
|
2
|
+
|
3
|
+
Rake::TestTask.new(:test) do |t|
|
4
|
+
t.libs << "test"
|
5
|
+
t.libs << "lib"
|
6
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
7
|
+
t.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Run flog complexity analysis on the entire project"
|
11
|
+
task :flog do
|
12
|
+
puts "Running flog complexity analysis..."
|
13
|
+
system("flog lib/ exe/ test/")
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Run flog with detailed method breakdown"
|
17
|
+
task :flog_detailed do
|
18
|
+
puts "Running detailed flog analysis..."
|
19
|
+
system("flog -d lib/ exe/ test/")
|
20
|
+
end
|
21
|
+
|
22
|
+
task default: :test
|
data/exe/markdown-run
CHANGED
@@ -1,443 +1,16 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
require "open3"
|
5
|
-
require "fileutils"
|
6
|
-
require "yaml"
|
7
|
-
require_relative "../lib/language_configs"
|
3
|
+
require_relative "../lib/markdown_run"
|
8
4
|
|
9
5
|
# Script to process markdown files, execute code blocks based on language,
|
10
6
|
# and insert their results back into the markdown.
|
11
7
|
|
12
|
-
def stderr_has_content?(stderr_output)
|
13
|
-
stderr_output && !stderr_output.strip.empty?
|
14
|
-
end
|
15
|
-
|
16
|
-
def format_captured_output(captured_status_obj, captured_stderr, captured_stdout, lang_config)
|
17
|
-
result_output = captured_stdout
|
18
|
-
stderr_output = captured_stderr
|
19
|
-
exit_status = captured_status_obj.exitstatus
|
20
|
-
|
21
|
-
# JS-specific: Append stderr to result if execution failed and stderr has content
|
22
|
-
if lang_config && lang_config[:error_handling] == :js_specific && exit_status != 0 && stderr_has_content?(stderr_output)
|
23
|
-
result_output += "\nStderr:\n#{stderr_output.strip}"
|
24
|
-
end
|
25
|
-
return exit_status, result_output, stderr_output
|
26
|
-
end
|
27
|
-
|
28
|
-
def add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
|
29
|
-
warn "Code execution failed for language '#{lang_key}' with status #{exit_status}."
|
30
|
-
warn "Stderr:\n#{stderr_output}" if stderr_has_content?(stderr_output)
|
31
|
-
|
32
|
-
is_js_error_already_formatted = lang_config && lang_config[:error_handling] == :js_specific && result_output.include?("Stderr:")
|
33
|
-
unless result_output.downcase.include?("error:") || is_js_error_already_formatted
|
34
|
-
error_prefix = "Execution failed (status: #{exit_status})."
|
35
|
-
error_prefix += " Stderr: #{stderr_output.strip}" if stderr_has_content?(stderr_output)
|
36
|
-
result_output = "#{error_prefix}\n#{result_output}"
|
37
|
-
end
|
38
|
-
result_output
|
39
|
-
end
|
40
|
-
|
41
|
-
def execute_code_block(code_content, lang, temp_dir)
|
42
|
-
captured_status_obj = nil
|
43
|
-
|
44
|
-
lang_key = lang.downcase # Normalize lang input for lookup
|
45
|
-
lang_config = SUPPORTED_LANGUAGES[lang_key]
|
46
|
-
|
47
|
-
if lang_config
|
48
|
-
exit_status = 0
|
49
|
-
warn "Executing #{lang_key} code block..." # Generic description
|
50
|
-
cmd_lambda = lang_config[:command]
|
51
|
-
temp_file_suffix = lang_config[:temp_file_suffix]
|
52
|
-
|
53
|
-
captured_stdout = nil
|
54
|
-
if temp_file_suffix # Needs a temporary file. Use lang_key as prefix.
|
55
|
-
Tempfile.create([ lang_key, temp_file_suffix ], temp_dir) do |temp_file|
|
56
|
-
temp_file.write(code_content)
|
57
|
-
temp_file.close
|
58
|
-
# Pass temp_file.path. Lambda decides if it needs code_content directly.
|
59
|
-
command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path)
|
60
|
-
captured_stdout, _, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
|
61
|
-
end
|
62
|
-
else # Direct command execution (e.g., psql that takes stdin)
|
63
|
-
# Pass nil for temp_file_path. Lambda decides if it needs code_content.
|
64
|
-
command_to_run, exec_options = cmd_lambda.call(code_content, nil)
|
65
|
-
captured_stdout, captured_stderr, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
|
66
|
-
end
|
67
|
-
else
|
68
|
-
warn "Unsupported language: #{lang}"
|
69
|
-
result_output = "ERROR: Unsupported language: #{lang}"
|
70
|
-
exit_status = 1 # Indicate an error
|
71
|
-
# captured_status_obj remains nil, so common assignments below won't run
|
72
|
-
end
|
73
|
-
|
74
|
-
if captured_status_obj
|
75
|
-
exit_status, result_output, stderr_output = format_captured_output(captured_status_obj, captured_stderr, captured_stdout, lang_config)
|
76
|
-
end
|
77
|
-
|
78
|
-
if exit_status != 0
|
79
|
-
result_output = add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
|
80
|
-
end
|
81
|
-
result_output
|
82
|
-
end
|
83
|
-
|
84
|
-
class MarkdownProcessor
|
85
|
-
def initialize(temp_dir)
|
86
|
-
@temp_dir = temp_dir
|
87
|
-
@output_lines = []
|
88
|
-
@state = :outside_code_block
|
89
|
-
@current_block_lang = ""
|
90
|
-
@current_code_content = ""
|
91
|
-
@current_block_rerun = false
|
92
|
-
@current_block_run = true
|
93
|
-
@aliases = {}
|
94
|
-
end
|
95
|
-
|
96
|
-
def process_file(file_enum)
|
97
|
-
parse_frontmatter(file_enum)
|
98
|
-
|
99
|
-
loop do
|
100
|
-
current_line = get_next_line(file_enum)
|
101
|
-
break unless current_line
|
102
|
-
|
103
|
-
handle_line(current_line, file_enum)
|
104
|
-
end
|
105
|
-
@output_lines
|
106
|
-
end
|
107
|
-
|
108
|
-
private
|
109
|
-
|
110
|
-
def parse_frontmatter(file_enum)
|
111
|
-
first_line = peek_next_line(file_enum)
|
112
|
-
return unless first_line&.strip == "---"
|
113
|
-
|
114
|
-
# Consume the opening ---
|
115
|
-
@output_lines << file_enum.next
|
116
|
-
frontmatter_lines = []
|
117
|
-
|
118
|
-
loop do
|
119
|
-
line = get_next_line(file_enum)
|
120
|
-
break unless line
|
121
|
-
|
122
|
-
if line.strip == "---"
|
123
|
-
@output_lines << line
|
124
|
-
break
|
125
|
-
end
|
126
|
-
|
127
|
-
frontmatter_lines << line
|
128
|
-
@output_lines << line
|
129
|
-
end
|
130
|
-
|
131
|
-
return if frontmatter_lines.empty?
|
132
|
-
|
133
|
-
begin
|
134
|
-
frontmatter = YAML.safe_load(frontmatter_lines.join)
|
135
|
-
extract_aliases(frontmatter) if frontmatter.is_a?(Hash)
|
136
|
-
rescue YAML::SyntaxError => e
|
137
|
-
warn "Warning: Invalid YAML frontmatter: #{e.message}"
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def extract_aliases(frontmatter)
|
142
|
-
markdown_run_config = frontmatter["markdown-run"]
|
143
|
-
return unless markdown_run_config.is_a?(Hash)
|
144
|
-
|
145
|
-
aliases = markdown_run_config["alias"]
|
146
|
-
return unless aliases.is_a?(Array)
|
147
|
-
|
148
|
-
aliases.each do |alias_config|
|
149
|
-
next unless alias_config.is_a?(Hash)
|
150
|
-
|
151
|
-
alias_config.each do |alias_name, target_lang|
|
152
|
-
@aliases[alias_name.to_s] = target_lang.to_s
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
def resolve_language(lang)
|
158
|
-
@aliases[lang] || lang
|
159
|
-
end
|
160
|
-
|
161
|
-
def ruby_style_result?(lang)
|
162
|
-
lang_config = SUPPORTED_LANGUAGES[lang]
|
163
|
-
lang_config && lang_config[:result_block_type] == "ruby"
|
164
|
-
end
|
165
|
-
|
166
|
-
def result_block_header(lang)
|
167
|
-
ruby_style_result?(lang) ? "```ruby RESULT\n" : "```RESULT\n"
|
168
|
-
end
|
169
|
-
|
170
|
-
def result_block_regex(lang)
|
171
|
-
ruby_style_result?(lang) ? /^```ruby\s+RESULT$/i : /^```RESULT$/i
|
172
|
-
end
|
173
|
-
|
174
|
-
def is_block_end?(line)
|
175
|
-
line.strip == "```"
|
176
|
-
end
|
177
|
-
|
178
|
-
def has_content?(content)
|
179
|
-
!content.strip.empty?
|
180
|
-
end
|
181
|
-
|
182
|
-
def add_result_block(result_output, blank_line_before_new_result)
|
183
|
-
@output_lines << "\n" if blank_line_before_new_result.nil?
|
184
|
-
@output_lines << result_block_header(@current_block_lang)
|
185
|
-
@output_lines << result_output
|
186
|
-
@output_lines << "\n" unless result_output.empty? || result_output.end_with?("\n")
|
187
|
-
@output_lines << "```\n\n"
|
188
|
-
end
|
189
|
-
|
190
|
-
def line_matches_pattern?(line, pattern)
|
191
|
-
line && line.match?(pattern)
|
192
|
-
end
|
193
|
-
|
194
|
-
def is_blank_line?(line)
|
195
|
-
line && line.strip == ""
|
196
|
-
end
|
197
|
-
|
198
|
-
def parse_rerun_option(options_string)
|
199
|
-
return false unless options_string
|
200
|
-
|
201
|
-
# Match rerun=true or rerun=false
|
202
|
-
match = options_string.match(/rerun\s*=\s*(true|false)/i)
|
203
|
-
return false unless match
|
204
|
-
|
205
|
-
match[1].downcase == "true"
|
206
|
-
end
|
207
|
-
|
208
|
-
def parse_run_option(options_string)
|
209
|
-
return true unless options_string
|
210
|
-
|
211
|
-
# Match run=true or run=false
|
212
|
-
match = options_string.match(/run\s*=\s*(true|false)/i)
|
213
|
-
return true unless match
|
214
|
-
|
215
|
-
match[1].downcase == "true"
|
216
|
-
end
|
217
|
-
|
218
|
-
def safe_enum_operation(file_enum, operation)
|
219
|
-
file_enum.send(operation)
|
220
|
-
rescue StopIteration
|
221
|
-
nil
|
222
|
-
end
|
223
|
-
|
224
|
-
def get_next_line(file_enum)
|
225
|
-
safe_enum_operation(file_enum, :next)
|
226
|
-
end
|
227
|
-
|
228
|
-
def peek_next_line(file_enum)
|
229
|
-
safe_enum_operation(file_enum, :peek)
|
230
|
-
end
|
231
|
-
|
232
|
-
def handle_line(current_line, file_enum)
|
233
|
-
case @state
|
234
|
-
when :outside_code_block
|
235
|
-
handle_outside_code_block(current_line, file_enum)
|
236
|
-
when :inside_code_block
|
237
|
-
handle_inside_code_block(current_line, file_enum)
|
238
|
-
when :inside_result_block
|
239
|
-
handle_inside_result_block(current_line, file_enum)
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
|
-
def handle_outside_code_block(current_line, file_enum)
|
244
|
-
if current_line.match?(/^```ruby\s+RESULT$/i)
|
245
|
-
handle_existing_ruby_result_block(current_line, file_enum)
|
246
|
-
elsif (match_data = current_line.match(/^```(\w+)(?:\s+(.*))?$/i))
|
247
|
-
lang = match_data[1].downcase
|
248
|
-
options_string = match_data[2]
|
249
|
-
resolved_lang = resolve_language(lang)
|
250
|
-
if SUPPORTED_LANGUAGES.key?(resolved_lang)
|
251
|
-
start_code_block(current_line, lang, options_string)
|
252
|
-
else
|
253
|
-
@output_lines << current_line
|
254
|
-
end
|
255
|
-
else
|
256
|
-
@output_lines << current_line
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
def handle_inside_code_block(current_line, file_enum)
|
261
|
-
if is_block_end?(current_line)
|
262
|
-
end_code_block(current_line, file_enum)
|
263
|
-
else
|
264
|
-
accumulate_code_content(current_line)
|
265
|
-
end
|
266
|
-
end
|
267
|
-
|
268
|
-
def handle_inside_result_block(current_line, file_enum)
|
269
|
-
@output_lines << current_line
|
270
|
-
if is_block_end?(current_line)
|
271
|
-
@state = :outside_code_block
|
272
|
-
end
|
273
|
-
end
|
274
|
-
|
275
|
-
def handle_existing_ruby_result_block(current_line, file_enum)
|
276
|
-
warn "Found existing '```ruby RESULT' block, passing through."
|
277
|
-
@output_lines << current_line
|
278
|
-
@state = :inside_result_block
|
279
|
-
end
|
280
|
-
|
281
|
-
def start_code_block(current_line, lang, options_string = nil)
|
282
|
-
@output_lines << current_line
|
283
|
-
@current_block_lang = resolve_language(lang)
|
284
|
-
@current_block_rerun = parse_rerun_option(options_string)
|
285
|
-
@current_block_run = parse_run_option(options_string)
|
286
|
-
@state = :inside_code_block
|
287
|
-
@current_code_content = ""
|
288
|
-
end
|
289
|
-
|
290
|
-
def accumulate_code_content(current_line)
|
291
|
-
@current_code_content += current_line
|
292
|
-
@output_lines << current_line
|
293
|
-
end
|
294
|
-
|
295
|
-
def end_code_block(current_line, file_enum)
|
296
|
-
@output_lines << current_line
|
297
|
-
|
298
|
-
decision = decide_execution(file_enum)
|
299
|
-
|
300
|
-
if decision[:execute]
|
301
|
-
# If we consumed lines for rerun, don't add them to output (they'll be replaced)
|
302
|
-
execute_and_add_result(decision[:blank_line])
|
303
|
-
else
|
304
|
-
skip_and_pass_through_result(decision[:lines_to_pass_through], file_enum)
|
305
|
-
end
|
306
|
-
|
307
|
-
reset_code_block_state
|
308
|
-
end
|
309
|
-
|
310
|
-
def decide_execution(file_enum)
|
311
|
-
# If run=false, skip execution entirely (no result block creation)
|
312
|
-
unless @current_block_run
|
313
|
-
return { execute: false, lines_to_pass_through: [] }
|
314
|
-
end
|
315
|
-
|
316
|
-
peek1 = peek_next_line(file_enum)
|
317
|
-
expected_header_regex = result_block_regex(@current_block_lang)
|
318
|
-
|
319
|
-
if line_matches_pattern?(peek1, expected_header_regex)
|
320
|
-
# If rerun=true, execute even if result block exists
|
321
|
-
if @current_block_rerun
|
322
|
-
# Consume the existing result block and execute
|
323
|
-
consumed_lines = [file_enum.next]
|
324
|
-
consume_existing_result_block(file_enum, consumed_lines)
|
325
|
-
return { execute: true, consumed_lines: consumed_lines }
|
326
|
-
else
|
327
|
-
return { execute: false, lines_to_pass_through: [file_enum.next] }
|
328
|
-
end
|
329
|
-
elsif is_blank_line?(peek1)
|
330
|
-
consumed_blank_line = file_enum.next
|
331
|
-
peek2 = peek_next_line(file_enum)
|
332
|
-
|
333
|
-
if line_matches_pattern?(peek2, expected_header_regex)
|
334
|
-
if @current_block_rerun
|
335
|
-
# Consume the blank line and existing result block, then execute
|
336
|
-
consumed_lines = [consumed_blank_line, file_enum.next]
|
337
|
-
consume_existing_result_block(file_enum, consumed_lines)
|
338
|
-
return { execute: true, consumed_lines: consumed_lines, blank_line: consumed_blank_line }
|
339
|
-
else
|
340
|
-
return { execute: false, lines_to_pass_through: [consumed_blank_line, file_enum.next] }
|
341
|
-
end
|
342
|
-
else
|
343
|
-
return { execute: true, blank_line: consumed_blank_line }
|
344
|
-
end
|
345
|
-
else
|
346
|
-
return { execute: true }
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
|
-
def execute_and_add_result(blank_line_before_new_result)
|
351
|
-
@output_lines << blank_line_before_new_result if blank_line_before_new_result
|
352
|
-
|
353
|
-
if has_content?(@current_code_content)
|
354
|
-
result_output = execute_code_block(@current_code_content, @current_block_lang, @temp_dir)
|
355
|
-
add_result_block(result_output, blank_line_before_new_result)
|
356
|
-
else
|
357
|
-
warn "Skipping empty code block for language '#{@current_block_lang}'."
|
358
|
-
end
|
359
|
-
end
|
360
|
-
|
361
|
-
def skip_and_pass_through_result(lines_to_pass_through, file_enum)
|
362
|
-
# Handle run=false case where there are no lines to pass through
|
363
|
-
if lines_to_pass_through.empty?
|
364
|
-
warn "Skipping execution due to run=false option."
|
365
|
-
return
|
366
|
-
end
|
367
|
-
|
368
|
-
lang_specific_result_type = ruby_style_result?(@current_block_lang) ? "```ruby RESULT" : "```RESULT"
|
369
|
-
|
370
|
-
warn "Found existing '#{lang_specific_result_type}' block for current #{@current_block_lang} block, skipping execution."
|
371
|
-
|
372
|
-
@output_lines.concat(lines_to_pass_through)
|
373
|
-
|
374
|
-
consume_result_block_content(file_enum)
|
375
|
-
end
|
376
|
-
|
377
|
-
def consume_result_block_content(file_enum)
|
378
|
-
begin
|
379
|
-
loop do
|
380
|
-
result_block_line = file_enum.next
|
381
|
-
@output_lines << result_block_line
|
382
|
-
break if is_block_end?(result_block_line)
|
383
|
-
end
|
384
|
-
rescue StopIteration
|
385
|
-
warn "Warning: End of file reached inside a skipped 'RESULT' block."
|
386
|
-
end
|
387
|
-
end
|
388
|
-
|
389
|
-
def consume_existing_result_block(file_enum, consumed_lines)
|
390
|
-
begin
|
391
|
-
loop do
|
392
|
-
result_block_line = file_enum.next
|
393
|
-
consumed_lines << result_block_line
|
394
|
-
break if is_block_end?(result_block_line)
|
395
|
-
end
|
396
|
-
rescue StopIteration
|
397
|
-
warn "Warning: End of file reached while consuming existing result block for rerun."
|
398
|
-
end
|
399
|
-
end
|
400
|
-
|
401
|
-
def reset_code_block_state
|
402
|
-
@state = :outside_code_block
|
403
|
-
@current_code_content = ""
|
404
|
-
@current_block_rerun = false
|
405
|
-
@current_block_run = true
|
406
|
-
end
|
407
|
-
end
|
408
|
-
|
409
|
-
def process_markdown_file_main(input_file_path)
|
410
|
-
unless File.exist?(input_file_path) && File.readable?(input_file_path)
|
411
|
-
warn "Error: Input file '#{input_file_path}' not found or not readable."
|
412
|
-
return false # Indicate failure
|
413
|
-
end
|
414
|
-
|
415
|
-
temp_dir = File.dirname(File.expand_path(input_file_path))
|
416
|
-
file_enum = File.foreach(input_file_path, chomp: false).to_enum
|
417
|
-
|
418
|
-
processor = MarkdownProcessor.new(temp_dir)
|
419
|
-
output_lines = processor.process_file(file_enum)
|
420
|
-
|
421
|
-
# Write the modified content back to the input file
|
422
|
-
Tempfile.create([ "md_exec_out_", File.extname(input_file_path) ], temp_dir) do |temp_output_file|
|
423
|
-
temp_output_file.write(output_lines.join(""))
|
424
|
-
temp_output_file.close
|
425
|
-
begin
|
426
|
-
FileUtils.mv(temp_output_file.path, input_file_path)
|
427
|
-
rescue Errno::EACCES, Errno::EXDEV
|
428
|
-
warn "Atomic move failed. Falling back to copy and delete."
|
429
|
-
FileUtils.cp(temp_output_file.path, input_file_path)
|
430
|
-
FileUtils.rm_f(temp_output_file.path)
|
431
|
-
end
|
432
|
-
end
|
433
|
-
warn "Markdown processing complete. Output written to #{input_file_path}"
|
434
|
-
true # Indicate success
|
435
|
-
end
|
436
|
-
|
437
8
|
if ARGV.empty?
|
438
|
-
puts "
|
439
|
-
|
9
|
+
puts "Usage: markdown-run <file.md>"
|
10
|
+
puts "Processes a markdown file and executes code blocks, inserting their results."
|
11
|
+
exit 1 unless $0 != __FILE__ # Don't exit when loaded as a library
|
440
12
|
else
|
441
|
-
|
13
|
+
success = MarkdownRun.run_code_blocks(ARGV[0])
|
14
|
+
exit success ? 0 : 1 unless $0 != __FILE__ # Don't exit when loaded as a library
|
442
15
|
end
|
443
16
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative "language_configs"
|
2
|
+
|
3
|
+
class CodeBlockParser
|
4
|
+
# Code block header pattern: ```language options
|
5
|
+
CODE_BLOCK_START_PATTERN = /^```(\w+)(?:\s+(.*))?$/i
|
6
|
+
RUBY_RESULT_BLOCK_PATTERN = /^```ruby\s+RESULT$/i
|
7
|
+
BLOCK_END_PATTERN = "```"
|
8
|
+
|
9
|
+
def initialize(frontmatter_parser)
|
10
|
+
@frontmatter_parser = frontmatter_parser
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse_code_block_header(line)
|
14
|
+
match_data = line.match(CODE_BLOCK_START_PATTERN)
|
15
|
+
return nil unless match_data
|
16
|
+
|
17
|
+
lang = match_data[1].downcase
|
18
|
+
options_string = match_data[2]
|
19
|
+
resolved_lang = resolve_language(lang)
|
20
|
+
|
21
|
+
{
|
22
|
+
original_lang: lang,
|
23
|
+
resolved_lang: resolved_lang,
|
24
|
+
options_string: options_string,
|
25
|
+
is_supported: SUPPORTED_LANGUAGES.key?(resolved_lang)
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def is_ruby_result_block?(line)
|
30
|
+
line.match?(RUBY_RESULT_BLOCK_PATTERN)
|
31
|
+
end
|
32
|
+
|
33
|
+
def is_block_end?(line)
|
34
|
+
line.strip == BLOCK_END_PATTERN
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_run_option(options_string)
|
38
|
+
parse_boolean_option(options_string, "run", true)
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_rerun_option(options_string)
|
42
|
+
parse_boolean_option(options_string, "rerun", false)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def resolve_language(lang)
|
48
|
+
@frontmatter_parser.resolve_language(lang)
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_boolean_option(options_string, option_name, default_value)
|
52
|
+
return default_value unless options_string
|
53
|
+
|
54
|
+
# Match option=true or option=false
|
55
|
+
match = options_string.match(/#{option_name}\s*=\s*(true|false)/i)
|
56
|
+
return default_value unless match
|
57
|
+
|
58
|
+
match[1].downcase == "true"
|
59
|
+
end
|
60
|
+
end
|