Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +9 -0
  5. data/app/helpers/action_text/content_helper.rb +30 -0
  6. data/app/helpers/action_text/tag_helper.rb +75 -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/201805281641_create_action_text_tables.rb +14 -0
  16. data/lib/action_text.rb +37 -0
  17. data/lib/action_text/attachable.rb +82 -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 +48 -0
  27. data/lib/action_text/content.rb +132 -0
  28. data/lib/action_text/engine.rb +50 -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 +32 -0
  40. data/package.json +29 -0
  41. metadata +158 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9eb946145656ee7d27e4ee9ce733a3e0f55e1b63ab16e4cea8741fecaac697f6
4
+ data.tar.gz: 658a5373916fd11abfdbd569746f1f4ed23fb71cc52294d513d063de47a148ab
5
+ SHA512:
6
+ metadata.gz: 8418aebfa611943071b773ec51544e039f4f3e8220a594c4a66bfced904754da4f2ee1fc7d0472e7d6e0e207f4aad70f8828509fd0f6a95df5db1557e42f34cb
7
+ data.tar.gz: 427541191977534c0d36bf0b4cfb0615a5525e0ef645a939b31e066dac689ec0288bc7b6dca919523c8adae6ffee56888bc0d0cb232cd592c51bd8f06daee357
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## Rails 6.0.0.beta1 (January 18, 2019) ##
2
+
3
+ * Added to Rails.
4
+
5
+ *DHH*
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Basecamp, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # Action Text
2
+
3
+ Action Text brings rich text content and editing to Rails. It includes the [Trix editor](https://trix-editor.org) that handles everything from formatting to links to quotes to lists to embedded images and galleries. The rich text content generated by the Trix editor is saved in its own RichText model that's associated with any existing Active Record model in the application. Any embedded images (or other attachments) are automatically stored using Active Storage and associated with the included RichText model.
4
+
5
+ You can read more about Action Text in the [Action Text Overview](https://edgeguides.rubyonrails.org/action_text_overview.html) guide.
6
+
7
+ ## License
8
+
9
+ Action Text is released under the [MIT License](https://opensource.org/licenses/MIT).
data/app/helpers/action_text/content_helper.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module ContentHelper
5
+ SANITIZER = Rails::Html::Sanitizer.white_list_sanitizer
6
+ ALLOWED_TAGS = SANITIZER.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ]
7
+ ALLOWED_ATTRIBUTES = SANITIZER.allowed_attributes + ActionText::Attachment::ATTRIBUTES
8
+
9
+ def render_action_text_content(content)
10
+ content = content.render_attachments do |attachment|
11
+ unless attachment.in?(content.gallery_attachments)
12
+ attachment.node.tap do |node|
13
+ node.inner_html = render(attachment, in_gallery: false).chomp
14
+ end
15
+ end
16
+ end
17
+
18
+ content = content.render_attachment_galleries do |attachment_gallery|
19
+ render(layout: attachment_gallery, object: attachment_gallery) do
20
+ attachment_gallery.attachments.map do |attachment|
21
+ attachment.node.inner_html = render(attachment, in_gallery: true).chomp
22
+ attachment.to_html
23
+ end.join("").html_safe
24
+ end.chomp
25
+ end
26
+
27
+ sanitize content.to_html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES
28
+ end
29
+ end
30
+ end
data/app/helpers/action_text/tag_helper.rb ADDED
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module TagHelper
5
+ cattr_accessor(:id, instance_accessor: false) { 0 }
6
+
7
+ # Returns a `trix-editor` tag that instantiates the Trix JavaScript editor as well as a hidden field
8
+ # that Trix will write to on changes, so the content will be sent on form submissions.
9
+ #
10
+ # ==== Options
11
+ # * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
12
+ #
13
+ # ==== Example
14
+ #
15
+ # rich_text_area_tag "content", message.content
16
+ # # <input type="hidden" name="content" id="trix_input_post_1">
17
+ # # <trix-editor id="content" input="trix_input_post_1" class="trix-content" ...></trix-editor>
18
+ def rich_text_area_tag(name, value = nil, options = {})
19
+ options = options.symbolize_keys
20
+
21
+ options[:input] ||= "trix_input_#{ActionText::TagHelper.id += 1}"
22
+ options[:class] ||= "trix-content"
23
+
24
+ options[:data] ||= {}
25
+ options[:data][:direct_upload_url] = main_app.rails_direct_uploads_url
26
+ options[:data][:blob_url_template] = main_app.rails_service_blob_url(":signed_id", ":filename")
27
+
28
+ editor_tag = content_tag("trix-editor", "", options)
29
+ input_tag = hidden_field_tag(name, value, id: options[:input])
30
+
31
+ input_tag + editor_tag
32
+ end
33
+ end
34
+ end
35
+
36
+ module ActionView::Helpers
37
+ class Tags::ActionText < Tags::Base
38
+ delegate :dom_id, to: ActionView::RecordIdentifier
39
+
40
+ def render
41
+ options = @options.stringify_keys
42
+ add_default_name_and_id(options)
43
+ options["input"] ||= dom_id(object, [options["id"], :trix_input].compact.join("_")) if object
44
+ @template_object.rich_text_area_tag(options.delete("name"), editable_value, options)
45
+ end
46
+
47
+ def editable_value
48
+ value&.body.try(:to_trix_html)
49
+ end
50
+ end
51
+
52
+ module FormHelper
53
+ # Returns a `trix-editor` tag that instantiates the Trix JavaScript editor as well as a hidden field
54
+ # that Trix will write to on changes, so the content will be sent on form submissions.
55
+ #
56
+ # ==== Options
57
+ # * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
58
+ #
59
+ # ==== Example
60
+ # form_with(model: @message) do |form|
61
+ # form.rich_text_area :content
62
+ # end
63
+ # # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1">
64
+ # # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor>
65
+ def rich_text_area(object_name, method, options = {})
66
+ Tags::ActionText.new(object_name, method, self, options).render
67
+ end
68
+ end
69
+
70
+ class FormBuilder
71
+ def rich_text_area(method, options = {})
72
+ @template.rich_text_area(@object_name, method, objectify_options(options))
73
+ end
74
+ end
75
+ end
data/app/javascript/actiontext/attachment_upload.js ADDED
@@ -0,0 +1,45 @@
1
+ import { DirectUpload } from "@rails/activestorage"
2
+
3
+ export class AttachmentUpload {
4
+ constructor(attachment, element) {
5
+ this.attachment = attachment
6
+ this.element = element
7
+ this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this)
8
+ }
9
+
10
+ start() {
11
+ this.directUpload.create(this.directUploadDidComplete.bind(this))
12
+ }
13
+
14
+ directUploadWillStoreFileWithXHR(xhr) {
15
+ xhr.upload.addEventListener("progress", event => {
16
+ const progress = event.loaded / event.total * 100
17
+ this.attachment.setUploadProgress(progress)
18
+ })
19
+ }
20
+
21
+ directUploadDidComplete(error, attributes) {
22
+ if (error) {
23
+ throw new Error(`Direct upload failed: ${error}`)
24
+ }
25
+
26
+ this.attachment.setAttributes({
27
+ sgid: attributes.attachable_sgid,
28
+ url: this.createBlobUrl(attributes.signed_id, attributes.filename)
29
+ })
30
+ }
31
+
32
+ createBlobUrl(signedId, filename) {
33
+ return this.blobUrlTemplate
34
+ .replace(":signed_id", signedId)
35
+ .replace(":filename", encodeURIComponent(filename))
36
+ }
37
+
38
+ get directUploadUrl() {
39
+ return this.element.dataset.directUploadUrl
40
+ }
41
+
42
+ get blobUrlTemplate() {
43
+ return this.element.dataset.blobUrlTemplate
44
+ }
45
+ }
data/app/javascript/actiontext/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import { AttachmentUpload } from "./attachment_upload"
2
+
3
+ addEventListener("trix-attachment-add", event => {
4
+ const { attachment, target } = event
5
+
6
+ if (attachment.file) {
7
+ const upload = new AttachmentUpload(attachment, target)
8
+ upload.start()
9
+ }
10
+ })
data/app/models/action_text/rich_text.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ # The RichText record holds the content produced by the Trix editor in a serialized `body` attribute.
5
+ # It also holds all the references to the embedded files, which are stored using Active Storage.
6
+ # This record is then associated with the Active Record model the application desires to have
7
+ # rich text content using the `has_rich_text` class method.
8
+ class RichText < ActiveRecord::Base
9
+ self.table_name = "action_text_rich_texts"
10
+
11
+ serialize :body, ActionText::Content
12
+ delegate :to_s, :nil?, to: :body
13
+
14
+ belongs_to :record, polymorphic: true, touch: true
15
+ has_many_attached :embeds
16
+
17
+ before_save do
18
+ self.embeds = body.attachments.map(&:attachable) if body.present?
19
+ end
20
+
21
+ def to_plain_text
22
+ body&.to_plain_text.to_s
23
+ end
24
+
25
+ delegate :blank?, :empty?, :present?, to: :to_plain_text
26
+ end
27
+ end
28
+
29
+ ActiveSupport.run_load_hooks :action_text_rich_text, ActionText::RichText
data/app/views/action_text/attachables/_missing_attachable.html.erb ADDED
@@ -0,0 +1 @@
1
+ <%= "☒" -%>
data/app/views/action_text/attachables/_remote_image.html.erb ADDED
@@ -0,0 +1,8 @@
1
+ <figure class="attachment attachment--preview">
2
+ <%= image_tag(remote_image.url, width: remote_image.width, height: remote_image.height) %>
3
+ <% if caption = remote_image.try(:caption) %>
4
+ <figcaption class="attachment__caption">
5
+ <%= caption %>
6
+ </figcaption>
7
+ <% end %>
8
+ </figure>
data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb ADDED
@@ -0,0 +1,3 @@
1
+ <div class="attachment-gallery attachment-gallery--<%= attachment_gallery.size %>">
2
+ <%= yield %>
3
+ </div>
data/app/views/action_text/content/_layout.html.erb ADDED
@@ -0,0 +1,3 @@
1
+ <div class="trix-content">
2
+ <%= render_action_text_content(content) %>
3
+ </div>
data/app/views/active_storage/blobs/_blob.html.erb ADDED
@@ -0,0 +1,14 @@
1
+ <figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
2
+ <% if blob.representable? %>
3
+ <%= image_tag blob.representation(resize_to_fit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
4
+ <% end %>
5
+
6
+ <figcaption class="attachment__caption">
7
+ <% if caption = blob.try(:caption) %>
8
+ <%= caption %>
9
+ <% else %>
10
+ <span class="attachment__name"><%= blob.filename %></span>
11
+ <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
12
+ <% end %>
13
+ </figcaption>
14
+ </figure>
data/db/migrate/201805281641_create_action_text_tables.rb ADDED
@@ -0,0 +1,14 @@
1
+ class CreateActionTextTables < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :action_text_rich_texts do |t|
4
+ t.string :name, null: false
5
+ t.text :body, limit: 16777215
6
+ t.references :record, null: false, polymorphic: true, index: false
7
+
8
+ t.datetime :created_at, null: false
9
+ t.datetime :updated_at, null: false
10
+
11
+ t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
12
+ end
13
+ end
14
+ end
data/lib/action_text.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/rails"
5
+
6
+ require "nokogiri"
7
+
8
+ module ActionText
9
+ extend ActiveSupport::Autoload
10
+
11
+ autoload :Attachable
12
+ autoload :AttachmentGallery
13
+ autoload :Attachment
14
+ autoload :Attribute
15
+ autoload :Content
16
+ autoload :Fragment
17
+ autoload :HtmlConversion
18
+ autoload :PlainTextConversion
19
+ autoload :Serialization
20
+ autoload :TrixAttachment
21
+
22
+ module Attachables
23
+ extend ActiveSupport::Autoload
24
+
25
+ autoload :ContentAttachment
26
+ autoload :MissingAttachable
27
+ autoload :RemoteImage
28
+ end
29
+
30
+ module Attachments
31
+ extend ActiveSupport::Autoload
32
+
33
+ autoload :Caching
34
+ autoload :Minification
35
+ autoload :TrixConversion
36
+ end
37
+ end
data/lib/action_text/attachable.rb ADDED
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module Attachable
5
+ extend ActiveSupport::Concern
6
+
7
+ LOCATOR_NAME = "attachable"
8
+
9
+ class << self
10
+ def from_node(node)
11
+ if attachable = attachable_from_sgid(node["sgid"])
12
+ attachable
13
+ elsif attachable = ActionText::Attachables::ContentAttachment.from_node(node)
14
+ attachable
15
+ elsif attachable = ActionText::Attachables::RemoteImage.from_node(node)
16
+ attachable
17
+ else
18
+ ActionText::Attachables::MissingAttachable
19
+ end
20
+ end
21
+
22
+ def from_attachable_sgid(sgid, options = {})
23
+ method = sgid.is_a?(Array) ? :locate_many_signed : :locate_signed
24
+ record = GlobalID::Locator.public_send(method, sgid, options.merge(for: LOCATOR_NAME))
25
+ record || raise(ActiveRecord::RecordNotFound)
26
+ end
27
+
28
+ private
29
+ def attachable_from_sgid(sgid)
30
+ from_attachable_sgid(sgid)
31
+ rescue ActiveRecord::RecordNotFound
32
+ nil
33
+ end
34
+ end
35
+
36
+ class_methods do
37
+ def from_attachable_sgid(sgid)
38
+ ActionText::Attachable.from_attachable_sgid(sgid, only: self)
39
+ end
40
+ end
41
+
42
+ def attachable_sgid
43
+ to_sgid(expires_in: nil, for: LOCATOR_NAME).to_s
44
+ end
45
+
46
+ def attachable_content_type
47
+ try(:content_type) || "application/octet-stream"
48
+ end
49
+
50
+ def attachable_filename
51
+ filename.to_s if respond_to?(:filename)
52
+ end
53
+
54
+ def attachable_filesize
55
+ try(:byte_size) || try(:filesize)
56
+ end
57
+
58
+ def attachable_metadata
59
+ try(:metadata) || {}
60
+ end
61
+
62
+ def previewable_attachable?
63
+ false
64
+ end
65
+
66
+ def as_json(*)
67
+ super.merge(attachable_sgid: attachable_sgid)
68
+ end
69
+
70
+ def to_rich_text_attributes(attributes = {})
71
+ attributes.dup.tap do |attrs|
72
+ attrs[:sgid] = attachable_sgid
73
+ attrs[:content_type] = attachable_content_type
74
+ attrs[:previewable] = true if previewable_attachable?
75
+ attrs[:filename] = attachable_filename
76
+ attrs[:filesize] = attachable_filesize
77
+ attrs[:width] = attachable_metadata[:width]
78
+ attrs[:height] = attachable_metadata[:height]
79
+ end.compact
80
+ end
81
+ end
82
+ end
data/lib/action_text/attachables/content_attachment.rb ADDED
@@ -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
data/lib/action_text/attachables/missing_attachable.rb ADDED
@@ -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
data/lib/action_text/attachables/remote_image.rb ADDED
@@ -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
data/lib/action_text/attachment.rb ADDED
@@ -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
data/lib/action_text/attachment_gallery.rb ADDED
@@ -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
data/lib/action_text/attachments/caching.rb ADDED
@@ -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
data/lib/action_text/attachments/minification.rb ADDED
@@ -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
data/lib/action_text/attachments/trix_conversion.rb ADDED
@@ -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
data/lib/action_text/attribute.rb ADDED
@@ -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
data/lib/action_text/content.rb ADDED
@@ -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
data/lib/action_text/engine.rb ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "action_controller/railtie"
5
+ require "active_record/railtie"
6
+ require "active_storage/engine"
7
+
8
+ require "action_text"
9
+
10
+ module ActionText
11
+ class Engine < Rails::Engine
12
+ isolate_namespace ActionText
13
+ config.eager_load_namespaces << ActionText
14
+
15
+ initializer "action_text.attribute" do
16
+ ActiveSupport.on_load(:active_record) do
17
+ include ActionText::Attribute
18
+ end
19
+ end
20
+
21
+ initializer "action_text.attachable" do
22
+ ActiveSupport.on_load(:active_storage_blob) do
23
+ include ActionText::Attachable
24
+
25
+ def previewable_attachable?
26
+ representable?
27
+ end
28
+ end
29
+ end
30
+
31
+ initializer "action_text.helper" do
32
+ ActiveSupport.on_load(:action_controller_base) do
33
+ helper ActionText::Engine.helpers
34
+ end
35
+ end
36
+
37
+ initializer "action_text.renderer" do |app|
38
+ app.executor.to_run { ActionText::Content.renderer = ApplicationController.renderer }
39
+ app.executor.to_complete { ActionText::Content.renderer = ApplicationController.renderer }
40
+
41
+ ActiveSupport.on_load(:action_text_content) do
42
+ self.renderer = ApplicationController.renderer
43
+ end
44
+
45
+ ActiveSupport.on_load(:action_controller_base) do
46
+ before_action { ActionText::Content.renderer = ApplicationController.renderer.new(request.env) }
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/action_text/fragment.rb ADDED
@@ -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
data/lib/action_text/gem_version.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ # Returns the currently-loaded version of Action Text as a <tt>Gem::Version</tt>.
5
+ def self.gem_version
6
+ Gem::Version.new VERSION::STRING
7
+ end
8
+
9
+ module VERSION
10
+ MAJOR = 6
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "beta1"
14
+
15
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
+ end
17
+ end
data/lib/action_text/html_conversion.rb ADDED
@@ -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
data/lib/action_text/plain_text_conversion.rb ADDED
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module PlainTextConversion
5
+ extend self
6
+
7
+ def node_to_plain_text(node)
8
+ remove_trailing_newlines(plain_text_for_node(node))
9
+ end
10
+
11
+ private
12
+ def plain_text_for_node(node, index = 0)
13
+ if respond_to?(plain_text_method_for_node(node), true)
14
+ send(plain_text_method_for_node(node), node, index)
15
+ else
16
+ plain_text_for_node_children(node)
17
+ end
18
+ end
19
+
20
+ def plain_text_for_node_children(node)
21
+ node.children.each_with_index.map do |child, index|
22
+ plain_text_for_node(child, index)
23
+ end.compact.join("")
24
+ end
25
+
26
+ def plain_text_method_for_node(node)
27
+ :"plain_text_for_#{node.name}_node"
28
+ end
29
+
30
+ def plain_text_for_block(node, index = 0)
31
+ "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n\n"
32
+ end
33
+
34
+ %i[ h1 p ul ol ].each do |element|
35
+ alias_method :"plain_text_for_#{element}_node", :plain_text_for_block
36
+ end
37
+
38
+ def plain_text_for_br_node(node, index)
39
+ "\n"
40
+ end
41
+
42
+ def plain_text_for_text_node(node, index)
43
+ remove_trailing_newlines(node.text)
44
+ end
45
+
46
+ def plain_text_for_div_node(node, index)
47
+ "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n"
48
+ end
49
+
50
+ def plain_text_for_figcaption_node(node, index)
51
+ "[#{remove_trailing_newlines(plain_text_for_node_children(node))}]"
52
+ end
53
+
54
+ def plain_text_for_blockquote_node(node, index)
55
+ text = plain_text_for_block(node)
56
+ text.sub(/\A(\s*)(.+?)(\s*)\Z/m, '\1“\2”\3')
57
+ end
58
+
59
+ def plain_text_for_li_node(node, index)
60
+ bullet = bullet_for_li_node(node, index)
61
+ text = remove_trailing_newlines(plain_text_for_node_children(node))
62
+ "#{bullet} #{text}\n"
63
+ end
64
+
65
+ def remove_trailing_newlines(text)
66
+ text.chomp("")
67
+ end
68
+
69
+ def bullet_for_li_node(node, index)
70
+ if list_node_name_for_li_node(node) == "ol"
71
+ "#{index + 1}."
72
+ else
73
+ "•"
74
+ end
75
+ end
76
+
77
+ def list_node_name_for_li_node(node)
78
+ node.ancestors.lazy.map(&:name).grep(/^[uo]l#x2F;).first
79
+ end
80
+ end
81
+ end
data/lib/action_text/serialization.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ module Serialization
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def load(content)
9
+ new(content) if content
10
+ end
11
+
12
+ def dump(content)
13
+ case content
14
+ when nil
15
+ nil
16
+ when self
17
+ content.to_html
18
+ else
19
+ new(content).to_html
20
+ end
21
+ end
22
+ end
23
+
24
+ # Marshal compatibility
25
+
26
+ class_methods do
27
+ alias_method :_load, :load
28
+ end
29
+
30
+ def _dump(*)
31
+ self.class.dump(self)
32
+ end
33
+ end
34
+ end
data/lib/action_text/trix_attachment.rb ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionText
4
+ class TrixAttachment
5
+ TAG_NAME = "figure"
6
+ SELECTOR = "[data-trix-attachment]"
7
+
8
+ COMPOSED_ATTRIBUTES = %w( caption presentation )
9
+ ATTRIBUTES = %w( sgid contentType url href filename filesize width height previewable content ) + COMPOSED_ATTRIBUTES
10
+ ATTRIBUTE_TYPES = {
11
+ "previewable" => ->(value) { value.to_s == "true" },
12
+ "filesize" => ->(value) { Integer(value.to_s) rescue value },
13
+ "width" => ->(value) { Integer(value.to_s) rescue nil },
14
+ "height" => ->(value) { Integer(value.to_s) rescue nil },
15
+ :default => ->(value) { value.to_s }
16
+ }
17
+
18
+ class << self
19
+ def from_attributes(attributes)
20
+ attributes = process_attributes(attributes)
21
+
22
+ trix_attachment_attributes = attributes.except(*COMPOSED_ATTRIBUTES)
23
+ trix_attributes = attributes.slice(*COMPOSED_ATTRIBUTES)
24
+
25
+ node = ActionText::HtmlConversion.create_element(TAG_NAME)
26
+ node["data-trix-attachment"] = JSON.generate(trix_attachment_attributes)
27
+ node["data-trix-attributes"] = JSON.generate(trix_attributes) if trix_attributes.any?
28
+
29
+ new(node)
30
+ end
31
+
32
+ private
33
+ def process_attributes(attributes)
34
+ typecast_attribute_values(transform_attribute_keys(attributes))
35
+ end
36
+
37
+ def transform_attribute_keys(attributes)
38
+ attributes.transform_keys { |key| key.to_s.underscore.camelize(:lower) }
39
+ end
40
+
41
+ def typecast_attribute_values(attributes)
42
+ attributes.map do |key, value|
43
+ typecast = ATTRIBUTE_TYPES[key] || ATTRIBUTE_TYPES[:default]
44
+ [key, typecast.call(value)]
45
+ end.to_h
46
+ end
47
+ end
48
+
49
+ attr_reader :node
50
+
51
+ def initialize(node)
52
+ @node = node
53
+ end
54
+
55
+ def attributes
56
+ @attributes ||= attachment_attributes.merge(composed_attributes).slice(*ATTRIBUTES)
57
+ end
58
+
59
+ def to_html
60
+ ActionText::HtmlConversion.node_to_html(node)
61
+ end
62
+
63
+ def to_s
64
+ to_html
65
+ end
66
+
67
+ private
68
+ def attachment_attributes
69
+ read_json_object_attribute("data-trix-attachment")
70
+ end
71
+
72
+ def composed_attributes
73
+ read_json_object_attribute("data-trix-attributes")
74
+ end
75
+
76
+ def read_json_object_attribute(name)
77
+ read_json_attribute(name) || {}
78
+ end
79
+
80
+ def read_json_attribute(name)
81
+ if value = node[name]
82
+ begin
83
+ JSON.parse(value)
84
+ rescue => e
85
+ Rails.logger.error "[#{self.class.name}] Couldn't parse JSON #{value} from NODE #{node.inspect}"
86
+ Rails.logger.error "[#{self.class.name}] Failed with #{e.class}: #{e.backtrace}"
87
+ nil
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
data/lib/action_text/version.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gem_version"
4
+
5
+ module ActionText
6
+ # Returns the currently-loaded version of Action Text as a <tt>Gem::Version</tt>.
7
+ def self.version
8
+ gem_version
9
+ end
10
+ end
data/lib/tasks/actiontext.rake ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :action_text do
4
+ # Prevent migration installation task from showing up twice.
5
+ Rake::Task["install:migrations"].clear_comments
6
+
7
+ desc "Copy over the migration, stylesheet, and JavaScript files"
8
+ task install: %w( environment run_installer copy_migrations )
9
+
10
+ task :run_installer do
11
+ installer_template = File.expand_path("../templates/installer.rb", __dir__)
12
+ system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{installer_template}"
13
+ end
14
+
15
+ task :copy_migrations do
16
+ Rake::Task["active_storage:install:migrations"].invoke
17
+ Rake::Task["railties:install:migrations"].reenable # Otherwise you can't run 2 migration copy tasks in one invocation
18
+ Rake::Task["action_text:install:migrations"].invoke
19
+ end
20
+ end
data/lib/templates/actiontext.scss ADDED
@@ -0,0 +1,36 @@
1
+ //
2
+ // Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and
3
+ // the trix-editor content (whether displayed or under editing). Feel free to incorporate this
4
+ // inclusion directly in any other asset bundle and remove this file.
5
+ //
6
+ //= require trix/dist/trix
7
+
8
+ // We need to override trix.css’s image gallery styles to accommodate the
9
+ // <action-text-attachment> element we wrap around attachments. Otherwise,
10
+ // images in galleries will be squished by the max-width: 33%; rule.
11
+ .trix-content {
12
+ .attachment-gallery {
13
+ > action-text-attachment,
14
+ > .attachment {
15
+ flex: 1 0 33%;
16
+ padding: 0 0.5em;
17
+ max-width: 33%;
18
+ }
19
+
20
+ &.attachment-gallery--2,
21
+ &.attachment-gallery--4 {
22
+ > action-text-attachment,
23
+ > .attachment {
24
+ flex-basis: 50%;
25
+ max-width: 50%;
26
+ }
27
+ }
28
+ }
29
+
30
+ action-text-attachment {
31
+ .attachment {
32
+ padding: 0 !important;
33
+ max-width: 100% !important;
34
+ }
35
+ }
36
+ }
data/lib/templates/fixtures.yml ADDED
@@ -0,0 +1,4 @@
1
+ # one:
2
+ # record: name_of_fixture (ClassOfFixture)
3
+ # name: content
4
+ # body: <p>In a <i>million</i> stars!</p>
data/lib/templates/installer.rb ADDED
@@ -0,0 +1,32 @@
1
+ require "pathname"
2
+ require "json"
3
+
4
+ APPLICATION_PACK_PATH = Pathname.new("app/javascript/packs/application.js")
5
+ JS_PACKAGE_PATH = Pathname.new("#{__dir__}/../../package.json")
6
+
7
+ JS_PACKAGE = JSON.load(JS_PACKAGE_PATH)
8
+ JS_DEPENDENCIES = JS_PACKAGE["peerDependencies"].dup.merge \
9
+ JS_PACKAGE["name"] => "^#{JS_PACKAGE["version"]}"
10
+
11
+ say "Copying actiontext.scss to app/assets/stylesheets"
12
+ copy_file "#{__dir__}/actiontext.scss", "app/assets/stylesheets/actiontext.scss"
13
+
14
+ say "Copying fixtures to test/fixtures/action_text/rich_texts.yml"
15
+ copy_file "#{__dir__}/fixtures.yml", "test/fixtures/action_text/rich_texts.yml"
16
+
17
+ say "Copying blob rendering partial to app/views/active_storage/blobs/_blob.html.erb"
18
+ copy_file "#{__dir__}/../../app/views/active_storage/blobs/_blob.html.erb",
19
+ "app/views/active_storage/blobs/_blob.html.erb"
20
+
21
+ say "Installing JavaScript dependencies"
22
+ run "yarn add #{JS_DEPENDENCIES.map { |name, version| "#{name}@#{version}" }.join(" ")}"
23
+
24
+ if APPLICATION_PACK_PATH.exist?
25
+ JS_DEPENDENCIES.keys.each do |name|
26
+ line = %[require("#{name}")]
27
+ unless APPLICATION_PACK_PATH.read.include? line
28
+ say "Adding #{name} to #{APPLICATION_PACK_PATH}"
29
+ append_to_file APPLICATION_PACK_PATH, "#{line}\n"
30
+ end
31
+ end
32
+ end
data/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@rails/actiontext",
3
+ "version": "6.0.0-beta1",
4
+ "description": "Edit and display rich text in Rails applications",
5
+ "main": "app/javascript/actiontext/index.js",
6
+ "files": [
7
+ "app/javascript/actiontext/*.js"
8
+ ],
9
+ "homepage": "http://rubyonrails.org/",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/rails/rails.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/rails/rails/issues"
16
+ },
17
+ "author": "Basecamp, LLC",
18
+ "contributors": [
19
+ "Javan Makhmali <javan@javan.us>",
20
+ "Sam Stephenson <sstephenson@gmail.com>"
21
+ ],
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "@rails/activestorage": "^6.0.0-alpha"
25
+ },
26
+ "peerDependencies": {
27
+ "trix": "^1.0.0"
28
+ }
29
+ }
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: actiontext
3
+ version: !ruby/object:Gem::Version
4
+ version: 6.0.0.beta1
5
+ platform: ruby
6
+ authors:
7
+ - Javan Makhmali
8
+ - Sam Stephenson
9
+ - David Heinemeier Hansson
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2019-01-18 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 6.0.0.beta1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - '='
27
+ - !ruby/object:Gem::Version
28
+ version: 6.0.0.beta1
29
+ - !ruby/object:Gem::Dependency
30
+ name: activerecord
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - '='
34
+ - !ruby/object:Gem::Version
35
+ version: 6.0.0.beta1
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - '='
41
+ - !ruby/object:Gem::Version
42
+ version: 6.0.0.beta1
43
+ - !ruby/object:Gem::Dependency
44
+ name: activestorage
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - '='
48
+ - !ruby/object:Gem::Version
49
+ version: 6.0.0.beta1
50
+ type: :runtime
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - '='
55
+ - !ruby/object:Gem::Version
56
+ version: 6.0.0.beta1
57
+ - !ruby/object:Gem::Dependency
58
+ name: actionpack
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - '='
62
+ - !ruby/object:Gem::Version
63
+ version: 6.0.0.beta1
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - '='
69
+ - !ruby/object:Gem::Version
70
+ version: 6.0.0.beta1
71
+ - !ruby/object:Gem::Dependency
72
+ name: nokogiri
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 1.8.5
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 1.8.5
85
+ description: Edit and display rich text in Rails applications.
86
+ email:
87
+ - javan@javan.us
88
+ - sstephenson@gmail.com
89
+ - david@loudthinking.com
90
+ executables: []
91
+ extensions: []
92
+ extra_rdoc_files: []
93
+ files:
94
+ - CHANGELOG.md
95
+ - MIT-LICENSE
96
+ - README.md
97
+ - app/helpers/action_text/content_helper.rb
98
+ - app/helpers/action_text/tag_helper.rb
99
+ - app/javascript/actiontext/attachment_upload.js
100
+ - app/javascript/actiontext/index.js
101
+ - app/models/action_text/rich_text.rb
102
+ - app/views/action_text/attachables/_missing_attachable.html.erb
103
+ - app/views/action_text/attachables/_remote_image.html.erb
104
+ - app/views/action_text/attachment_galleries/_attachment_gallery.html.erb
105
+ - app/views/action_text/content/_layout.html.erb
106
+ - app/views/active_storage/blobs/_blob.html.erb
107
+ - db/migrate/201805281641_create_action_text_tables.rb
108
+ - lib/action_text.rb
109
+ - lib/action_text/attachable.rb
110
+ - lib/action_text/attachables/content_attachment.rb
111
+ - lib/action_text/attachables/missing_attachable.rb
112
+ - lib/action_text/attachables/remote_image.rb
113
+ - lib/action_text/attachment.rb
114
+ - lib/action_text/attachment_gallery.rb
115
+ - lib/action_text/attachments/caching.rb
116
+ - lib/action_text/attachments/minification.rb
117
+ - lib/action_text/attachments/trix_conversion.rb
118
+ - lib/action_text/attribute.rb
119
+ - lib/action_text/content.rb
120
+ - lib/action_text/engine.rb
121
+ - lib/action_text/fragment.rb
122
+ - lib/action_text/gem_version.rb
123
+ - lib/action_text/html_conversion.rb
124
+ - lib/action_text/plain_text_conversion.rb
125
+ - lib/action_text/serialization.rb
126
+ - lib/action_text/trix_attachment.rb
127
+ - lib/action_text/version.rb
128
+ - lib/tasks/actiontext.rake
129
+ - lib/templates/actiontext.scss
130
+ - lib/templates/fixtures.yml
131
+ - lib/templates/installer.rb
132
+ - package.json
133
+ homepage: https://rubyonrails.org
134
+ licenses:
135
+ - MIT
136
+ metadata:
137
+ source_code_uri: https://github.com/rails/rails/tree/v6.0.0.beta1/actiontext
138
+ changelog_uri: https://github.com/rails/rails/blob/v6.0.0.beta1/actiontext/CHANGELOG.md
139
+ post_install_message:
140
+ rdoc_options: []
141
+ require_paths:
142
+ - lib
143
+ required_ruby_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: 2.5.0
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">"
151
+ - !ruby/object:Gem::Version
152
+ version: 1.3.1
153
+ requirements: []
154
+ rubygems_version: 3.0.1
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: Rich text framework.
158
+ test_files: []