# # File 'document.rb' created on 17 apr 2008 at 18:04:42. # # See 'dokkit.rb' or +LICENSE+ for licence information. # # (C) 2006, 2007, 2008 Andrea Fazzi (and contributors). # require 'yaml' require 'erb' require 'dokkit/hash' require 'dokkit/resource/extensions/builtin' require 'dokkit/resource/filenamehelper' module Dokkit module Resource # Document is the core resource class of dokkit. Document # instances are usually created on demand by # Dokkit::Resource::Factory. class Document include FilenameHelper # Includes the builtin extensions to be used by Document objects. include Extension::Builtin # Set the default filters chain for commonly used output format. DEFAULT_FILTERS_CHAIN = { 'html' => ['deplate-html'], 'latex' => ['deplate-latex'], 'text' => ['deplate-text'] } attr_reader :configuration attr_reader :source_fn attr_reader :basename, :name_noext, :dirname, :relativename attr_reader :config_fns, :layout_fns, :target_fns attr_reader :targets, :layouts, :deps attr_reader :default_config_ext, :default_format attr_accessor :default_configuration_value # Initialize a Document instance. # +source_fn+:: is the file name of the document source file. # +configuration+:: is the configuration hash to be associated with the document. # +logger+:: is the logger instance. # +cache+:: is the cache manager instance. def initialize(source_fn, configuration, logger, cache, resource_factory, filter_factory, &blk) @source_fn, @logger, @cache, @resource_factory, @filter_factory = source_fn, logger, cache, resource_factory, filter_factory @config_fns = [] @targets = { } @layouts = { } @deps = { } # Get basename stripping out path and extension from +source_fn+ @basename = File.basename(source_fn, File.extname(source_fn)) # Get the directory part of source_fn @dirname = File.dirname(source_fn) # Set the document name with path but without extension @name_noext = File.join(@dirname, @basename) # Get name relative to configuration.document_dir @relativename = filename_helper(@name_noext, configuration[:document_dir], '') # Set defaults @default_config_ext = '.yaml' @default_format = 'html' # yield self to an optional configuration block yield self if block_given? # Setup hashes setup_header_configuration setup_configuration_hash(configuration.recursive_merge('layout' => @relativename)) setup_targets(@default_format) # Configure the document configure collect_all end # Read the configuration file +fn+ and return: # * the resulting configuration hash # * an array containing the names of the read config files # If +fn+ contains configuration data in the # header then this method extract it. Note that the method read # *recursively* the configuration file specified in config key # loading it from config_dir folder. # For example: # # --- # key: value # config: required # # # The resulting configuration hash will be the key/value pair # *plus* the configuration in +config_dir/required.yaml+. def add_config(fn) File.exists?(fn) ? merge_configuration_file(fn) : @logger.error("Configuration file '#{fn}' not found for '#{@source_fn}'!") end def add_layout(name, format = @default_format) if File.exists?(get_layout_filename(name, format)) @layouts[format] << get_layout_filename(name, format) else @logger.warn("Layout file '#{get_layout_filename(name, format)}' does not exists for '#{source_fn}'!") end end # Return the filters chain associated with the given format. def filters_for(format) if @targets.has_key?(format) if @targets[format].has_key?('filter') @targets[format]['filter'] elsif DEFAULT_FILTERS_CHAIN.has_key?(format) DEFAULT_FILTERS_CHAIN[format] end elsif DEFAULT_FILTERS_CHAIN.has_key?(format) DEFAULT_FILTERS_CHAIN[format] else @logger.error("No defined filters chain for format '#{format}'!") end end def target_for(format) @targets[format][:target_fn] end def deps_for(format) @deps[format] end def current_format @current_format || @default_format end def include(fn) @cache.add_dependency(source_fn, current_format, fn) erb = ERB.new(File.read(fn)) erb.filename = fn erb.result end # Render the document in the specified format. def render(args = { }) @current_format = args[:format] args = { :format => @default_format }.merge args if args.has_key?(:document) document = @resource_factory.get(:document, args[:document]) @cache.add_dependency(source_fn, args[:format], document.source_fn) document.render(:format => args[:format]) else do_render!(args[:format]) end end # Return the content of the source document file. def source @source ||= read_source end private # Read the content of the source document file from disk. def read_source File.read(source_fn) end def process_config_configuration_key @configuration['config'].to_a.each { |fn| load_config_file(fn) } end def load_config_file(fn) config_fn = File.join(@configuration[:config_dir], fn) config_fn += @default_config_ext if File.extname(config_fn).empty? @configuration.delete('config') add_config(config_fn) end def merge_configuration_file(fn) fn == @source_fn ? @configuration.recursive_merge!(@header_configuration) : @configuration.recursive_merge!(YAML::load_file(fn)) @config_fns << fn process_config_configuration_key if @configuration.has_key?('config') end # Setup configuration hash with initial value. def setup_configuration_hash(configuration) @default_configuration_value = "not defined" @configuration = Hash.new do |h, k| @logger.warn("Configuration key '#{k}' is not defined for #{source_fn}.") h[k] = @default_configuration_value end @configuration.merge! configuration end def setup_header_configuration @header_configuration = extract_configuration_from_source! end # Setup targets hash initializing it with a default format. def setup_targets(format) @targets[format] = { :target_fn => target_fn(format) } end # Render document in the given format. def do_render!(format) if @targets[format] render_source!(format) render_all_layouts!(format) else @logger.error("Don't know how to render '#{source_fn}': format '#{format}' is unknown.") end @content_for_layout end # Produce output from source. def render_source!(format) unless @source.nil? erb = ERB.new(@source) erb.filename = @source_fn @content_for_layout = apply_filters(erb.result(binding), format) end end # Injects rendered content from +source_fn+ in the layout chain. def render_all_layouts!(format) unless !@layouts[format] || @layouts[format].empty? @layouts[format].each { |layout_fn| render_layout!(layout_fn) } end end def render_layout!(layout_fn) erb = ERB.new(File.read(layout_fn)) erb.filename = layout_fn @content_for_layout = erb.result(binding) end # Collect all dependencies. def collect_all collect_formats collect_layouts collect_deps end # Configure the document reading all the configuration files. # Configuration infos are read in this order: # 1. configuration in header if present. # 2. configuration in ./+basename+.yaml if file exists. # 3. configuration in COMMON.yaml file (or files) exist. # 4. configuration in doc/configs/+basename+.yaml if file exists. def configure # :doc: add_config(config_fn_relative_to(:config_dir)) if File.exists?(config_fn_relative_to(:config_dir)) add_common_config add_config(config_fn_relative_to(:document_dir)) if File.exists?(config_fn_relative_to(:document_dir)) add_config(@source_fn) if @header_configuration end # Collect the layout files traversing +targets+ hash. def collect_layouts @targets.each_key do |format| @layouts[format] = [] process_layout_configuration_key(format) @layouts[format].uniq! @layouts[format].compact! end @layouts end def process_layout_configuration_key(format) @configuration['layout'].to_a.each { |name| add_layout(name, format) } end def get_layout_filename(name, format) File.join(@configuration[:layout_dir], name + ".#{format.to_s}") end # Process +format+ key in configuration hash. def process_formats @targets = { } @configuration['format'].to_a.each do |format| @targets[format] = { :target_fn => target_fn(format) } if format.is_a?(String) @targets[format.keys.first] = process_format_configuration_key(format) if format.is_a?(Hash) end end # Process option hash related to a particular target. def process_format_configuration_key(format) format_key = format.keys.first opts = format.values.first if opts ext = (opts['ext'] if opts.has_key?('ext')) || format_key filters = (opts['filter'] if opts.has_key?('filter')) || DEFAULT_FILTERS_CHAIN['html'] { :target_fn => target_fn(ext.to_sym), 'filter' => filters } else @logger.error("You must define format '#{format}'!") end end # Iterates through configuration +:targets+ key (if # present) and setup target options. def collect_formats process_formats if @configuration.has_key?('format') end # Collect all the dependency. def collect_deps @targets.each do |format, target| @deps[format] = [] @deps[format] << @source_fn # the essential dependency from source file @deps[format].concat @config_fns # dependency from configuration files @deps[format].concat @layouts[format] if @layouts.has_key?(format) # dependency from layout files @deps[format].concat @cache.deps[source_fn][format] if @cache.deps[source_fn] and @cache.deps[source_fn][format] @deps[format].uniq! # remove duplicates end @deps end # Strip configuration from header (if present) and recursive # merge it in +configuration hash. def extract_configuration_from_source! header_configuration_regexp = /\A-{3}$(.*?)-{3}$/m header_configuration = source.scan(header_configuration_regexp).to_s source.sub!(header_configuration_regexp, '') header_configuration = YAML::load(header_configuration) end # Configure from commons configuration files. def add_common_config resolve_common_configs(@dirname).reverse.each do |file| add_config(file) end end # Collect common configuration files COMMON.yaml. def resolve_common_configs(dir, arr = []) parent = File.expand_path(File.join(dir, '..')) resolve_common_configs(parent, arr) unless parent == dir # at root fn = File.join(dir,'COMMON.yaml') arr << File.expand_path(fn) if (File.exists?(fn) && File.readable?(fn)) arr.reverse! end # Return configuration filename from +source_fn+. # # Example: # # source_fn = 'doc/pages/subdir/document.ext' # config_fn #=> 'doc/configs/subdir/document.yaml' # def config_fn_relative_to(dir_config_key) filename_helper(@name_noext, @configuration[:document_dir], @configuration[dir_config_key], @default_config_ext) end # Return target filename for a given format. # # Example: # # source_fn = 'doc/pages/document.ext' # format = :html # target_fn(format) #=> 'output/document.html' # def target_fn(format) filename_helper(@name_noext, @configuration[:document_dir], @configuration[:output_dir], ".#{format.to_s}") end # Apply filters on text to produce the given format. def apply_filters(text, format) filters_chain = @targets[format]['filter'] || DEFAULT_FILTERS_CHAIN[format] filters = [] if filters_chain filters_chain.each do |filter| filters << @filter_factory.get(filter) end filters.inject(text) { |s, f| f.filter(s) } else @logger.error("Don't know how to render '#{source_fn}': cannot find filters for format '#{format}'!") end end end end end