markdown_exec 3.5.1 → 3.5.2

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 (105) 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 +12 -1
  7. data/Gemfile.lock +1 -1
  8. data/Rakefile +2 -0
  9. data/ai-principles.md +516 -0
  10. data/architecture-decisions.md +190 -0
  11. data/bats/block-hide.bats +1 -1
  12. data/bats/block-type-bash.bats +5 -5
  13. data/bats/block-type-link.bats +1 -1
  14. data/bats/block-type-opts.bats +3 -3
  15. data/bats/block-type-port.bats +2 -2
  16. data/bats/block-type-shell-require-ux.bats +2 -2
  17. data/bats/block-type-ux-allowed.bats +4 -4
  18. data/bats/block-type-ux-auto.bats +1 -1
  19. data/bats/block-type-ux-chained.bats +1 -1
  20. data/bats/block-type-ux-default.bats +1 -1
  21. data/bats/block-type-ux-echo-hash-transform.bats +1 -1
  22. data/bats/block-type-ux-echo-hash.bats +2 -2
  23. data/bats/block-type-ux-echo.bats +3 -3
  24. data/bats/block-type-ux-exec-hash-transform.bats +1 -1
  25. data/bats/block-type-ux-exec-hash.bats +2 -2
  26. data/bats/block-type-ux-exec.bats +1 -1
  27. data/bats/block-type-ux-force.bats +1 -1
  28. data/bats/block-type-ux-formats.bats +1 -1
  29. data/bats/block-type-ux-hidden.bats +1 -1
  30. data/bats/block-type-ux-invalid.bats +1 -1
  31. data/bats/block-type-ux-readonly.bats +1 -1
  32. data/bats/block-type-ux-require-chained.bats +2 -2
  33. data/bats/block-type-ux-require-context.bats +2 -2
  34. data/bats/block-type-ux-require.bats +2 -2
  35. data/bats/block-type-ux-required-variables.bats +1 -1
  36. data/bats/block-type-ux-row-format.bats +1 -1
  37. data/bats/block-type-ux-sources.bats +4 -4
  38. data/bats/block-type-ux-transform.bats +1 -1
  39. data/bats/block-type-vars.bats +3 -3
  40. data/bats/border.bats +1 -1
  41. data/bats/cli.bats +11 -11
  42. data/bats/command-substitution-options.bats +2 -2
  43. data/bats/command-substitution.bats +1 -1
  44. data/bats/document-shell.bats +1 -1
  45. data/bats/history.bats +5 -5
  46. data/bats/import-conflict.bats +1 -1
  47. data/bats/import-directive-line-continuation.bats +1 -1
  48. data/bats/import-directive-parameter-symbols.bats +1 -1
  49. data/bats/import-duplicates.bats +6 -6
  50. data/bats/import-parameter-symbols.bats +1 -1
  51. data/bats/import-with-text-substitution.bats +1 -1
  52. data/bats/import.bats +3 -3
  53. data/bats/indented-block-type-vars.bats +1 -1
  54. data/bats/indented-multi-line-output.bats +1 -1
  55. data/bats/line-decor-dynamic.bats +1 -1
  56. data/bats/line-wrapping.bats +1 -1
  57. data/bats/load-vars-state-demo.bats +4 -4
  58. data/bats/markup.bats +4 -4
  59. data/bats/mde.bats +4 -4
  60. data/bats/option-expansion.bats +1 -1
  61. data/bats/options-collapse.bats +4 -4
  62. data/bats/options.bats +47 -17
  63. data/bats/plain.bats +1 -1
  64. data/bats/publish.bats +2 -2
  65. data/bats/table-column-truncate.bats +1 -1
  66. data/bats/table.bats +2 -2
  67. data/bats/variable-expansion-multiline.bats +1 -1
  68. data/bats/variable-expansion.bats +6 -6
  69. data/conversation-template.md +611 -0
  70. data/docs/block-execution-modes.md +177 -0
  71. data/docs/block-filtering.md +252 -0
  72. data/docs/block-naming-patterns.md +210 -0
  73. data/docs/block-scanning-patterns.md +248 -0
  74. data/docs/cli-reference.md +370 -0
  75. data/docs/dev/block-hide.md +1 -1
  76. data/docs/dev/block-type-ux-transform.md +5 -4
  77. data/docs/dev/print_bytes.md +3 -0
  78. data/docs/dev/shebang.md +6 -0
  79. data/docs/docker-testing.md +5 -0
  80. data/docs/execution-control.md +384 -0
  81. data/docs/getting-started.md +209 -0
  82. data/docs/import-options.md +391 -0
  83. data/docs/tab-completion.md +7 -0
  84. data/docs/ux-blocks.md +376 -0
  85. data/examples/linked1.md +8 -1
  86. data/implementation-decisions.md +212 -0
  87. data/lib/cached_nested_file_reader.rb +138 -1
  88. data/lib/command_result.rb +27 -6
  89. data/lib/executed_shell_command.rb +512 -0
  90. data/lib/filter.rb +7 -7
  91. data/lib/hash_delegator.rb +403 -350
  92. data/lib/link_history.rb +22 -11
  93. data/lib/markdown_exec/version.rb +1 -1
  94. data/lib/mdoc.rb +103 -44
  95. data/lib/menu.src.yml +110 -83
  96. data/lib/menu.yml +149 -83
  97. data/lib/transformed_shell_command.rb +449 -0
  98. data/lib/wl.rb +15 -0
  99. data/lib/ww.rb +16 -5
  100. data/requirements.md +111 -0
  101. data/semantic-tokens.md +132 -0
  102. data/tasks.md +69 -0
  103. metadata +26 -4
  104. data/docs/ux-blocks-examples.md +0 -120
  105. 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,6 +47,7 @@ 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 = {})
@@ -94,6 +96,11 @@ class CachedNestedFileReader
94
96
  wwt :readline, 'depth:', depth, 'filename:', filename, 'ind:', ind,
95
97
  'segment:', segment
96
98
 
99
+ # [REQ:SHEBANG_HIDING] Filter shebang lines from processed output
100
+ # [IMPL:SHEBANG_FILTERING] [ARCH:SHEBANG_EXTRACTION] [REQ:SHEBANG_HIDING]
101
+ # first line is a shebang line if it starts with '#!'
102
+ next if @hide_shebang && is_shebang_line?(segment, ind == 0)
103
+
97
104
  if continued_line || (Regexp.new(@import_directive_line_pattern) =~ segment)
98
105
  line = (continued_line || '') + segment
99
106
  # if segment ends in a continuation, prepend to next line
@@ -314,6 +321,15 @@ class CachedNestedFileReader
314
321
  sorted_params = substitutions.sort.to_h.hash
315
322
  "#{filename}##{name_strip}##{params_string}##{sorted_params}"
316
323
  end
324
+
325
+ # [REQ:SHEBANG_HIDING] Detect shebang lines at the start of file content
326
+ # [IMPL:SHEBANG_DETECTION] [ARCH:SHEBANG_EXTRACTION] [REQ:SHEBANG_HIDING]
327
+ # Matches the beginning of the first line as '#!' - anything after that matches a shebang
328
+ def is_shebang_line?(line, is_first_line)
329
+ return false unless is_first_line
330
+
331
+ line.start_with?('#!')
332
+ end
317
333
  end
318
334
 
319
335
  return if $PROGRAM_NAME != __FILE__
@@ -501,4 +517,125 @@ class CachedNestedFileReaderTest < Minitest::Test
501
517
  importing_file.close
502
518
  importing_file.unlink
503
519
  end
520
+
521
+ # [REQ:SHEBANG_HIDING] Test shebang line detection and filtering
522
+ # [IMPL:SHEBANG_DETECTION] [IMPL:SHEBANG_FILTERING] [ARCH:SHEBANG_EXTRACTION] [REQ:SHEBANG_HIDING]
523
+ def test_readlines_with_shebang_hidden
524
+ file_with_shebang = Tempfile.new('test_shebang.txt')
525
+ file_with_shebang.write("#!/usr/bin/env mde\nLine1\nLine2")
526
+ file_with_shebang.rewind
527
+
528
+ reader_with_hide = CachedNestedFileReader.new(
529
+ import_directive_line_pattern:
530
+ /^(?<indention> *)@import +(?<name>\S+)(?<params>(?: +[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S+))*) *$/,
531
+ import_directive_parameter_scan:
532
+ /([A-Za-z_]\w*)(:=|\?=|!=|=)(?:"([^"]*)"|'([^']*)'|(\S+))/,
533
+ import_parameter_variable_assignment: '%{key}=%{value}',
534
+ shell: 'bash',
535
+ shell_block_name: '(document_shell)',
536
+ symbol_command_substitution: ':c=',
537
+ symbol_evaluated_expression: ':e=',
538
+ symbol_raw_literal: '=',
539
+ symbol_force_quoted_literal: ':q=',
540
+ symbol_variable_reference: ':v=',
541
+ hide_shebang: true
542
+ )
543
+
544
+ result = reader_with_hide.readlines(file_with_shebang.path).map(&:to_s)
545
+ assert_equal ['Line1', 'Line2'], result, 'Shebang line should be filtered when hide_shebang is true'
546
+
547
+ file_with_shebang.close
548
+ file_with_shebang.unlink
549
+ end
550
+
551
+ def test_readlines_with_shebang_shown
552
+ file_with_shebang = Tempfile.new('test_shebang.txt')
553
+ file_with_shebang.write("#!/usr/bin/env mde\nLine1\nLine2")
554
+ file_with_shebang.rewind
555
+
556
+ reader_without_hide = CachedNestedFileReader.new(
557
+ import_directive_line_pattern:
558
+ /^(?<indention> *)@import +(?<name>\S+)(?<params>(?: +[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S+))*) *$/,
559
+ import_directive_parameter_scan:
560
+ /([A-Za-z_]\w*)(:=|\?=|!=|=)(?:"([^"]*)"|'([^']*)'|(\S+))/,
561
+ import_parameter_variable_assignment: '%{key}=%{value}',
562
+ shell: 'bash',
563
+ shell_block_name: '(document_shell)',
564
+ symbol_command_substitution: ':c=',
565
+ symbol_evaluated_expression: ':e=',
566
+ symbol_raw_literal: '=',
567
+ symbol_force_quoted_literal: ':q=',
568
+ symbol_variable_reference: ':v=',
569
+ hide_shebang: false
570
+ )
571
+
572
+ result = reader_without_hide.readlines(file_with_shebang.path).map(&:to_s)
573
+ assert_equal ['#!/usr/bin/env mde', 'Line1', 'Line2'], result, 'Shebang line should be included when hide_shebang is false'
574
+
575
+ file_with_shebang.close
576
+ file_with_shebang.unlink
577
+ end
578
+
579
+ def test_readlines_with_shebang_in_imported_file
580
+ imported_file = Tempfile.new('imported_shebang.txt')
581
+ imported_file.write("#!/usr/bin/env mde\nImportedLine1\nImportedLine2")
582
+ imported_file.rewind
583
+
584
+ main_file = Tempfile.new('main_file.txt')
585
+ main_file.write("Line1\n @import #{imported_file.path}\nLine2")
586
+ main_file.rewind
587
+
588
+ reader_with_hide = CachedNestedFileReader.new(
589
+ import_directive_line_pattern:
590
+ /^(?<indention> *)@import +(?<name>\S+)(?<params>(?: +[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S+))*) *$/,
591
+ import_directive_parameter_scan:
592
+ /([A-Za-z_]\w*)(:=|\?=|!=|=)(?:"([^"]*)"|'([^']*)'|(\S+))/,
593
+ import_parameter_variable_assignment: '%{key}=%{value}',
594
+ shell: 'bash',
595
+ shell_block_name: '(document_shell)',
596
+ symbol_command_substitution: ':c=',
597
+ symbol_evaluated_expression: ':e=',
598
+ symbol_raw_literal: '=',
599
+ symbol_force_quoted_literal: ':q=',
600
+ symbol_variable_reference: ':v=',
601
+ hide_shebang: true
602
+ )
603
+
604
+ result = reader_with_hide.readlines(main_file.path).map(&:to_s)
605
+ assert_equal ['Line1', ' ImportedLine1', ' ImportedLine2', 'Line2'], result,
606
+ 'Shebang in imported file should be filtered when hide_shebang is true'
607
+
608
+ imported_file.close
609
+ imported_file.unlink
610
+ main_file.close
611
+ main_file.unlink
612
+ end
613
+
614
+ def test_readlines_without_shebang
615
+ file_without_shebang = Tempfile.new('test_no_shebang.txt')
616
+ file_without_shebang.write("Line1\nLine2\nLine3")
617
+ file_without_shebang.rewind
618
+
619
+ reader_with_hide = CachedNestedFileReader.new(
620
+ import_directive_line_pattern:
621
+ /^(?<indention> *)@import +(?<name>\S+)(?<params>(?: +[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S+))*) *$/,
622
+ import_directive_parameter_scan:
623
+ /([A-Za-z_]\w*)(:=|\?=|!=|=)(?:"([^"]*)"|'([^']*)'|(\S+))/,
624
+ import_parameter_variable_assignment: '%{key}=%{value}',
625
+ shell: 'bash',
626
+ shell_block_name: '(document_shell)',
627
+ symbol_command_substitution: ':c=',
628
+ symbol_evaluated_expression: ':e=',
629
+ symbol_raw_literal: '=',
630
+ symbol_force_quoted_literal: ':q=',
631
+ symbol_variable_reference: ':v=',
632
+ hide_shebang: true
633
+ )
634
+
635
+ result = reader_with_hide.readlines(file_without_shebang.path).map(&:to_s)
636
+ assert_equal ['Line1', 'Line2', 'Line3'], result, 'File without shebang should work normally'
637
+
638
+ file_without_shebang.close
639
+ file_without_shebang.unlink
640
+ end
504
641
  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