hx 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/bin/hx +3 -0
- data/lib/hx.rb +749 -0
- data/lib/hx/commandline.rb +161 -0
- data/spec/cache_spec.rb +31 -0
- data/spec/hx_dummy.rb +0 -0
- data/spec/hx_dummy2.rb +0 -0
- data/spec/nullsource_spec.rb +25 -0
- data/spec/overlay_spec.rb +40 -0
- data/spec/pathfilter_spec.rb +45 -0
- data/spec/pathops_spec.rb +72 -0
- data/spec/site_spec.rb +65 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +33 -0
- metadata +92 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
# hx/commandline - A very small website generator; commandline interface
|
2
|
+
#
|
3
|
+
# Copyright (c) 2009 MenTaLguY <mental@rydia.net>
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
# a copy of this software and associated documentation files (the
|
7
|
+
# "Software"), to deal in the Software without restriction, including
|
8
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
# the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be
|
14
|
+
# included in all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
require 'hx'
|
25
|
+
require 'ostruct'
|
26
|
+
require 'optparse'
|
27
|
+
require 'pathname'
|
28
|
+
require 'tempfile'
|
29
|
+
|
30
|
+
module Hx
|
31
|
+
module Commandline
|
32
|
+
|
33
|
+
DEFAULT_CONFIG_FILENAME = "hx-config.yaml"
|
34
|
+
|
35
|
+
def self.main(*args)
|
36
|
+
options = OpenStruct.new
|
37
|
+
options.config_file = nil
|
38
|
+
|
39
|
+
OptionParser.new do |opts|
|
40
|
+
opts.banner = "Usage: hx [--config CONFIG_FILE]"
|
41
|
+
|
42
|
+
opts.on("-c", "--config CONFIG_FILE",
|
43
|
+
"Use CONFIG_FILE instead of searching for " +
|
44
|
+
DEFAULT_CONFIG_FILENAME) \
|
45
|
+
do |config_file|
|
46
|
+
options.config_file = Pathname.new(config_file)
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on_tail("-h", "--help", "Show this usage information") do
|
50
|
+
puts opts
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.parse!(args)
|
55
|
+
end
|
56
|
+
|
57
|
+
options.config_file ||= ENV['HX_CONFIG']
|
58
|
+
|
59
|
+
unless options.config_file
|
60
|
+
Pathname.getwd.ascend do |ancestor|
|
61
|
+
filename = ancestor + DEFAULT_CONFIG_FILENAME
|
62
|
+
if filename.exist?
|
63
|
+
options.config_file = filename
|
64
|
+
break
|
65
|
+
end
|
66
|
+
end
|
67
|
+
unless options.config_file
|
68
|
+
raise RuntimeError, "No #{DEFAULT_CONFIG_FILENAME} found"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
site = nil
|
73
|
+
options.config_file.open("r") do |stream|
|
74
|
+
site = Hx::Site.load(stream, options.config_file)
|
75
|
+
end
|
76
|
+
|
77
|
+
subcommand = args.shift || "regen"
|
78
|
+
method_name = "cmd_#{subcommand}".intern
|
79
|
+
begin
|
80
|
+
m = method(method_name)
|
81
|
+
rescue NameError
|
82
|
+
raise ArgumentError, "Unrecognized subcommand: #{subcommand}"
|
83
|
+
end
|
84
|
+
m.call(site, *args)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.cmd_regen(site)
|
88
|
+
output_dir = Hx.get_pathname(site.options, :output_dir)
|
89
|
+
builder = Hx::FileBuilder.new(output_dir.to_s)
|
90
|
+
site.each_entry do |path, entry|
|
91
|
+
puts "===> #{path}"
|
92
|
+
builder.build_file(path, entry)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.cmd_edit(site, pathspec)
|
97
|
+
do_edit(site, pathspec, false)
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.cmd_create(site, pathspec)
|
101
|
+
do_edit(site, pathspec, true)
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.do_edit(site, pathspec, create)
|
105
|
+
source_name, path = pathspec.split(':', 2)
|
106
|
+
path, source_name = source_name, path unless source_name
|
107
|
+
|
108
|
+
if create
|
109
|
+
prototype = {
|
110
|
+
'title' => Hx.make_default_title(site.options, path),
|
111
|
+
'author' => Hx.get_default_author(site.options),
|
112
|
+
'content' => ""
|
113
|
+
}
|
114
|
+
else
|
115
|
+
prototype = nil
|
116
|
+
end
|
117
|
+
|
118
|
+
if source_name
|
119
|
+
source = site.sources[source_name]
|
120
|
+
raise ArgumentError, "No such source #{source_name}" unless source
|
121
|
+
else
|
122
|
+
source = site
|
123
|
+
end
|
124
|
+
|
125
|
+
catch(:unchanged) do
|
126
|
+
begin
|
127
|
+
tempfile = Tempfile.new('hx-entry')
|
128
|
+
original_text = nil
|
129
|
+
loop do
|
130
|
+
begin
|
131
|
+
source.edit_entry(path, prototype) do |text|
|
132
|
+
unless original_text
|
133
|
+
File.open(tempfile.path, 'w') { |s| s << text }
|
134
|
+
original_text = text
|
135
|
+
end
|
136
|
+
# TODO: deal with conflict if text != original_text
|
137
|
+
editor = ENV['EDITOR'] || 'vi'
|
138
|
+
system(editor, tempfile.path)
|
139
|
+
new_text = File.open(tempfile.path, 'r') { |s| s.read }
|
140
|
+
throw(:unchanged) if new_text == text
|
141
|
+
new_text
|
142
|
+
end
|
143
|
+
break
|
144
|
+
rescue Exception => e
|
145
|
+
$stderr.puts e
|
146
|
+
$stderr.print "Edit failed; retry? [Yn] "
|
147
|
+
$stderr.flush
|
148
|
+
response = $stdin.gets.strip
|
149
|
+
unless response =~ /^y/i
|
150
|
+
raise e
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
ensure
|
155
|
+
tempfile.unlink
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|
data/spec/cache_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'hx'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
describe Hx::Cache do
|
7
|
+
before(:each) do
|
8
|
+
@source = FakeSource.new
|
9
|
+
@source.add_entry('foo', 'BLAH')
|
10
|
+
@source.add_entry('bar', 'EEP')
|
11
|
+
@cache = Hx::Cache.new(@source)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should return itself from each_entry" do
|
15
|
+
@cache.each_entry {}.should == @cache
|
16
|
+
end
|
17
|
+
|
18
|
+
it "enumerates the same entries from the source" do
|
19
|
+
@cache.each_entry do |path, entry|
|
20
|
+
entry.should == @source.get_entry(path)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "only reads the source once" do
|
25
|
+
@cache.each_entry {}
|
26
|
+
def @source.each_entry
|
27
|
+
raise RuntimeError, "should not be called"
|
28
|
+
end
|
29
|
+
@cache.each_entry {}
|
30
|
+
end
|
31
|
+
end
|
data/spec/hx_dummy.rb
ADDED
File without changes
|
data/spec/hx_dummy2.rb
ADDED
File without changes
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'hx'
|
4
|
+
|
5
|
+
describe Hx::NullSource do
|
6
|
+
before(:each) do
|
7
|
+
@null_source = Hx::NullSource.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should return itself from each_entry" do
|
11
|
+
@null_source.each_entry {}.should == @null_source
|
12
|
+
end
|
13
|
+
|
14
|
+
it "enumerates no entry paths" do
|
15
|
+
@null_source.each_entry do |path, entry|
|
16
|
+
raise RuntimeError("No entries")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe Hx::NULL_SOURCE do
|
22
|
+
it "is an instance of Hx::NullSource" do
|
23
|
+
Hx::NULL_SOURCE.should be_an_instance_of(Hx::NullSource)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'hx'
|
4
|
+
|
5
|
+
describe Hx::Overlay do
|
6
|
+
before(:each) do
|
7
|
+
@a = FakeSource.new
|
8
|
+
@a.add_entry('foo', 'foo:A')
|
9
|
+
@a.add_entry('bar', 'bar:A')
|
10
|
+
@b = FakeSource.new
|
11
|
+
@b.add_entry('bar', 'bar:B')
|
12
|
+
@b.add_entry('baz', 'baz:B')
|
13
|
+
@overlay = Hx::Overlay.new(@a, @b)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should return itself from each_entry" do
|
17
|
+
@overlay.each_entry {}.should == @overlay
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should expose the union of paths" do
|
21
|
+
actual_paths = []
|
22
|
+
@overlay.each_entry do |path, entry|
|
23
|
+
actual_paths << path
|
24
|
+
end
|
25
|
+
actual_paths.sort.should == %w(foo bar baz).sort
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should give earlier sources precedence" do
|
29
|
+
@overlay.each_entry do |path, entry|
|
30
|
+
entry.should == @a.get_entry('bar') if path == 'bar'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should expose entries from all sources" do
|
35
|
+
@overlay.each_entry do |path, entry|
|
36
|
+
entry.should == @a.get_entry('foo') if path == 'foo'
|
37
|
+
entry.should == @b.get_entry('baz') if path == 'baz'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'hx'
|
4
|
+
|
5
|
+
describe Hx::PathSubset::Predicate do
|
6
|
+
it "accepts anything when the accept and reject patterns are nil" do
|
7
|
+
filter = Hx::PathSubset::Predicate.new(nil, nil)
|
8
|
+
filter.accept?("blah").should be_true
|
9
|
+
end
|
10
|
+
|
11
|
+
it "accepts only paths matching the accept pattern when it is specified" do
|
12
|
+
filter = Hx::PathSubset::Predicate.new("foo", nil)
|
13
|
+
filter.accept?("foo").should be_true
|
14
|
+
filter.accept?("bar").should be_false
|
15
|
+
end
|
16
|
+
|
17
|
+
it "accepts only paths not matching reject, when only that is specified" do
|
18
|
+
filter = Hx::PathSubset::Predicate.new(nil, "foo")
|
19
|
+
filter.accept?("foo").should be_false
|
20
|
+
filter.accept?("bar").should be_true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "matches the difference of accept and reject when both are specified" do
|
24
|
+
filter = Hx::PathSubset::Predicate.new("foo/*", "foo/bar")
|
25
|
+
filter.accept?("foo/bar").should be_false
|
26
|
+
filter.accept?("foo/baz").should be_true
|
27
|
+
filter.accept?("bar").should be_false
|
28
|
+
end
|
29
|
+
|
30
|
+
it "shouldn't match across slashes with single star" do
|
31
|
+
filter = Hx::PathSubset::Predicate.new("foo/*", nil)
|
32
|
+
filter.accept?("bar").should be_false
|
33
|
+
filter.accept?("foo").should be_false
|
34
|
+
filter.accept?("foo/bar").should be_true
|
35
|
+
filter.accept?("foo/bar/baz").should be_false
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should match across slashes with double star" do
|
39
|
+
filter = Hx::PathSubset::Predicate.new("foo/**", nil)
|
40
|
+
filter.accept?("bar").should be_false
|
41
|
+
filter.accept?("foo").should be_false
|
42
|
+
filter.accept?("foo/bar").should be_true
|
43
|
+
filter.accept?("foo/bar/baz").should be_true
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'hx'
|
5
|
+
|
6
|
+
describe Hx::AddPath do
|
7
|
+
before(:each) do
|
8
|
+
@before_paths = Set['foo', 'bar']
|
9
|
+
@after_paths = Set['XXXfooYYY', 'XXXbarYYY']
|
10
|
+
@source = FakeSource.new
|
11
|
+
@source.add_entry('foo', 'FOO')
|
12
|
+
@source.add_entry('bar', 'BAR')
|
13
|
+
@add = Hx::AddPath.new(@source, :prefix => 'XXX', :suffix => 'YYY')
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should return itself from each_entry" do
|
17
|
+
@add.each_entry {}.should == @add
|
18
|
+
end
|
19
|
+
|
20
|
+
it "yields augmented paths from each_entry" do
|
21
|
+
paths = Set[]
|
22
|
+
@add.each_entry do |path, entry|
|
23
|
+
paths.add path
|
24
|
+
end
|
25
|
+
paths.should == @after_paths
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe Hx::StripPath do
|
30
|
+
before(:each) do @before_paths = Set['XXXfooYYY', 'XXXbarYYY', 'lemur']
|
31
|
+
@after_paths = Set['foo', 'bar']
|
32
|
+
@source = FakeSource.new
|
33
|
+
@source.add_entry('XXXfooYYY', 'FOO')
|
34
|
+
@source.add_entry('XXXbarYYY', 'BAR')
|
35
|
+
@strip = Hx::StripPath.new(@source, :prefix => 'XXX', :suffix => 'YYY')
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should return itself from each_entry" do
|
39
|
+
@strip.each_entry {}.should == @strip
|
40
|
+
end
|
41
|
+
|
42
|
+
it "yields stripped paths from each_entry" do
|
43
|
+
paths = Set[]
|
44
|
+
@strip.each_entry do |path, entry|
|
45
|
+
paths.add path
|
46
|
+
end
|
47
|
+
paths.should == @after_paths
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe Hx::PathSubset do
|
52
|
+
before(:each) do
|
53
|
+
@source = FakeSource.new
|
54
|
+
@all_paths = Set['lemur', 'foo/bar', 'foo/baz', 'hoge/hoge']
|
55
|
+
@all_paths.each do |path|
|
56
|
+
@source.add_entry(path, path)
|
57
|
+
end
|
58
|
+
@subset = Hx::PathSubset.new(@source, :only => 'foo/*')
|
59
|
+
end
|
60
|
+
|
61
|
+
it "returns itself from each_entry" do
|
62
|
+
@subset.each_entry {}.should == @subset
|
63
|
+
end
|
64
|
+
|
65
|
+
it "enumerates paths according to the subset filter" do
|
66
|
+
actual_paths = Set[]
|
67
|
+
@subset.each_entry do |path, entry|
|
68
|
+
actual_paths.add path
|
69
|
+
end
|
70
|
+
actual_paths.should == Set['foo/bar', 'foo/baz']
|
71
|
+
end
|
72
|
+
end
|
data/spec/site_spec.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'hx'
|
4
|
+
|
5
|
+
describe "Hx::Site.load" do
|
6
|
+
it "requires dependencies from the config" do
|
7
|
+
yaml_config = YAML.dump({'require' => ['hx_dummy.rb']})
|
8
|
+
Hx::Site.load yaml_config, __FILE__
|
9
|
+
$".should include('hx_dummy.rb')
|
10
|
+
|
11
|
+
yaml_config = YAML.dump({'require' => 'hx_dummy2.rb'})
|
12
|
+
Hx::Site.load yaml_config, __FILE__
|
13
|
+
$".should include('hx_dummy2.rb')
|
14
|
+
end
|
15
|
+
|
16
|
+
it "returns an Hx::Site object" do
|
17
|
+
yaml_config = YAML.dump({})
|
18
|
+
site = Hx::Site.load yaml_config, __FILE__
|
19
|
+
site.should be_an_instance_of(Hx::Site)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "takes default base_dir from config file path" do
|
23
|
+
yaml_config = YAML.dump({})
|
24
|
+
site = Hx::Site.load yaml_config, 'foo/baz/config.hx'
|
25
|
+
site.options[:base_dir].should == 'foo/baz'
|
26
|
+
yaml_config = YAML.dump({'options' => {'base_dir' => 'foo/bar'}})
|
27
|
+
site = Hx::Site.load yaml_config, 'foo/baz/config.hx'
|
28
|
+
site.options[:base_dir].should == 'foo/bar'
|
29
|
+
end
|
30
|
+
|
31
|
+
it "gets global options from the config, with symbol keys" do
|
32
|
+
input_options = {'foo' => 'bar', 'base_dir' => "xyz"}
|
33
|
+
output_options = {:foo => 'bar', :base_dir => "xyz"}
|
34
|
+
yaml_config = YAML.dump({'options' => input_options})
|
35
|
+
site = Hx::Site.load yaml_config, __FILE__
|
36
|
+
site.options[:foo].should == 'bar'
|
37
|
+
site.options[:base_dir].should == 'xyz'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "creates a source for every entry in sources" do
|
41
|
+
yaml_config = YAML.dump({'sources' => {'foo' => {},
|
42
|
+
'bar' => {}}})
|
43
|
+
site = Hx::Site.load(yaml_config, __FILE__)
|
44
|
+
site.sources.size.should == 2
|
45
|
+
site.sources.should include('foo')
|
46
|
+
site.sources.should include('bar')
|
47
|
+
end
|
48
|
+
|
49
|
+
it "creates an output for every entry in outputs" do
|
50
|
+
yaml_config = YAML.dump({'outputs' => [{}, {}]})
|
51
|
+
site = Hx::Site.load(yaml_config, __FILE__)
|
52
|
+
site.outputs.size.should == 2
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe Hx::Site do
|
57
|
+
before(:each) do
|
58
|
+
yaml_config = YAML.dump({})
|
59
|
+
@site = Hx::Site.load(yaml_config, __FILE__)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "returns itself from each_entry" do
|
63
|
+
@site.each_entry {}.should == @site
|
64
|
+
end
|
65
|
+
end
|