prosemirror 0.4.0 → 0.5.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 +6 -0
- data/README.md +18 -0
- data/ROADMAP.md +5 -3
- data/TYPES.md +43 -0
- data/lib/prosemirror/checker.rb +2 -2
- data/lib/prosemirror/coerce.rb +7 -0
- data/lib/prosemirror/document.rb +6 -0
- data/lib/prosemirror/html/element.rb +1 -1
- data/lib/prosemirror/html/selector.rb +12 -4
- data/lib/prosemirror/html_parser.rb +9 -2
- data/lib/prosemirror/json_parser.rb +71 -40
- data/lib/prosemirror/mark.rb +8 -3
- data/lib/prosemirror/mark_type.rb +15 -0
- data/lib/prosemirror/markdown/builder.rb +19 -9
- data/lib/prosemirror/markdown_serializer.rb +516 -0
- data/lib/prosemirror/marks/basic/code.rb +2 -0
- data/lib/prosemirror/marks/basic/em.rb +2 -0
- data/lib/prosemirror/marks/basic/link.rb +2 -0
- data/lib/prosemirror/marks/basic/strong.rb +2 -0
- data/lib/prosemirror/marks/bold.rb +2 -0
- data/lib/prosemirror/marks/code.rb +2 -0
- data/lib/prosemirror/marks/italic.rb +2 -0
- data/lib/prosemirror/marks/link.rb +2 -0
- data/lib/prosemirror/marks/strike.rb +2 -0
- data/lib/prosemirror/node.rb +21 -6
- data/lib/prosemirror/node_type.rb +13 -0
- data/lib/prosemirror/nodes/basic/code_block.rb +4 -0
- data/lib/prosemirror/nodes/basic/image.rb +4 -0
- data/lib/prosemirror/nodes/basic/ordered_list.rb +4 -0
- data/lib/prosemirror/nodes/blockquote.rb +4 -0
- data/lib/prosemirror/nodes/bullet_list.rb +6 -0
- data/lib/prosemirror/nodes/code_block.rb +15 -0
- data/lib/prosemirror/nodes/hard_break.rb +5 -0
- data/lib/prosemirror/nodes/heading.rb +6 -0
- data/lib/prosemirror/nodes/horizontal_rule.rb +5 -0
- data/lib/prosemirror/nodes/image.rb +16 -0
- data/lib/prosemirror/nodes/list_item.rb +4 -0
- data/lib/prosemirror/nodes/ordered_list.rb +11 -0
- data/lib/prosemirror/nodes/paragraph.rb +8 -0
- data/lib/prosemirror/nodes/table.rb +36 -0
- data/lib/prosemirror/nodes/text.rb +4 -0
- data/lib/prosemirror/schema.rb +14 -2
- data/lib/prosemirror/type_definition.rb +33 -13
- data/lib/prosemirror/version.rb +1 -1
- metadata +2 -1
|
@@ -16,6 +16,10 @@ module Prosemirror
|
|
|
16
16
|
def view_template(&)
|
|
17
17
|
ol(start: (@order unless @order == 1), &)
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
def to_markdown(markdown, parent, index)
|
|
21
|
+
Nodes::OrderedList.numbered(markdown, node, @order, markdown.list_after_list?(parent, index) ? ")" : ".")
|
|
22
|
+
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
end
|
|
@@ -8,6 +8,12 @@ module Prosemirror
|
|
|
8
8
|
parse_html tag: "ul"
|
|
9
9
|
|
|
10
10
|
def view_template(&) = ul(&)
|
|
11
|
+
|
|
12
|
+
# `-` items, or `*` right after another bullet list, which would otherwise continue it.
|
|
13
|
+
def to_markdown(markdown, parent, index)
|
|
14
|
+
marker = markdown.list_after_list?(parent, index) ? "* " : "- "
|
|
15
|
+
markdown.render_list(node, " ") { marker }
|
|
16
|
+
end
|
|
11
17
|
end
|
|
12
18
|
end
|
|
13
19
|
end
|
|
@@ -19,6 +19,21 @@ module Prosemirror
|
|
|
19
19
|
def view_template(&)
|
|
20
20
|
pre { code(class: @language && "language-#{@language}", &) }
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
def to_markdown(markdown, _parent, _index)
|
|
24
|
+
self.class.fenced(markdown, node, @language)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A fenced code block, with a fence longer than any run of backticks in the code.
|
|
28
|
+
def self.fenced(markdown, node, language)
|
|
29
|
+
code = node.text_content
|
|
30
|
+
fence = "`" * [3, (code.scan(/`{3,}/).map(&:length).max || 0) + 1].max
|
|
31
|
+
markdown.write("#{fence}#{language}\n")
|
|
32
|
+
markdown.text(code, escape: false)
|
|
33
|
+
markdown.ensure_new_line
|
|
34
|
+
markdown.write(fence)
|
|
35
|
+
markdown.close_block(node)
|
|
36
|
+
end
|
|
22
37
|
end
|
|
23
38
|
end
|
|
24
39
|
end
|
|
@@ -10,6 +10,11 @@ module Prosemirror
|
|
|
10
10
|
parse_html tag: "br"
|
|
11
11
|
|
|
12
12
|
def view_template = br
|
|
13
|
+
|
|
14
|
+
# A backslash before the newline, unless only line breaks follow.
|
|
15
|
+
def to_markdown(markdown, parent, index)
|
|
16
|
+
markdown.write("\\\n") if parent.content[(index + 1)..].any? { |sibling| sibling.type != node.type }
|
|
17
|
+
end
|
|
13
18
|
end
|
|
14
19
|
end
|
|
15
20
|
end
|
|
@@ -10,6 +10,12 @@ module Prosemirror
|
|
|
10
10
|
(1..6).each { |level| parse_html tag: "h#{level}", attrs: {level:} }
|
|
11
11
|
|
|
12
12
|
def view_template(&) = tag(:"h#{@level}", &)
|
|
13
|
+
|
|
14
|
+
def to_markdown(markdown, _parent, _index)
|
|
15
|
+
markdown.write("#{"#" * @level} ")
|
|
16
|
+
markdown.render_inline(node, from_block_start: false)
|
|
17
|
+
markdown.close_block(node)
|
|
18
|
+
end
|
|
13
19
|
end
|
|
14
20
|
end
|
|
15
21
|
end
|
|
@@ -17,6 +17,22 @@ module Prosemirror
|
|
|
17
17
|
src = safe_url(@src)
|
|
18
18
|
img(src: src, alt: @alt, title: @title, width: @width, height: @height) if src
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
# ``, or HTML with `html: true` when it has dimensions. Images in a row share a line,
|
|
22
|
+
# as Markdown would give them back: in separate paragraphs, each would come back after an empty one.
|
|
23
|
+
def to_markdown(markdown, parent, index)
|
|
24
|
+
markdown.write(" ") if index.positive? && parent.content[index - 1].type == node.type
|
|
25
|
+
if markdown.html? && (@width || @height)
|
|
26
|
+
markdown.write(markdown.node_html(node))
|
|
27
|
+
else
|
|
28
|
+
markdown.write(self.class.markdown_image(markdown, @src, @alt, @title))
|
|
29
|
+
end
|
|
30
|
+
markdown.close_block(node) unless parent.content[index + 1]&.type == node.type
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.markdown_image(markdown, src, alt, title)
|
|
34
|
+
"}#{markdown.title(title)})"
|
|
35
|
+
end
|
|
20
36
|
end
|
|
21
37
|
end
|
|
22
38
|
end
|
|
@@ -31,6 +31,17 @@ module Prosemirror
|
|
|
31
31
|
def view_template(&)
|
|
32
32
|
ol(start: (@start unless @start == 1), type: @type, &)
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
def to_markdown(markdown, parent, index)
|
|
36
|
+
self.class.numbered(markdown, node, @start, markdown.list_after_list?(parent, index) ? ")" : ".")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Items numbered from `start`, with the numbers aligned. `)` instead of `.` keeps a list right after
|
|
40
|
+
# another ordered list apart from it.
|
|
41
|
+
def self.numbered(markdown, node, start, delimiter = ".")
|
|
42
|
+
width = (start + node.content.size - 1).to_s.length
|
|
43
|
+
markdown.render_list(node, " " * (width + 2)) { |index| "#{(start + index).to_s.rjust(width)}#{delimiter} " }
|
|
44
|
+
end
|
|
34
45
|
end
|
|
35
46
|
end
|
|
36
47
|
end
|
|
@@ -8,6 +8,14 @@ module Prosemirror
|
|
|
8
8
|
parse_html tag: "p"
|
|
9
9
|
|
|
10
10
|
def view_template(&) = p(&)
|
|
11
|
+
|
|
12
|
+
# Markdown has no empty paragraphs: they are left out.
|
|
13
|
+
def to_markdown(markdown, _parent, _index)
|
|
14
|
+
return if node.content.empty?
|
|
15
|
+
|
|
16
|
+
markdown.render_inline(node)
|
|
17
|
+
markdown.close_block(node)
|
|
18
|
+
end
|
|
11
19
|
end
|
|
12
20
|
end
|
|
13
21
|
end
|
|
@@ -10,6 +10,42 @@ module Prosemirror
|
|
|
10
10
|
def view_template(&)
|
|
11
11
|
table { tbody(&) }
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
# A GitHub Flavored Markdown table: the first row is the header, and gives the columns' alignments. A
|
|
15
|
+
# cell's paragraphs, images and line breaks are joined with `<br>`, and its other blocks, like lists, are
|
|
16
|
+
# written as HTML, which Markdown import reads back with the herb gem.
|
|
17
|
+
def to_markdown(markdown, _parent, _index)
|
|
18
|
+
rows = node.content.map { |row| row.content.map { |cell| cell_markdown(markdown, cell) } }
|
|
19
|
+
columns = rows.map(&:size).max.to_i
|
|
20
|
+
return markdown.close_block(node) if columns.zero?
|
|
21
|
+
|
|
22
|
+
aligns = node.content.first.content.map { |cell| cell.attrs[:align] }
|
|
23
|
+
delimiter = Array.new(columns) do |index|
|
|
24
|
+
{"left" => ":--", "center" => ":-:", "right" => "--:"}.fetch(aligns[index].to_s, "---")
|
|
25
|
+
end
|
|
26
|
+
lines = [rows.first, delimiter, *rows.drop(1)].map do |cells|
|
|
27
|
+
"| #{Array.new(columns) { |index| cells[index].to_s }.join(" | ")} |"
|
|
28
|
+
end
|
|
29
|
+
markdown.text(lines.join("\n"), escape: false)
|
|
30
|
+
markdown.close_block(node)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def cell_markdown(markdown, cell)
|
|
36
|
+
parts = cell.content.map do |block|
|
|
37
|
+
type = markdown.schema.node_type(block.type)
|
|
38
|
+
if type && (type <= Paragraph || type <= Image)
|
|
39
|
+
serializer = MarkdownSerializer.new(markdown.schema, html: markdown.html?)
|
|
40
|
+
serializer.render(block, cell, 0)
|
|
41
|
+
serializer.out.strip.gsub(/\\\n|\n+/, "<br>")
|
|
42
|
+
else
|
|
43
|
+
markdown.node_html(block).gsub("\n", " ")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
# A space after a line break is dropped, as in HTML.
|
|
47
|
+
parts.join("<br>").gsub(/(?:<br>[[:space:]]*)+/, "<br>").delete_prefix("<br>").delete_suffix("<br>").gsub("|", "\\|")
|
|
48
|
+
end
|
|
13
49
|
end
|
|
14
50
|
end
|
|
15
51
|
end
|
data/lib/prosemirror/schema.rb
CHANGED
|
@@ -57,6 +57,8 @@ module Prosemirror
|
|
|
57
57
|
def after_initialize
|
|
58
58
|
@node_lookup = lookup(@nodes)
|
|
59
59
|
@mark_lookup = lookup(@marks)
|
|
60
|
+
@node_names = spellings(@node_lookup)
|
|
61
|
+
@mark_names = spellings(@mark_lookup)
|
|
60
62
|
@textblocks = @nodes.to_h { |type| [type, textblock_type?(type)] }
|
|
61
63
|
@content_matches = @nodes.to_h { |type| [type, ContentMatch.compile(type.content, self)] }
|
|
62
64
|
raise ArgumentError, "the schema has no #{@top_node} node type" unless node_type(@top_node)
|
|
@@ -100,11 +102,11 @@ module Prosemirror
|
|
|
100
102
|
|
|
101
103
|
# The node type for a name, alias, or camelCase name.
|
|
102
104
|
def node_type(name)
|
|
103
|
-
@node_lookup[Naming.snake(name).to_sym]
|
|
105
|
+
@node_names[name] || @node_lookup[Naming.snake(name).to_sym]
|
|
104
106
|
end
|
|
105
107
|
|
|
106
108
|
def mark_type(name)
|
|
107
|
-
@mark_lookup[Naming.snake(name).to_sym]
|
|
109
|
+
@mark_names[name] || @mark_lookup[Naming.snake(name).to_sym]
|
|
108
110
|
end
|
|
109
111
|
|
|
110
112
|
# Build a document from JSON, given as a Hash or a String.
|
|
@@ -228,6 +230,16 @@ module Prosemirror
|
|
|
228
230
|
end
|
|
229
231
|
end
|
|
230
232
|
|
|
233
|
+
# The names of a lookup table, as Symbols and Strings, in snake_case and camelCase, so that most names
|
|
234
|
+
# are found without converting them.
|
|
235
|
+
def spellings(lookup)
|
|
236
|
+
table = {}
|
|
237
|
+
lookup.each { |name, type| table[name] = table[name.name] = type }
|
|
238
|
+
lookup.each { |name, type| table[Naming.camel(name).freeze] ||= type }
|
|
239
|
+
table.each_key.select { |name| name.is_a?(String) }.each { |name| table[name.to_sym] ||= table[name] }
|
|
240
|
+
table.freeze
|
|
241
|
+
end
|
|
242
|
+
|
|
231
243
|
def merge_types(types, additions)
|
|
232
244
|
additions.reduce(types) do |list, addition|
|
|
233
245
|
index = list.index { |type| type.type_name == addition.type_name }
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Prosemirror
|
|
4
4
|
# Class-level declarations shared by node types and mark types.
|
|
5
5
|
module TypeDefinition
|
|
6
|
+
EMPTY_HASH = {}.freeze
|
|
6
7
|
# The name of the type, as a snake_case Symbol. It defaults to the class name (`BulletList` → `:bullet_list`),
|
|
7
8
|
# and subclasses of a concrete type keep its name.
|
|
8
9
|
def type_name(name = nil)
|
|
@@ -24,7 +25,12 @@ module Prosemirror
|
|
|
24
25
|
# - `parse_html:` a block reading the attribute from an HTML element, when importing HTML. By default,
|
|
25
26
|
# the HTML attribute with the JSON name is read.
|
|
26
27
|
def prop(name, type, kind = :keyword, json: nil, aliases: [], parse_html: nil, **, &)
|
|
27
|
-
|
|
28
|
+
json = (json || name).to_s
|
|
29
|
+
aliases = aliases.map(&:to_s)
|
|
30
|
+
# The keys accepted in JSON: each name as written, in snake_case and in camelCase.
|
|
31
|
+
keys = [json, *aliases].flat_map { |key| [key, Naming.snake(key), Naming.camel(key)] }.uniq.map(&:freeze).freeze
|
|
32
|
+
attribute_names[name] = {json:, aliases:, parse_html:, keys:}.freeze
|
|
33
|
+
@known_keys = nil
|
|
28
34
|
super(name, type, kind, **, &)
|
|
29
35
|
end
|
|
30
36
|
|
|
@@ -54,7 +60,10 @@ module Prosemirror
|
|
|
54
60
|
# Invalid values are converted when the meaning is clear, or replaced by their default.
|
|
55
61
|
# Returns nil when a required attribute is missing or invalid.
|
|
56
62
|
def build_attrs(given, report:)
|
|
57
|
-
given = given.is_a?(Hash)
|
|
63
|
+
given = if !given.is_a?(Hash) then EMPTY_HASH
|
|
64
|
+
elsif keys?(given) { |key| key.is_a?(String) } then given
|
|
65
|
+
else given.transform_keys(&:to_s)
|
|
66
|
+
end
|
|
58
67
|
|
|
59
68
|
literal_properties.each_with_object({}) do |property, attrs|
|
|
60
69
|
json_name = attribute_names.dig(property.name, :json)
|
|
@@ -86,14 +95,19 @@ module Prosemirror
|
|
|
86
95
|
|
|
87
96
|
# The JSON attributes this type doesn't declare, with String keys, so that `to_h` gives them back.
|
|
88
97
|
def unknown_attrs(given)
|
|
89
|
-
return
|
|
98
|
+
return EMPTY_HASH unless given.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
known = known_keys
|
|
101
|
+
return EMPTY_HASH if keys?(given) { |key| known.include?(key.is_a?(Symbol) ? key.name : key) }
|
|
90
102
|
|
|
91
|
-
known = attribute_names.values.flat_map do |names|
|
|
92
|
-
[names[:json], *names[:aliases]].flat_map { |name| [name, Naming.snake(name), Naming.camel(name)] }
|
|
93
|
-
end
|
|
94
103
|
given.transform_keys(&:to_s).except(*known)
|
|
95
104
|
end
|
|
96
105
|
|
|
106
|
+
# The JSON keys of the declared attributes, in all their spellings.
|
|
107
|
+
def known_keys
|
|
108
|
+
@known_keys ||= attribute_names.each_value.flat_map { |names| names[:keys] }.to_set.freeze
|
|
109
|
+
end
|
|
110
|
+
|
|
97
111
|
# Attributes as JSON, with their JSON names.
|
|
98
112
|
def serialize_attrs(attrs)
|
|
99
113
|
attrs.to_h { |name, value| [attribute_names.dig(name, :json) || name.to_s, value] }
|
|
@@ -122,17 +136,23 @@ module Prosemirror
|
|
|
122
136
|
superclass.send(:inherited_setting, ivar, default)
|
|
123
137
|
end
|
|
124
138
|
|
|
139
|
+
# Whether the block is true for all keys of the hash, without the Enumerator `each_key.all?` allocates.
|
|
140
|
+
def keys?(hash)
|
|
141
|
+
hash.each_key { |key| return false unless yield(key) }
|
|
142
|
+
true
|
|
143
|
+
end
|
|
144
|
+
|
|
125
145
|
def attribute_key(given, names)
|
|
126
|
-
|
|
127
|
-
[name, Naming.snake(name), Naming.camel(name)].each do |candidate|
|
|
128
|
-
return candidate if given.key?(candidate)
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
nil
|
|
146
|
+
names[:keys].find { |key| given.key?(key) }
|
|
132
147
|
end
|
|
133
148
|
|
|
149
|
+
# The gem's coercions don't use `self`: they are called directly, without Literal's `instance_exec`.
|
|
134
150
|
def coerce_attribute(property, value)
|
|
135
|
-
|
|
151
|
+
coercion = property.coercion
|
|
152
|
+
if coercion.nil? then value
|
|
153
|
+
elsif Coerce::PLAIN.include?(coercion) then coercion.call(value)
|
|
154
|
+
else property.coerce(value, context: self)
|
|
155
|
+
end
|
|
136
156
|
rescue
|
|
137
157
|
value
|
|
138
158
|
end
|
data/lib/prosemirror/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: prosemirror
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hans Lemuet
|
|
@@ -84,6 +84,7 @@ files:
|
|
|
84
84
|
- lib/prosemirror/mark.rb
|
|
85
85
|
- lib/prosemirror/mark_type.rb
|
|
86
86
|
- lib/prosemirror/markdown/builder.rb
|
|
87
|
+
- lib/prosemirror/markdown_serializer.rb
|
|
87
88
|
- lib/prosemirror/marks/basic/code.rb
|
|
88
89
|
- lib/prosemirror/marks/basic/em.rb
|
|
89
90
|
- lib/prosemirror/marks/basic/link.rb
|