implement the sections_dropzone liquid tag (WIP, 99% done)

Didier Lafforgue committed May 30, 2018
commit 5eb896a23bac38aea3df6862b7883fb0aacd17d4
Showing 10 changed files with 271 additions and 100 deletions
locomotive/steam.rb b/lib/locomotive/steam.rb +0 -1
@@ @@ -21,7 +21,6 @@ module Locomotive
CONTENT_ENTRY_ENGINE_CLASS_NAME = /^Locomotive::ContentEntry(.*)$/o.freeze
SECTIONS_SETTINGS_VARIABLE_REGEXP = /^\s*([a-z]+\.)?settings\.(?<id>.*)\s*$/o.freeze
- # SECTIONS_SETTINGS_TAG_REGEXP = /(?<tag><[^\>]+>)\s*\z/mo.freeze
SECTIONS_BLOCK_FORLOOP_REGEXP = /(?<name>.+)-section\.blocks$/o.freeze
IsHTTP = /\Ahttps?:\/\//o.freeze
locomotive/steam/liquid/drops/page.rb b/lib/locomotive/steam/liquid/drops/page.rb +2 -1
@@ @@ -4,7 +4,8 @@ module Locomotive
module Drops
class Page < I18nBase
- delegate :position, :fullpath, :depth, :seo_title, :meta_keywords, :meta_description, :redirect_url, :handle, to: :@_source
+ delegate :position, :fullpath, :depth, :sections_content, :redirect_url, :handle, to: :@_source
+ delegate :seo_title, :meta_keywords, :meta_description, to: :@_source
delegate :listed?, :published?, :redirect?, :is_layout?, :templatized?, to: :@_source
def title
locomotive/steam/liquid/drops/section.rb b/lib/locomotive/steam/liquid/drops/section.rb +1 -1
@@ @@ -97,7 +97,7 @@ module Locomotive
def text_inputs(settings)
settings.map do |input|
- %w(string text).include?(input['type']) ? input['id'] : nil
+ %w(text textarea).include?(input['type']) ? input['id'] : nil
end.compact
end
locomotive/steam/liquid/tags/concerns/section.rb b/lib/locomotive/steam/liquid/tags/concerns/section.rb +90 -0
@@ @@ -0,0 +1,90 @@
+ module Locomotive::Steam::Liquid::Tags::Concerns
+
+ module Section
+
+ private
+
+ def render_section(context, template, section, content)
+ context.stack do
+ # build a drop from the content and add it to the new context
+ context['section'] = Locomotive::Steam::Liquid::Drops::Section.new(
+ section,
+ content
+ )
+
+ begin
+ _render(context, template)
+ rescue Locomotive::Steam::ParsingRenderingError => e
+ e.file = section.name + ' [Section]'
+ raise e
+ end
+ end
+ end
+
+ def _render(context, template)
+ if context.registers[:live_editing]
+ editor_settings_lookup(template.root)
+ end
+
+ context.stack do
+ html = template.render(context)
+
+ tag_id = "locomotive-section-#{context['section'].id}"
+ tag_class = ['locomotive-section', context['section'].css_class].compact.join(' ')
+
+ %(<div id="#{tag_id}" class="#{tag_class}">#{html}</div>)
+ end
+ end
+
+ # in order to enable string/text synchronization with the editor:
+ # - find variables like {{ section.settings.<id> }} or {{ block.settings.<id> }}
+ # - once found, get the closest tag
+ # - add custom data attributes to it
+ def editor_settings_lookup(root)
+ previous_node = nil
+ new_nodelist = []
+
+ root.nodelist.each_with_index do |node, index|
+ if node.is_a?(::Liquid::Variable) && previous_node.is_a?(::Liquid::Token)
+ matches = node.raw.match(Locomotive::Steam::SECTIONS_SETTINGS_VARIABLE_REGEXP)
+
+ # is a section setting variable?
+ if matches && matches[:id] && wrapped_around_tag?(index, root.nodelist)
+ # open the closest HTML tag
+ previous_node.gsub!(/>(\s*)\z/, '\1')
+
+ # here we go, add a liquid variable!
+ new_nodelist.push(::Liquid::Variable.new(
+ "section.editor_setting_data.#{matches[:id]}",
+ node.instance_variable_get(:@options))
+ )
+
+ # close the tag
+ new_nodelist.push(::Liquid::Token.new('>', previous_node.line_number))
+ end
+ elsif node.respond_to?(:nodelist)
+ editor_settings_lookup(node)
+ end
+
+ new_nodelist.push(node)
+
+ previous_node = node
+ end
+
+ root.instance_variable_set(:@nodelist, new_nodelist)
+ end
+
+ def wrapped_around_tag?(index, nodelist)
+ return false if index + 1 >= nodelist.size
+
+ previous_node = nodelist[index - 1]
+ next_node = nodelist[index + 1]
+
+ return false unless next_node.is_a?(::Liquid::Token)
+
+ (previous_node =~ /\>\s*\z/).present? && (next_node =~ /\A\s*\</).present?
+ end
+
+ end
+
+ end
locomotive/steam/liquid/tags/section.rb b/lib/locomotive/steam/liquid/tags/section.rb +10 -87
@@ @@ -4,6 +4,8 @@ module Locomotive
module Tags
class Section < ::Liquid::Include
+ include Concerns::Section
+
def parse(tokens)
ActiveSupport::Notifications.instrument('steam.parse.section', name: evaluate_section_name)
end
@@ @@ -12,94 +14,26 @@ module Locomotive
# @options doesn't include the page key if cache is on
@options[:page] = context.registers[:page]
- # 1. get the type/slug of the section
+ # get the type/slug of the section
@section_type = evaluate_section_name(context)
@template_name = "sections-#{@section_type}"
- # 2. get the section
- section = find_section(context)
+ section = find_section(context)
+ template = load_cached_partial(context)
- # 3. if the tag is called by the Section middleware, use the content
+ # if the tag is called by the Section middleware, use the content
# from the request.
- section_content = context.registers[:_section_content]
+ content = context.registers[:_section_content]
- # 4. since it's considered as static and if no content, get the
+ # since it's considered as static and if no content, get the
# content from the current site.
- section_content ||= context['site']&.sections_content&.fetch(@section_type, nil)
-
- puts section_content.inspect
+ content ||= context['site']&.sections_content&.fetch(@section_type, nil)
- # 5. enhance the context by setting the "section" variable
- context['section'] = Locomotive::Steam::Liquid::Drops::Section.new(
- section,
- section_content
- )
-
- begin
- _render(context)
- rescue Locomotive::Steam::ParsingRenderingError => e
- e.file = @template_name + ' [Section]'
- raise e
- end
+ render_section(context, template, section, content)
end
private
- def _render(context)
- partial = load_cached_partial(context)
-
- if context.registers[:live_editing]
- editor_settings_lookup(partial.root)
- end
-
- context.stack do
- html = partial.render(context)
-
- tag_id = "locomotive-section-#{context['section'].id}"
- tag_class = ['locomotive-section', context['section'].css_class].join(' ')
-
- %(<div id="#{tag_id}" class="#{tag_class}">#{html}</div>)
- end
- end
-
- # in order to enable string/text synchronization with the editor:
- # - find variables like {{ section.settings.<id> }} or {{ block.settings.<id> }}
- # - once found, get the closest tag
- # - add custom data attributes to it
- def editor_settings_lookup(root)
- previous_node = nil
- new_nodelist = []
-
- root.nodelist.each_with_index do |node, index|
- if node.is_a?(::Liquid::Variable) && previous_node.is_a?(::Liquid::Token)
- matches = node.raw.match(SECTIONS_SETTINGS_VARIABLE_REGEXP)
-
- # is a section setting variable?
- if matches && matches[:id] && wrapped_around_tag?(index, root.nodelist)
- # open the closest HTML tag
- previous_node.gsub!(/>(\s*)\z/, '\1')
-
- # here we go, add a liquid variable!
- new_nodelist.push(::Liquid::Variable.new(
- "section.editor_setting_data.#{matches[:id]}",
- node.instance_variable_get(:@options))
- )
-
- # close the tag
- new_nodelist.push(::Liquid::Token.new('>', previous_node.line_number))
- end
- elsif node.respond_to?(:nodelist)
- editor_settings_lookup(node)
- end
-
- new_nodelist.push(node)
-
- previous_node = node
- end
-
- root.instance_variable_set(:@nodelist, new_nodelist)
- end
-
def read_template_from_file_system(context)
section = find_section(context)
raise SectionNotFound.new("Section with slug '#{@section_type}' was not found") if section.nil?
@@ @@ -116,17 +50,6 @@ module Locomotive
@template_name
end
- def wrapped_around_tag?(index, nodelist)
- return false if index + 1 >= nodelist.size
-
- previous_node = nodelist[index - 1]
- next_node = nodelist[index + 1]
-
- return false unless next_node.is_a?(::Liquid::Token)
-
- (previous_node =~ /\>\s*\z/).present? && (next_node =~ /\A\s*\</).present?
- end
-
end
::Liquid::Template.register_tag('section'.freeze, Section)
locomotive/steam/liquid/tags/sections_dropzone.rb b/lib/locomotive/steam/liquid/tags/sections_dropzone.rb +54 -0
@@ @@ -0,0 +1,54 @@
+ module Locomotive
+ module Steam
+ module Liquid
+
+ module Tags
+
+ class SectionsDropzone < ::Liquid::Tag
+
+ include Concerns::Section
+
+ def parse(tokens)
+ ActiveSupport::Notifications.instrument('steam.parse.sections_dropzone')
+ end
+
+ def render(context)
+ section_contents = context['page']&.sections_content || []
+
+ section_contents.each_with_index.map do |content, index|
+ # find the liquid source of the section
+ section = find_section(context, content['type'])
+
+ next if section.nil? # the section doesn't exist anymore?
+
+ # assign a new id to the section
+ content['id'] = index
+
+ # parse the template of the section
+ template = build_template(section)
+
+ render_section(context, template, section, content)
+ end
+ end
+
+ private
+
+ def find_section(context, type)
+ # TODO: add some cache (useful if there are sections with the same type)
+ context.registers[:services].section_finder.find(type)
+ end
+
+ def build_template(section)
+ # TODO: add some cache here (useful if there are sections with the same type)
+ ::Liquid::Template.parse(section.liquid_source, @options)
+ end
+
+ end
+
+ ::Liquid::Template.register_tag('sections_dropzone'.freeze, SectionsDropzone)
+
+ end
+
+ end
+ end
+ end
locomotive/steam/middlewares/section.rb b/lib/locomotive/steam/middlewares/section.rb +3 -4
@@ @@ -6,7 +6,7 @@ module Locomotive::Steam
include Concerns::LiquidContext
def _call
- if section_type = get_section_type(env['PATH_INFO'])
+ if section_type = get_section_type
html = render(section_type)
render_response(html, 200)
end
@@ @@ -14,9 +14,8 @@ module Locomotive::Steam
private
- def get_section_type(path_info)
- matchs = path_info.match(/^\/_sections\/(?<section_type>[a-z0-9]+$)/)
- matchs['section_type'] if matchs
+ def get_section_type
+ request.get_header('HTTP_LOCOMOTIVE_SECTION_TYPE')
end
def section_finder
spec/unit/liquid/tags/section_spec.rb +4 -3
@@ @@ -18,12 +18,12 @@ describe Locomotive::Steam::Liquid::Tags::Section do
type: 'header',
class: 'my-awesome-header',
settings: [
- { id: 'brand', type: 'string', label: 'Brand' },
+ { id: 'brand', type: 'text', label: 'Brand' },
{ id: 'image', type: 'image_picker' }
],
blocks: [
{ type: 'menu_item', settings: [
- { id: 'title', type: 'string' },
+ { id: 'title', type: 'text' },
{ id: 'image', type: 'image_picker' }
]}
],
@@ @@ -72,7 +72,7 @@ describe Locomotive::Steam::Liquid::Tags::Section do
it { is_expected.to eq 'Locomotive <div id="locomotive-section-header" class="locomotive-section my-awesome-header"><a href="/" data-locomotive-editor-setting="section-header-block.42.title">Home</a></div>' }
- context 'with a non string type input' do
+ context 'with a non text type input' do
let(:liquid_source) { '{% for foo in section.blocks %}<a>{{ foo.settings.image }}</a>{% endfor %}' }
@@ @@ -87,6 +87,7 @@ describe Locomotive::Steam::Liquid::Tags::Section do
let(:live_editing) { false }
let(:liquid_source) { '{% action "Hello world" %}a.b(+}{% endaction %}' }
let(:section) { instance_double('section',
+ name: 'Hero',
liquid_source: liquid_source,
definition: { settings: [], blocks: [] }
)}
spec/unit/liquid/tags/sections_dropzone_spec.rb +103 -0
@@ @@ -0,0 +1,103 @@
+ require 'spec_helper'
+
+ describe Locomotive::Steam::Liquid::Tags::SectionsDropzone do
+
+ let(:services) { Locomotive::Steam::Services.build_instance(nil) }
+ let(:finder) { services.section_finder }
+ let(:source) { '{% sections_dropzone %}' }
+ let(:live_editing) { true }
+ let(:page) { liquid_instance_double('Page', sections_content: content) }
+ let(:assigns) { { 'page' => page } }
+ let(:registers) { { services: services, live_editing: live_editing } }
+ let(:context) { ::Liquid::Context.new(assigns, {}, registers) }
+
+ describe 'rendering' do
+
+ subject { render_template(source, context) }
+
+ context 'no sections' do
+
+ let(:content) { [] }
+
+ it 'renders an empty string' do
+ is_expected.to eq ''
+ end
+
+ end
+
+ context 'with sections' do
+
+ let(:content) { [
+ {
+ type: 'hero',
+ settings: { title: 'Hello world' },
+ blocks: []
+ }.deep_stringify_keys,
+ {
+ type: 'slideshow',
+ settings: {},
+ blocks: [{ settings: { title: 'Slide 1' } }, { settings: { title: 'Slide 2' } }]
+ }.deep_stringify_keys
+ ] }
+
+ let(:hero_source) { %(<h1>{{ section.settings.title }}</h1>) }
+ let(:slideshow_source) { %({% for block in section.blocks %}<p>{{ block.settings.title }}</p>{% endfor %}) }
+
+ let(:hero_section) {
+ instance_double('Hero',
+ slug: 'hero',
+ type: 'hero',
+ definition: { settings: [{ id: 'title', type: 'text' }], blocks: [] }.deep_stringify_keys,
+ liquid_source: hero_source)
+ }
+ let(:slideshow_section) {
+ instance_double('Slideshow',
+ slug: 'slideshow',
+ type: 'slideshow',
+ definition: {
+ settings: [],
+ blocks: [{ settings: [{ id: 'title', type: 'text' }] }]
+ }.deep_stringify_keys,
+ liquid_source: slideshow_source) }
+
+ before do
+ allow(finder).to receive(:find).with('hero').and_return(hero_section)
+ allow(finder).to receive(:find).with('slideshow').and_return(slideshow_section)
+ end
+
+ it 'renders the list of sections' do
+ is_expected.to eq <<-HTML
+ <div id="locomotive-section-0" class="locomotive-section">
+ <h1 data-locomotive-editor-setting="section-0.title">Hello world</h1>
+ </div>
+ <div id="locomotive-section-1" class="locomotive-section">
+ <p data-locomotive-editor-setting="section-1-block.0.title">Slide 1</p>
+ <p data-locomotive-editor-setting="section-1-block.1.title">Slide 2</p>
+ </div>
+ HTML
+ .strip.gsub(/\n\s+/, '')
+ end
+
+ context 'live editing is off' do
+
+ let(:live_editing) { false }
+
+ it 'renders the list of sections' do
+ is_expected.to eq <<-HTML
+ <div id="locomotive-section-0" class="locomotive-section">
+ <h1>Hello world</h1>
+ </div>
+ <div id="locomotive-section-1" class="locomotive-section">
+ <p>Slide 1</p>
+ <p>Slide 2</p>
+ </div>
+ HTML
+ .strip.gsub(/\n\s+/, '')
+ end
+
+ end
+
+ end
+
+ end
+ end
spec/unit/middlewares/section_spec.rb +4 -3
@@ @@ -8,7 +8,7 @@ require_relative '../../../lib/locomotive/steam/middlewares/section'
describe Locomotive::Steam::Middlewares::Section do
let(:app) { ->(env) { [200, env, 'app'] }}
- let(:url) { 'http://example.com/_sections/header' }
+ let(:url) { 'http://example.com/foo/bar' }
let(:env) { env_for(url, 'steam.site' => site) }
let(:drop) { liquid_instance_double('SiteDrop', sections_content: { 'header' => { 'settings' => { 'name' => 'HTML' } } }) }
@@ @@ -30,6 +30,7 @@ describe Locomotive::Steam::Middlewares::Section do
env['steam.locale'] = :en
env['steam.liquid_assigns'] = {}
env['steam.request'] = Rack::Request.new(env)
+ env['steam.request'].add_header('HTTP_LOCOMOTIVE_SECTION_TYPE', 'header')
allow(section_finder).to receive(:find).with('header').and_return(section)
end
@@ @@ -42,7 +43,7 @@ describe Locomotive::Steam::Middlewares::Section do
is_expected.to eq [
200,
{ "Content-Type" => "text/html" },
- [%(<div id="locomotive-section-fancy_section" class="locomotive-section ">Here some HTML</div>)]
+ [%(<div id="locomotive-section-fancy_section" class="locomotive-section">Here some HTML</div>)]
]
end
@@ @@ -58,7 +59,7 @@ describe Locomotive::Steam::Middlewares::Section do
is_expected.to eq [
200,
{ "Content-Type" => "text/html" },
- [%(<div id="locomotive-section-fancy_section" class="locomotive-section ">Here some modified HTML</div>)]
+ [%(<div id="locomotive-section-fancy_section" class="locomotive-section">Here some modified HTML</div>)]
]
end