lib/hx.rb in hx-0.6.1 vs lib/hx.rb in hx-0.7.0
- old
+ new
@@ -20,10 +20,11 @@
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require 'rubygems'
+require 'thread'
require 'set'
require 'pathname'
require 'yaml'
module Hx
@@ -34,53 +35,76 @@
end
class EditingNotSupportedError < RuntimeError
end
-module Source
+module Filter
def edit_entry(path, prototype=nil)
raise EditingNotSupportedError, "Editing not supported for #{path}"
end
+ def each_entry_path
+ each_entry { |path, entry| yield path }
+ end
+
def each_entry
- raise NotImplementedError, "#{self.class}#each_entry not implemented"
+ each_entry_path do |path|
+ begin
+ entry = get_entry(path)
+ rescue NoSuchEntryError
+ next # entries may come and go during the enumeration
+ end
+ yield path, entry
+ end
end
+
+ def get_entry(path)
+ each_entry do |entry_path, entry|
+ return entry if entry_path == path
+ end
+ raise NoSuchEntryError, path
+ end
end
-class NullSource
- include Source
+class NullInput
+ include Filter
def each_entry
self
end
end
-NULL_SOURCE = NullSource.new
+NULL_INPUT = NullInput.new
class PathSubset
- include Source
+ include Filter
- def initialize(source, options)
- @source = source
+ def initialize(input, options)
+ @input = input
@path_filter = Predicate.new(options[:only], options[:except])
end
def edit_entry(path, prototype=nil)
if @path_filter.accept? path
- @source.edit_entry(path, prototype) { |text| yield text }
+ @input.edit_entry(path, prototype) { |text| yield text }
else
raise EditingNotSupportedError, "Editing not supported for #{path}"
end
self
end
- def each_entry
- @source.each_entry do |path, entry|
- yield path, entry if @path_filter.accept? path
+ def each_entry_path
+ @input.each_entry_path do |path|
+ yield path if @path_filter.accept? path
end
self
end
+
+ def get_entry(path)
+ raise NoSuchEntryError, path unless @path_filter.accept? path
+ @input.get_entry(path)
+ end
end
class PathSubset::Predicate
def initialize(accept, reject)
@accept_re = patterns_to_re(accept)
@@ -113,33 +137,43 @@
end
private :pattern_to_re
end
class Overlay
- include Source
+ include Filter
- def initialize(*sources)
- @sources = sources
+ def initialize(*inputs)
+ @inputs = inputs
end
- def each_entry
+ def each_entry_path
seen = Set[]
- @sources.each do |source|
- source.each_entry do |path, entry|
- yield path, entry unless seen.include? path
+ @inputs.each do |input|
+ input.each_entry_path do |path|
+ yield path unless seen.include? path
seen.add path
end
end
self
end
+
+ def get_entry(path)
+ @inputs.each do |input|
+ begin
+ return input.get_entry(path)
+ rescue NoSuchEntryError
+ end
+ end
+ raise NoSuchEntryError, path
+ end
end
module CircumfixPath
- include Source
+ include Filter
- def initialize(source, options)
- @source = source
+ def initialize(input, options)
+ @input = input
@prefix = options[:prefix]
@suffix = options[:suffix]
prefix = Regexp.quote(@prefix.to_s)
suffix = Regexp.quote(@suffix.to_s)
@regexp = Regexp.new("^#{prefix}(.*)#{suffix}$")
@@ -159,85 +193,114 @@
include CircumfixPath
def edit_entry(path, prototype=nil)
path = strip_circumfix(path)
raise EditingNotSupportedError, "Editing not supported for #{path}" unless path
- @source.edit_entry(path, prototype) { |text| yield text }
+ @input.edit_entry(path, prototype) { |text| yield text }
self
end
- def each_entry
- @source.each_entry do |path, entry|
- yield add_circumfix(path), entry
- end
+ def each_entry_path
+ @input.each_entry_path { |path| yield add_circumfix(path) }
self
end
+
+ def get_entry(path)
+ path = strip_circumfix(path)
+ raise NoSuchEntryError, path unless path
+ @input.get_entry(path)
+ end
end
class StripPath
include CircumfixPath
def edit_entry(path, prototype=nil)
path = add_circumfix(path)
- @source.edit_entry(path, prototype) { |text| yield text }
+ @input.edit_entry(path, prototype) { |text| yield text }
self
end
- def each_entry
- @source.each_entry do |path, entry|
+ def each_entry_path
+ @input.each_entry_path do |path|
path = strip_circumfix(path)
- yield path, entry if path
+ yield path if path
end
self
end
+
+ def get_entry(path)
+ @input.get_entry(add_circumfix(path))
+ end
end
class Cache
- include Source
+ include Filter
- def initialize(source, options={})
- @source = source
+ def initialize(input, options={})
+ @input = input
+ @lock = Mutex.new
@entries = nil
+ @entries_by_path = {}
end
def edit_entry(path, prototype=nil)
- @source.edit_entry(path, prototype) { |text| yield text }
+ @input.edit_entry(path, prototype) { |text| yield text }
self
end
def each_entry
- unless @entries
- entries = []
- @source.each_entry do |path, entry|
- entries << [path, entry]
+ entries = nil
+ @lock.synchronize do
+ if @entries
+ entries = @entries
+ else
+ entries = []
+ @input.each_entry do |path, entry|
+ @entries_by_path[path] = entry
+ entries << [path, entry]
+ end
+ @entries = entries
end
- @entries = entries
end
- @entries.each do |path, entry|
+ entries.each do |path, entry|
yield path, entry.dup
end
self
end
+
+ def get_entry(path)
+ entry = nil
+ @lock.synchronize do
+ if @entries_by_path.has_key? path
+ entry = @entries_by_path[path]
+ else
+ entry = @input.get_entry(path)
+ @entries_by_path[path] = entry
+ end
+ end
+ return entry.dup
+ end
end
class Sort
- include Source
+ include Filter
- def initialize(source, options)
- @source = source
+ def initialize(input, options)
+ @input = input
@key_fields = Array(options[:sort_by] || []).map { |f| f.to_s }
@reverse = !!options[:reverse]
end
def edit_entry(path, prototype=nil)
- @source.edit_entry(path, prototype) { |text| yield text }
+ @input.edit_entry(path, prototype) { |text| yield text }
self
end
def each_entry
entries = []
- @source.each_entry do |path, entry|
+ @input.each_entry do |path, entry|
entries << [path, entry]
end
unless @key_fields.empty?
entries = entries.sort_by do |path, entry|
@key_fields.map { |f| entry[f] }
@@ -247,21 +310,25 @@
entries.each do |path, entry|
yield path, entry
end
self
end
+
+ def get_entry(path)
+ @input.get_entry(path)
+ end
end
Chain = Object.new
-def Chain.new(source, options)
+def Chain.new(input, options)
filters = options[:chain] || []
options = options.dup
options.delete(:chain) # prevent inheritance
for raw_filter in filters
- source = Hx.build_source(options, source, {}, raw_filter)
+ input = Hx.build_source(options, input, {}, raw_filter)
end
- source
+ input
end
def self.make_default_title(options, path)
name = path.split('/').last
words = name.split(/[_\s-]/)
@@ -284,12 +351,19 @@
def self.local_require(options, library)
saved_require_path = $:.dup
begin
$:.delete(".")
- $:.push Hx.get_pathname(options, :lib_dir).to_s
+ lib_dir = Hx.get_pathname(options, :lib_dir)
+ if lib_dir.relative?
+ $:.push "./#{lib_dir}"
+ else
+ $:.push lib_dir.to_s
+ end
require library
+ rescue LoadError
+ raise
ensure
$:[0..-1] = saved_require_path
end
end
@@ -299,36 +373,36 @@
rescue NameError
raise NameError, "Unable to resolve #{qualified_name}"
end
end
-def self.expand_chain(raw_source)
- case raw_source
+def self.expand_chain(raw_input)
+ case raw_input
when Array # rewrite array to Hx::Chain
- return NULL_SOURCE if raw_source.empty?
+ return NULL_INPUT if raw_input.empty?
- filter_defs = raw_source.dup
+ filter_defs = raw_input.dup
first_filter = filter_defs[0] = filter_defs[0].dup
- raw_source = {
+ raw_input = {
'filter' => 'Hx::Chain',
'options' => {'chain' => filter_defs}
}
- if first_filter.has_key? 'source' # use input of first filter for chain
- raw_source['source'] = first_filter['source']
- first_filter.delete('source')
+ if first_filter.has_key? 'input' # use input of first filter for chain
+ raw_input['input'] = first_filter['input']
+ first_filter.delete('input')
end
end
- raw_source
+ raw_input
end
def self.build_source(options, default_input, sources, raw_source)
raw_source = expand_chain(raw_source)
- if raw_source.has_key? 'source'
- input_name = raw_source['source']
+ if raw_source.has_key? 'input'
+ input_name = raw_source['input']
begin
source = sources.fetch(input_name)
rescue IndexError
raise NameError, "No source named #{input_name} in scope"
end
@@ -381,19 +455,25 @@
source
end
class Site
- include Source
+ include Filter
attr_reader :options
attr_reader :sources
attr_reader :outputs
class << self
private :new
+ def load_file(config_file)
+ File.open(config_file, 'r') do |stream|
+ load(stream, config_file)
+ end
+ end
+
def load(io, config_path)
raw_config = YAML.load(io)
options = {}
options[:base_dir] = File.dirname(config_path)
for key, value in raw_config.fetch('options', {})
@@ -407,20 +487,20 @@
end
raw_sources_by_name = raw_config.fetch('sources', {})
source_names = raw_sources_by_name.keys
- # build source dependency graph
+ # build input dependency graph
source_dependencies = {}
for name, raw_source in raw_sources_by_name
raw_source = Hx.expand_chain(raw_source)
- if raw_source.has_key? 'source'
- source_dependencies[name] = raw_source['source']
+ if raw_source.has_key? 'input'
+ source_dependencies[name] = raw_source['input']
end
end
- # calculate depth for each source in the graph
+ # calculate depth for each input in the graph
source_depths = Hash.new(0)
for name in source_names
seen = Set[] # for cycle detection
while source_dependencies.has_key? name
if seen.include? name
@@ -437,17 +517,17 @@
depth_first_names = source_names.sort_by { |n| -source_depths[n] }
sources = {}
for name in depth_first_names
raw_source = raw_sources_by_name[name]
- sources[name] = Hx.build_source(options, NULL_SOURCE, sources,
+ sources[name] = Hx.build_source(options, NULL_INPUT, sources,
raw_source)
end
outputs = []
for raw_output in raw_config.fetch('outputs', [])
- outputs << Hx.build_source(options, NULL_SOURCE, sources, raw_output)
+ outputs << Hx.build_source(options, NULL_INPUT, sources, raw_output)
end
new(options, sources, outputs)
end
end
@@ -462,55 +542,48 @@
def edit_entry(path, prototype=nil)
@combined_output.edit_entry(path, prototype) { |text| yield text }
self
end
- def each_entry
- @combined_output.each_entry do |path, entry|
- yield path, entry
- end
+ def each_entry_path
+ @combined_output.each_entry_path { |path| yield path }
self
end
-end
-class FileBuilder
- def initialize(output_dir)
- @output_dir = Pathname.new(output_dir)
+ def get_entry(path)
+ @combined_output.get_entry(path)
end
+end
- def build_file(path, entry)
- build_file_helper(path, entry, false)
+def self.refresh_file(pathname, content, update_time)
+ begin
+ return false if update_time and update_time < pathname.mtime
+ rescue Errno::ENOENT
end
+ write_file(pathname, content)
+ true
+end
- def build_file_if_updated(path, entry)
- build_file_helper(path, entry, true)
- end
-
- def build_file_helper(path, entry, update_only)
- filename = @output_dir + path
- return self if update_only and filename.exist? and \
- entry['updated'] and filename.mtime >= entry['updated']
- dirname = filename.parent
- dirname.mkpath()
- filename.open("wb") do |stream|
- stream.write entry['content'].to_s
- end
- self
- end
- private :build_file_helper
+def self.write_file(pathname, content)
+ pathname.parent.mkpath()
+ pathname.open("wb") { |stream| stream << content.to_s }
+ nil
end
class LazyContent
def initialize(&block)
raise ArgumentError, "No block given" unless block
+ @lock = Mutex.new
@content = nil
@block = block
end
def to_s
- if @block
- @content = @block.call
- @block = nil
+ @lock.synchronize do
+ if @block
+ @content = @block.call
+ @block = nil
+ end
end
@content
end
def to_yaml(*args)