ad_hoc_template 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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