hx 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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