markdown_exec 2.8.2 → 2.8.3

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: 066c72f2f85462a89163b7834669dee6526e516ee3ebfe4be2e8b269cb4ece36
4
- data.tar.gz: 48c2bce9ac82cda2cbecca5bea4564b28013cdbab7005e249e8a856d0280afa0
3
+ metadata.gz: dfa1920cdbee40306492b597411dff701777c0ac158dc969a02d3ac2db7351b2
4
+ data.tar.gz: fe5c8229410f4263b9aa394c99f1d1be61b6a119684ca5c29e8f0a7851d4391a
5
5
  SHA512:
6
- metadata.gz: fe1270b8cf11efc8ad9e81cb43052bc75cfc217e362c7d6e30acb6981a6d0be749f290b9b15ce1a2655d6a7e157177c4cd975aa61de22a6f6bafe2a835285228
7
- data.tar.gz: 9445285db86af55fe5ba0b784a24d30dd418ac3d1dc551183a5a1b3b865df27a6b314ddc08b4a013de5e926e023439753fe08cd4ea8d6c6da0c0d9547e4a9546
6
+ metadata.gz: e2566c1f28e9eb898c490aa9076d762fe76b8f2092a1b6b4a9d37ff889d02bea021cf84fad63c2f9598d7ba17d7a6ac63840d49958d6dfad9f45b57250a4d3b7
7
+ data.tar.gz: 44a40a84a65eb477951b6929307837cc8493e70b9fe76b5065bb86e54b2b10f07241de175552132979ddb00b5c8bdce031d64688189b78f5da9f953e74399f4a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.8.3] - 2025-02-27
4
+
5
+ ### Added
6
+
7
+ - "echo" support to UX blocks for evaluated shell expressions
8
+
9
+ Introduces the `echo` key to UX blocks, allowing the output of an
10
+ evaluated shell expression to be assigned to a named variable.
11
+ Implements `export_echo_with_code` to execute the expression safely
12
+ and handle invalidated outputs. Also ensures proper transformation of
13
+ export values.
14
+
15
+ - Persist allocated FCBs.
16
+
17
+ - Read-only flag to UX menu blocks
18
+
19
+ Introduces a `readonly` flag for UX menu blocks, allowing exports to
20
+ specify whether a block should be immutable. The flag is assigned from
21
+ the export definition and enforced in the menu logic.
22
+
23
+ ### Changed
24
+
25
+ - Correct indent of all lines displayed for a block.
26
+ - Retain whitespace in output from shell blocks.
27
+ - Remove a function to report the compiled version from the tab completion script. It is not used.
28
+
3
29
  ## [2.8.2] - 2025-02-19
4
30
 
5
31
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- markdown_exec (2.8.2)
4
+ markdown_exec (2.8.3)
5
5
  clipboard (~> 1.3.6)
6
6
  open3 (~> 0.1.1)
7
7
  optparse (~> 0.1.1)
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bats
2
+
3
+ load 'test_helper'
4
+
5
+ @test 'automatic block - default' {
6
+ spec_mde_xansi_dname_doc_blocks_expect docs/dev/block-type-ux-echo.md \
7
+ 'VAR=markdown_exec_IAB='
8
+ }
9
+
10
+ @test 'inherited lines' {
11
+ spec_mde_xansi_dname_doc_blocks_expect docs/dev/block-type-ux-echo.md \
12
+ '(menu_with_inherited_lines)' \
13
+ 'VAR=markdown_exec_VAR=markdown_exec_IAB='
14
+ }
15
+
16
+ @test 'selected block' {
17
+ spec_mde_xansi_dname_doc_blocks_expect docs/dev/block-type-ux-echo.md \
18
+ '(VAR_has_count)' '[IAB_has_count]' \
19
+ 'VAR=14_IAB=1414'
20
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bats
2
+
3
+ load 'test_helper'
4
+
5
+ @test 'An undefined variable is a precondition' {
6
+ spec_mde_xansi_dname_doc_blocks_expect docs/dev/block-type-ux-preconditions.md \
7
+ 'A value must exist for: MISSING_VARIABLE_SPECIES='
8
+ }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bats
2
+
3
+ load 'test_helper'
4
+
5
+ @test 'automatic block - disabled' {
6
+ spec_mde_args_expect \
7
+ docs/dev/block-type-ux-readonly.md \
8
+ --list-blocks-message readonly --list-blocks-type 3 --list-blocks \
9
+ true
10
+ }
@@ -7,8 +7,8 @@ load 'test_helper'
7
7
  # includes output from automatic vars blocks
8
8
  @test 'Vars block - auto load' {
9
9
  BATS_OUTPUT_FILTER=A
10
- spec_mde_args_expect docs/dev/block-type-vars.md show \
11
- 'Species = Not specified Genus = Not specified Species: Not specified VAULT:'
10
+ spec_mde_xansi_dname_doc_blocks_expect docs/dev/block-type-vars.md show \
11
+ 'Species = Not specified_Genus = Not specified__Species: Not specified_VAULT: '
12
12
  }
13
13
 
14
14
  # includes output from assignment and from shell block
@@ -22,5 +22,5 @@ load 'test_helper'
22
22
  @test 'Vars block - invalid YAML' {
23
23
  BATS_OUTPUT_FILTER=A
24
24
  spec_mde_args_expect docs/dev/block-type-vars.md '[invalid_yaml]' show \
25
- 'Species = Not specified Genus = Not specified Species: Not specified VAULT:'
25
+ 'Species = Not specified Genus = Not specified Species: Not specified VAULT: '
26
26
  }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bats
2
+
3
+ load 'test_helper'
4
+
5
+ @test 'Retain whitespace in output from shell blocks' {
6
+ spec_mde_xansi_dname_doc_blocks_expect docs/dev/indented-multi-line-output.md \
7
+ '[make-output]' \
8
+ '_Species_ Genus_ Family_Order'
9
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bats
2
+
3
+ load 'test_helper'
4
+
5
+ @test 'Line decor, dynamic - new pattern, line_decor_pre, ansi' {
6
+ spec_mde_dname_doc_blocks_expect docs/dev/line-decor-dynamic.md \
7
+ $'\e[38;2;200;200;33m\e[48;2;60;60;32mSpecies\e[0m'
8
+ }
@@ -209,6 +209,16 @@ spec_mde_args_grep_filter_expect () {
209
209
  [[ -n $status ]]
210
210
  }
211
211
 
212
+ spec_mde_dname_doc_blocks_expect () {
213
+ BATS_SAFE=_
214
+ spec_mde_args_expect "$1" \
215
+ "${@:2:$(($#-2))}" \
216
+ --list-blocks-message dname \
217
+ --list-blocks-type 3 \
218
+ --list-blocks \
219
+ "${!#}"
220
+ }
221
+
212
222
  spec_mde_xansi_dname_doc_blocks_expect () {
213
223
  BATS_OUTPUT_FILTER=A
214
224
  BATS_SAFE=_
@@ -12,10 +12,6 @@ __filedirs_all()
12
12
  COMPREPLY='.'
13
13
  }
14
14
 
15
- _mde_echo_version() {
16
- echo "2.8.2"
17
- }
18
-
19
15
  _mde() {
20
16
  local cur prev opts
21
17
  cur="${COMP_WORDS[COMP_CWORD]}"
@@ -213,4 +209,3 @@ _mde() {
213
209
  }
214
210
 
215
211
  complete -o filenames -o nospace -F _mde mde
216
- # _mde_echo_version
@@ -12,10 +12,6 @@ __filedirs_all()
12
12
  COMPREPLY='.'
13
13
  }
14
14
 
15
- _mde_echo_version() {
16
- echo "<%= MarkdownExec::VERSION %>"
17
- }
18
-
19
15
  _mde() {
20
16
  local cur prev opts
21
17
  cur="${COMP_WORDS[COMP_CWORD]}"
@@ -104,4 +100,3 @@ _mde() {
104
100
  }
105
101
 
106
102
  complete -o filenames -o nospace -F _mde mde
107
- # _mde_echo_version
@@ -0,0 +1,21 @@
1
+ / This automatic block sets VAR and displays the current value in the menu.
2
+ ```ux :[document_ux_VAR]
3
+ default: :echo
4
+ echo: $(basename `pwd`)
5
+ name: VAR
6
+ ```
7
+ / This block is not visible. Execute to display the inherited lines for testing.
8
+ ```opts :(menu_with_inherited_lines)
9
+ menu_with_inherited_lines: true
10
+ ```
11
+ / This block is not visible. Execute to set a new value, displayed by the block above.
12
+ ```ux :(VAR_has_count)
13
+ echo: $(basename `pwd` | wc -c)
14
+ name: VAR
15
+ ```
16
+ / This block is visible. Execute to set a new value, displayed by the block above.
17
+ ```ux :[IAB_has_count]
18
+ echo: $VAR$VAR
19
+ name: IAB
20
+ ```
21
+ @import bats-document-configuration.md
@@ -0,0 +1,9 @@
1
+ / an automatic UX block that has a precondition that must be met before it is executed
2
+ ```ux :[document_ux_SPECIES]
3
+ default: :exec
4
+ exec: printf "$MISSING_VARIABLE"
5
+ name: SPECIES
6
+ preconditions:
7
+ - MISSING_VARIABLE
8
+ ```
9
+ @import bats-document-configuration.md
@@ -0,0 +1,7 @@
1
+ / This automatic block sets VAR and displays the current value in the menu.
2
+ ```ux :[document_ux_VAR]
3
+ default: Pongo tapanuliensis
4
+ name: SPECIES
5
+ readonly: true
6
+ ```
7
+ @import bats-document-configuration.md
@@ -4,25 +4,29 @@ ENTITY='Pongo tapanuliensis,Pongo'
4
4
  ```
5
5
  ```ux :[document_ux_SPECIES] +(shell) +[ux_GENUS]
6
6
  default: :exec
7
- exec: printf "${ENTITY%%,*}"
7
+ exec: echo "${ENTITY%%,*}"
8
8
  name: SPECIES
9
+ transform: :chomp
9
10
  ```
10
11
  / required ux block requires another
11
12
  ```ux :[ux_GENUS] +[ux_NAME]
12
13
  default: :exec
13
- exec: printf "${ENTITY##*,}"
14
+ exec: echo "${ENTITY##*,}"
14
15
  name: GENUS
16
+ transform: :chomp
15
17
  ```
16
18
  / executed in context of prior ux blocks, uses their initial values
17
19
  ```ux :[ux_NAME]
18
20
  default: :exec
19
- exec: printf "$SPECIES - $GENUS"
21
+ exec: echo "$SPECIES - $GENUS"
20
22
  name: NAME
23
+ transform: :chomp
21
24
  ```
22
25
  / executed after other ux blocks, uses their initial values
23
26
  ```ux :[document_ux_NAME2]
24
27
  default: :exec
25
- exec: printf "$NAME"
28
+ exec: echo "$NAME"
26
29
  name: NAME2
30
+ transform: :chomp
27
31
  ```
28
32
  @import bats-document-configuration.md
@@ -0,0 +1,11 @@
1
+ / Retain whitespace in output from shell blocks
2
+ ``` :[make-output]
3
+ echo 'Species'
4
+ echo -e " Genus\n Family\tOrder"
5
+ ```
6
+ @import bats-document-configuration.md
7
+ ```opts :(document_opts)
8
+ line_decor_pre:
9
+ - :color_method: :ansi_38_2_200_200_33__48_2_60_60_32__0
10
+ :pattern: '%([^_]{0,64})%'
11
+ ```
@@ -0,0 +1,9 @@
1
+ / Demonstrate dynamic color names
2
+ / line_decor_pre is performed before line_decor_main and line_decor_post
3
+ %Species%
4
+ @import bats-document-configuration.md
5
+ ```opts :(document_opts)
6
+ line_decor_pre:
7
+ - :color_method: :ansi_38_2_200_200_33__48_2_60_60_32__0
8
+ :pattern: '%([^_]{0,64})%'
9
+ ```
data/lib/ansi_string.rb CHANGED
@@ -14,6 +14,15 @@ class AnsiString < String
14
14
  def method_missing(method_name, *args, &block)
15
15
  if dynamic_color_method?(method_name)
16
16
  case method_name.to_s
17
+ when /^ansi_/
18
+ segments = $'.split('__')
19
+ codes = ''
20
+ segments[0..-2].each do |segment|
21
+ codes += "\033[#{segment.split('_').join(';')}m"
22
+ end
23
+ codes += self.to_s
24
+ codes += "\033[#{segments.last.split('_').join(';')}m"
25
+ self.class.new(codes)
17
26
  when /^fg_bg_rgb_/
18
27
  bytes = $'.split('_')
19
28
  fg_bg_rgb_color(bytes[0..2].join(';'), bytes[3..5].join(';'))
@@ -148,6 +157,6 @@ class AnsiString < String
148
157
  # @param method_name [Symbol] The name of the method being checked.
149
158
  # @return [Boolean] True if the method name matches a dynamic color method.
150
159
  def dynamic_color_method?(method_name)
151
- method_name.to_s =~ /^(fg_bg_rgb_|fg_bg_rgbh_|fg_rgb_|fg_rgbh_)/
160
+ method_name.to_s =~ /^(ansi_|fg_bg_rgb_|fg_bg_rgbh_|fg_rgb_|fg_rgbh_)/
152
161
  end
153
162
  end
data/lib/fcb.rb CHANGED
@@ -20,11 +20,13 @@ def parse_yaml_of_ux_block(
20
20
  OpenStruct.new(
21
21
  allowed: export['allowed'],
22
22
  default: export['default'],
23
+ echo: export['echo'],
23
24
  exec: export['exec'],
24
25
  menu_format: export['menu_format'] || menu_format,
25
26
  name: name,
26
27
  preconditions: export['preconditions'],
27
28
  prompt: export['prompt'] || prompt,
29
+ readonly: export['readonly'].nil? ? false : export['readonly'],
28
30
  transform: export['transform'],
29
31
  validate: export['validate'] || validate
30
32
  )
@@ -137,6 +139,7 @@ module MarkdownExec
137
139
 
138
140
  @attrs[:center] = table_center
139
141
  oname = @attrs[:oname] = format(export.menu_format, export.to_h)
142
+ @attrs[:readonly] = export.readonly
140
143
  else
141
144
  # triggered by an empty block
142
145
  raise "Invalid data type: #{data.inspect}"
@@ -149,8 +152,6 @@ module MarkdownExec
149
152
  )
150
153
  end
151
154
 
152
- private
153
-
154
155
  # Formats multiline body content as a title string.
155
156
  # indents all but first line with two spaces
156
157
  # so it displays correctly in menu.
@@ -182,6 +183,17 @@ module MarkdownExec
182
183
 
183
184
  public
184
185
 
186
+ def name_in_menu!(indented_multi_line)
187
+ # Indent has been extracted from the first line,
188
+ # remove indent from the remaining lines.
189
+ @attrs[:dname] =
190
+ if @attrs[:indent].empty?
191
+ indented_multi_line
192
+ else
193
+ indented_multi_line.gsub("\n#{@attrs[:indent]}", "\n")
194
+ end
195
+ end
196
+
185
197
  def respond_to_missing?(method_name, include_private = false)
186
198
  @attrs.key?(method_name.to_sym) || super
187
199
  end
@@ -256,8 +268,8 @@ if $PROGRAM_NAME == __FILE__
256
268
  end
257
269
 
258
270
  def sort_hash_recursively(hash)
259
- hash.each_with_object({}) do |(k, v), new_hash|
260
- new_hash[k] = v.is_a?(Hash) ? sort_hash_recursively(v) : v
271
+ hash.transform_values do |v|
272
+ v.is_a?(Hash) ? sort_hash_recursively(v) : v
261
273
  end.sort.to_h
262
274
  end
263
275
 
data/lib/filter.rb CHANGED
@@ -127,8 +127,9 @@ module MarkdownExec
127
127
  filters[:fcb_chrome] = fcb.fetch(:chrome, false)
128
128
  filters[:shell_default] = (fcb.type == BlockType::SHELL)
129
129
 
130
- if options[:block_disable_match].present? &&
131
- fcb.start_line =~ Regexp.new(options[:block_disable_match])
130
+ if fcb.readonly ||
131
+ (options[:block_disable_match].present? &&
132
+ fcb.start_line =~ Regexp.new(options[:block_disable_match]))
132
133
  fcb.disabled = TtyMenu::DISABLE
133
134
  end
134
135
  return unless name.present?
@@ -395,10 +395,17 @@ module HashDelegatorSelf
395
395
  # @param [String] line The line to be processed.
396
396
  # @param [Array<Symbol>] selected_types A list of message types to check.
397
397
  # @param [Proc] block The block to be called with the line data.
398
- def yield_line_if_selected(line, selected_types, source_id: '', &block)
398
+ def yield_line_if_selected(line, selected_types, all_fcbs: nil,
399
+ source_id: '', &block)
399
400
  return unless block && block_type_selected?(selected_types, :line)
400
401
 
401
- block.call(:line, MarkdownExec::FCB.new(body: [line], id: source_id))
402
+ block.call(:line, persist_fcb_self(all_fcbs, body: [line], id: source_id))
403
+ end
404
+
405
+ def persist_fcb_self(all_fcbs, options)
406
+ fcb = MarkdownExec::FCB.new(options)
407
+ all_fcbs << fcb if all_fcbs
408
+ fcb
402
409
  end
403
410
  end
404
411
 
@@ -609,6 +616,7 @@ module MarkdownExec
609
616
  @dml_link_state = Struct.new(:document_filename, :inherited_lines)
610
617
  .new(@delegate_object[:filename], [])
611
618
  @dml_menu_blocks = []
619
+ @fcb_store = [] # all fcbs created
612
620
 
613
621
  @p_all_arguments = []
614
622
  @p_options_parsed = []
@@ -737,7 +745,7 @@ module MarkdownExec
737
745
 
738
746
  formatted_name = format(@delegate_object[:menu_link_format],
739
747
  HashDelegator.safeval(option_name))
740
- chrome_block = FCB.new(
748
+ chrome_block = persist_fcb(
741
749
  chrome: true,
742
750
  dname: HashDelegator.new(@delegate_object).string_send_color(
743
751
  formatted_name, :menu_chrome_color
@@ -781,7 +789,7 @@ module MarkdownExec
781
789
  chrome_blocks = link_state.inherited_lines_map do |line|
782
790
  formatted = format(@delegate_object[:menu_inherited_lines_format],
783
791
  { line: line })
784
- FCB.new(
792
+ persist_fcb(
785
793
  chrome: true,
786
794
  disabled: TtyMenu::DISABLE,
787
795
  dname: HashDelegator.new(@delegate_object).string_send_color(
@@ -840,25 +848,6 @@ module MarkdownExec
840
848
  end
841
849
  end
842
850
 
843
- # private
844
-
845
- def expand_references!(fcb, link_state)
846
- expand_variable_references!(
847
- blocks: [fcb],
848
- initial_code_required: false,
849
- link_state: link_state
850
- )
851
- expand_variable_references!(
852
- blocks: [fcb],
853
- echo_format: '%s',
854
- group_name: :command,
855
- initial_code_required: false,
856
- key_format: '$(%s)',
857
- link_state: link_state,
858
- pattern: options_command_substitution_regexp
859
- )
860
- end
861
-
862
851
  # Iterates through nested files to collect various types
863
852
  # of blocks, including dividers, tasks, and others.
864
853
  # The method categorizes blocks based on their type and processes them accordingly.
@@ -913,30 +902,6 @@ module MarkdownExec
913
902
  HashDelegator.error_handler('blocks_from_nested_files')
914
903
  end
915
904
 
916
- # find a block by its original (undecorated) name or nickname (not visible in menu)
917
- # if matched, the block returned has properties that it is from cli and not ui
918
- def block_state_for_name_from_cli(block_name)
919
- SelectedBlockMenuState.new(
920
- blocks_find_by_block_name(@dml_blocks_in_file, block_name),
921
- OpenStruct.new(
922
- block_name_from_cli: true,
923
- block_name_from_ui: false
924
- ),
925
- MenuState::CONTINUE
926
- )
927
- end
928
-
929
- def blocks_find_by_block_name(blocks, block_name)
930
- @dml_blocks_in_file.find do |item|
931
- # 2024-08-04 match oname for long block names
932
- # 2024-08-04 match nickname for long block names
933
- block_name == item.pub_name || block_name == item.nickname || block_name == item.oname
934
- end || @dml_menu_blocks.find do |item|
935
- # 2024-08-22 search in menu blocks to allow matching of automatic chrome with nickname
936
- block_name == item.pub_name || block_name == item.nickname || block_name == item.oname
937
- end
938
- end
939
-
940
905
  def build_menu_options(exit_option, display_mode_option,
941
906
  menu_entries, display_format)
942
907
  [exit_option,
@@ -1061,6 +1026,19 @@ module MarkdownExec
1061
1026
  exportable = true
1062
1027
  if only_default
1063
1028
  value = case export.default
1029
+ # echo > default
1030
+ when :echo
1031
+ raise unless export.echo.present?
1032
+
1033
+ output = export_echo_with_code(
1034
+ export, inherited_code, code_lines, required
1035
+ )
1036
+ if output == :invalidated
1037
+ return :ux_exec_prohibited
1038
+ end
1039
+
1040
+ transform_export_value(output, export)
1041
+
1064
1042
  # exec > default
1065
1043
  when :exec
1066
1044
  raise unless export.exec.present?
@@ -1073,6 +1051,7 @@ module MarkdownExec
1073
1051
  end
1074
1052
 
1075
1053
  transform_export_value(output, export)
1054
+
1076
1055
  # default
1077
1056
  else
1078
1057
  export.default.to_s
@@ -1080,8 +1059,17 @@ module MarkdownExec
1080
1059
  else
1081
1060
  value = nil
1082
1061
 
1062
+ # echo > exec
1063
+ if export.echo
1064
+ value = export_echo_with_code(
1065
+ export, inherited_code, code_lines, required
1066
+ )
1067
+ if value == :invalidated
1068
+ return :ux_exec_prohibited
1069
+ end
1070
+
1083
1071
  # exec > allowed
1084
- if export.exec
1072
+ elsif export.exec
1085
1073
  value = export_exec_with_code(
1086
1074
  export, inherited_code, code_lines, required
1087
1075
  )
@@ -1382,7 +1370,7 @@ module MarkdownExec
1382
1370
  next if exclude_types.include?(block.type)
1383
1371
 
1384
1372
  # Scan each block name for matches of the pattern
1385
- ([block.oname || ''] + block.body).join("\n").scan(pattern) do |(_, _variable_name)|
1373
+ count_named_group_occurrences_block_body_fix_indent(block).scan(pattern) do |(_, _variable_name)|
1386
1374
  pattern.match($LAST_MATCH_INFO.to_s) # Reapply match for named groups
1387
1375
  occurrence_count[$LAST_MATCH_INFO[group_name]] += 1
1388
1376
  end
@@ -1391,6 +1379,11 @@ module MarkdownExec
1391
1379
  occurrence_count
1392
1380
  end
1393
1381
 
1382
+ def count_named_group_occurrences_block_body_fix_indent(block)
1383
+ ### actually double the entries, but not a problem since it's used as a boolean
1384
+ ([block.oname || ''] + block.body).join("\n")
1385
+ end
1386
+
1394
1387
  ##
1395
1388
  # Creates and adds a formatted block to the blocks array
1396
1389
  # based on the provided match and format options.
@@ -1492,7 +1485,7 @@ module MarkdownExec
1492
1485
  fcb.type = type
1493
1486
  use_fcb = false # next line is new record
1494
1487
  else
1495
- fcb = FCB.new(
1488
+ fcb = persist_fcb(
1496
1489
  center: center,
1497
1490
  chrome: true,
1498
1491
  collapse: collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse,
@@ -1579,7 +1572,7 @@ module MarkdownExec
1579
1572
  oname = format(@delegate_object[:menu_divider_format],
1580
1573
  HashDelegator.safeval(@delegate_object[divider_key]))
1581
1574
 
1582
- FCB.new(
1575
+ persist_fcb(
1583
1576
  chrome: true,
1584
1577
  disabled: TtyMenu::DISABLE,
1585
1578
  dname: string_send_color(oname, :menu_divider_color),
@@ -1989,7 +1982,7 @@ module MarkdownExec
1989
1982
  end
1990
1983
 
1991
1984
  def execute_block_in_state(block_name)
1992
- @dml_block_state = block_state_for_name_from_cli(block_name)
1985
+ @dml_block_state = find_block_state_by_name(block_name)
1993
1986
  dump_and_warn_block_state(name: block_name,
1994
1987
  selected: @dml_block_state.block)
1995
1988
  next_block_state =
@@ -2449,6 +2442,23 @@ module MarkdownExec
2449
2442
  end
2450
2443
  end
2451
2444
 
2445
+ def expand_references!(fcb, link_state)
2446
+ expand_variable_references!(
2447
+ blocks: [fcb],
2448
+ initial_code_required: false,
2449
+ link_state: link_state
2450
+ )
2451
+ expand_variable_references!(
2452
+ blocks: [fcb],
2453
+ echo_format: '%s',
2454
+ group_name: :command,
2455
+ initial_code_required: false,
2456
+ key_format: '$(%s)',
2457
+ link_state: link_state,
2458
+ pattern: options_command_substitution_regexp
2459
+ )
2460
+ end
2461
+
2452
2462
  def expand_variable_references!(
2453
2463
  blocks:,
2454
2464
  echo_format: 'echo $%s',
@@ -2479,6 +2489,18 @@ module MarkdownExec
2479
2489
  expand_blocks_with_replacements(blocks, replacements)
2480
2490
  end
2481
2491
 
2492
+ def export_echo_with_code(export, inherited_code, code_lines, required)
2493
+ value = execute_temporary_script(
2494
+ %Q{eval printf '%s' "#{export.echo}"},
2495
+ (inherited_code || []) +
2496
+ code_lines + required[:code]
2497
+ )
2498
+ if value == :invalidated
2499
+ warn "A value must exist for: #{export.preconditions.join(', ')}"
2500
+ end
2501
+ value
2502
+ end
2503
+
2482
2504
  def export_exec_with_code(export, inherited_code, code_lines, required)
2483
2505
  value = execute_temporary_script(
2484
2506
  export.exec,
@@ -2524,6 +2546,31 @@ module MarkdownExec
2524
2546
  { size: file_size, lines: line_count }
2525
2547
  end
2526
2548
 
2549
+ # Search in @dml_blocks_in_file first,
2550
+ # fallback to @dml_menu_blocks if not found.
2551
+ def find_block_by_name(blocks, block_name)
2552
+ match_block = ->(item) do
2553
+ [item.pub_name, item.nickname,
2554
+ item.oname, item.s2title].include?(block_name)
2555
+ end
2556
+
2557
+ @dml_blocks_in_file.find(&match_block) ||
2558
+ @dml_menu_blocks.find(&match_block)
2559
+ end
2560
+
2561
+ # find a block by its original (undecorated) name or nickname (not visible in menu)
2562
+ # if matched, the block returned has properties that it is from cli and not ui
2563
+ def find_block_state_by_name(block_name)
2564
+ SelectedBlockMenuState.new(
2565
+ find_block_by_name(@dml_blocks_in_file, block_name),
2566
+ OpenStruct.new(
2567
+ block_name_from_cli: true,
2568
+ block_name_from_ui: false
2569
+ ),
2570
+ MenuState::CONTINUE
2571
+ )
2572
+ end
2573
+
2527
2574
  def format_and_execute_command(
2528
2575
  code_lines:,
2529
2576
  erls:,
@@ -2532,6 +2579,7 @@ module MarkdownExec
2532
2579
  formatted_command = code_lines.flatten.join("\n")
2533
2580
  @fout.fout fetch_color(data_sym: :script_execution_head,
2534
2581
  color_sym: :script_execution_frame_color)
2582
+
2535
2583
  command_execute(
2536
2584
  formatted_command,
2537
2585
  args: @pass_args,
@@ -2641,10 +2689,8 @@ module MarkdownExec
2641
2689
  @process_mutex.synchronize do
2642
2690
  Thread.new do
2643
2691
  stream.each_line do |line|
2644
- line.strip!
2645
2692
  if @run_state.files.streams
2646
- @run_state.files.append_stream_line(file_type,
2647
- line)
2693
+ @run_state.files.append_stream_line(file_type, line)
2648
2694
  end
2649
2695
 
2650
2696
  puts line if @delegate_object[:output_stdout]
@@ -2692,7 +2738,7 @@ module MarkdownExec
2692
2738
  Regexp.new(@delegate_object.fetch(
2693
2739
  :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
2694
2740
  )),
2695
- fcb: MarkdownExec::FCB.new(id: 'INIT'),
2741
+ fcb: persist_fcb(id: 'INIT'),
2696
2742
  in_fenced_block: false,
2697
2743
  headings: []
2698
2744
  }
@@ -3180,7 +3226,7 @@ module MarkdownExec
3180
3226
  #
3181
3227
  menu_blocks.each do |fcb|
3182
3228
  fcb.body = fcb.raw_body || fcb.body || []
3183
- fcb.dname = fcb.raw_dname || fcb.dname
3229
+ fcb.name_in_menu!(fcb.raw_dname || fcb.dname)
3184
3230
  fcb.s0printable = fcb.raw_s0printable || fcb.s0printable
3185
3231
  fcb.s1decorated = fcb.raw_s1decorated || fcb.s1decorated
3186
3232
  expand_references!(fcb, link_state)
@@ -3212,7 +3258,7 @@ module MarkdownExec
3212
3258
  #
3213
3259
  return unless block.nil?
3214
3260
 
3215
- chrome_block = FCB.new(
3261
+ chrome_block = persist_fcb(
3216
3262
  chrome: true,
3217
3263
  disabled: TtyMenu::DISABLE,
3218
3264
  dname: HashDelegator.new(@delegate_object).string_send_color(
@@ -3371,6 +3417,12 @@ module MarkdownExec
3371
3417
  prompt_select_continue == MenuState::EXIT
3372
3418
  end
3373
3419
 
3420
+ def persist_fcb(options)
3421
+ MarkdownExec::FCB.new(options).tap do |fcb|
3422
+ @fcb_store << fcb
3423
+ end
3424
+ end
3425
+
3374
3426
  def pop_add_current_code_to_head_and_trigger_load(
3375
3427
  link_state, block_names, code_lines,
3376
3428
  dependencies, selected, next_block_name: nil
@@ -4195,7 +4247,7 @@ module MarkdownExec
4195
4247
  TtyMenu::ENABLE
4196
4248
  end
4197
4249
 
4198
- MarkdownExec::FCB.new(
4250
+ persist_fcb(
4199
4251
  body: [],
4200
4252
  call: rest.match(
4201
4253
  Regexp.new(@delegate_object[:block_calls_scan])
@@ -4320,7 +4372,9 @@ module MarkdownExec
4320
4372
  # add line if it is depth 0 or option allows it
4321
4373
  #
4322
4374
  HashDelegator.yield_line_if_selected(
4323
- line, selected_types, source_id: source_id, &block
4375
+ line, selected_types,
4376
+ all_fcbs: @fcb_store,
4377
+ source_id: source_id, &block
4324
4378
  )
4325
4379
  end
4326
4380
  end
@@ -4359,7 +4413,7 @@ module MarkdownExec
4359
4413
  end
4360
4414
 
4361
4415
  def vux_execute_and_prompt(block_name)
4362
- @dml_block_state = block_state_for_name_from_cli(block_name)
4416
+ @dml_block_state = find_block_state_by_name(block_name)
4363
4417
  if @dml_block_state.block &&
4364
4418
  @dml_block_state.block.type == BlockType::OPTS
4365
4419
  debounce_reset
@@ -4532,7 +4586,8 @@ module MarkdownExec
4532
4586
 
4533
4587
  inherited_block_names = []
4534
4588
  inherited_dependencies = {}
4535
- selected = FCB.new(oname: 'load_code')
4589
+ selected = persist_fcb(oname: 'load_code')
4590
+
4536
4591
  pop_add_current_code_to_head_and_trigger_load(
4537
4592
  @dml_link_state, inherited_block_names,
4538
4593
  code_lines, inherited_dependencies, selected
@@ -4800,7 +4855,7 @@ module MarkdownExec
4800
4855
  def vux_user_selected_block_name
4801
4856
  if @dml_link_state.block_name.present?
4802
4857
  # @prior_block_was_link = true
4803
- @dml_block_state.block = blocks_find_by_block_name(
4858
+ @dml_block_state.block = find_block_by_name(
4804
4859
  @dml_blocks_in_file,
4805
4860
  @dml_link_state.block_name
4806
4861
  )
@@ -5724,7 +5779,7 @@ module MarkdownExec
5724
5779
  Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) }
5725
5780
 
5726
5781
  @hd.wait_for_stream_processing
5727
- assert_equal ['line 1', 'line 2'],
5782
+ assert_equal ["line 1\n", "line 2\n"],
5728
5783
  @hd.instance_variable_get(:@run_state)
5729
5784
  .files.stream_lines(ExecutionStreams::STD_OUT)
5730
5785
  end
@@ -7,5 +7,5 @@ module MarkdownExec
7
7
  BIN_NAME = 'mde'
8
8
  GEM_NAME = 'markdown_exec'
9
9
  TAP_DEBUG = 'MDE_DEBUG'
10
- VERSION = '2.8.2'
10
+ VERSION = '2.8.3'
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdown_exec
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.2
4
+ version: 2.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fareed Stevenson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-19 00:00:00.000000000 Z
11
+ date: 2025-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clipboard
@@ -110,7 +110,10 @@ files:
110
110
  - bats/block-hide.bats
111
111
  - bats/block-type-opts.bats
112
112
  - bats/block-type-ux-auto.bats
113
+ - bats/block-type-ux-echo.bats
113
114
  - bats/block-type-ux-exec.bats
115
+ - bats/block-type-ux-preconditions.bats
116
+ - bats/block-type-ux-readonly.bats
114
117
  - bats/block-type-ux-require.bats
115
118
  - bats/block-type-ux-row-format.bats
116
119
  - bats/block-type-ux-transform.bats
@@ -123,6 +126,8 @@ files:
123
126
  - bats/fail.bats
124
127
  - bats/history.bats
125
128
  - bats/import.bats
129
+ - bats/indented-multi-line-output.bats
130
+ - bats/line-decor-dynamic.bats
126
131
  - bats/line-wrapping.bats
127
132
  - bats/markup.bats
128
133
  - bats/mde.bats
@@ -147,7 +152,10 @@ files:
147
152
  - docs/dev/block-type-opts.md
148
153
  - docs/dev/block-type-port.md
149
154
  - docs/dev/block-type-ux-auto.md
155
+ - docs/dev/block-type-ux-echo.md
150
156
  - docs/dev/block-type-ux-exec.md
157
+ - docs/dev/block-type-ux-preconditions.md
158
+ - docs/dev/block-type-ux-readonly.md
151
159
  - docs/dev/block-type-ux-require.md
152
160
  - docs/dev/block-type-ux-row-format.md
153
161
  - docs/dev/block-type-ux-transform.md
@@ -159,6 +167,8 @@ files:
159
167
  - docs/dev/document-shell.md
160
168
  - docs/dev/import-missing.md
161
169
  - docs/dev/import.md
170
+ - docs/dev/indented-multi-line-output.md
171
+ - docs/dev/line-decor-dynamic.md
162
172
  - docs/dev/line-wrapping.md
163
173
  - docs/dev/linked-file.md
164
174
  - docs/dev/load1.sh