baby_erubis 0.1.0 → 1.0.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.
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