
After go1.16, go will use module mode by default, even when the repository is checked out under GOPATH or in a one-off directory. Add go.mod, go.sum to keep this repo buildable without opting out of the module mode. > go mod init github.com/mmcgrana/gobyexample > go mod tidy > go mod vendor In module mode, the 'vendor' directory is special and its contents will be actively maintained by the go command. pygments aren't the dependency the go will know about, so it will delete the contents from vendor directory. Move it to `third_party` directory now. And, vendor the blackfriday package. Note: the tutorial contents are not affected by the change in go1.16 because all the examples in this tutorial ask users to run the go command with the explicit list of files to be compiled (e.g. `go run hello-world.go` or `go build command-line-arguments.go`). When the source list is provided, the go command does not have to compute the build list and whether it's running in GOPATH mode or module mode becomes irrelevant.
1853 lines
51 KiB
Ruby
1853 lines
51 KiB
Ruby
module CodeRay
|
|
module Scanners
|
|
|
|
class Ruby < Scanner
|
|
|
|
RESERVED_WORDS = [
|
|
'and', 'def', 'end', 'in', 'or', 'unless', 'begin',
|
|
'defined?', 'ensure', 'module', 'redo', 'super', 'until',
|
|
'BEGIN', 'break', 'do', 'next', 'rescue', 'then',
|
|
'when', 'END', 'case', 'else', 'for', 'retry',
|
|
'while', 'alias', 'class', 'elsif', 'if', 'not', 'return',
|
|
'undef', 'yield',
|
|
]
|
|
|
|
DEF_KEYWORDS = ['def']
|
|
MODULE_KEYWORDS = ['class', 'module']
|
|
DEF_NEW_STATE = WordList.new(:initial).
|
|
add(DEF_KEYWORDS, :def_expected).
|
|
add(MODULE_KEYWORDS, :module_expected)
|
|
|
|
WORDS_ALLOWING_REGEXP = [
|
|
'and', 'or', 'not', 'while', 'until', 'unless', 'if', 'elsif', 'when'
|
|
]
|
|
REGEXP_ALLOWED = WordList.new(false).
|
|
add(WORDS_ALLOWING_REGEXP, :set)
|
|
|
|
PREDEFINED_CONSTANTS = [
|
|
'nil', 'true', 'false', 'self',
|
|
'DATA', 'ARGV', 'ARGF', '__FILE__', '__LINE__',
|
|
]
|
|
|
|
IDENT_KIND = WordList.new(:ident).
|
|
add(RESERVED_WORDS, :reserved).
|
|
add(PREDEFINED_CONSTANTS, :pre_constant)
|
|
|
|
METHOD_NAME = / #{IDENT} [?!]? /xo
|
|
METHOD_NAME_EX = /
|
|
#{METHOD_NAME} # common methods: split, foo=, empty?, gsub!
|
|
| \*\*? # multiplication and power
|
|
| [-+~]@? # plus, minus
|
|
| [\/%&|^`] # division, modulo or format strings, &and, |or, ^xor, `system`
|
|
| \[\]=? # array getter and setter
|
|
| <=?>? | >=? # comparison, rocket operator
|
|
| << | >> # append or shift left, shift right
|
|
| ===? # simple equality and case equality
|
|
/ox
|
|
GLOBAL_VARIABLE = / \$ (?: #{IDENT} | \d+ | [~&+`'=\/,;_.<>!@0$?*":F\\] | -[a-zA-Z_0-9] ) /ox
|
|
|
|
DOUBLEQ = / " [^"\#\\]* (?: (?: \#\{.*?\} | \#(?:$")? | \\. ) [^"\#\\]* )* "? /ox
|
|
SINGLEQ = / ' [^'\\]* (?: \\. [^'\\]* )* '? /ox
|
|
STRING = / #{SINGLEQ} | #{DOUBLEQ} /ox
|
|
SHELL = / ` [^`\#\\]* (?: (?: \#\{.*?\} | \#(?:$`)? | \\. ) [^`\#\\]* )* `? /ox
|
|
REGEXP = / \/ [^\/\#\\]* (?: (?: \#\{.*?\} | \#(?:$\/)? | \\. ) [^\/\#\\]* )* \/? /ox
|
|
|
|
DECIMAL = /\d+(?:_\d+)*/ # doesn't recognize 09 as octal error
|
|
OCTAL = /0_?[0-7]+(?:_[0-7]+)*/
|
|
HEXADECIMAL = /0x[0-9A-Fa-f]+(?:_[0-9A-Fa-f]+)*/
|
|
BINARY = /0b[01]+(?:_[01]+)*/
|
|
|
|
EXPONENT = / [eE] [+-]? #{DECIMAL} /ox
|
|
FLOAT = / #{DECIMAL} (?: #{EXPONENT} | \. #{DECIMAL} #{EXPONENT}? ) /
|
|
INTEGER = /#{OCTAL}|#{HEXADECIMAL}|#{BINARY}|#{DECIMAL}/
|
|
|
|
def reset
|
|
super
|
|
@regexp_allowed = false
|
|
end
|
|
|
|
def next_token
|
|
return if @scanner.eos?
|
|
|
|
kind = :error
|
|
if @scanner.scan(/\s+/) # in every state
|
|
kind = :space
|
|
@regexp_allowed = :set if @regexp_allowed or @scanner.matched.index(?\n) # delayed flag setting
|
|
|
|
elsif @state == :def_expected
|
|
if @scanner.scan(/ (?: (?:#{IDENT}(?:\.|::))* | (?:@@?|$)? #{IDENT}(?:\.|::) ) #{METHOD_NAME_EX} /ox)
|
|
kind = :method
|
|
@state = :initial
|
|
else
|
|
@scanner.getch
|
|
end
|
|
@state = :initial
|
|
|
|
elsif @state == :module_expected
|
|
if @scanner.scan(/<</)
|
|
kind = :operator
|
|
else
|
|
if @scanner.scan(/ (?: #{IDENT} (?:\.|::))* #{IDENT} /ox)
|
|
kind = :method
|
|
else
|
|
@scanner.getch
|
|
end
|
|
@state = :initial
|
|
end
|
|
|
|
elsif # state == :initial
|
|
# IDENTIFIERS, KEYWORDS
|
|
if @scanner.scan(GLOBAL_VARIABLE)
|
|
kind = :global_variable
|
|
elsif @scanner.scan(/ @@ #{IDENT} /ox)
|
|
kind = :class_variable
|
|
elsif @scanner.scan(/ @ #{IDENT} /ox)
|
|
kind = :instance_variable
|
|
elsif @scanner.scan(/ __END__\n ( (?!\#CODE\#) .* )? | \#[^\n]* | =begin(?=\s).*? \n=end(?=\s|\z)(?:[^\n]*)? /mx)
|
|
kind = :comment
|
|
elsif @scanner.scan(METHOD_NAME)
|
|
if @last_token_dot
|
|
kind = :ident
|
|
else
|
|
matched = @scanner.matched
|
|
kind = IDENT_KIND[matched]
|
|
if kind == :ident and matched =~ /^[A-Z]/
|
|
kind = :constant
|
|
elsif kind == :reserved
|
|
@state = DEF_NEW_STATE[matched]
|
|
@regexp_allowed = REGEXP_ALLOWED[matched]
|
|
end
|
|
end
|
|
|
|
elsif @scanner.scan(STRING)
|
|
kind = :string
|
|
elsif @scanner.scan(SHELL)
|
|
kind = :shell
|
|
elsif @scanner.scan(/<<
|
|
(?:
|
|
([a-zA-Z_0-9]+)
|
|
(?: .*? ^\1$ | .* )
|
|
|
|
|
-([a-zA-Z_0-9]+)
|
|
(?: .*? ^\s*\2$ | .* )
|
|
|
|
|
(["\'`]) (.+?) \3
|
|
(?: .*? ^\4$ | .* )
|
|
|
|
|
- (["\'`]) (.+?) \5
|
|
(?: .*? ^\s*\6$ | .* )
|
|
)
|
|
/mxo)
|
|
kind = :string
|
|
elsif @scanner.scan(/\//) and @regexp_allowed
|
|
@scanner.unscan
|
|
@scanner.scan(REGEXP)
|
|
kind = :regexp
|
|
/%(?:[Qqxrw](?:\([^)#\\\\]*(?:(?:#\{.*?\}|#|\\\\.)[^)#\\\\]*)*\)?|\[[^\]#\\\\]*(?:(?:#\{.*?\}|#|\\\\.)[^\]#\\\\]*)*\]?|\{[^}#\\\\]*(?:(?:#\{.*?\}|#|\\\\.)[^}#\\\\]*)*\}?|<[^>#\\\\]*(?:(?:#\{.*?\}|#|\\\\.)[^>#\\\\]*)*>?|([^a-zA-Z\\\\])(?:(?!\1)[^#\\\\])*(?:(?:#\{.*?\}|#|\\\\.)(?:(?!\1)[^#\\\\])*)*\1?)|\([^)#\\\\]*(?:(?:#\{.*?\}|#|\\\\.)[^)#\\\\]*)*\)?|\[[^\]#\\\\]*(?:(?:#\{.*?\}|#|\\\\.)[^\]#\\\\]*)*\]?|\{[^}#\\\\]*(?:(?:#\{.*?\}|#|\\\\.)[^}#\\\\]*)*\}?|<[^>#\\\\]*(?:(?:#\{.*?\}|#|\\\\.)[^>#\\\\]*)*>?|([^a-zA-Z\s\\\\])(?:(?!\2)[^#\\\\])*(?:(?:#\{.*?\}|#|\\\\.)(?:(?!\2)[^#\\\\])*)*\2?|\\\\[^#\\\\]*(?:(?:#\{.*?\}|#)[^#\\\\]*)*\\\\?)/
|
|
elsif @scanner.scan(/:(?:#{GLOBAL_VARIABLE}|#{METHOD_NAME_EX}|#{STRING})/ox)
|
|
kind = :symbol
|
|
elsif @scanner.scan(/
|
|
\? (?:
|
|
[^\s\\]
|
|
|
|
|
\\ (?:M-\\C-|C-\\M-|M-\\c|c\\M-|c|C-|M-))? (?: \\ (?: . | [0-7]{3} | x[0-9A-Fa-f][0-9A-Fa-f] )
|
|
)
|
|
/mox)
|
|
kind = :integer
|
|
|
|
elsif @scanner.scan(/ [-+*\/%=<>;,|&!()\[\]{}~?] | \.\.?\.? | ::? /x)
|
|
kind = :operator
|
|
@regexp_allowed = :set if @scanner.matched[-1,1] =~ /[~=!<>|&^,\(\[+\-\/\*%]\z/
|
|
elsif @scanner.scan(FLOAT)
|
|
kind = :float
|
|
elsif @scanner.scan(INTEGER)
|
|
kind = :integer
|
|
else
|
|
@scanner.getch
|
|
end
|
|
end
|
|
|
|
token = Token.new @scanner.matched, kind
|
|
|
|
if kind == :regexp
|
|
token.text << @scanner.scan(/[eimnosux]*/)
|
|
end
|
|
|
|
@regexp_allowed = (@regexp_allowed == :set) # delayed flag setting
|
|
|
|
token
|
|
end
|
|
end
|
|
|
|
register Ruby, 'ruby', 'rb'
|
|
|
|
end
|
|
end
|
|
class Set
|
|
include Enumerable
|
|
|
|
# Creates a new set containing the given objects.
|
|
def self.[](*ary)
|
|
new(ary)
|
|
end
|
|
|
|
# Creates a new set containing the elements of the given enumerable
|
|
# object.
|
|
#
|
|
# If a block is given, the elements of enum are preprocessed by the
|
|
# given block.
|
|
def initialize(enum = nil, &block) # :yields: o
|
|
@hash ||= Hash.new
|
|
|
|
enum.nil? and return
|
|
|
|
if block
|
|
enum.each { |o| add(block[o]) }
|
|
else
|
|
merge(enum)
|
|
end
|
|
end
|
|
|
|
# Copy internal hash.
|
|
def initialize_copy(orig)
|
|
@hash = orig.instance_eval{@hash}.dup
|
|
end
|
|
|
|
# Returns the number of elements.
|
|
def size
|
|
@hash.size
|
|
end
|
|
alias length size
|
|
|
|
# Returns true if the set contains no elements.
|
|
def empty?
|
|
@hash.empty?
|
|
end
|
|
|
|
# Removes all elements and returns self.
|
|
def clear
|
|
@hash.clear
|
|
self
|
|
end
|
|
|
|
# Replaces the contents of the set with the contents of the given
|
|
# enumerable object and returns self.
|
|
def replace(enum)
|
|
if enum.class == self.class
|
|
@hash.replace(enum.instance_eval { @hash })
|
|
else
|
|
enum.is_a?(Enumerable) or raise ArgumentError, "value must be enumerable"
|
|
clear
|
|
enum.each { |o| add(o) }
|
|
end
|
|
|
|
self
|
|
end
|
|
|
|
# Converts the set to an array. The order of elements is uncertain.
|
|
def to_a
|
|
@hash.keys
|
|
end
|
|
|
|
def flatten_merge(set, seen = Set.new)
|
|
set.each { |e|
|
|
if e.is_a?(Set)
|
|
if seen.include?(e_id = e.object_id)
|
|
raise ArgumentError, "tried to flatten recursive Set"
|
|
end
|
|
|
|
seen.add(e_id)
|
|
flatten_merge(e, seen)
|
|
seen.delete(e_id)
|
|
else
|
|
add(e)
|
|
end
|
|
}
|
|
|
|
self
|
|
end
|
|
protected :flatten_merge
|
|
|
|
# Returns a new set that is a copy of the set, flattening each
|
|
# containing set recursively.
|
|
def flatten
|
|
self.class.new.flatten_merge(self)
|
|
end
|
|
|
|
# Equivalent to Set#flatten, but replaces the receiver with the
|
|
# result in place. Returns nil if no modifications were made.
|
|
def flatten!
|
|
if detect { |e| e.is_a?(Set) }
|
|
replace(flatten())
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Returns true if the set contains the given object.
|
|
def include?(o)
|
|
@hash.include?(o)
|
|
end
|
|
alias member? include?
|
|
|
|
# Returns true if the set is a superset of the given set.
|
|
def superset?(set)
|
|
set.is_a?(Set) or raise ArgumentError, "value must be a set"
|
|
return false if size < set.size
|
|
set.all? { |o| include?(o) }
|
|
end
|
|
|
|
# Returns true if the set is a proper superset of the given set.
|
|
def proper_superset?(set)
|
|
set.is_a?(Set) or raise ArgumentError, "value must be a set"
|
|
return false if size <= set.size
|
|
set.all? { |o| include?(o) }
|
|
end
|
|
|
|
# Returns true if the set is a subset of the given set.
|
|
def subset?(set)
|
|
set.is_a?(Set) or raise ArgumentError, "value must be a set"
|
|
return false if set.size < size
|
|
all? { |o| set.include?(o) }
|
|
end
|
|
|
|
# Returns true if the set is a proper subset of the given set.
|
|
def proper_subset?(set)
|
|
set.is_a?(Set) or raise ArgumentError, "value must be a set"
|
|
return false if set.size <= size
|
|
all? { |o| set.include?(o) }
|
|
end
|
|
|
|
# Calls the given block once for each element in the set, passing
|
|
# the element as parameter.
|
|
def each
|
|
@hash.each_key { |o| yield(o) }
|
|
self
|
|
end
|
|
|
|
# Adds the given object to the set and returns self. Use +merge+ to
|
|
# add several elements at once.
|
|
def add(o)
|
|
@hash[o] = true
|
|
self
|
|
end
|
|
alias << add
|
|
|
|
# Adds the given object to the set and returns self. If the
|
|
# object is already in the set, returns nil.
|
|
def add?(o)
|
|
if include?(o)
|
|
nil
|
|
else
|
|
add(o)
|
|
end
|
|
end
|
|
|
|
# Deletes the given object from the set and returns self. Use +subtract+ to
|
|
# delete several items at once.
|
|
def delete(o)
|
|
@hash.delete(o)
|
|
self
|
|
end
|
|
|
|
# Deletes the given object from the set and returns self. If the
|
|
# object is not in the set, returns nil.
|
|
def delete?(o)
|
|
if include?(o)
|
|
delete(o)
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Deletes every element of the set for which block evaluates to
|
|
# true, and returns self.
|
|
def delete_if
|
|
@hash.delete_if { |o,| yield(o) }
|
|
self
|
|
end
|
|
|
|
# Do collect() destructively.
|
|
def collect!
|
|
set = self.class.new
|
|
each { |o| set << yield(o) }
|
|
replace(set)
|
|
end
|
|
alias map! collect!
|
|
|
|
# Equivalent to Set#delete_if, but returns nil if no changes were
|
|
# made.
|
|
def reject!
|
|
n = size
|
|
delete_if { |o| yield(o) }
|
|
size == n ? nil : self
|
|
end
|
|
|
|
# Merges the elements of the given enumerable object to the set and
|
|
# returns self.
|
|
def merge(enum)
|
|
if enum.is_a?(Set)
|
|
@hash.update(enum.instance_eval { @hash })
|
|
else
|
|
enum.is_a?(Enumerable) or raise ArgumentError, "value must be enumerable"
|
|
enum.each { |o| add(o) }
|
|
end
|
|
|
|
self
|
|
end
|
|
|
|
# Deletes every element that appears in the given enumerable object
|
|
# and returns self.
|
|
def subtract(enum)
|
|
enum.is_a?(Enumerable) or raise ArgumentError, "value must be enumerable"
|
|
enum.each { |o| delete(o) }
|
|
self
|
|
end
|
|
|
|
# Returns a new set built by merging the set and the elements of the
|
|
# given enumerable object.
|
|
def |(enum)
|
|
enum.is_a?(Enumerable) or raise ArgumentError, "value must be enumerable"
|
|
dup.merge(enum)
|
|
end
|
|
alias + | ##
|
|
alias union | ##
|
|
|
|
# Returns a new set built by duplicating the set, removing every
|
|
# element that appears in the given enumerable object.
|
|
def -(enum)
|
|
enum.is_a?(Enumerable) or raise ArgumentError, "value must be enumerable"
|
|
dup.subtract(enum)
|
|
end
|
|
alias difference - ##
|
|
|
|
# Returns a new array containing elements common to the set and the
|
|
# given enumerable object.
|
|
def &(enum)
|
|
enum.is_a?(Enumerable) or raise ArgumentError, "value must be enumerable"
|
|
n = self.class.new
|
|
enum.each { |o| n.add(o) if include?(o) }
|
|
n
|
|
end
|
|
alias intersection & ##
|
|
|
|
# Returns a new array containing elements exclusive between the set
|
|
# and the given enumerable object. (set ^ enum) is equivalent to
|
|
# ((set | enum) - (set & enum)).
|
|
def ^(enum)
|
|
enum.is_a?(Enumerable) or raise ArgumentError, "value must be enumerable"
|
|
n = dup
|
|
enum.each { |o| if n.include?(o) then n.delete(o) else n.add(o) end }
|
|
n
|
|
end
|
|
|
|
# Returns true if two sets are equal. The equality of each couple
|
|
# of elements is defined according to Object#eql?.
|
|
def ==(set)
|
|
equal?(set) and return true
|
|
|
|
set.is_a?(Set) && size == set.size or return false
|
|
|
|
hash = @hash.dup
|
|
set.all? { |o| hash.include?(o) }
|
|
end
|
|
|
|
def hash # :nodoc:
|
|
@hash.hash
|
|
end
|
|
|
|
def eql?(o) # :nodoc:
|
|
return false unless o.is_a?(Set)
|
|
@hash.eql?(o.instance_eval{@hash})
|
|
end
|
|
|
|
# Classifies the set by the return value of the given block and
|
|
# returns a hash of {value => set of elements} pairs. The block is
|
|
# called once for each element of the set, passing the element as
|
|
# parameter.
|
|
#
|
|
# e.g.:
|
|
#
|
|
# require 'set'
|
|
# files = Set.new(Dir.glob("*.rb"))
|
|
# hash = files.classify { |f| File.mtime(f).year }
|
|
# p hash # => {2000=>#<Set: {"a.rb", "b.rb"}>,
|
|
# # 2001=>#<Set: {"c.rb", "d.rb", "e.rb"}>,
|
|
# # 2002=>#<Set: {"f.rb"}>}
|
|
def classify # :yields: o
|
|
h = {}
|
|
|
|
each { |i|
|
|
x = yield(i)
|
|
(h[x] ||= self.class.new).add(i)
|
|
}
|
|
|
|
h
|
|
end
|
|
|
|
# Divides the set into a set of subsets according to the commonality
|
|
# defined by the given block.
|
|
#
|
|
# If the arity of the block is 2, elements o1 and o2 are in common
|
|
# if block.call(o1, o2) is true. Otherwise, elements o1 and o2 are
|
|
# in common if block.call(o1) == block.call(o2).
|
|
#
|
|
# e.g.:
|
|
#
|
|
# require 'set'
|
|
# numbers = Set[1, 3, 4, 6, 9, 10, 11]
|
|
# set = numbers.divide { |i,j| (i - j).abs == 1 }
|
|
# p set # => #<Set: {#<Set: {1}>,
|
|
# # #<Set: {11, 9, 10}>,
|
|
# # #<Set: {3, 4}>,
|
|
# # #<Set: {6}>}>
|
|
def divide(&func)
|
|
if func.arity == 2
|
|
require 'tsort'
|
|
|
|
class << dig = {} # :nodoc:
|
|
include TSort
|
|
|
|
alias tsort_each_node each_key
|
|
def tsort_each_child(node, &block)
|
|
fetch(node).each(&block)
|
|
end
|
|
end
|
|
|
|
each { |u|
|
|
dig[u] = a = []
|
|
each{ |v| func.call(u, v) and a << v }
|
|
}
|
|
|
|
set = Set.new()
|
|
dig.each_strongly_connected_component { |css|
|
|
set.add(self.class.new(css))
|
|
}
|
|
set
|
|
else
|
|
Set.new(classify(&func).values)
|
|
end
|
|
end
|
|
|
|
InspectKey = :__inspect_key__ # :nodoc:
|
|
|
|
# Returns a string containing a human-readable representation of the
|
|
# set. ("#<Set: {element1, element2, ...}>")
|
|
def inspect
|
|
ids = (Thread.current[InspectKey] ||= [])
|
|
|
|
if ids.include?(object_id)
|
|
return sprintf('#<%s: {...}>', self.class.name)
|
|
end
|
|
|
|
begin
|
|
ids << object_id
|
|
return sprintf('#<%s: {%s}>', self.class, to_a.inspect[1..-2])
|
|
ensure
|
|
ids.pop
|
|
end
|
|
end
|
|
|
|
def pretty_print(pp) # :nodoc:
|
|
pp.text sprintf('#<%s: {', self.class.name)
|
|
pp.nest(1) {
|
|
pp.seplist(self) { |o|
|
|
pp.pp o
|
|
}
|
|
}
|
|
pp.text "}>"
|
|
end
|
|
|
|
def pretty_print_cycle(pp) # :nodoc:
|
|
pp.text sprintf('#<%s: {%s}>', self.class.name, empty? ? '' : '...')
|
|
end
|
|
end
|
|
|
|
# SortedSet implements a set which elements are sorted in order. See Set.
|
|
class SortedSet < Set
|
|
@@setup = false
|
|
|
|
class << self
|
|
def [](*ary) # :nodoc:
|
|
new(ary)
|
|
end
|
|
|
|
def setup # :nodoc:
|
|
@@setup and return
|
|
|
|
begin
|
|
require 'rbtree'
|
|
|
|
module_eval %{
|
|
def initialize(*args, &block)
|
|
@hash = RBTree.new
|
|
super
|
|
end
|
|
}
|
|
rescue LoadError
|
|
module_eval %{
|
|
def initialize(*args, &block)
|
|
@keys = nil
|
|
super
|
|
end
|
|
|
|
def clear
|
|
@keys = nil
|
|
super
|
|
end
|
|
|
|
def replace(enum)
|
|
@keys = nil
|
|
super
|
|
end
|
|
|
|
def add(o)
|
|
@keys = nil
|
|
@hash[o] = true
|
|
self
|
|
end
|
|
alias << add
|
|
|
|
def delete(o)
|
|
@keys = nil
|
|
@hash.delete(o)
|
|
self
|
|
end
|
|
|
|
def delete_if
|
|
n = @hash.size
|
|
@hash.delete_if { |o,| yield(o) }
|
|
@keys = nil if @hash.size != n
|
|
self
|
|
end
|
|
|
|
def merge(enum)
|
|
@keys = nil
|
|
super
|
|
end
|
|
|
|
def each
|
|
to_a.each { |o| yield(o) }
|
|
end
|
|
|
|
def to_a
|
|
(@keys = @hash.keys).sort! unless @keys
|
|
@keys
|
|
end
|
|
}
|
|
end
|
|
|
|
@@setup = true
|
|
end
|
|
end
|
|
|
|
def initialize(*args, &block) # :nodoc:
|
|
SortedSet.setup
|
|
initialize(*args, &block)
|
|
end
|
|
end
|
|
|
|
module Enumerable
|
|
# Makes a set from the enumerable object with given arguments.
|
|
def to_set(klass = Set, *args, &block)
|
|
klass.new(self, *args, &block)
|
|
end
|
|
end
|
|
|
|
# =begin
|
|
# == RestricedSet class
|
|
# RestricedSet implements a set with restrictions defined by a given
|
|
# block.
|
|
#
|
|
# === Super class
|
|
# Set
|
|
#
|
|
# === Class Methods
|
|
# --- RestricedSet::new(enum = nil) { |o| ... }
|
|
# --- RestricedSet::new(enum = nil) { |rset, o| ... }
|
|
# Creates a new restricted set containing the elements of the given
|
|
# enumerable object. Restrictions are defined by the given block.
|
|
#
|
|
# If the block's arity is 2, it is called with the RestrictedSet
|
|
# itself and an object to see if the object is allowed to be put in
|
|
# the set.
|
|
#
|
|
# Otherwise, the block is called with an object to see if the object
|
|
# is allowed to be put in the set.
|
|
#
|
|
# === Instance Methods
|
|
# --- restriction_proc
|
|
# Returns the restriction procedure of the set.
|
|
#
|
|
# =end
|
|
#
|
|
# class RestricedSet < Set
|
|
# def initialize(*args, &block)
|
|
# @proc = block or raise ArgumentError, "missing a block"
|
|
#
|
|
# if @proc.arity == 2
|
|
# instance_eval %{
|
|
# def add(o)
|
|
# @hash[o] = true if @proc.call(self, o)
|
|
# self
|
|
# end
|
|
# alias << add
|
|
#
|
|
# def add?(o)
|
|
# if include?(o) || !@proc.call(self, o)
|
|
# nil
|
|
# else
|
|
# @hash[o] = true
|
|
# self
|
|
# end
|
|
# end
|
|
#
|
|
# def replace(enum)
|
|
# enum.is_a?(Enumerable) or raise ArgumentError, "value must be enumerable"
|
|
# clear
|
|
# enum.each { |o| add(o) }
|
|
#
|
|
# self
|
|
# end
|
|
#
|
|
# def merge(enum)
|
|
# enum.is_a?(Enumerable) or raise ArgumentError, "value must be enumerable"
|
|
# enum.each { |o| add(o) }
|
|
#
|
|
# self
|
|
# end
|
|
# }
|
|
# else
|
|
# instance_eval %{
|
|
# def add(o)
|
|
# if @proc.call(o)
|
|
# @hash[o] = true
|
|
# end
|
|
# self
|
|
# end
|
|
# alias << add
|
|
#
|
|
# def add?(o)
|
|
# if include?(o) || !@proc.call(o)
|
|
# nil
|
|
# else
|
|
# @hash[o] = true
|
|
# self
|
|
# end
|
|
# end
|
|
# }
|
|
# end
|
|
#
|
|
# super(*args)
|
|
# end
|
|
#
|
|
# def restriction_proc
|
|
# @proc
|
|
# end
|
|
# end
|
|
|
|
if $0 == __FILE__
|
|
eval DATA.read, nil, $0, __LINE__+4
|
|
end
|
|
|
|
# = rweb - CGI Support Library
|
|
#
|
|
# Author:: Johannes Barre (mailto:rweb@igels.net)
|
|
# Copyright:: Copyright (c) 2003, 04 by Johannes Barre
|
|
# License:: GNU Lesser General Public License (COPYING, http://www.gnu.org/copyleft/lesser.html)
|
|
# Version:: 0.1.0
|
|
# CVS-ID:: $Id: example.rb 39 2005-11-05 03:33:55Z murphy $
|
|
#
|
|
# == What is Rweb?
|
|
# Rweb is a replacement for the cgi class included in the ruby distribution.
|
|
#
|
|
# == How to use
|
|
#
|
|
# === Basics
|
|
#
|
|
# This class is made to be as easy as possible to use. An example:
|
|
#
|
|
# require "rweb"
|
|
#
|
|
# web = Rweb.new
|
|
# web.out do
|
|
# web.puts "Hello world!"
|
|
# end
|
|
#
|
|
# The visitor will get a simple "Hello World!" in his browser. Please notice,
|
|
# that won't set html-tags for you, so you should better do something like this:
|
|
#
|
|
# require "rweb"
|
|
#
|
|
# web = Rweb.new
|
|
# web.out do
|
|
# web.puts "<html><body>Hello world!</body></html>"
|
|
# end
|
|
#
|
|
# === Set headers
|
|
# Of course, it's also possible to tell the browser, that the content of this
|
|
# page is plain text instead of html code:
|
|
#
|
|
# require "rweb"
|
|
#
|
|
# web = Rweb.new
|
|
# web.out do
|
|
# web.header("content-type: text/plain")
|
|
# web.puts "Hello plain world!"
|
|
# end
|
|
#
|
|
# Please remember, headers can't be set after the page content has been send.
|
|
# You have to set all nessessary headers before the first puts oder print. It's
|
|
# possible to cache the content until everything is complete. Doing it this
|
|
# way, you can set headers everywhere.
|
|
#
|
|
# If you set a header twice, the second header will replace the first one. The
|
|
# header name is not casesensitive, it will allways converted in to the
|
|
# capitalised form suggested by the w3c (http://w3.org)
|
|
#
|
|
# === Set cookies
|
|
# Setting cookies is quite easy:
|
|
# include 'rweb'
|
|
#
|
|
# web = Rweb.new
|
|
# Cookie.new("Visits", web.cookies['visits'].to_i +1)
|
|
# web.out do
|
|
# web.puts "Welcome back! You visited this page #{web.cookies['visits'].to_i +1} times"
|
|
# end
|
|
#
|
|
# See the class Cookie for more details.
|
|
#
|
|
# === Get form and cookie values
|
|
# There are four ways to submit data from the browser to the server and your
|
|
# ruby script: via GET, POST, cookies and file upload. Rweb doesn't support
|
|
# file upload by now.
|
|
#
|
|
# include 'rweb'
|
|
#
|
|
# web = Rweb.new
|
|
# web.out do
|
|
# web.print "action: #{web.get['action']} "
|
|
# web.puts "The value of the cookie 'visits' is #{web.cookies['visits']}"
|
|
# web.puts "The post parameter 'test['x']' is #{web.post['test']['x']}"
|
|
# end
|
|
|
|
RWEB_VERSION = "0.1.0"
|
|
RWEB = "rweb/#{RWEB_VERSION}"
|
|
|
|
#require 'rwebcookie' -> edit by bunny :-)
|
|
|
|
class Rweb
|
|
# All parameter submitted via the GET method are available in attribute
|
|
# get. This is Hash, where every parameter is available as a key-value
|
|
# pair.
|
|
#
|
|
# If your input tag has a name like this one, it's value will be available
|
|
# as web.get["fieldname"]
|
|
# <input name="fieldname">
|
|
# You can submit values as a Hash
|
|
# <input name="text['index']">
|
|
# <input name="text['index2']">
|
|
# will be available as
|
|
# web.get["text"]["index"]
|
|
# web.get["text"]["index2"]
|
|
# Integers are also possible
|
|
# <input name="int[2]">
|
|
# <input name="int[3]['hi']>
|
|
# will be available as
|
|
# web.get["int"][2]
|
|
# web.get["int"][3]["hi"]
|
|
# If you specify no index, the lowest unused index will be used:
|
|
# <input name="int[]"><!-- First Field -->
|
|
# <input name="int[]"><!-- Second one -->
|
|
# will be available as
|
|
# web.get["int"][0] # First Field
|
|
# web.get["int"][1] # Second one
|
|
# Please notice, this doesn'd work like you might expect:
|
|
# <input name="text[index]">
|
|
# It will not be available as web.get["text"]["index"] but
|
|
# web.get["text[index]"]
|
|
attr_reader :get
|
|
|
|
# All parameters submitted via POST are available in the attribute post. It
|
|
# works like the get attribute.
|
|
# <input name="text[0]">
|
|
# will be available as
|
|
# web.post["text"][0]
|
|
attr_reader :post
|
|
|
|
# All cookies submitted by the browser are available in cookies. This is a
|
|
# Hash, where every cookie is a key-value pair.
|
|
attr_reader :cookies
|
|
|
|
# The name of the browser identification is submitted as USER_AGENT and
|
|
# available in this attribute.
|
|
attr_reader :user_agent
|
|
|
|
# The IP address of the client.
|
|
attr_reader :remote_addr
|
|
|
|
# Creates a new Rweb object. This should only done once. You can set various
|
|
# options via the settings hash.
|
|
#
|
|
# "cache" => true: Everything you script send to the client will be cached
|
|
# until the end of the out block or until flush is called. This way, you
|
|
# can modify headers and cookies even after printing something to the client.
|
|
#
|
|
# "safe" => level: Changes the $SAFE attribute. By default, $SAFE will be set
|
|
# to 1. If $SAFE is already higher than this value, it won't be changed.
|
|
#
|
|
# "silend" => true: Normaly, Rweb adds automaticly a header like this
|
|
# "X-Powered-By: Rweb/x.x.x (Ruby/y.y.y)". With the silend option you can
|
|
# suppress this.
|
|
def initialize (settings = {})
|
|
# {{{
|
|
@header = {}
|
|
@cookies = {}
|
|
@get = {}
|
|
@post = {}
|
|
|
|
# Internal attributes
|
|
@status = nil
|
|
@reasonPhrase = nil
|
|
@setcookies = []
|
|
@output_started = false;
|
|
@output_allowed = false;
|
|
|
|
@mod_ruby = false
|
|
@env = ENV.to_hash
|
|
|
|
if defined?(MOD_RUBY)
|
|
@output_method = "mod_ruby"
|
|
@mod_ruby = true
|
|
elsif @env['SERVER_SOFTWARE'] =~ /^Microsoft-IIS/i
|
|
@output_method = "nph"
|
|
else
|
|
@output_method = "ph"
|
|
end
|
|
|
|
unless settings.is_a?(Hash)
|
|
raise TypeError, "settings must be a Hash"
|
|
end
|
|
@settings = settings
|
|
|
|
unless @settings.has_key?("safe")
|
|
@settings["safe"] = 1
|
|
end
|
|
|
|
if $SAFE < @settings["safe"]
|
|
$SAFE = @settings["safe"]
|
|
end
|
|
|
|
unless @settings.has_key?("cache")
|
|
@settings["cache"] = false
|
|
end
|
|
|
|
# mod_ruby sets no QUERY_STRING variable, if no GET-Parameters are given
|
|
unless @env.has_key?("QUERY_STRING")
|
|
@env["QUERY_STRING"] = ""
|
|
end
|
|
|
|
# Now we split the QUERY_STRING by the seperators & and ; or, if
|
|
# specified, settings['get seperator']
|
|
unless @settings.has_key?("get seperator")
|
|
get_args = @env['QUERY_STRING'].split(/[&;]/)
|
|
else
|
|
get_args = @env['QUERY_STRING'].split(@settings['get seperator'])
|
|
end
|
|
|
|
get_args.each do | arg |
|
|
arg_key, arg_val = arg.split(/=/, 2)
|
|
arg_key = Rweb::unescape(arg_key)
|
|
arg_val = Rweb::unescape(arg_val)
|
|
|
|
# Parse names like name[0], name['text'] or name[]
|
|
pattern = /^(.+)\[("[^\]]*"|'[^\]]*'|[0-9]*)\]$/
|
|
keys = []
|
|
while match = pattern.match(arg_key)
|
|
arg_key = match[1]
|
|
keys = [match[2]] + keys
|
|
end
|
|
keys = [arg_key] + keys
|
|
|
|
akt = @get
|
|
last = nil
|
|
lastkey = nil
|
|
keys.each do |key|
|
|
if key == ""
|
|
# No key specified (like in "test[]"), so we use the
|
|
# lowerst unused Integer as key
|
|
key = 0
|
|
while akt.has_key?(key)
|
|
key += 1
|
|
end
|
|
elsif /^[0-9]*$/ =~ key
|
|
# If the index is numerical convert it to an Integer
|
|
key = key.to_i
|
|
elsif key[0].chr == "'" || key[0].chr == '"'
|
|
key = key[1, key.length() -2]
|
|
end
|
|
if !akt.has_key?(key) || !akt[key].class == Hash
|
|
# create an empty Hash if there isn't already one
|
|
akt[key] = {}
|
|
end
|
|
last = akt
|
|
lastkey = key
|
|
akt = akt[key]
|
|
end
|
|
last[lastkey] = arg_val
|
|
end
|
|
|
|
if @env['REQUEST_METHOD'] == "POST"
|
|
if @env.has_key?("CONTENT_TYPE") && @env['CONTENT_TYPE'] == "application/x-www-form-urlencoded" && @env.has_key?('CONTENT_LENGTH')
|
|
unless @settings.has_key?("post seperator")
|
|
post_args = $stdin.read(@env['CONTENT_LENGTH'].to_i).split(/[&;]/)
|
|
else
|
|
post_args = $stdin.read(@env['CONTENT_LENGTH'].to_i).split(@settings['post seperator'])
|
|
end
|
|
post_args.each do | arg |
|
|
arg_key, arg_val = arg.split(/=/, 2)
|
|
arg_key = Rweb::unescape(arg_key)
|
|
arg_val = Rweb::unescape(arg_val)
|
|
|
|
# Parse names like name[0], name['text'] or name[]
|
|
pattern = /^(.+)\[("[^\]]*"|'[^\]]*'|[0-9]*)\]$/
|
|
keys = []
|
|
while match = pattern.match(arg_key)
|
|
arg_key = match[1]
|
|
keys = [match[2]] + keys
|
|
end
|
|
keys = [arg_key] + keys
|
|
|
|
akt = @post
|
|
last = nil
|
|
lastkey = nil
|
|
keys.each do |key|
|
|
if key == ""
|
|
# No key specified (like in "test[]"), so we use
|
|
# the lowerst unused Integer as key
|
|
key = 0
|
|
while akt.has_key?(key)
|
|
key += 1
|
|
end
|
|
elsif /^[0-9]*$/ =~ key
|
|
# If the index is numerical convert it to an Integer
|
|
key = key.to_i
|
|
elsif key[0].chr == "'" || key[0].chr == '"'
|
|
key = key[1, key.length() -2]
|
|
end
|
|
if !akt.has_key?(key) || !akt[key].class == Hash
|
|
# create an empty Hash if there isn't already one
|
|
akt[key] = {}
|
|
end
|
|
last = akt
|
|
lastkey = key
|
|
akt = akt[key]
|
|
end
|
|
last[lastkey] = arg_val
|
|
end
|
|
else
|
|
# Maybe we should print a warning here?
|
|
$stderr.print("Unidentified form data recived and discarded.")
|
|
end
|
|
end
|
|
|
|
if @env.has_key?("HTTP_COOKIE")
|
|
cookie = @env['HTTP_COOKIE'].split(/; ?/)
|
|
cookie.each do | c |
|
|
cookie_key, cookie_val = c.split(/=/, 2)
|
|
|
|
@cookies [Rweb::unescape(cookie_key)] = Rweb::unescape(cookie_val)
|
|
end
|
|
end
|
|
|
|
if defined?(@env['HTTP_USER_AGENT'])
|
|
@user_agent = @env['HTTP_USER_AGENT']
|
|
else
|
|
@user_agent = nil;
|
|
end
|
|
|
|
if defined?(@env['REMOTE_ADDR'])
|
|
@remote_addr = @env['REMOTE_ADDR']
|
|
else
|
|
@remote_addr = nil
|
|
end
|
|
# }}}
|
|
end
|
|
|
|
# Prints a String to the client. If caching is enabled, the String will
|
|
# buffered until the end of the out block ends.
|
|
def print(str = "")
|
|
# {{{
|
|
unless @output_allowed
|
|
raise "You just can write to output inside of a Rweb::out-block"
|
|
end
|
|
|
|
if @settings["cache"]
|
|
@buffer += [str.to_s]
|
|
else
|
|
unless @output_started
|
|
sendHeaders
|
|
end
|
|
$stdout.print(str)
|
|
end
|
|
nil
|
|
# }}}
|
|
end
|
|
|
|
# Prints a String to the client and adds a line break at the end. Please
|
|
# remember, that a line break is not visible in HTML, use the <br> HTML-Tag
|
|
# for this. If caching is enabled, the String will buffered until the end
|
|
# of the out block ends.
|
|
def puts(str = "")
|
|
# {{{
|
|
self.print(str + "\n")
|
|
# }}}
|
|
end
|
|
|
|
# Alias to print.
|
|
def write(str = "")
|
|
# {{{
|
|
self.print(str)
|
|
# }}}
|
|
end
|
|
|
|
# If caching is enabled, all cached data are send to the cliend and the
|
|
# cache emptied.
|
|
def flush
|
|
# {{{
|
|
unless @output_allowed
|
|
raise "You can't use flush outside of a Rweb::out-block"
|
|
end
|
|
buffer = @buffer.join
|
|
|
|
unless @output_started
|
|
sendHeaders
|
|
end
|
|
$stdout.print(buffer)
|
|
|
|
@buffer = []
|
|
# }}}
|
|
end
|
|
|
|
# Sends one or more header to the client. All headers are cached just
|
|
# before body data are send to the client. If the same header are set
|
|
# twice, only the last value is send.
|
|
#
|
|
# Example:
|
|
# web.header("Last-Modified: Mon, 16 Feb 2004 20:15:41 GMT")
|
|
# web.header("Location: http://www.ruby-lang.org")
|
|
#
|
|
# You can specify more than one header at the time by doing something like
|
|
# this:
|
|
# web.header("Content-Type: text/plain\nContent-Length: 383")
|
|
# or
|
|
# web.header(["Content-Type: text/plain", "Content-Length: 383"])
|
|
def header(str)
|
|
# {{{
|
|
if @output_started
|
|
raise "HTTP-Headers are already send. You can't change them after output has started!"
|
|
end
|
|
unless @output_allowed
|
|
raise "You just can set headers inside of a Rweb::out-block"
|
|
end
|
|
if str.is_a?Array
|
|
str.each do | value |
|
|
self.header(value)
|
|
end
|
|
|
|
elsif str.split(/\n/).length > 1
|
|
str.split(/\n/).each do | value |
|
|
self.header(value)
|
|
end
|
|
|
|
elsif str.is_a? String
|
|
str.gsub!(/\r/, "")
|
|
|
|
if (str =~ /^HTTP\/1\.[01] [0-9]{3} ?.*$/) == 0
|
|
pattern = /^HTTP\/1.[01] ([0-9]{3}) ?(.*)$/
|
|
|
|
result = pattern.match(str)
|
|
self.setstatus(result[0], result[1])
|
|
elsif (str =~ /^status: [0-9]{3} ?.*$/i) == 0
|
|
pattern = /^status: ([0-9]{3}) ?(.*)$/i
|
|
|
|
result = pattern.match(str)
|
|
self.setstatus(result[0], result[1])
|
|
else
|
|
a = str.split(/: ?/, 2)
|
|
|
|
@header[a[0].downcase] = a[1]
|
|
end
|
|
end
|
|
# }}}
|
|
end
|
|
|
|
# Changes the status of this page. There are several codes like "200 OK",
|
|
# "302 Found", "404 Not Found" or "500 Internal Server Error". A list of
|
|
# all codes is available at
|
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10
|
|
#
|
|
# You can just send the code number, the reason phrase will be added
|
|
# automaticly with the recommendations from the w3c if not specified. If
|
|
# you set the status twice or more, only the last status will be send.
|
|
# Examples:
|
|
# web.status("401 Unauthorized")
|
|
# web.status("410 Sad but true, this lonely page is gone :(")
|
|
# web.status(206)
|
|
# web.status("400")
|
|
#
|
|
# The default status is "200 OK". If a "Location" header is set, the
|
|
# default status is "302 Found".
|
|
def status(str)
|
|
# {{{
|
|
if @output_started
|
|
raise "HTTP-Headers are already send. You can't change them after output has started!"
|
|
end
|
|
unless @output_allowed
|
|
raise "You just can set headers inside of a Rweb::out-block"
|
|
end
|
|
if str.is_a?Integer
|
|
@status = str
|
|
elsif str.is_a?String
|
|
p1 = /^([0-9]{3}) ?(.*)$/
|
|
p2 = /^HTTP\/1\.[01] ([0-9]{3}) ?(.*)$/
|
|
p3 = /^status: ([0-9]{3}) ?(.*)$/i
|
|
|
|
if (a = p1.match(str)) == nil
|
|
if (a = p2.match(str)) == nil
|
|
if (a = p3.match(str)) == nil
|
|
raise ArgumentError, "Invalid argument", caller
|
|
end
|
|
end
|
|
end
|
|
@status = a[1].to_i
|
|
if a[2] != ""
|
|
@reasonPhrase = a[2]
|
|
else
|
|
@reasonPhrase = getReasonPhrase(@status)
|
|
end
|
|
else
|
|
raise ArgumentError, "Argument of setstatus must be integer or string", caller
|
|
end
|
|
# }}}
|
|
end
|
|
|
|
# Handles the output of your content and rescues all exceptions. Send all
|
|
# data in the block to this method. For example:
|
|
# web.out do
|
|
# web.header("Content-Type: text/plain")
|
|
# web.puts("Hello, plain world!")
|
|
# end
|
|
def out
|
|
# {{{
|
|
@output_allowed = true
|
|
@buffer = []; # We use an array as buffer, because it's more performant :)
|
|
|
|
begin
|
|
yield
|
|
rescue Exception => exception
|
|
$stderr.puts "Ruby exception rescued (#{exception.class}): #{exception.message}"
|
|
$stderr.puts exception.backtrace.join("\n")
|
|
|
|
unless @output_started
|
|
self.setstatus(500)
|
|
@header = {}
|
|
end
|
|
|
|
unless (@settings.has_key?("hide errors") and @settings["hide errors"] == true)
|
|
unless @output_started
|
|
self.header("Content-Type: text/html")
|
|
self.puts "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Strict//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">"
|
|
self.puts "<html>"
|
|
self.puts "<head>"
|
|
self.puts "<title>500 Internal Server Error</title>"
|
|
self.puts "</head>"
|
|
self.puts "<body>"
|
|
end
|
|
if @header.has_key?("content-type") and (@header["content-type"] =~ /^text\/html/i) == 0
|
|
self.puts "<h1>Internal Server Error</h1>"
|
|
self.puts "<p>The server encountered an exception and was unable to complete your request.</p>"
|
|
self.puts "<p>The exception has provided the following information:</p>"
|
|
self.puts "<pre style=\"background: #FFCCCC; border: black solid 2px; margin-left: 2cm; margin-right: 2cm; padding: 2mm;\"><b>#{exception.class}</b>: #{exception.message} <b>on</b>"
|
|
self.puts
|
|
self.puts "#{exception.backtrace.join("\n")}</pre>"
|
|
self.puts "</body>"
|
|
self.puts "</html>"
|
|
else
|
|
self.puts "The server encountered an exception and was unable to complete your request"
|
|
self.puts "The exception has provided the following information:"
|
|
self.puts "#{exception.class}: #{exception.message}"
|
|
self.puts
|
|
self.puts exception.backtrace.join("\n")
|
|
end
|
|
end
|
|
end
|
|
|
|
if @settings["cache"]
|
|
buffer = @buffer.join
|
|
|
|
unless @output_started
|
|
unless @header.has_key?("content-length")
|
|
self.header("content-length: #{buffer.length}")
|
|
end
|
|
|
|
sendHeaders
|
|
end
|
|
$stdout.print(buffer)
|
|
elsif !@output_started
|
|
sendHeaders
|
|
end
|
|
@output_allowed = false;
|
|
# }}}
|
|
end
|
|
|
|
# Decodes URL encoded data, %20 for example stands for a space.
|
|
def Rweb.unescape(str)
|
|
# {{{
|
|
if defined? str and str.is_a? String
|
|
str.gsub!(/\+/, " ")
|
|
str.gsub(/%.{2}/) do | s |
|
|
s[1,2].hex.chr
|
|
end
|
|
end
|
|
# }}}
|
|
end
|
|
|
|
protected
|
|
def sendHeaders
|
|
# {{{
|
|
|
|
Cookie.disallow # no more cookies can be set or modified
|
|
if !(@settings.has_key?("silent") and @settings["silent"] == true) and !@header.has_key?("x-powered-by")
|
|
if @mod_ruby
|
|
header("x-powered-by: #{RWEB} (Ruby/#{RUBY_VERSION}, #{MOD_RUBY})");
|
|
else
|
|
header("x-powered-by: #{RWEB} (Ruby/#{RUBY_VERSION})");
|
|
end
|
|
end
|
|
|
|
if @output_method == "ph"
|
|
if ((@status == nil or @status == 200) and !@header.has_key?("content-type") and !@header.has_key?("location"))
|
|
header("content-type: text/html")
|
|
end
|
|
|
|
if @status != nil
|
|
$stdout.print "Status: #{@status} #{@reasonPhrase}\r\n"
|
|
end
|
|
|
|
@header.each do |key, value|
|
|
key = key *1 # "unfreeze" key :)
|
|
key[0] = key[0,1].upcase![0]
|
|
|
|
key = key.gsub(/-[a-z]/) do |char|
|
|
"-" + char[1,1].upcase
|
|
end
|
|
|
|
$stdout.print "#{key}: #{value}\r\n"
|
|
end
|
|
cookies = Cookie.getHttpHeader # Get all cookies as an HTTP Header
|
|
if cookies
|
|
$stdout.print cookies
|
|
end
|
|
|
|
$stdout.print "\r\n"
|
|
|
|
elsif @output_method == "nph"
|
|
elsif @output_method == "mod_ruby"
|
|
r = Apache.request
|
|
|
|
if ((@status == nil or @status == 200) and !@header.has_key?("content-type") and !@header.has_key?("location"))
|
|
header("text/html")
|
|
end
|
|
|
|
if @status != nil
|
|
r.status_line = "#{@status} #{@reasonPhrase}"
|
|
end
|
|
|
|
r.send_http_header
|
|
@header.each do |key, value|
|
|
key = key *1 # "unfreeze" key :)
|
|
|
|
key[0] = key[0,1].upcase![0]
|
|
key = key.gsub(/-[a-z]/) do |char|
|
|
"-" + char[1,1].upcase
|
|
end
|
|
puts "#{key}: #{value.class}"
|
|
#r.headers_out[key] = value
|
|
end
|
|
end
|
|
@output_started = true
|
|
# }}}
|
|
end
|
|
|
|
def getReasonPhrase (status)
|
|
# {{{
|
|
if status == 100
|
|
"Continue"
|
|
elsif status == 101
|
|
"Switching Protocols"
|
|
elsif status == 200
|
|
"OK"
|
|
elsif status == 201
|
|
"Created"
|
|
elsif status == 202
|
|
"Accepted"
|
|
elsif status == 203
|
|
"Non-Authoritative Information"
|
|
elsif status == 204
|
|
"No Content"
|
|
elsif status == 205
|
|
"Reset Content"
|
|
elsif status == 206
|
|
"Partial Content"
|
|
elsif status == 300
|
|
"Multiple Choices"
|
|
elsif status == 301
|
|
"Moved Permanently"
|
|
elsif status == 302
|
|
"Found"
|
|
elsif status == 303
|
|
"See Other"
|
|
elsif status == 304
|
|
"Not Modified"
|
|
elsif status == 305
|
|
"Use Proxy"
|
|
elsif status == 307
|
|
"Temporary Redirect"
|
|
elsif status == 400
|
|
"Bad Request"
|
|
elsif status == 401
|
|
"Unauthorized"
|
|
elsif status == 402
|
|
"Payment Required"
|
|
elsif status == 403
|
|
"Forbidden"
|
|
elsif status == 404
|
|
"Not Found"
|
|
elsif status == 405
|
|
"Method Not Allowed"
|
|
elsif status == 406
|
|
"Not Acceptable"
|
|
elsif status == 407
|
|
"Proxy Authentication Required"
|
|
elsif status == 408
|
|
"Request Time-out"
|
|
elsif status == 409
|
|
"Conflict"
|
|
elsif status == 410
|
|
"Gone"
|
|
elsif status == 411
|
|
"Length Required"
|
|
elsif status == 412
|
|
"Precondition Failed"
|
|
elsif status == 413
|
|
"Request Entity Too Large"
|
|
elsif status == 414
|
|
"Request-URI Too Large"
|
|
elsif status == 415
|
|
"Unsupported Media Type"
|
|
elsif status == 416
|
|
"Requested range not satisfiable"
|
|
elsif status == 417
|
|
"Expectation Failed"
|
|
elsif status == 500
|
|
"Internal Server Error"
|
|
elsif status == 501
|
|
"Not Implemented"
|
|
elsif status == 502
|
|
"Bad Gateway"
|
|
elsif status == 503
|
|
"Service Unavailable"
|
|
elsif status == 504
|
|
"Gateway Time-out"
|
|
elsif status == 505
|
|
"HTTP Version not supported"
|
|
else
|
|
raise "Unknown Statuscode. See http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 for more information."
|
|
end
|
|
# }}}
|
|
end
|
|
end
|
|
|
|
class Cookie
|
|
attr_reader :name, :value, :maxage, :path, :domain, :secure, :comment
|
|
|
|
# Sets a cookie. Please see below for details of the attributes.
|
|
def initialize (name, value = nil, maxage = nil, path = nil, domain = nil, secure = false)
|
|
# {{{
|
|
# HTTP headers (Cookies are a HTTP header) can only set, while no content
|
|
# is send. So an exception will be raised, when @@allowed is set to false
|
|
# and a new cookie has set.
|
|
unless defined?(@@allowed)
|
|
@@allowed = true
|
|
end
|
|
unless @@allowed
|
|
raise "You can't set cookies after the HTTP headers are send."
|
|
end
|
|
|
|
unless defined?(@@list)
|
|
@@list = []
|
|
end
|
|
@@list += [self]
|
|
|
|
unless defined?(@@type)
|
|
@@type = "netscape"
|
|
end
|
|
|
|
unless name.class == String
|
|
raise TypeError, "The name of a cookie must be a string", caller
|
|
end
|
|
if value.class.superclass == Integer || value.class == Float
|
|
value = value.to_s
|
|
elsif value.class != String && value != nil
|
|
raise TypeError, "The value of a cookie must be a string, integer, float or nil", caller
|
|
end
|
|
if maxage.class == Time
|
|
maxage = maxage - Time.now
|
|
elsif !maxage.class.superclass == Integer || !maxage == nil
|
|
raise TypeError, "The maxage date of a cookie must be an Integer or Time object or nil.", caller
|
|
end
|
|
unless path.class == String || path == nil
|
|
raise TypeError, "The path of a cookie must be nil or a string", caller
|
|
end
|
|
unless domain.class == String || domain == nil
|
|
raise TypeError, "The value of a cookie must be nil or a string", caller
|
|
end
|
|
unless secure == true || secure == false
|
|
raise TypeError, "The secure field of a cookie must be true or false", caller
|
|
end
|
|
|
|
@name, @value, @maxage, @path, @domain, @secure = name, value, maxage, path, domain, secure
|
|
@comment = nil
|
|
# }}}
|
|
end
|
|
|
|
# Modifies the value of this cookie. The information you want to store. If the
|
|
# value is nil, the cookie will be deleted by the client.
|
|
#
|
|
# This attribute can be a String, Integer or Float object or nil.
|
|
def value=(value)
|
|
# {{{
|
|
if value.class.superclass == Integer || value.class == Float
|
|
value = value.to_s
|
|
elsif value.class != String && value != nil
|
|
raise TypeError, "The value of a cookie must be a string, integer, float or nil", caller
|
|
end
|
|
@value = value
|
|
# }}}
|
|
end
|
|
|
|
# Modifies the maxage of this cookie. This attribute defines the lifetime of
|
|
# the cookie, in seconds. A value of 0 means the cookie should be discarded
|
|
# imediatly. If it set to nil, the cookie will be deleted when the browser
|
|
# will be closed.
|
|
#
|
|
# Attention: This is different from other implementations like PHP, where you
|
|
# gives the seconds since 1/1/1970 0:00:00 GMT.
|
|
#
|
|
# This attribute must be an Integer or Time object or nil.
|
|
def maxage=(maxage)
|
|
# {{{
|
|
if maxage.class == Time
|
|
maxage = maxage - Time.now
|
|
elsif maxage.class.superclass == Integer || !maxage == nil
|
|
raise TypeError, "The maxage of a cookie must be an Interger or Time object or nil.", caller
|
|
end
|
|
@maxage = maxage
|
|
# }}}
|
|
end
|
|
|
|
# Modifies the path value of this cookie. The client will send this cookie
|
|
# only, if the requested document is this directory or a subdirectory of it.
|
|
#
|
|
# The value of the attribute must be a String object or nil.
|
|
def path=(path)
|
|
# {{{
|
|
unless path.class == String || path == nil
|
|
raise TypeError, "The path of a cookie must be nil or a string", caller
|
|
end
|
|
@path = path
|
|
# }}}
|
|
end
|
|
|
|
# Modifies the domain value of this cookie. The client will send this cookie
|
|
# only if it's connected with this domain (or a subdomain, if the first
|
|
# character is a dot like in ".ruby-lang.org")
|
|
#
|
|
# The value of this attribute must be a String or nil.
|
|
def domain=(domain)
|
|
# {{{
|
|
unless domain.class == String || domain == nil
|
|
raise TypeError, "The domain of a cookie must be a String or nil.", caller
|
|
end
|
|
@domain = domain
|
|
# }}}
|
|
end
|
|
|
|
# Modifies the secure flag of this cookie. If it's true, the client will only
|
|
# send this cookie if it is secured connected with us.
|
|
#
|
|
# The value od this attribute has to be true or false.
|
|
def secure=(secure)
|
|
# {{{
|
|
unless secure == true || secure == false
|
|
raise TypeError, "The secure field of a cookie must be true or false", caller
|
|
end
|
|
@secure = secure
|
|
# }}}
|
|
end
|
|
|
|
# Modifies the comment value of this cookie. The comment won't be send, if
|
|
# type is "netscape".
|
|
def comment=(comment)
|
|
# {{{
|
|
unless comment.class == String || comment == nil
|
|
raise TypeError, "The comment of a cookie must be a string or nil", caller
|
|
end
|
|
@comment = comment
|
|
# }}}
|
|
end
|
|
|
|
# Changes the type of all cookies.
|
|
# Allowed values are RFC2109 and netscape (default).
|
|
def Cookie.type=(type)
|
|
# {{{
|
|
unless @@allowed
|
|
raise "The cookies are allready send, so you can't change the type anymore."
|
|
end
|
|
unless type.downcase == "rfc2109" && type.downcase == "netscape"
|
|
raise "The type of the cookies must be \"RFC2109\" or \"netscape\"."
|
|
end
|
|
@@type = type;
|
|
# }}}
|
|
end
|
|
|
|
# After sending this message, no cookies can be set or modified. Use it, when
|
|
# HTTP-Headers are send. Rweb does this for you.
|
|
def Cookie.disallow
|
|
# {{{
|
|
@@allowed = false
|
|
true
|
|
# }}}
|
|
end
|
|
|
|
# Returns a HTTP header (type String) with all cookies. Rweb does this for
|
|
# you.
|
|
def Cookie.getHttpHeader
|
|
# {{{
|
|
if defined?(@@list)
|
|
if @@type == "netscape"
|
|
str = ""
|
|
@@list.each do |cookie|
|
|
if cookie.value == nil
|
|
cookie.maxage = 0
|
|
cookie.value = ""
|
|
end
|
|
# TODO: Name and value should be escaped!
|
|
str += "Set-Cookie: #{cookie.name}=#{cookie.value}"
|
|
unless cookie.maxage == nil
|
|
expire = Time.now + cookie.maxage
|
|
expire.gmtime
|
|
str += "; Expire=#{expire.strftime("%a, %d-%b-%Y %H:%M:%S %Z")}"
|
|
end
|
|
unless cookie.domain == nil
|
|
str += "; Domain=#{cookie.domain}"
|
|
end
|
|
unless cookie.path == nil
|
|
str += "; Path=#{cookie.path}"
|
|
end
|
|
if cookie.secure
|
|
str += "; Secure"
|
|
end
|
|
str += "\r\n"
|
|
end
|
|
return str
|
|
else # type == "RFC2109"
|
|
str = "Set-Cookie: "
|
|
comma = false;
|
|
|
|
@@list.each do |cookie|
|
|
if cookie.value == nil
|
|
cookie.maxage = 0
|
|
cookie.value = ""
|
|
end
|
|
if comma
|
|
str += ","
|
|
end
|
|
comma = true
|
|
|
|
str += "#{cookie.name}=\"#{cookie.value}\""
|
|
unless cookie.maxage == nil
|
|
str += "; Max-Age=\"#{cookie.maxage}\""
|
|
end
|
|
unless cookie.domain == nil
|
|
str += "; Domain=\"#{cookie.domain}\""
|
|
end
|
|
unless cookie.path == nil
|
|
str += "; Path=\"#{cookie.path}\""
|
|
end
|
|
if cookie.secure
|
|
str += "; Secure"
|
|
end
|
|
unless cookie.comment == nil
|
|
str += "; Comment=\"#{cookie.comment}\""
|
|
end
|
|
str += "; Version=\"1\""
|
|
end
|
|
str
|
|
end
|
|
else
|
|
false
|
|
end
|
|
# }}}
|
|
end
|
|
end
|
|
|
|
require 'strscan'
|
|
|
|
module BBCode
|
|
DEBUG = true
|
|
|
|
use 'encoder', 'tags', 'tagstack', 'smileys'
|
|
|
|
=begin
|
|
The Parser class takes care of the encoding.
|
|
It scans the given BBCode (as plain text), finds tags
|
|
and smilies and also makes links of urls in text.
|
|
|
|
Normal text is send directly to the encoder.
|
|
|
|
If a tag was found, an instance of a Tag subclass is created
|
|
to handle the case.
|
|
|
|
The @tagstack manages tag nesting and ensures valid HTML.
|
|
=end
|
|
|
|
class Parser
|
|
class Attribute
|
|
# flatten and use only one empty_arg
|
|
def self.create attr
|
|
attr = flatten attr
|
|
return @@empty_attr if attr.empty?
|
|
new attr
|
|
end
|
|
|
|
private_class_method :new
|
|
|
|
# remove leading and trailing whitespace; concat lines
|
|
def self.flatten attr
|
|
attr.strip.gsub(/\n/, ' ')
|
|
# -> ^ and $ can only match at begin and end now
|
|
end
|
|
|
|
ATTRIBUTE_SCAN = /
|
|
(?!$) # don't match at end
|
|
\s*
|
|
( # $1 = key
|
|
[^=\s\]"\\]*
|
|
(?:
|
|
(?: \\. | "[^"\\]*(?:\\.[^"\\]*)*"? )
|
|
[^=\s\]"\\]*
|
|
)*
|
|
)
|
|
(?:
|
|
=
|
|
( # $2 = value
|
|
[^\s\]"\\]*
|
|
(?:
|
|
(?: \\. | "[^"\\]*(?:\\.[^"\\]*)*"? )
|
|
[^\s\]"\\]*
|
|
)*
|
|
)?
|
|
)?
|
|
\s*
|
|
/x
|
|
|
|
def self.parse source
|
|
source = source.dup
|
|
# empty_tag: the tag looks like [... /]
|
|
# slice!: this deletes the \s*/] at the end
|
|
# \s+ because [url=http://rubybb.org/forum/] is NOT an empty tag.
|
|
# In RubyBBCode, you can use [url=http://rubybb.org/forum/ /], and this has to be
|
|
# interpreted correctly.
|
|
empty_tag = source.sub!(/^:/, '=') or source.slice!(/\/$/)
|
|
debug 'PARSE: ' + source.inspect + ' => ' + empty_tag.inspect
|
|
#-> we have now an attr that's EITHER empty OR begins and ends with non-whitespace.
|
|
|
|
attr = Hash.new
|
|
attr[:flags] = []
|
|
source.scan(ATTRIBUTE_SCAN) { |key, value|
|
|
if not value
|
|
attr[:flags] << unescape(key)
|
|
else
|
|
next if value.empty? and key.empty?
|
|
attr[unescape(key)] = unescape(value)
|
|
end
|
|
}
|
|
debug attr.inspect
|
|
|
|
return empty_tag, attr
|
|
end
|
|
|
|
def self.unescape_char esc
|
|
esc[1]
|
|
end
|
|
|
|
def self.unquote qt
|
|
qt[1..-1].chomp('"').gsub(/\\./) { |esc| unescape_char esc }
|
|
end
|
|
|
|
def self.unescape str
|
|
str.gsub(/ (\\.) | (" [^"\\]* (?:\\.[^"\\]*)* "?) /x) {
|
|
if $1
|
|
unescape_char $1
|
|
else
|
|
unquote $2
|
|
end
|
|
}
|
|
end
|
|
|
|
include Enumerable
|
|
def each &block
|
|
@args.each(&block)
|
|
end
|
|
|
|
attr_reader :source, :args, :value
|
|
|
|
def initialize source
|
|
@source = source
|
|
debug 'Attribute#new(%p)' % source
|
|
@empty_tag, @attr = Attribute.parse source
|
|
@value = @attr[''].to_s
|
|
end
|
|
|
|
def empty?
|
|
self == @@empty_attr
|
|
end
|
|
|
|
def empty_tag?
|
|
@empty_tag
|
|
end
|
|
|
|
def [] *keys
|
|
res = @attr[*keys]
|
|
end
|
|
|
|
def flags
|
|
attr[:flags]
|
|
end
|
|
|
|
def to_s
|
|
@attr
|
|
end
|
|
|
|
def inspect
|
|
'ATTR[' + @attr.inspect + (@empty_tag ? ' | empty tag' : '') + ']'
|
|
end
|
|
end
|
|
class Attribute
|
|
@@empty_attr = new ''
|
|
end
|
|
end
|
|
|