lib/nanoc3/base/item_rep.rb in nanoc3-3.1.9 vs lib/nanoc3/base/item_rep.rb in nanoc3-3.2.0a1
- old
+ new
@@ -4,30 +4,17 @@
# 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`
- #
- # 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 descriptive strings for each outdatedness reason. This hash is used
# by the {#outdatedness_reason} method.
OUTDATEDNESS_REASON_DESCRIPTIONS = {
- :no_mtime => 'No file modification time is available.',
- :forced => 'All pages are recompiled because of a `--force` flag given to the compilation command.',
- :no_raw_path => 'The routing rules do not specify a path where this item should be written to, i.e. the item representation will never be written to the output directory.',
+ :not_enough_data => 'Not enough data is present to correctly determine whether the item is outdated.',
+ :forced => 'All items are recompiled because of a `--force` flag given to the compilation command.',
:not_written => 'This item representation has not yet been written to the output directory (but it does have a path).',
:source_modified => 'The source file of this item has been modified since the last time this item representation was compiled.',
:layouts_outdated => 'The source of one or more layouts has been modified since the last time this item representation was compiled.',
:code_outdated => 'The code snippets in the `lib/` directory have been modified since the last time this item representation was compiled.',
:config_outdated => 'The site configuration has been modified since the last time this item representation was compiled.',
@@ -39,72 +26,60 @@
# @return [Symbol] The representation's unique name
attr_reader :name
# @return [Boolean] true if this rep is forced to be dirty (e.g. because
- # of the `--force` commandline option); false otherwise
+ # of the `--force` commandline option); false otherwise
attr_accessor :force_outdated
# @return [Boolean] true if this rep is currently binary; false otherwise
attr_reader :binary
alias_method :binary?, :binary
- # @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
-
- # @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
-
# @return [Boolean] true if this representation has already been compiled
- # during the current or last compilation session; false otherwise
+ # during the current or last compilation session; false otherwise
attr_accessor :compiled
alias_method :compiled?, :compiled
- # @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
+ # @return [Hash<Symbol,String>] A hash containing the raw paths (paths
+ # including the path to the output directory and the filename) for all
+ # snapshots. The keys correspond with the snapshot names, and the values
+ # with the path.
+ attr_accessor :raw_paths
- # @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
+ # @return [Hash<Symbol,String>] A hash containing the paths for all
+ # snapshots. The keys correspond with the snapshot names, and the values
+ # with the path.
+ attr_accessor :paths
- # @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
+ # @return [Hash<Symbol,String>] A hash containing the content at all
+ # snapshots. The keys correspond with the snapshot names, and the
+ # values with the content.
+ attr_accessor :content
# Creates a new item representation for the given item.
#
# @param [Nanoc3::Item] item The item to which the new representation will
- # belong.
+ # belong.
#
# @param [Symbol] name The unique name for the new item representation.
def initialize(item, name)
# Set primary attributes
@item = item
@name = name
# Set binary
@binary = @item.binary?
- # Initialize content and filenames
- initialize_content
+ # Initialize content and filenames and paths
+ @raw_paths = {}
+ @paths = {}
@old_content = nil
+ initialize_content
# Reset flags
@compiled = false
- @modified = false
- @created = false
- @written = false
@force_outdated = false
end
# Calculates the reason why this item representation is outdated. The
# output will be a hash with a `:type` key, containing the reason why the
@@ -118,44 +93,31 @@
# outdated, both in the form of a symbol and as a descriptive string, or
# nil if the item representation is not outdated.
def outdatedness_reason
# Get reason symbol
reason = lambda do
- # Outdated if we don't know
- return :no_mtime if @item.mtime.nil?
-
- # Outdated if the dependency tracker says so
+ # Outdated if we’re compiling with --force
return :forced if @force_outdated
+ # Outdated if checksums are missing
+ if !@item.old_checksum || !@item.new_checksum
+ return :not_enough_data
+ end
+
# Outdated if compiled file doesn't exist (yet)
- return :no_raw_path if self.raw_path.nil?
- return :not_written if !File.file?(self.raw_path)
+ return :not_written if self.raw_path && !File.file?(self.raw_path)
- # Get compiled mtime
- compiled_mtime = File.stat(self.raw_path).mtime
-
# Outdated if file too old
- return :source_modified if @item.mtime > compiled_mtime
-
- # Outdated if layouts outdated
- return :layouts_outdated if @item.site.layouts.any? do |l|
- l.mtime.nil? || l.mtime > compiled_mtime
+ if @item.old_checksum != @item.new_checksum
+ return :source_modified
end
- # Outdated if code outdated
- return :code_outdated if @item.site.code_snippets.any? do |cs|
- cs.mtime.nil? || cs.mtime > compiled_mtime
- end
+ # Outdated if other site parts outdated
+ return :code_outdated if @item.site.code_snippets.any? { |cs| cs.outdated? }
+ return :config_outdated if @item.site.config_outdated?
+ return :rules_outdated if @item.site.rules_outdated?
- # Outdated if config outdated
- return :config_outdated if @item.site.config_mtime.nil?
- return :config_outdated if @item.site.config_mtime > compiled_mtime
-
- # Outdated if rules outdated
- return :rules_outdated if @item.site.rules_mtime.nil?
- return :rules_outdated if @item.site.rules_mtime > compiled_mtime
-
return nil
end[]
# Build reason symbol and description
if reason.nil?
@@ -167,17 +129,17 @@
}
end
end
# @return [Boolean] true if this item rep's output file is outdated and
- # must be regenerated, false otherwise
+ # must be regenerated, false otherwise
def outdated?
!outdatedness_reason.nil?
end
# @return [Hash] The assignments that should be available when compiling
- # the content.
+ # the content.
def assigns
if self.binary?
content_or_filename_assigns = { :filename => @filenames[:last] }
else
content_or_filename_assigns = { :content => @content[:last] }
@@ -194,26 +156,23 @@
end
# Returns the compiled content from a given snapshot.
#
# @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).
+ # fetch the compiled content. By default, the returned compiled content
+ # will be the content compiled right before the first layout call (if
+ # any).
#
# @return [String] The compiled content at the given snapshot (or the
- # default snapshot if no snapshot is specified)
+ # default snapshot if no snapshot is specified)
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?
+ raise Nanoc3::Errors::UnmetDependency.new(self) if !compiled? && !params[:force]
# Get name of last pre-layout snapshot
snapshot_name = params[:snapshot]
if @content[:pre]
snapshot_name ||= :pre
@@ -221,17 +180,61 @@
snapshot_name ||= :last
end
# Check presence of snapshot
if @content[snapshot_name].nil?
- warn "WARNING: The “#{self.item.identifier}” item (rep “#{self.name}”) does not have the requested snapshot named #{snapshot_name.inspect}.\n\n* Make sure that you are requesting the correct snapshot.\n* It is not possible to request the compiled content of a binary item representation; if this item is marked as binary even though you believe it should be textual, you may need to add the extension of this item to the site configuration’s `text_extensions` array.".make_compatible_with_env
+ warn(('-' * 78 + "\nWARNING: The “#{self.item.identifier}” item (rep “#{self.name}”) does not have the requested snapshot named #{snapshot_name.inspect}.\n\n* Make sure that you are requesting the correct snapshot.\n* It is not possible to request the compiled content of a binary item representation; if this item is marked as binary even though you believe it should be textual, you may need to add the extension of this item to the site configuration’s `text_extensions` array.\n" + '-' * 78).make_compatible_with_env)
end
# Get content
@content[snapshot_name]
end
+ # Checks whether content exists at a given snapshot.
+ #
+ # @return [Boolean] True if content exists for the snapshot with the
+ # given name, false otherwise
+ def has_snapshot?(snapshot_name)
+ !@content[snapshot_name].nil?
+ end
+
+ # Returns the item rep’s raw path. It includes the path to the output
+ # directory and the full filename.
+ #
+ # @option params [Symbol] :snapshot (:last) The snapshot for which the
+ # path should be returned
+ #
+ # @return [String] The item rep’s path
+ def raw_path(params={})
+ snapshot_name = params[:snapshot] || :last
+ @raw_paths[snapshot_name]
+ end
+
+ # Returns 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.
+ #
+ # @option params [Symbol] :snapshot (:last) The snapshot for which the
+ # path should be returned
+ #
+ # @return [String] The item rep’s path
+ def path(params={})
+ snapshot_name = params[:snapshot] || :last
+ @paths[snapshot_name]
+ end
+
+ # @deprecated Modify the {#raw_paths} attribute instead
+ def raw_path=(raw_path)
+ raw_paths[:last] = raw_path
+ end
+
+ # @deprecated Modify the {#paths} attribute instead
+ def path=(path)
+ paths[:last] = path
+ end
+
# @deprecated Use {Nanoc3::ItemRep#compiled_content} instead.
def content_at_snapshot(snapshot=:pre)
compiled_content(:snapshot => snapshot)
end
@@ -250,14 +253,14 @@
#
# 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
+ # representations' content through
#
# @param [Hash] filter_args The filter arguments that should be passed to
- # the filter's #run method
+ # the filter's #run method
#
# @return [void]
def filter(filter_name, filter_args={})
# Get filter class
klass = filter_named(filter_name)
@@ -290,143 +293,180 @@
raise RuntimeError,
"The #{filter_name.inspect} filter did not write anything to the required output file, #{filter.output_filename}."
end
# Create snapshot
- snapshot(@content[:post] ? :post : :pre) unless self.binary?
+ snapshot(@content[:post] ? :post : :pre, :final => false) unless self.binary?
end
# 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 item
- # should be laid out with
+ # should be laid out with
#
# @return [void]
def layout(layout_identifier)
# Check whether item can be laid out
raise Nanoc3::Errors::CannotLayoutBinaryItem.new(self) if self.binary?
# Create "pre" snapshot
- snapshot(:pre) unless @content[:pre]
+ if @content[:post].nil?
+ snapshot(:pre, :final => true)
+ end
# Create filter
layout = layout_with_identifier(layout_identifier)
filter, filter_name, filter_args = filter_for_layout(layout)
+ # Visit
+ Nanoc3::NotificationCenter.post(:visit_started, layout)
+ Nanoc3::NotificationCenter.post(:visit_ended, layout)
+
# 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)
@item.site.compiler.stack.pop
# Create "post" snapshot
- snapshot(:post)
+ snapshot(:post, :final => false)
end
# Creates a snapshot of the current compiled item content.
#
# @param [Symbol] snapshot_name The name of the snapshot to create
#
+ # @option params [Boolean] :final (true) True if this is the final time
+ # the snapshot will be updated; false if it is a non-final moving
+ # snapshot (such as `:pre`, `:post` or `:last`)
+ #
# @return [void]
- def snapshot(snapshot_name)
- target = self.binary? ? @filenames : @content
- target[snapshot_name] = target[:last]
+ def snapshot(snapshot_name, params={})
+ # Parse params
+ params[:final] = true if !params.has_key?(:final)
+
+ # Create snapshot
+ @content[snapshot_name] = @content[:last] unless self.binary?
+
+ # Write
+ write(snapshot_name) if params[:final]
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.
#
+ # @param [String, nil] raw_path The raw path to write the compiled rep to.
+ # If nil, the default raw path will be used.
+ #
# @return [void]
- def write
+ def write(snapshot=:last)
+ # Get raw path
+ raw_path = self.raw_path(:snapshot => snapshot)
+ return if raw_path.nil?
+
# Create parent directory
- FileUtils.mkdir_p(File.dirname(self.raw_path))
+ FileUtils.mkdir_p(File.dirname(raw_path))
# Check if file will be created
- @created = !File.file?(self.raw_path)
+ is_created = !File.file?(raw_path)
- if self.binary?
- # Calculate hash of old content
- if File.file?(self.raw_path)
- hash_old = hash_for_file(self.raw_path)
- size_old = File.size(self.raw_path)
- end
- size_new = File.size(@filenames[:last])
- hash_new = hash_for_file(@filenames[:last]) if size_old == size_new
+ # Calculate characteristics of old content
+ if File.file?(raw_path)
+ hash_old = Nanoc3::Checksummer.checksum_for(raw_path)
+ size_old = File.size(raw_path)
+ end
- # Check if file was modified
- @modified = (size_old != size_new || hash_old != hash_new)
-
+ if self.binary?
# Copy
- if @modified
- FileUtils.cp(@filenames[:last], self.raw_path)
- end
- @written = true
+ FileUtils.cp(@filenames[:last], raw_path)
else
- # Remember old content
- if File.file?(self.raw_path)
- @old_content = File.read(self.raw_path)
- end
-
# Write
- new_content = @content[:last]
- if @old_content != new_content
- File.open(self.raw_path, 'w') { |io| io.write(new_content) }
- end
- @written = true
+ File.open(raw_path, 'w') { |io| io.write(@content[:last]) }
# Generate diff
generate_diff
-
- # Check if file was modified
- @modified = File.read(self.raw_path) != @old_content
end
+
+ # Check if file was modified
+ size_new = File.size(raw_path)
+ hash_new = Nanoc3::Checksummer.checksum_for(raw_path) if size_old == size_new
+ is_modified = (size_old != size_new || hash_old != hash_new)
+
+ # Notify
+ Nanoc3::NotificationCenter.post(
+ :rep_written,
+ self, raw_path, is_created, is_modified
+ )
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
+ # content in `diff(1)` format, or nil if there is no previous compiled
+ # content
def diff
if self.binary?
nil
else
@diff_thread.join if @diff_thread
@diff
end
end
+ # @deprecated
+ def created
+ raise NotImplementedError, "Nanoc3::ItemRep#created is no longer implemented"
+ end
+
+ # @deprecated
+ def created?
+ raise NotImplementedError, "Nanoc3::ItemRep#created? is no longer implemented"
+ end
+
+ # @deprecated
+ def modified
+ raise NotImplementedError, "Nanoc3::ItemRep#modified is no longer implemented"
+ end
+
+ # @deprecated
+ def modified?
+ raise NotImplementedError, "Nanoc3::ItemRep#modified? is no longer implemented"
+ end
+
+ # @deprecated
+ def written
+ raise NotImplementedError, "Nanoc3::ItemRep#written is no longer implemented"
+ end
+
+ # @deprecated
+ def written?
+ raise NotImplementedError, "Nanoc3::ItemRep#written? is no longer implemented"
+ end
+
def inspect
"<#{self.class}:0x#{self.object_id.to_s(16)} name=#{self.name} binary=#{self.binary?} raw_path=#{self.raw_path} item.identifier=#{self.item.identifier}>"
end
private
def initialize_content
# Initialize content and filenames
if self.binary?
- @filenames = {
- :raw => @item.raw_filename,
- :last => @item.raw_filename
- }
- @content = {}
+ @filenames = { :last => @item.raw_filename }
+ @content = {}
else
- @content = {
- :raw => @item.raw_content,
- :last => @item.raw_content,
- :pre => @item.raw_content
- }
+ @content = { :last => @item.raw_content }
@filenames = {}
end
end
def filter_named(name)
@@ -440,11 +480,11 @@
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?
+ 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?
@@ -457,11 +497,10 @@
def generate_diff
if @old_content.nil? || self.raw_path.nil? || !@item.site.config[:enable_output_diff]
@diff = nil
else
- require 'thread'
@diff_thread = Thread.new do
@diff = diff_strings(@old_content, @content[:last])
sleep 2
@diff_thread = nil
end
@@ -489,21 +528,9 @@
end
rescue Errno::ENOENT
warn 'Failed to run `diff`, so no diff with the previously compiled ' \
'content will be available.'
nil
- end
-
- # Returns a hash of the given filename
- def hash_for_file(filename)
- digest = Digest::SHA1.new
- File.open(filename, 'r') do |io|
- until io.eof
- data = io.readpartial(2**10)
- digest.update(data)
- end
- end
- digest.hexdigest
end
end
end