app/services/locomotive/page_parsing_service.rb
require 'active_support/benchmarkable'
module Locomotive
class PageParsingService < Struct.new(:site, :locale)
include ActiveSupport::Benchmarkable
def find_all_elements(page)
find_or_create_editable_elements(page)&.slice(:elements, :sections)
end
def find_or_create_editable_elements(page)
benchmark "Parse page #{page._id} find_or_create_editable_elements" do
parsed = {
extends: {},
blocks: {},
super_blocks: {},
elements: [],
sections: { top: [], bottom: [], dropzone: false }
}
subscribe(parsed) do
parse(page)
# remove the sections if hidden (sections might be hidden by an overidding block).
# also sort them by their placement.
# finally, only return their types (+ sectionId)
extract_section_attributes!(page, parsed)
# !Important! Non visible editable elements are not removed
persist_editable_elements!(page, parsed)
end
parsed
end
rescue Exception => e
logger.error "[PageParsing] " + e.message + "\n\t" + e.backtrace.join("\n\t")
puts "[PageParsing] " + e.message + "\n\t" + e.backtrace.join("\n\t")
nil
end
# Each element of the elements parameter is a couple: Page, EditableElement
def group_and_sort_editable_elements(elements)
elements.group_by { |(_, el)| el.block }.tap do |groups|
groups.each do |_, list|
list.sort! { |(_, a), (_, b)| (b.priority || 0) <=> (a.priority || 0) }
end
end
end
def blocks_from_grouped_editable_elements(groups)
groups.map do |block, elements|
next if elements.empty?
element = elements.first.last
{ name: block, label: element.block_label, priority: element.block_priority || 0 }
end.compact.sort { |a, b| b[:priority] <=> a[:priority] }
end
private
def subscribe(parsed, &block)
subscribers = [
subscribe_to_extends(parsed[:extends]),
subscribe_to_blocks(parsed[:blocks], parsed[:super_blocks]),
subscribe_to_editable_elements(parsed[:elements]),
subscribe_to_sections(parsed[:sections])
]
yield.tap do
subscribers.each do |subscriber|
ActiveSupport::Notifications.unsubscribe(subscriber)
end
end
end
def subscribe_to_extends(extends)
ActiveSupport::Notifications.subscribe('steam.parse.extends') do |name, start, finish, id, payload|
parent_id, page_id = payload[:parent]._id, payload[:page]._id
extends[parent_id] = page_id
end
end
def subscribe_to_blocks(blocks, super_blocks)
ActiveSupport::Notifications.subscribe('steam.parse.inherited_block') do |name, start, finish, id, payload|
page_id, block_name, found_super = payload[:page]._id, payload[:name], payload[:found_super]
super_blocks[page_id] ||= {}
super_blocks[page_id][block_name] = found_super
blocks[block_name] ||= payload.slice(:short_name, :priority)
end
end
def subscribe_to_editable_elements(elements)
ActiveSupport::Notifications.subscribe(/\Asteam\.parse\.editable\./) do |name, start, finish, id, payload|
page, attributes = payload[:page], payload[:attributes]
elements << [page, attributes]
end
end
def subscribe_to_sections(sections)
ActiveSupport::Notifications.subscribe('steam.parse.section') do |name, start, finish, id, payload|
page, block, attributes = payload[:page], payload[:block], payload[:attributes]
if attributes[:is_dropzone]
sections[:dropzone] ||= [page, block, attributes]
else
placement = attributes[:placement] || :top
attributes[:position] ||= block.blank? ? 0 : block.split('/').size
sections[placement].push([page, block, attributes])
end
end
end
def parse(page)
entity = repository.build(page.attributes.dup)
decorated_page = Locomotive::Steam::Decorators::TemplateDecorator.new(entity, self.locale, self.site.default_locale)
parser = services.liquid_parser
parser.parse(decorated_page)
end
# Remove sections from hidden blocks.
# It also tells if there is a visible sections dropzone.
def extract_section_attributes!(page, parsed)
[:top, :bottom].each do |placement|
parsed[:sections][placement] = parsed[:sections][placement].map do |(_page, block, attributes)|
# we don't want hidden sections
next unless is_element_visible?(page, parsed, _page, block)
attributes.slice(:source, :type, :key, :id, :label, :position)
end.compact.sort_by { |attributes| attributes[:position] }
end
if parsed[:sections][:dropzone]
# we don't want a hidden dropzone
_page, block, _ = parsed[:sections][:dropzone]
parsed[:sections][:dropzone] = is_element_visible?(page, parsed, _page, block)
else
parsed[:sections][:dropzone] = false
end
end
def persist_editable_elements!(page, parsed)
modified_pages, pages = [], { page._id => page } # group modifications by page
parsed[:elements].map! do |couple|
_page, attributes = couple
next if !is_element_visible?(page, parsed, _page, attributes[:block])
# Note: _page is a Steam entity but we need a Mongoid document to save the elements
_page = attributes[:fixed] ? find_page(_page._id, pages) : page
element = add_or_modify_editable_element(_page, attributes)
couple[0], couple[1] = _page, element # we get now a Mongoid document instead of a Steam entity
assign_block_information(element, parsed[:blocks])
modified_pages << _page
couple
end.compact!
modified_pages.uniq.map(&:save!)
end
def is_element_visible?(page, parsed, _page, block)
if page._id == _page._id # same page, we are sure that it's visible
true
elsif block.blank? # Ex: an editable_element outside a block (impossible to remove it in pages extending this template)
true
else
block_visible?(_page._id, parsed, { block: block })
end
end
def block_visible?(page_id, parsed, attributes)
block_name = attributes[:block]
descendant = parsed[:extends][page_id]
return true if descendant.nil?
# find if the descendant hides the block
if (blocks = parsed[:super_blocks][descendant]).blank?
# we can not know for sure, ask the descendant of the descendant
block_visible?(descendant, parsed, attributes)
else
found_super = blocks[block_name]
hidden = blocks.keys.any? { |name| block_name =~ /\A#{name}(\Z|\/)/ }
if found_super || !hidden
# again, we need to ask the descendant of the descendant
block_visible?(descendant, parsed, attributes)
end
end
end
def add_or_modify_editable_element(page, attributes)
if element = page.editable_elements.by_block_and_slug(attributes[:block], attributes[:slug]).first
# context: the editable element has been created from the page YAML header
existing_content = element._type.nil? && !element.content.blank?
# FIXME: we don't want to deal here with the generic Locomotive::EditableElement class
element = page.editable_elements.with_same_class!(element, "Locomotive::#{attributes[:type].to_s.classify}".constantize)
element.attributes = attributes
# we know now this was an editable_text element. If it already had
# a content, then it shouldn't be flagged as default_content.
if element.respond_to?(:default_content?) && existing_content
element.default_content = false
end
element
else
klass = "Locomotive::#{attributes[:type].to_s.classify}".constantize
page.editable_elements.build(attributes, klass)
end
end
def assign_block_information(element, blocks)
if element.block && (options = blocks[element.block])
element.block_name = element.block.split('/').last if options[:short_name]
element.block_priority = options[:priority]
end
end
def services
@services ||= Locomotive::Steam::Services.build_instance.tap do |services|
services.set_site(self.site)
services.locale = self.locale
end
end
def repository
services.repositories.page
end
def find_page(id, in_memory)
in_memory[id] ||= Locomotive::Page.find(id)
end
def logger
Rails.logger
end
end
end