summaryrefslogtreecommitdiff
path: root/tests/examplefiles/example.rb
diff options
context:
space:
mode:
Diffstat (limited to 'tests/examplefiles/example.rb')
-rw-r--r--tests/examplefiles/example.rb1852
1 files changed, 1852 insertions, 0 deletions
diff --git a/tests/examplefiles/example.rb b/tests/examplefiles/example.rb
new file mode 100644
index 00000000..93f8dc2b
--- /dev/null
+++ b/tests/examplefiles/example.rb
@@ -0,0 +1,1852 @@
+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
+