prosemirror 0.1.0 → 0.3.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 +16 -0
- data/README.md +69 -46
- data/ROADMAP.md +14 -10
- data/TYPES.md +218 -0
- data/lib/prosemirror/content_match.rb +241 -0
- data/lib/prosemirror/html/builder.rb +154 -0
- data/lib/prosemirror/html/element.rb +84 -0
- data/lib/prosemirror/html/entities.rb +87 -0
- data/lib/prosemirror/html/entities.txt +2231 -0
- data/lib/prosemirror/html/selector.rb +95 -0
- data/lib/prosemirror/html/splitter.rb +68 -0
- data/lib/prosemirror/html/text.rb +23 -0
- data/lib/prosemirror/html_parser.rb +503 -0
- data/lib/prosemirror/json_parser.rb +1 -12
- data/lib/prosemirror/mark_type.rb +11 -0
- data/lib/prosemirror/markdown/builder.rb +200 -0
- data/lib/prosemirror/marks/basic/code.rb +16 -0
- data/lib/prosemirror/marks/basic/em.rb +4 -0
- data/lib/prosemirror/marks/basic/link.rb +1 -0
- data/lib/prosemirror/marks/basic/strong.rb +4 -0
- data/lib/prosemirror/marks/bold.rb +8 -0
- data/lib/prosemirror/marks/code.rb +5 -0
- data/lib/prosemirror/marks/highlight.rb +3 -1
- data/lib/prosemirror/marks/italic.rb +4 -0
- data/lib/prosemirror/marks/link.rb +1 -0
- data/lib/prosemirror/marks/strike.rb +5 -0
- data/lib/prosemirror/marks/subscript.rb +2 -0
- data/lib/prosemirror/marks/superscript.rb +2 -0
- data/lib/prosemirror/marks/underline.rb +3 -0
- data/lib/prosemirror/node.rb +12 -0
- data/lib/prosemirror/node_type.rb +22 -0
- data/lib/prosemirror/nodes/basic/code_block.rb +1 -0
- data/lib/prosemirror/nodes/basic/image.rb +1 -0
- data/lib/prosemirror/nodes/basic/ordered_list.rb +4 -1
- data/lib/prosemirror/nodes/blockquote.rb +1 -0
- data/lib/prosemirror/nodes/bullet_list.rb +1 -0
- data/lib/prosemirror/nodes/code_block.rb +6 -1
- data/lib/prosemirror/nodes/hard_break.rb +2 -0
- data/lib/prosemirror/nodes/heading.rb +1 -0
- data/lib/prosemirror/nodes/horizontal_rule.rb +1 -0
- data/lib/prosemirror/nodes/image.rb +1 -0
- data/lib/prosemirror/nodes/list_item.rb +1 -0
- data/lib/prosemirror/nodes/ordered_list.rb +20 -2
- data/lib/prosemirror/nodes/paragraph.rb +1 -0
- data/lib/prosemirror/nodes/table.rb +1 -0
- data/lib/prosemirror/nodes/table_cell.rb +12 -2
- data/lib/prosemirror/nodes/table_header.rb +2 -0
- data/lib/prosemirror/nodes/table_row.rb +1 -0
- data/lib/prosemirror/parse_rule.rb +50 -0
- data/lib/prosemirror/schema.rb +87 -15
- data/lib/prosemirror/type_definition.rb +34 -4
- data/lib/prosemirror/utf8.rb +16 -0
- data/lib/prosemirror/version.rb +1 -1
- data/lib/prosemirror.rb +22 -1
- metadata +15 -1
|
@@ -68,18 +68,7 @@ module Prosemirror
|
|
|
68
68
|
nodes = content.each_with_index.filter_map do |child, index|
|
|
69
69
|
build_node(child, "#{path}.content[#{index}]", depth + 1)
|
|
70
70
|
end
|
|
71
|
-
join_text(nodes)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def join_text(nodes)
|
|
75
|
-
nodes.each_with_object([]) do |node, joined|
|
|
76
|
-
previous = joined.last
|
|
77
|
-
if previous&.text? && node.text? && previous.marks == node.marks
|
|
78
|
-
joined[-1] = previous.with(text: previous.text + node.text)
|
|
79
|
-
else
|
|
80
|
-
joined << node
|
|
81
|
-
end
|
|
82
|
-
end
|
|
71
|
+
Node.join_text(nodes)
|
|
83
72
|
end
|
|
84
73
|
|
|
85
74
|
def build_marks(marks, path)
|
|
@@ -27,6 +27,17 @@ module Prosemirror
|
|
|
27
27
|
inherited_setting(:@excludes, nil)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Whether this mark can't coexist with marks of the other type.
|
|
31
|
+
def excludes?(other)
|
|
32
|
+
case excludes
|
|
33
|
+
when nil then other.type_name == type_name
|
|
34
|
+
when "_" then true
|
|
35
|
+
else
|
|
36
|
+
names = excludes.split.map(&:to_sym)
|
|
37
|
+
names.include?(other.type_name) || names.intersect?(other.group)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
30
41
|
def group(*names)
|
|
31
42
|
return @group = names.flat_map { |n| n.to_s.split }.map(&:to_sym) unless names.empty?
|
|
32
43
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
module Markdown
|
|
5
|
+
# Builds the Elements and Texts of HTML import from Markdown, with commonmarker's syntax tree
|
|
6
|
+
# (https://github.com/gjtorikian/commonmarker), so that the schema's parse rules apply to Markdown too.
|
|
7
|
+
# Each Markdown node becomes the element commonmarker renders for it, without going through HTML.
|
|
8
|
+
#
|
|
9
|
+
# - GitHub Flavored Markdown: tables, strikethrough and autolinks. Task list items stay as text (`[ ] `).
|
|
10
|
+
# - Raw HTML can span several Markdown blocks, like `<del>` around a paragraph: with the herb gem, a
|
|
11
|
+
# document holding raw HTML is rendered to HTML by commonmarker and imported. Without it, raw HTML
|
|
12
|
+
# stays as text.
|
|
13
|
+
# - Code blocks lose their final newline, as in prosemirror-markdown.
|
|
14
|
+
# - YAML front matter, between `---` lines at the start, is dropped.
|
|
15
|
+
class Builder
|
|
16
|
+
MAX_DEPTH = HTML::Builder::MAX_DEPTH
|
|
17
|
+
EXTENSIONS = {
|
|
18
|
+
strikethrough: true, table: true, autolink: true, tasklist: false, shortcodes: false, header_ids: nil,
|
|
19
|
+
front_matter_delimiter: "---"
|
|
20
|
+
}.freeze
|
|
21
|
+
RENDER_OPTIONS = {extension: EXTENSIONS, render: {unsafe: true, hardbreaks: false, github_pre_lang: false}}.freeze
|
|
22
|
+
TAGS = {
|
|
23
|
+
paragraph: "p", block_quote: "blockquote", item: "li", thematic_break: "hr", table_row: "tr",
|
|
24
|
+
emph: "em", strong: "strong", strikethrough: "s", linebreak: "br"
|
|
25
|
+
}.freeze
|
|
26
|
+
# A table's delimiter row, like `| :-- | --: |`, at the end of its line.
|
|
27
|
+
DELIMITER_ROW = /\|?(?:\s*:?-+:?\s*\|)*\s*:?-+:?\s*\|?\s*\z/
|
|
28
|
+
# The bytes commonmarker escapes in URLs, as comrak's `escape_href`: all but these, and `%` when it
|
|
29
|
+
# doesn't start an escape. `&` and `'` are kept: comrak writes them as HTML references.
|
|
30
|
+
URL_UNSAFE = %r{%(?!\h\h)|[^-_.+!*(),#@?=;:/$~A-Za-z0-9&'%]}n
|
|
31
|
+
# An IPv6 host at the start of a URL, like `http://[::1]`, kept as it is.
|
|
32
|
+
IPV6_HOST = %r{\A[A-Za-z][A-Za-z0-9+.-]*://\[[\h:.]+\]}
|
|
33
|
+
|
|
34
|
+
def self.load_commonmarker
|
|
35
|
+
require "commonmarker"
|
|
36
|
+
rescue LoadError
|
|
37
|
+
raise Error, 'Markdown import needs the commonmarker gem: add `gem "commonmarker"` to your Gemfile'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A root Element holding the content.
|
|
41
|
+
def call(markdown)
|
|
42
|
+
self.class.load_commonmarker
|
|
43
|
+
markdown = UTF8.string(markdown)
|
|
44
|
+
document = parse(markdown)
|
|
45
|
+
return from_html(document) if raw_html?(document) && HTML::Builder.herb?
|
|
46
|
+
|
|
47
|
+
@lines = markdown.lines
|
|
48
|
+
root = HTML::Element.new("#document")
|
|
49
|
+
build(document, root)
|
|
50
|
+
root
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def parse(markdown)
|
|
56
|
+
Commonmarker.parse(markdown, options: {extension: EXTENSIONS})
|
|
57
|
+
rescue Commonmarker::NestingDepthExceededError => error
|
|
58
|
+
raise Error, "Markdown nested too deeply: #{error.message}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Walks the tree without recursion. Each item of the stack is a node, the element to add it to, and
|
|
62
|
+
# the depth of that element. Elements nested deeper than MAX_DEPTH are flattened.
|
|
63
|
+
def build(document, root)
|
|
64
|
+
stack = document.to_a.reverse.map { |node| [node, root, 0] }
|
|
65
|
+
until stack.empty?
|
|
66
|
+
node, target, depth = stack.pop
|
|
67
|
+
element = add(node, target)
|
|
68
|
+
next unless element
|
|
69
|
+
|
|
70
|
+
if depth >= MAX_DEPTH
|
|
71
|
+
element = target
|
|
72
|
+
else
|
|
73
|
+
target.append(element)
|
|
74
|
+
end
|
|
75
|
+
node.to_a.reverse_each { |child| stack << [child, element, element.equal?(target) ? depth : depth + 1] }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Adds a node to the target. Returns the element to add its children to, when they need adding.
|
|
80
|
+
def add(node, target)
|
|
81
|
+
case node.type
|
|
82
|
+
when *TAGS.keys then HTML::Element.new(TAGS[node.type])
|
|
83
|
+
when :heading then HTML::Element.new("h#{node.header_level}")
|
|
84
|
+
when :list then list(node)
|
|
85
|
+
when :code_block then code_block(node, target)
|
|
86
|
+
when :table then table(node)
|
|
87
|
+
when :table_cell then table_cell(node)
|
|
88
|
+
when :link then HTML::Element.new("a", {"href" => escape_url(node.url), "title" => presence(node.title)}.compact)
|
|
89
|
+
when :image then image(node, target)
|
|
90
|
+
when :code then text(target, node.string_content, "code")
|
|
91
|
+
when :text then text(target, node.string_content)
|
|
92
|
+
when :softbreak then text(target, "\n")
|
|
93
|
+
when :html_block then text(target.append(HTML::Element.new("p")), node.literal.chomp)
|
|
94
|
+
when :html_inline then text(target, node.literal)
|
|
95
|
+
when :frontmatter then nil
|
|
96
|
+
else HTML::Element.new("span") # Unwrapped by HTML import, like escaped characters (`\*`).
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def list(node)
|
|
101
|
+
return HTML::Element.new("ul") unless node.list_type == :ordered
|
|
102
|
+
|
|
103
|
+
HTML::Element.new("ol", (node.list_start == 1) ? {} : {"start" => node.list_start.to_s})
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def code_block(node, target)
|
|
107
|
+
language = node.fence_info.to_s.split.first
|
|
108
|
+
code = HTML::Element.new("code", language ? {"class" => "language-#{language}"} : {})
|
|
109
|
+
code.append(HTML::Text.new(node.string_content.chomp))
|
|
110
|
+
target.append(HTML::Element.new("pre")).append(code)
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def table(node)
|
|
115
|
+
@alignments = alignments(node)
|
|
116
|
+
@row = 0
|
|
117
|
+
HTML::Element.new("table")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The first row holds header cells.
|
|
121
|
+
def table_cell(node)
|
|
122
|
+
index = cell_index(node)
|
|
123
|
+
@row += 1 if index.zero?
|
|
124
|
+
align = @alignments[index]
|
|
125
|
+
HTML::Element.new((@row == 1) ? "th" : "td", align ? {"style" => "text-align: #{align}"} : {})
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def cell_index(node)
|
|
129
|
+
index = 0
|
|
130
|
+
index += 1 while (node = node.previous_sibling)
|
|
131
|
+
index
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# commonmarker doesn't give the alignments of a table's columns: they are read from its delimiter row,
|
|
135
|
+
# the line after its first line.
|
|
136
|
+
def alignments(node)
|
|
137
|
+
row = @lines[node.source_position[:start_line]].to_s.chomp[DELIMITER_ROW].to_s
|
|
138
|
+
row.strip.delete_prefix("|").delete_suffix("|").split("|").map do |cell|
|
|
139
|
+
cell = cell.strip
|
|
140
|
+
if cell.start_with?(":") && cell.end_with?(":") then "center"
|
|
141
|
+
elsif cell.end_with?(":") then "right"
|
|
142
|
+
elsif cell.start_with?(":") then "left"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def image(node, target)
|
|
148
|
+
attributes = {"src" => escape_url(node.url), "alt" => plain_text(node), "title" => presence(node.title)}.compact
|
|
149
|
+
target.append(HTML::Element.new("img", attributes))
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def text(target, value, tag = nil)
|
|
154
|
+
target = target.append(HTML::Element.new(tag)) if tag
|
|
155
|
+
target.append(HTML::Text.new(value))
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def raw_html?(document)
|
|
160
|
+
document.walk { |node| return true if node.type == :html_block || node.type == :html_inline }
|
|
161
|
+
false
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def from_html(document)
|
|
165
|
+
root = HTML::Builder.new.call(document.to_html(options: RENDER_OPTIONS, plugins: {syntax_highlighter: nil}))
|
|
166
|
+
chomp_code_blocks(root)
|
|
167
|
+
root
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The code blocks commonmarker renders, `<pre><code>`, not the `<pre>` elements of raw HTML.
|
|
171
|
+
def chomp_code_blocks(element)
|
|
172
|
+
element.children.each do |child|
|
|
173
|
+
next unless child.element?
|
|
174
|
+
next chomp_code_blocks(child) unless child.name == "pre"
|
|
175
|
+
|
|
176
|
+
code = child.children.first
|
|
177
|
+
next unless code&.element? && code.name == "code" && child.children.size == 1
|
|
178
|
+
|
|
179
|
+
last = code.children.last
|
|
180
|
+
code.children[-1] = HTML::Text.new(last.value.chomp).tap { |text| text.parent = code } if last.is_a?(HTML::Text)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The URL as commonmarker renders it, with the characters URLs can't hold percent-encoded.
|
|
185
|
+
def escape_url(url)
|
|
186
|
+
host = url[IPV6_HOST].to_s
|
|
187
|
+
rest = url.delete_prefix(host).b.gsub(URL_UNSAFE) { |byte| byte.ord.zero? ? "%EF%BF%BD" : format("%%%02X", byte.ord) }
|
|
188
|
+
host + rest.force_encoding(Encoding::UTF_8)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def plain_text(node)
|
|
192
|
+
node.to_a.map { |child| %i[text code].include?(child.type) ? child.string_content : plain_text(child) }.join
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def presence(value)
|
|
196
|
+
value unless value.nil? || value.empty?
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
module Marks
|
|
5
|
+
module Basic
|
|
6
|
+
# ProseMirror's inline code, which can be combined with other marks.
|
|
7
|
+
class Code < MarkType
|
|
8
|
+
code
|
|
9
|
+
|
|
10
|
+
parse_html tag: "code"
|
|
11
|
+
|
|
12
|
+
def view_template(&) = code(&)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -5,6 +5,10 @@ module Prosemirror
|
|
|
5
5
|
module Basic
|
|
6
6
|
class Strong < MarkType
|
|
7
7
|
aliases :bold
|
|
8
|
+
parse_html tag: "strong"
|
|
9
|
+
parse_html(tag: "b") { |element| element.style["font-weight"] != "normal" && {} }
|
|
10
|
+
parse_html style: "font-weight=400", clear_mark: true
|
|
11
|
+
parse_html(style: "font-weight") { |value| value.match?(Bold::BOLD) && {} }
|
|
8
12
|
|
|
9
13
|
def view_template(&) = strong(&)
|
|
10
14
|
end
|
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
module Prosemirror
|
|
4
4
|
module Marks
|
|
5
5
|
class Bold < MarkType
|
|
6
|
+
# The `font-weight` values that mean bold.
|
|
7
|
+
BOLD = /\A(bold(er)?|[5-9]\d{2,})\z/
|
|
8
|
+
|
|
6
9
|
aliases :strong
|
|
7
10
|
|
|
11
|
+
parse_html tag: "strong"
|
|
12
|
+
parse_html(tag: "b") { |element| element.style["font-weight"] != "normal" && {} }
|
|
13
|
+
parse_html style: "font-weight=400", clear_mark: true
|
|
14
|
+
parse_html(style: "font-weight") { |value| value.match?(BOLD) && {} }
|
|
15
|
+
|
|
8
16
|
def view_template(&) = strong(&)
|
|
9
17
|
end
|
|
10
18
|
end
|
|
@@ -6,7 +6,9 @@ module Prosemirror
|
|
|
6
6
|
class Highlight < MarkType
|
|
7
7
|
COLOR = %r{\A(\#\h{3,8}|[a-z]+|(rgb|hsl)a?\([\d\s.,%/]+\))\z}i
|
|
8
8
|
|
|
9
|
-
prop :color, _String?(COLOR)
|
|
9
|
+
prop :color, _String?(COLOR), parse_html: ->(element) { element["data-color"] || element.style["background-color"] }
|
|
10
|
+
|
|
11
|
+
parse_html tag: "mark"
|
|
10
12
|
|
|
11
13
|
def view_template(&)
|
|
12
14
|
mark(data_color: @color, style: @color && "background-color: #{@color}; color: inherit", &)
|
|
@@ -4,6 +4,10 @@ module Prosemirror
|
|
|
4
4
|
module Marks
|
|
5
5
|
class Italic < MarkType
|
|
6
6
|
aliases :em
|
|
7
|
+
parse_html tag: "em"
|
|
8
|
+
parse_html(tag: "i") { |element| element.style["font-style"] != "normal" && {} }
|
|
9
|
+
parse_html style: "font-style=normal", clear_mark: true
|
|
10
|
+
parse_html style: "font-style=italic"
|
|
7
11
|
|
|
8
12
|
def view_template(&) = em(&)
|
|
9
13
|
end
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
module Prosemirror
|
|
4
4
|
module Marks
|
|
5
5
|
class Strike < MarkType
|
|
6
|
+
parse_html tag: "s"
|
|
7
|
+
parse_html tag: "del"
|
|
8
|
+
parse_html tag: "strike"
|
|
9
|
+
parse_html(style: "text-decoration", consuming: false) { |value| value.include?("line-through") ? {} : false }
|
|
10
|
+
|
|
6
11
|
def view_template(&) = s(&)
|
|
7
12
|
end
|
|
8
13
|
end
|
data/lib/prosemirror/node.rb
CHANGED
|
@@ -18,6 +18,18 @@ module Prosemirror
|
|
|
18
18
|
prop :marks, _Array(Mark), default: [].freeze, &Coerce::FROZEN
|
|
19
19
|
prop :text, _String?, &Coerce::FROZEN
|
|
20
20
|
|
|
21
|
+
# The nodes with adjacent text nodes that have the same marks joined, as in ProseMirror.
|
|
22
|
+
def self.join_text(nodes)
|
|
23
|
+
nodes.each_with_object([]) do |node, joined|
|
|
24
|
+
previous = joined.last
|
|
25
|
+
if previous&.text? && node.text? && previous.marks == node.marks
|
|
26
|
+
joined[-1] = previous.with(text: previous.text + node.text)
|
|
27
|
+
else
|
|
28
|
+
joined << node
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
21
33
|
# A copy with some properties changed.
|
|
22
34
|
def with(**changes)
|
|
23
35
|
self.class.new(**to_h, **changes)
|
|
@@ -86,6 +86,28 @@ module Prosemirror
|
|
|
86
86
|
value = leaf_text
|
|
87
87
|
value.respond_to?(:call) ? value.call(node).to_s : value.to_s
|
|
88
88
|
end
|
|
89
|
+
|
|
90
|
+
# This inline node stands for a newline, as ProseMirror's `linebreakReplacement`: when importing
|
|
91
|
+
# HTML whose whitespace is preserved, newlines become nodes of this type.
|
|
92
|
+
def linebreak_replacement(value = true)
|
|
93
|
+
@linebreak_replacement = value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def linebreak_replacement?
|
|
97
|
+
inherited_setting(:@linebreak_replacement, false)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Whether marks of this type can be applied inside this node, according to its `marks` option.
|
|
101
|
+
def allows_mark?(mark_type, schema)
|
|
102
|
+
case marks
|
|
103
|
+
when nil then schema.content_match(self).inline_content?
|
|
104
|
+
when "_" then true
|
|
105
|
+
when "" then false
|
|
106
|
+
else
|
|
107
|
+
names = marks.split.map(&:to_sym)
|
|
108
|
+
names.include?(mark_type.type_name) || names.intersect?(mark_type.group)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
89
111
|
end
|
|
90
112
|
|
|
91
113
|
# The node being rendered.
|
|
@@ -8,7 +8,10 @@ module Prosemirror
|
|
|
8
8
|
group :block, :list
|
|
9
9
|
content "list_item+"
|
|
10
10
|
|
|
11
|
-
prop :order, Integer, default: 1, aliases: ["start"],
|
|
11
|
+
prop :order, Integer, default: 1, aliases: ["start"],
|
|
12
|
+
parse_html: ->(element) { element.key?("start") ? element["start"].to_i : 1 }, &Coerce::INTEGER
|
|
13
|
+
|
|
14
|
+
parse_html tag: "ol"
|
|
12
15
|
|
|
13
16
|
def view_template(&)
|
|
14
17
|
ol(start: (@order unless @order == 1), &)
|
|
@@ -9,7 +9,12 @@ module Prosemirror
|
|
|
9
9
|
marks ""
|
|
10
10
|
code
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# From the `language-*` class of the `<code>` inside `<pre>`.
|
|
13
|
+
prop :language, _String?(/\A[\w#+.-]+\z/), aliases: ["params"], parse_html: lambda { |element|
|
|
14
|
+
element.first_element_child&.classes&.find { |name| name.start_with?("language-") }&.delete_prefix("language-")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
parse_html tag: "pre", preserve_whitespace: :full
|
|
13
18
|
|
|
14
19
|
def view_template(&)
|
|
15
20
|
pre { code(class: @language && "language-#{@language}", &) }
|
|
@@ -4,11 +4,29 @@ module Prosemirror
|
|
|
4
4
|
module Nodes
|
|
5
5
|
# Tiptap's ordered list. ProseMirror's `order` attribute is read as `start`.
|
|
6
6
|
class OrderedList < NodeType
|
|
7
|
+
# CSS list styles, used by Google Docs and Word, and their `type`.
|
|
8
|
+
LIST_STYLE_TYPES = {
|
|
9
|
+
"upper-roman" => "I", "lower-roman" => "i", "upper-alpha" => "A", "upper-latin" => "A",
|
|
10
|
+
"lower-alpha" => "a", "lower-latin" => "a"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
7
13
|
group :block, :list
|
|
8
14
|
content "list_item+"
|
|
9
15
|
|
|
10
|
-
prop :start, Integer, default: 1, aliases: ["order"],
|
|
11
|
-
|
|
16
|
+
prop :start, Integer, default: 1, aliases: ["order"],
|
|
17
|
+
parse_html: ->(element) { element.key?("start") ? element["start"].to_i : 1 }, &Coerce::INTEGER
|
|
18
|
+
prop :type, _Union?("1", "a", "A", "i", "I"), parse_html: ->(element) { OrderedList.type_from_html(element) }
|
|
19
|
+
|
|
20
|
+
parse_html tag: "ol"
|
|
21
|
+
|
|
22
|
+
# From the `type` attribute, or the list style of the list or of its first item.
|
|
23
|
+
def self.type_from_html(element)
|
|
24
|
+
element["type"] || list_style_type(element) || element.query("li")&.then { |item| list_style_type(item) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.list_style_type(element)
|
|
28
|
+
LIST_STYLE_TYPES[element.style["list-style-type"].to_s.downcase]
|
|
29
|
+
end
|
|
12
30
|
|
|
13
31
|
def view_template(&)
|
|
14
32
|
ol(start: (@start unless @start == 1), type: @type, &)
|
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
module Prosemirror
|
|
4
4
|
module Nodes
|
|
5
5
|
class TableCell < NodeType
|
|
6
|
+
ALIGNMENTS = %w[left right center].freeze
|
|
7
|
+
|
|
6
8
|
content "block+"
|
|
7
9
|
|
|
8
10
|
prop :colspan, _Integer(1..), default: 1, &Coerce::INTEGER
|
|
9
11
|
prop :rowspan, _Integer(1..), default: 1, &Coerce::INTEGER
|
|
10
|
-
prop :colwidth, _Array?(_Integer(1..))
|
|
12
|
+
prop :colwidth, _Array?(_Integer(1..)),
|
|
13
|
+
parse_html: ->(element) { element["colwidth"]&.split(",")&.map(&:to_i) }
|
|
14
|
+
# From the `text-align` style, or the old `align` attribute.
|
|
15
|
+
prop :align, _Union?(*ALIGNMENTS), parse_html: lambda { |element|
|
|
16
|
+
(element.style["text-align"] || element["align"]).to_s.strip.downcase.then { |value| value if ALIGNMENTS.include?(value) }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
parse_html tag: "td"
|
|
11
20
|
|
|
12
21
|
def view_template(&)
|
|
13
22
|
td(**cell_attributes, &)
|
|
@@ -16,7 +25,8 @@ module Prosemirror
|
|
|
16
25
|
private
|
|
17
26
|
|
|
18
27
|
def cell_attributes
|
|
19
|
-
{colspan: (@colspan unless @colspan == 1), rowspan: (@rowspan unless @rowspan == 1)
|
|
28
|
+
{colspan: (@colspan unless @colspan == 1), rowspan: (@rowspan unless @rowspan == 1),
|
|
29
|
+
style: @align && "text-align: #{@align}"}
|
|
20
30
|
end
|
|
21
31
|
end
|
|
22
32
|
end
|