directory_template 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,239 @@
1
+ # encoding: utf-8
2
+
3
+
4
+
5
+ require 'erb'
6
+ require 'directory_template/blank_slate'
7
+
8
+
9
+
10
+ class DirectoryTemplate
11
+
12
+ # @author Stefan Rusterholz <stefan.rusterholz@gmail.com>
13
+ #
14
+ # A helper class for ERB. Allows constructs like the one in the examples to
15
+ # enable simple use of variables/methods in templates.
16
+ #
17
+ # @example A simple ERB template being rendered
18
+ # tmpl = DirectoryTemplate::ErbTemplate.new("Hello <%= name %>!")
19
+ # tmpl.result(self, :name => 'world') # => 'Hello World!'
20
+ #
21
+ class ErbTemplate
22
+
23
+ # @author Stefan Rusterholz <stefan.rusterholz@gmail.com>
24
+ #
25
+ # Variables is similar to OpenStruct, but slightly optimized for create once, use once
26
+ # and giving diagnostics on exceptions/missing keys.
27
+ #
28
+ # @example
29
+ # variables = Variables.new(delegator, :x => "Value of X") { |exception|
30
+ # do_something_with(exception)
31
+ # }
32
+ # variables.x # => "Value of X"
33
+ #
34
+ class Variables < BlankSlate
35
+
36
+ # A proc for &on_error in DirectoryTemplate::ErbTemplate::Variables::new or
37
+ # DirectoryTemplate::ErbTemplate#result.
38
+ # Raises the error further on.
39
+ Raiser = proc { |e|
40
+ raise(e)
41
+ }
42
+
43
+ # A proc for &on_error in DirectoryTemplate::ErbTemplate::Variables.new or
44
+ # DirectoryTemplate::ErbTemplate#result.
45
+ # Inserts <<error_class: error_message>> in the place where the error
46
+ # occurred.
47
+ Teller = proc { |e|
48
+ "<<#{e.class}: #{e}>>"
49
+ }
50
+
51
+ # An empty Hash
52
+ EmptyHash = {}.freeze
53
+
54
+ # Regex to match setter method names
55
+ SetterPattern = /=\z/.freeze
56
+
57
+ # @param [Object] delegate
58
+ # All method calls and undefined variables are delegated to this object as method
59
+ # call.
60
+ # @param [Hash<Symbol,Object>] variables
61
+ # A hash with variables in it, keys must be Symbols.
62
+ # @param [Symbol] on_error_name
63
+ # Instead of a block you can pass the name of an existing handler, e.g. :Raiser
64
+ # or :Teller.
65
+ #
66
+ # @yield [exception] description
67
+ # The block is yielded in case of an exception with the exception as argument.
68
+ #
69
+ def initialize(delegate=nil, variables={}, on_error_name=nil, &on_error)
70
+ @delegate = delegate
71
+ @table = (@delegate ? Hash.new { |h,k| @delegate.send(k) } : EmptyHash).merge(variables)
72
+ if !on_error && on_error_name then
73
+ @on_error = self.class.const_get(on_error_name)
74
+ else
75
+ @on_error = on_error || Raiser
76
+ end
77
+ end
78
+
79
+ # @return [Array<Symbol>]
80
+ # All keys this Variables instance provides, if the include_delegate argument is
81
+ # true and the object to delegate to responds to __keys__, then it will add the
82
+ # keys of the delegate.
83
+ def __keys__(include_delegate=true)
84
+ @table.keys + ((include_delegate && @delegate.respond_to?(:__keys__)) ? @delegate.__keys__ : [])
85
+ end
86
+
87
+ # @return [Binding] Make the binding publicly available
88
+ def __binding__
89
+ binding
90
+ end
91
+
92
+ # @private
93
+ # @see Object#respond_to_missing?
94
+ def respond_to_missing?(key)
95
+ @table.respond_to?(key) || (@delegate && @delegate.respond_to?(key))
96
+ end
97
+
98
+ # @private
99
+ # Set or get the value associated with the key matching the method name.
100
+ def method_missing(m, *args) # :nodoc:
101
+ argn = args.length
102
+ if argn.zero? && @table.has_key?(m) then
103
+ @table[m]
104
+ elsif argn == 1 && m.to_s =~ SetterPattern
105
+ @table[m] = args.first
106
+ elsif @delegate
107
+ @delegate.send(m, *args)
108
+ end
109
+ rescue => e
110
+ @on_error.call(e)
111
+ end
112
+
113
+ # @private
114
+ # See Object#inspect
115
+ def inspect # :nodoc:
116
+ sprintf "#<%s:0x%08x @delegate=%s %s>",
117
+ self.class,
118
+ __id__,
119
+ @table.map { |k,v| "#{k}=#{v.inspect}" }.join(', '),
120
+ @delegate ? "#<%s:0x%08x ...>" % [@delegate.class, @delegate.object_id << 1] : "nil"
121
+ end
122
+ end
123
+
124
+ # Option defaults
125
+ Opt = {
126
+ :safe_level => nil,
127
+ :trim_mode => '%<>',
128
+ :eoutvar => '_erbout'
129
+ }
130
+
131
+ # An UnboundMethod instance of instance_eval
132
+ InstanceEvaler = Object.instance_method(:instance_eval)
133
+
134
+
135
+ # A proc for &on_error in DirectoryTemplate::ErbTemplate::Variables::new or DirectoryTemplate::ErbTemplate#result.
136
+ # Raises the error further on.
137
+ Raiser = proc { |e|
138
+ raise
139
+ }
140
+
141
+ # A proc for &on_error in DirectoryTemplate::ErbTemplate::Variables::new or DirectoryTemplate::ErbTemplate#result.
142
+ # Inserts <<error_class: error_message>> in the place where the error
143
+ # occurred.
144
+ Teller = proc { |e|
145
+ "<<#{e.class}: #{e}>>"
146
+ }
147
+
148
+ # The template string
149
+ attr_reader :string
150
+
151
+ # Like ErbTemplate.new, but instead of a template string, the path to the file
152
+ # containing the template. Sets :filename.
153
+ #
154
+ # @param [String] path
155
+ # The path to the file to use as a template
156
+ #
157
+ # @param [Hash] options
158
+ # See ErbTemplate::new for the options
159
+ def self.file(path, options=nil)
160
+ options = options ? options.merge(:filename => path) : {:filename => path}
161
+
162
+ new(File.read(path), options)
163
+ end
164
+
165
+ # @param [String] string
166
+ # The template string
167
+ # @param [Hash] options
168
+ # A couple of options
169
+ #
170
+ # @option options [String] :filename
171
+ # The filename used for the evaluation (useful for error messages)
172
+ # @option options [Integer] :safe_level
173
+ # See ERB.new
174
+ # @option options [String] :trim_mode
175
+ # See ERB.new
176
+ # @option options [Symbol, String] :eoutvar
177
+ # See ERB.new
178
+ def initialize(string, options={})
179
+ options, string = string, nil if string.kind_of?(Hash)
180
+ options = Opt.merge(options)
181
+ filename = options.delete(:filename)
182
+ raise ArgumentError, "String or filename must be given" unless string || filename
183
+
184
+ @string = string || File.read(filename)
185
+ @erb = ERB.new(@string, *options.values_at(:safe_level, :trim_mode, :eoutvar))
186
+ @erb.filename = filename if filename
187
+ end
188
+
189
+ # @return [String]
190
+ # The evaluated template. Default &on_error is the
191
+ # DirectoryTemplate::ErbTemplate::Raiser proc.
192
+ def result(variables=nil, on_error_name=nil, &on_error)
193
+ variables ||= {}
194
+ on_error ||= Raiser
195
+ variables = Variables.new(nil, variables, on_error_name, &on_error)
196
+ @erb.result(variables.__binding__)
197
+ rescue NameError => e
198
+ raise NameError, e.message+" for #{self.inspect} with #{variables.inspect}", e.backtrace
199
+ end
200
+
201
+ # @param [Hash] options
202
+ # A couple of options
203
+ #
204
+ # @option options [Hash] :variables
205
+ # A hash with all the variables that should be available in the template.
206
+ # @option options [Object] :delegate
207
+ # An object, to which methods should be delegated.
208
+ # @option options [Proc, Symbol, #to_proc] :on_error
209
+ # The callback to use in case of an exception.
210
+ #
211
+ # @return [String]
212
+ # The evaluated template. Default &on_error is the
213
+ # DirectoryTemplate::ErbTemplate::Raiser proc.
214
+ def result_with(options, &block)
215
+ options = options.dup
216
+ variables = options.delete(:variables) || {}
217
+ delegate = options.delete(:delegate)
218
+ on_error = options.delete(:on_error) || block
219
+ if on_error.is_a?(Symbol) then
220
+ on_error_name = on_error
221
+ on_error = nil
222
+ end
223
+ variables = Variables.new(delegate, variables, on_error_name, &on_error)
224
+
225
+ @erb.result(variables.__binding__)
226
+ rescue NameError => e
227
+ raise NameError, e.message+" for #{self.inspect} with #{variables.inspect}", e.backtrace
228
+ end
229
+
230
+ # @private
231
+ # See Object#inspect
232
+ def inspect # :nodoc:
233
+ sprintf "#<%s:0x%x string=%s>",
234
+ self.class,
235
+ object_id << 1,
236
+ @string.inspect
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,94 @@
1
+ class DirectoryTemplate
2
+
3
+ # ProcessData is the value that gets passed to processors.
4
+ # The processor must mutate it.
5
+ class ProcessData
6
+
7
+ # A reference to the DirectoryTemplate of which this file/directory is part of
8
+ attr_reader :directory_template
9
+
10
+ # The path as it is in the current stage of processing.
11
+ attr_reader :path
12
+
13
+ # The filename, as it is in the current stage of processing.
14
+ attr_reader :filename
15
+
16
+ # The directory, as it is in the current stage of processing.
17
+ attr_reader :directory
18
+
19
+ # The suffix, as it is in the current stage of processing.
20
+ attr_reader :suffix
21
+
22
+ # The file-content, as it is in the current stage of processing (nil for directories).
23
+ attr_accessor :content
24
+
25
+ # Variables to be used for path preprocessing
26
+ attr_reader :path_variables
27
+
28
+ # Variables to be used for filecontent preprocessing
29
+ attr_reader :content_variables
30
+
31
+ # Variables to be used for both, path- and filecontent preprocessing
32
+ attr_reader :variables
33
+
34
+
35
+ # A content of nil means this is a directory
36
+ # @param [Hash] env
37
+ # A hash with additional information, used to parametrize and preprocess the template.
38
+ # @options env [Hash<Symbol,String>] :variables
39
+ # Variables used for both, path- and filecontent processing.
40
+ # @options env [Hash<Symbol,String>] :path_variables
41
+ # Variables only used for path-processing.
42
+ # @options env [Hash<Symbol,String>] :content_variables
43
+ # Variables only used for filecontent processing.
44
+ def initialize(directory_template, path, content, env)
45
+ @directory_template = directory_template
46
+ @content = content
47
+ @variables = env[:variables] || {}
48
+ @path_variables = @variables.merge(env[:path_variables] || {})
49
+ @content_variables = @variables.merge(env[:content_variables] || {})
50
+ self.path = path
51
+ end
52
+
53
+ # Whether the processor is suitable for the given ProcessData.
54
+ # Simply delegates the job to the Processor.
55
+ #
56
+ # @see Processor#===
57
+ def ===(processor)
58
+ processor === self
59
+ end
60
+
61
+ # @return [Boolean]
62
+ # Whether the item is a file. The alternative is, that it is a directory.
63
+ #
64
+ # @see #directory?
65
+ def file?
66
+ !!@content
67
+ end
68
+
69
+ # @return [Boolean]
70
+ # Whether the item is a directory. The alternative is, that it is a file.
71
+ #
72
+ # @see #file?
73
+ def directory?
74
+ !@content
75
+ end
76
+
77
+ # @param [String] value
78
+ # The new path
79
+ #
80
+ # Sets the path, filename, directory and suffix of this item.
81
+ def path=(value)
82
+ @path = value
83
+ @filename = File.basename(value)
84
+ @directory = File.dirname(value)
85
+ @suffix = File.extname(value)
86
+ end
87
+
88
+ # Removes the current suffix. E.g. "foo/bar.baz.quuz" would be "foo/bar.baz" after the
89
+ # operation.
90
+ def chomp_suffix!
91
+ self.path = @path.chomp(@suffix)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+
4
+
5
+ require 'directory_template/erb_template'
6
+
7
+
8
+
9
+ class DirectoryTemplate
10
+ class Processor
11
+
12
+ # The ERB Processor treats the file-content as ERB template.
13
+ Erb = Processor.register(:erb, '*.erb', 'ERB Template Processor') do |data|
14
+ data.content = ErbTemplate.new(data.content).result(data.content_variables)
15
+ data.chomp_suffix!
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+
4
+
5
+ class DirectoryTemplate
6
+ class Processor
7
+
8
+ # The standard processor for file- and directory-paths. It simply uses String#% style
9
+ # keyword replacement. I.e., `%{key}` is replaced by the variable value passed with
10
+ # :key.
11
+ Format = Processor.new(:format, nil, '%{variable} format processor') do |data|
12
+ data.path = data.path % data.path_variables if data.path_variables
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+
4
+
5
+ class DirectoryTemplate
6
+ class Processor
7
+
8
+ # The ERB Processor treats the file-content as ERB template.
9
+ Markdown = Processor.register(:markdown_to_html, '*.html.markdown', 'Markdown to HTML Template Processor') do |data|
10
+ Markdown.require 'kramdown'
11
+ data.content = Kramdown::Document.new(data.content).to_html
12
+ data.chomp_suffix!
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+
4
+
5
+ class DirectoryTemplate
6
+ class Processor
7
+
8
+ # The
9
+ Stop = Processor.register(:stop, '*.stop', 'Terminate processing queue', 'After .stop, no processor will be run anymore') do |data|
10
+ data.chomp_suffix!
11
+ throw :stop_processing
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,127 @@
1
+ # encoding: utf-8
2
+
3
+ class DirectoryTemplate
4
+
5
+ # The definition of a processor
6
+ #
7
+ # Use {Processor.register} to register a processor. A registered processor is available
8
+ # in all DirectoryTemplate instances.
9
+ #
10
+ # Processor::register takes the same arguments as {Processor#initialize Processor::new}, so look there for how
11
+ # to define a processor.
12
+ #
13
+ # Take a look at {ProcessData} to see what data your processor gets and can work on.
14
+ class Processor
15
+
16
+ # A matcher-proc to never match
17
+ Never = proc { |data| false }
18
+
19
+ # Searches for all processors and registers them
20
+ def self.register_all
21
+ $LOAD_PATH.each do |path|
22
+ Dir.glob(File.join(path, 'directory_template', 'processor', '**', '*.rb')) do |processor|
23
+ require processor
24
+ end
25
+ end
26
+ end
27
+
28
+ # Creates a Processor and registers it.
29
+ # The arguments are passed verbatim to Processor::new.
30
+ #
31
+ # @return [Processor]
32
+ def self.register(*arguments, &block)
33
+ processor = new(*arguments, &block)
34
+ DirectoryTemplate.register(processor)
35
+
36
+ processor
37
+ end
38
+
39
+ # The pattern matching proc used to figure whether the processor applies to a
40
+ # ProcessData or not.
41
+ attr_reader :pattern
42
+
43
+ # The source used to create the pattern proc. I.e., the value passed to ::new as the
44
+ # pattern parameter.
45
+ attr_reader :pattern_source
46
+
47
+ # A human identifiable name
48
+ attr_reader :name
49
+
50
+ # A human understandable description of the processor
51
+ attr_reader :description
52
+
53
+ # The implementation of the processor. I.e., the block passed to ::new.
54
+ attr_reader :execute
55
+
56
+ # @param [Symbol] id
57
+ # A (in the set of Processors) unique id
58
+ #
59
+ # @param [String, Regexp, Proc, #to_proc, nil] pattern
60
+ # The pattern determines upon what {ProcessData} this processor is being invoked.
61
+ #
62
+ # If you provide a String, it is interpreted as a glob-like-pattern, e.g.
63
+ # '*.html.haml' will match any files whose suffix is '.html.haml'.
64
+ # See File::fnmatch for an extensive documentation of glob-patterns.
65
+ #
66
+ # If you provide a Regexp, the filename is matched against that regexp.
67
+ #
68
+ # If you provide a Proc (or anything that is converted to a proc), the proc gets
69
+ # the ProcessData as sole argument and should return true/false, to indicate,
70
+ # whether the Processor should be invoked or not.
71
+ #
72
+ # If you pass nil as pattern, the Processor will never be invoked. This is useful
73
+ # for processors that serve only as path processors.
74
+ #
75
+ # @param [String] name
76
+ # The name of the processor
77
+ #
78
+ # @param [String] description
79
+ # A description, what the processor does
80
+ #
81
+ # @param [#call] execute
82
+ # The implementation of the processor
83
+ def initialize(id, pattern, name=nil, description=nil, &execute)
84
+ raise ArgumentError, "ID must be a Symbol" unless id.is_a?(Symbol)
85
+ @id = id
86
+ @pattern_source = pattern
87
+ @pattern = case pattern
88
+ when String then proc { |data| File.fnmatch?(pattern, data.path) }
89
+ when Regexp then proc { |data| pattern =~ data.path }
90
+ when Proc then pattern
91
+ when nil then Never
92
+ else
93
+ raise ArgumentError, "Expected a String, Regexp or Proc as pattern, but got #{pattern.class}"
94
+ end
95
+ @name = name
96
+ @description = description
97
+ @execute = execute
98
+ end
99
+
100
+ # @return [Boolean]
101
+ # Whether the processor is suitable for the given ProcessData
102
+ def ===(process_data)
103
+ @pattern.call(process_data)
104
+ end
105
+
106
+ # Apply the processor on a ProcessData instance.
107
+ #
108
+ # @param [DirectoryTemplate::ProcessData] process_data
109
+ # The process data to apply this processor on.
110
+ #
111
+ # @return [DirectoryTemplate::ProcessData]
112
+ # The process_data passed to this method.
113
+ def call(process_data)
114
+ @execute.call(process_data)
115
+
116
+ process_data
117
+ end
118
+
119
+ # Invokes Kernel#require, and fails with a custom error message.
120
+ # @see Kernel#require
121
+ def require(lib)
122
+ super(lib)
123
+ rescue LoadError
124
+ raise "The #{@name || @id} processor requires #{lib} in order to work"
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ begin
4
+ require 'rubygems/version' # newer rubygems use this
5
+ rescue LoadError
6
+ require 'gem/version' # older rubygems use this
7
+ end
8
+
9
+ class DirectoryTemplate
10
+
11
+ # The version of DirectoryTemplate
12
+ Version = Gem::Version.new("1.0.0")
13
+ end