prosemirror 0.0.1 → 0.2.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 +20 -0
- data/COOKBOOK.md +108 -0
- data/LICENSE.txt +21 -0
- data/README.md +155 -12
- data/ROADMAP.md +215 -0
- data/TYPES.md +201 -0
- data/lib/prosemirror/coerce.rb +17 -0
- data/lib/prosemirror/content_expression.rb +94 -0
- data/lib/prosemirror/content_match.rb +241 -0
- data/lib/prosemirror/document.rb +76 -0
- data/lib/prosemirror/html/builder.rb +146 -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 +498 -0
- data/lib/prosemirror/html_renderer.rb +20 -0
- data/lib/prosemirror/json_parser.rb +114 -0
- data/lib/prosemirror/mark.rb +19 -0
- data/lib/prosemirror/mark_type.rb +71 -0
- data/lib/prosemirror/marks/basic/code.rb +16 -0
- data/lib/prosemirror/marks/basic/em.rb +17 -0
- data/lib/prosemirror/marks/basic/link.rb +21 -0
- data/lib/prosemirror/marks/basic/strong.rb +17 -0
- data/lib/prosemirror/marks/bold.rb +19 -0
- data/lib/prosemirror/marks/code.rb +16 -0
- data/lib/prosemirror/marks/highlight.rb +18 -0
- data/lib/prosemirror/marks/italic.rb +15 -0
- data/lib/prosemirror/marks/link.rb +22 -0
- data/lib/prosemirror/marks/strike.rb +14 -0
- data/lib/prosemirror/marks/subscript.rb +13 -0
- data/lib/prosemirror/marks/superscript.rb +13 -0
- data/lib/prosemirror/marks/underline.rb +12 -0
- data/lib/prosemirror/naming.rb +16 -0
- data/lib/prosemirror/node.rb +77 -0
- data/lib/prosemirror/node_type.rb +123 -0
- data/lib/prosemirror/nodes/basic/code_block.rb +20 -0
- data/lib/prosemirror/nodes/basic/image.rb +23 -0
- data/lib/prosemirror/nodes/basic/ordered_list.rb +22 -0
- data/lib/prosemirror/nodes/blockquote.rb +13 -0
- data/lib/prosemirror/nodes/bullet_list.rb +13 -0
- data/lib/prosemirror/nodes/code_block.rb +24 -0
- data/lib/prosemirror/nodes/doc.rb +10 -0
- data/lib/prosemirror/nodes/hard_break.rb +15 -0
- data/lib/prosemirror/nodes/heading.rb +15 -0
- data/lib/prosemirror/nodes/horizontal_rule.rb +12 -0
- data/lib/prosemirror/nodes/image.rb +22 -0
- data/lib/prosemirror/nodes/list_item.rb +12 -0
- data/lib/prosemirror/nodes/ordered_list.rb +36 -0
- data/lib/prosemirror/nodes/paragraph.rb +13 -0
- data/lib/prosemirror/nodes/table.rb +15 -0
- data/lib/prosemirror/nodes/table_cell.rb +33 -0
- data/lib/prosemirror/nodes/table_header.rb +15 -0
- data/lib/prosemirror/nodes/table_row.rb +12 -0
- data/lib/prosemirror/nodes/text.rb +10 -0
- data/lib/prosemirror/parse_rule.rb +50 -0
- data/lib/prosemirror/rendering.rb +70 -0
- data/lib/prosemirror/schema.rb +241 -0
- data/lib/prosemirror/text_serializer.rb +46 -0
- data/lib/prosemirror/type_definition.rb +149 -0
- data/lib/prosemirror/version.rb +1 -1
- data/lib/prosemirror.rb +43 -2
- metadata +113 -16
- data/.rubocop.yml +0 -13
- data/Gemfile +0 -12
- data/Rakefile +0 -16
- data/prosemirror.gemspec +0 -35
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
# A set of node and mark types, with rendering options.
|
|
5
|
+
#
|
|
6
|
+
# schema = Prosemirror::Schema.tiptap(case: :camel).with(ImageBlock)
|
|
7
|
+
# schema.parse(json).to_html
|
|
8
|
+
class Schema < Literal::Data
|
|
9
|
+
DEFAULT_URL_SCHEMES = %w[http https mailto tel].freeze
|
|
10
|
+
|
|
11
|
+
prop :nodes, _Array(_Class(NodeType)), &Coerce::FROZEN
|
|
12
|
+
prop :marks, _Array(_Class(MarkType)), &Coerce::FROZEN
|
|
13
|
+
prop :top_node, Symbol, default: :doc, &Coerce::SYMBOL
|
|
14
|
+
# `:snake` or `:camel`, for type names in JSON output.
|
|
15
|
+
prop :case, _Union(:snake, :camel), default: :snake, reader: false
|
|
16
|
+
# The URL schemes allowed in links and images. Relative URLs are always allowed.
|
|
17
|
+
prop :url_schemes, _Array(String), default: DEFAULT_URL_SCHEMES do |schemes|
|
|
18
|
+
schemes.map(&:downcase).freeze
|
|
19
|
+
end
|
|
20
|
+
# Called with each URL before it is checked, to rewrite it.
|
|
21
|
+
prop :resolve_url, _Callable?, reader: false
|
|
22
|
+
# Raise `InvalidDocument` instead of fixing invalid documents.
|
|
23
|
+
prop :strict, _Boolean, default: false, reader: false, predicate: :public
|
|
24
|
+
# Called with an `InvalidDocument` error for each problem fixed.
|
|
25
|
+
prop :on_invalid, _Callable?, reader: false
|
|
26
|
+
|
|
27
|
+
# Tiptap's StarterKit and common extensions.
|
|
28
|
+
def self.tiptap(**)
|
|
29
|
+
new(
|
|
30
|
+
nodes: [
|
|
31
|
+
Nodes::Doc, Nodes::Paragraph, Nodes::Text, Nodes::Heading, Nodes::Blockquote, Nodes::BulletList,
|
|
32
|
+
Nodes::OrderedList, Nodes::ListItem, Nodes::CodeBlock, Nodes::HardBreak, Nodes::HorizontalRule,
|
|
33
|
+
Nodes::Image, Nodes::Table, Nodes::TableRow, Nodes::TableHeader, Nodes::TableCell
|
|
34
|
+
],
|
|
35
|
+
# In Tiptap's order, which sorts and nests marks.
|
|
36
|
+
marks: [
|
|
37
|
+
Marks::Link, Marks::Bold, Marks::Code, Marks::Italic, Marks::Strike, Marks::Underline,
|
|
38
|
+
Marks::Subscript, Marks::Superscript, Marks::Highlight
|
|
39
|
+
],
|
|
40
|
+
**
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# ProseMirror's prosemirror-schema-basic and prosemirror-schema-list.
|
|
45
|
+
def self.prosemirror(**)
|
|
46
|
+
new(
|
|
47
|
+
nodes: [
|
|
48
|
+
Nodes::Doc, Nodes::Paragraph, Nodes::Blockquote, Nodes::HorizontalRule, Nodes::Heading,
|
|
49
|
+
Nodes::Basic::CodeBlock, Nodes::Text, Nodes::Basic::Image, Nodes::HardBreak, Nodes::Basic::OrderedList,
|
|
50
|
+
Nodes::BulletList, Nodes::ListItem
|
|
51
|
+
],
|
|
52
|
+
marks: [Marks::Basic::Link, Marks::Basic::Em, Marks::Basic::Strong, Marks::Basic::Code],
|
|
53
|
+
**
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def after_initialize
|
|
58
|
+
@node_lookup = lookup(@nodes)
|
|
59
|
+
@mark_lookup = lookup(@marks)
|
|
60
|
+
@textblocks = @nodes.to_h { |type| [type, textblock_type?(type)] }
|
|
61
|
+
@content_matches = @nodes.to_h { |type| [type, ContentMatch.compile(type.content, self)] }
|
|
62
|
+
raise ArgumentError, "the schema has no #{@top_node} node type" unless node_type(@top_node)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The start state of a node type's content expression.
|
|
66
|
+
def content_match(type)
|
|
67
|
+
@content_matches.fetch(type)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The node types with this name or in this group, in schema order.
|
|
71
|
+
def node_types_named(name)
|
|
72
|
+
@nodes.select { |type| type.type_name == name || type.group.include?(name) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A node of this type, created empty and filled with the content it requires, or nil when its
|
|
76
|
+
# type, or one it requires, has required attributes.
|
|
77
|
+
def create_and_fill(type)
|
|
78
|
+
return nil if type.required_attrs?
|
|
79
|
+
|
|
80
|
+
fill = content_match(type).fill_before([], self, to_end: true)
|
|
81
|
+
content = fill&.map { |child| create_and_fill(child) }
|
|
82
|
+
return nil if content.nil? || content.include?(nil)
|
|
83
|
+
|
|
84
|
+
Node.new(type: type.type_name, attrs: type.default_attrs, content: content)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A copy of the schema with other options.
|
|
88
|
+
def with_options(**)
|
|
89
|
+
self.class.new(**to_h, **)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A copy of the schema with more types. A type replaces the type with the same name.
|
|
93
|
+
def with(*types)
|
|
94
|
+
nodes = types.select { |type| type.is_a?(Class) && type < NodeType }
|
|
95
|
+
marks = types.select { |type| type.is_a?(Class) && type < MarkType }
|
|
96
|
+
raise ArgumentError, "not a node or mark type" if nodes.size + marks.size != types.size
|
|
97
|
+
|
|
98
|
+
self.class.new(**to_h, nodes: merge_types(@nodes, nodes), marks: merge_types(@marks, marks))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The node type for a name, alias, or camelCase name.
|
|
102
|
+
def node_type(name)
|
|
103
|
+
@node_lookup[Naming.snake(name).to_sym]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def mark_type(name)
|
|
107
|
+
@mark_lookup[Naming.snake(name).to_sym]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Build a document from JSON, given as a Hash or a String.
|
|
111
|
+
def parse(json)
|
|
112
|
+
Document.new(JSONParser.new(self).parse(json), schema: self)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Build a document from HTML, with the parse rules of the schema's types. Needs the `herb` gem.
|
|
116
|
+
def parse_html(html)
|
|
117
|
+
Document.new(HTMLParser.new(self).parse(html), schema: self)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The type name as written in JSON.
|
|
121
|
+
def json_type_name(name)
|
|
122
|
+
(@case == :camel) ? Naming.camel(name) : name.to_s
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The set of marks with this mark added, in schema order, as ProseMirror's `Mark#addToSet`: a mark
|
|
126
|
+
# replaces the marks it excludes, and isn't added when a mark of the set excludes it.
|
|
127
|
+
def add_mark(mark, set)
|
|
128
|
+
type = mark_type(mark.type)
|
|
129
|
+
result = []
|
|
130
|
+
placed = false
|
|
131
|
+
set.each do |other|
|
|
132
|
+
return set if other == mark
|
|
133
|
+
|
|
134
|
+
other_type = mark_type(other.type)
|
|
135
|
+
next if type.excludes?(other_type)
|
|
136
|
+
return set if other_type.excludes?(type)
|
|
137
|
+
|
|
138
|
+
if !placed && mark_rank(other.type) > mark_rank(mark.type)
|
|
139
|
+
result << mark
|
|
140
|
+
placed = true
|
|
141
|
+
end
|
|
142
|
+
result << other
|
|
143
|
+
end
|
|
144
|
+
result << mark unless placed
|
|
145
|
+
result
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Whether a URL's scheme is allowed, or the URL is relative. Browsers ignore leading control characters
|
|
149
|
+
# and spaces, and tabs and newlines anywhere in a URL.
|
|
150
|
+
def allowed_url?(url)
|
|
151
|
+
return false unless url.is_a?(String)
|
|
152
|
+
|
|
153
|
+
normalized = url.delete("\t\n\r").sub(/\A[\x00-\x20]+/, "")
|
|
154
|
+
scheme = normalized[%r{\A([^/?#]*):}, 1]
|
|
155
|
+
return true if scheme.nil?
|
|
156
|
+
|
|
157
|
+
scheme.match?(/\A[a-z][a-z\d+.-]*\z/i) && @url_schemes.include?(scheme.downcase)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# The order of marks in a node. Marks of unknown types come last.
|
|
161
|
+
def mark_rank(name)
|
|
162
|
+
@marks.index(mark_type(name)) || @marks.size
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Whether nodes of this type contain inline content, like a paragraph.
|
|
166
|
+
def textblock?(type)
|
|
167
|
+
@textblocks.fetch(type, false)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The URL after `resolve_url`, when its scheme is allowed. Otherwise nil, and the problem is reported.
|
|
171
|
+
def safe_url(url)
|
|
172
|
+
return nil unless url.is_a?(String)
|
|
173
|
+
|
|
174
|
+
resolved = @resolve_url ? @resolve_url.call(url) : url
|
|
175
|
+
return report("URL could not be resolved: #{url.inspect}") unless resolved.is_a?(String)
|
|
176
|
+
return report("URL scheme not allowed: #{resolved.inspect}") unless allowed_url?(resolved)
|
|
177
|
+
|
|
178
|
+
resolved.gsub("&", "&")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Raise in strict mode, otherwise call `on_invalid`.
|
|
182
|
+
def report(message, path: nil)
|
|
183
|
+
error = InvalidDocument.new(message, path: path)
|
|
184
|
+
raise error if @strict
|
|
185
|
+
|
|
186
|
+
@on_invalid&.call(error)
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The document as JSON, as a Hash.
|
|
191
|
+
def serialize(node)
|
|
192
|
+
hash = {"type" => json_type_name(node.type)}
|
|
193
|
+
hash["text"] = node.text if node.text?
|
|
194
|
+
attrs = serialize_node_attrs(node)
|
|
195
|
+
hash["attrs"] = attrs unless attrs.empty?
|
|
196
|
+
hash["content"] = node.content.map { |child| serialize(child) } unless node.content.empty?
|
|
197
|
+
hash["marks"] = node.marks.map { |mark| serialize_mark(mark) } unless node.marks.empty?
|
|
198
|
+
hash
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def lookup(types)
|
|
204
|
+
types.each_with_object({}) do |type, table|
|
|
205
|
+
[type.type_name, *type.aliases].each { |name| table[name] ||= type }
|
|
206
|
+
table[type.type_name] = type
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def merge_types(types, additions)
|
|
211
|
+
additions.reduce(types) do |list, addition|
|
|
212
|
+
index = list.index { |type| type.type_name == addition.type_name }
|
|
213
|
+
index ? list.dup.tap { |copy| copy[index] = addition } : list + [addition]
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def textblock_type?(type)
|
|
218
|
+
return false if type.leaf?
|
|
219
|
+
|
|
220
|
+
type.content.names.any? do |name|
|
|
221
|
+
name == :text || name == :inline || node_types_named(name).any?(&:inline?)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Declared attributes with their JSON names, then the unknown ones, as they were.
|
|
226
|
+
def serialize_node_attrs(node)
|
|
227
|
+
type = node_type(node.type)
|
|
228
|
+
attrs = type ? type.serialize_attrs(node.attrs) : node.attrs.transform_keys(&:to_s)
|
|
229
|
+
attrs.merge(node.unknown_attrs)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def serialize_mark(mark)
|
|
233
|
+
hash = {"type" => json_type_name(mark.type)}
|
|
234
|
+
type = mark_type(mark.type)
|
|
235
|
+
attrs = type ? type.serialize_attrs(mark.attrs) : mark.attrs.transform_keys(&:to_s)
|
|
236
|
+
attrs = attrs.merge(mark.unknown_attrs)
|
|
237
|
+
hash["attrs"] = attrs unless attrs.empty?
|
|
238
|
+
hash
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
# Extracts the text of a document, as ProseMirror's `textBetween` and Tiptap's `getText` do:
|
|
5
|
+
# textblocks and leaf blocks with text are separated by `block_separator`,
|
|
6
|
+
# and leaf nodes such as hard breaks are replaced by their `leaf_text`.
|
|
7
|
+
class TextSerializer
|
|
8
|
+
def initialize(schema, block_separator: "\n\n")
|
|
9
|
+
@schema = schema
|
|
10
|
+
@block_separator = block_separator
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(root)
|
|
14
|
+
text = +""
|
|
15
|
+
first = true
|
|
16
|
+
|
|
17
|
+
root.descendants do |node|
|
|
18
|
+
node_type = @schema.node_type(node.type)
|
|
19
|
+
node_text = text_of(node, node_type)
|
|
20
|
+
|
|
21
|
+
if separated?(node_type, node_text)
|
|
22
|
+
text << @block_separator unless first
|
|
23
|
+
first = false
|
|
24
|
+
end
|
|
25
|
+
text << node_text
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
text
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def text_of(node, node_type)
|
|
34
|
+
return node.text if node.text?
|
|
35
|
+
return "" unless node_type&.leaf?
|
|
36
|
+
|
|
37
|
+
node_type.text_for(node)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def separated?(node_type, node_text)
|
|
41
|
+
return false unless node_type&.block?
|
|
42
|
+
|
|
43
|
+
node_type.leaf? ? !node_text.empty? : @schema.textblock?(node_type)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosemirror
|
|
4
|
+
# Class-level declarations shared by node types and mark types.
|
|
5
|
+
module TypeDefinition
|
|
6
|
+
# The name of the type, as a snake_case Symbol. It defaults to the class name (`BulletList` → `:bullet_list`),
|
|
7
|
+
# and subclasses of a concrete type keep its name.
|
|
8
|
+
def type_name(name = nil)
|
|
9
|
+
return @type_name = Naming.snake(name).to_sym if name
|
|
10
|
+
|
|
11
|
+
@type_name ||= concrete_superclass&.type_name || derived_type_name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Other names accepted on input for this type, such as `:strong` for `:bold`.
|
|
15
|
+
def aliases(*names)
|
|
16
|
+
return @aliases = names.map { |n| Naming.snake(n).to_sym } unless names.empty?
|
|
17
|
+
|
|
18
|
+
inherited_setting(:@aliases, [])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Declare an attribute. Same as Literal's `prop`, with these options:
|
|
22
|
+
# - `json:` the attribute name in JSON, when it differs from the property name (`"textAlign"`);
|
|
23
|
+
# - `aliases:` other JSON names accepted on input (`["order"]`);
|
|
24
|
+
# - `parse_html:` a block reading the attribute from an HTML element, when importing HTML. By default,
|
|
25
|
+
# the HTML attribute with the JSON name is read.
|
|
26
|
+
def prop(name, type, kind = :keyword, json: nil, aliases: [], parse_html: nil, **, &)
|
|
27
|
+
attribute_names[name] = {json: (json || name).to_s, aliases: aliases.map(&:to_s), parse_html:}.freeze
|
|
28
|
+
super(name, type, kind, **, &)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Declare how to recognize this type in HTML. See ParseRule for the options. The block, if given, is the
|
|
32
|
+
# rule's `get_attrs`. A subclass that declares rules replaces the rules of its parent.
|
|
33
|
+
def parse_html(tag: nil, style: nil, **options, &get_attrs)
|
|
34
|
+
(@parse_rules ||= []) << ParseRule.new(type: self, tag:, style:, get_attrs:, **options)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_rules
|
|
38
|
+
inherited_setting(:@parse_rules, [])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The attributes read from an HTML element, by JSON name, without the missing ones.
|
|
42
|
+
def attrs_from_html(element)
|
|
43
|
+
attribute_names.each_value.with_object({}) do |names, attrs|
|
|
44
|
+
value = names[:parse_html] ? names[:parse_html].call(element) : element[names[:json]]
|
|
45
|
+
attrs[names[:json]] = value unless value.nil?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def attribute_names
|
|
50
|
+
@attribute_names ||= concrete_superclass ? concrete_superclass.attribute_names.dup : {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Build valid attributes from JSON attributes (a Hash with String or Symbol keys).
|
|
54
|
+
# Invalid values are converted when the meaning is clear, or replaced by their default.
|
|
55
|
+
# Returns nil when a required attribute is missing or invalid.
|
|
56
|
+
def build_attrs(given, report:)
|
|
57
|
+
given = given.is_a?(Hash) ? given.transform_keys(&:to_s) : {}
|
|
58
|
+
|
|
59
|
+
literal_properties.each_with_object({}) do |property, attrs|
|
|
60
|
+
json_name = attribute_names.dig(property.name, :json)
|
|
61
|
+
key = attribute_key(given, attribute_names.fetch(property.name))
|
|
62
|
+
|
|
63
|
+
if key
|
|
64
|
+
value = coerce_attribute(property, given[key])
|
|
65
|
+
next attrs[property.name] = value if property.type === value
|
|
66
|
+
|
|
67
|
+
report.call("invalid attribute #{json_name}: #{given[key].inspect}")
|
|
68
|
+
elsif !optional_attribute?(property)
|
|
69
|
+
report.call("missing attribute #{json_name}")
|
|
70
|
+
end
|
|
71
|
+
return nil unless optional_attribute?(property)
|
|
72
|
+
|
|
73
|
+
attrs[property.name] = default_attribute(property)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Whether an attribute has no default, so that a node of this type can't be created empty.
|
|
78
|
+
def required_attrs?
|
|
79
|
+
literal_properties.any? { |property| !optional_attribute?(property) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The attributes of a node of this type created empty: the defaults.
|
|
83
|
+
def default_attrs
|
|
84
|
+
build_attrs({}, report: ->(_) {})
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The JSON attributes this type doesn't declare, with String keys, so that `to_h` gives them back.
|
|
88
|
+
def unknown_attrs(given)
|
|
89
|
+
return {} unless given.is_a?(Hash)
|
|
90
|
+
|
|
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
|
+
given.transform_keys(&:to_s).except(*known)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Attributes as JSON, with their JSON names.
|
|
98
|
+
def serialize_attrs(attrs)
|
|
99
|
+
attrs.to_h { |name, value| [attribute_names.dig(name, :json) || name.to_s, value] }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def base_type?
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def concrete_superclass
|
|
109
|
+
superclass unless superclass.base_type?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def derived_type_name
|
|
113
|
+
raise ArgumentError, "anonymous types need a type_name" if name.nil?
|
|
114
|
+
|
|
115
|
+
Naming.snake(name.split("::").last).to_sym
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def inherited_setting(ivar, default)
|
|
119
|
+
return instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
|
120
|
+
return default if superclass.base_type?
|
|
121
|
+
|
|
122
|
+
superclass.send(:inherited_setting, ivar, default)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def attribute_key(given, names)
|
|
126
|
+
[names[:json], *names[:aliases]].each do |name|
|
|
127
|
+
[name, Naming.snake(name), Naming.camel(name)].each do |candidate|
|
|
128
|
+
return candidate if given.key?(candidate)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def coerce_attribute(property, value)
|
|
135
|
+
property.coercion ? property.coerce(value, context: self) : value
|
|
136
|
+
rescue
|
|
137
|
+
value
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def optional_attribute?(property)
|
|
141
|
+
# Checks whether the Literal type accepts nil, so `.nil?` would be wrong here.
|
|
142
|
+
property.default? || property.type === nil # standard:disable Style/NilComparison
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def default_attribute(property)
|
|
146
|
+
property.default? ? property.default_value(self) : nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
data/lib/prosemirror/version.rb
CHANGED
data/lib/prosemirror.rb
CHANGED
|
@@ -1,8 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Phlex 2.4.1 checks attribute values against Date without loading it. Remove once a release includes the fix:
|
|
4
|
+
# https://github.com/yippee-fun/phlex/issues/974
|
|
5
|
+
require "date"
|
|
6
|
+
require "json"
|
|
7
|
+
require "literal"
|
|
8
|
+
require "phlex"
|
|
9
|
+
require "zeitwerk"
|
|
4
10
|
|
|
11
|
+
loader = Zeitwerk::Loader.for_gem
|
|
12
|
+
loader.inflector.inflect(
|
|
13
|
+
"html" => "HTML", "html_parser" => "HTMLParser", "html_renderer" => "HTMLRenderer", "json_parser" => "JSONParser"
|
|
14
|
+
)
|
|
15
|
+
loader.setup
|
|
16
|
+
|
|
17
|
+
# Render ProseMirror and Tiptap documents.
|
|
5
18
|
module Prosemirror
|
|
6
19
|
class Error < StandardError; end
|
|
7
|
-
|
|
20
|
+
|
|
21
|
+
# Raised in strict mode, and passed to `on_invalid`, when a document doesn't match its schema.
|
|
22
|
+
class InvalidDocument < Error
|
|
23
|
+
attr_reader :path
|
|
24
|
+
|
|
25
|
+
def initialize(message, path: nil)
|
|
26
|
+
@path = path
|
|
27
|
+
super(path ? "#{path}: #{message}" : message)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
attr_writer :schema
|
|
33
|
+
|
|
34
|
+
# The schema used when none is given. Defaults to `Schema.tiptap`.
|
|
35
|
+
def schema
|
|
36
|
+
@schema ||= Schema.tiptap
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Build a document from ProseMirror JSON, given as a Hash or a String.
|
|
40
|
+
def parse(json, schema: self.schema)
|
|
41
|
+
schema.parse(json)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Build a document from HTML, with the schema's parse rules. Needs the `herb` gem.
|
|
45
|
+
def from_html(html, schema: self.schema)
|
|
46
|
+
schema.parse_html(html)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
8
49
|
end
|
metadata
CHANGED
|
@@ -1,38 +1,136 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: prosemirror
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hans Lemuet
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
-
dependencies:
|
|
13
|
-
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: literal
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.9'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: phlex
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.4'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.4'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: zeitwerk
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.6'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.6'
|
|
54
|
+
description: Render ProseMirror and Tiptap JSON documents to safe HTML or plain text,
|
|
55
|
+
with typed attributes and custom node types.
|
|
14
56
|
email:
|
|
15
57
|
- hans@etamin.studio
|
|
16
58
|
executables: []
|
|
17
59
|
extensions: []
|
|
18
60
|
extra_rdoc_files: []
|
|
19
61
|
files:
|
|
20
|
-
- ".rubocop.yml"
|
|
21
62
|
- CHANGELOG.md
|
|
22
|
-
-
|
|
63
|
+
- COOKBOOK.md
|
|
64
|
+
- LICENSE.txt
|
|
23
65
|
- README.md
|
|
24
|
-
-
|
|
66
|
+
- ROADMAP.md
|
|
67
|
+
- TYPES.md
|
|
25
68
|
- lib/prosemirror.rb
|
|
69
|
+
- lib/prosemirror/coerce.rb
|
|
70
|
+
- lib/prosemirror/content_expression.rb
|
|
71
|
+
- lib/prosemirror/content_match.rb
|
|
72
|
+
- lib/prosemirror/document.rb
|
|
73
|
+
- lib/prosemirror/html/builder.rb
|
|
74
|
+
- lib/prosemirror/html/element.rb
|
|
75
|
+
- lib/prosemirror/html/entities.rb
|
|
76
|
+
- lib/prosemirror/html/entities.txt
|
|
77
|
+
- lib/prosemirror/html/selector.rb
|
|
78
|
+
- lib/prosemirror/html/splitter.rb
|
|
79
|
+
- lib/prosemirror/html/text.rb
|
|
80
|
+
- lib/prosemirror/html_parser.rb
|
|
81
|
+
- lib/prosemirror/html_renderer.rb
|
|
82
|
+
- lib/prosemirror/json_parser.rb
|
|
83
|
+
- lib/prosemirror/mark.rb
|
|
84
|
+
- lib/prosemirror/mark_type.rb
|
|
85
|
+
- lib/prosemirror/marks/basic/code.rb
|
|
86
|
+
- lib/prosemirror/marks/basic/em.rb
|
|
87
|
+
- lib/prosemirror/marks/basic/link.rb
|
|
88
|
+
- lib/prosemirror/marks/basic/strong.rb
|
|
89
|
+
- lib/prosemirror/marks/bold.rb
|
|
90
|
+
- lib/prosemirror/marks/code.rb
|
|
91
|
+
- lib/prosemirror/marks/highlight.rb
|
|
92
|
+
- lib/prosemirror/marks/italic.rb
|
|
93
|
+
- lib/prosemirror/marks/link.rb
|
|
94
|
+
- lib/prosemirror/marks/strike.rb
|
|
95
|
+
- lib/prosemirror/marks/subscript.rb
|
|
96
|
+
- lib/prosemirror/marks/superscript.rb
|
|
97
|
+
- lib/prosemirror/marks/underline.rb
|
|
98
|
+
- lib/prosemirror/naming.rb
|
|
99
|
+
- lib/prosemirror/node.rb
|
|
100
|
+
- lib/prosemirror/node_type.rb
|
|
101
|
+
- lib/prosemirror/nodes/basic/code_block.rb
|
|
102
|
+
- lib/prosemirror/nodes/basic/image.rb
|
|
103
|
+
- lib/prosemirror/nodes/basic/ordered_list.rb
|
|
104
|
+
- lib/prosemirror/nodes/blockquote.rb
|
|
105
|
+
- lib/prosemirror/nodes/bullet_list.rb
|
|
106
|
+
- lib/prosemirror/nodes/code_block.rb
|
|
107
|
+
- lib/prosemirror/nodes/doc.rb
|
|
108
|
+
- lib/prosemirror/nodes/hard_break.rb
|
|
109
|
+
- lib/prosemirror/nodes/heading.rb
|
|
110
|
+
- lib/prosemirror/nodes/horizontal_rule.rb
|
|
111
|
+
- lib/prosemirror/nodes/image.rb
|
|
112
|
+
- lib/prosemirror/nodes/list_item.rb
|
|
113
|
+
- lib/prosemirror/nodes/ordered_list.rb
|
|
114
|
+
- lib/prosemirror/nodes/paragraph.rb
|
|
115
|
+
- lib/prosemirror/nodes/table.rb
|
|
116
|
+
- lib/prosemirror/nodes/table_cell.rb
|
|
117
|
+
- lib/prosemirror/nodes/table_header.rb
|
|
118
|
+
- lib/prosemirror/nodes/table_row.rb
|
|
119
|
+
- lib/prosemirror/nodes/text.rb
|
|
120
|
+
- lib/prosemirror/parse_rule.rb
|
|
121
|
+
- lib/prosemirror/rendering.rb
|
|
122
|
+
- lib/prosemirror/schema.rb
|
|
123
|
+
- lib/prosemirror/text_serializer.rb
|
|
124
|
+
- lib/prosemirror/type_definition.rb
|
|
26
125
|
- lib/prosemirror/version.rb
|
|
27
|
-
- prosemirror.gemspec
|
|
28
126
|
- sig/prosemirror.rbs
|
|
29
127
|
homepage: https://github.com/etaminstudio/prosemirror
|
|
30
|
-
licenses:
|
|
128
|
+
licenses:
|
|
129
|
+
- MIT
|
|
31
130
|
metadata:
|
|
32
|
-
homepage_uri: https://github.com/etaminstudio/prosemirror
|
|
33
131
|
source_code_uri: https://github.com/etaminstudio/prosemirror
|
|
34
132
|
changelog_uri: https://github.com/etaminstudio/prosemirror/blob/main/CHANGELOG.md
|
|
35
|
-
|
|
133
|
+
rubygems_mfa_required: 'true'
|
|
36
134
|
rdoc_options: []
|
|
37
135
|
require_paths:
|
|
38
136
|
- lib
|
|
@@ -40,15 +138,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
40
138
|
requirements:
|
|
41
139
|
- - ">="
|
|
42
140
|
- !ruby/object:Gem::Version
|
|
43
|
-
version:
|
|
141
|
+
version: 3.3.0
|
|
44
142
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
143
|
requirements:
|
|
46
144
|
- - ">="
|
|
47
145
|
- !ruby/object:Gem::Version
|
|
48
146
|
version: '0'
|
|
49
147
|
requirements: []
|
|
50
|
-
rubygems_version:
|
|
51
|
-
signing_key:
|
|
148
|
+
rubygems_version: 4.0.20
|
|
52
149
|
specification_version: 4
|
|
53
|
-
summary:
|
|
150
|
+
summary: Render ProseMirror and Tiptap documents to HTML and text.
|
|
54
151
|
test_files: []
|
data/.rubocop.yml
DELETED