plum-cms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +252 -0
- data/app/assets/builds/tailwind.css +2 -0
- data/app/assets/images/plum-mark.svg +7 -0
- data/app/assets/images/table-needs-logo.svg +83 -0
- data/app/assets/javascripts/plum/application.js +15 -0
- data/app/assets/stylesheets/application.css +10 -0
- data/app/assets/stylesheets/plum/control_panel.css +2 -0
- data/app/assets/tailwind/application.css +47 -0
- data/app/controllers/plum/application_controller.rb +34 -0
- data/app/controllers/plum/cp/assets_controller.rb +84 -0
- data/app/controllers/plum/cp/base_controller.rb +45 -0
- data/app/controllers/plum/cp/content_types_controller.rb +59 -0
- data/app/controllers/plum/cp/dashboard_controller.rb +10 -0
- data/app/controllers/plum/cp/entries_controller.rb +240 -0
- data/app/controllers/plum/cp/form_definitions_controller.rb +105 -0
- data/app/controllers/plum/cp/form_submissions_controller.rb +26 -0
- data/app/controllers/plum/cp/globals_controller.rb +84 -0
- data/app/controllers/plum/cp/nav_items_controller.rb +68 -0
- data/app/controllers/plum/cp/nav_menus_controller.rb +56 -0
- data/app/controllers/plum/cp/site_settings_controller.rb +87 -0
- data/app/controllers/plum/cp/taxonomies_controller.rb +56 -0
- data/app/controllers/plum/cp/terms_controller.rb +55 -0
- data/app/controllers/plum/cp/theme_previews_controller.rb +27 -0
- data/app/controllers/plum/cp/themes_controller.rb +98 -0
- data/app/controllers/plum/form_submissions_controller.rb +44 -0
- data/app/controllers/plum/pages_controller.rb +161 -0
- data/app/controllers/plum/sessions_controller.rb +25 -0
- data/app/controllers/plum/theme_assets_controller.rb +14 -0
- data/app/helpers/application_helper.rb +2 -0
- data/app/helpers/markdown_helper.rb +21 -0
- data/app/helpers/plum/cp/assets_helper.rb +21 -0
- data/app/helpers/plum/cp/theme_settings_helper.rb +26 -0
- data/app/javascript/application.js +18 -0
- data/app/javascript/controllers/application.js +9 -0
- data/app/javascript/controllers/index.js +4 -0
- data/app/javascript/controllers/plum/blocks_editor_controller.js +275 -0
- data/app/javascript/controllers/plum/blueprint_controller.js +120 -0
- data/app/javascript/controllers/plum/form_fields_controller.js +84 -0
- data/app/javascript/controllers/plum/image_picker_controller.js +143 -0
- data/app/javascript/controllers/plum/theme_settings_controller.js +17 -0
- data/app/models/plum/application_record.rb +5 -0
- data/app/models/plum/asset.rb +69 -0
- data/app/models/plum/content_type.rb +35 -0
- data/app/models/plum/entry.rb +67 -0
- data/app/models/plum/entry_term.rb +8 -0
- data/app/models/plum/form_definition.rb +68 -0
- data/app/models/plum/form_submission.rb +76 -0
- data/app/models/plum/global.rb +24 -0
- data/app/models/plum/nav_item.rb +58 -0
- data/app/models/plum/nav_menu.rb +24 -0
- data/app/models/plum/site.rb +112 -0
- data/app/models/plum/site_scoped.rb +17 -0
- data/app/models/plum/site_setting.rb +35 -0
- data/app/models/plum/taxonomy.rb +24 -0
- data/app/models/plum/term.rb +23 -0
- data/app/models/plum/user.rb +16 -0
- data/app/services/plum/base_blocks.rb +161 -0
- data/app/services/plum/block_editor_config.rb +65 -0
- data/app/services/plum/block_library.rb +31 -0
- data/app/services/plum/builder_renderer.rb +63 -0
- data/app/services/plum/field_expander.rb +78 -0
- data/app/services/plum/form_renderer.rb +110 -0
- data/app/services/plum/liquid_context.rb +335 -0
- data/app/services/plum/liquid_filters.rb +58 -0
- data/app/services/plum/liquid_renderer.rb +49 -0
- data/app/services/plum/theme.rb +160 -0
- data/app/services/plum/theme_asset_path.rb +31 -0
- data/app/services/plum/theme_package_installer.rb +287 -0
- data/app/services/plum/theme_registry.rb +63 -0
- data/app/services/plum/theme_resolver.rb +55 -0
- data/app/services/plum/theme_settings_params.rb +54 -0
- data/app/themes/bagel-boy/assets/screenshot.svg +92 -0
- data/app/themes/bagel-boy/assets/theme.css +184 -0
- data/app/themes/bagel-boy/blocks/hours.liquid +4 -0
- data/app/themes/bagel-boy/blocks/menu_item.liquid +7 -0
- data/app/themes/bagel-boy/layouts/base.liquid +64 -0
- data/app/themes/bagel-boy/templates/collections/posts.liquid +32 -0
- data/app/themes/bagel-boy/templates/entries/_default.liquid +13 -0
- data/app/themes/bagel-boy/templates/entries/landing.liquid +4 -0
- data/app/themes/bagel-boy/templates/entries/pages.liquid +18 -0
- data/app/themes/bagel-boy/templates/entries/posts.liquid +8 -0
- data/app/themes/bagel-boy/templates/index.liquid +10 -0
- data/app/themes/bagel-boy/templates/search.liquid +28 -0
- data/app/themes/bagel-boy/theme.yml +47 -0
- data/app/themes/bagel-shop/assets/screenshot.svg +28 -0
- data/app/themes/bagel-shop/assets/theme.css +7 -0
- data/app/themes/bagel-shop/layouts/base.liquid +507 -0
- data/app/themes/bagel-shop/templates/entries/_default.liquid +15 -0
- data/app/themes/bagel-shop/templates/entries/pages.liquid +12 -0
- data/app/themes/bagel-shop/templates/entries/posts.liquid +19 -0
- data/app/themes/bagel-shop/templates/index.liquid +55 -0
- data/app/themes/bagel-shop/theme.yml +30 -0
- data/app/themes/default/assets/screenshot.svg +24 -0
- data/app/themes/default/assets/theme.css +89 -0
- data/app/themes/default/layouts/base.liquid +117 -0
- data/app/themes/default/templates/collections/_default.liquid +16 -0
- data/app/themes/default/templates/collections/posts.liquid +27 -0
- data/app/themes/default/templates/entries/_default.liquid +26 -0
- data/app/themes/default/templates/entries/landing.liquid +5 -0
- data/app/themes/default/templates/entries/pages.liquid +17 -0
- data/app/themes/default/templates/entries/posts.liquid +20 -0
- data/app/themes/default/templates/index.liquid +22 -0
- data/app/themes/default/templates/search.liquid +28 -0
- data/app/themes/default/templates/taxonomies/_default.liquid +25 -0
- data/app/themes/default/templates/taxonomies/_default_index.liquid +14 -0
- data/app/themes/default/theme.yml +23 -0
- data/app/views/layouts/plum/cp.html.erb +138 -0
- data/app/views/layouts/plum/session.html.erb +43 -0
- data/app/views/plum/cp/assets/_form.html.erb +44 -0
- data/app/views/plum/cp/assets/edit.html.erb +14 -0
- data/app/views/plum/cp/assets/index.html.erb +52 -0
- data/app/views/plum/cp/assets/new.html.erb +9 -0
- data/app/views/plum/cp/content_types/_form.html.erb +113 -0
- data/app/views/plum/cp/content_types/edit.html.erb +8 -0
- data/app/views/plum/cp/content_types/index.html.erb +48 -0
- data/app/views/plum/cp/content_types/new.html.erb +5 -0
- data/app/views/plum/cp/content_types/show.html.erb +59 -0
- data/app/views/plum/cp/dashboard/show.html.erb +50 -0
- data/app/views/plum/cp/entries/_form.html.erb +196 -0
- data/app/views/plum/cp/entries/edit.html.erb +9 -0
- data/app/views/plum/cp/entries/index.html.erb +51 -0
- data/app/views/plum/cp/entries/new.html.erb +5 -0
- data/app/views/plum/cp/form_definitions/_form.html.erb +90 -0
- data/app/views/plum/cp/form_definitions/edit.html.erb +11 -0
- data/app/views/plum/cp/form_definitions/index.html.erb +43 -0
- data/app/views/plum/cp/form_definitions/new.html.erb +5 -0
- data/app/views/plum/cp/form_definitions/show.html.erb +70 -0
- data/app/views/plum/cp/form_submissions/show.html.erb +20 -0
- data/app/views/plum/cp/globals/_form.html.erb +44 -0
- data/app/views/plum/cp/globals/edit.html.erb +11 -0
- data/app/views/plum/cp/globals/index.html.erb +41 -0
- data/app/views/plum/cp/globals/new.html.erb +5 -0
- data/app/views/plum/cp/globals/show.html.erb +14 -0
- data/app/views/plum/cp/nav_items/_form.html.erb +56 -0
- data/app/views/plum/cp/nav_items/edit.html.erb +11 -0
- data/app/views/plum/cp/nav_items/new.html.erb +6 -0
- data/app/views/plum/cp/nav_menus/_form.html.erb +33 -0
- data/app/views/plum/cp/nav_menus/_nav_item.html.erb +24 -0
- data/app/views/plum/cp/nav_menus/edit.html.erb +11 -0
- data/app/views/plum/cp/nav_menus/index.html.erb +41 -0
- data/app/views/plum/cp/nav_menus/new.html.erb +5 -0
- data/app/views/plum/cp/nav_menus/show.html.erb +24 -0
- data/app/views/plum/cp/shared/_icon.html.erb +39 -0
- data/app/views/plum/cp/shared/_image_picker.html.erb +38 -0
- data/app/views/plum/cp/site_settings/edit.html.erb +145 -0
- data/app/views/plum/cp/site_settings/show.html.erb +62 -0
- data/app/views/plum/cp/taxonomies/_form.html.erb +38 -0
- data/app/views/plum/cp/taxonomies/edit.html.erb +2 -0
- data/app/views/plum/cp/taxonomies/index.html.erb +38 -0
- data/app/views/plum/cp/taxonomies/new.html.erb +2 -0
- data/app/views/plum/cp/taxonomies/show.html.erb +46 -0
- data/app/views/plum/cp/terms/_form.html.erb +30 -0
- data/app/views/plum/cp/terms/edit.html.erb +2 -0
- data/app/views/plum/cp/terms/new.html.erb +2 -0
- data/app/views/plum/cp/themes/index.html.erb +160 -0
- data/app/views/plum/form_mailer/submission_notification.html.erb +13 -0
- data/app/views/plum/form_mailer/submission_notification.text.erb +7 -0
- data/app/views/plum/sessions/new.html.erb +20 -0
- data/compat/plum-cms.rb +1 -0
- data/config/locales/en.yml +31 -0
- data/config/plum_importmap.rb +4 -0
- data/config/plum_routes.rb +34 -0
- data/db/engine_migrate/20260101000001_create_plum_tables.rb +154 -0
- data/lib/generators/plum/install/install_generator.rb +54 -0
- data/lib/generators/plum/install/templates/plum_initializer.rb +37 -0
- data/lib/plum/configuration.rb +88 -0
- data/lib/plum/content_source.rb +45 -0
- data/lib/plum/content_source_context.rb +30 -0
- data/lib/plum/content_source_registry.rb +120 -0
- data/lib/plum/engine.rb +30 -0
- data/lib/plum/liquid_tags/form_tag.rb +17 -0
- data/lib/plum/version.rb +7 -0
- data/lib/plum.rb +11 -0
- data/vendor/javascript/lexxy.js +11859 -0
- metadata +405 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module Plum
|
|
2
|
+
class FormRenderer
|
|
3
|
+
FIELD_CLASSES = "plum-form-field".freeze
|
|
4
|
+
|
|
5
|
+
def initialize(form)
|
|
6
|
+
@form = form
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def render
|
|
10
|
+
return "" if form.blank?
|
|
11
|
+
|
|
12
|
+
<<~HTML
|
|
13
|
+
<form class="plum-form" method="post" action="#{h(form['action'])}" data-plum-form-handle="#{h(form['handle'])}">
|
|
14
|
+
#{hidden_inputs}
|
|
15
|
+
#{fields_html}
|
|
16
|
+
<button class="plum-form-submit" type="submit">Submit</button>
|
|
17
|
+
</form>
|
|
18
|
+
HTML
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :form
|
|
24
|
+
|
|
25
|
+
def hidden_inputs
|
|
26
|
+
[
|
|
27
|
+
hidden_input(form["csrf_param"], form["csrf_token"]),
|
|
28
|
+
hidden_input("return_to", form["return_to"])
|
|
29
|
+
].compact.join("\n ")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fields_html
|
|
33
|
+
Array(form["fields"]).map { |field| field_html(field) }.join("\n ")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def field_html(field)
|
|
37
|
+
handle = field["handle"].to_s
|
|
38
|
+
type = field["type"].to_s
|
|
39
|
+
label = field_label(field)
|
|
40
|
+
input_id = "plum_form_#{form['handle']}_#{handle}"
|
|
41
|
+
|
|
42
|
+
<<~HTML.strip
|
|
43
|
+
<div class="#{FIELD_CLASSES}">
|
|
44
|
+
<label for="#{h(input_id)}">#{h(label)}</label>
|
|
45
|
+
#{input_html(field, input_id)}
|
|
46
|
+
</div>
|
|
47
|
+
HTML
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def input_html(field, input_id)
|
|
51
|
+
case field["type"]
|
|
52
|
+
when "textarea"
|
|
53
|
+
textarea_html(field, input_id)
|
|
54
|
+
when "select"
|
|
55
|
+
select_html(field, input_id)
|
|
56
|
+
when "checkbox"
|
|
57
|
+
checkbox_html(field, input_id)
|
|
58
|
+
else
|
|
59
|
+
text_input_html(field, input_id)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def text_input_html(field, input_id)
|
|
64
|
+
type = field["type"] == "email" ? "email" : "text"
|
|
65
|
+
|
|
66
|
+
%(<input type="#{type}" id="#{h(input_id)}" name="#{field_name(field)}"#{required_attribute(field)}>)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def textarea_html(field, input_id)
|
|
70
|
+
%(<textarea id="#{h(input_id)}" name="#{field_name(field)}"#{required_attribute(field)}></textarea>)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def select_html(field, input_id)
|
|
74
|
+
options = Array(field["options"]).map do |option|
|
|
75
|
+
%(<option value="#{h(option)}">#{h(option)}</option>)
|
|
76
|
+
end.join
|
|
77
|
+
|
|
78
|
+
%(<select id="#{h(input_id)}" name="#{field_name(field)}"#{required_attribute(field)}><option value=""></option>#{options}</select>)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def checkbox_html(field, input_id)
|
|
82
|
+
<<~HTML.squish
|
|
83
|
+
<input type="hidden" name="#{field_name(field)}" value="0">
|
|
84
|
+
<input type="checkbox" id="#{h(input_id)}" name="#{field_name(field)}" value="1"#{required_attribute(field)}>
|
|
85
|
+
HTML
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def hidden_input(name, value)
|
|
89
|
+
return if name.blank? || value.blank?
|
|
90
|
+
|
|
91
|
+
%(<input type="hidden" name="#{h(name)}" value="#{h(value)}">)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def field_name(field)
|
|
95
|
+
h("form_submission[data][#{field['handle']}]")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def field_label(field)
|
|
99
|
+
field["label"].presence || field["handle"].to_s.humanize
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def required_attribute(field)
|
|
103
|
+
ActiveModel::Type::Boolean.new.cast(field["required"]) ? " required" : ""
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def h(value)
|
|
107
|
+
ERB::Util.html_escape(value.to_s)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
|
|
3
|
+
module Plum
|
|
4
|
+
class LiquidContext
|
|
5
|
+
MAX_RELATIONSHIP_DEPTH = 2
|
|
6
|
+
|
|
7
|
+
def initialize(controller:, site:, entry: nil, theme_name: nil, theme_settings: nil)
|
|
8
|
+
@controller = controller
|
|
9
|
+
@site = site
|
|
10
|
+
@entry = entry
|
|
11
|
+
@theme_name = theme_name
|
|
12
|
+
@theme_settings = theme_settings
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{
|
|
17
|
+
"site" => site_context,
|
|
18
|
+
"entry" => entry ? entry_context(entry, expand_blocks: true) : nil,
|
|
19
|
+
"entries" => entries_context,
|
|
20
|
+
"taxonomies" => taxonomies_context,
|
|
21
|
+
"forms" => forms_context,
|
|
22
|
+
"globals" => globals_context,
|
|
23
|
+
"nav" => nav_context
|
|
24
|
+
}.compact.merge(Plum.content_sources_for(controller, site: site))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :controller, :site, :entry, :theme_name, :theme_settings
|
|
30
|
+
|
|
31
|
+
def site_context
|
|
32
|
+
@site_context ||= begin
|
|
33
|
+
settings = SiteSetting.instance(site)
|
|
34
|
+
{
|
|
35
|
+
"name" => settings.name,
|
|
36
|
+
"tagline" => settings.tagline,
|
|
37
|
+
"seo_title" => settings.seo_title,
|
|
38
|
+
"seo_description" => settings.seo_description,
|
|
39
|
+
"primary_color" => settings.primary_color,
|
|
40
|
+
"support_email" => settings.support_email,
|
|
41
|
+
"logo" => image_asset_context(settings.logo),
|
|
42
|
+
"theme_name" => theme_name.presence || settings.theme_name,
|
|
43
|
+
"theme_settings" => theme_settings || site.theme_settings,
|
|
44
|
+
"theme_asset_base_url" => theme_asset_base_url,
|
|
45
|
+
"custom_css" => site.custom_css,
|
|
46
|
+
"url" => public_root_path,
|
|
47
|
+
"meta_title" => settings.seo_title,
|
|
48
|
+
"meta_description" => settings.seo_description,
|
|
49
|
+
"powered_by_name" => Plum.configuration.powered_by_name,
|
|
50
|
+
"powered_by_url" => Plum.configuration.powered_by_url
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def entry_context(entry, relationship_depth: MAX_RELATIONSHIP_DEPTH, expand_blocks: false)
|
|
56
|
+
ctx = {
|
|
57
|
+
"title" => entry.title,
|
|
58
|
+
"slug" => entry.slug,
|
|
59
|
+
"published_at" => entry.published_at,
|
|
60
|
+
"url" => public_entry_path(entry),
|
|
61
|
+
"data" => entry_data_context(entry, relationship_depth: relationship_depth, expand_blocks: expand_blocks)
|
|
62
|
+
}
|
|
63
|
+
if entry.association(:terms).loaded? && entry.terms.any?
|
|
64
|
+
ctx["terms"] = entry.terms.group_by { |t| t.taxonomy.handle }.transform_values do |terms|
|
|
65
|
+
terms.map { |t| { "name" => t.name, "slug" => t.slug, "url" => "/#{t.taxonomy.slug}/#{t.slug}" } }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
ctx.merge!(adjacent_entry_context(entry)) if expand_blocks
|
|
69
|
+
ctx
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def entry_data_context(entry, relationship_depth:, expand_blocks: false)
|
|
73
|
+
data = (entry.data || {}).deep_dup
|
|
74
|
+
|
|
75
|
+
entry.content_type.fields.each do |field|
|
|
76
|
+
handle = field["handle"].to_s
|
|
77
|
+
next if handle.blank?
|
|
78
|
+
|
|
79
|
+
case field["type"]
|
|
80
|
+
when "image"
|
|
81
|
+
data[handle] = image_asset_context(data[handle])
|
|
82
|
+
when "rich_text"
|
|
83
|
+
data[handle] = resolve_rich_text(data[handle])
|
|
84
|
+
when "relationship"
|
|
85
|
+
data[handle] = relationship_entry_context(data[handle], relationship_depth: relationship_depth)
|
|
86
|
+
when "blocks"
|
|
87
|
+
data[handle] = expand_blocks ? blocks_html(data[handle]) : "".html_safe
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
data
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def taxonomies_context
|
|
95
|
+
@taxonomies_context ||= Taxonomy.for_site(site).includes(:terms).each_with_object({}) do |taxonomy, hash|
|
|
96
|
+
hash[taxonomy.handle] = {
|
|
97
|
+
"name" => taxonomy.name,
|
|
98
|
+
"handle" => taxonomy.handle,
|
|
99
|
+
"slug" => taxonomy.slug,
|
|
100
|
+
"terms" => taxonomy.terms.sort_by(&:position).map { |term|
|
|
101
|
+
{ "name" => term.name, "slug" => term.slug, "url" => "/#{taxonomy.slug}/#{term.slug}" }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def entries_context
|
|
108
|
+
@entries_context ||= begin
|
|
109
|
+
ContentType.for_site(site).each_with_object({}) do |content_type, hash|
|
|
110
|
+
hash[content_type.handle] = Array(live_entries_by_type[content_type.id]).map { |item| entry_context(item) }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def globals_context
|
|
116
|
+
@globals_context ||= Global.for_site(site).each_with_object({}) do |global, hash|
|
|
117
|
+
hash[global.handle] = global.data
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def forms_context
|
|
122
|
+
@forms_context ||= FormDefinition.for_site(site).each_with_object({}) do |form, hash|
|
|
123
|
+
hash[form.handle] = {
|
|
124
|
+
"name" => form.name,
|
|
125
|
+
"handle" => form.handle,
|
|
126
|
+
"fields" => form.form_fields,
|
|
127
|
+
"action" => public_form_path(form),
|
|
128
|
+
"csrf_token" => form_authenticity_token,
|
|
129
|
+
"csrf_param" => "authenticity_token",
|
|
130
|
+
"return_to" => controller.request.fullpath
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def nav_context
|
|
136
|
+
@nav_context ||= begin
|
|
137
|
+
items_by_menu = NavItem.for_site(site)
|
|
138
|
+
.includes(entry: [ :content_type, { terms: :taxonomy } ])
|
|
139
|
+
.group_by(&:nav_menu_id)
|
|
140
|
+
|
|
141
|
+
NavMenu.for_site(site).each_with_object({}) do |menu, hash|
|
|
142
|
+
menu_items = Array(items_by_menu[menu.id])
|
|
143
|
+
items_by_parent = menu_items.group_by(&:parent_id)
|
|
144
|
+
hash[menu.handle] = {
|
|
145
|
+
"name" => menu.name,
|
|
146
|
+
"handle" => menu.handle,
|
|
147
|
+
"items" => Array(items_by_parent[nil]).map { |item| nav_item_context(item, items_by_parent) }
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def nav_item_context(item, items_by_parent)
|
|
154
|
+
{
|
|
155
|
+
"label" => item.label,
|
|
156
|
+
"url" => nav_item_url(item),
|
|
157
|
+
"entry" => item.entry ? entry_context(item.entry) : nil,
|
|
158
|
+
"children" => Array(items_by_parent[item.id]).map { |child| nav_item_context(child, items_by_parent) }
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def nav_item_url(item)
|
|
163
|
+
item.entry ? public_entry_path(item.entry) : item.url
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def image_asset_context(value)
|
|
167
|
+
return if value.blank?
|
|
168
|
+
|
|
169
|
+
asset_cache[value.to_i]&.to_liquid
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def relationship_entry_context(value, relationship_depth:)
|
|
173
|
+
return if value.blank? || relationship_depth <= 0
|
|
174
|
+
|
|
175
|
+
related_entry = relationship_entry_cache[value.to_i]
|
|
176
|
+
return unless related_entry
|
|
177
|
+
|
|
178
|
+
entry_context(related_entry, relationship_depth: relationship_depth - 1)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
LEXXY_CSS_VARS = {
|
|
182
|
+
"var(--highlight-1)" => "rgb(136, 118, 38)",
|
|
183
|
+
"var(--highlight-2)" => "rgb(185, 94, 6)",
|
|
184
|
+
"var(--highlight-3)" => "rgb(207, 0, 0)",
|
|
185
|
+
"var(--highlight-4)" => "rgb(216, 28, 170)",
|
|
186
|
+
"var(--highlight-5)" => "rgb(144, 19, 254)",
|
|
187
|
+
"var(--highlight-6)" => "rgb(5, 98, 185)",
|
|
188
|
+
"var(--highlight-7)" => "rgb(17, 138, 15)",
|
|
189
|
+
"var(--highlight-8)" => "rgb(148, 82, 22)",
|
|
190
|
+
"var(--highlight-9)" => "rgb(102, 102, 102)",
|
|
191
|
+
"var(--highlight-bg-1)" => "rgba(229, 223, 6, 0.3)",
|
|
192
|
+
"var(--highlight-bg-2)" => "rgba(255, 185, 87, 0.3)",
|
|
193
|
+
"var(--highlight-bg-3)" => "rgba(255, 118, 118, 0.3)",
|
|
194
|
+
"var(--highlight-bg-4)" => "rgba(248, 137, 216, 0.3)",
|
|
195
|
+
"var(--highlight-bg-5)" => "rgba(190, 165, 255, 0.3)",
|
|
196
|
+
"var(--highlight-bg-6)" => "rgba(124, 192, 252, 0.3)",
|
|
197
|
+
"var(--highlight-bg-7)" => "rgba(140, 255, 129, 0.3)",
|
|
198
|
+
"var(--highlight-bg-8)" => "rgba(221, 170, 123, 0.3)",
|
|
199
|
+
"var(--highlight-bg-9)" => "rgba(200, 200, 200, 0.3)"
|
|
200
|
+
}.freeze
|
|
201
|
+
|
|
202
|
+
def resolve_rich_text(html)
|
|
203
|
+
return html if html.blank?
|
|
204
|
+
|
|
205
|
+
result = html.to_s
|
|
206
|
+
|
|
207
|
+
result.gsub!(/<action-text-attachment[^>]*>.*?<\/action-text-attachment>/m) do |tag|
|
|
208
|
+
url = tag[/url="([^"]*)"/, 1]
|
|
209
|
+
alt = tag[/alt="([^"]*)"/, 1] || ""
|
|
210
|
+
width = tag[/width="([^"]*)"/, 1]
|
|
211
|
+
height = tag[/height="([^"]*)"/, 1]
|
|
212
|
+
|
|
213
|
+
attrs = %(src="#{url}" alt="#{alt}")
|
|
214
|
+
attrs += %( width="#{width}") if width
|
|
215
|
+
attrs += %( height="#{height}") if height
|
|
216
|
+
%(<img #{attrs}>)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
LEXXY_CSS_VARS.each { |var, val| result.gsub!(var, val) }
|
|
220
|
+
|
|
221
|
+
result.gsub!(/<mark\b/, "<span")
|
|
222
|
+
result.gsub!(%r{</mark>}, "</span>")
|
|
223
|
+
|
|
224
|
+
add_heading_ids(result)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def add_heading_ids(html)
|
|
228
|
+
fragment = Nokogiri::HTML.fragment(html)
|
|
229
|
+
used_ids = Hash.new(0)
|
|
230
|
+
|
|
231
|
+
fragment.css("h2, h3").each do |heading|
|
|
232
|
+
base_id = heading["id"].presence || heading.text.parameterize.presence || "section"
|
|
233
|
+
used_ids[base_id] += 1
|
|
234
|
+
heading["id"] = used_ids[base_id] == 1 ? base_id : "#{base_id}-#{used_ids[base_id]}"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
fragment.to_html
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def adjacent_entry_context(current_entry)
|
|
241
|
+
siblings = Array(live_entries_by_type[current_entry.content_type_id]).sort_by do |item|
|
|
242
|
+
[ item.data&.fetch("position", nil).to_i, item.title.to_s ]
|
|
243
|
+
end
|
|
244
|
+
index = siblings.index { |item| item.id == current_entry.id }
|
|
245
|
+
return {} unless index
|
|
246
|
+
|
|
247
|
+
{
|
|
248
|
+
"previous" => index.positive? ? navigation_entry_context(siblings[index - 1]) : nil,
|
|
249
|
+
"next" => index < siblings.length - 1 ? navigation_entry_context(siblings[index + 1]) : nil
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def navigation_entry_context(item)
|
|
254
|
+
{ "title" => item.title, "slug" => item.slug, "url" => public_entry_path(item) }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Renders a blocks field value (an array of block instances) into HTML using
|
|
258
|
+
# the active theme's Liquid block partials. The shared site context (site,
|
|
259
|
+
# globals, nav, forms, registered content sources) is exposed inside each
|
|
260
|
+
# block, but the global `entries` collection is intentionally excluded to
|
|
261
|
+
# avoid an infinite render loop, since every entry's blocks are expanded.
|
|
262
|
+
def blocks_html(value)
|
|
263
|
+
return "".html_safe unless value.is_a?(Array) && value.any?
|
|
264
|
+
|
|
265
|
+
BuilderRenderer.new(
|
|
266
|
+
blocks: value,
|
|
267
|
+
base_assigns: blocks_base_assigns,
|
|
268
|
+
site: site,
|
|
269
|
+
theme: blocks_theme,
|
|
270
|
+
registers: blocks_registers
|
|
271
|
+
).render
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def blocks_theme
|
|
275
|
+
@blocks_theme ||= ThemeRegistry.new.fetch(blocks_theme_name)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def blocks_theme_name
|
|
279
|
+
theme_name.presence || SiteSetting.instance(site).theme_name
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def blocks_base_assigns
|
|
283
|
+
@blocks_base_assigns ||= {
|
|
284
|
+
"site" => site_context,
|
|
285
|
+
"globals" => globals_context,
|
|
286
|
+
"nav" => nav_context,
|
|
287
|
+
"forms" => forms_context
|
|
288
|
+
}.merge(Plum.content_sources_for(controller, site: site))
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def blocks_registers
|
|
292
|
+
base_url = theme_asset_base_url
|
|
293
|
+
{
|
|
294
|
+
theme_asset_url_builder: ->(path) { ThemeAssetPath.url(base_url: base_url, path: path) },
|
|
295
|
+
form_renderer: ->(handle) { FormRenderer.new(forms_context[handle]).render }
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def asset_cache
|
|
300
|
+
@asset_cache ||= Asset.for_site(site).with_attached_file.index_by(&:id)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def relationship_entry_cache
|
|
304
|
+
@relationship_entry_cache ||= Entry.for_site(site).live.includes(:content_type).index_by(&:id)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def live_entries_by_type
|
|
308
|
+
@live_entries_by_type ||= Entry.for_site(site).live
|
|
309
|
+
.includes(:content_type, terms: :taxonomy)
|
|
310
|
+
.order(published_at: :desc, created_at: :desc)
|
|
311
|
+
.group_by(&:content_type_id)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def public_root_path
|
|
315
|
+
controller.request.script_name.presence || "/"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def theme_asset_base_url
|
|
319
|
+
"#{controller.request.script_name.to_s.chomp("/")}/theme_assets/#{theme_name.presence || site.theme_name}"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def public_entry_path(entry)
|
|
323
|
+
path = [ entry.content_type.route_prefix, entry.slug ].compact.join("/")
|
|
324
|
+
"#{controller.request.script_name.to_s.chomp("/")}/#{path}"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def public_form_path(form)
|
|
328
|
+
"#{controller.request.script_name.to_s.chomp("/")}/forms/#{form.handle}"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def form_authenticity_token
|
|
332
|
+
controller.send(:form_authenticity_token) if controller.respond_to?(:form_authenticity_token, true)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "redcarpet"
|
|
2
|
+
require "nokogiri"
|
|
3
|
+
|
|
4
|
+
module Plum
|
|
5
|
+
module LiquidFilters
|
|
6
|
+
def markdown(input)
|
|
7
|
+
return "" if input.blank?
|
|
8
|
+
|
|
9
|
+
text = input.to_s
|
|
10
|
+
return text if text.match?(/<[a-z][\s\S]*>/i)
|
|
11
|
+
|
|
12
|
+
renderer = Redcarpet::Render::HTML.new(
|
|
13
|
+
hard_wrap: true,
|
|
14
|
+
link_attributes: { target: "_blank", rel: "noopener" }
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
md = Redcarpet::Markdown.new(renderer,
|
|
18
|
+
autolink: true,
|
|
19
|
+
tables: true,
|
|
20
|
+
fenced_code_blocks: true,
|
|
21
|
+
strikethrough: true,
|
|
22
|
+
superscript: true,
|
|
23
|
+
no_intra_emphasis: true
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
md.render(text)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def image_url(input, size = nil)
|
|
30
|
+
return "" if input.blank?
|
|
31
|
+
return input["url"].to_s unless size && input.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
input[size].presence || input["url"].to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def theme_asset_url(input)
|
|
37
|
+
builder = @context.registers[:theme_asset_url_builder]
|
|
38
|
+
return "" unless builder
|
|
39
|
+
|
|
40
|
+
builder.call(input)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def table_of_contents(input)
|
|
44
|
+
return "" if input.blank?
|
|
45
|
+
|
|
46
|
+
headings = Nokogiri::HTML.fragment(input.to_s).css("h2, h3")
|
|
47
|
+
return "" if headings.empty?
|
|
48
|
+
|
|
49
|
+
items = headings.map do |heading|
|
|
50
|
+
id = heading["id"].presence || heading.text.parameterize
|
|
51
|
+
label = ERB::Util.html_escape(heading.text)
|
|
52
|
+
%(<li class="toc-#{heading.name}"><a href="##{id}">#{label}</a></li>)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
%(<nav class="table-of-contents" aria-label="On this page"><p>On this page</p><ol>#{items.join}</ol></nav>)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "liquid"
|
|
2
|
+
require_relative "liquid_filters"
|
|
3
|
+
require "plum/liquid_tags/form_tag"
|
|
4
|
+
|
|
5
|
+
module Plum
|
|
6
|
+
class LiquidRenderer
|
|
7
|
+
Liquid::Environment.default.register_tag("form", LiquidTags::FormTag)
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def render_template(template_name, context = {})
|
|
11
|
+
theme = current_theme(context)
|
|
12
|
+
resolver = ThemeResolver.new
|
|
13
|
+
template_path = resolver.find_template(theme, template_name)
|
|
14
|
+
|
|
15
|
+
raise "Template not found: #{template_name}" unless template_path
|
|
16
|
+
|
|
17
|
+
template_content = File.read(template_path)
|
|
18
|
+
template = Liquid::Template.parse(template_content)
|
|
19
|
+
registers = render_registers(context)
|
|
20
|
+
|
|
21
|
+
rendered_content = template.render(context, filters: [ LiquidFilters ], registers: registers)
|
|
22
|
+
|
|
23
|
+
layout_path = resolver.find_layout(theme)
|
|
24
|
+
if layout_path
|
|
25
|
+
layout_content = File.read(layout_path)
|
|
26
|
+
layout = Liquid::Template.parse(layout_content)
|
|
27
|
+
layout.render(context.merge("content" => rendered_content), filters: [ LiquidFilters ], registers: registers)
|
|
28
|
+
else
|
|
29
|
+
rendered_content
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def current_theme(context)
|
|
36
|
+
context.dig("site", "theme_name").presence || "default"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render_registers(context)
|
|
40
|
+
base_url = context.dig("site", "theme_asset_base_url").presence || "/theme_assets/#{current_theme(context)}"
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
theme_asset_url_builder: ->(path) { ThemeAssetPath.url(base_url: base_url, path: path) },
|
|
44
|
+
form_renderer: ->(handle) { FormRenderer.new(context.dig("forms", handle)).render }
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|