ad_hoc_template 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/ad_hoc_template.gemspec +1 -1
  3. data/lib/ad_hoc_template/command_line_interface.rb +53 -26
  4. data/lib/ad_hoc_template/config_manager.rb +129 -0
  5. data/lib/ad_hoc_template/default_tag_formatter.rb +6 -1
  6. data/lib/ad_hoc_template/entry_format_generator.rb +83 -12
  7. data/lib/ad_hoc_template/parser.rb +64 -59
  8. data/lib/ad_hoc_template/recipe_manager.rb +168 -0
  9. data/lib/ad_hoc_template/record_reader.rb +73 -16
  10. data/lib/ad_hoc_template/utils.rb +29 -0
  11. data/lib/ad_hoc_template/version.rb +1 -1
  12. data/lib/ad_hoc_template.rb +77 -25
  13. data/samples/en/inner_iteration/data.aht +21 -0
  14. data/samples/en/inner_iteration/data.csv +5 -0
  15. data/samples/en/inner_iteration/data.yaml +14 -0
  16. data/samples/en/inner_iteration/data2.yaml +14 -0
  17. data/samples/en/inner_iteration/for_csv.sh +3 -0
  18. data/samples/en/inner_iteration/template.html +18 -0
  19. data/samples/en/recipe/main.aht +9 -0
  20. data/samples/en/recipe/template.html +20 -0
  21. data/samples/for_recipe.sh +3 -0
  22. data/samples/ja/inner_iteration/data.aht +21 -0
  23. data/samples/ja/inner_iteration/data.csv +5 -0
  24. data/samples/ja/inner_iteration/data.yaml +14 -0
  25. data/samples/ja/inner_iteration/data2.yaml +14 -0
  26. data/samples/ja/inner_iteration/for_csv.sh +3 -0
  27. data/samples/ja/inner_iteration/template.html +18 -0
  28. data/samples/ja/recipe/main.aht +9 -0
  29. data/samples/ja/recipe/template.html +20 -0
  30. data/samples/recipe.yaml +34 -0
  31. data/spec/ad_hoc_template_spec.rb +71 -1
  32. data/spec/command_line_interface_spec.rb +105 -11
  33. data/spec/config_manager_spec.rb +142 -0
  34. data/spec/default_tag_formatter_spec.rb +13 -0
  35. data/spec/entry_format_generator_spec.rb +160 -17
  36. data/spec/parser_spec.rb +64 -20
  37. data/spec/recipe_manager_spec.rb +419 -0
  38. data/spec/record_reader_spec.rb +122 -1
  39. data/spec/test_data/en/inner_iteration/data.csv +5 -0
  40. data/spec/test_data/en/recipe/expected_result.html +32 -0
  41. data/spec/test_data/en/recipe/main.aht +9 -0
  42. data/spec/test_data/en/recipe/template.html +20 -0
  43. data/spec/test_data/ja/inner_iteration/data.csv +5 -0
  44. data/spec/test_data/ja/recipe/expected_result.html +32 -0
  45. data/spec/test_data/ja/recipe/main.aht +9 -0
  46. data/spec/test_data/ja/recipe/template.html +20 -0
  47. data/spec/test_data/recipe.yaml +34 -0
  48. metadata +47 -4
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ad_hoc_template'
4
+ require 'ad_hoc_template/utils'
5
+
6
+ module AdHocTemplate
7
+ class RecipeManager
8
+ include Utils
9
+
10
+ attr_reader :output_file, :template_encoding, :template
11
+ attr_reader :records, :recipe
12
+
13
+ def self.update_output_files_in_recipe(recipe, force_update=false)
14
+ recipe_source = open(File.expand_path(recipe)) {|file| file.read }
15
+ recipes = YAML.load_stream(recipe_source)
16
+ recipes.each do |recipe|
17
+ manager = new(recipe)
18
+ if manager.modified_after_last_output? or force_update
19
+ manager.update_output_file
20
+ end
21
+ end
22
+ end
23
+
24
+ def initialize(recipe_source)
25
+ @default = {}
26
+ read_recipe(recipe_source)
27
+ end
28
+
29
+ def read_recipe(recipe_source)
30
+ @recipe = if recipe_source.kind_of? String
31
+ RecordReader::YAMLReader.read_record(recipe_source)
32
+ else
33
+ recipe_source
34
+ end
35
+ setup_default!(@recipe)
36
+ @template_encoding = @default['template_encoding']
37
+ @output_file = @default['output_file']
38
+ @recipe
39
+ end
40
+
41
+ def load_records
42
+ @records = prepare_block_data(@recipe).tap do |main_block|
43
+ @recipe['blocks'].each do |block_source|
44
+ block = prepare_block_data(block_source)
45
+ block.keys.each do |key|
46
+ main_block[key] ||= block[key]
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def prepare_block_data(block)
53
+ determine_data_format!(block)
54
+ return {} unless block['data']
55
+ data_source = read_file(block['data'],
56
+ block['data_encoding'])
57
+ data_format = prepare_data_format(block)
58
+ RecordReader.read_record(data_source, data_format)
59
+ end
60
+
61
+ def parse_template
62
+ template_path = File.expand_path(@recipe['template'])
63
+ template_source = open(template_path,
64
+ open_mode(@template_encoding)) do |file|
65
+ file.read
66
+ end
67
+ tag_type = @recipe['tag_type'] || :default
68
+ tag_type = tag_type.to_sym unless tag_type.kind_of? Symbol
69
+ @template = Parser.parse(template_source, tag_type)
70
+ end
71
+
72
+ def update_output_file
73
+ @records ||= load_records
74
+ parse_template
75
+ content = AdHocTemplate::DataLoader.format(@template, @records)
76
+ mode = @template_encoding ? "wb:#{@template_encoding}" : 'wb'
77
+ if @output_file
78
+ open(File.expand_path(@output_file), mode) {|file| file.print content }
79
+ else
80
+ STDOUT.print content
81
+ end
82
+ end
83
+
84
+ def modified_after_last_output?
85
+ return true unless @output_file
86
+ output_path = File.expand_path(@output_file)
87
+ return true unless File.exist? output_path
88
+ output_time = File.mtime(output_path)
89
+ return true if modified_time(@recipe['template']) >= output_time
90
+ data_files = @recipe['blocks'].map {|block| block['data'] }
91
+ data_files.unshift(@recipe['data'])
92
+ data_files.any? {|data_file| modified_time(data_file) >= output_time }
93
+ end
94
+
95
+ private
96
+
97
+ def setup_default!(recipe)
98
+ recipe.each do |key, val|
99
+ @default[key] = val unless val.kind_of? Array
100
+ end
101
+
102
+ setup_sub_blocks!(recipe)
103
+ setup_main_label
104
+ end
105
+
106
+ def setup_sub_blocks!(recipe)
107
+ unless recipe['blocks']
108
+ recipe['blocks'] = []
109
+ return
110
+ end
111
+
112
+ recipe['blocks'].each do |block|
113
+ @default.keys.each do |key|
114
+ block[key] ||= @default[key]
115
+ end
116
+ end
117
+ end
118
+
119
+ def setup_main_label
120
+ if data_format = @default['data_format'] and
121
+ [:csv, :tsv].include? data_format
122
+ @default['label'] ||= RecordReader::CSVReader::HEADER_POSITION::LEFT
123
+ end
124
+ end
125
+
126
+ def determine_data_format!(block)
127
+ data_format = block['data_format']
128
+ if not data_format and block['data']
129
+ data_format = guess_file_format(block['data'])
130
+ end
131
+
132
+ block['data_format'] ||= data_format
133
+ end
134
+
135
+ def read_file(file_name, encoding)
136
+ open(File.expand_path(file_name),
137
+ open_mode(encoding)) do |file|
138
+ file.read
139
+ end
140
+ end
141
+
142
+ def open_mode(encoding)
143
+ encoding ||= Encoding.default_external.names[0]
144
+ mode = "rb"
145
+ return mode unless encoding and not encoding.empty?
146
+ bom = /\AUTF/i =~ encoding ? 'BOM|' : ''
147
+ mode += ":#{bom}#{encoding}"
148
+ end
149
+
150
+ def prepare_data_format(block)
151
+ data_format = block['data_format']
152
+ if not data_format or data_format.empty?
153
+ data_format = :default
154
+ end
155
+ data_format = data_format.to_sym
156
+ return data_format unless [:csv, :tsv].include? data_format
157
+ if label = block['label']
158
+ label = label.sub(/\A#/, '')
159
+ data_format = { data_format => label }
160
+ end
161
+ data_format
162
+ end
163
+
164
+ def modified_time(filename)
165
+ File.mtime(File.expand_path(filename))
166
+ end
167
+ end
168
+ end
@@ -8,7 +8,7 @@ module AdHocTemplate
8
8
  module RecordReader
9
9
  module YAMLReader
10
10
  def self.read_record(yaml_data)
11
- YAML.load(yaml_data)
11
+ RecordReader.convert_values_to_string(YAML.load(yaml_data))
12
12
  end
13
13
 
14
14
  def self.dump(config_data)
@@ -19,7 +19,7 @@ module AdHocTemplate
19
19
 
20
20
  module JSONReader
21
21
  def self.read_record(json_data)
22
- JSON.parse(json_data)
22
+ RecordReader.convert_values_to_string(JSON.parse(json_data))
23
23
  end
24
24
 
25
25
  def self.dump(config_data)
@@ -34,19 +34,21 @@ module AdHocTemplate
34
34
  tsv: "\t"
35
35
  }
36
36
 
37
+ module HEADER_POSITION
38
+ TOP = '__header_top__'
39
+ LEFT = '__header_left__'
40
+ end
41
+
37
42
  class NotSupportedError < StandardError; end
38
43
 
39
44
  def self.read_record(csv_data, config={ csv: nil })
40
45
  label, sep = parse_config(config)
41
- header, *data = CSV.new(csv_data, col_sep: sep).to_a
42
- records = data.map {|row| convert_to_hash(header, row) }
43
- if label
44
- { '#' + label => records }
45
- elsif records.length == 1
46
- records[0]
47
- else
48
- records
46
+ header, *data = csv_to_array(csv_data, sep, label)
47
+ csv_records = data.map {|row| convert_to_hash(header, row) }
48
+ if label and label.index('|')
49
+ return compose_inner_iteration_records(csv_records, label)
49
50
  end
51
+ compose_record(csv_records, label)
50
52
  end
51
53
 
52
54
  def self.dump(config_data, col_sep=COL_SEP[:csv])
@@ -84,8 +86,51 @@ module AdHocTemplate
84
86
  return label, col_sep
85
87
  end
86
88
 
89
+ def self.csv_to_array(csv_data, col_sep, label)
90
+ array = CSV.new(csv_data, col_sep: col_sep).to_a
91
+ if not label or label == HEADER_POSITION::LEFT
92
+ array = array.transpose
93
+ end
94
+ array
95
+ end
96
+
97
+ def self.compose_record(csv_records, label)
98
+ if label
99
+ { '#' + label => csv_records }
100
+ elsif csv_records.length == 1
101
+ csv_records[0]
102
+ else
103
+ csv_records
104
+ end
105
+ end
106
+
107
+ def self.compose_inner_iteration_records(csv_records, given_label,
108
+ main_record={})
109
+ outer_label, inner_label, key = ('#' + given_label).split(/\|/, 3)
110
+ values = inner_iteration_records(csv_records, key)
111
+ labels = inner_iteration_labels(outer_label, inner_label, values.keys)
112
+ unless main_record[outer_label]
113
+ main_record[outer_label] = values.keys.map {|k| { key => k } }
114
+ end
115
+ values.keys.each {|k| main_record[labels[k]] = values[k] }
116
+ main_record
117
+ end
118
+
119
+ def self.inner_iteration_records(csv_records, key)
120
+ values = Hash.new {|h, k| h[k] = [] }
121
+ csv_records.each {|record| values[record[key]].push record }
122
+ values
123
+ end
124
+
125
+ def self.inner_iteration_labels(outer_label, inner_label, keys)
126
+ labels = keys.inject({}) do |h, key|
127
+ h[key] = [outer_label, inner_label, key].join('|')
128
+ h
129
+ end
130
+ end
131
+
87
132
  def self.csv_compatible_format?(data)
88
- iteration_blocks_count = data.values.select {|v| v.kind_of? Array }.size
133
+ iteration_blocks_count = data.values.count {|v| v.kind_of? Array }
89
134
  iteration_blocks_count == 0 or (iteration_blocks_count == 1 && data.size == 1)
90
135
  end
91
136
 
@@ -111,6 +156,10 @@ module AdHocTemplate
111
156
  end
112
157
 
113
158
  private_class_method :convert_to_hash, :parse_config
159
+ private_class_method :csv_to_array, :compose_record
160
+ private_class_method :compose_inner_iteration_records
161
+ private_class_method :inner_iteration_records
162
+ private_class_method :inner_iteration_labels
114
163
  private_class_method :csv_compatible_format?, :hashes_to_arrays
115
164
  private_class_method :find_sub_records, :array_to_csv
116
165
  end
@@ -134,6 +183,7 @@ module AdHocTemplate
134
183
  ITERATION_HEAD = /\A\/\/\/@#/o
135
184
  EMPTY_LINE = /\A#{LINE_END_STR}\Z/o
136
185
  ITERATION_MARK = /\A#/o
186
+ COMMENT_HEAD = /\A\/\/\/\//
137
187
  READERS_RE = {
138
188
  key_value: SEPARATOR,
139
189
  iteration: ITERATION_HEAD,
@@ -274,6 +324,7 @@ module AdHocTemplate
274
324
  end
275
325
 
276
326
  def read(line)
327
+ return if COMMENT_HEAD =~ line
277
328
  key, value = line.split(SEPARATOR, 2)
278
329
  @stack.current_record[key] = value.chomp
279
330
  end
@@ -294,7 +345,7 @@ module AdHocTemplate
294
345
  case line
295
346
  when BLOCK_HEAD
296
347
  setup_new_block(line, String.new)
297
- when EMPTY_LINE
348
+ when EMPTY_LINE, COMMENT_HEAD
298
349
  block_value << line unless block_value.empty?
299
350
  else
300
351
  block_value << line
@@ -405,10 +456,16 @@ module AdHocTemplate
405
456
  end
406
457
 
407
458
  def self.parse_if_necessary(source)
408
- if source.kind_of? String
409
- RecordReader.read_record(source)
410
- else
411
- source
459
+ source.kind_of?(String) ? read_record(source) : source
460
+ end
461
+
462
+ def self.convert_values_to_string(data)
463
+ data.each do |k, v|
464
+ if v.kind_of? Array
465
+ v.each {|sub_rec| convert_values_to_string(sub_rec) }
466
+ elsif v and not v.kind_of? String
467
+ data[k] = v.to_s
468
+ end
412
469
  end
413
470
  end
414
471
  end
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module AdHocTemplate
4
+ module Utils
5
+ FILE_EXTENTIONS = {
6
+ /\.ya?ml\Z/i => :yaml,
7
+ /\.json\Z/i => :json,
8
+ /\.csv\Z/i => :csv,
9
+ /\.tsv\Z/i => :tsv,
10
+ }
11
+
12
+ def guess_file_format(filename)
13
+ if_any_regex_match(FILE_EXTENTIONS, filename) do |ext_re, format|
14
+ return format
15
+ end
16
+ end
17
+
18
+ def if_any_regex_match(regex_table, target, failure_message=nil)
19
+ regex_table.each do |re, paired_value|
20
+ if re =~ target
21
+ yield re, paired_value
22
+ return
23
+ end
24
+ end
25
+ STDERR.puts failure_message if failure_message
26
+ nil
27
+ end
28
+ end
29
+ end
@@ -1,3 +1,3 @@
1
1
  module AdHocTemplate
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -4,9 +4,28 @@ require "ad_hoc_template/record_reader"
4
4
  require "ad_hoc_template/default_tag_formatter"
5
5
  require "ad_hoc_template/pseudohiki_formatter"
6
6
  require "ad_hoc_template/entry_format_generator"
7
+ require "ad_hoc_template/config_manager"
7
8
 
8
9
  module AdHocTemplate
9
10
  class DataLoader
11
+ class InnerLabel
12
+ attr_reader :inner_label
13
+
14
+ def self.labels(inner_labels, cur_label)
15
+ inner_labels.map {|label| new(label, cur_label) }
16
+ end
17
+
18
+ def initialize(inner_label, cur_label)
19
+ @inner_label = inner_label
20
+ @label, @key = inner_label.sub(/\A#/, ''.freeze).split(/\|/, 2)
21
+ @cur_label = cur_label
22
+ end
23
+
24
+ def full_label(record)
25
+ [@cur_label, @label, record[@key]].join('|')
26
+ end
27
+ end
28
+
10
29
  def self.format(template, record, tag_formatter=DefaultTagFormatter.new)
11
30
  if record.kind_of? Array
12
31
  return format_multi_records(template, record, tag_formatter)
@@ -26,45 +45,42 @@ module AdHocTemplate
26
45
  @tag_formatter = tag_formatter
27
46
  end
28
47
 
29
- def visit(tree)
48
+ def visit(tree, memo)
30
49
  case tree
31
- when Parser::IterationTagNode
32
- format_iteration_tag(tree)
33
- when Parser::FallbackTagNode
50
+ when Parser::IterationNode
51
+ format_iteration_tag(tree, memo)
52
+ when Parser::FallbackNode
34
53
  ''.freeze
35
- when Parser::TagNode
36
- format_tag(tree)
54
+ when Parser::ValueNode
55
+ format_value_tag(tree, memo)
37
56
  when Parser::Leaf
38
57
  tree.join
39
58
  else
40
- tree.map {|node| node.accept(self) }
59
+ tree.map {|node| node.accept(self, memo) }
41
60
  end
42
61
  end
43
62
 
44
- def format_iteration_tag(tag_node)
45
- sub_records = @record[tag_node.type]||[@record]
46
- tag_node = cast(tag_node)
47
- fallback_nodes = tag_node.select {|sub_node| sub_node.kind_of? Parser::FallbackTagNode }
63
+ def format_iteration_tag(iteration_tag_node, memo)
64
+ tag_node = cast(iteration_tag_node)
48
65
 
49
- sub_records.map do |record|
66
+ prepare_sub_records(iteration_tag_node).map do |record|
50
67
  if tag_node.contains_any_value_assigned_tag_node?(record)
51
- data_loader = AdHocTemplate::DataLoader.new(record, @tag_formatter)
52
- tag_node.map {|leaf| leaf.accept(data_loader) }.join
53
- elsif not fallback_nodes.empty?
54
- format_fallback_tags(fallback_nodes, record)
68
+ visit_with_sub_record(tag_node, record, memo)
69
+ elsif fallback_nodes = select_fallback_nodes(tag_node)
70
+ format_fallback_tags(fallback_nodes, record, memo)
55
71
  else
56
72
  "".freeze
57
73
  end
58
74
  end
59
75
  end
60
76
 
61
- def format_tag(tag_node)
62
- leafs = tag_node.map {|leaf| leaf.accept(self) }
77
+ def format_value_tag(tag_node, memo)
78
+ leafs = tag_node.map {|leaf| leaf.accept(self, memo) }
63
79
  @tag_formatter.format(tag_node.type, leafs.join.strip, @record)
64
80
  end
65
81
 
66
- def format(tree)
67
- tree.accept(self).join
82
+ def format(tree, memo=nil)
83
+ tree.accept(self, memo).join
68
84
  end
69
85
 
70
86
  private
@@ -73,12 +89,44 @@ module AdHocTemplate
73
89
  node_type.new.concat(node.clone)
74
90
  end
75
91
 
76
- def format_fallback_tags(fallback_nodes, record)
92
+ def prepare_sub_records(tag_node)
93
+ cur_label = tag_node.type
94
+ sub_records = @record[cur_label]||[@record]
95
+ return sub_records unless cur_label
96
+ inner_labels = tag_node.inner_iteration_tag_labels
97
+ return sub_records unless inner_labels
98
+ inner_labels = InnerLabel.labels(inner_labels, cur_label)
99
+ sub_records.map do |record|
100
+ prepare_inner_iteration_records(record, inner_labels)
101
+ end
102
+ end
103
+
104
+ def prepare_inner_iteration_records(record, inner_labels)
105
+ new_record = nil
106
+ inner_labels.each do |label|
107
+ if inner_data = @record[label.full_label(record)]
108
+ new_record ||= record.dup
109
+ new_record[label.inner_label] = inner_data
110
+ end
111
+ end
112
+ new_record || record
113
+ end
114
+
115
+ def visit_with_sub_record(tag_node, record, memo)
116
+ data_loader = AdHocTemplate::DataLoader.new(record, @tag_formatter)
117
+ tag_node.map {|leaf| leaf.accept(data_loader, memo) }.join
118
+ end
119
+
120
+ def select_fallback_nodes(tag_node)
121
+ tags = tag_node.select {|sub_node| sub_node.kind_of? Parser::FallbackNode }
122
+ tags.empty? ? nil : tags
123
+ end
124
+
125
+ def format_fallback_tags(fallback_nodes, record, memo)
77
126
  data_loader = AdHocTemplate::DataLoader.new(record, @tag_formatter)
78
- fallback_nodes = fallback_nodes.map {|node| cast(node, Parser::IterationTagNode) }
79
- fallback_nodes = cast(fallback_nodes)
80
- fallback_nodes.map do |node|
81
- node.contains_any_value_tag? ? node.accept(data_loader) : node.join
127
+ fallback_nodes.map do |fallback_node|
128
+ node = cast(fallback_node, Parser::IterationNode)
129
+ node.contains_any_value_tag? ? node.accept(data_loader, memo) : node.join
82
130
  end
83
131
  end
84
132
  end
@@ -89,4 +137,8 @@ module AdHocTemplate
89
137
  record = RecordReader.read_record(record_data, data_format)
90
138
  DataLoader.format(tree, record, tag_formatter)
91
139
  end
140
+
141
+ def self.local_settings(&config_block)
142
+ ConfigManager.configure(&config_block)
143
+ end
92
144
  end
@@ -0,0 +1,21 @@
1
+ ///@#authors
2
+
3
+ name: Albert Camus
4
+
5
+ name: Marcel Aymé
6
+
7
+ ///@#authors|works|Albert Camus
8
+
9
+ title: L'Étranger
10
+ year: 1942
11
+
12
+ title: La Peste
13
+ year: 1947
14
+
15
+ ///@#authors|works|Marcel Aymé
16
+
17
+ title: Le Passe-muraille
18
+ year: 1943
19
+
20
+ title: Les Contes du chat perché
21
+ year: 1934-1946
@@ -0,0 +1,5 @@
1
+ name,title,year
2
+ Albert Camus,L'Étranger,1942
3
+ Albert Camus,La Peste,1947
4
+ Marcel Aymé,Le Passe-muraille,1943
5
+ Marcel Aymé,Les Contes du chat perché,1934-1946
@@ -0,0 +1,14 @@
1
+ ---
2
+ "#authors":
3
+ - name: Albert Camus
4
+ - name: Marcel Aymé
5
+ "#authors|works|Albert Camus":
6
+ - title: L'Étranger
7
+ year: 1942
8
+ - title: La Peste
9
+ year: 1947
10
+ "#authors|works|Marcel Aymé":
11
+ - title: Le Passe-muraille
12
+ year: 1943
13
+ - title: Les Contes du chat perché
14
+ year: 1934-1946
@@ -0,0 +1,14 @@
1
+ ---
2
+ "#authors":
3
+ - name: Albert Camus
4
+ "#works|name":
5
+ - title: L'Étranger
6
+ year: 1942
7
+ - title: La Peste
8
+ year: 1947
9
+ - name: Marcel Aymé
10
+ "#works|name":
11
+ - title: Le Passe-muraille
12
+ year: 1943
13
+ - title: Les Contes du chat perché
14
+ year: 1934-1946
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ ad_hoc_template -t xml_comment_like -d 'csv:authors|works|name' template.html data.csv
@@ -0,0 +1,18 @@
1
+ <h1>Authors</h1>
2
+
3
+ <!--%iterate%-->authors:
4
+ <h2><!--%h name %--></h2>
5
+
6
+ <table summary="List of <!--%h name %-->&#39;s famous works with publication year">
7
+ <caption>Famous works</caption>
8
+ <thead>
9
+ <tr><th scope="col">Title</th><th scope="col">Publication Year</th></tr>
10
+ </thead>
11
+ <tbody>
12
+ <!--%iterate%-->works|name:
13
+ <tr><td><!--%h title %--></td><td><!--%h year %--></td></tr>
14
+ <!--%/iterate%-->
15
+ </tbody>
16
+ </table>
17
+
18
+ <!--%/iterate%-->
@@ -0,0 +1,9 @@
1
+ country: French
2
+
3
+ ///@#authors
4
+
5
+ name: Albert Camus
6
+ birth_year: 1913
7
+
8
+ name: Marcel Aymé
9
+ birth_year: 1902
@@ -0,0 +1,20 @@
1
+ <h1>Famous authors of <!--%h country %--> literature</h1>
2
+
3
+ <!--%iterate%-->authors:
4
+ <h2><!--%h name %--></h2>
5
+
6
+ <p>Born in <!--%h birth_year %--></p>
7
+
8
+ <table summary="List of <!--%h name %-->&#39;s famous works with publication year">
9
+ <caption>Famous works</caption>
10
+ <thead>
11
+ <tr><th scope="col">Title</th><th scope="col">Publication Year</th></tr>
12
+ </thead>
13
+ <tbody>
14
+ <!--%iterate%-->works|name:
15
+ <tr><td><!--%h title %--></td><td><!--%h year %--></td></tr>
16
+ <!--%/iterate%-->
17
+ </tbody>
18
+ </table>
19
+
20
+ <!--%/iterate%-->
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ ad_hoc_template -c recipe.yaml
@@ -0,0 +1,21 @@
1
+ ///@#authors
2
+
3
+ name: アルベール・カミユ
4
+
5
+ name: マルセル・エイメ
6
+
7
+ ///@#authors|works|アルベール・カミユ
8
+
9
+ title: 異邦人
10
+ year: 1942
11
+
12
+ title: ペスト
13
+ year: 1947
14
+
15
+ ///@#authors|works|マルセル・エイメ
16
+
17
+ title: 壁抜け男
18
+ year: 1943
19
+
20
+ title: おにごっこ物語
21
+ year: 1934-1946
@@ -0,0 +1,5 @@
1
+ name,title,year
2
+ アルベール・カミユ,異邦人,1942
3
+ アルベール・カミユ,ペスト,1947
4
+ マルセル・エイメ,壁抜け男,1943
5
+ マルセル・エイメ,おにごっこ物語,1934-1946
@@ -0,0 +1,14 @@
1
+ ---
2
+ "#authors":
3
+ - name: "アルベール・カミユ"
4
+ - name: "マルセル・エイメ"
5
+ "#authors|works|アルベール・カミユ":
6
+ - title: "異邦人"
7
+ year: '1942'
8
+ - title: "ペスト"
9
+ year: '1947'
10
+ "#authors|works|マルセル・エイメ":
11
+ - title: "壁抜け男"
12
+ year: '1943'
13
+ - title: "おにごっこ物語"
14
+ year: 1934-1946
@@ -0,0 +1,14 @@
1
+ ---
2
+ "#authors":
3
+ - name: "アルベール・カミユ"
4
+ "#works|name":
5
+ - title: "異邦人"
6
+ year: '1942'
7
+ - title: "ペスト"
8
+ year: '1947'
9
+ - name: "マルセル・エイメ"
10
+ "#works|name":
11
+ - title: "壁抜け男"
12
+ year: '1943'
13
+ - title: "おにごっこ物語"
14
+ year: 1934-1946