richer_text 0.5.0 → 0.6.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc9681628df0cea14882f90ddc28774653a87d2d7db1ef38049bcbc3e67ddb49
4
- data.tar.gz: e99f8e2774aede03570dd03ec2f995f6b3e8ab46a9c7f4eedcdd7527fde69995
3
+ metadata.gz: 823a38cf937e83676b16a06828bf3a3fa461f8b3d38ae628beecd33dfc3927c2
4
+ data.tar.gz: 4f02bdb13f66fb9b896dee5d52a01806dfab1cf842c4aa33250d1420b49c8fad
5
5
  SHA512:
6
- metadata.gz: 93038afde3fae59acedbdd62d1879cdb7825e2d134d8aa520ae620a66619de0b32a594e5f34114e7dac66566f66523150d689a39500968076317313f15f00267
7
- data.tar.gz: 176ea00a46823251b32ddcb0cd8e9a59eb00eb6f452a2c7c98b43a797bf95e683f4d6df99452ad903ef5c5d6bc2fadf3ff9174f19a31476d07a44455d00b4dd0
6
+ metadata.gz: 1cf13ee1015adee390ccdb1da3a3f2f317f01c70ea2f10364762c526a1619f56aa79085ddae04ab1690ed602bca0ccdedc3f83c0691fca21c9d4f46cc4314ade
7
+ data.tar.gz: 9f1fffb59f348833c3a7ebdf1b86075bddfb2a4b4b4459bb7bdb211418e6fbf34c2ffe42c3b9c70f9c2610f6bd2874386003a11a0aaa75adbc64cc55149f6aa6
@@ -18,6 +18,7 @@
18
18
 
19
19
  .richer-text table td>*,
20
20
  .richer-text table th>* {
21
+ margin-top: 0;
21
22
  margin-bottom: 0;
22
23
  }
23
24
 
@@ -32,5 +33,11 @@
32
33
  }
33
34
 
34
35
  .richer-text img {
35
- width: 100%;
36
+ width: auto;
37
+ }
38
+
39
+ .richer-text blockquote {
40
+ border-left: 2px solid #ced4da;
41
+ margin: 1rem 0.5rem;
42
+ padding: 0 0 0 10px;
36
43
  }
@@ -10,7 +10,8 @@ module ActionView::Helpers
10
10
  options = @options.stringify_keys
11
11
  add_default_name_and_id(options)
12
12
  options["input"] ||= dom_id(object, [options["id"], :richer_text_input].compact.join("_")) if object
13
- options["value"] = options.fetch("value") { value&.to_html }
13
+ options["value"] = options.fetch("value") { value&.to_editor_format }
14
+ options["serializer"] = options.fetch("serializer") { object.class.send(:"richer_text_#{@method_name}_json") ? "json" : "html" }
14
15
 
15
16
  @template_object.richer_text_area_tag(options.delete("name"), options["value"], options.except("value"))
16
17
  end
@@ -40,6 +41,9 @@ module RicherText
40
41
  # So that we can access the content in the tiptap editor
41
42
  options[:content] ||= value
42
43
 
44
+ # So we can choose the serializer to use, e.g. "html" or "json"
45
+ options[:serializer] ||= "html"
46
+
43
47
  input_tag = hidden_field_tag(name, value, id: options[:input])
44
48
  editor_tag = tag("richer-text-editor", options)
45
49
 
@@ -0,0 +1,43 @@
1
+ module RicherText
2
+ class JsonText < ApplicationRecord
3
+ DEFAULT_BODY = {"type" => "doc", "content" => [{"type" => "paragraph", "attrs" => {"textAlign" => "left"}}]}
4
+
5
+ belongs_to :record, polymorphic: true, touch: true
6
+
7
+ serialize :body, JSON, default: DEFAULT_BODY.to_json
8
+
9
+ has_many_attached :images
10
+
11
+ before_save :update_images
12
+
13
+ def to_editor_format
14
+ body
15
+ end
16
+
17
+ def to_s
18
+ RicherText.default_renderer.visit(document)
19
+ end
20
+
21
+ private
22
+
23
+ def update_images
24
+ self.images = image_nodes.map(&:signed_id)
25
+ end
26
+
27
+ def image_nodes
28
+ find_nodes_of_type(RicherText::Nodes::Image).flatten.compact_blank
29
+ end
30
+
31
+ def find_nodes_of_type(type, node = document)
32
+ if node.children.any?
33
+ node.children.map { |child| find_nodes_of_type(type, child) }
34
+ else
35
+ node.is_a?(type) ? node : nil
36
+ end
37
+ end
38
+
39
+ def document
40
+ @document ||= RicherText::Node.build(body)
41
+ end
42
+ end
43
+ end
@@ -15,6 +15,10 @@ module RicherText
15
15
  body&.to_html&.to_s
16
16
  end
17
17
 
18
+ def to_editor_format
19
+ body&.to_html&.to_s
20
+ end
21
+
18
22
  private
19
23
 
20
24
  def update_images
@@ -0,0 +1,13 @@
1
+ class CreateRicherTextJsonTexts < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :richer_text_json_texts do |t|
4
+ t.string :name, null: false
5
+ t.text :body, size: :long
6
+ t.references :record, null: false, polymorphic: true
7
+
8
+ t.timestamps
9
+
10
+ t.index [:record_type, :record_id, :name], name: "index_richer_texts_rich_json_uniqueness", unique: true
11
+ end
12
+ end
13
+ end
@@ -3,7 +3,11 @@ module RicherText
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  class_methods do
6
- def has_richer_text(name)
6
+ def has_richer_text(name, json: false)
7
+ # Store if the attribute is using JSON or not.
8
+ class_attribute :"richer_text_#{name}_json", instance_writer: false
9
+ self.send(:"richer_text_#{name}_json=", json)
10
+
7
11
  class_eval <<-CODE, __FILE__, __LINE__ + 1
8
12
  def #{name}
9
13
  richer_text_#{name} || build_richer_text_#{name}
@@ -19,7 +23,7 @@ module RicherText
19
23
  CODE
20
24
 
21
25
  has_one :"richer_text_#{name}", -> { where(name: name) },
22
- class_name: "RicherText::RichText", as: :record, inverse_of: :record, autosave: true, dependent: :destroy
26
+ class_name: json ? "RicherText::JsonText" : "RicherText::RichText", as: :record, inverse_of: :record, autosave: true, dependent: :destroy
23
27
 
24
28
  scope :"with_richer_text_#{name}", -> { includes("richer_text_#{name}") }
25
29
  end
@@ -0,0 +1,85 @@
1
+ module RicherText
2
+ class HTMLVisitor
3
+ include ActionView::Helpers::TagHelper
4
+
5
+ def visit(node)
6
+ node.accept(self)
7
+ end
8
+
9
+ def visit_blockquote(node)
10
+ "<blockquote>#{visit_children(node).join}</blockquote>"
11
+ end
12
+
13
+ def visit_bullet_list(node)
14
+ "<ul>#{visit_children(node).join}</ul>"
15
+ end
16
+
17
+ def visit_callout(node)
18
+ "<div class='callout' data-color='#{node.color}'>#{visit_children(node).join}</div>"
19
+ end
20
+
21
+ def visit_children(node)
22
+ node.children.map { |child| visit(child) }
23
+ end
24
+
25
+ def visit_code_block(node)
26
+ "<pre><code>#{visit_children(node).join("\n")}</code></pre>"
27
+ end
28
+
29
+ def visit_doc(node)
30
+ "<div class='richer-text'>#{visit_children(node).join("\n")}</div>".html_safe
31
+ end
32
+
33
+ def visit_paragraph(node)
34
+ "<p style='#{node.style}'>#{visit_children(node).join}</p>"
35
+ end
36
+
37
+ def visit_list_item(node)
38
+ "<li>#{visit_children(node).join}</li>"
39
+ end
40
+
41
+ def visit_ordered_list(node)
42
+ "<ol>#{visit_children(node).join}</ol>"
43
+ end
44
+
45
+ def visit_heading(node)
46
+ "<h#{node.level}>#{visit_children(node).join}</h#{node.level}>"
47
+ end
48
+
49
+ def visit_hard_break(_node)
50
+ "<br>"
51
+ end
52
+
53
+ def visit_horizontal_rule(_node)
54
+ "<div><hr></div>"
55
+ end
56
+
57
+ def visit_image(node)
58
+ "<img src='#{node.src}' />"
59
+ end
60
+
61
+ def visit_text(node, marks)
62
+ if marks.empty?
63
+ node.text
64
+ else
65
+ content_tag(marks[0].tag, visit_text(node, marks[1..]), marks[0].attrs)
66
+ end
67
+ end
68
+
69
+ def visit_table(node)
70
+ "<table>#{visit_children(node).join}</table>"
71
+ end
72
+
73
+ def visit_table_row(node)
74
+ "<tr>#{visit_children(node).join}</tr>"
75
+ end
76
+
77
+ def visit_table_cell(node)
78
+ "<td>#{visit_children(node).join}</td>"
79
+ end
80
+
81
+ def visit_table_header(node)
82
+ "<th>#{visit_children(node).join}</th>"
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,23 @@
1
+ module RicherText
2
+ class Mark
3
+ attr_reader :attrs, :type
4
+
5
+ TAGS = {
6
+ "bold" => "strong",
7
+ "italic" => "em",
8
+ "strike" => "del",
9
+ "code" => "code",
10
+ "highlight" => "mark",
11
+ "link" => "a"
12
+ }
13
+
14
+ def initialize(json)
15
+ @attrs = json.fetch("attrs", {})
16
+ @type = json.fetch("type", nil)
17
+ end
18
+
19
+ def tag
20
+ TAGS[type] || "span"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ module RicherText
2
+ class Node
3
+ attr_reader :json, :attrs, :children, :type
4
+
5
+ STYLES = {
6
+ "textAlign" => "text-align",
7
+ }
8
+
9
+ def self.build(json)
10
+ node = json.is_a?(String) ? JSON.parse(json) : json
11
+ klass = "RicherText::Nodes::#{node["type"].underscore.classify}".safe_constantize
12
+ klass.new(node)
13
+ end
14
+
15
+ def initialize(json)
16
+ @json = json
17
+ @attrs = json.fetch("attrs", {})
18
+ @type = json.fetch("type", "text")
19
+ @children = json.fetch("content", []).map { |child| self.class.build(child) }
20
+ end
21
+
22
+ def style
23
+ @attrs.select { |k, v| STYLES.key?(k) }.map { |k, v| "#{STYLES[k]}: #{v};" }.join
24
+ end
25
+
26
+ def accept(visitor)
27
+ visitor.send("visit_#{type.underscore}", self)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class Blockquote < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class BulletList < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module RicherText
2
+ module Nodes
3
+ class Callout < ::RicherText::Node
4
+ def color
5
+ attrs["data-color"]
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class CodeBlock < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class Doc < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class HardBreak < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module RicherText
2
+ module Nodes
3
+ class Heading < ::RicherText::Node
4
+ def level
5
+ attrs["level"]
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class HorizontalRule < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ module RicherText
2
+ module Nodes
3
+ class Image < ::RicherText::Node
4
+ def src
5
+ @src ||= attrs['src']
6
+ end
7
+
8
+ def signed_id
9
+ @signed_id = attrs['signedId']
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class ListItem < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class OrderedList < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class Paragraph < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class Table < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class TableCell < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class TableHeader < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module RicherText
2
+ module Nodes
3
+ class TableRow < ::RicherText::Node
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,18 @@
1
+ module RicherText
2
+ module Nodes
3
+ class Text < ::RicherText::Node
4
+ def initialize(json)
5
+ @marks = json.fetch("marks", []).map { |mark| RicherText::Mark.new(mark) }
6
+ super(json)
7
+ end
8
+
9
+ def text
10
+ json["text"]
11
+ end
12
+
13
+ def accept(visitor)
14
+ visitor.visit_text(self, @marks)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,3 @@
1
1
  module RicherText
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
data/lib/richer_text.rb CHANGED
@@ -13,4 +13,38 @@ module RicherText
13
13
  autoload :Fragment
14
14
  autoload :Serialization
15
15
  autoload :TagHelper
16
+
17
+ autoload :Node
18
+ autoload :Mark
19
+ autoload :HTMLVisitor
20
+
21
+ module Nodes
22
+ extend ActiveSupport::Autoload
23
+
24
+ autoload :Blockquote
25
+ autoload :BulletList
26
+ autoload :Callout
27
+ autoload :CodeBlock
28
+ autoload :Doc
29
+ autoload :HardBreak
30
+ autoload :Heading
31
+ autoload :HorizontalRule
32
+ autoload :Image
33
+ autoload :ListItem
34
+ autoload :Paragraph
35
+ autoload :OrderedList
36
+ autoload :Text
37
+ autoload :Table
38
+ autoload :TableCell
39
+ autoload :TableRow
40
+ autoload :TableHeader
41
+ end
42
+
43
+ class << self
44
+ def default_renderer
45
+ @default_renderer ||= RicherText::HTMLVisitor.new
46
+ end
47
+
48
+ attr_writer :default_renderer
49
+ end
16
50
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: richer_text
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrea Fomera
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-02 00:00:00.000000000 Z
11
+ date: 2023-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -43,17 +43,39 @@ files:
43
43
  - app/jobs/richer_text/application_job.rb
44
44
  - app/mailers/richer_text/application_mailer.rb
45
45
  - app/models/richer_text/application_record.rb
46
+ - app/models/richer_text/json_text.rb
46
47
  - app/models/richer_text/rich_text.rb
47
48
  - app/views/layouts/richer_text/application.html.erb
48
49
  - app/views/richer_text/contents/_content.html.erb
49
50
  - config/routes.rb
50
51
  - db/migrate/20230107020316_create_richer_text_rich_texts.rb
52
+ - db/migrate/20230916224959_create_richer_text_json_texts.rb
51
53
  - lib/generators/richer_text/install/install_generator.rb
52
54
  - lib/richer_text.rb
53
55
  - lib/richer_text/attribute.rb
54
56
  - lib/richer_text/content.rb
55
57
  - lib/richer_text/engine.rb
56
58
  - lib/richer_text/fragment.rb
59
+ - lib/richer_text/html_visitor.rb
60
+ - lib/richer_text/mark.rb
61
+ - lib/richer_text/node.rb
62
+ - lib/richer_text/nodes/blockquote.rb
63
+ - lib/richer_text/nodes/bullet_list.rb
64
+ - lib/richer_text/nodes/callout.rb
65
+ - lib/richer_text/nodes/code_block.rb
66
+ - lib/richer_text/nodes/doc.rb
67
+ - lib/richer_text/nodes/hard_break.rb
68
+ - lib/richer_text/nodes/heading.rb
69
+ - lib/richer_text/nodes/horizontal_rule.rb
70
+ - lib/richer_text/nodes/image.rb
71
+ - lib/richer_text/nodes/list_item.rb
72
+ - lib/richer_text/nodes/ordered_list.rb
73
+ - lib/richer_text/nodes/paragraph.rb
74
+ - lib/richer_text/nodes/table.rb
75
+ - lib/richer_text/nodes/table_cell.rb
76
+ - lib/richer_text/nodes/table_header.rb
77
+ - lib/richer_text/nodes/table_row.rb
78
+ - lib/richer_text/nodes/text.rb
57
79
  - lib/richer_text/rendering.rb
58
80
  - lib/richer_text/serialization.rb
59
81
  - lib/richer_text/version.rb