actiontext5 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +2 -0
  3. data/.gitignore +13 -0
  4. data/.travis.yml +17 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +140 -0
  7. data/LICENSE +21 -0
  8. data/README.md +1 -0
  9. data/Rakefile +27 -0
  10. data/actiontext.gemspec +29 -0
  11. data/app/helpers/action_text/content_helper.rb +30 -0
  12. data/app/helpers/action_text/tag_helper.rb +75 -0
  13. data/app/javascript/actiontext/attachment_upload.js +45 -0
  14. data/app/javascript/actiontext/index.js +11 -0
  15. data/app/models/action_text/rich_text.rb +25 -0
  16. data/app/views/action_text/attachables/_missing_attachable.html.erb +1 -0
  17. data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
  18. data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb +3 -0
  19. data/app/views/action_text/content/_layout.html.erb +3 -0
  20. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  21. data/bin/test +6 -0
  22. data/bin/webpack +29 -0
  23. data/bin/webpack-dev-server +29 -0
  24. data/db/migrate/201805281641_create_action_text_tables.rb +14 -0
  25. data/lib/action_text/attachable.rb +82 -0
  26. data/lib/action_text/attachables/content_attachment.rb +38 -0
  27. data/lib/action_text/attachables/missing_attachable.rb +11 -0
  28. data/lib/action_text/attachables/remote_image.rb +46 -0
  29. data/lib/action_text/attachment.rb +103 -0
  30. data/lib/action_text/attachment_gallery.rb +65 -0
  31. data/lib/action_text/attachments/caching.rb +16 -0
  32. data/lib/action_text/attachments/minification.rb +17 -0
  33. data/lib/action_text/attachments/trix_conversion.rb +34 -0
  34. data/lib/action_text/attribute.rb +48 -0
  35. data/lib/action_text/content.rb +126 -0
  36. data/lib/action_text/engine.rb +45 -0
  37. data/lib/action_text/fragment.rb +57 -0
  38. data/lib/action_text/html_conversion.rb +24 -0
  39. data/lib/action_text/plain_text_conversion.rb +81 -0
  40. data/lib/action_text/serialization.rb +34 -0
  41. data/lib/action_text/trix_attachment.rb +92 -0
  42. data/lib/action_text/version.rb +5 -0
  43. data/lib/action_text.rb +38 -0
  44. data/lib/actiontext5.rb +1 -0
  45. data/lib/tasks/actiontext.rake +20 -0
  46. data/lib/templates/actiontext.scss +36 -0
  47. data/lib/templates/fixtures.yml +4 -0
  48. data/lib/templates/installer.rb +22 -0
  49. data/package.json +21 -0
  50. data/test/dummy/.babelrc +18 -0
  51. data/test/dummy/.postcssrc.yml +3 -0
  52. data/test/dummy/Rakefile +6 -0
  53. data/test/dummy/app/assets/config/manifest.js +3 -0
  54. data/test/dummy/app/assets/images/.keep +0 -0
  55. data/test/dummy/app/assets/stylesheets/application.css +16 -0
  56. data/test/dummy/app/assets/stylesheets/messages.css +4 -0
  57. data/test/dummy/app/assets/stylesheets/scaffold.css +80 -0
  58. data/test/dummy/app/channels/application_cable/channel.rb +4 -0
  59. data/test/dummy/app/channels/application_cable/connection.rb +4 -0
  60. data/test/dummy/app/controllers/application_controller.rb +2 -0
  61. data/test/dummy/app/controllers/concerns/.keep +0 -0
  62. data/test/dummy/app/controllers/messages_controller.rb +58 -0
  63. data/test/dummy/app/helpers/application_helper.rb +2 -0
  64. data/test/dummy/app/helpers/messages_helper.rb +2 -0
  65. data/test/dummy/app/javascript/packs/application.js +1 -0
  66. data/test/dummy/app/jobs/application_job.rb +2 -0
  67. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  68. data/test/dummy/app/models/application_record.rb +3 -0
  69. data/test/dummy/app/models/concerns/.keep +0 -0
  70. data/test/dummy/app/models/message.rb +4 -0
  71. data/test/dummy/app/models/person.rb +7 -0
  72. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  73. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  74. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  75. data/test/dummy/app/views/messages/_form.html.erb +27 -0
  76. data/test/dummy/app/views/messages/edit.html.erb +6 -0
  77. data/test/dummy/app/views/messages/index.html.erb +29 -0
  78. data/test/dummy/app/views/messages/new.html.erb +5 -0
  79. data/test/dummy/app/views/messages/show.html.erb +13 -0
  80. data/test/dummy/app/views/people/_trix_content_attachment.html.erb +3 -0
  81. data/test/dummy/bin/bundle +3 -0
  82. data/test/dummy/bin/rails +4 -0
  83. data/test/dummy/bin/rake +4 -0
  84. data/test/dummy/bin/setup +36 -0
  85. data/test/dummy/bin/update +31 -0
  86. data/test/dummy/bin/yarn +11 -0
  87. data/test/dummy/config/application.rb +19 -0
  88. data/test/dummy/config/boot.rb +5 -0
  89. data/test/dummy/config/cable.yml +10 -0
  90. data/test/dummy/config/database.yml +25 -0
  91. data/test/dummy/config/environment.rb +5 -0
  92. data/test/dummy/config/environments/development.rb +63 -0
  93. data/test/dummy/config/environments/production.rb +96 -0
  94. data/test/dummy/config/environments/test.rb +46 -0
  95. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  96. data/test/dummy/config/initializers/assets.rb +14 -0
  97. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  98. data/test/dummy/config/initializers/content_security_policy.rb +22 -0
  99. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  100. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  101. data/test/dummy/config/initializers/inflections.rb +16 -0
  102. data/test/dummy/config/initializers/mime_types.rb +4 -0
  103. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  104. data/test/dummy/config/locales/en.yml +33 -0
  105. data/test/dummy/config/puma.rb +34 -0
  106. data/test/dummy/config/routes.rb +4 -0
  107. data/test/dummy/config/spring.rb +6 -0
  108. data/test/dummy/config/storage.yml +35 -0
  109. data/test/dummy/config/webpack/development.js +3 -0
  110. data/test/dummy/config/webpack/environment.js +3 -0
  111. data/test/dummy/config/webpack/production.js +3 -0
  112. data/test/dummy/config/webpack/test.js +3 -0
  113. data/test/dummy/config/webpacker.yml +65 -0
  114. data/test/dummy/config.ru +5 -0
  115. data/test/dummy/db/migrate/20180208205311_create_messages.rb +8 -0
  116. data/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb +26 -0
  117. data/test/dummy/db/migrate/2018052816_create_action_text_tables.rb +14 -0
  118. data/test/dummy/db/migrate/20181003185713_create_people.rb +9 -0
  119. data/test/dummy/db/schema.rb +58 -0
  120. data/test/dummy/lib/assets/.keep +0 -0
  121. data/test/dummy/log/.keep +0 -0
  122. data/test/dummy/package.json +11 -0
  123. data/test/dummy/public/404.html +67 -0
  124. data/test/dummy/public/422.html +67 -0
  125. data/test/dummy/public/500.html +66 -0
  126. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  127. data/test/dummy/public/apple-touch-icon.png +0 -0
  128. data/test/dummy/public/favicon.ico +0 -0
  129. data/test/dummy/storage/.keep +0 -0
  130. data/test/dummy/tmp/.keep +0 -0
  131. data/test/dummy/tmp/storage/.keep +0 -0
  132. data/test/dummy/yarn.lock +6071 -0
  133. data/test/fixtures/files/racecar.jpg +0 -0
  134. data/test/template/form_helper_test.rb +71 -0
  135. data/test/test_helper.rb +30 -0
  136. data/test/unit/attachment_test.rb +60 -0
  137. data/test/unit/content_test.rb +116 -0
  138. data/test/unit/model_test.rb +47 -0
  139. data/test/unit/plain_text_conversion_test.rb +94 -0
  140. data/test/unit/trix_attachment_test.rb +83 -0
  141. data/yarn.lock +11 -0
  142. metadata +372 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module Attachables
5
+ class ContentAttachment
6
+ include ActiveModel::Model
7
+
8
+ def self.from_node(node)
9
+ if node["content-type"]
10
+ if matches = node["content-type"].match(/vnd\.rubyonrails\.(.+)\.html/)
11
+ attachment = new(name: matches[1])
12
+ attachment if attachment.valid?
13
+ end
14
+ end
15
+ end
16
+
17
+ attr_accessor :name
18
+ validates_inclusion_of :name, in: %w( horizontal-rule )
19
+
20
+ def attachable_plain_text_representation(caption)
21
+ case name
22
+ when "horizontal-rule"
23
+ " ┄ "
24
+ else
25
+ " "
26
+ end
27
+ end
28
+
29
+ def to_partial_path
30
+ "action_text/attachables/content_attachment"
31
+ end
32
+
33
+ def to_trix_content_attachment_partial_path
34
+ "action_text/attachables/content_attachments/#{name.underscore}"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ module ActionText
2
+ module Attachables
3
+ module MissingAttachable
4
+ extend ActiveModel::Naming
5
+
6
+ def self.to_partial_path
7
+ "action_text/attachables/missing_attachable"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module Attachables
5
+ class RemoteImage
6
+ extend ActiveModel::Naming
7
+
8
+ class << self
9
+ def from_node(node)
10
+ if node["url"] && content_type_is_image?(node["content-type"])
11
+ new(attributes_from_node(node))
12
+ end
13
+ end
14
+
15
+ private
16
+ def content_type_is_image?(content_type)
17
+ content_type.to_s =~ /^image(\/.+|$)/
18
+ end
19
+
20
+ def attributes_from_node(node)
21
+ { url: node["url"],
22
+ content_type: node["content-type"],
23
+ width: node["width"],
24
+ height: node["height"] }
25
+ end
26
+ end
27
+
28
+ attr_reader :url, :content_type, :width, :height
29
+
30
+ def initialize(attributes = {})
31
+ @url = attributes[:url]
32
+ @content_type = attributes[:content_type]
33
+ @width = attributes[:width]
34
+ @height = attributes[:height]
35
+ end
36
+
37
+ def attachable_plain_text_representation(caption)
38
+ "[#{caption || "Image"}]"
39
+ end
40
+
41
+ def to_partial_path
42
+ "action_text/attachables/remote_image"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ class Attachment
5
+ include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
6
+
7
+ TAG_NAME = "action-text-attachment"
8
+ SELECTOR = TAG_NAME
9
+ ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation caption )
10
+
11
+ class << self
12
+ def fragment_by_canonicalizing_attachments(content)
13
+ fragment_by_minifying_attachments(fragment_by_converting_trix_attachments(content))
14
+ end
15
+
16
+ def from_node(node, attachable = nil)
17
+ new(node, attachable || ActionText::Attachable.from_node(node))
18
+ end
19
+
20
+ def from_attachables(attachables)
21
+ Array(attachables).map { |attachable| from_attachable(attachable) }.compact
22
+ end
23
+
24
+ def from_attachable(attachable, attributes = {})
25
+ if node = node_from_attributes(attachable.to_rich_text_attributes(attributes))
26
+ new(node, attachable)
27
+ end
28
+ end
29
+
30
+ def from_attributes(attributes, attachable = nil)
31
+ if node = node_from_attributes(attributes)
32
+ from_node(node, attachable)
33
+ end
34
+ end
35
+
36
+ private
37
+ def node_from_attributes(attributes)
38
+ if attributes = process_attributes(attributes).presence
39
+ ActionText::HtmlConversion.create_element(TAG_NAME, attributes)
40
+ end
41
+ end
42
+
43
+ def process_attributes(attributes)
44
+ attributes.transform_keys { |key| key.to_s.underscore.dasherize }.slice(*ATTRIBUTES)
45
+ end
46
+ end
47
+
48
+ attr_reader :node, :attachable
49
+
50
+ delegate :to_param, to: :attachable
51
+ delegate_missing_to :attachable
52
+
53
+ def initialize(node, attachable)
54
+ @node = node
55
+ @attachable = attachable
56
+ end
57
+
58
+ def caption
59
+ node_attributes["caption"].presence
60
+ end
61
+
62
+ def full_attributes
63
+ node_attributes.merge(attachable_attributes).merge(sgid_attributes)
64
+ end
65
+
66
+ def with_full_attributes
67
+ self.class.from_attributes(full_attributes, attachable)
68
+ end
69
+
70
+ def to_plain_text
71
+ if respond_to?(:attachable_plain_text_representation)
72
+ attachable_plain_text_representation(caption)
73
+ else
74
+ caption.to_s
75
+ end
76
+ end
77
+
78
+ def to_html
79
+ HtmlConversion.node_to_html(node)
80
+ end
81
+
82
+ def to_s
83
+ to_html
84
+ end
85
+
86
+ def inspect
87
+ "#<#{self.class.name} attachable=#{attachable.inspect}>"
88
+ end
89
+
90
+ private
91
+ def node_attributes
92
+ @node_attributes ||= ATTRIBUTES.map { |name| [ name.underscore, node[name] ] }.to_h.compact
93
+ end
94
+
95
+ def attachable_attributes
96
+ @attachable_attributes ||= (attachable.try(:to_rich_text_attributes) || {}).stringify_keys
97
+ end
98
+
99
+ def sgid_attributes
100
+ @sgid_attributes ||= node_attributes.slice("sgid").presence || attachable_attributes.slice("sgid")
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ class AttachmentGallery
5
+ include ActiveModel::Model
6
+
7
+ class << self
8
+ def fragment_by_canonicalizing_attachment_galleries(content)
9
+ fragment_by_replacing_attachment_gallery_nodes(content) do |node|
10
+ "<#{TAG_NAME}>#{node.inner_html}</#{TAG_NAME}>"
11
+ end
12
+ end
13
+
14
+ def fragment_by_replacing_attachment_gallery_nodes(content)
15
+ Fragment.wrap(content).update do |source|
16
+ find_attachment_gallery_nodes(source).each do |node|
17
+ node.replace(yield(node).to_s)
18
+ end
19
+ end
20
+ end
21
+
22
+ def find_attachment_gallery_nodes(content)
23
+ Fragment.wrap(content).find_all(SELECTOR).select do |node|
24
+ node.children.all? do |child|
25
+ if child.text?
26
+ child.text =~ /\A(\n|\ )*\z/
27
+ else
28
+ child.matches? ATTACHMENT_SELECTOR
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def from_node(node)
35
+ new(node)
36
+ end
37
+ end
38
+
39
+ attr_reader :node
40
+
41
+ def initialize(node)
42
+ @node = node
43
+ end
44
+
45
+ def attachments
46
+ @attachments ||= node.css(ATTACHMENT_SELECTOR).map do |node|
47
+ ActionText::Attachment.from_node(node).with_full_attributes
48
+ end
49
+ end
50
+
51
+ def size
52
+ attachments.size
53
+ end
54
+
55
+ def inspect
56
+ "#<#{self.class.name} size=#{size.inspect}>"
57
+ 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
+ end
65
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module Attachments
5
+ module Caching
6
+ def cache_key(*args)
7
+ [self.class.name, cache_digest, *attachable.cache_key(*args)].join("/")
8
+ end
9
+
10
+ private
11
+ def cache_digest
12
+ Digest::SHA256.hexdigest(node.to_s)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module Attachments
5
+ module Minification
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def fragment_by_minifying_attachments(content)
10
+ Fragment.wrap(content).replace(ActionText::Attachment::SELECTOR) do |node|
11
+ node.tap { |n| n.inner_html = "" }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module Attachments
5
+ module TrixConversion
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def fragment_by_converting_trix_attachments(content)
10
+ Fragment.wrap(content).replace(TrixAttachment::SELECTOR) do |node|
11
+ from_trix_attachment(TrixAttachment.new(node))
12
+ end
13
+ end
14
+
15
+ def from_trix_attachment(trix_attachment)
16
+ from_attributes(trix_attachment.attributes)
17
+ end
18
+ end
19
+
20
+ def to_trix_attachment(content = trix_attachment_content)
21
+ attributes = full_attributes.dup
22
+ attributes["content"] = content if content
23
+ TrixAttachment.from_attributes(attributes)
24
+ end
25
+
26
+ private
27
+ def trix_attachment_content
28
+ if partial_path = attachable.try(:to_trix_content_attachment_partial_path)
29
+ ActionText.renderer.render(partial: partial_path, object: self, as: model_name.element)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module Attribute
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ # Provides access to a dependent RichText model that holds the body and attachments for a single named rich text attribute.
9
+ # This dependent attribute is lazily instantiated and will be auto-saved when it's been changed. Example:
10
+ #
11
+ # class Message < ActiveRecord::Base
12
+ # has_rich_text :content
13
+ # end
14
+ #
15
+ # message = Message.create!(content: "<h1>Funny times!</h1>")
16
+ # message.content.to_s # => "<h1>Funny times!</h1>"
17
+ # message.content.to_plain_text # => "Funny times!"
18
+ #
19
+ # The dependent RichText model will also automatically process attachments links as sent via the Trix-powered editor.
20
+ # These attachments are associated with the RichText model using Active Storage.
21
+ #
22
+ # If you wish to preload the dependent RichText model, you can use the named scope:
23
+ #
24
+ # Message.all.with_rich_text_content # Avoids N+1 queries when you just want the body, not the attachments.
25
+ # Message.all.with_rich_text_content_and_embeds # Avoids N+1 queries when you just want the body and attachments.
26
+ def has_rich_text(name)
27
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
28
+ def #{name}
29
+ self.rich_text_#{name} ||= ActionText::RichText.new(name: "#{name}", record: self)
30
+ end
31
+
32
+ def #{name}=(body)
33
+ self.#{name}.body = body
34
+ end
35
+ CODE
36
+
37
+ has_one :"rich_text_#{name}", -> { where(name: name) }, class_name: "ActionText::RichText", as: :record, inverse_of: :record, dependent: :destroy
38
+
39
+ scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") }
40
+ scope :"with_rich_text_#{name}_and_embeds", -> { includes("rich_text_#{name}": { embeds_attachments: :blob }) }
41
+
42
+ after_save do
43
+ public_send(name).save if public_send(name).changed?
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ class Content
5
+ include Serialization
6
+
7
+ attr_reader :fragment
8
+
9
+ delegate :blank?, :empty?, :html_safe, :present?, to: :to_html # Delegating to to_html to avoid including the layout
10
+
11
+ class << self
12
+ def fragment_by_canonicalizing_content(content)
13
+ fragment = ActionText::Attachment.fragment_by_canonicalizing_attachments(content)
14
+ fragment = ActionText::AttachmentGallery.fragment_by_canonicalizing_attachment_galleries(fragment)
15
+ fragment
16
+ end
17
+ end
18
+
19
+ def initialize(content = nil, options = {})
20
+ options.with_defaults! canonicalize: true
21
+
22
+ if options[:canonicalize]
23
+ @fragment = self.class.fragment_by_canonicalizing_content(content)
24
+ else
25
+ @fragment = ActionText::Fragment.wrap(content)
26
+ end
27
+ end
28
+
29
+ def links
30
+ @links ||= fragment.find_all("a[href]").map { |a| a["href"] }.uniq
31
+ end
32
+
33
+ def attachments
34
+ @attachments ||= attachment_nodes.map do |node|
35
+ attachment_for_node(node)
36
+ end
37
+ end
38
+
39
+ def attachment_galleries
40
+ @attachment_galleries ||= attachment_gallery_nodes.map do |node|
41
+ attachment_gallery_for_node(node)
42
+ end
43
+ end
44
+
45
+ def gallery_attachments
46
+ @gallery_attachments ||= attachment_galleries.flat_map(&:attachments)
47
+ end
48
+
49
+ def attachables
50
+ @attachables ||= attachment_nodes.map do |node|
51
+ ActionText::Attachable.from_node(node)
52
+ end
53
+ end
54
+
55
+ def append_attachables(attachables)
56
+ attachments = ActionText::Attachment.from_attachables(attachables)
57
+ self.class.new([self.to_s.presence, *attachments].compact.join("\n"))
58
+ end
59
+
60
+ def render_attachments(**options, &block)
61
+ content = fragment.replace(ActionText::Attachment::SELECTOR) do |node|
62
+ block.call(attachment_for_node(node, **options))
63
+ end
64
+ self.class.new(content, canonicalize: false)
65
+ end
66
+
67
+ def render_attachment_galleries(&block)
68
+ content = ActionText::AttachmentGallery.fragment_by_replacing_attachment_gallery_nodes(fragment) do |node|
69
+ block.call(attachment_gallery_for_node(node))
70
+ end
71
+ self.class.new(content, canonicalize: false)
72
+ end
73
+
74
+ def to_plain_text
75
+ render_attachments(with_full_attributes: false, &:to_plain_text).fragment.to_plain_text
76
+ end
77
+
78
+ def to_trix_html
79
+ render_attachments(&:to_trix_attachment).to_html
80
+ end
81
+
82
+ def to_html
83
+ fragment.to_html
84
+ end
85
+
86
+ def to_rendered_html_with_layout
87
+ ActionText.renderer.render(partial: "action_text/content/layout", locals: { content: self })
88
+ end
89
+
90
+ def to_s
91
+ to_rendered_html_with_layout
92
+ end
93
+
94
+ def as_json(*)
95
+ to_html
96
+ end
97
+
98
+ def inspect
99
+ "#<#{self.class.name} #{to_s.truncate(25).inspect}>"
100
+ end
101
+
102
+ def ==(other)
103
+ if other.is_a?(self.class)
104
+ to_s == other.to_s
105
+ end
106
+ end
107
+
108
+ private
109
+ def attachment_nodes
110
+ @attachment_nodes ||= fragment.find_all(ActionText::Attachment::SELECTOR)
111
+ end
112
+
113
+ def attachment_gallery_nodes
114
+ @attachment_gallery_nodes ||= ActionText::AttachmentGallery.find_attachment_gallery_nodes(fragment)
115
+ end
116
+
117
+ def attachment_for_node(node, with_full_attributes: true)
118
+ attachment = ActionText::Attachment.from_node(node)
119
+ with_full_attributes ? attachment.with_full_attributes : attachment
120
+ end
121
+
122
+ def attachment_gallery_for_node(node)
123
+ ActionText::AttachmentGallery.from_node(node)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module ActionText
6
+ class Engine < Rails::Engine
7
+ isolate_namespace ActionText
8
+ config.eager_load_namespaces << ActionText
9
+
10
+ initializer "action_text.attribute" do
11
+ ActiveSupport.on_load(:active_record) do
12
+ include ActionText::Attribute
13
+ end
14
+ end
15
+
16
+ initializer "action_text.active_storage_extension" do
17
+ ActiveSupport.on_load(:active_storage_blob) do
18
+ include ActionText::Attachable
19
+
20
+ def previewable_attachable?
21
+ representable?
22
+ end
23
+ end
24
+ end
25
+
26
+ initializer "action_text.helper" do
27
+ ActiveSupport.on_load(:action_controller_base) do
28
+ helper ActionText::Engine.helpers
29
+ end
30
+ end
31
+
32
+ initializer "action_text.config" do
33
+ config.after_initialize do |app|
34
+ ActionText.renderer ||= ApplicationController.renderer
35
+
36
+ # FIXME: ApplicationController should have a per-request specific renderer
37
+ # that's been set with the request.env env, and ActionText should just piggyback off
38
+ # that by default rather than doing this work directly.
39
+ ApplicationController.before_action do
40
+ ActionText.renderer = ActionText.renderer.new(request.env)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ class Fragment
5
+ class << self
6
+ def wrap(fragment_or_html)
7
+ case fragment_or_html
8
+ when self
9
+ fragment_or_html
10
+ when Nokogiri::HTML::DocumentFragment
11
+ new(fragment_or_html)
12
+ else
13
+ from_html(fragment_or_html)
14
+ end
15
+ end
16
+
17
+ def from_html(html)
18
+ new(ActionText::HtmlConversion.fragment_for_html(html.to_s.strip))
19
+ end
20
+ end
21
+
22
+ attr_reader :source
23
+
24
+ def initialize(source)
25
+ @source = source
26
+ end
27
+
28
+ def find_all(selector)
29
+ source.css(selector)
30
+ end
31
+
32
+ def update
33
+ yield source = self.source.clone
34
+ self.class.new(source)
35
+ end
36
+
37
+ def replace(selector)
38
+ update do |source|
39
+ source.css(selector).each do |node|
40
+ node.replace(yield(node).to_s)
41
+ end
42
+ end
43
+ end
44
+
45
+ def to_plain_text
46
+ @plain_text ||= PlainTextConversion.node_to_plain_text(source)
47
+ end
48
+
49
+ def to_html
50
+ @html ||= HtmlConversion.node_to_html(source)
51
+ end
52
+
53
+ def to_s
54
+ to_html
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module HtmlConversion
5
+ extend self
6
+
7
+ def node_to_html(node)
8
+ node.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_HTML)
9
+ end
10
+
11
+ def fragment_for_html(html)
12
+ document.fragment(html)
13
+ end
14
+
15
+ def create_element(tag_name, attributes = {})
16
+ document.create_element(tag_name, attributes)
17
+ end
18
+
19
+ private
20
+ def document
21
+ Nokogiri::HTML::Document.new.tap { |doc| doc.encoding = "UTF-8" }
22
+ end
23
+ end
24
+ end