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 | |