magneto 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2012 Don Melton
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,44 @@
1
+ # Magneto
2
+
3
+ Magneto is a static site generator.
4
+
5
+ ## About
6
+
7
+ Hi, I'm Don Melton. I wrote Magneto to generate [my own website](http://donmelton.com/). Magneto was inspired by [nanoc](http://nanoc.stoneship.org/) and [Jekyll](http://jekyllrb.com/), but it's a much simpler tool with fewer features and less policy.
8
+
9
+ For example, Magneto is not "blog aware" like some other systems, but it allows you to write a site controller script and plugins which can easily generate blog posts, an index page and a RSS feed. This is how I use it. There may be more work up front compared to other tools, but Magneto gives you very precise control over its behavior and output.
10
+
11
+ Before using Magneto, realize that it does have limitations due to its simplicity and that its programming interface may change because it's still under development.
12
+
13
+ ## Usage
14
+
15
+ magneto [OPTION]...
16
+
17
+ Source file items, their templates and the site controller script are loaded from the `items` and `templates` directories and from the `script.rb` file, all within the current directory. These are watched for changes and reloaded when automatic regeneration is enabled.
18
+
19
+ Ruby library files are loaded from the `plugins` directory only once.
20
+
21
+ The generated site is written to the `output` directory.
22
+
23
+ Configuration is loaded from `config.yaml` but can be overriden using the following options:
24
+
25
+ -c, --config PATH use specific YAML configuration file
26
+ -s, --source PATH use specific source directory
27
+ -o, --output PATH use specific output directory
28
+
29
+ --[no-]hidden include [exclude] hidden source files
30
+ --[no-]remove remove [keep] obsolete output
31
+ --[no-]auto enable [disable] automatic regeneration
32
+
33
+ -h, --help display this help and exit
34
+ --version output version information and exit
35
+
36
+ ## Dependencies
37
+
38
+ Magneto doesn't have any dependencies for basic operation.
39
+
40
+ Enabling automatic regeneration requires installation of the Directory Watcher gem:
41
+
42
+ sudo gem install directory_watcher
43
+
44
+ Using any of the built-in filters could require additional gem installations.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+
5
+ require 'magneto'
6
+
7
+ Magneto.application.run
@@ -0,0 +1,30 @@
1
+ require 'English'
2
+ require 'fileutils'
3
+ require 'optparse'
4
+ require 'rubygems'
5
+ require 'set'
6
+ require 'yaml'
7
+
8
+ require 'magneto/application'
9
+ require 'magneto/context'
10
+ require 'magneto/core_ext'
11
+ require 'magneto/filter'
12
+ require 'magneto/filters'
13
+ require 'magneto/item'
14
+ require 'magneto/readable'
15
+ require 'magneto/render_context'
16
+ require 'magneto/script_context'
17
+ require 'magneto/site'
18
+ require 'magneto/template'
19
+
20
+ module Magneto
21
+
22
+ VERSION = '0.1.0'
23
+
24
+ class << self
25
+
26
+ def application
27
+ @application ||= Application.new
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,153 @@
1
+ module Magneto
2
+
3
+ class Application
4
+
5
+ attr_reader :config, :filters, :site
6
+
7
+ def initialize
8
+ super
9
+ @options = {}
10
+ @config_path = 'config.yaml'
11
+ @config = {
12
+ :source_path => '.',
13
+ :output_path => 'output',
14
+ :hidden_files => false,
15
+ :remove_obsolete => false,
16
+ :auto_regeneration => false
17
+ }
18
+ @filters = {}
19
+ @site = nil
20
+ end
21
+
22
+ def run
23
+ parse_options
24
+ load_configuration
25
+
26
+ @config.deep_merge! @options
27
+
28
+ [:source_path, :output_path].each do |path|
29
+ @config[path] = File.expand_path(@config[path])
30
+ @config[path].freeze
31
+ end
32
+
33
+ load_plugins
34
+ find_filters
35
+
36
+ @site = Site.new(@config, @filters)
37
+
38
+ if @config[:auto_regeneration]
39
+ puts 'Automatic regeneration enabled.'
40
+
41
+ require 'directory_watcher'
42
+
43
+ dw = DirectoryWatcher.new(@config[:source_path])
44
+ dw.glob = [@config[:hidden_files] ? 'items/**/{.[^.],}*' : 'items/**/*', 'templates/*.*', 'script.rb']
45
+ dw.interval = 1
46
+ dw.add_observer { |*args| @site.generate }
47
+ dw.start
48
+ gets
49
+ dw.stop
50
+ else
51
+ @site.generate
52
+ end
53
+
54
+ exit
55
+ end
56
+
57
+ private
58
+
59
+ def parse_options
60
+ begin
61
+ OptionParser.new do |opts|
62
+ opts.banner = "Magneto is a static site generator."
63
+ opts.separator ""
64
+ opts.separator " Usage: #{File.basename($PROGRAM_NAME)} [OPTION]..."
65
+ opts.separator ""
66
+ opts.separator " Source file items, their templates and the site controller script"
67
+ opts.separator " are loaded from the 'items' and 'templates' directories and from the"
68
+ opts.separator " 'script.rb' file, all within the current directory. These are watched"
69
+ opts.separator " for changes and reloaded when automatic regeneration is enabled."
70
+ opts.separator ""
71
+ opts.separator " Ruby library files are loaded from the 'plugins' directory only once."
72
+ opts.separator ""
73
+ opts.separator " The generated site is written to the 'output' directory."
74
+ opts.separator ""
75
+ opts.separator " Configuration is loaded from 'config.yaml' but can be overriden"
76
+ opts.separator " using the following options:"
77
+ opts.separator ""
78
+
79
+ opts.on('-c', '--config PATH', 'use specific YAML configuration file') { |cp| @config_path = cp }
80
+ opts.on('-s', '--source PATH', 'use specific source directory') { |sp| @options[:source_path] = sp }
81
+ opts.on('-o', '--output PATH', 'use specific output directory') { |op| @options[:output_path] = op }
82
+
83
+ opts.separator ""
84
+
85
+ opts.on('--[no-]hidden', 'include [exclude] hidden source files') { |hf| @options[:hidden_files] = hf }
86
+ opts.on('--[no-]remove', 'remove [keep] obsolete output') { |ro| @options[:remove_obsolete] = ro }
87
+ opts.on('--[no-]auto', 'enable [disable] automatic regeneration') { |ar| @options[:auto_regeneration] = ar }
88
+
89
+ opts.separator ""
90
+
91
+ opts.on_tail('-h', '--help', 'display this help and exit') do
92
+ puts opts
93
+ exit
94
+ end
95
+
96
+ opts.on_tail '--version', 'output version information and exit' do
97
+ puts <<VERSION_HERE
98
+ #{File.basename($PROGRAM_NAME)} #{VERSION}
99
+ Copyright (c) 2012 Don Melton
100
+ This is free software; see the source for copying conditions. There is NO
101
+ warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
102
+ VERSION_HERE
103
+ exit
104
+ end
105
+ end.parse!
106
+ rescue => ex
107
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
108
+ usage
109
+ end
110
+
111
+ if ARGV.size > 0
112
+ $stderr.print "#{File.basename($PROGRAM_NAME)}: unknown argument(s):"
113
+ ARGV.each { |a| $stderr.print " #{a}" }
114
+ $stderr.puts
115
+ usage
116
+ end
117
+ end
118
+
119
+ def usage
120
+ $stderr.puts "Try '#{File.basename($PROGRAM_NAME)} --help' for more information."
121
+ exit false
122
+ end
123
+
124
+ def load_configuration
125
+ puts 'Loading configuration...'
126
+
127
+ begin
128
+ configuration = YAML.load_file(@config_path)
129
+ raise unless configuration.is_a? Hash
130
+ rescue => ex
131
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
132
+ $stderr.puts "WARNING: Couldn't load configuration. Using defaults (and options)."
133
+ configuration = {}
134
+ end
135
+
136
+ @config.deep_merge! configuration.symbolize_keys
137
+ end
138
+
139
+ def load_plugins
140
+ puts 'Loading plugins...'
141
+ Dir[@config[:source_path] + '/plugins/**/*.rb'].each { |plugin| require plugin unless File.directory? plugin }
142
+ end
143
+
144
+ def find_filters
145
+ puts 'Finding filters...'
146
+
147
+ Magneto::Filter.subclasses.each do |subclass|
148
+ filter = subclass.new(@config)
149
+ @filters[filter.name.to_sym] = filter
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,21 @@
1
+ module Magneto
2
+
3
+ # Adapted from `nanoc/base/context.rb` with thanks to Denis Defreyne and
4
+ # contributors.
5
+ class Context
6
+
7
+ def initialize(ivars)
8
+ super()
9
+ eigenclass = class << self ; self ; end
10
+
11
+ ivars.each do |symbol, value|
12
+ instance_variable_set('@' + symbol.to_s, value)
13
+ eigenclass.send(:define_method, symbol) { value } unless eigenclass.send(:respond_to?, symbol)
14
+ end
15
+ end
16
+
17
+ def get_binding
18
+ binding
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,58 @@
1
+ class Hash
2
+
3
+ # Copied from `active_support/core_ext/hash/keys.rb` with thanks to David
4
+ # Heinemeier Hansson and contributors.
5
+
6
+ # Return a new hash with all keys converted to strings.
7
+ def stringify_keys
8
+ dup.stringify_keys!
9
+ end
10
+
11
+ # Destructively convert all keys to strings.
12
+ def stringify_keys!
13
+ keys.each do |key|
14
+ self[key.to_s] = delete(key)
15
+ end
16
+ self
17
+ end
18
+
19
+ # Return a new hash with all keys converted to symbols, as long as
20
+ # they respond to +to_sym+.
21
+ def symbolize_keys
22
+ dup.symbolize_keys!
23
+ end
24
+
25
+ # Destructively convert all keys to symbols, as long as they respond
26
+ # to +to_sym+.
27
+ def symbolize_keys!
28
+ keys.each do |key|
29
+ self[(key.to_sym rescue key) || key] = delete(key)
30
+ end
31
+ self
32
+ end
33
+
34
+ # Adapted from `jekyll/core_ext.rb` with thanks to Tom Preston-Werner.
35
+
36
+ # Merges self with another hash, recursively.
37
+ #
38
+ # This code was lovingly stolen (now adapted) from some random gem:
39
+ # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
40
+ #
41
+ # Thanks to whoever made it.
42
+ def deep_merge(hash)
43
+ dup.deep_merge! hash
44
+ end
45
+
46
+ def deep_merge!(hash)
47
+ hash.keys.each do |key|
48
+ if hash[key].is_a? Hash and self[key].is_a? Hash
49
+ self[key] = self[key].deep_merge(hash[key])
50
+ next
51
+ end
52
+
53
+ self[key] = hash[key]
54
+ end
55
+
56
+ self
57
+ end
58
+ end
@@ -0,0 +1,21 @@
1
+ module Magneto
2
+
3
+ # Adapted from `jekyll/plugin.rb` with thanks to Tom Preston-Werner.
4
+ class Filter
5
+
6
+ class << self
7
+
8
+ def inherited(subclass)
9
+ subclasses << subclass
10
+ end
11
+
12
+ def subclasses
13
+ @subclasses ||= []
14
+ end
15
+ end
16
+
17
+ def initialize(config)
18
+ @config = config
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ require 'magneto/filters/bluecloth'
2
+ require 'magneto/filters/coffeescript'
3
+ require 'magneto/filters/erb'
4
+ require 'magneto/filters/erubis'
5
+ require 'magneto/filters/haml'
6
+ require 'magneto/filters/kramdown'
7
+ require 'magneto/filters/less'
8
+ require 'magneto/filters/maruku'
9
+ require 'magneto/filters/rdiscount'
10
+ require 'magneto/filters/redcarpet'
11
+ require 'magneto/filters/redcloth'
12
+ require 'magneto/filters/rubypants'
13
+ require 'magneto/filters/sass'
@@ -0,0 +1,22 @@
1
+ module Magneto
2
+
3
+ class BlueClothFilter < Filter
4
+
5
+ def name
6
+ 'bluecloth'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'bluecloth'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use BlueCloth. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install bluecloth'
16
+ raise 'Missing dependency: bluecloth'
17
+ end
18
+
19
+ BlueCloth.new(content, (ivars[:bluecloth].symbolize_keys rescue {})).to_html
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module Magneto
2
+
3
+ class CoffeeScriptFilter < Filter
4
+
5
+ def name
6
+ 'coffeescript'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'coffee-script'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use CoffeeScript. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install coffee-script'
16
+ raise 'Missing dependency: coffee-script'
17
+ end
18
+
19
+ CoffeeScript.compile(content, (ivars[:coffeescript].symbolize_keys rescue {}))
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module Magneto
2
+
3
+ class ERBFilter < Filter
4
+
5
+ def name
6
+ 'erb'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ require 'erb'
11
+
12
+ # Adapted from `nanoc/filters/erb.rb` with thanks to Denis Defreyne and
13
+ # contributors.
14
+ context = RenderContext.new(ivars)
15
+ proc = ivars[:content] ? lambda { ivars[:content] } : lambda {}
16
+ b = context.get_binding(&proc)
17
+
18
+ args = ivars[:erb].symbolize_keys rescue {}
19
+ ERB.new(content, args[:safe_level], args[:trim_mode]).result b
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ module Magneto
2
+
3
+ class ErubisFilter < Filter
4
+
5
+ def name
6
+ 'erubis'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'erubis'
12
+ require 'erubis/engine/enhanced'
13
+ rescue LoadError => ex
14
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
15
+ $stderr.puts "You're missing a library required to use Erubis. Try running:"
16
+ $stderr.puts ' $ [sudo] gem install erubis'
17
+ raise 'Missing dependency: erubis'
18
+ end
19
+
20
+ # Adapted from "nanoc/filters/erubis.rb" with thanks to Denis Defreyne and contributors.
21
+ context = RenderContext.new(ivars)
22
+ proc = ivars[:content] ? lambda { ivars[:content] } : lambda {}
23
+ b = context.get_binding(&proc)
24
+
25
+ Erubis::ErboutEruby.new(content, (ivars[:erubis].symbolize_keys rescue {})).result b
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ module Magneto
2
+
3
+ class HamlFilter < Filter
4
+
5
+ def name
6
+ 'haml'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'haml'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use Haml. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install haml'
16
+ raise 'Missing dependency: haml'
17
+ end
18
+
19
+ # Adapted from "nanoc/filters/haml.rb" with thanks to Denis Defreyne and contributors.
20
+ context = RenderContext.new(ivars)
21
+ proc = ivars[:content] ? lambda { ivars[:content] } : lambda {}
22
+
23
+ Haml::Engine.new(content, (ivars[:haml].symbolize_keys rescue {})).render(context, ivars, &proc)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ module Magneto
2
+
3
+ class KramdownFilter < Filter
4
+
5
+ def name
6
+ 'kramdown'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'kramdown'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use kramdown. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install kramdown'
16
+ raise 'Missing dependency: kramdown'
17
+ end
18
+
19
+ Kramdown::Document.new(content, (ivars[:kramdown].symbolize_keys rescue {})).to_html
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module Magneto
2
+
3
+ class LessFilter < Filter
4
+
5
+ def name
6
+ 'less'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'less'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use Less. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install less'
16
+ raise 'Missing dependency: less'
17
+ end
18
+
19
+ args = ivars[:less].symbolize_keys rescue {}
20
+ Less::Parser.new(args).parse(content).to_css(args)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ module Magneto
2
+
3
+ class MarukuFilter < Filter
4
+
5
+ def name
6
+ 'maruku'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'maruku'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use Maruku. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install maruku'
16
+ raise 'Missing dependency: maruku'
17
+ end
18
+
19
+ Maruku.new(content, (ivars[:maruku].symbolize_keys rescue {})).to_html
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module Magneto
2
+
3
+ class RDiscountFilter < Filter
4
+
5
+ def name
6
+ 'rdiscount'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'rdiscount'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use RDiscount. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install rdiscount'
16
+ raise 'Missing dependency: rdiscount'
17
+ end
18
+
19
+ RDiscount.new(content, *((ivars[:rdiscount].symbolize_keys rescue {})[:extensions] || [])).to_html
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ module Magneto
2
+
3
+ class RedcarpetFilter < Filter
4
+
5
+ def name
6
+ 'redcarpet'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'redcarpet'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use Redcarpet. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install redcarpet'
16
+ raise 'Missing dependency: redcarpet'
17
+ end
18
+
19
+ args = ivars[:redcarpet].symbolize_keys rescue {}
20
+ renderer_class = Kernel.const_get(args[:renderer_name]) rescue args[:renderer_class] || Redcarpet::Render::HTML
21
+ args.delete(:renderer_name)
22
+ args.delete(:renderer_class)
23
+ renderer_options = args[:renderer_options].symbolize_keys rescue {}
24
+ args.delete(:renderer_options)
25
+
26
+ Redcarpet::Markdown.new(renderer_class.new(renderer_options), args).render(content)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ module Magneto
2
+
3
+ class RedClothFilter < Filter
4
+
5
+ def name
6
+ 'redcloth'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'redcloth'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use RedCloth. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install RedCloth'
16
+ raise 'Missing dependency: RedCloth'
17
+ end
18
+
19
+ textile_doc = RedCloth.new(content)
20
+ ivars[:redcloth].each { |rule, value| textile_doc.send((rule.to_s + '=').to_sym, value) }
21
+ textile_doc.to_html
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module Magneto
2
+
3
+ class RubyPantsFilter < Filter
4
+
5
+ def name
6
+ 'rubypants'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'rubypants'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use RubyPants. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install rubypants'
16
+ raise 'Missing dependency: rubypants'
17
+ end
18
+
19
+ RubyPants.new(content, *((ivars[:rubypants].symbolize_keys rescue {})[:options] || [])).to_html
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module Magneto
2
+
3
+ class SassFilter < Filter
4
+
5
+ def name
6
+ 'sass'
7
+ end
8
+
9
+ def apply(content, ivars)
10
+ begin
11
+ require 'sass'
12
+ rescue LoadError => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ $stderr.puts "You're missing a library required to use Sass. Try running:"
15
+ $stderr.puts ' $ [sudo] gem install sass'
16
+ raise 'Missing dependency: sass'
17
+ end
18
+
19
+ Sass::Engine.new(content, (ivars[:sass].symbolize_keys rescue {})).render
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,118 @@
1
+ require 'magneto/readable'
2
+
3
+ module Magneto
4
+
5
+ class Item
6
+ include Readable
7
+
8
+ attr_reader :site, :origin
9
+
10
+ INVALID_LOCATION_MATCH_PATTERN = %r{(^[^/].*$|^.*/$)}
11
+
12
+ def initialize(site, origin = '')
13
+ super()
14
+ @site = site
15
+ @origin = origin.sub(INVALID_LOCATION_MATCH_PATTERN, '')
16
+ @destination = nil
17
+ @metadata = nil
18
+ @content = nil
19
+ @precomposed_content = nil
20
+ end
21
+
22
+ def relocated?
23
+ @origin == ''
24
+ end
25
+
26
+ def relocate
27
+ @origin = ''
28
+ end
29
+
30
+ def abandoned?
31
+ @destination == ''
32
+ end
33
+
34
+ def abandon
35
+ @destination = ''
36
+ end
37
+
38
+ def destination
39
+ @destination ||= @origin.dup
40
+ @destination
41
+ end
42
+
43
+ def destination=(destination)
44
+ @destination = (destination || '').sub(INVALID_LOCATION_MATCH_PATTERN, '')
45
+ end
46
+
47
+ def origin_path
48
+ @site.items_path + @origin
49
+ end
50
+
51
+ alias_method :path, :origin_path
52
+
53
+ def destination_path
54
+ @site.config[:output_path] + (@destination || @origin)
55
+ end
56
+
57
+ def import_metadata
58
+ self.destination = @metadata[:destination] unless @metadata.nil? || @metadata[:destination].nil?
59
+ end
60
+
61
+ def precomposed_content
62
+ read if @content.nil?
63
+ @precomposed_content || @content.dup
64
+ end
65
+
66
+ def apply_filter(filter_name, args = {})
67
+ filter = @site.filters[filter_name.to_sym]
68
+
69
+ if filter.nil?
70
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
71
+ raise "Couldn't find filter: '#{filter_name.to_s}'"
72
+ end
73
+
74
+ read if @content.nil?
75
+
76
+ @content = filter.apply(@content, @site.config.deep_merge(@metadata || {}).deep_merge({
77
+ :config => @site.config,
78
+ :site => @site,
79
+ :item => self
80
+ }).deep_merge(filter_name.to_sym => args))
81
+ end
82
+
83
+ def apply_template(template_name, args = {})
84
+ template = @site.templates[template_name.to_sym]
85
+
86
+ if template.nil?
87
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
88
+ raise "Couldn't find template: '#{template_name.to_s}'"
89
+ end
90
+
91
+ read if @content.nil?
92
+ @precomposed_content ||= @content.dup
93
+
94
+ @content = template.filter.apply(template.content, {
95
+ template.filter.name.to_sym => {}
96
+ }.deep_merge(@site.config).deep_merge(@metadata || {}).deep_merge(template.metadata || {}).deep_merge({
97
+ :config => @site.config,
98
+ :site => @site,
99
+ :item => self,
100
+ :content => @content
101
+ }).deep_merge(args.symbolize_keys))
102
+ end
103
+
104
+ def write
105
+ unless abandoned?
106
+ if content?
107
+ FileUtils.mkdir_p File.dirname(destination_path)
108
+ File.open(destination_path, 'w') { |f| f.write content }
109
+ else
110
+ unless relocated?
111
+ FileUtils.mkdir_p File.dirname(destination_path)
112
+ FileUtils.cp origin_path, destination_path
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,55 @@
1
+ module Magneto
2
+
3
+ module Readable
4
+
5
+ def metadata?
6
+ not @metadata.nil? || @metadata.empty?
7
+ end
8
+
9
+ def metadata
10
+ read if @metadata.nil?
11
+ @metadata || {}
12
+ end
13
+
14
+ def metadata=(metadata)
15
+ @metadata = metadata || {}
16
+ end
17
+
18
+ def content?
19
+ not @content.nil? || @content.empty?
20
+ end
21
+
22
+ def content
23
+ read if @content.nil?
24
+ @content || ''
25
+ end
26
+
27
+ alias to_s content
28
+
29
+ def content=(content)
30
+ @content = content || ''
31
+ end
32
+
33
+ # Adapted from `jekyll/convertible.rb` with thanks to Tom Preston-Werner.
34
+ def read
35
+ @metadata = {}
36
+ @content = File.read(path)
37
+
38
+ if @content =~ /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
39
+ @content = $POSTMATCH
40
+
41
+ begin
42
+ @metadata = YAML.load($1)
43
+ raise unless @metadata.is_a? Hash
44
+ rescue => ex
45
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
46
+ $stderr.puts "WARNING: Couldn't load metadata."
47
+ @metadata = {}
48
+ end
49
+
50
+ @metadata.symbolize_keys!
51
+ import_metadata
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,51 @@
1
+ module Magneto
2
+
3
+ # Adapted from `nanoc/helpers/rendering.rb` and `nanoc/helpers/capturing.rb`
4
+ # with thanks to Denis Defreyne and contributors.
5
+ class RenderContext < Context
6
+
7
+ def render(template_name, args = {}, &block)
8
+ template = @site.templates[template_name.to_sym]
9
+
10
+ if template.nil?
11
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
12
+ raise "Couldn't find template: '#{template_name.to_s}'"
13
+ end
14
+
15
+ if block_given?
16
+ # Get templating system output instance.
17
+ erbout = eval('_erbout', block.binding)
18
+
19
+ # Save output length.
20
+ erbout_length = erbout.length
21
+
22
+ # Execute block (which may cause recursion).
23
+ block.call
24
+
25
+ # Use additional output from block execution as content.
26
+ current_content = erbout[erbout_length..-1]
27
+
28
+ # Remove addition from templating system output.
29
+ erbout[erbout_length..-1] = ''
30
+ else
31
+ current_content = nil
32
+ end
33
+
34
+ ivars = {}
35
+ self.instance_variables.each { |ivar| ivars[ivar[1..-1].to_sym] = self.instance_variable_get(ivar) }
36
+
37
+ result = template.filter.apply(template.content, ivars.deep_merge({
38
+ :content => current_content
39
+ }).deep_merge(template.metadata || {}).deep_merge(args.symbolize_keys))
40
+
41
+ if block_given?
42
+ # Append filter result to templating system output and return empty
43
+ # string.
44
+ erbout << result
45
+ ''
46
+ else
47
+ result
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,18 @@
1
+ module Magneto
2
+
3
+ class ScriptContext < Context
4
+
5
+ def initialize(ivars)
6
+ super
7
+ puts 'Evaluating script...'
8
+
9
+ begin
10
+ self.instance_eval File.read(@source_path + '/script.rb')
11
+ @site.write
12
+ rescue => ex
13
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
14
+ raise 'Script evaluation failed.'
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,116 @@
1
+ module Magneto
2
+
3
+ class Site
4
+
5
+ attr_reader :config, :filters, :items_path, :items, :templates
6
+
7
+ def initialize(config, filters)
8
+ super()
9
+ @config = config
10
+ @filters = filters
11
+ @items_path = @config[:source_path] + '/items'
12
+ @items_path.freeze
13
+ reset
14
+ end
15
+
16
+ def generate
17
+ find_items
18
+ find_templates
19
+ find_existing_output
20
+
21
+ ScriptContext.new(@config.deep_merge({
22
+ :config => @config,
23
+ :site => self
24
+ }))
25
+
26
+ reset
27
+ end
28
+
29
+ def write
30
+ return if @written
31
+
32
+ puts 'Writing output...'
33
+ @written = true
34
+ output = Set.new unless @existing_output.empty?
35
+
36
+ @items.each do |item|
37
+ item.write
38
+
39
+ unless @existing_output.empty?
40
+ next if item.abandoned?
41
+ path = item.destination_path
42
+
43
+ while path.start_with? @config[:output_path] do
44
+ output << path
45
+ path = File.dirname(path)
46
+ end
47
+ end
48
+ end
49
+
50
+ unless @existing_output.empty?
51
+ obsolete_output = @existing_output - output
52
+
53
+ unless obsolete_output.empty?
54
+ if @config[:remove_obsolete]
55
+ puts 'Removing obsolete output...'
56
+
57
+ obsolete_output.to_a.sort.reverse.each do |path|
58
+ if File.directory? path
59
+ FileUtils.rmdir path
60
+ else
61
+ FileUtils.rm path
62
+ end
63
+ end
64
+ else
65
+ puts 'Listing obsolete output...'
66
+ puts obsolete_output.to_a.sort.reverse
67
+ end
68
+ end
69
+ end
70
+
71
+ puts 'Site generation succeeded.'
72
+ end
73
+
74
+ private
75
+
76
+ def reset
77
+ @items = []
78
+ @templates = {}
79
+ @existing_output = Set.new
80
+ @written = false
81
+ end
82
+
83
+ def find_items
84
+ puts 'Finding items...'
85
+
86
+ if File.directory? @items_path
87
+ Dir.chdir @items_path do
88
+ Dir[@config[:hidden_files] ? '**/{.[^.],}*' : '**/*'].each do |path|
89
+ unless File.directory? path
90
+ @items << Item.new(self, '/' + path)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def find_templates
98
+ puts 'Finding templates...'
99
+
100
+ Dir[@config[:source_path] + '/templates/*.*'].each do |path|
101
+ unless File.directory? path
102
+ name = File.basename(path).split('.')[0..-2].join('.')
103
+ @templates[name.to_sym] = Template.new(self, path, name)
104
+ end
105
+ end
106
+ end
107
+
108
+ def find_existing_output
109
+ puts 'Finding existing output...'
110
+
111
+ Dir[@config[:output_path] + '/**/{.[^.],}*'].each do |path|
112
+ @existing_output << path
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,31 @@
1
+ module Magneto
2
+
3
+ class Template
4
+ include Readable
5
+
6
+ attr_reader :site, :path, :name, :filter
7
+
8
+ def initialize(site, path, name)
9
+ super()
10
+ @site = site
11
+ @path = path
12
+ @name = name
13
+ @filter = nil
14
+ @metadata = nil
15
+ @content = nil
16
+ end
17
+
18
+ def use_filter(filter_name)
19
+ @filter = @site.filters[filter_name.to_sym]
20
+
21
+ if @filter.nil?
22
+ $stderr.puts "#{File.basename($PROGRAM_NAME)}: #{ex.to_s}"
23
+ raise "Couldn't find filter: '#{filter_name.to_s}'"
24
+ end
25
+ end
26
+
27
+ def import_metadata
28
+ @filter = use_filter(@metadata[:filter]) unless @metadata.nil? || @metadata[:filter].nil?
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/lib')
2
+
3
+ require 'magneto'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'magneto'
7
+ s.version = Magneto::VERSION
8
+ s.summary = 'A static site generator.'
9
+ s.description = 'Magneto is a static site generator.'
10
+ s.authors = ['Don Melton']
11
+ s.email = 'don@blivet.com'
12
+ s.homepage = 'https://github.com/donmelton/magneto'
13
+ s.files = Dir['{bin,lib}/**/*'] + Dir['[A-Z]*'] + ['magneto.gemspec']
14
+ s.executables = ['magneto']
15
+ s.extra_rdoc_files = ['LICENSE']
16
+ s.require_paths = ['lib']
17
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: magneto
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Don Melton
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-17 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Magneto is a static site generator.
15
+ email: don@blivet.com
16
+ executables:
17
+ - magneto
18
+ extensions: []
19
+ extra_rdoc_files:
20
+ - LICENSE
21
+ files:
22
+ - bin/magneto
23
+ - lib/magneto/application.rb
24
+ - lib/magneto/context.rb
25
+ - lib/magneto/core_ext.rb
26
+ - lib/magneto/filter.rb
27
+ - lib/magneto/filters/bluecloth.rb
28
+ - lib/magneto/filters/coffeescript.rb
29
+ - lib/magneto/filters/erb.rb
30
+ - lib/magneto/filters/erubis.rb
31
+ - lib/magneto/filters/haml.rb
32
+ - lib/magneto/filters/kramdown.rb
33
+ - lib/magneto/filters/less.rb
34
+ - lib/magneto/filters/maruku.rb
35
+ - lib/magneto/filters/rdiscount.rb
36
+ - lib/magneto/filters/redcarpet.rb
37
+ - lib/magneto/filters/redcloth.rb
38
+ - lib/magneto/filters/rubypants.rb
39
+ - lib/magneto/filters/sass.rb
40
+ - lib/magneto/filters.rb
41
+ - lib/magneto/item.rb
42
+ - lib/magneto/readable.rb
43
+ - lib/magneto/render_context.rb
44
+ - lib/magneto/script_context.rb
45
+ - lib/magneto/site.rb
46
+ - lib/magneto/template.rb
47
+ - lib/magneto.rb
48
+ - LICENSE
49
+ - README.md
50
+ - magneto.gemspec
51
+ homepage: https://github.com/donmelton/magneto
52
+ licenses: []
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 1.8.24
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: A static site generator.
75
+ test_files: []