ad_hoc_template 0.3.0 → 0.4.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.
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