actiontext 6.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actiontext might be problematic. Click here for more details.

Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +35 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +9 -0
  5. data/app/helpers/action_text/content_helper.rb +37 -0
  6. data/app/helpers/action_text/tag_helper.rb +79 -0
  7. data/app/javascript/actiontext/attachment_upload.js +45 -0
  8. data/app/javascript/actiontext/index.js +10 -0
  9. data/app/models/action_text/rich_text.rb +29 -0
  10. data/app/views/action_text/attachables/_missing_attachable.html.erb +1 -0
  11. data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
  12. data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb +3 -0
  13. data/app/views/action_text/content/_layout.html.erb +3 -0
  14. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  15. data/db/migrate/20180528164100_create_action_text_tables.rb +13 -0
  16. data/lib/action_text.rb +37 -0
  17. data/lib/action_text/attachable.rb +86 -0
  18. data/lib/action_text/attachables/content_attachment.rb +38 -0
  19. data/lib/action_text/attachables/missing_attachable.rb +13 -0
  20. data/lib/action_text/attachables/remote_image.rb +46 -0
  21. data/lib/action_text/attachment.rb +103 -0
  22. data/lib/action_text/attachment_gallery.rb +65 -0
  23. data/lib/action_text/attachments/caching.rb +16 -0
  24. data/lib/action_text/attachments/minification.rb +17 -0
  25. data/lib/action_text/attachments/trix_conversion.rb +34 -0
  26. data/lib/action_text/attribute.rb +45 -0
  27. data/lib/action_text/content.rb +132 -0
  28. data/lib/action_text/engine.rb +54 -0
  29. data/lib/action_text/fragment.rb +57 -0
  30. data/lib/action_text/gem_version.rb +17 -0
  31. data/lib/action_text/html_conversion.rb +24 -0
  32. data/lib/action_text/plain_text_conversion.rb +81 -0
  33. data/lib/action_text/serialization.rb +34 -0
  34. data/lib/action_text/trix_attachment.rb +92 -0
  35. data/lib/action_text/version.rb +10 -0
  36. data/lib/tasks/actiontext.rake +20 -0
  37. data/lib/templates/actiontext.scss +36 -0
  38. data/lib/templates/fixtures.yml +4 -0
  39. data/lib/templates/installer.rb +45 -0
  40. data/package.json +29 -0
  41. metadata +161 -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module Attachables
5
+ module MissingAttachable
6
+ extend ActiveModel::Naming
7
+
8
+ def self.to_partial_path
9
+ "action_text/attachables/missing_attachable"
10
+ end
11
+ end
12
+ end
13
+ 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::Content.renderer.render(partial: partial_path, object: self, as: model_name.element)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,45 @@
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
+ rich_text_#{name} || build_rich_text_#{name}
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) },
38
+ class_name: "ActionText::RichText", as: :record, inverse_of: :record, autosave: true, dependent: :destroy
39
+
40
+ scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") }
41
+ scope :"with_rich_text_#{name}_and_embeds", -> { includes("rich_text_#{name}": { embeds_attachments: :blob }) }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
4
+
5
+ module ActionText
6
+ class Content
7
+ include Serialization
8
+
9
+ thread_cattr_accessor :renderer
10
+
11
+ attr_reader :fragment
12
+
13
+ delegate :blank?, :empty?, :html_safe, :present?, to: :to_html # Delegating to to_html to avoid including the layout
14
+
15
+ class << self
16
+ def fragment_by_canonicalizing_content(content)
17
+ fragment = ActionText::Attachment.fragment_by_canonicalizing_attachments(content)
18
+ fragment = ActionText::AttachmentGallery.fragment_by_canonicalizing_attachment_galleries(fragment)
19
+ fragment
20
+ end
21
+ end
22
+
23
+ def initialize(content = nil, options = {})
24
+ options.with_defaults! canonicalize: true
25
+
26
+ if options[:canonicalize]
27
+ @fragment = self.class.fragment_by_canonicalizing_content(content)
28
+ else
29
+ @fragment = ActionText::Fragment.wrap(content)
30
+ end
31
+ end
32
+
33
+ def links
34
+ @links ||= fragment.find_all("a[href]").map { |a| a["href"] }.uniq
35
+ end
36
+
37
+ def attachments
38
+ @attachments ||= attachment_nodes.map do |node|
39
+ attachment_for_node(node)
40
+ end
41
+ end
42
+
43
+ def attachment_galleries
44
+ @attachment_galleries ||= attachment_gallery_nodes.map do |node|
45
+ attachment_gallery_for_node(node)
46
+ end
47
+ end
48
+
49
+ def gallery_attachments
50
+ @gallery_attachments ||= attachment_galleries.flat_map(&:attachments)
51
+ end
52
+
53
+ def attachables
54
+ @attachables ||= attachment_nodes.map do |node|
55
+ ActionText::Attachable.from_node(node)
56
+ end
57
+ end
58
+
59
+ def append_attachables(attachables)
60
+ attachments = ActionText::Attachment.from_attachables(attachables)
61
+ self.class.new([self.to_s.presence, *attachments].compact.join("\n"))
62
+ end
63
+
64
+ def render_attachments(**options, &block)
65
+ content = fragment.replace(ActionText::Attachment::SELECTOR) do |node|
66
+ block.call(attachment_for_node(node, **options))
67
+ end
68
+ self.class.new(content, canonicalize: false)
69
+ end
70
+
71
+ def render_attachment_galleries(&block)
72
+ content = ActionText::AttachmentGallery.fragment_by_replacing_attachment_gallery_nodes(fragment) do |node|
73
+ block.call(attachment_gallery_for_node(node))
74
+ end
75
+ self.class.new(content, canonicalize: false)
76
+ end
77
+
78
+ def to_plain_text
79
+ render_attachments(with_full_attributes: false, &:to_plain_text).fragment.to_plain_text
80
+ end
81
+
82
+ def to_trix_html
83
+ render_attachments(&:to_trix_attachment).to_html
84
+ end
85
+
86
+ def to_html
87
+ fragment.to_html
88
+ end
89
+
90
+ def to_rendered_html_with_layout
91
+ renderer.render(partial: "action_text/content/layout", locals: { content: self })
92
+ end
93
+
94
+ def to_s
95
+ to_rendered_html_with_layout
96
+ end
97
+
98
+ def as_json(*)
99
+ to_html
100
+ end
101
+
102
+ def inspect
103
+ "#<#{self.class.name} #{to_s.truncate(25).inspect}>"
104
+ end
105
+
106
+ def ==(other)
107
+ if other.is_a?(self.class)
108
+ to_s == other.to_s
109
+ end
110
+ end
111
+
112
+ private
113
+ def attachment_nodes
114
+ @attachment_nodes ||= fragment.find_all(ActionText::Attachment::SELECTOR)
115
+ end
116
+
117
+ def attachment_gallery_nodes
118
+ @attachment_gallery_nodes ||= ActionText::AttachmentGallery.find_attachment_gallery_nodes(fragment)
119
+ end
120
+
121
+ def attachment_for_node(node, with_full_attributes: true)
122
+ attachment = ActionText::Attachment.from_node(node)
123
+ with_full_attributes ? attachment.with_full_attributes : attachment
124
+ end
125
+
126
+ def attachment_gallery_for_node(node)
127
+ ActionText::AttachmentGallery.from_node(node)
128
+ end
129
+ end
130
+ end
131
+
132
+ ActiveSupport.run_load_hooks :action_text_content, ActionText::Content