All Files ( 81.33% covered at 30.16 hits/line )
26 files in total.
675 relevant lines,
549 lines covered and
126 lines missed.
(
81.33%
)
# frozen_string_literal: true
- 1
require "rails-html-sanitizer"
- 1
module ActionText
- 1
module ContentHelper
- 2
mattr_accessor(:sanitizer) { Rails::Html::Sanitizer.safe_list_sanitizer.new }
- 2
mattr_accessor(:allowed_tags) { sanitizer.class.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ] }
- 2
mattr_accessor(:allowed_attributes) { sanitizer.class.allowed_attributes + ActionText::Attachment::ATTRIBUTES }
- 1
mattr_accessor(:scrubber)
- 1
def render_action_text_content(content)
- 39
sanitize_action_text_content(render_action_text_attachments(content))
end
- 1
def sanitize_action_text_content(content)
- 39
sanitizer.sanitize(content.to_html, tags: allowed_tags, attributes: allowed_attributes, scrubber: scrubber).html_safe
end
- 1
def render_action_text_attachments(content)
content.render_attachments do |attachment|
- 13
unless attachment.in?(content.gallery_attachments)
- 13
attachment.node.tap do |node|
- 13
node.inner_html = render(attachment, in_gallery: false).chomp
end
end
- 39
end.render_attachment_galleries do |attachment_gallery|
render(layout: attachment_gallery, object: attachment_gallery) do
attachment_gallery.attachments.map do |attachment|
- 6
attachment.node.inner_html = render(attachment, in_gallery: true).chomp
- 6
attachment.to_html
- 3
end.join.html_safe
- 3
end.chomp
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/object/try"
- 1
require "action_view/helpers/tags/placeholderable"
- 1
module ActionText
- 1
module TagHelper
- 2
cattr_accessor(:id, instance_accessor: false) { 0 }
# Returns a +trix-editor+ tag that instantiates the Trix JavaScript editor as well as a hidden field
# that Trix will write to on changes, so the content will be sent on form submissions.
#
# ==== Options
# * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
#
# ==== Example
#
# rich_text_area_tag "content", message.content
# # <input type="hidden" name="content" id="trix_input_post_1">
# # <trix-editor id="content" input="trix_input_post_1" class="trix-content" ...></trix-editor>
- 1
def rich_text_area_tag(name, value = nil, options = {})
- 6
options = options.symbolize_keys
- 6
options[:input] ||= "trix_input_#{ActionText::TagHelper.id += 1}"
- 6
options[:class] ||= "trix-content"
- 6
options[:data] ||= {}
- 6
options[:data][:direct_upload_url] = main_app.rails_direct_uploads_url
- 6
options[:data][:blob_url_template] = main_app.rails_service_blob_url(":signed_id", ":filename")
- 6
editor_tag = content_tag("trix-editor", "", options)
- 6
input_tag = hidden_field_tag(name, value, id: options[:input])
- 6
input_tag + editor_tag
end
end
end
- 1
module ActionView::Helpers
- 1
class Tags::ActionText < Tags::Base
- 1
include Tags::Placeholderable
- 1
delegate :dom_id, to: ActionView::RecordIdentifier
- 1
def render
- 6
options = @options.stringify_keys
- 6
add_default_name_and_id(options)
- 6
options["input"] ||= dom_id(object, [options["id"], :trix_input].compact.join("_")) if object
- 6
@template_object.rich_text_area_tag(options.delete("name"), editable_value, options)
end
- 1
def editable_value
- 6
value&.body.try(:to_trix_html)
end
end
- 1
module FormHelper
# Returns a +trix-editor+ tag that instantiates the Trix JavaScript editor as well as a hidden field
# that Trix will write to on changes, so the content will be sent on form submissions.
#
# ==== Options
# * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
#
# ==== Example
# form_with(model: @message) do |form|
# form.rich_text_area :content
# end
# # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1">
# # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor>
- 1
def rich_text_area(object_name, method, options = {})
- 6
Tags::ActionText.new(object_name, method, self, options).render
end
end
- 1
class FormBuilder
- 1
def rich_text_area(method, options = {})
- 6
@template.rich_text_area(@object_name, method, objectify_options(options))
end
end
end
# frozen_string_literal: true
- 1
module ActionText
# The RichText record holds the content produced by the Trix editor in a serialized +body+ attribute.
# It also holds all the references to the embedded files, which are stored using Active Storage.
# This record is then associated with the Active Record model the application desires to have
# rich text content using the +has_rich_text+ class method.
- 1
class RichText < ActiveRecord::Base
- 1
self.table_name = "action_text_rich_texts"
- 1
serialize :body, ActionText::Content
- 1
delegate :to_s, :nil?, to: :body
- 1
belongs_to :record, polymorphic: true, touch: true
- 1
has_many_attached :embeds
- 1
before_save do
- 11
self.embeds = body.attachables.grep(ActiveStorage::Blob).uniq if body.present?
end
- 1
def to_plain_text
- 16
body&.to_plain_text.to_s
end
- 1
delegate :blank?, :empty?, :present?, to: :to_plain_text
end
end
- 1
ActiveSupport.run_load_hooks :action_text_rich_text, ActionText::RichText
# frozen_string_literal: true
- 1
require "active_support"
- 1
require "active_support/rails"
- 1
require "nokogiri"
- 1
module ActionText
- 1
extend ActiveSupport::Autoload
- 1
autoload :Attachable
- 1
autoload :AttachmentGallery
- 1
autoload :Attachment
- 1
autoload :Attribute
- 1
autoload :Content
- 1
autoload :Fragment
- 1
autoload :HtmlConversion
- 1
autoload :PlainTextConversion
- 1
autoload :Serialization
- 1
autoload :TrixAttachment
- 1
module Attachables
- 1
extend ActiveSupport::Autoload
- 1
autoload :ContentAttachment
- 1
autoload :MissingAttachable
- 1
autoload :RemoteImage
end
- 1
module Attachments
- 1
extend ActiveSupport::Autoload
- 1
autoload :Caching
- 1
autoload :Minification
- 1
autoload :TrixConversion
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
module Attachable
- 1
extend ActiveSupport::Concern
- 1
LOCATOR_NAME = "attachable"
- 1
class << self
- 1
def from_node(node)
- 46
if attachable = attachable_from_sgid(node["sgid"])
- 13
attachable
- 33
elsif attachable = ActionText::Attachables::ContentAttachment.from_node(node)
attachable
- 33
elsif attachable = ActionText::Attachables::RemoteImage.from_node(node)
- 7
attachable
else
- 26
ActionText::Attachables::MissingAttachable
end
end
- 1
def from_attachable_sgid(sgid, options = {})
- 46
method = sgid.is_a?(Array) ? :locate_many_signed : :locate_signed
- 46
record = GlobalID::Locator.public_send(method, sgid, options.merge(for: LOCATOR_NAME))
- 44
record || raise(ActiveRecord::RecordNotFound)
end
- 1
private
- 1
def attachable_from_sgid(sgid)
- 46
from_attachable_sgid(sgid)
rescue ActiveRecord::RecordNotFound
- 33
nil
end
end
- 1
class_methods do
- 1
def from_attachable_sgid(sgid)
ActionText::Attachable.from_attachable_sgid(sgid, only: self)
end
end
- 1
def attachable_sgid
- 21
to_sgid(expires_in: nil, for: LOCATOR_NAME).to_s
end
- 1
def attachable_content_type
- 17
try(:content_type) || "application/octet-stream"
end
- 1
def attachable_filename
- 15
filename.to_s if respond_to?(:filename)
end
- 1
def attachable_filesize
- 15
try(:byte_size) || try(:filesize)
end
- 1
def attachable_metadata
- 30
try(:metadata) || {}
end
- 1
def previewable_attachable?
- 2
false
end
- 1
def as_json(*)
super.merge(attachable_sgid: attachable_sgid)
end
- 1
def to_trix_content_attachment_partial_path
- 1
to_partial_path
end
- 1
def to_rich_text_attributes(attributes = {})
attributes.dup.tap do |attrs|
- 15
attrs[:sgid] = attachable_sgid
- 15
attrs[:content_type] = attachable_content_type
- 15
attrs[:previewable] = true if previewable_attachable?
- 15
attrs[:filename] = attachable_filename
- 15
attrs[:filesize] = attachable_filesize
- 15
attrs[:width] = attachable_metadata[:width]
- 15
attrs[:height] = attachable_metadata[:height]
- 15
end.compact
end
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
module Attachables
- 1
class ContentAttachment
- 1
include ActiveModel::Model
- 1
def self.from_node(node)
- 33
if node["content-type"]
- 10
if matches = node["content-type"].match(/vnd\.rubyonrails\.(.+)\.html/)
attachment = new(name: matches[1])
attachment if attachment.valid?
end
end
end
- 1
attr_accessor :name
- 1
validates_inclusion_of :name, in: %w( horizontal-rule )
- 1
def attachable_plain_text_representation(caption)
case name
when "horizontal-rule"
" ┄ "
else
" "
end
end
- 1
def to_partial_path
"action_text/attachables/content_attachment"
end
- 1
def to_trix_content_attachment_partial_path
"action_text/attachables/content_attachments/#{name.underscore}"
end
end
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
module Attachables
- 1
module MissingAttachable
- 1
extend ActiveModel::Naming
- 1
def self.to_partial_path
- 16
"action_text/attachables/missing_attachable"
end
end
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
module Attachables
- 1
class RemoteImage
- 1
extend ActiveModel::Naming
- 1
class << self
- 1
def from_node(node)
- 33
if node["url"] && content_type_is_image?(node["content-type"])
- 7
new(attributes_from_node(node))
end
end
- 1
private
- 1
def content_type_is_image?(content_type)
- 7
content_type.to_s.match?(/^image(\/.+|$)/)
end
- 1
def attributes_from_node(node)
- 7
{ url: node["url"],
content_type: node["content-type"],
width: node["width"],
height: node["height"] }
end
end
- 1
attr_reader :url, :content_type, :width, :height
- 1
def initialize(attributes = {})
- 7
@url = attributes[:url]
- 7
@content_type = attributes[:content_type]
- 7
@width = attributes[:width]
- 7
@height = attributes[:height]
end
- 1
def attachable_plain_text_representation(caption)
- 2
"[#{caption || "Image"}]"
end
- 1
def to_partial_path
- 2
"action_text/attachables/remote_image"
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/object/try"
- 1
module ActionText
- 1
class Attachment
- 1
include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
- 1
TAG_NAME = "action-text-attachment"
- 1
SELECTOR = TAG_NAME
- 1
ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation caption )
- 1
class << self
- 1
def fragment_by_canonicalizing_attachments(content)
- 78
fragment_by_minifying_attachments(fragment_by_converting_trix_attachments(content))
end
- 1
def from_node(node, attachable = nil)
- 71
new(node, attachable || ActionText::Attachable.from_node(node))
end
- 1
def from_attachables(attachables)
- 7
Array(attachables).map { |attachable| from_attachable(attachable) }.compact
end
- 1
def from_attachable(attachable, attributes = {})
- 9
if node = node_from_attributes(attachable.to_rich_text_attributes(attributes))
- 9
new(node, attachable)
end
end
- 1
def from_attributes(attributes, attachable = nil)
- 34
if node = node_from_attributes(attributes)
- 33
from_node(node, attachable)
end
end
- 1
private
- 1
def node_from_attributes(attributes)
- 43
if attributes = process_attributes(attributes).presence
- 42
ActionText::HtmlConversion.create_element(TAG_NAME, attributes)
end
end
- 1
def process_attributes(attributes)
- 178
attributes.transform_keys { |key| key.to_s.underscore.dasherize }.slice(*ATTRIBUTES)
end
end
- 1
attr_reader :node, :attachable
- 1
delegate :to_param, to: :attachable
- 1
delegate_missing_to :attachable
- 1
def initialize(node, attachable)
- 80
@node = node
- 80
@attachable = attachable
end
- 1
def caption
- 14
node_attributes["caption"].presence
end
- 1
def full_attributes
- 34
node_attributes.merge(attachable_attributes).merge(sgid_attributes)
end
- 1
def with_full_attributes
- 32
self.class.from_attributes(full_attributes, attachable)
end
- 1
def to_plain_text
- 8
if respond_to?(:attachable_plain_text_representation)
- 8
attachable_plain_text_representation(caption)
else
caption.to_s
end
end
- 1
def to_html
- 11
HtmlConversion.node_to_html(node)
end
- 1
def to_s
- 5
to_html
end
- 1
def inspect
"#<#{self.class.name} attachable=#{attachable.inspect}>"
end
- 1
private
- 1
def node_attributes
- 610
@node_attributes ||= ATTRIBUTES.map { |name| [ name.underscore, node[name] ] }.to_h.compact
end
- 1
def attachable_attributes
- 37
@attachable_attributes ||= (attachable.try(:to_rich_text_attributes) || {}).stringify_keys
end
- 1
def sgid_attributes
- 34
@sgid_attributes ||= node_attributes.slice("sgid").presence || attachable_attributes.slice("sgid")
end
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
class AttachmentGallery
- 1
include ActiveModel::Model
- 1
class << self
- 1
def fragment_by_canonicalizing_attachment_galleries(content)
- 78
fragment_by_replacing_attachment_gallery_nodes(content) do |node|
- 3
"<#{TAG_NAME}>#{node.inner_html}</#{TAG_NAME}>"
end
end
- 1
def fragment_by_replacing_attachment_gallery_nodes(content)
- 117
Fragment.wrap(content).update do |source|
- 117
find_attachment_gallery_nodes(source).each do |node|
- 6
node.replace(yield(node).to_s)
end
end
end
- 1
def find_attachment_gallery_nodes(content)
- 127
Fragment.wrap(content).find_all(SELECTOR).select do |node|
- 9
node.children.all? do |child|
- 27
if child.text?
- 9
/\A(\n|\ )*\z/.match?(child.text)
else
- 18
child.matches? ATTACHMENT_SELECTOR
end
end
end
end
- 1
def from_node(node)
- 6
new(node)
end
end
- 1
attr_reader :node
- 1
def initialize(node)
- 6
@node = node
end
- 1
def attachments
- 9
@attachments ||= node.css(ATTACHMENT_SELECTOR).map do |node|
- 12
ActionText::Attachment.from_node(node).with_full_attributes
end
end
- 1
def size
- 3
attachments.size
end
- 1
def inspect
"#<#{self.class.name} size=#{size.inspect}>"
end
- 1
TAG_NAME = "div"
- 1
ATTACHMENT_SELECTOR = "#{ActionText::Attachment::SELECTOR}[presentation=gallery]"
- 1
SELECTOR = "#{TAG_NAME}:has(#{ATTACHMENT_SELECTOR} + #{ATTACHMENT_SELECTOR})"
- 1
private_constant :TAG_NAME, :ATTACHMENT_SELECTOR, :SELECTOR
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
module Attachments
- 1
module Caching
- 1
def cache_key(*args)
[self.class.name, cache_digest, *attachable.cache_key(*args)].join("/")
end
- 1
private
- 1
def cache_digest
Digest::SHA256.hexdigest(node.to_s)
end
end
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
module Attachments
- 1
module Minification
- 1
extend ActiveSupport::Concern
- 1
class_methods do
- 1
def fragment_by_minifying_attachments(content)
- 78
Fragment.wrap(content).replace(ActionText::Attachment::SELECTOR) do |node|
- 62
node.tap { |n| n.inner_html = "" }
end
end
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/object/try"
- 1
module ActionText
- 1
module Attachments
- 1
module TrixConversion
- 1
extend ActiveSupport::Concern
- 1
class_methods do
- 1
def fragment_by_converting_trix_attachments(content)
- 78
Fragment.wrap(content).replace(TrixAttachment::SELECTOR) do |node|
- 2
from_trix_attachment(TrixAttachment.new(node))
end
end
- 1
def from_trix_attachment(trix_attachment)
- 2
from_attributes(trix_attachment.attributes)
end
end
- 1
def to_trix_attachment(content = trix_attachment_content)
- 2
attributes = full_attributes.dup
- 2
attributes["content"] = content if content
- 2
TrixAttachment.from_attributes(attributes)
end
- 1
private
- 1
def trix_attachment_content
- 2
if partial_path = attachable.try(:to_trix_content_attachment_partial_path)
- 1
ActionText::Content.renderer.render(partial: partial_path, object: self, as: model_name.element)
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
module Attribute
- 1
extend ActiveSupport::Concern
- 1
class_methods do
# Provides access to a dependent RichText model that holds the body and attachments for a single named rich text attribute.
# This dependent attribute is lazily instantiated and will be auto-saved when it's been changed. Example:
#
# class Message < ActiveRecord::Base
# has_rich_text :content
# end
#
# message = Message.create!(content: "<h1>Funny times!</h1>")
# message.content? #=> true
# message.content.to_s # => "<h1>Funny times!</h1>"
# message.content.to_plain_text # => "Funny times!"
#
# The dependent RichText model will also automatically process attachments links as sent via the Trix-powered editor.
# These attachments are associated with the RichText model using Active Storage.
#
# If you wish to preload the dependent RichText model, you can use the named scope:
#
# Message.all.with_rich_text_content # Avoids N+1 queries when you just want the body, not the attachments.
# Message.all.with_rich_text_content_and_embeds # Avoids N+1 queries when you just want the body and attachments.
- 1
def has_rich_text(name)
- 3
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}
rich_text_#{name} || build_rich_text_#{name}
end
def #{name}?
rich_text_#{name}.present?
end
def #{name}=(body)
self.#{name}.body = body
end
CODE
- 21
has_one :"rich_text_#{name}", -> { where(name: name) },
class_name: "ActionText::RichText", as: :record, inverse_of: :record, autosave: true, dependent: :destroy
- 3
scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") }
- 3
scope :"with_rich_text_#{name}_and_embeds", -> { includes("rich_text_#{name}": { embeds_attachments: :blob }) }
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/module/attribute_accessors_per_thread"
- 1
module ActionText
- 1
class Content
- 1
include Serialization
- 1
thread_cattr_accessor :renderer
- 1
attr_reader :fragment
- 1
delegate :blank?, :empty?, :html_safe, :present?, to: :to_html # Delegating to to_html to avoid including the layout
- 1
class << self
- 1
def fragment_by_canonicalizing_content(content)
- 78
fragment = ActionText::Attachment.fragment_by_canonicalizing_attachments(content)
- 78
fragment = ActionText::AttachmentGallery.fragment_by_canonicalizing_attachment_galleries(fragment)
- 78
fragment
end
end
- 1
def initialize(content = nil, options = {})
- 182
options.with_defaults! canonicalize: true
- 182
if options[:canonicalize]
- 78
@fragment = self.class.fragment_by_canonicalizing_content(content)
else
- 104
@fragment = ActionText::Fragment.wrap(content)
end
end
- 1
def links
- 3
@links ||= fragment.find_all("a[href]").map { |a| a["href"] }.uniq
end
- 1
def attachments
- 12
@attachments ||= attachment_nodes.map do |node|
- 7
attachment_for_node(node)
end
end
- 1
def attachment_galleries
- 10
@attachment_galleries ||= attachment_gallery_nodes.map do |node|
- 3
attachment_gallery_for_node(node)
end
end
- 1
def gallery_attachments
- 13
@gallery_attachments ||= attachment_galleries.flat_map(&:attachments)
end
- 1
def attachables
- 11
@attachables ||= attachment_nodes.map do |node|
- 7
ActionText::Attachable.from_node(node)
end
end
- 1
def append_attachables(attachables)
- 3
attachments = ActionText::Attachment.from_attachables(attachables)
- 3
self.class.new([self.to_s.presence, *attachments].compact.join("\n"))
end
- 1
def render_attachments(**options, &block)
- 65
content = fragment.replace(ActionText::Attachment::SELECTOR) do |node|
- 19
block.call(attachment_for_node(node, **options))
end
- 65
self.class.new(content, canonicalize: false)
end
- 1
def render_attachment_galleries(&block)
- 39
content = ActionText::AttachmentGallery.fragment_by_replacing_attachment_gallery_nodes(fragment) do |node|
- 3
block.call(attachment_gallery_for_node(node))
end
- 39
self.class.new(content, canonicalize: false)
end
- 1
def to_plain_text
- 26
render_attachments(with_full_attributes: false, &:to_plain_text).fragment.to_plain_text
end
- 1
def to_trix_html
render_attachments(&:to_trix_attachment).to_html
end
- 1
def to_html
- 98
fragment.to_html
end
- 1
def to_rendered_html_with_layout
- 39
renderer.render(partial: "action_text/content/layout", locals: { content: self })
end
- 1
def to_s
- 39
to_rendered_html_with_layout
end
- 1
def as_json(*)
to_html
end
- 1
def inspect
"#<#{self.class.name} #{to_s.truncate(25).inspect}>"
end
- 1
def ==(other)
- 42
if other.is_a?(self.class)
- 10
to_s == other.to_s
end
end
- 1
private
- 1
def attachment_nodes
- 19
@attachment_nodes ||= fragment.find_all(ActionText::Attachment::SELECTOR)
end
- 1
def attachment_gallery_nodes
- 10
@attachment_gallery_nodes ||= ActionText::AttachmentGallery.find_attachment_gallery_nodes(fragment)
end
- 1
def attachment_for_node(node, with_full_attributes: true)
- 26
attachment = ActionText::Attachment.from_node(node)
- 26
with_full_attributes ? attachment.with_full_attributes : attachment
end
- 1
def attachment_gallery_for_node(node)
- 6
ActionText::AttachmentGallery.from_node(node)
end
end
end
- 1
ActiveSupport.run_load_hooks :action_text_content, ActionText::Content
# frozen_string_literal: true
- 1
require "rails"
- 1
require "action_controller/railtie"
- 1
require "active_record/railtie"
- 1
require "active_storage/engine"
- 1
require "action_text"
- 1
module ActionText
- 1
class Engine < Rails::Engine
- 1
isolate_namespace ActionText
- 1
config.eager_load_namespaces << ActionText
- 1
initializer "action_text.attribute" do
- 1
ActiveSupport.on_load(:active_record) do
- 1
include ActionText::Attribute
end
end
- 1
initializer "action_text.attachable" do
- 1
ActiveSupport.on_load(:active_storage_blob) do
- 1
include ActionText::Attachable
- 1
def previewable_attachable?
- 13
representable?
end
- 1
def attachable_plain_text_representation(caption = nil)
- 6
"[#{caption || filename}]"
end
- 1
def to_trix_content_attachment_partial_path
nil
end
end
end
- 1
initializer "action_text.helper" do
- 1
ActiveSupport.on_load(:action_controller_base) do
- 1
helper ActionText::Engine.helpers
end
end
- 1
initializer "action_text.renderer" do |app|
- 4
app.executor.to_run { ActionText::Content.renderer = ApplicationController.renderer }
- 4
app.executor.to_complete { ActionText::Content.renderer = ApplicationController.renderer }
- 1
ActiveSupport.on_load(:action_text_content) do
- 1
self.renderer = ApplicationController.renderer
end
- 1
ActiveSupport.on_load(:action_controller_base) do
- 1
before_action { ActionText::Content.renderer = ApplicationController.renderer.new(request.env) }
end
end
- 1
initializer "action_text.system_test_helper" do
- 1
ActiveSupport.on_load(:action_dispatch_system_test_case) do
require "action_text/system_test_helper"
include ActionText::SystemTestHelper
end
end
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
class Fragment
- 1
class << self
- 1
def wrap(fragment_or_html)
- 505
case fragment_or_html
when self
- 310
fragment_or_html
when Nokogiri::HTML::DocumentFragment
- 117
new(fragment_or_html)
else
- 78
from_html(fragment_or_html)
end
end
- 1
def from_html(html)
- 78
new(ActionText::HtmlConversion.fragment_for_html(html.to_s.strip))
end
end
- 1
attr_reader :source
- 1
def initialize(source)
- 533
@source = source
end
- 1
def find_all(selector)
- 147
source.css(selector)
end
- 1
def update
- 338
yield source = self.source.clone
- 338
self.class.new(source)
end
- 1
def replace(selector)
- 221
update do |source|
- 221
source.css(selector).each do |node|
- 52
node.replace(yield(node).to_s)
end
end
end
- 1
def to_plain_text
- 26
@plain_text ||= PlainTextConversion.node_to_plain_text(source)
end
- 1
def to_html
- 98
@html ||= HtmlConversion.node_to_html(source)
end
- 1
def to_s
to_html
end
end
end
# frozen_string_literal: true
module ActionText
# Returns the currently-loaded version of Action Text as a <tt>Gem::Version</tt>.
def self.gem_version
Gem::Version.new VERSION::STRING
end
module VERSION
MAJOR = 6
MINOR = 1
TINY = 0
PRE = "alpha"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
module HtmlConversion
- 1
extend self
- 1
def node_to_html(node)
- 84
node.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_HTML)
end
- 1
def fragment_for_html(html)
- 78
document.fragment(html)
end
- 1
def create_element(tag_name, attributes = {})
- 57
document.create_element(tag_name, attributes)
end
- 1
private
- 1
def document
- 270
Nokogiri::HTML::Document.new.tap { |doc| doc.encoding = "UTF-8" }
end
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
module PlainTextConversion
- 1
extend self
- 1
def node_to_plain_text(node)
- 26
remove_trailing_newlines(plain_text_for_node(node))
end
- 1
private
- 1
def plain_text_for_node(node, index = 0)
- 1124
if respond_to?(plain_text_method_for_node(node), true)
- 1096
send(plain_text_method_for_node(node), node, index)
else
- 28
plain_text_for_node_children(node)
end
end
- 1
def plain_text_for_node_children(node)
- 1066
texts = []
- 1066
node.children.each_with_index do |child, index|
- 1098
texts << plain_text_for_node(child, index)
end
- 1066
texts.join
end
- 1
def plain_text_method_for_node(node)
- 2220
:"plain_text_for_#{node.name}_node"
end
- 1
def plain_text_for_block(node, index = 0)
- 18
"#{remove_trailing_newlines(plain_text_for_node_children(node))}\n\n"
end
- 1
%i[ h1 p ul ol ].each do |element|
- 4
alias_method :"plain_text_for_#{element}_node", :plain_text_for_block
end
- 1
def plain_text_for_br_node(node, index)
- 4
"\n"
end
- 1
def plain_text_for_text_node(node, index)
- 54
remove_trailing_newlines(node.text)
end
- 1
def plain_text_for_div_node(node, index)
- 1009
"#{remove_trailing_newlines(plain_text_for_node_children(node))}\n"
end
- 1
def plain_text_for_figcaption_node(node, index)
- 1
"[#{remove_trailing_newlines(plain_text_for_node_children(node))}]"
end
- 1
def plain_text_for_blockquote_node(node, index)
- 2
text = plain_text_for_block(node)
- 2
text.sub(/\A(\s*)(.+?)(\s*)\Z/m, '\1“\2”\3')
end
- 1
def plain_text_for_li_node(node, index)
- 10
bullet = bullet_for_li_node(node, index)
- 10
text = remove_trailing_newlines(plain_text_for_node_children(node))
- 10
"#{bullet} #{text}\n"
end
- 1
def remove_trailing_newlines(text)
- 1118
text.chomp("")
end
- 1
def bullet_for_li_node(node, index)
- 10
if list_node_name_for_li_node(node) == "ol"
- 2
"#{index + 1}."
else
- 8
"•"
end
end
- 1
def list_node_name_for_li_node(node)
- 10
node.ancestors.lazy.map(&:name).grep(/^[uo]l$/).first
end
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
module Serialization
- 1
extend ActiveSupport::Concern
- 1
class_methods do
- 1
def load(content)
- 136
new(content) if content
end
- 1
def dump(content)
- 42
case content
when nil
nil
when self
- 32
content.to_html
else
- 10
new(content).to_html
end
end
end
# Marshal compatibility
- 1
class_methods do
- 1
alias_method :_load, :load
end
- 1
def _dump(*)
- 1
self.class.dump(self)
end
end
end
# frozen_string_literal: true
module ActionText
module SystemTestHelper
# Locates a Trix editor and fills it in with the given HTML.
#
# The editor can be found by:
# * its +id+
# * its +placeholder+
# * its +aria-label+
# * the +name+ of its input
#
# Examples:
#
# # <trix-editor id="message_content" ...></trix-editor>
# fill_in_rich_text_area "message_content", with: "Hello <em>world!</em>"
#
# # <trix-editor placeholder="Your message here" ...></trix-editor>
# fill_in_rich_text_area "Your message here", with: "Hello <em>world!</em>"
#
# # <trix-editor aria-label="Message content" ...></trix-editor>
# fill_in_rich_text_area "Message content", with: "Hello <em>world!</em>"
#
# # <input id="trix_input_1" name="message[content]" type="hidden">
# # <trix-editor input="trix_input_1"></trix-editor>
# fill_in_rich_text_area "message[content]", with: "Hello <em>world!</em>"
def fill_in_rich_text_area(locator = nil, with:)
find(:rich_text_area, locator).execute_script("this.editor.loadHTML(arguments[0])", with.to_s)
end
end
end
Capybara.add_selector :rich_text_area do
label "rich-text area"
xpath do |locator|
if locator.nil?
XPath.descendant(:"trix-editor")
else
input_located_by_name = XPath.anywhere(:input).where(XPath.attr(:name) == locator).attr(:id)
XPath.descendant(:"trix-editor").where \
XPath.attr(:id).equals(locator) |
XPath.attr(:placeholder).equals(locator) |
XPath.attr(:"aria-label").equals(locator) |
XPath.attr(:input).equals(input_located_by_name)
end
end
end
# frozen_string_literal: true
- 1
module ActionText
- 1
class TrixAttachment
- 1
TAG_NAME = "figure"
- 1
SELECTOR = "[data-trix-attachment]"
- 1
COMPOSED_ATTRIBUTES = %w( caption presentation )
- 1
ATTRIBUTES = %w( sgid contentType url href filename filesize width height previewable content ) + COMPOSED_ATTRIBUTES
- 1
ATTRIBUTE_TYPES = {
- 8
"previewable" => ->(value) { value.to_s == "true" },
- 7
"filesize" => ->(value) { Integer(value.to_s) rescue value },
"width" => ->(value) { Integer(value.to_s) rescue nil },
"height" => ->(value) { Integer(value.to_s) rescue nil },
- 15
:default => ->(value) { value.to_s }
}
- 1
class << self
- 1
def from_attributes(attributes)
- 15
attributes = process_attributes(attributes)
- 15
trix_attachment_attributes = attributes.except(*COMPOSED_ATTRIBUTES)
- 15
trix_attributes = attributes.slice(*COMPOSED_ATTRIBUTES)
- 15
node = ActionText::HtmlConversion.create_element(TAG_NAME)
- 15
node["data-trix-attachment"] = JSON.generate(trix_attachment_attributes)
- 15
node["data-trix-attributes"] = JSON.generate(trix_attributes) if trix_attributes.any?
- 15
new(node)
end
- 1
private
- 1
def process_attributes(attributes)
- 15
typecast_attribute_values(transform_attribute_keys(attributes))
end
- 1
def transform_attribute_keys(attributes)
- 45
attributes.transform_keys { |key| key.to_s.underscore.camelize(:lower) }
end
- 1
def typecast_attribute_values(attributes)
attributes.map do |key, value|
- 30
typecast = ATTRIBUTE_TYPES[key] || ATTRIBUTE_TYPES[:default]
- 30
[key, typecast.call(value)]
- 15
end.to_h
end
end
- 1
attr_reader :node
- 1
def initialize(node)
- 17
@node = node
end
- 1
def attributes
- 23
@attributes ||= attachment_attributes.merge(composed_attributes).slice(*ATTRIBUTES)
end
- 1
def to_html
ActionText::HtmlConversion.node_to_html(node)
end
- 1
def to_s
to_html
end
- 1
private
- 1
def attachment_attributes
- 16
read_json_object_attribute("data-trix-attachment")
end
- 1
def composed_attributes
- 16
read_json_object_attribute("data-trix-attributes")
end
- 1
def read_json_object_attribute(name)
- 32
read_json_attribute(name) || {}
end
- 1
def read_json_attribute(name)
- 32
if value = node[name]
- 19
begin
- 19
JSON.parse(value)
- 1
rescue => e
- 1
Rails.logger.error "[#{self.class.name}] Couldn't parse JSON #{value} from NODE #{node.inspect}"
- 1
Rails.logger.error "[#{self.class.name}] Failed with #{e.class}: #{e.backtrace}"
- 1
nil
end
end
end
end
end
# frozen_string_literal: true
require_relative "gem_version"
module ActionText
# Returns the currently-loaded version of Action Text as a <tt>Gem::Version</tt>.
def self.version
gem_version
end
end
# frozen_string_literal: true
require "pathname"
require "json"
module ActionText
module Generators
class InstallGenerator < ::Rails::Generators::Base
source_root File.expand_path("templates", __dir__)
def install_javascript_dependencies
rails_command "app:binstub:yarn", inline: true
say "Installing JavaScript dependencies", :green
run "bin/yarn add #{js_dependencies.map { |name, version| "#{name}@#{version}" }.join(" ")}",
abort_on_failure: true, capture: true
end
def append_dependencies_to_package_file
if (app_javascript_pack_path = Pathname.new("app/javascript/packs/application.js")).exist?
js_dependencies.each_key do |dependency|
line = %[require("#{dependency}")]
unless app_javascript_pack_path.read.include? line
say "Adding #{dependency} to #{app_javascript_pack_path}", :green
append_to_file app_javascript_pack_path, "\n#{line}"
end
end
else
say <<~WARNING, :red
WARNING: Action Text can't locate your JavaScript bundle to add its package dependencies.
Add these lines to any bundles:
require("trix")
require("@rails/actiontext")
Alternatively, install and setup the webpacker gem then rerun `bin/rails action_text:install`
to have these dependencies added automatically.
WARNING
end
end
def create_actiontext_files
template "actiontext.scss", "app/assets/stylesheets/actiontext.scss"
copy_file "#{GEM_ROOT}/app/views/active_storage/blobs/_blob.html.erb",
"app/views/active_storage/blobs/_blob.html.erb"
end
def create_migrations
rails_command "railties:install:migrations FROM=active_storage,action_text", inline: true
end
hook_for :test_framework
private
GEM_ROOT = "#{__dir__}/../../../.."
def js_dependencies
js_package = JSON.load(Pathname.new("#{GEM_ROOT}/package.json"))
js_package["peerDependencies"].merge \
js_package["name"] => "^#{js_package["version"]}"
end
end
end
end
# frozen_string_literal: true
module TestUnit
module Generators
class InstallGenerator < ::Rails::Generators::Base
source_root File.expand_path("templates", __dir__)
def create_test_files
template "fixtures.yml", "test/fixtures/action_text/rich_texts.yml"
end
end
end
end