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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +5 -4
- data/bats/block-type-ux-echo-hash-transform.bats +1 -1
- data/bats/block-type-ux-exec-hash-transform.bats +8 -0
- data/bats/block-type-ux-exec-hash.bats +15 -0
- data/bats/block-type-ux-force.bats +9 -0
- data/bats/block-type-ux-formats.bats +8 -0
- data/bats/block-type-ux-no-name.bats +8 -0
- data/bats/block-type-ux-readonly.bats +1 -1
- data/bats/block-type-ux-row-format.bats +1 -1
- data/bats/command-substitution-options.bats +2 -2
- data/bats/import-directive-line-continuation.bats +9 -0
- data/bats/import-directive-parameter-symbols.bats +9 -0
- data/bats/import-duplicates.bats +4 -2
- data/bats/import-parameter-symbols.bats +8 -0
- data/bats/markup.bats +1 -1
- data/bats/option-expansion.bats +1 -1
- data/bats/options.bats +1 -1
- data/bats/table-column-truncate.bats +1 -1
- data/bats/table.bats +1 -1
- data/bats/test_helper.bash +4 -3
- data/bin/tab_completion.sh +5 -1
- data/docs/dev/block-type-ux-echo-hash-transform.md +14 -12
- data/docs/dev/block-type-ux-exec-hash-transform.md +37 -0
- data/docs/dev/block-type-ux-exec-hash.md +93 -0
- data/docs/dev/block-type-ux-force.md +20 -0
- data/docs/dev/block-type-ux-formats.md +58 -0
- data/docs/dev/block-type-ux-no-name.md +17 -0
- data/docs/dev/block-type-ux-row-format.md +1 -1
- data/docs/dev/hexdump_format.md +267 -0
- data/docs/dev/import/parameter-symbols.md +6 -0
- data/docs/dev/import-directive-line-continuation.md +6 -0
- data/docs/dev/import-directive-parameter-symbols.md +9 -0
- data/docs/dev/import-parameter-symbols-template.md +24 -0
- data/docs/dev/import-parameter-symbols.md +6 -0
- data/docs/dev/load-vars-state-demo.md +35 -0
- data/docs/dev/table-column-truncate.md +1 -1
- data/docs/ux-blocks-examples.md +2 -3
- data/examples/import_with_substitution_demo.md +130 -26
- data/examples/imports/organism_template.md +86 -29
- data/lib/cached_nested_file_reader.rb +279 -29
- data/lib/constants.rb +8 -1
- data/lib/env_interface.rb +13 -7
- data/lib/evaluate_shell_expressions.rb +1 -0
- data/lib/fcb.rb +133 -33
- data/lib/format_table.rb +77 -29
- data/lib/fout.rb +5 -0
- data/lib/hash_delegator.rb +1159 -348
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +2 -0
- data/lib/mdoc.rb +13 -11
- data/lib/menu.src.yml +166 -62
- data/lib/menu.yml +143 -59
- data/lib/string_util.rb +80 -0
- data/lib/table_extractor.rb +170 -64
- data/lib/ww.rb +810 -36
- metadata +22 -2
@@ -1,42 +1,99 @@
|
|
1
1
|
# COMMON_NAME Classification
|
2
2
|
|
3
|
-
**Common Name:** COMMON_NAME
|
4
|
-
**Species:**
|
5
|
-
**Genus:** GENUS
|
6
|
-
**Family:** FAMILY
|
7
|
-
**Order:** ORDER
|
8
|
-
**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
|
16
|
-
export
|
17
|
-
export
|
18
|
-
export
|
19
|
-
export
|
20
|
-
export
|
21
|
-
export
|
22
|
-
|
23
|
-
|
24
|
-
echo "
|
25
|
-
echo "
|
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:
|
65
|
+
common_name: ${COMMON_NAME}
|
66
|
+
scientific_name: ${PLAIN_SPECIES}
|
34
67
|
taxonomy:
|
35
|
-
genus: GENUS
|
36
|
-
family: FAMILY
|
37
|
-
order: ORDER
|
38
|
-
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
|
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(
|
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
|
-
@
|
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,
|
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
|
-
|
60
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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(
|
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
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
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
|
-
|
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(
|
159
|
-
|
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
|
-
|
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
|
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 =>
|
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
|
-
|
14
|
-
value = ENV[key]
|
15
|
-
return default if value.nil?
|
14
|
+
value = ENV.fetch(key, nil)
|
16
15
|
|
17
|
-
|
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)
|