omg-actiontext 8.0.0.alpha3

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.
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