omg-actiontext 8.0.0.alpha3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +42 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +13 -0
  5. data/app/assets/javascripts/actiontext.esm.js +911 -0
  6. data/app/assets/javascripts/actiontext.js +884 -0
  7. data/app/assets/javascripts/trix.js +12165 -0
  8. data/app/assets/stylesheets/trix.css +412 -0
  9. data/app/helpers/action_text/content_helper.rb +76 -0
  10. data/app/helpers/action_text/tag_helper.rb +106 -0
  11. data/app/javascript/actiontext/attachment_upload.js +62 -0
  12. data/app/javascript/actiontext/index.js +10 -0
  13. data/app/models/action_text/encrypted_rich_text.rb +11 -0
  14. data/app/models/action_text/record.rb +11 -0
  15. data/app/models/action_text/rich_text.rb +93 -0
  16. data/app/views/action_text/attachables/_content_attachment.html.erb +3 -0
  17. data/app/views/action_text/attachables/_missing_attachable.html.erb +1 -0
  18. data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
  19. data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb +3 -0
  20. data/app/views/action_text/contents/_content.html.erb +1 -0
  21. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  22. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  23. data/db/migrate/20180528164100_create_action_text_tables.rb +25 -0
  24. data/lib/action_text/attachable.rb +156 -0
  25. data/lib/action_text/attachables/content_attachment.rb +42 -0
  26. data/lib/action_text/attachables/missing_attachable.rb +29 -0
  27. data/lib/action_text/attachables/remote_image.rb +48 -0
  28. data/lib/action_text/attachment.rb +148 -0
  29. data/lib/action_text/attachment_gallery.rb +72 -0
  30. data/lib/action_text/attachments/caching.rb +18 -0
  31. data/lib/action_text/attachments/minification.rb +19 -0
  32. data/lib/action_text/attachments/trix_conversion.rb +38 -0
  33. data/lib/action_text/attribute.rb +105 -0
  34. data/lib/action_text/content.rb +197 -0
  35. data/lib/action_text/deprecator.rb +9 -0
  36. data/lib/action_text/encryption.rb +40 -0
  37. data/lib/action_text/engine.rb +94 -0
  38. data/lib/action_text/fixture_set.rb +68 -0
  39. data/lib/action_text/fragment.rb +62 -0
  40. data/lib/action_text/gem_version.rb +19 -0
  41. data/lib/action_text/html_conversion.rb +26 -0
  42. data/lib/action_text/plain_text_conversion.rb +114 -0
  43. data/lib/action_text/rendering.rb +35 -0
  44. data/lib/action_text/serialization.rb +38 -0
  45. data/lib/action_text/system_test_helper.rb +61 -0
  46. data/lib/action_text/trix_attachment.rb +94 -0
  47. data/lib/action_text/version.rb +12 -0
  48. data/lib/action_text.rb +59 -0
  49. data/lib/generators/action_text/install/install_generator.rb +84 -0
  50. data/lib/generators/action_text/install/templates/actiontext.css +440 -0
  51. data/lib/rails/generators/test_unit/install_generator.rb +15 -0
  52. data/lib/rails/generators/test_unit/templates/fixtures.yml +4 -0
  53. data/lib/tasks/actiontext.rake +6 -0
  54. data/package.json +39 -0
  55. 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("&lt;script&gt;alert()&lt;/script&gt;")
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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ def self.deprecator # :nodoc:
7
+ @deprecator ||= ActiveSupport::Deprecation.new
8
+ end
9
+ end
@@ -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