alchemy_cms 8.0.0.a → 8.0.0.c

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 (216) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -0
  3. data/app/assets/builds/alchemy/admin/page-select.css +1 -1
  4. data/app/assets/builds/alchemy/admin.css +1 -1
  5. data/app/assets/builds/alchemy/dark-theme.css +1 -0
  6. data/app/assets/builds/alchemy/light-theme.css +1 -0
  7. data/app/assets/builds/alchemy/theme.css +1 -0
  8. data/app/assets/builds/alchemy/welcome.css +1 -1
  9. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  10. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -0
  11. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  12. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/content.min.css +1 -0
  13. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -0
  14. data/app/assets/images/alchemy/element_icons/layout-bottom-2-line.svg +1 -0
  15. data/app/assets/images/alchemy/icons-sprite.svg +1 -1
  16. data/app/components/alchemy/admin/element_select.rb +39 -0
  17. data/app/components/alchemy/admin/link_dialog/tabs.rb +1 -1
  18. data/app/components/alchemy/admin/locale_select.rb +38 -0
  19. data/app/components/alchemy/ingredients/datetime_view.rb +4 -2
  20. data/app/controllers/alchemy/admin/attachments_controller.rb +2 -0
  21. data/app/controllers/alchemy/admin/elements_controller.rb +2 -0
  22. data/app/controllers/alchemy/admin/pages_controller.rb +3 -1
  23. data/app/controllers/alchemy/admin/pictures_controller.rb +26 -34
  24. data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
  25. data/app/controllers/alchemy/pages_controller.rb +19 -2
  26. data/app/controllers/concerns/alchemy/admin/resource_filter.rb +1 -0
  27. data/app/decorators/alchemy/ingredient_editor.rb +9 -1
  28. data/app/helpers/alchemy/admin/attachments_helper.rb +5 -5
  29. data/app/helpers/alchemy/admin/base_helper.rb +0 -7
  30. data/app/helpers/alchemy/admin/form_helper.rb +2 -1
  31. data/app/helpers/alchemy/pages_helper.rb +1 -1
  32. data/app/javascript/alchemy_admin/components/auto_submit.js +20 -0
  33. data/app/javascript/alchemy_admin/components/datepicker.js +8 -5
  34. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +3 -2
  35. data/app/javascript/alchemy_admin/components/element_editor.js +25 -15
  36. data/app/javascript/alchemy_admin/components/element_select.js +43 -0
  37. data/app/javascript/alchemy_admin/components/index.js +5 -0
  38. data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
  39. data/app/javascript/alchemy_admin/components/remote_select.js +5 -1
  40. data/app/javascript/alchemy_admin/components/tinymce.js +93 -16
  41. data/app/javascript/alchemy_admin/dialog.js +1 -1
  42. data/app/javascript/alchemy_admin/file_editors.js +1 -1
  43. data/app/javascript/alchemy_admin/image_loader.js +4 -2
  44. data/app/javascript/alchemy_admin/picture_editors.js +7 -4
  45. data/app/javascript/alchemy_admin/picture_selector.js +4 -4
  46. data/app/jobs/alchemy/delete_picture_job.rb +12 -0
  47. data/app/models/alchemy/attachment.rb +2 -9
  48. data/app/models/alchemy/element.rb +1 -0
  49. data/app/models/alchemy/element_definition.rb +31 -0
  50. data/app/models/alchemy/ingredient.rb +1 -1
  51. data/app/models/alchemy/ingredients/boolean.rb +2 -1
  52. data/app/models/alchemy/language.rb +2 -7
  53. data/app/models/alchemy/page/page_naming.rb +4 -11
  54. data/app/models/alchemy/page/page_natures.rb +16 -11
  55. data/app/models/alchemy/page/publisher.rb +1 -1
  56. data/app/models/alchemy/page.rb +1 -6
  57. data/app/models/alchemy/page_definition.rb +1 -1
  58. data/app/models/alchemy/picture.rb +6 -17
  59. data/app/models/alchemy/resource.rb +15 -2
  60. data/app/models/alchemy/site/layout.rb +1 -0
  61. data/app/models/alchemy/site.rb +1 -6
  62. data/app/models/alchemy/storage_adapter/dragonfly/picture_url.rb +7 -2
  63. data/app/models/alchemy/storage_adapter/dragonfly.rb +24 -2
  64. data/app/models/concerns/alchemy/relatable_resource.rb +28 -0
  65. data/app/stylesheets/alchemy/_custom-properties.scss +162 -0
  66. data/app/stylesheets/alchemy/_mixins.scss +12 -24
  67. data/app/stylesheets/alchemy/_themes.scss +540 -0
  68. data/app/stylesheets/alchemy/admin/archive.scss +28 -8
  69. data/app/stylesheets/alchemy/admin/attachments.scss +10 -33
  70. data/app/stylesheets/alchemy/admin/base.scss +4 -1
  71. data/app/stylesheets/alchemy/admin/buttons.scss +7 -32
  72. data/app/stylesheets/alchemy/admin/dashboard.scss +13 -0
  73. data/app/stylesheets/alchemy/admin/dialogs.scss +17 -7
  74. data/app/stylesheets/alchemy/admin/element-select.scss +11 -0
  75. data/app/stylesheets/alchemy/admin/elements.scss +95 -34
  76. data/app/stylesheets/alchemy/admin/filters.scss +8 -9
  77. data/app/stylesheets/alchemy/admin/flatpickr.scss +12 -27
  78. data/app/stylesheets/alchemy/admin/form_fields.scss +0 -15
  79. data/app/stylesheets/alchemy/admin/forms.scss +3 -8
  80. data/app/stylesheets/alchemy/admin/frame.scss +5 -7
  81. data/app/stylesheets/alchemy/admin/icons.scss +0 -9
  82. data/app/stylesheets/alchemy/admin/image_library.scss +13 -55
  83. data/app/stylesheets/alchemy/admin/navigation.scss +1 -11
  84. data/app/stylesheets/alchemy/admin/node-select.scss +1 -10
  85. data/app/stylesheets/alchemy/admin/nodes.scss +6 -2
  86. data/app/stylesheets/alchemy/admin/notices.scss +5 -4
  87. data/app/stylesheets/alchemy/admin/page-select.scss +16 -0
  88. data/app/stylesheets/alchemy/admin/pagination.scss +1 -8
  89. data/app/stylesheets/alchemy/admin/preview_window.scss +12 -1
  90. data/app/stylesheets/alchemy/admin/resource_info.scss +106 -3
  91. data/app/stylesheets/alchemy/admin/search.scss +1 -1
  92. data/app/stylesheets/alchemy/admin/selects.scss +58 -31
  93. data/app/stylesheets/alchemy/admin/shoelace.scss +32 -62
  94. data/app/stylesheets/alchemy/admin/sitemap.scss +7 -18
  95. data/app/stylesheets/alchemy/admin/tables.scss +3 -3
  96. data/app/stylesheets/alchemy/admin/tags.scss +18 -35
  97. data/app/stylesheets/alchemy/admin/toolbar.scss +0 -6
  98. data/app/stylesheets/alchemy/admin/typography.scss +2 -5
  99. data/app/stylesheets/alchemy/admin.scss +1 -1
  100. data/app/stylesheets/alchemy/dark-theme.scss +5 -0
  101. data/app/stylesheets/alchemy/light-theme.scss +6 -0
  102. data/app/stylesheets/alchemy/theme.scss +13 -0
  103. data/app/stylesheets/tinymce/skins/content/alchemy/content.scss +8 -8
  104. data/app/stylesheets/tinymce/skins/content/alchemy-dark/content.scss +70 -0
  105. data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +28 -43
  106. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/content.scss +1 -0
  107. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +3784 -0
  108. data/app/views/alchemy/admin/attachments/_files_list.html.erb +20 -10
  109. data/app/views/alchemy/admin/attachments/assign.js.erb +4 -3
  110. data/app/views/alchemy/admin/attachments/show.html.erb +55 -43
  111. data/app/views/alchemy/admin/crop.html.erb +1 -1
  112. data/app/views/alchemy/admin/dashboard/index.html.erb +1 -1
  113. data/app/views/alchemy/admin/dashboard/info.html.erb +36 -6
  114. data/app/views/alchemy/admin/elements/_form.html.erb +9 -9
  115. data/app/views/alchemy/admin/elements/_header.html.erb +12 -10
  116. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -1
  117. data/app/views/alchemy/admin/nodes/_form.html.erb +5 -1
  118. data/app/views/alchemy/admin/pages/info.html.erb +1 -1
  119. data/app/views/alchemy/admin/partials/_search_form.html.erb +1 -0
  120. data/app/views/alchemy/admin/pictures/_archive.html.erb +13 -23
  121. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -6
  122. data/app/views/alchemy/admin/pictures/_form.html.erb +10 -5
  123. data/app/views/alchemy/admin/pictures/_infos.html.erb +21 -52
  124. data/app/views/alchemy/admin/pictures/_library_sidebar.html.erb +7 -0
  125. data/app/views/alchemy/admin/pictures/_picture.html.erb +15 -16
  126. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +20 -16
  127. data/app/views/alchemy/admin/pictures/_sorting_select.html.erb +13 -0
  128. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +1 -1
  129. data/app/views/alchemy/admin/pictures/edit_multiple.html.erb +1 -6
  130. data/app/views/alchemy/admin/pictures/index.html.erb +3 -12
  131. data/app/views/alchemy/admin/pictures/show.html.erb +17 -14
  132. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
  133. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +5 -15
  134. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +36 -0
  135. data/app/views/alchemy/admin/styleguide/index.html.erb +118 -66
  136. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -1
  137. data/app/views/alchemy/base/error_notice.html.erb +1 -1
  138. data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -1
  139. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -1
  140. data/app/views/alchemy/ingredients/_select_editor.html.erb +1 -2
  141. data/app/views/layouts/alchemy/admin.html.erb +25 -23
  142. data/config/locales/alchemy.en.yml +26 -8
  143. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +14 -0
  144. data/lib/alchemy/configuration/base_option.rb +18 -5
  145. data/lib/alchemy/configuration/boolean_option.rb +2 -5
  146. data/lib/alchemy/configuration/collection_option.rb +69 -0
  147. data/lib/alchemy/configuration/configuration_option.rb +35 -0
  148. data/lib/alchemy/configuration/pathname_option.rb +12 -0
  149. data/lib/alchemy/configuration.rb +44 -6
  150. data/lib/alchemy/configurations/format_matchers.rb +1 -1
  151. data/lib/alchemy/configurations/importmap.rb +11 -0
  152. data/lib/alchemy/configurations/mailer.rb +2 -2
  153. data/lib/alchemy/configurations/main.rb +148 -3
  154. data/lib/alchemy/configurations/page_cache.rb +19 -0
  155. data/lib/alchemy/configurations/uploader.rb +2 -2
  156. data/lib/alchemy/deprecation.rb +1 -1
  157. data/lib/alchemy/engine.rb +43 -21
  158. data/lib/alchemy/install/tasks.rb +0 -12
  159. data/lib/alchemy/name_conversions.rb +6 -0
  160. data/lib/alchemy/tasks/tidy.rb +18 -0
  161. data/lib/alchemy/test_support/config_stubbing.rb +13 -4
  162. data/lib/alchemy/test_support/factories/language_factory.rb +8 -4
  163. data/lib/alchemy/test_support/factories/page_factory.rb +1 -0
  164. data/lib/alchemy/test_support/factories/picture_factory.rb +1 -0
  165. data/lib/alchemy/test_support/relatable_resource_examples.rb +58 -0
  166. data/lib/alchemy/tinymce.rb +0 -1
  167. data/lib/alchemy/version.rb +1 -1
  168. data/lib/alchemy.rb +18 -171
  169. data/lib/generators/alchemy/install/install_generator.rb +21 -10
  170. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +88 -13
  171. data/lib/tasks/alchemy/assets.rake +1 -1
  172. data/lib/tasks/alchemy/tidy.rake +6 -0
  173. data/lib/tasks/alchemy/usage.rake +2 -0
  174. data/vendor/assets/stylesheets/tinymce/skins/content/dark/content.min.css +1 -0
  175. data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +1 -0
  176. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide/skin.min.css +1 -0
  177. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/content.min.css +1 -0
  178. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/skin.min.css +1 -0
  179. data/vendor/javascript/clipboard.min.js +1 -1
  180. data/vendor/javascript/cropperjs.min.js +1 -1
  181. data/vendor/javascript/handlebars.min.js +3 -3
  182. data/vendor/javascript/jquery.min.js +1 -1
  183. data/vendor/javascript/select2.min.js +3 -3
  184. data/vendor/javascript/shoelace.min.js +92 -76
  185. data/vendor/javascript/sortable.min.js +2 -2
  186. data/vendor/javascript/tinymce.min.js +1 -1
  187. data/vendor/javascript/ungap-custom-elements.min.js +2 -2
  188. metadata +51 -36
  189. data/CHANGELOG.md +0 -2100
  190. data/CODE_OF_CONDUCT.md +0 -13
  191. data/CONTRIBUTING.md +0 -73
  192. data/Gemfile +0 -78
  193. data/Rakefile +0 -102
  194. data/SECURITY.md +0 -13
  195. data/alchemy_cms.gemspec +0 -97
  196. data/app/assets/builds/alchemy/custom-properties.css +0 -1
  197. data/app/helpers/alchemy/admin/elements_helper.rb +0 -25
  198. data/app/stylesheets/alchemy/custom-properties.css +0 -244
  199. data/bin/importmap +0 -4
  200. data/bin/rails +0 -9
  201. data/bin/rspec +0 -3
  202. data/bin/setup +0 -30
  203. data/bin/start +0 -17
  204. data/bun.lockb +0 -0
  205. data/bundles/remixicon.mjs +0 -153
  206. data/bundles/shoelace.js +0 -12
  207. data/bundles/tinymce.js +0 -22
  208. data/eslint.config.js +0 -18
  209. data/lib/alchemy/configuration/class_set_option.rb +0 -46
  210. data/lib/alchemy/configuration/integer_list_option.rb +0 -13
  211. data/lib/alchemy/configuration/list_option.rb +0 -22
  212. data/lib/alchemy/configuration/string_list_option.rb +0 -13
  213. data/lib/alchemy/upgrader/.keep +0 -0
  214. data/lib/alchemy/upgrader/tasks/.keep +0 -0
  215. data/rollup.config.mjs +0 -108
  216. data/vitest.config.js +0 -21
@@ -9,15 +9,28 @@ module Alchemy
9
9
 
10
10
  def initialize(value:, name:, **args)
11
11
  @name = name
12
- @value = validate(value) unless value.nil?
12
+ validate(value) unless value.nil?
13
+ @value = value
13
14
  end
14
15
  attr_reader :name, :value
15
16
 
16
- private
17
-
18
17
  def validate(value)
19
- raise TypeError, "#{name} must be set as a #{self.class.value_class.name}, given #{value.inspect}" unless value.is_a?(self.class.value_class)
20
- value
18
+ raise ConfigurationError.new(name, value, allowed_classes) unless allowed_classes.any? { value.is_a?(_1) }
19
+ end
20
+
21
+ def allowed_classes
22
+ [self.class.value_class]
23
+ end
24
+
25
+ def raw_value = @value
26
+
27
+ def ==(other)
28
+ self.class == other.class && raw_value == other.raw_value
29
+ end
30
+ alias_method :eql?, :==
31
+
32
+ def hash
33
+ [self.class, raw_value].hash
21
34
  end
22
35
  end
23
36
  end
@@ -5,11 +5,8 @@ require "alchemy/configuration/base_option"
5
5
  module Alchemy
6
6
  class Configuration
7
7
  class BooleanOption < BaseOption
8
- private
9
-
10
- def validate(value)
11
- raise TypeError, "#{name} must be a Boolean, given #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
12
- value
8
+ def allowed_classes
9
+ [TrueClass, FalseClass]
13
10
  end
14
11
  end
15
12
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "alchemy/configuration/base_option"
4
+
5
+ module Alchemy
6
+ class Configuration
7
+ class CollectionOption < BaseOption
8
+ include Enumerable
9
+
10
+ def self.value_class
11
+ Enumerable
12
+ end
13
+
14
+ attr_reader :collection_class, :item_class, :item_args
15
+
16
+ def initialize(value:, name:, item_type:, collection_class: Array, **args)
17
+ @collection_class = collection_class
18
+ @item_class = get_item_class(item_type)
19
+ @item_args = args
20
+ value = [] if value.nil?
21
+ collection = @collection_class.new(value.map { |value| to_item(value) })
22
+ super(value: collection, name: name)
23
+ rescue ConfigurationError => configuration_error
24
+ raise ConfigurationError.new(name, configuration_error.value, configuration_error.allowed_classes)
25
+ end
26
+
27
+ def value
28
+ self
29
+ end
30
+
31
+ def <<(value)
32
+ @value << to_item(value)
33
+ end
34
+ alias_method(:add, :<<)
35
+
36
+ def concat(values)
37
+ values.each do |value|
38
+ add(value)
39
+ end
40
+ end
41
+
42
+ delegate :join, :[], to: :to_a
43
+
44
+ delegate :clear, :empty?, to: :@value
45
+
46
+ def each(&block)
47
+ @value.each do |option|
48
+ yield option.value
49
+ end
50
+ end
51
+
52
+ def to_serializable_array
53
+ to_a.map do |item|
54
+ item.respond_to?(:to_h) ? item.to_h : item
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def to_item(value)
61
+ @item_class.new(value: value, name: "#{name}_item", **item_args)
62
+ end
63
+
64
+ def get_item_class(item_type)
65
+ "Alchemy::Configuration::#{item_type.to_s.classify}Option".constantize
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "alchemy/configuration/base_option"
4
+
5
+ module Alchemy
6
+ class Configuration
7
+ class ConfigurationOption < BaseOption
8
+ def self.value_class
9
+ Hash
10
+ end
11
+
12
+ attr_reader :config_class
13
+
14
+ def initialize(value:, name:, config_class:, **args)
15
+ @name = name
16
+ @config_class = config_class
17
+ validate(value)
18
+ @value = if value.is_a?(config_class)
19
+ value
20
+ else
21
+ config_class.new(value)
22
+ end
23
+ end
24
+
25
+ def validate(value)
26
+ return true if value.is_a?(config_class)
27
+ super
28
+ end
29
+
30
+ def allowed_classes
31
+ super + [config_class]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "alchemy/configuration/base_option"
4
+ module Alchemy
5
+ class Configuration
6
+ class PathnameOption < BaseOption
7
+ def self.value_class
8
+ Pathname
9
+ end
10
+ end
11
+ end
12
+ end
@@ -4,16 +4,28 @@ require "active_support"
4
4
  require "active_support/core_ext/string"
5
5
 
6
6
  require "alchemy/configuration/boolean_option"
7
+ require "alchemy/configuration/collection_option"
8
+ require "alchemy/configuration/configuration_option"
7
9
  require "alchemy/configuration/class_option"
8
- require "alchemy/configuration/class_set_option"
9
10
  require "alchemy/configuration/integer_option"
10
- require "alchemy/configuration/integer_list_option"
11
+ require "alchemy/configuration/pathname_option"
11
12
  require "alchemy/configuration/regexp_option"
12
- require "alchemy/configuration/string_list_option"
13
13
  require "alchemy/configuration/string_option"
14
14
 
15
15
  module Alchemy
16
16
  class Configuration
17
+ class ConfigurationError < StandardError
18
+ attr_reader :name, :value, :allowed_classes
19
+
20
+ def initialize(name, value, allowed_classes)
21
+ @name = name
22
+ @value = value
23
+ @allowed_classes = allowed_classes
24
+ expected_classes_message = allowed_classes.map(&:name).to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
25
+ super("Invalid configuration value for #{name}: #{value.inspect} (expected #{expected_classes_message})")
26
+ end
27
+ end
28
+
17
29
  def initialize(configuration_hash = {})
18
30
  set(configuration_hash)
19
31
  end
@@ -45,7 +57,8 @@ module Alchemy
45
57
 
46
58
  def to_h
47
59
  self.class.defined_options.map do |option|
48
- [option, send(option)]
60
+ value = send(option)
61
+ [option, value.respond_to?(:to_serializable_array) ? value.to_serializable_array : value]
49
62
  end.concat(
50
63
  self.class.defined_configurations.map do |configuration|
51
64
  [configuration, send(configuration).to_h]
@@ -58,6 +71,10 @@ module Alchemy
58
71
 
59
72
  def defined_options = []
60
73
 
74
+ def defined_values
75
+ defined_options + defined_configurations
76
+ end
77
+
61
78
  def configuration(name, configuration_class)
62
79
  # The defined configurations on a class are all those defined directly on
63
80
  # that class as well as those defined on ancestors.
@@ -99,11 +116,19 @@ module Alchemy
99
116
  super() + singleton_options
100
117
  end
101
118
 
102
- define_method(name) do
119
+ define_method("#{name}_option") do
103
120
  unless instance_variable_defined?(:"@#{name}")
104
121
  send(:"#{name}=", default)
105
122
  end
106
- instance_variable_get(:"@#{name}").value
123
+ instance_variable_get(:"@#{name}")
124
+ end
125
+
126
+ define_method(name) do
127
+ send("#{name}_option").value
128
+ end
129
+
130
+ define_method("raw_#{name}") do
131
+ send("#{name}_option").raw_value
107
132
  end
108
133
 
109
134
  define_method(:"#{name}=") do |value|
@@ -111,5 +136,18 @@ module Alchemy
111
136
  end
112
137
  end
113
138
  end
139
+
140
+ def hash
141
+ self.class.defined_values.map do |ivar|
142
+ [ivar, send(ivar).hash]
143
+ end.hash
144
+ end
145
+
146
+ def ==(other)
147
+ equal?(other) || self.class == other.class && self.class.defined_values.all? do |var|
148
+ send(var) == other.send(var)
149
+ end
150
+ end
151
+ alias_method :eql?, :==
114
152
  end
115
153
  end
@@ -4,7 +4,7 @@ module Alchemy
4
4
  module Configurations
5
5
  class FormatMatchers < Alchemy::Configuration
6
6
  option :email, :regexp, default: /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
7
- option :url, :regexp, default: /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\z/ix
7
+ option :url, :regexp, default: /\A[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\z/ix
8
8
  option :link_url, :regexp, default: /^(tel:|mailto:|\/|[a-z]+:\/\/)/
9
9
  end
10
10
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Configurations
5
+ class Importmap < Alchemy::Configuration
6
+ option :importmap_path, :pathname
7
+ option :source_paths, :collection, item_type: :pathname
8
+ option :name, :string
9
+ end
10
+ end
11
+ end
@@ -9,8 +9,8 @@ module Alchemy
9
9
  option :mail_from, :string, default: "your.mail@your-domain.com"
10
10
  option :mail_to, :string, default: "your.mail@your-domain.com"
11
11
  option :subject, :string, default: "A new contact form message"
12
- option :fields, :string_list, default: %w[salutation firstname lastname address zip city phone email message]
13
- option :validate_fields, :string_list, default: %w[lastname email]
12
+ option :fields, :collection, item_type: :string, default: %w[salutation firstname lastname address zip city phone email message]
13
+ option :validate_fields, :collection, item_type: :string, default: %w[lastname email]
14
14
  end
15
15
  end
16
16
  end
@@ -3,8 +3,10 @@
3
3
  require "alchemy/configuration"
4
4
  require "alchemy/configurations/default_language"
5
5
  require "alchemy/configurations/default_site"
6
+ require "alchemy/configurations/importmap"
6
7
  require "alchemy/configurations/format_matchers"
7
8
  require "alchemy/configurations/mailer"
9
+ require "alchemy/configurations/page_cache"
8
10
  require "alchemy/configurations/preview"
9
11
  require "alchemy/configurations/sitemap"
10
12
  require "alchemy/configurations/uploader"
@@ -31,6 +33,12 @@ module Alchemy
31
33
  #
32
34
  option :cache_pages, :boolean, default: true
33
35
 
36
+ # === Page caching max age
37
+ #
38
+ # max-age [Integer] # The duration in seconds for which the page is cached before revalidation.
39
+ # stale-while-revalidate [Boolean] # If true, enables the stale-while-revalidate caching strategy.
40
+ configuration :page_cache, PageCache
41
+
34
42
  # === Sitemap
35
43
  #
36
44
  # Alchemy creates a XML, Google compatible, sitemap for you.
@@ -155,7 +163,7 @@ module Alchemy
155
163
  # user_roles:
156
164
  # rolename: Name of the role
157
165
  #
158
- option :user_roles, :string_list, default: %w[member author editor admin]
166
+ option :user_roles, :collection, item_type: :string, default: %w[member author editor admin]
159
167
 
160
168
  # === Uploader Settings
161
169
  #
@@ -177,7 +185,7 @@ module Alchemy
177
185
  #
178
186
  # jQuery(a[data-link-target="overlay"]).dialog();
179
187
  #
180
- option :link_target_options, :string_list, default: %w[blank]
188
+ option :link_target_options, :collection, item_type: :string, default: %w[blank]
181
189
 
182
190
  # === Format matchers
183
191
  #
@@ -194,7 +202,7 @@ module Alchemy
194
202
  option :admin_page_preview_layout, :string, default: "application"
195
203
 
196
204
  # The sizes for the preview size select in the page editor.
197
- option :page_preview_sizes, :integer_list, default: [360, 640, 768, 1024, 1280, 1440]
205
+ option :page_preview_sizes, :collection, item_type: :integer, default: [360, 640, 768, 1024, 1280, 1440]
198
206
 
199
207
  # Enable full text search configuration
200
208
  #
@@ -211,6 +219,143 @@ module Alchemy
211
219
  # The storage adapter for Pictures and Attachments
212
220
  #
213
221
  option :storage_adapter, :string, default: "dragonfly"
222
+
223
+ # Define page preview sources
224
+ #
225
+ # A preview source is a Ruby class returning an URL
226
+ # that is used as source for the preview frame in the
227
+ # admin UI.
228
+ #
229
+ # == Example
230
+ #
231
+ # # lib/acme/preview_source.rb
232
+ # class Acme::PreviewSource < Alchemy::Admin::PreviewUrl
233
+ # def url_for(page)
234
+ # if page.site.name == "Next"
235
+ # "https://user:#{ENV['PREVIEW_HTTP_PASS']}@next.acme.com"
236
+ # else
237
+ # "https://www.acme.com"
238
+ # end
239
+ # end
240
+ # end
241
+ #
242
+ # # config/initializers/alchemy.rb
243
+ # require "acme/preview_source"
244
+ # Alchemy.config.preview_sources << "Acme::PreviewSource"
245
+ #
246
+ # # config/locales/de.yml
247
+ # de:
248
+ # activemodel:
249
+ # models:
250
+ # acme/preview_source: Acme Vorschau
251
+ #
252
+ option :preview_sources, :collection, item_type: :class, collection_class: Set, default: ["Alchemy::Admin::PreviewUrl"]
253
+
254
+ # Additional JS modules to be imported in the Alchemy admin UI
255
+ #
256
+ # Be sure to also pin the modules with +Alchemy.importmap+.
257
+ #
258
+ # == Example
259
+ #
260
+ # Alchemy.importmap.pin "flatpickr/de",
261
+ # to: "https://ga.jspm.io/npm:flatpickr@4.6.13/dist/l10n/de.js"
262
+ #
263
+ # Alchemy.config.admin_js_imports << "flatpickr/de"
264
+ #
265
+ option :admin_js_imports, :collection, item_type: :string, collection_class: Set, default: []
266
+
267
+ # Additional importmaps to be included in the Alchemy admin UI
268
+ #
269
+ # Be sure to also pin modules with +Alchemy.importmap+.
270
+ #
271
+ # == Example
272
+ #
273
+ # # config/alchemy/importmap.rb
274
+ # Alchemy.importmap.pin "alchemy_solidus", to: "alchemy_solidus.js", preload: true
275
+ # Alchemy.importmap.pin_all_from Alchemy::Solidus::Engine.root.join("app/javascript/alchemy_solidus"),
276
+ # under: "alchemy_solidus",
277
+ # preload: true
278
+ #
279
+ # # lib/alchemy/solidus/engine.rb
280
+ # initializer "alchemy_solidus.assets", before: "alchemy.importmap" do |app|
281
+ # Alchemy.admin_importmaps.add({
282
+ # importmap_path: root.join("config/importmap.rb"),
283
+ # source_paths: [
284
+ # root.join("app/javascript")
285
+ # ],
286
+ # name: "alchemy_solidus"
287
+ # })
288
+ # app.config.assets.precompile << "alchemy_solidus/manifest.js"
289
+ # end
290
+ #
291
+ option :admin_importmaps, :collection, collection_class: Set, item_type: :configuration, config_class: Alchemy::Configurations::Importmap, default: []
292
+
293
+ # Additional stylesheets to be included in the Alchemy admin UI
294
+ #
295
+ # == Example
296
+ #
297
+ # # lib/alchemy/devise/engine.rb
298
+ # initializer "alchemy.devise.stylesheets", before: "alchemy.admin_stylesheets" do
299
+ # Alchemy.config.admin_stylesheets << "alchemy/devise/admin.css"
300
+ # end
301
+ #
302
+ option :admin_stylesheets, :collection, collection_class: Set, item_type: :string, default: ["alchemy/admin/custom.css"]
303
+
304
+ # Define page publish targets
305
+ #
306
+ # A publish target is a ActiveJob that gets performed
307
+ # whenever a user clicks the publish page button.
308
+ #
309
+ # Use this to trigger deployment hooks of external
310
+ # services in an asychronous way.
311
+ #
312
+ # == Example
313
+ #
314
+ # # app/jobs/publish_job.rb
315
+ # class PublishJob < ApplicationJob
316
+ # def perform(page)
317
+ # RestClient.post(ENV['BUILD_HOOK_URL'])
318
+ # end
319
+ # end
320
+ #
321
+ # # config/initializers/alchemy.rb
322
+ # Alchemy.config.publish_targets << PublishJob
323
+ #
324
+ option :publish_targets, :collection, collection_class: Set, item_type: :class, default: []
325
+
326
+ # Configure tabs in the link dialog
327
+ #
328
+ # With this configuration that tabs in the link dialog can be extended
329
+ # without overwriting or defacing the Admin Interface.
330
+ #
331
+ # == Example
332
+ #
333
+ # # components/acme/link_tab.rb
334
+ # module Acme
335
+ # class LinkTab < ::Alchemy::Admin::LinkDialog::BaseTab
336
+ # def title
337
+ # "Awesome Tab Title"
338
+ # end
339
+ #
340
+ # def name
341
+ # :unique_name
342
+ # end
343
+ #
344
+ # def fields
345
+ # [ title_input, target_select ]
346
+ # end
347
+ # end
348
+ # end
349
+ #
350
+ # # config/initializers/alchemy.rb
351
+ # Alchemy.config.link_dialog_tabs << "Acme::LinkTab"
352
+ #
353
+ option :link_dialog_tabs, :collection, collection_class: Set, item_type: :class, default: [
354
+ "Alchemy::Admin::LinkDialog::InternalTab",
355
+ "Alchemy::Admin::LinkDialog::AnchorTab",
356
+ "Alchemy::Admin::LinkDialog::ExternalTab",
357
+ "Alchemy::Admin::LinkDialog::FileTab"
358
+ ]
214
359
  end
215
360
  end
216
361
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module Configurations
5
+ class PageCache < Alchemy::Configuration
6
+ # === Page caching max age
7
+ #
8
+ # Control the max-age duration in seconds in the cache-control header.
9
+ #
10
+ option :max_age, :integer, default: 600
11
+
12
+ # === Page caching stale-while-revalidate
13
+ #
14
+ # Set stale-while-revalidate cache-control header.
15
+ #
16
+ option :stale_while_revalidate, :integer
17
+ end
18
+ end
19
+ end
@@ -4,8 +4,8 @@ module Alchemy
4
4
  module Configurations
5
5
  class Uploader < Alchemy::Configuration
6
6
  class AllowedFileTypes < Alchemy::Configuration
7
- option :alchemy_attachments, :string_list, default: ["*"]
8
- option :alchemy_pictures, :string_list, default: %w[jpg jpeg gif png svg webp]
7
+ option :alchemy_attachments, :collection, item_type: :string, default: ["*"]
8
+ option :alchemy_pictures, :collection, item_type: :string, default: %w[jpg jpeg gif png svg webp]
9
9
 
10
10
  def set(configuration_hash)
11
11
  super(configuration_hash.transform_keys { transform_key(_1) })
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- Deprecation = ActiveSupport::Deprecation.new("8.0", "Alchemy")
4
+ Deprecation = ActiveSupport::Deprecation.new("9.0", "Alchemy")
5
5
  end
@@ -26,7 +26,7 @@ module Alchemy
26
26
 
27
27
  initializer "alchemy.admin_stylesheets" do |app|
28
28
  if defined?(Sprockets)
29
- Alchemy.admin_stylesheets.each do |stylesheet|
29
+ Alchemy.config.admin_stylesheets.each do |stylesheet|
30
30
  app.config.assets.precompile << stylesheet
31
31
  end
32
32
  end
@@ -42,33 +42,48 @@ module Alchemy
42
42
  end
43
43
  end
44
44
 
45
+ initializer "alchemy.admin_importmap" do
46
+ Alchemy.config.admin_importmaps.add(
47
+ importmap_path: root.join("config/importmap.rb"),
48
+ source_paths: [
49
+ root.join("app/javascript"),
50
+ root.join("vendor/javascript")
51
+ ],
52
+ name: "alchemy_admin"
53
+ )
54
+ end
55
+
45
56
  initializer "alchemy.importmap" do |app|
46
- watch_paths = []
47
-
48
- Alchemy.admin_importmaps.each do |admin_import|
49
- Alchemy.importmap.draw admin_import[:importmap_path]
50
- watch_paths += admin_import[:source_paths]
51
- app.config.assets.paths += admin_import[:source_paths]
52
- if admin_import[:name] != "alchemy_admin"
53
- Alchemy.admin_js_imports.add(admin_import[:name])
57
+ app.config.to_prepare do
58
+ watch_paths = []
59
+
60
+ Alchemy.config.admin_importmaps.each do |admin_import|
61
+ Alchemy.importmap.draw admin_import.importmap_path
62
+ watch_paths += admin_import.source_paths.to_a
63
+ app.config.assets.paths += admin_import.source_paths.to_a
64
+ if admin_import[:name] != "alchemy_admin"
65
+ Alchemy.config.admin_js_imports.add(admin_import.name)
66
+ end
54
67
  end
55
- end
56
68
 
57
- if app.config.importmap.sweep_cache
58
- Alchemy.importmap.cache_sweeper(watches: watch_paths)
59
- ActiveSupport.on_load(:action_controller_base) do
60
- before_action { Alchemy.importmap.cache_sweeper.execute_if_updated }
69
+ if app.config.importmap.sweep_cache
70
+ Alchemy.importmap.cache_sweeper(watches: watch_paths)
71
+ ActiveSupport.on_load(:action_controller_base) do
72
+ before_action { Alchemy.importmap.cache_sweeper.execute_if_updated }
73
+ end
61
74
  end
62
75
  end
63
76
  end
64
77
 
78
+ # All the initialization that needs to be re-triggered during reloads
65
79
  config.to_prepare do
80
+ # Definition files
66
81
  elements_reloader = Rails.application.config.file_watcher.new([ElementDefinition.definitions_file_path]) do
67
- Rails.logger.info "[#{engine_name}] Reloading Element Definitions."
82
+ Rails.logger.info "[alchemy] Reloading Element Definitions."
68
83
  ElementDefinition.reset!
69
84
  end
70
85
  page_layouts_reloader = Rails.application.config.file_watcher.new([PageDefinition.layouts_file_path]) do
71
- Rails.logger.info "[#{engine_name}] Reloading Page Layouts."
86
+ Rails.logger.info "[alchemy] Reloading Page Layouts."
72
87
  PageDefinition.reset!
73
88
  end
74
89
  [elements_reloader, page_layouts_reloader].each do |reloader|
@@ -77,12 +92,19 @@ module Alchemy
77
92
  reloader.execute_if_updated
78
93
  end
79
94
  end
80
- end
81
95
 
82
- # Gutentag downcases all tags before save
83
- # and Gutentag validations are not case sensitive.
84
- # But we support having tags with uppercase characters.
85
- config.to_prepare do
96
+ # The storage adapter for Pictures and Attachments
97
+ #
98
+ # Chose between 'active_storage' (default) or 'dragonfly' (legacy)
99
+ #
100
+ # Can be set via 'ALCHEMY_STORAGE_ADAPTER' env var.
101
+ Alchemy.storage_adapter = Alchemy::StorageAdapter.new(
102
+ ENV.fetch("ALCHEMY_STORAGE_ADAPTER", Alchemy.config.storage_adapter)
103
+ )
104
+
105
+ # Gutentag downcases all tags before save
106
+ # and Gutentag validations are not case sensitive.
107
+ # But we support having tags with uppercase characters.
86
108
  Gutentag.normaliser = ->(value) { value.to_s }
87
109
  Gutentag.tag_validations = Alchemy::TagValidations
88
110
  end
@@ -22,18 +22,6 @@ module Alchemy
22
22
  {after: SENTINEL, verbose: true}
23
23
  end
24
24
 
25
- def set_primary_language(code: "en", name: "English", auto_accept: false)
26
- unless auto_accept
27
- code = ask("- What is the language code of your site's primary language?", default: code)
28
- end
29
- unless auto_accept
30
- name = ask("- What is the name of your site's primary language?", default: name)
31
- end
32
- gsub_file "./config/alchemy/config.yml", /default_language:\n\s\scode:\sen\n\s\sname:\sEnglish/m do
33
- "default_language:\n code: #{code}\n name: #{name}"
34
- end
35
- end
36
-
37
25
  def inject_seeder
38
26
  seed_file = Rails.root.join("db", "seeds.rb")
39
27
  args = [seed_file, "Alchemy::Seeder.seed!\n"]
@@ -22,5 +22,11 @@ module Alchemy
22
22
  def convert_to_humanized_name(name, suffix)
23
23
  name.gsub(/\.#{::Regexp.quote(suffix)}$/i, "").tr("_", " ").strip
24
24
  end
25
+
26
+ # Sanitizes a given filename by removing directory traversal attempts and HTML entities.
27
+ def sanitized_filename(file_name)
28
+ file_name = File.basename(file_name)
29
+ CGI.escapeHTML(file_name)
30
+ end
25
31
  end
26
32
  end