lib/webgen/website.rb in webgen-0.4.7 vs lib/webgen/website.rb in webgen-0.5.0

- old
+ new

@@ -1,334 +1,212 @@ -# -#-- -# -# $Id: website.rb 601 2007-02-14 21:20:44Z thomas $ -# -# webgen: template based static website generator -# Copyright (C) 2004 Thomas Leitner -# -# This program is free software; you can redistribute it and/or modify it under the terms of the GNU -# General Public License as published by the Free Software Foundation; either version 2 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with this program; if not, -# write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -#++ -# +# Standard lib requires +require 'logger' +require 'set' -require 'pathname' -require 'yaml' -require 'fileutils' -require 'webgen/config' -require 'webgen/plugin' +# Requirements for Website +require 'webgen/loggable' +require 'webgen/logger' +require 'webgen/configuration' +require 'webgen/websiteaccess' +require 'webgen/blackboard' +require 'webgen/cache' +require 'webgen/tree' -module Webgen +# Files for autoloading +require 'webgen/source' +require 'webgen/output' +require 'webgen/sourcehandler' +require 'webgen/contentprocessor' +# Load other needed files +require 'webgen/path' +require 'webgen/node' +require 'webgen/page' - # Base class for directories which have a README file with information stored in YAML format. - # Should not be used directly, use its child classes! - class DirectoryInfo - # The unique name. - attr_reader :name +# The Webgen namespace houses all classes/modules used by webgen. +module Webgen - # Contains additional information, like a description or the creator. - attr_reader :infos - - # Returns a new object for the given +name+. - def initialize( name ) - @name = name - raise ArgumentError.new( "'#{name}' is not a directory!" ) if !File.directory?( path ) - @infos = YAML::load( File.read( File.join( path, 'README' ) ) ) - raise ArgumentError.new( "'#{name}/README' does not contain key-value pairs in YAML format!" ) unless @infos.kind_of?( Hash ) + # Returns the data directory for webgen. + def self.data_dir + unless defined?(@@data_dir) + require 'rbconfig' + @@data_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'data', 'webgen')) + @@data_dir = File.expand_path(File.join(Config::CONFIG["datadir"], "webgen")) if !File.exists?(@@data_dir) + raise "Could not find webgen data directory! This is a bug, report it please!" unless File.directory?(@@data_dir) end - - # The absolute directory path. Requires that child classes have defined a constant +BASE_PATH+. - def path - File.expand_path( File.join( self.class::BASE_PATH, name ) ) - end - - # The files under the directory. - def files - Dir.glob( File.join( path, '**', '*' ), File::FNM_CASEFOLD ) - end - - # Copies the files returned by +#files+ into the directory +dest+, preserving the directory - # hierarchy. - def copy_to( dest ) - files.collect do |file| - destpath = File.join( dest, File.dirname( file ).sub( /^#{path}/, '' ) ) - FileUtils.mkdir_p( File.dirname( destpath ) ) - if File.directory?( file ) - FileUtils.mkdir_p( File.join( destpath, File.basename( file ) ) ) - else - FileUtils.cp( file, destpath ) - end - File.join( destpath, File.basename( file ) ) - end - end - - # Returns all available entries. - def self.entries - unless defined?( @entries ) - @entries = {} - Dir.glob( File.join( self::BASE_PATH, '*' ), File::FNM_CASEFOLD ).each do |f| - next unless File.directory?( f ) - name = File.basename( f ); - @entries[name] = self.new( name ) - end - end - @entries - end - + @@data_dir end - # A Web site template is a collection of files which provide a starting point for a Web site. - # These files provide stubs for the content and should not contain any style information. - class WebSiteTemplate < DirectoryInfo + # Represents a webgen website and is used to render it. + class Website - # Base path for the templates. - BASE_PATH = File.join( Webgen.data_dir, 'website_templates' ) + # Raised when the configuration file of the website is invalid. + class ConfigFileInvalid < RuntimeError; end - end + include Loggable + # The website configuration. Can only be used after #init has been called (which is + # automatically done in #render). + attr_reader :config - # A Web site style provides style information for a Web site. This means it contains, at least, a - # template file and a CSS file. - class WebSiteStyle < DirectoryInfo + # The blackboard used for inter-object communication. Can only be used after #init has been + # called. + attr_reader :blackboard - # Base path for the styles. - BASE_PATH = File.join( Webgen.data_dir, 'website_styles' ) + # A cache to store information that should be available between runs. Can only be used after + # #init has been called. + attr_reader :cache - # See DirectoryInfo#files - def files - super.select {|f| f != File.join( path, 'README' )} - end + # The internal data structure used to store information about individual nodes. + attr_reader :tree - end + # The logger used for logging. If set to +nil+, logging is disabled. + attr_accessor :logger + # The website directory. + attr_reader :directory - # A gallery style provides style information for gallery pages. It should contains the files - # +gallery_main.template+, +gallery_gallery.template+ and +gallery_image.template+ and an optional - # readme file. - class GalleryStyle < DirectoryInfo - - # Base path for the styles. - BASE_PATH = File.join( Webgen.data_dir, 'gallery_styles' ) - - # See DirectoryInfo#files - def files - super.select {|f| f != File.join( path, 'README' )} - plugin_files + # Create a new webgen website for the website in the directory +dir+. You can provide a + # block (has to take the configuration object as parameter) for adjusting the configuration + # values during the initialization. + def initialize(dir, logger=Webgen::Logger.new($stdout, false), &block) + @blackboard = nil + @cache = nil + @logger = logger + @config_block = block + @directory = dir end - def plugin_files - plugin_files = [] - @infos['plugin files'].each do |pfile| - plugin_files += Dir.glob( File.join( path, pfile ), File::FNM_CASEFOLD ) - end if @infos['plugin files'] - plugin_files + # Define a service +service_name+ provided by the instance of +klass+. The parameter +method+ + # needs to define the method which should be invoked when the service is invoked. Can only be + # used after #init has been called. + def autoload_service(service_name, klass, method = service_name) + blackboard.add_service(service_name) {|*args| cache.instance(klass).send(method, *args)} end - end + # Initialize the configuration, blackboard and cache objects and load the default configuration + # as well as website specific extension files. An already existing configuration/blackboard is + # deleted! + def init + execute_in_env do + @blackboard = Blackboard.new + @config = Configuration.new - # A sipttra style provides a template and other files for styling sipttra files. It should contain - # at least a +sipttra.template+. - class SipttraStyle < DirectoryInfo + load 'webgen/default_config.rb' + Dir.glob(File.join(@directory, 'ext', '**/init.rb')) {|f| load(f)} + read_config_file - # Base path for the styles. - BASE_PATH = File.join( Webgen.data_dir, 'sipttra_styles' ) - - # See DirectoryInfo#files - def files - super.select {|f| f != File.join( path, 'README' )} + @config_block.call(@config) if @config_block + restore_tree_and_cache + end + self end - end + # Render the website (after calling #init if the website is not already initialized). + def render + execute_in_env do + init unless @config + puts "Starting webgen..." + shm = SourceHandler::Main.new + shm.render(@tree) + save_tree_and_cache + puts "Finished" - # A WebSite object represents a webgen website directory and is used for manipulating it. - class WebSite - - # The website directory. - attr_reader :directory - - # The logger used for the website - attr_reader :logger - - # The plugin manager used for this website. - attr_reader :manager - - # Creates a new WebSite object for the given +directory+ and loads its plugins. If the - # +plugin_config+ parameter is given, it is used to resolve the values for plugin parameters. - # Otherwise, a ConfigurationFile instance is used as plugin configuration. - def initialize( directory = Dir.pwd, plugin_config = nil ) - @directory = File.expand_path( directory ) - @logger = Webgen::Logger.new - - wrapper_mod = Module.new - wrapper_mod.module_eval { include DEFAULT_WRAPPER_MODULE } - @loader = PluginLoader.new( wrapper_mod ) - @loader.load_from_dir( File.join( @directory, Webgen::PLUGIN_DIR ) ) - - @manager = PluginManager.new( [DEFAULT_PLUGIN_LOADER, @loader], DEFAULT_PLUGIN_LOADER.plugin_classes + @loader.plugin_classes ) - @manager.logger = @logger - set_plugin_config( plugin_config ) - end - - # Returns a modified value for Configuration:srcDir, Configuration:outDir and Configuration:websiteDir. - def param_for_plugin( plugin_name, param ) - case [plugin_name, param] - when ['Core/Configuration', 'srcDir'] then @srcDir - when ['Core/Configuration', 'outDir'] then @outDir - when ['Core/Configuration', 'websiteDir'] then @directory - else - (@plugin_config ? @plugin_config.param_for_plugin( plugin_name, param ) : PluginParamValueNotFound) + if @logger && @logger.log_output.length > 0 + puts "\nLog messages:" + puts @logger.log_output + end end end - # Initializes all plugins and renders the website. - def render( files = [] ) - @logger.level = @manager.param_for_plugin( 'Core/Configuration', 'loggerLevel' ) - @manager.init + # Clean the website directory from all generated output files (including the cache file). If + # +del_outdir+ is +true+, then the base output directory is also deleted. When a delete + # operation fails, the error is silently ignored and the clean operation continues. + # + # Note: Uses the configured output instance for the operations! + def clean(del_outdir = false) + init + execute_in_env do + output = @blackboard.invoke(:output_instance) + @tree.node_access[:alcn].each do |name, node| + next if node.is_fragment? || node['no_output'] || node.path == '/' || node == @tree.dummy_root + output.delete(node.path) rescue nil + end - @logger.info( 'WebSite#render' ) { "Starting rendering of website <#{directory}>..." } - @logger.info( 'WebSite#render' ) { "Using webgen data directory at <#{Webgen.data_dir}>!" } - if files.empty? - @manager['Core/FileHandler'].render_site - else - @manager['Core/FileHandler'].render_files( files ) - end - @logger.info( 'WebSite#render' ) { "Rendering of website <#{directory}> finished" } - end + if @config['website.cache'].first == :file + FileUtils.rm(File.join(@directory, @config['website.cache'].last)) rescue nil + end - # Loads the configuration file from the +directory+. - def self.load_config_file( directory = Dir.pwd ) - begin - ConfigurationFile.new( File.join( directory, 'config.yaml' ) ) - rescue ConfigurationFileInvalid => e - nil + if del_outdir + output.delete('/') rescue nil + end end end - # Create a website in the +directory+, using the template +template_name+ and the style +style_name+. - def self.create_website( directory, template_name = 'default', style_name = 'default' ) - template = WebSiteTemplate.entries[template_name] - style = WebSiteStyle.entries[style_name] - raise ArgumentError.new( "Invalid website template '#{template}'" ) if template.nil? - raise ArgumentError.new( "Invalid website style '#{style}'" ) if style.nil? - - raise ArgumentError.new( "Directory <#{directory}> does already exist!") if File.exists?( directory ) - FileUtils.mkdir( directory ) - return template.copy_to( directory ) + style.copy_to( File.join( directory, Webgen::SRC_DIR) ) + # The provided block is executed within a proper environment sothat any object can access the + # Website object. + def execute_in_env + set_back = Thread.current[:webgen_website].nil? + Thread.current[:webgen_website] = self + yield + ensure + Thread.current[:webgen_website] = nil if set_back end - # Copies the style files for +style+ to the source directory of the website +directory+ - # overwritting exisiting files. - def self.use_website_style( directory, style_name ) - style = WebSiteStyle.entries[style_name] - raise ArgumentError.new( "Invalid website style '#{style_name}'" ) if style.nil? - src_dir = File.join( directory, Webgen::SRC_DIR ) - raise ArgumentError.new( "Directory <#{src_dir}> does not exist!") unless File.exists?( src_dir ) - return style.copy_to( src_dir ) - end - - # Copies the gallery style files for +style+ to the source directory of the website +directory+ - # overwritting exisiting files. - def self.use_gallery_style( directory, style_name ) - style = GalleryStyle.entries[style_name] - raise ArgumentError.new( "Invalid gallery style '#{style_name}'" ) if style.nil? - src_dir = File.join( directory, Webgen::SRC_DIR ) - plugin_dir = File.join( directory, Webgen::PLUGIN_DIR ) - raise ArgumentError.new( "Directory <#{src_dir}> does not exist!") unless File.exists?( src_dir ) - plugin_files = style.plugin_files - FileUtils.mkdir( plugin_dir ) unless File.exists?( plugin_dir ) - FileUtils.cp( plugin_files, plugin_dir ) - return style.copy_to( src_dir ) + plugin_files.collect {|f| File.join( plugin_dir, File.basename( f ) )} - end - - # Copies the sipttra style files for +style+ to the source directory of the website +directory+ - # overwritting exisiting files. - def self.use_sipttra_style( directory, style_name ) - style = SipttraStyle.entries[style_name] - raise ArgumentError.new( "Invalid sipttra style '#{style_name}'" ) if style.nil? - src_dir = File.join( directory, Webgen::SRC_DIR ) - raise ArgumentError.new( "Directory <#{src_dir}> does not exist!") unless File.exists?( src_dir ) - return style.copy_to( src_dir ) - end - ####### private ####### - def set_plugin_config( plugin_config ) - @manager.plugin_config = ( plugin_config ? plugin_config : self.class.load_config_file( @directory ) ) - @srcDir = File.join( @directory, Webgen::SRC_DIR ) - outDir = @manager.param_for_plugin( 'Core/Configuration', 'outDir' ) - @outDir = (/^(\/|[A-Za-z]:)/ =~ outDir ? outDir : File.join( @directory, outDir ) ) - @plugin_config = @manager.plugin_config - @manager.plugin_config = self + # Restore the tree and the cache from +website.cache+ and returns the Tree object. + def restore_tree_and_cache + @cache = Cache.new + @tree = Tree.new + data = if config['website.cache'].first == :file + cache_file = File.join(@directory, config['website.cache'].last) + File.read(cache_file) if File.exists?(cache_file) + else + config['website.cache'].last + end + cache_data, @tree = Marshal.load(data) rescue nil + @cache.restore(cache_data) if cache_data end - end - - - # Raised when a configuration file has an invalid structure - class ConfigurationFileInvalid < RuntimeError; end - - # Represents the configuration file of a website. - class ConfigurationFile - - # Returns the whole configuration. - attr_reader :config - - # Reads the content of the given configuration file and initialize a new object with it. - def initialize( config_file ) - if File.exists?( config_file ) - begin - @config = YAML::load( File.read( config_file ) ) - rescue ArgumentError => e - raise ConfigurationFileInvalid, e.message - end + # Save the +tree+ and the +cache+ to +website.cache+. + def save_tree_and_cache + cache_data = [@cache.dump, @tree] + if config['website.cache'].first == :file + cache_file = File.join(@directory, config['website.cache'].last) + File.open(cache_file, 'wb') {|f| Marshal.dump(cache_data, f)} else - @config = {} + config['website.cache'][1] = Marshal.dump(cache_data) end - check_config end - # See PluginManager#param_for_plugin . - def param_for_plugin( plugin_name, param ) - if @config.has_key?( plugin_name ) && @config[plugin_name].has_key?( param ) - @config[plugin_name][param] - else - PluginParamValueNotFound - end - end - - ####### - private - ####### - - def check_config - if !@config.kind_of?( Hash ) || !@config.all? {|k,v| v.kind_of?( Hash )} - raise ConfigurationFileInvalid.new( 'Structure of config file is not valid, has to be a Hash of Hashes' ) - end - - if !@config.has_key?( 'Core/FileHandler' ) || !@config['Core/FileHandler'].has_key?( 'defaultMetaInfo' ) - @config.each_key do |plugin_name| - next unless plugin_name =~ /File\// - if @config[plugin_name]['defaultMetaInfo'].kind_of?( Hash ) - ((@config['Core/FileHandler'] ||= {})['defaultMetaInfo'] ||= {})[plugin_name] = @config[plugin_name]['defaultMetaInfo'] - @config[plugin_name].delete( 'defaultMetaInfo' ) + # Update the configuration object for the website with infos found in the configuration file. + def read_config_file + file = File.join(@directory, 'config.yaml') + if File.exists?(file) + begin + config = YAML::load(File.read(file)) || {} + raise 'Structure of config file is not valid, has to be a Hash' if !config.kind_of?(Hash) + config.each do |key, value| + if key == 'default_meta_info' + value.each do |klass_name, options| + @config['sourcehandler.default_meta_info'][klass_name].update(options) + end + else + @config[key] = value + end end + rescue RuntimeError, ArgumentError => e + raise ConfigFileInvalid, "Configuration invalid: " + e.message end + elsif File.exists?(File.join(@directory, 'config.yml')) + log(:warn) { "No configuration file called config.yaml found (there is a config.yml - spelling error?)" } end - end end end