` is the name of your project. This will create a folder with that
13 | name, along with some sample files to get you started.
14 |
15 | Starting
16 | --------
17 |
18 | Begin working on your project by starting the Proton webserver. This is
19 | optional, but is recommended as it's a nice way to see your changes in real
20 | time.
21 |
22 | $ proton start
23 |
24 | After typing this, you will see the server has started. Point your web browser to
25 | `http://localhost:4833` to see your site. You should now see your project's
26 | default "welcome" page.
27 |
28 |
29 | Editing your site
30 | -----------------
31 |
32 | Your project has a subfolder called `site` -- this is where all the site's files are
33 | stored. In general, dropping any file in this folder will make it accessible with the
34 | same filename.
35 |
36 | Try this: create a file called `products.html` and fill it up like you would an
37 | HTML page. After that, point your browser to `http://localhost:4833/products.html`,
38 | which should now show the page you were working on.
39 |
40 | You may also put your files in subfolders. If you were to create the file
41 | `site/assets/my_style.css`, it should be accessible through
42 | `http://localhost:4833/assets/my_style.css`.
43 |
44 | Dynamic files
45 | -------------
46 |
47 | Proton supports many templating languages like HAML, Less, and ERB (more on this later).
48 | If your file ends in one of these supported extensions (e.g., `index.haml`), it
49 | is assumed to be a dynamic file and will be rendered by it's corresponding templating
50 | engine (in this case, HAML).
51 |
52 | Building HTML files
53 | -------------------
54 |
55 | The `proton start` webserver is good for local development, but when it's time to
56 | deploy your site, you will need to build your files. This process outputs raw
57 | HTML files for your entire site (for the dynamic files), with Proton translating
58 | any files that need translation (e.g., HAML and ERB files).
59 |
60 | Build your files by typing this in the command prompt:
61 |
62 | $ proton build
63 |
64 | This will create a folder called `public/` where the built files are stored.
65 | You can now deploy this folder to your webserver.
66 |
67 |
--------------------------------------------------------------------------------
/lib/proton.rb:
--------------------------------------------------------------------------------
1 | $:.push *Dir[File.expand_path('../../vendor/*/lib', __FILE__)]
2 |
3 | require 'fileutils'
4 | require 'ostruct'
5 | require 'hashie'
6 | require 'yaml'
7 | require 'tilt'
8 | require 'shake'
9 |
10 | # For Compass and such
11 | Encoding.default_external = 'utf-8' if defined?(::Encoding)
12 |
13 | # HTML files as ERB
14 | Tilt.mappings['html'] = Tilt.mappings['erb']
15 |
16 | # Class: Proton
17 | # The Proton class.
18 | #
19 | # ## Description
20 | # This is the main class.
21 | #
22 | # This class is also aliased as `Hyde` for backward-compatibility with
23 | # versions <= 0.2.x, when the project was still called Hyde.
24 |
25 | class Proton
26 | PREFIX = File.expand_path('../', __FILE__)
27 |
28 | # Class: Proton::Error
29 | # An error.
30 | #
31 | # ## Description
32 | # The class Error describes any error thrown by the application.
33 | #
34 | # ## Example
35 | #
36 | # begin
37 | # Proton::Project.new './my_project/'
38 | # rescue Proton::LegacyError => e
39 | # puts "Old version."
40 | # rescue Proton::VersionError => e
41 | # puts "The project requires a newer version of Proton."
42 | # rescue Proton::Error => e
43 | # puts e
44 | # end
45 |
46 | Error = Class.new(StandardError)
47 | LegacyError = Class.new(Error)
48 | VersionError = Class.new(Error)
49 |
50 | # Constant: CONFIG_FILES (Proton)
51 | # An array of the allowed config filenames.
52 | CONFIG_FILES = ['Protonfile', 'proton.conf', '.protonrc', 'hyde.conf', '.hyderc']
53 |
54 | autoload :Project, "#{PREFIX}/proton/project"
55 | autoload :Page, "#{PREFIX}/proton/page"
56 | autoload :Meta, "#{PREFIX}/proton/meta"
57 | autoload :Config, "#{PREFIX}/proton/config"
58 | autoload :CLI, "#{PREFIX}/proton/cli"
59 | autoload :Set, "#{PREFIX}/proton/set"
60 | autoload :Layout, "#{PREFIX}/proton/layout"
61 | autoload :Partial, "#{PREFIX}/proton/partial"
62 | autoload :Helpers, "#{PREFIX}/proton/helpers"
63 | autoload :Cacheable, "#{PREFIX}/proton/cacheable"
64 |
65 | require "#{PREFIX}/proton/version"
66 |
67 | class << self
68 | # Attribute: project (Proton)
69 | # Returns the latest project.
70 | #
71 | # ## Example
72 | # Proton.project.path(:site)
73 |
74 | attr_accessor :project
75 | end
76 | end
77 |
78 | # For backward compatibility reasons, Hyde is an alias for Proton.
79 | Hyde = Proton
80 |
--------------------------------------------------------------------------------
/lib/proton/cli/helpers.rb:
--------------------------------------------------------------------------------
1 | class Proton
2 | class CLI
3 | module Helpers
4 | def show_help_for(name)
5 | task = task(name)
6 | pass "No such command. Try: #{executable} help" unless task
7 |
8 | help = task.help
9 | if help
10 | help.each { |line| err line }
11 | err
12 | else
13 | err "Usage: #{executable} #{task.usage || name}"
14 | err "#{task.description}." if task.description
15 | end
16 | end
17 |
18 | def tasks_for(category)
19 | tasks.select { |name, t| t.category == category }
20 | end
21 |
22 | def other_tasks
23 | tasks.select { |name, t| t.category.nil? }
24 | end
25 |
26 | def say_info(str)
27 | say_status '*', str, 30
28 | end
29 |
30 | def say_error(str)
31 | say_status 'error', str, 31
32 | end
33 |
34 | def say_status(what, cmd, color=32)
35 | c1 = "\033[0;#{color}m"
36 | c0 = "\033[0;m"
37 | puts "#{c1}%10s#{c0} %s" % [ what, cmd ]
38 | end
39 |
40 | def show_needed_gem(name)
41 | err
42 | say_error "You will need additional gems for this project."
43 | say_info "To install: gem install #{name}"
44 | end
45 |
46 | def no_project
47 | "Error: no Proton config file found.\n" +
48 | "(Looked for #{Proton::CONFIG_FILES.join(', ')})\n\n" +
49 | "You start by creating a config file for this project:\n" +
50 | " $ #{executable} create .\n\n" +
51 | "You may also create an empty project in a new directory:\n" +
52 | " $ #{executable} create NAME\n"
53 | end
54 |
55 | def project?
56 | !! @config_file
57 | end
58 |
59 | # Gets the gem name from a LoadError exception.
60 | def gem_name(e)
61 | name = e.message.split(' ').last
62 | name = 'RedCloth' if name == 'redcloth'
63 | name = 'haml' if name == 'sass/plugin'
64 | name
65 | end
66 |
67 | def project
68 | @project ||= begin
69 | pass no_project unless project?
70 | Dir.chdir File.dirname(@config_file)
71 |
72 | begin
73 | project = Proton.project || Proton::Project.new
74 | pass no_project unless project.config_file
75 | rescue LegacyError
76 | err "This is a legacy Hyde project."
77 | err "To force it, try editing `hyde.conf` and upgrade the version line to `hyde_requirement: 0.1`."
78 | pass
79 | rescue VersionError => e
80 | err e.message
81 | pass
82 | end
83 |
84 | project
85 | end
86 | end
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/lib/proton/server.rb:
--------------------------------------------------------------------------------
1 | require 'cuba'
2 | require 'rack'
3 | require 'proton'
4 |
5 | # The only time this file gets loaded is if you require it explicity, ie,
6 | # in a config.ru. Disable caching for when it's ran as a development-time
7 | # server.
8 | Proton::Cacheable.disable!
9 |
10 | # Module: Proton::Server
11 | # The Proton server rack application.
12 |
13 | class Proton
14 | Server = Cuba.dup
15 |
16 | module Server; end
17 |
18 | module Server::PageHelpers
19 | def not_found
20 | show_status nil
21 | res.status = 404
22 | res.write "File Not Found
The path #{env['PATH_INFO']} was not found." + " "*1024
23 | end
24 |
25 | def options
26 | @options ||= Hash.new
27 | end
28 |
29 | def show_status(page)
30 | return if options[:quiet]
31 | path = env['PATH_INFO']
32 | return if path == '/favicon.ico'
33 |
34 | status = page ? "\033[0;32m[ OK ]" : "\033[0;31m[404 ]"
35 | verb = get ? 'GET ' : (post ? 'POST' : '')
36 | puts "%s\033[0;m %s %s" % [ status, verb, env['PATH_INFO'] ]
37 | puts " src: #{page.filepath} (#{page.tilt_engine_name})" if page && page.tilt?
38 | end
39 |
40 | def mime_type_for(page)
41 | type = page.mime_type
42 | type ||= Rack::Mime::MIME_TYPES[File.extname(page.file)]
43 | type
44 | end
45 |
46 | def server
47 | Proton::Server
48 | end
49 | end
50 |
51 | module Proton::Server
52 | Ron.send :include, PageHelpers
53 |
54 | define do
55 | on default do
56 | begin
57 | page = Proton::Page[env['PATH_INFO']] or break not_found
58 |
59 | # Make the clients use If-Modified-Since
60 | res['Cache-Control'] = 'max-age=86400, public, must-revalidate'
61 |
62 | mtime = [server.options[:last_modified].to_i, File.mtime(page.file).to_i].compact.max
63 | res['Last-Modified'] = mtime.to_s if mtime
64 |
65 | # Get the MIME type from Proton, then fallback to Rack
66 | type = mime_type_for(page)
67 | res['Content-Type'] = type if type
68 |
69 | # Okay, we're done
70 | res.write page.to_html
71 | show_status page
72 | rescue => e
73 | res['Content-Type'] = 'text/html'
74 | res.write "
#{e.class}: #{e.message}
#{e.backtrace.map{|l|"- #{l}
"}.join('')}
"
75 | end
76 | end
77 | end
78 | end
79 | end
80 |
81 | module Proton::Server
82 | # Available options:
83 | # :last_modified -- timestamp for all files
84 | def self.options
85 | @options ||= Hash.new
86 | end
87 |
88 | # :Host, :Port
89 | def self.run!(options={})
90 | self.options.merge options
91 | handler = rack_handler or return false
92 | handler.run self, options
93 | end
94 |
95 | def self.rack_handler
96 | %w(thin mongrel webrick).each do |svr|
97 | begin
98 | return Rack::Handler.get(svr)
99 | rescue LoadError
100 | rescue NameError
101 | end
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/manual/index.textile:
--------------------------------------------------------------------------------
1 | title: Proton manual
2 | brief: Proton is a website preprocessor.
3 | --
4 |
5 |
6 | $ gem install proton
7 | $ proton
8 |
9 |
10 | h2. Why use Proton?
11 |
12 | It's like building a static site, but better! You can use Proton for:
13 |
14 | * Building HTML prototypes
15 | * Building sites with no dynamic logic
16 | * Creating a blog where the entries are stored in a source repository
17 |
18 | h2. Getting started
19 |
20 |
21 |
22 | $ proton create myproject
23 | create myproject/
24 | create myproject/index.md
25 | create myproject/Protonfile
26 | ...
27 |
28 | $ cd myproject
29 |
30 |
31 | h4. Create your first project
32 |
33 | Create your first project using "@proton create@":proton_create.html. This will create a new folder for you.
34 |
35 |
36 |
37 |
38 | $ proton start
39 | * Starting server...
40 | >> Thin web server (v1.2.11 codename Bat-Shit Crazy)
41 | >> Maximum connections set to 1024
42 | >> Listening on 0.0.0.0:4833, CTRL+C to stop
43 |
44 |
45 | h4. Open a live preview
46 |
47 | Typing "@proton start@":proton_start.html, will get you a live preview in `http://localhost:4833`. Point your browser there to see your changes as you make them.
48 |
49 |
50 |
51 |
52 | [myproject/index.haml (haml)]
53 | %h1 Helol teher!
54 | %p Welcome nt omy site.
55 |
56 |
57 |
58 | [myproject/about_us.markdown (markdown)]
59 | About us
60 | ========
61 |
62 | This is text written in Markdown.
63 |
64 |
65 | h4. Edit your pages
66 |
67 | Use your favorite text editor to edit your files. You can write in Markdown, Textile or "HAML":http://hamllang.com for HTML, or "Sass":http://sass-lang.com for CSS, and many more.
68 |
69 |
70 |
71 |
72 | $ proton build
73 | * _output/index.html
74 | * _output/about_us.html
75 |
76 |
77 | h4. Build your HTML
78 |
79 | By typing "@proton build@":proton_build.html, you get your site as simple HTML files.
80 |
81 |
82 | h2. Features
83 |
84 | * __Layouts and Partials:__ Proton lets you define your site's header and footer layouts in a separate file, and even separate snippets of your site into reuseable components (partials). Your code will be much cleaner and DRYer!
85 |
86 | * __Template languages:__ Proton lets you write your site in any template language you need -- the translation will be taken care of for you. Proton supports HAML, LessCSS, Markdown, SASS and Textile by default, and more can be added through extensions.
87 |
88 | * __Extendable:__ Proton is easily customizable with extensions. You can add new helpers, new commands, support for new languages and many more by simply adding the extensions to your project folder.
89 |
90 | * __Design fast:__ Proton comes with a web server that lets you serve up your site locally. Any changes to your files will be seen on your next browser refresh!
91 |
92 | * __Build your site as static HTMLs:__ You can export your site as plain HTML files with one simple command.
93 |
94 | Source
95 |
--------------------------------------------------------------------------------
/lib/proton/config.rb:
--------------------------------------------------------------------------------
1 | class Proton
2 | # Class: Proton::Config
3 | # Configuration.
4 | #
5 | # ## Common usage
6 | #
7 | # Access it via `Proton.project`.
8 | #
9 | # Proton.project.config
10 | #
11 | # You may access config variables as attributes.
12 | #
13 | # Proton.project.config.site_path
14 | # Proton.project.config.layouts_path
15 | # Proton.project.config.extensions_path
16 | # Proton.project.config.output_path
17 | #
18 | # Tilt options:
19 | #
20 | # Proton.project.config.tilt_options('sass')[:load_path]
21 | # Proton.project.config.tilt_options_for('filename.haml')[:style]
22 | #
23 | class Config
24 | DEFAULTS = {
25 | :site_path => '.',
26 | :layouts_path => '_layouts',
27 | :extensions_path => '_extensions',
28 | :partials_path => '_layouts',
29 | :output_path => '_output',
30 | :tilt_options => {
31 | :haml => {
32 | :escape_html => true
33 | },
34 | :sass => {
35 | :load_paths => ['css', '.'],
36 | :style => :compact,
37 | :line_numbers => true
38 | },
39 | :scss => {
40 | :load_paths => ['css', '.'],
41 | :style => :compact,
42 | :line_numbers => true
43 | },
44 | },
45 | :tilt_build_options => {
46 | :scss => {
47 | :style => :compressed,
48 | :line_numbers => false
49 | },
50 | :sass => {
51 | :style => :compressed,
52 | :line_numbers => false
53 | }
54 | }
55 | }
56 |
57 | def self.load(config_file)
58 | new(YAML::load_file(config_file)) rescue new
59 | end
60 |
61 | def initialize(options={})
62 | # Try to emulate proper .merge behavior in Ruby 1.8
63 | #DEFAULTS.each { |k, v| options[k] ||= v }
64 | @table = Hashie::Mash.new
65 | @table.deep_merge! DEFAULTS
66 | @table.deep_merge! options
67 | end
68 |
69 | # Passthru
70 | def method_missing(meth, *args, &blk)
71 | @table.send meth, *args
72 | end
73 |
74 | def requirement
75 | # Backward compatibility; this config option used to be called
76 | # `hyde_requirement` before the project was renamed to Proton.
77 | self[:requirement] || self[:hyde_requirement]
78 | end
79 |
80 | # Method: tilt_options_for (Proton::Config)
81 | # Returns tilt options for a given file.
82 | #
83 | # ## Usage
84 | # tilt_options_for(filename, options={})
85 | #
86 | # ## Example
87 | # tilt_options_for('index.haml') # { :escape_html => ... }
88 | #
89 | def tilt_options_for(file, options={})
90 | ext = file.split('.').last.downcase
91 | opts = tilt_options(ext) || Hash.new
92 | opts = opts.merge(tilt_options(ext, :tilt_build_options)) if options[:build]
93 |
94 | to_hash opts
95 | end
96 |
97 | # Method: tilt_options (Proton::Config)
98 | # Returns tilt options for a given engine.
99 | #
100 | # ## Usage
101 | # tilt_options(engine_name)
102 | #
103 | # ## Example
104 | # tilt_options('haml') # { :escape_html => ... }
105 | #
106 | def tilt_options(what, key=:tilt_options)
107 | @table[key] ||= Hash.new
108 | @table[key][what.to_s] ||= Hash.new
109 | end
110 |
111 | private
112 | def to_hash(mash)
113 | mash.inject(Hash.new) { |h, (k, v)| h[k.to_sym] = v; h }
114 | end
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/test/unit/fixture_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../../helper', __FILE__)
2 |
3 | class FixtureTest < TestCase
4 | def assert_fixture_works(path, options={})
5 | build_fixture(path, options) { |control, var|
6 | assert File.exists?(var), "#{var} doesn't exist"
7 | if read(control) != read(var)
8 | flunk "Failed in #{var}\n" +
9 | "Control:\n" +
10 | read(control).gsub(/^/, '| ') + "\n\n" +
11 | "Variable:\n" +
12 | read(var).gsub(/^/, '| ')
13 | end
14 | }
15 | end
16 |
17 | def assert_fixture_fails(path, error=Proton::Error, &blk)
18 | begin
19 | build_fixture(path)
20 | rescue error => e
21 | yield e
22 | else
23 | flunk "Assertion failed"
24 | end
25 | end
26 |
27 |
28 | def build_fixture(path, options={}, &blk)
29 | # Build
30 | project = build path
31 |
32 | options[:control_path] ||= '_control'
33 | options[:public_path] ||= '_public'
34 |
35 | control_path = project.root(options[:control_path])
36 | public_path = project.root(options[:public_path])
37 |
38 | from = Dir["#{control_path}/**/*"].map { |dir| dir.gsub(control_path, '') }.sort
39 | to = Dir["#{public_path}/**/*"].map { |dir| dir.gsub(public_path, '') }.sort
40 |
41 | assert_equal from, to, "The build happened to make less files"
42 |
43 | Dir["#{control_path}/**/*"].each do |control|
44 | next unless File.file?(control)
45 | var = control.sub(control_path, public_path)
46 | yield control, var
47 | end if block_given?
48 | end
49 |
50 | def read(file)
51 | File.open(file) { |f| f.read }
52 | end
53 |
54 | teardown do
55 | # Remove the generated
56 | ( Dir[fixture('*', 'public')] +
57 | Dir[fixture('*', '_public')]
58 | ).each { |dir| FileUtils.rm_rf dir }
59 | end
60 |
61 | test "fixture one" do
62 | assert_fixture_works fixture('one')
63 | end
64 |
65 | test "fixture subclass" do
66 | assert_fixture_works fixture('subclass')
67 | end
68 |
69 | test "fixture extensions" do
70 | assert_fixture_works fixture('extensions')
71 | end
72 |
73 | test "fixture parent" do
74 | assert_fixture_works fixture('parent')
75 | end
76 |
77 | test "fixture sort" do
78 | assert_fixture_works fixture('sort')
79 | end
80 |
81 | test "fixture ignores" do
82 | assert_fixture_works fixture('ignores')
83 | end
84 |
85 | test "fixture metadata" do
86 | assert_fixture_works fixture('metadata')
87 | end
88 |
89 | test "fixture nested_layout" do
90 | assert_fixture_works fixture('nested_layout')
91 | end
92 |
93 | test "fixture build_options" do
94 | assert_fixture_works fixture('build_options')
95 | end
96 |
97 | test "compass support" do
98 | assert_fixture_works fixture('compass')
99 | end
100 |
101 | test "fixture empty_config" do
102 | assert_fixture_works fixture('empty_config'),
103 | :control_path => '_control',
104 | :public_path => '_output'
105 | end
106 |
107 | test "fixture high_version" do
108 | assert_fixture_fails(fixture('high_version')) { |e|
109 | assert e.message.include?('>= 9000')
110 | }
111 |
112 | assert_fixture_fails(fixture('high_version_2')) { |e|
113 | assert e.message.include?('>= 9000')
114 | }
115 | end
116 |
117 | test "fixture fail_type" do
118 | assert_fixture_fails(fixture('fail_type')) { |e|
119 | assert e.message.include?('nonexistent')
120 | }
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/lib/proton/helpers.rb:
--------------------------------------------------------------------------------
1 | # Module: Proton::Helpers
2 | # Helpers you can use in your pages.
3 | #
4 | # ## Creating your own helpers
5 | # To create for own helpers, make an extension. See [Extending Proton:
6 | # Helpers][1] for more info.
7 | #
8 | # [1]: /extending/helpers.html
9 |
10 | class Proton
11 | module Helpers
12 |
13 | # Helper: partial (Helpers)
14 | # Renders a partial.
15 | #
16 | # ## Usage
17 | # <%= partial path, locals %>
18 | #
19 | # ## Description
20 | # See [Introduction: Partials](/introduction/partials.html) for more
21 | # info.
22 | #
23 | # ## Example
24 | #
25 | # If your `_layouts/_banner.erb` looks like this:
26 | #
27 | #
28 | # Welcome to <%= start.title %>
29 | #
30 | #
31 | # ...Then this will embed the partial `_layouts/_nav.erb`. The partial
32 | # will be rendered with `start` being set to the current page.
33 | #
34 | # <%= partial '_banner', :start => page %>
35 | #
36 | def partial(path, locals={})
37 | partial = Partial[path.to_s, page] or return ''
38 | partial.to_html locals.merge(:page => self)
39 | end
40 |
41 | # Helper: rel (Helpers)
42 | # Turns a path into a relative path.
43 | #
44 | # ## Usage
45 | # <%= rel(path) %>
46 | #
47 | # ## Description
48 | # `rel` takes a given absolute path that begins with a `/` (for instance,
49 | # `/x/y/z.html`) and returns a relative path (maybe `../y/z.html`). This is
50 | # useful if your site will not be be hosted on it's own domain.
51 | #
52 | # ## Example
53 | # <% page.children.each do |child| %>
54 | #
55 | # <%= child.title %>
56 | #
57 | # <% end %>
58 | #
59 | # This may output:
60 | #
61 | #
62 | # Foobar
63 | #
64 | #
65 | # ...where the `../../` depends on the current page's path.
66 | #
67 | def rel(path, from=page.path)
68 | if path[0] == '/'
69 | depth = from.count('/')
70 | dotdot = depth > 1 ? ('../' * (depth-1)) : './'
71 | str = (dotdot[0...-1] + path).squeeze('/')
72 | str = str[2..-1] if str[0..-1] == './'
73 | str
74 | else
75 | path
76 | end
77 | end
78 |
79 | # Helper: content_for (Helpers)
80 | # Content for.
81 | #
82 | # ## See also
83 | #
84 | # * {Helpers:has_content?}
85 | # * {Helpers:content_for}
86 | # * {Helpers:yield_content}
87 | #
88 | def content_for(key, &blk)
89 | content_blocks[key.to_sym] = blk
90 | end
91 |
92 | def content_blocks
93 | $content_blocks ||= Hash.new
94 | $content_blocks[page.path] ||= Hash.new
95 | end
96 |
97 | # Helper: has_content? (Helpers)
98 | # Checks if there's something defined for a given content block.
99 | #
100 | # ## Example
101 | # See {Helpers:content_for} for an example.
102 | #
103 | # ## See also
104 | # * {Helpers:has_content?}
105 | # * {Helpers:content_for}
106 | # * {Helpers:yield_content}
107 | #
108 | def has_content?(key)
109 | content_blocks.member? key.to_sym
110 | end
111 |
112 | # Helper: yield_content (Helpers)
113 | # Yield
114 | #
115 | # ## Example
116 | # See {Helpers:content_for} for an example.
117 | #
118 | # ## See also
119 | # * {Helpers:has_content?}
120 | # * {Helpers:content_for}
121 | # * {Helpers:yield_content}
122 | #
123 | def yield_content(key, *args)
124 | content = content_blocks[key.to_sym]
125 | if respond_to?(:block_is_haml?) && block_is_haml?(content)
126 | capture_haml(*args, &content)
127 | elsif content
128 | content.call(*args)
129 | end
130 | end
131 | end
132 | end
133 |
--------------------------------------------------------------------------------
/lib/proton/project.rb:
--------------------------------------------------------------------------------
1 | class Proton
2 | # Class: Proton::Project
3 | # A project.
4 | #
5 | # ## Common usage
6 | #
7 | # Instanciating:
8 | #
9 | # project = Project.new('~/spur')
10 | # project == Proton.project # the last defined project
11 | #
12 | # Building:
13 | #
14 | # project.build
15 | # project.build { |file| puts "Building #{file}..." }
16 | #
17 | # Getting pages:
18 | #
19 | # Proton::Page['/index.html'] # ~/spur/index.md; uses Proton.project
20 | #
21 | # Configuration:
22 | #
23 | # project.config_file # ~/spur/hyde.conf
24 | # project.config # Config from above file (OpenStruct)
25 | # project.config.site_path
26 | #
27 | # Paths:
28 | #
29 | # project.path(:site) # ~/spur/site (based on config site_path)
30 | # project.path(:extensions)
31 | #
32 | # project.root('a/b', 'c') # ~/spur/a/b/c
33 | #
34 | # Indexing:
35 | #
36 | # project.pages # [<#Page>, <#Page>, ...]
37 | # project.files # ['/index.md', '/style.sass', ...]
38 | # # (only site files)
39 | # project.ignored_files
40 | #
41 | class Project
42 | def initialize(root=Dir.pwd)
43 | @root = root
44 | Proton.project = self
45 |
46 | validate_version
47 | load_extensions
48 |
49 | load File.expand_path('../compass_support.rb', __FILE__)
50 | end
51 |
52 | def validate_version
53 | return unless config_file?
54 | req = config.requirement.to_s
55 |
56 | v = lambda { |version| Gem::Version.new version }
57 |
58 | if req.empty?
59 | # pass
60 | elsif v[req] < v["0.1"]
61 | raise LegacyError, "This is a legacy project"
62 | elsif v[req] > v[Proton.version]
63 | raise VersionError, "You will need Proton version >= #{req} for this project."
64 | end
65 | end
66 |
67 | def load_extensions
68 | path = path(:extensions)
69 |
70 | ( Dir[path(:extensions, '*.rb')] +
71 | Dir[path(:extensions, '*', '*.rb')]
72 | ).sort.each { |f| require f } if path
73 | end
74 |
75 | # Method: config_file (Proton::Project)
76 | # Returns the configuration file for the project.
77 |
78 | def config_file
79 | try = lambda { |path| p = root(path); p if File.file?(p) }
80 | Proton::CONFIG_FILES.inject(nil) { |acc, file| acc ||= try[file] }
81 | end
82 |
83 | def config_file?
84 | config_file
85 | end
86 |
87 | # Method: config (Proton::Project)
88 | # Returns a Proton::Config instance.
89 |
90 | def config
91 | @config ||= Config.load(config_file)
92 | end
93 |
94 | # Method: path (Proton::Project)
95 | # Returns the path for a certain aspect.
96 | #
97 | # ## Example
98 | #
99 | # Proton.project.path(:site)
100 |
101 | def path(what, *a)
102 | return nil unless [:output, :site, :layouts, :extensions, :partials].include?(what)
103 | path = config.send(:"#{what}_path")
104 | root path, *a if path
105 | end
106 |
107 | # Method: root (Proton::Project)
108 | # Returns the root path of the project.
109 |
110 | def root(*args)
111 | File.join @root, *(args.compact)
112 | end
113 |
114 | # Method: pages (Proton::Project)
115 | # Returns the pages for the project.
116 |
117 | def pages
118 | files.map { |f| Page[f, self] }.compact
119 | end
120 |
121 | # Method: files (Proton::Project)
122 | # Returns the site files for the project, free from the ignored files.
123 |
124 | def files
125 | files = Dir[File.join(path(:site), '**', '*')]
126 | files = files.select { |f| File.file?(f) }
127 | files = files.map { |f| File.expand_path(f) }
128 | files - ignored_files
129 | end
130 |
131 | # Method: ignored_files (Proton::Project)
132 | # Returns the files to be ignored for the project.
133 |
134 | def ignored_files
135 | specs = [*config.ignore].map { |s| root(s) }
136 | specs << config_file
137 |
138 | # Ignore the standard files
139 | [:layouts, :extensions, :partials, :output].each do |aspect|
140 | specs << File.join(config.send(:"#{aspect}_path"), '**/*') if path(aspect) && path(aspect) != path(:site)
141 | end
142 |
143 | # Ignore dotfiles and hyde.conf files by default
144 | specs += %w[.* _* *~ README* /config.ru Gemfile Gemfile.lock]
145 | specs += Proton::CONFIG_FILES.map { |s| "/#{s}" }
146 |
147 | specs.compact.map { |s| glob(s) }.flatten.uniq
148 | end
149 |
150 | # Method: build (Proton::Project)
151 | # Builds.
152 |
153 | def build(&blk)
154 | pages.each do |page|
155 | yield page
156 | page.write
157 | end
158 | ensure
159 | build_cleanup
160 | end
161 |
162 | protected
163 | def glob(str)
164 | if str[0] == '/'
165 | Dir[str] + Dir[root(str)] + Dir["#{root(str)}/**"]
166 | else
167 | Dir[root("**/#{str}")] + Dir[root("**/#{str}/**")]
168 | end
169 | end
170 |
171 | def build_cleanup
172 | FileUtils.rm_rf '.sass_cache'
173 | end
174 | end
175 | end
176 |
177 |
--------------------------------------------------------------------------------
/lib/proton/cli.rb:
--------------------------------------------------------------------------------
1 | # Class: Proton::CLI
2 | # Command line runner.
3 |
4 | class Proton
5 | class CLI < Shake
6 | autoload :Helpers, "#{PREFIX}/proton/cli/helpers"
7 |
8 | extend Helpers
9 | include Defaults
10 |
11 | task(:create) do
12 | wrong_usage unless params.size == 1
13 | template = File.expand_path('../../../data/new_site', __FILE__)
14 | target = params.first
15 |
16 | if target == '.'
17 | pass "This is already a Proton project." if @protonfile
18 | FileUtils.cp_r File.join(template, 'Protonfile'), target
19 | say_status :create, 'Protonfile'
20 | pass
21 | end
22 |
23 | pass "Error: target directory already exists." if File.directory?(target)
24 |
25 | puts "Creating files in #{target}:"
26 | puts
27 |
28 | FileUtils.cp_r template, target
29 | Dir[File.join(target, '**', '*')].sort.each do |f|
30 | say_status :create, f if File.file?(f)
31 | end
32 |
33 | puts ""
34 | puts "Done! You've created a new project in #{target}."
35 | puts "Get started now:"
36 | puts ""
37 | puts " $ cd #{target}"
38 | puts " $ #{executable} start"
39 | puts ""
40 | puts "Or build the HTML files:"
41 | puts ""
42 | puts " $ #{executable} build"
43 | puts ""
44 | end
45 |
46 | task.description = "Starts a new Proton project"
47 | task.usage = "create NAME"
48 | task.category = :create
49 |
50 | task(:build) do
51 | pre = project.config.output_path
52 |
53 | project.build { |page|
54 | c, handler = if page.tilt?
55 | [ 33, "#{page.tilt_engine_name.downcase}" ]
56 | else
57 | [ 30, '*' ]
58 | end
59 |
60 | puts ("\033[0;#{c}m%10s\033[0;32m #{pre}\033[0;m%s" % [ handler, page.path ]).strip
61 | }
62 | project.send :build_cleanup
63 | end
64 |
65 | task.description = "Builds the current project"
66 | task.category = :project
67 |
68 | task(:start) do
69 | project
70 |
71 | port = (params.extract('-p') || 4833).to_i
72 | host = (params.extract('-o') || '0.0.0.0')
73 | daemon = (!! params.delete('-D'))
74 |
75 | require 'proton/server'
76 |
77 | if daemon
78 | pid = fork { Proton::Server.run! :Host => host, :Port => port, :quiet => true }
79 | sleep 2
80 | puts
81 | puts "Listening on #{host}:#{port} on pid #{pid}."
82 | puts "To stop: kill #{pid}"
83 | else
84 | Proton::Server.run! :Host => host, :Port => port
85 | end
86 | end
87 |
88 | task.description = "Starts the server"
89 | task.category = :project
90 | task.help = %{
91 | Usage:
92 |
93 | #{executable} start [-p PORT] [-o HOST] [-D]
94 |
95 | Starts an HTTP server so you may rapidly test your project locally.
96 |
97 | If the -p and/or -o is specified, it will listen on the specified HOST:PORT.
98 | Otherwise, the default is 0.0.0.0:4833.
99 |
100 | If -D is specified, it goes into daemon mode.
101 | }.gsub(/^ {4}/, '').strip.split("\n")
102 |
103 | task(:rack) do
104 | project
105 |
106 | from = File.expand_path("#{PREFIX}/../data/rack/*")
107 | files = Dir[from]
108 |
109 | files.each do |f|
110 | FileUtils.cp f, '.'
111 | say_status :create, File.basename(f)
112 | end
113 |
114 | err ""
115 | err "Done! Your project is now Rack-compatible."
116 | err "Test it out locally by:"
117 | err ""
118 | err " $ rackup"
119 | err ""
120 | err "You may now use your project as-is in a Rack-compatible environment,"
121 | err "such as Pow, Heroku or a host that supports Passenger."
122 | end
123 |
124 | task.description = "Makes a project Rack-compatible."
125 | task.category = :project
126 |
127 | task(:version) do
128 | puts "Proton #{Proton::VERSION}"
129 | end
130 |
131 | task.description = "Shows the current version"
132 | task.category = :misc
133 |
134 | task(:help) do
135 | show_help_for(params.first) and pass if params.any?
136 |
137 | show_task = Proc.new { |name, t| err " %-20s %s" % [ t.usage || name, t.description ] }
138 |
139 | err "Usage: #{executable} "
140 |
141 | unless project?
142 | err "\nCommands:"
143 | tasks_for(:create).each &show_task
144 | end
145 |
146 | if project?
147 | err "\nProject commands:"
148 | tasks_for(:project).each &show_task
149 | end
150 |
151 | if other_tasks.any?
152 | err "\nOthers:"
153 | other_tasks.each &show_task
154 | end
155 | err "\nMisc commands:"
156 | tasks_for(:misc).each &show_task
157 |
158 | unless project?
159 | err
160 | err "Get started by typing:"
161 | err " $ #{executable} create my_project"
162 | end
163 | err
164 | err "Type `#{executable} help COMMAND` for additional help on a command."
165 | end
166 |
167 | task.description = "Shows help for a given command"
168 | task.usage = "help [COMMAND]"
169 | task.category = :misc
170 |
171 | invalid do
172 | task = task(command)
173 | if task
174 | err "Invalid usage."
175 | err "Try: #{executable} #{task.usage}"
176 | err
177 | err "Type `#{executable} help` for more info."
178 | else
179 | err "Invalid command: #{command}"
180 | err "Type `#{executable} help` for more info."
181 | end
182 | end
183 |
184 | def self.run(*argv)
185 | return invoke(:version) if argv == ['-v'] || argv == ['--version']
186 | trace = (!!argv.delete('--trace'))
187 |
188 |
189 | begin
190 | super *argv
191 |
192 | rescue SyntaxError => e
193 | raise e if trace
194 | err
195 | say_error e.message.split("\n").last
196 | err
197 | say_error "You have a syntax error."
198 | say_info "Use --trace for more info."
199 |
200 | # Convert 'can't load redcloth' to a friendly 'please gem install RedCloth'
201 | rescue LoadError => e
202 | raise e if trace
203 | show_needed_gem gem_name(e)
204 |
205 | # Print generic errors as something friendlier
206 | rescue => e
207 | raise e if trace
208 |
209 | # Can't assume that HAML is always available.
210 | if Object.const_defined?(:Haml) && e.is_a?(Haml::Error)
211 | # Convert HAML's "Can't run XX filter; required 'yy'" messages
212 | # to something friendlier
213 | needed = %w(rdiscount stringio sass/plugin redcloth)
214 | needed.detect { |what| show_needed_gem(what) && true if e.message.include?(what) }
215 | else
216 | err
217 | say_error "#{e.class}: #{e.message}"
218 | say_info "#{e.backtrace.first}"
219 | err
220 | say_error "Oops! An error occured."
221 | say_info "Use --trace for more info."
222 | end
223 | end
224 | end
225 |
226 | def self.find_config_file
227 | Proton::CONFIG_FILES.inject(nil) { |a, fname| a ||= find_in_project(fname) }
228 | end
229 |
230 | def self.run!(options={})
231 | @config_file = options[:file] || find_config_file
232 | Proton::Project.new rescue nil
233 | super *[]
234 | end
235 | end
236 | end
237 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | Proton v0.3.4 - Aug 01, 2011
2 | ----------------------------
3 |
4 | ### Added:
5 | * __Speedup by at least 8x__ by implementing smarter caching.
6 |
7 | ### Fixed:
8 | * Stop adding a 'Gemfile.lock' file when doing 'proton rack'.
9 |
10 | ### Misc:
11 | * Update documentation rake tasks.
12 | * Upgrade Tilt to v1.3.2.
13 | * Use the HTML5 doctype in the default site.
14 |
15 | Proton v0.3.3 - Jul 14, 2011
16 | ----------------------------
17 |
18 | Small fixes again.
19 |
20 | ### Added:
21 | * a confirmation message when running 'proton rack'.
22 |
23 | ### Changed:
24 | * the default site to read 'Proton' instead of 'Hyde' in the default title.
25 | * Update 'proton rack' to generate Gemfile.lock.
26 |
27 | Proton v0.3.2 - Jun 22, 2011
28 | ----------------------------
29 |
30 | Hotfixes.
31 |
32 | ### Fixed:
33 | * `Gemfile` and `Gemfile.lock` are now auto ignored when doing *proton build*.
34 | * Fixed the `rel()` helper.
35 |
36 | Proton v0.3.0 - Jun 22, 2011
37 | ----------------------------
38 |
39 | **The project has been renamed to Proton** (previously called *Hyde*).
40 |
41 | A manual is also in progress, currently hosted at
42 | [sinefunc.com/hyde/manual](http://sinefunc.com/hyde/manual).
43 |
44 | ### Renamed:
45 | * * The main class is now called `Proton` instead of `Hyde`. However, *Hyde*
46 | still works as an alias.
47 | * The main executable is now called `proton` (and not `hyde`).
48 | * The configuration file is now called `Protonfile`. The legacy *hyde.conf*
49 | and *.hyderc* still works for backward-compatibility.
50 |
51 | ### Added:
52 | * New `proton rack` command to Rackify a given project.
53 |
54 | ### Changed:
55 | * Creating a project with `proton create` will now not include any gem
56 | manifest or *config.ru* file.
57 |
58 | Hyde v0.2.3 - Jun 20, 2011
59 | --------------------------
60 |
61 | ### Changed:
62 | * Use a bundler Gemfile for Hyde sites if a Gemfile is found.
63 |
64 | ### Fixed:
65 | * **Fixed `hyde create`.**
66 | * Fixed overriding of Hyde::CLI via commands not working.
67 | * Fixed the rel() helper.
68 |
69 | Hyde v0.2.2 - Jun 16, 2011
70 | --------------------------
71 |
72 | ### Added:
73 | * **Built-in Compass support.**
74 | * **Enable Sass, SCSS, Haml, Textile and Markdown by default.**
75 | * Extensions from `_extensions/*rb` will now be loaded.
76 |
77 | ### Changed:
78 | * The default `hyde.conf` now comments out the default stuff you don't need to set.
79 | * Update dependencies to Cuba 2.0, and Hashie 1.0.
80 |
81 | Hyde v0.1.14 - Jun 04, 2011
82 | ---------------------------
83 |
84 | ### Fixed:
85 | * Fixed a syntax error in page.rb.
86 | * Don't impose gem versions explicitly, and don't load Rubygems if used as a
87 | library.
88 |
89 | ### Misc:
90 | * Added some doc comments to the main classes.
91 |
92 | Hyde v0.1.13
93 | ------------
94 |
95 | - Try to fix an edge case where the path '/' gives you an arbitrary dotfile.
96 |
97 | Hyde v0.1.11
98 | ------------
99 |
100 | - Deprecate in-app caching for rack-cache.
101 | - The Hyde server now sends the Last-Modified HTTP header.
102 | - New Hyde sites will now use rack-cache (optionally) when used as a Rack site.
103 |
104 | Hyde v0.1.10
105 | -------
106 |
107 | - New Hyde sites will now have a gems manifest. This means you can
108 | push your Hyde sites to Heroku instantly.
109 | - Add cuba to dependencies.
110 | - The server now serves the right MIME types.
111 | - When used as a Rack app, Hyde sends cache headers.
112 |
113 | Hyde v0.1.9
114 | -----------
115 |
116 | - Hotfix: tilt_build_options should work even if there was no tilt_options specified.
117 |
118 | Hyde v0.1.8
119 | -----------
120 |
121 | - Fix: 404 pages in 'hyde start' no longer throws an exception log to the viewer.
122 | - Implemented the `content_for` and `yield_content` helpers.
123 | - Fix partial locals not working.
124 | - Allow having the same layouts/partials path as the site.
125 | - Implement `Hyde.project` which returns the latest project. (deprecates $project)
126 | - Support tilt_build_options in the config.
127 |
128 | Hyde v0.1.7
129 | -----------
130 |
131 | - Show friendlier error messages.
132 | - Show steps on how to install missing gems when any are encountered.
133 | - Change 'hyde build' display format to look better.
134 |
135 | Hyde v0.1.6
136 | -----------
137 |
138 | - Fix an edge case where files containing '---' somewhere is treated wrong.
139 | - Ruby 1.8 compatibility.
140 |
141 | Hyde v0.1.4
142 | -----------
143 |
144 | - Fix: `hyde start` was giving errors.
145 |
146 | Hyde v0.1.3
147 | -----------
148 |
149 | - .html files are now being treated as .erb.
150 | - Implement `page.children.find`.
151 | - Implement `page.children.except`.
152 | - Fix #children and sorting giving errors.
153 | - Fix #siblings.
154 | - Revise the 'no config file found' error message.
155 | - Allow `.hyderc` as a filename.
156 | - Add help for `hyde help start`.
157 | - Support `hyde start -D` which is a very hackish solution to have
158 | Hyde start as a daemon.
159 |
160 | Hyde v0.1.2
161 | -----------
162 |
163 | - Allow `hyde create .` to add a hyde.conf in the current folder.
164 | - Revamp the help screen.
165 | - Fix: change the default load path for Sass/SCSS to 'css'.
166 | - Add Page#depth.
167 | - Fix Page#breadcrumbs.
168 | - Fix Page#parent.
169 | - Add the `rel` helper.
170 | - Generated Hyde projects are now Rack-compatible.
171 |
172 | Hyde v0.1.1
173 | -----------
174 |
175 | - Default project is now simpler. The site_path is `.`.
176 | - Implement `Project#build`.
177 | - If YAML parsing of page metadata fails, treat it as content.
178 | - All options in `hyde.conf` are now optional (even `hyde_requirement`).
179 | - Page metadata can now only be a hash.
180 | - Fix `hyde start`.
181 | - Minimum Ruby version is now at 1.8.6.
182 |
183 | Hyde v0.1.0
184 | -----------
185 |
186 | **Complete rewrite.** Many thing have been deprecated.
187 |
188 | - Now uses Tilt (for templates), Shake (for CLI) and Cuba (for the server).
189 | - Now supports everything Tilt supports: CoffeeScript, Liquid, etc.
190 | - Allow `tilt_options` in hyde.conf.
191 | - Old extensions will be broken (but who made any yet, anyway?)
192 | - Update the `hyde create` template's gitignore file to account for _public.
193 |
194 | Hyde v0.0.8
195 | -----------
196 |
197 | - Add support for subclassing. (just add a 'type' meta)
198 | - Implement Project#all.
199 | - Implement Page#all and Page#all(type).
200 | - Fix binary files losing extensions on 'hyde build'.
201 | - Allow "layout: false" to ensure that a page doesn't have a layout.
202 | - Fix bug where "index.html.haml" and "index.rss.haml" clash.
203 | - Implement Page#content.
204 |
205 | Hyde v0.0.7
206 | -----------
207 |
208 | - Add support for Sass and SCSS.
209 | - Add support for ignored files.
210 |
211 | Hyde v0.0.6
212 | -----------
213 |
214 | - Added support for blocks for `yield_content` (as default text).
215 | - `Page#referrer` now is a page (instead of a string of the name).
216 | - Partials path is now not ignored by default.
217 | - Add support for page ordering (by the `position` key in metadata).
218 | - You can now start an IRB session with the `hyde console` command.
219 | - Implement traversion methods for page: #next, #previous, #siblings,
220 | and #parent.
221 | - Implement the 'page' variable to be available in pages.
222 | - Add Page#title.
223 | - Add Page#path.
224 | - Add Page#breadcrumbs.
225 | - Implement Utils#escape_html.
226 | - Hyde now always tries 'layouts/default.*' as the default layout, if none's
227 | specified. (TODO: layout: false)
228 | - Implement Renderer.layoutable?, which dictates if a renderer is capable of
229 | having a layout.
230 |
231 | Hyde v0.0.5 - 2010-05-30
232 | ------------------------
233 |
234 | - Implemented `content_for` and `yield_content` helpers
235 | - Added `partials_path` config variable
236 | - Changed helper method `partial`s syntax (from `partial X, :locals => { ... }` to `partial X, ...`)
237 | - Line numbers for errors are shown now
238 | - Added rudimentary 404 page
239 | - Added `hyde_requirement` config variable -- Hyde will now not proceed if the project needs a later version of Hyde
240 | - Extensions are now auto-guessed (for example, 'foo.less' will be accessible as 'foo.css')
241 |
242 | Hyde v0.0.4 - 2010-05-25
243 | ------------------------
244 |
245 | - First public release
246 |
--------------------------------------------------------------------------------
/lib/proton/page.rb:
--------------------------------------------------------------------------------
1 | class Proton
2 | # Class: Proton::Page
3 | # A page.
4 | #
5 | # ## Common usage
6 | #
7 | # Getting pages from paths:
8 | #
9 | # # Feed it a URL path, not a filename.
10 | # page = Proton::Page['/index.html'] # uses Proton.project
11 | # page = Proton::Page['/index.html', project]
12 | #
13 | # Getting pages from files:
14 | #
15 | # # Feed it a file name, not a URL path.
16 | # # Also, this does no sanity checks.
17 | # page = Proton::Page.new('/home/rsc/index.html', project)
18 | #
19 | # page.exists?
20 | # page.valid?
21 | #
22 | # Paths:
23 | #
24 | # page.filepath #=> "index.haml" -- path in the filesystem
25 | # page.path #=> "/index.html" -- path as a RUL
26 | #
27 | # Meta:
28 | #
29 | # page.meta #=> OpenStruct of the metadata
30 | # page.title #=> "Welcome to my site!"
31 | #
32 | # page.layout # Proton::Layout or nil
33 | # page.layout?
34 | #
35 | # Types:
36 | #
37 | # page.html?
38 | # page.mime_type #=> "text/html" or nil -- only for tilt? == true
39 | # page.default_ext #=> "html"
40 | #
41 | # Contents:
42 | #
43 | # page.to_html
44 | # page.to_html(locals={})
45 | # page.content
46 | # page.markup
47 | #
48 | # Traversion:
49 | #
50 | #
51 | # # Pages (a Proton::Page or nil)
52 | # page.parent
53 | # page.next
54 | #
55 | # # Sets (a Proton::Set)
56 | # page.children
57 | # page.siblings
58 | # page.breadcrumbs
59 | #
60 | # # Misc
61 | # page.index? # if it's an index.html
62 | # page.parent?
63 | # page.root? # true if no parents
64 | # page.depth
65 | #
66 | # Tilt:
67 | #
68 | # page.tilt? # true, if it's a dynamic file
69 | # page.tilt_engine_name #=> 'RedCloth'
70 | #
71 | # Building:
72 | #
73 | # page.write
74 | # page.write('~/foo.html')
75 | #
76 | class Page
77 | # Attribute: project (Proton::Page)
78 | # A reference to the project.
79 | #
80 | attr_reader :project
81 |
82 | # Attribute: file (Proton::Page)
83 | # The full path of the source file.
84 | #
85 | # ## Example
86 | # page.filepath #=> "/index.haml"
87 | # page.file #=> "/home/rsc/project/index.haml"
88 | #
89 | # ## See also
90 | # - {Proton::Page.filepath}
91 | #
92 | attr_reader :file
93 |
94 | def self.[](id, project=Proton.project)
95 | Cacheable.cache(:lookup, id, project.root) do
96 | site_path = root_path(project)
97 | return nil if site_path.nil?
98 |
99 | site = lambda { |*x| File.join site_path, *(x.compact) }
100 | try = lambda { |_id| p = new(_id, project); p if p.exists? }
101 |
102 | # For paths like '/' or '/hello/'
103 | nonfile = File.basename(id).gsub('/','').empty?
104 |
105 | # Account for:
106 | # ~/mysite/site/about/us.html.haml
107 | # about/us.html.haml => ~/mysite/site/about/us.html.haml
108 | # about/us.html => ~/mysite/site/about/us.html.*
109 | # about/us.html => ~/mysite/site/about/us.*
110 | # about/us => ~/mysite/site/about/us/index.*
111 | #
112 | page = try[id]
113 | page ||= try[site[id]]
114 | unless nonfile
115 | page ||= try[Dir[site["#{id}.*"]].first]
116 | page ||= try[Dir[site["#{id.to_s.sub(/\.[^\.]*/,'')}.*"]].first]
117 | end
118 | page ||= try[Dir[site[id, "index.*"]].first]
119 |
120 | # Subclass
121 | if page && page.tilt? && page.meta[:type]
122 | klass = Page.get_type(page.meta[:type])
123 | raise Error, "#{page.filepath}: Class for type '#{page.meta[:type]}' not found" unless klass
124 | page = klass.new(id, project)
125 | end
126 |
127 | page
128 | end
129 | end
130 |
131 | def initialize(file, project=Proton.project)
132 | @file = File.expand_path(file) if file.is_a?(String)
133 | @project = project
134 | raise Error if project.nil?
135 | end
136 |
137 | # Returns the URL path for a page.
138 | def path
139 | path = @file.sub(File.expand_path(root_path), '')
140 |
141 | # if xx.haml (but not xx.html.haml),
142 | if tilt?
143 | path = path.sub(/\.[^\.]*$/, "")
144 | path += ".#{default_ext}" unless File.basename(path).include?('.')
145 | end
146 |
147 | path
148 | end
149 |
150 | # Attribute: filepath (Proton::Page)
151 | # Returns a short filepath relative to the project path.
152 | #
153 | # ## Description
154 | # This is different from {Proton::Page.file} as this only returns the
155 | # path relative to the project's root instead of an absolute path.
156 | #
157 | # ## Example
158 | # See {Proton::Page.file} for an example.
159 | #
160 | def filepath
161 | root = project.root
162 | fpath = file
163 | fpath = fpath[root.size..-1] if fpath[0...root.size] == root
164 | fpath
165 | end
166 |
167 | # Attribute: title (Proton::Page)
168 | # Returns the page title as a string.
169 | #
170 | # ## Description
171 | # This attribute tries to infer the page's title based on metadata. If the
172 | # `title` key is not in the page's header metadata, then it returns the
173 | # path name instead.
174 | #
175 | # This is also aliased as `to_s`.
176 | #
177 | def title
178 | (meta.title if tilt?) || path
179 | end
180 |
181 | alias to_s title
182 |
183 | def position
184 | meta[:position] || title
185 | end
186 |
187 | def <=>(other)
188 | result = self.position <=> other.position
189 | result ||= self.position.to_s <=> other.position.to_s
190 | result
191 | end
192 |
193 | # Method: html? (Proton::Page)
194 | # Returns true if the page is an HTML page.
195 |
196 | def html?
197 | mime_type == 'text/html'
198 | end
199 |
200 | # Attribute: mime_type (Proton::Page)
201 | # The MIME type for the page, based on what template engine was used.
202 | #
203 | # ## Example
204 | # Page['/style.css'].mime_type #=> 'text/css'
205 | # Page['/index.html'].mime_type #=> 'text/html'
206 | #
207 | # ## See also
208 | # - {Proton::Page::default_ext}
209 | #
210 | def mime_type
211 | return nil unless tilt?
212 |
213 | mime = nil
214 | mime = tilt_engine.default_mime_type if tilt_engine.respond_to?(:default_mime_type)
215 |
216 | mime ||= case tilt_engine.name
217 | when 'Tilt::SassTemplate' then 'text/css'
218 | when 'Tilt::ScssTemplate' then 'text/css'
219 | when 'Tilt::LessTemplate' then 'text/css'
220 | when 'Tilt::CoffeeScriptTemplate' then 'application/javascript'
221 | when 'Tilt::NokogiriTemplate' then 'text/xml'
222 | when 'Tilt::BuilderTemplate' then 'text/xml'
223 | else 'text/html'
224 | end
225 | end
226 |
227 | # Attribute: default_ext (Proton::Page)
228 | # Returns a default extension for the page based on the page's MIME type.
229 | #
230 | # ## Example
231 | # Page['/style.css'].default_ext #=> 'css'
232 | # Page['/index.html'].default_ext #=> 'html'
233 | #
234 | # ## See also
235 | # - {Proton::Page::mime_type}
236 |
237 | def default_ext
238 | case mime_type
239 | when 'text/html' then 'html'
240 | when 'text/css' then 'css'
241 | when 'text/xml' then 'xml'
242 | when 'application/javascript' then 'js'
243 | end
244 | end
245 |
246 | # Method: get_type (Proton::Page)
247 | # Returns a page subtype.
248 | #
249 | # ## Example
250 | # Page.get_type('post') => Proton::Page::Post
251 |
252 | def self.get_type(type)
253 | type = type.to_s
254 | klass = type[0..0].upcase + type[1..-1].downcase
255 | klass = klass.to_sym
256 | self.const_get(klass) if self.const_defined?(klass)
257 | end
258 |
259 | def exists?
260 | @file and File.file?(@file||'') and valid?
261 | end
262 |
263 | # Ensures that the page is in the right folder.
264 | def valid?
265 | prefix = File.expand_path(root_path)
266 | prefix == File.expand_path(@file)[0...prefix.size]
267 | end
268 |
269 | def content(locals={}, tilt_options={}, &blk)
270 | return markup unless tilt?
271 | tilt(tilt_options).render(dup.extend(Helpers), locals, &blk)
272 | end
273 |
274 | # Method: to_html (Proton::Page)
275 | # Returns the full HTML document for the page.
276 | #
277 | def to_html(locals={}, tilt_options={}, &blk)
278 | html = content(locals, tilt_options, &blk)
279 | html = layout.to_html(locals, tilt_options) { html } if layout?
280 | html
281 | end
282 |
283 | def layout
284 | layout = meta.layout
285 | layout ||= default_layout unless meta.layout == false
286 | Layout[layout, page] if layout
287 | end
288 |
289 | def page
290 | self
291 | end
292 |
293 | def layout?
294 | !! layout
295 | end
296 |
297 | # Method: meta (Proton::Page)
298 | # Returns the metadata for the page.
299 | #
300 | # ## Description
301 | # This returns an instance of {Proton::Meta}.
302 | #
303 | def meta
304 | @meta ||= Meta.new(parts.first)
305 | end
306 |
307 | # Method: write (Proton::Page)
308 | # Writes to the given output file.
309 | #
310 | # ## Description
311 | # This is the method used by `proton build`.
312 | #
313 | def write(out=nil)
314 | out ||= project.path(:output, path)
315 | FileUtils.mkdir_p File.dirname(out)
316 |
317 | if tilt?
318 | File.open(out, 'w') { |f| f.write to_html({}, :build => true) }
319 | else
320 | FileUtils.cp file, out
321 | end
322 | end
323 |
324 | # Method: tilt? (Proton::Page)
325 | # Checks if the file is supported by tilt.
326 | #
327 | def tilt?
328 | !! tilt_engine
329 | end
330 |
331 | # Attribute: tilt_engine (Proton::Page)
332 | # Returns the Tilt engine (eg Tilt::HamlEngine).
333 | def tilt_engine
334 | Tilt[@file]
335 | end
336 |
337 | def tilt_engine_name
338 | tilt_engine.name.match(/:([^:]*)(?:Template?)$/)[1]
339 | end
340 |
341 | # Attribute: tilt (Proton::Page)
342 | # Returns the tilt layout.
343 | #
344 | # This returns an instance of `Tilt`.
345 | #
346 | def tilt(tilt_options={})
347 | if tilt?
348 | parts
349 | # HAML options and such (like :escape_html)
350 | options = project.config.tilt_options_for(@file, tilt_options)
351 | offset = @offset || 1
352 | Tilt.new(@file, offset, options) { markup }
353 | end
354 | end
355 |
356 | def markup
357 | parts.last
358 | end
359 |
360 | def method_missing(meth, *args, &blk)
361 | super unless meta.instance_variable_get(:@table).keys.include?(meth.to_sym)
362 | meta.send(meth)
363 | end
364 |
365 | # Attribute: parent (Proton::Page)
366 | # Returns the page's parent page, or nil.
367 | #
368 | # ## Usage
369 | # page.parent
370 | #
371 | # ## Description
372 | # This will return the page's parent (also a {Proton::Page} instance), or
373 | # `nil` if it's the page is already the root.
374 | #
375 | def parent
376 | parts = path.split('/') # ['', 'about', 'index.html']
377 |
378 | try = lambda { |newpath| p = self.class[newpath, project]; p if p && p.path != path }
379 |
380 | # Absolute root
381 | return nil if index? and parts.size <= 2
382 |
383 | parent = try[parts[0...-1].join('/')] # ['','about'] => '/about'
384 | parent ||= try['/'] # Home
385 | end
386 |
387 | # Method: children (Proton::Page)
388 | # Returns a Set of the page's subpages.
389 | #
390 | def children
391 | files = if index?
392 | # about/index.html => about/*
393 | File.expand_path('../*', @file)
394 | else
395 | # products.html => products/*
396 | base = File.basename(@file, '.*')
397 | File.expand_path("../#{base}/*", @file)
398 | end
399 |
400 | Set.new Dir[files].
401 | reject { |f| f == @file || project.ignored_files.include?(f) }.
402 | map { |f| self.class[f, project] }.
403 | compact.sort
404 | end
405 |
406 | # Method: siblings (Proton::Page)
407 | # Returns a Set of pages that share the same parent as the current page.
408 | #
409 | def siblings
410 | pages = (p = parent and p.children)
411 | return Set.new unless pages
412 | return Set.new unless pages.include?(self)
413 | Set.new(pages)
414 | end
415 |
416 | # Attribute: breadcrumbs (Proton::Page)
417 | # Returns an array of the page's ancestors, including itself.
418 | #
419 | # ## Example
420 | # Proton::Page['/about/company/contact.html'].breadcrumbs
421 | #
422 | # May look like:
423 | # [ Page, Page, Page ]
424 | #
425 | def breadcrumbs
426 | Set.new(parent? ? (parent.breadcrumbs + [self]) : [self])
427 | end
428 |
429 | # Method: index? (Proton::Page)
430 | # Returns true if the page is and index page.
431 | #
432 | def index?
433 | File.basename(path, '.*') == 'index'
434 | end
435 |
436 | # Method: parent? (Proton::Page)
437 | # Returns true if the page has a parent.
438 | #
439 | # ## Description
440 | # This is the opposite of {Proton::Page::root?}.
441 | #
442 | # ## See also
443 | # - {Proton::Page::root?}
444 |
445 | def parent?
446 | !parent.nil?
447 | end
448 |
449 | # Method: root? (Proton::Page)
450 | # Returns true if the page is the home page.
451 | #
452 | # ## Description
453 | # This is the opposite of {Proton::Page::parent?}.
454 | #
455 | # ## See also
456 | # - {Proton::Page::parent?}
457 |
458 | def root?
459 | parent.nil?
460 | end
461 |
462 | # Attribute: depth (Proton::Page)
463 | # Returns how deep the page is in the heirarchy.
464 | #
465 | # ## Description
466 | # This counts the number of pages from the root page. This means:
467 | #
468 | # * The root page (eg, `/index.html`) has a depth of `1`
469 | # * A child page of the root (eg, `/about.html`) has a depth of `2`
470 | # * A child of that (eg, `/about/company.html`) has a depth of `3`
471 | # * ...and so on
472 | #
473 | #
474 | def depth
475 | breadcrumbs.size
476 | end
477 |
478 | def next
479 | page = self
480 | while true do
481 | page.siblings.index(self)
482 | end
483 | end
484 |
485 | def ==(other)
486 | self.path == other.path
487 | end
488 |
489 | def inspect
490 | "<##{self.class.name} #{path.inspect}>"
491 | end
492 |
493 | protected
494 |
495 | # Method: default_layout (Proton::Page)
496 | # Returns the default layout.
497 | #
498 | # This method may be overridden by subclasses as needed.
499 |
500 | def default_layout
501 | 'default' if html?
502 | end
503 |
504 | # Returns the two parts of the markup.
505 | def parts
506 | @parts ||= begin
507 | t = File.open(@file).read
508 | t = t.force_encoding('UTF-8') if t.respond_to?(:force_encoding)
509 | m = t.match(/^(.*?)\n--+\n(.*)$/m)
510 |
511 | if m.nil?
512 | [{}, t]
513 | else
514 | @offset = m[1].count("\n") + 2
515 | data = YAML::load(m[1])
516 | raise ArgumentError unless data.is_a?(Hash)
517 | [data, m[2]]
518 | end
519 | rescue ArgumentError
520 | [{}, t]
521 | end
522 | end
523 |
524 | extend Cacheable
525 | cache_method :children, :siblings, :parent, :next, :breadcrumbs, :path, :tilt
526 |
527 | def self.root_path(project, *a)
528 | project.path(:site, *a)
529 | end
530 |
531 | def root_path(*a)
532 | self.class.root_path(project, *a)
533 | end
534 | end
535 | end
536 |
--------------------------------------------------------------------------------