iron-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/MIT-LICENSE +20 -0
- data/README.md +174 -0
- data/Rakefile +8 -0
- data/app/assets/builds/iron.css +2 -0
- data/app/assets/config/iron_manifest.js +1 -0
- data/app/assets/images/iron/icons/sprite.svg +9425 -0
- data/app/assets/tailwind/iron/actiontext.css +550 -0
- data/app/assets/tailwind/iron/application.css +66 -0
- data/app/assets/tailwind/iron/components/button.css +74 -0
- data/app/assets/tailwind/iron/components/dropdown.css +42 -0
- data/app/assets/tailwind/iron/components/fieldset.css +18 -0
- data/app/assets/tailwind/iron/components/form.css +14 -0
- data/app/assets/tailwind/iron/components/icon-picker.css +9 -0
- data/app/assets/tailwind/iron/components/input.css +44 -0
- data/app/assets/tailwind/iron/components/page.css +7 -0
- data/app/assets/tailwind/iron/components/select.css +36 -0
- data/app/assets/tailwind/iron/components/sidebar.css +21 -0
- data/app/assets/tailwind/iron/components/textarea.css +27 -0
- data/app/controllers/concerns/iron/authentication.rb +66 -0
- data/app/controllers/concerns/iron/authorization.rb +11 -0
- data/app/controllers/concerns/iron/locale_aware.rb +20 -0
- data/app/controllers/concerns/iron/web_page.rb +18 -0
- data/app/controllers/iron/application_controller.rb +7 -0
- data/app/controllers/iron/block_definitions/field_definitions_controller.rb +8 -0
- data/app/controllers/iron/block_definitions_controller.rb +52 -0
- data/app/controllers/iron/blocks_controller.rb +12 -0
- data/app/controllers/iron/content_types/field_definitions_controller.rb +8 -0
- data/app/controllers/iron/content_types_controller.rb +52 -0
- data/app/controllers/iron/entries_controller.rb +103 -0
- data/app/controllers/iron/field_definitions_controller.rb +55 -0
- data/app/controllers/iron/first_runs_controller.rb +29 -0
- data/app/controllers/iron/home_controller.rb +6 -0
- data/app/controllers/iron/icons_controller.rb +10 -0
- data/app/controllers/iron/locales_controller.rb +52 -0
- data/app/controllers/iron/passwords_controller.rb +36 -0
- data/app/controllers/iron/references_controller.rb +11 -0
- data/app/controllers/iron/schemas_controller.rb +32 -0
- data/app/controllers/iron/sessions_controller.rb +32 -0
- data/app/controllers/iron/settings_controller.rb +23 -0
- data/app/controllers/iron/users_controller.rb +66 -0
- data/app/helpers/iron/application_helper.rb +25 -0
- data/app/helpers/iron/avatar_helper.rb +35 -0
- data/app/helpers/iron/block_helper.rb +4 -0
- data/app/helpers/iron/buttons_helper.rb +14 -0
- data/app/helpers/iron/components/badge_helper.rb +70 -0
- data/app/helpers/iron/components/dropdown_helper.rb +161 -0
- data/app/helpers/iron/content_types_helper.rb +11 -0
- data/app/helpers/iron/entries_helper.rb +13 -0
- data/app/helpers/iron/field_definitions_helper.rb +5 -0
- data/app/helpers/iron/form_builder.rb +43 -0
- data/app/helpers/iron/icons_helper.rb +30 -0
- data/app/helpers/iron/image_helper.rb +50 -0
- data/app/javascript/iron/application.js +6 -0
- data/app/javascript/iron/controllers/application.js +9 -0
- data/app/javascript/iron/controllers/element_controller.js +11 -0
- data/app/javascript/iron/controllers/file_upload_controller.js +18 -0
- data/app/javascript/iron/controllers/form_controller.js +7 -0
- data/app/javascript/iron/controllers/handle_controller.js +27 -0
- data/app/javascript/iron/controllers/icon_picker_controller.js +48 -0
- data/app/javascript/iron/controllers/index.js +4 -0
- data/app/javascript/iron/controllers/sortable_list_controller.js +125 -0
- data/app/javascript/iron/controllers/toggle_controller.js +14 -0
- data/app/javascript/iron/controllers/trix_controller.js +19 -0
- data/app/javascript/iron/lib/lexorank.js +51 -0
- data/app/jobs/iron/application_job.rb +4 -0
- data/app/jobs/iron/generate_entry_routes_job.rb +15 -0
- data/app/mailers/iron/application_mailer.rb +6 -0
- data/app/mailers/passwords_mailer.rb +6 -0
- data/app/models/concerns/iron/csv_serializable.rb +28 -0
- data/app/models/iron/account/joinable.rb +16 -0
- data/app/models/iron/account.rb +13 -0
- data/app/models/iron/application_record.rb +5 -0
- data/app/models/iron/block_definition/portable.rb +20 -0
- data/app/models/iron/block_definition.rb +10 -0
- data/app/models/iron/content_type/field_queryable.rb +101 -0
- data/app/models/iron/content_type/portable.rb +30 -0
- data/app/models/iron/content_type/titlable.rb +13 -0
- data/app/models/iron/content_type/web_publishable.rb +47 -0
- data/app/models/iron/content_type.rb +39 -0
- data/app/models/iron/content_types/collection.rb +6 -0
- data/app/models/iron/content_types/single.rb +9 -0
- data/app/models/iron/current.rb +12 -0
- data/app/models/iron/entry/deep_validation.rb +32 -0
- data/app/models/iron/entry/presentable.rb +40 -0
- data/app/models/iron/entry/schemable.rb +16 -0
- data/app/models/iron/entry/titlable.rb +35 -0
- data/app/models/iron/entry/web_publishable.rb +85 -0
- data/app/models/iron/entry.rb +16 -0
- data/app/models/iron/field/belongs_to_entry.rb +29 -0
- data/app/models/iron/field.rb +20 -0
- data/app/models/iron/field_definition/portable.rb +36 -0
- data/app/models/iron/field_definition.rb +42 -0
- data/app/models/iron/field_definitions/block.rb +23 -0
- data/app/models/iron/field_definitions/block_list.rb +8 -0
- data/app/models/iron/field_definitions/boolean.rb +7 -0
- data/app/models/iron/field_definitions/date.rb +7 -0
- data/app/models/iron/field_definitions/file.rb +67 -0
- data/app/models/iron/field_definitions/number.rb +7 -0
- data/app/models/iron/field_definitions/reference.rb +8 -0
- data/app/models/iron/field_definitions/reference_list.rb +8 -0
- data/app/models/iron/field_definitions/rich_text_area.rb +4 -0
- data/app/models/iron/field_definitions/text_area.rb +7 -0
- data/app/models/iron/field_definitions/text_field.rb +25 -0
- data/app/models/iron/fields/block.rb +40 -0
- data/app/models/iron/fields/block_list.rb +16 -0
- data/app/models/iron/fields/boolean.rb +7 -0
- data/app/models/iron/fields/date.rb +7 -0
- data/app/models/iron/fields/file.rb +29 -0
- data/app/models/iron/fields/number.rb +7 -0
- data/app/models/iron/fields/reference.rb +14 -0
- data/app/models/iron/fields/reference_list.rb +17 -0
- data/app/models/iron/fields/rich_text_area.rb +9 -0
- data/app/models/iron/fields/text_area.rb +7 -0
- data/app/models/iron/fields/text_field.rb +19 -0
- data/app/models/iron/first_run.rb +13 -0
- data/app/models/iron/icon_catalog.rb +45 -0
- data/app/models/iron/locale.rb +30 -0
- data/app/models/iron/reference.rb +8 -0
- data/app/models/iron/schema_archive.rb +71 -0
- data/app/models/iron/schema_exporter.rb +15 -0
- data/app/models/iron/schema_importer.rb +278 -0
- data/app/models/iron/session.rb +5 -0
- data/app/models/iron/user/role.rb +21 -0
- data/app/models/iron/user.rb +37 -0
- data/app/views/active_storage/blobs/_blob.html.erb +14 -0
- data/app/views/iron/block_definitions/_block_definition.html.erb +21 -0
- data/app/views/iron/block_definitions/_form.html.erb +46 -0
- data/app/views/iron/block_definitions/edit.html.erb +8 -0
- data/app/views/iron/block_definitions/index.html.erb +18 -0
- data/app/views/iron/block_definitions/new.html.erb +8 -0
- data/app/views/iron/block_definitions/show.html.erb +34 -0
- data/app/views/iron/blocks/new.turbo_stream.erb +6 -0
- data/app/views/iron/content_types/_content_type.html.erb +21 -0
- data/app/views/iron/content_types/_form.html.erb +137 -0
- data/app/views/iron/content_types/edit.html.erb +8 -0
- data/app/views/iron/content_types/index.html.erb +28 -0
- data/app/views/iron/content_types/new.html.erb +8 -0
- data/app/views/iron/content_types/show.html.erb +49 -0
- data/app/views/iron/entries/_entry.html.erb +17 -0
- data/app/views/iron/entries/_entry_option.html.erb +19 -0
- data/app/views/iron/entries/_form.html.erb +63 -0
- data/app/views/iron/entries/edit.html.erb +52 -0
- data/app/views/iron/entries/entry.html.erb +21 -0
- data/app/views/iron/entries/fields/_block.html.erb +92 -0
- data/app/views/iron/entries/fields/_block_list.html.erb +45 -0
- data/app/views/iron/entries/fields/_boolean.html.erb +13 -0
- data/app/views/iron/entries/fields/_date.html.erb +11 -0
- data/app/views/iron/entries/fields/_file.html.erb +60 -0
- data/app/views/iron/entries/fields/_number.html.erb +11 -0
- data/app/views/iron/entries/fields/_reference.html.erb +12 -0
- data/app/views/iron/entries/fields/_reference_item.html.erb +28 -0
- data/app/views/iron/entries/fields/_reference_list.html.erb +44 -0
- data/app/views/iron/entries/fields/_rich_text_area.html.erb +13 -0
- data/app/views/iron/entries/fields/_text_area.html.erb +11 -0
- data/app/views/iron/entries/fields/_text_field.html.erb +28 -0
- data/app/views/iron/entries/index.html.erb +21 -0
- data/app/views/iron/entries/new.html.erb +8 -0
- data/app/views/iron/entries/search.html.erb +33 -0
- data/app/views/iron/field_definitions/_field_definition.html.erb +37 -0
- data/app/views/iron/field_definitions/block/_form.html.erb +16 -0
- data/app/views/iron/field_definitions/block_list/_form.html.erb +13 -0
- data/app/views/iron/field_definitions/boolean/_form.html.erb +2 -0
- data/app/views/iron/field_definitions/date/_form.html.erb +2 -0
- data/app/views/iron/field_definitions/edit.html.erb +16 -0
- data/app/views/iron/field_definitions/file/_form.html.erb +40 -0
- data/app/views/iron/field_definitions/index.html.erb +14 -0
- data/app/views/iron/field_definitions/layouts/_form.html.erb +47 -0
- data/app/views/iron/field_definitions/new.html.erb +22 -0
- data/app/views/iron/field_definitions/number/_form.html.erb +2 -0
- data/app/views/iron/field_definitions/reference/_form.html.erb +13 -0
- data/app/views/iron/field_definitions/reference_list/_form.html.erb +13 -0
- data/app/views/iron/field_definitions/rich_text_area/_form.html.erb +2 -0
- data/app/views/iron/field_definitions/text_area/_form.html.erb +2 -0
- data/app/views/iron/field_definitions/text_field/_form.html.erb +17 -0
- data/app/views/iron/first_runs/show.html.erb +44 -0
- data/app/views/iron/home/show.html.erb +2 -0
- data/app/views/iron/icons/index.html.erb +21 -0
- data/app/views/iron/locales/_form.html.erb +24 -0
- data/app/views/iron/locales/_locale.html.erb +15 -0
- data/app/views/iron/locales/edit.html.erb +8 -0
- data/app/views/iron/locales/index.html.erb +12 -0
- data/app/views/iron/locales/new.html.erb +8 -0
- data/app/views/iron/passwords/edit.html.erb +7 -0
- data/app/views/iron/passwords/new.html.erb +26 -0
- data/app/views/iron/passwords_mailer/reset.html.erb +4 -0
- data/app/views/iron/passwords_mailer/reset.text.erb +2 -0
- data/app/views/iron/published_pages/show.html.erb +10 -0
- data/app/views/iron/references/new.turbo_stream.erb +6 -0
- data/app/views/iron/schemas/new.html.erb +57 -0
- data/app/views/iron/sessions/new.html.erb +36 -0
- data/app/views/iron/settings/show.html.erb +38 -0
- data/app/views/iron/shared/_icon_picker.html.erb +60 -0
- data/app/views/iron/users/_form.html.erb +21 -0
- data/app/views/iron/users/_user.html.erb +27 -0
- data/app/views/iron/users/edit.html.erb +12 -0
- data/app/views/iron/users/index.html.erb +21 -0
- data/app/views/iron/users/new.html.erb +29 -0
- data/app/views/iron/users/show.html.erb +21 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/app/views/layouts/iron/_sidebar.html.erb +83 -0
- data/app/views/layouts/iron/_sidebar_item.html.erb +14 -0
- data/app/views/layouts/iron/_toast.html.erb +29 -0
- data/app/views/layouts/iron/application.html.erb +46 -0
- data/app/views/layouts/iron/authentication.html.erb +41 -0
- data/config/importmap.rb +9 -0
- data/config/routes.rb +40 -0
- data/db/migrate/20250422131656_initial_iron_schema.rb +105 -0
- data/db/migrate/20250427100754_create_iron_accounts.rb +10 -0
- data/db/migrate/20250427104020_add_role_to_iron_users.rb +5 -0
- data/db/migrate/20250428094923_add_default_iron_locale_to_iron_account.rb +5 -0
- data/db/migrate/20250504144731_create_join_table_content_types_field_definitions.rb +25 -0
- data/db/migrate/20250505090716_add_title_field_definition_to_iron_content_types.rb +5 -0
- data/db/migrate/20250512210352_create_iron_references.rb +11 -0
- data/db/migrate/20250524220355_add_referenced_entry_id_to_iron_fields.rb +5 -0
- data/db/migrate/20250529211703_add_metadata_to_iron_field_definitions.rb +5 -0
- data/db/migrate/20250529211704_add_value_boolean_to_iron_fields.rb +5 -0
- data/db/migrate/20250531172837_add_type_to_iron_content_types.rb +5 -0
- data/db/migrate/20250601080146_add_icon_to_iron_content_types.rb +5 -0
- data/db/migrate/20250609091605_add_web_publishing_to_iron_content_types.rb +7 -0
- data/db/migrate/20250609091813_add_route_to_iron_entries.rb +6 -0
- data/lib/generators/iron/pages/pages_generator.rb +34 -0
- data/lib/generators/iron/pages/templates/pages_controller.rb +14 -0
- data/lib/generators/iron/pages/templates/show.html.erb +9 -0
- data/lib/generators/iron/template/template_generator.rb +35 -0
- data/lib/generators/iron/template/templates/content_type.html.erb +5 -0
- data/lib/iron/cva.rb +69 -0
- data/lib/iron/engine.rb +42 -0
- data/lib/iron/image_analyzer.rb +26 -0
- data/lib/iron/lexorank/exceptions.rb +4 -0
- data/lib/iron/lexorank/rankable.rb +185 -0
- data/lib/iron/lexorank/rebalance_rank_job.rb +10 -0
- data/lib/iron/lexorank/utils.rb +77 -0
- data/lib/iron/lexorank.rb +7 -0
- data/lib/iron/routing.rb +66 -0
- data/lib/iron/sdk.rb +19 -0
- data/lib/iron/version.rb +3 -0
- data/lib/iron.rb +8 -0
- data/lib/puma/plugin/iron_tailwindcss.rb +78 -0
- metadata +458 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
class CreateJoinTableContentTypesFieldDefinitions < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
# create_join_table :content_types, :field_definitions do |t|
|
4
|
+
# # t.index [:content_type_id, :field_definition_id]
|
5
|
+
# # t.index [:field_definition_id, :content_type_id]
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# create_table "iron_content_types_field_definitions", id: false, force: :cascade do |t|
|
9
|
+
# t.integer "content_type_id", null: false
|
10
|
+
# t.integer "field_definition_id", null: false
|
11
|
+
# t.index [ "content_type_id", "field_definition_id" ], name: "idx_on_block_definition_id_field_definition_id_b671d92478", unique: true
|
12
|
+
# t.index [ "content_type_id" ], name: "idx_on_block_definition_id_601f892a8c"
|
13
|
+
# t.index [ "field_definition_id", "block_definition_id" ], name: "idx_on_field_definition_id_block_definition_id_be1c71c596", unique: true
|
14
|
+
# t.index [ "field_definition_id" ], name: "idx_on_field_definition_id_d46765b01a"
|
15
|
+
# end
|
16
|
+
|
17
|
+
create_join_table :content_types, :field_definitions, table_name: :iron_content_types_field_definitions do |t|
|
18
|
+
t.index [ :content_type_id, :field_definition_id ], name: "idx_on_content_type_id_field_definition_id", unique: true
|
19
|
+
t.index [ :field_definition_id, :content_type_id ], name: "idx_on_field_definition_id_content_type_id", unique: true
|
20
|
+
end
|
21
|
+
|
22
|
+
add_foreign_key :iron_content_types_field_definitions, :iron_content_types, column: :content_type_id
|
23
|
+
add_foreign_key :iron_content_types_field_definitions, :iron_field_definitions, column: :field_definition_id
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateIronReferences < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :iron_references do |t|
|
4
|
+
t.references :entry, null: false, foreign_key: { to_table: :iron_entries }
|
5
|
+
t.references :list, null: false, foreign_key: { to_table: :iron_fields }
|
6
|
+
t.text :rank
|
7
|
+
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
class AddWebPublishingToIronContentTypes < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
add_column :iron_content_types, :web_publishing_enabled, :boolean, default: false
|
4
|
+
add_reference :iron_content_types, :web_page_title_field_definition, null: true, foreign_key: { to_table: :iron_field_definitions }
|
5
|
+
add_column :iron_content_types, :base_path, :string
|
6
|
+
end
|
7
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module Iron
|
4
|
+
module Generators
|
5
|
+
class PagesGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
7
|
+
|
8
|
+
def create_controller
|
9
|
+
template "pages_controller.rb", "app/controllers/pages_controller.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_views
|
13
|
+
template "show.html.erb", "app/views/pages/show.html.erb"
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_routes
|
17
|
+
route "iron_pages"
|
18
|
+
end
|
19
|
+
|
20
|
+
def display_instructions
|
21
|
+
say "\n=== Iron CMS Pages Setup Complete ===\n", :green
|
22
|
+
say "The pages controller has been installed in your application.\n"
|
23
|
+
say "\nNext steps:"
|
24
|
+
say "1. Create custom views for your content types in app/views/templates/"
|
25
|
+
say " For a content type with handle 'article', create:"
|
26
|
+
say " - app/views/templates/article.html.erb"
|
27
|
+
say "\n2. The default view (app/views/pages/show.html.erb) will be used"
|
28
|
+
say " for any content types without custom views."
|
29
|
+
say "\n3. The iron_entry_path helper is available from the Iron engine."
|
30
|
+
say "\n4. Remember to restart your Rails server after installation."
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class PagesController < ApplicationController
|
2
|
+
include Iron::LocaleAware
|
3
|
+
|
4
|
+
def show
|
5
|
+
@content_type = Iron::ContentType.web_published.find_by_handle!(params[:content_type])
|
6
|
+
@entry = Iron::SDK.send(@content_type.handle).find(params[:route])
|
7
|
+
|
8
|
+
raise ActiveRecord::RecordNotFound unless @entry
|
9
|
+
|
10
|
+
render "templates/#{@content_type.handle}"
|
11
|
+
rescue ActionView::MissingTemplate
|
12
|
+
render "pages/show"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module Iron
|
4
|
+
module Generators
|
5
|
+
class TemplateGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
7
|
+
|
8
|
+
argument :content_type_handle, type: :string, desc: "The handle of the content type"
|
9
|
+
|
10
|
+
def create_template_file
|
11
|
+
@content_type = Iron::ContentType.find_by!(handle: content_type_handle)
|
12
|
+
template "content_type.html.erb", "app/views/templates/#{content_type_handle}.html.erb"
|
13
|
+
end
|
14
|
+
|
15
|
+
def display_instructions
|
16
|
+
say "\n=== Template created for '#{content_type_name}' ===\n", :green
|
17
|
+
say "You can now customize the template at:"
|
18
|
+
say " app/views/templates/#{content_type_handle}.html.erb"
|
19
|
+
say "\nAvailable variables in the template:"
|
20
|
+
say " @entry - The current entry with all its fields"
|
21
|
+
say " @content_type - The content type definition"
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def content_type_name
|
27
|
+
@content_type.name
|
28
|
+
end
|
29
|
+
|
30
|
+
def content_type_fields
|
31
|
+
@content_type.fields
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/iron/cva.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module Iron
|
2
|
+
module Cva
|
3
|
+
class VariantBuilder
|
4
|
+
def initialize(base_classes: "", variants: {}, defaults: {}, compound_variants: [], required: [])
|
5
|
+
@base_classes = base_classes.split(" ")
|
6
|
+
@variants = normalize_keys(variants)
|
7
|
+
@defaults = normalize_keys(defaults)
|
8
|
+
@compound_variants = compound_variants.map { |cv| normalize_keys(cv) }
|
9
|
+
@required = required.map(&:to_sym)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(options = {})
|
13
|
+
options = normalize_keys(options)
|
14
|
+
additional_classes = options.delete(:class)&.split(" ") || []
|
15
|
+
classes = @base_classes.dup
|
16
|
+
|
17
|
+
# Apply default variants
|
18
|
+
options = @defaults.merge(options)
|
19
|
+
|
20
|
+
# Apply variants
|
21
|
+
@variants.each do |variant_name, variant_options|
|
22
|
+
if value = options[variant_name]
|
23
|
+
# Convert the value to symbol if the variant options use symbol keys
|
24
|
+
value = value.to_sym if variant_options.keys.first.is_a?(Symbol)
|
25
|
+
if variant_classes = variant_options[value]
|
26
|
+
classes.concat(variant_classes.split(" "))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Apply compound variants
|
32
|
+
@compound_variants.each do |compound|
|
33
|
+
conditions = compound.except(:class)
|
34
|
+
matches = conditions.all? do |key, expected_value|
|
35
|
+
actual_value = options[key]
|
36
|
+
# Convert values to the same type for comparison
|
37
|
+
if expected_value.is_a?(Symbol)
|
38
|
+
actual_value = actual_value.to_sym if actual_value
|
39
|
+
elsif expected_value.is_a?(String)
|
40
|
+
actual_value = actual_value.to_s if actual_value
|
41
|
+
end
|
42
|
+
actual_value == expected_value
|
43
|
+
end
|
44
|
+
|
45
|
+
if matches && compound[:class]
|
46
|
+
classes.concat(compound[:class].split(" "))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Validate required variants
|
51
|
+
@required.each do |required_variant|
|
52
|
+
unless options.key?(required_variant)
|
53
|
+
raise ArgumentError, "Missing required variant: #{required_variant}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
result = classes.uniq
|
58
|
+
result.concat(additional_classes)
|
59
|
+
result.join(" ")
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def normalize_keys(hash)
|
65
|
+
hash.transform_keys(&:to_sym)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/iron/engine.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "importmap-rails"
|
2
|
+
require "turbo-rails"
|
3
|
+
require "stimulus-rails"
|
4
|
+
require "active_storage/engine"
|
5
|
+
require "action_text/engine"
|
6
|
+
require "bcrypt"
|
7
|
+
require "ostruct"
|
8
|
+
require "iron/lexorank"
|
9
|
+
require "iron/sdk"
|
10
|
+
require "iron/routing"
|
11
|
+
require "iron/image_analyzer"
|
12
|
+
|
13
|
+
module Iron
|
14
|
+
class Engine < ::Rails::Engine
|
15
|
+
isolate_namespace Iron
|
16
|
+
|
17
|
+
initializer "iron.setup", before: "importmap" do |app|
|
18
|
+
app.config.assets.paths.unshift(root.join("vendor/javascript"))
|
19
|
+
app.config.assets.paths << root.join("app/javascript")
|
20
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
21
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
22
|
+
|
23
|
+
app.config.assets.precompile += %w[iron_manifest]
|
24
|
+
end
|
25
|
+
|
26
|
+
initializer "iron.routing" do
|
27
|
+
ActionDispatch::Routing::Mapper.include Iron::Routing
|
28
|
+
end
|
29
|
+
|
30
|
+
initializer "iron.active_storage_analyzers" do |app|
|
31
|
+
app.config.active_storage.analyzers.prepend Iron::ImageAnalyzer
|
32
|
+
end
|
33
|
+
|
34
|
+
config.to_prepare do
|
35
|
+
ActionView::Base.include(Module.new do
|
36
|
+
def attachment_url(attachment, **options)
|
37
|
+
attachment.url(**options)
|
38
|
+
end
|
39
|
+
end)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Iron
|
2
|
+
class ImageAnalyzer < ActiveStorage::Analyzer::ImageAnalyzer::Vips
|
3
|
+
def metadata
|
4
|
+
download_blob_to_tempfile do |file|
|
5
|
+
super.merge(blur_data_uri: generate_blur_placeholder(file))
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def generate_blur_placeholder(file)
|
12
|
+
processed = ImageProcessing::Vips
|
13
|
+
.source(file)
|
14
|
+
.resize_to_limit(20, nil)
|
15
|
+
.gaussblur(2)
|
16
|
+
.convert("jpg")
|
17
|
+
.saver(quality: 20, strip: true)
|
18
|
+
.call
|
19
|
+
|
20
|
+
image_data = File.read(processed.path)
|
21
|
+
"data:image/jpeg;base64,#{Base64.strict_encode64(image_data)}"
|
22
|
+
ensure
|
23
|
+
processed&.close if processed.respond_to?(:close)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module Iron
|
2
|
+
module Lexorank
|
3
|
+
LONG_RANK_SIZE = 10
|
4
|
+
module Rankable
|
5
|
+
module HasRank
|
6
|
+
attr_reader :rank_column, :rank_scoped_by, :has_rank_applied
|
7
|
+
|
8
|
+
def has_rank(column: :rank, scoped_by: nil)
|
9
|
+
self.class_variable_set("@@rank_column", column)
|
10
|
+
cattr_reader :rank_column, instance_reader: false
|
11
|
+
|
12
|
+
self.class_variable_set("@@rank_scoped_by", grab_column(scoped_by))
|
13
|
+
cattr_reader :rank_scoped_by, instance_reader: false
|
14
|
+
|
15
|
+
@has_rank_applied = true
|
16
|
+
|
17
|
+
scope :ranked, ->(direction: :asc) { where.not("#{column}": nil).order("#{column}": direction) }
|
18
|
+
|
19
|
+
after_save_commit :request_rank_rebalance, if: -> {
|
20
|
+
rank.present? && rank.length > Lexorank::LONG_RANK_SIZE
|
21
|
+
}
|
22
|
+
|
23
|
+
include InstanceMethods
|
24
|
+
extend ClassMethods
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def grab_column(column)
|
30
|
+
return unless column
|
31
|
+
|
32
|
+
if association = reflect_on_association(column)
|
33
|
+
if association.options[:polymorphic]
|
34
|
+
[ association.foreign_type.to_sym, association.foreign_key.to_sym ]
|
35
|
+
else
|
36
|
+
association.foreign_key.to_sym
|
37
|
+
end
|
38
|
+
else
|
39
|
+
column
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
include Iron::Lexorank::Utils
|
46
|
+
|
47
|
+
def rebalance(rank_scope = nil)
|
48
|
+
if rank_scope.present?
|
49
|
+
if rank_scoped_by.is_a?(Array)
|
50
|
+
type_column, id_column = rank_scoped_by
|
51
|
+
ranked_scoped_items = ranked.where(
|
52
|
+
"#{type_column}": rank_scope.first,
|
53
|
+
"#{id_column}": rank_scope.second
|
54
|
+
)
|
55
|
+
else
|
56
|
+
ranked_scoped_items = ranked.where("#{rank_scoped_by}": rank_scope)
|
57
|
+
end
|
58
|
+
|
59
|
+
interval = optimal_rank_numeric_interval_for(ranked_scoped_items.count)
|
60
|
+
|
61
|
+
self.transaction do
|
62
|
+
ranked_scoped_items.each.with_index(1) do |item, index|
|
63
|
+
item.rank = to_rank(interval * index) + "-rebalancing"
|
64
|
+
item.save!
|
65
|
+
end
|
66
|
+
|
67
|
+
ranked_scoped_items.each do |item|
|
68
|
+
item.rank = item.rank.gsub("-rebalancing", "")
|
69
|
+
item.save!
|
70
|
+
end
|
71
|
+
end
|
72
|
+
else
|
73
|
+
raise "Not implemented yet"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
module InstanceMethods
|
79
|
+
include Iron::Lexorank::Utils
|
80
|
+
|
81
|
+
def move_to(position)
|
82
|
+
# exceptions:
|
83
|
+
# move to the beginning (aka move to position 0)
|
84
|
+
# move to end (aka position = collection.size - 1)
|
85
|
+
# when moving to the end of the collection the offset and limit statement automatically handles
|
86
|
+
# that 'after' is nil which is the same like [collection.last, nil]
|
87
|
+
before, after =
|
88
|
+
if position.zero?
|
89
|
+
[ nil, scoped_collection.first ]
|
90
|
+
else
|
91
|
+
scoped_collection.where.not(id: id).offset(position - 1).limit(2)
|
92
|
+
end
|
93
|
+
|
94
|
+
rank =
|
95
|
+
if self == after && send(rank_column).present?
|
96
|
+
send(rank_column)
|
97
|
+
else
|
98
|
+
value_between(before&.send(rank_column), after&.send(rank_column))
|
99
|
+
end
|
100
|
+
|
101
|
+
send("#{rank_column}=", rank)
|
102
|
+
end
|
103
|
+
|
104
|
+
def move_to!(position)
|
105
|
+
move_to(position)
|
106
|
+
save
|
107
|
+
end
|
108
|
+
|
109
|
+
def move_to_top
|
110
|
+
after = scoped_collection.first
|
111
|
+
rank = value_between(nil, after&.send(rank_column))
|
112
|
+
send("#{rank_column}=", rank)
|
113
|
+
end
|
114
|
+
|
115
|
+
def move_to_top!
|
116
|
+
move_to_top
|
117
|
+
save
|
118
|
+
end
|
119
|
+
|
120
|
+
def move_to_bottom
|
121
|
+
before = scoped_collection.last
|
122
|
+
rank = value_between(before&.send(rank_column), nil)
|
123
|
+
send("#{rank_column}=", rank)
|
124
|
+
end
|
125
|
+
|
126
|
+
def move_to_bottom!
|
127
|
+
move_to_bottom
|
128
|
+
save
|
129
|
+
end
|
130
|
+
|
131
|
+
def no_rank?
|
132
|
+
!send(rank_column)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def rank_column
|
138
|
+
self.class.rank_column
|
139
|
+
end
|
140
|
+
|
141
|
+
def rank_scoped_by
|
142
|
+
self.class.rank_scoped_by
|
143
|
+
end
|
144
|
+
|
145
|
+
def scoped_collection
|
146
|
+
collection = base_class.ranked
|
147
|
+
if rank_scoped_by.present?
|
148
|
+
if rank_scoped_by.is_a?(Array)
|
149
|
+
type_column, id_column = rank_scoped_by
|
150
|
+
collection = collection.where(
|
151
|
+
"#{type_column}": send(type_column),
|
152
|
+
"#{id_column}": send(id_column)
|
153
|
+
)
|
154
|
+
else
|
155
|
+
collection = collection.where("#{rank_scoped_by}": send(rank_scoped_by))
|
156
|
+
end
|
157
|
+
end
|
158
|
+
collection
|
159
|
+
end
|
160
|
+
|
161
|
+
def base_class
|
162
|
+
if self.class.has_rank_applied
|
163
|
+
self.class
|
164
|
+
else
|
165
|
+
self.class.base_class
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def request_rank_rebalance
|
170
|
+
if rank_scoped_by.is_a?(Array)
|
171
|
+
type_column, id_column = rank_scoped_by
|
172
|
+
rank_scope = [ send(type_column), send(id_column) ]
|
173
|
+
else
|
174
|
+
rank_scope = send(rank_scoped_by)
|
175
|
+
end
|
176
|
+
Iron::Lexorank::RebalanceRankJob.perform_later(base_class.name, rank_scope)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
ActiveSupport.on_load :active_record do
|
184
|
+
extend Iron::Lexorank::Rankable::HasRank
|
185
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Iron::Lexorank::Utils
|
2
|
+
MIN_CHAR = "0"
|
3
|
+
MAX_CHAR = "z"
|
4
|
+
BASE = MAX_CHAR.ord - MIN_CHAR.ord + 1
|
5
|
+
|
6
|
+
def value_between(before_, after_)
|
7
|
+
before = before_ || MIN_CHAR
|
8
|
+
after = after_ || MAX_CHAR
|
9
|
+
|
10
|
+
rank = ""
|
11
|
+
|
12
|
+
(before.length + after.length).times do |i|
|
13
|
+
prev_char = get_char(before, i, MIN_CHAR)
|
14
|
+
after_char = get_char(after, i, MAX_CHAR)
|
15
|
+
|
16
|
+
if prev_char == after_char
|
17
|
+
rank += prev_char
|
18
|
+
next
|
19
|
+
end
|
20
|
+
|
21
|
+
mid_char = mid(prev_char, after_char)
|
22
|
+
if mid_char == prev_char || mid_char == after_char
|
23
|
+
rank += prev_char
|
24
|
+
next
|
25
|
+
end
|
26
|
+
|
27
|
+
rank += mid_char
|
28
|
+
break
|
29
|
+
end
|
30
|
+
|
31
|
+
# Problem: If we try to get a rank before the character '0' or after 'z' the algorithm would return the same char
|
32
|
+
# This first of all breaks a possible unique constraint and of course makes no sense when ordering the items.
|
33
|
+
#
|
34
|
+
# Thoughts: I think this issue will never happen with the Lexorank::Rankable module
|
35
|
+
# Why? Let's look at '0' as a rank:
|
36
|
+
# Because the algorithm always chooses the char in between two other chars, '0' can only happen when before is nil and after is '1'
|
37
|
+
# In this case the algorithm will return '0U' though. This means there will never be an item with rank '0' which is why this condition
|
38
|
+
# should never equal to true.
|
39
|
+
#
|
40
|
+
# Please report if you have another opinion about that or if you reached the exception! (of course you can force it by using `value_between(nil, '0')`)
|
41
|
+
if rank >= after
|
42
|
+
raise Lexorank::Exceptions::InvalidRankError,
|
43
|
+
"This rank should not be achievable using the Lexorank::Rankable module! " \
|
44
|
+
"The supplied ranks were #{before_.inspect} and #{after_.inspect}."
|
45
|
+
end
|
46
|
+
rank
|
47
|
+
end
|
48
|
+
|
49
|
+
def mid(prev, after)
|
50
|
+
middle_ascii = ((prev.ord + after.ord) / 2).round
|
51
|
+
middle_ascii.chr
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_char(string, index, default_char)
|
55
|
+
index >= string.length ? default_char : string[index]
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_rank(number)
|
59
|
+
return MIN_CHAR if number.zero?
|
60
|
+
|
61
|
+
result = ""
|
62
|
+
while number > 0
|
63
|
+
remainder = number % BASE
|
64
|
+
number /= BASE
|
65
|
+
result = (MIN_CHAR.ord + remainder).chr + result
|
66
|
+
end
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
def optimal_rank_numeric_interval_for(items_count)
|
71
|
+
i = 1
|
72
|
+
while items_count >= BASE ** i
|
73
|
+
i += 1
|
74
|
+
end
|
75
|
+
(BASE ** i) / (items_count + 1)
|
76
|
+
end
|
77
|
+
end
|