ad_hoc_template 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +23 -0
- data/.travis.yml +7 -1
- data/Gemfile +3 -2
- data/ad_hoc_template.gemspec +4 -3
- data/lib/ad_hoc_template.rb +51 -69
- data/lib/ad_hoc_template/command_line_interface.rb +36 -37
- data/lib/ad_hoc_template/config_manager.rb +5 -10
- data/lib/ad_hoc_template/default_tag_formatter.rb +8 -8
- data/lib/ad_hoc_template/entry_format_generator.rb +24 -13
- data/lib/ad_hoc_template/parser.rb +144 -59
- data/lib/ad_hoc_template/pseudohiki_formatter.rb +4 -2
- data/lib/ad_hoc_template/recipe_manager.rb +19 -27
- data/lib/ad_hoc_template/record_reader.rb +59 -56
- data/lib/ad_hoc_template/shim.rb +26 -0
- data/lib/ad_hoc_template/utils.rb +11 -5
- data/lib/ad_hoc_template/version.rb +3 -1
- data/output.html +14 -0
- data/spec/command_line_interface_spec.rb +31 -0
- data/spec/config_manager_spec.rb +4 -4
- data/spec/entry_format_generator_spec.rb +1 -1
- data/spec/parser_spec.rb +15 -1
- data/spec/recipe_manager_spec.rb +14 -14
- metadata +26 -9
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'pseudohikiparser'
|
4
4
|
|
@@ -20,6 +20,8 @@ module AdHocTemplate
|
|
20
20
|
private_class_method :choose_parser
|
21
21
|
end
|
22
22
|
|
23
|
-
assign_format(
|
23
|
+
assign_format('ph') do |var, record|
|
24
|
+
PseudoHikiFormatter.to_xhtml(var, record)
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'ad_hoc_template'
|
4
4
|
require 'ad_hoc_template/utils'
|
@@ -10,12 +10,12 @@ module AdHocTemplate
|
|
10
10
|
attr_reader :output_file, :template_encoding, :template
|
11
11
|
attr_reader :records, :recipe
|
12
12
|
|
13
|
-
def self.update_output_files_in_recipe(
|
14
|
-
recipe_source = open(File.expand_path(
|
13
|
+
def self.update_output_files_in_recipe(recipe_file, force_update=false)
|
14
|
+
recipe_source = File.open(File.expand_path(recipe_file), &:read)
|
15
15
|
recipes = YAML.load_stream(recipe_source)
|
16
16
|
recipes.each do |recipe|
|
17
17
|
manager = new(recipe)
|
18
|
-
if manager.modified_after_last_output?
|
18
|
+
if manager.modified_after_last_output? || force_update
|
19
19
|
manager.update_output_file
|
20
20
|
end
|
21
21
|
end
|
@@ -60,10 +60,8 @@ module AdHocTemplate
|
|
60
60
|
|
61
61
|
def parse_template
|
62
62
|
template_path = File.expand_path(@recipe['template'])
|
63
|
-
template_source = open(template_path,
|
64
|
-
|
65
|
-
file.read
|
66
|
-
end
|
63
|
+
template_source = File.open(template_path,
|
64
|
+
open_mode(@template_encoding), &:read)
|
67
65
|
tag_type = @recipe['tag_type'] || :default
|
68
66
|
tag_type = tag_type.to_sym unless tag_type.kind_of? Symbol
|
69
67
|
@template = Parser.parse(template_source, tag_type)
|
@@ -75,7 +73,8 @@ module AdHocTemplate
|
|
75
73
|
content = AdHocTemplate::DataLoader.format(@template, @records)
|
76
74
|
mode = @template_encoding ? "wb:#{@template_encoding}" : 'wb'
|
77
75
|
if @output_file
|
78
|
-
open(File.expand_path(@output_file),
|
76
|
+
File.open(File.expand_path(@output_file),
|
77
|
+
mode) {|file| file.print content }
|
79
78
|
else
|
80
79
|
STDOUT.print content
|
81
80
|
end
|
@@ -117,15 +116,14 @@ module AdHocTemplate
|
|
117
116
|
end
|
118
117
|
|
119
118
|
def setup_main_label
|
120
|
-
if data_format = @default['data_format']
|
121
|
-
[:csv, :tsv].include? data_format
|
119
|
+
if (data_format = @default['data_format']) && csv_or_tsv?(data_format)
|
122
120
|
@default['label'] ||= RecordReader::CSVReader::HEADER_POSITION::LEFT
|
123
121
|
end
|
124
122
|
end
|
125
123
|
|
126
124
|
def determine_data_format!(block)
|
127
125
|
data_format = block['data_format']
|
128
|
-
if
|
126
|
+
if !data_format && block['data']
|
129
127
|
data_format = guess_file_format(block['data'])
|
130
128
|
end
|
131
129
|
|
@@ -133,31 +131,25 @@ module AdHocTemplate
|
|
133
131
|
end
|
134
132
|
|
135
133
|
def read_file(file_name, encoding)
|
136
|
-
open(File.expand_path(file_name),
|
137
|
-
|
138
|
-
file.read
|
139
|
-
end
|
134
|
+
File.open(File.expand_path(file_name),
|
135
|
+
open_mode(encoding), &:read)
|
140
136
|
end
|
141
137
|
|
142
138
|
def open_mode(encoding)
|
143
139
|
encoding ||= Encoding.default_external.names[0]
|
144
|
-
mode =
|
145
|
-
return mode unless encoding
|
140
|
+
mode = 'rb'
|
141
|
+
return mode unless encoding && !encoding.empty?
|
146
142
|
bom = /\AUTF/i =~ encoding ? 'BOM|' : ''
|
147
|
-
mode
|
143
|
+
"#{mode}:#{bom}#{encoding}"
|
148
144
|
end
|
149
145
|
|
150
146
|
def prepare_data_format(block)
|
151
147
|
data_format = block['data_format']
|
152
|
-
if
|
153
|
-
data_format = :default
|
154
|
-
end
|
148
|
+
data_format = :default if !data_format || data_format.empty?
|
155
149
|
data_format = data_format.to_sym
|
156
|
-
return data_format unless
|
157
|
-
|
158
|
-
|
159
|
-
data_format = { data_format => label }
|
160
|
-
end
|
150
|
+
return data_format unless csv_or_tsv? data_format
|
151
|
+
label = block['label']
|
152
|
+
return { data_format => label.sub(/\A#/, '') } if label
|
161
153
|
data_format
|
162
154
|
end
|
163
155
|
|
@@ -1,14 +1,17 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'yaml'
|
4
4
|
require 'json'
|
5
5
|
require 'csv'
|
6
6
|
|
7
|
+
require 'ad_hoc_template/shim'
|
8
|
+
|
7
9
|
module AdHocTemplate
|
8
10
|
module RecordReader
|
9
11
|
module YAMLReader
|
10
12
|
def self.read_record(yaml_data)
|
11
|
-
RecordReader.convert_values_to_string(YAML.
|
13
|
+
RecordReader.convert_values_to_string(YAML.safe_load(yaml_data,
|
14
|
+
[Symbol]))
|
12
15
|
end
|
13
16
|
|
14
17
|
def self.dump(config_data)
|
@@ -29,10 +32,12 @@ module AdHocTemplate
|
|
29
32
|
end
|
30
33
|
|
31
34
|
module CSVReader
|
35
|
+
using Shim unless ''.respond_to?(:+@)
|
36
|
+
|
32
37
|
COL_SEP = {
|
33
38
|
csv: CSV::DEFAULT_OPTIONS[:col_sep],
|
34
|
-
tsv: "\t"
|
35
|
-
}
|
39
|
+
tsv: "\t",
|
40
|
+
}.freeze
|
36
41
|
|
37
42
|
module HEADER_POSITION
|
38
43
|
TOP = '__header_top__'
|
@@ -42,10 +47,10 @@ module AdHocTemplate
|
|
42
47
|
class NotSupportedError < StandardError; end
|
43
48
|
|
44
49
|
def self.read_record(csv_data, config={ csv: nil })
|
45
|
-
label, sep
|
50
|
+
label, sep = parse_config(config)
|
46
51
|
header, *data = csv_to_array(csv_data, sep, label)
|
47
52
|
csv_records = data.map {|row| convert_to_hash(header, row) }
|
48
|
-
if label
|
53
|
+
if label && label.index('|')
|
49
54
|
return compose_inner_iteration_records(csv_records, label)
|
50
55
|
end
|
51
56
|
compose_record(csv_records, label)
|
@@ -55,20 +60,15 @@ module AdHocTemplate
|
|
55
60
|
data = RecordReader.parse_if_necessary(config_data)
|
56
61
|
raise NotSupportedError unless csv_compatible_format?(data)
|
57
62
|
|
58
|
-
|
59
|
-
|
60
|
-
else
|
61
|
-
records = data.to_a.transpose
|
62
|
-
end
|
63
|
+
kv_pairs = find_sub_records(data)
|
64
|
+
records = kv_pairs ? hashes_to_arrays(kv_pairs) : data.to_a.transpose
|
63
65
|
|
64
66
|
array_to_csv(records, col_sep)
|
65
67
|
end
|
66
68
|
|
67
69
|
def self.convert_to_hash(header, row_array)
|
68
70
|
{}.tap do |record|
|
69
|
-
header.zip(row_array).each
|
70
|
-
record[key] = value
|
71
|
-
end
|
71
|
+
header.zip(row_array).each {|key, value| record[key] = value }
|
72
72
|
end
|
73
73
|
# if RUBY_VERSION >= 2.1.0: header.zip(row_array).to_h
|
74
74
|
end
|
@@ -82,15 +82,13 @@ module AdHocTemplate
|
|
82
82
|
when Hash
|
83
83
|
format, label = config.to_a[0]
|
84
84
|
end
|
85
|
-
col_sep = COL_SEP[format
|
86
|
-
|
85
|
+
col_sep = COL_SEP[format || :csv]
|
86
|
+
[label, col_sep]
|
87
87
|
end
|
88
88
|
|
89
89
|
def self.csv_to_array(csv_data, col_sep, label)
|
90
90
|
array = CSV.new(csv_data, col_sep: col_sep).to_a
|
91
|
-
if
|
92
|
-
array = array.transpose
|
93
|
-
end
|
91
|
+
array = array.transpose if !label || label == HEADER_POSITION::LEFT
|
94
92
|
array
|
95
93
|
end
|
96
94
|
|
@@ -123,15 +121,15 @@ module AdHocTemplate
|
|
123
121
|
end
|
124
122
|
|
125
123
|
def self.inner_iteration_labels(outer_label, inner_label, keys)
|
126
|
-
|
127
|
-
|
128
|
-
h
|
124
|
+
keys.each_with_object({}) do |key, labels|
|
125
|
+
labels[key] = [outer_label, inner_label, key].join('|')
|
129
126
|
end
|
130
127
|
end
|
131
128
|
|
132
129
|
def self.csv_compatible_format?(data)
|
133
130
|
iteration_blocks_count = data.values.count {|v| v.kind_of? Array }
|
134
|
-
iteration_blocks_count
|
131
|
+
iteration_blocks_count.zero? ||
|
132
|
+
(iteration_blocks_count == 1 && data.size == 1)
|
135
133
|
end
|
136
134
|
|
137
135
|
def self.hashes_to_arrays(data)
|
@@ -147,10 +145,11 @@ module AdHocTemplate
|
|
147
145
|
def self.array_to_csv(records, col_sep)
|
148
146
|
# I do not adopt "records.map {|rec| rec.to_csv }.join",
|
149
147
|
# because I'm not sure if it is sufficient for certain data or not.
|
150
|
-
# For example, a field value may contain carriage returns or line
|
151
|
-
# and in that case, improper handling of the end of record
|
148
|
+
# For example, a field value may contain carriage returns or line
|
149
|
+
# feeds, and in that case, improper handling of the end of record
|
150
|
+
# would be damaging.
|
152
151
|
|
153
|
-
CSV.generate('', col_sep: col_sep) do |csv|
|
152
|
+
CSV.generate(+'', col_sep: col_sep) do |csv|
|
154
153
|
records.each {|record| csv << record }
|
155
154
|
end
|
156
155
|
end
|
@@ -179,17 +178,17 @@ module AdHocTemplate
|
|
179
178
|
|
180
179
|
module DefaultFormReader
|
181
180
|
SEPARATOR = /:\s*/o
|
182
|
-
BLOCK_HEAD =
|
183
|
-
ITERATION_HEAD =
|
181
|
+
BLOCK_HEAD = %r{\A///@}
|
182
|
+
ITERATION_HEAD = %r{\A///@#}
|
184
183
|
EMPTY_LINE = /\A#{LINE_END_STR}\Z/o
|
185
184
|
ITERATION_MARK = /\A#/o
|
186
|
-
COMMENT_HEAD =
|
185
|
+
COMMENT_HEAD = %r{\A////}
|
187
186
|
READERS_RE = {
|
188
187
|
key_value: SEPARATOR,
|
189
188
|
iteration: ITERATION_HEAD,
|
190
189
|
block: BLOCK_HEAD,
|
191
190
|
empty_line: EMPTY_LINE,
|
192
|
-
}
|
191
|
+
}.freeze
|
193
192
|
|
194
193
|
class ReaderState
|
195
194
|
attr_accessor :current_block_label
|
@@ -266,6 +265,8 @@ module AdHocTemplate
|
|
266
265
|
end
|
267
266
|
|
268
267
|
class Reader
|
268
|
+
using Shim unless //.respond_to? :match?
|
269
|
+
|
269
270
|
def self.setup_reader(stack)
|
270
271
|
readers = {}
|
271
272
|
{
|
@@ -273,9 +274,7 @@ module AdHocTemplate
|
|
273
274
|
key_value: KeyValueReader,
|
274
275
|
block: BlockReader,
|
275
276
|
iteration: IterationReader,
|
276
|
-
}.each
|
277
|
-
readers[k] = v.new(stack, readers)
|
278
|
-
end
|
277
|
+
}.each {|k, v| readers[k] = v.new(stack, readers) }
|
279
278
|
stack.push readers[:base]
|
280
279
|
readers
|
281
280
|
end
|
@@ -289,28 +288,28 @@ module AdHocTemplate
|
|
289
288
|
@stack.pop
|
290
289
|
end
|
291
290
|
|
292
|
-
def read(line)
|
293
|
-
end
|
291
|
+
def read(line); end
|
294
292
|
|
295
293
|
private
|
296
294
|
|
297
295
|
def push_reader_if_match(line, readers)
|
298
296
|
readers.each do |reader|
|
299
|
-
|
297
|
+
if READERS_RE[reader].match?(line)
|
298
|
+
return @stack.push(@readers[reader])
|
299
|
+
end
|
300
300
|
end
|
301
301
|
end
|
302
302
|
|
303
303
|
def setup_new_block(line, initial_value)
|
304
|
-
label = line.sub(BLOCK_HEAD,
|
304
|
+
label = line.sub(BLOCK_HEAD, '').chomp
|
305
305
|
@stack.current_record[label] ||= initial_value
|
306
306
|
@stack.current_block_label = label
|
307
307
|
end
|
308
308
|
end
|
309
309
|
|
310
|
-
|
311
310
|
class BaseReader < Reader
|
312
311
|
def setup_stack(line)
|
313
|
-
push_reader_if_match(line, [
|
312
|
+
push_reader_if_match(line, %i[iteration block key_value])
|
314
313
|
end
|
315
314
|
end
|
316
315
|
|
@@ -320,7 +319,7 @@ module AdHocTemplate
|
|
320
319
|
when EMPTY_LINE, ITERATION_HEAD, BLOCK_HEAD
|
321
320
|
pop_stack
|
322
321
|
end
|
323
|
-
push_reader_if_match(line, [
|
322
|
+
push_reader_if_match(line, %i[iteration block])
|
324
323
|
end
|
325
324
|
|
326
325
|
def read(line)
|
@@ -331,20 +330,22 @@ module AdHocTemplate
|
|
331
330
|
end
|
332
331
|
|
333
332
|
class BlockReader < Reader
|
333
|
+
using Shim unless ''.respond_to?(:+@)
|
334
|
+
|
334
335
|
def setup_stack(line)
|
335
336
|
case line
|
336
337
|
when ITERATION_HEAD, BLOCK_HEAD
|
337
338
|
@stack.remove_trailing_empty_lines_from_last_block!
|
338
339
|
pop_stack
|
339
340
|
end
|
340
|
-
push_reader_if_match(line, [
|
341
|
+
push_reader_if_match(line, %i[iteration block])
|
341
342
|
end
|
342
343
|
|
343
344
|
def read(line)
|
344
345
|
block_value = @stack.last_block_value
|
345
346
|
case line
|
346
347
|
when BLOCK_HEAD
|
347
|
-
setup_new_block(line,
|
348
|
+
setup_new_block(line, +'')
|
348
349
|
when EMPTY_LINE, COMMENT_HEAD
|
349
350
|
block_value << line unless block_value.empty?
|
350
351
|
else
|
@@ -389,7 +390,12 @@ module AdHocTemplate
|
|
389
390
|
iteration_part = format_iteration_block(iteration_keys, labels)
|
390
391
|
block_part = format_key_value_block(block_keys, labels)
|
391
392
|
|
392
|
-
[key_value_part, iteration_part, block_part].join($/)
|
393
|
+
all_parts = [key_value_part, iteration_part, block_part].join($/)
|
394
|
+
remove_redundant_newlines(all_parts)
|
395
|
+
end
|
396
|
+
|
397
|
+
def self.remove_redundant_newlines(str)
|
398
|
+
str.sub(/(#{$/}+)\Z/, $/)
|
393
399
|
end
|
394
400
|
|
395
401
|
def self.format_key_value_pairs(key_names, labels={})
|
@@ -399,33 +405,32 @@ module AdHocTemplate
|
|
399
405
|
def self.format_key_value_block(key_names, labels)
|
400
406
|
[].tap do |blocks|
|
401
407
|
key_names.each do |key|
|
402
|
-
blocks.push "///@#{key}#{
|
408
|
+
blocks.push "///@#{key}#{$/ * 2}#{labels[key]}"
|
403
409
|
end
|
404
410
|
end.join($/)
|
405
411
|
end
|
406
412
|
|
407
413
|
def self.format_iteration_block(key_names, labels)
|
408
414
|
key_names.map do |iteration_label|
|
409
|
-
iteration_block = [
|
410
|
-
|
411
|
-
iteration_block.push format_key_value_pairs(sub_record.keys, sub_record)
|
415
|
+
iteration_block = labels[iteration_label].map do |sub_record|
|
416
|
+
format_key_value_pairs(sub_record.keys, sub_record)
|
412
417
|
end
|
413
|
-
iteration_block.
|
418
|
+
iteration_block.unshift("///@#{iteration_label}#{$/}")
|
414
419
|
end.join($/)
|
415
420
|
end
|
416
421
|
|
417
422
|
def self.categorize_keys(labels)
|
418
|
-
iteration_part, rest = labels.partition
|
419
|
-
|
420
|
-
end.map {|e| e.map(&:first) }
|
423
|
+
iteration_part, rest = labels.partition {|e| e[1].kind_of? Array }
|
424
|
+
.map {|e| e.map(&:first) }
|
421
425
|
|
422
426
|
block_part, key_value_part = rest.partition do |e|
|
423
427
|
LINE_END_RE =~ labels[e]
|
424
428
|
end
|
425
429
|
|
426
|
-
|
430
|
+
[iteration_part, key_value_part, block_part]
|
427
431
|
end
|
428
432
|
|
433
|
+
private_class_method :remove_redundant_newlines
|
429
434
|
private_class_method :format_key_value_pairs
|
430
435
|
private_class_method :format_key_value_block
|
431
436
|
private_class_method :format_iteration_block
|
@@ -438,9 +443,7 @@ module AdHocTemplate
|
|
438
443
|
csv: CSVReader,
|
439
444
|
tsv: TSVReader,
|
440
445
|
default: DefaultFormReader,
|
441
|
-
}
|
442
|
-
|
443
|
-
FORMAT_NAME_TO_READER.default = DefaultFormReader
|
446
|
+
}.tap {|h| h.default = DefaultFormReader }.freeze
|
444
447
|
|
445
448
|
def self.dump(data_source, target_format=:default)
|
446
449
|
FORMAT_NAME_TO_READER[target_format].dump(data_source)
|
@@ -463,7 +466,7 @@ module AdHocTemplate
|
|
463
466
|
data.each do |k, v|
|
464
467
|
if v.kind_of? Array
|
465
468
|
v.each {|sub_rec| convert_values_to_string(sub_rec) }
|
466
|
-
elsif v
|
469
|
+
elsif v && !v.kind_of?(String)
|
467
470
|
data[k] = v.to_s
|
468
471
|
end
|
469
472
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdHocTemplate
|
4
|
+
##
|
5
|
+
# Provide methods that are not availabe in older versions of Ruby.
|
6
|
+
|
7
|
+
module Shim
|
8
|
+
refine Regexp do
|
9
|
+
##
|
10
|
+
# Regexp.match?() is available for Ruby >= 2.4,
|
11
|
+
# and the following implementation does not satisfy
|
12
|
+
# the full specification of the original method.
|
13
|
+
|
14
|
+
alias_method(:match?, :===)
|
15
|
+
end
|
16
|
+
|
17
|
+
refine String do
|
18
|
+
##
|
19
|
+
# Regexp.match?() is available for Ruby >= 2.3,
|
20
|
+
# and the following implementation does not satisfy
|
21
|
+
# the full specification of the original method.
|
22
|
+
|
23
|
+
alias_method(:+@, :dup)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,16 +1,16 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module AdHocTemplate
|
4
4
|
module Utils
|
5
|
-
|
5
|
+
FILE_EXTENTIONS = {
|
6
6
|
/\.ya?ml\Z/i => :yaml,
|
7
7
|
/\.json\Z/i => :json,
|
8
8
|
/\.csv\Z/i => :csv,
|
9
9
|
/\.tsv\Z/i => :tsv,
|
10
|
-
}
|
10
|
+
}.freeze
|
11
11
|
|
12
12
|
def guess_file_format(filename)
|
13
|
-
if_any_regex_match(FILE_EXTENTIONS, filename) do |
|
13
|
+
if_any_regex_match(FILE_EXTENTIONS, filename) do |_, format|
|
14
14
|
return format
|
15
15
|
end
|
16
16
|
end
|
@@ -19,11 +19,17 @@ module AdHocTemplate
|
|
19
19
|
regex_table.each do |re, paired_value|
|
20
20
|
if re =~ target
|
21
21
|
yield re, paired_value
|
22
|
-
return
|
22
|
+
return nil
|
23
23
|
end
|
24
24
|
end
|
25
25
|
STDERR.puts failure_message if failure_message
|
26
26
|
nil
|
27
27
|
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def csv_or_tsv?(format)
|
32
|
+
%i[csv tsv].include? format
|
33
|
+
end
|
28
34
|
end
|
29
35
|
end
|