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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/COOKBOOK.md +108 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +155 -12
  6. data/ROADMAP.md +215 -0
  7. data/TYPES.md +201 -0
  8. data/lib/prosemirror/coerce.rb +17 -0
  9. data/lib/prosemirror/content_expression.rb +94 -0
  10. data/lib/prosemirror/content_match.rb +241 -0
  11. data/lib/prosemirror/document.rb +76 -0
  12. data/lib/prosemirror/html/builder.rb +146 -0
  13. data/lib/prosemirror/html/element.rb +84 -0
  14. data/lib/prosemirror/html/entities.rb +87 -0
  15. data/lib/prosemirror/html/entities.txt +2231 -0
  16. data/lib/prosemirror/html/selector.rb +95 -0
  17. data/lib/prosemirror/html/splitter.rb +68 -0
  18. data/lib/prosemirror/html/text.rb +23 -0
  19. data/lib/prosemirror/html_parser.rb +498 -0
  20. data/lib/prosemirror/html_renderer.rb +20 -0
  21. data/lib/prosemirror/json_parser.rb +114 -0
  22. data/lib/prosemirror/mark.rb +19 -0
  23. data/lib/prosemirror/mark_type.rb +71 -0
  24. data/lib/prosemirror/marks/basic/code.rb +16 -0
  25. data/lib/prosemirror/marks/basic/em.rb +17 -0
  26. data/lib/prosemirror/marks/basic/link.rb +21 -0
  27. data/lib/prosemirror/marks/basic/strong.rb +17 -0
  28. data/lib/prosemirror/marks/bold.rb +19 -0
  29. data/lib/prosemirror/marks/code.rb +16 -0
  30. data/lib/prosemirror/marks/highlight.rb +18 -0
  31. data/lib/prosemirror/marks/italic.rb +15 -0
  32. data/lib/prosemirror/marks/link.rb +22 -0
  33. data/lib/prosemirror/marks/strike.rb +14 -0
  34. data/lib/prosemirror/marks/subscript.rb +13 -0
  35. data/lib/prosemirror/marks/superscript.rb +13 -0
  36. data/lib/prosemirror/marks/underline.rb +12 -0
  37. data/lib/prosemirror/naming.rb +16 -0
  38. data/lib/prosemirror/node.rb +77 -0
  39. data/lib/prosemirror/node_type.rb +123 -0
  40. data/lib/prosemirror/nodes/basic/code_block.rb +20 -0
  41. data/lib/prosemirror/nodes/basic/image.rb +23 -0
  42. data/lib/prosemirror/nodes/basic/ordered_list.rb +22 -0
  43. data/lib/prosemirror/nodes/blockquote.rb +13 -0
  44. data/lib/prosemirror/nodes/bullet_list.rb +13 -0
  45. data/lib/prosemirror/nodes/code_block.rb +24 -0
  46. data/lib/prosemirror/nodes/doc.rb +10 -0
  47. data/lib/prosemirror/nodes/hard_break.rb +15 -0
  48. data/lib/prosemirror/nodes/heading.rb +15 -0
  49. data/lib/prosemirror/nodes/horizontal_rule.rb +12 -0
  50. data/lib/prosemirror/nodes/image.rb +22 -0
  51. data/lib/prosemirror/nodes/list_item.rb +12 -0
  52. data/lib/prosemirror/nodes/ordered_list.rb +36 -0
  53. data/lib/prosemirror/nodes/paragraph.rb +13 -0
  54. data/lib/prosemirror/nodes/table.rb +15 -0
  55. data/lib/prosemirror/nodes/table_cell.rb +33 -0
  56. data/lib/prosemirror/nodes/table_header.rb +15 -0
  57. data/lib/prosemirror/nodes/table_row.rb +12 -0
  58. data/lib/prosemirror/nodes/text.rb +10 -0
  59. data/lib/prosemirror/parse_rule.rb +50 -0
  60. data/lib/prosemirror/rendering.rb +70 -0
  61. data/lib/prosemirror/schema.rb +241 -0
  62. data/lib/prosemirror/text_serializer.rb +46 -0
  63. data/lib/prosemirror/type_definition.rb +149 -0
  64. data/lib/prosemirror/version.rb +1 -1
  65. data/lib/prosemirror.rb +43 -2
  66. metadata +113 -16
  67. data/.rubocop.yml +0 -13
  68. data/Gemfile +0 -12
  69. data/Rakefile +0 -16
  70. 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("&", "&amp;")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prosemirror
4
- VERSION = "0.0.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/prosemirror.rb CHANGED
@@ -1,8 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "prosemirror/version"
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
- # Your code goes here...
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.1
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: 2023-07-03 00:00:00.000000000 Z
12
- dependencies: []
13
- description:
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
- - Gemfile
63
+ - COOKBOOK.md
64
+ - LICENSE.txt
23
65
  - README.md
24
- - Rakefile
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
- post_install_message:
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: 2.6.0
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: 3.3.26
51
- signing_key:
148
+ rubygems_version: 4.0.20
52
149
  specification_version: 4
53
- summary: Convert HTML to ProseMirror JSON and back.
150
+ summary: Render ProseMirror and Tiptap documents to HTML and text.
54
151
  test_files: []
data/.rubocop.yml DELETED
@@ -1,13 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.6
3
-
4
- Style/StringLiterals:
5
- Enabled: true
6
- EnforcedStyle: double_quotes
7
-
8
- Style/StringLiteralsInInterpolation:
9
- Enabled: true
10
- EnforcedStyle: double_quotes
11
-
12
- Layout/LineLength:
13
- Max: 120
data/Gemfile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- # Specify your gem's dependencies in prosemirror.gemspec
6
- gemspec
7
-
8
- gem "rake", "~> 13.0"
9
-
10
- gem "minitest", "~> 5.0"
11
-
12
- gem "rubocop", "~> 1.21"