markdown_exec 3.5.1 → 3.6.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.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-agent-instructions +54 -0
  3. data/.cursorrules +198 -0
  4. data/.rubocop.wide.yml +5 -0
  5. data/.rubocop.yml +7 -2
  6. data/CHANGELOG.md +19 -1
  7. data/Gemfile.lock +1 -1
  8. data/README.md +1 -1
  9. data/Rakefile +2 -0
  10. data/ai-principles.md +516 -0
  11. data/architecture-decisions.md +190 -0
  12. data/bats/block-hide.bats +1 -1
  13. data/bats/block-type-bash.bats +5 -5
  14. data/bats/block-type-link.bats +1 -1
  15. data/bats/block-type-opts.bats +3 -3
  16. data/bats/block-type-port.bats +2 -2
  17. data/bats/block-type-shell-context-eval.bats +48 -0
  18. data/bats/block-type-shell-require-ux.bats +2 -2
  19. data/bats/block-type-ux-allowed.bats +4 -4
  20. data/bats/block-type-ux-auto.bats +1 -1
  21. data/bats/block-type-ux-chained.bats +1 -1
  22. data/bats/block-type-ux-default.bats +1 -1
  23. data/bats/block-type-ux-echo-hash-transform.bats +1 -1
  24. data/bats/block-type-ux-echo-hash.bats +2 -2
  25. data/bats/block-type-ux-echo.bats +4 -4
  26. data/bats/block-type-ux-exec-hash-transform.bats +1 -1
  27. data/bats/block-type-ux-exec-hash.bats +2 -2
  28. data/bats/block-type-ux-exec.bats +1 -1
  29. data/bats/block-type-ux-force.bats +2 -2
  30. data/bats/block-type-ux-formats.bats +1 -1
  31. data/bats/block-type-ux-hidden.bats +1 -1
  32. data/bats/block-type-ux-invalid.bats +2 -2
  33. data/bats/block-type-ux-readonly.bats +1 -1
  34. data/bats/block-type-ux-require-chained.bats +2 -2
  35. data/bats/block-type-ux-require-context.bats +2 -2
  36. data/bats/block-type-ux-require.bats +3 -3
  37. data/bats/block-type-ux-required-variables.bats +1 -1
  38. data/bats/block-type-ux-row-format.bats +1 -1
  39. data/bats/block-type-ux-sources.bats +4 -4
  40. data/bats/block-type-ux-transform.bats +1 -1
  41. data/bats/block-type-vars.bats +3 -3
  42. data/bats/border.bats +1 -1
  43. data/bats/cli.bats +11 -11
  44. data/bats/command-substitution-options.bats +2 -2
  45. data/bats/command-substitution.bats +1 -1
  46. data/bats/document-shell.bats +3 -3
  47. data/bats/history.bats +5 -5
  48. data/bats/import-conflict.bats +1 -1
  49. data/bats/import-directive-line-continuation.bats +1 -1
  50. data/bats/import-directive-parameter-symbols.bats +1 -1
  51. data/bats/import-duplicates.bats +6 -6
  52. data/bats/import-parameter-symbols.bats +1 -1
  53. data/bats/import-with-text-substitution.bats +1 -1
  54. data/bats/import.bats +4 -4
  55. data/bats/indented-block-type-vars.bats +1 -1
  56. data/bats/indented-multi-line-output.bats +1 -1
  57. data/bats/line-decor-dynamic.bats +1 -1
  58. data/bats/line-wrapping.bats +1 -1
  59. data/bats/load-vars-state-demo.bats +8 -8
  60. data/bats/markup.bats +4 -4
  61. data/bats/mde.bats +4 -4
  62. data/bats/option-expansion.bats +1 -1
  63. data/bats/options-collapse.bats +4 -4
  64. data/bats/options.bats +47 -17
  65. data/bats/plain.bats +1 -1
  66. data/bats/publish.bats +2 -2
  67. data/bats/table-column-truncate.bats +1 -1
  68. data/bats/table.bats +2 -2
  69. data/bats/variable-expansion-multiline.bats +1 -1
  70. data/bats/variable-expansion.bats +6 -6
  71. data/bin/tab_completion.sh +3 -3
  72. data/conversation-template.md +611 -0
  73. data/docs/block-execution-modes.md +177 -0
  74. data/docs/block-filtering.md +252 -0
  75. data/docs/block-naming-patterns.md +210 -0
  76. data/docs/block-scanning-patterns.md +248 -0
  77. data/docs/cli-reference.md +370 -0
  78. data/docs/dev/bats-document-configuration.md +1 -1
  79. data/docs/dev/block-hide.md +1 -1
  80. data/docs/dev/block-type-shell-context-eval.md +52 -0
  81. data/docs/dev/block-type-shell-require-ux.md +6 -2
  82. data/docs/dev/block-type-ux-echo.md +3 -3
  83. data/docs/dev/block-type-ux-force.md +1 -1
  84. data/docs/dev/block-type-ux-require-chained.md +1 -1
  85. data/docs/dev/block-type-ux-require.md +2 -2
  86. data/docs/dev/block-type-ux-transform.md +5 -4
  87. data/docs/dev/import-parameter-symbols.md +1 -1
  88. data/docs/dev/linked-file.md +1 -1
  89. data/docs/dev/load-vars-state-demo.md +1 -1
  90. data/docs/dev/print_bytes.md +3 -0
  91. data/docs/dev/requiring-blocks.md +1 -1
  92. data/docs/dev/shebang.md +6 -0
  93. data/docs/dev/specs.md +2 -2
  94. data/docs/docker-testing.md +5 -0
  95. data/docs/execution-control.md +384 -0
  96. data/docs/getting-started.md +209 -0
  97. data/docs/import-options.md +391 -0
  98. data/docs/shell-script-evaluation.md +78 -0
  99. data/docs/tab-completion.md +7 -0
  100. data/docs/ux-blocks.md +376 -0
  101. data/examples/link-blocks-vars.md +2 -2
  102. data/examples/linked.md +1 -1
  103. data/examples/linked1.md +1 -1
  104. data/examples/opts-blocks.md +2 -2
  105. data/examples/port-blocks.md +1 -1
  106. data/examples/variable-expansion.md +1 -1
  107. data/implementation-decisions.md +212 -0
  108. data/lib/cached_nested_file_reader.rb +145 -5
  109. data/lib/command_result.rb +27 -6
  110. data/lib/executed_shell_command.rb +512 -0
  111. data/lib/filter.rb +7 -7
  112. data/lib/hash_delegator.rb +699 -605
  113. data/lib/input_sequencer.rb +4 -3
  114. data/lib/link_history.rb +95 -36
  115. data/lib/markdown_exec/version.rb +1 -1
  116. data/lib/mdoc.rb +138 -51
  117. data/lib/menu.src.yml +115 -88
  118. data/lib/menu.yml +154 -88
  119. data/lib/transformed_shell_command.rb +449 -0
  120. data/lib/wl.rb +15 -0
  121. data/lib/ww.rb +17 -6
  122. data/requirements.md +111 -0
  123. data/semantic-tokens.md +132 -0
  124. data/tasks.md +69 -0
  125. metadata +29 -4
  126. data/docs/ux-blocks-examples.md +0 -120
  127. data/docs/ux-blocks-init-act.md +0 -100
@@ -33,7 +33,8 @@ class CachedNestedFileReader
33
33
  symbol_evaluated_expression:,
34
34
  symbol_force_quoted_literal:,
35
35
  symbol_raw_literal:,
36
- symbol_variable_reference:
36
+ symbol_variable_reference:,
37
+ hide_shebang: true
37
38
  )
38
39
  @file_cache = {}
39
40
  @import_directive_line_pattern = import_directive_line_pattern
@@ -46,16 +47,18 @@ class CachedNestedFileReader
46
47
  @symbol_force_quoted_literal = symbol_force_quoted_literal
47
48
  @symbol_raw_literal = symbol_raw_literal
48
49
  @symbol_variable_reference = symbol_variable_reference
50
+ @hide_shebang = hide_shebang
49
51
  end
50
52
 
51
53
  def error_handler(name = '', opts = {})
52
54
  Exceptions.error_handler(
53
55
  "CachedNestedFileReader.#{name} -- #{$!}",
56
+ caller.deref(6),
54
57
  opts
55
58
  )
56
59
  end
57
60
 
58
- def warn_format(name, message, opts = {})
61
+ def self.warn_format(name, message, opts = {})
59
62
  Exceptions.warn_format(
60
63
  "CachedNestedFileReader.#{name} -- #{message}",
61
64
  opts
@@ -94,6 +97,11 @@ class CachedNestedFileReader
94
97
  wwt :readline, 'depth:', depth, 'filename:', filename, 'ind:', ind,
95
98
  'segment:', segment
96
99
 
100
+ # [REQ:SHEBANG_HIDING] Filter shebang lines from processed output
101
+ # [IMPL:SHEBANG_FILTERING] [ARCH:SHEBANG_EXTRACTION] [REQ:SHEBANG_HIDING]
102
+ # first line is a shebang line if it starts with '#!'
103
+ next if @hide_shebang && is_shebang_line?(segment, ind == 0)
104
+
97
105
  if continued_line || (Regexp.new(@import_directive_line_pattern) =~ segment)
98
106
  line = (continued_line || '') + segment
99
107
  # if segment ends in a continuation, prepend to next line
@@ -106,7 +114,7 @@ class CachedNestedFileReader
106
114
 
107
115
  # apply substitutions to the @import line
108
116
  line_sub1 = apply_line_substitutions(line, substitutions,
109
- use_template_delimiters)
117
+ use_template_delimiters)
110
118
 
111
119
  # parse the @import line
112
120
  Regexp.new(@import_directive_line_pattern) =~ line_sub1
@@ -190,8 +198,10 @@ class CachedNestedFileReader
190
198
  wwt :read_document_code, 'processed_lines:', processed_lines
191
199
  @file_cache[cache_key] = processed_lines
192
200
  rescue Errno::ENOENT => err
193
- warn_format('readlines', "#{err} @@ #{context}",
194
- { abort: true })
201
+ CachedNestedFileReader.warn_format(
202
+ 'readlines', "#{err} @@ #{context}",
203
+ { abort: true }
204
+ )
195
205
  rescue StandardError
196
206
  wwe $!
197
207
  end
@@ -314,6 +324,15 @@ class CachedNestedFileReader
314
324
  sorted_params = substitutions.sort.to_h.hash
315
325
  "#{filename}##{name_strip}##{params_string}##{sorted_params}"
316
326
  end
327
+
328
+ # [REQ:SHEBANG_HIDING] Detect shebang lines at the start of file content
329
+ # [IMPL:SHEBANG_DETECTION] [ARCH:SHEBANG_EXTRACTION] [REQ:SHEBANG_HIDING]
330
+ # Matches the beginning of the first line as '#!' - anything after that matches a shebang
331
+ def is_shebang_line?(line, is_first_line)
332
+ return false unless is_first_line
333
+
334
+ line.start_with?('#!')
335
+ end
317
336
  end
318
337
 
319
338
  return if $PROGRAM_NAME != __FILE__
@@ -501,4 +520,125 @@ class CachedNestedFileReaderTest < Minitest::Test
501
520
  importing_file.close
502
521
  importing_file.unlink
503
522
  end
523
+
524
+ # [REQ:SHEBANG_HIDING] Test shebang line detection and filtering
525
+ # [IMPL:SHEBANG_DETECTION] [IMPL:SHEBANG_FILTERING] [ARCH:SHEBANG_EXTRACTION] [REQ:SHEBANG_HIDING]
526
+ def test_readlines_with_shebang_hidden
527
+ file_with_shebang = Tempfile.new('test_shebang.txt')
528
+ file_with_shebang.write("#!/usr/bin/env mde\nLine1\nLine2")
529
+ file_with_shebang.rewind
530
+
531
+ reader_with_hide = CachedNestedFileReader.new(
532
+ import_directive_line_pattern:
533
+ /^(?<indention> *)@import +(?<name>\S+)(?<params>(?: +[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S+))*) *$/,
534
+ import_directive_parameter_scan:
535
+ /([A-Za-z_]\w*)(:=|\?=|!=|=)(?:"([^"]*)"|'([^']*)'|(\S+))/,
536
+ import_parameter_variable_assignment: '%{key}=%{value}',
537
+ shell: 'bash',
538
+ shell_block_name: '(document_shell)',
539
+ symbol_command_substitution: ':c=',
540
+ symbol_evaluated_expression: ':e=',
541
+ symbol_raw_literal: '=',
542
+ symbol_force_quoted_literal: ':q=',
543
+ symbol_variable_reference: ':v=',
544
+ hide_shebang: true
545
+ )
546
+
547
+ result = reader_with_hide.readlines(file_with_shebang.path).map(&:to_s)
548
+ assert_equal %w[Line1 Line2], result, 'Shebang line should be filtered when hide_shebang is true'
549
+
550
+ file_with_shebang.close
551
+ file_with_shebang.unlink
552
+ end
553
+
554
+ def test_readlines_with_shebang_shown
555
+ file_with_shebang = Tempfile.new('test_shebang.txt')
556
+ file_with_shebang.write("#!/usr/bin/env mde\nLine1\nLine2")
557
+ file_with_shebang.rewind
558
+
559
+ reader_without_hide = CachedNestedFileReader.new(
560
+ import_directive_line_pattern:
561
+ /^(?<indention> *)@import +(?<name>\S+)(?<params>(?: +[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S+))*) *$/,
562
+ import_directive_parameter_scan:
563
+ /([A-Za-z_]\w*)(:=|\?=|!=|=)(?:"([^"]*)"|'([^']*)'|(\S+))/,
564
+ import_parameter_variable_assignment: '%{key}=%{value}',
565
+ shell: 'bash',
566
+ shell_block_name: '(document_shell)',
567
+ symbol_command_substitution: ':c=',
568
+ symbol_evaluated_expression: ':e=',
569
+ symbol_raw_literal: '=',
570
+ symbol_force_quoted_literal: ':q=',
571
+ symbol_variable_reference: ':v=',
572
+ hide_shebang: false
573
+ )
574
+
575
+ result = reader_without_hide.readlines(file_with_shebang.path).map(&:to_s)
576
+ assert_equal ['#!/usr/bin/env mde', 'Line1', 'Line2'], result, 'Shebang line should be included when hide_shebang is false'
577
+
578
+ file_with_shebang.close
579
+ file_with_shebang.unlink
580
+ end
581
+
582
+ def test_readlines_with_shebang_in_imported_file
583
+ imported_file = Tempfile.new('imported_shebang.txt')
584
+ imported_file.write("#!/usr/bin/env mde\nImportedLine1\nImportedLine2")
585
+ imported_file.rewind
586
+
587
+ main_file = Tempfile.new('main_file.txt')
588
+ main_file.write("Line1\n @import #{imported_file.path}\nLine2")
589
+ main_file.rewind
590
+
591
+ reader_with_hide = CachedNestedFileReader.new(
592
+ import_directive_line_pattern:
593
+ /^(?<indention> *)@import +(?<name>\S+)(?<params>(?: +[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S+))*) *$/,
594
+ import_directive_parameter_scan:
595
+ /([A-Za-z_]\w*)(:=|\?=|!=|=)(?:"([^"]*)"|'([^']*)'|(\S+))/,
596
+ import_parameter_variable_assignment: '%{key}=%{value}',
597
+ shell: 'bash',
598
+ shell_block_name: '(document_shell)',
599
+ symbol_command_substitution: ':c=',
600
+ symbol_evaluated_expression: ':e=',
601
+ symbol_raw_literal: '=',
602
+ symbol_force_quoted_literal: ':q=',
603
+ symbol_variable_reference: ':v=',
604
+ hide_shebang: true
605
+ )
606
+
607
+ result = reader_with_hide.readlines(main_file.path).map(&:to_s)
608
+ assert_equal ['Line1', ' ImportedLine1', ' ImportedLine2', 'Line2'], result,
609
+ 'Shebang in imported file should be filtered when hide_shebang is true'
610
+
611
+ imported_file.close
612
+ imported_file.unlink
613
+ main_file.close
614
+ main_file.unlink
615
+ end
616
+
617
+ def test_readlines_without_shebang
618
+ file_without_shebang = Tempfile.new('test_no_shebang.txt')
619
+ file_without_shebang.write("Line1\nLine2\nLine3")
620
+ file_without_shebang.rewind
621
+
622
+ reader_with_hide = CachedNestedFileReader.new(
623
+ import_directive_line_pattern:
624
+ /^(?<indention> *)@import +(?<name>\S+)(?<params>(?: +[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S+))*) *$/,
625
+ import_directive_parameter_scan:
626
+ /([A-Za-z_]\w*)(:=|\?=|!=|=)(?:"([^"]*)"|'([^']*)'|(\S+))/,
627
+ import_parameter_variable_assignment: '%{key}=%{value}',
628
+ shell: 'bash',
629
+ shell_block_name: '(document_shell)',
630
+ symbol_command_substitution: ':c=',
631
+ symbol_evaluated_expression: ':e=',
632
+ symbol_raw_literal: '=',
633
+ symbol_force_quoted_literal: ':q=',
634
+ symbol_variable_reference: ':v=',
635
+ hide_shebang: true
636
+ )
637
+
638
+ result = reader_with_hide.readlines(file_without_shebang.path).map(&:to_s)
639
+ assert_equal %w[Line1 Line2 Line3], result, 'File without shebang should work normally'
640
+
641
+ file_without_shebang.close
642
+ file_without_shebang.unlink
643
+ end
504
644
  end
@@ -3,11 +3,18 @@
3
3
 
4
4
  # encoding=utf-8
5
5
 
6
+ ALL = [
7
+ BASH = 'bash',
8
+ FISH = 'fish',
9
+ SH = 'sh'
10
+ ].freeze
11
+
6
12
  # Encapsulates the result of executing a system command, storing its output,
7
13
  # exit status, and any number of additional, arbitrary attributes.
8
14
  #
9
15
  # @example
10
- # result = CommandResult.new(stdout: output, exit_status: $?.exitstatus, duration: 1.23)
16
+ # result = CommandResult.new(stdout: output, exit_status: $?.exitstatus,
17
+ # duration: 1.23)
11
18
  # result.stdout # => output
12
19
  # result.exit_status # => 0
13
20
  # result.duration # => 1.23
@@ -15,13 +22,19 @@
15
22
  # result.new_field # => 42
16
23
  # result.success? # => true
17
24
  class CommandResult
25
+ ALL = [
26
+ EXIT_STATUS_OK = 0,
27
+ EXIT_STATUS_FAIL = 127,
28
+ EXIT_STATUS_REQUIRED_EMPTY = 248
29
+ ].freeze
30
+
18
31
  # @param attributes [Hash{Symbol=>Object}] initial named attributes
19
32
  def initialize(**attributes)
20
- @attributes = {}
21
- @attributes[:exit_status] = 0
22
- @attributes[:stdout] = ''
23
- @attributes[:warning] = ''
24
- attributes.each { |name, value| @attributes[name] = value }
33
+ @attributes = {
34
+ exit_status: EXIT_STATUS_OK,
35
+ stdout: '',
36
+ warning: ''
37
+ }.merge(attributes)
25
38
  end
26
39
 
27
40
  def failure?
@@ -39,6 +52,14 @@ class CommandResult
39
52
  value
40
53
  end
41
54
 
55
+ def stdout
56
+ @attributes[:stdout]
57
+ end
58
+
59
+ def transformed
60
+ @attributes[:transformed]
61
+ end
62
+
42
63
  # # trap assignment to new_lines
43
64
  # def new_lines=(value)
44
65
  # ww caller.deref[0..4], value