actiontext 6.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/MIT-LICENSE +21 -0
- data/README.md +9 -0
- data/app/helpers/action_text/content_helper.rb +30 -0
- data/app/helpers/action_text/tag_helper.rb +75 -0
- data/app/javascript/actiontext/attachment_upload.js +45 -0
- data/app/javascript/actiontext/index.js +10 -0
- data/app/models/action_text/rich_text.rb +29 -0
- data/app/views/action_text/attachables/_missing_attachable.html.erb +1 -0
- data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
- data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb +3 -0
- data/app/views/action_text/content/_layout.html.erb +3 -0
- data/app/views/active_storage/blobs/_blob.html.erb +14 -0
- data/db/migrate/201805281641_create_action_text_tables.rb +14 -0
- data/lib/action_text.rb +37 -0
- data/lib/action_text/attachable.rb +82 -0
- data/lib/action_text/attachables/content_attachment.rb +38 -0
- data/lib/action_text/attachables/missing_attachable.rb +13 -0
- data/lib/action_text/attachables/remote_image.rb +46 -0
- data/lib/action_text/attachment.rb +103 -0
- data/lib/action_text/attachment_gallery.rb +65 -0
- data/lib/action_text/attachments/caching.rb +16 -0
- data/lib/action_text/attachments/minification.rb +17 -0
- data/lib/action_text/attachments/trix_conversion.rb +34 -0
- data/lib/action_text/attribute.rb +48 -0
- data/lib/action_text/content.rb +132 -0
- data/lib/action_text/engine.rb +50 -0
- data/lib/action_text/fragment.rb +57 -0
- data/lib/action_text/gem_version.rb +17 -0
- data/lib/action_text/html_conversion.rb +24 -0
- data/lib/action_text/plain_text_conversion.rb +81 -0
- data/lib/action_text/serialization.rb +34 -0
- data/lib/action_text/trix_attachment.rb +92 -0
- data/lib/action_text/version.rb +10 -0
- data/lib/tasks/actiontext.rake +20 -0
- data/lib/templates/actiontext.scss +36 -0
- data/lib/templates/fixtures.yml +4 -0
- data/lib/templates/installer.rb +32 -0
- data/package.json +29 -0
- metadata +158 -0
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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$/).first
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
+
}
|
@@ -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
|