mill 0.1 → 0.3
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.
- checksums.yaml +5 -5
- data/Gemfile +0 -2
- data/TODO.txt +48 -25
- data/bin/mill +19 -0
- data/lib/mill.rb +7 -316
- data/lib/mill/error.rb +7 -0
- data/lib/mill/html_helpers.rb +57 -67
- data/lib/mill/navigator.rb +26 -50
- data/lib/mill/resource.rb +78 -59
- data/lib/mill/resources/dir.rb +31 -0
- data/lib/mill/resources/feed.rb +13 -22
- data/lib/mill/resources/google_site_verification.rb +24 -0
- data/lib/mill/resources/image.rb +12 -5
- data/lib/mill/resources/other.rb +29 -0
- data/lib/mill/resources/redirect.rb +7 -13
- data/lib/mill/resources/robots.rb +6 -9
- data/lib/mill/resources/sitemap.rb +3 -7
- data/lib/mill/resources/stylesheet.rb +27 -0
- data/lib/mill/resources/text.rb +139 -59
- data/lib/mill/site.rb +304 -0
- data/lib/mill/version.rb +2 -2
- data/mill.gemspec +1 -3
- data/test/Gemfile +7 -0
- data/test/Rakefile +7 -0
- data/test/content/a.md +3 -0
- data/test/content/b/index.md +3 -0
- data/test/content/index.md +3 -0
- metadata +24 -37
- data/lib/mill/file_types.rb +0 -30
- data/lib/mill/resources/generic.rb +0 -15
- data/lib/mill/tasks.rake +0 -31
data/lib/mill/error.rb
ADDED
data/lib/mill/html_helpers.rb
CHANGED
@@ -1,91 +1,59 @@
|
|
1
1
|
module HTMLHelpers
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
3
|
+
LinkElementsXPath = '//@href | //@src'
|
4
|
+
|
5
|
+
def html_document(type=:html4_transitional, &block)
|
6
|
+
doc = Nokogiri::HTML::Document.new
|
7
|
+
doc.encoding = 'UTF-8'
|
8
|
+
doc.internal_subset.remove
|
9
|
+
case type
|
10
|
+
when :html4_transitional
|
11
|
+
doc.create_internal_subset('html', '-//W3C//DTD HTML 4.01 Transitional//EN', 'http://www.w3.org/TR/html4/loose.dtd')
|
12
|
+
when :html5
|
13
|
+
doc.create_internal_subset('html', nil, nil)
|
14
|
+
else
|
15
|
+
raise "Unknown HTML type: #{type.inspect}"
|
16
|
+
end
|
17
|
+
Nokogiri::HTML::Builder.with(doc) do |doc|
|
15
18
|
yield(doc)
|
16
19
|
end
|
17
|
-
|
20
|
+
doc
|
18
21
|
end
|
19
22
|
|
20
23
|
def html_fragment(&block)
|
21
24
|
html = Nokogiri::HTML::DocumentFragment.parse('')
|
22
25
|
Nokogiri::HTML::Builder.with(html) do |html|
|
23
|
-
yield(html)
|
26
|
+
yield(html) if block_given?
|
24
27
|
end
|
25
28
|
html
|
26
29
|
end
|
27
30
|
|
28
31
|
def parse_html(str)
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
if str.strip.empty?
|
33
|
+
html = html_fragment
|
34
|
+
else
|
35
|
+
html = Nokogiri::HTML::Document.parse(str) { |config| config.strict }
|
36
|
+
check_errors(html)
|
33
37
|
end
|
34
38
|
html
|
35
39
|
end
|
36
40
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
IgnoreErrors.include?(error[:error])
|
42
|
-
end
|
43
|
-
unless errors.empty?
|
44
|
-
full_error = StringIO.new('')
|
45
|
-
full_error.puts "invalid HTML:"
|
46
|
-
html_lines = html_str.split(/\n/)
|
47
|
-
errors.each do |error|
|
48
|
-
full_error.puts "\t#{error[:msg]}:"
|
49
|
-
html_lines.each_with_index do |html_line, i|
|
50
|
-
if i >= [0, error[:line] - 2].max && i <= [error[:line] + 2, html_lines.length].min
|
51
|
-
if i == error[:line]
|
52
|
-
output = [
|
53
|
-
error[:column] > 0 ? (html_line[0 .. error[:column] - 1]) : '',
|
54
|
-
Term::ANSIColor.negative,
|
55
|
-
html_line[error[:column]],
|
56
|
-
Term::ANSIColor.clear,
|
57
|
-
html_line[error[:column] + 1 .. -1],
|
58
|
-
]
|
59
|
-
else
|
60
|
-
output = [html_line]
|
61
|
-
end
|
62
|
-
full_error.puts "\t\t%3s: %s" % [i + 1, output.join]
|
63
|
-
end
|
64
|
-
end
|
65
|
-
if block_given?
|
66
|
-
yield(full_error.string)
|
67
|
-
else
|
68
|
-
STDERR.print(full_error.string)
|
69
|
-
end
|
70
|
-
raise HTMLError, "HTML error: #{error[:msg]}" if error[:type] == :error
|
71
|
-
end
|
72
|
-
end
|
41
|
+
def parse_html_fragment(str)
|
42
|
+
html = Nokogiri::HTML::DocumentFragment.parse(str) { |config| config.strict }
|
43
|
+
check_errors(html)
|
44
|
+
html
|
73
45
|
end
|
74
46
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
78
|
-
error_str =~ /^line (\d+) column (\d+) - (.*?): (.*)$/ or raise "Can't parse error: #{error_str}"
|
79
|
-
{
|
80
|
-
msg: error_str,
|
81
|
-
line: $1.to_i - 1,
|
82
|
-
column: $2.to_i - 1,
|
83
|
-
type: $3.downcase.to_sym,
|
84
|
-
error: $4.strip,
|
85
|
-
}
|
47
|
+
def check_errors(html)
|
48
|
+
html.errors.each do |error|
|
49
|
+
raise Mill::Error, "HTML error #{error}" unless error.message =~ /Tag .+? invalid$/
|
86
50
|
end
|
87
51
|
end
|
88
52
|
|
53
|
+
def find_link_elements(html)
|
54
|
+
html.xpath(LinkElementsXPath)
|
55
|
+
end
|
56
|
+
|
89
57
|
def replace_element(html, xpath, &block)
|
90
58
|
html.xpath(xpath).each do |elem|
|
91
59
|
elem.replace(yield(elem))
|
@@ -145,6 +113,15 @@ module HTMLHelpers
|
|
145
113
|
end
|
146
114
|
end
|
147
115
|
|
116
|
+
def link_if(state, html, &block)
|
117
|
+
elem = html_fragment { |h| yield(h) }
|
118
|
+
if state
|
119
|
+
html.a(href: uri) { html << elem.to_html }
|
120
|
+
else
|
121
|
+
html << elem.to_html
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
148
125
|
class PreText < String
|
149
126
|
|
150
127
|
def to_html
|
@@ -157,8 +134,21 @@ module HTMLHelpers
|
|
157
134
|
|
158
135
|
class ::String
|
159
136
|
|
160
|
-
|
161
|
-
|
137
|
+
Converters = {
|
138
|
+
nil => RubyPants,
|
139
|
+
smart_quotes: RubyPants,
|
140
|
+
markdown: Kramdown::Document,
|
141
|
+
textile: RedCloth,
|
142
|
+
pre: PreText,
|
143
|
+
}
|
144
|
+
|
145
|
+
def to_html(options={})
|
146
|
+
converter = Converters[options[:mode]] or raise "Unknown to_html mode: #{options[:mode].inspect}"
|
147
|
+
html = Nokogiri::HTML::DocumentFragment.parse(converter.new(self).to_html)
|
148
|
+
if !options[:multiline] && (p_elem = html.at_xpath('p'))
|
149
|
+
html = p_elem.children.to_html
|
150
|
+
end
|
151
|
+
html.to_html
|
162
152
|
end
|
163
153
|
|
164
154
|
end
|
data/lib/mill/navigator.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
module Mill
|
2
2
|
|
3
3
|
class Navigator
|
4
4
|
|
@@ -7,76 +7,52 @@ class Mill
|
|
7
7
|
attr_accessor :uri
|
8
8
|
attr_accessor :title
|
9
9
|
|
10
|
-
def initialize(
|
11
|
-
params.each { |k, v| send("#{k}=", v) }
|
12
|
-
end
|
13
|
-
|
14
|
-
def uri=(uri)
|
10
|
+
def initialize(uri:, title: nil)
|
15
11
|
@uri = Addressable::URI.parse(uri)
|
12
|
+
@title = title
|
16
13
|
end
|
17
14
|
|
18
15
|
end
|
19
16
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
17
|
+
def initialize(items: [])
|
18
|
+
@items = Hash[
|
19
|
+
items.map do |uri, title|
|
20
|
+
item = Item.new(uri: uri, title: title)
|
21
|
+
[item.uri, item]
|
22
|
+
end
|
23
|
+
]
|
25
24
|
end
|
26
25
|
|
27
|
-
def
|
28
|
-
|
29
|
-
if (item = @items.find { |item| item.uri.relative? && item.uri == uri })
|
30
|
-
current_item = item
|
31
|
-
else
|
32
|
-
within_item = @items.select do |item|
|
33
|
-
item.uri.relative? && uri.path.start_with?(item.uri.path)
|
34
|
-
end.sort_by do |item|
|
35
|
-
item.uri.path.count('/')
|
36
|
-
end.last
|
37
|
-
end
|
38
|
-
@items.each do |item|
|
39
|
-
if item == current_item
|
40
|
-
state = :current
|
41
|
-
elsif item == within_item
|
42
|
-
state = :within
|
43
|
-
else
|
44
|
-
state = :other
|
45
|
-
end
|
46
|
-
yield(item, state)
|
47
|
-
end
|
26
|
+
def items
|
27
|
+
@items.values
|
48
28
|
end
|
49
29
|
|
50
30
|
def first_item
|
51
|
-
@items.first
|
31
|
+
@items.values.first
|
52
32
|
end
|
53
33
|
|
54
34
|
def last_item
|
55
|
-
@items.last
|
35
|
+
@items.values.last
|
56
36
|
end
|
57
37
|
|
58
38
|
def previous_item(uri)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
39
|
+
if (item = @items[uri])
|
40
|
+
i = @items.values.index(item)
|
41
|
+
if i > 0
|
42
|
+
return @items.values[i - 1]
|
43
|
+
end
|
64
44
|
end
|
45
|
+
nil
|
65
46
|
end
|
66
47
|
|
67
48
|
def next_item(uri)
|
68
|
-
|
69
|
-
|
70
|
-
@items
|
71
|
-
|
72
|
-
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def find_item_index_by_uri(uri)
|
77
|
-
if (item = @items.find { |item| item.uri == uri })
|
78
|
-
@items.index(item)
|
49
|
+
if (item = @items[uri])
|
50
|
+
i = @items.values.index(item)
|
51
|
+
if i < @items.length - 1
|
52
|
+
return @items.values[i + 1]
|
53
|
+
end
|
79
54
|
end
|
55
|
+
nil
|
80
56
|
end
|
81
57
|
|
82
58
|
end
|
data/lib/mill/resource.rb
CHANGED
@@ -1,68 +1,103 @@
|
|
1
|
-
|
1
|
+
module Mill
|
2
2
|
|
3
3
|
class Resource
|
4
4
|
|
5
|
+
FileTypes = []
|
6
|
+
|
5
7
|
attr_accessor :input_file
|
6
8
|
attr_accessor :output_file
|
9
|
+
attr_accessor :type
|
7
10
|
attr_accessor :date
|
8
11
|
attr_accessor :public
|
9
12
|
attr_accessor :content
|
10
|
-
attr_accessor :
|
11
|
-
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
@output_file = Path.new(
|
13
|
+
attr_accessor :site
|
14
|
+
|
15
|
+
def initialize(input_file: nil,
|
16
|
+
output_file: nil,
|
17
|
+
type: nil,
|
18
|
+
date: nil,
|
19
|
+
public: false,
|
20
|
+
content: nil,
|
21
|
+
site: nil)
|
22
|
+
if input_file
|
23
|
+
@input_file = Path.new(input_file)
|
24
|
+
@date = input_file.mtime.to_datetime
|
25
|
+
else
|
26
|
+
@date = DateTime.now
|
27
|
+
end
|
28
|
+
@output_file = Path.new(output_file) if output_file
|
29
|
+
@type = type
|
30
|
+
self.date = date if date
|
31
|
+
self.public = public
|
32
|
+
@content = content
|
33
|
+
@site = site
|
26
34
|
end
|
27
35
|
|
28
|
-
def date=(
|
29
|
-
@date = case
|
30
|
-
when String
|
31
|
-
DateTime.parse(
|
32
|
-
when
|
33
|
-
|
34
|
-
when Date, DateTime
|
35
|
-
x
|
36
|
+
def date=(date)
|
37
|
+
@date = case date
|
38
|
+
when String, Time
|
39
|
+
DateTime.parse(date.to_s)
|
40
|
+
when Date, DateTime, nil
|
41
|
+
date
|
36
42
|
else
|
37
|
-
raise "Can't assign date: #{
|
43
|
+
raise Error, "Can't assign 'date' attribute: #{date.inspect}"
|
38
44
|
end
|
39
45
|
end
|
40
46
|
|
41
|
-
def public=(
|
42
|
-
@public = case
|
47
|
+
def public=(public)
|
48
|
+
@public = case public
|
43
49
|
when 'false', FalseClass
|
44
50
|
false
|
45
51
|
when 'true', TrueClass
|
46
52
|
true
|
47
53
|
else
|
48
|
-
raise "Can't assign public: #{
|
54
|
+
raise Error, "Can't assign 'public' attribute: #{public.inspect}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def public?
|
59
|
+
@public
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
"<%p> input_file: %p, output_file: %p, type: %p, date: %s, public: %p, content: <%p>" % [
|
64
|
+
self.class,
|
65
|
+
@input_file ? @input_file.relative_to(@site.input_dir).to_s : nil,
|
66
|
+
@output_file ? @output_file.relative_to(@site.output_dir).to_s : nil,
|
67
|
+
@type.to_s,
|
68
|
+
@date.to_s,
|
69
|
+
@public,
|
70
|
+
@content && @content.class,
|
71
|
+
]
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_sibling_resources(klass=nil)
|
75
|
+
# parent_uri = parent_uri
|
76
|
+
@site.resources.select do |resource|
|
77
|
+
resource != self &&
|
78
|
+
(klass.nil? || resource.kind_of?(klass)) &&
|
79
|
+
resource.parent_uri == parent_uri
|
49
80
|
end
|
50
81
|
end
|
51
82
|
|
52
83
|
def uri
|
53
|
-
raise "#{@input_file}: No output file defined for #{self.class}" unless @output_file
|
54
|
-
path = '/' + @output_file.relative_to(@
|
84
|
+
raise Error, "#{@input_file}: No output file defined for #{self.class}" unless @output_file
|
85
|
+
path = '/' + @output_file.relative_to(@site.output_dir).to_s
|
55
86
|
path.sub!(%r{/index\.html$}, '/')
|
56
|
-
path.sub!(%r{\.html$}, '') if @
|
57
|
-
Addressable::URI.
|
87
|
+
path.sub!(%r{\.html$}, '') if @site.shorten_uris
|
88
|
+
Addressable::URI.encode(path, Addressable::URI)
|
89
|
+
end
|
90
|
+
|
91
|
+
def parent_uri
|
92
|
+
uri + '.'
|
58
93
|
end
|
59
94
|
|
60
95
|
def absolute_uri
|
61
|
-
@
|
96
|
+
@site.site_uri + uri
|
62
97
|
end
|
63
98
|
|
64
99
|
def tag_uri
|
65
|
-
@
|
100
|
+
@site.tag_uri + uri
|
66
101
|
end
|
67
102
|
|
68
103
|
def change_frequency
|
@@ -74,40 +109,24 @@ class Mill
|
|
74
109
|
end
|
75
110
|
|
76
111
|
def load
|
77
|
-
|
78
|
-
self.date ||= @input_file ? @input_file.mtime : DateTime.now
|
79
|
-
@mill.update_resource(self)
|
112
|
+
# implemented in subclass
|
80
113
|
end
|
81
114
|
|
82
115
|
def build
|
116
|
+
# implemented in subclass
|
117
|
+
end
|
118
|
+
|
119
|
+
def save
|
83
120
|
@output_file.dirname.mkpath
|
84
|
-
if (
|
121
|
+
if (content = final_content)
|
85
122
|
# ;;warn "#{uri}: writing #{@input_file} to #{@output_file}"
|
86
|
-
@output_file.write(
|
123
|
+
@output_file.write(content.to_s)
|
87
124
|
@output_file.utime(@date.to_time, @date.to_time)
|
88
125
|
elsif @input_file
|
89
126
|
# ;;warn "#{uri}: copying #{@input_file} to #{@output_file}"
|
90
127
|
@input_file.copy(@output_file)
|
91
128
|
else
|
92
|
-
raise "Can't build resource without content or input file: #{uri}"
|
93
|
-
end
|
94
|
-
validate
|
95
|
-
end
|
96
|
-
|
97
|
-
def validate
|
98
|
-
if (schema = @mill.schema_for_type(self.class.type))
|
99
|
-
validate_xml(schema)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def validate_xml(schema)
|
104
|
-
doc = Nokogiri::XML::Document.parse(@output_file.open)
|
105
|
-
errors = doc.errors + schema.validate(doc)
|
106
|
-
unless errors.empty?
|
107
|
-
errors.each do |error|
|
108
|
-
warn "[#{error.file}:#{error.line}:#{error.column}] #{error}"
|
109
|
-
end
|
110
|
-
raise "#{uri}: Validation failed"
|
129
|
+
raise Error, "Can't build resource without content or input file: #{uri}"
|
111
130
|
end
|
112
131
|
end
|
113
132
|
|