module Nanoc::Helpers # Provides functionality for “capturing” content in one place and reusing # this content elsewhere. # # For example, suppose you want the sidebar of your site to contain a short # summary of the item. You could put the summary in the meta file, but # that’s not possible when the summary contains eRuby. You could also put # the sidebar inside the actual item, but that’s not very pretty. Instead, # you write the summary on the item itself, but capture it, and print it in # the sidebar layout. # # This helper has been tested with ERB and Haml. Other filters may not work # correctly. # # @example Capturing content for a summary # # <% content_for :summary do %> #

On this item, Nanoc is introduced, blah blah.

# <% end %> # # @example Showing captured content in a sidebar # # module Capturing # @api private class CapturesStore def initialize @store = {} end def []=(item, name, content) @store[item.identifier] ||= {} @store[item.identifier][name] = content end def [](item, name) @store[item.identifier] ||= {} @store[item.identifier][name] end def reset_for(item) @store[item.identifier] = {} end end class ::Nanoc::Int::Site # @api private def captures_store @captures_store ||= CapturesStore.new end # @api private def captures_store_compiled_items require 'set' @captures_store_compiled_items ||= Set.new end end # @overload content_for(name, params = {}, &block) # # Captures the content inside the block and stores it so that it can be # referenced later on. The same method, {#content_for}, is used for # getting the captured content as well as setting it. When capturing, # the content of the block itself will not be outputted. # # By default, capturing content with the same name will raise an error if the newly captured # content differs from the previously captured content. This behavior can be changed by # providing a different `:existing` option to this method: # # * `:error`: When content already exists and is not identical, raise an error. # # * `:overwrite`: Overwrite the previously captured content with the newly captured content. # # * `:append`: Append the newly captured content to the previously captured content. # # @param [Symbol, String] name The base name of the attribute into which # the content should be stored # # @option params [Symbol] existing Can be either `:error`, `:overwrite`, or `:append` # # @return [void] # # @overload content_for(item, name) # # Fetches the capture with the given name from the given item and # returns it. # # @param [Nanoc::Int::Item] item The item for which to get the capture # # @param [Symbol, String] name The name of the capture to fetch # # @return [String] The stored captured content def content_for(*args, &block) if block_given? # Set content # Get args case args.size when 1 name = args[0] params = {} when 2 name = args[0] params = args[1] else raise ArgumentError, 'expected 1 or 2 argument (the name ' \ "of the capture, and optionally params) but got #{args.size} instead" end name = args[0] existing_behavior = params.fetch(:existing, :error) # Capture content = capture(&block) # Prepare for store store = @site.unwrap.captures_store case existing_behavior when :overwrite store[@item, name.to_sym] = '' when :append store[@item, name.to_sym] ||= '' when :error if store[@item, name.to_sym] && store[@item, name.to_sym] != content raise "a capture named #{name.inspect} for #{@item.identifier} already exists" else store[@item, name.to_sym] = '' end else raise ArgumentError, 'expected :existing_behavior param to #content_for to be one of ' \ ":overwrite, :append, or :error, but #{existing_behavior.inspect} was given" end # Store @site.unwrap.captures_store_compiled_items << @item.unwrap store[@item, name.to_sym] << content else # Get content # Get args if args.size != 2 raise ArgumentError, 'expected 2 arguments (the item ' \ "and the name of the capture) but got #{args.size} instead" end item = args[0].is_a?(Nanoc::ItemWithRepsView) ? args[0].unwrap : args[0] name = args[1] # Create dependency if @item.nil? || item != @item.unwrap Nanoc::Int::NotificationCenter.post(:visit_started, item) Nanoc::Int::NotificationCenter.post(:visit_ended, item) # This is an extremely ugly hack to get the compiler to recompile the # item from which we use content. For this, we need to manually edit # the content attribute to reset it. :( # FIXME: clean this up unless @site.unwrap.captures_store_compiled_items.include?(item) @site.unwrap.captures_store.reset_for(item) item.forced_outdated = true @site.unwrap.compiler.reps[item].each do |r| r.snapshot_contents = { last: item.content } raise Nanoc::Int::Errors::UnmetDependency.new(r) end end end # Get content @site.unwrap.captures_store[item, name.to_sym] end end # Evaluates the given block and returns its contents. The contents of the # block is not outputted. # # @return [String] The captured result def capture(&block) # Get erbout so far erbout = eval('_erbout', block.binding) erbout_length = erbout.length # Execute block yield # Get new piece of erbout erbout_addition = erbout[erbout_length..-1] # Remove addition erbout[erbout_length..-1] = '' # Depending on how the filter outputs, the result might be a # single string or an array of strings (slim outputs the latter). erbout_addition = erbout_addition.join if erbout_addition.is_a? Array # Done. erbout_addition end end end