require 'sinatra' require 'honeybadger' if ENV['HONEYBADGER_API_KEY'] require 'uri' require 'redcarpet' require 'dry/inflector' module Voom module Presenters module WebClient class App < Sinatra::Base include Trace set :root, File.expand_path('../../../../..', __FILE__) set :router_, WebClient::Router set :bind, '0.0.0.0' set :views, Proc.new {File.join(root, "views", ENV['VIEW_ENGINE'] || 'mdc')} set :dump_errors, false set :protection, :except => :frame_options configure do enable :logging end helpers Helpers::FormHelpers helpers Helpers::PaddingHelpers helpers Helpers::ExpandHash helpers do def render_component(scope, comp, components, index) ComponentRenderer.new(comp, render: method(:render), scope: scope, components: components, index: index).render end def markdown(text) unless @markdown renderer = CustomRender.new(hard_wrap: false, filter_html: true) options = { autolink: false, no_intra_emphasis: true, fenced_code_blocks: true, lax_html_blocks: true, strikethrough: true, superscript: true, disable_indented_code_blocks: true } @markdown = Redcarpet::Markdown.new(renderer, options) end @markdown.render(text) end def inflector @inflector ||= Dry::Inflector.new end def eq(attrib, value) attrib.to_s == value.to_s end def h(text) Rack::Utils.escape_html(text) end def include?(array, value) array.map(&:to_s).include?(value.to_s) end def includes_one?(array1, array2) (array2.map(&:to_sym)-array1.map(&:to_sym)).size != array2.size end def unique_id(comp) "#{comp.id}-#{SecureRandom.hex(4)}" end def expand_text(text, markdown: true) if markdown self.markdown(Array(text).join("\n\n")) #.gsub("\n\n", "
") else Array(text).join('
') end end def color_classname(comp, affects = nil, color_attr = :color) color = comp&.public_send(color_attr) return unless color return "v-#{comp.type}__primary" if eq(color, :primary) return "v-#{comp.type}__secondary" if eq(color, :secondary) "v-#{affects}color__#{color}" end def color_style(comp, affects = nil, color_attr = :color) color = comp.public_send(color_attr) "#{affects}color: #{color};" unless %w(primary secondary).include?(color.to_s) || color.nil? end def snake_to_camel(hash, except: []) Hash[hash.map {|k, v| next [k, v] if except.include?(k) new_key = k.to_s.split('_').collect(&:capitalize).join new_key[0] = new_key[0].downcase [new_key, v]} ] end def to_hash(ostruct_or_hash) {}.tap do |h| ostruct_or_hash.to_h.each {|key, value| h[key.to_sym] = transform(value)} end end def transform(thing) case thing when OpenStruct to_hash(thing) when Array thing.map {|v| transform(v)} else thing end end def plugin_headers(pom) PluginHeaders.new(pom: pom, render: method(:render)).render end def custom_css(path, host=nil) CustomCss.new(path, root: Presenters::Settings.config.presenters.root, host: host).render end def custom_js custom_js_path = Presenters::Settings.config.presenters.web_client.custom_js Dir.glob(custom_js_path).map do |file| _build_script_tag_(file) end.join("\n") if custom_js_path end def _build_script_tag_(path) (<<~JS) JS end end get '/' do pass unless Presenters::App.registered?('index') presenter = Presenters::App['index'].call render_presenter(presenter) end get '/:_presenter_' do fq_presenter = [params[:_presenter_], 'index'].join(':') pass unless Presenters::App.registered?(params[:_presenter_]) || Presenters::App.registered?(fq_presenter) presenter = (Presenters::App.registered?(fq_presenter) ? Presenters::App[fq_presenter] : Presenters::App[params[:_presenter_]]).call render_presenter(presenter) end get '/:_namespace1_/:_presenter_' do fq_presenter = [params[:_namespace1_], params[:_presenter_]].join(':') pass unless Presenters::App.registered?(fq_presenter) presenter = Presenters::App[fq_presenter].call render_presenter(presenter) end get '/:_namespace1_/:_namespace2_/:_presenter_' do fq_presenter = [params[:_namespace1_], params[:_namespace2_], params[:_presenter_]].join(':') pass unless Presenters::App.registered?(fq_presenter) presenter = Presenters::App[fq_presenter].call render_presenter(presenter) end # Forms engine demo post '/__post__/:presenter' do @pom = JSON.parse(request.body.read, object_class: OpenStruct) @grid_nesting = Integer(params[:grid_nesting] || 0) @base_url = request.base_url layout = !(request.env['HTTP_X_NO_LAYOUT'] == 'true') erb :web, layout: layout end private # analogous to Voom::Presenters::Api::App#render_presenter def render_presenter(presenter) @grid_nesting = Integer(params[:grid_nesting] || 0) begin before_render = Presenters::Settings.config.presenters.before_render render_instead, ctx = before_render .lazy .map { |p| p.call(request) } .detect(&:itself) if Presenters::App.registered?(render_instead) presenter = Presenters::App[render_instead].call end p = params.merge(ctx || {}) @pom = presenter.expand(router: router, context: prepare_context(p)) @base_url = request.base_url layout = !(request.env['HTTP_X_NO_LAYOUT'] == 'true') response.headers['X-Frame-Options'] = ENV['ALLOWALL_FRAME_OPTIONS'] || presenter.options.fetch(:allow_all_frame_options, false) ? 'ALLOWALL' : 'SAMEORIGIN' erb :web, layout: layout rescue StandardError => e Presenters::Settings.config.presenters.error_logger.call( @env['rack.errors'], e, params, presenter.name ) raise e rescue Presenters::Errors::Unprocessable => e content_type :json status 422 JSON.dump({error: e.message}) end end def router settings.router_.new(base_url: "#{request.base_url}") end def prepare_context(base_params = params) prepare_context = Presenters::Settings.config.presenters.web_client.prepare_context.dup prepare_context.push(method(:scrub_context)) context = base_params.dup prepare_context.reduce(context) do |params, context_proc| context = context_proc.call(params, session, env) end context end def scrub_context(params, _session, _env) %i(splat captures grid_nesting input_tag).each do |key| params.delete(key) {params.delete(key.to_s)} end params end end end end end