ad_hoc_template 0.0.1 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ac72ef7ef92a4a6aa9be31762b2cc29177cecd44
4
- data.tar.gz: 6fd32a26232fbb869f73632610c301e470e3178b
3
+ metadata.gz: 476f29030bdd0bf5050ef7e961affd45a4a64667
4
+ data.tar.gz: 04901a83516ee55be4a9f887d4c050f12cabbd4b
5
5
  SHA512:
6
- metadata.gz: e6935f638dd7c1cb8979c86bbfbd653ec75a7a4aca0d7579391026b47e60840b55a4444696cef0f2203bf6a5460b95822324b7322754cd9a215fb92e357a76a7
7
- data.tar.gz: 42d8b17110f59b24fe529029da629fa4150ce4c531b15721d47e7783da12edee4acf56086a6f3ec86b03d84197bdcf71967a9c989d4a0a784972682e6c27fe13
6
+ metadata.gz: 04719a11683310dc6ff684782ea384f1c2a55056ec99d1ab845660f763c7a90ca000e7fa75670dc9537bfbfd047563e41ad24a9ba709de0b438d8562da83288d
7
+ data.tar.gz: bd458ab6de4d391f4a4ea7fafce0412841f46f449c4b88e8a37b08c53c08ed38b48ea9fb099bdb14a1f12323d7595843c04c222a977480246d83973361681bf5
data/.gitignore CHANGED
@@ -15,3 +15,5 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ *~
19
+ .ruby-version
data/Gemfile CHANGED
@@ -2,7 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in ad_hoc_template.gemspec
4
4
 
5
- gem 'pseudohikiparser', :git => 'https://github.com/nico-hn/PseudoHikiParser.git', :tag => '0.0.2'
5
+ gem 'pseudohikiparser', '0.0.5.develop'
6
6
 
7
7
  group :development do
8
8
  gem "bundler", "~> 1.3"
data/README.md CHANGED
@@ -63,7 +63,7 @@ the second paragraph in block
63
63
  2. Execute the following at the command line:
64
64
 
65
65
  ```
66
- ad_hoc_template template.txt sample_data.txt
66
+ $ ad_hoc_template template.txt sample_data.txt
67
67
  ```
68
68
 
69
69
  Then you will get the following result:
@@ -77,7 +77,6 @@ the value of sub_key2 is value1-2
77
77
  the value of sub_key1 is value2-1
78
78
  the value of sub_key2 is value2-2
79
79
 
80
-
81
80
  the first line of block
82
81
  the second line of block
83
82
 
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
- spec.add_runtime_dependency "pseudohikiparser", "0.0.2"
20
+ spec.add_runtime_dependency "pseudohikiparser", "0.0.5.develop"
21
21
 
22
22
  spec.add_development_dependency "bundler", "~> 1.3"
23
23
  spec.add_development_dependency "rake", "~> 10.1"
@@ -5,11 +5,36 @@ require 'optparse'
5
5
 
6
6
  module AdHocTemplate
7
7
  class CommandLineInterface
8
- attr_accessor :output_filename, :template_data, :record_data
8
+ attr_accessor :output_filename, :template_data, :record_data, :tag_type, :data_format
9
+
10
+ TAG_RE_TO_TYPE = {
11
+ /\Ad(efault)?/i => :default,
12
+ /\Ac(urly_brackets)?/i => :curly_brackets,
13
+ /\As(quare_brackets)?/i => :square_brackets,
14
+ /\Axml_like1/i => :xml_like1,
15
+ /\Axml_like2/i => :xml_like2,
16
+ }
17
+
18
+ FORMAT_RE_TO_FORMAT = {
19
+ /\Ad(efault)?/i => :default,
20
+ /\Ay(a?ml)?/i => :yaml,
21
+ /\Aj(son)?/i => :json,
22
+ /\Ac(sv)?/i => :csv,
23
+ /\At(sv)?/i => :tsv,
24
+ }
25
+
26
+ FILE_EXTENTIONS = {
27
+ /\.ya?ml\Z/i => :yaml,
28
+ /\.json\Z/i => :json,
29
+ /\.csv\Z/i => :csv,
30
+ /\.tsv\Z/i => :tsv,
31
+ }
9
32
 
10
33
  def initialize
11
- @formatter = AdHocTemplate::DefaultTagFormatter.new
34
+ @tag_formatter = AdHocTemplate::DefaultTagFormatter.new
12
35
  @output_filename = nil
36
+ @tag_type = :default
37
+ @data_format = nil
13
38
  end
14
39
 
15
40
  def set_encoding(given_opt)
@@ -19,7 +44,7 @@ module AdHocTemplate
19
44
  end
20
45
 
21
46
  def parse_command_line_options
22
- OptionParser.new do |opt|
47
+ OptionParser.new("USAGE: #{File.basename($0)} [OPTION]... TEMPLATE_FILE DATA_FILE") do |opt|
23
48
  opt.on("-E [ex[:in]]", "--encoding [=ex[:in]]",
24
49
  "Specify the default external and internal character encodings (same as the option of MRI") do |given_opt|
25
50
  self.set_encoding(given_opt)
@@ -30,7 +55,27 @@ module AdHocTemplate
30
55
  @output_filename = File.expand_path(output_file)
31
56
  end
32
57
 
33
- opt.parse!
58
+ opt.on("-t [tag_type]", "--tag-type [=tag_type]",
59
+ "Choose a template tag type: default, curly_brackets or square_brackets") do |given_type|
60
+ choose_tag_type(given_type)
61
+ end
62
+
63
+ opt.on("-d [data_format]", "--data-format [=data_format]",
64
+ "Specify the format of input data: default, yaml, json, csv or tsv") do |data_format|
65
+ choose_data_format(data_format)
66
+ end
67
+
68
+ opt.on("-u [tag_config.yaml]","--user-defined-tag [=tag_config.yaml]",
69
+ "Configure a user-defined tag. The configuration file is in YAML format.") do |tag_config_yaml|
70
+ register_user_defined_tag_type(tag_config_yaml)
71
+ end
72
+
73
+ opt.parse!
74
+ end
75
+
76
+ unless @data_format
77
+ guessed_format = ARGV.length < 2 ? :default : guess_file_format(ARGV[1])
78
+ @data_format = guessed_format || :default
34
79
  end
35
80
  end
36
81
 
@@ -46,7 +91,8 @@ module AdHocTemplate
46
91
  end
47
92
 
48
93
  def convert
49
- AdHocTemplate::Converter.convert(@record_data, @template_data, @formatter)
94
+ AdHocTemplate.convert(@record_data, @template_data, @tag_type,
95
+ @data_format, @tag_formatter)
50
96
  end
51
97
 
52
98
  def open_output
@@ -62,9 +108,50 @@ module AdHocTemplate
62
108
  def execute
63
109
  parse_command_line_options
64
110
  read_input_files
65
- open_output do |out|
66
- out.print convert
111
+ open_output {|out| out.print convert }
112
+ end
113
+
114
+ private
115
+
116
+ def choose_tag_type(given_type)
117
+ if_any_regex_match(TAG_RE_TO_TYPE, given_type,
118
+ "The given type is not found. The default tag is chosen.") do |re, tag_type|
119
+ @tag_type = tag_type
120
+ end
121
+ end
122
+
123
+ def choose_data_format(data_format)
124
+ if_any_regex_match(FORMAT_RE_TO_FORMAT, data_format,
125
+ "The given format is not found. The default format is chosen.") do |re, format|
126
+ @data_format = [:csv, :tsv].include?(format) ? make_csv_option(data_format, format) : format
127
+ end
128
+ end
129
+
130
+ def register_user_defined_tag_type(tag_config_yaml)
131
+ config = File.read(File.expand_path(tag_config_yaml))
132
+ @tag_type = Parser.register_user_defined_tag_type(config)
133
+ end
134
+
135
+ def make_csv_option(data_format, format)
136
+ iteration_label = data_format.sub(/\A(csv|tsv):?/, "")
137
+ iteration_label.empty? ? format : { format => iteration_label }
138
+ end
139
+
140
+ def guess_file_format(filename)
141
+ if_any_regex_match(FILE_EXTENTIONS, filename) do |ext_re, format|
142
+ return format
143
+ end
144
+ end
145
+
146
+ def if_any_regex_match(regex_table, target, failure_message=nil)
147
+ regex_table.each do |re, paired_value|
148
+ if re =~ target
149
+ yield re, paired_value
150
+ return
151
+ end
67
152
  end
153
+ STDERR.puts failure_message if failure_message
154
+ nil
68
155
  end
69
156
  end
70
157
  end
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pseudohiki/inlineparser"
4
+ require "htmlelement"
5
+
6
+ module AdHocTemplate
7
+ class Parser < TreeStack
8
+ class TagNode < Parser::Node
9
+ attr_reader :type
10
+
11
+ def push(node=TreeStack::Node.new)
12
+ node[0] = assign_type(node[0]) if self.empty?
13
+ super
14
+ end
15
+
16
+ def assign_type(first_leaf)
17
+ if not first_leaf.kind_of? String or /\A\s/ =~ first_leaf
18
+ return first_leaf.sub(/\A(?:\r?\n|\r)/, "")
19
+ end
20
+ @type, first_leaf_content = split_by_newline_or_spaces(first_leaf)
21
+ @type = '#'.freeze + @type if kind_of? IterationTagNode
22
+ first_leaf_content||""
23
+ end
24
+
25
+ def split_by_newline_or_spaces(first_leaf)
26
+ sep = /\A\S*(?:\r?\n|\r)/ =~ first_leaf ? /(?:\r?\n|\r)/ : /\s+/
27
+ first_leaf.split(sep, 2)
28
+ end
29
+ private :assign_type, :split_by_newline_or_spaces
30
+
31
+ def contains_any_value_assigned_tag_node?(record)
32
+ self.select {|n| n.kind_of?(TagNode) }.each do |node|
33
+ if node.kind_of? IterationTagNode
34
+ return true if any_value_assigned_to_iteration_tag?(node, record)
35
+ else
36
+ val = record[node.join.strip]
37
+ return true if val and not val.empty?
38
+ end
39
+ end
40
+ false
41
+ end
42
+
43
+ private
44
+
45
+ def empty_sub_records?(record, node)
46
+ sub_records = record[node.type]
47
+ return true if sub_records.nil? or sub_records.empty?
48
+ sub_records.each do |rec|
49
+ return false if rec.values.find {|val| val and not val.empty? }
50
+ end
51
+ end
52
+
53
+ def any_value_assigned_to_iteration_tag?(tag_node, record)
54
+ if tag_node.type
55
+ not empty_sub_records?(record, tag_node)
56
+ else
57
+ tag_node.contains_any_value_assigned_tag_node?(record)
58
+ end
59
+ end
60
+ end
61
+
62
+ class IterationTagNode < TagNode; end
63
+ class Leaf < Parser::Leaf; end
64
+
65
+ class TagType
66
+ attr_reader :head, :tail, :token_pat, :remove_iteration_indent
67
+ attr_reader :iteration_start, :iteration_end
68
+ @types = {}
69
+
70
+ def self.[](tag_name)
71
+ @types[tag_name]
72
+ end
73
+
74
+ def self.register(tag_name=:default, tag=["<%", "%>"], iteration_tag=["<%#", "#%>"],
75
+ remove_iteration_indent=false)
76
+ @types[tag_name] = new(tag, iteration_tag, remove_iteration_indent)
77
+ end
78
+
79
+ def initialize(tag, iteration_tag, remove_iteration_indent)
80
+ assign_type(tag, iteration_tag)
81
+ @token_pat = PseudoHiki.compile_token_pat(@head.keys, @tail.keys)
82
+ @remove_iteration_indent = remove_iteration_indent
83
+ end
84
+
85
+ def assign_type(tag, iteration_tag)
86
+ @iteration_start, @iteration_end = iteration_tag
87
+ @head, @tail = {}, {}
88
+ [
89
+ [TagNode, tag],
90
+ [IterationTagNode, iteration_tag]
91
+ ].each do |node_type, head_tail|
92
+ head, tail = head_tail
93
+ @head[head] = node_type
94
+ @tail[tail] = node_type
95
+ end
96
+ end
97
+
98
+ register
99
+ register(:square_brackets, ["[[", "]]"], ["[[#", "#]]"])
100
+ register(:curly_brackets, ["{{", "}}"], ["{{#", "#}}"])
101
+ register(:xml_like1, ["<!--%", "%-->"], ["<iterate>", "</iterate>"], true)
102
+ register(:xml_like2, ["<fill>", "</fill>"], ["<iterate>", "</iterate>"], true)
103
+ register(:xml_comment_like, ["<!--%", "%-->"], ["<!--%iterate%-->", "<!--%/iterate%-->"], true)
104
+ end
105
+
106
+ class UserDefinedTagTypeConfigError < StandardError; end
107
+
108
+ def self.parse(str, tag_name=:default)
109
+ if TagType[tag_name].remove_iteration_indent
110
+ str = remove_indent_before_iteration_tags(str, TagType[tag_name])
111
+ end
112
+ new(str, TagType[tag_name]).parse.tree
113
+ end
114
+
115
+ def self.remove_indent_before_iteration_tags(template_source, tag_type)
116
+ [
117
+ tag_type.iteration_start,
118
+ tag_type.iteration_end
119
+ ].inject(template_source) do |s, tag|
120
+ s.gsub(/^([ \t]+#{Regexp.escape(tag)}(?:\r?\n|\r))/) { $1.lstrip }
121
+ end
122
+ end
123
+
124
+ def self.register_user_defined_tag_type(config_source)
125
+ config = YAML.load(config_source)
126
+ %w(tag_name tag iteration_tag).each do |item|
127
+ config[item] || raise(UserDefinedTagTypeConfigError,
128
+ "\"#{item}\" should be defined.")
129
+ end
130
+ TagType.register(registered_tag_name = config["tag_name"].to_sym,
131
+ config["tag"],
132
+ config["iteration_tag"],
133
+ config["remove_indent"] || false)
134
+ registered_tag_name
135
+ end
136
+
137
+ def initialize(str, tag)
138
+ @tag = tag
139
+ str = remove_trailing_newline_of_iteration_end_tag(str, @tag.iteration_end)
140
+ @tokens = PseudoHiki.split_into_tokens(str, @tag.token_pat)
141
+ super()
142
+ end
143
+
144
+ def parse
145
+ while token = @tokens.shift
146
+ next if @tag.tail[token] == current_node.class and self.pop
147
+ next if @tag.head[token] and self.push @tag.head[token].new
148
+ self.push Leaf.create(token)
149
+ end
150
+
151
+ self
152
+ end
153
+
154
+ private
155
+
156
+ def remove_trailing_newline_of_iteration_end_tag(str, iteration_end_tag)
157
+ str.gsub(/#{Regexp.escape(iteration_end_tag)}(?:\r?\n|\r)/, iteration_end_tag)
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'csv'
6
+
7
+ module AdHocTemplate
8
+ module RecordReader
9
+ module YAMLReader
10
+ def self.read_record(yaml_data)
11
+ YAML.load(yaml_data)
12
+ end
13
+
14
+ def self.to_yaml(config_data)
15
+ data = RecordReader.read_record(config_data)
16
+ YAML.dump(data)
17
+ end
18
+ end
19
+
20
+ module JSONReader
21
+ def self.read_record(json_data)
22
+ JSON.parse(json_data)
23
+ end
24
+
25
+ def self.to_json(config_data)
26
+ data = RecordReader.read_record(config_data)
27
+ JSON.dump(data)
28
+ end
29
+ end
30
+
31
+ module CSVReader
32
+ def self.read_record(csv_data, config={ csv: nil })
33
+ label, sep = parse_config(config)
34
+ header, *data = CSV.new(csv_data, col_sep: sep).to_a
35
+ records = data.map {|row| convert_to_hash(header, row) }
36
+ if label
37
+ { '#' + label => records }
38
+ elsif records.length == 1
39
+ records[0]
40
+ else
41
+ records
42
+ end
43
+ end
44
+
45
+ def self.convert_to_hash(header, row_array)
46
+ {}.tap do |record|
47
+ header.zip(row_array).each do |key, value|
48
+ record[key] = value
49
+ end
50
+ end
51
+ # if RUBY_VERSION >= 2.1.0: header.zip(row_array).to_h
52
+ end
53
+
54
+ def self.parse_config(config)
55
+ case config
56
+ when Symbol
57
+ format, label = config, nil
58
+ when String
59
+ format, label = :csv, config
60
+ when Hash
61
+ format, label = config.to_a[0]
62
+ end
63
+ field_sep = format == :tsv ? "\t" : CSV::DEFAULT_OPTIONS[:col_sep]
64
+ return label, field_sep
65
+ end
66
+
67
+ private_class_method :convert_to_hash
68
+ end
69
+
70
+ SEPARATOR = /:\s*/o
71
+ BLOCK_HEAD = /\A\/\/@/o
72
+ ITERATION_HEAD = /\A\/\/@#/o
73
+ EMPTY_LINE = /\A(?:\r?\n|\r)\Z/o
74
+ ITERATION_MARK = /\A#/o
75
+ READERS_RE = {
76
+ key_value: SEPARATOR,
77
+ iteration: ITERATION_HEAD,
78
+ block: BLOCK_HEAD,
79
+ empty_line: EMPTY_LINE,
80
+ }
81
+
82
+ class ReaderState
83
+ attr_accessor :current_block_label
84
+
85
+ def initialize(config={}, stack=[])
86
+ @stack = stack
87
+ @configs = [config]
88
+ setup_reader
89
+ end
90
+
91
+ def push(reader)
92
+ @stack.push reader
93
+ end
94
+
95
+ def pop
96
+ @stack.pop unless @stack.length == 1
97
+ end
98
+
99
+ def setup_stack(line)
100
+ @stack[-1].setup_stack(line)
101
+ end
102
+
103
+ def current_reader
104
+ @stack[-1]
105
+ end
106
+
107
+ def read(line)
108
+ @stack[-1].read(line)
109
+ end
110
+
111
+ def push_new_record
112
+ new_record = {}
113
+ @configs.push new_record
114
+ new_record
115
+ end
116
+
117
+ def pop_current_record
118
+ @configs.pop
119
+ end
120
+
121
+ def current_record
122
+ @configs[-1]
123
+ end
124
+
125
+ def parsed_record
126
+ @configs[0]
127
+ end
128
+
129
+ def read_record(lines)
130
+ lines = lines.each_line.to_a if lines.kind_of? String
131
+ lines.each do |line|
132
+ setup_stack(line)
133
+ read(line)
134
+ end
135
+ remove_trailing_empty_lines_from_last_block!
136
+ parsed_record
137
+ end
138
+
139
+ def last_block_value
140
+ current_record[current_block_label]
141
+ end
142
+
143
+ def remove_trailing_empty_lines_from_last_block!
144
+ if current_reader.kind_of? BlockReader
145
+ last_block_value.sub!(/(#{$/})+\Z/, $/)
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def setup_reader
152
+ Reader.setup_reader(self)
153
+ end
154
+ end
155
+
156
+ class Reader
157
+ def self.setup_reader(stack)
158
+ readers = {}
159
+ {
160
+ base: BaseReader,
161
+ key_value: KeyValueReader,
162
+ block: BlockReader,
163
+ iteration: IterationReader,
164
+ }.each do |k, v|
165
+ readers[k] = v.new(stack, readers)
166
+ end
167
+ stack.push readers[:base]
168
+ readers
169
+ end
170
+
171
+ def initialize(stack, readers)
172
+ @stack = stack
173
+ @readers = readers
174
+ end
175
+
176
+ def pop_stack
177
+ @stack.pop
178
+ end
179
+
180
+ def read(line)
181
+ end
182
+
183
+ private
184
+
185
+ def push_reader_if_match(line, readers)
186
+ readers.each do |reader|
187
+ return @stack.push(@readers[reader]) if READERS_RE[reader] === line
188
+ end
189
+ end
190
+
191
+ def setup_new_block(line, initial_value)
192
+ label = line.sub(BLOCK_HEAD, "").chomp
193
+ @stack.current_record[label] ||= initial_value
194
+ @stack.current_block_label = label
195
+ end
196
+ end
197
+
198
+
199
+ class BaseReader < Reader
200
+ def setup_stack(line)
201
+ push_reader_if_match(line, [:iteration, :block, :key_value])
202
+ end
203
+ end
204
+
205
+ class KeyValueReader < Reader
206
+ def setup_stack(line)
207
+ case line
208
+ when EMPTY_LINE, ITERATION_HEAD, BLOCK_HEAD
209
+ pop_stack
210
+ end
211
+ push_reader_if_match(line, [:iteration, :block])
212
+ end
213
+
214
+ def read(line)
215
+ key, value = line.split(SEPARATOR, 2)
216
+ @stack.current_record[key] = value.chomp
217
+ end
218
+ end
219
+
220
+ class BlockReader < Reader
221
+ def setup_stack(line)
222
+ case line
223
+ when ITERATION_HEAD, BLOCK_HEAD
224
+ @stack.remove_trailing_empty_lines_from_last_block!
225
+ pop_stack
226
+ end
227
+ push_reader_if_match(line, [:iteration, :block])
228
+ end
229
+
230
+ def read(line)
231
+ block_value = @stack.last_block_value
232
+ case line
233
+ when BLOCK_HEAD
234
+ setup_new_block(line, String.new)
235
+ when EMPTY_LINE
236
+ block_value << line unless block_value.empty?
237
+ else
238
+ block_value << line
239
+ end
240
+ end
241
+ end
242
+
243
+ class IterationReader < Reader
244
+ def setup_stack(line)
245
+ case line
246
+ when ITERATION_HEAD
247
+ @stack.pop_current_record
248
+ when BLOCK_HEAD
249
+ @stack.pop_current_record
250
+ pop_stack
251
+ @stack.push @readers[:block]
252
+ when SEPARATOR
253
+ @stack.pop_current_record
254
+ @stack.last_block_value.push @stack.push_new_record
255
+ @stack.push @readers[:key_value]
256
+ end
257
+ end
258
+
259
+ def read(line)
260
+ case line
261
+ when ITERATION_HEAD
262
+ setup_new_block(line, [])
263
+ @stack.push_new_record
264
+ end
265
+ end
266
+ end
267
+
268
+ def self.read_record(input, source_format=:default)
269
+ case source_format
270
+ when :default
271
+ ReaderState.new.read_record(input)
272
+ when :yaml
273
+ YAMLReader.read_record(input)
274
+ when :json
275
+ JSONReader.read_record(input)
276
+ when :csv, :tsv, Hash
277
+ CSVReader.read_record(input, source_format)
278
+ end
279
+ end
280
+ end
281
+ end
@@ -1,3 +1,3 @@
1
1
  module AdHocTemplate
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end