lib/nanoc3/base/item_rep.rb in nanoc3-3.0.9 vs lib/nanoc3/base/item_rep.rb in nanoc3-3.1.0a1

- old
+ new

@@ -1,73 +1,76 @@ # encoding: utf-8 module Nanoc3 - # A Nanoc3::ItemRep is a single representation (rep) of an item - # (Nanoc3::Item). An item can have multiple representations. A representation - # has its own output file. A single item can therefore have multiple output - # files, each run through a different set of filters with a different - # layout. + # A single representation (rep) of an item ({Nanoc3::Item}). An item can + # have multiple representations. A representation has its own output file. + # A single item can therefore have multiple output files, each run through + # a different set of filters with a different layout. # # An item representation is observable. The following events will be # notified: # - # * :compilation_started - # * :compilation_ended - # * :filtering_started - # * :filtering_ended + # * `:compilation_started` + # * `:compilation_ended` + # * `:filtering_started` + # * `:filtering_ended` # # The compilation-related events have one parameters (the item # representation); the filtering-related events have two (the item # representation, and a symbol containing the filter class name). class ItemRep - # The item (Nanoc3::Item) to which this representation belongs. + # @return [Nanoc3::Item] The item to which this rep belongs attr_reader :item - # This item representation's unique name. + # @return [Symbol] The representation's unique name attr_reader :name - # Indicates whether this rep is forced to be dirty by the user. + # @return [Boolean] true if this rep is forced to be dirty (e.g. because + # of the `--force` commandline option); false otherwise attr_accessor :force_outdated - # Indicates whether this rep's output file has changed the last time it - # was compiled. + # @return [Boolean] true if this rep’s output file has changed since the + # last time it was compiled; false otherwise attr_accessor :modified alias_method :modified?, :modified - # Indicates whether this rep's output file was created the last time it - # was compiled. + # @return [Boolean] true if this rep’s output file was created during the + # current or last compilation session; false otherwise attr_accessor :created alias_method :created?, :created - # Indicates whether this rep has already been compiled. + # @return [Boolean] true if this representation has already been + # compiled during the current or last compilation session; false + # otherwise attr_accessor :compiled alias_method :compiled?, :compiled - # Indicates whether this rep's compiled content has been written during - # the current or last compilation session. + # @return [Boolean] true if this representation’s compiled content has + # been written during the current or last compilation session; false + # otherwise attr_reader :written alias_method :written?, :written - # The item rep's path, as used when being linked to. It starts with a - # slash and it is relative to the output directory. It does not include - # the path to the output directory. It will not include the filename if - # the filename is an index filename. + # @return [String] The item rep's path, as used when being linked to. It + # starts with a slash and it is relative to the output directory. It + # does not include the path to the output directory. It will not include + # the filename if the filename is an index filename. attr_accessor :path - # The item rep's raw path. It is relative to the current working directory - # and includes the path to the output directory. It also includes the - # filename, even if it is an index filename. + # @return [String] The item rep's raw path. It is relative to the current + # working directory and includes the path to the output directory. It + # also includes the filename, even if it is an index filename. attr_accessor :raw_path # Creates a new item representation for the given item. # - # +item+:: The item (Nanoc3::Item) to which the new representation will - # belong. + # @param [Nanoc3::Item] item The item to which the new representation will + # belong. # - # +name+:: The unique name for the new item representation. + # @param [Symbol] name The unique name for the new item representation. def initialize(item, name) # Set primary attributes @item = item @name = name @@ -75,21 +78,22 @@ @content = { :raw => @item.raw_content, :last => @item.raw_content, :pre => @item.raw_content } + @old_content = nil # Reset flags @compiled = false @modified = false @created = false @written = false @force_outdated = false end - # Returns true if this item rep's output file is outdated and must be - # regenerated, false otherwise. + # @return [Boolean] true if this item rep's output file is outdated and + # must be regenerated, false otherwise def outdated? # Outdated if we don't know return true if @item.mtime.nil? # Outdated if the dependency tracker says so @@ -124,11 +128,12 @@ return true if @item.site.rules_mtime > compiled_mtime return false end - # Returns the assignments that should be available when compiling the content. + # @return [Hash] The assignments that should be available when compiling + # the content. def assigns { :content => @content[:last], :item => self.item, :item_rep => self, @@ -137,26 +142,58 @@ :config => self.item.site.config, :site => self.item.site } end - # Returns the item representation content at the given snapshot. + # Returns the compiled content from a given snapshot. # - # +snapshot+:: The snapshot from which the content should be fetched. To - # get the raw, uncompiled content, use +:raw+. - def content_at_snapshot(snapshot=:pre) + # @option params [String] :snapshot The name of the snapshot from + # which to fetch the compiled content. By default, the returned compiled + # content will be the content compiled right before the first layout + # call (if any). + def compiled_content(params={}) + # Notify Nanoc3::NotificationCenter.post(:visit_started, self.item) Nanoc3::NotificationCenter.post(:visit_ended, self.item) + # Debug puts "*** Attempting to fetch content for #{self.inspect}" if $DEBUG + # Require compilation raise Nanoc3::Errors::UnmetDependency.new(self) unless compiled? - @content[snapshot] + # Get name of last pre-layout snapshot + snapshot_name = params[:snapshot] + if @content[:pre] + snapshot_name ||= :pre + else + snapshot_name ||= :last + end + + # Get content + @content[snapshot_name] end + # @deprecated Use {Nanoc3::ItemRep#compiled_content} instead. + def content_at_snapshot(snapshot=:pre) + compiled_content(:snapshot => snapshot) + end + # Runs the item content through the given filter with the given arguments. + # This method will replace the content of the `:last` snapshot with the + # filtered content of the last snapshot. + # + # This method is supposed to be called only in a compilation rule block + # (see {Nanoc3::CompilerDSL#compile}). + # + # @param [Symbol] filter_name The name of the filter to run the item + # representations' content through + # + # @param [Hash] filter_args The filter arguments that should be passed to + # the filter's #run method + # + # @return [void] def filter(filter_name, filter_args={}) # Create filter klass = Nanoc3::Filter.named(filter_name) raise Nanoc3::Errors::UnknownFilter.new(filter_name) if klass.nil? filter = klass.new(assigns) @@ -168,30 +205,29 @@ # Create snapshot snapshot(@content[:post] ? :post : :pre) end - # Lays out the item using the given layout. + # Lays out the item using the given layout. This method will replace the + # content of the `:last` snapshot with the laid out content of the last + # snapshot. + # + # This method is supposed to be called only in a compilation rule block + # (see {Nanoc3::CompilerDSL#compile}). + # + # @param [String] layout_identifier The identifier of the layout the ite + # should be laid out with + # + # @return [void] def layout(layout_identifier) - # Get layout - layout ||= @item.site.layouts.find { |l| l.identifier == layout_identifier.cleaned_identifier } - raise Nanoc3::Errors::UnknownLayout.new(layout_identifier) if layout.nil? + # Create "pre" snapshot + snapshot(:pre) unless @content[:pre] - # Get filter - filter_name, filter_args = @item.site.compiler.filter_for_layout(layout) - raise Nanoc3::Errors::CannotDetermineFilter.new(layout_identifier) if filter_name.nil? - - # Get filter class - filter_class = Nanoc3::Filter.named(filter_name) - raise Nanoc3::Errors::UnknownFilter.new(filter_name) if filter_class.nil? - # Create filter - filter = filter_class.new(assigns.merge({ :layout => layout })) + layout = layout_with_identifier(layout_identifier) + filter, filter_name, filter_args = filter_for_layout(layout) - # Create "pre" snapshot - snapshot(:pre) - # Layout @item.site.compiler.stack.push(layout) Nanoc3::NotificationCenter.post(:filtering_started, self, filter_name) @content[:last] = filter.run(layout.raw_content, filter_args) Nanoc3::NotificationCenter.post(:filtering_ended, self, filter_name) @@ -200,36 +236,111 @@ # Create "post" snapshot snapshot(:post) end # Creates a snapshot of the current compiled item content. + # + # @param [Symbol] snapshot_name The name of the snapshot to create + # + # @return [void] def snapshot(snapshot_name) @content[snapshot_name] = @content[:last] end # Writes the item rep's compiled content to the rep's output file. + # + # This method should not be called directly, even in a compilation block; + # the compiler is responsible for calling this method. + # + # @return [void] def write # Create parent directory FileUtils.mkdir_p(File.dirname(self.raw_path)) # Check if file will be created @created = !File.file?(self.raw_path) # Remember old content if File.file?(self.raw_path) - old_content = File.read(self.raw_path) + @old_content = File.read(self.raw_path) end # Write File.open(self.raw_path, 'w') { |io| io.write(@content[:last]) } @written = true # Check if file was modified - @modified = File.read(self.raw_path) != old_content + @modified = File.read(self.raw_path) != @old_content end + # Creates and returns a diff between the compiled content before the + # current compilation session and the content compiled in the current + # compilation session. + # + # @return [String, nil] The difference between the old and new compiled + # content in `diff(1)` format, or nil if there is no previous compiled + # content + def diff + # Check if old content exists + if @old_content.nil? or self.raw_path.nil? + nil + else + diff_strings(@old_content, @content[:last]) + end + end + def inspect "<#{self.class}:0x#{self.object_id.to_s(16)} name=#{self.name} item.identifier=#{self.item.identifier}>" + end + + private + + def layout_with_identifier(layout_identifier) + layout ||= @item.site.layouts.find { |l| l.identifier == layout_identifier.cleaned_identifier } + raise Nanoc3::Errors::UnknownLayout.new(layout_identifier) if layout.nil? + layout + end + + def filter_for_layout(layout) + # Get filter name and args + filter_name, filter_args = @item.site.compiler.filter_for_layout(layout) + raise Nanoc3::Errors::CannotDetermineFilter.new(layout_identifier) if filter_name.nil? + + # Get filter class + filter_class = Nanoc3::Filter.named(filter_name) + raise Nanoc3::Errors::UnknownFilter.new(filter_name) if filter_class.nil? + + # Create filter + filter = filter_class.new(assigns.merge({ :layout => layout })) + + # Done + [ filter, filter_name, filter_args ] + end + + def diff_strings(a, b) + # TODO Rewrite this string-diffing method in pure Ruby. It should not + # use the "diff" executable, because this will most likely not work on + # operating systems without it, such as Windows. + + require 'tempfile' + require 'open3' + + # Create files + old_file = Tempfile.new('old') + new_file = Tempfile.new('new') + + # Write files + old_file.write(a) + new_file.write(b) + + # Diff + stdin, stdout, stderr = Open3.popen3('diff', '-u', old_file.path, new_file.path) + result = stdout.read + result == '' ? nil : result + rescue Errno::ENOENT + warn 'Failed to run `diff`, so no diff with the previously compiled ' \ + 'content will be available.' + nil end end end