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,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionText
|
6
|
+
module Attachments
|
7
|
+
module Minification
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def fragment_by_minifying_attachments(content)
|
12
|
+
Fragment.wrap(content).replace(ActionText::Attachment.tag_name) do |node|
|
13
|
+
node.tap { |n| n.inner_html = "" }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "active_support/core_ext/object/try"
|
6
|
+
|
7
|
+
module ActionText
|
8
|
+
module Attachments
|
9
|
+
module TrixConversion
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
def fragment_by_converting_trix_attachments(content)
|
14
|
+
Fragment.wrap(content).replace(TrixAttachment::SELECTOR) do |node|
|
15
|
+
from_trix_attachment(TrixAttachment.new(node))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def from_trix_attachment(trix_attachment)
|
20
|
+
from_attributes(trix_attachment.attributes)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_trix_attachment(content = trix_attachment_content)
|
25
|
+
attributes = full_attributes.dup
|
26
|
+
attributes["content"] = content if content
|
27
|
+
TrixAttachment.from_attributes(attributes)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def trix_attachment_content
|
32
|
+
if partial_path = attachable.try(:to_trix_content_attachment_partial_path)
|
33
|
+
ActionText::Content.render(partial: partial_path, formats: :html, object: self, as: model_name.element)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionText
|
6
|
+
module Attribute
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
# Provides access to a dependent RichText model that holds the body and
|
11
|
+
# attachments for a single named rich text attribute. This dependent attribute
|
12
|
+
# is lazily instantiated and will be auto-saved when it's been changed. Example:
|
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? #=> true
|
20
|
+
# message.content.to_s # => "<h1>Funny times!</h1>"
|
21
|
+
# message.content.to_plain_text # => "Funny times!"
|
22
|
+
#
|
23
|
+
# The dependent RichText model will also automatically process attachments links
|
24
|
+
# as sent via the Trix-powered editor. These attachments are associated with the
|
25
|
+
# RichText model using Active Storage.
|
26
|
+
#
|
27
|
+
# If you wish to preload the dependent RichText model, you can use the named
|
28
|
+
# scope:
|
29
|
+
#
|
30
|
+
# Message.all.with_rich_text_content # Avoids N+1 queries when you just want the body, not the attachments.
|
31
|
+
# Message.all.with_rich_text_content_and_embeds # Avoids N+1 queries when you just want the body and attachments.
|
32
|
+
# Message.all.with_all_rich_text # Loads all rich text associations.
|
33
|
+
#
|
34
|
+
# #### Options
|
35
|
+
#
|
36
|
+
# * `:encrypted` - Pass true to encrypt the rich text attribute. The
|
37
|
+
# encryption will be non-deterministic. See
|
38
|
+
# `ActiveRecord::Encryption::EncryptableRecord.encrypts`. Default: false.
|
39
|
+
#
|
40
|
+
# * `:strict_loading` - Pass true to force strict loading. When omitted,
|
41
|
+
# `strict_loading:` will be set to the value of the
|
42
|
+
# `strict_loading_by_default` class attribute (false by default).
|
43
|
+
#
|
44
|
+
# * `:store_if_blank` - Pass false to not create RichText records with empty values,
|
45
|
+
# if a blank value is provided. Default: true.
|
46
|
+
#
|
47
|
+
#
|
48
|
+
# Note: Action Text relies on polymorphic associations, which in turn store
|
49
|
+
# class names in the database. When renaming classes that use `has_rich_text`,
|
50
|
+
# make sure to also update the class names in the
|
51
|
+
# `action_text_rich_texts.record_type` polymorphic type column of the
|
52
|
+
# corresponding rows.
|
53
|
+
def has_rich_text(name, encrypted: false, strict_loading: strict_loading_by_default, store_if_blank: true)
|
54
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
55
|
+
def #{name}
|
56
|
+
rich_text_#{name} || build_rich_text_#{name}
|
57
|
+
end
|
58
|
+
|
59
|
+
def #{name}?
|
60
|
+
rich_text_#{name}.present?
|
61
|
+
end
|
62
|
+
CODE
|
63
|
+
|
64
|
+
if store_if_blank
|
65
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
66
|
+
def #{name}=(body)
|
67
|
+
self.#{name}.body = body
|
68
|
+
end
|
69
|
+
CODE
|
70
|
+
else
|
71
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
72
|
+
def #{name}=(body)
|
73
|
+
if body.present?
|
74
|
+
self.#{name}.body = body
|
75
|
+
else
|
76
|
+
if #{name}?
|
77
|
+
self.#{name}.body = body
|
78
|
+
self.#{name}.mark_for_destruction
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
CODE
|
83
|
+
end
|
84
|
+
|
85
|
+
rich_text_class_name = encrypted ? "ActionText::EncryptedRichText" : "ActionText::RichText"
|
86
|
+
has_one :"rich_text_#{name}", -> { where(name: name) },
|
87
|
+
class_name: rich_text_class_name, as: :record, inverse_of: :record, autosave: true, dependent: :destroy,
|
88
|
+
strict_loading: strict_loading
|
89
|
+
|
90
|
+
scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") }
|
91
|
+
scope :"with_rich_text_#{name}_and_embeds", -> { includes("rich_text_#{name}": { embeds_attachments: :blob }) }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Eager load all dependent RichText models in bulk.
|
95
|
+
def with_all_rich_text
|
96
|
+
includes(rich_text_association_names)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the names of all rich text associations.
|
100
|
+
def rich_text_association_names
|
101
|
+
reflect_on_all_associations(:has_one).collect(&:name).select { |n| n.start_with?("rich_text_") }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionText
|
6
|
+
# # Action Text Content
|
7
|
+
#
|
8
|
+
# The `ActionText::Content` class wraps an HTML fragment to add support for
|
9
|
+
# parsing, rendering and serialization. It can be used to extract links and
|
10
|
+
# attachments, convert the fragment to plain text, or serialize the fragment to
|
11
|
+
# the database.
|
12
|
+
#
|
13
|
+
# The ActionText::RichText record serializes the `body` attribute as
|
14
|
+
# `ActionText::Content`.
|
15
|
+
#
|
16
|
+
# class Message < ActiveRecord::Base
|
17
|
+
# has_rich_text :content
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# message = Message.create!(content: "<h1>Funny times!</h1>")
|
21
|
+
# body = message.content.body # => #<ActionText::Content "<div class=\"trix-conte...">
|
22
|
+
# body.to_s # => "<h1>Funny times!</h1>"
|
23
|
+
# body.to_plain_text # => "Funny times!"
|
24
|
+
class Content
|
25
|
+
include Rendering, Serialization, ContentHelper
|
26
|
+
|
27
|
+
attr_reader :fragment
|
28
|
+
|
29
|
+
delegate :deconstruct, to: :fragment
|
30
|
+
delegate :blank?, :empty?, :html_safe, :present?, to: :to_html # Delegating to to_html to avoid including the layout
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def fragment_by_canonicalizing_content(content)
|
34
|
+
fragment = ActionText::Attachment.fragment_by_canonicalizing_attachments(content)
|
35
|
+
fragment = ActionText::AttachmentGallery.fragment_by_canonicalizing_attachment_galleries(fragment)
|
36
|
+
fragment
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(content = nil, options = {})
|
41
|
+
options.with_defaults! canonicalize: true
|
42
|
+
|
43
|
+
if options[:canonicalize]
|
44
|
+
@fragment = self.class.fragment_by_canonicalizing_content(content)
|
45
|
+
else
|
46
|
+
@fragment = ActionText::Fragment.wrap(content)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Extracts links from the HTML fragment:
|
51
|
+
#
|
52
|
+
# html = '<a href="http://example.com/">Example</a>'
|
53
|
+
# content = ActionText::Content.new(html)
|
54
|
+
# content.links # => ["http://example.com/"]
|
55
|
+
def links
|
56
|
+
@links ||= fragment.find_all("a[href]").map { |a| a["href"] }.uniq
|
57
|
+
end
|
58
|
+
|
59
|
+
# Extracts +ActionText::Attachment+s from the HTML fragment:
|
60
|
+
#
|
61
|
+
# attachable = ActiveStorage::Blob.first
|
62
|
+
# html = %Q(<action-text-attachment sgid="#{attachable.attachable_sgid}" caption="Captioned"></action-text-attachment>)
|
63
|
+
# content = ActionText::Content.new(html)
|
64
|
+
# content.attachments # => [#<ActionText::Attachment attachable=#<ActiveStorage::Blob...
|
65
|
+
def attachments
|
66
|
+
@attachments ||= attachment_nodes.map do |node|
|
67
|
+
attachment_for_node(node)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def attachment_galleries
|
72
|
+
@attachment_galleries ||= attachment_gallery_nodes.map do |node|
|
73
|
+
attachment_gallery_for_node(node)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def gallery_attachments
|
78
|
+
@gallery_attachments ||= attachment_galleries.flat_map(&:attachments)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Extracts +ActionText::Attachable+s from the HTML fragment:
|
82
|
+
#
|
83
|
+
# attachable = ActiveStorage::Blob.first
|
84
|
+
# html = %Q(<action-text-attachment sgid="#{attachable.attachable_sgid}" caption="Captioned"></action-text-attachment>)
|
85
|
+
# content = ActionText::Content.new(html)
|
86
|
+
# content.attachables # => [attachable]
|
87
|
+
def attachables
|
88
|
+
@attachables ||= attachment_nodes.map do |node|
|
89
|
+
ActionText::Attachable.from_node(node)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def append_attachables(attachables)
|
94
|
+
attachments = ActionText::Attachment.from_attachables(attachables)
|
95
|
+
self.class.new([self.to_s.presence, *attachments].compact.join("\n"))
|
96
|
+
end
|
97
|
+
|
98
|
+
def render_attachments(**options, &block)
|
99
|
+
content = fragment.replace(ActionText::Attachment.tag_name) do |node|
|
100
|
+
if node.key?("content")
|
101
|
+
sanitized_content = sanitize_content_attachment(node.remove_attribute("content").to_s)
|
102
|
+
node["content"] = sanitized_content if sanitized_content.present?
|
103
|
+
end
|
104
|
+
block.call(attachment_for_node(node, **options))
|
105
|
+
end
|
106
|
+
self.class.new(content, canonicalize: false)
|
107
|
+
end
|
108
|
+
|
109
|
+
def render_attachment_galleries(&block)
|
110
|
+
content = ActionText::AttachmentGallery.fragment_by_replacing_attachment_gallery_nodes(fragment) do |node|
|
111
|
+
block.call(attachment_gallery_for_node(node))
|
112
|
+
end
|
113
|
+
self.class.new(content, canonicalize: false)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns a plain-text version of the markup contained by the content, with tags
|
117
|
+
# removed but HTML entities encoded.
|
118
|
+
#
|
119
|
+
# content = ActionText::Content.new("<h1>Funny times!</h1>")
|
120
|
+
# content.to_plain_text # => "Funny times!"
|
121
|
+
#
|
122
|
+
# content = ActionText::Content.new("<div onclick='action()'>safe<script>unsafe</script></div>")
|
123
|
+
# content.to_plain_text # => "safeunsafe"
|
124
|
+
#
|
125
|
+
# NOTE: that the returned string is not HTML safe and should not be rendered in
|
126
|
+
# browsers.
|
127
|
+
#
|
128
|
+
# content = ActionText::Content.new("<script>alert()</script>")
|
129
|
+
# content.to_plain_text # => "<script>alert()</script>"
|
130
|
+
def to_plain_text
|
131
|
+
render_attachments(with_full_attributes: false, &:to_plain_text).fragment.to_plain_text
|
132
|
+
end
|
133
|
+
|
134
|
+
def to_trix_html
|
135
|
+
render_attachments(&:to_trix_attachment).to_html
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_html
|
139
|
+
fragment.to_html
|
140
|
+
end
|
141
|
+
|
142
|
+
def to_rendered_html_with_layout
|
143
|
+
render layout: "action_text/contents/content", partial: to_partial_path, formats: :html, locals: { content: self }
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_partial_path
|
147
|
+
"action_text/contents/content"
|
148
|
+
end
|
149
|
+
|
150
|
+
# Safely transforms Content into an HTML String.
|
151
|
+
#
|
152
|
+
# content = ActionText::Content.new(content: "<h1>Funny times!</h1>")
|
153
|
+
# content.to_s # => "<h1>Funny times!</h1>"
|
154
|
+
#
|
155
|
+
# content = ActionText::Content.new("<div onclick='action()'>safe<script>unsafe</script></div>")
|
156
|
+
# content.to_s # => "<div>safeunsafe</div>"
|
157
|
+
def to_s
|
158
|
+
to_rendered_html_with_layout
|
159
|
+
end
|
160
|
+
|
161
|
+
def as_json(*)
|
162
|
+
to_html
|
163
|
+
end
|
164
|
+
|
165
|
+
def inspect
|
166
|
+
"#<#{self.class.name} #{to_html.truncate(25).inspect}>"
|
167
|
+
end
|
168
|
+
|
169
|
+
def ==(other)
|
170
|
+
if self.class == other.class
|
171
|
+
to_html == other.to_html
|
172
|
+
elsif other.is_a?(self.class)
|
173
|
+
to_s == other.to_s
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
def attachment_nodes
|
179
|
+
@attachment_nodes ||= fragment.find_all(ActionText::Attachment.tag_name)
|
180
|
+
end
|
181
|
+
|
182
|
+
def attachment_gallery_nodes
|
183
|
+
@attachment_gallery_nodes ||= ActionText::AttachmentGallery.find_attachment_gallery_nodes(fragment)
|
184
|
+
end
|
185
|
+
|
186
|
+
def attachment_for_node(node, with_full_attributes: true)
|
187
|
+
attachment = ActionText::Attachment.from_node(node)
|
188
|
+
with_full_attributes ? attachment.with_full_attributes : attachment
|
189
|
+
end
|
190
|
+
|
191
|
+
def attachment_gallery_for_node(node)
|
192
|
+
ActionText::AttachmentGallery.from_node(node)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
ActiveSupport.run_load_hooks :action_text_content, ActionText::Content
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionText
|
6
|
+
module Encryption
|
7
|
+
def encrypt
|
8
|
+
transaction do
|
9
|
+
super
|
10
|
+
encrypt_rich_texts if has_encrypted_rich_texts?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def decrypt
|
15
|
+
transaction do
|
16
|
+
super
|
17
|
+
decrypt_rich_texts if has_encrypted_rich_texts?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def encrypt_rich_texts
|
23
|
+
encryptable_rich_texts.each(&:encrypt)
|
24
|
+
end
|
25
|
+
|
26
|
+
def decrypt_rich_texts
|
27
|
+
encryptable_rich_texts.each(&:decrypt)
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_encrypted_rich_texts?
|
31
|
+
encryptable_rich_texts.present?
|
32
|
+
end
|
33
|
+
|
34
|
+
def encryptable_rich_texts
|
35
|
+
@encryptable_rich_texts ||= self.class.rich_text_association_names
|
36
|
+
.filter_map { |attribute_name| send(attribute_name) }
|
37
|
+
.find_all { |record| record.is_a?(ActionText::EncryptedRichText) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "rails"
|
6
|
+
require "action_controller/railtie"
|
7
|
+
require "active_record/railtie"
|
8
|
+
require "active_storage/engine"
|
9
|
+
|
10
|
+
require "action_text"
|
11
|
+
|
12
|
+
module ActionText
|
13
|
+
class Engine < Rails::Engine
|
14
|
+
isolate_namespace ActionText
|
15
|
+
config.eager_load_namespaces << ActionText
|
16
|
+
|
17
|
+
config.action_text = ActiveSupport::OrderedOptions.new
|
18
|
+
config.action_text.attachment_tag_name = "action-text-attachment"
|
19
|
+
config.autoload_once_paths = %W(
|
20
|
+
#{root}/app/helpers
|
21
|
+
#{root}/app/models
|
22
|
+
)
|
23
|
+
|
24
|
+
initializer "action_text.deprecator", before: :load_environment_config do |app|
|
25
|
+
app.deprecators[:action_text] = ActionText.deprecator
|
26
|
+
end
|
27
|
+
|
28
|
+
initializer "action_text.attribute" do
|
29
|
+
ActiveSupport.on_load(:active_record) do
|
30
|
+
include ActionText::Attribute
|
31
|
+
prepend ActionText::Encryption
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
initializer "action_text.asset" do
|
36
|
+
if Rails.application.config.respond_to?(:assets)
|
37
|
+
Rails.application.config.assets.precompile += %w( actiontext.js actiontext.esm.js trix.js trix.css )
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
initializer "action_text.attachable" do
|
42
|
+
ActiveSupport.on_load(:active_storage_blob) do
|
43
|
+
include ActionText::Attachable
|
44
|
+
|
45
|
+
def previewable_attachable?
|
46
|
+
representable?
|
47
|
+
end
|
48
|
+
|
49
|
+
def attachable_plain_text_representation(caption = nil)
|
50
|
+
"[#{caption || filename}]"
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_trix_content_attachment_partial_path
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
initializer "action_text.helper" do
|
60
|
+
%i[action_controller_base action_mailer].each do |base|
|
61
|
+
ActiveSupport.on_load(base) do
|
62
|
+
helper ActionText::Engine.helpers
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
initializer "action_text.renderer" do
|
68
|
+
%i[action_controller_base action_mailer].each do |base|
|
69
|
+
ActiveSupport.on_load(base) do
|
70
|
+
around_action do |controller, action|
|
71
|
+
ActionText::Content.with_renderer(controller, &action)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
initializer "action_text.system_test_helper" do
|
78
|
+
ActiveSupport.on_load(:action_dispatch_system_test_case) do
|
79
|
+
require "action_text/system_test_helper"
|
80
|
+
include ActionText::SystemTestHelper
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
initializer "action_text.configure" do |app|
|
85
|
+
ActionText::Attachment.tag_name = app.config.action_text.attachment_tag_name
|
86
|
+
end
|
87
|
+
|
88
|
+
config.after_initialize do |app|
|
89
|
+
if klass = app.config.action_text.sanitizer_vendor
|
90
|
+
ActionText::ContentHelper.sanitizer = klass.safe_list_sanitizer.new
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionText
|
6
|
+
# # Action Text FixtureSet
|
7
|
+
#
|
8
|
+
# Fixtures are a way of organizing data that you want to test against; in short,
|
9
|
+
# sample data.
|
10
|
+
#
|
11
|
+
# To learn more about fixtures, read the ActiveRecord::FixtureSet documentation.
|
12
|
+
#
|
13
|
+
# ### YAML
|
14
|
+
#
|
15
|
+
# Like other Active Record-backed models, ActionText::RichText records inherit
|
16
|
+
# from ActiveRecord::Base instances and can therefore be populated by fixtures.
|
17
|
+
#
|
18
|
+
# Consider an `Article` class:
|
19
|
+
#
|
20
|
+
# class Article < ApplicationRecord
|
21
|
+
# has_rich_text :content
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# To declare fixture data for the related `content`, first declare fixture data
|
25
|
+
# for `Article` instances in `test/fixtures/articles.yml`:
|
26
|
+
#
|
27
|
+
# first:
|
28
|
+
# title: An Article
|
29
|
+
#
|
30
|
+
# Then declare the ActionText::RichText fixture data in
|
31
|
+
# `test/fixtures/action_text/rich_texts.yml`, making sure to declare each
|
32
|
+
# entry's `record:` key as a polymorphic relationship:
|
33
|
+
#
|
34
|
+
# first:
|
35
|
+
# record: first (Article)
|
36
|
+
# name: content
|
37
|
+
# body: <div>Hello, world.</div>
|
38
|
+
#
|
39
|
+
# When processed, Active Record will insert database records for each fixture
|
40
|
+
# entry and will ensure the Action Text relationship is intact.
|
41
|
+
class FixtureSet
|
42
|
+
# Fixtures support Action Text attachments as part of their `body` HTML.
|
43
|
+
#
|
44
|
+
# ### Examples
|
45
|
+
#
|
46
|
+
# For example, consider a second `Article` fixture declared in
|
47
|
+
# `test/fixtures/articles.yml`:
|
48
|
+
#
|
49
|
+
# second:
|
50
|
+
# title: Another Article
|
51
|
+
#
|
52
|
+
# You can attach a mention of `articles(:first)` to `second`'s `content` by
|
53
|
+
# embedding a call to `ActionText::FixtureSet.attachment` in the `body:` value
|
54
|
+
# in `test/fixtures/action_text/rich_texts.yml`:
|
55
|
+
#
|
56
|
+
# second:
|
57
|
+
# record: second (Article)
|
58
|
+
# name: content
|
59
|
+
# body: <div>Hello, <%= ActionText::FixtureSet.attachment("articles", :first) %></div>
|
60
|
+
#
|
61
|
+
def self.attachment(fixture_set_name, label, column_type: :integer)
|
62
|
+
signed_global_id = ActiveRecord::FixtureSet.signed_global_id fixture_set_name, label,
|
63
|
+
column_type: column_type, for: ActionText::Attachable::LOCATOR_NAME
|
64
|
+
|
65
|
+
%(<action-text-attachment sgid="#{signed_global_id}"></action-text-attachment>)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionText
|
6
|
+
class Fragment
|
7
|
+
class << self
|
8
|
+
def wrap(fragment_or_html)
|
9
|
+
case fragment_or_html
|
10
|
+
when self
|
11
|
+
fragment_or_html
|
12
|
+
when Nokogiri::XML::DocumentFragment # base class for all fragments
|
13
|
+
new(fragment_or_html)
|
14
|
+
else
|
15
|
+
from_html(fragment_or_html)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def from_html(html)
|
20
|
+
new(ActionText::HtmlConversion.fragment_for_html(html.to_s.strip))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :source
|
25
|
+
|
26
|
+
delegate :deconstruct, to: "source.elements"
|
27
|
+
|
28
|
+
def initialize(source)
|
29
|
+
@source = source
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_all(selector)
|
33
|
+
source.css(selector)
|
34
|
+
end
|
35
|
+
|
36
|
+
def update
|
37
|
+
yield source = self.source.dup
|
38
|
+
self.class.new(source)
|
39
|
+
end
|
40
|
+
|
41
|
+
def replace(selector)
|
42
|
+
update do |source|
|
43
|
+
source.css(selector).each do |node|
|
44
|
+
replacement_node = yield(node)
|
45
|
+
node.replace(replacement_node.to_s) if node != replacement_node
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_plain_text
|
51
|
+
@plain_text ||= PlainTextConversion.node_to_plain_text(source)
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_html
|
55
|
+
@html ||= HtmlConversion.node_to_html(source)
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
to_html
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionText
|
6
|
+
# Returns the currently loaded version of Action Text as a `Gem::Version`.
|
7
|
+
def self.gem_version
|
8
|
+
Gem::Version.new VERSION::STRING
|
9
|
+
end
|
10
|
+
|
11
|
+
module VERSION
|
12
|
+
MAJOR = 8
|
13
|
+
MINOR = 0
|
14
|
+
TINY = 0
|
15
|
+
PRE = "alpha3"
|
16
|
+
|
17
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionText
|
6
|
+
module HtmlConversion
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def node_to_html(node)
|
10
|
+
node.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_HTML)
|
11
|
+
end
|
12
|
+
|
13
|
+
def fragment_for_html(html)
|
14
|
+
document.fragment(html)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_element(tag_name, attributes = {})
|
18
|
+
document.create_element(tag_name, attributes)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def document
|
23
|
+
ActionText.html_document_class.new.tap { |doc| doc.encoding = "UTF-8" }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|