hx 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.1
1
+ 0.7.0
data/lib/hx.rb CHANGED
@@ -22,6 +22,7 @@
22
22
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
 
24
24
  require 'rubygems'
25
+ require 'thread'
25
26
  require 'set'
26
27
  require 'pathname'
27
28
  require 'yaml'
@@ -36,49 +37,72 @@ end
36
37
  class EditingNotSupportedError < RuntimeError
37
38
  end
38
39
 
39
- module Source
40
+ module Filter
40
41
  def edit_entry(path, prototype=nil)
41
42
  raise EditingNotSupportedError, "Editing not supported for #{path}"
42
43
  end
43
44
 
45
+ def each_entry_path
46
+ each_entry { |path, entry| yield path }
47
+ end
48
+
44
49
  def each_entry
45
- raise NotImplementedError, "#{self.class}#each_entry not implemented"
50
+ each_entry_path do |path|
51
+ begin
52
+ entry = get_entry(path)
53
+ rescue NoSuchEntryError
54
+ next # entries may come and go during the enumeration
55
+ end
56
+ yield path, entry
57
+ end
58
+ end
59
+
60
+ def get_entry(path)
61
+ each_entry do |entry_path, entry|
62
+ return entry if entry_path == path
63
+ end
64
+ raise NoSuchEntryError, path
46
65
  end
47
66
  end
48
67
 
49
- class NullSource
50
- include Source
68
+ class NullInput
69
+ include Filter
51
70
 
52
71
  def each_entry
53
72
  self
54
73
  end
55
74
  end
56
75
 
57
- NULL_SOURCE = NullSource.new
76
+ NULL_INPUT = NullInput.new
58
77
 
59
78
  class PathSubset
60
- include Source
79
+ include Filter
61
80
 
62
- def initialize(source, options)
63
- @source = source
81
+ def initialize(input, options)
82
+ @input = input
64
83
  @path_filter = Predicate.new(options[:only], options[:except])
65
84
  end
66
85
 
67
86
  def edit_entry(path, prototype=nil)
68
87
  if @path_filter.accept? path
69
- @source.edit_entry(path, prototype) { |text| yield text }
88
+ @input.edit_entry(path, prototype) { |text| yield text }
70
89
  else
71
90
  raise EditingNotSupportedError, "Editing not supported for #{path}"
72
91
  end
73
92
  self
74
93
  end
75
94
 
76
- def each_entry
77
- @source.each_entry do |path, entry|
78
- yield path, entry if @path_filter.accept? path
95
+ def each_entry_path
96
+ @input.each_entry_path do |path|
97
+ yield path if @path_filter.accept? path
79
98
  end
80
99
  self
81
100
  end
101
+
102
+ def get_entry(path)
103
+ raise NoSuchEntryError, path unless @path_filter.accept? path
104
+ @input.get_entry(path)
105
+ end
82
106
  end
83
107
 
84
108
  class PathSubset::Predicate
@@ -115,29 +139,39 @@ class PathSubset::Predicate
115
139
  end
116
140
 
117
141
  class Overlay
118
- include Source
142
+ include Filter
119
143
 
120
- def initialize(*sources)
121
- @sources = sources
144
+ def initialize(*inputs)
145
+ @inputs = inputs
122
146
  end
123
147
 
124
- def each_entry
148
+ def each_entry_path
125
149
  seen = Set[]
126
- @sources.each do |source|
127
- source.each_entry do |path, entry|
128
- yield path, entry unless seen.include? path
150
+ @inputs.each do |input|
151
+ input.each_entry_path do |path|
152
+ yield path unless seen.include? path
129
153
  seen.add path
130
154
  end
131
155
  end
132
156
  self
133
157
  end
158
+
159
+ def get_entry(path)
160
+ @inputs.each do |input|
161
+ begin
162
+ return input.get_entry(path)
163
+ rescue NoSuchEntryError
164
+ end
165
+ end
166
+ raise NoSuchEntryError, path
167
+ end
134
168
  end
135
169
 
136
170
  module CircumfixPath
137
- include Source
171
+ include Filter
138
172
 
139
- def initialize(source, options)
140
- @source = source
173
+ def initialize(input, options)
174
+ @input = input
141
175
  @prefix = options[:prefix]
142
176
  @suffix = options[:suffix]
143
177
  prefix = Regexp.quote(@prefix.to_s)
@@ -161,16 +195,20 @@ class AddPath
161
195
  def edit_entry(path, prototype=nil)
162
196
  path = strip_circumfix(path)
163
197
  raise EditingNotSupportedError, "Editing not supported for #{path}" unless path
164
- @source.edit_entry(path, prototype) { |text| yield text }
198
+ @input.edit_entry(path, prototype) { |text| yield text }
165
199
  self
166
200
  end
167
201
 
168
- def each_entry
169
- @source.each_entry do |path, entry|
170
- yield add_circumfix(path), entry
171
- end
202
+ def each_entry_path
203
+ @input.each_entry_path { |path| yield add_circumfix(path) }
172
204
  self
173
205
  end
206
+
207
+ def get_entry(path)
208
+ path = strip_circumfix(path)
209
+ raise NoSuchEntryError, path unless path
210
+ @input.get_entry(path)
211
+ end
174
212
  end
175
213
 
176
214
  class StripPath
@@ -178,64 +216,89 @@ class StripPath
178
216
 
179
217
  def edit_entry(path, prototype=nil)
180
218
  path = add_circumfix(path)
181
- @source.edit_entry(path, prototype) { |text| yield text }
219
+ @input.edit_entry(path, prototype) { |text| yield text }
182
220
  self
183
221
  end
184
222
 
185
- def each_entry
186
- @source.each_entry do |path, entry|
223
+ def each_entry_path
224
+ @input.each_entry_path do |path|
187
225
  path = strip_circumfix(path)
188
- yield path, entry if path
226
+ yield path if path
189
227
  end
190
228
  self
191
229
  end
230
+
231
+ def get_entry(path)
232
+ @input.get_entry(add_circumfix(path))
233
+ end
192
234
  end
193
235
 
194
236
  class Cache
195
- include Source
237
+ include Filter
196
238
 
197
- def initialize(source, options={})
198
- @source = source
239
+ def initialize(input, options={})
240
+ @input = input
241
+ @lock = Mutex.new
199
242
  @entries = nil
243
+ @entries_by_path = {}
200
244
  end
201
245
 
202
246
  def edit_entry(path, prototype=nil)
203
- @source.edit_entry(path, prototype) { |text| yield text }
247
+ @input.edit_entry(path, prototype) { |text| yield text }
204
248
  self
205
249
  end
206
250
 
207
251
  def each_entry
208
- unless @entries
209
- entries = []
210
- @source.each_entry do |path, entry|
211
- entries << [path, entry]
252
+ entries = nil
253
+ @lock.synchronize do
254
+ if @entries
255
+ entries = @entries
256
+ else
257
+ entries = []
258
+ @input.each_entry do |path, entry|
259
+ @entries_by_path[path] = entry
260
+ entries << [path, entry]
261
+ end
262
+ @entries = entries
212
263
  end
213
- @entries = entries
214
264
  end
215
- @entries.each do |path, entry|
265
+ entries.each do |path, entry|
216
266
  yield path, entry.dup
217
267
  end
218
268
  self
219
269
  end
270
+
271
+ def get_entry(path)
272
+ entry = nil
273
+ @lock.synchronize do
274
+ if @entries_by_path.has_key? path
275
+ entry = @entries_by_path[path]
276
+ else
277
+ entry = @input.get_entry(path)
278
+ @entries_by_path[path] = entry
279
+ end
280
+ end
281
+ return entry.dup
282
+ end
220
283
  end
221
284
 
222
285
  class Sort
223
- include Source
286
+ include Filter
224
287
 
225
- def initialize(source, options)
226
- @source = source
288
+ def initialize(input, options)
289
+ @input = input
227
290
  @key_fields = Array(options[:sort_by] || []).map { |f| f.to_s }
228
291
  @reverse = !!options[:reverse]
229
292
  end
230
293
 
231
294
  def edit_entry(path, prototype=nil)
232
- @source.edit_entry(path, prototype) { |text| yield text }
295
+ @input.edit_entry(path, prototype) { |text| yield text }
233
296
  self
234
297
  end
235
298
 
236
299
  def each_entry
237
300
  entries = []
238
- @source.each_entry do |path, entry|
301
+ @input.each_entry do |path, entry|
239
302
  entries << [path, entry]
240
303
  end
241
304
  unless @key_fields.empty?
@@ -249,17 +312,21 @@ class Sort
249
312
  end
250
313
  self
251
314
  end
315
+
316
+ def get_entry(path)
317
+ @input.get_entry(path)
318
+ end
252
319
  end
253
320
 
254
321
  Chain = Object.new
255
- def Chain.new(source, options)
322
+ def Chain.new(input, options)
256
323
  filters = options[:chain] || []
257
324
  options = options.dup
258
325
  options.delete(:chain) # prevent inheritance
259
326
  for raw_filter in filters
260
- source = Hx.build_source(options, source, {}, raw_filter)
327
+ input = Hx.build_source(options, input, {}, raw_filter)
261
328
  end
262
- source
329
+ input
263
330
  end
264
331
 
265
332
  def self.make_default_title(options, path)
@@ -286,8 +353,15 @@ def self.local_require(options, library)
286
353
  saved_require_path = $:.dup
287
354
  begin
288
355
  $:.delete(".")
289
- $:.push Hx.get_pathname(options, :lib_dir).to_s
356
+ lib_dir = Hx.get_pathname(options, :lib_dir)
357
+ if lib_dir.relative?
358
+ $:.push "./#{lib_dir}"
359
+ else
360
+ $:.push lib_dir.to_s
361
+ end
290
362
  require library
363
+ rescue LoadError
364
+ raise
291
365
  ensure
292
366
  $:[0..-1] = saved_require_path
293
367
  end
@@ -301,32 +375,32 @@ def self.resolve_constant(qualified_name, root=Object)
301
375
  end
302
376
  end
303
377
 
304
- def self.expand_chain(raw_source)
305
- case raw_source
378
+ def self.expand_chain(raw_input)
379
+ case raw_input
306
380
  when Array # rewrite array to Hx::Chain
307
- return NULL_SOURCE if raw_source.empty?
381
+ return NULL_INPUT if raw_input.empty?
308
382
 
309
- filter_defs = raw_source.dup
383
+ filter_defs = raw_input.dup
310
384
  first_filter = filter_defs[0] = filter_defs[0].dup
311
385
 
312
- raw_source = {
386
+ raw_input = {
313
387
  'filter' => 'Hx::Chain',
314
388
  'options' => {'chain' => filter_defs}
315
389
  }
316
390
 
317
- if first_filter.has_key? 'source' # use input of first filter for chain
318
- raw_source['source'] = first_filter['source']
319
- first_filter.delete('source')
391
+ if first_filter.has_key? 'input' # use input of first filter for chain
392
+ raw_input['input'] = first_filter['input']
393
+ first_filter.delete('input')
320
394
  end
321
395
  end
322
- raw_source
396
+ raw_input
323
397
  end
324
398
 
325
399
  def self.build_source(options, default_input, sources, raw_source)
326
400
  raw_source = expand_chain(raw_source)
327
401
 
328
- if raw_source.has_key? 'source'
329
- input_name = raw_source['source']
402
+ if raw_source.has_key? 'input'
403
+ input_name = raw_source['input']
330
404
  begin
331
405
  source = sources.fetch(input_name)
332
406
  rescue IndexError
@@ -383,7 +457,7 @@ def self.build_source(options, default_input, sources, raw_source)
383
457
  end
384
458
 
385
459
  class Site
386
- include Source
460
+ include Filter
387
461
 
388
462
  attr_reader :options
389
463
  attr_reader :sources
@@ -392,6 +466,12 @@ class Site
392
466
  class << self
393
467
  private :new
394
468
 
469
+ def load_file(config_file)
470
+ File.open(config_file, 'r') do |stream|
471
+ load(stream, config_file)
472
+ end
473
+ end
474
+
395
475
  def load(io, config_path)
396
476
  raw_config = YAML.load(io)
397
477
  options = {}
@@ -409,16 +489,16 @@ class Site
409
489
  raw_sources_by_name = raw_config.fetch('sources', {})
410
490
  source_names = raw_sources_by_name.keys
411
491
 
412
- # build source dependency graph
492
+ # build input dependency graph
413
493
  source_dependencies = {}
414
494
  for name, raw_source in raw_sources_by_name
415
495
  raw_source = Hx.expand_chain(raw_source)
416
- if raw_source.has_key? 'source'
417
- source_dependencies[name] = raw_source['source']
496
+ if raw_source.has_key? 'input'
497
+ source_dependencies[name] = raw_source['input']
418
498
  end
419
499
  end
420
500
 
421
- # calculate depth for each source in the graph
501
+ # calculate depth for each input in the graph
422
502
  source_depths = Hash.new(0)
423
503
  for name in source_names
424
504
  seen = Set[] # for cycle detection
@@ -439,13 +519,13 @@ class Site
439
519
  sources = {}
440
520
  for name in depth_first_names
441
521
  raw_source = raw_sources_by_name[name]
442
- sources[name] = Hx.build_source(options, NULL_SOURCE, sources,
522
+ sources[name] = Hx.build_source(options, NULL_INPUT, sources,
443
523
  raw_source)
444
524
  end
445
525
 
446
526
  outputs = []
447
527
  for raw_output in raw_config.fetch('outputs', [])
448
- outputs << Hx.build_source(options, NULL_SOURCE, sources, raw_output)
528
+ outputs << Hx.build_source(options, NULL_INPUT, sources, raw_output)
449
529
  end
450
530
 
451
531
  new(options, sources, outputs)
@@ -464,52 +544,45 @@ class Site
464
544
  self
465
545
  end
466
546
 
467
- def each_entry
468
- @combined_output.each_entry do |path, entry|
469
- yield path, entry
470
- end
547
+ def each_entry_path
548
+ @combined_output.each_entry_path { |path| yield path }
471
549
  self
472
550
  end
473
- end
474
551
 
475
- class FileBuilder
476
- def initialize(output_dir)
477
- @output_dir = Pathname.new(output_dir)
478
- end
479
-
480
- def build_file(path, entry)
481
- build_file_helper(path, entry, false)
552
+ def get_entry(path)
553
+ @combined_output.get_entry(path)
482
554
  end
555
+ end
483
556
 
484
- def build_file_if_updated(path, entry)
485
- build_file_helper(path, entry, true)
557
+ def self.refresh_file(pathname, content, update_time)
558
+ begin
559
+ return false if update_time and update_time < pathname.mtime
560
+ rescue Errno::ENOENT
486
561
  end
562
+ write_file(pathname, content)
563
+ true
564
+ end
487
565
 
488
- def build_file_helper(path, entry, update_only)
489
- filename = @output_dir + path
490
- return self if update_only and filename.exist? and \
491
- entry['updated'] and filename.mtime >= entry['updated']
492
- dirname = filename.parent
493
- dirname.mkpath()
494
- filename.open("wb") do |stream|
495
- stream.write entry['content'].to_s
496
- end
497
- self
498
- end
499
- private :build_file_helper
566
+ def self.write_file(pathname, content)
567
+ pathname.parent.mkpath()
568
+ pathname.open("wb") { |stream| stream << content.to_s }
569
+ nil
500
570
  end
501
571
 
502
572
  class LazyContent
503
573
  def initialize(&block)
504
574
  raise ArgumentError, "No block given" unless block
575
+ @lock = Mutex.new
505
576
  @content = nil
506
577
  @block = block
507
578
  end
508
579
 
509
580
  def to_s
510
- if @block
511
- @content = @block.call
512
- @block = nil
581
+ @lock.synchronize do
582
+ if @block
583
+ @content = @block.call
584
+ @block = nil
585
+ end
513
586
  end
514
587
  @content
515
588
  end
@@ -33,7 +33,7 @@ module Hx
33
33
  module Backend
34
34
 
35
35
  class CouchDB
36
- include Hx::Source
36
+ include Hx::Filter
37
37
 
38
38
  class HTTPError < RuntimeError
39
39
  attr_reader :path
@@ -51,7 +51,7 @@ class CouchDB
51
51
  end
52
52
  end
53
53
 
54
- def initialize(source, options)
54
+ def initialize(input, options)
55
55
  couchdb_server = options.fetch(:couchdb_server, "http://localhost:5984/")
56
56
  couchdb_database = options.fetch(:couchdb_database, "entries")
57
57
  uri = URI.parse(couchdb_server)
@@ -81,25 +81,25 @@ class CouchDB
81
81
  self
82
82
  end
83
83
 
84
- def each_entry
84
+ def each_entry_path
85
85
  listing = JSON.parse(get_document('_all_docs'))
86
- for row in listing['rows']
87
- path = row['id']
88
- begin
89
- entry = JSON.parse(get_document(path))
90
- rescue HTTPError => e
91
- raise e unless e.code == 404
92
- # the document may have gone away since we requested the list
93
- next
94
- end
95
- for field in %(created updated)
96
- entry[field] = Time.parse(entry[field] || "")
97
- end
98
- yield path, entry
99
- end
86
+ listing['rows'].each { |row| yield row['id'] }
100
87
  self
101
88
  end
102
89
 
90
+ def get_entry(path)
91
+ begin
92
+ entry = JSON.parse(get_document(path))
93
+ rescue HTTPError => e
94
+ raise e unless e.code == 404
95
+ raise Hx::NoSuchEntryError, path
96
+ end
97
+ for field in %(created updated)
98
+ entry[field] = Time.parse(entry[field] || "")
99
+ end
100
+ entry
101
+ end
102
+
103
103
  private
104
104
  def request_path_for(id)
105
105
  "#{@prefix}#{CGI.escape(id)}"
@@ -22,19 +22,19 @@
22
22
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
 
24
24
  require 'rubygems'
25
- require 'time'
26
- require 'pathname'
27
25
  require 'yaml'
28
26
  require 'hx'
27
+ require 'hx/backend/rawfiles'
29
28
 
30
29
  module Hx
31
30
  module Backend
32
31
 
33
32
  class Hobix
34
- include Hx::Source
33
+ include Hx::Filter
35
34
 
36
- def initialize(source, options)
37
- @entry_dir = Hx.get_pathname(options, :entry_dir)
35
+ def initialize(input, options)
36
+ files = Hx::Backend::RawFiles.new(input, options)
37
+ @source = Hx::StripPath.new(files, :suffix => ".yaml")
38
38
  end
39
39
 
40
40
  def yaml_repr(value)
@@ -43,49 +43,48 @@ class Hobix
43
43
  private :yaml_repr
44
44
 
45
45
  def edit_entry(path, prototype=nil)
46
- entry_filename = @entry_dir + "#{path}.yaml"
47
- begin
48
- text = entry_filename.read
49
- previous_mtime = entry_filename.mtime
50
- rescue Errno::ENOENT
51
- raise Hx::NoSuchEntryError, path unless prototype
46
+ if prototype
52
47
  prototype = prototype.dup
53
48
  prototype['content'] = (prototype['content'] || "").dup
54
49
  content = prototype['content']
55
50
  def content.to_yaml_style ; :literal ; end
56
51
  native = YAML::DomainType.new('hobix.com,2004', 'entry', prototype)
57
- text = YAML.dump(native)
58
- previous_mtime = nil
52
+ prototype = { 'content' => YAML.dump(native) }
53
+ end
54
+ @source.edit_entry(path, prototype) do |text|
55
+ begin
56
+ previous_mtime = @source.get_entry(path)['updated']
57
+ rescue Hx::NoSuchEntryError
58
+ previous_mtime = nil
59
+ end
60
+ text = yield text
61
+ repr = YAML.parse(text)
62
+ keys = {}
63
+ repr.value.each_key { |key| keys[key.value] = key }
64
+ %w(created updated).each { |name| keys[name] ||= yaml_repr(name) }
65
+ update_time = Time.now
66
+ update_time_repr = yaml_repr(update_time)
67
+ previous_mtime ||= update_time
68
+ previous_mtime_repr = yaml_repr(previous_mtime)
69
+ repr.add(keys['created'], previous_mtime_repr) unless repr['created']
70
+ repr.add(keys['updated'], update_time_repr)
71
+ repr.emit
59
72
  end
60
- text = yield text
61
- repr = YAML.parse(text)
62
- keys = {}
63
- repr.value.each_key { |key| keys[key.value] = key }
64
- %w(created updated).each { |name| keys[name] ||= yaml_repr(name) }
65
- update_time = Time.now
66
- update_time_repr = yaml_repr(update_time)
67
- previous_mtime ||= update_time
68
- previous_mtime_repr = yaml_repr(previous_mtime)
69
- repr.add(keys['created'], previous_mtime_repr) unless repr['created']
70
- repr.add(keys['updated'], update_time_repr)
71
- entry_filename.parent.mkpath()
72
- entry_filename.open('w') { |stream| stream << repr.emit }
73
73
  self
74
74
  end
75
75
 
76
- def each_entry
77
- Pathname.glob(@entry_dir + '**/*.yaml') do |entry_filename|
78
- path = entry_filename.relative_path_from(@entry_dir).to_s
79
- path.sub!(/\.yaml$/, '')
80
- entry = entry_filename.open('r') do |stream|
81
- YAML.load(stream).value
82
- end
83
- entry['updated'] ||= entry_filename.mtime
84
- entry['created'] ||= entry['updated']
85
- yield path, entry
86
- end
76
+ def each_entry_path
77
+ @source.each_entry_path { |path| yield path }
87
78
  self
88
79
  end
80
+
81
+ def get_entry(path)
82
+ raw_entry = @source.get_entry(path)
83
+ entry = YAML.load(raw_entry['content'].to_s).value
84
+ entry['updated'] ||= raw_entry['updated']
85
+ entry['created'] ||= raw_entry['created'] || raw_entry['updated']
86
+ entry
87
+ end
89
88
  end
90
89
 
91
90
  end
@@ -0,0 +1,78 @@
1
+ # hx/backend/hobix - Hobix filesystem backend for Hx
2
+ #
3
+ # Copyright (c) 2009-2010 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 'rubygems'
25
+ require 'pathname'
26
+ require 'hx'
27
+
28
+ module Hx
29
+ module Backend
30
+
31
+ class RawFiles
32
+ include Hx::Filter
33
+
34
+ def initialize(input, options)
35
+ @entry_dir = Hx.get_pathname(options, :entry_dir)
36
+ end
37
+
38
+ def path_to_pathname(path) ; @entry_dir + path ; end
39
+ private :path_to_pathname
40
+
41
+ def pathname_to_path(pathname)
42
+ pathname.relative_path_from(@entry_dir).to_s
43
+ end
44
+ private :pathname_to_path
45
+
46
+ def edit_entry(path, prototype=nil)
47
+ pathname = path_to_pathname(path)
48
+ begin
49
+ body = pathname.read
50
+ rescue Errno::ENOENT
51
+ raise NoSuchEntryError, path unless prototype
52
+ text = prototype['content'].to_s
53
+ end
54
+ text = yield text
55
+ Hx.write_file(pathname, text)
56
+ self
57
+ end
58
+
59
+ def each_entry_path
60
+ Pathname.glob(@entry_dir + '**/*') do |pathname|
61
+ yield pathname_to_path(pathname) if pathname.file?
62
+ end
63
+ self
64
+ end
65
+
66
+ def get_entry(path)
67
+ pathname = path_to_pathname(path)
68
+ begin
69
+ { 'updated' => pathname.mtime,
70
+ 'content' => Hx::LazyContent.new { pathname.read } }
71
+ rescue Errno::ENOENT
72
+ raise Hx::NoSuchEntryError, path
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+ end
@@ -104,14 +104,16 @@ end
104
104
 
105
105
  def self.do_gen(site, update_only)
106
106
  output_dir = Hx.get_pathname(site.options, :output_dir)
107
- builder = Hx::FileBuilder.new(output_dir.to_s)
108
107
  site.each_entry do |path, entry|
109
- puts "===> #{path}"
108
+ pathname = output_dir + path
109
+ content = entry['content']
110
110
  if update_only
111
- builder.build_file_if_updated(path, entry)
111
+ update_time = entry['updated']
112
112
  else
113
- builder.build_file(path, entry)
113
+ update_time = nil
114
114
  end
115
+ written = Hx.refresh_file(pathname, content, update_time)
116
+ puts "===> #{path}" if written
115
117
  end
116
118
  end
117
119
 
@@ -28,15 +28,15 @@ module Hx
28
28
  module Listing
29
29
 
30
30
  class Limit
31
- include Hx::Source
31
+ include Hx::Filter
32
32
 
33
- def initialize(source, options)
34
- @source = source
33
+ def initialize(input, options)
34
+ @input = input
35
35
  @limit = options[:limit]
36
36
  end
37
37
 
38
38
  def each_entry
39
- @source.each_entry do |path, entry|
39
+ @input.each_entry do |path, entry|
40
40
  if entry['items']
41
41
  trimmed_entry = entry.dup
42
42
  trimmed_entry['items'] = entry['items'][0...@limit]
@@ -28,15 +28,15 @@ module Hx
28
28
  module Listing
29
29
 
30
30
  class Paginate
31
- include Hx::Source
31
+ include Hx::Filter
32
32
 
33
- def initialize(source, options)
34
- @source = source
33
+ def initialize(input, options)
34
+ @input = input
35
35
  @page_size = options[:page_size]
36
36
  end
37
37
 
38
38
  def each_entry
39
- @source.each_entry do |index_path, index_entry|
39
+ @input.each_entry do |index_path, index_entry|
40
40
  items = index_entry['items'] || []
41
41
  if items.empty?
42
42
  index_entry = index_entry.dup
@@ -30,10 +30,10 @@ module Hx
30
30
  module Listing
31
31
 
32
32
  class RecursiveIndex
33
- include Hx::Source
33
+ include Hx::Filter
34
34
 
35
- def self.new(source, options)
36
- listing = super(source, options)
35
+ def self.new(input, options)
36
+ listing = super(input, options)
37
37
  if options.has_key? :limit
38
38
  listing = Limit.new(listing, :limit => options[:limit])
39
39
  end
@@ -43,13 +43,13 @@ class RecursiveIndex
43
43
  listing
44
44
  end
45
45
 
46
- def initialize(source, options)
47
- @source = source
46
+ def initialize(input, options)
47
+ @input = input
48
48
  end
49
49
 
50
50
  def each_entry
51
51
  indexes = Hash.new { |h,k| h[k] = {'items' => []} }
52
- @source.each_entry do |path, entry|
52
+ @input.each_entry do |path, entry|
53
53
  components = path.split("/")
54
54
  until components.empty?
55
55
  components.pop
@@ -32,7 +32,7 @@ module Hx
32
32
  module Output
33
33
 
34
34
  class LiquidTemplate
35
- include Hx::Source
35
+ include Hx::Filter
36
36
 
37
37
  module TextFilters
38
38
  def textilize(input)
@@ -61,8 +61,8 @@ class LiquidTemplate
61
61
  end
62
62
  end
63
63
 
64
- def initialize(source, options)
65
- @source = source
64
+ def initialize(input, options)
65
+ @input = input
66
66
  @options = {}
67
67
  for key, value in options
68
68
  @options[key.to_s] = value
@@ -73,30 +73,42 @@ class LiquidTemplate
73
73
  Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_dir)
74
74
  Liquid::Template.register_filter(TextFilters)
75
75
  template_file = template_dir + options[:template]
76
- @template = template_file.open('r') { |s| Liquid::Template.parse(s.read) }
76
+ @template = Liquid::Template.parse(template_file.read)
77
77
  @extension = options[:extension]
78
+ @mime_type = options[:mime_type]
79
+ @strip_extension_re = nil
80
+ @strip_extension_re = /\.#{Regexp.quote(@extension)}$/ if @extension
78
81
  end
79
82
 
80
- def each_entry
81
- @source.each_entry do |path, entry|
83
+ def each_entry_path
84
+ @input.each_entry_path do |path|
82
85
  unless @extension.nil?
83
- output_path = "#{path}.#{@extension}"
86
+ yield "#{path}.#{@extension}"
84
87
  else
85
- output_path = path
88
+ yield path
86
89
  end
87
- output_entry = entry.dup
88
- output_entry['content'] = Hx::LazyContent.new do
89
- @template.render(
90
- 'now' => Time.now,
91
- 'options' => @options,
92
- 'path' => path,
93
- 'entry' => entry
94
- )
95
- end
96
- yield output_path, output_entry
97
90
  end
98
91
  self
99
92
  end
93
+
94
+ def get_entry(path)
95
+ path = path.sub(@strip_extension_re, '') if @strip_extension_re
96
+ entry = @input.get_entry(path)
97
+ output_entry = {}
98
+ output_entry['content'] = Hx::LazyContent.new do
99
+ @template.render(
100
+ 'now' => Time.now,
101
+ 'options' => @options,
102
+ 'path' => path,
103
+ 'entry' => entry
104
+ )
105
+ end
106
+ output_entry['mime_type'] = @mime_type if @mime_type
107
+ if entry.has_key? 'updated'
108
+ output_entry['created'] = output_entry['updated'] = entry['updated']
109
+ end
110
+ output_entry
111
+ end
100
112
  end
101
113
 
102
114
  end
data/lib/hx/rack.rb ADDED
@@ -0,0 +1,24 @@
1
+ # hx/rack - Rack bits for Hx
2
+ #
3
+ # Copyright (c) 2009-2010 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/rack/application'
@@ -0,0 +1,97 @@
1
+ # hx/rack/application - Rack application to serve an Hx site dynamically
2
+ #
3
+ # Copyright (c) 2009-2010 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 'rubygems'
25
+ require 'cgi'
26
+ require 'hx'
27
+ require 'rack/mime'
28
+
29
+ module Hx
30
+ module Rack
31
+
32
+ class Application
33
+ def self.load_file(config_path)
34
+ site = Hx::Site.load_file(config_path)
35
+ new(site, site.options)
36
+ end
37
+
38
+ def initialize(input, options)
39
+ @input = input
40
+ @index_names = Array(options[:index_names] || ['index.html'])
41
+ end
42
+
43
+ def call(env)
44
+ path = CGI.unescape(env['PATH_INFO'])
45
+ path = '/' if path.empty?
46
+ entry = nil
47
+
48
+ has_trailing_slash = (path[-1..-1] == '/')
49
+
50
+ # for non-slash-terminated paths, try the path directly
51
+ unless has_trailing_slash
52
+ begin
53
+ effective_path = path[1..-1]
54
+ entry = @input.get_entry(effective_path)
55
+ rescue NoSuchEntryError
56
+ end
57
+ end
58
+
59
+ # no entry? maybe it's a directory; look for an index entry
60
+ unless entry
61
+ path =~ %r(^/(.*?)/?$)
62
+ prefix = $1
63
+ prefix = "#{prefix}/" unless prefix.empty?
64
+ for index_name in @index_names
65
+ begin
66
+ effective_path = "#{prefix}#{index_name}"
67
+ entry = @input.get_entry(effective_path)
68
+ break
69
+ rescue NoSuchEntryError
70
+ end
71
+ end
72
+
73
+ # directory exists, but missing trailing slash?
74
+ if entry and not has_trailing_slash
75
+ return [301, {'Content-Type' => 'text/plain',
76
+ 'Location' => "#{env['SCRIPT_NAME']}#{path}/"},
77
+ ["301 Moved Permanently"]]
78
+ end
79
+ end
80
+
81
+ if entry
82
+ mime_type = entry['mime_type']
83
+ unless mime_type
84
+ effective_path =~ /(\.[^.]+)$/
85
+ mime_type = ::Rack::Mime.mime_type($1 || '')
86
+ end
87
+ content = entry['content'].to_s
88
+ [200, {'Content-Type' => mime_type}, [content]]
89
+ else
90
+ message = "#{env['SCRIPT_NAME']}#{path} not found"
91
+ [404, {'Content-Type' => "text/plain"}, [message]]
92
+ end
93
+ end
94
+ end
95
+
96
+ end
97
+ end
data/spec/cache_spec.rb CHANGED
@@ -5,7 +5,7 @@ require 'set'
5
5
 
6
6
  describe Hx::Cache do
7
7
  before(:each) do
8
- @source = FakeSource.new
8
+ @source = FakeInput.new
9
9
  @source.add_entry('foo', 'BLAH')
10
10
  @source.add_entry('bar', 'EEP')
11
11
  @cache = Hx::Cache.new(@source)
@@ -2,9 +2,9 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
3
  require 'hx'
4
4
 
5
- describe Hx::NullSource do
5
+ describe Hx::NullInput do
6
6
  before(:each) do
7
- @null_source = Hx::NullSource.new
7
+ @null_source = Hx::NullInput.new
8
8
  end
9
9
 
10
10
  it "should return itself from each_entry" do
@@ -18,8 +18,8 @@ describe Hx::NullSource do
18
18
  end
19
19
  end
20
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)
21
+ describe Hx::NULL_INPUT do
22
+ it "is an instance of Hx::NullInput" do
23
+ Hx::NULL_INPUT.should be_an_instance_of(Hx::NullInput)
24
24
  end
25
25
  end
data/spec/overlay_spec.rb CHANGED
@@ -4,10 +4,10 @@ require 'hx'
4
4
 
5
5
  describe Hx::Overlay do
6
6
  before(:each) do
7
- @a = FakeSource.new
7
+ @a = FakeInput.new
8
8
  @a.add_entry('foo', 'foo:A')
9
9
  @a.add_entry('bar', 'bar:A')
10
- @b = FakeSource.new
10
+ @b = FakeInput.new
11
11
  @b.add_entry('bar', 'bar:B')
12
12
  @b.add_entry('baz', 'baz:B')
13
13
  @overlay = Hx::Overlay.new(@a, @b)
data/spec/pathops_spec.rb CHANGED
@@ -7,7 +7,7 @@ describe Hx::AddPath do
7
7
  before(:each) do
8
8
  @before_paths = Set['foo', 'bar']
9
9
  @after_paths = Set['XXXfooYYY', 'XXXbarYYY']
10
- @source = FakeSource.new
10
+ @source = FakeInput.new
11
11
  @source.add_entry('foo', 'FOO')
12
12
  @source.add_entry('bar', 'BAR')
13
13
  @add = Hx::AddPath.new(@source, :prefix => 'XXX', :suffix => 'YYY')
@@ -29,7 +29,7 @@ end
29
29
  describe Hx::StripPath do
30
30
  before(:each) do @before_paths = Set['XXXfooYYY', 'XXXbarYYY', 'lemur']
31
31
  @after_paths = Set['foo', 'bar']
32
- @source = FakeSource.new
32
+ @source = FakeInput.new
33
33
  @source.add_entry('XXXfooYYY', 'FOO')
34
34
  @source.add_entry('XXXbarYYY', 'BAR')
35
35
  @strip = Hx::StripPath.new(@source, :prefix => 'XXX', :suffix => 'YYY')
@@ -50,7 +50,7 @@ end
50
50
 
51
51
  describe Hx::PathSubset do
52
52
  before(:each) do
53
- @source = FakeSource.new
53
+ @source = FakeInput.new
54
54
  @all_paths = Set['lemur', 'foo/bar', 'foo/baz', 'hoge/hoge']
55
55
  @all_paths.each do |path|
56
56
  @source.add_entry(path, path)
data/spec/rack_spec.rb ADDED
@@ -0,0 +1,71 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ require 'hx'
4
+ require 'hx/rack'
5
+ require 'rack/mock'
6
+
7
+ describe Hx::Rack::Application do
8
+ before(:each) do
9
+ @input = FakeInput.new
10
+ @app = Hx::Rack::Application.new(@input, {})
11
+ @service = Rack::MockRequest.new(@app)
12
+ end
13
+
14
+ it "should return 404 for unknown paths" do
15
+ response = @service.get("/bogus", :lint => true, :fatal => true)
16
+ response.status.to_i.should == 404
17
+ end
18
+
19
+ it "should return 200 with content for known paths" do
20
+ path = "existing"
21
+ content = "BLAH"
22
+ @input.add_entry(path, content)
23
+ response = @service.get("/#{path}", :lint => true, :fatal => true)
24
+ response.status.to_i.should == 200
25
+ response.body.to_s.should == content
26
+ end
27
+
28
+ it "should infer content type from file extension by default" do
29
+ cases = { "html" => "text/html",
30
+ "jpeg" => "image/jpeg" }
31
+ for extension, mime_type in cases
32
+ path = "blah.#{extension}"
33
+ @input.add_entry(path, "")
34
+ response = @service.get("/#{path}", :lint => true, :fatal => true)
35
+ response.status.to_i.should == 200
36
+ response['Content-Type'].should == mime_type
37
+ end
38
+ end
39
+
40
+ it "should allow entries to specify their mime type" do
41
+ path = "blah.html"
42
+ mime_type = "junk/garbage"
43
+ @input.add_entry(path, "", 'mime_type' => mime_type)
44
+ response = @service.get("/#{path}", :lint => true, :fatal => true)
45
+ response.status.to_i.should == 200
46
+ response['Content-Type'].should == mime_type
47
+ end
48
+
49
+ it "should redirect for directories with indices" do
50
+ @input.add_entry("dir/index.html", "")
51
+ response = @service.get("/dir", :lint => true, :fatal => true)
52
+ response.status.to_i.should == 301
53
+ response['Location'].should == "/dir/"
54
+ end
55
+
56
+ it "should return index for directories" do
57
+ content = "SOME INDEX STUFF"
58
+ @input.add_entry("dir/index.html", content)
59
+ response = @service.get("/dir/", :lint => true, :fatal => true)
60
+ response.status.to_i.should == 200
61
+ response.body.to_s.should == content
62
+ end
63
+
64
+ it "should return index for the root" do
65
+ content = "ROOT INDEX YAY"
66
+ @input.add_entry("index.html", content)
67
+ response = @service.get("/", :lint => true, :fatal => true)
68
+ response.status.to_i.should == 200
69
+ response.body.to_s.should == content
70
+ end
71
+ end
data/spec/spec_helper.rb CHANGED
@@ -5,26 +5,30 @@ require 'ostruct'
5
5
  require 'spec'
6
6
  require 'spec/autorun'
7
7
 
8
- class FakeSource
9
- include Hx::Source
8
+ class FakeInput
9
+ include Hx::Filter
10
10
 
11
11
  def initialize
12
12
  @entries = {}
13
13
  end
14
14
 
15
15
  def add_entry(path, content, metadata={})
16
- entry = OpenStruct.new(metadata)
17
- entry.content = content
16
+ entry = metadata.dup
17
+ entry['content'] = content
18
18
  @entries[path] = entry
19
19
  end
20
20
 
21
- def each_entry
22
- @entries.each { |path, entry| yield path, entry }
21
+ def each_entry_path
22
+ @entries.each_key { |path| yield path }
23
23
  self
24
24
  end
25
25
 
26
26
  def get_entry(path)
27
- @entries[path]
27
+ begin
28
+ @entries.fetch(path)
29
+ rescue IndexError
30
+ raise Hx::NoSuchEntryError, path
31
+ end
28
32
  end
29
33
  end
30
34
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MenTaLguY
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-07 00:00:00 -05:00
12
+ date: 2010-02-16 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -47,18 +47,22 @@ files:
47
47
  - lib/hx.rb
48
48
  - lib/hx/backend/couchdb.rb
49
49
  - lib/hx/backend/hobix.rb
50
+ - lib/hx/backend/rawfiles.rb
50
51
  - lib/hx/commandline.rb
51
52
  - lib/hx/listing/limit.rb
52
53
  - lib/hx/listing/paginate.rb
53
54
  - lib/hx/listing/recursiveindex.rb
54
55
  - lib/hx/output/liquidtemplate.rb
56
+ - lib/hx/rack.rb
57
+ - lib/hx/rack/application.rb
55
58
  - spec/cache_spec.rb
56
59
  - spec/hx_dummy.rb
57
60
  - spec/hx_dummy2.rb
58
- - spec/nullsource_spec.rb
61
+ - spec/nullinput_spec.rb
59
62
  - spec/overlay_spec.rb
60
63
  - spec/pathfilter_spec.rb
61
64
  - spec/pathops_spec.rb
65
+ - spec/rack_spec.rb
62
66
  - spec/site_spec.rb
63
67
  - spec/spec.opts
64
68
  - spec/spec_helper.rb
@@ -95,8 +99,9 @@ test_files:
95
99
  - spec/cache_spec.rb
96
100
  - spec/hx_dummy.rb
97
101
  - spec/pathfilter_spec.rb
98
- - spec/nullsource_spec.rb
102
+ - spec/nullinput_spec.rb
99
103
  - spec/site_spec.rb
100
104
  - spec/hx_dummy2.rb
105
+ - spec/rack_spec.rb
101
106
  - spec/overlay_spec.rb
102
107
  - spec/pathops_spec.rb