require 'webgen/websiteaccess' require 'webgen/loggable' require 'webgen/path' require 'uri' require 'set' require 'pathname' module Webgen # Represents a file, a directory or a fragment. A node always belongs to a Tree. class Node include WebsiteAccess include Loggable # The parent node. attr_reader :parent # The children of this node. attr_reader :children # The full output path of this node. attr_reader :path # The tree to which this node belongs. attr_reader :tree # The canonical name of this node. attr_reader :cn # The absolute canonical name of this node. attr_reader :absolute_cn # The localized canonical name of this node. attr_reader :lcn # The absolute localized canonical name of this node. attr_reader :absolute_lcn # The level of the node. The level specifies how deep the node is in the hierarchy. attr_reader :level # The language of this node. attr_reader :lang # Meta information associated with the node. attr_reader :meta_info # Set by other objects to +true+ if they think the object has changed since the last run. Must # not be set to +false+ once it is +true+! attr_accessor :dirty # Has the node been created or has it been read from the cache? attr_accessor :created # Create a new Node instance. # # +parent+ (immutable):: # The parent node under which this nodes should be created. # +path+ (immutable):: # The full output path for this node. If this node is a directory, the path must have a # trailing slash (dir/). If it is a fragment, the hash sign must be the first # character of the path (#fragment). This can also be an absolute path like # http://myhost.com/. # +cn+ (immutable):: # The canonical name for this node. Needs to be of the form basename.ext or # basename where +basename+ does not contain any dots. Also, the +basename+ must not # include a language part! # +meta_info+:: # A hash with meta information for the new node. # # The language of a node is taken from the meta information +lang+ and the entry is deleted from # the meta information hash. The language cannot be changed afterwards! If no +lang+ key is # found, the node is language neutral. def initialize(parent, path, cn, meta_info = {}) @parent = parent @path = path.freeze @cn = cn.chomp('/').freeze @lang = meta_info.delete('lang').freeze @lang = nil unless is_file? @meta_info = meta_info @children = [] @dirty = true @created = true init_rest end # Return the meta information item for +key+. def [](key) @meta_info[key] end # Assign +value+ to the meta information item for +key+. def []=(key, value) @meta_info[key] = value end # Return the node information hash which contains information for processing the node. def node_info tree.node_info[@absolute_lcn] ||= {} end # Check if the node is a directory. def is_directory?; @path[-1] == ?/; end # Check if the node is a file. def is_file?; !is_directory? && !is_fragment?; end # Check if the node is a fragment. def is_fragment?; @cn[0] == ?# end # Check if the node is the root node. def is_root?; self == tree.root; end # Return +true+ if the node has changed since the last webgen run. If it has changed, +dirty+ is # set to +true+. def changed? @dirty = node_info[:used_nodes].any? {|n| n != @absolute_lcn && (!tree[n] || tree[n].changed?)} unless @dirty website.blackboard.dispatch_msg(:node_changed?, self) unless @dirty @dirty end # Return an informative representation of the node. def inspect "<##{self.class.name}: alcn=#{@absolute_lcn}>" end # Return +true+ if the alcn matches the pattern. See File.fnmatch for useable patterns. def =~(pattern) File.fnmatch(pattern, @absolute_lcn, File::FNM_DOTMATCH|File::FNM_CASEFOLD|File::FNM_PATHNAME) end # Sort nodes by using the meta info +sort_info+ (or +title+ if +sort_info+ is not set) of both # involved nodes. def <=>(other) self_so = (self['sort_info'] && self['sort_info'].to_s) || self['title'] || '' other_so = (other['sort_info'] && other['sort_info'].to_s) || other['title'] || '' if self_so !~ /\D/ && other_so !~ /\D/ self_so = self_so.to_i other_so = other_so.to_i end self_so <=> other_so end # Construct the absolute (localized) canonical name by using the +parent+ node and +name+ (which # can be a cn or an lcn). The +type+ can be either +:alcn+ or +:acn+. def self.absolute_name(parent, name, type) if parent.kind_of?(Tree) '' else parent = parent.parent while parent.is_fragment? # Handle fragment nodes specially in case they are nested parent_name = (type == :alcn ? parent.absolute_lcn : parent.absolute_cn) parent_name + (parent_name !~ /\/$/ && (parent.is_directory? || parent == parent.tree.dummy_root) ? '/' : '') + name end end # Construct an internal URL for the given +name+ which can be a acn/alcn/path. def self.url(name) url = URI::parse(name) url = URI::parse('webgen://webgen.localhost/') + url unless url.absolute? url end # Check if the this node is in the subtree which is spanned by +node+. The check is performed # using only the +parent+ information of the involved nodes, NOT the actual path/alcn values! def in_subtree_of?(node) temp = self temp = temp.parent while temp != tree.dummy_root && temp != node temp != tree.dummy_root end # Return the node with the same canonical name but in language +lang+ or, if no such node # exists, an unlocalized version of the node. If no such node is found either, +nil+ is # returned. def in_lang(lang) avail = @tree.node_access[:acn][@absolute_cn] avail.find do |n| n = n.parent while n.is_fragment? n.lang == lang end || avail.find do |n| n = n.parent while n.is_fragment? n.lang.nil? end end # Return the node representing the given +path+ which can be an acn/alcn. The path can be # absolute (i.e. starting with a slash) or relative to the current node. If no node exists for # the given path or if the path is invalid, +nil+ is returned. # # If the +path+ is an alcn and a node is found, it is returned. If the +path+ is an acn, the # correct localized node according to +lang+ is returned or if no such node exists but an # unlocalized version does, the unlocalized node is returned. def resolve(path, lang = nil) url = self.class.url(self.is_directory? ? File.join(@absolute_lcn, '/') : @absolute_lcn) + path path = url.path + (url.fragment.nil? ? '' : '#' + url.fragment) path.chomp!('/') unless path == '/' return nil if path =~ /^\/\.\./ node = @tree[path, :alcn] if node && node.absolute_cn != path node else (node = @tree[path, :acn]) && node.in_lang(lang) end end # Return the relative path to the given path +other+. The parameter +other+ can be a Node or a # String. def route_to(other) my_url = self.class.url(@path) other_url = if other.kind_of?(Node) self.class.url(other.routing_node(@lang).path) elsif other.kind_of?(String) my_url + other else raise ArgumentError, "improper class for argument" end # resolve any '.' and '..' paths in the target url if other_url.path =~ /\/\.\.?\// && other_url.scheme == 'webgen' other_url.path = Pathname.new(other_url.path).cleanpath.to_s end route = my_url.route_to(other_url).to_s (route == '' ? File.basename(self.path) : route) end # Return the routing node in language +lang+ which is the node that is used when routing to this # node. The returned node can differ from the node itself in case of a directory where the # routing node is the directory index node. def routing_node(lang) if !is_directory? self else key = [absolute_lcn, :index_node, lang] vcache = website.cache.volatile return vcache[key] if vcache.has_key?(key) index_path = self.meta_info['index_path'] if index_path.nil? vcache[key] = self else index_node = resolve(index_path, lang) if index_node vcache[key] = index_node log(:info) { "Directory index path for <#{absolute_lcn}> => <#{index_node.absolute_lcn}>" } else vcache[key] = self log(:warn) { "No directory index path found for directory <#{absolute_lcn}>" } end end vcache[key] end end # Return a HTML link from this node to the +node+ or, if this node and +node+ are the same and # the parameter website.link_to_current_page is +false+, a +span+ element with the link # text. # # You can optionally specify additional attributes for the HTML element in the +attr+ Hash. # Also, the meta information +link_attrs+ of the given +node+ is used, if available, to set # attributes. However, the +attr+ parameter takes precedence over the +link_attrs+ meta # information. If the special value :link_text is present in the attributes, it will be # used as the link text; otherwise the title of the +node+ will be used. Be aware that all # key-value pairs with Symbol keys are removed before the attributes are written. Therefore you # always need to specify general attributes with Strings! def link_to(node, attr = {}) attr = node['link_attrs'].merge(attr) if node['link_attrs'].kind_of?(Hash) link_text = attr[:link_text] || node.routing_node(@lang)['routed_title'] || node['title'] attr.delete_if {|k,v| k.kind_of?(Symbol)} use_link = (node != self || website.config['website.link_to_current_page']) attr['href'] = self.route_to(node) if use_link attrs = attr.collect {|name,value| "#{name.to_s}=\"#{value}\"" }.sort.unshift('').join(' ') (use_link ? "#{link_text}" : "#{link_text}") end ####### private ####### # Do the rest of the initialization. def init_rest @lcn = Path.lcn(@cn, @lang) @absolute_cn = self.class.absolute_name(@parent, @cn, :acn) @absolute_lcn = self.class.absolute_name(@parent, @lcn, :alcn) @level = -1 @tree = @parent (@level += 1; @tree = @tree.parent) while !@tree.kind_of?(Tree) @tree.register_node(self) @parent.children << self unless @parent == @tree self.node_info[:used_nodes] = Set.new end # Delegate missing methods to a processor. The current node is placed into the argument array as # the first argument before the method +name+ is invoked on the processor. def method_missing(name, *args, &block) if node_info[:processor] website.cache.instance(node_info[:processor]).send(name, *([self] + args), &block) else super end end end end