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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +42 -0
- data/MIT-LICENSE +21 -0
- data/README.md +13 -0
- data/app/assets/javascripts/actiontext.esm.js +911 -0
- data/app/assets/javascripts/actiontext.js +884 -0
- data/app/assets/javascripts/trix.js +12165 -0
- data/app/assets/stylesheets/trix.css +412 -0
- data/app/helpers/action_text/content_helper.rb +76 -0
- data/app/helpers/action_text/tag_helper.rb +106 -0
- data/app/javascript/actiontext/attachment_upload.js +62 -0
- data/app/javascript/actiontext/index.js +10 -0
- data/app/models/action_text/encrypted_rich_text.rb +11 -0
- data/app/models/action_text/record.rb +11 -0
- data/app/models/action_text/rich_text.rb +93 -0
- data/app/views/action_text/attachables/_content_attachment.html.erb +3 -0
- data/app/views/action_text/attachables/_missing_attachable.html.erb +1 -0
- data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
- data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb +3 -0
- data/app/views/action_text/contents/_content.html.erb +1 -0
- data/app/views/active_storage/blobs/_blob.html.erb +14 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/db/migrate/20180528164100_create_action_text_tables.rb +25 -0
- data/lib/action_text/attachable.rb +156 -0
- data/lib/action_text/attachables/content_attachment.rb +42 -0
- data/lib/action_text/attachables/missing_attachable.rb +29 -0
- data/lib/action_text/attachables/remote_image.rb +48 -0
- data/lib/action_text/attachment.rb +148 -0
- data/lib/action_text/attachment_gallery.rb +72 -0
- data/lib/action_text/attachments/caching.rb +18 -0
- data/lib/action_text/attachments/minification.rb +19 -0
- data/lib/action_text/attachments/trix_conversion.rb +38 -0
- data/lib/action_text/attribute.rb +105 -0
- data/lib/action_text/content.rb +197 -0
- data/lib/action_text/deprecator.rb +9 -0
- data/lib/action_text/encryption.rb +40 -0
- data/lib/action_text/engine.rb +94 -0
- data/lib/action_text/fixture_set.rb +68 -0
- data/lib/action_text/fragment.rb +62 -0
- data/lib/action_text/gem_version.rb +19 -0
- data/lib/action_text/html_conversion.rb +26 -0
- data/lib/action_text/plain_text_conversion.rb +114 -0
- data/lib/action_text/rendering.rb +35 -0
- data/lib/action_text/serialization.rb +38 -0
- data/lib/action_text/system_test_helper.rb +61 -0
- data/lib/action_text/trix_attachment.rb +94 -0
- data/lib/action_text/version.rb +12 -0
- data/lib/action_text.rb +59 -0
- data/lib/generators/action_text/install/install_generator.rb +84 -0
- data/lib/generators/action_text/install/templates/actiontext.css +440 -0
- data/lib/rails/generators/test_unit/install_generator.rb +15 -0
- data/lib/rails/generators/test_unit/templates/fixtures.yml +4 -0
- data/lib/tasks/actiontext.rake +6 -0
- data/package.json +39 -0
- metadata +190 -0
@@ -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: "<script>alert()</script>")
|
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 @@
|
|
1
|
+
<%= "☒" -%>
|
@@ -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 @@
|
|
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,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
|