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