Import Action Text

This commit is contained in:
George Claghorn 2019-01-04 19:43:11 -05:00
commit 0decd2ddc4
144 changed files with 9447 additions and 5 deletions

View File

@ -9,6 +9,7 @@ AllCops:
- 'actionpack/lib/action_dispatch/journey/parser.rb'
- 'railties/test/fixtures/tmp/**/*'
- 'actionmailbox/test/dummy/**/*'
- 'actiontext/test/dummy/**/*'
- 'node_modules/**/*'
Performance:
@ -135,6 +136,7 @@ Style/FrozenStringLiteralComment:
- 'actionpack/test/**/*.ruby'
- 'activestorage/db/migrate/**/*.rb'
- 'actionmailbox/db/migrate/**/*.rb'
- 'actiontext/db/migrate/**/*.rb'
Style/RedundantFreeze:
Enabled: true

View File

@ -57,7 +57,7 @@ env:
- "JRUBY_OPTS='--dev -J-Xmx1024M'"
matrix:
- "GEM=actionpack,actioncable"
- "GEM=actionmailer,activemodel,activesupport,actionview,activejob,activestorage,actionmailbox"
- "GEM=actionmailer,activemodel,activesupport,actionview,activejob,activestorage,actionmailbox,actiontext"
- "GEM=activesupport PRESERVE_TIMEZONES=1"
- "GEM=activerecord:sqlite3"
- "GEM=guides"

View File

@ -51,6 +51,12 @@ PATH
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actiontext (6.0.0.alpha)
actionpack (= 6.0.0.alpha)
activerecord (= 6.0.0.alpha)
activestorage (= 6.0.0.alpha)
activesupport (= 6.0.0.alpha)
nokogiri (>= 1.8.5)
actionview (6.0.0.alpha)
activesupport (= 6.0.0.alpha)
builder (~> 3.1)
@ -79,6 +85,7 @@ PATH
actionmailbox (= 6.0.0.alpha)
actionmailer (= 6.0.0.alpha)
actionpack (= 6.0.0.alpha)
actiontext (= 6.0.0.alpha)
actionview (= 6.0.0.alpha)
activejob (= 6.0.0.alpha)
activemodel (= 6.0.0.alpha)

5
actiontext/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/test/dummy/db/*.sqlite3
/test/dummy/db/*.sqlite3-journal
/test/dummy/log/*.log
/test/dummy/tmp/
/tmp/

3
actiontext/CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
* Added to Rails.
*DHH*

21
actiontext/MIT-LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Basecamp, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

67
actiontext/README.md Normal file
View File

@ -0,0 +1,67 @@
# Action Text
Action Text brings rich text content and editing to Rails. It includes the [Trix editor](https://trix-editor.org/) that handles everything from formatting to links to quotes to lists to embedded images and galleries. The rich text content generated by the Trix editor is saved in its own RichText model that's associated with any existing Active Record model in the application. Any embedded images (or other attachments) are automatically stored using Active Storage and associated with the included RichText model.
## Trix compared to other rich text editors
Most WYSIWYG editors are wrappers around HTMLs `contenteditable` and `execCommand` APIs, designed by Microsoft to support live editing of web pages in Internet Explorer 5.5, and [eventually reverse-engineered](https://blog.whatwg.org/the-road-to-html-5-contenteditable#history) and copied by other browsers.
Because these APIs were never fully specified or documented, and because WYSIWYG HTML editors are enormous in scope, each browsers implementation has its own set of bugs and quirks, and JavaScript developers are left to resolve the inconsistencies.
Trix sidesteps these inconsistencies by treating contenteditable as an I/O device: when input makes its way to the editor, Trix converts that input into an editing operation on its internal document model, then re-renders that document back into the editor. This gives Trix complete control over what happens after every keystroke, and avoids the need to use execCommand at all.
## Installation
Run `rails action_text:install` to add the Yarn package and copy over the necessary migration.
## Examples
Adding a rich text field to an existing model:
```ruby
# app/models/message.rb
class Message < ApplicationRecord
has_rich_text :content
end
```
Then refer to this field in the form for the model:
```erb
<%# app/views/messages/_form.html.erb %>
<%= form_with(model: message) do |form| %>
<div class="field">
<%= form.label :content %>
<%= form.rich_text_area :content %>
</div>
<% end %>
```
And finally display the sanitized rich text on a page:
```erb
<%= @message.content %>
```
To accept the rich text content, all you have to do is permit the referenced attribute:
```ruby
class MessagesController < ApplicationController
def create
message = Message.create! params.require(:message).permit(:title, :content)
redirect_to message
end
end
```
## Custom styling
By default, the Action Text editor and content is styled by the Trix defaults. If you want to change these defaults, you'll want to remove the `app/assets/stylesheets/actiontext.css` linker and base your stylings on the [contents of that file](https://raw.githubusercontent.com/basecamp/trix/master/dist/trix.css).
You can also style the HTML used for embedded images and other attachments (known as blobs). On installation, Action Text will copy over a partial to `app/views/active_storage/blobs/_blob.html.erb`, which you can specialize.
## License
Action Text is released under the [MIT License](https://opensource.org/licenses/MIT).

13
actiontext/Rakefile Normal file
View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require "bundler/setup"
require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"
t.verbose = true
end
task default: :test

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip
# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = "actiontext"
s.version = version
s.summary = "Rich text framework."
s.description = "Edit and display rich text in Rails applications."
s.required_ruby_version = ">= 2.5.0"
s.license = "MIT"
s.authors = ["Javan Makhmali", "Sam Stephenson", "David Heinemeier Hansson"]
s.email = ["javan@javan.us", "sstephenson@gmail.com", "david@loudthinking.com"]
s.homepage = "https://rubyonrails.org"
s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"]
s.require_path = "lib"
s.metadata = {
"source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actiontext",
"changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actiontext/CHANGELOG.md"
}
# NOTE: Please read our dependency guidelines before updating versions:
# https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
s.add_dependency "activesupport", version
s.add_dependency "activerecord", version
s.add_dependency "activestorage", version
s.add_dependency "actionpack", version
s.add_dependency "nokogiri", ">= 1.8.5"
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module ActionText
module ContentHelper
SANITIZER = Rails::Html::Sanitizer.white_list_sanitizer
ALLOWED_TAGS = SANITIZER.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ]
ALLOWED_ATTRIBUTES = SANITIZER.allowed_attributes + ActionText::Attachment::ATTRIBUTES
def render_action_text_content(content)
content = content.render_attachments do |attachment|
unless attachment.in?(content.gallery_attachments)
attachment.node.tap do |node|
node.inner_html = render(attachment, in_gallery: false).chomp
end
end
end
content = content.render_attachment_galleries do |attachment_gallery|
render(layout: attachment_gallery, object: attachment_gallery) do
attachment_gallery.attachments.map do |attachment|
attachment.node.inner_html = render(attachment, in_gallery: true).chomp
attachment.to_html
end.join("").html_safe
end.chomp
end
sanitize content.to_html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES
end
end
end

View File

@ -0,0 +1,75 @@
# frozen_string_literal: true
module ActionText
module TagHelper
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>
def rich_text_area_tag(name, value = nil, options = {})
options = options.symbolize_keys
options[:input] ||= "trix_input_#{ActionText::TagHelper.id += 1}"
options[:class] ||= "trix-content"
options[:data] ||= {}
options[:data][:direct_upload_url] = main_app.rails_direct_uploads_url
options[:data][:blob_url_template] = main_app.rails_service_blob_url(":signed_id", ":filename")
editor_tag = content_tag("trix-editor", "", options)
input_tag = hidden_field_tag(name, value, id: options[:input])
input_tag + editor_tag
end
end
end
module ActionView::Helpers
class Tags::ActionText < Tags::Base
delegate :dom_id, to: ActionView::RecordIdentifier
def render
options = @options.stringify_keys
add_default_name_and_id(options)
options["input"] ||= dom_id(object, [options["id"], :trix_input].compact.join("_")) if object
@template_object.rich_text_area_tag(options.delete("name"), editable_value, options)
end
def editable_value
value&.body.try(:to_trix_html)
end
end
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>
def rich_text_area(object_name, method, options = {})
Tags::ActionText.new(object_name, method, self, options).render
end
end
class FormBuilder
def rich_text_area(method, options = {})
@template.rich_text_area(@object_name, method, objectify_options(options))
end
end
end

View File

@ -0,0 +1,45 @@
import { DirectUpload } from "activestorage"
export class AttachmentUpload {
constructor(attachment, element) {
this.attachment = attachment
this.element = element
this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this)
}
start() {
this.directUpload.create(this.directUploadDidComplete.bind(this))
}
directUploadWillStoreFileWithXHR(xhr) {
xhr.upload.addEventListener("progress", event => {
const progress = event.loaded / event.total * 100
this.attachment.setUploadProgress(progress)
})
}
directUploadDidComplete(error, attributes) {
if (error) {
throw new Error(`Direct upload failed: ${error}`)
}
this.attachment.setAttributes({
sgid: attributes.attachable_sgid,
url: this.createBlobUrl(attributes.signed_id, attributes.filename)
})
}
createBlobUrl(signedId, filename) {
return this.blobUrlTemplate
.replace(":signed_id", signedId)
.replace(":filename", encodeURIComponent(filename))
}
get directUploadUrl() {
return this.element.dataset.directUploadUrl
}
get blobUrlTemplate() {
return this.element.dataset.blobUrlTemplate
}
}

View File

@ -0,0 +1,11 @@
import * as Trix from "trix"
import { AttachmentUpload } from "./attachment_upload"
addEventListener("trix-attachment-add", event => {
const { attachment, target } = event
if (attachment.file) {
const upload = new AttachmentUpload(attachment, target)
upload.start()
}
})

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
# 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.
class ActionText::RichText < ActiveRecord::Base
self.table_name = "action_text_rich_texts"
serialize :body, ActionText::Content
delegate :to_s, :nil?, to: :body
belongs_to :record, polymorphic: true, touch: true
has_many_attached :embeds
before_save do
self.embeds = body.attachments.map(&:attachable) if body.present?
end
def to_plain_text
body&.to_plain_text.to_s
end
delegate :blank?, :empty?, :present?, to: :to_plain_text
end

View File

@ -0,0 +1 @@
<%= "☒" -%>

View File

@ -0,0 +1,8 @@
<figure class="attachment attachment--preview">
<%= image_tag(remote_image.url, width: remote_image.width, height: remote_image.height) %>
<% if caption = remote_image.try(:caption) %>
<figcaption class="attachment__caption">
<%= caption %>
</figcaption>
<% end %>
</figure>

View File

@ -0,0 +1,3 @@
<div class="attachment-gallery attachment-gallery--<%= attachment_gallery.size %>">
<%= yield %>
</div>

View File

@ -0,0 +1,3 @@
<div class="trix-content">
<%= render_action_text_content(content) %>
</div>

View File

@ -0,0 +1,14 @@
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
<% if blob.representable? %>
<%= image_tag blob.representation(resize_to_fit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
<% end %>
<figcaption class="attachment__caption">
<% if caption = blob.try(:caption) %>
<%= caption %>
<% else %>
<span class="attachment__name"><%= blob.filename %></span>
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
<% end %>
</figcaption>
</figure>

5
actiontext/bin/test Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
COMPONENT_ROOT = File.expand_path("..", __dir__)
require_relative "../../tools/test"

29
actiontext/bin/webpack Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'webpack' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
bundle_binstub = File.expand_path("../bundle", __FILE__)
if File.file?(bundle_binstub)
if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 150))
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("webpacker", "webpack")

View File

@ -0,0 +1,29 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'webpack-dev-server' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
bundle_binstub = File.expand_path("../bundle", __FILE__)
if File.file?(bundle_binstub)
if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 150))
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("webpacker", "webpack-dev-server")

View File

@ -0,0 +1,14 @@
class CreateActionTextTables < ActiveRecord::Migration[6.0]
def change
create_table :action_text_rich_texts do |t|
t.string :name, null: false
t.text :body, limit: 16777215
t.references :record, null: false, polymorphic: true, index: false
t.datetime :created_at, null: false
t.datetime :updated_at, null: false
t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
end
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require "active_support"
require "active_support/rails"
require "nokogiri"
module ActionText
extend ActiveSupport::Autoload
autoload :Attachable
autoload :AttachmentGallery
autoload :Attachment
autoload :Attribute
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

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
module ActionText
module Attachable
extend ActiveSupport::Concern
LOCATOR_NAME = "attachable"
class << self
def from_node(node)
if attachable = attachable_from_sgid(node["sgid"])
attachable
elsif attachable = ActionText::Attachables::ContentAttachment.from_node(node)
attachable
elsif attachable = ActionText::Attachables::RemoteImage.from_node(node)
attachable
else
ActionText::Attachables::MissingAttachable
end
end
def from_attachable_sgid(sgid, options = {})
method = sgid.is_a?(Array) ? :locate_many_signed : :locate_signed
record = GlobalID::Locator.public_send(method, sgid, options.merge(for: LOCATOR_NAME))
record || raise(ActiveRecord::RecordNotFound)
end
private
def attachable_from_sgid(sgid)
from_attachable_sgid(sgid)
rescue ActiveRecord::RecordNotFound
nil
end
end
class_methods do
def from_attachable_sgid(sgid)
ActionText::Attachable.from_attachable_sgid(sgid, only: self)
end
end
def attachable_sgid
to_sgid(expires_in: nil, for: LOCATOR_NAME).to_s
end
def attachable_content_type
try(:content_type) || "application/octet-stream"
end
def attachable_filename
filename.to_s if respond_to?(:filename)
end
def attachable_filesize
try(:byte_size) || try(:filesize)
end
def attachable_metadata
try(:metadata) || {}
end
def previewable_attachable?
false
end
def as_json(*)
super.merge(attachable_sgid: attachable_sgid)
end
def to_rich_text_attributes(attributes = {})
attributes.dup.tap do |attrs|
attrs[:sgid] = attachable_sgid
attrs[:content_type] = attachable_content_type
attrs[:previewable] = true if previewable_attachable?
attrs[:filename] = attachable_filename
attrs[:filesize] = attachable_filesize
attrs[:width] = attachable_metadata[:width]
attrs[:height] = attachable_metadata[:height]
end.compact
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module ActionText
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
"action_text/attachables/content_attachment"
end
def to_trix_content_attachment_partial_path
"action_text/attachables/content_attachments/#{name.underscore}"
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module ActionText
module Attachables
module MissingAttachable
extend ActiveModel::Naming
def self.to_partial_path
"action_text/attachables/missing_attachable"
end
end
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module ActionText
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
"action_text/attachables/remote_image"
end
end
end
end

View File

@ -0,0 +1,103 @@
# frozen_string_literal: true
module ActionText
class Attachment
include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
TAG_NAME = "action-text-attachment"
SELECTOR = TAG_NAME
ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation 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 || ActionText::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_rich_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
ActionText::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_rich_text_attributes) || {}).stringify_keys
end
def sgid_attributes
@sgid_attributes ||= node_attributes.slice("sgid").presence || attachable_attributes.slice("sgid")
end
end
end

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
module ActionText
class AttachmentGallery
include ActiveModel::Model
class << self
def fragment_by_canonicalizing_attachment_galleries(content)
fragment_by_replacing_attachment_gallery_nodes(content) do |node|
"<#{TAG_NAME}>#{node.inner_html}</#{TAG_NAME}>"
end
end
def fragment_by_replacing_attachment_gallery_nodes(content)
Fragment.wrap(content).update do |source|
find_attachment_gallery_nodes(source).each do |node|
node.replace(yield(node).to_s)
end
end
end
def find_attachment_gallery_nodes(content)
Fragment.wrap(content).find_all(SELECTOR).select do |node|
node.children.all? do |child|
if child.text?
child.text =~ /\A(\n|\ )*\z/
else
child.matches? ATTACHMENT_SELECTOR
end
end
end
end
def from_node(node)
new(node)
end
end
attr_reader :node
def initialize(node)
@node = node
end
def attachments
@attachments ||= node.css(ATTACHMENT_SELECTOR).map do |node|
ActionText::Attachment.from_node(node).with_full_attributes
end
end
def size
attachments.size
end
def inspect
"#<#{self.class.name} size=#{size.inspect}>"
end
TAG_NAME = "div"
ATTACHMENT_SELECTOR = "#{ActionText::Attachment::SELECTOR}[presentation=gallery]"
SELECTOR = "#{TAG_NAME}:has(#{ATTACHMENT_SELECTOR} + #{ATTACHMENT_SELECTOR})"
private_constant :TAG_NAME, :ATTACHMENT_SELECTOR, :SELECTOR
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module ActionText
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

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module ActionText
module Attachments
module Minification
extend ActiveSupport::Concern
class_methods do
def fragment_by_minifying_attachments(content)
Fragment.wrap(content).replace(ActionText::Attachment::SELECTOR) do |node|
node.tap { |n| n.inner_html = "" }
end
end
end
end
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
module ActionText
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)
ActionText::Content.renderer.render(partial: partial_path, object: self, as: model_name.element)
end
end
end
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
module ActionText
module Attribute
extend ActiveSupport::Concern
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.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.
def has_rich_text(name)
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}
self.rich_text_#{name} ||= ActionText::RichText.new(name: "#{name}", record: self)
end
def #{name}=(body)
self.#{name}.body = body
end
CODE
has_one :"rich_text_#{name}", -> { where(name: name) }, class_name: "ActionText::RichText", as: :record, inverse_of: :record, dependent: :destroy
scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") }
scope :"with_rich_text_#{name}_and_embeds", -> { includes("rich_text_#{name}": { embeds_attachments: :blob }) }
after_save do
public_send(name).save if public_send(name).changed?
end
end
end
end
end

View File

@ -0,0 +1,132 @@
# frozen_string_literal: true
require "active_support/core_ext/module/attribute_accessors_per_thread"
module ActionText
class Content
include Serialization
thread_cattr_accessor :renderer
attr_reader :fragment
delegate :blank?, :empty?, :html_safe, :present?, to: :to_html # Delegating to to_html to avoid including the layout
class << self
def fragment_by_canonicalizing_content(content)
fragment = ActionText::Attachment.fragment_by_canonicalizing_attachments(content)
fragment = ActionText::AttachmentGallery.fragment_by_canonicalizing_attachment_galleries(fragment)
fragment
end
end
def initialize(content = nil, options = {})
options.with_defaults! canonicalize: true
if options[:canonicalize]
@fragment = self.class.fragment_by_canonicalizing_content(content)
else
@fragment = ActionText::Fragment.wrap(content)
end
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 attachment_galleries
@attachment_galleries ||= attachment_gallery_nodes.map do |node|
attachment_gallery_for_node(node)
end
end
def gallery_attachments
@gallery_attachments ||= attachment_galleries.flat_map(&:attachments)
end
def attachables
@attachables ||= attachment_nodes.map do |node|
ActionText::Attachable.from_node(node)
end
end
def append_attachables(attachables)
attachments = ActionText::Attachment.from_attachables(attachables)
self.class.new([self.to_s.presence, *attachments].compact.join("\n"))
end
def render_attachments(**options, &block)
content = fragment.replace(ActionText::Attachment::SELECTOR) do |node|
block.call(attachment_for_node(node, **options))
end
self.class.new(content, canonicalize: false)
end
def render_attachment_galleries(&block)
content = ActionText::AttachmentGallery.fragment_by_replacing_attachment_gallery_nodes(fragment) do |node|
block.call(attachment_gallery_for_node(node))
end
self.class.new(content, canonicalize: false)
end
def to_plain_text
render_attachments(with_full_attributes: false, &:to_plain_text).fragment.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_rendered_html_with_layout
renderer.render(partial: "action_text/content/layout", locals: { content: self })
end
def to_s
to_rendered_html_with_layout
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(ActionText::Attachment::SELECTOR)
end
def attachment_gallery_nodes
@attachment_gallery_nodes ||= ActionText::AttachmentGallery.find_attachment_gallery_nodes(fragment)
end
def attachment_for_node(node, with_full_attributes: true)
attachment = ActionText::Attachment.from_node(node)
with_full_attributes ? attachment.with_full_attributes : attachment
end
def attachment_gallery_for_node(node)
ActionText::AttachmentGallery.from_node(node)
end
end
end
ActiveSupport.run_load_hooks :action_text_content, ActionText::Content

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require "rails"
require "action_controller/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_text"
module ActionText
class Engine < Rails::Engine
isolate_namespace ActionText
config.eager_load_namespaces << ActionText
initializer "action_text.attribute" do
ActiveSupport.on_load(:active_record) do
include ActionText::Attribute
end
end
initializer "action_text.attachable" do
ActiveSupport.on_load(:active_storage_blob) do
include ActionText::Attachable
def previewable_attachable?
representable?
end
end
end
initializer "action_text.helper" do
ActiveSupport.on_load(:action_controller_base) do
helper ActionText::Engine.helpers
end
end
initializer "action_text.renderer" do
ActiveSupport.on_load(:action_text_content) do
self.renderer ||= ApplicationController.renderer
end
ActiveSupport.on_load(:action_controller_base) do
before_action { ActionText::Content.renderer = ActionText::Content.renderer.new(request.env) }
end
end
end
end

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
module ActionText
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(ActionText::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

View File

@ -0,0 +1,17 @@
# 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 = 0
TINY = 0
PRE = "alpha"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module ActionText
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

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
module ActionText
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 |child, index|
plain_text_for_node(child, 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[ h1 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

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
module ActionText
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

View File

@ -0,0 +1,92 @@
# frozen_string_literal: true
module ActionText
class TrixAttachment
TAG_NAME = "figure"
SELECTOR = "[data-trix-attachment]"
COMPOSED_ATTRIBUTES = %w( caption presentation )
ATTRIBUTES = %w( sgid contentType url href filename filesize width height previewable content ) + COMPOSED_ATTRIBUTES
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(*COMPOSED_ATTRIBUTES)
trix_attributes = attributes.slice(*COMPOSED_ATTRIBUTES)
node = ActionText::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
ActionText::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

View File

@ -0,0 +1,10 @@
# 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

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
namespace :action_text do
# Prevent migration installation task from showing up twice.
Rake::Task["install:migrations"].clear_comments
desc "Copy over the migration, stylesheet, and JavaScript files"
task install: %w( environment run_installer copy_migrations )
task :run_installer do
installer_template = File.expand_path("../templates/installer.rb", __dir__)
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{installer_template}"
end
task :copy_migrations do
Rake::Task["active_storage:install:migrations"].invoke
Rake::Task["railties:install:migrations"].reenable # Otherwise you can't run 2 migration copy tasks in one invocation
Rake::Task["action_text:install:migrations"].invoke
end
end

View File

@ -0,0 +1,36 @@
//
// Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and
// the trix-editor content (whether displayed or under editing). Feel free to incorporate this
// inclusion directly in any other asset bundle and remove this file.
//
//= require trix/dist/trix
// We need to override trix.csss image gallery styles to accommodate the
// <action-text-attachment> element we wrap around attachments. Otherwise,
// images in galleries will be squished by the max-width: 33%; rule.
.trix-content {
.attachment-gallery {
> action-text-attachment,
> .attachment {
flex: 1 0 33%;
padding: 0 0.5em;
max-width: 33%;
}
&.attachment-gallery--2,
&.attachment-gallery--4 {
> action-text-attachment,
> .attachment {
flex-basis: 50%;
max-width: 50%;
}
}
}
action-text-attachment {
.attachment {
padding: 0 !important;
max-width: 100% !important;
}
}
}

View File

@ -0,0 +1,4 @@
# one:
# record: name_of_fixture (ClassOfFixture)
# name: content
# body: <p>In a <i>million</i> stars!</p>

View File

@ -0,0 +1,22 @@
say "Copying actiontext.scss to app/assets/stylesheets"
copy_file "#{__dir__}/actiontext.scss", "app/assets/stylesheets/actiontext.scss"
say "Copying fixtures to test/fixtures/action_text/rich_texts.yml"
copy_file "#{__dir__}/fixtures.yml", "test/fixtures/action_text/rich_texts.yml"
say "Copying blob rendering partial to app/views/active_storage/blobs/_blob.html.erb"
copy_file "#{__dir__}/../../app/views/active_storage/blobs/_blob.html.erb",
"app/views/active_storage/blobs/_blob.html.erb"
# FIXME: Replace with release version on release
say "Installing JavaScript dependency"
run "yarn add https://github.com/rails/actiontext"
APPLICATION_PACK_PATH = "app/javascript/packs/application.js"
if File.exists?(APPLICATION_PACK_PATH) && File.read(APPLICATION_PACK_PATH) !~ /import "actiontext"/
say "Adding import to default JavaScript pack"
append_to_file APPLICATION_PACK_PATH, <<-EOS
import "actiontext"
EOS
end

27
actiontext/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "actiontext",
"version": "6.0.0-alpha",
"description": "Edit and display rich text in Rails applications",
"main": "app/javascript/actiontext/index.js",
"files": [
"app/javascript/actiontext/*.js"
],
"homepage": "http://rubyonrails.org/",
"repository": {
"type": "git",
"url": "git+https://github.com/rails/rails.git"
},
"bugs": {
"url": "https://github.com/rails/rails/issues"
},
"author": "Basecamp, LLC",
"contributors": [
"Javan Makhmali <javan@javan.us>",
"Sam Stephenson <sstephenson@gmail.com>"
],
"license": "MIT",
"dependencies": {
"trix": ">=1.0.0",
"activestorage": "6.0.0-alpha"
}
}

View File

@ -0,0 +1,18 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": "> 1%",
"uglify": true
},
"useBuiltIns": true
}]
],
"plugins": [
"syntax-dynamic-import",
"transform-object-rest-spread",
["transform-class-properties", { "spec": true }]
]
}

View File

@ -0,0 +1,3 @@
plugins:
postcss-import: {}
postcss-cssnext: {}

View File

@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative 'config/application'
Rails.application.load_tasks

View File

@ -0,0 +1,3 @@
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css

View File

@ -0,0 +1,16 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require trix/dist/trix.css
*= require_tree .
*= require_self
*/

View File

@ -0,0 +1,4 @@
/*
Place all the styles related to the matching controller here.
They will automatically be included in application.css.
*/

View File

@ -0,0 +1,80 @@
body {
background-color: #fff;
color: #333;
margin: 33px;
}
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a {
color: #000;
}
a:visited {
color: #666;
}
a:hover {
color: #fff;
background-color: #000;
}
th {
padding-bottom: 5px;
}
td {
padding: 0 5px 7px;
}
div.field,
div.actions {
margin-bottom: 10px;
}
#notice {
color: green;
}
.field_with_errors {
padding: 2px;
background-color: red;
display: table;
}
#error_explanation {
width: 450px;
border: 2px solid red;
padding: 7px 7px 0;
margin-bottom: 20px;
background-color: #f0f0f0;
}
#error_explanation h2 {
text-align: left;
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
margin: -7px -7px 0;
background-color: #c00;
color: #fff;
}
#error_explanation ul li {
font-size: 12px;
list-style: square;
}
label {
display: block;
}

View File

@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@ -0,0 +1,4 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View File

@ -0,0 +1,2 @@
class ApplicationController < ActionController::Base
end

View File

@ -0,0 +1,58 @@
class MessagesController < ApplicationController
before_action :set_message, only: [:show, :edit, :update, :destroy]
# GET /messages
def index
@messages = Message.all
end
# GET /messages/1
def show
end
# GET /messages/new
def new
@message = Message.new
end
# GET /messages/1/edit
def edit
end
# POST /messages
def create
@message = Message.new(message_params)
if @message.save
redirect_to @message, notice: 'Message was successfully created.'
else
render :new
end
end
# PATCH/PUT /messages/1
def update
if @message.update(message_params)
redirect_to @message, notice: 'Message was successfully updated.'
else
render :edit
end
end
# DELETE /messages/1
def destroy
@message.destroy
redirect_to messages_url, notice: 'Message was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_message
@message = Message.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def message_params
params.require(:message).permit(:subject, :content)
end
end

View File

@ -0,0 +1,2 @@
module ApplicationHelper
end

View File

@ -0,0 +1,2 @@
module MessagesHelper
end

View File

@ -0,0 +1 @@
import "actiontext"

View File

@ -0,0 +1,2 @@
class ApplicationJob < ActiveJob::Base
end

View File

@ -0,0 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
end

View File

@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

View File

@ -0,0 +1,4 @@
class Message < ApplicationRecord
has_rich_text :content
has_rich_text :body
end

View File

@ -0,0 +1,7 @@
class Person < ApplicationRecord
include ActionText::Attachable
def to_trix_content_attachment_partial_path
"people/trix_content_attachment"
end
end

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Dummy</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_pack_tag 'application' %>
</head>
<body>
<%= yield %>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Email styles need to be inline */
</style>
</head>
<body>
<%= yield %>
</body>
</html>

View File

@ -0,0 +1 @@
<%= yield %>

View File

@ -0,0 +1,27 @@
<%= form_with(model: message, local: true) do |form| %>
<% if message.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(message.errors.count, "error") %> prohibited this message from being saved:</h2>
<ul>
<% message.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :subject %>
<%= form.text_field :subject %>
</div>
<div class="field">
<%= form.label :content %>
<%= form.rich_text_area :content, class: "trix-content" %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>

View File

@ -0,0 +1,6 @@
<h1>Editing Message</h1>
<%= render 'form', message: @message %>
<%= link_to 'Show', @message %> |
<%= link_to 'Back', messages_path %>

View File

@ -0,0 +1,29 @@
<p id="notice"><%= notice %></p>
<h1>Messages</h1>
<table>
<thead>
<tr>
<th>Subject</th>
<th>Content</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @messages.each do |message| %>
<tr>
<td><%= message.subject %></td>
<td><%= message.content %></td>
<td><%= link_to 'Show', message %></td>
<td><%= link_to 'Edit', edit_message_path(message) %></td>
<td><%= link_to 'Destroy', message, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Message', new_message_path %>

View File

@ -0,0 +1,5 @@
<h1>New Message</h1>
<%= render 'form', message: @message %>
<%= link_to 'Back', messages_path %>

View File

@ -0,0 +1,13 @@
<p id="notice"><%= notice %></p>
<p>
<strong>Subject:</strong>
<%= @message.subject %>
</p>
<div class="trix-content">
<%= @message.content.html_safe %>
</div>
<%= link_to 'Edit', edit_message_path(@message) %> |
<%= link_to 'Back', messages_path %>

View File

@ -0,0 +1,3 @@
<span class="mentionable-person" gid="<%= person.to_gid %>">
<%= person.name %>
</span>

View File

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
load Gem.bin_path('bundler', 'bundle')

View File

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

4
actiontext/test/dummy/bin/rake Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require_relative '../config/boot'
require 'rake'
Rake.application.run

36
actiontext/test/dummy/bin/setup Executable file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env ruby
require 'fileutils'
include FileUtils
# path to your application root.
APP_ROOT = File.expand_path('..', __dir__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
chdir APP_ROOT do
# This script is a starting point to setup your application.
# Add necessary setup steps to this file.
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
# Install JavaScript dependencies if using Yarn
# system('bin/yarn')
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
# cp 'config/database.yml.sample', 'config/database.yml'
# end
puts "\n== Preparing database =="
system! 'bin/rails db:setup'
puts "\n== Removing old logs and tempfiles =="
system! 'bin/rails log:clear tmp:clear'
puts "\n== Restarting application server =="
system! 'bin/rails restart'
end

View File

@ -0,0 +1,31 @@
#!/usr/bin/env ruby
require 'fileutils'
include FileUtils
# path to your application root.
APP_ROOT = File.expand_path('..', __dir__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
chdir APP_ROOT do
# This script is a way to update your development environment automatically.
# Add necessary update steps to this file.
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
# Install JavaScript dependencies if using Yarn
# system('bin/yarn')
puts "\n== Updating database =="
system! 'bin/rails db:migrate'
puts "\n== Removing old logs and tempfiles =="
system! 'bin/rails log:clear tmp:clear'
puts "\n== Restarting application server =="
system! 'bin/rails restart'
end

11
actiontext/test/dummy/bin/yarn Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env ruby
APP_ROOT = File.expand_path('..', __dir__)
Dir.chdir(APP_ROOT) do
begin
exec "yarnpkg", *ARGV
rescue Errno::ENOENT
$stderr.puts "Yarn executable was not detected in the system."
$stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
exit 1
end
end

View File

@ -0,0 +1,5 @@
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run Rails.application

View File

@ -0,0 +1,19 @@
require_relative 'boot'
require 'rails/all'
Bundler.require(*Rails.groups)
require "action_text"
module Dummy
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
end
end

View File

@ -0,0 +1,5 @@
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)

View File

@ -0,0 +1,10 @@
development:
adapter: async
test:
adapter: async
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: dummy_production

View File

@ -0,0 +1,25 @@
# SQLite version 3.x
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: db/test.sqlite3
production:
<<: *default
database: db/production.sqlite3

View File

@ -0,0 +1,5 @@
# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!

View File

@ -0,0 +1,63 @@
Rails.application.configure do
# Verifies that versions and hashed value of the package contents in the project's package.json
# config.webpacker.check_yarn_integrity = true
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Do not eager load code on boot.
config.eager_load = false
# Show full error reports.
config.consider_all_requests_local = true
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join('tmp', 'caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.cache_store = :memory_store
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
# Store uploaded files on the local file system (see config/storage.yml for options)
config.active_storage.service = :local
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.perform_caching = false
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = true
# Suppress logger output for asset requests.
config.assets.quiet = true
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
end

View File

@ -0,0 +1,96 @@
Rails.application.configure do
# Verifies that versions and hashed value of the package contents in the project's package.json
config.webpacker.check_yarn_integrity = false
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Store uploaded files on the local file system (see config/storage.yml for options)
config.active_storage.service = :local
# Mount Action Cable outside main process or domain
# config.action_cable.mount_path = nil
# config.action_cable.url = 'wss://example.com/cable'
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
config.log_level = :debug
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment)
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "dummy_#{Rails.env}"
config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups.
# require 'syslog/logger'
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
end

View File

@ -0,0 +1,46 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
# Do not eager load code on boot. This avoids loading your whole application
# just for the purpose of running a single test. If you are using a tool that
# preloads Rails for running tests, you may have to set it to true.
config.eager_load = false
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{1.hour.to_i}"
}
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
# Store uploaded files on the local file system in a temporary directory
config.active_storage.service = :test
config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
end

View File

@ -0,0 +1,8 @@
# Be sure to restart your server when you modify this file.
# ActiveSupport::Reloader.to_prepare do
# ApplicationController.renderer.defaults.merge!(
# http_host: 'example.org',
# https: false
# )
# end

View File

@ -0,0 +1,14 @@
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = '1.0'
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Add Yarn node_modules folder to the asset load path.
Rails.application.config.assets.paths << Rails.root.join('node_modules')
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
# folder are already added.
# Rails.application.config.assets.precompile += %w( admin.js admin.css )

View File

@ -0,0 +1,7 @@
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers!

View File

@ -0,0 +1,22 @@
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
# Rails.application.config.content_security_policy do |policy|
# policy.default_src :self, :https
# policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
# policy.style_src :self, :https, :unsafe_inline
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# end
# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true

View File

@ -0,0 +1,5 @@
# Be sure to restart your server when you modify this file.
# Specify a serializer for the signed and encrypted cookie jars.
# Valid options are :json, :marshal, and :hybrid.
Rails.application.config.action_dispatch.cookies_serializer = :json

View File

@ -0,0 +1,4 @@
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
Rails.application.config.filter_parameters += [:password]

Some files were not shown because too many files have changed in this diff Show More