hx 0.3.2

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.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 MenTaLguY <mental@rydia.net>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,7 @@
1
+ = hx
2
+
3
+ A miniature web site generator.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2010 MenTaLguY. See LICENSE for details.
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "hx"
8
+ gem.executables << 'hx'
9
+ gem.summary = %Q{A miniature site generator.}
10
+ gem.description = %Q{A miniature site generator.}
11
+ gem.email = "mental@rydia.net"
12
+ gem.homepage = "http://github.com/mental/hx"
13
+ gem.authors = ["MenTaLguY"]
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
41
+
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = "hx #{version}"
44
+ rdoc.rdoc_files.include('README*')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.2
data/bin/hx ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'hx/commandline'
3
+ Hx::Commandline.main(*ARGV)
@@ -0,0 +1,749 @@
1
+ # hx - A very small website generator.
2
+ #
3
+ # Copyright (c) 2009 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 'cgi'
25
+ require 'rubygems'
26
+ require 'ostruct'
27
+ require 'set'
28
+ require 'date'
29
+ require 'time'
30
+ require 'fileutils'
31
+ require 'pathname'
32
+ require 'yaml'
33
+ require 'liquid'
34
+ require 'redcloth'
35
+
36
+ module Hx
37
+
38
+ class NoSuchEntryError < RuntimeError
39
+ end
40
+
41
+ class EditingNotSupportedError < RuntimeError
42
+ end
43
+
44
+ module Source
45
+ def edit_entry(path, prototype=nil)
46
+ raise EditingNotSupportedError, "Editing not supported for #{path}"
47
+ end
48
+
49
+ def each_entry
50
+ raise NotImplementedError, "#{self.class}#each_entry not implemented"
51
+ end
52
+ end
53
+
54
+ class NullSource
55
+ include Source
56
+
57
+ def each_entry
58
+ self
59
+ end
60
+ end
61
+
62
+ NULL_SOURCE = NullSource.new
63
+
64
+ class PathSubset
65
+ include Source
66
+
67
+ def initialize(source, options)
68
+ @source = source
69
+ @path_filter = Predicate.new(options[:only], options[:except])
70
+ end
71
+
72
+ def edit_entry(path, prototype=nil)
73
+ if @path_filter.accept? path
74
+ @source.edit_entry(path, prototype) { |text| yield text }
75
+ else
76
+ raise EditingNotSupportedError, "Editing not supported for #{path}"
77
+ end
78
+ self
79
+ end
80
+
81
+ def each_entry
82
+ @source.each_entry do |path, entry|
83
+ yield path, entry if @path_filter.accept? path
84
+ end
85
+ self
86
+ end
87
+ end
88
+
89
+ class PathSubset::Predicate
90
+ def initialize(accept, reject)
91
+ @accept_re = patterns_to_re(accept)
92
+ @reject_re = patterns_to_re(reject)
93
+ end
94
+
95
+ def accept?(path)
96
+ (not @accept_re or path =~ @accept_re) and
97
+ (not @reject_re or path !~ @reject_re)
98
+ end
99
+
100
+ def patterns_to_re(patterns)
101
+ return nil if patterns.nil? or patterns.empty?
102
+ patterns = Array(patterns)
103
+ Regexp.new("(?:#{patterns.map { |p| pattern_to_re(p) }.join("|")})")
104
+ end
105
+ private :patterns_to_re
106
+
107
+ def pattern_to_re(pattern)
108
+ "^#{pattern.scan(/(\*\*)|(\*)|([^*]+)/).map { |s2, s, r|
109
+ case
110
+ when s2
111
+ ".*"
112
+ when s
113
+ "[^/]*"
114
+ when r
115
+ Regexp.quote(r)
116
+ end
117
+ }.join("")}$"
118
+ end
119
+ private :pattern_to_re
120
+ end
121
+
122
+ class Overlay
123
+ include Source
124
+
125
+ def initialize(*sources)
126
+ @sources = sources
127
+ end
128
+
129
+ def edit_entry(path, prototype=nil)
130
+ @sources.each do |source|
131
+ begin
132
+ source.edit_entry(path, prototype) { |text| yield text }
133
+ break
134
+ rescue EditingNotSupportedError
135
+ end
136
+ end
137
+ self
138
+ end
139
+
140
+ def each_entry
141
+ seen = Set[]
142
+ @sources.each do |source|
143
+ source.each_entry do |path, entry|
144
+ yield path, entry unless seen.include? path
145
+ seen.add path
146
+ end
147
+ end
148
+ self
149
+ end
150
+ end
151
+
152
+ module CircumfixPath
153
+ include Source
154
+
155
+ def initialize(source, options)
156
+ @source = source
157
+ @prefix = options[:prefix]
158
+ @suffix = options[:suffix]
159
+ prefix = Regexp.quote(@prefix.to_s)
160
+ suffix = Regexp.quote(@suffix.to_s)
161
+ @regexp = Regexp.new("^#{prefix}(.*)#{suffix}$")
162
+ end
163
+
164
+ private
165
+ def add_circumfix(path)
166
+ "#{@prefix}#{path}#{@suffix}"
167
+ end
168
+
169
+ def strip_circumfix(path)
170
+ path =~ @regexp ; $1
171
+ end
172
+ end
173
+
174
+ class AddPath
175
+ include CircumfixPath
176
+
177
+ def edit_entry(path, prototype=nil)
178
+ path = strip_circumfix(path)
179
+ raise EditingNotSupportedError, "Editing not supported for #{path}" unless path
180
+ @source.edit_entry(path, prototype) { |text| yield text }
181
+ self
182
+ end
183
+
184
+ def each_entry
185
+ @source.each_entry do |path, entry|
186
+ yield add_circumfix(path), entry
187
+ end
188
+ self
189
+ end
190
+ end
191
+
192
+ class StripPath
193
+ include CircumfixPath
194
+
195
+ def edit_entry(path, prototype=nil)
196
+ path = add_circumfix(path)
197
+ @source.edit_entry(path, prototype) { |text| yield text }
198
+ self
199
+ end
200
+
201
+ def each_entry
202
+ @source.each_entry do |path, entry|
203
+ path = strip_circumfix(path)
204
+ yield path, entry if path
205
+ end
206
+ self
207
+ end
208
+ end
209
+
210
+ class Cache
211
+ include Source
212
+
213
+ def initialize(source, options={})
214
+ @source = source
215
+ @entries = nil
216
+ end
217
+
218
+ def edit_entry(path, prototype=nil)
219
+ @source.edit_entry(path, prototype) { |text| yield text }
220
+ self
221
+ end
222
+
223
+ def each_entry
224
+ unless @entries
225
+ @entries = []
226
+ @source.each_entry do |path, entry|
227
+ @entries << [path, entry]
228
+ end
229
+ end
230
+ @entries.each do |path, entry|
231
+ yield path, entry.dup
232
+ end
233
+ self
234
+ end
235
+ end
236
+
237
+ class Sort
238
+ include Source
239
+
240
+ def initialize(source, options)
241
+ @source = source
242
+ @key_fields = Array(options[:sort_by] || []).map { |f| f.to_s }
243
+ @reverse = !!options[:reverse]
244
+ end
245
+
246
+ def edit_entry(path, prototype=nil)
247
+ @source.edit_entry(path, prototype) { |text| yield text }
248
+ self
249
+ end
250
+
251
+ def each_entry
252
+ entries = []
253
+ @source.each_entry do |path, entry|
254
+ entries << [path, entry]
255
+ end
256
+ unless @key_fields.empty?
257
+ entries = entries.sort_by do |path, entry|
258
+ @key_fields.map { |f| entry[f] }
259
+ end
260
+ end
261
+ entries.reverse! if @reverse
262
+ entries.each do |path, entry|
263
+ yield path, entry
264
+ end
265
+ self
266
+ end
267
+ end
268
+
269
+ Chain = Object.new
270
+ def Chain.new(source, options)
271
+ filters = options[:chain] || []
272
+ options = options.dup
273
+ options.delete(:chain) # prevent inheritance
274
+ for raw_filter in filters
275
+ source = Hx.build_source(options, source, {}, raw_filter)
276
+ end
277
+ source
278
+ end
279
+
280
+ def self.make_default_title(options, path)
281
+ name = path.split('/').last
282
+ words = name.split(/[_\s-]/)
283
+ words.map { |w| w.capitalize }.join(' ')
284
+ end
285
+
286
+ def self.get_pathname(options, key)
287
+ dir = Pathname.new(options[key] || ".")
288
+ if dir.relative?
289
+ base_dir = Pathname.new(options[:base_dir])
290
+ (base_dir + dir).cleanpath(true)
291
+ else
292
+ dir
293
+ end
294
+ end
295
+
296
+ def self.get_default_author(options)
297
+ options.fetch(:default_author, "nobody")
298
+ end
299
+
300
+ def self.local_require(options, library)
301
+ saved_require_path = $:.dup
302
+ begin
303
+ $:.delete(".")
304
+ $:.push Hx.get_pathname(options, :lib_dir).to_s
305
+ require library
306
+ ensure
307
+ $:[0..-1] = saved_require_path
308
+ end
309
+ end
310
+
311
+ def self.resolve_constant(qualified_name, root=Object)
312
+ begin
313
+ qualified_name.split('::').inject(root) { |c, n| c.const_get(n) }
314
+ rescue NameError
315
+ raise NameError, "Unable to resolve #{qualified_name}"
316
+ end
317
+ end
318
+
319
+ def self.expand_chain(raw_source)
320
+ case raw_source
321
+ when Array # rewrite array to Hx::Chain
322
+ return NULL_SOURCE if raw_source.empty?
323
+
324
+ filter_defs = raw_source.dup
325
+ first_filter = filter_defs[0] = filter_defs[0].dup
326
+
327
+ raw_source = {
328
+ 'filter' => 'Hx::Chain',
329
+ 'options' => {'chain' => filter_defs}
330
+ }
331
+
332
+ if first_filter.has_key? 'source' # use input of first filter for chain
333
+ raw_source['source'] = first_filter['source']
334
+ first_filter.delete('source')
335
+ end
336
+ end
337
+ raw_source
338
+ end
339
+
340
+ def self.build_source(options, default_input, sources, raw_source)
341
+ raw_source = expand_chain(raw_source)
342
+
343
+ if raw_source.has_key? 'source'
344
+ input_name = raw_source['source']
345
+ begin
346
+ source = sources.fetch(input_name)
347
+ rescue IndexError
348
+ raise NameError, "No source named #{input_name} in scope"
349
+ end
350
+ else
351
+ source = default_input
352
+ end
353
+
354
+ if raw_source.has_key? 'filter'
355
+ if raw_source.has_key? 'options'
356
+ filter_options = options.dup
357
+ for key, value in raw_source['options']
358
+ filter_options[key.intern] = value
359
+ end
360
+ else
361
+ filter_options = options
362
+ end
363
+ filter = raw_source['filter']
364
+ begin
365
+ factory = Hx.resolve_constant(filter)
366
+ rescue NameError
367
+ library = filter.gsub(/::/, '/').downcase
368
+ Hx.local_require(options, library)
369
+ factory = Hx.resolve_constant(filter)
370
+ end
371
+ source = factory.new(source, filter_options)
372
+ end
373
+
374
+ if raw_source.has_key? 'only' or raw_source.has_key? 'except'
375
+ source = PathSubset.new(source, :only => raw_source['only'],
376
+ :except => raw_source['except'])
377
+ end
378
+
379
+ if raw_source.has_key? 'strip_prefix' or
380
+ raw_source.has_key? 'strip_suffix'
381
+ source = StripPath.new(source, :prefix => raw_source['strip_prefix'],
382
+ :suffix => raw_source['strip_suffix'])
383
+ end
384
+
385
+ if raw_source.has_key? 'add_prefix' or raw_source.has_key? 'add_suffix'
386
+ source = AddPath.new(source, :prefix => raw_source['add_prefix'],
387
+ :suffix => raw_source['add_suffix'])
388
+ end
389
+
390
+ if raw_source.has_key? 'sort_by' or raw_source.has_key? 'reverse'
391
+ source = Sort.new(source, :sort_by => raw_source['sort_by'],
392
+ :reverse => raw_source['reverse'])
393
+ end
394
+
395
+ source = Cache.new(source) if raw_source['cache']
396
+
397
+ source
398
+ end
399
+
400
+ class Site
401
+ include Source
402
+
403
+ attr_reader :options
404
+ attr_reader :sources
405
+ attr_reader :outputs
406
+
407
+ class << self
408
+ private :new
409
+
410
+ def load(io, config_path)
411
+ raw_config = YAML.load(io)
412
+ options = {}
413
+ options[:base_dir] = File.dirname(config_path)
414
+ for key, value in raw_config.fetch('options', {})
415
+ options[key.intern] = value
416
+ end
417
+
418
+ if raw_config.has_key? 'require'
419
+ for library in raw_config['require']
420
+ Hx.local_require(options, library)
421
+ end
422
+ end
423
+
424
+ raw_sources_by_name = raw_config.fetch('sources', {})
425
+ source_names = raw_sources_by_name.keys
426
+
427
+ # build source dependency graph
428
+ source_dependencies = {}
429
+ for name, raw_source in raw_sources_by_name
430
+ raw_source = Hx.expand_chain(raw_source)
431
+ if raw_source.has_key? 'source'
432
+ source_dependencies[name] = raw_source['source']
433
+ end
434
+ end
435
+
436
+ # calculate depth for each source in the graph
437
+ source_depths = Hash.new(0)
438
+ for name in source_names
439
+ seen = Set[] # for cycle detection
440
+ while source_dependencies.has_key? name
441
+ if seen.include? name
442
+ raise RuntimeError, "cycle in source graph at #{name}"
443
+ end
444
+ seen.add name
445
+ depth = source_depths[name] + 1
446
+ name = source_dependencies[name]
447
+ source_depths[name] = depth if depth > source_depths[name]
448
+ end
449
+ end
450
+
451
+ # depth-first topological sort
452
+ depth_first_names = source_names.sort_by { |n| -source_depths[n] }
453
+
454
+ sources = {}
455
+ for name in depth_first_names
456
+ raw_source = raw_sources_by_name[name]
457
+ sources[name] = Hx.build_source(options, NULL_SOURCE, sources,
458
+ raw_source)
459
+ end
460
+
461
+ outputs = []
462
+ for raw_output in raw_config.fetch('outputs', [])
463
+ outputs << Hx.build_source(options, NULL_SOURCE, sources, raw_output)
464
+ end
465
+
466
+ new(options, sources, outputs)
467
+ end
468
+ end
469
+
470
+ def initialize(options, sources, outputs)
471
+ @options = options
472
+ @sources = sources
473
+ @outputs = outputs
474
+ @combined_output = Overlay.new(*@outputs)
475
+ end
476
+
477
+ def edit_entry(path, prototype=nil)
478
+ @combined_output.edit_entry(path, prototype) { |text| yield text }
479
+ self
480
+ end
481
+
482
+ def each_entry
483
+ @combined_output.each_entry do |path, entry|
484
+ yield path, entry
485
+ end
486
+ self
487
+ end
488
+ end
489
+
490
+ class FileBuilder
491
+ def initialize(output_dir)
492
+ @output_dir = output_dir
493
+ end
494
+
495
+ def build_file(path, entry)
496
+ filename = File.join(@output_dir, path)
497
+ dirname = File.dirname(filename)
498
+ FileUtils.mkdir_p dirname
499
+ File.open(filename, "wb") do |stream|
500
+ stream.write entry['content'].to_s
501
+ end
502
+ end
503
+ end
504
+
505
+ module Backend
506
+
507
+ class Hobix
508
+ include Source
509
+
510
+ def initialize(source, options)
511
+ @entry_dir = Hx.get_pathname(options, :entry_dir)
512
+ end
513
+
514
+ def yaml_repr(value)
515
+ YAML.parse(YAML.dump(value))
516
+ end
517
+ private :yaml_repr
518
+
519
+ def edit_entry(path, prototype=nil)
520
+ entry_filename = @entry_dir + "#{path}.yaml"
521
+ begin
522
+ text = entry_filename.read
523
+ previous_mtime = entry_filename.mtime
524
+ rescue Errno::ENOENT
525
+ raise NoSuchEntryError, path unless prototype
526
+ prototype = prototype.dup
527
+ prototype['content'] = (prototype['content'] || "").dup
528
+ content = prototype['content']
529
+ def content.to_yaml_style ; :literal ; end
530
+ native = YAML::DomainType.new('hobix.com,2004', 'entry', prototype)
531
+ text = YAML.dump(native)
532
+ previous_mtime = nil
533
+ end
534
+ text = yield text
535
+ repr = YAML.parse(text)
536
+ keys = {}
537
+ repr.value.each_key { |key| keys[key.value] = key }
538
+ %w(created updated).each { |name| keys[name] ||= yaml_repr(name) }
539
+ update_time = Time.now
540
+ update_time_repr = yaml_repr(update_time)
541
+ previous_mtime ||= update_time
542
+ previous_mtime_repr = yaml_repr(previous_mtime)
543
+ repr.add(keys['created'], previous_mtime_repr) unless repr['created']
544
+ repr.add(keys['updated'], update_time_repr)
545
+ entry_filename.parent.mkpath()
546
+ entry_filename.open('w') { |stream| stream << repr.emit }
547
+ self
548
+ end
549
+
550
+ def each_entry
551
+ Pathname.glob(@entry_dir + '**/*.yaml') do |entry_filename|
552
+ path = entry_filename.relative_path_from(@entry_dir).to_s
553
+ path.sub!(/\.yaml$/, '')
554
+ entry = entry_filename.open('r') do |stream|
555
+ YAML.load(stream).value
556
+ end
557
+ entry['updated'] ||= entry_filename.mtime
558
+ entry['created'] ||= entry['updated']
559
+ yield path, entry
560
+ end
561
+ self
562
+ end
563
+ end
564
+
565
+ end
566
+
567
+ module Listing
568
+
569
+ class RecursiveIndex
570
+ include Source
571
+
572
+ def self.new(source, options)
573
+ listing = super(source, options)
574
+ if options.has_key? :limit
575
+ listing = Limit.new(listing, :limit => options[:limit])
576
+ end
577
+ if options.has_key? :page_size
578
+ listing = Paginate.new(listing, :page_size => options[:page_size])
579
+ end
580
+ listing
581
+ end
582
+
583
+ def initialize(source, options)
584
+ @source = source
585
+ end
586
+
587
+ def each_entry
588
+ indexes = Hash.new { |h,k| h[k] = {'items' => []} }
589
+ @source.each_entry do |path, entry|
590
+ components = path.split("/")
591
+ until components.empty?
592
+ components.pop
593
+ index_path = (components + ["index"]).join("/")
594
+ index = indexes[index_path]
595
+ index['items'] << {'path' => path, 'entry' => entry}
596
+ if entry['modified'] and
597
+ (not index['modified'] or entry['modified'] > index['modified'])
598
+ index['modified'] = entry['modified']
599
+ end
600
+ end
601
+ end
602
+ indexes.each do |path, entry|
603
+ yield path, entry
604
+ end
605
+ self
606
+ end
607
+ end
608
+
609
+ class Paginate
610
+ include Source
611
+
612
+ def initialize(source, options)
613
+ @source = source
614
+ @page_size = options[:page_size]
615
+ end
616
+
617
+ def each_entry
618
+ @source.each_entry do |index_path, index_entry|
619
+ items = index_entry['items'] || []
620
+ if items.empty?
621
+ index_entry = index_entry.dup
622
+ index_entry['pages'] = [index_entry]
623
+ index_entry['page_index'] = 0
624
+ yield index_path, index_entry
625
+ else
626
+ pages = []
627
+ n_pages = (items.size + @page_size - 1) / @page_size
628
+ for num in 0...n_pages
629
+ page_items = items[@page_size * num, @page_size]
630
+ entry = index_entry.dup
631
+ entry['items'] = page_items
632
+ entry['prev_page'] = "#{num}"
633
+ entry['next_page'] = "#{num+2}"
634
+ entry['pages'] = pages
635
+ entry['page_index'] = num
636
+ pages << {'path' => "#{index_path}/#{num+1}", 'entry' => entry}
637
+ end
638
+ pages[0]['path'] = index_path
639
+ pages[0]['entry'].delete('prev_page')
640
+ if pages.size > 1
641
+ index_name = index_path.split('/').last
642
+ pages[0]['entry']['next_page'] = "#{index_name}/2"
643
+ pages[1]['entry']['prev_page'] = "../#{index_name}"
644
+ end
645
+ pages[-1]['entry'].delete('next_page')
646
+ pages.each do |page|
647
+ yield page['path'], page['entry']
648
+ end
649
+ end
650
+ end
651
+ self
652
+ end
653
+ end
654
+
655
+ class Limit
656
+ include Source
657
+
658
+ def initialize(source, options)
659
+ @source = source
660
+ @limit = options[:limit]
661
+ end
662
+
663
+ def each_entry
664
+ @source.each_entry do |path, entry|
665
+ if entry['items']
666
+ trimmed_entry = entry.dup
667
+ trimmed_entry['items'] = entry['items'][0...@limit]
668
+ else
669
+ trimmed_entry = entry
670
+ end
671
+ yield path, trimmed_entry
672
+ end
673
+ self
674
+ end
675
+ end
676
+
677
+ end
678
+
679
+ module Output
680
+
681
+ class LiquidTemplate
682
+ include Source
683
+
684
+ module TextFilters
685
+ def textilize(input)
686
+ RedCloth.new(input).to_html
687
+ end
688
+
689
+ def escape_url(input)
690
+ CGI.escape(input)
691
+ end
692
+
693
+ def escape_xml(input)
694
+ CGI.escapeHTML(input)
695
+ end
696
+
697
+ def path_to_url(input, base_url)
698
+ "#{base_url}#{input}"
699
+ end
700
+
701
+ def handleize(input)
702
+ "id_#{input.to_s.gsub(/[^A-Za-z0-9]/, '_')}"
703
+ end
704
+
705
+ def xsd_datetime(input)
706
+ input = Time.parse(input) unless Time === input
707
+ input.xmlschema
708
+ end
709
+ end
710
+
711
+ def initialize(source, options)
712
+ @source = source
713
+ @options = {}
714
+ for key, value in options
715
+ @options[key.to_s] = value
716
+ end
717
+ template_dir = Hx.get_pathname(options, :template_dir)
718
+ # global, so all LiquidTemplate instances kind of have to agree on the
719
+ # same template directory for things to work right
720
+ Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_dir)
721
+ Liquid::Template.register_filter(TextFilters)
722
+ template_file = template_dir + options[:template]
723
+ @template = template_file.open('r') { |s| Liquid::Template.parse(s.read) }
724
+ @extension = options[:extension]
725
+ end
726
+
727
+ def each_entry
728
+ @source.each_entry do |path, entry|
729
+ unless @extension.nil?
730
+ output_path = "#{path}.#{@extension}"
731
+ else
732
+ output_path = path
733
+ end
734
+ output_entry = entry.dup
735
+ output_entry['content'] = @template.render(
736
+ 'now' => Time.now,
737
+ 'options' => @options,
738
+ 'path' => path,
739
+ 'entry' => entry
740
+ )
741
+ yield output_path, output_entry
742
+ end
743
+ self
744
+ end
745
+ end
746
+
747
+ end
748
+
749
+ end