proton 0.3.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. data/AUTHORS +6 -0
  2. data/HISTORY.md +200 -0
  3. data/README.md +75 -0
  4. data/Rakefile +5 -0
  5. data/TODO.md +6 -0
  6. data/bin/proton +7 -0
  7. data/data/new_site/Protonfile +12 -0
  8. data/data/new_site/README.md +10 -0
  9. data/data/new_site/_layouts/default.haml +28 -0
  10. data/data/new_site/index.haml +5 -0
  11. data/data/rack/Gemfile +2 -0
  12. data/data/rack/Gemfile.lock +40 -0
  13. data/data/rack/config.ru +24 -0
  14. data/lib/proton.rb +78 -0
  15. data/lib/proton/cli.rb +227 -0
  16. data/lib/proton/cli/helpers.rb +89 -0
  17. data/lib/proton/compass_support.rb +8 -0
  18. data/lib/proton/config.rb +116 -0
  19. data/lib/proton/helpers.rb +126 -0
  20. data/lib/proton/layout.rb +20 -0
  21. data/lib/proton/meta.rb +17 -0
  22. data/lib/proton/page.rb +431 -0
  23. data/lib/proton/partial.rb +12 -0
  24. data/lib/proton/project.rb +176 -0
  25. data/lib/proton/server.rb +99 -0
  26. data/lib/proton/set.rb +32 -0
  27. data/lib/proton/version.rb +13 -0
  28. data/test/fixture/build_options/control/page.html +0 -0
  29. data/test/fixture/build_options/control/style.css +1 -0
  30. data/test/fixture/build_options/hyde.conf +18 -0
  31. data/test/fixture/build_options/site/page.haml +0 -0
  32. data/test/fixture/build_options/site/style.scss +1 -0
  33. data/test/fixture/compass/hyde.conf +4 -0
  34. data/test/fixture/compass/site/style.scss +5 -0
  35. data/test/fixture/empty_config/hyde.conf +0 -0
  36. data/test/fixture/extensions/control/index.html +1 -0
  37. data/test/fixture/extensions/extensions/a/a.rb +1 -0
  38. data/test/fixture/extensions/extensions/hi.rb +1 -0
  39. data/test/fixture/extensions/hyde.conf +8 -0
  40. data/test/fixture/extensions/site/index.haml +1 -0
  41. data/test/fixture/fail_type/control/about/index.html +2 -0
  42. data/test/fixture/fail_type/control/about/us.html +2 -0
  43. data/test/fixture/fail_type/control/index.html +1 -0
  44. data/test/fixture/fail_type/hyde.conf +8 -0
  45. data/test/fixture/fail_type/site/index.haml +4 -0
  46. data/test/fixture/high_version/hyde.conf +1 -0
  47. data/test/fixture/high_version_2/hyde.conf +1 -0
  48. data/test/fixture/html/control/index.html +2 -0
  49. data/test/fixture/html/hyde.conf +8 -0
  50. data/test/fixture/html/site/index.html +2 -0
  51. data/test/fixture/ignores/control/about.html +1 -0
  52. data/test/fixture/ignores/hyde.conf +10 -0
  53. data/test/fixture/ignores/site/about.haml +1 -0
  54. data/test/fixture/ignores/site/hi.haml +1 -0
  55. data/test/fixture/metadata/control/index.html +4 -0
  56. data/test/fixture/metadata/hyde.conf +8 -0
  57. data/test/fixture/metadata/site/index.haml +8 -0
  58. data/test/fixture/nested_layout/control/index.html +2 -0
  59. data/test/fixture/nested_layout/hyde.conf +9 -0
  60. data/test/fixture/nested_layout/layouts/default.haml +2 -0
  61. data/test/fixture/nested_layout/layouts/post.haml +3 -0
  62. data/test/fixture/nested_layout/site/index.haml +4 -0
  63. data/test/fixture/one/control/about/index.css +1 -0
  64. data/test/fixture/one/control/about/us.html +1 -0
  65. data/test/fixture/one/control/cheers.html +5 -0
  66. data/test/fixture/one/control/css/bar.css +0 -0
  67. data/test/fixture/one/control/css/style.css +1 -0
  68. data/test/fixture/one/control/hello.html +5 -0
  69. data/test/fixture/one/control/hi.html +1 -0
  70. data/test/fixture/one/control/images/bar.gif +0 -0
  71. data/test/fixture/one/control/images/baz.png +0 -0
  72. data/test/fixture/one/control/images/foo.jpg +0 -0
  73. data/test/fixture/one/control/index.html +7 -0
  74. data/test/fixture/one/hyde.conf +9 -0
  75. data/test/fixture/one/layouts/default.haml +4 -0
  76. data/test/fixture/one/partials/menu.haml +3 -0
  77. data/test/fixture/one/public/about/index.css +1 -0
  78. data/test/fixture/one/public/about/us.html +1 -0
  79. data/test/fixture/one/public/cheers.html +5 -0
  80. data/test/fixture/one/public/css/bar.css +0 -0
  81. data/test/fixture/one/public/css/style.css +1 -0
  82. data/test/fixture/one/public/hello.html +5 -0
  83. data/test/fixture/one/public/hi.html +1 -0
  84. data/test/fixture/one/public/images/bar.gif +0 -0
  85. data/test/fixture/one/public/images/baz.png +0 -0
  86. data/test/fixture/one/public/images/foo.jpg +0 -0
  87. data/test/fixture/one/public/index.html +7 -0
  88. data/test/fixture/one/site/about/index.scss +1 -0
  89. data/test/fixture/one/site/about/us.haml +3 -0
  90. data/test/fixture/one/site/cheers.html.haml +1 -0
  91. data/test/fixture/one/site/css/bar.scss +0 -0
  92. data/test/fixture/one/site/css/style.scss +2 -0
  93. data/test/fixture/one/site/hello.haml +3 -0
  94. data/test/fixture/one/site/hi.html +3 -0
  95. data/test/fixture/one/site/images/bar.gif +0 -0
  96. data/test/fixture/one/site/images/baz.png +0 -0
  97. data/test/fixture/one/site/images/foo.jpg +0 -0
  98. data/test/fixture/one/site/index.haml +7 -0
  99. data/test/fixture/parent/control/about/index.html +2 -0
  100. data/test/fixture/parent/control/about/us.html +2 -0
  101. data/test/fixture/parent/control/index.html +1 -0
  102. data/test/fixture/parent/hyde.conf +8 -0
  103. data/test/fixture/parent/site/about/index.haml +3 -0
  104. data/test/fixture/parent/site/about/us.haml +3 -0
  105. data/test/fixture/parent/site/index.haml +3 -0
  106. data/test/fixture/sort/control/about.html +6 -0
  107. data/test/fixture/sort/control/about/hardy.html +1 -0
  108. data/test/fixture/sort/control/about/intrepid.html +1 -0
  109. data/test/fixture/sort/hyde.conf +8 -0
  110. data/test/fixture/sort/site/about.haml +3 -0
  111. data/test/fixture/sort/site/about/hardy.haml +4 -0
  112. data/test/fixture/sort/site/about/intrepid.haml +4 -0
  113. data/test/fixture/subclass/control/index.html +1 -0
  114. data/test/fixture/subclass/extensions/a/a.rb +12 -0
  115. data/test/fixture/subclass/hyde.conf +9 -0
  116. data/test/fixture/subclass/layouts/default.haml +1 -0
  117. data/test/fixture/subclass/layouts/post.haml +1 -0
  118. data/test/fixture/subclass/site/index.haml +4 -0
  119. data/test/helper.rb +36 -0
  120. data/test/unit/build_options_test.rb +18 -0
  121. data/test/unit/extensions_test.rb +17 -0
  122. data/test/unit/fixture_test.rb +122 -0
  123. data/test/unit/page_test.rb +58 -0
  124. data/test/unit/proton_test.rb +27 -0
  125. data/test/unit/set_test.rb +26 -0
  126. metadata +301 -0
@@ -0,0 +1,20 @@
1
+ class Proton
2
+ class Layout < Page
3
+ attr_accessor :page
4
+
5
+ def self.[](id, page)
6
+ object = super(id, page.project)
7
+ object.page = page if object
8
+ object
9
+ end
10
+
11
+ protected
12
+ def self.root_path(project, *a)
13
+ project.path(:layouts, *a)
14
+ end
15
+
16
+ def default_layout
17
+ nil
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # Class: Proton::Meta
2
+ # Metadata.
3
+ #
4
+ # This is usually accessed via {Proton::Page.meta}.
5
+
6
+ class Proton
7
+ class Meta < OpenStruct
8
+ def merge!(hash)
9
+ @table.merge(hash)
10
+ end
11
+
12
+ # For Ruby 1.8.6 compatibility ([:type] instead of .type)
13
+ def [](id)
14
+ @table[id.to_sym]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,431 @@
1
+ class Proton
2
+ # A project.
3
+ #
4
+ # Getting pages from paths:
5
+ #
6
+ # # Feed it a URL path, not a filename.
7
+ # page = Proton::Page['/index.html'] # uses Proton.project
8
+ # page = Proton::Page['/index.html', project]
9
+ #
10
+ # Getting pages from files:
11
+ #
12
+ # # Feed it a file name, not a URL path.
13
+ # # Also, this does no sanity checks.
14
+ # page = Proton::Page.new('/home/rsc/index.html', project)
15
+ #
16
+ # page.exists?
17
+ # page.valid?
18
+ #
19
+ # Paths:
20
+ #
21
+ # page.filepath #=> "index.haml" -- path in the filesystem
22
+ # page.path #=> "/index.html" -- path as a RUL
23
+ #
24
+ # Meta:
25
+ #
26
+ # page.meta #=> OpenStruct of the metadata
27
+ # page.title #=> "Welcome to my site!"
28
+ #
29
+ # page.layout # Proton::Layout or nil
30
+ # page.layout?
31
+ #
32
+ # Types:
33
+ #
34
+ # page.html?
35
+ # page.mime_type #=> "text/html" or nil -- only for tilt? == true
36
+ # page.default_ext #=> "html"
37
+ #
38
+ # Contents:
39
+ #
40
+ # page.to_html
41
+ # page.to_html(locals={})
42
+ # page.content
43
+ # page.markup
44
+ #
45
+ # Traversion:
46
+ #
47
+ #
48
+ # # Pages (a Proton::Page or nil)
49
+ # page.parent
50
+ # page.next
51
+ #
52
+ # # Sets (a Proton::Set)
53
+ # page.children
54
+ # page.siblings
55
+ # page.breadcrumbs
56
+ #
57
+ # # Misc
58
+ # page.index? # if it's an index.html
59
+ # page.parent?
60
+ # page.root? # true if no parents
61
+ # page.depth
62
+ #
63
+ # Tilt:
64
+ #
65
+ # page.tilt? # true, if it's a dynamic file
66
+ # page.tilt_engine_name #=> 'RedCloth'
67
+ #
68
+ # Building:
69
+ #
70
+ # page.write
71
+ # page.write('~/foo.html')
72
+ #
73
+ class Page
74
+ attr_reader :project
75
+ attr_reader :file
76
+
77
+ def self.[](id, project=Proton.project)
78
+ site_path = root_path(project)
79
+ return nil if site_path.nil?
80
+
81
+ site = lambda { |*x| File.join site_path, *(x.compact) }
82
+ try = lambda { |_id| p = new(_id, project); p if p.exists? }
83
+
84
+ # For paths like '/' or '/hello/'
85
+ nonfile = File.basename(id).gsub('/','').empty?
86
+
87
+ # Account for:
88
+ # ~/mysite/site/about/us.html.haml
89
+ # about/us.html.haml => ~/mysite/site/about/us.html.haml
90
+ # about/us.html => ~/mysite/site/about/us.html.*
91
+ # about/us.html => ~/mysite/site/about/us.*
92
+ # about/us => ~/mysite/site/about/us/index.*
93
+ #
94
+ page = try[id]
95
+ page ||= try[site[id]]
96
+ unless nonfile
97
+ page ||= try[Dir[site["#{id}.*"]].first]
98
+ page ||= try[Dir[site["#{id.to_s.sub(/\.[^\.]*/,'')}.*"]].first]
99
+ end
100
+ page ||= try[Dir[site[id, "index.*"]].first]
101
+
102
+ # Subclass
103
+ if page && page.tilt? && page.meta[:type]
104
+ klass = Page.get_type(page.meta[:type])
105
+ raise Error, "#{page.filepath}: Class for type '#{page.meta[:type]}' not found" unless klass
106
+ page = klass.new(id, project)
107
+ end
108
+
109
+ page
110
+ end
111
+
112
+ def initialize(file, project=Proton.project)
113
+ @file = File.expand_path(file) if file.is_a?(String)
114
+ @project = project
115
+ raise Error if project.nil?
116
+ end
117
+
118
+ # Returns the URL path for a page.
119
+ def path
120
+ path = @file.sub(File.expand_path(root_path), '')
121
+
122
+ # if xx.haml (but not xx.html.haml),
123
+ if tilt?
124
+ path = path.sub(/\.[^\.]*$/, "")
125
+ path += ".#{default_ext}" unless File.basename(path).include?('.')
126
+ end
127
+
128
+ path
129
+ end
130
+
131
+ # Returns a short filepath relative to the project path
132
+ def filepath
133
+ root = project.root
134
+ fpath = file
135
+ fpath = fpath[root.size..-1] if fpath[0...root.size] == root
136
+ fpath
137
+ end
138
+
139
+ def title
140
+ (meta.title if tilt?) || path
141
+ end
142
+
143
+ alias to_s title
144
+
145
+ def position
146
+ meta[:position] || title
147
+ end
148
+
149
+ def <=>(other)
150
+ result = self.position <=> other.position
151
+ result ||= self.position.to_s <=> other.position.to_s
152
+ result
153
+ end
154
+
155
+ # Method: html? (Proton::Page)
156
+ # Returns true if the page is an HTML page.
157
+
158
+ def html?
159
+ mime_type == 'text/html'
160
+ end
161
+
162
+ # Method: mime_type (Proton::Page)
163
+ # Returns a MIME type for the page, based on what template engine was used.
164
+ #
165
+ # ## Example
166
+ # Page['/style.css'].mime_type #=> 'text/css'
167
+ # Page['/index.html'].mime_type #=> 'text/html'
168
+ #
169
+ # ## See also
170
+ # - {Proton::Page::default_ext}
171
+ #
172
+ def mime_type
173
+ return nil unless tilt?
174
+
175
+ mime = nil
176
+ mime = tilt_engine.default_mime_type if tilt_engine.respond_to?(:default_mime_type)
177
+
178
+ mime ||= case tilt_engine.name
179
+ when 'Tilt::SassTemplate' then 'text/css'
180
+ when 'Tilt::ScssTemplate' then 'text/css'
181
+ when 'Tilt::LessTemplate' then 'text/css'
182
+ when 'Tilt::CoffeeScriptTemplate' then 'application/javascript'
183
+ when 'Tilt::NokogiriTemplate' then 'text/xml'
184
+ when 'Tilt::BuilderTemplate' then 'text/xml'
185
+ else 'text/html'
186
+ end
187
+ end
188
+
189
+ # Method: default_ext (Proton::Page)
190
+ # Returns a default extension for the page based on the page's MIME type.
191
+ #
192
+ # ## Example
193
+ # Page['/style.css'].default_ext #=> 'css'
194
+ # Page['/index.html'].default_ext #=> 'html'
195
+ #
196
+ # ## See also
197
+ # - {Proton::Page::mime_type}
198
+
199
+ def default_ext
200
+ case mime_type
201
+ when 'text/html' then 'html'
202
+ when 'text/css' then 'css'
203
+ when 'text/xml' then 'xml'
204
+ when 'application/javascript' then 'js'
205
+ end
206
+ end
207
+
208
+ # Method: get_type (Proton::Page)
209
+ # Returns a page subtype.
210
+ #
211
+ # ## Example
212
+ # Page.get_type('post') => Proton::Page::Post
213
+
214
+ def self.get_type(type)
215
+ type = type.to_s
216
+ klass = type[0..0].upcase + type[1..-1].downcase
217
+ klass = klass.to_sym
218
+ self.const_get(klass) if self.const_defined?(klass)
219
+ end
220
+
221
+ def exists?
222
+ @file and File.file?(@file||'') and valid?
223
+ end
224
+
225
+ # Make sure that it's in the right folder.
226
+ def valid?
227
+ prefix = File.expand_path(root_path)
228
+ prefix == File.expand_path(@file)[0...prefix.size]
229
+ end
230
+
231
+ def content(locals={}, tilt_options={}, &blk)
232
+ return markup unless tilt?
233
+ tilt(tilt_options).render(dup.extend(Helpers), locals, &blk)
234
+ end
235
+
236
+ def to_html(locals={}, tilt_options={}, &blk)
237
+ html = content(locals, tilt_options, &blk)
238
+ html = layout.to_html(locals, tilt_options) { html } if layout?
239
+ html
240
+ end
241
+
242
+ def layout
243
+ layout = meta.layout
244
+ layout ||= default_layout unless meta.layout == false
245
+ Layout[layout, page] if layout
246
+ end
247
+
248
+ def page
249
+ self
250
+ end
251
+
252
+ def layout?
253
+ !! layout
254
+ end
255
+
256
+ def meta
257
+ @meta ||= Meta.new(parts.first)
258
+ end
259
+
260
+ # Writes to the given output file.
261
+ def write(out=nil)
262
+ out ||= project.path(:output, path)
263
+ FileUtils.mkdir_p File.dirname(out)
264
+
265
+ if tilt?
266
+ File.open(out, 'w') { |f| f.write to_html({}, :build => true) }
267
+ else
268
+ FileUtils.cp file, out
269
+ end
270
+ end
271
+
272
+ # Checks if the file is supported by tilt.
273
+ def tilt?
274
+ !! tilt_engine
275
+ end
276
+
277
+ # Returns the Tilt engine (eg Tilt::HamlEngine).
278
+ def tilt_engine
279
+ Tilt[@file]
280
+ end
281
+
282
+ def tilt_engine_name
283
+ tilt_engine.name.match(/:([^:]*)(?:Template?)$/)[1]
284
+ end
285
+
286
+ # Returns the tilt layout.
287
+ def tilt(tilt_options={})
288
+ if tilt?
289
+ parts
290
+ # HAML options and such (like :escape_html)
291
+ options = project.config.tilt_options_for(@file, tilt_options)
292
+ offset = @offset || 1
293
+ Tilt.new(@file, offset, options) { markup }
294
+ end
295
+ end
296
+
297
+ def markup
298
+ parts.last
299
+ end
300
+
301
+ def method_missing(meth, *args, &blk)
302
+ super unless meta.instance_variable_get(:@table).keys.include?(meth.to_sym)
303
+ meta.send(meth)
304
+ end
305
+
306
+ def parent
307
+ parts = path.split('/') # ['', 'about', 'index.html']
308
+
309
+ try = lambda { |newpath| p = self.class[newpath, project]; p if p && p.path != path }
310
+
311
+ # Absolute root
312
+ return nil if index? and parts.size <= 2
313
+
314
+ parent = try[parts[0...-1].join('/')] # ['','about'] => '/about'
315
+ parent ||= try['/'] # Home
316
+ end
317
+
318
+ def children
319
+ files = if index?
320
+ # about/index.html => about/*
321
+ File.expand_path('../*', @file)
322
+ else
323
+ # products.html => products/*
324
+ base = File.basename(@file, '.*')
325
+ File.expand_path("../#{base}/*", @file)
326
+ end
327
+
328
+ Set.new Dir[files].
329
+ reject { |f| f == @file || project.ignored_files.include?(f) }.
330
+ map { |f| self.class[f, project] }.
331
+ compact.sort
332
+ end
333
+
334
+ def siblings
335
+ pages = (p = parent and p.children)
336
+ return Set.new unless pages
337
+ return Set.new unless pages.include?(self)
338
+ Set.new(pages)
339
+ end
340
+
341
+ def breadcrumbs
342
+ Set.new(parent? ? (parent.breadcrumbs + [self]) : [self])
343
+ end
344
+
345
+ def index?
346
+ File.basename(path, '.*') == 'index'
347
+ end
348
+
349
+ # Method: parent? (Proton::Page)
350
+ # Returns true if the page has a parent.
351
+ #
352
+ # This is the opposite of {Proton::Page::root?}.
353
+ #
354
+ # ## See also
355
+ # - {Proton::Page::root?}
356
+
357
+ def parent?
358
+ !parent.nil?
359
+ end
360
+
361
+ # Method: root? (Proton::Page)
362
+ # Returns true if the page is the home page.
363
+ #
364
+ # This is the opposite of {Proton::Page::parent?}.
365
+ #
366
+ # ## See also
367
+ # - {Proton::Page::parent?}
368
+
369
+ def root?
370
+ parent.nil?
371
+ end
372
+
373
+ def depth
374
+ breadcrumbs.size
375
+ end
376
+
377
+ def next
378
+ page = self
379
+ while true do
380
+ page.siblings.index(self)
381
+ end
382
+ end
383
+
384
+ def ==(other)
385
+ self.path == other.path
386
+ end
387
+
388
+ def inspect
389
+ "<##{self.class.name} #{path.inspect}>"
390
+ end
391
+
392
+ protected
393
+
394
+ # Method: default_layout (Proton::Page)
395
+ # Returns the default layout.
396
+ #
397
+ # This method may be overridden by subclasses as needed.
398
+
399
+ def default_layout
400
+ 'default' if html?
401
+ end
402
+
403
+ # Returns the two parts of the markup.
404
+ def parts
405
+ @parts ||= begin
406
+ t = File.open(@file).read
407
+ t = t.force_encoding('UTF-8') if t.respond_to?(:force_encoding)
408
+ m = t.match(/^(.*?)\n--+\n(.*)$/m)
409
+
410
+ if m.nil?
411
+ [{}, t]
412
+ else
413
+ @offset = m[1].count("\n") + 2
414
+ data = YAML::load(m[1])
415
+ raise ArgumentError unless data.is_a?(Hash)
416
+ [data, m[2]]
417
+ end
418
+ rescue ArgumentError
419
+ [{}, t]
420
+ end
421
+ end
422
+
423
+ def self.root_path(project, *a)
424
+ project.path(:site, *a)
425
+ end
426
+
427
+ def root_path(*a)
428
+ self.class.root_path(project, *a)
429
+ end
430
+ end
431
+ end