omg-actiontext 8.0.0.alpha3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +42 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +13 -0
  5. data/app/assets/javascripts/actiontext.esm.js +911 -0
  6. data/app/assets/javascripts/actiontext.js +884 -0
  7. data/app/assets/javascripts/trix.js +12165 -0
  8. data/app/assets/stylesheets/trix.css +412 -0
  9. data/app/helpers/action_text/content_helper.rb +76 -0
  10. data/app/helpers/action_text/tag_helper.rb +106 -0
  11. data/app/javascript/actiontext/attachment_upload.js +62 -0
  12. data/app/javascript/actiontext/index.js +10 -0
  13. data/app/models/action_text/encrypted_rich_text.rb +11 -0
  14. data/app/models/action_text/record.rb +11 -0
  15. data/app/models/action_text/rich_text.rb +93 -0
  16. data/app/views/action_text/attachables/_content_attachment.html.erb +3 -0
  17. data/app/views/action_text/attachables/_missing_attachable.html.erb +1 -0
  18. data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
  19. data/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb +3 -0
  20. data/app/views/action_text/contents/_content.html.erb +1 -0
  21. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  22. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  23. data/db/migrate/20180528164100_create_action_text_tables.rb +25 -0
  24. data/lib/action_text/attachable.rb +156 -0
  25. data/lib/action_text/attachables/content_attachment.rb +42 -0
  26. data/lib/action_text/attachables/missing_attachable.rb +29 -0
  27. data/lib/action_text/attachables/remote_image.rb +48 -0
  28. data/lib/action_text/attachment.rb +148 -0
  29. data/lib/action_text/attachment_gallery.rb +72 -0
  30. data/lib/action_text/attachments/caching.rb +18 -0
  31. data/lib/action_text/attachments/minification.rb +19 -0
  32. data/lib/action_text/attachments/trix_conversion.rb +38 -0
  33. data/lib/action_text/attribute.rb +105 -0
  34. data/lib/action_text/content.rb +197 -0
  35. data/lib/action_text/deprecator.rb +9 -0
  36. data/lib/action_text/encryption.rb +40 -0
  37. data/lib/action_text/engine.rb +94 -0
  38. data/lib/action_text/fixture_set.rb +68 -0
  39. data/lib/action_text/fragment.rb +62 -0
  40. data/lib/action_text/gem_version.rb +19 -0
  41. data/lib/action_text/html_conversion.rb +26 -0
  42. data/lib/action_text/plain_text_conversion.rb +114 -0
  43. data/lib/action_text/rendering.rb +35 -0
  44. data/lib/action_text/serialization.rb +38 -0
  45. data/lib/action_text/system_test_helper.rb +61 -0
  46. data/lib/action_text/trix_attachment.rb +94 -0
  47. data/lib/action_text/version.rb +12 -0
  48. data/lib/action_text.rb +59 -0
  49. data/lib/generators/action_text/install/install_generator.rb +84 -0
  50. data/lib/generators/action_text/install/templates/actiontext.css +440 -0
  51. data/lib/rails/generators/test_unit/install_generator.rb +15 -0
  52. data/lib/rails/generators/test_unit/templates/fixtures.yml +4 -0
  53. data/lib/tasks/actiontext.rake +6 -0
  54. data/package.json +39 -0
  55. metadata +190 -0
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ module PlainTextConversion
7
+ extend self
8
+
9
+ def node_to_plain_text(node)
10
+ remove_trailing_newlines(plain_text_for_node(node))
11
+ end
12
+
13
+ private
14
+ def plain_text_for_node(node, index = 0)
15
+ if respond_to?(plain_text_method_for_node(node), true)
16
+ send(plain_text_method_for_node(node), node, index)
17
+ else
18
+ plain_text_for_node_children(node)
19
+ end
20
+ end
21
+
22
+ def plain_text_for_node_children(node)
23
+ texts = []
24
+ node.children.each_with_index do |child, index|
25
+ texts << plain_text_for_node(child, index)
26
+ end
27
+ texts.join
28
+ end
29
+
30
+ def plain_text_method_for_node(node)
31
+ :"plain_text_for_#{node.name}_node"
32
+ end
33
+
34
+ def plain_text_for_block(node, index = 0)
35
+ "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n\n"
36
+ end
37
+
38
+ %i[ h1 p ].each do |element|
39
+ alias_method :"plain_text_for_#{element}_node", :plain_text_for_block
40
+ end
41
+
42
+ def plain_text_for_list(node, index)
43
+ "#{break_if_nested_list(node, plain_text_for_block(node))}"
44
+ end
45
+
46
+ %i[ ul ol ].each do |element|
47
+ alias_method :"plain_text_for_#{element}_node", :plain_text_for_list
48
+ end
49
+
50
+ def plain_text_for_br_node(node, index)
51
+ "\n"
52
+ end
53
+
54
+ def plain_text_for_text_node(node, index)
55
+ remove_trailing_newlines(node.text)
56
+ end
57
+
58
+ def plain_text_for_div_node(node, index)
59
+ "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n"
60
+ end
61
+
62
+ def plain_text_for_figcaption_node(node, index)
63
+ "[#{remove_trailing_newlines(plain_text_for_node_children(node))}]"
64
+ end
65
+
66
+ def plain_text_for_blockquote_node(node, index)
67
+ text = plain_text_for_block(node)
68
+ text.sub(/\A(\s*)(.+?)(\s*)\Z/m, '\1“\2”\3')
69
+ end
70
+
71
+ def plain_text_for_li_node(node, index)
72
+ bullet = bullet_for_li_node(node, index)
73
+ text = remove_trailing_newlines(plain_text_for_node_children(node))
74
+ indentation = indentation_for_li_node(node)
75
+
76
+ "#{indentation}#{bullet} #{text}\n"
77
+ end
78
+
79
+ def remove_trailing_newlines(text)
80
+ text.chomp("")
81
+ end
82
+
83
+ def bullet_for_li_node(node, index)
84
+ if list_node_name_for_li_node(node) == "ol"
85
+ "#{index + 1}."
86
+ else
87
+ "•"
88
+ end
89
+ end
90
+
91
+ def list_node_name_for_li_node(node)
92
+ node.ancestors.lazy.map(&:name).grep(/^[uo]l$/).first
93
+ end
94
+
95
+ def indentation_for_li_node(node)
96
+ depth = list_node_depth_for_node(node)
97
+ if depth > 1
98
+ " " * (depth - 1)
99
+ end
100
+ end
101
+
102
+ def list_node_depth_for_node(node)
103
+ node.ancestors.map(&:name).grep(/^[uo]l$/).count
104
+ end
105
+
106
+ def break_if_nested_list(node, text)
107
+ if list_node_depth_for_node(node) > 0
108
+ "\n#{text}"
109
+ else
110
+ text
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "active_support/concern"
6
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
7
+
8
+ module ActionText
9
+ module Rendering # :nodoc:
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ thread_cattr_accessor :renderer, instance_accessor: false
14
+ delegate :render, to: :class
15
+ end
16
+
17
+ class_methods do
18
+ def action_controller_renderer
19
+ @action_controller_renderer ||= Class.new(ActionController::Base).renderer
20
+ end
21
+
22
+ def with_renderer(renderer)
23
+ previous_renderer = self.renderer
24
+ self.renderer = renderer
25
+ yield
26
+ ensure
27
+ self.renderer = previous_renderer
28
+ end
29
+
30
+ def render(*args, &block)
31
+ (renderer || action_controller_renderer).render_to_string(*args, &block)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ module Serialization
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def load(content)
11
+ new(content) if content
12
+ end
13
+
14
+ def dump(content)
15
+ case content
16
+ when nil
17
+ nil
18
+ when self
19
+ content.to_html
20
+ when ActionText::RichText
21
+ content.body.to_html
22
+ else
23
+ new(content).to_html
24
+ end
25
+ end
26
+ end
27
+
28
+ # Marshal compatibility
29
+
30
+ class_methods do
31
+ alias_method :_load, :load
32
+ end
33
+
34
+ def _dump(*)
35
+ self.class.dump(self)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ module SystemTestHelper
7
+ # Locates a Trix editor and fills it in with the given HTML.
8
+ #
9
+ # The editor can be found by:
10
+ # * its `id`
11
+ # * its `placeholder`
12
+ # * the text from its `label` element
13
+ # * its `aria-label`
14
+ # * the `name` of its input
15
+ #
16
+ #
17
+ # Examples:
18
+ #
19
+ # # <trix-editor id="message_content" ...></trix-editor>
20
+ # fill_in_rich_textarea "message_content", with: "Hello <em>world!</em>"
21
+ #
22
+ # # <trix-editor placeholder="Your message here" ...></trix-editor>
23
+ # fill_in_rich_textarea "Your message here", with: "Hello <em>world!</em>"
24
+ #
25
+ # # <label for="message_content">Message content</label>
26
+ # # <trix-editor id="message_content" ...></trix-editor>
27
+ # fill_in_rich_textarea "Message content", with: "Hello <em>world!</em>"
28
+ #
29
+ # # <trix-editor aria-label="Message content" ...></trix-editor>
30
+ # fill_in_rich_textarea "Message content", with: "Hello <em>world!</em>"
31
+ #
32
+ # # <input id="trix_input_1" name="message[content]" type="hidden">
33
+ # # <trix-editor input="trix_input_1"></trix-editor>
34
+ # fill_in_rich_textarea "message[content]", with: "Hello <em>world!</em>"
35
+ def fill_in_rich_textarea(locator = nil, with:)
36
+ find(:rich_textarea, locator).execute_script("this.editor.loadHTML(arguments[0])", with.to_s)
37
+ end
38
+ alias_method :fill_in_rich_text_area, :fill_in_rich_textarea
39
+ end
40
+ end
41
+
42
+ %i[rich_textarea rich_text_area].each do |rich_textarea|
43
+ Capybara.add_selector rich_textarea do
44
+ label "rich-text area"
45
+ xpath do |locator|
46
+ if locator.nil?
47
+ XPath.descendant(:"trix-editor")
48
+ else
49
+ input_located_by_name = XPath.anywhere(:input).where(XPath.attr(:name) == locator).attr(:id)
50
+ input_located_by_label = XPath.anywhere(:label).where(XPath.string.n.is(locator)).attr(:for)
51
+
52
+ XPath.descendant(:"trix-editor").where \
53
+ XPath.attr(:id).equals(locator) |
54
+ XPath.attr(:placeholder).equals(locator) |
55
+ XPath.attr(:"aria-label").equals(locator) |
56
+ XPath.attr(:input).equals(input_located_by_name) |
57
+ XPath.attr(:id).equals(input_located_by_label)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionText
6
+ class TrixAttachment
7
+ TAG_NAME = "figure"
8
+ SELECTOR = "[data-trix-attachment]"
9
+
10
+ COMPOSED_ATTRIBUTES = %w( caption presentation )
11
+ ATTRIBUTES = %w( sgid contentType url href filename filesize width height previewable content ) + COMPOSED_ATTRIBUTES
12
+ ATTRIBUTE_TYPES = {
13
+ "previewable" => ->(value) { value.to_s == "true" },
14
+ "filesize" => ->(value) { Integer(value.to_s, exception: false) || value },
15
+ "width" => ->(value) { Integer(value.to_s, exception: false) },
16
+ "height" => ->(value) { Integer(value.to_s, exception: false) },
17
+ :default => ->(value) { value.to_s }
18
+ }
19
+
20
+ class << self
21
+ def from_attributes(attributes)
22
+ attributes = process_attributes(attributes)
23
+
24
+ trix_attachment_attributes = attributes.except(*COMPOSED_ATTRIBUTES)
25
+ trix_attributes = attributes.slice(*COMPOSED_ATTRIBUTES)
26
+
27
+ node = ActionText::HtmlConversion.create_element(TAG_NAME)
28
+ node["data-trix-attachment"] = JSON.generate(trix_attachment_attributes)
29
+ node["data-trix-attributes"] = JSON.generate(trix_attributes) if trix_attributes.any?
30
+
31
+ new(node)
32
+ end
33
+
34
+ private
35
+ def process_attributes(attributes)
36
+ typecast_attribute_values(transform_attribute_keys(attributes))
37
+ end
38
+
39
+ def transform_attribute_keys(attributes)
40
+ attributes.transform_keys { |key| key.to_s.underscore.camelize(:lower) }
41
+ end
42
+
43
+ def typecast_attribute_values(attributes)
44
+ attributes.to_h do |key, value|
45
+ typecast = ATTRIBUTE_TYPES[key] || ATTRIBUTE_TYPES[:default]
46
+ [key, typecast.call(value)]
47
+ end
48
+ end
49
+ end
50
+
51
+ attr_reader :node
52
+
53
+ def initialize(node)
54
+ @node = node
55
+ end
56
+
57
+ def attributes
58
+ @attributes ||= attachment_attributes.merge(composed_attributes).slice(*ATTRIBUTES)
59
+ end
60
+
61
+ def to_html
62
+ ActionText::HtmlConversion.node_to_html(node)
63
+ end
64
+
65
+ def to_s
66
+ to_html
67
+ end
68
+
69
+ private
70
+ def attachment_attributes
71
+ read_json_object_attribute("data-trix-attachment")
72
+ end
73
+
74
+ def composed_attributes
75
+ read_json_object_attribute("data-trix-attributes")
76
+ end
77
+
78
+ def read_json_object_attribute(name)
79
+ read_json_attribute(name) || {}
80
+ end
81
+
82
+ def read_json_attribute(name)
83
+ if value = node[name]
84
+ begin
85
+ JSON.parse(value)
86
+ rescue => e
87
+ Rails.logger.error "[#{self.class.name}] Couldn't parse JSON #{value} from NODE #{node.inspect}"
88
+ Rails.logger.error "[#{self.class.name}] Failed with #{e.class}: #{e.backtrace}"
89
+ nil
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require_relative "gem_version"
6
+
7
+ module ActionText
8
+ # Returns the currently loaded version of Action Text as a `Gem::Version`.
9
+ def self.version
10
+ gem_version
11
+ end
12
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/rails"
5
+
6
+ require "action_text/version"
7
+ require "action_text/deprecator"
8
+
9
+ require "nokogiri"
10
+
11
+ # :markup: markdown
12
+ # :include: ../README.md
13
+ module ActionText
14
+ extend ActiveSupport::Autoload
15
+
16
+ autoload :Attachable
17
+ autoload :AttachmentGallery
18
+ autoload :Attachment
19
+ autoload :Attribute
20
+ autoload :Content
21
+ autoload :Encryption
22
+ autoload :Fragment
23
+ autoload :FixtureSet
24
+ autoload :HtmlConversion
25
+ autoload :PlainTextConversion
26
+ autoload :Rendering
27
+ autoload :Serialization
28
+ autoload :TrixAttachment
29
+
30
+ module Attachables
31
+ extend ActiveSupport::Autoload
32
+
33
+ autoload :ContentAttachment
34
+ autoload :MissingAttachable
35
+ autoload :RemoteImage
36
+ end
37
+
38
+ module Attachments
39
+ extend ActiveSupport::Autoload
40
+
41
+ autoload :Caching
42
+ autoload :Minification
43
+ autoload :TrixConversion
44
+ end
45
+
46
+ class << self
47
+ def html_document_class
48
+ return @html_document_class if defined?(@html_document_class)
49
+ @html_document_class =
50
+ defined?(Nokogiri::HTML5) ? Nokogiri::HTML5::Document : Nokogiri::HTML4::Document
51
+ end
52
+
53
+ def html_document_fragment_class
54
+ return @html_document_fragment_class if defined?(@html_document_fragment_class)
55
+ @html_document_fragment_class =
56
+ defined?(Nokogiri::HTML5) ? Nokogiri::HTML5::DocumentFragment : Nokogiri::HTML4::DocumentFragment
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "pathname"
6
+ require "json"
7
+
8
+ module ActionText
9
+ module Generators
10
+ class InstallGenerator < ::Rails::Generators::Base
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ def install_javascript_dependencies
14
+ say "Installing JavaScript dependencies", :green
15
+ if using_bun?
16
+ run "bun add @rails/actiontext trix"
17
+ elsif using_node?
18
+ run "yarn add @rails/actiontext trix"
19
+ end
20
+ end
21
+
22
+ def append_javascript_dependencies
23
+ destination = Pathname(destination_root)
24
+
25
+ if (application_javascript_path = destination.join("app/javascript/application.js")).exist?
26
+ insert_into_file application_javascript_path.to_s, %(\nimport "trix"\nimport "@rails/actiontext"\n)
27
+ else
28
+ say <<~INSTRUCTIONS, :green
29
+ You must import the @rails/actiontext and trix JavaScript modules in your application entrypoint.
30
+ INSTRUCTIONS
31
+ end
32
+
33
+ if (importmap_path = destination.join("config/importmap.rb")).exist?
34
+ append_to_file importmap_path.to_s, %(pin "trix"\npin "@rails/actiontext", to: "actiontext.esm.js"\n)
35
+ end
36
+ end
37
+
38
+ def create_actiontext_files
39
+ template "actiontext.css", "app/assets/stylesheets/actiontext.css"
40
+
41
+ gem_root = "#{__dir__}/../../../.."
42
+
43
+ copy_file "#{gem_root}/app/views/active_storage/blobs/_blob.html.erb",
44
+ "app/views/active_storage/blobs/_blob.html.erb"
45
+
46
+ copy_file "#{gem_root}/app/views/layouts/action_text/contents/_content.html.erb",
47
+ "app/views/layouts/action_text/contents/_content.html.erb"
48
+ end
49
+
50
+ def enable_image_processing_gem
51
+ if (gemfile_path = Pathname(destination_root).join("Gemfile")).exist?
52
+ say "Ensure image_processing gem has been enabled so image uploads will work (remember to bundle!)"
53
+ image_processing_regex = /gem ["']image_processing["']/
54
+ if File.readlines(gemfile_path).grep(image_processing_regex).any?
55
+ uncomment_lines gemfile_path, image_processing_regex
56
+ else
57
+ run "bundle add --skip-install image_processing"
58
+ end
59
+ end
60
+ end
61
+
62
+ def create_migrations
63
+ rails_command "railties:install:migrations FROM=active_storage,action_text", inline: true
64
+ end
65
+
66
+ def using_js_runtime?
67
+ @using_js_runtime ||= Pathname(destination_root).join("package.json").exist?
68
+ end
69
+
70
+ def using_bun?
71
+ # Cannot assume yarn.lock has been generated yet so we look for a file known to
72
+ # be generated by the jsbundling-rails gem
73
+ @using_bun ||= using_js_runtime? && Pathname(destination_root).join("bun.config.js").exist?
74
+ end
75
+
76
+ def using_node?
77
+ # Bun is the only runtime that _isn't_ node.
78
+ @using_node ||= using_js_runtime? && !Pathname(destination_root).join("bun.config.js").exist?
79
+ end
80
+
81
+ hook_for :test_framework
82
+ end
83
+ end
84
+ end