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
@@ -0,0 +1,31 @@
|
|
1
|
+
module Mill
|
2
|
+
|
3
|
+
class Resource
|
4
|
+
|
5
|
+
class Index < Text
|
6
|
+
|
7
|
+
include HTMLHelpers
|
8
|
+
|
9
|
+
attr_accessor :pages
|
10
|
+
|
11
|
+
def initialize(pages:, **args)
|
12
|
+
@pages = pages
|
13
|
+
super(**args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def load
|
17
|
+
super
|
18
|
+
@content = html_fragment do |html|
|
19
|
+
html.dl do
|
20
|
+
@pages.each do |page|
|
21
|
+
html.dt(page.title)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
data/lib/mill/resources/feed.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# see http://www.sitemaps.org/protocol.php
|
2
2
|
|
3
|
-
|
3
|
+
module Mill
|
4
4
|
|
5
5
|
class Resource
|
6
6
|
|
@@ -8,25 +8,21 @@ class Mill
|
|
8
8
|
|
9
9
|
include HTMLHelpers
|
10
10
|
|
11
|
-
def
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
def load
|
16
|
-
resources = @mill.public_resources.sort_by(&:date)
|
11
|
+
def build
|
12
|
+
resources = @site.feed_resources
|
17
13
|
builder = Nokogiri::XML::Builder.new do |xml|
|
18
14
|
xml.feed(xmlns: 'http://www.w3.org/2005/Atom') do
|
19
|
-
xml.id(@
|
20
|
-
xml.generator(*@
|
21
|
-
xml.title(@
|
22
|
-
xml.link(rel: 'alternate', type: 'text/html', href: @
|
15
|
+
xml.id(@site.tag_uri)
|
16
|
+
# xml.generator(*@site.feed_generator)
|
17
|
+
xml.title(@site.site_title) if @site.site_title
|
18
|
+
xml.link(rel: 'alternate', type: 'text/html', href: @site.home_resource.uri) if @site.home_resource
|
23
19
|
xml.link(rel: 'self', type: 'application/atom+xml', href: uri)
|
24
20
|
xml.author do
|
25
|
-
xml.name(@
|
26
|
-
xml.uri(@
|
27
|
-
xml.email(@
|
21
|
+
xml.name(@site.feed_author_name) if @site.feed_author_name
|
22
|
+
xml.uri(@site.feed_author_uri) if @site.feed_author_uri
|
23
|
+
xml.email(@site.feed_author_email) if @site.feed_author_email
|
28
24
|
end
|
29
|
-
xml.updated(resources.last.date.iso8601)
|
25
|
+
xml.updated(resources.last.date.iso8601) unless resources.empty?
|
30
26
|
resources.each do |resource|
|
31
27
|
xml.entry do
|
32
28
|
xml.title(resource.title) if resource.title
|
@@ -34,13 +30,8 @@ class Mill
|
|
34
30
|
xml.id(resource.tag_uri)
|
35
31
|
xml.updated(resource.date.iso8601)
|
36
32
|
xml.published(resource.date.iso8601)
|
37
|
-
if (resource.
|
38
|
-
type
|
39
|
-
xml.summary(type: type) { xml.cdata(data) } if type && data
|
40
|
-
end
|
41
|
-
if (resource.respond_to?(:feed_content))
|
42
|
-
type, data = resource.feed_content
|
43
|
-
xml.content(type: type) { xml.cdata(data) }
|
33
|
+
if (html = resource.feed_content)
|
34
|
+
xml.content(type: 'html') { xml.cdata(html.to_html) }
|
44
35
|
end
|
45
36
|
end
|
46
37
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Mill
|
2
|
+
|
3
|
+
class Resource
|
4
|
+
|
5
|
+
class GoogleSiteVerification < Resource
|
6
|
+
|
7
|
+
attr_accessor :key
|
8
|
+
|
9
|
+
def initialize(key:, **args)
|
10
|
+
@key = key
|
11
|
+
@public = false
|
12
|
+
super(**args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def load
|
16
|
+
@content = "google-site-verification: #{@key}.html\n"
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/lib/mill/resources/image.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
module Mill
|
2
2
|
|
3
3
|
class Resource
|
4
4
|
|
@@ -6,13 +6,20 @@ class Mill
|
|
6
6
|
|
7
7
|
include HTMLHelpers
|
8
8
|
|
9
|
+
FileTypes = %w{
|
10
|
+
image/gif
|
11
|
+
image/jpeg
|
12
|
+
image/png
|
13
|
+
image/tiff
|
14
|
+
image/vnd.microsoft.icon
|
15
|
+
image/x-icon
|
16
|
+
image/svg
|
17
|
+
image/svg+xml
|
18
|
+
}
|
19
|
+
|
9
20
|
attr_accessor :width
|
10
21
|
attr_accessor :height
|
11
22
|
|
12
|
-
def self.type
|
13
|
-
:image
|
14
|
-
end
|
15
|
-
|
16
23
|
def load
|
17
24
|
info = ImageSize.path(@input_file.to_s)
|
18
25
|
@width, @height = *info.size
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Mill
|
2
|
+
|
3
|
+
class Resource
|
4
|
+
|
5
|
+
class Other < Resource
|
6
|
+
|
7
|
+
FileTypes = %w{
|
8
|
+
application/pdf
|
9
|
+
|
10
|
+
application/x-shockwave-flash
|
11
|
+
|
12
|
+
application/font-sfnt
|
13
|
+
application/x-font-opentype
|
14
|
+
application/x-font-otf
|
15
|
+
application/x-font-truetype
|
16
|
+
application/x-font-ttf
|
17
|
+
|
18
|
+
application/rtf
|
19
|
+
|
20
|
+
application/javascript
|
21
|
+
application/x-javascript
|
22
|
+
text/javascript
|
23
|
+
}
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
module Mill
|
2
2
|
|
3
3
|
class Resource
|
4
4
|
|
@@ -7,16 +7,10 @@ class Mill
|
|
7
7
|
attr_accessor :redirect_uri
|
8
8
|
attr_accessor :redirect_code
|
9
9
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
def initialize(params={})
|
15
|
-
super(
|
16
|
-
{
|
17
|
-
redirect_code: 303,
|
18
|
-
}.merge(params)
|
19
|
-
)
|
10
|
+
def initialize(redirect_uri:, redirect_code: 303, **args)
|
11
|
+
@redirect_uri = redirect_uri
|
12
|
+
@redirect_code = redirect_code
|
13
|
+
super(**args)
|
20
14
|
end
|
21
15
|
|
22
16
|
def load
|
@@ -24,7 +18,7 @@ class Mill
|
|
24
18
|
super
|
25
19
|
end
|
26
20
|
|
27
|
-
def
|
21
|
+
def save
|
28
22
|
@output_file = @output_file.add_extension('.redirect')
|
29
23
|
super
|
30
24
|
end
|
@@ -33,4 +27,4 @@ class Mill
|
|
33
27
|
|
34
28
|
end
|
35
29
|
|
36
|
-
end
|
30
|
+
end
|
@@ -1,19 +1,16 @@
|
|
1
1
|
# see http://www.robotstxt.org/robotstxt.html
|
2
2
|
|
3
|
-
|
3
|
+
module Mill
|
4
4
|
|
5
5
|
class Resource
|
6
6
|
|
7
7
|
class Robots < Resource
|
8
8
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
info = {
|
15
|
-
'Sitemap' => @mill.sitemap_resource.absolute_uri,
|
16
|
-
}
|
9
|
+
def build
|
10
|
+
info = {}
|
11
|
+
info['User-Agent'] = '*'
|
12
|
+
info['Disallow'] = '/' unless @site.allow_robots
|
13
|
+
info['Sitemap'] = @site.sitemap_resource.absolute_uri if @site.make_sitemap
|
17
14
|
@content = info.map { |key, value| "#{key}: #{value}" }.join("\n")
|
18
15
|
super
|
19
16
|
end
|
@@ -1,21 +1,17 @@
|
|
1
1
|
# see http://www.sitemaps.org/protocol.php
|
2
2
|
|
3
|
-
|
3
|
+
module Mill
|
4
4
|
|
5
5
|
class Resource
|
6
6
|
|
7
7
|
class Sitemap < Resource
|
8
8
|
|
9
|
-
def
|
10
|
-
:sitemap
|
11
|
-
end
|
12
|
-
|
13
|
-
def load
|
9
|
+
def build
|
14
10
|
builder = Nokogiri::XML::Builder.new do |xml|
|
15
11
|
xml.urlset('xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9',
|
16
12
|
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
|
17
13
|
'xsi:schemaLocation' => 'http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd') do
|
18
|
-
@
|
14
|
+
@site.public_resources.each do |resource|
|
19
15
|
xml.url do
|
20
16
|
xml.loc(resource.absolute_uri)
|
21
17
|
xml.lastmod(resource.date.iso8601)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Mill
|
2
|
+
|
3
|
+
class Resource
|
4
|
+
|
5
|
+
class Stylesheet < Resource
|
6
|
+
|
7
|
+
FileTypes = %w{
|
8
|
+
text/css
|
9
|
+
}
|
10
|
+
|
11
|
+
def load
|
12
|
+
super
|
13
|
+
unless @input_file.basename.to_s.end_with?('.min.css')
|
14
|
+
engine = Sass::Engine.for_file(@input_file.to_s, syntax: :scss, style: :compressed)
|
15
|
+
begin
|
16
|
+
@content = engine.render
|
17
|
+
rescue Sass::SyntaxError => e
|
18
|
+
raise "#{input_file}: error parsing CSS: #{e}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
data/lib/mill/resources/text.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
module Mill
|
2
2
|
|
3
3
|
class Resource
|
4
4
|
|
@@ -6,54 +6,79 @@ class Mill
|
|
6
6
|
|
7
7
|
include HTMLHelpers
|
8
8
|
|
9
|
-
|
9
|
+
FileTypes = %w{
|
10
|
+
text/plain
|
11
|
+
text/html
|
12
|
+
}
|
10
13
|
|
11
|
-
|
12
|
-
|
14
|
+
attr_accessor :title
|
15
|
+
attr_accessor :summary
|
16
|
+
attr_accessor :author
|
17
|
+
|
18
|
+
def initialize(title: nil, summary: nil, author: nil, public: true, **args)
|
19
|
+
@title = title
|
20
|
+
@summary = summary
|
21
|
+
@author = author
|
22
|
+
super(public: public, **args)
|
13
23
|
end
|
14
24
|
|
15
|
-
def
|
16
|
-
super
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
def inspect
|
26
|
+
super + ", title: %p, summary: %p, author: %p" % [
|
27
|
+
@title,
|
28
|
+
@summary,
|
29
|
+
@author,
|
30
|
+
]
|
21
31
|
end
|
22
32
|
|
23
33
|
def load
|
34
|
+
super
|
24
35
|
if @input_file
|
25
36
|
@content = @input_file.read
|
26
|
-
|
37
|
+
mode = case @input_file.extname.downcase
|
27
38
|
when '.md', '.mdown', '.markdown'
|
28
|
-
|
39
|
+
:markdown
|
29
40
|
when '.textile'
|
30
|
-
|
41
|
+
:textile
|
31
42
|
when '.txt'
|
32
|
-
|
43
|
+
:pre
|
44
|
+
when '.htm', '.html'
|
45
|
+
:html
|
33
46
|
else
|
34
|
-
|
47
|
+
raise "Unknown text type: #{@input_file}"
|
35
48
|
end
|
36
|
-
if
|
49
|
+
if mode != :html
|
37
50
|
parse_text_header
|
38
|
-
|
39
|
-
@content = markup_class.new(@content).to_html
|
51
|
+
@content = (@content || '').to_html(mode: mode, multiline: true)
|
40
52
|
@output_file = @output_file.replace_extension('.html')
|
41
53
|
end
|
42
54
|
begin
|
43
55
|
@content = parse_html(@content)
|
44
|
-
rescue
|
45
|
-
raise "
|
56
|
+
rescue Error => e
|
57
|
+
raise e, "#{@input_file}: #{e}"
|
46
58
|
end
|
47
59
|
parse_html_header
|
48
60
|
end
|
49
|
-
|
50
|
-
|
61
|
+
end
|
62
|
+
|
63
|
+
def build
|
64
|
+
replace_elements
|
51
65
|
super
|
52
66
|
end
|
53
67
|
|
68
|
+
def replace_elements
|
69
|
+
replace_pages_element
|
70
|
+
remove_comments
|
71
|
+
add_image_sizes
|
72
|
+
# shorten_links
|
73
|
+
end
|
74
|
+
|
54
75
|
def parse_html_header
|
55
|
-
|
56
|
-
|
76
|
+
unless @title
|
77
|
+
if (title_elem = @content.at_xpath('/html/head/title'))
|
78
|
+
@title = title_elem.text
|
79
|
+
else
|
80
|
+
@title = uri.to_s
|
81
|
+
end
|
57
82
|
end
|
58
83
|
@content.xpath('/html/head/meta[@name]').each do |meta|
|
59
84
|
send("#{meta['name']}=", meta['content'])
|
@@ -61,7 +86,7 @@ class Mill
|
|
61
86
|
end
|
62
87
|
|
63
88
|
def parse_text_header
|
64
|
-
if @content =~ /^\w+:\s+/
|
89
|
+
if @content.split(/\n/, 2).first =~ /^\w+:\s+/
|
65
90
|
header, @content = @content.split(/\n\n/, 2)
|
66
91
|
header.split(/\n/).map do |line|
|
67
92
|
key, value = line.strip.split(/:\s+/, 2)
|
@@ -72,82 +97,137 @@ class Mill
|
|
72
97
|
end
|
73
98
|
|
74
99
|
def final_content
|
75
|
-
html_document do |doc|
|
100
|
+
html_document(@site.html_version) do |doc|
|
76
101
|
doc.html(lang: 'en') do |html|
|
77
|
-
html.
|
78
|
-
|
102
|
+
html << head.to_html
|
103
|
+
html << body.to_html
|
104
|
+
end
|
105
|
+
end.to_html
|
106
|
+
end
|
107
|
+
|
108
|
+
def head(&block)
|
109
|
+
html_fragment do |html|
|
110
|
+
html.head do
|
111
|
+
head = content_head
|
112
|
+
if (title = @title || (head && head.at_xpath('title')))
|
113
|
+
html.title { html << title.to_html }
|
79
114
|
end
|
80
|
-
html
|
81
|
-
|
115
|
+
yield(html) if block_given?
|
116
|
+
if head
|
117
|
+
head.children.reject { |e| e.text? || e.name == 'title' }.each do |e|
|
118
|
+
html << e.to_html
|
119
|
+
end
|
82
120
|
end
|
83
121
|
end
|
84
122
|
end
|
85
123
|
end
|
86
124
|
|
87
|
-
def
|
88
|
-
|
125
|
+
def body(&block)
|
126
|
+
html_fragment do |html|
|
127
|
+
html.body do
|
128
|
+
if (elem = content_body)
|
129
|
+
html << elem.children.to_html
|
130
|
+
end
|
131
|
+
yield(html) if block_given?
|
132
|
+
end
|
133
|
+
end
|
89
134
|
end
|
90
135
|
|
91
|
-
def
|
92
|
-
@content.at_xpath('/html/
|
136
|
+
def content_head
|
137
|
+
@content && @content.at_xpath('/html/head')
|
93
138
|
end
|
94
139
|
|
95
|
-
def
|
96
|
-
|
97
|
-
warn "#{uri}: #{error_str}"
|
98
|
-
end
|
140
|
+
def content_body
|
141
|
+
@content && @content.at_xpath('/html/body')
|
99
142
|
end
|
100
143
|
|
101
144
|
def add_external_link_targets
|
102
145
|
@content.xpath('//a').each do |a|
|
103
146
|
if a['href'] && a['href'] =~ /^\w+:/
|
104
147
|
a['target'] = '_blank'
|
148
|
+
a['rel'] = 'noopener noreferrer'
|
105
149
|
end
|
106
150
|
end
|
107
151
|
end
|
108
152
|
|
153
|
+
def remove_comments
|
154
|
+
@content.xpath('//comment()').each do |comment|
|
155
|
+
comment.remove
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
109
159
|
def add_image_sizes
|
110
160
|
@content.xpath('//img').each do |img|
|
111
161
|
# skip elements that already have width/height defined
|
112
162
|
next if img[:width] || img[:height]
|
113
163
|
img_link = Addressable::URI.parse(img['src'])
|
114
|
-
raise "
|
164
|
+
raise Error, "No link in <img> element: #{img.to_s}" if img_link.nil? || img_link.empty?
|
115
165
|
next if img_link.host
|
116
166
|
img_uri = uri + img_link
|
117
|
-
img_resource = @
|
167
|
+
img_resource = @site.find_resource(img_uri) or raise Error, "Can't find image for #{img_uri}"
|
118
168
|
img[:width], img[:height] = img_resource.width, img_resource.height
|
119
169
|
end
|
120
170
|
end
|
121
171
|
|
122
|
-
def
|
123
|
-
@
|
124
|
-
|
172
|
+
def shorten_links
|
173
|
+
find_link_elements(@content).each do |attribute|
|
174
|
+
elem = attribute.parent
|
175
|
+
link_uri = Addressable::URI.parse(attribute.value) or raise Error, "Can't parse #{attribute.value.inspect} from #{xpath.inspect}"
|
176
|
+
link_uri = uri + link_uri
|
177
|
+
if link_uri.relative?
|
178
|
+
self_uri = uri.normalize
|
179
|
+
self_uri.scheme = 'http'
|
180
|
+
link_uri.scheme = 'http'
|
181
|
+
attribute.value = self_uri.route_to(link_uri)
|
182
|
+
# ;;warn "[#{uri}] shortened link #{elem.name}/@#{attribute.name}: #{link_uri} => #{attribute.value}"
|
183
|
+
end
|
125
184
|
end
|
126
185
|
end
|
127
186
|
|
128
|
-
def
|
129
|
-
@content
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
187
|
+
def replace_pages_element
|
188
|
+
replace_element(@content, '//pages') do |elem|
|
189
|
+
html_fragment do |html|
|
190
|
+
html.dl do
|
191
|
+
find_sibling_resources(Resource::Text).each do |page|
|
192
|
+
html.dt do
|
193
|
+
html.a(href: page.uri) { html << page.title.to_html }
|
194
|
+
end
|
195
|
+
html.dd do
|
196
|
+
if (summary = page.summary)
|
197
|
+
html << summary.to_html
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
135
202
|
end
|
136
203
|
end
|
137
204
|
end
|
138
205
|
|
139
|
-
def
|
140
|
-
|
141
|
-
if (p = @content.at_xpath('/html/body/p[1]'))
|
142
|
-
['html', p.to_html]
|
143
|
-
else
|
144
|
-
nil
|
145
|
-
end
|
206
|
+
def summary
|
207
|
+
@summary || ((p = feed_content.at_xpath('//p')) && p.children)
|
146
208
|
end
|
147
209
|
|
148
210
|
def feed_content
|
149
|
-
body =
|
150
|
-
|
211
|
+
if (body = content_body)
|
212
|
+
# If we have a main element (<div class="main"> or <main>), use that.
|
213
|
+
# Otherwise, use the body, but delete header/footer/nav divs or elements.
|
214
|
+
if (main = body.at_xpath('//div[@id="main"]')) || (main = body.at_xpath('//main'))
|
215
|
+
main.children
|
216
|
+
elsif (article = body.at_xpath('//article'))
|
217
|
+
article.children
|
218
|
+
else
|
219
|
+
body_elem = body.clone
|
220
|
+
%w{header nav masthead footer}.each do |name|
|
221
|
+
if (elem = body_elem.at_xpath("//div[@id=\"#{name}\"]")) || (elem = body_elem.at_xpath("//#{name}"))
|
222
|
+
elem.remove
|
223
|
+
end
|
224
|
+
end
|
225
|
+
body_elem.children
|
226
|
+
end
|
227
|
+
else
|
228
|
+
warn "Warning: Resource #{uri} (#{self.class}) has no content"
|
229
|
+
nil
|
230
|
+
end
|
151
231
|
end
|
152
232
|
|
153
233
|
end
|