hx 0.6.1 → 0.7.0

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/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