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(/<#\\\\]*(?:(?:#\{.*?\}|#|\\\\.)[^>#\\\\]*)*>?|([^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=>#, # # 2001=>#, # # 2002=>#} 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 # => #, # # #, # # #, # # #}> 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. ("#") 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: rweb.rb 6 2004-06-16 15:56:26Z igel $ # # == 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 "Hello world!" # 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"] # # You can submit values as a Hash # # # will be available as # web.get["text"]["index"] # web.get["text"]["index2"] # Integers are also possible # # # # 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: # # 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. # # 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
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 "" self.puts "" self.puts "" self.puts "500 Internal Server Error" self.puts "" self.puts "" end if @header.has_key?("content-type") and (@header["content-type"] =~ /^text\/html/i) == 0 self.puts "

Internal Server Error

" 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} on"
                    self.puts
                    self.puts "#{exception.backtrace.join("\n")}
" self.puts "" self.puts "" 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 class Parser def Parser.flatten str # replace mac & dos newlines with unix style str.gsub(/\r\n?/, "\n") end def initialize input = '' # input manager @scanner = StringScanner.new '' # output manager @encoder = Encoder.new @output = '' # tag manager @tagstack = TagStack.new(@encoder) @do_magic = true # set the input feed input end # if you want, you can feed a parser instance after creating, # or even feed it repeatedly. def feed food @scanner.string = Parser.flatten food end # parse through the string using parse_token def parse parse_token until @scanner.eos? @tagstack.close_all @output = parse_magic @encoder.output end def output @output end # ok, internals start here private # the default output functions. everything should use them or the tags. def add_text text = @scanner.matched @encoder.add_text text end # use this carefully def add_html html @encoder.add_html html end # highlights the text as error def add_garbage garbage add_html '' if DEBUG add_text garbage add_html '' if DEBUG end # unknown and incorrectly nested tags are ignored and # sent as plaintext (garbage in - garbage out). # in debug mode, garbage is marked with lime background. def garbage_out start @scanner.pos = start garbage = @scanner.scan(/./m) debug 'GARBAGE: ' + garbage add_garbage garbage end # simple text; everything but [, \[ allowed SIMPLE_TEXT_SCAN_ = / [^\[\\]* # normal* (?: # ( \\.? # special [^\[\\]* # normal* )* # )* /mx SIMPLE_TEXT_SCAN = /[^\[]+/ =begin WHAT IS A TAG? ============== Tags in BBCode can be much more than just a simple [b]. I use many terms here to differ the parts of each tag. Basic scheme: [ code ] TAG START TAG INFO TAG END Most tags need a second tag to close the range it opened. This is done with CLOSING TAGS: [/code] or by using empty tags that have no content and close themselfes: [url=winamp.com /] You surely know this from HTML. These slashes define the TAG KIND = normal|closing|empty and cannot be used together. Everything between [ and ] and expluding the slashes is called the TAG INFO. This info may contain: - TAG ID - TAG NAME including the tag id - attributes The TAG ID is the first char of the info: TAG | ID ----------+---- [quote] | q [±] | & ["[b]"] | " [/url] | u [---] | - As you can see, the tag id shows the TAG TYPE, it can be a normal tag, a formatting tag or an entity. Therefor, the parser first scans the id to decide how to go on with parsing. =end # tag # TODO more complex expression allowing # [quote="[ladico]"] and [quote=\[ladico\]] to be correct tags TAG_BEGIN_SCAN = / \[ # tag start ( \/ )? # $1 = closing tag? ( [^\]] ) # $2 = tag id /x TAG_END_SCAN = / [^\]]* # rest that was not handled \]? # tag end /x CLOSE_TAG_SCAN = / ( [^\]]* ) # $1 = the rest of the tag info ( \/ )? # $2 = empty tag? \]? # tag end /x UNCLOSED_TAG_SCAN = / \[ /x CLASSIC_TAG_SCAN = / [a-z]* /ix SEPARATOR_TAG_SCAN = / \** /x FORMAT_TAG_SCAN = / -- -* /x QUOTED_SCAN = / ( # $1 = quoted text [^"\\]* # normal* (?: # ( \\. # special [^"\\]* # normal* )* # )* ) "? # end quote " /mx ENTITY_SCAN = / ( [^;\]]+ ) # $1 = entity code ;? # optional ending semicolon /ix SMILEY_SCAN = Smileys::SMILEY_PATTERN # this is the main parser loop that separates # text - everything until "[" # from # tags - starting with "[", ending with "]" def parse_token if @scanner.scan(SIMPLE_TEXT_SCAN) add_text else handle_tag end end def handle_tag tag_start = @scanner.pos unless @scanner.scan TAG_BEGIN_SCAN garbage_out tag_start return end closing, id = @scanner[1], @scanner[2] #debug 'handle_tag(%p)' % @scanner.matched handled = case id when /[a-z]/i if @scanner.scan(CLASSIC_TAG_SCAN) if handle_classic_tag(id + @scanner.matched, closing) already_closed = true end end when '*' if @scanner.scan(SEPARATOR_TAG_SCAN) handle_asterisk tag_start, id + @scanner.matched true end when '-' if @scanner.scan(FORMAT_TAG_SCAN) #format = id + @scanner.matched @encoder.add_html "\n
\n" true end when '"' if @scanner.scan(QUOTED_SCAN) @encoder.add_text unescape(@scanner[1]) true end when '&' if @scanner.scan(ENTITY_SCAN) @encoder.add_entity @scanner[1] true end when Smileys::SMILEY_START_CHARSET @scanner.pos = @scanner.pos - 1 # (ungetch) if @scanner.scan(SMILEY_SCAN) @encoder.add_html Smileys.smiley_to_image(@scanner.matched) true end end # case return garbage_out(tag_start) unless handled @scanner.scan(TAG_END_SCAN) unless already_closed end ATTRIBUTES_SCAN = / ( [^\]"\\]* (?: (?: \\. | " [^"\\]* (?: \\. [^"\\]* )* "? ) [^\]"\\]* )* ) \]? /x def handle_classic_tag name, closing debug 'TAG: ' + (closing ? '/' : '') + name # flatten name.downcase! tag_class = TAG_LIST[name] return unless tag_class #debug((opening ? 'OPEN ' : 'CLOSE ') + tag_class.name) # create an attribute object to handle it @scanner.scan(ATTRIBUTES_SCAN) #debug name + ':' + @scanner[1] attr = Attribute.create @scanner[1] #debug 'ATTRIBUTES %p ' % attr #unless attr.empty? #debug 'closing: %p; name=%s, attr=%p' % [closing, name, attr] # OPEN if not closing and tag = @tagstack.try_open_class(tag_class, attr) #debug 'opening' tag.do_open @scanner # this should be done by the tag itself. if attr.empty_tag? tag.handle_empty @tagstack.close_tag elsif tag.special_content? handle_special_content(tag) @tagstack.close_tag # # ignore asterisks directly after the opening; these are phpBBCode # elsif tag.respond_to? :asterisk # debug 'SKIP ASTERISKS: ' if @scanner.skip(ASTERISK_TAGS_SCAN) end # CLOSE elsif @tagstack.try_close_class(tag_class) #debug 'closing' # GARBAGE else return end true end def handle_asterisk tag_start, stars #debug 'ASTERISK: ' + stars.to_s # rule for asterisk tags: they belong to the last tag # that handles them. tags opened after this tag are closed. # if no open tag uses them, all are closed. tag = @tagstack.close_all_until { |tag| tag.respond_to? :asterisk } unless tag and tag.asterisk stars, @scanner garbage_out tag_start end end def handle_special_content tag scanned = @scanner.scan_until(tag.closing_tag) if scanned scanned.slice!(-(@scanner.matched.size)..-1) else scanned = @scanner.scan(/.*/m).to_s end #debug 'SPECIAL CONTENT: ' + scanned tag.handle_content(scanned) end def unescape text # input: correctly formatted quoted string (without the quotes) text.gsub(/\\(?:(["\\])|.)/) { $1 or $& } end # MAGIC FEAUTURES URL_PATTERN = /(?:(?:www|ftp)\.|(?>\w{3,}):\/\/)\S+/ EMAIL_PATTERN = /(?>[\w\-_.]+)@[\w\-\.]+\.\w+/ HAS_MAGIC = /[&@#{Smileys::SMILEY_START_CHARS}]|(?i:www|ftp)/ MAGIC_PATTERN = Regexp.new('(\W|^)(%s)' % [Smileys::MAGIC_SMILEY_PATTERN, URL_PATTERN, EMAIL_PATTERN].map { |pattern| pattern.to_s }.join('|') ) IS_SMILEY_PATTERN = Regexp.new('^%s' % Smileys::SMILEY_START_CHARSET.to_s ) IS_URL_PATTERN = /^(?:(?i:www|ftp)\.|(?>\w+):\/\/)/ URL_STARTS_WITH_PROTOCOL = /^\w+:\/\// IS_EMAIL_PATTERN = /^[\w\-_.]+@/ def to_magic text # debug MAGIC_PATTERN.to_s text.gsub!(MAGIC_PATTERN) { magic = $2 $1 + case magic when IS_SMILEY_PATTERN Smileys.smiley_to_img magic when IS_URL_PATTERN last = magic.slice_punctation! # no punctation in my URL href = magic href.insert(0, 'http://') unless magic =~ URL_STARTS_WITH_PROTOCOL '' + magic + '' + last when IS_EMAIL_PATTERN last = magic.slice_punctation! '' + magic + '' + last else raise '{{{' + magic + '}}}' end } text end # handles smileys and urls def parse_magic html return html unless @do_magic scanner = StringScanner.new html out = '' while scanner.rest? if scanner.scan(/ < (?: a\s .*? <\/a> | pre\W .*? <\/pre> | [^>]* > ) /mx) out << scanner.matched elsif scanner.scan(/ [^<]+ /x) out << to_magic(scanner.matched) # this should never happen elsif scanner.scan(/./m) raise 'ERROR: else case reached' end end out end end # Parser end class String def slice_punctation! slice!(/[.:,!\?]+$/).to_s # return '' instead of nil end end # # = Grammar # # An implementation of common algorithms on grammars. # # This is used by Shinobu, a visualization tool for educating compiler-building. # # Thanks to Andreas Kunert for his wonderful LR(k) Pamphlet (German, see http://www.informatik.hu-berlin.de/~kunert/papers/lr-analyse), and Aho/Sethi/Ullman for their Dragon Book. # # Homepage:: http://shinobu.cYcnus.de (not existing yet) # Author:: murphy (Kornelius Kalnbach) # Copyright:: (cc) 2005 cYcnus # License:: GPL # Version:: 0.2.0 (2005-03-27) require 'set_hash' require 'ctype' require 'tools' require 'rules' require 'trace' require 'first' require 'follow' # = Grammar # # == Syntax # # === Rules # # Each line is a rule. # The syntax is # # left - right # # where +left+ and +right+ can be uppercase and lowercase letters, # and - can be any combination of <, >, - or whitespace. # # === Symbols # # Uppercase letters stand for meta symbols, lowercase for terminals. # # You can make epsilon-derivations by leaving empty. # # === Example # S - Ac # A - Sc # A - b # A - class Grammar attr_reader :tracer # Creates a new Grammar. # If $trace is true, the algorithms explain (textual) what they do to $stdout. def initialize data, tracer = Tracer.new @tracer = tracer @rules = Rules.new @terminals, @meta_symbols = SortedSet.new, Array.new @start_symbol = nil add_rules data end attr_reader :meta_symbols, :terminals, :rules, :start_symbol alias_method :sigma, :terminals alias_method :alphabet, :terminals alias_method :variables, :meta_symbols alias_method :nonterminals, :meta_symbols # A string representation of the grammar for debugging. def inspect productions_too = false 'Grammar(meta symbols: %s; alphabet: %s; productions: [%s]; start symbol: %s)' % [ meta_symbols.join(', '), terminals.join(', '), if productions_too @rules.inspect else @rules.size end, start_symbol ] end # Add rules to the grammar. +rules+ should be a String or respond to +scan+ in a similar way. # # Syntax: see Grammar. def add_rules grammar @rules = Rules.parse grammar do |rule| @start_symbol ||= rule.left @meta_symbols << rule.left @terminals.merge rule.right.split('').select { |s| terminal? s } end @meta_symbols.uniq! update end # Returns a hash acting as FIRST operator, so that # first["ABC"] is FIRST(ABC). # See http://en.wikipedia.org/wiki/LL_parser "Constructing an LL(1) parsing table" for details. def first first_operator end # Returns a hash acting as FOLLOW operator, so that # first["A"] is FOLLOW(A). # See http://en.wikipedia.org/wiki/LL_parser "Constructing an LL(1) parsing table" for details. def follow follow_operator end LLError = Class.new(Exception) LLErrorType1 = Class.new(LLError) LLErrorType2 = Class.new(LLError) # Tests if the grammar is LL(1). def ll1? begin for meta in @meta_symbols first_sets = @rules[meta].map { |alpha| first[alpha] } first_sets.inject(Set[]) do |already_used, another_first_set| unless already_used.disjoint? another_first_set raise LLErrorType1 end already_used.merge another_first_set end if first[meta].include? EPSILON and not first[meta].disjoint? follow[meta] raise LLErrorType2 end end rescue LLError false else true end end private def first_operator @first ||= FirstOperator.new self end def follow_operator @follow ||= FollowOperator.new self end def update @first = @follow = nil end end if $0 == __FILE__ eval DATA.read, nil, $0, __LINE__+4 end require 'test/unit' class TestCaseGrammar < Test::Unit::TestCase include Grammar::Symbols def fifo s Set[*s.split('')] end def test_fifo assert_equal Set[], fifo('') assert_equal Set[EPSILON, END_OF_INPUT, 'x', 'Y'], fifo('?xY$') end TEST_GRAMMAR_1 = <<-EOG S - ABCD A - a A - B - b B - C - c C - D - S D - EOG def test_symbols assert EPSILON assert END_OF_INPUT end def test_first_1 g = Grammar.new TEST_GRAMMAR_1 f = nil assert_nothing_raised { f = g.first } assert_equal(Set['a', EPSILON], f['A']) assert_equal(Set['b', EPSILON], f['B']) assert_equal(Set['c', EPSILON], f['C']) assert_equal(Set['a', 'b', 'c', EPSILON], f['D']) assert_equal(f['D'], f['S']) end def test_follow_1 g = Grammar.new TEST_GRAMMAR_1 f = nil assert_nothing_raised { f = g.follow } assert_equal(Set['a', 'b', 'c', END_OF_INPUT], f['A']) assert_equal(Set['a', 'b', 'c', END_OF_INPUT], f['B']) assert_equal(Set['a', 'b', 'c', END_OF_INPUT], f['C']) assert_equal(Set[END_OF_INPUT], f['D']) assert_equal(Set[END_OF_INPUT], f['S']) end TEST_GRAMMAR_2 = <<-EOG S - Ed E - EpT E - EmT E - T T - TuF T - TdF T - F F - i F - n F - aEz EOG def test_first_2 g = Grammar.new TEST_GRAMMAR_2 f = nil assert_nothing_raised { f = g.first } assert_equal(Set['a', 'n', 'i'], f['E']) assert_equal(Set['a', 'n', 'i'], f['F']) assert_equal(Set['a', 'n', 'i'], f['T']) assert_equal(Set['a', 'n', 'i'], f['S']) end def test_follow_2 g = Grammar.new TEST_GRAMMAR_2 f = nil assert_nothing_raised { f = g.follow } assert_equal(Set['m', 'd', 'z', 'p'], f['E']) assert_equal(Set['m', 'd', 'z', 'p', 'u'], f['F']) assert_equal(Set['m', 'd', 'z', 'p', 'u'], f['T']) assert_equal(Set[END_OF_INPUT], f['S']) end LLError = Grammar::LLError TEST_GRAMMAR_3 = <<-EOG E - TD D - pTD D - T - FS S - uFS S - S - p F - aEz F - i EOG NoError = Class.new(Exception) def test_first_3 g = Grammar.new TEST_GRAMMAR_3 # Grammar 3 is LL(1), so all first-sets must be disjoint. f = nil assert_nothing_raised { f = g.first } assert_equal(Set['a', 'i'], f['E']) assert_equal(Set[EPSILON, 'p'], f['D']) assert_equal(Set['a', 'i'], f['F']) assert_equal(Set['a', 'i'], f['T']) assert_equal(Set[EPSILON, 'u', 'p'], f['S']) for m in g.meta_symbols r = g.rules[m] firsts = r.map { |x| f[x] }.to_set assert_nothing_raised do firsts.inject(Set.new) do |already_used, another_first_set| raise LLError, 'not disjoint!' unless already_used.disjoint? another_first_set already_used.merge another_first_set end end end end def test_follow_3 g = Grammar.new TEST_GRAMMAR_3 # Grammar 3 is not LL(1), because epsilon is in FIRST(S), # but FIRST(S) and FOLLOW(S) are not disjoint. f = nil assert_nothing_raised { f = g.follow } assert_equal(Set['z', END_OF_INPUT], f['E']) assert_equal(Set['z', END_OF_INPUT], f['D']) assert_equal(Set['z', 'p', 'u', END_OF_INPUT], f['F']) assert_equal(Set['p', 'z', END_OF_INPUT], f['T']) assert_equal(Set['p', 'z', END_OF_INPUT], f['S']) for m in g.meta_symbols first_m = g.first[m] next unless first_m.include? EPSILON assert_raise(m == 'S' ? LLError : NoError) do if first_m.disjoint? f[m] raise NoError # this is fun :D else raise LLError end end end end TEST_GRAMMAR_3b = <<-EOG E - TD D - pTD D - PTD D - T - FS S - uFS S - F - aEz F - i P - p EOG def test_first_3b g = Grammar.new TEST_GRAMMAR_3b # Grammar 3b is NOT LL(1), since not all first-sets are disjoint. f = nil assert_nothing_raised { f = g.first } assert_equal(Set['a', 'i'], f['E']) assert_equal(Set[EPSILON, 'p'], f['D']) assert_equal(Set['p'], f['P']) assert_equal(Set['a', 'i'], f['F']) assert_equal(Set['a', 'i'], f['T']) assert_equal(Set[EPSILON, 'u'], f['S']) for m in g.meta_symbols r = g.rules[m] firsts = r.map { |x| f[x] } assert_raise(m == 'D' ? LLError : NoError) do firsts.inject(Set.new) do |already_used, another_first_set| raise LLError, 'not disjoint!' unless already_used.disjoint? another_first_set already_used.merge another_first_set end raise NoError end end end def test_follow_3b g = Grammar.new TEST_GRAMMAR_3b # Although Grammar 3b is NOT LL(1), the FOLLOW-condition is satisfied. f = nil assert_nothing_raised { f = g.follow } assert_equal(fifo('z$'), f['E'], 'E') assert_equal(fifo('z$'), f['D'], 'D') assert_equal(fifo('ai'), f['P'], 'P') assert_equal(fifo('z$pu'), f['F'], 'F') assert_equal(fifo('z$p'), f['T'], 'T') assert_equal(fifo('z$p'), f['S'], 'S') for m in g.meta_symbols first_m = g.first[m] next unless first_m.include? EPSILON assert_raise(NoError) do if first_m.disjoint? f[m] raise NoError # this is fun :D else raise LLError end end end end def test_ll1? assert_equal false, Grammar.new(TEST_GRAMMAR_3).ll1?, 'Grammar 3' assert_equal false, Grammar.new(TEST_GRAMMAR_3b).ll1?, 'Grammar 3b' end def test_new assert_nothing_raised { Grammar.new '' } assert_nothing_raised { Grammar.new TEST_GRAMMAR_1 } assert_nothing_raised { Grammar.new TEST_GRAMMAR_2 } assert_nothing_raised { Grammar.new TEST_GRAMMAR_3 } assert_nothing_raised { Grammar.new TEST_GRAMMAR_1 + TEST_GRAMMAR_2 + TEST_GRAMMAR_3 } assert_raise(ArgumentError) { Grammar.new 'S - ?' } end end # vim:foldmethod=syntax #!/usr/bin/env ruby require 'fox12' include Fox class Window < FXMainWindow def initialize(app) super(app, app.appName + ": First Set Calculation", nil, nil, DECOR_ALL, 0, 0, 800, 600, 0, 0) # {{{ menubar menubar = FXMenuBar.new(self, LAYOUT_SIDE_TOP|LAYOUT_FILL_X) filemenu = FXMenuPane.new(self) FXMenuCommand.new(filemenu, "&Start\tCtl-S\tStart the application.", nil, getApp()).connect(SEL_COMMAND, method(:start)) FXMenuCommand.new(filemenu, "&Quit\tAlt-F4\tQuit the application.", nil, getApp(), FXApp::ID_QUIT) FXMenuTitle.new(menubar, "&File", nil, filemenu) # }}} menubar # {{{ statusbar @statusbar = FXStatusBar.new(self, LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|STATUSBAR_WITH_DRAGCORNER) # }}} statusbar # {{{ window content horizontalsplitt = FXSplitter.new(self, SPLITTER_VERTICAL|LAYOUT_SIDE_TOP|LAYOUT_FILL) @productions = FXList.new(horizontalsplitt, nil, 0, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FIX_HEIGHT|LIST_SINGLESELECT) @productions.height = 100 @result = FXTable.new(horizontalsplitt, nil, 0, LAYOUT_FILL) @result.height = 200 @result.setTableSize(2, 2, false) @result.rowHeaderWidth = 0 header = @result.columnHeader header.setItemText 0, 'X' header.setItemText 1, 'FIRST(X)' for item in header item.justification = FXHeaderItem::CENTER_X end @debug = FXText.new(horizontalsplitt, nil, 0, LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|LAYOUT_FIX_HEIGHT) @debug.height = 200 # }}} window content end def load_grammar grammar @tracer = FirstTracer.new(self) @grammar = Grammar.new grammar, @tracer @rules_indexes = Hash.new @grammar.rules.each_with_index do |rule, i| @productions.appendItem rule.inspect @rules_indexes[rule] = i end end def create super show(PLACEMENT_SCREEN) end def rule rule @productions.selectItem @rules_indexes[rule] sleep 0.1 end def iterate i setTitle i.to_s sleep 0.1 end def missing what @debug.appendText what + "\n" sleep 0.1 end def start sender, sel, pointer Thread.new do begin @grammar.first rescue => boom @debug.appendText [boom.to_s, *boom.backtrace].join("\n") end end end end $: << 'grammar' require 'grammar' require 'first_tracer' app = FXApp.new("Shinobu", "cYcnus") # fenster erzeugen window = Window.new app unless ARGV.empty? grammar = File.read(ARGV.first) else grammar = <<-EOG1 Z --> S S --> Sb S --> bAa A --> aSc A --> a A --> aSb EOG1 end window.load_grammar grammar app.create app.run require 'erb' require 'ftools' require 'yaml' require 'redcloth' module WhyTheLuckyStiff class Book attr_accessor :author, :title, :terms, :image, :teaser, :chapters, :expansion_paks, :encoding, :credits def [] x @lang.fetch(x) do warn warning = "[not translated: '#{x}'!]" warning end end end def Book::load( file_name ) YAML::load( File.open( file_name ) ) end class Section attr_accessor :index, :header, :content def initialize( i, h, c ) @index, @header, @content = i, h, RedCloth::new( c.to_s ) end end class Sidebar attr_accessor :title, :content end YAML::add_domain_type( 'whytheluckystiff.net,2003', 'sidebar' ) do |taguri, val| YAML::object_maker( Sidebar, 'title' => val.keys.first, 'content' => RedCloth::new( val.values.first ) ) end class Chapter attr_accessor :index, :title, :sections def initialize( i, t, sects ) @index = i @title = t i = 0 @sections = sects.collect do |s| if s.respond_to?( :keys ) i += 1 Section.new( i, s.keys.first, s.values.first ) else s end end end end YAML::add_domain_type( 'whytheluckystiff.net,2003', 'book' ) do |taguri, val| ['chapters', 'expansion_paks'].each do |chaptype| i = 0 val[chaptype].collect! do |c| i += 1 Chapter::new( i, c.keys.first, c.values.first ) end end val['teaser'].collect! do |t| Section::new( 1, t.keys.first, t.values.first ) end val['terms'] = RedCloth::new( val['terms'] ) YAML::object_maker( Book, val ) end class Image attr_accessor :file_name end YAML::add_domain_type( 'whytheluckystiff.net,2003', 'img' ) do |taguri, val| YAML::object_maker( Image, 'file_name' => "i/" + val ) end end # # Convert the book to HTML # if __FILE__ == $0 unless ARGV[0] puts "Usage: #{$0} [/path/to/save/html]" exit end site_path = ARGV[0] book = WhyTheLuckyStiff::Book::load( 'poignant.yml' ) chapter = nil # Write index page index_tpl = ERB::new( File.open( 'index.erb' ).read ) File.open( File.join( site_path, 'index.html' ), 'w' ) do |out| out << index_tpl.result end book.chapters = book.chapters[0,3] if ARGV.include? '-fast' # Write chapter pages chapter_tpl = ERB::new( File.open( 'chapter.erb' ).read ) book.chapters.each do |chapter| File.open( File.join( site_path, "chapter-#{ chapter.index }.html" ), 'w' ) do |out| out << chapter_tpl.result end end exit if ARGV.include? '-fast' # Write expansion pak pages expak_tpl = ERB::new( File.open( 'expansion-pak.erb' ).read ) book.expansion_paks.each do |pak| File.open( File.join( site_path, "expansion-pak-#{ pak.index }.html" ), 'w' ) do |out| out << expak_tpl.result( binding ) end end # Write printable version print_tpl = ERB::new( File.open( 'print.erb' ).read ) File.open( File.join( site_path, "print.html" ), 'w' ) do |out| out << print_tpl.result end # Copy css + images into site copy_list = ["guide.css"] + Dir["i/*"].find_all { |image| image =~ /\.(gif|jpg|png)$/ } File.makedirs( File.join( site_path, "i" ) ) copy_list.each do |copy_file| File.copy( copy_file, File.join( site_path, copy_file ) ) end end #!/usr/bin/env ruby require 'fox' begin require 'opengl' rescue LoadError require 'fox/missingdep' MSG = <(side) self.num <=> side.num end def init_facelet(pos, *side_nums) sides = side_nums.map { |num| @sides[num] }.sort @fl_by_side[sides] = pos end def []=(color, *sides) @facelets[@fl_by_side[sides.sort]].color = color end def values_at(*sides) sides.map { |sides| @facelets[@fl_by_side[sides.sort]] } end def inspect(range=nil) if range @facelets.values_at(*(range.to_a)).join(' ') else <<-EOS.gsub(/\d/) { |num| @facelets[num.to_i] }.gsub(/[ABCD]/) { |side| @sides[side[0]-?A].num.to_s } A 0 1 2 D 3 4 5 B 6 7 8 C EOS end end def get_edge(side) trio = (-1..1).map { |x| (side + x) % 4 } prev_side, this_side, next_side = @sides.values_at(*trio) e = Edge.new( self .values_at( [this_side], [this_side, next_side] ) + this_side.values_at( [self, prev_side], [self ], [self, next_side] ) ) #puts 'Edge created for side %d: ' % side + e.inspect e end def turn(dir) #p 'turn side %d in %d' % [num, dir] edges = (0..3).map { |n| get_edge n } for i in 0..3 edges[i].apply edges[(i-dir) % 4] end end end class Cube def initialize @sides = [] %w(left front right back top bottom).each_with_index { |side, i| eval("@sides[#{i}] = @#{side} = Side.new(#{i})") } @left.sides = [@top, @front, @bottom, @back] @front.sides = [@top, @right, @bottom, @left] @right.sides = [@top, @back, @bottom, @front] @back.sides = [@top, @left, @bottom, @right] @top.sides = [@back, @right, @front, @left] @bottom.sides = [@front, @right, @back, @left] end def read_facelets(fs) pattern = Regexp.new(<<-EOP.gsub(/\w/, '\w').gsub(/\s+/, '\s*')) (w w w) (w w w) (w w w) (r r r) (g g g) (b b b) (o o o) (r r r) (g g g) (b b b) (o o o) (r r r) (g g g) (b b b) (o o o) (y y y) (y y y) (y y y) EOP md = pattern.match(fs).to_a @top.facelets = parse_facelets(md.values_at(1,2,3)) @left.facelets = parse_facelets(md.values_at(4,8,12)) @front.facelets = parse_facelets(md.values_at(5,9,13)) @right.facelets = parse_facelets(md.values_at(6,10,14)) @back.facelets = parse_facelets(md.values_at(7,11,15)) @bottom.facelets = parse_facelets(md.values_at(16,17,18)) end def turn(side, dir) #p 'turn %d in %d' % [side, dir] @sides[side].turn(dir) #puts inspect end def inspect <<-EOF.gsub(/(\d):(\d)-(\d)/) { @sides[$1.to_i].inspect(Range.new($2.to_i, $3.to_i)) } 4:0-2 4:3-5 4:6-8 0:0-2 1:0-2 2:0-2 3:0-2 0:3-5 1:3-5 2:3-5 3:3-5 0:6-8 1:6-8 2:6-8 3:6-8 5:0-2 5:3-5 5:6-8 EOF end private def parse_facelets(rows) rows.join.delete(' ').split(//) end end #$stdin = DATA gets.to_i.times do |i| puts "Scenario ##{i+1}:" fs = '' 9.times { fs << gets } cube = Cube.new cube.read_facelets fs gets.to_i.times do |t| side, dir = gets.split.map {|s| s.to_i} cube.turn(side, dir) end puts cube.inspect puts end # 2004 by murphy # GPL class Scenario class TimePoint attr_reader :data def initialize *data @data = data end def [] i @data[i] or 0 end include Comparable def <=> tp r = 0 [@data.size, tp.data.size].max.times do |i| r = self[i] <=> tp[i] return r if r.nonzero? end 0 end def - tp r = [] [@data.size, tp.data.size].max.times do |i| r << self[i] - tp[i] end r end def inspect # 01/01/1800 00:00:00 '%02d/%02d/%04d %02d:%02d:%02d' % @data.values_at(1, 2, 0, 3, 4, 5) end end ONE_HOUR = TimePoint.new 0, 0, 0, 1, 0, 0 APPOINTMENT_PATTERN = / ( \d{4} ) \s ( \d{2} ) \s ( \d{2} ) \s ( \d{2} ) \s ( \d{2} ) \s ( \d{2} ) \s ( \d{4} ) \s ( \d{2} ) \s ( \d{2} ) \s ( \d{2} ) \s ( \d{2} ) \s ( \d{2} ) /x def initialize io @team_size = io.gets.to_i @data = [ [TimePoint.new(1800, 01, 01, 00, 00, 00), @team_size] ] @team_size.times do # each team member io.gets.to_i.times do # each appointment m = APPOINTMENT_PATTERN.match io.gets @data << [TimePoint.new(*m.captures[0,6].map { |x| x.to_i }), -1] @data << [TimePoint.new(*m.captures[6,6].map { |x| x.to_i }), +1] end end @data << [TimePoint.new(2200, 01, 01, 00, 00, 00), -@team_size] end def print_time_plan n = 0 appointment = nil no_appointment = true @data.sort_by { |x| x[0] }.each do |x| tp, action = *x n += action # at any time during the meeting, at least two team members need to be there # and at most one team member is allowed to be absent if n >= 2 and (@team_size - n) <= 1 appointment ||= tp else if appointment # the meeting should be at least one hour in length if TimePoint.new(*(tp - appointment)) >= ONE_HOUR puts 'appointment possible from %p to %p' % [appointment, tp] no_appointment = false end appointment = false end end end puts 'no appointment possible' if no_appointment end end # read the data DATA.gets.to_i.times do |si| # each scenario puts 'Scenario #%d:' % (si + 1) sc = Scenario.new DATA sc.print_time_plan puts end #__END__ 2 3 3 2002 06 28 15 00 00 2002 06 28 18 00 00 TUD Contest Practice Session 2002 06 29 10 00 00 2002 06 29 15 00 00 TUD Contest 2002 11 15 15 00 00 2002 11 17 23 00 00 NWERC Delft 4 2002 06 25 13 30 00 2002 06 25 15 30 00 FIFA World Cup Semifinal I 2002 06 26 13 30 00 2002 06 26 15 30 00 FIFA World Cup Semifinal II 2002 06 29 13 00 00 2002 06 29 15 00 00 FIFA World Cup Third Place 2002 06 30 13 00 00 2002 06 30 15 00 00 FIFA World Cup Final 1 2002 06 01 00 00 00 2002 06 29 18 00 00 Preparation of Problem Set 2 1 1800 01 01 00 00 00 2200 01 01 00 00 00 Solving Problem 8 0 require 'token_consts' require 'symbol' require 'ctype' require 'error' class Fixnum # Treat char as a digit and return it's value as Fixnum. # Returns nonsense for non-digits. # Examples: # # RUBY_VERSION[0].digit == '1.8.2'[0].digit == 1 # # # # ?6.digit == 6 # # # # ?A.digit == 17 # def digit self - ?0 end end ## # Stellt einen einfachen Scanner für die lexikalische Analyse der Sprache Pas-0 dar. # # @author Andreas Kunert # Ruby port by murphy class Scanner include TokenConsts attr_reader :line, :pos # To allow Scanner.new without parameters. DUMMY_INPUT = 'dummy file' def DUMMY_INPUT.getc nil end ## # Erzeugt einen Scanner, der als Eingabe das übergebene IO benutzt. def initialize input = DUMMY_INPUT @line = 1 @pos = 0 begin @input = input @next_char = @input.getc rescue IOError # TODO show the reason! Error.ioError raise end end ## # Liest das nchste Zeichen von der Eingabe. def read_next_char begin @pos += 1 @current_char = @next_char @next_char = @input.getc rescue IOError Error.ioError raise end end ## # Sucht das nächste Symbol, identifiziert es, instantiiert ein entsprechendes # PascalSymbol-Objekt und gibt es zurück. # @see Symbol # @return das gefundene Symbol als PascalSymbol-Objekt def get_symbol current_symbol = nil until current_symbol read_next_char if @current_char.alpha? identifier = @current_char.chr while @next_char.alpha? or @next_char.digit? identifier << @next_char read_next_char end current_symbol = handle_identifier(identifier.upcase) elsif @current_char.digit? current_symbol = number else case @current_char when ?\s # ignore when ?\n new_line when nil current_symbol = PascalSymbol.new EOP when ?{ comment when ?: if @next_char == ?= read_next_char current_symbol = PascalSymbol.new BECOMES else current_symbol = PascalSymbol.new COLON end when ?< if (@next_char == ?=) read_next_char current_symbol = PascalSymbol.new LEQSY elsif (@next_char == ?>) read_next_char current_symbol = PascalSymbol.new NEQSY else current_symbol = PascalSymbol.new LSSSY end when ?> if (@next_char == ?=) read_next_char current_symbol = PascalSymbol.new GEQSY else current_symbol = PascalSymbol.new GRTSY end when ?. then current_symbol = PascalSymbol.new PERIOD when ?( then current_symbol = PascalSymbol.new LPARENT when ?, then current_symbol = PascalSymbol.new COMMA when ?* then current_symbol = PascalSymbol.new TIMES when ?/ then current_symbol = PascalSymbol.new SLASH when ?+ then current_symbol = PascalSymbol.new PLUS when ?- then current_symbol = PascalSymbol.new MINUS when ?= then current_symbol = PascalSymbol.new EQLSY when ?) then current_symbol = PascalSymbol.new RPARENT when ?; then current_symbol = PascalSymbol.new SEMICOLON else Error.error(100, @line, @pos) if @current_char > ?\s end end end current_symbol end private ## # Versucht, in dem gegebenen String ein Schlüsselwort zu erkennen. # Sollte dabei ein Keyword gefunden werden, so gibt er ein PascalSymbol-Objekt zurück, das # das entsprechende Keyword repräsentiert. Ansonsten besteht die Rückgabe aus # einem SymbolIdent-Objekt (abgeleitet von PascalSymbol), das den String 1:1 enthält # @see symbol # @return falls Keyword gefunden, zugehöriges PascalSymbol, sonst SymbolIdent def handle_identifier identifier if sym = KEYWORD_SYMBOLS[identifier] PascalSymbol.new sym else SymbolIdent.new identifier end end MAXINT = 2**31 - 1 MAXINT_DIV_10 = MAXINT / 10 MAXINT_MOD_10 = MAXINT % 10 ## # Versucht, aus dem gegebenen Zeichen und den folgenden eine Zahl zusammenzusetzen. # Dabei wird der relativ intuitive Algorithmus benutzt, die endgültige Zahl bei # jeder weiteren Ziffer mit 10 zu multiplizieren und diese dann mit der Ziffer zu # addieren. Sonderfälle bestehen dann nur noch in der Behandlung von reellen Zahlen. #
# Treten dabei kein Punkt oder ein E auf, so gibt diese Methode ein SymbolIntCon-Objekt # zurück, ansonsten (reelle Zahl) ein SymbolRealCon-Objekt. Beide Symbole enthalten # jeweils die Zahlwerte. #
# Anmerkung: Diese Funktion ist mit Hilfe der Java/Ruby-API deutlich leichter zu realisieren. # Sie wurde dennoch so implementiert, um den Algorithmus zu demonstrieren # @see symbol # @return SymbolIntcon- oder SymbolRealcon-Objekt, das den Zahlwert enthält def number is_integer = true integer_too_long = false exponent = 0 exp_counter = -1 exp_sign = 1 integer_mantisse = @current_char.digit while (@next_char.digit? and integer_mantisse < MAXINT_DIV_10) or (integer_mantisse == MAXINT_DIV_10 and @next_char.digit <= MAXINT_MOD_10) integer_mantisse *= 10 integer_mantisse += @next_char.digit read_next_char end real_mantisse = integer_mantisse while @next_char.digit? integer_too_long = true real_mantisse *= 10 real_mantisse += @next_char.digit read_next_char end if @next_char == ?. read_next_char is_integer = false unless @next_char.digit? Error.error 101, @line, @pos end while @next_char.digit? real_mantisse += @next_char.digit * (10 ** exp_counter) read_next_char exp_counter -= 1 end end if @next_char == ?E is_integer = false read_next_char if @next_char == ?- exp_sign = -1 read_next_char end unless @next_char.digit? Error.error 101, @line, @pos end while @next_char.digit? exponent *= 10 exponent += @next_char.digit read_next_char end end if is_integer if integer_too_long Error.error 102, @line, @pos end SymbolIntcon.new integer_mantisse else SymbolRealcon.new real_mantisse * (10 ** (exp_sign * exponent)) end end ## # Sorgt für ein Überlesen von Kommentaren. # Es werden einfach alle Zeichen bis zu einer schließenden Klammer eingelesen # und verworfen. def comment while @current_char != ?} forbid_eop new_line if @current_char == ?\n read_next_char end end def new_line @line += 1 @pos = 0 end def forbid_eop if eop? Error.error 103, @line, @pos end exit end def eop? @current_char.nil? end end ## # Läßt ein Testprogramm ablaufen. # Dieses erzeugt sich ein Scanner-Objekt und ruft an diesem kontinuierlich bis zum Dateiende # get_symbol auf. if $0 == __FILE__ scan = Scanner.new(File.new(ARGV[0] || 'test.pas')) loop do c = scan.get_symbol puts c break if c.typ == TokenConsts::EOP end end