# (The MIT License) # # Copyright (c) 2007 Dizan Vasquez # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # 'Software'), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # Copyright (c) 2009 Michael Fellinger # # Textpow is the underlying library for the UltraViolet syntax-highlighting engine. # It appears to be under MIT license, at least that is the only license I could find. # # So I simply included it into VER and modified it to: # * Work faster and more efficient # * Use the builtin Nokogiri capabilities of Ruby 1.9 # * Parse Plists with Nokogiri. # # There is no dependency on Nokogiri in VER itself, as we ship the converted # files as JSON, which is in Ruby stdlib and suited very well for transporting # and processing Plists compared to XML or YAML. module Textpow class Processor def start_parsing(name) end def end_parsing(name) end def new_line(line) end def open_tag(name, pos) end def close_tag(name, mark) end end ParsingError = Class.new(RuntimeError) class SyntaxProxy def initialize proxy, syntax @proxy, @syntax = proxy, syntax @proxy_value = nil end def method_missing(method, *args, &block) if @proxy @proxy_value ||= proxy if @proxy_value @proxy_value.send(method, *args, &block) else # warn "Failed proxying #{@proxy}.#{method}(#{args.join(', ')})" end else super end end def proxy case @proxy when /^#(?<proxy>.+)/ return unless @syntax.repository @syntax.repository[$~[:proxy]] when "$self", "$base" @syntax else @syntax.syntaxes[@proxy] end end end class SyntaxNode OPTIONS = {} #:options => Oniguruma::OPTION_CAPTURE_GROUP} @@syntaxes = {} attr_accessor :processor, :syntax, :firstLineMatch, :foldingStartMarker, :foldingStopMarker, :match, :begin, :content, :fileTypes, :name, :contentName, :end, :scopeName, :keyEquivalent, :captures, :beginCaptures, :endCaptures, :repository, :patterns def self.load(filename, name_space = :default) filename = filename.to_s table = case filename when /(\.tmSyntax|\.plist)$/ Plist::parse_xml(filename) when /\.json$/i JSON.load(File.read(filename)) when /\.ya?ml$/i YAML.load_file(filename) else raise ArgumentError, "Unknown filename extension" end SyntaxNode.new(table, nil, name_space) if table end def initialize(hash, syntax = nil, name_space = :default) @name_space = name_space prepare_scope_name(hash['scopeName']) @syntax = syntax || self hash.each do |key, value| case key when "firstLineMatch", "foldingStartMarker", "foldingStopMarker", "match", "begin" begin send("#{key}=", Regexp.new(value)) rescue ArgumentError, RegexpError => exception warn "Parsing error in %p => %p: %s" % [key, value, exception] end when "content", "fileTypes", "name", "contentName", "end", "scopeName", "keyEquivalent" send("#{key}=", value) when "captures", "beginCaptures", "endCaptures" send("#{key}=", value.sort) when "repository" parse_repository value when "patterns" create_children value else $stderr.puts "Ignoring: #{key} => #{value.to_s.gsub("\n", "\n>>")}" if $DEBUG end end end def prepare_scope_name(scopeName) @@syntaxes[@name_space] ||= {} return unless scopeName @@syntaxes[@name_space][scopeName] = self end def syntaxes @@syntaxes[@name_space] end def parse(string, processor = Processor.new) processor.start_parsing scopeName stack = [[self, nil]] string.each_line do |line| parse_line(stack, line, processor) end processor.end_parsing self.scopeName processor end def parse_repository(repository) @repository = {} repository.each do |key, value| if include = value["include"] @repository[key] = SyntaxProxy.new(include, self.syntax) else @repository[key] = SyntaxNode.new(value, self.syntax, @name_space) end end end def create_children(patterns) @patterns = [] syntax = self.syntax patterns.each do |pattern| if include = pattern["include"] @patterns << SyntaxProxy.new(include, syntax) else @patterns << SyntaxNode.new(pattern, syntax, @name_space) end end end def parse_captures(name, pattern, match, processor) all_starts = [] all_ends = [] pattern.match_captures(name, match).each do |group, range, match_name| range_first = range.first next unless range_first range_last = range.last next if range_first == range_last all_starts << [range_first, group, match_name] all_ends << [range_last, -group, match_name] end starts = all_starts.sort.reverse ends = all_ends.sort.reverse until starts.empty? && ends.empty? if starts.empty? pos, key, name = ends.pop processor.close_tag name, pos elsif ends.empty? pos, key, name = starts.pop processor.open_tag name, pos elsif ends.last[1].abs < starts.last[1] pos, key, name = ends.pop processor.close_tag name, pos else pos, key, name = starts.pop processor.open_tag name, pos end end end def match_captures(name, match) matches = [] if captures = send(name) captures.each do |key, value| if key =~ /^\d*$/ key = key.to_i matches << [key, match.offset(key), value["name"]] if key < match.size else key = key.to_sym match_to_key = match.to_index(key) matches << [match_to_key, match.offset(key), value["name"]] if match_to_key end end end matches end def match_first(string, position) if self.match if match = self.match.match(string, position) return [self, match] end elsif self_begin = self.begin if match = self_begin.match(string, position) return [self, match] end elsif self.end else return match_first_son(string, position) end nil end def match_end(string, match, position) regstring = self.end.clone regstring.gsub!(/\\([1-9])/){ match[$1.to_i] } regstring.gsub!(/\\k<(.*?)>/){ match[$1.to_sym] } regstring = '\\\\' if regstring == '\\' Regexp.new(regstring).match(string, position) end def match_first_son(string, position) return unless patterns match = nil patterns.each do |pattern| tmatch = pattern.match_first(string, position) next unless tmatch if !match || match[1].offset(0).first > tmatch[1].offset(0).first match = tmatch end end return match end def parse_line(stack, line, processor) processor.new_line(line) top, match = stack.last position = 0 while true if top.patterns pattern, pattern_match = top.match_first_son(line, position) else pattern, pattern_match = nil end end_match = nil if top.end end_match = top.match_end(line, match, position) end if end_match && ( !pattern_match || pattern_match.offset(0).first >= end_match.offset(0).first ) pattern_match = end_match pattern_match_first_offset = pattern_match.offset(0) start_pos = pattern_match_first_offset.first end_pos = pattern_match_first_offset.last top_contentName = top.contentName processor.close_tag top_contentName, start_pos if top_contentName parse_captures "captures", top, pattern_match, processor parse_captures "endCaptures", top, pattern_match, processor top_name = top.name processor.close_tag top_name, end_pos if top_name stack.pop top, match = stack.last else break unless pattern start_pos = pattern_match.offset(0).first end_pos = pattern_match.offset(0).last pattern_name = pattern.name if pattern.begin processor.open_tag pattern_name, start_pos if pattern_name parse_captures "captures", pattern, pattern_match, processor parse_captures "beginCaptures", pattern, pattern_match, processor pattern_contentName = pattern.contentName processor.open_tag pattern_contentName, end_pos if pattern_contentName top = pattern match = pattern_match stack << [top, match] elsif pattern.match processor.open_tag pattern_name, start_pos if pattern_name parse_captures "captures", pattern, pattern_match, processor processor.close_tag pattern_name, end_pos if pattern_name end end if position >= end_pos # raise "Parser didn't move forward on line: %p" % [line] return else position = end_pos end end end end end