# The ScopeHandler class, and its friends, are wrapper around the # "scope rich", "xml style" output of the current document that # TextMate[www.macromates.com] provides. ScopeHandler processes the file # with the help of ScopefileParser, and creates a ScopeTree containing all # the structure. The nodes of the tree are instances of ScopeNode. # (Feel free to) use at your own risk. # # Version:: 1.2.2 # Copyright:: Charilaos Skiadas # Homepage:: http://skiadas.dcostanet.net/afterthought/the-scopehandler-class/ # # Revision history: # [4/12/06] Initial commit. # [4/13/06] Added RDoc support. # [4/14/06] Added nodesWithScope method. # [4/15/06] Added firstChild/nextChild and nextScope methods. # [11/5/06] Simplified the parsing considerably. Still very inefficient. # # ----- # # The ScopeHandler class is the class you would use to access the information. # # Usage: # require 'scopeHandler.rb' # handler = ScopeHandler.new(STDIN) # # do stuff with handler # class ScopeHandler include Enumerable # You can pass either an IO object, like STDIN, or the string directly def initialize(file) if file.respond_to?(:read) data = file.read # Case where they pass us an IO object, like STDIN else data = file # Case where they pass us the string directly, like STDIN.read end parser = ScopefileParser.new(data) @scopeTree = ScopeTree.new(parser) end # Return the ScopeNode instance corresponding to the location. # +location+ has the form +[line,column]+. def nodeForTextLocation(location) desiredLine, desiredColumn = *location currentLine, currentColumn = 1, 1 textNodes = @scopeTree.find_all{|i| i.text?} node = textNodes.each{|i| currentLine += i.data.count("\n") return i if currentLine > desiredLine next if currentLine < desiredLine currentColumn += (i.data.split("\n")[-1] || [] ).length return i if currentColumn >= desiredColumn # pp i.data, currentLine, currentColumn } return node end # Return the ScopeTree instance that contains all the scope nodes. def tree @scopeTree end # Iterate over all nodes of the tree, calling the block on each node. def each @scopeTree.each{|node| yield node} end # Return the array +[line,column]+ corresponding to the location where # the scope associated to he node starts. def coordinatesForNode(node) node.coords end # Return the deepest node that contains the +selection+ text and is _active_ # at the +location+. def upScope(selection, location) selection = "" unless selection selection = selection.data if selection.respond_to?(:data) node = nodeForTextLocation(location) until (node.scopeName == :root) || ((node.data.include? selection) && (node.data != selection)) node = node.parent end return node end # Return the node that is _previous_ to the smallest scope at the +location+ # containing the +selection+ text. def previousScope(selection, location) selection = "" unless selection return selection.previous if selection.respond_to?(:previous) node = nodeForTextLocation(location).parent node = node.previous if node.data == selection return node end # Return all nodes whose scope matches the +string+. A +Regexp object can be used instead. def nodesWithScope(scope) if scope.kind_of?(String) nodes = self.find_all{|node| node.scopeName.kind_of?(String) \ && node.scopeName.include?(scope)} else if scope.kind_of?(Regexp) nodes = self.find_all{|node| node.scopeName.kind_of?(String) \ && (scope =~ node.scopeName)} else raise "scope must be a string or regular expression." end end end end # The ScopefileParser class is essentially a lexer, scanning through the file # and returning each token when asked. This is done via the #pull method # # Usage: # parser = ScopefileParser.new(data) # while parser.eof? # token = parser.pull # # do things with token # end # # Alternatively: # parser = ScopefileParser.new(data) # parser.each do |token| # # do things with token # end # class ScopefileParser include Enumerable attr_reader :data # Create a ScopefileParser. data needs to be a string def initialize(data) raise "data is nil." unless data @data = [] data.scan(/<(\/)?([^>]*)>|([^<]*)(?=<)/) do |piece| if !$3.nil? @data << {:type => :text, :value => $3} elsif $1.nil? @data << {:type => :scopeBegin, :value => $2} else @data << {:type => :scopeEnd, :value => $2} end end @originalData = data end # Reset the parser. Start from the beginning again. def reset @data = @originalData end # End of file. def eof? @data.empty? end # Apply block to each token in order. def each # :yields: token # reset # until eof? @data.each do |i| yield i end end end # ScopeNode class. Nodes come in three flavors: # * The root node. +scopeName+ is equal to :root. # * Scope nodes, corresponding to a particular scope. +scopeName+ is # equal to the scope string. +data+ contains all text corresponding # to this scope. # * Text nodes, corresponding to chunks of text. +scopeName+ is :text. # class ScopeNode include Enumerable # The node's children. Includes text nodes. Text nodes don't have children. attr_accessor :children # The node's parent. The root's parent is +nil+. attr_accessor :parent # The name of the scope. Equal to :root for the root node, and # to :text for the text nodes. attr_accessor :scopeName # A string containing all text that this scope covers. attr_accessor :data # An array +[line,column]+ containing the location where the scope starts. attr_accessor :coords def initialize(parent = nil) @children = Array.new @parent = parent @data = "" @scopeName = nil @coords = Array.new end # Return the depth of the node. def depth return 0 if self.scopeName == :root return self.parent.depth + 1 end # A node is _simple_ if it has no children, or if it has one child which is # itself simple. def simple? case parent.children.length when 0 return true when 1 return parent.children[0].simple? else return false end end def not_simple? return !self.simple? end # Return whether +self+ is the first child of its parent. def first_child? self.parent.children.index(self) == 0 end # Return whether +self+ is the last child of its parent. def last_child? self.parent.children.index(self) == self.parent.children.length-1 end # Return the first child of +self+. def firstChild self.first end # Return the last child of +self+. def lastChild self.last end # Return the first child of +self+. Deprecated, use #firstChild instead. def first self.children.first end # Return the last child of +self+. Deprecated, use #lastChild instead. def last self.children.last end # Returns the previous sibling, or +nil+ if it is the first child. def previousSibling return self.parent.children.previous(self) end # Returns the next sibling, or +nil+ if it is the last child. def nextSibling return self.parent.children.next(self) end # Return the node previous to +self+. def previous return self.previousSibling if !self.first_child? candidate = self.parent.previous until !candidate.last do candidate = candidate.last end return candidate end # Return the node next to +self+. def next return self.nextSibling if !self.last_child? candidate = self.parent.next until !candidate.first do candidate = candidate.first end return candidate end # Apply block to +self+ and recursively to each of its descendants. def each yield self for child in @children child.each{|node| yield node} end end # Return whether the node is a text node. def text? @scopeName == :text end # A string representation of the node, with lots of useful information. # Use for debugging. def to_s "scopeName: #{@scopeName} line:#{@coords[0]} column:#{@coords[1]} data: #{@data} children: #{@children.length}" end # Add +newNode+ as the last child to +self+. def add(newNode) @children << newNode newNode.parent = self end end # # The ScopeTree class is a tree structure consisting of ScopeNode's. # class ScopeTree include Enumerable # The root of the tree. attr_accessor :root # Create a new ScopeTree using an instance of ScopeParser to generate the # nodes. def initialize(parser) @root = ScopeNode.new @root.scopeName = :root parse(parser) end def parse(parser) line = 1 column = 1 @node = @root @root.coords = [line,column] parser.each{ |i| case i[:type] when :scopeBegin newNode = ScopeNode.new(@node) newNode.scopeName = i[:value] newNode.coords = [line,column] @node.add(newNode) @node = newNode when :scopeEnd raise "Invalid closing scope: #{i[:value]}" unless @node.scopeName == i[:value] parent = @node.parent parent.data << @node.data @node = parent when :text newNode = ScopeNode.new(@node) newNode.scopeName = :text data = i[:value] newNode.data = data newNode.coords = [line,column] @node.data << newNode.data @node.add(newNode) newlines = data.count("\n") line += newlines if newlines == 0 column += data.length else column = data.scan(/[^\n]*\z/).length+1 end else raise "Unexpected type: #{i[:type]}" end } raise "Incomplete tree" unless @node == @root end # Call the block on each node of the tree. def each @root.each{|node| yield node} end private :parse end class Array # Returns the previous item to its argument. def previous(item) theindex = self.index(item) return nil if theindex == 0 return self[theindex-1] end def next(item) theindex = self.index(item) return nil if theindex == self.length - 1 return self[theindex+1] end end class String # Escape self for xml use, i.e. ampersands and greater than/less than signs are converted to their corresponding entities. def escapeScope self.gsub('&','&').gsub('<','<').gsub('>','>') end # Unescape self from xml format, i.e. the entities for ampersands and greater than/less than signs are converted to the corresponding characters. def unescapeScope self.gsub('<','<').gsub('>','>').gsub('&','&') end end # # # For testing purposes: # # file = File.open("testing2.xml") # handler = ScopeHandler.new(file.read) # file.close # require 'pp' # handler.each{|node| pp node.next.scopeName; pp node.previous.scopeName}