gaco_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.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +48 -0
  3. data/Rakefile +10 -0
  4. data/app/assets/builds/gaco_cms.css +18257 -0
  5. data/app/assets/builds/gaco_cms.js +43235 -0
  6. data/app/assets/builds/gaco_cms.js.map +7 -0
  7. data/app/assets/builds/gaco_cms_front.css +18237 -0
  8. data/app/assets/builds/gaco_cms_front.js +10722 -0
  9. data/app/assets/builds/gaco_cms_front.js.map +7 -0
  10. data/app/assets/javascripts/gaco_cms/base.js +24 -0
  11. data/app/assets/javascripts/gaco_cms/controllers/confirm_controller.ts +20 -0
  12. data/app/assets/javascripts/gaco_cms/controllers/deletable_row_controller.ts +46 -0
  13. data/app/assets/javascripts/gaco_cms/controllers/editor_controller.ts +58 -0
  14. data/app/assets/javascripts/gaco_cms/controllers/file_input_controller.ts +82 -0
  15. data/app/assets/javascripts/gaco_cms/controllers/modal_controller.ts +73 -0
  16. data/app/assets/javascripts/gaco_cms/controllers/remote_content_controller.ts +27 -0
  17. data/app/assets/javascripts/gaco_cms/controllers/repeatable_field_controller.ts +26 -0
  18. data/app/assets/javascripts/gaco_cms/controllers/sortable_controller.ts +27 -0
  19. data/app/assets/javascripts/gaco_cms/controllers/toggle_field_controller.ts +37 -0
  20. data/app/assets/javascripts/gaco_cms/controllers/translatable_controller.ts +86 -0
  21. data/app/assets/javascripts/gaco_cms/lib/progress_bar.ts +7 -0
  22. data/app/assets/javascripts/gaco_cms/lib/request.ts +21 -0
  23. data/app/assets/javascripts/gaco_cms/lib/turbo_request.ts +61 -0
  24. data/app/assets/javascripts/gaco_cms/stimulus.ts +9 -0
  25. data/app/assets/javascripts/gaco_cms.js +7 -0
  26. data/app/assets/javascripts/gaco_cms_front.js +3 -0
  27. data/app/assets/stylesheets/gaco_cms/bootstrap_custom.scss +15 -0
  28. data/app/assets/stylesheets/gaco_cms/shared.scss +22 -0
  29. data/app/assets/stylesheets/gaco_cms.css.scss +8 -0
  30. data/app/assets/stylesheets/gaco_cms_front.css.scss +6 -0
  31. data/app/controllers/concerns/gaco_cms/turbo_concern.rb +52 -0
  32. data/app/controllers/gaco_cms/admin/base_controller.rb +20 -0
  33. data/app/controllers/gaco_cms/admin/field_groups_controller.rb +87 -0
  34. data/app/controllers/gaco_cms/admin/field_groups_manager_controller.rb +66 -0
  35. data/app/controllers/gaco_cms/admin/field_groups_renderer_controller.rb +33 -0
  36. data/app/controllers/gaco_cms/admin/fields_controller.rb +20 -0
  37. data/app/controllers/gaco_cms/admin/page_types_controller.rb +56 -0
  38. data/app/controllers/gaco_cms/admin/pages_controller.rb +70 -0
  39. data/app/controllers/gaco_cms/admin/themes_controller.rb +55 -0
  40. data/app/controllers/gaco_cms/admin_controller.rb +47 -0
  41. data/app/controllers/gaco_cms/front_controller.rb +39 -0
  42. data/app/helpers/gaco_cms/application_helper.rb +26 -0
  43. data/app/jobs/gaco_cms/application_job.rb +6 -0
  44. data/app/mailers/gaco_cms/application_mailer.rb +8 -0
  45. data/app/models/concerns/gaco_cms/fields_assignable.rb +40 -0
  46. data/app/models/concerns/gaco_cms/union_scope.rb +14 -0
  47. data/app/models/gaco_cms/application_record.rb +25 -0
  48. data/app/models/gaco_cms/draft_record.rb +16 -0
  49. data/app/models/gaco_cms/field.rb +78 -0
  50. data/app/models/gaco_cms/field_group.rb +55 -0
  51. data/app/models/gaco_cms/field_value.rb +77 -0
  52. data/app/models/gaco_cms/media_file.rb +22 -0
  53. data/app/models/gaco_cms/page.rb +57 -0
  54. data/app/models/gaco_cms/page_type.rb +38 -0
  55. data/app/models/gaco_cms/theme.rb +28 -0
  56. data/app/services/gaco_cms/application_service.rb +33 -0
  57. data/app/services/gaco_cms/shortcode_parser.rb +217 -0
  58. data/app/services/gaco_cms/theme_content_generator.rb +55 -0
  59. data/app/views/gaco_cms/admin/base/index.html.haml +4 -0
  60. data/app/views/gaco_cms/admin/field_groups_manager/_field.html.haml +53 -0
  61. data/app/views/gaco_cms/admin/field_groups_manager/_group.html.haml +54 -0
  62. data/app/views/gaco_cms/admin/field_groups_manager/index.html.haml +14 -0
  63. data/app/views/gaco_cms/admin/field_groups_renderer/_field.html.haml +31 -0
  64. data/app/views/gaco_cms/admin/field_groups_renderer/_fields_frame.html.haml +16 -0
  65. data/app/views/gaco_cms/admin/field_groups_renderer/_group.html.haml +24 -0
  66. data/app/views/gaco_cms/admin/field_groups_renderer/default_value/_page.html.haml +3 -0
  67. data/app/views/gaco_cms/admin/field_groups_renderer/fields/_editor.html.haml +3 -0
  68. data/app/views/gaco_cms/admin/field_groups_renderer/fields/_file.html.haml +4 -0
  69. data/app/views/gaco_cms/admin/field_groups_renderer/fields/_page.html.haml +3 -0
  70. data/app/views/gaco_cms/admin/field_groups_renderer/fields/_text_area.html.haml +3 -0
  71. data/app/views/gaco_cms/admin/field_groups_renderer/fields/_text_field.html.haml +3 -0
  72. data/app/views/gaco_cms/admin/field_groups_renderer/index.html.haml +20 -0
  73. data/app/views/gaco_cms/admin/page_types/form.html.haml +27 -0
  74. data/app/views/gaco_cms/admin/page_types/index.html.haml +27 -0
  75. data/app/views/gaco_cms/admin/pages/edit.html.haml +57 -0
  76. data/app/views/gaco_cms/admin/pages/index.html.haml +23 -0
  77. data/app/views/gaco_cms/admin/pages/new.html.haml +16 -0
  78. data/app/views/gaco_cms/admin/themes/edit.html.haml +36 -0
  79. data/app/views/gaco_cms/admin/themes/index.html.haml +24 -0
  80. data/app/views/gaco_cms/admin/themes/new.html.haml +21 -0
  81. data/app/views/layouts/gaco_cms/_breadcrumb.html.haml +7 -0
  82. data/app/views/layouts/gaco_cms/_flash_messages.html.haml +13 -0
  83. data/app/views/layouts/gaco_cms/admin.haml +55 -0
  84. data/config/routes.rb +26 -0
  85. data/db/migrate/20230202133738_create_cms_pages.rb +16 -0
  86. data/db/migrate/20230202134840_create_cms_field_groups.rb +17 -0
  87. data/db/migrate/20230202135314_create_cms_fields.rb +21 -0
  88. data/db/migrate/20230202135739_create_cms_field_values.rb +16 -0
  89. data/db/migrate/20230202181744_create_cms_media_files.rb +10 -0
  90. data/db/migrate/20230206231547_create_cms_page_types.rb +23 -0
  91. data/db/migrate/20230209090008_create_cms_themes.rb +19 -0
  92. data/lib/gaco_cms/config.rb +19 -0
  93. data/lib/gaco_cms/engine.rb +31 -0
  94. data/lib/gaco_cms/version.rb +5 -0
  95. data/lib/gaco_cms.rb +18 -0
  96. data/lib/generators/gaco_cms/install_generator.rb +16 -0
  97. data/lib/generators/gaco_cms/install_template/gaco_cms.rb +11 -0
  98. data/lib/generators/gaco_cms/install_template/themes/default/index.html.haml +2 -0
  99. data/lib/generators/gaco_cms/install_template/themes/default/layouts/application.html.haml +34 -0
  100. data/lib/generators/gaco_cms/install_template/themes/default/page.html.haml +2 -0
  101. data/lib/tasks/auto_annotate_models.rake +59 -0
  102. data/lib/tasks/gaco_cms_tasks.rake +5 -0
  103. metadata +174 -0
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GacoCms
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+
7
+ def cache_key_locale(*keys)
8
+ "#{cache_key_with_version}/#{I18n.locale}/#{keys.join('-')}"
9
+ end
10
+
11
+ def activestorage_url(file)
12
+ return '' unless file&.blob
13
+ return file.url.split('?').first if file.service.name == :amazon
14
+
15
+ Rails.application.routes.url_helpers.rails_blob_path(file, only_path: true)
16
+ end
17
+
18
+ class << self
19
+ alias attr_label human_attribute_name
20
+ def human_name(args = {})
21
+ model_name.human(args)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: simple_cms_draft_records
6
+ #
7
+ # id :integer not null, primary key
8
+ # record_type :string
9
+ # created_at :datetime not null
10
+ # updated_at :datetime not null
11
+ #
12
+ module GacoCms
13
+ class DraftRecord < ApplicationRecord
14
+ include GacoCms::FieldsAssignable
15
+ end
16
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: simple_cms_fields
6
+ #
7
+ # id :integer not null, primary key
8
+ # data :text
9
+ # def_value :text
10
+ # description :text
11
+ # key :string
12
+ # kind :string default("text_field")
13
+ # position :integer default(0)
14
+ # repeat :boolean default(FALSE)
15
+ # required :boolean default(FALSE)
16
+ # title :text
17
+ # translatable :boolean default(FALSE)
18
+ # created_at :datetime not null
19
+ # updated_at :datetime not null
20
+ # field_group_id :integer not null
21
+ #
22
+ # Indexes
23
+ #
24
+ # index_simple_cms_fields_on_field_group_id (field_group_id)
25
+ #
26
+ # Foreign Keys
27
+ #
28
+ # field_group_id (field_group_id => simple_cms_field_groups.id)
29
+ #
30
+
31
+ module GacoCms
32
+ class Field < ApplicationRecord
33
+ include BuddyTranslatable
34
+ translatable :title, :description, :def_value
35
+
36
+ EXTRA_KINDS = GacoCms::Config.extra_fields
37
+ KINDS = %i[text_field text_area editor file page].concat(EXTRA_KINDS.keys)
38
+ enum kind: KINDS.map { |k| [k, k.to_s] }.to_h
39
+
40
+ belongs_to :field_group, optional: false
41
+ has_many :field_values, dependent: :destroy, inverse_of: :field
42
+ after_update_commit :update_values_key, if: :saved_change_to_key?
43
+
44
+ validates :key, uniqueness: { scope: :field_group_id }
45
+ scope :ordered, -> { order(position: :asc) }
46
+
47
+ def self.dropdown_data
48
+ KINDS.map do |key|
49
+ title = key.to_s.titleize
50
+ title = ApplicationHelper.translated_value_for(EXTRA_KINDS[key][:label]) if EXTRA_KINDS[key]
51
+ [title, key]
52
+ end
53
+ end
54
+
55
+ def default_value_tpl
56
+ return "/gaco_cms/admin/field_groups_renderer/default_value/#{kind}" if kind == 'page'
57
+
58
+ EXTRA_KINDS.dig(kind&.to_sym, :default_value_tpl)
59
+ end
60
+
61
+ def allow_translation?
62
+ return false if %w[page].include?(kind)
63
+
64
+ res = EXTRA_KINDS.dig(kind&.to_sym, :translatable)
65
+ res.nil? ? true : res
66
+ end
67
+
68
+ def tpl
69
+ EXTRA_KINDS.dig(kind&.to_sym, :tpl) || "/gaco_cms/admin/field_groups_renderer/fields/#{kind}"
70
+ end
71
+
72
+ private
73
+
74
+ def update_values_key
75
+ field_values.each { |v| v.update!(field_key: key) }
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: simple_cms_field_groups
6
+ #
7
+ # id :integer not null, primary key
8
+ # description :text
9
+ # key :string
10
+ # position :integer default(0)
11
+ # record_type :string
12
+ # repeat :boolean default(FALSE)
13
+ # title :text
14
+ # created_at :datetime not null
15
+ # updated_at :datetime not null
16
+ # record_id :integer
17
+ # template :text default('')
18
+ #
19
+ # Indexes
20
+ #
21
+ # index_simple_cms_field_groups_on_record (record_type,record_id)
22
+ #
23
+
24
+ module GacoCms
25
+ class FieldGroup < ApplicationRecord
26
+ include UnionScope
27
+ include BuddyTranslatable
28
+ translatable :title, :description
29
+
30
+ belongs_to :record, polymorphic: true
31
+ has_many :fields, -> { ordered }, dependent: :destroy
32
+
33
+ scope :ordered, -> { order(position: :asc) }
34
+ accepts_nested_attributes_for :fields, allow_destroy: true
35
+
36
+ def available_records
37
+ map_data = ->(item) { [item.title, "#{item.class.name}/#{item.id}"] }
38
+ {
39
+ Page.human_name(count: 2) => GacoCms::Page.title_ordered.select(:title, :id).map(&map_data),
40
+ 'All Pages Under' => PageType.title_ordered.select(:title, :id).map(&map_data),
41
+ Theme.human_name(count: 2) => GacoCms::Theme.ordered.map(&map_data)
42
+ }
43
+ end
44
+
45
+ def selected_record
46
+ "#{record_type}/#{record_id}"
47
+ end
48
+
49
+ def record_label
50
+ return "All pages under \"#{record.title}\"" if record_type.include?('PageType')
51
+
52
+ "#{record.class.human_name} => \"#{record.title}\""
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: simple_cms_field_values
6
+ #
7
+ # id :integer not null, primary key
8
+ # field_key :string
9
+ # group_no :integer default(0)
10
+ # position :integer default(0)
11
+ # record_type :string
12
+ # value :text
13
+ # created_at :datetime not null
14
+ # updated_at :datetime not null
15
+ # field_id :integer not null
16
+ # record_id :integer
17
+ #
18
+ # Indexes
19
+ #
20
+ # index_simple_cms_field_values_on_field_id (field_id)
21
+ # index_simple_cms_field_values_on_record (record_type,record_id)
22
+ #
23
+ # Foreign Keys
24
+ #
25
+ # field_id (field_id => simple_cms_fields.id)
26
+ #
27
+
28
+ module GacoCms
29
+ class FieldValue < ApplicationRecord
30
+ include BuddyTranslatable
31
+ translatable :value
32
+
33
+ belongs_to :record, polymorphic: true, touch: true
34
+ belongs_to :field, optional: false
35
+ before_validation :retrieve_field_key, unless: :field_key
36
+ validates :field_key, presence: true
37
+ validates :record, presence: true
38
+
39
+ scope :for_group, ->(group) { joins(:field).merge(GacoCms::Field.where(field_group_id: group.id)) }
40
+ scope :ordered, -> { order(position: :asc) }
41
+ delegate :translatable, :repeat, :required, :kind, :def_value, :def_value_data, to: :field
42
+
43
+ def self.group_nos_for(group)
44
+ for_group(group).pluck(:group_no).uniq.sort
45
+ end
46
+
47
+ def self.all_or_new_for(field, group_no)
48
+ items = where(field: field).where(group_no: group_no)
49
+ items.any? ? items.ordered : [items.new]
50
+ end
51
+
52
+ def self.grouped
53
+ groups = reorder(group_no: :asc).pluck('distinct(group_no)')
54
+ groups.map do |g_no|
55
+ where(group_no: g_no).ordered
56
+ end
57
+ end
58
+
59
+ def value_for_input
60
+ @value_for_input ||= begin
61
+ return new_record? ? def_value : value unless translatable
62
+
63
+ new_record? ? def_value_data.to_json : value_data.to_json
64
+ end
65
+ end
66
+
67
+ def the_value
68
+ ShortcodeParser.call(value, record)
69
+ end
70
+
71
+ private
72
+
73
+ def retrieve_field_key
74
+ self.field_key = field.key
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: simple_cms_media_files
6
+ #
7
+ # id :integer not null, primary key
8
+ # name :string
9
+ # created_at :datetime not null
10
+ # updated_at :datetime not null
11
+ #
12
+
13
+ module GacoCms
14
+ class MediaFile < ApplicationRecord
15
+ has_one_attached :file # TODO: if image, then create a thumb version 100x100
16
+ validates :file, presence: true
17
+
18
+ def url
19
+ activestorage_url(file)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: simple_cms_pages
6
+ #
7
+ # id :integer not null, primary key
8
+ # content :text
9
+ # key :text
10
+ # photo_url :text
11
+ # summary :text
12
+ # template :text
13
+ # title :text
14
+ # created_at :datetime not null
15
+ # updated_at :datetime not null
16
+ # page_type_id :integer not null
17
+ #
18
+ # Indexes
19
+ #
20
+ # index_simple_cms_pages_on_page_type_id (page_type_id)
21
+ #
22
+ # Foreign Keys
23
+ #
24
+ # page_type_id (page_type_id => simple_cms_page_types.id)
25
+ #
26
+
27
+ require 'byebug'
28
+ module GacoCms
29
+ class Page < ApplicationRecord
30
+ include BuddyTranslatable
31
+ include GacoCms::FieldsAssignable
32
+ translatable :key, :title, :summary, :content
33
+
34
+ belongs_to :page_type
35
+ scope :title_ordered, -> { order(title: :asc) }
36
+
37
+ def self.by_key(key)
38
+ where_key_with(key).take
39
+ end
40
+
41
+ # TODO: deprecated
42
+ def all_field_groups
43
+ FieldGroup.union_scope(field_groups, page_type.field_groups)
44
+ end
45
+
46
+ def the_content(cache: true)
47
+ callback = proc do
48
+ tpl = template.presence || page_type.template.to_s
49
+ tpl = "[page_content]#{tpl}" unless tpl.include?('[page_content]')
50
+ ShortcodeParser.call(tpl, self).to_s
51
+ end
52
+ return callback.call unless cache
53
+
54
+ Rails.cache.fetch(cache_key_locale(:the_content), expires_at: Time.current.end_of_day, &callback)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: simple_cms_page_types
6
+ #
7
+ # id :integer not null, primary key
8
+ # key :string
9
+ # template :text default("")
10
+ # title :text
11
+ # created_at :datetime not null
12
+ # updated_at :datetime not null
13
+ #
14
+ # Indexes
15
+ #
16
+ # index_simple_cms_page_types_on_key (key)
17
+ #
18
+
19
+ module GacoCms
20
+ class PageType < ApplicationRecord
21
+ include BuddyTranslatable
22
+ translatable :title
23
+
24
+ has_many :pages, dependent: :destroy
25
+ has_many :field_groups, -> { ordered }, as: :record, inverse_of: :record, dependent: :destroy
26
+ after_update_commit :touch_pages
27
+ accepts_nested_attributes_for :field_groups, allow_destroy: true
28
+
29
+ scope :title_ordered, -> { order(title: :asc) }
30
+
31
+ private
32
+
33
+ # reset pages cached values
34
+ def touch_pages
35
+ pages.find_each(&:touch)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # == Schema Information
4
+ #
5
+ # Table name: simple_cms_themes
6
+ #
7
+ # id :integer not null, primary key
8
+ # active :boolean default(FALSE)
9
+ # key :string not null
10
+ # title :string not null
11
+ # created_at :datetime not null
12
+ # updated_at :datetime not null
13
+ #
14
+
15
+ module GacoCms
16
+ class Theme < ApplicationRecord
17
+ include GacoCms::FieldsAssignable
18
+
19
+ validates :key, presence: true
20
+ validates :title, presence: true, uniqueness: true
21
+
22
+ scope :ordered, -> { order(title: :asc) }
23
+
24
+ def self.current
25
+ find_by(active: true)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GacoCms
4
+ class ApplicationService
5
+ def initialize(*args); end
6
+
7
+ class << self
8
+ # @see #initialize
9
+ def call(*args, **kwargs)
10
+ new(*args, **kwargs).call
11
+ end
12
+
13
+ # Code style definition for Sidekiq delay method
14
+ # @!method delay(settings = {})
15
+ # @param [Hash] settings
16
+ # @return [self] returns itself
17
+ end
18
+
19
+ private
20
+
21
+ #
22
+ # @param [String] message
23
+ # @param [:info|:warn|:error] mode
24
+ #
25
+ def log(message, mode: :info)
26
+ Rails.logger.send(mode, "#{self.class.name} => #{message}")
27
+ end
28
+
29
+ def print_error(error)
30
+ log("Failed with #{error.message}. #{error.backtrace[0..10]}")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GacoCms
4
+ class ShortcodeParser < ApplicationService # rubocop:disable Metrics/ClassLength
5
+ attr_reader :content, :record, :ignored_shortcodes
6
+
7
+ def initialize(content, record, ignore: [])
8
+ @content = content
9
+ @record = record
10
+ @ignored_shortcodes = Array(ignore)
11
+ super
12
+ end
13
+
14
+ def call
15
+ counter = 0
16
+ loop do
17
+ detected = @content.scan(/#{shortcodes_regex}/).to_a
18
+ break if detected.empty?
19
+ break log("Too many levels detected, skipping after #{counter} levels", mode: :error) if counter > 5
20
+
21
+ detected.each { |item| @content = replace_shortcode(item) }
22
+ counter += 1
23
+ end
24
+ @content
25
+ end
26
+
27
+ private
28
+
29
+ def shortcodes # rubocop:disable Metrics/MethodLength
30
+ pp_sample = '<img src="[page_field key=\'photo\']"/> Or [page_field key=\'photo\' page_id=10 group_no=1]'
31
+ pif_sample = '[page_img_field key="photo"] Or [page_img_field key="photo" page_id=10 style="width: 100%"]'
32
+ pmf_sample = '[page_field_multiple key="thumbnail" content="<li><img src=\'{field_yield}\' /></li>" '\
33
+ 'wrapper="<ul>{yield}</ul>"] Or [page_field_multiple key="thumbnail" page_id=10 content=...'
34
+ pgf_sample = '[page_grouped_fields keys="name,photo" wrapper="<ul>{yield}</ul>" content="<li>{name_yield}: '\
35
+ '{photo_yield}</li>"] Or [page_grouped_fields keys="name,photo" page_id=10 content=...'
36
+ tf_sample = '<img src="[theme_field key=\'photo\'] or [theme_field key=\'photo\' group_no=1]" />'
37
+ pf_sample = '[page_photo] or [page_photo page_id=10 style=".."]'
38
+ pg_sample = '[page_group key="my_key"] or [page_group key="my_key" page_id=10 wrapper="<div class=\'row\'>{yield}</div>"]'
39
+ {
40
+ page_content: { render: method(:page_content_parser), sample: '[page_content] or [page_content page_id=10]' },
41
+ page_title: { render: method(:page_title_parser), sample: '[page_title] or [page_title page_id=10]' },
42
+ page_photo: { render: method(:page_photo_parser), sample: pp_sample },
43
+ page_field: { render: method(:page_field_parser), sample: pf_sample },
44
+ page_img_field: { render: method(:page_img_field_parser), sample: pif_sample },
45
+ page_field_multiple: { render: method(:page_field_multiple_parser), sample: pmf_sample },
46
+ # page_fields: { render: method(:page_fields_parser), sample: },
47
+ page_grouped_fields: { render: method(:page_grouped_fields_parser), sample: pgf_sample },
48
+ page_url: { render: method(:page_url_parser), sample: '[page_url] or [page_url page_id=10]' },
49
+ page_group: { render: method(:page_group_parser), sample: pg_sample },
50
+ theme_field: { render: method(:theme_field_parser), sample: tf_sample },
51
+ theme_img_field: { render: method(:theme_img_field_parser), sample: '[theme_img_field key="my_img"]' }
52
+ }.except(*ignored_shortcodes)
53
+ end
54
+
55
+ def shortcodes_regex
56
+ "(\\[(#{shortcodes.keys.join('|')})((\s)((?!\\]).)*|)\\])"
57
+ end
58
+
59
+ # @param item [Array[shortcode, code, attrs]]
60
+ def replace_shortcode(item)
61
+ shortcode, code, attrs = item
62
+ attrs = parse_shortcode_attrs(attrs)
63
+ shortcode, shortcode_content = shortcode_content_for(shortcode, code)
64
+ args = { shortcode: shortcode, code: code, shortcode_content: shortcode_content }
65
+ content.sub(shortcode, parse_shortcode(code, attrs, args).to_s)
66
+ end
67
+
68
+ def shortcode_content_for(shortcode, code)
69
+ close_code = "[/#{code}]"
70
+ return [shortcode, nil] unless content.include?(close_code)
71
+
72
+ shortcode_bk = shortcode
73
+ tmp_content = content[content.index(shortcode)..]
74
+ shortcode = tmp_content[0..(tmp_content.index(close_code) + close_code.size - 1)]
75
+ [shortcode, shortcode.sub(shortcode_bk, '').sub(close_code, '')]
76
+ end
77
+
78
+ # determine the content to replace instead the shortcode
79
+ def parse_shortcode(code, attrs, args = {})
80
+ renderer = shortcodes[code.to_sym][:render]
81
+ if renderer.is_a?(String)
82
+ params = { attributes: attrs, args: args }
83
+ ActionController::Base.new.render_to_string template: renderer, locals: params, formats: [:html]
84
+ else
85
+ shortcodes[code.to_sym][:render].call(attrs, args)
86
+ end
87
+ end
88
+
89
+ # parse the attributes of a shortcode
90
+ def parse_shortcode_attrs(text)
91
+ return {} if text.blank?
92
+
93
+ res = {}
94
+ regex = /(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/ # rubocop:disable Style/RedundantRegexpEscape, Layout/LineLength
95
+ text.scan(regex).each do |item|
96
+ item.each_with_index do |c, index|
97
+ break res[c] = item[index + 1] if c.present?
98
+ end
99
+ end
100
+ res
101
+ end
102
+
103
+ def img_tag_for(url, attrs)
104
+ attrs = attrs.slice('style', 'alt')
105
+ props = attrs.map { |k, v| "#{k}=\"#{v}\"" }.join(' ')
106
+ "<img src='#{url}' #{props} />"
107
+ end
108
+
109
+ def replace_field_wrapper(attrs, content)
110
+ return content unless attrs.key?('wrapper')
111
+
112
+ attrs['wrapper'].to_s.sub('{yield}', content).gsub('&lt;', '<').gsub('&gt;', '>')
113
+ end
114
+
115
+ ############################
116
+ def calc_context(context, attrs, klass: Page)
117
+ return context if context
118
+ return Page.where_key_with(attrs['page_key']).take if attrs['page_key'].present?
119
+
120
+ attrs['page_id'].present? ? klass.find_by(id: attrs['page_id']) : record
121
+ end
122
+
123
+ def page_field_parser(attrs, _args, context = nil)
124
+ context = calc_context(context, attrs)
125
+ return '--PageNotFound--' unless context
126
+
127
+ context.the_value(attrs['key'])
128
+ end
129
+
130
+ def page_field_multiple_parser(attrs, _args, context = nil)
131
+ context = calc_context(context, attrs)
132
+ return '--PageNotFound--' unless context
133
+
134
+ res = context.the_values(attrs['key']).map do |val|
135
+ attrs['content'].to_s.gsub('{field_yield}', val)
136
+ end.join
137
+ replace_field_wrapper(attrs, res)
138
+ end
139
+
140
+ def page_img_field_parser(attrs, _args, context = nil)
141
+ context = calc_context(context, attrs)
142
+ return '--PageNotFound--' unless context
143
+
144
+ src = context.the_value(attrs['key'])
145
+ img_tag_for(src, attrs)
146
+ end
147
+
148
+ def page_grouped_fields_parser(attrs, _args, context = nil)
149
+ context = calc_context(context, attrs)
150
+ return '--PageNotFound--' unless context
151
+
152
+ keys = attrs['keys'].to_s.split(',')
153
+ context.the_grouped_values(*keys).map do |fields|
154
+ content = attrs['content'].to_s
155
+ keys.each do |key|
156
+ content = content.gsub("{#{key}_yield}", fields[key])
157
+ end
158
+ replace_field_wrapper(attrs, content)
159
+ end.join
160
+ end
161
+
162
+ def page_content_parser(attrs, _args)
163
+ context = calc_context(nil, attrs)
164
+ return '--PageNotFound--' unless context
165
+
166
+ context.content
167
+ end
168
+
169
+ def page_photo_parser(attrs, _args)
170
+ context = calc_context(nil, attrs)
171
+ return '--PageNotFound--' unless context
172
+
173
+ img_tag_for(context.photo_url, attrs)
174
+ end
175
+
176
+ def page_title_parser(attrs, _args)
177
+ context = calc_context(nil, attrs)
178
+ return '--PageNotFound--' unless context
179
+
180
+ context.title
181
+ end
182
+
183
+ def page_url_parser(attrs, _args)
184
+ context = calc_context(nil, attrs)
185
+ return '--PageNotFound--' unless context
186
+
187
+ ApplicationHelper.page_url_for(context.id)
188
+ end
189
+
190
+ # TODO: add missing test cases
191
+ def page_group_parser(attrs, _args)
192
+ context = calc_context(nil, attrs)
193
+ return '--PageNotFound--' unless context
194
+
195
+ group = context.field_groups.find_by(key: attrs['key'])
196
+ return '--GroupNotFound--' unless group
197
+
198
+ keys = group.fields.pluck(:key)
199
+ result = context.the_grouped_values(*keys).map do |fields|
200
+ content = group.template.to_s
201
+ keys.each do |key|
202
+ content = content.gsub("{#{key}_yield}", fields[key])
203
+ end
204
+ content
205
+ end.join
206
+ replace_field_wrapper(attrs, result)
207
+ end
208
+
209
+ def theme_field_parser(attrs, args)
210
+ page_field_parser(attrs, args, Theme.current)
211
+ end
212
+
213
+ def theme_img_field_parser(attrs, args)
214
+ page_img_field_parser(attrs, args, Theme.current)
215
+ end
216
+ end
217
+ end