# 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}