module Jsus # Generic exception for 'bad' source files (no yaml header, for example) class BadSourceFileException < Exception; end # # SourceFile is a base for any Jsus operation. # # It contains basic info about source as well as file content. # class SourceFile attr_accessor :relative_filename, :filename, :package # :nodoc: # Constructors # Basic constructor. # # You probably should use SourceFile.from_file instead. # # But if you know what you are doing, it accepts the following values: # * +package+ -- an instance of Package, normally passed by a parent # * +relative_filename+ -- used in Package, for generating tree structure of the source files # * +filename+ -- full filename for the given package # * +content+ -- file content of the source file # * +pool+ -- an instance of Pool def initialize(options = {}) [:package, :header, :relative_filename, :filename, :content, :pool].each do |field| send("#{field}=", options[field]) if options[field] end end # # Initializes a SourceFile given the filename and options # # options: # * :pool: -- an instance of Pool # * :package: -- an instance of Package # # returns either an instance of SourceFile or nil when it's not possible to parse the input # def self.from_file(filename, options = {}) if File.exists?(filename) source = File.open(filename, 'r:utf-8') {|f| f.read } bom = RUBY_VERSION =~ /1.9/ ? "\uFEFF" : "\xEF\xBB\xBF" source.gsub!(bom, "") yaml_data = source.match(%r(^/\*\s*(---.*?)\*/)m) if (yaml_data && yaml_data[1] && header = YAML.load(yaml_data[1])) options[:header] = header options[:relative_filename] = filename options[:filename] = File.expand_path(filename) options[:content] = source new(options) else raise BadSourceFileException, "#{filename} is missing a header or header is invalid" end else raise BadSourceFileException, "Referenced #{filename} does not exist. #{options[:package] ? "Referenced from package #{options[:package].name}" : ""}" end rescue Exception => e if !e.kind_of?(BadSourceFileException) # if we didn't raise the error; like in YAML, for example raise "Exception #{e.inspect} happened on #{filename}. Please take appropriate measures" else # if we did it, just reraise raise e end end # Public API # # Returns a header parsed from YAML-formatted source file first comment. # Contains information about authorship, naming, source files, etc. # def header self.header = {} unless @header @header end # # A string containing the description of the source file. # def description header["description"] end # # Returns an array of dependencies tags. Unordered. # def dependencies @dependencies end alias_method :requires, :dependencies # # Returns an array with names of dependencies. Unordered. # Accepts options: # * :short: -- whether inner dependencies should not prepend package name # e.g. 'Class' instead of 'Core/Class' when in package 'Core'). # Doesn't change anything for external dependencies # def dependencies_names(options = {}) dependencies.map {|d| d.name(options) } end alias_method :requires_names, :dependencies_names # # Returns an array of external dependencies tags. Unordered. # def external_dependencies dependencies.select {|d| d.external? } end # # Returns an array with names for external dependencies. Unordered. # def external_dependencies_names external_dependencies.map {|d| d.name } end # # Returns an array with provides tags. # def provides @provides end # # Returns an array with provides names. # Accepts options: # * :short: -- whether provides should not prepend package name # e.g. 'Class' instead of 'Core/Class' when in package 'Core') def provides_names(options = {}) provides.map {|p| p.name(options)} end # # Returns a tag for replaced file, if any # def replaces @replaces end # # Returns a tag for source file, which this one is an extension for. # # E.g.: file Foo.js in package Core provides ['Class', 'Hash']. File Bar.js in package Bar # extends 'Core/Class'. That means its contents would be appended to the Foo.js when compiling # the result. # def extends @extends end # # Returns whether the source file is an extension. # def extension? extends && !extends.empty? end # # Returns an array of included extensions for given source. # def extensions @extensions ||= [] @extensions = @extensions.flatten.compact.uniq @extensions end def extensions=(new_value) # :nodoc: @extensions = new_value end # # Looks up for extensions in the #pool and then includes # extensions for all the provides tag this source file has. # Caches the result. # def include_extensions @included_extensions ||= include_extensions! end def include_extensions! # :nodoc: if pool provides.each do |p| extensions << pool.lookup_extensions(p) end end end # # Returns an array of files required by this files including all the filenames for extensions. # SourceFile filename always goes first, all the extensions are unordered. # def required_files include_extensions [filename, extensions.map {|e| e.filename}].flatten end # # Returns a hash containing basic info with dependencies/provides tags' names # and description for source file. # def to_hash { "desc" => description, "requires" => dependencies_names(:short => true), "provides" => provides_names(:short => true) } end def inspect # :nodoc: self.to_hash.inspect end # Private API def header=(new_header) # :nodoc: @header = new_header # prepare defaults @header["description"] ||= "" # handle tags @dependencies = parse_tag_list(Array(@header["requires"])) @provides = parse_tag_list(Array(@header["provides"])) @extends = case @header["extends"] when Array then Tag.new(@header["extends"][0]) when String then Tag.new(@header["extends"]) else nil end @replaces = case @header["replaces"] when Array then Tag.new(@header["replaces"][0]) when String then Tag.new(@header["replaces"]) else nil end end def content=(new_value) # :nodoc: @content = new_value end def content # :nodoc: include_extensions [@content, extensions.map {|e| e.content}].flatten.compact.join("\n") end def original_content # :nodoc: @content end def parse_tag_list(tag_list) tag_list.map do |tag_name| case tag_name when String Tag.new(tag_name, :package => package) when Hash tags = [] tag_name.each do |pkg_name, sources| normalized_package_name = pkg_name.sub(/(.+)\/.*$/, "\\1") Array(sources).each do |source| tags << Tag.new([normalized_package_name, source].join("/")) end end tags end end.flatten end # parse_tag_list # Assigns an instance of Jsus::Pool to the source file. # Also performs push to that pool. def pool=(new_value) @pool = new_value @pool << self if @pool end # A pool which the source file is assigned to. Used in #include_extensions! def pool @pool end def ==(other) # :nodoc: eql?(other) end def eql?(other) # :nodoc: other.kind_of?(SourceFile) && filename == other.filename end def hash [self.class, filename].hash end end end