liquidoc 0.7.0 → 0.8.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/lib/liquidoc/version.rb +1 -1
- data/lib/liquidoc.rb +191 -29
- metadata +2 -3
- data/lib/yaml_plus.rb +0 -60
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 680eeb03f5bdca5f09d010b37c2ba8a4fa2f7060
|
4
|
+
data.tar.gz: c5b06675ef4d7e2fcdcf5e0e1838295864edf094
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9658b656c88006e9fae3d16e35c00f12132f19a57fc963241650bc5a6b2849ce7efa4198f101c51f319f0b674936d624c29cd67ae5d292082cda773bebe5e77b
|
7
|
+
data.tar.gz: d63050fcd42fce26362c7cf9300a75f2a51213da2606e3cfb2fed84792b249c7b9a317da21120dc91194657124e11fdb1e9dc0a2378f0aa41f2046b2382d0f8c
|
data/lib/liquidoc/version.rb
CHANGED
data/lib/liquidoc.rb
CHANGED
@@ -9,6 +9,7 @@ require 'logger'
|
|
9
9
|
require 'csv'
|
10
10
|
require 'crack/xml'
|
11
11
|
require 'fileutils'
|
12
|
+
require 'jekyll'
|
12
13
|
|
13
14
|
# ===
|
14
15
|
# Table of Contents
|
@@ -32,6 +33,8 @@ require 'fileutils'
|
|
32
33
|
|
33
34
|
@base_dir_def = Dir.pwd + '/'
|
34
35
|
@base_dir = @base_dir_def
|
36
|
+
@build_dir_def = @base_dir + '_build'
|
37
|
+
@build_dir = @build_dir_def
|
35
38
|
@configs_dir = @base_dir + '_configs'
|
36
39
|
@templates_dir = @base_dir + '_templates/'
|
37
40
|
@data_dir = @base_dir + '_data/'
|
@@ -43,12 +46,20 @@ require 'fileutils'
|
|
43
46
|
@attributes = {}
|
44
47
|
@passed_attrs = {}
|
45
48
|
@verbose = false
|
49
|
+
@quiet = false
|
50
|
+
@explicit = false
|
46
51
|
|
52
|
+
# Instantiate the main Logger object, which is always running
|
47
53
|
@logger = Logger.new(STDOUT)
|
48
|
-
@logger.level = Logger::INFO
|
49
54
|
@logger.formatter = proc do |severity, datetime, progname, msg|
|
50
55
|
"#{severity}: #{msg}\n"
|
51
56
|
end
|
57
|
+
@logger.level = Logger::INFO # suppresses DEBUG-level messages
|
58
|
+
|
59
|
+
|
60
|
+
FileUtils::mkdir_p("#{@build_dir}") unless File.exists?("#{@build_dir}")
|
61
|
+
FileUtils::mkdir_p("#{@build_dir}/pre") unless File.exists?("#{@build_dir}/pre")
|
62
|
+
|
52
63
|
|
53
64
|
# ===
|
54
65
|
# Executive procs
|
@@ -77,6 +88,7 @@ def iterate_build cfg
|
|
77
88
|
for step in cfg.steps # iterate through each node in the 'config' object, which should start with an 'action' parameter
|
78
89
|
stepcount = stepcount + 1
|
79
90
|
step = BuildConfigStep.new(step) # create an instance of the Action class, validating the top-level step hash (now called 'step') in the process
|
91
|
+
@explainer.info step.message
|
80
92
|
type = step.type
|
81
93
|
case type # a switch to evaluate the 'action' parameter for each step in the iteration...
|
82
94
|
when "parse"
|
@@ -85,7 +97,8 @@ def iterate_build cfg
|
|
85
97
|
for bld in builds
|
86
98
|
build = Build.new(bld, type) # create an instance of the Build class; Build.new accepts a 'bld' hash & action 'type'
|
87
99
|
if build.template
|
88
|
-
|
100
|
+
@explainer.info build.message
|
101
|
+
liquify(data, build.template, build.output, build.variables) # perform the liquify operation
|
89
102
|
else
|
90
103
|
regurgidata(data, build.output)
|
91
104
|
end
|
@@ -96,12 +109,14 @@ def iterate_build cfg
|
|
96
109
|
copy_assets(step.source, step.target, inclusive)
|
97
110
|
when "render"
|
98
111
|
validate_file_input(step.source, "source") if step.source
|
99
|
-
doc = AsciiDocument.new(step.source)
|
100
|
-
attrs = ingest_attributes(step.data) if step.data # Set attributes in from YAML files
|
101
|
-
doc.add_attrs!(attrs) # Set attributes from the action-level data file
|
102
112
|
builds = step.builds
|
103
113
|
for bld in builds
|
114
|
+
doc = AsciiDocument.new(step.source)
|
115
|
+
attrs = ingest_attributes(step.data) if step.data # Set attributes from from YAML files
|
116
|
+
doc.add_attrs!(attrs) # Set attributes from the action-level data file
|
104
117
|
build = Build.new(bld, type) # create an instance of the Build class; Build.new accepts a 'bld' hash & action 'type' string
|
118
|
+
build.set("backend", derive_backend(doc.type, build.output) ) unless build.backend
|
119
|
+
@explainer.info build.message
|
105
120
|
render_doc(doc, build) # perform the render operation
|
106
121
|
end
|
107
122
|
when "deploy"
|
@@ -145,6 +160,27 @@ def validate_config_structure config
|
|
145
160
|
# TODO More validation needed
|
146
161
|
end
|
147
162
|
|
163
|
+
def explainer_init out=nil
|
164
|
+
unless @explainer
|
165
|
+
if out == "STDOUT"
|
166
|
+
@explainer = Logger.new(STDOUT)
|
167
|
+
else
|
168
|
+
out = "#{@build_dir}/pre/config-explainer.adoc" if out.nil?
|
169
|
+
File.open(out, 'w') unless File.exists?(out)
|
170
|
+
file = File.open(out, File::WRONLY)
|
171
|
+
begin
|
172
|
+
@explainer = Logger.new(file)
|
173
|
+
rescue Exception => ex
|
174
|
+
@logger.error ex
|
175
|
+
raise "ExplainerCreateError"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
@explainer.formatter = proc do |severity, datetime, progname, msg|
|
179
|
+
"#{msg}\n"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
148
184
|
# ===
|
149
185
|
# Core classes
|
150
186
|
# ===
|
@@ -211,10 +247,59 @@ class BuildConfigStep
|
|
211
247
|
return @step['options']
|
212
248
|
end
|
213
249
|
|
250
|
+
def stage
|
251
|
+
return @step['stage']
|
252
|
+
end
|
253
|
+
|
214
254
|
def builds
|
215
255
|
return @step['builds']
|
216
256
|
end
|
217
257
|
|
258
|
+
def message
|
259
|
+
# dynamically build a human-friendly log message, possibly appending a reason
|
260
|
+
unless @step['message']
|
261
|
+
reason = ", #{@step['reason']}" if @step['reason']
|
262
|
+
noninclusively = ", without carrying the parent directory" if self.options.is_a?(Hash) && self.options['inclusive'] == false && File.directory?(self.source)
|
263
|
+
stage = "" ; stage = "[#{self.stage}] " if self.stage
|
264
|
+
case self.type
|
265
|
+
when "migrate"
|
266
|
+
text = ". #{stage}Copies `#{self.source}` to `#{self.target}`#{noninclusively}#{reason}."
|
267
|
+
when "parse"
|
268
|
+
if self.data.is_a? Array
|
269
|
+
if self.data.count > 1
|
270
|
+
text = ". Draws data from the following files:"
|
271
|
+
self.data.each do |file|
|
272
|
+
text.concat("\n * `#{file}`.")
|
273
|
+
end
|
274
|
+
text.concat("\n")
|
275
|
+
else
|
276
|
+
text = ". #{stage}Draws data from `#{self.data[0]}`"
|
277
|
+
end
|
278
|
+
else
|
279
|
+
text = ". #{stage}Draws data from `#{self.data['file']}`"
|
280
|
+
end
|
281
|
+
text.concat("#{reason},") if reason
|
282
|
+
text.concat(" and parses it as follows:")
|
283
|
+
return text
|
284
|
+
when "render"
|
285
|
+
if self.source
|
286
|
+
text = ". #{stage}Using the index file `#{self.source}` as a map#{reason}, and ingesting AsciiDoc attributes from "
|
287
|
+
if self.data.is_a? Array
|
288
|
+
text.concat("the following data files:")
|
289
|
+
self.data.each do |file|
|
290
|
+
text.concat("\n * `#{file}`.")
|
291
|
+
end
|
292
|
+
else
|
293
|
+
text.concat("`#{self.data}`")
|
294
|
+
end
|
295
|
+
return text
|
296
|
+
end
|
297
|
+
end
|
298
|
+
else
|
299
|
+
return @step['message']
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
218
303
|
def validate
|
219
304
|
case self.type
|
220
305
|
when "parse"
|
@@ -267,6 +352,58 @@ class Build
|
|
267
352
|
@build['props']
|
268
353
|
end
|
269
354
|
|
355
|
+
def variables
|
356
|
+
@build['variables']
|
357
|
+
end
|
358
|
+
|
359
|
+
def message
|
360
|
+
# dynamically build a message, possibly appending a reason
|
361
|
+
unless @build['message']
|
362
|
+
reason = ", #{@build['reason']}" if @build['reason']
|
363
|
+
case @type
|
364
|
+
when "parse"
|
365
|
+
text = ".. Builds `#{self.output}` pressed with the template `#{self.template}`#{reason}."
|
366
|
+
when "render"
|
367
|
+
case self.backend
|
368
|
+
when "pdf"
|
369
|
+
text = ".. Uses Asciidoctor/Prawn to generate a PDF file `#{self.output}`"
|
370
|
+
text.concat("#{reason}") if reason
|
371
|
+
text.concat(".")
|
372
|
+
when "html5"
|
373
|
+
text = ".. Compiles a standard Asciidoctor HTML5 file, `#{self.output}`"
|
374
|
+
text.concat("#{reason}") if reason
|
375
|
+
text.concat(".")
|
376
|
+
when "jekyll"
|
377
|
+
text = ".. Uses Jekyll config files:\n+\n--"
|
378
|
+
files = self.props['files']
|
379
|
+
if files.is_a? String
|
380
|
+
if files.include? ","
|
381
|
+
files = files.split(",")
|
382
|
+
else
|
383
|
+
files = files.split
|
384
|
+
end
|
385
|
+
else
|
386
|
+
unless files.is_a? Array
|
387
|
+
@logger.error "The Jekyll configuration file must be a single filename, a comma-separated list of filenames, or an array of filenames."
|
388
|
+
end
|
389
|
+
end
|
390
|
+
files.each do |file|
|
391
|
+
text.concat("\n * `#{file}`")
|
392
|
+
end
|
393
|
+
text.concat("\n\nto generate a static site")
|
394
|
+
if self.props && self.props['arguments']
|
395
|
+
text.concat(" at `#{self.props['arguments']['destination']}`")
|
396
|
+
end
|
397
|
+
text.concat("#{reason}") if reason
|
398
|
+
text.concat(".\n--\n")
|
399
|
+
end
|
400
|
+
return text
|
401
|
+
end
|
402
|
+
else
|
403
|
+
@build['message']
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
270
407
|
def prop_files_array
|
271
408
|
if props
|
272
409
|
if props['files']
|
@@ -333,7 +470,7 @@ class Build
|
|
333
470
|
end
|
334
471
|
end
|
335
472
|
|
336
|
-
end #class Build
|
473
|
+
end # class Build
|
337
474
|
|
338
475
|
class DataSrc
|
339
476
|
# initialization means establishing a proper hash for the 'data' param
|
@@ -355,8 +492,12 @@ class DataSrc
|
|
355
492
|
else
|
356
493
|
if datasrc.is_a? String
|
357
494
|
@datasrc['ext'] = File.extname(datasrc)
|
358
|
-
else
|
359
|
-
|
495
|
+
else
|
496
|
+
if datasrc.is_a? Array
|
497
|
+
|
498
|
+
else
|
499
|
+
raise "InvalidDataSource"
|
500
|
+
end
|
360
501
|
end
|
361
502
|
end
|
362
503
|
end
|
@@ -522,9 +663,13 @@ def parse_regex data_file, pattern
|
|
522
663
|
end
|
523
664
|
|
524
665
|
# Parse given data using given template, generating given output
|
525
|
-
def liquify datasrc, template_file, output
|
666
|
+
def liquify datasrc, template_file, output, variables=nil
|
526
667
|
data = get_data(datasrc)
|
527
668
|
validate_file_input(template_file, "template")
|
669
|
+
if variables
|
670
|
+
vars = { "vars" => variables }
|
671
|
+
data.merge!vars
|
672
|
+
end
|
528
673
|
begin
|
529
674
|
template = File.read(template_file) # reads the template file
|
530
675
|
template = Liquid::Template.parse(template) # compiles template
|
@@ -546,7 +691,7 @@ def liquify datasrc, template_file, output
|
|
546
691
|
raise "FileNotBuilt"
|
547
692
|
end
|
548
693
|
if File.exists?(output_file)
|
549
|
-
@logger.info "File built: #{
|
694
|
+
@logger.info "File built: #{output_file}"
|
550
695
|
else
|
551
696
|
@logger.error "Hrmp! File not built."
|
552
697
|
raise "FileNotBuilt"
|
@@ -638,7 +783,11 @@ def ingest_attributes attr_file
|
|
638
783
|
raise "AttributeBlockError"
|
639
784
|
end
|
640
785
|
begin
|
641
|
-
|
786
|
+
if new_attrs.is_a? Hash
|
787
|
+
attrs.merge!new_attrs
|
788
|
+
else
|
789
|
+
@logger.warn "The AsciiDoc attributes file #{filename} is not formatted as a hash, so its data was not ingested."
|
790
|
+
end
|
642
791
|
rescue Exception => ex
|
643
792
|
raise "AttributesMergeError #{ex.message}"
|
644
793
|
end
|
@@ -657,7 +806,6 @@ def derive_backend type, out_file
|
|
657
806
|
end
|
658
807
|
|
659
808
|
def render_doc doc, build
|
660
|
-
build.set("backend", derive_backend(doc.type, build.output) ) unless build.backend
|
661
809
|
case build.backend
|
662
810
|
when "html5", "pdf"
|
663
811
|
asciidocify(doc, build)
|
@@ -734,15 +882,15 @@ def generate_site doc, build
|
|
734
882
|
attrs.merge!(build.attributes) if build.attributes
|
735
883
|
attrs = {"asciidoctor" => {"attributes" => attrs} }
|
736
884
|
attrs_yaml = attrs.to_yaml # Convert it all back to Yaml, as we're going to write a file to feed back to Jekyll
|
737
|
-
|
738
|
-
|
739
|
-
build.add_config_file("build/pre/_attributes.yml")
|
885
|
+
File.open("#{@build_dir}/pre/_attributes.yml", 'w') { |file| file.write(attrs_yaml) }
|
886
|
+
build.add_config_file("#{@build_dir}/pre/_attributes.yml")
|
740
887
|
config_list = build.prop_files_array.join(',') # flatten the Array back down for the CLI
|
741
888
|
opts_args = ""
|
889
|
+
quiet = "--quiet" if @quiet || @explicit
|
742
890
|
if build.props['arguments']
|
743
891
|
opts_args = build.props['arguments'].to_opts_args
|
744
892
|
end
|
745
|
-
command = "bundle exec jekyll build --config #{config_list} #{opts_args}"
|
893
|
+
command = "bundle exec jekyll build --config #{config_list} #{opts_args} #{quiet}"
|
746
894
|
end
|
747
895
|
@logger.info "Running #{command}"
|
748
896
|
@logger.debug "AsciiDoc attributes: #{doc.attributes.to_yaml} "
|
@@ -839,6 +987,8 @@ end
|
|
839
987
|
|
840
988
|
# Extending Liquid filters/text manipulation
|
841
989
|
module CustomFilters
|
990
|
+
include Jekyll::Filters
|
991
|
+
|
842
992
|
def plainwrap input
|
843
993
|
input.wrap
|
844
994
|
end
|
@@ -875,7 +1025,7 @@ Liquid::Template.register_filter(CustomFilters)
|
|
875
1025
|
command_parser = OptionParser.new do|opts|
|
876
1026
|
opts.banner = "Usage: liquidoc [options]"
|
877
1027
|
|
878
|
-
opts.on("-a KEY=VALUE", "For passing an AsciiDoc attribute parameter to Asciidoctor. Ex: -a
|
1028
|
+
opts.on("-a KEY=VALUE", "For passing an AsciiDoc attribute parameter to Asciidoctor. Ex: -a imagesdir=some/path -a custom_var='my value'") do |n|
|
879
1029
|
pair = {}
|
880
1030
|
k,v = n.split('=')
|
881
1031
|
pair[k] = v
|
@@ -884,7 +1034,11 @@ command_parser = OptionParser.new do|opts|
|
|
884
1034
|
|
885
1035
|
# Global Options
|
886
1036
|
opts.on("-b PATH", "--base=PATH", "The base directory, relative to this script. Defaults to `.`, or pwd." ) do |n|
|
887
|
-
@
|
1037
|
+
@base_dir = n
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
opts.on("-B PATH", "--build=PATH", "The directory under which LiquiDoc should save automatically preprocessed files. Defaults to #{@base_dir}_build. Can be absolute or relative to the base path (-b/--base=). Do NOT append '/' to the build path." ) do |n|
|
1041
|
+
@build_dir = n
|
888
1042
|
end
|
889
1043
|
|
890
1044
|
opts.on("-c", "--config=PATH", "Configuration file, enables preset source, template, and output.") do |n|
|
@@ -911,17 +1065,25 @@ command_parser = OptionParser.new do|opts|
|
|
911
1065
|
@template_file = @base_dir + n
|
912
1066
|
end
|
913
1067
|
|
914
|
-
opts.on("--verbose", "Run verbose") do |n|
|
1068
|
+
opts.on("--verbose", "Run verbose debug logging.") do |n|
|
915
1069
|
@logger.level = Logger::DEBUG
|
916
1070
|
@verbose = true
|
917
1071
|
end
|
918
1072
|
|
919
|
-
opts.on("--
|
920
|
-
@
|
1073
|
+
opts.on("--quiet", "Run with only WARN- and error-level logs written to console.") do |n|
|
1074
|
+
@logger.level = Logger::WARN
|
1075
|
+
@quiet = true
|
1076
|
+
end
|
1077
|
+
|
1078
|
+
opts.on("--explicit", "Log explicit step descriptions to console as build progresses. (Otherwise writes to file at #{@build_dir}/pre/config-explainer.adoc .)") do |n|
|
1079
|
+
explainer_init("STDOUT")
|
1080
|
+
@explainer.level = Logger::INFO
|
1081
|
+
@logger.level = Logger::WARN # Suppress all those INFO-level messages
|
1082
|
+
@explicit = true
|
921
1083
|
end
|
922
1084
|
|
923
|
-
opts.on("--
|
924
|
-
@
|
1085
|
+
opts.on("--stdout", "Puts the output in STDOUT instead of writing to a file.") do
|
1086
|
+
@output_type = "stdout"
|
925
1087
|
end
|
926
1088
|
|
927
1089
|
opts.on("--deploy", "EXPERIMENTAL: Trigger a jekyll serve operation against the destination dir of a Jekyll render step.") do
|
@@ -938,21 +1100,21 @@ end
|
|
938
1100
|
command_parser.parse!
|
939
1101
|
|
940
1102
|
# Upfront debug output
|
941
|
-
@logger.debug "Base dir: #{@base_dir}"
|
942
|
-
|
1103
|
+
@logger.debug "Base dir: #{@base_dir} (The path from which LiquiDoc CLI commands are relative.)"
|
1104
|
+
|
1105
|
+
explainer_init
|
943
1106
|
|
944
1107
|
# ===
|
945
1108
|
# Execute
|
946
1109
|
# ===
|
947
|
-
|
948
|
-
FileUtils.remove_dir(@clean_dir)
|
949
|
-
end
|
1110
|
+
|
950
1111
|
unless @config_file
|
1112
|
+
@logger.debug "Executing config-free build based on API/CLI arguments alone."
|
951
1113
|
if @data_file
|
952
1114
|
liquify(@data_file, @template_file, @output_file)
|
953
1115
|
end
|
954
1116
|
if @index_file
|
955
|
-
@logger.warn "
|
1117
|
+
@logger.warn "Rendering via command line arguments is not yet implemented. Use a config file."
|
956
1118
|
end
|
957
1119
|
else
|
958
1120
|
@logger.debug "Executing... config_build"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: liquidoc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian Dominick
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -51,7 +51,6 @@ files:
|
|
51
51
|
- bin/liquidoc
|
52
52
|
- lib/liquidoc.rb
|
53
53
|
- lib/liquidoc/version.rb
|
54
|
-
- lib/yaml_plus.rb
|
55
54
|
homepage: https://github.com/scalingdata/liquidoc
|
56
55
|
licenses:
|
57
56
|
- MIT
|
data/lib/yaml_plus.rb
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
require 'safe_yaml/load'
|
2
|
-
module YamlPlus
|
3
|
-
|
4
|
-
def self.parse data
|
5
|
-
puts data
|
6
|
-
return
|
7
|
-
# self.new.load((::SafeYAML.load raw_data), theme_data)
|
8
|
-
# input_data ||= ::OpenStruct.new
|
9
|
-
# return input_data unless ::Hash === data
|
10
|
-
# data.inject(input_data) {|data, (key, val)| process_entry key, val, data }
|
11
|
-
# input_data.base_align ||= 'left'
|
12
|
-
# input_data
|
13
|
-
end
|
14
|
-
|
15
|
-
def process_entry key, val, data
|
16
|
-
if ::Hash === val
|
17
|
-
val.each do |key2, val2|
|
18
|
-
process_entry %(#{key}_#{key2.tr '-', '_'}), val2, data
|
19
|
-
end
|
20
|
-
else
|
21
|
-
data[key] = evaluate val, data
|
22
|
-
end
|
23
|
-
data
|
24
|
-
end
|
25
|
-
|
26
|
-
def evaluate expr, vars
|
27
|
-
case expr
|
28
|
-
when ::String
|
29
|
-
evaluate_math(expand_vars expr, vars)
|
30
|
-
when ::Array
|
31
|
-
expr.map {|e| evaluate e, vars }
|
32
|
-
else
|
33
|
-
expr
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def expand_vars expr, vars
|
38
|
-
if (idx = (expr.index '$'))
|
39
|
-
if idx == 0 && expr =~ LoneVariableRx
|
40
|
-
if vars.respond_to? $1
|
41
|
-
vars[$1]
|
42
|
-
else
|
43
|
-
warn %(asciidoctor: WARNING: unknown variable reference: $#{$1})
|
44
|
-
expr
|
45
|
-
end
|
46
|
-
else
|
47
|
-
expr.gsub(VariableRx) {
|
48
|
-
if vars.respond_to? $1
|
49
|
-
vars[$1]
|
50
|
-
else
|
51
|
-
warn %(asciidoctor: WARNING: unknown variable reference: $#{$1})
|
52
|
-
$&
|
53
|
-
end
|
54
|
-
}
|
55
|
-
end
|
56
|
-
else
|
57
|
-
expr
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|