actiontext 6.1.4.1 → 7.0.0.alpha1

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.

Potentially problematic release.


This version of actiontext might be problematic. Click here for more details.

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