├── .gitignore ├── bin └── magneto ├── lib ├── magneto │ ├── filter.rb │ ├── script_context.rb │ ├── filters.rb │ ├── context.rb │ ├── filters │ │ ├── erb.rb │ │ ├── sass.rb │ │ ├── maruku.rb │ │ ├── less.rb │ │ ├── bluecloth.rb │ │ ├── kramdown.rb │ │ ├── rdiscount.rb │ │ ├── rubypants.rb │ │ ├── coffeescript.rb │ │ ├── redcloth.rb │ │ ├── haml.rb │ │ ├── erubis.rb │ │ └── redcarpet.rb │ ├── template.rb │ ├── readable.rb │ ├── core_ext.rb │ ├── render_context.rb │ ├── site.rb │ ├── item.rb │ └── application.rb └── magneto.rb ├── magneto.gemspec ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .rvmrc 3 | *.gem 4 | -------------------------------------------------------------------------------- /bin/magneto: -------------------------------------------------------------------------------- 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 8 | -------------------------------------------------------------------------------- /lib/magneto/filter.rb: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /lib/magneto/script_context.rb: -------------------------------------------------------------------------------- 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 19 | -------------------------------------------------------------------------------- /lib/magneto/filters.rb: -------------------------------------------------------------------------------- 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' 14 | -------------------------------------------------------------------------------- /lib/magneto/context.rb: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /lib/magneto/filters/erb.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /lib/magneto/filters/sass.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /lib/magneto/filters/maruku.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /lib/magneto/filters/less.rb: -------------------------------------------------------------------------------- 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 24 | -------------------------------------------------------------------------------- /lib/magneto.rb: -------------------------------------------------------------------------------- 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 31 | -------------------------------------------------------------------------------- /lib/magneto/filters/bluecloth.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /lib/magneto/filters/kramdown.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /lib/magneto/filters/rdiscount.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /lib/magneto/filters/rubypants.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /lib/magneto/filters/coffeescript.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /magneto.gemspec: -------------------------------------------------------------------------------- 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 = ['Lisa Melton'] 11 | s.email = '115488+lisamelton@users.noreply.github.com' 12 | s.homepage = 'https://github.com/lisamelton/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 18 | -------------------------------------------------------------------------------- /lib/magneto/filters/redcloth.rb: -------------------------------------------------------------------------------- 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 25 | -------------------------------------------------------------------------------- /lib/magneto/template.rb: -------------------------------------------------------------------------------- 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 32 | -------------------------------------------------------------------------------- /lib/magneto/filters/haml.rb: -------------------------------------------------------------------------------- 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 27 | -------------------------------------------------------------------------------- /lib/magneto/filters/erubis.rb: -------------------------------------------------------------------------------- 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 29 | -------------------------------------------------------------------------------- /lib/magneto/filters/redcarpet.rb: -------------------------------------------------------------------------------- 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 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2024 Lisa 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. 20 | -------------------------------------------------------------------------------- /lib/magneto/readable.rb: -------------------------------------------------------------------------------- 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 56 | -------------------------------------------------------------------------------- /lib/magneto/core_ext.rb: -------------------------------------------------------------------------------- 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 59 | -------------------------------------------------------------------------------- /lib/magneto/render_context.rb: -------------------------------------------------------------------------------- 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 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magneto 2 | 3 | Magneto is a static site generator. 4 | 5 | ## About 6 | 7 | Hi, I'm Lisa Melton. I wrote Magneto to generate my own website. 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 | ## Installation 14 | 15 | Magneto is [available as a gem](https://rubygems.org/gems/magneto) which you can install like this: 16 | 17 | sudo gem install magneto 18 | 19 | ## Usage 20 | 21 | magneto [OPTION]... 22 | 23 | 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. 24 | 25 | Ruby library files are loaded from the `plugins` directory only once. 26 | 27 | The generated site is written to the `output` directory. 28 | 29 | Configuration is loaded from `config.yaml` but can be overriden using the following options: 30 | 31 | -c, --config PATH use specific YAML configuration file 32 | -s, --source PATH use specific source directory 33 | -o, --output PATH use specific output directory 34 | 35 | --[no-]hidden include [exclude] hidden source files 36 | --[no-]remove remove [keep] obsolete output 37 | --[no-]auto enable [disable] automatic regeneration 38 | 39 | -h, --help display this help and exit 40 | --version output version information and exit 41 | 42 | ## Dependencies 43 | 44 | Magneto doesn't have any dependencies for basic operation. 45 | 46 | Enabling automatic regeneration requires installation of the Directory Watcher gem: 47 | 48 | sudo gem install directory_watcher 49 | 50 | Using any of the built-in filters could require additional gem installations. 51 | 52 | ## License 53 | 54 | Magneto is copyright Lisa Melton and available under a [MIT license](https://github.com/lisamelton/magneto/blob/master/LICENSE). 55 | -------------------------------------------------------------------------------- /lib/magneto/site.rb: -------------------------------------------------------------------------------- 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 117 | -------------------------------------------------------------------------------- /lib/magneto/item.rb: -------------------------------------------------------------------------------- 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 119 | -------------------------------------------------------------------------------- /lib/magneto/application.rb: -------------------------------------------------------------------------------- 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 < 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 154 | --------------------------------------------------------------------------------