Initial import from BC3 RichText
This commit is contained in:
parent
e22ba227a6
commit
68d350ddac
|
@ -69,6 +69,7 @@ PATH
|
||||||
activetext (0.1)
|
activetext (0.1)
|
||||||
activerecord (>= 5.2.0)
|
activerecord (>= 5.2.0)
|
||||||
activestorage (>= 5.2.0)
|
activestorage (>= 5.2.0)
|
||||||
|
nokogiri
|
||||||
rails (>= 5.2.0)
|
rails (>= 5.2.0)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
|
|
|
@ -12,6 +12,7 @@ Gem::Specification.new do |s|
|
||||||
s.add_dependency "rails", ">= 5.2.0"
|
s.add_dependency "rails", ">= 5.2.0"
|
||||||
s.add_dependency "activerecord", ">= 5.2.0"
|
s.add_dependency "activerecord", ">= 5.2.0"
|
||||||
s.add_dependency "activestorage", ">= 5.2.0"
|
s.add_dependency "activestorage", ">= 5.2.0"
|
||||||
|
s.add_dependency "nokogiri"
|
||||||
|
|
||||||
s.add_development_dependency "bundler", "~> 1.15"
|
s.add_development_dependency "bundler", "~> 1.15"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,32 @@
|
||||||
require "active_record"
|
require "active_record"
|
||||||
require "active_text/engine"
|
require "active_text/engine"
|
||||||
|
require "nokogiri"
|
||||||
|
|
||||||
module ActiveText
|
module ActiveText
|
||||||
|
extend ActiveSupport::Autoload
|
||||||
|
|
||||||
|
autoload :Attachable
|
||||||
|
autoload :Attachment
|
||||||
|
autoload :Content
|
||||||
|
autoload :Fragment
|
||||||
|
autoload :HtmlConversion
|
||||||
|
autoload :PlainTextConversion
|
||||||
|
autoload :Serialization
|
||||||
|
autoload :TrixAttachment
|
||||||
|
|
||||||
|
module Attachables
|
||||||
|
extend ActiveSupport::Autoload
|
||||||
|
|
||||||
|
autoload :ContentAttachment
|
||||||
|
autoload :MissingAttachable
|
||||||
|
autoload :RemoteImage
|
||||||
|
end
|
||||||
|
|
||||||
|
module Attachments
|
||||||
|
extend ActiveSupport::Autoload
|
||||||
|
|
||||||
|
autoload :Caching
|
||||||
|
autoload :Minification
|
||||||
|
autoload :TrixConversion
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
module ActiveText
|
||||||
|
module Attachable
|
||||||
|
class << self
|
||||||
|
def from_node(node)
|
||||||
|
if attachable = attachable_from_sgid(node["sgid"])
|
||||||
|
attachable
|
||||||
|
elsif attachable = ActiveText::Attachables::ContentAttachment.from_node(node)
|
||||||
|
attachable
|
||||||
|
elsif attachable = ActiveText::Attachables::RemoteImage.from_node(node)
|
||||||
|
attachable
|
||||||
|
else
|
||||||
|
ActiveText::Attachables::MissingAttachable
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def attachable_from_sgid(sgid)
|
||||||
|
::Attachable.from_attachable_sgid(sgid)
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,36 @@
|
||||||
|
module ActiveText
|
||||||
|
module Attachables
|
||||||
|
class ContentAttachment
|
||||||
|
include ActiveModel::Model
|
||||||
|
|
||||||
|
def self.from_node(node)
|
||||||
|
if node["content-type"]
|
||||||
|
if matches = node["content-type"].match(/vnd\.rubyonrails\.(.+)\.html/)
|
||||||
|
attachment = new(name: matches[1])
|
||||||
|
attachment if attachment.valid?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_accessor :name
|
||||||
|
validates_inclusion_of :name, in: %w( horizontal-rule )
|
||||||
|
|
||||||
|
def attachable_plain_text_representation(caption)
|
||||||
|
case name
|
||||||
|
when "horizontal-rule"
|
||||||
|
" ┄ "
|
||||||
|
else
|
||||||
|
" "
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_partial_path
|
||||||
|
"active_text/attachables/content_attachment"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_trix_content_attachment_partial_path
|
||||||
|
"active_text/attachables/content_attachments/#{name.underscore}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,11 @@
|
||||||
|
module ActiveText
|
||||||
|
module Attachables
|
||||||
|
module MissingAttachable
|
||||||
|
extend ActiveModel::Naming
|
||||||
|
|
||||||
|
def self.to_partial_path
|
||||||
|
"active_text/attachables/missing_attachable"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,44 @@
|
||||||
|
module ActiveText
|
||||||
|
module Attachables
|
||||||
|
class RemoteImage
|
||||||
|
extend ActiveModel::Naming
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def from_node(node)
|
||||||
|
if node["url"] && content_type_is_image?(node["content-type"])
|
||||||
|
new(attributes_from_node(node))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def content_type_is_image?(content_type)
|
||||||
|
content_type.to_s =~ /^image(\/.+|$)/
|
||||||
|
end
|
||||||
|
|
||||||
|
def attributes_from_node(node)
|
||||||
|
{ url: node["url"],
|
||||||
|
content_type: node["content-type"],
|
||||||
|
width: node["width"],
|
||||||
|
height: node["height"] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :url, :content_type, :width, :height
|
||||||
|
|
||||||
|
def initialize(attributes = {})
|
||||||
|
@url = attributes[:url]
|
||||||
|
@content_type = attributes[:content_type]
|
||||||
|
@width = attributes[:width]
|
||||||
|
@height = attributes[:height]
|
||||||
|
end
|
||||||
|
|
||||||
|
def attachable_plain_text_representation(caption)
|
||||||
|
"[#{caption || "Image"}]"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_partial_path
|
||||||
|
"active_text/attachables/remote_image"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,101 @@
|
||||||
|
module ActiveText
|
||||||
|
class Attachment
|
||||||
|
include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
|
||||||
|
|
||||||
|
TAG_NAME = "active-text-attachment"
|
||||||
|
SELECTOR = TAG_NAME
|
||||||
|
ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable caption )
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def fragment_by_canonicalizing_attachments(content)
|
||||||
|
fragment_by_minifying_attachments(fragment_by_converting_trix_attachments(content))
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_node(node, attachable = nil)
|
||||||
|
new(node, attachable || ActiveText::Attachable.from_node(node))
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_attachables(attachables)
|
||||||
|
Array(attachables).map { |attachable| from_attachable(attachable) }.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_attachable(attachable, attributes = {})
|
||||||
|
if node = node_from_attributes(attachable.to_active_text_attributes(attributes))
|
||||||
|
new(node, attachable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_attributes(attributes, attachable = nil)
|
||||||
|
if node = node_from_attributes(attributes)
|
||||||
|
from_node(node, attachable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def node_from_attributes(attributes)
|
||||||
|
if attributes = process_attributes(attributes).presence
|
||||||
|
ActiveText::HtmlConversion.create_element(TAG_NAME, attributes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_attributes(attributes)
|
||||||
|
attributes.transform_keys { |key| key.to_s.underscore.dasherize }.slice(*ATTRIBUTES)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :node, :attachable
|
||||||
|
|
||||||
|
delegate :to_param, to: :attachable
|
||||||
|
delegate_missing_to :attachable
|
||||||
|
|
||||||
|
def initialize(node, attachable)
|
||||||
|
@node = node
|
||||||
|
@attachable = attachable
|
||||||
|
end
|
||||||
|
|
||||||
|
def caption
|
||||||
|
node_attributes["caption"].presence
|
||||||
|
end
|
||||||
|
|
||||||
|
def full_attributes
|
||||||
|
node_attributes.merge(attachable_attributes).merge(sgid_attributes)
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_full_attributes
|
||||||
|
self.class.from_attributes(full_attributes, attachable)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_plain_text
|
||||||
|
if respond_to?(:attachable_plain_text_representation)
|
||||||
|
attachable_plain_text_representation(caption)
|
||||||
|
else
|
||||||
|
caption.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_html
|
||||||
|
HtmlConversion.node_to_html(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
to_html
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect
|
||||||
|
"#<#{self.class.name} attachable=#{attachable.inspect}>"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def node_attributes
|
||||||
|
@node_attributes ||= ATTRIBUTES.map { |name| [ name.underscore, node[name] ] }.to_h.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def attachable_attributes
|
||||||
|
@attachable_attributes ||= (attachable.try(:to_active_text_attributes) || {}).stringify_keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def sgid_attributes
|
||||||
|
@sgid_attributes ||= node_attributes.slice("sgid").presence || attachable_attributes.slice("sgid")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
module ActiveText
|
||||||
|
module Attachments
|
||||||
|
module Caching
|
||||||
|
def cache_key(*args)
|
||||||
|
[self.class.name, cache_digest, *attachable.cache_key(*args)].join("/")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def cache_digest
|
||||||
|
Digest::SHA256.hexdigest(node.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,15 @@
|
||||||
|
module ActiveText
|
||||||
|
module Attachments
|
||||||
|
module Minification
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
def fragment_by_minifying_attachments(content)
|
||||||
|
Fragment.wrap(content).replace(ActiveText::Attachment::SELECTOR) do |node|
|
||||||
|
node.tap { |node| node.inner_html = "" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
module ActiveText
|
||||||
|
module Attachments
|
||||||
|
module TrixConversion
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
def fragment_by_converting_trix_attachments(content)
|
||||||
|
Fragment.wrap(content).replace(TrixAttachment::SELECTOR) do |node|
|
||||||
|
from_trix_attachment(TrixAttachment.new(node))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_trix_attachment(trix_attachment)
|
||||||
|
from_attributes(trix_attachment.attributes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_trix_attachment(content = trix_attachment_content)
|
||||||
|
attributes = full_attributes.dup
|
||||||
|
attributes["content"] = content if content
|
||||||
|
TrixAttachment.from_attributes(attributes)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def trix_attachment_content
|
||||||
|
if partial_path = attachable.try(:to_trix_content_attachment_partial_path)
|
||||||
|
ApplicationRenderer.render(partial: partial_path, object: self, as: model_name.element)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,80 @@
|
||||||
|
module ActiveText
|
||||||
|
class Content
|
||||||
|
include Serialization
|
||||||
|
|
||||||
|
attr_reader :fragment
|
||||||
|
|
||||||
|
delegate :blank?, :empty?, :present?, to: :to_s
|
||||||
|
|
||||||
|
def initialize(content = nil)
|
||||||
|
@fragment = ActiveText::Attachment.fragment_by_canonicalizing_attachments(content)
|
||||||
|
end
|
||||||
|
|
||||||
|
def links
|
||||||
|
@links ||= fragment.find_all("a[href]").map { |a| a["href"] }.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def attachments
|
||||||
|
@attachments ||= attachment_nodes.map do |node|
|
||||||
|
attachment_for_node(node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attachables
|
||||||
|
@attachables ||= attachment_nodes.map do |node|
|
||||||
|
ActiveText::Attachable.from_node(node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_attachables(attachables)
|
||||||
|
attachments = ActiveText::Attachment.from_attachables(attachables)
|
||||||
|
self.class.new([self.to_s.presence, *attachments].compact.join("\n"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_attachments(**options, &block)
|
||||||
|
fragment.replace(ActiveText::Attachment::SELECTOR) do |node|
|
||||||
|
block.call(attachment_for_node(node, **options))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_plain_text
|
||||||
|
render_attachments(with_full_attributes: false, &:to_plain_text).to_plain_text
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_trix_html
|
||||||
|
render_attachments(&:to_trix_attachment).to_html
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_html
|
||||||
|
fragment.to_html
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
to_html
|
||||||
|
end
|
||||||
|
|
||||||
|
def as_json(*)
|
||||||
|
to_html
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect
|
||||||
|
"#<#{self.class.name} #{to_s.truncate(25).inspect}>"
|
||||||
|
end
|
||||||
|
|
||||||
|
def ==(other)
|
||||||
|
if other.is_a?(self.class)
|
||||||
|
to_s == other.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def attachment_nodes
|
||||||
|
@attachment_nodes ||= fragment.find_all(ActiveText::Attachment::SELECTOR)
|
||||||
|
end
|
||||||
|
|
||||||
|
def attachment_for_node(node, with_full_attributes: true)
|
||||||
|
attachment = ActiveText::Attachment.from_node(node)
|
||||||
|
with_full_attributes ? attachment.with_full_attributes : attachment
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,55 @@
|
||||||
|
module ActiveText
|
||||||
|
class Fragment
|
||||||
|
class << self
|
||||||
|
def wrap(fragment_or_html)
|
||||||
|
case fragment_or_html
|
||||||
|
when self
|
||||||
|
fragment_or_html
|
||||||
|
when Nokogiri::HTML::DocumentFragment
|
||||||
|
new(fragment_or_html)
|
||||||
|
else
|
||||||
|
from_html(fragment_or_html)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_html(html)
|
||||||
|
new(ActiveText::HtmlConversion.fragment_for_html(html.to_s.strip))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :source
|
||||||
|
|
||||||
|
def initialize(source)
|
||||||
|
@source = source
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_all(selector)
|
||||||
|
source.css(selector)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
yield source = self.source.clone
|
||||||
|
self.class.new(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
def replace(selector)
|
||||||
|
update do |source|
|
||||||
|
source.css(selector).each do |node|
|
||||||
|
node.replace(yield(node).to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_plain_text
|
||||||
|
@plain_text ||= PlainTextConversion.node_to_plain_text(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_html
|
||||||
|
@html ||= HtmlConversion.node_to_html(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
to_html
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,22 @@
|
||||||
|
module ActiveText
|
||||||
|
module HtmlConversion
|
||||||
|
extend self
|
||||||
|
|
||||||
|
def node_to_html(node)
|
||||||
|
node.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_HTML)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fragment_for_html(html)
|
||||||
|
document.fragment(html)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_element(tag_name, attributes = {})
|
||||||
|
document.create_element(tag_name, attributes)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def document
|
||||||
|
Nokogiri::HTML::Document.new.tap { |doc| doc.encoding = "UTF-8" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,79 @@
|
||||||
|
module ActiveText
|
||||||
|
module PlainTextConversion
|
||||||
|
extend self
|
||||||
|
|
||||||
|
def node_to_plain_text(node)
|
||||||
|
remove_trailing_newlines(plain_text_for_node(node))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def plain_text_for_node(node, index = 0)
|
||||||
|
if respond_to?(plain_text_method_for_node(node), true)
|
||||||
|
send(plain_text_method_for_node(node), node, index)
|
||||||
|
else
|
||||||
|
plain_text_for_node_children(node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def plain_text_for_node_children(node)
|
||||||
|
node.children.each_with_index.map do |node, index|
|
||||||
|
plain_text_for_node(node, index)
|
||||||
|
end.compact.join("")
|
||||||
|
end
|
||||||
|
|
||||||
|
def plain_text_method_for_node(node)
|
||||||
|
:"plain_text_for_#{node.name}_node"
|
||||||
|
end
|
||||||
|
|
||||||
|
def plain_text_for_block(node, index = 0)
|
||||||
|
"#{remove_trailing_newlines(plain_text_for_node_children(node))}\n\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[ p ul ol ].each do |element|
|
||||||
|
alias_method :"plain_text_for_#{element}_node", :plain_text_for_block
|
||||||
|
end
|
||||||
|
|
||||||
|
def plain_text_for_br_node(node, index)
|
||||||
|
"\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def plain_text_for_text_node(node, index)
|
||||||
|
remove_trailing_newlines(node.text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def plain_text_for_div_node(node, index)
|
||||||
|
"#{remove_trailing_newlines(plain_text_for_node_children(node))}\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def plain_text_for_figcaption_node(node, index)
|
||||||
|
"[#{remove_trailing_newlines(plain_text_for_node_children(node))}]"
|
||||||
|
end
|
||||||
|
|
||||||
|
def plain_text_for_blockquote_node(node, index)
|
||||||
|
text = plain_text_for_block(node)
|
||||||
|
text.sub(/\A(\s*)(.+?)(\s*)\Z/m, '\1“\2”\3')
|
||||||
|
end
|
||||||
|
|
||||||
|
def plain_text_for_li_node(node, index)
|
||||||
|
bullet = bullet_for_li_node(node, index)
|
||||||
|
text = remove_trailing_newlines(plain_text_for_node_children(node))
|
||||||
|
"#{bullet} #{text}\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_trailing_newlines(text)
|
||||||
|
text.chomp("")
|
||||||
|
end
|
||||||
|
|
||||||
|
def bullet_for_li_node(node, index)
|
||||||
|
if list_node_name_for_li_node(node) == "ol"
|
||||||
|
"#{index + 1}."
|
||||||
|
else
|
||||||
|
"•"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_node_name_for_li_node(node)
|
||||||
|
node.ancestors.lazy.map(&:name).grep(/^[uo]l$/).first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
module ActiveText
|
||||||
|
module Serialization
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
def load(content)
|
||||||
|
new(content) if content
|
||||||
|
end
|
||||||
|
|
||||||
|
def dump(content)
|
||||||
|
case content
|
||||||
|
when nil
|
||||||
|
nil
|
||||||
|
when self
|
||||||
|
content.to_html
|
||||||
|
else
|
||||||
|
new(content).to_html
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Marshal compatibility
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
alias_method :_load, :load
|
||||||
|
end
|
||||||
|
|
||||||
|
def _dump(*)
|
||||||
|
self.class.dump(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,89 @@
|
||||||
|
module ActiveText
|
||||||
|
class TrixAttachment
|
||||||
|
TAG_NAME = "figure"
|
||||||
|
SELECTOR = "[data-trix-attachment]"
|
||||||
|
|
||||||
|
ATTRIBUTES = %w( sgid contentType url href filename filesize width height previewable content caption )
|
||||||
|
ATTRIBUTE_TYPES = {
|
||||||
|
"previewable" => ->(value) { value.to_s == "true" },
|
||||||
|
"filesize" => ->(value) { Integer(value.to_s) rescue value },
|
||||||
|
"width" => ->(value) { Integer(value.to_s) rescue nil },
|
||||||
|
"height" => ->(value) { Integer(value.to_s) rescue nil },
|
||||||
|
:default => ->(value) { value.to_s }
|
||||||
|
}
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def from_attributes(attributes)
|
||||||
|
attributes = process_attributes(attributes)
|
||||||
|
|
||||||
|
trix_attachment_attributes = attributes.except("caption")
|
||||||
|
trix_attributes = attributes.slice("caption")
|
||||||
|
|
||||||
|
node = ActiveText::HtmlConversion.create_element(TAG_NAME)
|
||||||
|
node["data-trix-attachment"] = JSON.generate(trix_attachment_attributes)
|
||||||
|
node["data-trix-attributes"] = JSON.generate(trix_attributes) if trix_attributes.any?
|
||||||
|
|
||||||
|
new(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def process_attributes(attributes)
|
||||||
|
typecast_attribute_values(transform_attribute_keys(attributes))
|
||||||
|
end
|
||||||
|
|
||||||
|
def transform_attribute_keys(attributes)
|
||||||
|
attributes.transform_keys { |key| key.to_s.underscore.camelize(:lower) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def typecast_attribute_values(attributes)
|
||||||
|
attributes.map do |key, value|
|
||||||
|
typecast = ATTRIBUTE_TYPES[key] || ATTRIBUTE_TYPES[:default]
|
||||||
|
[key, typecast.call(value)]
|
||||||
|
end.to_h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :node
|
||||||
|
|
||||||
|
def initialize(node)
|
||||||
|
@node = node
|
||||||
|
end
|
||||||
|
|
||||||
|
def attributes
|
||||||
|
@attributes ||= attachment_attributes.merge(composed_attributes).slice(*ATTRIBUTES)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_html
|
||||||
|
ActiveText::HtmlConversion.node_to_html(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
to_html
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def attachment_attributes
|
||||||
|
read_json_object_attribute("data-trix-attachment")
|
||||||
|
end
|
||||||
|
|
||||||
|
def composed_attributes
|
||||||
|
read_json_object_attribute("data-trix-attributes")
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_json_object_attribute(name)
|
||||||
|
read_json_attribute(name) || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_json_attribute(name)
|
||||||
|
if value = node[name]
|
||||||
|
begin
|
||||||
|
JSON.parse(value)
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "[#{self.class.name}] Couldn't parse JSON #{value} from NODE #{node.inspect}"
|
||||||
|
Rails.logger.error "[#{self.class.name}] Failed with #{e.class}: #{e.backtrace}"
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue