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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 921255b90c33af932d6ac33284a24d4f0fc61b5fe3ec0afb45f912c8fcc6e678
4
- data.tar.gz: 076f40ed6dc79bafbc7bdba3198f39fabf56754d74e1ef6bd38f451cc34ccafd
3
+ metadata.gz: 1139e0ccb6ed1b5b4ee2cef44cf067c83fc8a92ad750eae2e256725282534b68
4
+ data.tar.gz: 8887f778eaec56e6bdbb84d0a48f8db82fbd52cabbcdea01c48f4cffb5ff410b
5
5
  SHA512:
6
- metadata.gz: 01c7168af16a7788b80acb43553f2ae81ee0a8d1dfe7bf7cf048a6c7a77a898257fb1308b277282d9494464189322eab67b164a8c694f0aa8437db8bc8654d36
7
- data.tar.gz: 4f719733a2fbea191224f8c0fbcbb44f559d2d9bec1352f9d6c6caf851550593eb3622b2453cfdd584513d3b186f085157cf17cde5dfe61706f60a6d8b2e8050
6
+ metadata.gz: 6656d86bf179be5a044a95ccbc1a9c11e68642a35ed2ffa99138dc0de653ed4b2e176a9763f08b765d409ec232e4b9eda4779b166c97b7304ed156a48b147c93
7
+ data.tar.gz: 3f8771ece0365e9a1c7073a6d749d41570776b59e9cbe0c38383516cce9cf0261a2ba287aadb30278ee0cf72e859fe4b4cb072fa89294bc3a525700fdec5ff9c
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.3.5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.10] - 2025-06-03
4
+
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)
9
+
10
+ ## [0.1.9] - 2025-06-02
11
+
12
+ - mermaid codeblocks
13
+
3
14
  ## [0.1.8] - 2025-06-01
4
15
 
5
16
  - Added run option
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
 
@@ -56,8 +57,21 @@ example vscode keybinding
56
57
 
57
58
  - `run=true` or `run=false` to control whether a code block should be executed at all. `run=true` is the default if not specified
58
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
+ - `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
+ - `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
59
62
 
60
- Options can be combined. If `run=false` is specified, the code block will not execute regardless of the `rerun` setting.
63
+ 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
+ ### Standalone Option Syntax
66
+
67
+ Options can also be specified using standalone syntax without explicit `=true`:
68
+
69
+ - `run` is equivalent to `run=true`
70
+ - `rerun` is equivalent to `rerun=true`
71
+ - `result` is equivalent to `result=true`
72
+ - `explain` is equivalent to `explain=true`
73
+
74
+ Explicit assignments (e.g., `run=false`) take precedence over standalone options.
61
75
 
62
76
  Examples:
63
77
 
@@ -65,7 +79,7 @@ Examples:
65
79
  console.log("This will not execute at all");
66
80
  ```
67
81
 
68
- ```js rerun=true
82
+ ```js rerun
69
83
  console.log("This will re-execute even if result exists");
70
84
  ```
71
85
 
@@ -73,6 +87,41 @@ console.log("This will re-execute even if result exists");
73
87
  console.log("This will execute only if no result exists");
74
88
  ```
75
89
 
90
+ ```ruby result=false run
91
+ puts "This executes but the result block is hidden"
92
+ ```
93
+
94
+ ```psql explain
95
+ SELECT * FROM users WHERE id = 1;
96
+ ```
97
+
98
+ ```psql explain=true
99
+ EXPLAIN (ANALYZE) SELECT * FROM large_table;
100
+ ```
101
+
102
+ ```psql result=false explain
103
+ SELECT * FROM large_table;
104
+ -- This will execute the explain query and show the Dalibo link but hide the result block
105
+ ```
106
+
107
+ ### Mermaid diagrams
108
+
109
+ Mermaid blocks generate SVG files and insert markdown image tags:
110
+
111
+ ```mermaid
112
+ graph TD
113
+ A[Start] --> B[Process]
114
+ B --> C[End]
115
+ ```
116
+
117
+ 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:
118
+
119
+ - Directory: `my-document/` (if the markdown file is `my-document.md`)
120
+ - Filename: `my-document-20250602-215507-a1289a799c29.svg`
121
+ - Image tag: `![Mermaid Diagram](my-document/my-document-20250602-215507-a1289a799c29.svg)`
122
+
123
+ Note: Requires `@mermaid-js/mermaid-cli` to be installed: `npm install -g @mermaid-js/mermaid-cli`
124
+
76
125
  ## Frontmatter
77
126
 
78
127
  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
- require "tempfile"
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 "Running tests..."
439
- require_relative "../test_markdown_exec"
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
- process_markdown_file_main(ARGV[0])
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,77 @@
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
+ def parse_explain_option(options_string)
46
+ parse_boolean_option(options_string, "explain", false)
47
+ end
48
+
49
+ def parse_result_option(options_string)
50
+ parse_boolean_option(options_string, "result", true)
51
+ end
52
+
53
+ private
54
+
55
+ def resolve_language(lang)
56
+ @frontmatter_parser.resolve_language(lang)
57
+ end
58
+
59
+ def parse_boolean_option(options_string, option_name, default_value)
60
+ return default_value unless options_string
61
+
62
+ # First, check for explicit option=true/false assignments (highest priority)
63
+ explicit_match = options_string.match(/#{option_name}\s*=\s*(true|false)/i)
64
+ if explicit_match
65
+ return explicit_match[1].downcase == "true"
66
+ end
67
+
68
+ # If no explicit assignment, check for standalone option (e.g., "rerun")
69
+ standalone_match = options_string.match(/\b#{option_name}\b(?!\s*=)/i)
70
+ if standalone_match
71
+ return true
72
+ end
73
+
74
+ # If neither found, return default value
75
+ default_value
76
+ end
77
+ end