ad_hoc_template 0.4.0 → 0.4.1
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/.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
|