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

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 (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