mill 0.16 → 0.18

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/Rakefile +1 -9
  4. data/TODO.md +4 -0
  5. data/bin/mill +1 -27
  6. data/lib/mill/command.rb +13 -0
  7. data/lib/mill/commands/build.rb +17 -0
  8. data/lib/mill/commands/check.rb +16 -0
  9. data/lib/mill/commands/diff.rb +18 -0
  10. data/lib/mill/commands/list.rb +20 -0
  11. data/lib/mill/commands/snapshot.rb +20 -0
  12. data/lib/mill/commands/tree.rb +17 -0
  13. data/lib/mill/commands/types.rb +18 -0
  14. data/lib/mill/commands/upload.rb +24 -0
  15. data/lib/mill/config.rb +27 -0
  16. data/lib/mill/resource.rb +74 -87
  17. data/lib/mill/resources/blob.rb +4 -0
  18. data/lib/mill/resources/feed.rb +5 -13
  19. data/lib/mill/resources/image.rb +10 -18
  20. data/lib/mill/resources/markdown.rb +20 -0
  21. data/lib/mill/resources/markup.rb +62 -0
  22. data/lib/mill/resources/page.rb +140 -0
  23. data/lib/mill/resources/redirect.rb +17 -16
  24. data/lib/mill/resources/robots.rb +3 -4
  25. data/lib/mill/resources/sitemap.rb +3 -5
  26. data/lib/mill/resources/stylesheet.rb +4 -4
  27. data/lib/mill/resources/textile.rb +27 -0
  28. data/lib/mill/resources.rb +89 -0
  29. data/lib/mill/site.rb +182 -283
  30. data/lib/mill.rb +27 -9
  31. data/mill.gemspec +19 -19
  32. data/test/content/c.md +4 -0
  33. data/test/content/d.md +4 -0
  34. data/test/main_test.rb +68 -0
  35. data/test/mill.yaml +8 -0
  36. metadata +118 -51
  37. data/Gemfile +0 -6
  38. data/TODO.txt +0 -61
  39. data/lib/mill/html_helpers.rb +0 -128
  40. data/lib/mill/navigator.rb +0 -61
  41. data/lib/mill/resources/dir.rb +0 -31
  42. data/lib/mill/resources/google_site_verification.rb +0 -30
  43. data/lib/mill/resources/text.rb +0 -218
  44. data/lib/mill/version.rb +0 -5
  45. data/test/test.rb +0 -56
data/lib/mill/site.rb CHANGED
@@ -2,391 +2,290 @@ module Mill
2
2
 
3
3
  class Site
4
4
 
5
- attr_accessor :input_dir
6
- attr_accessor :output_dir
7
- attr_accessor :site_rsync
8
- attr_accessor :site_title
9
- attr_accessor :site_uri
10
- attr_accessor :site_email
11
- attr_accessor :site_control_date
12
- attr_accessor :html_version
13
- attr_accessor :feed_resource
14
- attr_accessor :sitemap_resource
15
- attr_accessor :robots_resource
16
- attr_accessor :shorten_uris
17
- attr_accessor :make_feed
18
- attr_accessor :make_sitemap
19
- attr_accessor :make_robots
20
- attr_accessor :allow_robots
21
- attr_accessor :htpasswd_file
22
- attr_accessor :navigator
23
- attr_accessor :resource_classes
24
- attr_accessor :redirects
25
- attr_accessor :resources
26
-
27
- DefaultResourceClasses = ObjectSpace.each_object(Class).select { |c| c < Resource }
28
-
29
- def initialize(input_dir: 'content',
30
- output_dir: 'public_html',
31
- site_rsync: nil,
32
- site_title: nil,
33
- site_uri: 'http://localhost',
34
- site_email: nil,
35
- site_control_date: Date.today.to_s,
36
- html_version: :html4_transitional,
37
- shorten_uris: true,
38
- make_feed: true,
39
- make_sitemap: true,
40
- make_robots: true,
41
- allow_robots: true,
42
- htpasswd_file: nil,
43
- navigator: nil,
44
- google_site_verification: nil,
45
- resource_classes: [],
46
- redirects: {})
47
-
48
- @input_dir = Path.new(input_dir)
49
- @output_dir = Path.new(output_dir)
50
- @site_rsync = site_rsync
51
- @site_title = site_title
52
- @site_uri = Addressable::URI.parse(site_uri)
53
- @site_email = Addressable::URI.parse(site_email) if site_email
54
- @site_control_date = Date.parse(site_control_date)
55
- @html_version = html_version
56
- @shorten_uris = shorten_uris
57
- @make_feed = make_feed
58
- @make_sitemap = make_sitemap
59
- @make_robots = make_robots
60
- @allow_robots = allow_robots
61
- @htpasswd_file = htpasswd_file ? Path.new(htpasswd_file) : nil
62
- @resource_classes = resource_classes
63
- @navigator = navigator
64
- @google_site_verification = google_site_verification
65
- @redirects = redirects
66
-
67
- @resources = {}
68
- @resources_tree = Tree::TreeNode.new('')
69
- build_file_types
70
- end
71
-
72
- def build_file_types
5
+ attr_accessor :config
6
+ attr_reader :feed_resource
7
+ attr_reader :sitemap_resource
8
+ attr_reader :robots_resource
9
+ attr_reader :redirects
10
+ attr_reader :resources
11
+ attr_reader :file_types
12
+
13
+ def self.load(dir=nil)
14
+ config = BaseConfig.make(dir: dir)
15
+ config = config.load_yaml(config.dir / ConfigFileName)
16
+ site_file = config.dir / config.code_dir / 'site.rb'
17
+ klass = load_site_class(site_file)
18
+ klass.new(config)
19
+ end
20
+
21
+ def self.load_site_class(site_file)
22
+ Kernel.load(site_file.expand_path.to_s) if site_file.exist?
23
+ site_classes = subclasses
24
+ if site_classes.length == 0
25
+ self
26
+ elsif site_classes.length > 1
27
+ raise Error, "More than one #{self.class} class defined"
28
+ else
29
+ site_classes.first
30
+ end
31
+ end
32
+
33
+ def initialize(config)
34
+ @config = config
35
+ @redirects = {}
36
+ @resources = Resources.new
37
+ make_file_types
38
+ end
39
+
40
+ def inspect
41
+ "<#{self.class}>"
42
+ end
43
+
44
+ def input_dir = @config.dir / @config.input_dir
45
+ def output_dir = @config.dir / @config.output_dir
46
+ def site_uri = @config.site_uri
47
+ def site_rsync = @config.site_rsync
48
+ def site_title = @config.site_title
49
+ def site_email = @config.site_email
50
+ def site_postal = @config.site_postal
51
+ def site_phone = @config.site_phone
52
+ def site_instagram = @config.site_instagram
53
+ def site_control_date = @config.site_control_date
54
+ def html_version = @config.html_version
55
+ def make_error? = @config.make_error
56
+ def make_feed? = @config.make_feed
57
+ def make_sitemap? = @config.make_sitemap
58
+ def make_robots? = @config.make_robots
59
+ def allow_robots? = @config.allow_robots
60
+
61
+ def make_file_types
73
62
  @file_types = {}
74
- (DefaultResourceClasses + @resource_classes).each do |resource_class|
63
+ get_file_types(Resource)
64
+ end
65
+
66
+ def get_file_types(klass)
67
+ klass.subclasses.each do |resource_class|
75
68
  resource_class.const_get(:FileTypes).each do |type|
76
69
  @file_types[type] = resource_class
77
70
  end
71
+ get_file_types(resource_class)
78
72
  end
79
73
  end
80
74
 
81
75
  def add_resource(resource)
82
- raise "Must assign resource to site" unless resource.site
83
- @resources[resource.path] = resource
84
- node = @resources_tree
85
- resource.path.split('/').reject(&:empty?).each do |component|
86
- node = node[component] || (node << Tree::TreeNode.new(component))
87
- end
88
- resource.node = node
89
- node.content = resource
90
- # ;;warn "added #{resource} as #{resource.path}"
76
+ # ;;warn "adding #{resource.class} as #{resource.path}"
77
+ resource.site = self
78
+ resource.load
79
+ @resources << resource
91
80
  end
92
81
 
93
82
  def find_resource(path)
94
- path = path.path if path.kind_of?(Addressable::URI)
95
- @resources[path] || @resources[path + '/']
83
+ @resources[path]
96
84
  end
97
85
 
98
- def home_resource
99
- find_resource('/')
86
+ def root_resource
87
+ @resources['/']
100
88
  end
101
89
 
102
90
  def tag_uri
103
91
  'tag:%s:' % [
104
92
  [
105
- @site_uri.host.downcase,
106
- @site_control_date
93
+ site_uri.host.downcase,
94
+ site_control_date
107
95
  ].join(','),
108
96
  ]
109
97
  end
110
98
 
111
99
  def feed_author_name
112
- @site_title
100
+ site_title
113
101
  end
114
102
 
115
103
  def feed_author_uri
116
- @site_uri
104
+ site_uri
117
105
  end
118
106
 
119
107
  def feed_author_email
120
- @site_email
121
- end
122
-
123
- def select_resources(selector=nil, &block)
124
- if block_given?
125
- @resources.values.select(&block)
126
- elsif selector.kind_of?(Class)
127
- @resources.values.select { |r| r.kind_of?(selector) }
128
- else
129
- @resources.values.select(selector)
130
- end
108
+ site_email
131
109
  end
132
110
 
133
111
  def feed_resources
134
- public_resources.sort_by(&:date)
112
+ primary_resources
135
113
  end
136
114
 
137
- def public_resources
138
- select_resources(&:public?)
115
+ def sitemap_resources
116
+ primary_resources
139
117
  end
140
118
 
141
- def redirect_resources
142
- select_resources(&:redirect?)
143
- end
144
-
145
- def text_resources
146
- select_resources(&:text?)
147
- end
148
-
149
- def make
150
- build
151
- save
152
- end
153
-
154
- def print_tree(node=nil, level=0)
155
- node ||= @resources_tree
156
- if node.is_root?
157
- print '*'
158
- else
159
- print "\t" * level
160
- end
161
- print " #{node.name.inspect}"
162
- print " <#{node.content&.path}>"
163
- print " (#{node.children.length} children)" if node.has_children?
164
- puts
165
- node.children { |child| print_tree(child, level + 1) }
166
- end
167
-
168
- ListKeys = {
169
- path: :to_s,
170
- input_file: :to_s,
171
- output_file: :to_s,
172
- date: :to_s,
173
- public: :to_s,
174
- class: :to_s,
175
- content: proc { |r| r.content ? ('%s (%dKB)' % [r.content.class, (r.content.to_s.length / 1024.0).ceil]) : nil },
176
- parent: proc { |r| r.parent&.path },
177
- siblings: proc { |r| r.siblings.map(&:path) },
178
- children: proc { |r| r.children.map(&:path) },
179
- }
180
-
181
- def list
182
- build
183
- width = ListKeys.keys.map(&:length).max
184
- select_resources.each do |resource|
185
- ListKeys.each do |key, converter|
186
- value = resource.send(key)
187
- value = case converter
188
- when nil
189
- value
190
- when Symbol
191
- value.send(converter)
192
- when Proc
193
- converter.call(resource)
194
- else
195
- raise
196
- end
197
- print '%*s: ' % [width, key]
198
- case value
199
- when Array
200
- if value.empty?
201
- puts '-'
202
- else
203
- value.each_with_index do |v, i|
204
- print '%*s ' % [width, ''] if i > 0
205
- puts (v.nil? ? '-' : v)
206
- end
207
- end
208
- else
209
- puts (value.nil? ? '-' : value)
210
- end
211
- end
212
- puts
213
- end
214
- puts
119
+ def primary_resources
120
+ @resources.select(&:primary?).sort_by(&:date)
215
121
  end
216
122
 
217
123
  def build
218
- import_resources
219
124
  load_resources
125
+ convert_resources
220
126
  build_resources
127
+ check
221
128
  end
222
129
 
223
- def import_resources
130
+ def load_resources
224
131
  add_files
225
132
  add_redirects
226
- add_google_site_verification if @google_site_verification
227
- add_feed if @make_feed
228
- add_sitemap if @make_sitemap
229
- add_robots if @make_robots
230
- add_htpasswd if @htpasswd_file
231
- end
232
-
233
- def load_resources
234
- on_each_resource do |resource|
235
- # ;;warn "#{resource.path}: loading"
236
- resource.load
237
- end
133
+ add_error if make_error?
134
+ add_feed if make_feed?
135
+ add_sitemap if make_sitemap?
136
+ add_robots if make_robots?
238
137
  end
239
138
 
240
139
  def build_resources
241
- on_each_resource do |resource|
140
+ @resources.each do |resource|
242
141
  # ;;warn "#{resource.path}: building"
243
142
  resource.build
244
143
  end
245
144
  end
246
145
 
247
- def save
248
- clean
249
- @output_dir.mkpath
250
- on_each_resource do |resource|
251
- # ;;warn "#{resource.path}: saving"
252
- resource.save
146
+ def convert_resources
147
+ @resources.select { |r| r.respond_to?(:convert) }.each do |resource|
148
+ new_resource = resource.convert
149
+ @resources.delete(resource)
150
+ if new_resource
151
+ new_resource.load
152
+ add_resource(new_resource)
153
+ end
253
154
  end
254
155
  end
255
156
 
256
- def clean
257
- if @output_dir.exist?
258
- @output_dir.children.reject { |p| p.basename.to_s == '.git' }.each do |path|
157
+ def save
158
+ if output_dir.exist?
159
+ output_dir.children.reject { |p| p.basename.to_s == '.git' }.each do |path|
259
160
  path.rm_rf
260
161
  end
162
+ else
163
+ output_dir.mkpath
164
+ end
165
+ @resources.each do |resource|
166
+ # ;;warn "#{resource.path}: saving"
167
+ resource.save
261
168
  end
262
169
  end
263
170
 
264
- def check
265
- build
266
- checker = WebChecker.new(site_uri: @site_uri, site_dir: @output_dir)
267
- end
268
-
269
- def snapshot
270
- @output_dir.chdir do
271
- system('git',
272
- 'init') unless Path.new('.git').exist?
273
- system('git',
274
- 'add',
275
- '.')
276
- system('git',
277
- 'commit',
278
- '-a',
279
- '-m',
280
- 'Update.')
171
+ def check(external: false)
172
+ build if @resources.empty?
173
+ @resources.of_class(Resource::Page).each do |resource|
174
+ resource.links.each do |link|
175
+ begin
176
+ check_uri(link, external: external)
177
+ rescue Error => e
178
+ warn "#{resource.path}: #{e}"
179
+ end
180
+ end
281
181
  end
282
182
  end
283
183
 
284
- def diff
285
- @output_dir.chdir do
286
- system('git',
287
- 'diff')
288
- end
184
+ def resource_class_for_file(file)
185
+ types = MIME::Types.type_for(file.to_s)
186
+ content_type = types.last&.content_type or raise Error, "Can't determine content type: #{file.to_s.inspect}"
187
+ resource_class_for_type(content_type) or raise Error, "Unknown file type: #{file.to_s.inspect} (#{types.join(', ')})"
289
188
  end
290
189
 
291
- def upload
292
- raise "site_rsync not defined" unless @site_rsync
293
- system('rsync',
294
- '--progress',
295
- '--verbose',
296
- '--archive',
297
- # '--append-verify',
298
- '--exclude=.git',
299
- '--delete-after',
300
- @output_dir.to_s,
301
- @site_rsync)
302
- end
303
-
304
- def on_each_resource(&block)
305
- @resources.values.each do |resource|
306
- begin
307
- yield(resource)
308
- rescue Error => e
309
- raise e, "#{resource.input_file || '-'} (#{resource.path}): #{e}"
310
- end
311
- end
190
+ def resource_class_for_type(type)
191
+ @file_types[type]
312
192
  end
313
193
 
314
194
  private
315
195
 
316
- def resource_class_for_file(file)
317
- type = MIME::Types.of(file.to_s).first
318
- if type && (klass = @file_types[type.content_type])
319
- klass
320
- else
321
- raise Error, "Unknown file type: #{file.to_s.inspect} (#{MIME::Types.of(file.to_s).join(', ')})"
322
- end
323
- end
324
-
325
196
  def add_files
326
- raise Error, "Input directory not found: #{@input_dir}" unless @input_dir.exist?
327
- @input_dir.find do |input_file|
197
+ raise Error, "Input directory not found: #{input_dir}" unless input_dir.exist?
198
+ input_dir.find do |input_file|
328
199
  if input_file.basename.to_s[0] == '.'
329
200
  Find.prune
201
+ elsif input_file.basename.to_s == "Icon\r"
202
+ # skip macOS garbage file
330
203
  elsif input_file.directory?
331
- # skip
204
+ # skip directories
332
205
  else (klass = resource_class_for_file(input_file))
333
- resource = klass.new(
334
- input_file: input_file,
335
- output_file: @output_dir / input_file.relative_to(@input_dir),
336
- site: self)
206
+ resource = klass.new(input: input_file, path: '/' + input_file.relative_to(input_dir).to_s)
337
207
  add_resource(resource)
338
208
  end
339
209
  end
340
210
  end
341
211
 
212
+ def add_error
213
+ input = Simple::Builder.parse_html_document(
214
+ Kramdown::Document.new(
215
+ %Q{
216
+ Something went wrong.
217
+ The page you were looking for doesn’t exist or couldn’t be displayed.
218
+ Please try another option.
219
+ }.gsub(/\s+/, ' ').strip
220
+ ).to_html
221
+ )
222
+ klass = resource_class_for_type('text/html')
223
+ @error_resource = klass.new(
224
+ path: '/error.html',
225
+ title: 'Error',
226
+ primary: false,
227
+ input: input)
228
+ add_resource(@error_resource)
229
+ end
230
+
342
231
  def add_feed
343
- @feed_resource = Resource::Feed.new(
344
- output_file: @output_dir / 'feed.xml',
345
- site: self)
232
+ @feed_resource = Resource::Feed.new(path: '/feed.xml')
346
233
  add_resource(@feed_resource)
347
234
  end
348
235
 
349
236
  def add_sitemap
350
- @sitemap_resource = Resource::Sitemap.new(
351
- output_file: @output_dir / 'sitemap.xml',
352
- site: self)
237
+ @sitemap_resource = Resource::Sitemap.new(path: '/sitemap.xml')
353
238
  add_resource(@sitemap_resource)
354
239
  end
355
240
 
356
241
  def add_robots
357
- @robots_resource = Resource::Robots.new(
358
- output_file: @output_dir / 'robots.txt',
359
- site: self)
242
+ @robots_resource = Resource::Robots.new(path: '/robots.txt')
360
243
  add_resource(@robots_resource)
361
244
  end
362
245
 
363
246
  def add_redirects
364
247
  if @redirects
365
248
  @redirects.each do |from, to|
366
- output_file = @output_dir / Path.new(from).relative_to('/')
367
- resource = Resource::Redirect.new(
368
- output_file: output_file,
369
- redirect_uri: to,
370
- site: self)
249
+ resource = Resource::Redirect.new(path: Path.new(from).add_extension('.redirect').to_s, redirect_uri: to)
371
250
  add_resource(resource)
372
251
  end
373
252
  end
374
253
  end
375
254
 
376
- def add_google_site_verification
377
- resource = Resource::GoogleSiteVerification.new(
378
- output_file: (@output_dir / @google_site_verification).add_extension('.html'),
379
- key: @google_site_verification,
380
- site: self)
381
- add_resource(resource)
255
+ def check_uri(uri, external: false)
256
+ if uri.relative?
257
+ unless find_resource(uri.path)
258
+ raise Error, "NOT FOUND: #{uri}"
259
+ end
260
+ elsif external
261
+ if uri.scheme.start_with?('http')
262
+ # warn "checking external URI: #{uri}"
263
+ begin
264
+ check_external_uri(uri)
265
+ rescue => e
266
+ raise Error, "external URI: #{uri}: #{e}"
267
+ end
268
+ else
269
+ warn "Don't know how to check URI: #{uri}"
270
+ end
271
+ end
382
272
  end
383
273
 
384
- def add_htpasswd
385
- resource = Resource.new(
386
- input_file: @htpasswd_file,
387
- output_file: @output_dir / '.htpasswd',
388
- site: self)
389
- add_resource(resource)
274
+ def check_external_uri(uri)
275
+ response = HTTP.timeout(3).get(uri)
276
+ case response.code
277
+ when 200...300
278
+ # ignore
279
+ when 300...400
280
+ redirect_uri = Addressable::URI.parse(response.headers['Location'])
281
+ check_external_uri(uri + redirect_uri)
282
+ when 404
283
+ raise Error, "URI not found: #{uri}"
284
+ when 999
285
+ # ignore bogus LinkedIn status
286
+ else
287
+ raise Error, "Bad status from #{uri}: #{response.inspect}"
288
+ end
390
289
  end
391
290
 
392
291
  end
data/lib/mill.rb CHANGED
@@ -1,30 +1,48 @@
1
1
  require 'addressable'
2
+ require 'hashstruct'
3
+ require 'http'
2
4
  require 'image_size'
3
5
  require 'kramdown'
4
6
  require 'mime/types'
5
7
  require 'nokogiri'
6
8
  require 'path'
7
- require 'pp'
8
- # require 'pry' rescue nil
9
9
  require 'RedCloth'
10
- require 'rubypants'
10
+ require 'run-command'
11
11
  require 'sassc'
12
+ require 'set_params'
13
+ require 'simple-builder'
14
+ require 'simple-command-parser'
15
+ require 'simple-configurator'
16
+ require 'simple-printer'
12
17
  require 'time'
13
18
  require 'tree'
14
- require 'web-checker'
15
19
 
20
+ class Class
21
+
22
+ def self.subclasses
23
+ constants
24
+ .map { |c| const_get(c) }
25
+ .select { |c| c.kind_of?(Class) }
26
+ end
27
+
28
+ end
29
+
30
+ require 'mill/command'
31
+ require 'mill/config'
16
32
  require 'mill/error'
17
- require 'mill/html_helpers'
18
- require 'mill/navigator'
19
33
  require 'mill/resource'
34
+ require 'mill/resources'
20
35
  require 'mill/resources/blob'
21
36
  require 'mill/resources/feed'
22
- require 'mill/resources/google_site_verification'
23
37
  require 'mill/resources/image'
38
+ require 'mill/resources/markup'
39
+ require 'mill/resources/markdown'
40
+ require 'mill/resources/page'
24
41
  require 'mill/resources/redirect'
25
42
  require 'mill/resources/robots'
26
43
  require 'mill/resources/sitemap'
27
44
  require 'mill/resources/stylesheet'
28
- require 'mill/resources/text'
45
+ require 'mill/resources/textile'
29
46
  require 'mill/site'
30
- require 'mill/version'
47
+
48
+ Path.new(__FILE__).dirname.glob('mill/commands/*.rb').each { |p| require p }