nyara 0.0.1.pre.6 → 0.0.1.pre.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/example/project.rb +11 -0
  3. data/example/stream.rb +6 -2
  4. data/ext/event.c +83 -32
  5. data/ext/hashes.c +6 -1
  6. data/ext/inc/epoll.h +1 -2
  7. data/ext/inc/kqueue.h +1 -2
  8. data/ext/nyara.h +2 -0
  9. data/ext/request.c +14 -9
  10. data/ext/test_response.c +2 -5
  11. data/ext/url_encoded.c +55 -1
  12. data/lib/nyara/config.rb +68 -17
  13. data/lib/nyara/controller.rb +76 -15
  14. data/lib/nyara/controllers/public_controller.rb +14 -0
  15. data/lib/nyara/cookie.rb +5 -4
  16. data/lib/nyara/flash.rb +2 -0
  17. data/lib/nyara/nyara.rb +153 -20
  18. data/lib/nyara/patches/to_query.rb +1 -2
  19. data/lib/nyara/request.rb +0 -5
  20. data/lib/nyara/route.rb +2 -2
  21. data/lib/nyara/route_entry.rb +5 -4
  22. data/lib/nyara/session.rb +47 -22
  23. data/lib/nyara/test.rb +13 -10
  24. data/lib/nyara/view.rb +27 -49
  25. data/lib/nyara/view_handlers/erb.rb +21 -0
  26. data/lib/nyara/view_handlers/erubis.rb +81 -0
  27. data/lib/nyara/view_handlers/haml.rb +17 -0
  28. data/lib/nyara/view_handlers/slim.rb +16 -0
  29. data/nyara.gemspec +3 -1
  30. data/readme.md +2 -2
  31. data/spec/apps/connect.rb +1 -1
  32. data/spec/config_spec.rb +76 -4
  33. data/spec/{test_spec.rb → integration_spec.rb} +47 -3
  34. data/spec/path_helper_spec.rb +1 -1
  35. data/spec/performance/escape.rb +10 -0
  36. data/spec/performance/layout.slim +14 -0
  37. data/spec/performance/page.slim +16 -0
  38. data/spec/performance_spec.rb +6 -1
  39. data/spec/public/empty file.html +0 -0
  40. data/spec/public/index.html +1 -0
  41. data/spec/request_delegate_spec.rb +1 -1
  42. data/spec/request_spec.rb +20 -0
  43. data/spec/route_entry_spec.rb +7 -0
  44. data/spec/session_spec.rb +8 -4
  45. data/spec/spec_helper.rb +1 -0
  46. data/spec/{ext_parse_spec.rb → url_encoded_spec.rb} +17 -5
  47. data/spec/views/edit.haml +2 -0
  48. data/spec/views/edit.slim +2 -0
  49. data/spec/views/index.liquid +0 -0
  50. data/spec/views/invalid_layout.liquid +0 -0
  51. data/spec/views/layout.erb +1 -0
  52. data/spec/views/show.slim +1 -0
  53. data/tools/foo.rb +9 -0
  54. data/tools/hello.rb +16 -11
  55. metadata +22 -4
@@ -0,0 +1,14 @@
1
+ module Nyara
2
+ # serve public dir
3
+ class PublicController < Controller
4
+ get '/%z' do |path|
5
+ path = Config.public_path path
6
+ if path and File.file?(path)
7
+ send_file path
8
+ else
9
+ status 404
10
+ Ext.request_send_data request, "HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
11
+ end
12
+ end
13
+ end
14
+ end
data/lib/nyara/cookie.rb CHANGED
@@ -1,15 +1,16 @@
1
1
  module Nyara
2
- # http://www.ietf.org/rfc/rfc6265.txt (don't look at rfc2109)
2
+ # rfc6265 (don't look at rfc2109)
3
3
  module Cookie
4
4
  extend self
5
5
 
6
6
  # encode to string value
7
7
  def encode h
8
8
  h.map do |k, v|
9
- "#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"
9
+ "#{Ext.escape k.to_s, false}=#{Ext.escape v.to_s, false}"
10
10
  end.join '; '
11
11
  end
12
12
 
13
+ # for test
13
14
  def decode header
14
15
  res = ParamHash.new
15
16
  if data = header['Cookie']
@@ -21,9 +22,9 @@ module Nyara
21
22
  def add_set_cookie header_lines, k, v, expires: nil, max_age: nil, domain: nil, path: nil, secure: nil, httponly: true
22
23
  r = "Set-Cookie: "
23
24
  if v.nil? or v == true
24
- r << "#{CGI.escape k.to_s}; "
25
+ r << "#{Ext.escape k.to_s, false}; "
25
26
  else
26
- r << "#{CGI.escape k.to_s}=#{CGI.escape v.to_s}; "
27
+ r << "#{Ext.escape k.to_s, false}=#{Ext.escape v.to_s, false}; "
27
28
  end
28
29
  r << "Expires=#{expires.to_time.gmtime.rfc2822}; " if expires
29
30
  r << "Max-Age=#{max_age.to_i}; " if max_age
data/lib/nyara/flash.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  module Nyara
2
+ # convenient thingy that let you can pass instant message to next request<br>
3
+ # it is consumed as soon as next request arrives
2
4
  class Flash
3
5
  def initialize session
4
6
  # NOTE no need to convert hash type because Session uses ParamHash for json parsing
data/lib/nyara/nyara.rb CHANGED
@@ -8,6 +8,7 @@ require "uri"
8
8
  require "openssl"
9
9
  require "socket"
10
10
  require "tilt"
11
+ require "time"
11
12
 
12
13
  require_relative "../../ext/nyara"
13
14
  require_relative "hashes/param_hash"
@@ -26,6 +27,9 @@ require_relative "view"
26
27
  require_relative "cpu_counter"
27
28
  require_relative "part"
28
29
 
30
+ # default controllers
31
+ require_relative "controllers/public_controller"
32
+
29
33
  module Nyara
30
34
  HTTP_STATUS_FIRST_LINES = Hash[HTTP_STATUS_CODES.map{|k,v|[k, "HTTP/1.1 #{k} #{v}\r\n".freeze]}].freeze
31
35
 
@@ -41,6 +45,18 @@ module Nyara
41
45
  OK_RESP_HEADER['X-Content-Type-Options'] = 'nosniff'
42
46
  OK_RESP_HEADER['X-Frame-Options'] = 'SAMEORIGIN'
43
47
 
48
+ START_CTX = {
49
+ 0 => $0.dup,
50
+ argv: ARGV.map(&:dup),
51
+ cwd: (begin
52
+ a = File.stat(pwd = ENV['PWD'])
53
+ b = File.stat(Dir.pwd)
54
+ a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
55
+ rescue
56
+ Dir.pwd
57
+ end)
58
+ }
59
+
44
60
  class << self
45
61
  def config
46
62
  raise ArgumentError, 'block not accepted, did you mean Nyara::Config.config?' if block_given?
@@ -49,12 +65,14 @@ module Nyara
49
65
 
50
66
  def setup
51
67
  Session.init
68
+ Config.init
52
69
  Route.compile
70
+ # todo lint if SomeController#request is re-defined
53
71
  View.init
54
72
  end
55
73
 
56
74
  def start_server
57
- port = Config[:port] || 3000
75
+ port = Config[:port]
58
76
 
59
77
  puts "starting #{Config[:env]} server at 0.0.0.0:#{port}"
60
78
  case Config[:env].to_s
@@ -74,28 +92,11 @@ module Nyara
74
92
  require_relative "patches/tcp_socket"
75
93
  end
76
94
 
77
- def start_production_server port
78
- workers = Config[:workers] || ((CpuCounter.count + 1)/ 2)
79
-
80
- puts "workers: #{workers}"
81
- server = TCPServer.new '0.0.0.0', port
82
- server.listen 1000
95
+ def start_development_server port
83
96
  trap :INT do
84
- @workers.each do |w|
85
- Process.kill :KILL, w
86
- end
97
+ exit!
87
98
  end
88
- GC.start
89
- @workers = workers.times.map do
90
- fork {
91
- Ext.init_queue
92
- Ext.run_queue server.fileno
93
- }
94
- end
95
- Process.waitall
96
- end
97
99
 
98
- def start_development_server port
99
100
  t = Thread.new do
100
101
  server = TCPServer.new '0.0.0.0', port
101
102
  server.listen 1000
@@ -104,5 +105,137 @@ module Nyara
104
105
  end
105
106
  t.join
106
107
  end
108
+
109
+ # Signals:
110
+ #
111
+ # [INT] kill -9 all workers, and exit
112
+ # [QUIT] graceful quit all workers, and exit if all children terminated
113
+ # [TERM] same as QUIT
114
+ # [USR1] restore worker number
115
+ # [USR2] graceful spawn a new master and workers, with all content respawned
116
+ # [TTIN] increase worker number
117
+ # [TTOUT] decrease worker number
118
+ #
119
+ # To make a graceful hot-restart:
120
+ #
121
+ # 1. USR2 -> old master
122
+ # 2. if good (workers are up, etc), QUIT -> old master, else QUIT -> new master and fail
123
+ # 3. if good (requests are working, etc), INT -> old master
124
+ # else QUIT -> new master and USR1 -> old master to restore workers
125
+ #
126
+ # NOTE in step 2/3 if an additional fork executed in new master and hangs,
127
+ # you may need send an additional INT to terminate it.<br>
128
+ # NOTE hot-restart reloads almost everything, including Gemfile changes and configures except port.
129
+ # but, if some critical environment variable or port configure needs change, you still need cold-restart.<br>
130
+ # TODO write to a file to show workers are good<br>
131
+ # TODO detect port config change
132
+ def start_production_server port
133
+ workers = Config[:workers]
134
+
135
+ puts "workers: #{workers}"
136
+
137
+ if (server_fd = ENV['NYARA_FD'].to_i) > 0
138
+ puts "inheriting server fd #{server_fd}"
139
+ @server = TCPServer.for_fd server_fd
140
+ end
141
+ unless @server
142
+ @server = TCPServer.new '0.0.0.0', port
143
+ @server.listen 1000
144
+ ENV['NYARA_FD'] = @server.fileno.to_s
145
+ end
146
+
147
+ GC.start
148
+ @workers = []
149
+ workers.times do
150
+ incr_workers nil
151
+ end
152
+
153
+ trap :INT, &method(:kill_all)
154
+ trap :QUIT, &method(:quit_all)
155
+ trap :TERM, &method(:quit_all)
156
+ trap :USR2, &method(:spawn_new_master)
157
+ trap :USR1, &method(:restore_workers)
158
+ trap :TTIN do
159
+ if Config[:workers] > 1
160
+ Config[:workers] -= 1
161
+ decr_workers nil
162
+ end
163
+ end
164
+ trap :TTOU do
165
+ Config[:workers] += 1
166
+ incr_workers nil
167
+ end
168
+ Process.waitall
169
+ end
170
+
171
+ private
172
+
173
+ # kill all workers and exit
174
+ def kill_all sig
175
+ @workers.each do |w|
176
+ Process.kill :KILL, w
177
+ end
178
+ exit!
179
+ end
180
+
181
+ # graceful quit all workers and exit
182
+ def quit_all sig
183
+ until @workers.empty?
184
+ decr_workers sig
185
+ end
186
+ # wait will finish the wait-and-quit job
187
+ end
188
+
189
+ # spawn a new master
190
+ def spawn_new_master sig
191
+ fork do
192
+ @server.close_on_exec = false
193
+ Dir.chdir START_CTX[:cwd]
194
+ if File.executable?(START_CTX[0])
195
+ exec START_CTX[0], *START_CTX[:argv], close_others: false
196
+ else
197
+ # gemset env should be correct because env is inherited
198
+ require "rbconfig"
199
+ ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
200
+ exec ruby, START_CTX[0], *START_CTX[:argv], close_others: false
201
+ end
202
+ end
203
+ end
204
+
205
+ # restore number of workers as Config
206
+ def restore_workers sig
207
+ (Config[:workers] - @workers.size).times do
208
+ incr_workers sig
209
+ end
210
+ end
211
+
212
+ # graceful decrease worker number by 1
213
+ def decr_workers sig
214
+ w = @workers.shift
215
+ puts "killing worker #{w}"
216
+ Process.kill :QUIT, w
217
+ end
218
+
219
+ # increase worker number by 1
220
+ def incr_workers sig
221
+ pid = fork {
222
+ $0 = "(nyara:worker) ruby #{$0}"
223
+
224
+ trap :QUIT do
225
+ Ext.graceful_quit @server.fileno
226
+ end
227
+
228
+ trap :TERM do
229
+ Ext.graceful_quit @server.fileno
230
+ end
231
+
232
+ t = Thread.new do
233
+ Ext.init_queue
234
+ Ext.run_queue @server.fileno
235
+ end
236
+ t.join
237
+ }
238
+ @workers << pid
239
+ end
107
240
  end
108
241
  end
@@ -1,5 +1,4 @@
1
1
  # copied from activesupport
2
- require "cgi"
3
2
 
4
3
  =begin
5
4
  Copyright (c) 2005-2013 David Heinemeier Hansson
@@ -93,7 +92,7 @@ class Object
93
92
  #
94
93
  # Note: This method is defined as a default implementation for all Objects for Hash#to_query to work.
95
94
  def to_query(key)
96
- "#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}"
95
+ "#{Nyara::Ext.escape key.to_param, false}=#{Nyara::Ext.escape to_param.to_s, false}"
97
96
  end
98
97
  end
99
98
 
data/lib/nyara/request.rb CHANGED
@@ -128,10 +128,5 @@ module Nyara
128
128
  q
129
129
  end
130
130
  end
131
-
132
- # todo rename and move it into Ext
133
- def not_found # :nodoc:
134
- Ext.request_send_data self, "HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
135
- end
136
131
  end
137
132
  end
data/lib/nyara/route.rb CHANGED
@@ -8,7 +8,7 @@ module Nyara
8
8
  raise ArgumentError, "route prefix should be a string"
9
9
  end
10
10
  scope = scope.dup.freeze
11
- (@controllers ||= {})[scope] = controller
11
+ (@controllers ||= []) << [scope, controller]
12
12
  end
13
13
 
14
14
  def compile
@@ -51,7 +51,7 @@ module Nyara
51
51
  def clear
52
52
  # gc mark fail if wrong order?
53
53
  Ext.clear_route
54
- @controllers = {}
54
+ @controllers = []
55
55
  end
56
56
 
57
57
  # private
@@ -70,7 +70,7 @@ module Nyara
70
70
  def compile_re suffix
71
71
  return ['', []] unless suffix
72
72
  conv = []
73
- re_segs = suffix.split(/(?<=%[dfsux])|(?=%[dfsux])/).map do |s|
73
+ re_segs = suffix.split(/(?<=%[dfsuxz])|(?=%[dfsuxz])/).map do |s|
74
74
  case s
75
75
  when '%d'
76
76
  conv << :to_i
@@ -90,7 +90,7 @@ module Nyara
90
90
  '([^/]+)'
91
91
  when '%z'
92
92
  conv << :to_s
93
- '(.+)'
93
+ '(.*)'
94
94
  else
95
95
  Regexp.quote s
96
96
  end
@@ -98,13 +98,14 @@ module Nyara
98
98
  ["^#{re_segs.join}$", conv]
99
99
  end
100
100
 
101
- # split the path into parts
101
+ # split the path into 2 parts: <br>
102
+ # fixed prefix and variable suffix
102
103
  def analyse_path path
103
104
  raise 'path must contain no new line' if path.index "\n"
104
105
  raise 'path must start with /' unless path.start_with? '/'
105
106
  path = path.sub(/\/$/, '') if path != '/'
106
107
 
107
- path.split(/(?=%[dfsux])/, 2)
108
+ path.split(/(?=%[dfsuxz])/, 2)
108
109
  end
109
110
  end
110
111
  end
data/lib/nyara/session.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Nyara
2
- # helper module for session management, cookie based<br>
2
+ # cookie based session<br>
3
3
  # (usually it's no need to call cache or database data a "session")<br><br>
4
4
  # session is by default DSA + SHA2/SHA1 signed, sub config options are:
5
5
  #
@@ -10,8 +10,11 @@ module Nyara
10
10
  # - +true+: always add +Secure+
11
11
  # - +false+: always no +Secure+
12
12
  # [key] DSA private key string, in der or pem format, use random if not given
13
- # [cipher_key] if exist, use aes-256-cbc to cipher the "sig/json"<br>
14
- # NOTE: it's no need to set +cipher_key+ if using https
13
+ # [cipher_key] if exist, use aes-256-cbc to cipher the json instead of just base64 it<br>
14
+ # it's useful if you need to hide something but can't stop yourself from putting it into session,<br>
15
+ # and one of the following condition matches:
16
+ # - not using http, and need to hide the info from middlemen
17
+ # - you've put something in session current_user should not see
15
18
  #
16
19
  # = example
17
20
  #
@@ -20,15 +23,27 @@ module Nyara
20
23
  # set 'session', 'expire', 30 * 60
21
24
  # end
22
25
  #
23
- module Session
24
- extend self
26
+ # Please be careful with session key and cipher key, they should be separated from source code, and never shown to public.
27
+ class Session < ParamHash
28
+ attr_reader :init_digest, :init_data
29
+
30
+ # if the session is init with nothing, and flash is clear
31
+ def vanila?
32
+ if @init_digest.nil?
33
+ empty? or size == 1 && has_key?('flash.next') && self['flash.next'].empty?
34
+ end
35
+ end
36
+ end
25
37
 
38
+ class << Session
26
39
  CIPHER_BLOCK_SIZE = 256/8
40
+ CIPHER_RAND_MAX = 36**CIPHER_BLOCK_SIZE
41
+ JSON_DECODE_OPTS = {create_additions: false, object_class: Session}
27
42
 
28
43
  # init from config
29
44
  def init
30
45
  c = Config['session'] ? Config['session'].dup : {}
31
- @name = (c.delete('name') || 'spare_me_plz').to_s
46
+ @name = Ext.escape (c.delete('name') || 'spare_me_plz').to_s, false
32
47
 
33
48
  if c['key']
34
49
  @dsa = OpenSSL::PKey::DSA.new c.delete 'key'
@@ -56,41 +71,51 @@ module Nyara
56
71
  cookie[@name] = encode h
57
72
  end
58
73
 
59
- # encode to value
74
+ # encode to value<br>
75
+ # return h.init_data if not changed
60
76
  def encode h
77
+ return h.init_data if h.vanila?
61
78
  str = h.to_json
62
- sig = @dsa.syssign @dss.digest str
63
- str = "#{encode64 sig}/#{encode64 str}"
64
- @cipher_key ? cipher(str) : str
79
+ str = @cipher_key ? cipher(str) : encode64(str)
80
+ digest = @dss.digest str
81
+ return h.init_data if digest == h.init_digest
82
+
83
+ sig = @dsa.syssign digest
84
+ "#{encode64 sig}/#{str}"
65
85
  end
66
86
 
67
87
  # encode as header line
68
88
  def encode_set_cookie h, secure
69
89
  secure = @secure unless @secure.nil?
70
90
  expire = (Time.now + @expire).gmtime.rfc2822 if @expire
71
- "Set-Cookie: #{@name}=#{encode h}; HttpOnly#{'; Secure' if secure}#{"; Expires=#{expire}" if expire}\r\n"
91
+ # NOTE +encode h+ may return empty value, but it's still fine
92
+ "Set-Cookie: #{@name}=#{encode h}; Path=/; HttpOnly#{'; Secure' if secure}#{"; Expires=#{expire}" if expire}\r\n"
72
93
  end
73
94
 
95
+ # decode the session hash from cookie
74
96
  def decode cookie
75
- str = cookie[@name].to_s
76
- return empty_hash if str.empty?
97
+ data = cookie[@name].to_s
98
+ return empty_hash if data.empty?
77
99
 
78
- str = decipher(str) if @cipher_key
79
- sig, str = str.split '/', 2
100
+ sig, str = data.split '/', 2
80
101
  return empty_hash unless str
81
102
 
103
+ h = nil
104
+ digest = nil
82
105
  begin
83
106
  sig = decode64 sig
84
- str = decode64 str
85
- verified = @dsa.sysverify @dss.digest(str), sig
86
- if verified
87
- h = JSON.parse str, create_additions: false, object_class: ParamHash
107
+ digest = @dss.digest str
108
+ if @dsa.sysverify(digest, sig)
109
+ str = @cipher_key ? decipher(str) : decode64(str)
110
+ h = JSON.parse str, JSON_DECODE_OPTS
88
111
  end
89
112
  ensure
90
113
  return empty_hash unless h
91
114
  end
92
115
 
93
- if h.is_a?(ParamHash)
116
+ if h.is_a?(Session)
117
+ h.instance_variable_set :@init_digest, digest
118
+ h.instance_variable_set :@init_data, data
94
119
  h
95
120
  else
96
121
  empty_hash
@@ -112,7 +137,7 @@ module Nyara
112
137
  end
113
138
 
114
139
  def cipher str
115
- iv = rand(36**CIPHER_BLOCK_SIZE).to_s(36).ljust CIPHER_BLOCK_SIZE
140
+ iv = rand(CIPHER_RAND_MAX).to_s(36).ljust CIPHER_BLOCK_SIZE
116
141
  c = new_cipher true, iv
117
142
  encode64(iv.dup << c.update(str) << c.final)
118
143
  end
@@ -135,7 +160,7 @@ module Nyara
135
160
 
136
161
  def empty_hash
137
162
  # todo invoke hook?
138
- ParamHash.new
163
+ Session.new
139
164
  end
140
165
 
141
166
  def new_cipher encrypt, iv
data/lib/nyara/test.rb CHANGED
@@ -26,7 +26,7 @@ module Nyara
26
26
  def initialize response_size_limit=5_000_000
27
27
  self.response_size_limit = response_size_limit
28
28
  self.cookie = ParamHash.new
29
- self.session = ParamHash.new
29
+ self.session = Session.new
30
30
  end
31
31
 
32
32
  def process_request_data data
@@ -39,14 +39,17 @@ module Nyara
39
39
  response_data = client.read_nonblock response_size_limit
40
40
  self.response = Response.new response_data
41
41
 
42
- # merge session
43
- session.clear
44
- session.merge! request.session
45
-
46
- # merge Set-Cookie
47
- response.set_cookies.each do |cookie_seg|
48
- # todo distinguish delete, value and set
49
- Ext.parse_url_encoded_seg cookie, cookie_seg, false
42
+ # no env when route not found
43
+ if request.session
44
+ # merge session
45
+ session.clear
46
+ session.merge! request.session
47
+
48
+ # merge Set-Cookie
49
+ response.set_cookies.each do |cookie_seg|
50
+ # todo distinguish delete, value and set
51
+ Ext.parse_url_encoded_seg cookie, cookie_seg, false
52
+ end
50
53
  end
51
54
 
52
55
  server.close
@@ -77,7 +80,7 @@ module Nyara
77
80
  Session.encode_to_cookie session, cookie
78
81
  headers['Cookie'] = Cookie.encode cookie
79
82
 
80
- request_data = ["#{meth.upcase} #{path} HTTP/1.1\r\n"]
83
+ request_data = ["#{meth.upcase} #{Ext.escape path, true} HTTP/1.1\r\n"]
81
84
  headers.each do |k, v|
82
85
  request_data << "#{k}: #{v}\r\n"
83
86
  end
data/lib/nyara/view.rb CHANGED
@@ -17,8 +17,6 @@ module Nyara
17
17
  #
18
18
  # Friend layout and friend page shares one buffer, but enemy layout just concats +buffer.join+ before we flush friend layout.
19
19
  # So the simple solution is: templates other than stream-friendly ones are not allowed to be a layout.
20
- #
21
- # Note on Erubis: to support streaming, Erubis is disabled even loaded.
22
20
  class View
23
21
  # ext (without dot) => most preferrable content type (e.g. "text/html")
24
22
  ENGINE_DEFAULT_CONTENT_TYPES = ParamHash.new
@@ -26,7 +24,18 @@ module Nyara
26
24
  # ext (without dot) => stream friendly
27
25
  ENGINE_STREAM_FRIENDLY = ParamHash.new
28
26
 
27
+ autoload :ERB, File.join(__dir__, "view_handlers/erb")
28
+ autoload :Erubis, File.join(__dir__, "view_handlers/erubis")
29
+ autoload :Haml, File.join(__dir__, "view_handlers/haml")
30
+ autoload :Slim, File.join(__dir__, "view_handlers/slim")
31
+
29
32
  class Buffer < Array
33
+ alias safe_append= <<
34
+
35
+ def append= thingy
36
+ self << CGI.escape_html(thingy.to_s)
37
+ end
38
+
30
39
  def join
31
40
  r = super
32
41
  clear
@@ -39,6 +48,11 @@ module Nyara
39
48
  @root = Config['views']
40
49
  @meth2ext = {} # meth => ext (without dot)
41
50
  @meth2sig = {}
51
+ @ext_list = Tilt.mappings.keys.delete_if(&:empty?).join ','
52
+ if @ext_list !~ /\bslim\b/
53
+ @ext_list = "slim,#{@ext_list}"
54
+ end
55
+ @ext_list = "{#{@ext_list}}"
42
56
  end
43
57
  attr_reader :root
44
58
 
@@ -80,7 +94,7 @@ module Nyara
80
94
  sig = @meth2sig[meth].map{|k| "#{k}: nil" }.join ','
81
95
  sig = '_={}' if sig.empty?
82
96
  sig = "(#{sig})" # 2.0.0-p0 requirement
83
- Renderable.class_eval <<-RUBY, path, 1
97
+ Renderable.class_eval <<-RUBY, path, 0
84
98
  def render#{sig}
85
99
  #{src}
86
100
  end
@@ -133,7 +147,6 @@ module Nyara
133
147
  # returns +[meth, ext_without_dot]+
134
148
  def template path, locals={}
135
149
  if File.extname(path).empty?
136
- @ext_list ||= Tilt.mappings.keys.delete_if(&:empty?).join ','
137
150
  Dir.chdir @root do
138
151
  paths = Dir.glob("#{path}.{#@ext_list}")
139
152
  if paths.size > 1
@@ -157,53 +170,18 @@ module Nyara
157
170
 
158
171
  # Block is lazy invoked when it's ok to read the template source.
159
172
  def precompile ext
160
- src_method =\
161
- case ext
162
- when 'slim'
163
- :slim_src
164
- when 'erb', 'rhtml'
165
- :erb_src
166
- when 'haml'
167
- :haml_src
173
+ case ext
174
+ when 'slim'
175
+ Slim.src yield
176
+ when 'erb', 'rhtml'
177
+ if Config['prefer_erb']
178
+ ERB.src yield
179
+ else
180
+ Erubis.src yield
168
181
  end
169
- return unless src_method
170
-
171
- send src_method, yield
172
- end
173
-
174
- def erb_src template
175
- @erb_compiler ||= begin
176
- c = ERB::Compiler.new '<>' # trim mode
177
- c.pre_cmd = ["_erbout = @_nyara_view.out"]
178
- c.put_cmd = "_erbout.push" # after newline
179
- c.insert_cmd = "_erbout.push" # before newline
180
- c.post_cmd = ["_erbout.join"]
181
- c
182
+ when 'haml'
183
+ Haml.src yield
182
184
  end
183
- src, enc = @erb_compiler.compile template
184
- # todo do sth with enc?
185
- src
186
- end
187
-
188
- def slim_src template
189
- # todo pretty by env
190
- t = Slim::Template.new(nil, nil, pretty: false){ template }
191
- src = t.instance_variable_get :@src
192
- if src.start_with?('_buf = []')
193
- src.sub! '_buf = []', '_buf = @_nyara_view.out'
194
- end
195
- src
196
- end
197
-
198
- def haml_src template
199
- e = Haml::Engine.new template
200
- # todo trim mode
201
- <<-RUBY
202
- _hamlout = Haml::Buffer.new(nil, encoding: 'utf-8')
203
- _hamlout.buffer = @_nyara_view.out
204
- #{e.precompiled}
205
- _hamlout.buffer.join
206
- RUBY
207
185
  end
208
186
 
209
187
  def path2meth path
@@ -0,0 +1,21 @@
1
+ require "erb"
2
+
3
+ module Nyara
4
+ class View
5
+ module ERB
6
+ def self.src template
7
+ @erb_compiler ||= begin
8
+ c = ::ERB::Compiler.new '<>' # trim mode
9
+ c.pre_cmd = ["_erbout = @_nyara_view.out"]
10
+ c.put_cmd = "_erbout.push" # after newline
11
+ c.insert_cmd = "_erbout.push" # before newline
12
+ c.post_cmd = ["_erbout.join"]
13
+ c
14
+ end
15
+ src, enc = @erb_compiler.compile template
16
+ # todo do sth with enc?
17
+ src
18
+ end
19
+ end
20
+ end
21
+ end