plato 0.0.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/.gitignore +6 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/lib/plato/config.rb +65 -0
- data/lib/plato/document.rb +131 -0
- data/lib/plato/manifest.rb +57 -0
- data/lib/plato/path_template.rb +56 -0
- data/lib/plato/rendering.rb +87 -0
- data/lib/plato/repo.rb +198 -0
- data/lib/plato/site.rb +120 -0
- data/lib/plato.rb +11 -0
- metadata +120 -0
data/.gitignore
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
ROOT_DIR = File.expand_path(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'rubygems' rescue nil
|
4
|
+
require 'rake'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
desc "Run all specs in spec directory."
|
10
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
11
|
+
t.spec_opts = ['--options', "\"#{ROOT_DIR}/spec/spec.opts\""]
|
12
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
13
|
+
end
|
14
|
+
|
15
|
+
# gemification with jeweler
|
16
|
+
begin
|
17
|
+
require 'jeweler'
|
18
|
+
Jeweler::Tasks.new do |gemspec|
|
19
|
+
gemspec.name = "plato"
|
20
|
+
gemspec.summary = "An ideal static site generator"
|
21
|
+
gemspec.description = "use templates and content to generate static sites."
|
22
|
+
gemspec.email = "matt@freels.name"
|
23
|
+
gemspec.homepage = "http://github.com/freels/plato"
|
24
|
+
gemspec.authors = ["Matt Freels"]
|
25
|
+
gemspec.add_dependency 'tilt', '>= 1.0.1'
|
26
|
+
|
27
|
+
# development
|
28
|
+
gemspec.add_development_dependency 'rspec'
|
29
|
+
gemspec.add_development_dependency 'rr'
|
30
|
+
end
|
31
|
+
rescue LoadError
|
32
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
33
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/lib/plato/config.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
module Plato
|
2
|
+
module Config
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def read(dsl_class, string = nil, &block)
|
6
|
+
config = dsl_class.new
|
7
|
+
|
8
|
+
if string
|
9
|
+
config.instance_eval(string)
|
10
|
+
else
|
11
|
+
block.arity == 1 ? call.block(config) : config.instance_eval(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
extract_ivars(config)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def extract_ivars(config)
|
20
|
+
config.instance_variables.inject({}) do |result, ivar|
|
21
|
+
result.update(ivar.sub('@', '') => config.instance_variable_get(ivar))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class ConfigDSL
|
27
|
+
# Config DSL
|
28
|
+
|
29
|
+
def base_url(url)
|
30
|
+
@base_url = url
|
31
|
+
end
|
32
|
+
alias url base_url
|
33
|
+
|
34
|
+
def options(hash)
|
35
|
+
@options = hash
|
36
|
+
end
|
37
|
+
|
38
|
+
def content(name, content_path_template, opts = {})
|
39
|
+
@content_categories ||= {}
|
40
|
+
@content_categories[name.to_s] =
|
41
|
+
ContentCategory.new(name, content_path_template,
|
42
|
+
opts[:to] || content_path_template, opts[:sort], opts[:template])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class ContentCategory
|
47
|
+
attr_reader :name, :documents, :src_parser, :dest_parser, :template
|
48
|
+
|
49
|
+
def initialize(name, src_t, dest_t, sort, template)
|
50
|
+
@name = name
|
51
|
+
@documents = DocumentCollection.new(sort)
|
52
|
+
@src_parser = PathTemplate.new(src_t)
|
53
|
+
@dest_parser = PathTemplate.new(dest_t)
|
54
|
+
@template = "_#{template}" if template
|
55
|
+
end
|
56
|
+
|
57
|
+
def match(path); src_parser.parse(path) end
|
58
|
+
def dest_path(data); dest_parser.materialize(data) end
|
59
|
+
|
60
|
+
def method_missing(method, *args, &block)
|
61
|
+
documents.send(method, *args, &block)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Plato
|
5
|
+
class Document
|
6
|
+
attr_reader :category, :attributes
|
7
|
+
|
8
|
+
def initialize(category, attributes)
|
9
|
+
@category = category
|
10
|
+
@attributes = attributes
|
11
|
+
end
|
12
|
+
|
13
|
+
def previous_document
|
14
|
+
category.documents.prev(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def next_document
|
18
|
+
category.documents.next(self)
|
19
|
+
end
|
20
|
+
|
21
|
+
def format
|
22
|
+
attributes["format"] || 'text'
|
23
|
+
end
|
24
|
+
|
25
|
+
def path
|
26
|
+
category.dest_path(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def date
|
30
|
+
@date ||= (attributes["date"] ? Time.parse(attributes["date"]) : nil)
|
31
|
+
end
|
32
|
+
|
33
|
+
RAW_TEXT = %w(text txt raw)
|
34
|
+
|
35
|
+
def body(context = nil)
|
36
|
+
if RAW_TEXT.include? format
|
37
|
+
attributes["body"]
|
38
|
+
else
|
39
|
+
@template ||= Tilt.new(format) { attributes["body"] }
|
40
|
+
@template.render(context)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def values_at(*keys)
|
45
|
+
keys.map {|k| send(k) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def respond_to?(attr)
|
49
|
+
attributes.has_key? attr.to_s or super
|
50
|
+
end
|
51
|
+
|
52
|
+
def method_missing(attr)
|
53
|
+
if date and date.respond_to? attr
|
54
|
+
date.send(attr)
|
55
|
+
else
|
56
|
+
attributes[attr.to_s] or super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class DocumentCollection
|
62
|
+
include Enumerable
|
63
|
+
|
64
|
+
attr_accessor :sort_attribute, :sort_order
|
65
|
+
|
66
|
+
def initialize(sort = nil)
|
67
|
+
@documents = []
|
68
|
+
@sort_attribute, @sort_order = sort.split(' ') if sort
|
69
|
+
end
|
70
|
+
|
71
|
+
def <<(doc)
|
72
|
+
@to_a = nil
|
73
|
+
@documents << doc
|
74
|
+
end
|
75
|
+
|
76
|
+
def [](*args); to_a.[](*args) end
|
77
|
+
|
78
|
+
def index(doc, strict = false)
|
79
|
+
unless index = to_a.index(doc)
|
80
|
+
raise ArgumentError, "document is not a member of this collection" unless strict
|
81
|
+
end
|
82
|
+
index
|
83
|
+
end
|
84
|
+
|
85
|
+
def first
|
86
|
+
to_a.first
|
87
|
+
end
|
88
|
+
|
89
|
+
def prev(doc)
|
90
|
+
idx = index(doc)
|
91
|
+
idx.zero? ? nil : to_a[idx - 1]
|
92
|
+
end
|
93
|
+
|
94
|
+
def next(doc)
|
95
|
+
to_a[index(doc) + 1]
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_a
|
99
|
+
sort! unless @to_a
|
100
|
+
@to_a.dup
|
101
|
+
end
|
102
|
+
|
103
|
+
def each(&block)
|
104
|
+
to_a.each(&block)
|
105
|
+
end
|
106
|
+
|
107
|
+
def sort!
|
108
|
+
@to_a =
|
109
|
+
if sorted?
|
110
|
+
@documents.sort! do |a, b|
|
111
|
+
a,b = [a, b].map {|d| d.send(sort_attribute) }
|
112
|
+
ascending? ? a <=> b : b <=> a
|
113
|
+
end
|
114
|
+
else
|
115
|
+
@documents
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def sorted?
|
120
|
+
!!@sort_attribute
|
121
|
+
end
|
122
|
+
|
123
|
+
def descending?
|
124
|
+
sorted? && !!(sort_order =~ /^desc/i)
|
125
|
+
end
|
126
|
+
|
127
|
+
def ascending?
|
128
|
+
sorted? && !descending?
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Plato
|
2
|
+
class Manifest
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :contents, :codec
|
6
|
+
|
7
|
+
def initialize(contents = nil, opts = {})
|
8
|
+
opts = { :codec => opts } unless opts.is_a? Hash
|
9
|
+
@codec = opts[:codec]
|
10
|
+
|
11
|
+
@contents =
|
12
|
+
if contents.nil?
|
13
|
+
{}
|
14
|
+
elsif contents.is_a? Hash
|
15
|
+
contents
|
16
|
+
elsif contents.is_a? String
|
17
|
+
Repo.new(contents, @codec).all &opts[:filter]
|
18
|
+
else
|
19
|
+
raise ArgumentError, "invalid contents"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def save_to(path, codec = nil)
|
24
|
+
repo = Repo.new(path, codec || self.codec)
|
25
|
+
@contents.each {|path, hash| repo.save(path, hash) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](key)
|
29
|
+
contents[key]
|
30
|
+
end
|
31
|
+
|
32
|
+
def []=(key, value)
|
33
|
+
contents[key] = value
|
34
|
+
end
|
35
|
+
alias store []=
|
36
|
+
|
37
|
+
# if given a block, block should return a hash of the new path and
|
38
|
+
# new data, or nil if the file should be skipped
|
39
|
+
def map(new_codec = nil)
|
40
|
+
new_contents =
|
41
|
+
if block_given?
|
42
|
+
@contents.inject({}) do |hash, (path, data)|
|
43
|
+
new_path_data = yield(path, data)
|
44
|
+
new_path_data ? hash.update(new_path_data) : hash
|
45
|
+
end
|
46
|
+
else
|
47
|
+
@contents.dup
|
48
|
+
end
|
49
|
+
|
50
|
+
self.class.new(new_contents, new_codec || self.codec)
|
51
|
+
end
|
52
|
+
|
53
|
+
def each(&block)
|
54
|
+
contents.each(&block)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Plato
|
2
|
+
class PathTemplate
|
3
|
+
attr_reader :template, :keys
|
4
|
+
|
5
|
+
def initialize(string)
|
6
|
+
@template = string
|
7
|
+
compile!
|
8
|
+
end
|
9
|
+
|
10
|
+
def materialize(attributes)
|
11
|
+
values = attributes.values_at(*keys).compact
|
12
|
+
unless values.length == keys.length
|
13
|
+
raise ArgumentError, "missing required values for path materialization (#{keys.join(', ')})"
|
14
|
+
end
|
15
|
+
|
16
|
+
@materializer.call(values)
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse(path)
|
20
|
+
match = path.match(@parser)
|
21
|
+
return nil unless match
|
22
|
+
|
23
|
+
match = match.to_a
|
24
|
+
match.shift
|
25
|
+
|
26
|
+
Hash[*keys.zip(match).flatten]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# basically lifted from sinatra
|
32
|
+
|
33
|
+
SCANNER = /(:\w+(?:\\\*)?)/
|
34
|
+
|
35
|
+
def compile!
|
36
|
+
@keys = []
|
37
|
+
|
38
|
+
pattern = Regexp.escape(@template).gsub SCANNER do |match|
|
39
|
+
case match
|
40
|
+
when /\\\*$/
|
41
|
+
keys << match[1..-3]
|
42
|
+
"(.*?)"
|
43
|
+
else
|
44
|
+
keys << match[1..-1]
|
45
|
+
"([^/?&#]+)"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
@parser = /\A#{pattern}\Z/
|
50
|
+
|
51
|
+
interpolation = @template.gsub('#', '\\#').gsub(SCANNER, '#{vals.shift}')
|
52
|
+
|
53
|
+
@materializer = eval(%{proc {|vals| "#{interpolation}" } })
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Plato
|
2
|
+
class RenderContext
|
3
|
+
include Tilt::CompileSite
|
4
|
+
|
5
|
+
attr_reader :site, :document
|
6
|
+
alias post document
|
7
|
+
|
8
|
+
def initialize(site, document)
|
9
|
+
@site = site
|
10
|
+
@document = document
|
11
|
+
end
|
12
|
+
|
13
|
+
def template(key)
|
14
|
+
site.templates["#{key}.html"] || site.templates[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def render(template, locals = {}, &block)
|
18
|
+
template = self.template(template) if template.is_a? String
|
19
|
+
raise ArgumentError, "template #{template.inspect} not found" unless template
|
20
|
+
template.render(self, locals, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def render_with_layout(template, format = nil, &block)
|
24
|
+
layout = template("_layout.#{format}") if format
|
25
|
+
|
26
|
+
if layout
|
27
|
+
render(layout) { render(template, &block) }
|
28
|
+
else
|
29
|
+
render(template, &block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def render_body
|
34
|
+
document.body(self)
|
35
|
+
end
|
36
|
+
alias body render_body
|
37
|
+
|
38
|
+
def self.load_view_helpers(path)
|
39
|
+
return if @helpers_loaded
|
40
|
+
@helpers_loaded = true
|
41
|
+
|
42
|
+
mod = Module.new
|
43
|
+
mod.module_eval(File.read(path), path, 1)
|
44
|
+
include mod.const_get(mod.constants.first)
|
45
|
+
end
|
46
|
+
|
47
|
+
# base set of helpers
|
48
|
+
|
49
|
+
def url_for(doc, opts = {})
|
50
|
+
return doc if doc.is_a? String and doc =~ /\Ahttp:\/\//
|
51
|
+
base = opts[:absolute] ? site.base_url : '/'
|
52
|
+
File.join(base, doc.respond_to?(:path) ? doc.path : doc.to_s)
|
53
|
+
end
|
54
|
+
|
55
|
+
def link_to(title, url)
|
56
|
+
%{<a href="#{url}">#{title}</a>}
|
57
|
+
end
|
58
|
+
|
59
|
+
def content; site.content end
|
60
|
+
|
61
|
+
def attribute_pairs(hash)
|
62
|
+
hash.map {|k,v| %{#{k}="#{v}"} }.join(' ')
|
63
|
+
end
|
64
|
+
|
65
|
+
def css_include(url, opts = {})
|
66
|
+
url = "#{url.gsub(/\.css\Z/, '')}.css"
|
67
|
+
opts = opts.merge :type => "text/css", :rel => "stylesheet", :href=> url_for(url)
|
68
|
+
%{<link #{attribute_pairs(opts)} />}
|
69
|
+
end
|
70
|
+
|
71
|
+
def script_include(url, opts = {})
|
72
|
+
url = "#{url.gsub(/\.js\Z/, '')}.js"
|
73
|
+
opts = opts.merge :src => url_for(url)
|
74
|
+
%{<script #{attribute_pairs(opts)}></script>}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class RubyTiltTemplate < Tilt::Template
|
79
|
+
def prepare; end
|
80
|
+
|
81
|
+
def precompiled_template(locals)
|
82
|
+
data
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
::Tilt.register('rb', RubyTiltTemplate)
|
87
|
+
end
|
data/lib/plato/repo.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Plato
|
5
|
+
class Repo
|
6
|
+
attr_reader :root, :codec
|
7
|
+
|
8
|
+
CODEC_MAP = {}
|
9
|
+
|
10
|
+
def initialize(root, codec = nil)
|
11
|
+
unless root == File.expand_path(root)
|
12
|
+
raise ArgumentError, "root is not an absolute path"
|
13
|
+
end
|
14
|
+
|
15
|
+
@codec = CODEC_MAP[codec] || StringCodec
|
16
|
+
@root = root.sub(/\/+\Z/, '') #remove trailing slash(es)
|
17
|
+
end
|
18
|
+
|
19
|
+
def load(path)
|
20
|
+
path = expanded_path(path)
|
21
|
+
if File.exist? path
|
22
|
+
@codec.read(path)
|
23
|
+
else
|
24
|
+
raise Filestore::NotFound
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def save(path, data)
|
29
|
+
path = expanded_path(path)
|
30
|
+
FileUtils.mkdir_p(File.dirname(path))
|
31
|
+
@codec.write(path, data)
|
32
|
+
end
|
33
|
+
|
34
|
+
def destroy(path)
|
35
|
+
File.unlink expanded_path(path)
|
36
|
+
|
37
|
+
until (path = File.dirname(path)) == '.'
|
38
|
+
expanded = expanded_path(path)
|
39
|
+
|
40
|
+
if Dir[File.join(expanded, '*')].empty?
|
41
|
+
File.unlink expanded
|
42
|
+
else
|
43
|
+
return
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def all
|
49
|
+
paths = Dir[File.join(root, '**/*')].select {|e| File.file? e }
|
50
|
+
|
51
|
+
if block_given?
|
52
|
+
paths = paths.select {|p| yield relative_path(p) }
|
53
|
+
end
|
54
|
+
|
55
|
+
paths.inject({}) do |hash, path|
|
56
|
+
hash.update relative_path(path) => load(path)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def expanded_path(path)
|
63
|
+
File.expand_path(path, root)
|
64
|
+
end
|
65
|
+
|
66
|
+
def relative_path(path)
|
67
|
+
path.sub(/\A#{Regexp.escape(root)}\//, '').tap do |relative|
|
68
|
+
raise ArgumentError, "path must subpath of root" if relative == path
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module RefCodec
|
73
|
+
extend self
|
74
|
+
|
75
|
+
def write(path, ref)
|
76
|
+
FileUtils.cp(ref, path)
|
77
|
+
end
|
78
|
+
|
79
|
+
def read(ref)
|
80
|
+
ref
|
81
|
+
end
|
82
|
+
end
|
83
|
+
CODEC_MAP[:refs] = RefCodec
|
84
|
+
|
85
|
+
module StringCodec
|
86
|
+
extend self
|
87
|
+
|
88
|
+
def write(path, data)
|
89
|
+
File.open(path, 'w') {|f| f.write data }
|
90
|
+
end
|
91
|
+
|
92
|
+
def read(path)
|
93
|
+
File.read(path)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
CODEC_MAP[:string] = StringCodec
|
97
|
+
|
98
|
+
module TemplateCodec
|
99
|
+
extend self
|
100
|
+
|
101
|
+
def read(path)
|
102
|
+
if template_class = Tilt[path]
|
103
|
+
template_class.new(path)
|
104
|
+
else
|
105
|
+
path
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def write(path, data)
|
110
|
+
raise "Templates cannot be directly written"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
CODEC_MAP[:template] = TemplateCodec
|
114
|
+
|
115
|
+
module HashCodec
|
116
|
+
extend StringCodec
|
117
|
+
extend self
|
118
|
+
|
119
|
+
def write(path, hash)
|
120
|
+
hash = stringify_keys(hash)
|
121
|
+
body = hash.delete('body')
|
122
|
+
|
123
|
+
data = [].tap do |buffer|
|
124
|
+
buffer << hash.map do |key, value|
|
125
|
+
"#{header_for(key)}: #{Sanitize.header(value)}"
|
126
|
+
end.join("\n")
|
127
|
+
|
128
|
+
buffer << "\n\n" << Sanitize.body(body) if body
|
129
|
+
buffer << "\n" unless buffer.last =~ /\n\Z/
|
130
|
+
end.join
|
131
|
+
|
132
|
+
super(path, data)
|
133
|
+
end
|
134
|
+
|
135
|
+
def read(path)
|
136
|
+
string = super
|
137
|
+
|
138
|
+
{}.tap do |result|
|
139
|
+
headers, body = string.split(/\n\n/, 2)
|
140
|
+
|
141
|
+
headers.split("\n").each do |line|
|
142
|
+
header, val = line.split(/:\s*/, 2)
|
143
|
+
|
144
|
+
result.update hash_key_for(header) => deserialize_value(val)
|
145
|
+
end
|
146
|
+
|
147
|
+
result['body'] = body.chomp if body
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def stringify_keys(hash)
|
154
|
+
hash.inject({}) {|h, (k, v)| h.update k.to_s => v }
|
155
|
+
end
|
156
|
+
|
157
|
+
def header_for(attr)
|
158
|
+
attr.to_s.gsub('_', ' ').gsub(/\b([a-z])/) {|m| m.capitalize }
|
159
|
+
end
|
160
|
+
|
161
|
+
def hash_key_for(header)
|
162
|
+
header.gsub(/\s+/, '_').downcase.to_s
|
163
|
+
end
|
164
|
+
|
165
|
+
def deserialize_value(val)
|
166
|
+
YAML.load(val) rescue val
|
167
|
+
end
|
168
|
+
end
|
169
|
+
CODEC_MAP[:hash] = HashCodec
|
170
|
+
|
171
|
+
module Sanitize
|
172
|
+
extend self
|
173
|
+
|
174
|
+
def header(header_val)
|
175
|
+
header_val.gsub(/(\r|\n)/) {|m| {"\r" => '\r', "\n" => '\n'}[m] }
|
176
|
+
end
|
177
|
+
|
178
|
+
def path_elem(elem)
|
179
|
+
elem.to_s.gsub(/\s+/, '_').gsub(/[^a-zA-Z0-9_-]/,'')
|
180
|
+
end
|
181
|
+
|
182
|
+
def body(body)
|
183
|
+
body # do we really need to do anything here?
|
184
|
+
end
|
185
|
+
|
186
|
+
def method_missing(method, value)
|
187
|
+
warn "Warning: not sanitizing #{method}."
|
188
|
+
value.to_s
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
def warn(warning)
|
194
|
+
$stderr.puts warning
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
data/lib/plato/site.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
module Plato
|
2
|
+
class Site
|
3
|
+
attr_accessor :root
|
4
|
+
|
5
|
+
def initialize(root = '.')
|
6
|
+
@root = File.expand_path(root)
|
7
|
+
end
|
8
|
+
|
9
|
+
def generate!
|
10
|
+
RenderContext.load_view_helpers(File.join(template_path, 'view_helpers.rb'))
|
11
|
+
resources.save_to(cache_path)
|
12
|
+
template_resources.save_to(cache_path)
|
13
|
+
rendered_templates.save_to(cache_path)
|
14
|
+
rendered_content.save_to(cache_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def base_url; config['base_url'] end
|
18
|
+
|
19
|
+
def config_path; File.join(root, "config.rb") end
|
20
|
+
def template_path; File.join(root, "template") end
|
21
|
+
def content_path; File.join(root, "content") end
|
22
|
+
def resources_path; File.join(root, "resources") end
|
23
|
+
def cache_path; File.join(root, "cache") end
|
24
|
+
|
25
|
+
def config
|
26
|
+
@config ||= Config.read(ConfigDSL, File.read(config_path))
|
27
|
+
end
|
28
|
+
|
29
|
+
def templates
|
30
|
+
return @templates if @templates
|
31
|
+
|
32
|
+
manifest = Manifest.new template_path, {
|
33
|
+
:codec => :template,
|
34
|
+
:filter => lambda {|p| p !~ /\Aview_helpers\.rb/ }
|
35
|
+
}
|
36
|
+
|
37
|
+
path_parser = PathTemplate.new(":name*.:format.:engine")
|
38
|
+
sass_parser = PathTemplate.new(":name*.sass")
|
39
|
+
|
40
|
+
@template_resources = Manifest.new({}, :refs)
|
41
|
+
@templates = manifest.map do |path, template|
|
42
|
+
if template.is_a? String
|
43
|
+
# could not find a template engine, assume we're a raw resource
|
44
|
+
@template_resources[path] = template
|
45
|
+
nil
|
46
|
+
else
|
47
|
+
if match = path_parser.parse(path)
|
48
|
+
name, format = match.values_at("name", "format")
|
49
|
+
{ "#{name}.#{format}" => Template.new(template, format) }
|
50
|
+
else name = sass_parser.parse(path).values_at("name")
|
51
|
+
{ "#{name}.css" => Template.new(template, 'css') }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def content
|
58
|
+
return @content if @content
|
59
|
+
|
60
|
+
@content = config["content_categories"]
|
61
|
+
categories = @content.values
|
62
|
+
manifest = Manifest.new(content_path, :hash)
|
63
|
+
|
64
|
+
manifest.each do |path, content_data|
|
65
|
+
if category = categories.find {|c| c.match path }
|
66
|
+
data = category.match(path).merge(content_data)
|
67
|
+
|
68
|
+
category.documents << Document.new(category, data)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
@content
|
73
|
+
end
|
74
|
+
|
75
|
+
def template_resources
|
76
|
+
templates unless @templates
|
77
|
+
@template_resources
|
78
|
+
end
|
79
|
+
|
80
|
+
def resources
|
81
|
+
@resources ||= Manifest.new(resources_path, :refs)
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# helpers
|
86
|
+
|
87
|
+
Template = Struct.new(:renderer, :format)
|
88
|
+
|
89
|
+
class Template
|
90
|
+
def method_missing(m, *a, &b); renderer.send(m, *a, &b) end
|
91
|
+
end
|
92
|
+
|
93
|
+
def render(template, format, document)
|
94
|
+
RenderContext.new(self, document).render_with_layout(template, format)
|
95
|
+
end
|
96
|
+
|
97
|
+
def rendered_templates
|
98
|
+
templates.map(:string) do |path, template|
|
99
|
+
if path =~ /\A_/
|
100
|
+
nil
|
101
|
+
else
|
102
|
+
{ path => render(template, template.format, nil) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def rendered_content
|
108
|
+
rendered = content.values.inject({}) do |hash, category|
|
109
|
+
if template = templates["_#{category.name}.html"]
|
110
|
+
category.documents.each do |document|
|
111
|
+
hash[document.path] = render(template, "html", document)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
hash
|
115
|
+
end
|
116
|
+
|
117
|
+
Manifest.new(rendered)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/lib/plato.rb
ADDED
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: plato
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 31
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 0.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Matt Freels
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-08-10 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: tilt
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 21
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
- 1
|
34
|
+
version: 1.0.1
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rspec
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 3
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
version: "0"
|
49
|
+
type: :development
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: rr
|
53
|
+
prerelease: false
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
type: :development
|
64
|
+
version_requirements: *id003
|
65
|
+
description: use templates and content to generate static sites.
|
66
|
+
email: matt@freels.name
|
67
|
+
executables: []
|
68
|
+
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
extra_rdoc_files: []
|
72
|
+
|
73
|
+
files:
|
74
|
+
- .gitignore
|
75
|
+
- Rakefile
|
76
|
+
- VERSION
|
77
|
+
- lib/plato.rb
|
78
|
+
- lib/plato/config.rb
|
79
|
+
- lib/plato/document.rb
|
80
|
+
- lib/plato/manifest.rb
|
81
|
+
- lib/plato/path_template.rb
|
82
|
+
- lib/plato/rendering.rb
|
83
|
+
- lib/plato/repo.rb
|
84
|
+
- lib/plato/site.rb
|
85
|
+
has_rdoc: true
|
86
|
+
homepage: http://github.com/freels/plato
|
87
|
+
licenses: []
|
88
|
+
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options:
|
91
|
+
- --charset=UTF-8
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
hash: 3
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
hash: 3
|
109
|
+
segments:
|
110
|
+
- 0
|
111
|
+
version: "0"
|
112
|
+
requirements: []
|
113
|
+
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 1.3.7
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: An ideal static site generator
|
119
|
+
test_files: []
|
120
|
+
|