# -*- encoding: utf-8 -*-
##
# Welcome to the API documentation of wegen!
#
# Have a look at the base webgen module which provides a good starting point!
# Standard lib requires
require 'logger'
require 'set'
require 'fileutils'
require 'facets/symbol/to_proc'
# Requirements for Website
require 'webgen/coreext'
require 'webgen/loggable'
require 'webgen/logger'
require 'webgen/configuration'
require 'webgen/websiteaccess'
require 'webgen/blackboard'
require 'webgen/cache'
require 'webgen/tree'
# Files for autoloading
require 'webgen/common'
require 'webgen/context'
require 'webgen/source'
require 'webgen/output'
require 'webgen/sourcehandler'
require 'webgen/contentprocessor'
require 'webgen/tag'
# Load other needed files
require 'webgen/path'
require 'webgen/node'
require 'webgen/page'
require 'webgen/version'
# Load deprecated classes/methods/...
require 'webgen/deprecated'
# The Webgen namespace houses all classes/modules used by webgen.
#
# = webgen
#
# webgen is a command line application for generating a web site from templates and content
# files. Despite this fact, the implementation also provides adequate support for using webgen as a
# library and *full* *support* for extending it.
#
# == Extending webgen
#
# webgen can be extended very easily. Any file called init.rb put into the ext/
# directory of the website or into one of its sub-directories is automatically loaded on
# Website#init.
#
# You can extend webgen in several ways. However, no magic or special knowledge is needed since
# webgen relies on the power of Ruby itself. So, for example, an extension is just a normal Ruby
# class. Most extension types provide a Base module for mixing into an extension which provides
# default implementations for needed methods.
#
# Following are links to detailed descriptions on how to develop specific types of extensions:
#
# [Webgen::Source] Information on how to implement a class that provides source paths for
# webgen. For example, one could implement a source class that uses a database as
# backend.
#
# [Webgen::Output] Information on how to implement a class that writes content to an output
# location. The default output class just writes to the file system. One could, for
# example, implement an output class that writes the generated files to multiple
# locations or to a remote server.
#
# [Webgen::ContentProcessor] Information on how to develop an extension that processes the
# content. For example, markup-to-HTML converters are implemented as
# content processors in webgen.
#
# [Webgen::SourceHandler::Base] Information on how to implement a class that handles objects of type
# source Path and creates Node instances. For example,
# Webgen::SourceHandler::Page handles the conversion of .page
# files to .html files.
#
# [Webgen::Tag::Base] Information on how to implement a webgen tag. webgen tags are used to provide
# an easy way for users to include dynamic content such as automatically
# generated menus.
#
# == Blackboard services
#
# The Blackboard class provides an easy communication facility between objects. It implements the
# Observer pattern on the one side and allows the definition of services on the other side. One
# advantage of a service over the direct use of an object instance method is that the caller does
# not need to how to find the object that provides the service. It justs uses the Website#blackboard
# instance. An other advantage is that one can easily exchange the place where the service was
# defined without breaking extensions that rely on it.
#
# Following is a list of all services available in the stock webgen distribution by the name and the
# method that implements it (which is useful for looking up the parameters of service).
#
# :create_fragment_nodes:: SourceHandler::Fragment#create_fragment_nodes
# :templates_for_node:: SourceHandler::Template#templates_for_node
# :parse_html_headers:: SourceHandler::Fragment#parse_html_headers
# :output_instance:: Output.instance
# :content_processor_names:: ContentProcessor.list
# :content_processor:: ContentProcessor.for_name
# :create_sitemap:: Common::Sitemap#create_sitemap
# :create_directories:: SourceHandler::Directory#create_directories
# :create_nodes:: SourceHandler::Main#create_nodes
# :create_nodes_from_paths:: SourceHandler::Main#create_nodes_from_paths
# :source_paths:: SourceHandler::Main#find_all_source_paths
#
# Following is the list of all messages that can be listened to:
#
# :node_flagged::
# See Node#flag
#
# :node_unflagged::
# See Node#unflag
#
# :node_changed?::
# See Node#changed?
# :node_meta_info_changed?::
# See Node#meta_info_changed?
#
# :before_node_created::
# Sent by the :create_nodes service before a node is created (handled by a source handler)
# with the +parent+ and the +path+ as arguments.
#
# :after_node_created::
# Sent by the :create_nodes service after a node has been created with the created node
# as argument.
#
# :before_node_deleted::
# See Tree#delete_node
#
# == Other places to look at
#
# Here is a list of modules/classes that are primarily used throughout webgen or provide useful
# methods for developing extensions:
#
# Common, Tree, Node, Path, Cache, Page
module Webgen
# 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
@@data_dir
end
# Represents a webgen website and is used to render it.
#
# Normally, webgen is used from the command line via the +webgen+ command or from Rakefiles via
# Webgen::WebgenTask. However, you can also easily use webgen as a library and this class provides
# the interface for this usage!
#
# Since a webgen website is, basically, just a directory, the only parameter needed for creating a
# new Website object is the website directory. After that you can work with the website:
#
# * If you want to render the website, you just need to call Website#render which initializes the
# website and does all the rendering. When the method call returns, everything has been rendered.
#
# * If you want to remove the generated output, you just need to invoke Website#clean and it will
# be done.
#
# * Finally, if you want to retrieve data from the website, you first have to call Website#init to
# initialize the website. After that you can use the various accessors to retrieve the needed
# data. *Note*: This is generally only useful if the website has been rendered because otherwise
# there is no data to retrieve.
#
class Website
# Raised when the configuration file of the website is invalid.
class ConfigFileInvalid < RuntimeError; end
include Loggable
# The website configuration. Can only be used after #init has been called (which is
# automatically done in #render).
attr_reader :config
# The blackboard used for inter-object communication. Can only be used after #init has been
# called.
attr_reader :blackboard
# A cache to store information that should be available between runs. Can only be used after
# #init has been called.
attr_reader :cache
# The internal data structure used to store information about individual nodes.
attr_reader :tree
# The logger used for logging. If set to +nil+, logging is disabled.
attr_accessor :logger
# The website directory.
attr_reader :directory
# Create a new webgen website for the website in the directory +dir+. If +dir+ is +nil+, the
# environment variable +WEBGEN_WEBSITE+ or, if it is not set either, the current working
# directory is used. You can provide a block (has to take the configuration object as parameter)
# for adjusting the configuration values during the initialization.
def initialize(dir = nil, logger=Webgen::Logger.new($stdout, false), &block)
@blackboard = nil
@cache = nil
@config = nil
@logger = logger
@config_block = block
@directory = (dir.nil? ? (ENV['WEBGEN_WEBSITE'].to_s.empty? ? Dir.pwd : ENV['WEBGEN_WEBSITE']) : dir)
end
# 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
# 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
load 'webgen/default_config.rb'
Dir.glob(File.join(@directory, 'ext', '**/init.rb')) {|f| load(f)}
read_config_file
@config_block.call(@config) if @config_block
restore_tree_and_cache
end
self
end
# Render the website (after calling #init if the website is not already initialized) and return
# a status code not equal to +nil+ if rendering was successful.
def render
result = nil
execute_in_env do
init
puts "Starting webgen..."
shm = SourceHandler::Main.new
result = shm.render
save_tree_and_cache if result
puts "Finished"
if @logger && @logger.log_output.length > 0
puts "\nLog messages:"
puts @logger.log_output
end
end
result
end
# 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
if @config['website.cache'].first == :file
FileUtils.rm(File.join(@directory, @config['website.cache'].last)) rescue nil
end
if del_outdir
output.delete('/') rescue nil
end
end
end
# 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]
Thread.current[:webgen_website] = self
yield
ensure
Thread.current[:webgen_website] = set_back
end
#######
private
#######
# 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.open(cache_file, 'rb') {|f| f.read} 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
# 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['website.cache'][1] = Marshal.dump(cache_data)
end
end
# 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|
case key
when *Webgen::Configuration::Helpers.public_instance_methods(false).map(&:to_s) then @config.send(key, value)
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