omg-actiontext 8.0.0.alpha3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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