actiontext 6.1.4.1 → 7.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -91
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +4 -0
  5. data/app/assets/javascripts/actiontext.js +880 -0
  6. data/app/assets/javascripts/trix.js +5278 -0
  7. data/app/assets/stylesheets/trix.css +375 -0
  8. data/app/helpers/action_text/content_helper.rb +17 -3
  9. data/app/helpers/action_text/tag_helper.rb +5 -7
  10. data/app/models/action_text/encrypted_rich_text.rb +9 -0
  11. data/app/models/action_text/record.rb +1 -1
  12. data/app/models/action_text/rich_text.rb +4 -0
  13. data/app/views/action_text/contents/_content.html.erb +1 -0
  14. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  15. data/db/migrate/20180528164100_create_action_text_tables.rb +14 -2
  16. data/lib/action_text/attachable.rb +4 -0
  17. data/lib/action_text/attachment.rb +4 -4
  18. data/lib/action_text/attachment_gallery.rb +14 -9
  19. data/lib/action_text/attachments/caching.rb +1 -1
  20. data/lib/action_text/attachments/minification.rb +1 -1
  21. data/lib/action_text/attribute.rb +18 -2
  22. data/lib/action_text/content.rb +7 -3
  23. data/lib/action_text/encryption.rb +38 -0
  24. data/lib/action_text/engine.rb +14 -0
  25. data/lib/action_text/fixture_set.rb +49 -0
  26. data/lib/action_text/gem_version.rb +4 -4
  27. data/lib/action_text/rendering.rb +1 -1
  28. data/lib/action_text/trix_attachment.rb +3 -3
  29. data/lib/action_text.rb +1 -0
  30. data/lib/generators/action_text/install/install_generator.rb +28 -35
  31. data/lib/generators/action_text/install/templates/actiontext.css +35 -0
  32. data/package.json +13 -3
  33. metadata +28 -22
  34. data/app/views/action_text/content/_layout.html.erb +0 -3
  35. data/lib/generators/action_text/install/templates/actiontext.scss +0 -36
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ class EncryptedRichText < RichText
5
+ self.table_name = "action_text_rich_texts"
6
+
7
+ encrypts :body
8
+ end
9
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionText
4
- class Record < ActiveRecord::Base #:nodoc:
4
+ class Record < ActiveRecord::Base # :nodoc:
5
5
  self.abstract_class = true
6
6
  end
7
7
  end
@@ -22,6 +22,10 @@ module ActionText
22
22
  body&.to_plain_text.to_s
23
23
  end
24
24
 
25
+ def to_trix_html
26
+ body&.to_trix_html
27
+ end
28
+
25
29
  delegate :blank?, :empty?, :present?, to: :to_plain_text
26
30
  end
27
31
  end
@@ -0,0 +1 @@
1
+ <%= render_action_text_content(content) %>
@@ -0,0 +1,3 @@
1
+ <div class="trix-content">
2
+ <%= yield -%>
3
+ </div>
@@ -1,13 +1,25 @@
1
1
  class CreateActionTextTables < ActiveRecord::Migration[6.0]
2
2
  def change
3
- create_table :action_text_rich_texts do |t|
3
+ # Use Active Record's configured type for primary and foreign keys
4
+ primary_key_type, foreign_key_type = primary_and_foreign_key_types
5
+
6
+ create_table :action_text_rich_texts, id: primary_key_type do |t|
4
7
  t.string :name, null: false
5
8
  t.text :body, size: :long
6
- t.references :record, null: false, polymorphic: true, index: false
9
+ t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
7
10
 
8
11
  t.timestamps
9
12
 
10
13
  t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
11
14
  end
12
15
  end
16
+
17
+ private
18
+ def primary_and_foreign_key_types
19
+ config = Rails.configuration.generators
20
+ setting = config.options[config.orm][:primary_key_type]
21
+ primary_key_type = setting || :primary_key
22
+ foreign_key_type = setting || :bigint
23
+ [primary_key_type, foreign_key_type]
24
+ end
13
25
  end
@@ -71,6 +71,10 @@ module ActionText
71
71
  to_partial_path
72
72
  end
73
73
 
74
+ def to_attachable_partial_path
75
+ to_partial_path
76
+ end
77
+
74
78
  def to_rich_text_attributes(attributes = {})
75
79
  attributes.dup.tap do |attrs|
76
80
  attrs[:sgid] = attachable_sgid
@@ -6,8 +6,8 @@ module ActionText
6
6
  class Attachment
7
7
  include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
8
8
 
9
- TAG_NAME = "action-text-attachment"
10
- SELECTOR = TAG_NAME
9
+ mattr_accessor :tag_name, default: "action-text-attachment"
10
+
11
11
  ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation caption )
12
12
 
13
13
  class << self
@@ -20,7 +20,7 @@ module ActionText
20
20
  end
21
21
 
22
22
  def from_attachables(attachables)
23
- Array(attachables).map { |attachable| from_attachable(attachable) }.compact
23
+ Array(attachables).filter_map { |attachable| from_attachable(attachable) }
24
24
  end
25
25
 
26
26
  def from_attachable(attachable, attributes = {})
@@ -38,7 +38,7 @@ module ActionText
38
38
  private
39
39
  def node_from_attributes(attributes)
40
40
  if attributes = process_attributes(attributes).presence
41
- ActionText::HtmlConversion.create_element(TAG_NAME, attributes)
41
+ ActionText::HtmlConversion.create_element(tag_name, attributes)
42
42
  end
43
43
  end
44
44
 
@@ -4,6 +4,9 @@ module ActionText
4
4
  class AttachmentGallery
5
5
  include ActiveModel::Model
6
6
 
7
+ TAG_NAME = "div"
8
+ private_constant :TAG_NAME
9
+
7
10
  class << self
8
11
  def fragment_by_canonicalizing_attachment_galleries(content)
9
12
  fragment_by_replacing_attachment_gallery_nodes(content) do |node|
@@ -20,12 +23,12 @@ module ActionText
20
23
  end
21
24
 
22
25
  def find_attachment_gallery_nodes(content)
23
- Fragment.wrap(content).find_all(SELECTOR).select do |node|
26
+ Fragment.wrap(content).find_all(selector).select do |node|
24
27
  node.children.all? do |child|
25
28
  if child.text?
26
29
  /\A(\n|\ )*\z/.match?(child.text)
27
30
  else
28
- child.matches? ATTACHMENT_SELECTOR
31
+ child.matches? attachment_selector
29
32
  end
30
33
  end
31
34
  end
@@ -34,6 +37,14 @@ module ActionText
34
37
  def from_node(node)
35
38
  new(node)
36
39
  end
40
+
41
+ def attachment_selector
42
+ "#{ActionText::Attachment.tag_name}[presentation=gallery]"
43
+ end
44
+
45
+ def selector
46
+ "#{TAG_NAME}:has(#{attachment_selector} + #{attachment_selector})"
47
+ end
37
48
  end
38
49
 
39
50
  attr_reader :node
@@ -43,7 +54,7 @@ module ActionText
43
54
  end
44
55
 
45
56
  def attachments
46
- @attachments ||= node.css(ATTACHMENT_SELECTOR).map do |node|
57
+ @attachments ||= node.css(ActionText::AttachmentGallery.attachment_selector).map do |node|
47
58
  ActionText::Attachment.from_node(node).with_full_attributes
48
59
  end
49
60
  end
@@ -55,11 +66,5 @@ module ActionText
55
66
  def inspect
56
67
  "#<#{self.class.name} size=#{size.inspect}>"
57
68
  end
58
-
59
- TAG_NAME = "div"
60
- ATTACHMENT_SELECTOR = "#{ActionText::Attachment::SELECTOR}[presentation=gallery]"
61
- SELECTOR = "#{TAG_NAME}:has(#{ATTACHMENT_SELECTOR} + #{ATTACHMENT_SELECTOR})"
62
-
63
- private_constant :TAG_NAME, :ATTACHMENT_SELECTOR, :SELECTOR
64
69
  end
65
70
  end
@@ -9,7 +9,7 @@ module ActionText
9
9
 
10
10
  private
11
11
  def cache_digest
12
- Digest::SHA256.hexdigest(node.to_s)
12
+ OpenSSL::Digest::SHA256.hexdigest(node.to_s)
13
13
  end
14
14
  end
15
15
  end
@@ -7,7 +7,7 @@ module ActionText
7
7
 
8
8
  class_methods do
9
9
  def fragment_by_minifying_attachments(content)
10
- Fragment.wrap(content).replace(ActionText::Attachment::SELECTOR) do |node|
10
+ Fragment.wrap(content).replace(ActionText::Attachment.tag_name) do |node|
11
11
  node.tap { |n| n.inner_html = "" }
12
12
  end
13
13
  end
@@ -24,7 +24,13 @@ module ActionText
24
24
  #
25
25
  # Message.all.with_rich_text_content # Avoids N+1 queries when you just want the body, not the attachments.
26
26
  # Message.all.with_rich_text_content_and_embeds # Avoids N+1 queries when you just want the body and attachments.
27
- def has_rich_text(name)
27
+ # Message.all.with_all_rich_text # Loads all rich text associations.
28
+ #
29
+ # === Options
30
+ #
31
+ # * <tt>:encrypted</tt> - Pass true to encrypt the rich text attribute. The encryption will be non-deterministic. See
32
+ # +ActiveRecord::Encryption::EncryptableRecord.encrypts+. Default: false.
33
+ def has_rich_text(name, encrypted: false)
28
34
  class_eval <<-CODE, __FILE__, __LINE__ + 1
29
35
  def #{name}
30
36
  rich_text_#{name} || build_rich_text_#{name}
@@ -39,12 +45,22 @@ module ActionText
39
45
  end
40
46
  CODE
41
47
 
48
+ rich_text_class_name = encrypted ? "ActionText::EncryptedRichText" : "ActionText::RichText"
42
49
  has_one :"rich_text_#{name}", -> { where(name: name) },
43
- class_name: "ActionText::RichText", as: :record, inverse_of: :record, autosave: true, dependent: :destroy
50
+ class_name: rich_text_class_name, as: :record, inverse_of: :record, autosave: true, dependent: :destroy
44
51
 
45
52
  scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") }
46
53
  scope :"with_rich_text_#{name}_and_embeds", -> { includes("rich_text_#{name}": { embeds_attachments: :blob }) }
47
54
  end
55
+
56
+ # Eager load all dependent RichText models in bulk.
57
+ def with_all_rich_text
58
+ eager_load(rich_text_association_names)
59
+ end
60
+
61
+ def rich_text_association_names
62
+ reflect_on_all_associations(:has_one).collect(&:name).select { |n| n.start_with?("rich_text_") }
63
+ end
48
64
  end
49
65
  end
50
66
  end
@@ -58,7 +58,7 @@ module ActionText
58
58
  end
59
59
 
60
60
  def render_attachments(**options, &block)
61
- content = fragment.replace(ActionText::Attachment::SELECTOR) do |node|
61
+ content = fragment.replace(ActionText::Attachment.tag_name) do |node|
62
62
  block.call(attachment_for_node(node, **options))
63
63
  end
64
64
  self.class.new(content, canonicalize: false)
@@ -84,7 +84,11 @@ module ActionText
84
84
  end
85
85
 
86
86
  def to_rendered_html_with_layout
87
- render partial: "action_text/content/layout", formats: :html, locals: { content: self }
87
+ render layout: "action_text/contents/content", partial: to_partial_path, formats: :html, locals: { content: self }
88
+ end
89
+
90
+ def to_partial_path
91
+ "action_text/contents/content"
88
92
  end
89
93
 
90
94
  def to_s
@@ -107,7 +111,7 @@ module ActionText
107
111
 
108
112
  private
109
113
  def attachment_nodes
110
- @attachment_nodes ||= fragment.find_all(ActionText::Attachment::SELECTOR)
114
+ @attachment_nodes ||= fragment.find_all(ActionText::Attachment.tag_name)
111
115
  end
112
116
 
113
117
  def attachment_gallery_nodes
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module Encryption
5
+ def encrypt
6
+ transaction do
7
+ super
8
+ encrypt_rich_texts if has_encrypted_rich_texts?
9
+ end
10
+ end
11
+
12
+ def decrypt
13
+ transaction do
14
+ super
15
+ decrypt_rich_texts if has_encrypted_rich_texts?
16
+ end
17
+ end
18
+
19
+ private
20
+ def encrypt_rich_texts
21
+ encryptable_rich_texts.each(&:encrypt)
22
+ end
23
+
24
+ def decrypt_rich_texts
25
+ encryptable_rich_texts.each(&:decrypt)
26
+ end
27
+
28
+ def has_encrypted_rich_texts?
29
+ encryptable_rich_texts.present?
30
+ end
31
+
32
+ def encryptable_rich_texts
33
+ @encryptable_rich_texts ||= self.class.rich_text_association_names
34
+ .filter_map { |attribute_name| send(attribute_name) }
35
+ .find_all { |record| record.is_a?(ActionText::EncryptedRichText) }
36
+ end
37
+ end
38
+ end
@@ -11,6 +11,9 @@ module ActionText
11
11
  class Engine < Rails::Engine
12
12
  isolate_namespace ActionText
13
13
  config.eager_load_namespaces << ActionText
14
+
15
+ config.action_text = ActiveSupport::OrderedOptions.new
16
+ config.action_text.attachment_tag_name = "action-text-attachment"
14
17
  config.autoload_once_paths = %W(
15
18
  #{root}/app/helpers
16
19
  #{root}/app/models
@@ -19,6 +22,13 @@ module ActionText
19
22
  initializer "action_text.attribute" do
20
23
  ActiveSupport.on_load(:active_record) do
21
24
  include ActionText::Attribute
25
+ prepend ActionText::Encryption
26
+ end
27
+ end
28
+
29
+ initializer "action_text.asset" do
30
+ if Rails.application.config.respond_to?(:assets)
31
+ Rails.application.config.assets.precompile += %w( actiontext.js trix.js trix.css )
22
32
  end
23
33
  end
24
34
 
@@ -68,5 +78,9 @@ module ActionText
68
78
  include ActionText::SystemTestHelper
69
79
  end
70
80
  end
81
+
82
+ initializer "action_text.configure" do |app|
83
+ ActionText::Attachment.tag_name = app.config.action_text.attachment_tag_name
84
+ end
71
85
  end
72
86
  end
@@ -1,7 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionText
4
+ # Fixtures are a way of organizing data that you want to test against; in
5
+ # short, sample data.
6
+ #
7
+ # To learn more about fixtures, read the
8
+ # {ActiveRecord::FixtureSet}[rdoc-ref:ActiveRecord::FixtureSet] documentation.
9
+ #
10
+ # === YAML
11
+ #
12
+ # Like other Active Record-backed models, ActionText::RichText records inherit
13
+ # from ActiveRecord::Base instances and therefore can be populated by
14
+ # fixtures.
15
+ #
16
+ # Consider a hypothetical <tt>Article</tt> model class, its related fixture
17
+ # data, as well as fixture data for related ActionText::RichText records:
18
+ #
19
+ # # app/models/article.rb
20
+ # class Article < ApplicationRecord
21
+ # has_rich_text :content
22
+ # end
23
+ #
24
+ # # tests/fixtures/articles.yml
25
+ # first:
26
+ # title: An Article
27
+ #
28
+ # # tests/fixtures/action_text/rich_texts.yml
29
+ # first_content:
30
+ # record: first (Article)
31
+ # name: content
32
+ # body: <div>Hello, world.</div>
33
+ #
34
+ # When processed, Active Record will insert database records for each fixture
35
+ # entry and will ensure the Action Text relationship is intact.
4
36
  class FixtureSet
37
+ # Fixtures support Action Text attachments as part of their <tt>body</tt>
38
+ # HTML.
39
+ #
40
+ # === Examples
41
+ #
42
+ # For example, consider a second <tt>Article</tt> record that mentions the
43
+ # first as part of its <tt>content</tt> HTML:
44
+ #
45
+ # # tests/fixtures/articles.yml
46
+ # second:
47
+ # title: Another Article
48
+ #
49
+ # # tests/fixtures/action_text/rich_texts.yml
50
+ # second_content:
51
+ # record: second (Article)
52
+ # name: content
53
+ # body: <div>Hello, <%= ActionText::FixtureSet.attachment("articles", :first) %></div>
5
54
  def self.attachment(fixture_set_name, label, column_type: :integer)
6
55
  signed_global_id = ActiveRecord::FixtureSet.signed_global_id fixture_set_name, label,
7
56
  column_type: column_type, for: ActionText::Attachable::LOCATOR_NAME
@@ -7,10 +7,10 @@ module ActionText
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 6
11
- MINOR = 1
12
- TINY = 4
13
- PRE = "1"
10
+ MAJOR = 7
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "alpha1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -4,7 +4,7 @@ require "active_support/concern"
4
4
  require "active_support/core_ext/module/attribute_accessors_per_thread"
5
5
 
6
6
  module ActionText
7
- module Rendering #:nodoc:
7
+ module Rendering # :nodoc:
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  included do
@@ -9,9 +9,9 @@ module ActionText
9
9
  ATTRIBUTES = %w( sgid contentType url href filename filesize width height previewable content ) + COMPOSED_ATTRIBUTES
10
10
  ATTRIBUTE_TYPES = {
11
11
  "previewable" => ->(value) { value.to_s == "true" },
12
- "filesize" => ->(value) { Integer(value.to_s) rescue value },
13
- "width" => ->(value) { Integer(value.to_s) rescue nil },
14
- "height" => ->(value) { Integer(value.to_s) rescue nil },
12
+ "filesize" => ->(value) { Integer(value.to_s, exception: false) || value },
13
+ "width" => ->(value) { Integer(value.to_s, exception: false) },
14
+ "height" => ->(value) { Integer(value.to_s, exception: false) },
15
15
  :default => ->(value) { value.to_s }
16
16
  }
17
17
 
data/lib/action_text.rb CHANGED
@@ -13,6 +13,7 @@ module ActionText
13
13
  autoload :Attachment
14
14
  autoload :Attribute
15
15
  autoload :Content
16
+ autoload :Encryption
16
17
  autoload :Fragment
17
18
  autoload :FixtureSet
18
19
  autoload :HtmlConversion
@@ -9,43 +9,45 @@ module ActionText
9
9
  source_root File.expand_path("templates", __dir__)
10
10
 
11
11
  def install_javascript_dependencies
12
- rails_command "app:binstub:yarn", inline: true
13
-
14
- say "Installing JavaScript dependencies", :green
15
- run "#{Thor::Util.ruby_command} bin/yarn add #{js_dependencies.map { |name, version| "#{name}@#{version}" }.join(" ")}",
16
- abort_on_failure: true, capture: true
12
+ if Pathname(destination_root).join("package.json").exist?
13
+ say "Installing JavaScript dependencies", :green
14
+ run "yarn add @rails/actiontext trix"
15
+ end
17
16
  end
18
17
 
19
- def append_dependencies_to_package_file
20
- if (app_javascript_pack_path = Pathname.new("app/javascript/packs/application.js")).exist?
21
- js_dependencies.each_key do |dependency|
22
- line = %[require("#{dependency}")]
18
+ def append_javascript_dependencies
19
+ destination = Pathname(destination_root)
23
20
 
24
- unless app_javascript_pack_path.read.include? line
25
- say "Adding #{dependency} to #{app_javascript_pack_path}", :green
26
- append_to_file app_javascript_pack_path, "\n#{line}"
27
- end
28
- end
21
+ if (application_javascript_path = destination.join("app/javascript/application.js")).exist?
22
+ insert_into_file application_javascript_path.to_s, %(import "trix"\nimport "@rails/actiontext"\n)
29
23
  else
30
- say <<~WARNING, :red
31
- WARNING: Action Text can't locate your JavaScript bundle to add its package dependencies.
32
-
33
- Add these lines to any bundles:
34
-
35
- require("trix")
36
- require("@rails/actiontext")
24
+ say <<~INSTRUCTIONS, :green
25
+ You must import the @rails/actiontext and trix JavaScript modules in your application entrypoint.
26
+ INSTRUCTIONS
27
+ end
37
28
 
38
- Alternatively, install and setup the webpacker gem then rerun `bin/rails action_text:install`
39
- to have these dependencies added automatically.
40
- WARNING
29
+ if (importmap_path = destination.join("config/importmap.rb")).exist?
30
+ append_to_file importmap_path.to_s, %(pin "trix"\npin "@rails/actiontext", to: "actiontext.js"\n)
41
31
  end
42
32
  end
43
33
 
44
34
  def create_actiontext_files
45
- template "actiontext.scss", "app/assets/stylesheets/actiontext.scss"
35
+ template "actiontext.css", "app/assets/stylesheets/actiontext.css"
36
+
37
+ gem_root = "#{__dir__}/../../../.."
46
38
 
47
- copy_file "#{GEM_ROOT}/app/views/active_storage/blobs/_blob.html.erb",
39
+ copy_file "#{gem_root}/app/views/active_storage/blobs/_blob.html.erb",
48
40
  "app/views/active_storage/blobs/_blob.html.erb"
41
+
42
+ copy_file "#{gem_root}/app/views/layouts/action_text/contents/_content.html.erb",
43
+ "app/views/layouts/action_text/contents/_content.html.erb"
44
+ end
45
+
46
+ def enable_image_processing_gem
47
+ if (gemfile_path = Pathname(destination_root).join("Gemfile")).exist?
48
+ say "Ensure image_processing gem has been enabled so image uploads will work (remember to bundle!)"
49
+ uncomment_lines gemfile_path, /gem "image_processing"/
50
+ end
49
51
  end
50
52
 
51
53
  def create_migrations
@@ -53,15 +55,6 @@ module ActionText
53
55
  end
54
56
 
55
57
  hook_for :test_framework
56
-
57
- private
58
- GEM_ROOT = "#{__dir__}/../../../.."
59
-
60
- def js_dependencies
61
- js_package = JSON.load(Pathname.new("#{GEM_ROOT}/package.json"))
62
- js_package["peerDependencies"].merge \
63
- js_package["name"] => "^#{js_package["version"]}"
64
- end
65
58
  end
66
59
  end
67
60
  end