omg-actiontext 8.0.0.alpha3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|