omg-actiontext 8.0.0.alpha3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +42 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +13 -0
  5. data/app/assets/javascripts/actiontext.esm.js +911 -0
  6. data/app/assets/javascripts/actiontext.js +884 -0
  7. data/app/assets/javascripts/trix.js +12165 -0
  8. data/app/assets/stylesheets/trix.css +412 -0
  9. data/app/helpers/action_text/content_helper.rb +76 -0
  10. data/app/helpers/action_text/tag_helper.rb +106 -0
  11. data/app/javascript/actiontext/attachment_upload.js +62 -0
  12. data/app/javascript/actiontext/index.js +10 -0
  13. data/app/models/action_text/encrypted_rich_text.rb +11 -0
  14. data/app/models/action_text/record.rb +11 -0
  15. data/app/models/action_text/rich_text.rb +93 -0
  16. data/app/views/action_text/attachables/_content_attachment.html.erb +3 -0
  17. data/app/views/action_text/attachables/_missing_attachable.html.erb +1 -0
  18. data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
  19. data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb +3 -0
  20. data/app/views/action_text/contents/_content.html.erb +1 -0
  21. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  22. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  23. data/db/migrate/20180528164100_create_action_text_tables.rb +25 -0
  24. data/lib/action_text/attachable.rb +156 -0
  25. data/lib/action_text/attachables/content_attachment.rb +42 -0
  26. data/lib/action_text/attachables/missing_attachable.rb +29 -0
  27. data/lib/action_text/attachables/remote_image.rb +48 -0
  28. data/lib/action_text/attachment.rb +148 -0
  29. data/lib/action_text/attachment_gallery.rb +72 -0
  30. data/lib/action_text/attachments/caching.rb +18 -0
  31. data/lib/action_text/attachments/minification.rb +19 -0
  32. data/lib/action_text/attachments/trix_conversion.rb +38 -0
  33. data/lib/action_text/attribute.rb +105 -0
  34. data/lib/action_text/content.rb +197 -0
  35. data/lib/action_text/deprecator.rb +9 -0
  36. data/lib/action_text/encryption.rb +40 -0
  37. data/lib/action_text/engine.rb +94 -0
  38. data/lib/action_text/fixture_set.rb +68 -0
  39. data/lib/action_text/fragment.rb +62 -0
  40. data/lib/action_text/gem_version.rb +19 -0
  41. data/lib/action_text/html_conversion.rb +26 -0
  42. data/lib/action_text/plain_text_conversion.rb +114 -0
  43. data/lib/action_text/rendering.rb +35 -0
  44. data/lib/action_text/serialization.rb +38 -0
  45. data/lib/action_text/system_test_helper.rb +61 -0
  46. data/lib/action_text/trix_attachment.rb +94 -0
  47. data/lib/action_text/version.rb +12 -0
  48. data/lib/action_text.rb +59 -0
  49. data/lib/generators/action_text/install/install_generator.rb +84 -0
  50. data/lib/generators/action_text/install/templates/actiontext.css +440 -0
  51. data/lib/rails/generators/test_unit/install_generator.rb +15 -0
  52. data/lib/rails/generators/test_unit/templates/fixtures.yml +4 -0
  53. data/lib/tasks/actiontext.rake +6 -0
  54. data/package.json +39 -0
  55. metadata +190 -0
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ class Record < ActiveRecord::Base # :nodoc:
7
+ self.abstract_class = true
8
+ end
9
+ end
10
+
11
+ ActiveSupport.run_load_hooks :action_text_record, ActionText::Record
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ # # Action Text RichText
7
+ #
8
+ # The RichText record holds the content produced by the Trix editor in a
9
+ # serialized `body` attribute. It also holds all the references to the embedded
10
+ # files, which are stored using Active Storage. This record is then associated
11
+ # with the Active Record model the application desires to have rich text content
12
+ # using the `has_rich_text` class method.
13
+ #
14
+ # class Message < ActiveRecord::Base
15
+ # has_rich_text :content
16
+ # end
17
+ #
18
+ # message = Message.create!(content: "<h1>Funny times!</h1>")
19
+ # message.content #=> #<ActionText::RichText....
20
+ # message.content.to_s # => "<h1>Funny times!</h1>"
21
+ # message.content.to_plain_text # => "Funny times!"
22
+ #
23
+ # message = Message.create!(content: "<div onclick='action()'>safe<script>unsafe</script></div>")
24
+ # message.content #=> #<ActionText::RichText....
25
+ # message.content.to_s # => "<div>safeunsafe</div>"
26
+ # message.content.to_plain_text # => "safeunsafe"
27
+ class RichText < Record
28
+ ##
29
+ # :method: to_s
30
+ #
31
+ # Safely transforms RichText into an HTML String.
32
+ #
33
+ # message = Message.create!(content: "<h1>Funny times!</h1>")
34
+ # message.content.to_s # => "<h1>Funny times!</h1>"
35
+ #
36
+ # message = Message.create!(content: "<div onclick='action()'>safe<script>unsafe</script></div>")
37
+ # message.content.to_s # => "<div>safeunsafe</div>"
38
+
39
+ serialize :body, coder: ActionText::Content
40
+ delegate :to_s, :nil?, to: :body
41
+
42
+ ##
43
+ # :method: record
44
+ #
45
+ # Returns the associated record.
46
+ belongs_to :record, polymorphic: true, touch: true
47
+
48
+ ##
49
+ # :method: embeds
50
+ #
51
+ # Returns the `ActiveStorage::Blob`s of the embedded files.
52
+ has_many_attached :embeds
53
+
54
+ before_save do
55
+ self.embeds = body.attachables.grep(ActiveStorage::Blob).uniq if body.present?
56
+ end
57
+
58
+ # Returns a plain-text version of the markup contained by the `body` attribute,
59
+ # with tags removed but HTML entities encoded.
60
+ #
61
+ # message = Message.create!(content: "<h1>Funny times!</h1>")
62
+ # message.content.to_plain_text # => "Funny times!"
63
+ #
64
+ # NOTE: that the returned string is not HTML safe and should not be rendered in
65
+ # browsers.
66
+ #
67
+ # message = Message.create!(content: "&lt;script&gt;alert()&lt;/script&gt;")
68
+ # message.content.to_plain_text # => "<script>alert()</script>"
69
+ def to_plain_text
70
+ body&.to_plain_text.to_s
71
+ end
72
+
73
+ # Returns the `body` attribute in a format that makes it editable in the Trix
74
+ # editor. Previews of attachments are rendered inline.
75
+ #
76
+ # content = "<h1>Funny Times!</h1><figure data-trix-attachment='{\"sgid\":\"..."\}'></figure>"
77
+ # message = Message.create!(content: content)
78
+ # message.content.to_trix_html # =>
79
+ # # <div class="trix-content">
80
+ # # <h1>Funny times!</h1>
81
+ # # <figure data-trix-attachment='{\"sgid\":\"..."\}'>
82
+ # # <img src="http://example.org/rails/active_storage/.../funny.jpg">
83
+ # # </figure>
84
+ # # </div>
85
+ def to_trix_html
86
+ body&.to_trix_html
87
+ end
88
+
89
+ delegate :blank?, :empty?, :present?, to: :to_plain_text
90
+ end
91
+ end
92
+
93
+ ActiveSupport.run_load_hooks :action_text_rich_text, ActionText::RichText
@@ -0,0 +1,3 @@
1
+ <figure class="attachment attachment--content">
2
+ <%= content_attachment.attachable %>
3
+ </figure>
@@ -0,0 +1,8 @@
1
+ <figure class="attachment attachment--preview">
2
+ <%= image_tag(remote_image.url, width: remote_image.width, height: remote_image.height) %>
3
+ <% if caption = remote_image.try(:caption) %>
4
+ <figcaption class="attachment__caption">
5
+ <%= caption %>
6
+ </figcaption>
7
+ <% end %>
8
+ </figure>
@@ -0,0 +1,3 @@
1
+ <div class="attachment-gallery attachment-gallery--<%= attachment_gallery.size %>">
2
+ <%= yield %>
3
+ </div>
@@ -0,0 +1 @@
1
+ <%= render_action_text_content(content) %>
@@ -0,0 +1,14 @@
1
+ <figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
2
+ <% if blob.representable? %>
3
+ <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
4
+ <% end %>
5
+
6
+ <figcaption class="attachment__caption">
7
+ <% if caption = blob.try(:caption) %>
8
+ <%= caption %>
9
+ <% else %>
10
+ <span class="attachment__name"><%= blob.filename %></span>
11
+ <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
12
+ <% end %>
13
+ </figcaption>
14
+ </figure>
@@ -0,0 +1,3 @@
1
+ <div class="trix-content">
2
+ <%= yield -%>
3
+ </div>
@@ -0,0 +1,25 @@
1
+ class CreateActionTextTables < ActiveRecord::Migration[6.0]
2
+ def change
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|
7
+ t.string :name, null: false
8
+ t.text :body, size: :long
9
+ t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
10
+
11
+ t.timestamps
12
+
13
+ t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
14
+ end
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
25
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ # # Action Text Attachable
7
+ #
8
+ # Include this module to make a record attachable to an ActionText::Content.
9
+ #
10
+ # class Person < ApplicationRecord
11
+ # include ActionText::Attachable
12
+ # end
13
+ #
14
+ # person = Person.create! name: "Javan"
15
+ # html = %Q(<action-text-attachment sgid="#{person.attachable_sgid}"></action-text-attachment>)
16
+ # content = ActionText::Content.new(html)
17
+ # content.attachables # => [person]
18
+ module Attachable
19
+ extend ActiveSupport::Concern
20
+
21
+ LOCATOR_NAME = "attachable"
22
+
23
+ class << self
24
+ # Extracts the `ActionText::Attachable` from the attachment HTML node:
25
+ #
26
+ # person = Person.create! name: "Javan"
27
+ # html = %Q(<action-text-attachment sgid="#{person.attachable_sgid}"></action-text-attachment>)
28
+ # fragment = ActionText::Fragment.wrap(html)
29
+ # attachment_node = fragment.find_all(ActionText::Attachment.tag_name).first
30
+ # ActionText::Attachable.from_node(attachment_node) # => person
31
+ def from_node(node)
32
+ if attachable = attachable_from_sgid(node["sgid"])
33
+ attachable
34
+ elsif attachable = ActionText::Attachables::ContentAttachment.from_node(node)
35
+ attachable
36
+ elsif attachable = ActionText::Attachables::RemoteImage.from_node(node)
37
+ attachable
38
+ else
39
+ ActionText::Attachables::MissingAttachable.new(node["sgid"])
40
+ end
41
+ end
42
+
43
+ def from_attachable_sgid(sgid, options = {})
44
+ method = sgid.is_a?(Array) ? :locate_many_signed : :locate_signed
45
+ record = GlobalID::Locator.public_send(method, sgid, options.merge(for: LOCATOR_NAME))
46
+ record || raise(ActiveRecord::RecordNotFound)
47
+ end
48
+
49
+ private
50
+ def attachable_from_sgid(sgid)
51
+ from_attachable_sgid(sgid)
52
+ rescue ActiveRecord::RecordNotFound
53
+ nil
54
+ end
55
+ end
56
+
57
+ class_methods do
58
+ def from_attachable_sgid(sgid)
59
+ ActionText::Attachable.from_attachable_sgid(sgid, only: self)
60
+ end
61
+
62
+ # Returns the path to the partial that is used for rendering missing
63
+ # attachables. Defaults to "action_text/attachables/missing_attachable".
64
+ #
65
+ # Override to render a different partial:
66
+ #
67
+ # class User < ApplicationRecord
68
+ # def self.to_missing_attachable_partial_path
69
+ # "users/missing_attachable"
70
+ # end
71
+ # end
72
+ def to_missing_attachable_partial_path
73
+ ActionText::Attachables::MissingAttachable::DEFAULT_PARTIAL_PATH
74
+ end
75
+ end
76
+
77
+ # Returns the Signed Global ID for the attachable. The purpose of the ID is set
78
+ # to 'attachable' so it can't be reused for other purposes.
79
+ def attachable_sgid
80
+ to_sgid(expires_in: nil, for: LOCATOR_NAME).to_s
81
+ end
82
+
83
+ def attachable_content_type
84
+ try(:content_type) || "application/octet-stream"
85
+ end
86
+
87
+ def attachable_filename
88
+ filename.to_s if respond_to?(:filename)
89
+ end
90
+
91
+ def attachable_filesize
92
+ try(:byte_size) || try(:filesize)
93
+ end
94
+
95
+ def attachable_metadata
96
+ try(:metadata) || {}
97
+ end
98
+
99
+ def previewable_attachable?
100
+ false
101
+ end
102
+
103
+ # Returns the path to the partial that is used for rendering the attachable in
104
+ # Trix. Defaults to `to_partial_path`.
105
+ #
106
+ # Override to render a different partial:
107
+ #
108
+ # class User < ApplicationRecord
109
+ # def to_trix_content_attachment_partial_path
110
+ # "users/trix_content_attachment"
111
+ # end
112
+ # end
113
+ def to_trix_content_attachment_partial_path
114
+ to_partial_path
115
+ end
116
+
117
+ # Returns the path to the partial that is used for rendering the attachable.
118
+ # Defaults to `to_partial_path`.
119
+ #
120
+ # Override to render a different partial:
121
+ #
122
+ # class User < ApplicationRecord
123
+ # def to_attachable_partial_path
124
+ # "users/attachable"
125
+ # end
126
+ # end
127
+ def to_attachable_partial_path
128
+ to_partial_path
129
+ end
130
+
131
+ def to_rich_text_attributes(attributes = {})
132
+ attributes.dup.tap do |attrs|
133
+ attrs[:sgid] = attachable_sgid
134
+ attrs[:content_type] = attachable_content_type
135
+ attrs[:previewable] = true if previewable_attachable?
136
+ attrs[:filename] = attachable_filename
137
+ attrs[:filesize] = attachable_filesize
138
+ attrs[:width] = attachable_metadata[:width]
139
+ attrs[:height] = attachable_metadata[:height]
140
+ end.compact
141
+ end
142
+
143
+ private
144
+ def attribute_names_for_serialization
145
+ super + ["attachable_sgid"]
146
+ end
147
+
148
+ def read_attribute_for_serialization(key)
149
+ if key == "attachable_sgid"
150
+ persisted? ? super : nil
151
+ else
152
+ super
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ module Attachables
7
+ class ContentAttachment # :nodoc:
8
+ include ActiveModel::Model
9
+
10
+ def self.from_node(node)
11
+ attachment = new(content_type: node["content-type"], content: node["content"])
12
+ attachment if attachment.valid?
13
+ end
14
+
15
+ attr_accessor :content_type, :content
16
+
17
+ validates_format_of :content_type, with: /html/
18
+ validates_presence_of :content
19
+
20
+ def attachable_plain_text_representation(caption)
21
+ content_instance.fragment.source
22
+ end
23
+
24
+ def to_html
25
+ @to_html ||= content_instance.render(content_instance)
26
+ end
27
+
28
+ def to_s
29
+ to_html
30
+ end
31
+
32
+ def to_partial_path
33
+ "action_text/attachables/content_attachment"
34
+ end
35
+
36
+ private
37
+ def content_instance
38
+ @content_instance ||= ActionText::Content.new(content)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ module Attachables
7
+ class MissingAttachable
8
+ extend ActiveModel::Naming
9
+
10
+ DEFAULT_PARTIAL_PATH = "action_text/attachables/missing_attachable"
11
+
12
+ def initialize(sgid)
13
+ @sgid = SignedGlobalID.parse(sgid, for: ActionText::Attachable::LOCATOR_NAME)
14
+ end
15
+
16
+ def to_partial_path
17
+ if model
18
+ model.to_missing_attachable_partial_path
19
+ else
20
+ DEFAULT_PARTIAL_PATH
21
+ end
22
+ end
23
+
24
+ def model
25
+ @sgid&.model_name.to_s.safe_constantize
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ module Attachables
7
+ class RemoteImage
8
+ extend ActiveModel::Naming
9
+
10
+ class << self
11
+ def from_node(node)
12
+ if node["url"] && content_type_is_image?(node["content-type"])
13
+ new(attributes_from_node(node))
14
+ end
15
+ end
16
+
17
+ private
18
+ def content_type_is_image?(content_type)
19
+ content_type.to_s.match?(/^image(\/.+|$)/)
20
+ end
21
+
22
+ def attributes_from_node(node)
23
+ { url: node["url"],
24
+ content_type: node["content-type"],
25
+ width: node["width"],
26
+ height: node["height"] }
27
+ end
28
+ end
29
+
30
+ attr_reader :url, :content_type, :width, :height
31
+
32
+ def initialize(attributes = {})
33
+ @url = attributes[:url]
34
+ @content_type = attributes[:content_type]
35
+ @width = attributes[:width]
36
+ @height = attributes[:height]
37
+ end
38
+
39
+ def attachable_plain_text_representation(caption)
40
+ "[#{caption || "Image"}]"
41
+ end
42
+
43
+ def to_partial_path
44
+ "action_text/attachables/remote_image"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "active_support/core_ext/object/try"
6
+
7
+ module ActionText
8
+ # # Action Text Attachment
9
+ #
10
+ # Attachments serialize attachables to HTML or plain text.
11
+ #
12
+ # class Person < ApplicationRecord
13
+ # include ActionText::Attachable
14
+ # end
15
+ #
16
+ # attachable = Person.create! name: "Javan"
17
+ # attachment = ActionText::Attachment.from_attachable(attachable)
18
+ # attachment.to_html # => "<action-text-attachment sgid=\"BAh7CEk..."
19
+ class Attachment
20
+ include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
21
+
22
+ mattr_accessor :tag_name, default: "action-text-attachment"
23
+
24
+ ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation caption content )
25
+
26
+ class << self
27
+ def fragment_by_canonicalizing_attachments(content)
28
+ fragment_by_minifying_attachments(fragment_by_converting_trix_attachments(content))
29
+ end
30
+
31
+ def from_node(node, attachable = nil)
32
+ new(node, attachable || ActionText::Attachable.from_node(node))
33
+ end
34
+
35
+ def from_attachables(attachables)
36
+ Array(attachables).filter_map { |attachable| from_attachable(attachable) }
37
+ end
38
+
39
+ def from_attachable(attachable, attributes = {})
40
+ if node = node_from_attributes(attachable.to_rich_text_attributes(attributes))
41
+ new(node, attachable)
42
+ end
43
+ end
44
+
45
+ def from_attributes(attributes, attachable = nil)
46
+ if node = node_from_attributes(attributes)
47
+ from_node(node, attachable)
48
+ end
49
+ end
50
+
51
+ private
52
+ def node_from_attributes(attributes)
53
+ if attributes = process_attributes(attributes).presence
54
+ ActionText::HtmlConversion.create_element(tag_name, attributes)
55
+ end
56
+ end
57
+
58
+ def process_attributes(attributes)
59
+ attributes.transform_keys { |key| key.to_s.underscore.dasherize }.slice(*ATTRIBUTES)
60
+ end
61
+ end
62
+
63
+ attr_reader :node, :attachable
64
+
65
+ delegate :to_param, to: :attachable
66
+ delegate_missing_to :attachable
67
+
68
+ def initialize(node, attachable)
69
+ @node = node
70
+ @attachable = attachable
71
+ end
72
+
73
+ def caption
74
+ node_attributes["caption"].presence
75
+ end
76
+
77
+ def full_attributes
78
+ node_attributes.merge(attachable_attributes).merge(sgid_attributes)
79
+ end
80
+
81
+ def with_full_attributes
82
+ self.class.from_attributes(full_attributes, attachable)
83
+ end
84
+
85
+ # Converts the attachment to plain text.
86
+ #
87
+ # attachable = ActiveStorage::Blob.find_by filename: "racecar.jpg"
88
+ # attachment = ActionText::Attachment.from_attachable(attachable)
89
+ # attachment.to_plain_text # => "[racecar.jpg]"
90
+ #
91
+ # Use the `caption` when set:
92
+ #
93
+ # attachment = ActionText::Attachment.from_attachable(attachable, caption: "Vroom vroom")
94
+ # attachment.to_plain_text # => "[Vroom vroom]"
95
+ #
96
+ # The presentation can be overridden by implementing the
97
+ # `attachable_plain_text_representation` method:
98
+ #
99
+ # class Person < ApplicationRecord
100
+ # include ActionText::Attachable
101
+ #
102
+ # def attachable_plain_text_representation
103
+ # "[#{name}]"
104
+ # end
105
+ # end
106
+ #
107
+ # attachable = Person.create! name: "Javan"
108
+ # attachment = ActionText::Attachment.from_attachable(attachable)
109
+ # attachment.to_plain_text # => "[Javan]"
110
+ def to_plain_text
111
+ if respond_to?(:attachable_plain_text_representation)
112
+ attachable_plain_text_representation(caption)
113
+ else
114
+ caption.to_s
115
+ end
116
+ end
117
+
118
+ # Converts the attachment to HTML.
119
+ #
120
+ # attachable = Person.create! name: "Javan"
121
+ # attachment = ActionText::Attachment.from_attachable(attachable)
122
+ # attachment.to_html # => "<action-text-attachment sgid=\"BAh7CEk...
123
+ def to_html
124
+ HtmlConversion.node_to_html(node)
125
+ end
126
+
127
+ def to_s
128
+ to_html
129
+ end
130
+
131
+ def inspect
132
+ "#<#{self.class.name} attachable=#{attachable.inspect}>"
133
+ end
134
+
135
+ private
136
+ def node_attributes
137
+ @node_attributes ||= ATTRIBUTES.to_h { |name| [ name.underscore, node[name] ] }.compact
138
+ end
139
+
140
+ def attachable_attributes
141
+ @attachable_attributes ||= (attachable.try(:to_rich_text_attributes) || {}).stringify_keys
142
+ end
143
+
144
+ def sgid_attributes
145
+ @sgid_attributes ||= node_attributes.slice("sgid").presence || attachable_attributes.slice("sgid")
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ class AttachmentGallery
7
+ include ActiveModel::Model
8
+
9
+ TAG_NAME = "div"
10
+ private_constant :TAG_NAME
11
+
12
+ class << self
13
+ def fragment_by_canonicalizing_attachment_galleries(content)
14
+ fragment_by_replacing_attachment_gallery_nodes(content) do |node|
15
+ "<#{TAG_NAME}>#{node.inner_html}</#{TAG_NAME}>"
16
+ end
17
+ end
18
+
19
+ def fragment_by_replacing_attachment_gallery_nodes(content)
20
+ Fragment.wrap(content).update do |source|
21
+ find_attachment_gallery_nodes(source).each do |node|
22
+ node.replace(yield(node).to_s)
23
+ end
24
+ end
25
+ end
26
+
27
+ def find_attachment_gallery_nodes(content)
28
+ Fragment.wrap(content).find_all(selector).select do |node|
29
+ node.children.all? do |child|
30
+ if child.text?
31
+ /\A(\n|\ )*\z/.match?(child.text)
32
+ else
33
+ child.matches? attachment_selector
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def from_node(node)
40
+ new(node)
41
+ end
42
+
43
+ def attachment_selector
44
+ "#{ActionText::Attachment.tag_name}[presentation=gallery]"
45
+ end
46
+
47
+ def selector
48
+ "#{TAG_NAME}:has(#{attachment_selector} + #{attachment_selector})"
49
+ end
50
+ end
51
+
52
+ attr_reader :node
53
+
54
+ def initialize(node)
55
+ @node = node
56
+ end
57
+
58
+ def attachments
59
+ @attachments ||= node.css(ActionText::AttachmentGallery.attachment_selector).map do |node|
60
+ ActionText::Attachment.from_node(node).with_full_attributes
61
+ end
62
+ end
63
+
64
+ def size
65
+ attachments.size
66
+ end
67
+
68
+ def inspect
69
+ "#<#{self.class.name} size=#{size.inspect}>"
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ module Attachments
7
+ module Caching
8
+ def cache_key(*args)
9
+ [self.class.name, cache_digest, *attachable.cache_key(*args)].join("/")
10
+ end
11
+
12
+ private
13
+ def cache_digest
14
+ OpenSSL::Digest::SHA256.hexdigest(node.to_s)
15
+ end
16
+ end
17
+ end
18
+ end