lib/jsus/source_file.rb in jsus-0.3.6 vs lib/jsus/source_file.rb in jsus-0.4.0

- old
+ new

@@ -6,145 +6,118 @@ # SourceFile is a base for any Jsus operation. # # It contains general info about source as well as file content. # class SourceFile - # Filename relative to package root - attr_accessor :relative_filename - # Full filename - attr_accessor :filename # Package owning the sourcefile + # Is not directly used in SourceFile, but might be useful for introspection. attr_accessor :package + # Original filename (immutable) + attr_accessor :original_filename + + # Full filename (when initialized from file) + attr_accessor :filename + alias_method :path, :filename + alias_method :path=, :filename= + + # Original source code (immutable) + attr_reader :original_source + + # Source code (mutable) + attr_accessor :source + + # Default namespace for source + attr_reader :namespace + + # Constructors # Basic constructor. # - # You probably should use SourceFile.from_file instead of this one. - # + # Initializes a file from source. + # @param [String] source original source for the file # @param [Hash] options - # @option options [Jsus::Package] :package package to assign source file to. - # @option options [String] :relative_filename used in Package to generate - # tree structure of the source files - # @option options [String] :filename full filename for the given source file - # @option options [String] :content file content of the source file - # @option options [Jsus::Pool] :pool owner pool for that file - # @option options [String] :header header of the file + # @option options [String] :namespace source file namespace # @api semipublic - def initialize(options = {}) - [:package, :header, :relative_filename, :filename, :content, :pool].each do |field| - send("#{field}=", options[field]) if options[field] - end + def initialize(source, options = {}) + @namespace = options[:namespace] + @original_source = source.dup + prepare_original_source + @source = @original_source.dup + parse_header + @original_source.freeze end # # Initializes a SourceFile given the filename and options # # @param [String] filename # @param [Hash] options - # @option options [Jsus::Pool] :pool owning pool - # @option options [Jsus::Package] :package owning package + # @option options [String] :namespace namespace to which the source file by default belongs # @return [Jsus::SourceFile] - # @raise [Jsus::BadSourceFileException] when file cannot be parsed + # @raise [Jsus::BadSourceFileException] when file cannot be parsed or does not exist # @api public 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 + filename = File.expand_path(filename) + raise BadSourceFileException, "File does not exist." unless File.exists?(filename) + source = Jsus::Util.read_file(filename) + source_file = new(source, options) + source_file.filename = source_file.original_filename = filename + source_file.original_filename.freeze + source_file 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 + e.message.sub! /^/, "Unexpected exception happened while processing #{filename}: " + Jsus.logger.error e.message + raise e end - # Public API - # @return [Hash] a header parsed from YAML-formatted source file first comment. # @api public def header - self.header = {} unless @header - @header + @header ||= {} end # @return [String] description of the source file. # @api public def description header["description"] end - # @return [Array] list of dependencies for given file - # @api public - def dependencies - @dependencies - end - alias_method :requires, :dependencies + # @return [String] license of source file + def license + header["license"] + end # license - # - # @param [Hash] options - # @option options [Boolean] :short whether inner dependencies should not - # prepend package name, e.g. 'Class' instead of 'Core/Class' when in - # package 'Core'. - # - # Note Doesn't change anything for external dependencies - # - # @return [Array] array with names of dependencies. Unordered. + # @return [Array] list of authors # @api public - def dependencies_names(options = {}) - dependencies.map {|d| d.name(options) } - end - alias_method :requires_names, :dependencies_names + def authors + @authors + end # authors - # @return [Array] array of external dependencies tags. Unordered. + # @return [Array] list of dependencies for given file # @api public - def external_dependencies - dependencies.select {|d| d.external? } + def requires + @requires end + alias_method :dependencies, :requires + alias_method :requirements, :requires - # @returns [Array] array with names for external dependencies. Unordered. - # @api public - def external_dependencies_names - external_dependencies.map {|d| d.name } - end - # @return [Array] array with provides tags. # @api public def provides @provides end + alias_method :provisions, :provides - # @param [Hash] options - # @option options [Boolean] :short whether provides should not prepend package - # name, e.g. 'Class' instead of 'Core/Class' when in package 'Core'. - # @return [Array] array with provides names. - # @api public - def provides_names(options = {}) - provides.map {|p| p.name(options)} - end # @return [Jsus::Tag] tag for replaced file, if any # @api public def replaces @replaces end - # @returns [Jsus::Tag] tag for source file, for which this one is an extension. # @example 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. # @api public @@ -156,150 +129,48 @@ # @api public def extension? extends && !extends.empty? end - # @return [Array] new_value array of included extensions for given source. + # @return [Boolean] whether the source file is an extension. # @api public - def extensions - @extensions ||= [] - @extensions = @extensions.flatten.compact.uniq - @extensions - end + def replacement? + replaces && !replaces.empty? + end # replacement? - # @param [Array] new_value list of extensions for given file - # @api semipublic - def extensions=(new_value) - @extensions = new_value - end + # @api private + def reset + @source = @original_source.dup + @filename = @original_filename.dup if @original_filename + end # reset_linked - # Looks up for extensions in the pool and then includes - # extensions for all the provides tag this source file has. - # Caches the result. - # - # @api semipublic - def include_extensions - @included_extensions ||= include_extensions! - end - - # @see #include_extensions - # @api semipublic - def include_extensions! - if pool - provides.each do |p| - extensions << pool.lookup_extensions(p) - end - end - end - # @return [Array] array of files required by this files including all the filenames for extensions. # SourceFile filename always goes first, all the extensions are unordered. # @api public def required_files - include_extensions - [filename, extensions.map {|e| e.filename}].flatten + [filename].flatten end # @return [Hash] hash containing basic info with dependencies/provides tags' names # and description for source file. # # @api public def to_hash { "desc" => description, - "requires" => dependencies_names(:short => true), - "provides" => provides_names(:short => true) + "requires" => requires.map {|tag| tag.namespace == namespace ? tag.name : tag.full_name}, + "provides" => provides.map {|tag| tag.name} } end # Human readable description of source file. # @return [String] # @api public def inspect - self.to_hash.inspect + self.to_hash.merge("namespace" => namespace).inspect end - # Parses header and gets info from it. - # @param [String] new_header header content - # @api private - def header=(new_header) - @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 - - # @param [String] new_value file content - # @api private - def content=(new_value) - @content = new_value - end - - # @return [String] file contents, *including* extensions - # @api semipublic - def content - include_extensions - [@content, extensions.map {|e| e.content}].flatten.compact.join("\n") - end - - # @return [String] Original file contents - # @api semipublic - def original_content - @content - end - - # @param [Enumerable] tag_list list of tags - # @return [Array] normalized tags list - # @api private - 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. - # @param [Jsus::Pool] new_value - # @api private - 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! - # @return [Jsus::Pool] - # @api semipublic - def pool - @pool - end - # @api public def ==(other) eql?(other) end @@ -310,7 +181,57 @@ # @api public def hash [self.class, filename].hash end + + private + + # @api private + def prepare_original_source + bom = RUBY_VERSION[/1.9/] ? "\uFEFF" : "\xEF\xBB\xBF" + original_source.gsub!(bom, "") + end # prepare_original_source + + # @api private + def parse_header + yaml_data = source.match(%r(^/\*\s*(---.*?)\*/)m) + if yaml_data && yaml_data[1] && header = YAML.load(yaml_data[1]) + @header = header + @authors = Array(@header["author"] || @header["authors"]) + @requires = process_tag_list(@header["requires"]) + @provides = process_tag_list(@header["provides"]) + @replaces = process_tag(@header["replaces"]) if @header["replaces"] + @extends = process_tag(@header["extends"]) if @header["extends"] + else + raise BadSourceFileException, "#{filename} is missing a header or header is invalid" + end + end # parse_header + + # @api private + def process_tag(tag_name) + if tag_name.kind_of?(String) + tag_name.sub!(%r{^\.?/}, "") # remove leading slash / dot+slash + if tag_name.index("/") || !namespace + Tag[tag_name] + else + Tag["#{namespace}/#{tag_name}"] + end + elsif tag_name.kind_of?(Hash) + # Quirky mootools tags + ns, tag = tag_name.first[0], tag_name.first[1] + # Removes strings like "/1.3.0" from the end of namespace part + ns = ns.sub(%r{/(\d+\.?)+\d+$}, "") + ns = Util::Inflection.random_case_to_mixed_case(ns) + "#{ns}/#{tag}" + else + nil + end + end # process_tag + + # @api private + def process_tag_list(tag_list) + Array(tag_list).map {|tag| process_tag(tag) }.compact + end # process_tag_list + end end