require 'alula/version'
require 'alula/core_ext'
require 'alula/config'
require 'alula/plugin'
require 'alula/storage'
require 'alula/content'
require 'alula/context'
require 'alula/generator'
require 'alula/attachment_processor'
require 'alula/compressors'
require 'alula/cdn'
require 'alula/helpers'
require 'alula/progress'
require 'thor'
require 'sprockets'
require 'i18n'
# require 'parallel'
require 'hashie/mash'
require 'json'
# Silence Tilt
require 'sass'
require 'coffee-script'
module Alula
class Site
def self.instance; @@instance; end
# Global configuration
attr_reader :config
# Storage
attr_reader :storage
# Context for rendering
attr_reader :context
# Progress displayer
attr_reader :progress
# CDN Resolver for Site
attr_reader :cdn
# Site Plugins
attr_reader :plugins
# Site filters
attr_reader :filters
# Compressors
attr_reader :compressors
# Site metadata information
attr_reader :metadata
# Site attachment mapping
attr_reader :attachments
# Theme
attr_reader :theme
# User generated content
attr_reader :content
# System generated content, pages, pagination, etc.
attr_reader :generated
def initialize(options)
@@instance = self
# Read local config
@config = Config.new(options)
@storage = Storage.load(site: self)
@metadata = Content::Metadata.new({
base_locale: @config.locale,
environment: @config.environment,
title: @config.title,
author: @config.author,
description: @config.description,
tagline: @config.tagline,
url: @config.url,
theme: @config.theme,
# Use this to store information of GIT site or note
git: ::File.directory?(".git"),
})
# Progress displayer
@progress = Progress.new(debug: options["debug"])
# Compressors
compressors = if @config.assets.compress
{
html: Alula::Compressors::HTMLCompressor.new,
css: Alula::Compressors::CSSCompressor.new,
js: Alula::Compressors::JSCompressor.new,
}
else
{
html: Alula::Compressors::DummyCompressor.new,
css: Alula::Compressors::DummyCompressor.new,
js: Alula::Compressors::DummyCompressor.new,
}
end
@compressors = Hashie::Mash.new(compressors)
@attachments = AttachmentProcessor.new(site: self)
# Set up CDN resolver
@cdn = CDN.load(site: self)
@plugins = {}
@filters = {}
# Set up I18n
l10n_path = File.join(File.dirname(__FILE__), "..", "..", "locales", "l10n", "*.yml")
locale_path = File.join(File.dirname(__FILE__), "..", "..", "locales", "*.yml")
custom_locale_path = File.join(@storage.path(:custom, "locales"), "*.yml")
I18n.load_path += Dir[l10n_path]
I18n.load_path += Dir[locale_path]
I18n.load_path += Dir[custom_locale_path]
I18n.default_locale = @config.locale
# Set up default head addons
Alula::Plugin.addon(:head, "")
Alula::Plugin.addon(:head, ->(context){""})
end
# Compiles a site to static website
def generate
banner
# Load our plugins and filters
load_plugins
load_filters
# Prepare public folder
prepare(true)
load_content
process_attachments
compile_assets
render
cleanup
compress
# Store cached version of configuration
cached_config = File.join(storage.path(:cache), "config.yml")
@config.write_cache(cached_config)
end
def deploy
banner
end
# Proxy to metadata
def method_missing(meth, *args, &blk)
# Proxy to metadata
if !meth[/=$/] and self.metadata.respond_to?(meth)
args.unshift(self.context.locale || self.config.locale) if args.empty?
self.metadata.send(meth, *args)
else
super
end
end
private
def banner
puts ""
puts "Alula #{Alula::VERSION::STRING}"
puts ""
end
def load_plugins
config.plugins.each do |name, options|
if plugin = Alula::Plugin.load(name, options)
@plugins[name] = plugin
end
end
if @plugins
puts "Plugins: " + @plugins.collect {|name, plugin| "#{name} #{plugin.version}"}.join(" ")
end
end
def load_filters
config.content.filters.each do |name, options|
if filter = Alula::Filter.load(name, options)
@filters[name] = filter
end
end
end
def prepare(preserve = false)
# say "==> Preparing environment" + (preserve ? " (preserving existing files)" : "")
# Delegate preparations to storage module
self.storage.prepare(preserve)
# Load theme
@context = Alula::Context.new(site: self, storage: self.storage)
@context.send(:extend, Helpers)
# @theme = Alula::Theme.load(config.theme, site: self)
@theme = Alula::Theme.load(config.theme.to_a.last[0], config.theme.to_a.last[1])
puts " Theme: #{@theme.name} #{@theme.version}"
puts ""
# Create our asset environment
@environment = Environment.new
# Add compressor support
# if config.environment == "production"
@environment.css_compressor = @compressors.css
@environment.js_compressor = @compressors.js
# end
@environment.context_class.class_eval do
# include Helpers
def context; Alula::Site.instance.context; end
def method_missing(meth, *args, &blk)
return context.send(meth, *args, &blk) if context.respond_to?(meth)
super
end
end
@context.environment = @environment
@context.attachments = self.attachments
# Add generated attachements
@environment.append_path @storage.path(:cache, "attachments")
# Add generated assets
@environment.append_path @storage.path(:cache, "assets")
# Theme, plugins, vendor and customisation
[
self.theme.path,
*plugins.collect{|name, plugin| plugin.path},
::File.join(File.dirname(__FILE__), "..", "..", "vendor"),
].each do |path|
%w{javascripts stylesheets images}.each {|p|
@environment.append_path ::File.join(path, "assets", p)
}
end
# Customisation
%w{javascripts stylesheets images}.each do |path|
@environment.prepend_path @storage.path(:custom, path)
end
end
def load_content
say "==> Loading site content"
# Read site content
@content = Content.new(site: self)
@content.load
# Do we have index page defined
if self.config.index
index_page = @content.by_slug(self.config.index)
if index_page
index_page.metadata.slug = "index"
index_page.metadata.template = "/:locale/:slug"
index_page.metadata.title = Hash[index_page.metadata.languages.collect{|lang| [lang, metadata.title(lang)]}]
index_page.metadata.description = Hash[index_page.metadata.languages.collect{|lang| [lang, metadata.description(lang)]}]
end
end
end
def process_attachments
puts "==> Processing attachments"
progress.create :attachments, title: "Attachments", total: self.content.attachments.count
progress.display
@@lock = Mutex.new
# Parallel.map(self.content.attachments, :in_threads => Parallel.processor_count) do |attachment|
self.content.attachments.each do |attachment|
if processor = attachments.get(attachment)
processor.process
end
@@lock.synchronize do
progress.step(:attachments)
end
end
progress.finish(:attachments)
progress.hide
# Output mapping information
File.open(self.storage.path(:cache) + "/mapping.yml", 'w') {|io| io.puts JSON(self.attachments.mapping.to_json).to_yaml}
end
def compile_assets
puts "==> Compiling assets"
# Generate stylesheet
@storage.output(:cache, "assets/style.css") do |io|
io.puts "/*"
# Theme style
io.puts " *= require #{@theme.name}"
# Plugins
@plugins.each do |name, plugin|
io.puts " *= require #{name}" unless Dir[File.join(plugin.path, "assets", "stylesheets", "#{name}*")].empty?
end
# Vendored
# Blog customization
@storage.custom(/stylesheets\/.*.css.*$/).each do |name, item|
name = File.basename(name).gsub(/(\.\S+)$/, '')
io.puts " *= require #{name}"
end
io.puts "*/"
end
# Add stlesheet to template
Alula::Plugin.prepend_addon(:head, ->(context){ context.stylesheet_link("style") })
# Generate javascript
@storage.output(:cache, "assets/script.js") do |io|
io.puts "/*"
# Theme scripts
io.puts " *= require #{@theme.name}"
# Plugins
@plugins.each do |name, plugin|
io.puts " *= require #{name}" unless Dir[File.join(plugin.path, "assets", "javascripts", "#{name}*")].empty?
end
# Vendored
io.puts " *= require lazyload" if self.config.attachments.image.lazyload
# Customisation
@storage.custom(/javascripts\/.*.js.*$/).each do |name, item|
name = File.basename(name).gsub(/(\.\S+)$/, '')
io.puts " *= require #{name}"
end
io.puts " */"
end
# Add javascript to end of body
Alula::Plugin.addon(:body, ->(context){ context.javascript_link("script") })
# Compile all assets
progress.create :assets, title: "Compiling assets", total: @environment.each_logical_path.count
progress.display
@environment.each_logical_path do |logical_path|
if asset = @environment.find_asset(logical_path)
target = File.join(@storage.path(:assets), asset.digest_path)
asset.write_to(target)
# asset.write_to("#{target}.gz") if target =~ /\.(css|js)$/ and self.config.assets.gzip
end
progress.step :assets
end
progress.finish(:assets)
progress.hide
end
def render
say "==> Render site"
progress.create :render, title: "Rendering content", total: (self.content.posts.count + self.content.pages.count)
progress.display
# Render all user content, parallel...
(self.content.posts + self.content.pages).each do |content|
# Write content to file
content.write
progress.title(:render, "%20s" % content.name[0..19]) if self.config.debug
progress.step(:render)
end
progress.finish(:render)
# Copy static content
# Create directory path
static_dirs = [
::File.join(File.dirname(__FILE__), "..", "..", "vendor"),
*plugins.collect{|name, plugin| plugin.path},
self.theme.path,
]
statics = static_dirs.collect{|path|
Dir[File.join(path, "static", "**", "*")].collect{|p|
{path: File.join(path, "static"), item: p}
}
}.flatten
progress.create :static, title: "Copy statics", total: self.content.statics.count + statics.count
(statics + self.content.statics).each do |static|
if static.kind_of?(Hash)
name = static[:item].gsub(/^#{static[:path]}\//, '')
@storage.output_public(name) {|io| File.read(static[:item]) }
else
@storage.output_public(static.name) { |io| static.read }
end
progress.step :static
end
progress.finish :static
progress.hide
end
def cleanup
say "==> Cleaning up"
asset_path = @storage.path(:assets)
assets = @environment.used.collect do |asset_name|
if asset = @environment[asset_name]
filename = File.join(asset_path, asset.digest_path)
# [filename, self.config.assets.gzip ? "#{filename}.gz" : ""]
end
end.flatten.reject { |u| u.nil? or !File.exists?(u) }
outputted = @storage.outputted.reject{|o|o[/^#{asset_path}/]}
keep = (assets + outputted).collect{|p| File.expand_path(p)}
Dir[File.join(@storage.path(:public), "**", "*")].each do |entry|
next unless File.file?(entry)
FileUtils.rm entry if File.file?(entry) and !keep.include?(File.expand_path(entry))
end
# Clean up empty directories
Dir[File.join(@storage.path(:public), "**", "*")].each do |entry|
next unless File.directory?(entry)
FileUtils.rmdir entry if Dir[File.join(entry, "**", "*")].count == 0
end
end
def compress
return unless config.assets.gzip
say "==> Compressing content"
Dir[File.join(@storage.path(:public), "**", "*")].each do |entry|
next unless config.gzip_types.include?(File.extname(entry)[1..-1])
gz = Zlib::GzipWriter.open("#{entry}.gz", Zlib::BEST_COMPRESSION) do |gz|
gz.write File.read(entry)
end
@storage.outputted << "#{entry}.gz"
end
end
# Output helpers
def say(msg)
@shell ||= Thor::Shell::Basic.new
@shell.say msg
end
end
end