proton 0.3.0.rc1

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