lib/nanoc3/base/site.rb in nanoc3-3.1.9 vs lib/nanoc3/base/site.rb in nanoc3-3.2.0a1

- old
+ new

@@ -37,17 +37,20 @@ # The default configuration for a site. A site's configuration overrides # these options: when a {Nanoc3::Site} is created with a configuration # that lacks some options, the default value will be taken from # `DEFAULT_CONFIG`. DEFAULT_CONFIG = { - :text_extensions => %w( css erb haml htm html js less markdown md php rb sass scss txt xhtml xml ), + :text_extensions => %w( css erb haml htm html js less markdown md php rb sass txt xml ), :output_dir => 'output', :data_sources => [ {} ], :index_filenames => [ 'index.html' ], :enable_output_diff => false } + # The name of the file where checksums will be stored. + CHECKSUMS_FILE_NAME = 'tmp/checksums' + # The site configuration. The configuration has the following keys: # # * `text_extensions` ({Array<String>}) - A list of file extensions that # will cause nanoc to threat the file as textual instead of binary. When # the data source finds a content file with an extension that is @@ -87,27 +90,37 @@ # would need the username of the account from which to fetch tweets. # # @return [Hash] The site configuration attr_reader :config - # @return [Time] The timestamp when the site configuration was last - # modified - attr_reader :config_mtime + # @return [String] The checksum of the site configuration that was in + # effect during the previous site compilation + attr_accessor :old_config_checksum - # @return [Time] The timestamp when the rules were last modified - attr_reader :rules_mtime + # @return [String] The current, up-to-date checksum of the site + # configuration + attr_reader :new_config_checksum + # @return [String] The checksum of the rules file that was in effect + # during the previous site compilation + attr_accessor :old_rules_checksum + + # @return [String] The current, up-to-date checksum of the rules file + attr_reader :new_rules_checksum + # @return [Proc] The code block that will be executed after all data is - # loaded but before the site is compiled + # loaded but before the site is compiled attr_accessor :preprocessor # Creates a site object for the site specified by the given # `dir_or_config_hash` argument. # # @param [Hash, String] dir_or_config_hash If a string, contains the path - # to the site directory; if a hash, contains the site configuration. + # to the site directory; if a hash, contains the site configuration. def initialize(dir_or_config_hash) + @new_checksums = {} + build_config(dir_or_config_hash) @code_snippets_loaded = false @items_loaded = false @layouts_loaded = false @@ -123,14 +136,14 @@ # Returns the data sources for this site. Will create a new data source if # none exists yet. # # @return [Array<Nanoc3::DataSource>] The list of data sources for this - # site + # site # # @raise [Nanoc3::Errors::UnknownDataSource] if the site configuration - # specifies an unknown data source + # specifies an unknown data source def data_sources @data_sources ||= begin @config[:data_sources].map do |data_source_hash| # Get data source class data_source_class = Nanoc3::DataSource.named(data_source_hash[:type]) @@ -160,11 +173,11 @@ # with the site and fetch all site data. The site data is cached, so # calling this method will not have any effect the second time, unless # the `force` parameter is true. # # @param [Boolean] force If true, will force load the site data even if it - # has been loaded before, to circumvent caching issues + # has been loaded before, to circumvent caching issues # # @return [void] def load_data(force=false) # Don't load data twice return if instance_variable_defined?(:@data_loaded) && @data_loaded && !force @@ -190,41 +203,65 @@ end # Returns this site’s code snippets. # # @return [Array<Nanoc3::CodeSnippet>] The list of code snippets in this - # site + # site # # @raise [Nanoc3::Errors::DataNotYetAvailable] if the site data hasn’t - # been loaded yet (call {#load_data} to load the site data) + # been loaded yet (call {#load_data} to load the site data) def code_snippets raise Nanoc3::Errors::DataNotYetAvailable.new('Code snippets', false) unless @code_snippets_loaded @code_snippets end # Returns this site’s items. # # @return [Array<Nanoc3::Item>] The list of items in this site # # @raise [Nanoc3::Errors::DataNotYetAvailable] if the site data hasn’t - # been loaded yet (call {#load_data} to load the site data) + # been loaded yet (call {#load_data} to load the site data) def items raise Nanoc3::Errors::DataNotYetAvailable.new('Items', true) unless @items_loaded @items end # Returns this site’s layouts. # # @return [Array<Nanoc3::Layouts>] The list of layout in this site # # @raise [Nanoc3::Errors::DataNotYetAvailable] if the site data hasn’t - # been loaded yet (call {#load_data} to load the site data) + # been loaded yet (call {#load_data} to load the site data) def layouts raise Nanoc3::Errors::DataNotYetAvailable.new('Layouts', true) unless @layouts_loaded @layouts end + # Stores the checksums into the checksums file. + # + # @return [void] + def store_checksums + # Store + FileUtils.mkdir_p(File.dirname(CHECKSUMS_FILE_NAME)) + store = PStore.new(CHECKSUMS_FILE_NAME) + store.transaction do + store[:checksums] = @new_checksums || {} + end + end + + # @return [Boolean] true if the site configuration was modified since the + # site was last compiled, false otherwise + def config_outdated? + !self.old_config_checksum || !self.new_config_checksum || self.old_config_checksum != self.new_config_checksum + end + + # @return [Boolean] true if the rules were modified since the site was + # last compiled, false otherwise + def rules_outdated? + !self.old_rules_checksum || !self.new_rules_checksum || self.old_rules_checksum != self.new_rules_checksum + end + private # Returns the Nanoc3::CompilerDSL that should be used for this site. def dsl @dsl ||= Nanoc3::CompilerDSL.new(self) @@ -238,14 +275,20 @@ # Get code snippets @code_snippets = Dir['lib/**/*.rb'].sort.map do |filename| Nanoc3::CodeSnippet.new( File.read(filename), filename, - File.stat(filename).mtime + :checksum => Nanoc3::Checksummer.checksum_for(filename) ) end + # Set checksums + @code_snippets.each do |cs| + cs.old_checksum = old_checksum_for(:code_snippet, cs.filename) + @new_checksums[ [ :code_snippet, cs.filename ] ] = cs.new_checksum + end + # Execute code snippets @code_snippets.each { |cs| cs.load } @code_snippets_loaded = true end @@ -255,12 +298,14 @@ # Find rules file rules_filename = [ 'Rules', 'rules', 'Rules.rb', 'rules.rb' ].find { |f| File.file?(f) } raise Nanoc3::Errors::NoRulesFileFound.new if rules_filename.nil? # Get rule data - @rules = File.read(rules_filename) - @rules_mtime = File.stat(rules_filename).mtime + @rules = File.read(rules_filename) + @new_rules_checksum = Nanoc3::Checksummer.checksum_for(rules_filename) + @old_rules_checksum = old_checksum_for(:misc, 'Rules') + @new_checksums[ [ :misc, 'Rules' ] ] = @new_rules_checksum # Load DSL dsl.instance_eval(@rules, "./#{rules_filename}") end @@ -272,10 +317,16 @@ items_in_ds = ds.items items_in_ds.each { |i| i.identifier = File.join(ds.items_root, i.identifier) } @items.concat(items_in_ds) end + # Set checksums + @items.each do |i| + i.old_checksum = old_checksum_for(:item, i.identifier) + @new_checksums[ [ :item, i.identifier ] ] = i.new_checksum + end + @items_loaded = true end # Loads this site’s layouts. def load_layouts @@ -284,10 +335,16 @@ layouts_in_ds = ds.layouts layouts_in_ds.each { |i| i.identifier = File.join(ds.layouts_root, i.identifier) } @layouts.concat(layouts_in_ds) end + # Set checksums + @layouts.each do |l| + l.old_checksum = old_checksum_for(:layout, l.identifier) + @new_checksums[ [ :layout, l.identifier ] ] = l.new_checksum + end + @layouts_loaded = true end # Links items, layouts and code snippets to the site. def link_everything_to_site @@ -335,56 +392,55 @@ # Determines the paths of all item representations. def route_reps reps = @items.map { |i| i.reps }.flatten reps.each do |rep| - # Find matching rule - rule = self.compiler.routing_rule_for(rep) - raise Nanoc3::Errors::NoMatchingRoutingRuleFound.new(rep) if rule.nil? + # Find matching rules + rules = self.compiler.routing_rules_for(rep) + raise Nanoc3::Errors::NoMatchingRoutingRuleFound.new(rep) if rules[:last].nil? - # Get basic path by applying matching rule - basic_path = rule.apply_to(rep) - next if basic_path.nil? - if basic_path !~ %r{^/} - raise RuntimeError, "The path returned for the #{rep.inspect} item representation, “#{basic_path}”, does not start with a slash. Please ensure that all routing rules return a path that starts with a slash.".make_compatible_with_env - end + rules.each_pair do |snapshot, rule| + # Get basic path by applying matching rule + basic_path = rule.apply_to(rep) + next if basic_path.nil? - # Get raw path by prepending output directory - rep.raw_path = self.config[:output_dir] + basic_path + # Get raw path by prepending output directory + rep.raw_paths[snapshot] = self.config[:output_dir] + basic_path - # Get normal path by stripping index filename - rep.path = basic_path - self.config[:index_filenames].each do |index_filename| - if rep.path[-index_filename.length..-1] == index_filename - # Strip and stop - rep.path = rep.path[0..-index_filename.length-1] - break + # Get normal path by stripping index filename + rep.paths[snapshot] = basic_path + self.config[:index_filenames].each do |index_filename| + if rep.paths[snapshot][-index_filename.length..-1] == index_filename + # Strip and stop + rep.paths[snapshot] = rep.paths[snapshot][0..-index_filename.length-1] + break + end end end end end # Builds the configuration hash based on the given argument. Also see - # #initialize for details. + # {#initialize} for details. def build_config(dir_or_config_hash) if dir_or_config_hash.is_a? String - # Check whether it is supported - if dir_or_config_hash != '.' - warn 'WARNING: Calling Nanoc3::Site.new with a directory that is not the current working directory is not supported. It is recommended to change the directory before calling Nanoc3::Site.new. For example, instead of Nanoc3::Site.new(\'abc\'), use Dir.chdir(\'abc\') { Nanoc3::Site.new(\'.\') }.' - end - # Read config from config.yaml in given dir config_path = File.join(dir_or_config_hash, 'config.yaml') @config = DEFAULT_CONFIG.merge(YAML.load_file(config_path).symbolize_keys) @config[:data_sources].map! { |ds| ds.symbolize_keys } - @config_mtime = File.stat(config_path).mtime + + @new_config_checksum = Nanoc3::Checksummer.checksum_for('config.yaml') + @new_checksums[ [ :misc, 'config.yaml' ] ] = @new_config_checksum else # Use passed config hash @config = DEFAULT_CONFIG.merge(dir_or_config_hash) - @config_mtime = nil + @new_config_checksum = nil end + # Build checksum + @old_config_checksum = old_checksum_for(:misc, 'config.yaml') + # Merge data sources with default data source config @config[:data_sources].map! { |ds| DEFAULT_DATA_SOURCE_CONFIG.merge(ds) } end # Returns a preprocessor context, creating one if none exists yet. @@ -393,9 +449,41 @@ :site => self, :config => self.config, :items => self.items, :layouts => self.layouts }) + end + + # Returns the checksums, loads the checksums from the cached checksums + # file first if necessary. The checksums returned is a hash in th following + # format: + # + # { + # [ :layout, '/identifier/' ] => checksum, + # [ :item, '/identifier/' ] => checksum, + # [ :code_snippet, 'lib/filename.rb' ] => checksum, + # } + def checksums + return @checksums if @checksums_loaded + + if !File.file?(CHECKSUMS_FILE_NAME) + @checksums = {} + else + require 'pstore' + store = PStore.new(CHECKSUMS_FILE_NAME) + store.transaction do + @checksums = store[:checksums] || {} + end + end + + @checksums_loaded = true + @checksums + end + + # Returns the old checksum for the given object. + def old_checksum_for(type, identifier) + key = [ type, identifier ] + checksums[key] end end end