tiptap-ruby 0.9.17 → 0.10.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +8 -0
- data/lib/tip_tap/markdown_renderable.rb +44 -0
- data/lib/tip_tap/node.rb +2 -0
- data/lib/tip_tap/nodes/blockquote.rb +9 -0
- data/lib/tip_tap/nodes/bullet_list.rb +4 -0
- data/lib/tip_tap/nodes/codeblock.rb +9 -0
- data/lib/tip_tap/nodes/hard_break.rb +4 -0
- data/lib/tip_tap/nodes/heading.rb +7 -0
- data/lib/tip_tap/nodes/horizontal_rule.rb +4 -0
- data/lib/tip_tap/nodes/image.rb +9 -0
- data/lib/tip_tap/nodes/list_item.rb +73 -0
- data/lib/tip_tap/nodes/ordered_list.rb +10 -0
- data/lib/tip_tap/nodes/paragraph.rb +4 -0
- data/lib/tip_tap/nodes/table.rb +35 -0
- data/lib/tip_tap/nodes/table_cell.rb +6 -0
- data/lib/tip_tap/nodes/table_header.rb +2 -2
- data/lib/tip_tap/nodes/table_row.rb +12 -0
- data/lib/tip_tap/nodes/task_item.rb +7 -8
- data/lib/tip_tap/nodes/task_list.rb +4 -0
- data/lib/tip_tap/nodes/text.rb +101 -1
- data/lib/tip_tap/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2745d7dc0e8cc41e8be3aca7a626fe0c20205ab6331768efe30d43a7dba967cd
|
4
|
+
data.tar.gz: cd62d869e49297848d9c27a647edc9992b78d55b3941ce04b73a930dbeb3fbc5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c49c2d3449cac786dcc90531dcfb25c010af8918302db6b6733fdcca8cc5ad52b0fd462684032405172647a9168e787ade66716074f7f74afeac74e249cc4ae2
|
7
|
+
data.tar.gz: 959a97bfe7556e49a36acf26a8a09a60338fc41d57bd1ead6d7111144d19735908e8f1af851709f383371ebf2cab6a1fcd8428954188044a3efbe60294723135
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -68,6 +68,14 @@ document.to_h # => { type: 'doc', content: […nodes]}
|
|
68
68
|
document.to_html # => <div class="tiptap-document"><h1><em>My Important Document</em></h1></div>
|
69
69
|
```
|
70
70
|
|
71
|
+
### Markdown
|
72
|
+
|
73
|
+
Generate GitHub-flavored Markdown that preserves nested lists, code blocks, tables, and inline marks.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
document.to_markdown # => "# My Important Document\n\n- Item one\n- Item two"
|
77
|
+
```
|
78
|
+
|
71
79
|
### Plain Text
|
72
80
|
|
73
81
|
Rendering to plain text is useful if you want to search the contents of your TipTap content.
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TipTap
|
4
|
+
module Markdown
|
5
|
+
# Rendering context for markdown serialization. Tracks indentation
|
6
|
+
# for nested blocks and whether we are currently inside a fenced
|
7
|
+
# code block so nodes can adjust formatting.
|
8
|
+
class Context
|
9
|
+
attr_reader :indent, :within_code_block
|
10
|
+
|
11
|
+
def initialize(indent: 0, within_code_block: false)
|
12
|
+
@indent = indent
|
13
|
+
@within_code_block = within_code_block
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.root
|
17
|
+
new
|
18
|
+
end
|
19
|
+
|
20
|
+
def indentation
|
21
|
+
" " * indent
|
22
|
+
end
|
23
|
+
|
24
|
+
def increase_indent(amount)
|
25
|
+
self.class.new(indent: indent + amount, within_code_block: within_code_block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def with_code_block
|
29
|
+
self.class.new(indent: indent, within_code_block: true)
|
30
|
+
end
|
31
|
+
|
32
|
+
def within_code_block?
|
33
|
+
within_code_block
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module MarkdownRenderable
|
39
|
+
def to_markdown(context = Markdown::Context.root)
|
40
|
+
rendered_blocks = content.map { |node| node.to_markdown(context) }.reject(&:blank?)
|
41
|
+
rendered_blocks.join("\n\n").gsub(/\n{3,}/, "\n\n").rstrip
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/tip_tap/node.rb
CHANGED
@@ -4,6 +4,7 @@ require "tip_tap/registerable"
|
|
4
4
|
require "tip_tap/html_renderable"
|
5
5
|
require "tip_tap/json_renderable"
|
6
6
|
require "tip_tap/plain_text_renderable"
|
7
|
+
require "tip_tap/markdown_renderable"
|
7
8
|
require "tip_tap/has_content"
|
8
9
|
|
9
10
|
# This is the base class for all TipTap nodes.
|
@@ -16,5 +17,6 @@ module TipTap
|
|
16
17
|
include HtmlRenderable
|
17
18
|
include JsonRenderable
|
18
19
|
include PlainTextRenderable
|
20
|
+
include MarkdownRenderable
|
19
21
|
end
|
20
22
|
end
|
@@ -14,6 +14,15 @@ module TipTap
|
|
14
14
|
|
15
15
|
add_content(Paragraph.new(&block))
|
16
16
|
end
|
17
|
+
|
18
|
+
def to_markdown(context = Markdown::Context.root)
|
19
|
+
inner = super(context)
|
20
|
+
lines = inner.split("\n", -1)
|
21
|
+
quoted = lines.map do |line|
|
22
|
+
line.strip.empty? ? ">" : "> #{line}"
|
23
|
+
end
|
24
|
+
quoted.join("\n")
|
25
|
+
end
|
17
26
|
end
|
18
27
|
end
|
19
28
|
end
|
@@ -11,6 +11,15 @@ module TipTap
|
|
11
11
|
def code(text)
|
12
12
|
add_content(Text.new(text, marks: [{type: "code"}]))
|
13
13
|
end
|
14
|
+
|
15
|
+
def to_markdown(context = Markdown::Context.root)
|
16
|
+
fence_language = attrs["language"].presence || attrs["lang"].presence
|
17
|
+
fence_header = "```#{fence_language}"
|
18
|
+
code_context = context.with_code_block
|
19
|
+
body = content.map { |node| node.to_markdown(code_context) }.join
|
20
|
+
body = "#{body}\n" unless body.end_with?("\n") || body.empty?
|
21
|
+
"#{fence_header}\n#{body}```"
|
22
|
+
end
|
14
23
|
end
|
15
24
|
end
|
16
25
|
end
|
@@ -15,6 +15,13 @@ module TipTap
|
|
15
15
|
def level
|
16
16
|
attrs["level"]
|
17
17
|
end
|
18
|
+
|
19
|
+
def to_markdown(context = Markdown::Context.root)
|
20
|
+
heading_level = [level.to_i, 1].max
|
21
|
+
prefix = "#" * heading_level
|
22
|
+
body = content.map { |node| node.to_markdown(context) }.join.strip
|
23
|
+
body.empty? ? prefix : "#{prefix} #{body}"
|
24
|
+
end
|
18
25
|
end
|
19
26
|
end
|
20
27
|
end
|
data/lib/tip_tap/nodes/image.rb
CHANGED
@@ -15,6 +15,15 @@ module TipTap
|
|
15
15
|
image_tag(src, alt: alt)
|
16
16
|
end
|
17
17
|
|
18
|
+
def to_markdown(context = Markdown::Context.root)
|
19
|
+
return "" if src.blank?
|
20
|
+
|
21
|
+
alt_text = alt.to_s
|
22
|
+
escaped_alt = alt_text.gsub("[", "\\[").gsub("]", "\\]")
|
23
|
+
escaped_src = src.to_s.gsub("(", "\\(").gsub(")", "\\)")
|
24
|
+
""
|
25
|
+
end
|
26
|
+
|
18
27
|
def alt
|
19
28
|
attrs["alt"]
|
20
29
|
end
|
@@ -14,6 +14,79 @@ module TipTap
|
|
14
14
|
|
15
15
|
add_content(Paragraph.new(&block))
|
16
16
|
end
|
17
|
+
|
18
|
+
def bullet_list(&block)
|
19
|
+
raise ArgumentError, "Block required" if block.nil?
|
20
|
+
|
21
|
+
add_content(BulletList.new(&block))
|
22
|
+
end
|
23
|
+
|
24
|
+
def ordered_list(&block)
|
25
|
+
raise ArgumentError, "Block required" if block.nil?
|
26
|
+
|
27
|
+
add_content(OrderedList.new(&block))
|
28
|
+
end
|
29
|
+
|
30
|
+
def task_list(&block)
|
31
|
+
raise ArgumentError, "Block required" if block.nil?
|
32
|
+
|
33
|
+
add_content(TaskList.new(&block))
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_markdown(context = Markdown::Context.root, marker: "- ")
|
37
|
+
marker_length = marker.length
|
38
|
+
first_prefix = context.indentation + marker
|
39
|
+
rest_prefix = " " * (context.indent + marker_length)
|
40
|
+
child_context = context.increase_indent(marker_length)
|
41
|
+
|
42
|
+
segments = content.map do |node|
|
43
|
+
if list_node?(node)
|
44
|
+
{type: :list, text: node.to_markdown(child_context)}
|
45
|
+
elsif paragraph_node?(node)
|
46
|
+
{type: :inline, text: node.to_markdown(child_context)}
|
47
|
+
else
|
48
|
+
{type: :block, text: node.to_markdown(child_context)}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
first_segment = segments.shift || {type: :inline, text: ""}
|
53
|
+
first_text = (first_segment[:type] == :list) ? "" : first_segment[:text]
|
54
|
+
result = format_block(first_text, first_prefix, rest_prefix)
|
55
|
+
|
56
|
+
segments.each do |segment|
|
57
|
+
case segment[:type]
|
58
|
+
when :list
|
59
|
+
result << "\n" unless result.end_with?("\n")
|
60
|
+
result << segment[:text]
|
61
|
+
else
|
62
|
+
result << "\n\n"
|
63
|
+
result << format_block(segment[:text], rest_prefix, rest_prefix)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def paragraph_node?(node)
|
73
|
+
node.is_a?(Paragraph)
|
74
|
+
end
|
75
|
+
|
76
|
+
def list_node?(node)
|
77
|
+
node.is_a?(BulletList) || node.is_a?(OrderedList) || node.is_a?(TaskList)
|
78
|
+
end
|
79
|
+
|
80
|
+
def format_block(text, first_prefix, rest_prefix)
|
81
|
+
lines = text.to_s.split("\n", -1)
|
82
|
+
return first_prefix if lines.empty?
|
83
|
+
|
84
|
+
formatted = "#{first_prefix}#{lines.first}"
|
85
|
+
lines[1..]&.each do |line|
|
86
|
+
formatted << "\n#{rest_prefix}#{line}"
|
87
|
+
end
|
88
|
+
formatted
|
89
|
+
end
|
17
90
|
end
|
18
91
|
end
|
19
92
|
end
|
@@ -18,6 +18,16 @@ module TipTap
|
|
18
18
|
def start
|
19
19
|
attrs["start"]
|
20
20
|
end
|
21
|
+
|
22
|
+
def to_markdown(context = Markdown::Context.root)
|
23
|
+
starting_index = start.to_i
|
24
|
+
starting_index = 1 if starting_index <= 0
|
25
|
+
|
26
|
+
content.each_with_index.map do |node, index|
|
27
|
+
marker = "#{starting_index + index}. "
|
28
|
+
node.to_markdown(context, marker: marker)
|
29
|
+
end.join("\n")
|
30
|
+
end
|
21
31
|
end
|
22
32
|
end
|
23
33
|
end
|
@@ -12,6 +12,10 @@ module TipTap
|
|
12
12
|
add_content(Text.new(text, marks: marks))
|
13
13
|
end
|
14
14
|
|
15
|
+
def to_markdown(context = Markdown::Context.root)
|
16
|
+
content.map { |node| node.to_markdown(context) }.join
|
17
|
+
end
|
18
|
+
|
15
19
|
# Override the default to_plain_text method to account for the nested Text nodes
|
16
20
|
# we don't want to use the separator when joining the text nodes since it could
|
17
21
|
# be a newline or some other character that we don't want to include in the plain text
|
data/lib/tip_tap/nodes/table.rb
CHANGED
@@ -13,6 +13,41 @@ module TipTap
|
|
13
13
|
|
14
14
|
add_content(TableRow.new(&block))
|
15
15
|
end
|
16
|
+
|
17
|
+
def to_markdown(context = Markdown::Context.root)
|
18
|
+
rows_data = content.map { |row| row.to_markdown_row(context) }
|
19
|
+
return "" if rows_data.empty?
|
20
|
+
|
21
|
+
column_count = rows_data.map { |row| row[:cells].size }.max || 0
|
22
|
+
return "" if column_count.zero?
|
23
|
+
|
24
|
+
header_index = rows_data.index { |row| row[:is_header] }
|
25
|
+
header_cells = if header_index
|
26
|
+
rows_data.delete_at(header_index)[:cells]
|
27
|
+
else
|
28
|
+
Array.new(column_count, "")
|
29
|
+
end
|
30
|
+
|
31
|
+
header_cells = pad_cells(header_cells, column_count)
|
32
|
+
separator_cells = Array.new(column_count, "---")
|
33
|
+
body_rows = rows_data.map { |row| pad_cells(row[:cells], column_count) }
|
34
|
+
|
35
|
+
lines = []
|
36
|
+
lines << format_row(header_cells)
|
37
|
+
lines << format_row(separator_cells)
|
38
|
+
body_rows.each { |cells| lines << format_row(cells) }
|
39
|
+
lines.join("\n")
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def pad_cells(cells, desired_size)
|
45
|
+
Array.new(desired_size) { |index| cells[index] || "" }
|
46
|
+
end
|
47
|
+
|
48
|
+
def format_row(cells)
|
49
|
+
"| #{cells.join(" | ")} |"
|
50
|
+
end
|
16
51
|
end
|
17
52
|
end
|
18
53
|
end
|
@@ -13,6 +13,12 @@ module TipTap
|
|
13
13
|
|
14
14
|
add_content(Paragraph.new(&block))
|
15
15
|
end
|
16
|
+
|
17
|
+
def to_markdown(context = Markdown::Context.root)
|
18
|
+
values = content.map { |node| node.to_markdown(context) }.reject(&:blank?)
|
19
|
+
joined = values.join("<br>")
|
20
|
+
joined.gsub("\n\n", "<br><br>").gsub("\n", "<br>")
|
21
|
+
end
|
16
22
|
end
|
17
23
|
end
|
18
24
|
end
|
@@ -19,6 +19,18 @@ module TipTap
|
|
19
19
|
|
20
20
|
add_content(TableHeader.new(&block))
|
21
21
|
end
|
22
|
+
|
23
|
+
def to_markdown(context = Markdown::Context.root)
|
24
|
+
row_data = to_markdown_row(context)
|
25
|
+
"| #{row_data[:cells].join(" | ")} |"
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_markdown_row(context)
|
29
|
+
{
|
30
|
+
cells: content.map { |node| node.to_markdown(context) },
|
31
|
+
is_header: content.all? { |node| node.is_a?(TableHeader) }
|
32
|
+
}
|
33
|
+
end
|
22
34
|
end
|
23
35
|
end
|
24
36
|
end
|
@@ -1,23 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "tip_tap/
|
3
|
+
require "tip_tap/nodes/list_item"
|
4
4
|
|
5
5
|
module TipTap
|
6
6
|
module Nodes
|
7
|
-
class TaskItem <
|
7
|
+
class TaskItem < ListItem
|
8
8
|
self.type_name = "taskItem"
|
9
9
|
self.html_tag = :li
|
10
10
|
self.html_class_name = proc { class_names("task-item", {checked: checked?}) }
|
11
11
|
|
12
|
-
def paragraph(&block)
|
13
|
-
raise ArgumentError, "Block required" if block.nil?
|
14
|
-
|
15
|
-
add_content(Paragraph.new(&block))
|
16
|
-
end
|
17
|
-
|
18
12
|
def checked?
|
19
13
|
attrs["checked"]
|
20
14
|
end
|
15
|
+
|
16
|
+
def to_markdown(context = Markdown::Context.root)
|
17
|
+
marker = checked? ? "- [x] " : "- [ ] "
|
18
|
+
super(context, marker: marker)
|
19
|
+
end
|
21
20
|
end
|
22
21
|
end
|
23
22
|
end
|
data/lib/tip_tap/nodes/text.rb
CHANGED
@@ -5,7 +5,9 @@ require "tip_tap/node"
|
|
5
5
|
module TipTap
|
6
6
|
module Nodes
|
7
7
|
class Text < Node
|
8
|
-
|
8
|
+
# Allow the text to be set and accessed directly
|
9
|
+
attr_accessor :text
|
10
|
+
attr_reader :marks
|
9
11
|
|
10
12
|
self.type_name = "text"
|
11
13
|
|
@@ -46,6 +48,27 @@ module TipTap
|
|
46
48
|
text
|
47
49
|
end
|
48
50
|
|
51
|
+
def to_markdown(context = Markdown::Context.root)
|
52
|
+
value = text.to_s
|
53
|
+
return "" if value.empty?
|
54
|
+
|
55
|
+
return value if context.within_code_block?
|
56
|
+
|
57
|
+
return wrap_with_backticks(value) if code?
|
58
|
+
|
59
|
+
value = escape_markdown(value)
|
60
|
+
value = apply_bold(value) if bold?
|
61
|
+
value = apply_italic(value) if italic?
|
62
|
+
value = apply_strike(value) if strike?
|
63
|
+
value = wrap_with_html_tag("u", value) if underline?
|
64
|
+
value = apply_highlight(value) if highlight?
|
65
|
+
value = apply_text_style(value) if text_style?
|
66
|
+
value = wrap_with_html_tag("sup", value) if superscript?
|
67
|
+
value = wrap_with_html_tag("sub", value) if subscript?
|
68
|
+
value = wrap_with_link(value) if link?
|
69
|
+
value
|
70
|
+
end
|
71
|
+
|
49
72
|
def italic?
|
50
73
|
has_mark_with_type?("italic")
|
51
74
|
end
|
@@ -123,6 +146,83 @@ module TipTap
|
|
123
146
|
end
|
124
147
|
content_tag(:mark, text, data: data, style: inline_style_content(styles))
|
125
148
|
end
|
149
|
+
|
150
|
+
def escape_markdown(value)
|
151
|
+
value.gsub(/([\\`*_{}\[\]()#+!><~-])/) { |char| "\\#{char}" }
|
152
|
+
end
|
153
|
+
|
154
|
+
def wrap_with_backticks(value)
|
155
|
+
max_tick_sequence = value.scan(/`+/).map(&:length).max || 0
|
156
|
+
wrapper = "`" * (max_tick_sequence + 1)
|
157
|
+
"#{wrapper}#{value}#{wrapper}"
|
158
|
+
end
|
159
|
+
|
160
|
+
def apply_bold(value)
|
161
|
+
"**#{value}**"
|
162
|
+
end
|
163
|
+
|
164
|
+
def apply_italic(value)
|
165
|
+
"_#{value}_"
|
166
|
+
end
|
167
|
+
|
168
|
+
def apply_strike(value)
|
169
|
+
"~~#{value}~~"
|
170
|
+
end
|
171
|
+
|
172
|
+
def apply_highlight(value)
|
173
|
+
attributes = {}
|
174
|
+
styles = {}
|
175
|
+
if highlight_color
|
176
|
+
attributes["data-color"] = highlight_color
|
177
|
+
styles["background-color"] = highlight_color
|
178
|
+
styles["color"] = "inherit"
|
179
|
+
end
|
180
|
+
wrap_with_html_tag("mark", value, attributes, styles)
|
181
|
+
end
|
182
|
+
|
183
|
+
def apply_text_style(value)
|
184
|
+
styles = text_styles || {}
|
185
|
+
return value if styles.empty?
|
186
|
+
|
187
|
+
wrap_with_html_tag("span", value, {}, styles)
|
188
|
+
end
|
189
|
+
|
190
|
+
def wrap_with_link(value)
|
191
|
+
href = link_href.to_s
|
192
|
+
return value if href.blank?
|
193
|
+
|
194
|
+
destination = escape_link_destination(href)
|
195
|
+
title = link_title
|
196
|
+
title_part = title.present? ? " \"#{escape_double_quotes(title)}\"" : ""
|
197
|
+
"[#{value}](#{destination}#{title_part})"
|
198
|
+
end
|
199
|
+
|
200
|
+
def wrap_with_html_tag(tag, value, attributes = {}, styles = {})
|
201
|
+
attr_segments = []
|
202
|
+
attributes.each do |key, attr_value|
|
203
|
+
next if attr_value.blank?
|
204
|
+
attr_segments << "#{key}=\"#{escape_double_quotes(attr_value)}\""
|
205
|
+
end
|
206
|
+
style_segment = inline_style_content(styles)
|
207
|
+
attr_segments << "style=\"#{escape_double_quotes(style_segment)}\"" if style_segment.present?
|
208
|
+
attributes_string = attr_segments.empty? ? "" : " #{attr_segments.join(" ")}"
|
209
|
+
"<#{tag}#{attributes_string}>#{value}</#{tag}>"
|
210
|
+
end
|
211
|
+
|
212
|
+
def escape_link_destination(href)
|
213
|
+
escaped = href.gsub("(", "\\(").gsub(")", "\\)")
|
214
|
+
escaped.gsub(/[\s]/) do |char|
|
215
|
+
(char == " ") ? "%20" : char
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def escape_double_quotes(value)
|
220
|
+
value.to_s.gsub('"', """)
|
221
|
+
end
|
222
|
+
|
223
|
+
def link_title
|
224
|
+
marks.find { |mark| mark["type"] == "link" }&.dig("attrs", "title")
|
225
|
+
end
|
126
226
|
end
|
127
227
|
end
|
128
228
|
end
|
data/lib/tip_tap/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tiptap-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chad Wilken
|
@@ -53,6 +53,7 @@ files:
|
|
53
53
|
- lib/tip_tap/has_content.rb
|
54
54
|
- lib/tip_tap/html_renderable.rb
|
55
55
|
- lib/tip_tap/json_renderable.rb
|
56
|
+
- lib/tip_tap/markdown_renderable.rb
|
56
57
|
- lib/tip_tap/node.rb
|
57
58
|
- lib/tip_tap/nodes/blockquote.rb
|
58
59
|
- lib/tip_tap/nodes/bullet_list.rb
|