hx 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|