Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow calling #write twice #1085

Merged
merged 10 commits into from Feb 17, 2017
24 changes: 18 additions & 6 deletions lib/nanoc/base/entities/item_rep.rb
Expand Up @@ -7,11 +7,11 @@ class ItemRep
attr_accessor :compiled
alias compiled? compiled

contract C::None => C::HashOf[Symbol => String]
attr_accessor :raw_paths
contract C::None => C::HashOf[Symbol => C::IterOf[String]]
attr_reader :raw_paths

contract C::None => C::HashOf[Symbol => String]
attr_accessor :paths
contract C::None => C::HashOf[Symbol => C::IterOf[String]]
attr_reader :paths

contract C::None => Nanoc::Int::Item
attr_reader :item
Expand Down Expand Up @@ -42,6 +42,18 @@ def initialize(item, name)
@modified = false
end

contract C::HashOf[Symbol => C::IterOf[String]] => self
def raw_paths=(val)
@raw_paths = val
self
end

contract C::HashOf[Symbol => C::IterOf[String]] => self
def paths=(val)
@paths = val
self
end

contract Symbol => C::Bool
def snapshot?(name)
snapshot_defs.any? { |sd| sd.name == name }
Expand All @@ -51,7 +63,7 @@ def snapshot?(name)
# Returns the item rep’s raw path. It includes the path to the output
# directory and the full filename.
def raw_path(snapshot: :last)
@raw_paths[snapshot]
@raw_paths.fetch(snapshot, []).first
end

contract C::KeywordArgs[snapshot: C::Optional[Symbol]] => C::Maybe[String]
Expand All @@ -60,7 +72,7 @@ def raw_path(snapshot: :last)
# include the path to the output directory. It will not include the
# filename if the filename is an index filename.
def path(snapshot: :last)
@paths[snapshot]
@paths.fetch(snapshot, []).first
end

# Returns an object that can be used for uniquely identifying objects.
Expand Down
24 changes: 15 additions & 9 deletions lib/nanoc/base/entities/processing_actions/snapshot.rb
Expand Up @@ -3,26 +3,32 @@ class Snapshot < Nanoc::Int::ProcessingAction
# snapshot :before_layout
# snapshot :before_layout, path: '/about.md'

attr_reader :snapshot_name
attr_reader :path
include Nanoc::Int::ContractsSupport

def initialize(snapshot_name, path)
@snapshot_name = snapshot_name
@path = path
attr_reader :snapshot_names
attr_reader :paths

contract C::IterOf[Symbol], C::IterOf[String] => C::Any
def initialize(snapshot_names, paths)
@snapshot_names = snapshot_names
@paths = paths
end

contract C::None => Array
def serialize
[:snapshot, @snapshot_name, true, @path]
[:snapshot, @snapshot_names, true, @paths]
end

NONE = Object.new

def copy(path: NONE)
self.class.new(@snapshot_name, path.equal?(NONE) ? @path : path)
contract C::KeywordArgs[snapshot_names: C::Optional[C::IterOf[Symbol]], paths: C::Optional[C::IterOf[String]]] => self
def update(snapshot_names: [], paths: [])
self.class.new(@snapshot_names + snapshot_names, @paths + paths)
end

contract C::None => String
def to_s
"snapshot #{@snapshot_name.inspect}, path: #{@path.inspect}"
"snapshot #{@snapshot_names.inspect}, paths: #{@paths.inspect}"
end
end
end
20 changes: 15 additions & 5 deletions lib/nanoc/base/entities/rule_memory.rb
Expand Up @@ -33,7 +33,7 @@ def add_layout(layout_identifier, params)
contract Symbol, C::Maybe[String] => self
def add_snapshot(snapshot_name, path)
will_add_snapshot(snapshot_name)
@actions << Nanoc::Int::ProcessingActions::Snapshot.new(snapshot_name, path)
@actions << Nanoc::Int::ProcessingActions::Snapshot.new([snapshot_name], path ? [path] : [])
self
end

Expand All @@ -47,11 +47,9 @@ def any_layouts?
@actions.any? { |a| a.is_a?(Nanoc::Int::ProcessingActions::Layout) }
end

contract C::None => Hash
contract C::None => Array
def paths
snapshot_actions.each_with_object({}) do |action, paths|
paths[action.snapshot_name] = action.path
end
snapshot_actions.map { |a| [a.snapshot_names, a.paths] }
end

# TODO: Add contract
Expand All @@ -73,6 +71,18 @@ def map
)
end

def compact_snapshots
actions = []
@actions.each do |action|
if [actions.last, action].all? { |a| a.is_a?(Nanoc::Int::ProcessingActions::Snapshot) }
actions[-1] = actions.last.update(snapshot_names: action.snapshot_names, paths: action.paths)
else
actions << action
end
end
self.class.new(@item_rep, actions: actions)
end

private

def will_add_snapshot(name)
Expand Down
7 changes: 5 additions & 2 deletions lib/nanoc/base/services/compiler/phases/recalculate.rb
Expand Up @@ -19,14 +19,17 @@ def run(rep, is_outdated:) # rubocop:disable Lint/UnusedMethodArgument

@compilation_context.snapshot_repo.set(rep, :last, rep.item.content)

@action_provider.memory_for(rep).each do |action|
actions = @action_provider.memory_for(rep)
actions.each do |action|
case action
when Nanoc::Int::ProcessingActions::Filter
executor.filter(action.filter_name, action.params)
when Nanoc::Int::ProcessingActions::Layout
executor.layout(action.layout_identifier, action.params)
when Nanoc::Int::ProcessingActions::Snapshot
executor.snapshot(action.snapshot_name)
action.snapshot_names.each do |snapshot_name|
executor.snapshot(snapshot_name)
end
else
raise Nanoc::Int::Errors::InternalInconsistency, "unknown action #{action.inspect}"
end
Expand Down
4 changes: 1 addition & 3 deletions lib/nanoc/base/services/compiler/phases/write.rb
Expand Up @@ -11,9 +11,7 @@ def initialize(snapshot_repo:, wrapped:)
def run(rep, is_outdated:)
@wrapped.run(rep, is_outdated: is_outdated)

rep.snapshot_defs.each do |sdef|
Nanoc::Int::ItemRepWriter.new.write(rep, @snapshot_repo, sdef.name)
end
Nanoc::Int::ItemRepWriter.new.write_all(rep, @snapshot_repo)
end
end
end
56 changes: 40 additions & 16 deletions lib/nanoc/base/services/item_rep_router.rb
Expand Up @@ -3,6 +3,8 @@ module Nanoc::Int
#
# @api private
class ItemRepRouter
include Nanoc::Int::ContractsSupport

class IdenticalRoutesError < ::Nanoc::Error
def initialize(output_path, rep_a, rep_b)
super("The item representations #{rep_a.inspect} and #{rep_b.inspect} are both routed to #{output_path}.")
Expand All @@ -22,34 +24,56 @@ def initialize(reps, action_provider, site)
end

def run
paths_to_reps = {}
assigned_paths = {}
@reps.each do |rep|
@action_provider.paths_for(rep).each do |snapshot_name, path|
route_rep(rep, path, snapshot_name, paths_to_reps)
# Sigh. We route reps twice, because the first time, the paths might not have converged
# yet. This isn’t ideal, but it’s the only way to work around the divergence issues that
# I can think of. For details, see
# https://github.com/nanoc/nanoc/pull/1085#issuecomment-280628426.

@action_provider.paths_for(rep).each do |(snapshot_names, paths)|
route_rep(rep, paths, snapshot_names, {})
end

@action_provider.paths_for(rep).each do |(snapshot_names, paths)|
route_rep(rep, paths, snapshot_names, assigned_paths)
end

# TODO: verify that paths converge
end
end

def route_rep(rep, path, snapshot_name, paths_to_reps)
basic_path = path
return if basic_path.nil?
basic_path = basic_path.encode('UTF-8')
contract Nanoc::Int::ItemRep, C::IterOf[String], C::IterOf[Symbol], C::HashOf[String => Nanoc::Int::ItemRep] => C::Any
def route_rep(rep, paths, snapshot_names, assigned_paths)
# Encode
paths = paths.map { |path| path.encode('UTF-8') }

unless basic_path.start_with?('/')
raise RouteWithoutSlashError.new(basic_path, rep)
# Validate format
paths.each do |path|
unless path.start_with?('/')
raise RouteWithoutSlashError.new(path, rep)
end
end

# Check for duplicate paths
if paths_to_reps.key?(basic_path)
raise IdenticalRoutesError.new(basic_path, paths_to_reps[basic_path], rep)
else
paths_to_reps[basic_path] = rep
# Validate uniqueness
paths.each do |path|
if assigned_paths.include?(path)
# TODO: Include snapshot names in error message
raise IdenticalRoutesError.new(path, assigned_paths[path], rep)
end
end
paths.each do |path|
assigned_paths[path] = rep
end

rep.raw_paths[snapshot_name] = @site.config[:output_dir] + basic_path
rep.paths[snapshot_name] = strip_index_filename(basic_path)
# Assign
snapshot_names.each do |snapshot_name|
rep.raw_paths[snapshot_name] = paths.map { |path| @site.config[:output_dir] + path }
rep.paths[snapshot_name] = paths.map { |path| strip_index_filename(path) }
end
end

contract String => String
def strip_index_filename(basic_path)
@site.config[:index_filenames].each do |index_filename|
slashed_index_filename = '/' + index_filename
Expand Down
22 changes: 19 additions & 3 deletions lib/nanoc/base/services/item_rep_writer.rb
Expand Up @@ -3,9 +3,25 @@ module Nanoc::Int
class ItemRepWriter
TMP_TEXT_ITEMS_DIR = 'text_items'.freeze

def write(item_rep, snapshot_repo, snapshot_name)
raw_path = item_rep.raw_path(snapshot: snapshot_name)
return unless raw_path
def write_all(item_rep, snapshot_repo)
written_paths = Set.new

item_rep.snapshot_defs.map(&:name).each do |snapshot_name|
write(item_rep, snapshot_repo, snapshot_name, written_paths)
end
end

def write(item_rep, snapshot_repo, snapshot_name, written_paths)
item_rep.raw_paths.fetch(snapshot_name, []).each do |raw_path|
write_single(item_rep, snapshot_repo, snapshot_name, raw_path, written_paths)
end
end

def write_single(item_rep, snapshot_repo, snapshot_name, raw_path, written_paths)
# Don’t write twice
# TODO: test written_paths behavior
return if written_paths.include?(raw_path)
written_paths << raw_path

# Create parent directory
FileUtils.mkdir_p(File.dirname(raw_path))
Expand Down
2 changes: 1 addition & 1 deletion lib/nanoc/base/services/pruner.rb
Expand Up @@ -28,7 +28,7 @@ def initialize(config, reps, dry_run: false, exclude: [])
def run
return unless File.directory?(@config[:output_dir])

compiled_files = @reps.flat_map { |r| r.raw_paths.values }.compact
compiled_files = @reps.flat_map { |r| r.raw_paths.values.flatten }.compact
present_files, present_dirs = files_and_dirs_in(@config[:output_dir] + '/')

remove_stray_files(present_files, compiled_files)
Expand Down
3 changes: 2 additions & 1 deletion lib/nanoc/checking/checks/stale.rb
Expand Up @@ -26,7 +26,8 @@ def item_rep_paths
.flat_map(&:reps)
.map(&:unwrap)
.flat_map(&:raw_paths)
.flat_map(&:values),
.flat_map(&:values)
.flatten,
)
end

Expand Down
3 changes: 2 additions & 1 deletion lib/nanoc/cli/commands/compile.rb
Expand Up @@ -356,7 +356,8 @@ def stop
Nanoc::Int::NotificationCenter.remove(:rep_written, self)

@reps.select { |r| !r.compiled? }.each do |rep|
rep.raw_paths.each do |_snapshot_name, raw_path|
raw_paths = rep.raw_paths.values.flatten
raw_paths.each do |raw_path|
log(:low, :skip, raw_path, nil)
end
end
Expand Down
6 changes: 4 additions & 2 deletions lib/nanoc/cli/commands/show-data.rb
Expand Up @@ -115,8 +115,10 @@ def print_item_rep_paths(items)
puts ' (not written)'
end
length = rep.raw_paths.keys.map { |s| s.to_s.length }.max
rep.raw_paths.each do |snapshot_name, raw_path|
puts format(" [ %-#{length}s ] %s", snapshot_name, raw_path)
rep.raw_paths.each do |snapshot_name, raw_paths|
raw_paths.each do |raw_path|
puts format(" [ %-#{length}s ] %s", snapshot_name, raw_path)
end
end
end
end
Expand Down
5 changes: 4 additions & 1 deletion lib/nanoc/rule_dsl/rule_context.rb
Expand Up @@ -74,7 +74,10 @@ def snapshot(snapshot_name, path: nil)
#
# @return [void]
def write(path)
snapshot(:last, path: path)
@_write_snapshot_counter ||= 0
snapshot_name = "_#{@_write_snapshot_counter}".to_sym
@_write_snapshot_counter += 1
snapshot(snapshot_name, path: path)
end
end
end
22 changes: 14 additions & 8 deletions lib/nanoc/rule_dsl/rule_memory_calculator.rb
Expand Up @@ -61,7 +61,9 @@ def snapshots_defs_for(rep)
self[rep].each do |action|
case action
when Nanoc::Int::ProcessingActions::Snapshot
snapshot_defs << Nanoc::Int::SnapshotDef.new(action.snapshot_name, binary: is_binary)
action.snapshot_names.each do |snapshot_name|
snapshot_defs << Nanoc::Int::SnapshotDef.new(snapshot_name, binary: is_binary)
end
when Nanoc::Int::ProcessingActions::Filter
is_binary = Nanoc::Filter.named!(action.filter_name).to_binary?
end
Expand Down Expand Up @@ -91,14 +93,14 @@ def new_rule_memory_for_rep(rep)
if rule_memory.any_layouts?
executor.snapshot(:post)
end
unless rule_memory.snapshot_actions.any? { |sa| sa.snapshot_name == :last }
unless rule_memory.snapshot_actions.any? { |sa| sa.snapshot_names.include?(:last) }
executor.snapshot(:last)
end
unless rule_memory.snapshot_actions.any? { |sa| sa.snapshot_name == :pre }
unless rule_memory.snapshot_actions.any? { |sa| sa.snapshot_names.include?(:pre) }
executor.snapshot(:pre)
end

copy_paths_from_routing_rules(rule_memory, rep: rep)
copy_paths_from_routing_rules(rule_memory.compact_snapshots, rep: rep)
end

# @param [Nanoc::Int::Layout] layout
Expand All @@ -118,7 +120,7 @@ def new_rule_memory_for_layout(layout)

def copy_paths_from_routing_rules(mem, rep:)
mem.map do |action|
if action.is_a?(Nanoc::Int::ProcessingActions::Snapshot) && action.path.nil?
if action.is_a?(Nanoc::Int::ProcessingActions::Snapshot) && action.paths.empty?
copy_path_from_routing_rule(action, rep: rep)
else
action
Expand All @@ -127,9 +129,13 @@ def copy_paths_from_routing_rules(mem, rep:)
end

def copy_path_from_routing_rule(action, rep:)
path_from_rules = basic_path_from_rules_for(rep, action.snapshot_name)
if path_from_rules
action.copy(path: path_from_rules.to_s)
paths_from_rules =
action.snapshot_names.map do |snapshot_name|
basic_path_from_rules_for(rep, snapshot_name)
end.compact

if paths_from_rules.any?
action.update(paths: paths_from_rules.map(&:to_s))
else
action
end
Expand Down
2 changes: 1 addition & 1 deletion lib/nanoc/spec.rb
Expand Up @@ -58,7 +58,7 @@ def create_layout(content, attributes, identifier)
# @param [Symbol] rep The rep name to create
def create_rep(item, path, rep = :default)
rep = Nanoc::Int::ItemRep.new(item.unwrap, rep)
rep.paths[:last] = path
rep.paths[:last] = [path]
@reps << rep
Nanoc::ItemRepView.new(rep, view_context)
end
Expand Down