baby_erubis 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  BabyErubis.rb
2
2
  =============
3
3
 
4
- $Release: 0.1.0 $
4
+ $Release: 1.0.0 $
5
5
 
6
6
  BabyErubis is an yet another eRuby implementation, based on Erubis.
7
7
 
@@ -10,7 +10,7 @@ BabyErubis is an yet another eRuby implementation, based on Erubis.
10
10
  * Supports HTML as well as plain text
11
11
  * Accepts both template file and template string
12
12
 
13
- BabyErubis support Ruby 1.9 or higher.
13
+ BabyErubis supports Ruby >= 1.8 and Rubinius >= 2.0.
14
14
 
15
15
 
16
16
 
@@ -41,6 +41,24 @@ Render template file:
41
41
  print output
42
42
 
43
43
 
44
+ (Use `BabyErubis::Text` instead of `BabyErubis::Html` when rendering plain text.)
45
+
46
+
47
+ Command-line examples (see `baby_erubis.rb --help` for details):
48
+
49
+ ## convert eRuby file into Ruby code
50
+ $ baby_erubis -x file.erb # text
51
+ $ baby_erubis -xH file.erb # html
52
+ $ baby_erubis -X file.erb # embedded code only
53
+ ## render eRuby file with context data
54
+ $ baby_erubis -c '{items: [A, B, C]}' file.erb # YAML
55
+ $ baby_erubis -c '@items=["A","B","C"]' file.erb # Ruby
56
+ $ baby_erubis -f data.yaml file.erb # or -f *.json, *.rb
57
+ ## debug eRuby file
58
+ $ baby_erubis -xH file.erb | ruby -wc # check syntax error
59
+ $ baby_erubis -XHNU file.erb # show embedded ruby code
60
+
61
+
44
62
 
45
63
  Template Syntax
46
64
  ===============
@@ -54,7 +72,7 @@ Expression in `<%= ... %>` is escaped according to template class.
54
72
  * `BabyErubis::Text` doesn't escape anything.
55
73
  It justs converts expression into a string.
56
74
  * `BabyErubis::Html` escapes html special characters.
57
- It converts '< > & " \'' into '&lt; &gt; &amp; &quot; &#39;' respectively.
75
+ It converts `< > & " '` into `&lt; &gt; &amp; &quot; &#39;` respectively.
58
76
 
59
77
  (Experimental) `<%- ... -%>` and `<%-= ... -%>` are handled same as
60
78
  `<% ... %>` and `<%= ... %>` respectively.
@@ -92,7 +110,7 @@ Example:
92
110
  end
93
111
 
94
112
  def render()
95
- return TEMPLATE.render(self)
113
+ return TEMPLATE.render(self) # use self as context object
96
114
  end
97
115
 
98
116
  end
@@ -105,8 +123,8 @@ Example:
105
123
  String#freeze()
106
124
  ---------------
107
125
 
108
- BabyErubis supports String#freeze() automatically when Ruby version >= 2.1.
109
- And you can controll whether to use freeze() or not.
126
+ BabyErubis supports String#freeze() automatically when on Ruby version >= 2.1.
127
+ And you can control whether to use freeze() or not.
110
128
 
111
129
  template_str = <<'END'
112
130
  <div>
@@ -292,9 +310,26 @@ Sample code:
292
310
 
293
311
 
294
312
 
313
+ Todo
314
+ ====
315
+
316
+ * Support Rails syntax (= `<%= form_for do |f| %>`)
317
+
318
+
319
+
295
320
  Changes
296
321
  =======
297
322
 
323
+
324
+ Release 1.0.0 (2014-05-17)
325
+ --------------------------
326
+
327
+ * [enhance] Provides script file `bin/baby_erubis`.
328
+ * [enhance] Supports Ruby 1.8 and Rubinius 2.x.
329
+ * [change] Define 'BabyErubis::RELEASE'.
330
+ * [bugfix] 'Template#render()' creates context object when nil passed.
331
+
332
+
298
333
  Release 0.1.0 (2014-05-06)
299
334
  --------------------------
300
335
 
data/Rakefile CHANGED
@@ -29,8 +29,14 @@ task :test do
29
29
  end
30
30
 
31
31
 
32
+ desc "remove *.rbc"
33
+ task :clean do
34
+ rm_f [Dir.glob("lib/**/*.rbc"), Dir.glob("test/**/*.rbc")]
35
+ end
36
+
37
+
32
38
  desc "copy files into 'dist/#{RELEASE}'"
33
- task :dist do
39
+ task :dist => :clean do
34
40
  require_release_number()
35
41
  spec_src = File.open('baby_erubis.gemspec') {|f| f.read }
36
42
  spec = eval spec_src
data/baby_erubis.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
3
  ###
4
- ### $Release: 0.1.0 $
4
+ ### $Release: 1.0.0 $
5
5
  ### $License: MIT License $
6
6
  ### $Copyright: copyright(c) 2014 kuwata-lab.com all rights reserved $
7
7
  ###
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
13
13
  s.name = "baby_erubis"
14
14
  s.author = "makoto kuwata"
15
15
  s.email = "kwa(at)kuwata-lab.com"
16
- s.version = "$Release: 0.1.0 $".split()[1]
16
+ s.version = "$Release: 1.0.0 $".split()[1]
17
17
  s.platform = Gem::Platform::RUBY
18
18
  s.homepage = "https://github.com/kwatch/BabyErubis/tree/ruby"
19
19
  s.summary = "yet another eRuby implementation based on Erubis"
@@ -25,14 +25,17 @@ BabyErubis is an yet another eRuby implementation, based on Erubis.
25
25
  * Accepts both template file and template string
26
26
  * Easy to customize
27
27
 
28
- BabyErubis support Ruby 1.9 or higher.
28
+ BabyErubis support Ruby 1.9 or higher, and will work on 1.8 very well.
29
29
  END
30
30
 
31
31
  ## files
32
32
  files = []
33
33
  files += Dir.glob('lib/*.rb')
34
34
  files += Dir.glob('test/*.rb')
35
+ files += ['bin/baby_erubis']
35
36
  files += %w[README.md MIT-LICENSE setup.rb baby_erubis.gemspec Rakefile]
36
37
  s.files = files
38
+ s.executables = ['baby_erubis']
39
+ s.bindir = 'bin'
37
40
  s.test_file = 'test/run_all.rb'
38
41
  end
data/bin/baby_erubis ADDED
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ ###
5
+ ### $Release: 1.0.0 $
6
+ ### $Copyright: copyright(c) 2014 kuwata-lab.com all rights reserved $
7
+ ### $License: MIT License $
8
+ ###
9
+
10
+
11
+ #require 'yaml' # on-demand load
12
+ #require 'json' # on-demand load
13
+ require 'baby_erubis'
14
+
15
+
16
+ module Cmdopt
17
+
18
+
19
+ def self.new(*args)
20
+ return Parser.new(*args)
21
+ end
22
+
23
+ def self.new_with(*optdef_strs)
24
+ parser = Parser.new
25
+ optdef_strs.each do |str|
26
+ parser.option(str)
27
+ end
28
+ return parser
29
+ end
30
+
31
+
32
+ class Schema
33
+
34
+ def initialize(optstr)
35
+ short = long = nil
36
+ case optstr
37
+ when /\A(?:<(\w+)>)? *-(\w) *, *--(\w+)(?:\[=(\S+)\]|=(\S+))?[ \t]*(?::[ \t]*(.*))?\z/
38
+ name, short, long, optarg, arg, desc = $1, $2, $3, $4, $5, $6
39
+ when /\A(?:<(\w+)>)? *-(\w)(?:\[(\S+)\]| +(\S+)?)?[ \t]*(?::[ \t]*(.*))?\z/
40
+ name, short, optarg, arg, desc = $1, $2, $3, $4, $5
41
+ when /\A(?:<(\w+)>)? *--(\w+)(?:\[=(\S+)\]|=(\S+))?[ \t]*(?::[ \t]*(.*))?\z/
42
+ name, long, optarg, arg, desc = $1, $2, $3, $4, $5
43
+ else
44
+ raise SchemaError.new("'#{optstr}': invalid option definition.")
45
+ end
46
+ @name = name
47
+ @short = short
48
+ @long = long
49
+ @arg = arg || optarg
50
+ @desc = desc
51
+ @arg_required = !! arg
52
+ @arg_optional = !! optarg
53
+ @validations = []
54
+ @action = nil
55
+ #
56
+ setup()
57
+ end
58
+
59
+ attr_reader :name, :short, :long, :arg, :desc
60
+
61
+ def setup
62
+ if @arg == 'N' || @optarg == 'N'
63
+ _add_validation(proc {|arg|
64
+ "integer expected." unless arg =~ /\A\d+\z/
65
+ })
66
+ _set_action(proc {|options, arg|
67
+ options[canonical_name()] = arg == true ? arg : arg.to_i
68
+ })
69
+ end
70
+ end
71
+ private :setup
72
+
73
+ def arg_required?
74
+ @arg_required
75
+ end
76
+
77
+ def arg_optional?
78
+ @arg_optional
79
+ end
80
+
81
+ def validate(optarg)
82
+ @validations.each do |block|
83
+ errmsg = block.call(optarg)
84
+ return errmsg if errmsg
85
+ end
86
+ return nil
87
+ end
88
+
89
+ def run_action(options, optarg)
90
+ if @action
91
+ @action.call(options, optarg)
92
+ else
93
+ options[canonical_name()] = optarg
94
+ end
95
+ nil
96
+ end
97
+
98
+ def canonical_name
99
+ return @name || @long || @short
100
+ end
101
+
102
+ def _add_validation(block)
103
+ @validations << block
104
+ end
105
+
106
+ def _set_action(block)
107
+ @action = block
108
+ end
109
+
110
+ def to_help_message(width)
111
+ if @short && @long
112
+ if @optarg ; s = "-#{@short}, --#{@long}[=#{@optarg}]"
113
+ elsif @arg ; s = "-#{@short}, --#{@long}=#{@arg}"
114
+ else ; s = "-#{@short}, --#{@long}"
115
+ end
116
+ elsif @short
117
+ if @optarg ; s = "-#{@short}[#{@optarg}]"
118
+ elsif @arg ; s = "-#{@short} #{@arg}"
119
+ else ; s = "-#{@short}"
120
+ end
121
+ elsif @long
122
+ if @optarg ; s = " --#{@long}[=#{@optarg}]"
123
+ elsif @arg ; s = " --#{@long}=#{@arg}"
124
+ else ; s = " --#{@long}"
125
+ end
126
+ end
127
+ s << ' ' * (width - s.length) if s.length < width
128
+ s << ': ' << @desc.to_s
129
+ s << "\n"
130
+ return s
131
+ end
132
+
133
+ end
134
+
135
+
136
+ class SchemaError < StandardError
137
+ end
138
+
139
+
140
+ class SchemaBuilder
141
+
142
+ def initialize(schema)
143
+ @schema = schema
144
+ end
145
+
146
+ def validation(&block)
147
+ @schema._add_validation(block)
148
+ return self
149
+ end
150
+
151
+ def action(&block)
152
+ @schema._set_action(block)
153
+ return self
154
+ end
155
+
156
+ end
157
+
158
+
159
+ class Parser
160
+
161
+ def initialize(cmdname=true)
162
+ cmdname = File.basename($0) if cmdname == true
163
+ @cmdname = cmdname
164
+ @schemas = []
165
+ end
166
+
167
+ def option(optstr)
168
+ schema = Schema.new(optstr)
169
+ @schemas << schema
170
+ return SchemaBuilder.new(schema)
171
+ end
172
+
173
+ def parse(argv)
174
+ options = {}
175
+ while argv[0] && argv[0] =~ /\A-/
176
+ optstr = argv.shift
177
+ if optstr == '--'
178
+ break
179
+ elsif optstr =~ /\A--(\w+)(?:=(.*))?/
180
+ optname = $1
181
+ optarg = $2 || true
182
+ schema = @schemas.find {|sch| sch.long == optname } or
183
+ raise error("--#{optname}: unknown option.")
184
+ if schema.arg_required? && optarg == true
185
+ raise error("--#{optname}: argument required.")
186
+ end
187
+ errmsg = schema.validate(optarg)
188
+ raise error("#{optstr}: #{errmsg}") if errmsg
189
+ schema.run_action(options, optarg)
190
+ else
191
+ i = 1
192
+ while i < optstr.length
193
+ optch = optstr[i, 1]
194
+ schema = @schemas.find {|sch| sch.short == optch } or
195
+ raise error("-#{optch}: unknown option.")
196
+ if schema.arg_required?
197
+ optarg = (i+1 < optstr.length) ? optstr[(i+1)..-1] : argv.shift or
198
+ raise error("-#{optch}: argument required.")
199
+ errmsg = schema.validate(optarg)
200
+ raise error("-#{optch} #{optarg}: #{errmsg}") if errmsg
201
+ i = optstr.length
202
+ elsif schema.arg_optional?
203
+ optarg = (i+1 < optstr.length) ? optstr[(i+1)..-1] : true
204
+ errmsg = optarg != true ? schema.validate(optarg) : nil
205
+ raise error("-#{optch}#{optarg}: #{errmsg}") if errmsg
206
+ i = optstr.length
207
+ else
208
+ optarg = true
209
+ i += 1
210
+ end
211
+ schema.run_action(options, optarg)
212
+ end
213
+ end
214
+ end
215
+ return options
216
+ end
217
+
218
+ def help_message(width=30)
219
+ s = ""
220
+ @schemas.each do |schema|
221
+ s << " " << schema.to_help_message(width-2) if schema.desc
222
+ end
223
+ return s
224
+ end
225
+
226
+ private
227
+
228
+ def error(message)
229
+ message = "#{@cmdname}: #{message}" if @cmdname && @cmdname.length > 0
230
+ return ParseError.new(message)
231
+ end
232
+
233
+ end
234
+
235
+
236
+ class ParseError < StandardError
237
+ end
238
+
239
+
240
+ end
241
+
242
+
243
+ module BabyErubis
244
+
245
+
246
+ module HideTextEnhander
247
+
248
+ def build_text(text)
249
+ return text.to_s.gsub(/^.*\n/, "\n").gsub(/./, ' ')
250
+ end
251
+ alias _t build_text
252
+
253
+ end
254
+
255
+
256
+ end
257
+
258
+
259
+ class Main
260
+
261
+ def self.main(argv=ARGV)
262
+ begin
263
+ self.new.run(argv)
264
+ return 0
265
+ rescue Cmdopt::ParseError => ex
266
+ $stderr << ex.message << "\n"
267
+ return 1
268
+ end
269
+ end
270
+
271
+ def initialize(cmdname=File.basename($0))
272
+ @cmdname = cmdname
273
+ end
274
+
275
+ def run(argv)
276
+ parser = build_parser()
277
+ options = parser.parse(argv)
278
+ if options['help']
279
+ $stdout << build_help_message(parser)
280
+ return
281
+ end
282
+ if options['version']
283
+ $stdout << BabyErubis::RELEASE << "\n"
284
+ return
285
+ end
286
+ #
287
+ klass = handle_format(options['format'] || (options['H'] ? 'html' : nil))
288
+ if options['X']
289
+ klass = Class.new(klass)
290
+ klass.class_eval { include BabyErubis::HideTextEnhander }
291
+ end
292
+ #
293
+ freeze = handle_freeze(options['freeze'])
294
+ tplopt = {:freeze=>freeze}
295
+ #
296
+ encoding = options['encoding'] || 'utf-8'
297
+ context = options['c']
298
+ datafile = options['f']
299
+ show_src = options['x'] || options['X']
300
+ (argv.empty? ? [nil] : argv).each do |filename|
301
+ if filename
302
+ template = klass.new(tplopt).from_file(filename, encoding)
303
+ else
304
+ template_str = $stdin.read()
305
+ template = klass.new(tplopt).from_str(template_str, '(stdin)')
306
+ end
307
+ if show_src
308
+ $stdout << edit_src(template.src, options['N'], options['U'], options['C'])
309
+ else
310
+ ctxobj = template.new_context({})
311
+ handle_datafile(datafile, encoding, ctxobj) if datafile
312
+ handle_context(context, encoding, ctxobj) if context
313
+ output = template.render(ctxobj)
314
+ $stdout << output
315
+ end
316
+ end
317
+ end
318
+
319
+ private
320
+
321
+ def build_parser
322
+ parser = Cmdopt.new(@cmdname)
323
+ parser.option("-h, --help : help")
324
+ parser.option("-v, --version : version")
325
+ parser.option("-x : show ruby code")
326
+ parser.option("-X : show ruby code only (no text part)")
327
+ parser.option("-N : numbering: add line numbers (for '-x/-X')")
328
+ parser.option("-U : unique: compress empty lines (for '-x/-X')")
329
+ parser.option("-C : compact: remove empty lines (for '-x/-X')")
330
+ parser.option("-c context : context string (yaml inline style or ruby code)")
331
+ parser.option("-f file : context data file (*.yaml, *.json, or *.rb)")
332
+ parser.option("-H : same as --format=html")
333
+ parser.option(" --format={text|html} : format (default: text)")\
334
+ .validation {|arg| "'text' or 'html' expected" if arg !~ /\A(text|html)\z/ }
335
+ parser.option(" --encoding=name : encoding (default: utf-8)")
336
+ parser.option(" --freeze={true|false} : use String#freeze() or not")\
337
+ .validation {|arg| "'true' or 'false' expected" if arg !~ /\A(true|false)\z/ }
338
+ parser.option("-D")
339
+ return parser
340
+ end
341
+
342
+ def build_help_message(parser)
343
+ s = "Usage: #{@cmdname} [..options..] [erubyfile]\n"
344
+ s << parser.help_message(30)
345
+ s << "\n"
346
+ s << <<"END"
347
+ Example:
348
+ ## convert eRuby file into Ruby code
349
+ $ #{@cmdname} -x file.erb # text
350
+ $ #{@cmdname} -xH file.erb # html
351
+ $ #{@cmdname} -X file.erb # embedded code only
352
+ ## render eRuby file with context data
353
+ $ #{@cmdname} -c '{items: [A, B, C]}' file.erb # YAML
354
+ $ #{@cmdname} -c '@items=["A","B","C"]' file.erb # Ruby
355
+ $ #{@cmdname} -f data.yaml file.erb # or -f *.json, *.rb
356
+ ## debug eRuby file
357
+ $ #{@cmdname} -xH file.erb | ruby -wc # check syntax error
358
+ $ #{@cmdname} -XHNU file.erb # show embedded ruby code
359
+ END
360
+ return s
361
+ end
362
+
363
+ def handle_format(format)
364
+ case format
365
+ when nil ; return BabyErubis::Text
366
+ when 'text'; return BabyErubis::Text
367
+ when 'html'; return BabyErubis::Html
368
+ else
369
+ raise "** unreachable: format=#{format.inspect}"
370
+ end
371
+ end
372
+
373
+ def handle_freeze(freeze)
374
+ case freeze
375
+ when nil ; return nil
376
+ when 'true' ; return true
377
+ when 'false'; return false
378
+ else
379
+ raise "** unreachable: options['freeze']=#{options['freeze'].inspect}"
380
+ end
381
+ end
382
+
383
+ def handle_context(context_str, encoding, context_obj)
384
+ return nil if context_str.nil? || context_str.empty?
385
+ if context_str =~ /\A\{/
386
+ kind = 'YAML'
387
+ require 'yaml'
388
+ dict = YAML.load(context_str) # raises Psych::SyntaxError
389
+ dict.is_a?(Hash) or
390
+ raise Cmdopt::ParseError.new("-c '#{context_str}': YAML mapping expected.")
391
+ dict.each {|k, v| context_obj.instance_variable_set("@#{k}", v) }
392
+ else
393
+ kind = 'Ruby'
394
+ _eval(context_str, context_obj) # raises SyntaxError
395
+ end
396
+ return context_obj
397
+ #rescue Psych::SyntaxError, SyntaxError => ex
398
+ rescue Exception => ex
399
+ errmsg = ex.to_s
400
+ case ex.class.name
401
+ when 'Psych::SyntaxError'; errmsg = errmsg.sub(/\(<unknown>\): /, '')
402
+ when 'SyntaxError'; errmsg = errmsg.sub(/\(eval\):\d: syntax error, /, '')
403
+ else
404
+ if ex.class == ArgumentError && errmsg =~ /^syntax error on line \d+, (col \d+)/
405
+ errmsg = $1 # for Rubinius
406
+ else
407
+ raise ex
408
+ end
409
+ end
410
+ raise Cmdopt::ParseError.new("-c '#{context_str}': #{kind} syntax error: (#{ex.class.name}) #{errmsg}")
411
+ end
412
+
413
+ def handle_datafile(datafile, encoding, context_obj)
414
+ return nil unless datafile
415
+ case datafile
416
+ when /\.ya?ml\z/
417
+ kind = 'YAML'
418
+ require 'yaml'
419
+ dict = File.open(datafile, "rb:utf-8") {|f| YAML.load(f) } # raises Psych::SyntaxError
420
+ dict.is_a?(Hash) or
421
+ raise Cmdopt::ParseError.new("-f #{datafile}: YAML mapping expected.")
422
+ dict.each {|k, v| context_obj.instance_variable_set("@#{k}", v) }
423
+ when /\.json\z/
424
+ kind = 'JSON'
425
+ require 'json'
426
+ json_str = File.open(datafile, "rb:utf-8") {|f| f.read() }
427
+ dict = JSON.load(json_str) # raises JSON::ParserError
428
+ dict.is_a?(Hash) or
429
+ raise Cmdopt::ParseError.new("-f #{datafile}: JSON object expected.")
430
+ dict.each {|k, v| context_obj.instance_variable_set("@#{k}", v) }
431
+ when /\.rb\z/
432
+ kind = 'Ruby'
433
+ context_str = File.open(datafile, "rb:utf-8") {|f| f.read() }
434
+ _eval(context_str, context_obj) # raises SyntaxError
435
+ else
436
+ raise Cmdopt::ParseError.new("-f #{datafile}: unknown suffix (expected '.yaml', '.json', or '.rb').")
437
+ end
438
+ return context_obj
439
+ rescue Errno::ENOENT => ex
440
+ raise Cmdopt::ParseError.new("-f #{datafile}: file not found.")
441
+ #rescue Psych::SyntaxError, JSON::ParserError, SyntaxError => ex
442
+ rescue Exception => ex
443
+ errmsg = ex.to_s
444
+ case ex.class.name
445
+ when 'Psych::SyntaxError'; errmsg = errmsg.sub(/\(<unknown>\): /, '')
446
+ when 'JSON::ParserError'; errmsg = errmsg.sub(/^(\d+): (.*) at .*\n?/, '\1: \2')
447
+ when 'SyntaxError'; errmsg = errmsg.sub(/\(eval\):\d: syntax error, /, '')
448
+ else
449
+ if ex.class == ArgumentError && errmsg =~ /^syntax error on line \d+, (col \d+)/
450
+ errmsg = $1 # for Rubinius
451
+ else
452
+ raise ex
453
+ end
454
+ end
455
+ raise Cmdopt::ParseError.new("-f #{datafile}: #{kind} syntax error: (#{ex.class.name}) #{errmsg}")
456
+ end
457
+
458
+ def _eval(_context_str, _context_obj)
459
+ _context_obj.instance_eval(_context_str)
460
+ end
461
+
462
+ def edit_src(src, numbering, unique, compact)
463
+ if numbering # -N
464
+ i = 0
465
+ src = src.gsub(/^/) { "%4d: " % (i+=1) }
466
+ src = src.gsub(/(^ *\d+:\s*\n)+/, "\n") if unique # -U
467
+ src = src.gsub(/^ *\d+:\s*\n/, '') if compact # -C
468
+ else
469
+ src = src.gsub(/(^\s*\n)+/, "\n") if unique # -U
470
+ src = src.gsub(/^\s*\n/, '') if compact # -C
471
+ end
472
+ return src
473
+ end
474
+
475
+
476
+ end
477
+
478
+
479
+ #if __FILE__ == $0
480
+ exit Main.main() unless defined? NOEXEC_SCRIPT
481
+ #end