Merge commit 'source/master'

This commit is contained in:
Markus Prinz 2008-05-13 19:48:11 +02:00
commit 5e56613a32
15 changed files with 497 additions and 95 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "vendor/rack"]
path = vendor/rack
url = git://github.com/chneukirchen/rack-mirror.git

2
CHANGELOG Normal file
View File

@ -0,0 +1,2 @@
v0.2.1 File upload fix and minor tweaks
v0.2.0 Released!

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2007 Blake Mizerany
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

45
Manifest Normal file
View File

@ -0,0 +1,45 @@
CHANGELOG
images/404.png
images/500.png
index.html
lib/sinatra/test/methods.rb
lib/sinatra/test/spec.rb
lib/sinatra/test/unit.rb
lib/sinatra.rb
README.rdoc
test/app_test.rb
test/application_test.rb
test/builder_test.rb
test/custom_error_test.rb
test/diddy_test.rb
test/erb_test.rb
test/event_context_test.rb
test/events_test.rb
test/haml_test.rb
test/helper.rb
test/mapped_error_test.rb
test/public/foo.xml
test/rest_test.rb
test/sass_test.rb
test/sessions_test.rb
test/streaming_test.rb
test/sym_params_test.rb
test/template_test.rb
test/use_in_file_templates_test.rb
test/views/foo.builder
test/views/foo.erb
test/views/foo.haml
test/views/foo.sass
test/views/foo_layout.erb
test/views/foo_layout.haml
test/views/layout_test/foo.builder
test/views/layout_test/foo.erb
test/views/layout_test/foo.haml
test/views/layout_test/foo.sass
test/views/layout_test/layout.builder
test/views/layout_test/layout.erb
test/views/layout_test/layout.haml
test/views/layout_test/layout.sass
test/views/no_layout/no_layout.builder
test/views/no_layout/no_layout.haml
Manifest

View File

@ -57,8 +57,14 @@ With params
Splat'n
get '/message/*' do
# matches /message/1/2/3/4/5
get '/say/*/to/*' do
# matches /say/hello/to/world
params["splat"] # => ["hello", "world"]
end
get '/download/*.*' do
# matches /download/path/to/file.xml
params["splat"] # => ["path/to/file", "xml"]
end
Get an agent!
@ -82,7 +88,7 @@ If a file exists that maps to the REQUEST_PATH then it is served and the request
= Views (if you need MVC)
All views are looked up in:
All file-based views are looked up in:
root
| - views/
@ -100,7 +106,7 @@ This will render <tt>./views/index.haml</tt>
=== Sass
get '/stylesheet.css' do
header 'Content-Type' => 'text/css; charset=utf-8'
content_type 'text/css', :charset => 'utf-8'
sass :stylesheet
end
@ -144,12 +150,12 @@ This one is cool:
__END__
## layout
@@ layout
X
= yield
X
## index
@@ index
%div.title Hello world!!!!!
Try it!
@ -174,7 +180,7 @@ This works like Haml except you use <tt>erb</tt> instead of <tt>haml</tt>
=== Sass
This works like Haml except you use <tt>sass</tt> instead of <tt>haml</tt>. It's also a good idea to add <tt>header 'Content-Type' => 'text/css; charset=utf-8'</tt> before your call to <tt>sass</tt> so Sinatra returns the proper content type header with the file.
This works like Haml except you use <tt>sass</tt> instead of <tt>haml</tt>. It's also a good idea to add <tt>content_type 'text/css', :charset => 'utf-8'</tt> before your call to <tt>sass</tt> so Sinatra returns the proper content type header with the file.
=== Builder
@ -301,7 +307,7 @@ Sinatra will pass you the error via the 'sinatra.error' in request.env
Custom error mapping:
error MyCustomError do
'So what happened was...' + request.env['sinatra.env'].message
'So what happened was...' + request.env['sinatra.error'].message
end
then if this happens:
@ -345,6 +351,20 @@ When using send_file or static files you may have mime types Sinatra doesn't und
= Testing
=== Methods
get_it path, params
get_it path, params.merge(:env => { 'HTTP_HOST' => 'www.sinatrarb.com' }) or
get_it path, params.merge(:env => { :host => 'www.sinatrarb.com' })
RESTful:
post_it '/foo', '<myxml></myxml>', 'HTTP_ACCEPT' => 'application/xml'
also works with:
get_it, post_it, put_it, delete_it, head_it
=== Test/Unit
require 'my_sinatra_app'
@ -401,7 +421,7 @@ Options are:
-h # help
-p # set the port (default is 4567)
-e # set the environment (default is development)
-x # turn on the mutext lock (default is off)
-x # turn on the mutex lock (default is off)
= Contribute
@ -418,13 +438,3 @@ at the top of your sinatra.rb file
get '/about' do
"I'm running on Version " + Sinatra::Version.combined
end

View File

@ -1,17 +1,14 @@
require 'rubygems'
require 'rake/testtask'
require 'rake/rdoctask'
require 'echoe'
task :default => :test
Rake::RDocTask.new do |rd|
rd.main = "README.rdoc"
rd.rdoc_files += ["README.rdoc"]
rd.rdoc_files += Dir.glob("lib/**/*.rb")
rd.rdoc_dir = 'doc'
end
Rake::TestTask.new do |t|
ENV['SINATRA_ENV'] = 'test'
t.pattern = File.dirname(__FILE__) + "/test/*_test.rb"
Echoe.new("sinatra") do |p|
p.author = "Blake Mizerany"
p.summary = "Classy web-development dressed in a DSL"
p.url = "http://www.sinatrarb.com"
p.docs_host = "sinatrarb.com:/var/www/blakemizerany.com/public/docs/"
p.dependencies = ["mongrel >=1.0.1"]
p.install_message = "*** Be sure to checkout the site for helpful tips! sinatrarb.com ***"
p.include_rakefile = true
end

View File

@ -1,9 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Your Page Title</title>
<meta http-equiv="REFRESH" content="0;url=http://sinatrarb.com"></HEAD>
<BODY>
This site has <a href="http:://sinatrarb.com">moved</a>.
</BODY>
</HTML>

View File

@ -1,6 +1,13 @@
Dir[File.dirname(__FILE__) + "/../vendor/*"].each do |l|
$:.unshift "#{File.expand_path(l)}/lib"
end
require 'rack'
require 'rubygems'
require 'uri'
require 'time'
require 'ostruct'
require "uri"
if ENV['SWIFT']
require 'swiftcore/swiftiplied_mongrel'
@ -10,9 +17,6 @@ elsif ENV['EVENT']
puts "Using Evented Mongrel"
end
require 'rack'
require 'ostruct'
class Class
def dslify_writer(*syms)
syms.each do |sym|
@ -48,7 +52,7 @@ module Rack #:nodoc:
end
def user_agent
env['HTTP_USER_AGENT']
@env['HTTP_USER_AGENT']
end
private
@ -78,7 +82,7 @@ module Sinatra
module Version
MAJOR = '0'
MINOR = '2'
REVISION = '0'
REVISION = '1'
def self.combined
[MAJOR, MINOR, REVISION].join('.')
end
@ -120,12 +124,32 @@ module Sinatra
app
end
def server
@server ||= case options.server
when "mongrel"
Rack::Handler::Mongrel
when "webrick"
Rack::Handler::WEBrick
when "cgi"
Rack::Handler::CGI
when "fastcgi"
Rack::Handler::FastCGI
else
if defined?(Rack::Handler::Thin)
Rack::Handler::Thin
else
options.server ||= "mongrel"
eval("Rack::Handler::#{options.server.capitalize}")
end
end
end
def run
begin
puts "== Sinatra has taken the stage on port #{port} for #{env}"
puts "== Sinatra has taken the stage on port #{port} for #{env} with backup by #{server.name}"
require 'pp'
Rack::Handler::Mongrel.run(build_application, :Port => port) do |server|
server.run(build_application, :Port => port) do |server|
trap(:INT) do
server.stop
puts "\n== Sinatra has ended his set (crowd applauds)"
@ -140,7 +164,7 @@ module Sinatra
class Event
URI_CHAR = '[^/?:,&#\.]'.freeze unless defined?(URI_CHAR)
PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
PARAM = /(:(#{URI_CHAR}+)|\*)/.freeze unless defined?(PARAM)
SPLAT = /(.*?)/
attr_reader :path, :block, :param_keys, :pattern, :options
@ -149,13 +173,18 @@ module Sinatra
@block = b
@param_keys = []
@options = options
regex = @path.to_s.gsub(PARAM) do
@param_keys << $1
"(#{URI_CHAR}+)"
splats = 0
regex = @path.to_s.gsub(PARAM) do |match|
if match == "*"
@param_keys << "_splat_#{splats}"
splats += 1
SPLAT.to_s
else
@param_keys << $2
"(#{URI_CHAR}+)"
end
end
regex.gsub!('*', SPLAT.to_s)
@pattern = /^#{regex}$/
end
@ -170,6 +199,11 @@ module Sinatra
end
return unless pattern =~ request.path_info.squeeze('/')
params.merge!(param_keys.zip($~.captures.map(&:from_param)).to_hash)
splats = params.select { |k, v| k =~ /^_splat_\d+$/ }.sort.map(&:last)
unless splats.empty?
params.delete_if { |k, v| k =~ /^_splat_\d+$/ }
params["splat"] = splats
end
Result.new(block, params, 200)
end
@ -193,14 +227,14 @@ module Sinatra
def invoke(request)
return unless File.file?(
Sinatra.application.options.public + request.path_info
Sinatra.application.options.public + request.path_info.http_unescape
)
Result.new(block, {}, 200)
end
def block
Proc.new do
send_file Sinatra.application.options.public + request.path_info,
send_file Sinatra.application.options.public + request.path_info.http_unescape,
:disposition => nil
end
end
@ -376,23 +410,130 @@ module Sinatra
header('Cache-Control' => 'private') if headers['Cache-Control'] == 'no-cache'
end
end
# Helper methods for building various aspects of the HTTP response.
module ResponseHelpers
# Immediately halt response execution by redirecting to the resource
# specified. The +path+ argument may be an absolute URL or a path
# relative to the site root. Additional arguments are passed to the
# halt.
#
# With no integer status code, a '302 Temporary Redirect' response is
# sent. To send a permanent redirect, pass an explicit status code of
# 301:
#
# redirect '/somewhere/else', 301
#
# NOTE: No attempt is made to rewrite the path based on application
# context. The 'Location' response header is set verbatim to the value
# provided.
def redirect(path, *args)
status(302)
headers 'Location' => path
header 'Location' => path
throw :halt, *args
end
# Access or modify response headers. With no argument, return the
# underlying headers Hash. With a Hash argument, add or overwrite
# existing response headers with the values provided:
#
# headers 'Content-Type' => "text/html;charset=utf-8",
# 'Last-Modified' => Time.now.httpdate,
# 'X-UA-Compatible' => 'IE=edge'
#
# This method also available in singular form (#header).
def headers(header = nil)
@response.headers.merge!(header) if header
@response.headers
end
alias :header :headers
# Set the content type of the response body (HTTP 'Content-Type' header).
#
# The +type+ argument may be an internet media type (e.g., 'text/html',
# 'application/xml+atom', 'image/png') or a Symbol key into the
# Rack::File::MIME_TYPES table.
#
# Media type parameters, such as "charset", may also be specified using the
# optional hash argument:
#
# get '/foo.html' do
# content_type 'text/html', :charset => 'utf-8'
# "<h1>Hello World</h1>"
# end
#
def content_type(type, params={})
type = Rack::File::MIME_TYPES[type.to_s] if type.kind_of?(Symbol)
fail "Invalid or undefined media_type: #{type}" if type.nil?
if params.any?
params = params.collect { |kv| "%s=%s" % kv }.join(', ')
type = [ type, params ].join(";")
end
response.header['Content-Type'] = type
end
# Set the last modified time of the resource (HTTP 'Last-Modified' header)
# and halt if conditional GET matches. The +time+ argument is a Time,
# DateTime, or other object that responds to +to_time+.
#
# When the current request includes an 'If-Modified-Since' header that
# matches the time specified, execution is immediately halted with a
# '304 Not Modified' response.
#
# Calling this method before perfoming heavy processing (e.g., lengthy
# database queries, template rendering, complex logic) can dramatically
# increase overall throughput with caching clients.
def last_modified(time)
time = time.to_time if time.respond_to?(:to_time)
time = time.httpdate if time.respond_to?(:httpdate)
response.header['Last-Modified'] = time
throw :halt, 304 if time == request.env['HTTP_IF_MODIFIED_SINCE']
time
end
# Set the response entity tag (HTTP 'ETag' header) and halt if conditional
# GET matches. The +value+ argument is an identifier that uniquely
# identifies the current version of the resource. The +strength+ argument
# indicates whether the etag should be used as a :strong (default) or :weak
# cache validator.
#
# When the current request includes an 'If-None-Match' header with a
# matching etag, execution is immediately halted. If the request method is
# GET or HEAD, a '304 Not Modified' response is sent. For all other request
# methods, a '412 Precondition Failed' response is sent.
#
# Calling this method before perfoming heavy processing (e.g., lengthy
# database queries, template rendering, complex logic) can dramatically
# increase overall throughput with caching clients.
#
# === See Also
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19[RFC2616: ETag],
# ResponseHelpers#last_modified
def entity_tag(value, strength=:strong)
value =
case strength
when :strong then '"%s"' % value
when :weak then 'W/"%s"' % value
else raise TypeError, "strength must be one of :strong or :weak"
end
response.header['ETag'] = value
# Check for If-None-Match request header and halt if match is found.
etags = (request.env['HTTP_IF_NONE_MATCH'] || '').split(/\s*,\s*/)
if etags.include?(value) || etags.include?('*')
# GET/HEAD requests: send Not Modified response
throw :halt, 304 if request.get? || request.head?
# Other requests: send Precondition Failed response
throw :halt, 412
end
end
alias :etag :entity_tag
end
module RenderingHelpers
def render(renderer, template, options={})
@ -473,7 +614,8 @@ module Sinatra
private
def render_haml(content, options = {}, &b)
::Haml::Engine.new(content).render(options[:scope] || self, options[:locals] || {}, &b)
haml_options = (options[:options] || {}).merge(Sinatra.options.haml || {})
::Haml::Engine.new(content, haml_options).render(options[:scope] || self, options[:locals] || {}, &b)
end
end
@ -678,7 +820,7 @@ module Sinatra
end
def session
@request.env['rack.session'] || {}
request.env['rack.session'] ||= {}
end
private
@ -697,15 +839,16 @@ module Sinatra
attr_writer :options
def self.default_options
root = File.expand_path(File.dirname($0))
@@default_options ||= {
:run => true,
:port => 4567,
:env => :development,
:root => Dir.pwd,
:views => Dir.pwd + '/views',
:public => Dir.pwd + '/public',
:root => root,
:views => root + '/views',
:public => root + '/public',
:sessions => false,
:logging => true,
:logging => true
}
end
@ -722,7 +865,8 @@ module Sinatra
OptionParser.new do |op|
op.on('-p port') { |port| default_options[:port] = port }
op.on('-e env') { |env| default_options[:env] = env }
op.on('-x') { |env| default_options[:mutex] = true }
op.on('-x') { default_options[:mutex] = true }
op.on('-s server') { |server| default_options[:server] = server }
end.parse!(ARGV.dup.select { |o| o !~ /--name/ })
end
@ -993,7 +1137,7 @@ def use_in_file_templates!
data = StringIO.new(templates)
current_template = nil
data.each do |line|
if line =~ /^##\s?(.*)/
if line =~ /^@@\s?(.*)/
current_template = $1.to_sym
Sinatra.application.templates[current_template] = ''
elsif current_template
@ -1040,14 +1184,16 @@ class String
# Converts +self+ to an escaped URI parameter value
# 'Foo Bar'.to_param # => 'Foo%20Bar'
def to_param
URI.escape(self)
Rack::Utils.escape(self)
end
alias :http_escape :to_param
# Converts +self+ from an escaped URI parameter value
# 'Foo%20Bar'.from_param # => 'Foo Bar'
def from_param
URI.unescape(self)
Rack::Utils.unescape(self)
end
alias :http_unescape :from_param
end

View File

@ -1,13 +1,3 @@
class Rack::MockRequest
class << self
alias :env_for_without_env :env_for
def env_for(uri = "", opts = {})
env = { 'HTTP_USER_AGENT' => opts.delete(:agent) }
env_for_without_env(uri, opts).merge(env)
end
end
end
module Sinatra
module Test
@ -16,12 +6,19 @@ module Sinatra
def easy_env_map
{
:accept => 'HTTP_ACCEPT',
:agent => 'HTTP_AGENT',
:host => 'HTTP_POST'
:accept => "HTTP_ACCEPT",
:agent => "HTTP_USER_AGENT",
:host => "HTTP_HOST",
:session => "HTTP_COOKIE",
:cookies => "HTTP_COOKIE"
}
end
def session(data, key = 'rack.session')
data = data.from_params if data.respond_to?(:from_params)
"#{Rack::Utils.escape(key)}=#{[Marshal.dump(data)].pack("m*")}"
end
def map_easys(params)
easy_env_map.inject(params.dup) do |m, (from, to)|
m[to] = m.delete(from) if m.has_key?(from); m
@ -30,14 +27,14 @@ module Sinatra
%w(get head post put delete).each do |m|
define_method("#{m}_it") do |path, *args|
request = Rack::MockRequest.new(Sinatra.build_application)
env, input = if args.size == 2
[args.last, args.first]
elsif args.size == 1
data = args.first
data.is_a?(Hash) ? [data.delete(:env), data.to_params] : [nil, data]
data.is_a?(Hash) ? [map_easys(data.delete(:env) || {}), data.to_params] : [nil, data]
end
@response = request.request(m.upcase, path, {:input => input}.merge(env || {}))
@request = Rack::MockRequest.new(Sinatra.build_application)
@response = @request.request(m.upcase, path, {:input => input}.merge(env || {}))
end
end
@ -46,7 +43,7 @@ module Sinatra
end
def method_missing(name, *args)
@response.send(name, *args)
@response.send(name, *args) rescue super
end
end

View File

@ -26,7 +26,62 @@ context "Sinatra" do
should.be.ok
body.should.equal 'Hello Blake'
end
specify "handles splats" do
get '/hi/*' do
params["splat"].kind_of?(Array).should.equal true
params["splat"].first
end
get_it '/hi/Blake'
should.be.ok
body.should.equal 'Blake'
end
specify "handles multiple splats" do
get '/say/*/to/*' do
params["splat"].join(' ')
end
get_it '/say/hello/to/world'
should.be.ok
body.should.equal 'hello world'
end
specify "allow empty splats" do
get '/say/*/to*/*' do
params["splat"].join(' ')
end
get_it '/say/hello/to/world'
should.be.ok
body.should.equal 'hello world' # second splat is empty
get_it '/say/hello/tomy/world'
should.be.ok
body.should.equal 'hello my world'
end
specify "gives access to underlying response header Hash" do
get '/' do
header['X-Test'] = 'Is this thing on?'
headers 'X-Test2' => 'Foo', 'X-Test3' => 'Bar'
''
end
get_it '/'
should.be.ok
headers.should.include 'X-Test'
headers['X-Test'].should.equal 'Is this thing on?'
headers.should.include 'X-Test3'
headers['X-Test3'].should.equal 'Bar'
end
specify "follows redirects" do
get '/' do
redirect '/blake'
@ -55,7 +110,18 @@ context "Sinatra" do
headers['Location'].should.equal 'foo'
body.should.equal 'blah'
end
specify "redirects permanently with 301 status code" do
get "/" do
redirect 'foo', 301
end
get_it '/'
should.be.redirection
headers['Location'].should.equal 'foo'
status.should.equal 301
body.should.be.empty
end
specify "body sets content and ends event" do
Sinatra::EventContext.any_instance.expects(:foo).never
@ -87,6 +153,70 @@ context "Sinatra" do
end
specify "should easily set response Content-Type" do
get '/foo.html' do
content_type 'text/html', :charset => 'utf-8'
"<h1>Hello, World</h1>"
end
get_it '/foo.html'
should.be.ok
headers['Content-Type'].should.equal 'text/html;charset=utf-8'
body.should.equal '<h1>Hello, World</h1>'
get '/foo_test.xml' do
content_type :xml
"<feed></feed>"
end
get_it '/foo_test.xml'
should.be.ok
headers['Content-Type'].should.equal 'application/xml'
body.should.equal '<feed></feed>'
end
specify "supports conditional GETs with last_modified" do
modified_at = Time.now
get '/maybe' do
last_modified modified_at
'response body, maybe'
end
get_it '/maybe'
should.be.ok
body.should.equal 'response body, maybe'
get_it '/maybe', :env => { 'HTTP_IF_MODIFIED_SINCE' => modified_at.httpdate }
status.should.equal 304
body.should.equal ''
end
specify "supports conditional GETs with entity_tag" do
get '/strong' do
entity_tag 'FOO'
'foo response'
end
get_it '/strong'
should.be.ok
body.should.equal 'foo response'
get_it '/strong', {},
'HTTP_IF_NONE_MATCH' => '"BAR"'
should.be.ok
body.should.equal 'foo response'
get_it '/strong', {},
'HTTP_IF_NONE_MATCH' => '"FOO"'
status.should.equal 304
body.should.equal ''
get_it '/strong', {},
'HTTP_IF_NONE_MATCH' => '"BAR", *'
status.should.equal 304
body.should.equal ''
end
specify "delegates HEAD requests to GET handlers" do
get '/invisible' do
"I am invisible to the world"

View File

@ -135,17 +135,17 @@ context "Events in an app" do
get '/', :agent => /Windows/ do
request.env['HTTP_USER_AGENT']
end
get_it '/', :env => { :agent => 'Windows' }
should.be.ok
body.should.equal 'Windows'
get_it '/', :agent => 'Mac'
get_it '/', :env => { :agent => 'Mac' }
should.not.be.ok
end
specify "can filters by agent" do
specify "can use regex to get parts of user-agent" do
get '/', :agent => /Windows (NT)/ do
params[:agent].first

View File

@ -178,4 +178,56 @@ context "Haml" do
end
describe 'Options passed to the HAML interpreter' do
setup do
Sinatra.application = nil
end
specify 'are empty be default' do
get '/' do
haml 'foo'
end
Haml::Engine.expects(:new).with('foo', {}).returns(stub(:render => 'foo'))
get_it '/'
should.be.ok
end
specify 'can be configured by passing :options to haml' do
get '/' do
haml 'foo', :options => {:format => :html4}
end
Haml::Engine.expects(:new).with('foo', {:format => :html4}).returns(stub(:render => 'foo'))
get_it '/'
should.be.ok
end
specify 'can be configured using set_option :haml' do
configure do
set_option :haml, :format => :html4,
:escape_html => true
end
get '/' do
haml 'foo'
end
Haml::Engine.expects(:new).with('foo', {:format => :html4,
:escape_html => true}).returns(stub(:render => 'foo'))
get_it '/'
should.be.ok
end
end
end

View File

@ -77,6 +77,12 @@ context "Static files (by default)" do
headers['Content-Transfer-Encoding'].should.be.nil
end
specify "should be served even if their path is url escaped" do
get_it('/fo%6f.xml')
should.be.ok
body.should.equal "<foo></foo>\n"
end
end
context "SendData" do

View File

@ -38,10 +38,10 @@ end
__END__
## foo
@@ foo
this is foo
## layout
@@ layout
X
= yield
X

1
vendor/rack vendored Submodule

@ -0,0 +1 @@
Subproject commit 67a7507fb07c9d49bf4be4fc6c4fc4578f1673ac