plugins/aspects/main.rb
# -*- coding: utf-8 -*-
description 'Aspect subsystem'
dependencies 'utils/cache'
Page.attributes do
enum :aspect do
Aspect.aspects.keys.inject({}) do |hash, name|
hash[name] = Locale.translate("aspect_#{name}", fallback: titlecase(name))
hash
end
end
end
# Aspect context
# A aspect context holds the request parameters and other
# variables used by the aspects.
# It is possible for a aspect to run sub-aspects. For this
# purpose you create a subcontext which inherits the variables.
class Context
include Hooks
has_hooks :initialized
attr_reader :page, :private, :params, :request, :header
def initialize(options = {})
@page = options[:page]
@private = options[:private] || Hash.with_indifferent_access
@params = Hash.with_indifferent_access.merge(options[:params] || {})
@request = options[:request]
@header = options[:header] || Hash.with_indifferent_access
invoke_hook(:initialized)
end
def [](key)
private[key]
end
def []=(key, value)
private[key] = value
end
def subcontext(options = {})
Context.new(page: options[:page] || page,
private: private.merge(options[:private] || {}),
params: params.merge(options[:params] || {}),
request: request,
header: header)
end
end
# An Aspect renders pages
# Aspects get a page as input and create text.
class Aspect
include PageHelper
include Templates
@aspects = {}
class NotAvailable < NameError
def initialize(name, page)
super(:aspect_not_available.t(aspect: name, page: page.path,
type: "#{page.mime.comment} (#{page.mime})"))
end
def status
:bad_request
end
end
FRAGMENT_MIME = 'text/x-html-fragment'
# Constructor for aspect
# Options:
# * priority: Aspect priority. The aspect with the lowest priority will be used for a page.
# * cacheable: Aspect is cacheable
def initialize(name, options)
@name = name.to_s
@hidden = !!options[:hidden]
@cacheable = !!options[:cacheable]
@priority = (options[:priority] || 99).to_i
@accepts = Regexp === options[:accepts] ? options[:accepts] : /\A(?:#{options[:accepts]})\Z/
@mime = options.include?(:mime) ? options[:mime] : FRAGMENT_MIME
@plugin = options[:plugin] || Plugin.for(self.class)
@description = options[:description] || @plugin.description
end
attr_reader :name, :priority, :mime, :accepts, :description, :plugin
attr_reader? :hidden, :cacheable
def layout?
mime == FRAGMENT_MIME
end
# Aspects hash
def self.aspects
@aspects
end
# Create aspect class. This is sugar to create and
# register an aspect class in one step.
def self.create(name, options = {}, &block)
options[:plugin] ||= Plugin.for(block)
klass = Class.new(self)
klass.class_eval(&block)
register klass.new(name, options)
end
# Register aspect instance
def self.register(aspect)
(@aspects[aspect.name] ||= []) << aspect
end
# Find all accepting aspects for a page
def self.find_all(page)
@aspects.values.map do |aspects|
aspects.sort_by(&:priority).find {|a| a.accepts?(page) }
end.compact
end
# Find appropiate aspect for page. An optional
# name can be given to claim a specific aspect.
# If no aspect is found a exception is raised.
def self.find!(page, options = {})
options[:name] ||= page.attributes['aspect']
aspects = options[:name] ? @aspects[options[:name].to_s] : @aspects.values.flatten
aspects = aspects.to_a.sort_by(&:priority).select {|a| a.accepts?(page) && (!options[:layout] || a.layout?) }
puts "PREF #{options[:preferred_mime]}"
aspect = (options[:preferred_mime] && aspects.find {|a| a.mime.include?(options[:preferred_mime]) }) || aspects.first
raise NotAvailable.new(options[:name], page) if !aspect
aspect.dup
end
# Find appropiate aspect for page. An optional
# name can be given to claim a specific aspect.
# If no aspect is found nil is returned.
def self.find(page, options = {})
find!(page, options) rescue nil
end
# Acceptor should return true if page would be accepted by this aspect.
# Reimplement this method.
def accepts?(page)
!@accepts || page.mime.to_s =~ @accepts
end
# Render page content.
# Reimplement this method.
def call(context, page)
raise NotImplementedError
end
end
# Plug-in the aspect subsystem
module ::Olelo::PageHelper
def render_page(page)
cache(['include', page.path, page.etag], update: no_cache?, defer: true) do
begin
context = Context.new(page: page, params: {included: true})
Aspect.find!(page, layout: true).call(context, page)
rescue Aspect::NotAvailable => ex
%{<span class="error">#{escape_html ex.message}</span>}
end
end
end
end
# Plug-in the aspect subsystem
class ::Olelo::Application
def show_page
params[:aspect] ||= 'subpages' if params[:path].to_s.ends_with? '/'
env['HTTP_ACCEPT'].to_s =~ %r{[\w\.\+\-\*]+/[\*\w\.\+\-]+}
preferred_mime = $&
@selected_aspect, cacheable, layout, header, content =
cache(['aspect', page.path, page.etag, params, preferred_mime], update: no_cache?, defer: true) do |cache|
aspect = Aspect.find!(page, name: params[:aspect], preferred_mime: preferred_mime)
cache.disable! if !aspect.cacheable?
context = Context.new(page: page, params: params, request: request)
context.header['Content-Type'] = aspect.mime ? aspect.mime.to_s : page.mime.to_s unless aspect.layout?
result = aspect.call(context, page)
[aspect.name, aspect.cacheable?, aspect.layout?, context.header.to_hash, result]
end
if cacheable
response['Vary'] = 'Accept'
else
cache_control no_cache: true
end
self.response.header.merge!(header)
layout ? render(:show, locals: {content: content}) : content
rescue Aspect::NotAvailable => ex
cache_control no_cache: true
raise unless http_accept? /html/
redirect build_path(page.path) if params[:path].to_s.ends_with? '/'
raise if params[:aspect]
flash.error ex.message
redirect build_path(page, action: :edit)
end
hook :menu do |menu|
if menu.name == :actions && view_menu = menu[:view]
view_menu.append(cache(['aspect-menu', page.path, page.etag, @selected_aspect], update: no_cache?, defer: true) do
aspects = Aspect.find_all(page).select {|a| !a.hidden? || a.name == @selected_aspect || a.name == page.attributes['aspect'] }.map do |a|
[Locale.translate("aspect_#{a.name}", fallback: titlecase(a.name)), a]
end.sort_by(&:first)
aspects.select {|label, a| a.layout? }.map do |label, a|
MenuItem.new(a.name, label: label, href: build_path(page, aspect: a.name), class: a.name == @selected_aspect ? 'selected' : nil)
end +
aspects.reject {|label, a| a.layout? }.map do |label, a|
MenuItem.new(a.name, label: label, href: build_path(page, aspect: a.name), class: 'download')
end
end)
end
end
redefine_method :footer do |content = nil, &block|
# FIXME: Use block instead of block_given?, block_given? returns always false. Is this a ruby issue?
if block || content
super(content, &block)
elsif page
cache(['footer', page.path, page.etag], update: no_cache?) { super() }
else
super()
end
end
end
__END__
@@ locale.yml
cs:
aspect_not_available: 'Vzhled %{aspect} není k dispozici pro stránku %{page} s typem %{type}.'
attribute_aspect: 'Přednastavený vzhled'
de:
aspect_not_available: 'Aspekt %{aspect} für die Seite %{page} mit dem Typ %{type} ist nicht verfügbar.'
attribute_aspect: 'Standard-Ansicht'
en:
aspect_not_available: 'Aspect %{aspect} is not available for page %{page} with type %{type}.'
attribute_aspect: 'Default Aspect'
fr:
aspect_not_available: "L'aspect %{aspect} n'est pas disponible pour la page %{page} avec le type %{type}."
attribute_aspect: "Aspect par défaut"