markdown_exec 3.2.0 → 3.4.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/Gemfile.lock +1 -1
  4. data/Rakefile +5 -4
  5. data/bats/block-type-ux-echo-hash-transform.bats +1 -1
  6. data/bats/block-type-ux-exec-hash-transform.bats +8 -0
  7. data/bats/block-type-ux-exec-hash.bats +15 -0
  8. data/bats/block-type-ux-force.bats +9 -0
  9. data/bats/block-type-ux-formats.bats +8 -0
  10. data/bats/block-type-ux-no-name.bats +8 -0
  11. data/bats/block-type-ux-readonly.bats +1 -1
  12. data/bats/block-type-ux-row-format.bats +1 -1
  13. data/bats/command-substitution-options.bats +2 -2
  14. data/bats/import-directive-line-continuation.bats +9 -0
  15. data/bats/import-directive-parameter-symbols.bats +9 -0
  16. data/bats/import-duplicates.bats +4 -2
  17. data/bats/import-parameter-symbols.bats +8 -0
  18. data/bats/markup.bats +1 -1
  19. data/bats/option-expansion.bats +1 -1
  20. data/bats/options.bats +1 -1
  21. data/bats/table-column-truncate.bats +1 -1
  22. data/bats/table.bats +1 -1
  23. data/bats/test_helper.bash +4 -3
  24. data/bin/tab_completion.sh +5 -1
  25. data/docs/dev/block-type-ux-echo-hash-transform.md +14 -12
  26. data/docs/dev/block-type-ux-exec-hash-transform.md +37 -0
  27. data/docs/dev/block-type-ux-exec-hash.md +93 -0
  28. data/docs/dev/block-type-ux-force.md +20 -0
  29. data/docs/dev/block-type-ux-formats.md +58 -0
  30. data/docs/dev/block-type-ux-no-name.md +17 -0
  31. data/docs/dev/block-type-ux-row-format.md +1 -1
  32. data/docs/dev/hexdump_format.md +267 -0
  33. data/docs/dev/import/parameter-symbols.md +6 -0
  34. data/docs/dev/import-directive-line-continuation.md +6 -0
  35. data/docs/dev/import-directive-parameter-symbols.md +9 -0
  36. data/docs/dev/import-parameter-symbols-template.md +24 -0
  37. data/docs/dev/import-parameter-symbols.md +6 -0
  38. data/docs/dev/load-vars-state-demo.md +35 -0
  39. data/docs/dev/table-column-truncate.md +1 -1
  40. data/docs/ux-blocks-examples.md +2 -3
  41. data/examples/import_with_substitution_demo.md +130 -26
  42. data/examples/imports/organism_template.md +86 -29
  43. data/lib/cached_nested_file_reader.rb +279 -29
  44. data/lib/constants.rb +8 -1
  45. data/lib/env_interface.rb +13 -7
  46. data/lib/evaluate_shell_expressions.rb +1 -0
  47. data/lib/fcb.rb +133 -33
  48. data/lib/format_table.rb +77 -29
  49. data/lib/fout.rb +5 -0
  50. data/lib/hash_delegator.rb +1159 -348
  51. data/lib/markdown_exec/version.rb +1 -1
  52. data/lib/markdown_exec.rb +2 -0
  53. data/lib/mdoc.rb +13 -11
  54. data/lib/menu.src.yml +166 -62
  55. data/lib/menu.yml +143 -59
  56. data/lib/string_util.rb +80 -0
  57. data/lib/table_extractor.rb +170 -64
  58. data/lib/ww.rb +810 -36
  59. metadata +22 -2
@@ -1,42 +1,99 @@
1
1
  # COMMON_NAME Classification
2
2
 
3
- **Common Name:** COMMON_NAME
4
- **Species:** SPECIES
5
- **Genus:** GENUS
6
- **Family:** FAMILY
7
- **Order:** ORDER
8
- **Class:** CLASS
9
- **Year Discovered:** YEAR_DISCOVERED
3
+ **Common Name:** ${COMMON_NAME}
4
+ **Species:** ${PLAIN_SPECIES}
5
+ **Genus:** ${GENUS}
6
+ **Family:** ${FAMILY}
7
+ **Order:** ${ORDER}
8
+ **Class:** ${CLASSIFICATION_TYPE}
9
+ **Year Discovered:** ${YEAR_DISCOVERED}
10
+
11
+ ## Parameter Processing Examples
12
+
13
+ __Raw Literal (=)__
14
+ - Report title: `REPORT_TITLE`
15
+ - Fixed classification: `CLASSIFICATION_TYPE`
16
+
17
+ __Force-Quoted Literal (:q=)__
18
+ - Quoted species name: `QUOTED_SPECIES`
19
+ - Quoted common name: `QUOTED_NAME`
20
+
21
+ __Variable Reference (:v=)__
22
+ - Common name from variable: `BASE_NAME`
23
+ - Discovery year from document: `DOC_YEAR`
24
+
25
+ __Evaluated Expression (:e=)__
26
+ - Generated filename: `GENERATED_FILE`
27
+ - Formatted description: `DESCRIPTION`
28
+
29
+ __Command Substitution (:c=)__
30
+ - Current timestamp: `TIMESTAMP`
31
+ - Latest report file: `LATEST_REPORT`
10
32
 
11
33
  ## Taxonomic Classification
12
34
 
13
35
  ```bash
14
- # Biological classification data
15
- export COMMON_NAME="COMMON_NAME"
16
- export SPECIES="SPECIES"
17
- export GENUS="GENUS"
18
- export FAMILY="FAMILY"
19
- export ORDER="ORDER"
20
- export CLASS="CLASS"
21
- export YEAR_DISCOVERED="YEAR_DISCOVERED"
22
-
23
- echo "Organism: COMMON_NAME"
24
- echo "Scientific name: SPECIES"
25
- echo "Discovered in: YEAR_DISCOVERED"
36
+ # Biological classification data with different parameter types
37
+ export common_name=COMMON_NAME
38
+ export species="PLAIN_SPECIES"
39
+ export genus="GENUS"
40
+ export family="FAMILY"
41
+ export order="ORDER"
42
+ export class="CLASSIFICATION_TYPE"
43
+ export year_discovered="YEAR_DISCOVERED"
44
+
45
+ # Examples of parameter processing types
46
+ echo "Report: REPORT_TITLE"
47
+ echo "Quoted species: QUOTED_SPECIES"
48
+ echo "Quoted name: QUOTED_NAME"
49
+ echo "Base name from var: BASE_NAME"
50
+ echo "Doc year from var: DOC_YEAR"
51
+ echo "Generated file: GENERATED_FILE"
52
+ echo "Description: DESCRIPTION"
53
+ echo "Timestamp: TIMESTAMP"
54
+ echo "Latest report: LATEST_REPORT"
55
+
56
+ echo "Organism: $common_name"
57
+ echo "Scientific name: $species"
58
+ echo "Discovered in: $year_discovered"
59
+ echo "Classification: $class"
26
60
  ```
27
61
 
28
62
  ## Classification Hierarchy
29
63
 
30
- ```yaml
31
64
  organism:
32
- common_name: COMMON_NAME
33
- scientific_name: SPECIES
65
+ common_name: ${COMMON_NAME}
66
+ scientific_name: ${PLAIN_SPECIES}
34
67
  taxonomy:
35
- genus: GENUS
36
- family: FAMILY
37
- order: ORDER
38
- class: CLASS
39
- discovery_year: YEAR_DISCOVERED
40
- ```
68
+ genus: ${GENUS}
69
+ family: ${FAMILY}
70
+ order: ${ORDER}
71
+ class: ${CLASSIFICATION_TYPE}
72
+ discovery_year: ${YEAR_DISCOVERED}
73
+
74
+ metadata:
75
+ report_title: ${REPORT_TITLE}
76
+ classification_type: ${CLASSIFICATION_TYPE}
77
+ quoted_species: ${QUOTED_SPECIES}
78
+ quoted_name: ${QUOTED_NAME}
79
+ base_name: ${BASE_NAME}
80
+ doc_year: ${DOC_YEAR}
81
+ generated_file: ${GENERATED_FILE}
82
+ description: ${DESCRIPTION}
83
+ timestamp: ${TIMESTAMP}
84
+ latest_report: ${LATEST_REPORT}
85
+
86
+ ## Generated Documentation
87
+
88
+ **Report:** ${REPORT_TITLE}
89
+ **Classification:** ${CLASSIFICATION_TYPE}
90
+ **Quoted Species:** ${QUOTED_SPECIES}
91
+ **Quoted Name:** ${QUOTED_NAME}
92
+ **Base Name:** ${BASE_NAME}
93
+ **Doc Year:** ${DOC_YEAR}
94
+ **Generated File:** ${GENERATED_FILE}
95
+ **Description:** ${DESCRIPTION}
96
+ **Timestamp:** ${TIMESTAMP}
97
+ **Latest Report:** ${LATEST_REPORT}
41
98
 
42
- Biological organism template using raw replacement for taxonomic data.
99
+ Biological organism template demonstrating all five parameter processing modes: raw literal (=), force-quoted literal (:q=), variable reference (:v=), evaluated expression (:e=), and command substitution (:c=).
@@ -9,6 +9,7 @@ require 'fileutils'
9
9
  require_relative 'constants'
10
10
  require_relative 'exceptions'
11
11
  require_relative 'find_files'
12
+ require_relative 'ww'
12
13
 
13
14
  ##
14
15
  # The CachedNestedFileReader class provides functionality to read file
@@ -22,9 +23,29 @@ require_relative 'find_files'
22
23
  class CachedNestedFileReader
23
24
  include Exceptions
24
25
 
25
- def initialize(import_pattern: /^ *#import (.+)$/)
26
+ def initialize(
27
+ import_directive_line_pattern:,
28
+ import_directive_parameter_scan:,
29
+ import_parameter_variable_assignment:,
30
+ shell:,
31
+ shell_block_name:,
32
+ symbol_command_substitution:,
33
+ symbol_evaluated_expression:,
34
+ symbol_force_quoted_literal:,
35
+ symbol_raw_literal:,
36
+ symbol_variable_reference:
37
+ )
26
38
  @file_cache = {}
27
- @import_pattern = import_pattern
39
+ @import_directive_line_pattern = import_directive_line_pattern
40
+ @import_directive_parameter_scan = import_directive_parameter_scan
41
+ @import_parameter_variable_assignment = import_parameter_variable_assignment
42
+ @shell = shell
43
+ @shell_block_name = shell_block_name
44
+ @symbol_command_substitution = symbol_command_substitution
45
+ @symbol_evaluated_expression = symbol_evaluated_expression
46
+ @symbol_force_quoted_literal = symbol_force_quoted_literal
47
+ @symbol_raw_literal = symbol_raw_literal
48
+ @symbol_variable_reference = symbol_variable_reference
28
49
  end
29
50
 
30
51
  def error_handler(name = '', opts = {})
@@ -45,25 +66,68 @@ class CachedNestedFileReader
45
66
  # return the processed lines
46
67
  def readlines(
47
68
  filename, depth = 0, context: '', import_paths: nil,
48
- indention: '', substitutions: {}, use_template_delimiters: false, &block
69
+ indention: '', substitutions: {}, use_template_delimiters: false,
70
+ clear_cache: true,
71
+ read_cache: false,
72
+ &block
49
73
  )
74
+ # clear cache if requested
75
+ @file_cache.clear if clear_cache
76
+
50
77
  cache_key = build_cache_key(filename, substitutions)
51
78
  if @file_cache.key?(cache_key)
79
+ return ["# dup #{cache_key}"] unless read_cache
80
+
52
81
  @file_cache[cache_key].each(&block) if block
53
82
  return @file_cache[cache_key]
83
+
84
+ # do not return duplicates per filename and substitutions
85
+ # return an indicator that the file was already read
86
+
54
87
  end
55
88
  raise Errno::ENOENT, filename unless filename
56
89
 
57
90
  directory_path = File.dirname(filename)
58
91
  processed_lines = []
59
- File.readlines(filename, chomp: true).each.with_index do |line, ind|
60
- if Regexp.new(@import_pattern) =~ line
92
+ continued_line = nil # continued import directive
93
+ File.readlines(filename, chomp: true).each.with_index do |segment, ind|
94
+ wwt :readline, 'depth:', depth, 'filename:', filename, 'ind:', ind,
95
+ 'segment:', segment
96
+
97
+ if continued_line || (Regexp.new(@import_directive_line_pattern) =~ segment)
98
+
99
+ line = (continued_line || '') + segment
100
+ # if segment ends in a continuation, prepend to next line
101
+ if line.end_with?('\\')
102
+ continued_line = line.chomp('\\')
103
+ next
104
+ end
105
+
106
+ continued_line = nil
107
+ Regexp.new(@import_directive_line_pattern) =~ line
61
108
  name_strip = $~[:name].strip
62
109
  params_string = $~[:params] || ''
63
110
  import_indention = indention + $~[:indention]
64
111
 
65
112
  # Parse parameters for text substitution
66
- import_substitutions = parse_import_params(params_string)
113
+ import_substitutions, add_code = parse_import_params(params_string)
114
+ if add_code
115
+ # strings as NestedLines
116
+ add_lines = add_code.map.with_index do |line2, ind2|
117
+ nested_line = NestedLine.new(
118
+ line2,
119
+ depth + 1,
120
+ import_indention,
121
+ filename,
122
+ ind2
123
+ )
124
+ block&.call(nested_line)
125
+
126
+ nested_line
127
+ end
128
+ ww 'add_lines:', add_lines
129
+ processed_lines += add_lines
130
+ end
67
131
  merged_substitutions = substitutions.merge(import_substitutions)
68
132
 
69
133
  included_file_path =
@@ -78,15 +142,28 @@ class CachedNestedFileReader
78
142
 
79
143
  raise Errno::ENOENT, name_strip unless included_file_path
80
144
 
81
- imported_lines = readlines(
82
- included_file_path, depth + 1,
83
- context: "#{filename}:#{ind + 1}",
84
- import_paths: import_paths,
85
- indention: import_indention,
86
- substitutions: merged_substitutions,
87
- use_template_delimiters: use_template_delimiters,
88
- &block
89
- )
145
+ # Create a cache key for the imported file that includes both filename and parameters
146
+ imported_cache_key = build_import_cache_key(included_file_path,
147
+ name_strip, params_string, merged_substitutions)
148
+
149
+ # Check if we've already loaded this specific import
150
+ if @file_cache.key?(imported_cache_key)
151
+ imported_lines = @file_cache[imported_cache_key]
152
+ else
153
+ imported_lines = readlines(
154
+ included_file_path, depth + 1,
155
+ context: "#{filename}:#{ind + 1}",
156
+ import_paths: import_paths,
157
+ indention: import_indention,
158
+ substitutions: merged_substitutions,
159
+ use_template_delimiters: use_template_delimiters,
160
+ clear_cache: false,
161
+ &block
162
+ )
163
+
164
+ # Cache the imported lines with the specific import cache key
165
+ @file_cache[imported_cache_key] = imported_lines
166
+ end
90
167
 
91
168
  # Apply text substitutions to imported content
92
169
  processed_imported_lines = apply_substitutions(
@@ -96,7 +173,7 @@ class CachedNestedFileReader
96
173
  processed_lines += processed_imported_lines
97
174
  else
98
175
  # Apply substitutions to the current line
99
- substituted_line = apply_line_substitutions(line, substitutions,
176
+ substituted_line = apply_line_substitutions(segment, substitutions,
100
177
  use_template_delimiters)
101
178
  nested_line = NestedLine.new(substituted_line, depth, indention,
102
179
  filename, ind)
@@ -105,6 +182,7 @@ class CachedNestedFileReader
105
182
  end
106
183
  end
107
184
 
185
+ wwt :read_document_code, 'processed_lines:', processed_lines
108
186
  @file_cache[cache_key] = processed_lines
109
187
  rescue Errno::ENOENT => err
110
188
  warn_format('readlines', "#{err} @@ #{context}",
@@ -113,21 +191,91 @@ class CachedNestedFileReader
113
191
 
114
192
  private
115
193
 
194
+ def shell_code_block_for_assignment(key, expression)
195
+ ["```#{@shell} :#{@shell_block_name}",
196
+ format(@import_parameter_variable_assignment,
197
+ { key: key, value: expression }),
198
+ '```'].tap { wwr _1 }
199
+ end
200
+
116
201
  # Parse key=value parameters from the import line
117
202
  def parse_import_params(params_string)
118
203
  return {} if params_string.nil? || params_string.strip.empty?
119
204
 
205
+ add_code = []
120
206
  params = {}
121
- # Match key=value pairs, handling quoted values
122
- params_string.scan(
123
- /([A-Za-z_]\w*)=(?:"([^"]*)"|'([^']*)'|(\S+))/
124
- ) do |key, quoted_double, quoted_single, unquoted|
207
+
208
+ # First loop: store all scanned parameters in a temporary variable
209
+ scanned_params = []
210
+ params_string.scan(@import_directive_parameter_scan) do |key, op, quoted_double, quoted_single, unquoted|
125
211
  value = quoted_double || quoted_single || unquoted
126
- # skip replacement of equal values
127
- # otherwise, the text is not available for other substitutions
128
- params[key] = value if key != value
212
+ scanned_params << [key, op, value]
129
213
  end
130
- params
214
+ wwt 'scanned_params:', scanned_params
215
+
216
+ # Preceding loop: select items where op = '='
217
+ equal_op_params = scanned_params.select { |_key, op, _value| op == '=' }
218
+ wwt 'equal_op_params:', equal_op_params
219
+ # Process remaining parameters (non-equal operators)
220
+ scanned_params.each do |key, op, value|
221
+ wwt :import, 'key:', key, 'op:', op, 'value:', value
222
+
223
+ # adjust key for current stem value
224
+ varname = key
225
+ case op
226
+ when @symbol_command_substitution,
227
+ @symbol_evaluated_expression,
228
+ @symbol_force_quoted_literal,
229
+ @symbol_variable_reference
230
+ # perform all substitutions per equal_op_params
231
+ equal_op_params.each do |equal_key, _equal_op, equal_value|
232
+ varname = varname.gsub(equal_key, equal_value)
233
+ end
234
+ wwt :import_key, 'varname:', varname
235
+ end
236
+
237
+ case op
238
+ when @symbol_raw_literal
239
+ # skip replacement of equal values otherwise,
240
+ # the text is not available for other substitutions
241
+ next unless key != value
242
+
243
+ # replace the literal below
244
+ when @symbol_command_substitution
245
+ # add code to set the variable to the value of the parameter
246
+ add_code += shell_code_block_for_assignment(
247
+ varname, %($(#{value}))
248
+ )
249
+ # replace key with expansion of the added variable
250
+ value = "${#{varname}}"
251
+ when @symbol_evaluated_expression
252
+ # add code to set the variable to the value of the parameter
253
+ add_code += shell_code_block_for_assignment(
254
+ varname, %("#{value}")
255
+ )
256
+ # replace key with expansion of the added variable
257
+ value = "${#{varname}}"
258
+ when @symbol_force_quoted_literal
259
+ # add code to set the variable to the value of the parameter
260
+ add_code += shell_code_block_for_assignment(
261
+ varname, Shellwords.escape(value)
262
+ )
263
+ # replace key with expansion of the added variable
264
+ value = "${#{varname}}"
265
+ when @symbol_variable_reference
266
+ # variable exists
267
+ value = "${#{value}}"
268
+ else
269
+ wwe "Invalid op '#{op}'"
270
+ end
271
+
272
+ params[key] = value
273
+ end
274
+ [params, add_code].tap do
275
+ wwr 'params:', _1[0], 'add_code:', _1[1]
276
+ end
277
+ rescue StandardError
278
+ wwe $!
131
279
  end
132
280
 
133
281
  # Apply text substitutions to a collection of NestedLine objects
@@ -155,8 +303,10 @@ class CachedNestedFileReader
155
303
  if use_template_delimiters
156
304
  # Replace template-style placeholders: ${KEY} or {{KEY}}
157
305
  substitutions.each do |key, value|
158
- substituted_line = substituted_line.gsub(/\$\{#{Regexp.escape(key)}\}/,
159
- value)
306
+ substituted_line = substituted_line.gsub(
307
+ /\$\{#{Regexp.escape(key)}\}/,
308
+ value
309
+ )
160
310
  substituted_line = substituted_line.gsub(
161
311
  /\{\{#{Regexp.escape(key)}\}\}/, value
162
312
  )
@@ -168,7 +318,6 @@ class CachedNestedFileReader
168
318
  # Replace each key with a unique temporary placeholder
169
319
  substitutions.each_with_index do |(key, value), index|
170
320
  temp_placeholder = "__MDE_TEMP_#{index}__"
171
- # pattern = /\b#{Regexp.escape(key)}\b/
172
321
  pattern = Regexp.new(Regexp.escape(key))
173
322
  substituted_line = substituted_line.gsub(pattern, temp_placeholder)
174
323
  temp_placeholders[temp_placeholder] = value
@@ -190,6 +339,13 @@ class CachedNestedFileReader
190
339
  substitution_hash = substitutions.sort.to_h.hash
191
340
  "#{filename}##{substitution_hash}"
192
341
  end
342
+
343
+ # Build a cache key specifically for imported files
344
+ def build_import_cache_key(filename, name_strip, params_string, substitutions)
345
+ # Sort parameters for consistent key
346
+ sorted_params = substitutions.sort.to_h.hash
347
+ "#{filename}##{name_strip}##{params_string}##{sorted_params}"
348
+ end
193
349
  end
194
350
 
195
351
  return if $PROGRAM_NAME != __FILE__
@@ -207,7 +363,18 @@ class CachedNestedFileReaderTest < Minitest::Test
207
363
  @file1.write("Line1\nLine2\n @import #{@file2.path}\nLine3")
208
364
  @file1.rewind
209
365
  @reader = CachedNestedFileReader.new(
210
- import_pattern: /^(?<indention> *)@import +(?<name>\S+)(?<params>(?: +[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S+))*) *$/
366
+ import_directive_line_pattern:
367
+ /^(?<indention> *)@import +(?<name>\S+)(?<params>(?: +[A-Za-z_]\w*=(?:"[^"]*"|'[^']*'|\S+))*) *$/,
368
+ import_directive_parameter_scan:
369
+ /([A-Za-z_]\w*)(:=|\?=|!=|=)(?:"([^"]*)"|'([^']*)'|(\S+))/,
370
+ import_parameter_variable_assignment: '%{key}=%{value}',
371
+ shell: 'bash',
372
+ shell_block_name: '(document_shell)',
373
+ symbol_command_substitution: ':c=',
374
+ symbol_evaluated_expression: ':e=',
375
+ symbol_raw_literal: '=',
376
+ symbol_force_quoted_literal: ':q=',
377
+ symbol_variable_reference: ':v='
211
378
  )
212
379
  end
213
380
 
@@ -278,9 +445,92 @@ class CachedNestedFileReaderTest < Minitest::Test
278
445
  @file2.reopen(@file2.path, 'w') { |f| f.write('ChangedLine') }
279
446
 
280
447
  # Second read (should read from cache, not the changed file)
281
- result2 = @reader.readlines(@file2.path).map(&:to_s)
448
+ result2 = @reader.readlines(@file2.path, clear_cache: false,
449
+ read_cache: true).map(&:to_s)
282
450
 
283
451
  assert_equal result1, result2
284
452
  assert_equal %w[ImportedLine1 ImportedLine2], result2
285
453
  end
454
+
455
+ def test_import_caching_with_same_parameters
456
+ # Create a file that will be imported multiple times
457
+ shared_file = Tempfile.new('shared.txt')
458
+ shared_file.write("Shared content line 1\nShared content line 2")
459
+ shared_file.rewind
460
+
461
+ # Create a file that imports the same file multiple times with same parameters
462
+ importing_file = Tempfile.new('importing_multiple.txt')
463
+ importing_file.write("Start\n @import #{shared_file.path} PARAM=value\nMiddle\n @import #{shared_file.path} PARAM=value\nEnd")
464
+ importing_file.rewind
465
+
466
+ # Track how many times the shared file is actually read
467
+ read_count = 0
468
+ original_readlines = File.method(:readlines)
469
+ File.define_singleton_method(:readlines) do |filename, **opts|
470
+ if filename == shared_file.path
471
+ read_count += 1
472
+ end
473
+ original_readlines.call(filename, **opts)
474
+ end
475
+
476
+ result = @reader.readlines(importing_file.path).map(&:to_s)
477
+
478
+ # The shared file should only be read once, not twice
479
+ assert_equal 1, read_count,
480
+ 'Shared file should only be read once when imported with same parameters'
481
+
482
+ # Verify the content is correct
483
+ expected = ['Start', ' Shared content line 1', ' Shared content line 2',
484
+ 'Middle', ' Shared content line 1', ' Shared content line 2', 'End']
485
+ assert_equal expected, result
486
+
487
+ # Restore original method
488
+ File.define_singleton_method(:readlines, original_readlines)
489
+
490
+ shared_file.close
491
+ shared_file.unlink
492
+ importing_file.close
493
+ importing_file.unlink
494
+ end
495
+
496
+ def test_import_caching_with_different_parameters
497
+ # Create a file that will be imported with different parameters
498
+ template_file = Tempfile.new('template.txt')
499
+ template_file.write('Hello NAME, your ID is ID')
500
+ template_file.rewind
501
+
502
+ # Create a file that imports the same file with different parameters
503
+ importing_file = Tempfile.new('importing_different.txt')
504
+ importing_file.write("Users:\n @import #{template_file.path} NAME=Alice ID=123\n @import #{template_file.path} NAME=Bob ID=456\nEnd")
505
+ importing_file.rewind
506
+
507
+ # Track how many times the template file is actually read
508
+ read_count = 0
509
+ original_readlines = File.method(:readlines)
510
+ File.define_singleton_method(:readlines) do |filename, **opts|
511
+ if filename == template_file.path
512
+ read_count += 1
513
+ end
514
+ original_readlines.call(filename, **opts)
515
+ end
516
+
517
+ result = @reader.readlines(importing_file.path).map(&:to_s)
518
+
519
+ # The template file should be read twice since parameters are different
520
+ assert_equal 2, read_count,
521
+ 'Template file should be read twice when imported with different parameters'
522
+
523
+ # Verify the content is correct
524
+ expected = ['Users:', ' Hello Alice, your 123 is 123',
525
+ ' Hello Bob, your 456 is 456', 'End']
526
+ assert_equal expected, result
527
+
528
+ # Restore original method
529
+ File.define_singleton_method(:readlines, original_readlines)
530
+
531
+ template_file.close
532
+ template_file.unlink
533
+ importing_file.close
534
+ importing_file.unlink
535
+ end
286
536
  end
data/lib/constants.rb CHANGED
@@ -27,7 +27,14 @@ BLOCK_TYPE_COLOR_OPTIONS = {
27
27
  BlockType::OPTS => :menu_opts_color,
28
28
  BlockType::SAVE => :menu_save_color,
29
29
  BlockType::SHELL => :menu_bash_color,
30
- BlockType::UX => :menu_ux_color,
30
+ BlockType::UX => {
31
+ :is_allow? => :menu_ux_color_allow,
32
+ :is_echo? => :menu_ux_color_echo,
33
+ :is_edit? => :menu_ux_color_edit,
34
+ :is_exec? => :menu_ux_color_exec,
35
+ :readonly => :menu_ux_color_readonly,
36
+ true => :menu_ux_color
37
+ },
31
38
  BlockType::VARS => :menu_vars_color
32
39
  }.freeze
33
40
 
data/lib/env_interface.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'ww'
3
4
 
4
5
  # A class that provides an interface to ENV variables with customizable getters and setters
@@ -10,11 +11,15 @@ class EnvInterface
10
11
  # @param transform [Proc] Optional transformation to apply to the value
11
12
  # @return [Object] The environment variable value
12
13
  def get(key, default: nil, transform: nil)
13
- wwt :env, key, \
14
- value = ENV[key]
15
- return default if value.nil?
14
+ value = ENV.fetch(key, nil)
16
15
 
17
- transform ? transform.call(value) : value
16
+ if value.nil?
17
+ default
18
+ else
19
+ transform ? transform.call(value) : value
20
+ end.tap do
21
+ wwt :env, key, _1
22
+ end
18
23
  end
19
24
 
20
25
  # Set an environment variable with optional transformation
@@ -23,9 +28,10 @@ wwt :env, key, \
23
28
  # @param transform [Proc] Optional transformation to apply before setting
24
29
  # @return [String] The set value
25
30
  def set(key, value, transform: nil)
26
- wwt :env, key, caller.deref[0..3], value
27
31
  transformed_value = transform ? transform.call(value) : value
28
- ENV[key] = transformed_value.to_s
32
+ ENV[key] = transformed_value.to_s.tap do
33
+ wwt :env, key, _1
34
+ end
29
35
  end
30
36
 
31
37
  # Check if an environment variable exists
@@ -54,4 +60,4 @@ wwt :env, key, caller.deref[0..3], value
54
60
  ENV.clear
55
61
  end
56
62
  end
57
- end
63
+ end
@@ -25,6 +25,7 @@ def evaluate_shell_expressions(initial_code, expressions, shell: '/bin/bash',
25
25
  script << "\necho #{token}#{index}\n"
26
26
  script << expression << "\n"
27
27
  end
28
+ wwt :eval, 'script:', script
28
29
 
29
30
  # Execute
30
31
  stdout_str, _, status = Open3.capture3(shell, '-c', script)