panda-cms 0.7.4 → 0.7.5

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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +37 -2
  3. data/Rakefile +2 -0
  4. data/app/builders/panda/cms/form_builder.rb +13 -5
  5. data/app/components/panda/cms/admin/heading_component.rb +5 -4
  6. data/app/components/panda/cms/admin/panel_component.rb +2 -2
  7. data/app/components/panda/cms/admin/statistics_component.rb +1 -2
  8. data/app/components/panda/cms/admin/user_activity_component.html.erb +3 -1
  9. data/app/components/panda/cms/admin/user_activity_component.rb +1 -3
  10. data/app/components/panda/cms/code_component.rb +8 -4
  11. data/app/components/panda/cms/menu_component.rb +7 -6
  12. data/app/components/panda/cms/page_menu_component.rb +15 -17
  13. data/app/components/panda/cms/rich_text_component.rb +5 -6
  14. data/app/components/panda/cms/text_component.rb +6 -7
  15. data/app/constraints/panda/cms/admin_constraint.rb +4 -1
  16. data/app/controllers/panda/cms/admin/dashboard_controller.rb +13 -9
  17. data/app/controllers/panda/cms/admin/forms_controller.rb +0 -2
  18. data/app/controllers/panda/cms/admin/my_profile_controller.rb +2 -1
  19. data/app/controllers/panda/cms/admin/pages_controller.rb +2 -1
  20. data/app/controllers/panda/cms/admin/posts_controller.rb +1 -2
  21. data/app/controllers/panda/cms/admin/sessions_controller.rb +3 -5
  22. data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +32 -25
  23. data/app/controllers/panda/cms/admin/settings_controller.rb +14 -10
  24. data/app/controllers/panda/cms/application_controller.rb +7 -2
  25. data/app/controllers/panda/cms/errors_controller.rb +5 -2
  26. data/app/controllers/panda/cms/form_submissions_controller.rb +2 -0
  27. data/app/controllers/panda/cms/pages_controller.rb +32 -29
  28. data/app/controllers/panda/cms/posts_controller.rb +2 -0
  29. data/app/helpers/panda/cms/admin/files_helper.rb +5 -1
  30. data/app/helpers/panda/cms/admin/pages_helper.rb +5 -1
  31. data/app/helpers/panda/cms/asset_helper.rb +182 -0
  32. data/app/helpers/panda/cms/pages_helper.rb +2 -0
  33. data/app/helpers/panda/cms/posts_helper.rb +2 -0
  34. data/app/helpers/panda/cms/theme_helper.rb +2 -0
  35. data/app/javascript/panda/cms/controllers/editor_form_controller.js +59 -6
  36. data/app/javascript/panda/cms/controllers/index.js +3 -9
  37. data/app/javascript/panda/cms/controllers/theme_form_controller.js +16 -0
  38. data/app/javascript/panda/cms/stimulus-loading.js +39 -0
  39. data/app/javascript/panda_cms/stimulus-loading.js +39 -0
  40. data/app/jobs/panda/cms/application_job.rb +2 -0
  41. data/app/jobs/panda/cms/record_visit_job.rb +2 -0
  42. data/app/mailers/panda/cms/application_mailer.rb +2 -0
  43. data/app/mailers/panda/cms/form_mailer.rb +3 -1
  44. data/app/models/panda/cms/application_record.rb +2 -0
  45. data/app/models/panda/cms/block.rb +4 -1
  46. data/app/models/panda/cms/block_content.rb +2 -0
  47. data/app/models/panda/cms/breadcrumb.rb +2 -0
  48. data/app/models/panda/cms/current.rb +2 -0
  49. data/app/models/panda/cms/form.rb +2 -0
  50. data/app/models/panda/cms/form_submission.rb +2 -0
  51. data/app/models/panda/cms/menu.rb +12 -9
  52. data/app/models/panda/cms/menu_item.rb +10 -6
  53. data/app/models/panda/cms/page.rb +14 -12
  54. data/app/models/panda/cms/post.rb +9 -5
  55. data/app/models/panda/cms/redirect.rb +6 -3
  56. data/app/models/panda/cms/template.rb +12 -7
  57. data/app/models/panda/cms/user.rb +2 -0
  58. data/app/models/panda/cms/visit.rb +2 -0
  59. data/app/models/panda/social/instagram_post.rb +2 -0
  60. data/app/services/panda/cms/html_to_editor_js_converter.rb +4 -2
  61. data/app/services/panda/social/instagram_feed_service.rb +3 -1
  62. data/app/views/layouts/different_page.html.erb +6 -0
  63. data/app/views/layouts/homepage.html.erb +37 -0
  64. data/app/views/layouts/page.html.erb +18 -0
  65. data/app/views/layouts/panda/cms/application.html.erb +1 -0
  66. data/app/views/panda/cms/admin/pages/new.html.erb +14 -8
  67. data/app/views/panda/cms/admin/settings/index.html.erb +1 -1
  68. data/app/views/panda/cms/shared/_header.html.erb +10 -2
  69. data/app/views/panda/cms/shared/_importmap.html.erb +1 -1
  70. data/app/views/shared/_footer.html.erb +3 -0
  71. data/app/views/shared/_header.html.erb +11 -0
  72. data/config/importmap.rb +2 -0
  73. data/config/initializers/inflections.rb +2 -0
  74. data/config/initializers/panda/cms/form_errors.rb +20 -21
  75. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +2 -0
  76. data/config/initializers/panda/cms.rb +2 -0
  77. data/config/initializers/zeitwork.rb +2 -0
  78. data/config/puma/test.rb +3 -1
  79. data/config/routes.rb +8 -8
  80. data/db/migrate/20240205223709_create_panda_cms_pages.rb +2 -0
  81. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +2 -0
  82. data/db/migrate/20240303002805_create_panda_cms_templates.rb +4 -1
  83. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +2 -0
  84. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +4 -1
  85. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +2 -0
  86. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +2 -0
  87. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +2 -0
  88. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +2 -0
  89. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +2 -0
  90. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +7 -5
  91. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +2 -0
  92. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +3 -1
  93. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +2 -0
  94. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +2 -0
  95. data/db/migrate/20240317010532_create_panda_cms_users.rb +2 -0
  96. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +2 -0
  97. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +2 -0
  98. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +2 -0
  99. data/db/migrate/20240317230622_create_panda_cms_visits.rb +2 -0
  100. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +5 -2
  101. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +2 -0
  102. data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +8 -6
  103. data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +2 -0
  104. data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +2 -0
  105. data/db/migrate/20240804235210_create_panda_cms_forms.rb +2 -0
  106. data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +2 -0
  107. data/db/migrate/20240805121123_create_panda_cms_posts.rb +3 -1
  108. data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +2 -0
  109. data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +2 -0
  110. data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +2 -0
  111. data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +2 -0
  112. data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +6 -4
  113. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +2 -0
  114. data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +2 -0
  115. data/db/migrate/20241120000419_remove_post_tag_references.rb +2 -0
  116. data/db/migrate/20241120110943_add_editor_js_to_posts.rb +2 -0
  117. data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +2 -0
  118. data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +2 -0
  119. data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +2 -0
  120. data/db/migrate/20250120235542_remove_paper_trail.rb +5 -4
  121. data/db/migrate/20250126234001_create_panda_social_instagram_posts.rb +2 -0
  122. data/db/migrate/20250504221812_add_current_theme_to_panda_cms_users.rb +2 -0
  123. data/db/seeds.rb +2 -0
  124. data/lib/generators/panda/cms/install_generator.rb +2 -0
  125. data/lib/panda/cms/asset_loader.rb +390 -0
  126. data/lib/panda/cms/bulk_editor.rb +7 -3
  127. data/lib/panda/cms/demo_site_generator.rb +2 -0
  128. data/lib/panda/cms/editor_js/blocks/alert.rb +2 -0
  129. data/lib/panda/cms/editor_js/blocks/base.rb +2 -0
  130. data/lib/panda/cms/editor_js/blocks/header.rb +2 -0
  131. data/lib/panda/cms/editor_js/blocks/image.rb +3 -0
  132. data/lib/panda/cms/editor_js/blocks/list.rb +2 -0
  133. data/lib/panda/cms/editor_js/blocks/paragraph.rb +3 -0
  134. data/lib/panda/cms/editor_js/blocks/quote.rb +3 -0
  135. data/lib/panda/cms/editor_js/blocks/table.rb +3 -1
  136. data/lib/panda/cms/editor_js/renderer.rb +3 -0
  137. data/lib/panda/cms/editor_js.rb +2 -0
  138. data/lib/panda/cms/editor_js_content.rb +47 -41
  139. data/lib/panda/cms/engine.rb +29 -32
  140. data/lib/panda/cms/exceptions_app.rb +2 -0
  141. data/lib/panda/cms/railtie.rb +2 -0
  142. data/lib/panda/cms/slug.rb +3 -1
  143. data/lib/panda-cms/version.rb +3 -1
  144. data/lib/panda-cms.rb +4 -2
  145. data/lib/tasks/assets.rake +547 -0
  146. data/lib/tasks/panda/cms/install.rake +2 -0
  147. data/lib/tasks/panda/social/instagram.rake +2 -0
  148. data/lib/tasks/panda_cms.rake +3 -30
  149. data/public/panda-cms-assets/manifest.json +20 -0
  150. data/public/panda-cms-assets/panda-cms-0.7.4.css +26 -0
  151. data/public/panda-cms-assets/panda-cms-0.7.4.js +150 -0
  152. metadata +168 -14
@@ -0,0 +1,390 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ # AssetLoader handles loading compiled assets from GitHub releases
6
+ # Falls back to local development assets when GitHub assets unavailable
7
+ class AssetLoader
8
+ class << self
9
+ # Generate HTML tags for loading Panda CMS assets
10
+ def asset_tags(options = {})
11
+ if use_github_assets?
12
+ github_asset_tags(options)
13
+ else
14
+ development_asset_tags(options)
15
+ end
16
+ end
17
+
18
+ # Get the JavaScript asset URL
19
+ def javascript_url
20
+ if use_github_assets?
21
+ github_javascript_url
22
+ else
23
+ development_javascript_url
24
+ end
25
+ end
26
+
27
+ # Get the CSS asset URL (if exists)
28
+ def css_url
29
+ if use_github_assets?
30
+ github_css_url
31
+ else
32
+ development_css_url
33
+ end
34
+ end
35
+
36
+ # Check if GitHub-hosted assets should be used
37
+ def use_github_assets?
38
+ # Use GitHub assets in production or when explicitly enabled
39
+ Rails.env.production? ||
40
+ ENV["PANDA_CMS_USE_GITHUB_ASSETS"] == "true" ||
41
+ !development_assets_available? ||
42
+ ((Rails.env.test? || in_test_environment?) && compiled_assets_available?)
43
+ end
44
+
45
+ # Download assets from GitHub to local cache
46
+ def ensure_assets_available!
47
+ return if development_assets_available? && !use_github_assets?
48
+
49
+ cache_dir = local_cache_directory
50
+ version = `git rev-parse --short HEAD`.strip
51
+
52
+ # Check if we already have cached assets for this version
53
+ if cached_assets_exist?(version)
54
+ Rails.logger.info "[Panda CMS] Using cached assets #{version}"
55
+ return
56
+ end
57
+
58
+ Rails.logger.info "[Panda CMS] Downloading assets #{version} from GitHub..."
59
+ download_github_assets(version, cache_dir)
60
+ end
61
+
62
+ private
63
+
64
+ def github_asset_tags(options = {})
65
+ version = asset_version
66
+ base_url = github_base_url(version)
67
+
68
+ tags = []
69
+
70
+ # JavaScript tag with integrity check
71
+ js_url = "#{base_url}panda-cms-#{version}.js"
72
+ integrity = asset_integrity(version, "panda-cms-#{version}.js")
73
+
74
+ js_attrs = {
75
+ src: js_url
76
+ }
77
+ # In CI environment, don't use defer to ensure immediate execution
78
+ js_attrs[:defer] = true unless ENV["GITHUB_ACTIONS"] == "true"
79
+ # Standalone bundles should NOT use type="module" - they're regular scripts
80
+ # Only use type="module" for importmap/ES module assets
81
+ if !js_url.include?("panda-cms-assets")
82
+ js_attrs[:type] = "module"
83
+ end
84
+ js_attrs[:integrity] = integrity if integrity
85
+ js_attrs[:crossorigin] = "anonymous" if integrity
86
+
87
+ tags << content_tag(:script, "", js_attrs)
88
+
89
+ # CSS tag if CSS bundle exists
90
+ css_url = "#{base_url}panda-cms-#{version}.css"
91
+ if github_asset_exists?(version, "panda-cms-#{version}.css")
92
+ css_integrity = asset_integrity(version, "panda-cms-#{version}.css")
93
+
94
+ css_attrs = {
95
+ rel: "stylesheet",
96
+ href: css_url
97
+ }
98
+ css_attrs[:integrity] = css_integrity if css_integrity
99
+ css_attrs[:crossorigin] = "anonymous" if css_integrity
100
+
101
+ tags << tag(:link, css_attrs)
102
+ end
103
+
104
+ tags.join("\n").html_safe
105
+ end
106
+
107
+ def development_asset_tags(options = {})
108
+ # In test environment with CI, always use compiled assets
109
+ if (Rails.env.test? || ENV["CI"].present?) && compiled_assets_available?
110
+ # Use the same logic as GitHub assets but with local paths
111
+ version = asset_version
112
+ js_url = "/panda-cms-assets/panda-cms-#{version}.js"
113
+ css_url = "/panda-cms-assets/panda-cms-#{version}.css"
114
+
115
+ tags = []
116
+
117
+ # JavaScript tag
118
+ tags << content_tag(:script, "", {
119
+ src: js_url,
120
+ defer: true
121
+ })
122
+
123
+ # CSS tag if exists
124
+ if cached_asset_exists?(css_url)
125
+ tags << tag(:link, {
126
+ rel: "stylesheet",
127
+ href: css_url
128
+ })
129
+ end
130
+
131
+ tags.join("\n").html_safe
132
+ else
133
+ # In development, just use a simple script tag
134
+ # The view will handle importmap tags separately
135
+ content_tag(:script, "", {
136
+ src: development_javascript_url,
137
+ type: "module",
138
+ defer: true
139
+ })
140
+ end
141
+ end
142
+
143
+ def github_javascript_url
144
+ version = asset_version
145
+ # In test environment with local compiled assets, use local URL
146
+ if Rails.env.test? && compiled_assets_available?
147
+ "/panda-cms-assets/panda-cms-#{version}.js"
148
+ else
149
+ "#{github_base_url(version)}panda-cms-#{version}.js"
150
+ end
151
+ end
152
+
153
+ def github_css_url
154
+ version = asset_version
155
+ # In test environment with local compiled assets, use local URL
156
+ if Rails.env.test? && compiled_assets_available?
157
+ "/panda-cms-assets/panda-cms-#{version}.css"
158
+ else
159
+ "#{github_base_url(version)}panda-cms-#{version}.css"
160
+ end
161
+ end
162
+
163
+ def development_javascript_url
164
+ # Try cached assets first, then importmap
165
+ version = asset_version
166
+ # Try root level first (standalone bundle), then versioned directory
167
+ root_path = "/panda-cms-assets/panda-cms-#{version}.js"
168
+ versioned_path = "/panda-cms-assets/#{version}/panda-cms-#{version}.js"
169
+
170
+ if cached_asset_exists?(root_path)
171
+ root_path
172
+ elsif cached_asset_exists?(versioned_path)
173
+ versioned_path
174
+ else
175
+ # Fallback to importmap or engine asset
176
+ "/assets/panda/cms/controllers/index.js"
177
+ end
178
+ end
179
+
180
+ def development_css_url
181
+ version = asset_version
182
+ # Try versioned directory first, then root level
183
+ versioned_path = "/panda-cms-assets/#{version}/panda-cms-#{version}.css"
184
+ root_path = "/panda-cms-assets/panda-cms-#{version}.css"
185
+
186
+ if cached_asset_exists?(versioned_path)
187
+ versioned_path
188
+ elsif cached_asset_exists?(root_path)
189
+ root_path
190
+ else
191
+ nil # No CSS in development mode typically
192
+ end
193
+ end
194
+
195
+ def github_base_url(version)
196
+ # In test environment with compiled assets, use local URLs
197
+ if (Rails.env.test? || in_test_environment?) && compiled_assets_available?
198
+ "/panda-cms-assets/"
199
+ else
200
+ "https://github.com/tastybamboo/panda-cms/releases/download/#{version}/"
201
+ end
202
+ end
203
+
204
+ def asset_version
205
+ # In test environment, use VERSION constant for consistency with compiled assets
206
+ # In other environments, use git SHA for dynamic versioning
207
+ # Also check for test environment indicators since Rails.env might be development in specs
208
+ if Rails.env.test? || ENV["CI"].present? || in_test_environment?
209
+ Panda::CMS::VERSION
210
+ else
211
+ `git rev-parse --short HEAD`.strip
212
+ end
213
+ end
214
+
215
+ def in_test_environment?
216
+ # Check if we're running specs even if Rails.env is development
217
+ defined?(RSpec) && RSpec.respond_to?(:configuration)
218
+ end
219
+
220
+ def compiled_assets_available?
221
+ # Check if compiled assets exist in test location
222
+ version = asset_version
223
+ js_file = Rails.public_path.join("panda-cms-assets", "panda-cms-#{version}.js")
224
+ css_file = Rails.public_path.join("panda-cms-assets", "panda-cms-#{version}.css")
225
+ js_file.exist? && css_file.exist?
226
+ end
227
+
228
+ def development_assets_available?
229
+ # Check if local development assets exist (importmap, etc.)
230
+ importmap_available? || engine_assets_available?
231
+ end
232
+
233
+ def importmap_available?
234
+ return false unless defined?(Rails.application.importmap)
235
+
236
+ begin
237
+ # Rails 8+ uses a different API for accessing importmap entries
238
+ if Rails.application.importmap.respond_to?(:to_json)
239
+ importmap_json = JSON.parse(Rails.application.importmap.to_json)
240
+ importmap_json.any? { |name, _| name.include?("panda") }
241
+ elsif Rails.application.importmap.respond_to?(:entries)
242
+ Rails.application.importmap.entries.any? { |entry| entry.name.include?("panda") }
243
+ else
244
+ false
245
+ end
246
+ rescue => e
247
+ Rails.logger.debug "[Panda CMS] Could not check importmap: #{e.message}"
248
+ false
249
+ end
250
+ end
251
+
252
+ def engine_assets_available?
253
+ # Check if engine's JavaScript files are available
254
+ engine_js_path = Rails.root.join("..", "..", "app", "javascript", "panda", "cms", "controllers", "index.js")
255
+ File.exist?(engine_js_path)
256
+ end
257
+
258
+ def cached_assets_exist?(version)
259
+ cache_dir = local_cache_directory.join(version)
260
+ cache_dir.exist? && cache_dir.join("panda-cms-#{version}.js").exist?
261
+ end
262
+
263
+ def cached_asset_exists?(path)
264
+ Rails.public_path.join(path.delete_prefix("/")).exist?
265
+ end
266
+
267
+ def local_cache_directory
268
+ Rails.public_path.join("panda-cms-assets")
269
+ end
270
+
271
+ def download_github_assets(version, cache_dir)
272
+ require "net/http"
273
+ require "uri"
274
+ require "json"
275
+
276
+ version_dir = cache_dir.join(version)
277
+ FileUtils.mkdir_p(version_dir)
278
+
279
+ # Download manifest first
280
+ manifest_url = "#{github_base_url(version)}manifest.json"
281
+
282
+ begin
283
+ manifest_response = fetch_url(manifest_url)
284
+ if manifest_response.success?
285
+ manifest = JSON.parse(manifest_response.body)
286
+
287
+ # Download each file
288
+ manifest["files"].each do |file_info|
289
+ filename = file_info["filename"]
290
+ file_url = "#{github_base_url(version)}#{filename}"
291
+
292
+ Rails.logger.debug "[Panda CMS] Downloading #{filename}..."
293
+
294
+ file_response = fetch_url(file_url)
295
+ if file_response.success?
296
+ File.write(version_dir.join(filename), file_response.body)
297
+ Rails.logger.debug "[Panda CMS] Downloaded #{filename}"
298
+ else
299
+ Rails.logger.warn "[Panda CMS] Failed to download #{filename}: #{file_response.code}"
300
+ end
301
+ end
302
+
303
+ # Save manifest
304
+ File.write(version_dir.join("manifest.json"), manifest_response.body)
305
+ Rails.logger.info "[Panda CMS] Assets cached locally"
306
+ else
307
+ Rails.logger.warn "[Panda CMS] Failed to download manifest: #{manifest_response.code}"
308
+ end
309
+ rescue => e
310
+ Rails.logger.error "[Panda CMS] Error downloading assets: #{e.message}"
311
+ # Fall back to development mode
312
+ end
313
+ end
314
+
315
+ def fetch_url(url)
316
+ uri = URI(url)
317
+ http = Net::HTTP.new(uri.host, uri.port)
318
+ http.use_ssl = (uri.scheme == "https")
319
+ http.open_timeout = 10
320
+ http.read_timeout = 30
321
+
322
+ request = Net::HTTP::Get.new(uri)
323
+ request["User-Agent"] = "Panda-CMS/#{`git rev-parse --short HEAD`}"
324
+
325
+ response = http.request(request)
326
+
327
+ OpenStruct.new(
328
+ success?: response.code.to_i == 200,
329
+ code: response.code,
330
+ body: response.body
331
+ )
332
+ rescue => e
333
+ Rails.logger.error "[Panda CMS] Network error: #{e.message}"
334
+ OpenStruct.new(success?: false, code: "error", body: nil)
335
+ end
336
+
337
+ def asset_integrity(version, filename)
338
+ # Try to get integrity from cached manifest
339
+ manifest_path = local_cache_directory.join(version, "manifest.json")
340
+ return nil unless manifest_path.exist?
341
+
342
+ begin
343
+ manifest = JSON.parse(File.read(manifest_path))
344
+ file_info = manifest["files"].find { |f| f["filename"] == filename }
345
+ return nil unless file_info
346
+
347
+ "sha256-#{Base64.strict_encode64([file_info["sha256"]].pack("H*"))}"
348
+ rescue => e
349
+ Rails.logger.warn "[Panda CMS] Error reading asset integrity: #{e.message}"
350
+ nil
351
+ end
352
+ end
353
+
354
+ def github_asset_exists?(version, filename)
355
+ # Quick check - assume it exists for now
356
+ # Could be enhanced to do HEAD request
357
+ true
358
+ end
359
+
360
+ def content_tag(name, content, options = {})
361
+ if defined?(ActionView::Helpers::TagHelper)
362
+ # Create a view context to render the tag
363
+ view_context = ActionView::Base.new(ActionView::LookupContext.new([]), {}, nil)
364
+ view_context.content_tag(name, content, options)
365
+ else
366
+ # Fallback implementation
367
+ attrs = options.map { |k, v| %(#{k}="#{v}") }.join(" ")
368
+ if content.present?
369
+ "<#{name} #{attrs}>#{content}</#{name}>"
370
+ else
371
+ "<#{name} #{attrs}></#{name}>"
372
+ end
373
+ end
374
+ end
375
+
376
+ def tag(name, options = {})
377
+ if defined?(ActionView::Helpers::TagHelper)
378
+ # Create a view context to render the tag
379
+ view_context = ActionView::Base.new(ActionView::LookupContext.new([]), {}, nil)
380
+ view_context.tag(name, options)
381
+ else
382
+ # Fallback implementation
383
+ attrs = options.map { |k, v| %(#{k}="#{v}") }.join(" ")
384
+ "<#{name} #{attrs}>"
385
+ end
386
+ end
387
+ end
388
+ end
389
+ end
390
+ end
@@ -1,4 +1,5 @@
1
- require "htmlentities"
1
+ # frozen_string_literal: true
2
+
2
3
  require "json"
3
4
 
4
5
  module Panda
@@ -77,11 +78,12 @@ module Panda
77
78
 
78
79
  if current_data.dig("pages", path, "contents", key).nil?
79
80
  raise "Unknown page 1" if page.nil?
81
+
80
82
  block = Panda::CMS::Block.find_or_create_by(key: key, template: page.template) do |block_meta|
81
83
  block_meta.name = key.titleize
82
84
  end
83
85
 
84
- if !block
86
+ unless block
85
87
  debug[:error] << "Error creating block '#{key.titleize}' on page '#{page.title}'"
86
88
  next
87
89
  end
@@ -104,6 +106,7 @@ module Panda
104
106
  elsif content != current_data["pages"][path]["contents"][key]["content"]
105
107
  # Content has changed
106
108
  raise "Unknown page 2" if page.nil?
109
+
107
110
  block = Panda::CMS::Block.find_by(key: key, template: page.template)
108
111
  if Panda::CMS::BlockContent.find_by(page: page, block: block)&.update(content: content)
109
112
  debug[:success] << "Updated '#{key.titleize}' content on page '#{page.title}'"
@@ -143,7 +146,8 @@ module Panda
143
146
  end
144
147
 
145
148
  # TODO: Eventually set the position of the block in the template, and then order from there rather than the name?
146
- Panda::CMS::BlockContent.includes(:block, page: [:template]).order("panda_cms_pages.lft ASC, panda_cms_blocks.key ASC").each do |block_content|
149
+ Panda::CMS::BlockContent.includes(:block,
150
+ page: [:template]).order("panda_cms_pages.lft ASC, panda_cms_blocks.key ASC").each do |block_content|
147
151
  item = data["pages"][block_content.page.path] ||= {}
148
152
  item["title"] = block_content.page.title
149
153
  item["template"] = block_content.page.template.name
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  class DemoSiteGenerator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  module EditorJs
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  module EditorJs
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  module EditorJs
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  module EditorJs
@@ -27,6 +29,7 @@ module Panda
27
29
 
28
30
  def caption_element(caption)
29
31
  return "" if caption.blank?
32
+
30
33
  "<figcaption>#{caption}</figcaption>"
31
34
  end
32
35
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  module EditorJs
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  module EditorJs
@@ -6,6 +8,7 @@ module Panda
6
8
  def render
7
9
  content = sanitize(data["text"])
8
10
  return "" if content.blank?
11
+
9
12
  html_safe("<p>#{content}</p>")
10
13
  end
11
14
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  module EditorJs
@@ -32,6 +34,7 @@ module Panda
32
34
 
33
35
  def caption_element(caption)
34
36
  return "" if caption.blank?
37
+
35
38
  "<figcaption>#{sanitize(caption)}</figcaption>"
36
39
  end
37
40
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Panda
2
4
  module CMS
3
5
  module EditorJs
@@ -23,7 +25,7 @@ module Panda
23
25
  index = 0
24
26
 
25
27
  while index < content.length
26
- rows << if index == 0 && with_headings
28
+ rows << if index.zero? && with_headings
27
29
  render_header_row(content[index])
28
30
  else
29
31
  render_data_row(content[index])
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sanitize"
2
4
 
3
5
  module Panda
@@ -60,6 +62,7 @@ module Panda
60
62
  # Only allow the exact valid content we expect
61
63
  valid_content = '<figure class="text-left"><blockquote><p>Valid HTML</p></blockquote><figcaption>Valid caption</figcaption></figure>'
62
64
  return html if html.strip == valid_content.strip
65
+
63
66
  return ""
64
67
  end
65
68
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "editor_js/blocks/base"
2
4
  require_relative "editor_js/blocks/alert"
3
5
  require_relative "editor_js/blocks/header"
@@ -1,55 +1,61 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
 
3
- module Panda::CMS::EditorJsContent
4
- extend ActiveSupport::Concern
5
+ module Panda
6
+ module CMS
7
+ module EditorJsContent
8
+ extend ActiveSupport::Concern
5
9
 
6
- included do
7
- include ActiveModel::Validations
8
- include ActiveModel::Callbacks
10
+ included do
11
+ include ActiveModel::Validations
12
+ include ActiveModel::Callbacks
9
13
 
10
- before_save :generate_cached_content
11
- end
14
+ before_save :generate_cached_content
15
+ end
12
16
 
13
- def content=(value)
14
- if value.is_a?(Hash)
15
- super(value.to_json)
16
- else
17
- super
18
- end
19
- end
17
+ def content=(value)
18
+ if value.is_a?(Hash)
19
+ super(value.to_json)
20
+ else
21
+ super
22
+ end
23
+ end
20
24
 
21
- def content
22
- value = super
23
- if value.is_a?(String)
24
- begin
25
- JSON.parse(value)
26
- rescue JSON::ParserError
27
- value
25
+ def content
26
+ value = super
27
+ if value.is_a?(String)
28
+ begin
29
+ JSON.parse(value)
30
+ rescue JSON::ParserError
31
+ value
32
+ end
33
+ else
34
+ value
35
+ end
28
36
  end
29
- else
30
- value
31
- end
32
- end
33
37
 
34
- def generate_cached_content
35
- if content.is_a?(String)
36
- begin
37
- parsed_content = JSON.parse(content)
38
- self.cached_content = if parsed_content.is_a?(Hash) && parsed_content["blocks"].present?
39
- Panda::CMS::EditorJs::Renderer.new(parsed_content).render
38
+ def generate_cached_content
39
+ if content.is_a?(String)
40
+ begin
41
+ parsed_content = JSON.parse(content)
42
+ self.cached_content = if parsed_content.is_a?(Hash) && parsed_content["blocks"].present?
43
+ Panda::CMS::EditorJs::Renderer.new(parsed_content).render
44
+ else
45
+ content
46
+ end
47
+ rescue JSON::ParserError
48
+ # If it's not JSON, treat it as plain text
49
+ self.cached_content = content
50
+ end
51
+ elsif content.is_a?(Hash) && content["blocks"].present?
52
+ # Process EditorJS content
53
+ self.cached_content = Panda::CMS::EditorJs::Renderer.new(content).render
40
54
  else
41
- content
55
+ # For any other case, store as is
56
+ self.cached_content = content.to_s
42
57
  end
43
- rescue JSON::ParserError
44
- # If it's not JSON, treat it as plain text
45
- self.cached_content = content
46
58
  end
47
- elsif content.is_a?(Hash) && content["blocks"].present?
48
- # Process EditorJS content
49
- self.cached_content = Panda::CMS::EditorJs::Renderer.new(content).render
50
- else
51
- # For any other case, store as is
52
- self.cached_content = content.to_s
53
59
  end
54
60
  end
55
61
  end