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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.md +69 -46
  4. data/ROADMAP.md +14 -10
  5. data/TYPES.md +218 -0
  6. data/lib/prosemirror/content_match.rb +241 -0
  7. data/lib/prosemirror/html/builder.rb +154 -0
  8. data/lib/prosemirror/html/element.rb +84 -0
  9. data/lib/prosemirror/html/entities.rb +87 -0
  10. data/lib/prosemirror/html/entities.txt +2231 -0
  11. data/lib/prosemirror/html/selector.rb +95 -0
  12. data/lib/prosemirror/html/splitter.rb +68 -0
  13. data/lib/prosemirror/html/text.rb +23 -0
  14. data/lib/prosemirror/html_parser.rb +503 -0
  15. data/lib/prosemirror/json_parser.rb +1 -12
  16. data/lib/prosemirror/mark_type.rb +11 -0
  17. data/lib/prosemirror/markdown/builder.rb +200 -0
  18. data/lib/prosemirror/marks/basic/code.rb +16 -0
  19. data/lib/prosemirror/marks/basic/em.rb +4 -0
  20. data/lib/prosemirror/marks/basic/link.rb +1 -0
  21. data/lib/prosemirror/marks/basic/strong.rb +4 -0
  22. data/lib/prosemirror/marks/bold.rb +8 -0
  23. data/lib/prosemirror/marks/code.rb +5 -0
  24. data/lib/prosemirror/marks/highlight.rb +3 -1
  25. data/lib/prosemirror/marks/italic.rb +4 -0
  26. data/lib/prosemirror/marks/link.rb +1 -0
  27. data/lib/prosemirror/marks/strike.rb +5 -0
  28. data/lib/prosemirror/marks/subscript.rb +2 -0
  29. data/lib/prosemirror/marks/superscript.rb +2 -0
  30. data/lib/prosemirror/marks/underline.rb +3 -0
  31. data/lib/prosemirror/node.rb +12 -0
  32. data/lib/prosemirror/node_type.rb +22 -0
  33. data/lib/prosemirror/nodes/basic/code_block.rb +1 -0
  34. data/lib/prosemirror/nodes/basic/image.rb +1 -0
  35. data/lib/prosemirror/nodes/basic/ordered_list.rb +4 -1
  36. data/lib/prosemirror/nodes/blockquote.rb +1 -0
  37. data/lib/prosemirror/nodes/bullet_list.rb +1 -0
  38. data/lib/prosemirror/nodes/code_block.rb +6 -1
  39. data/lib/prosemirror/nodes/hard_break.rb +2 -0
  40. data/lib/prosemirror/nodes/heading.rb +1 -0
  41. data/lib/prosemirror/nodes/horizontal_rule.rb +1 -0
  42. data/lib/prosemirror/nodes/image.rb +1 -0
  43. data/lib/prosemirror/nodes/list_item.rb +1 -0
  44. data/lib/prosemirror/nodes/ordered_list.rb +20 -2
  45. data/lib/prosemirror/nodes/paragraph.rb +1 -0
  46. data/lib/prosemirror/nodes/table.rb +1 -0
  47. data/lib/prosemirror/nodes/table_cell.rb +12 -2
  48. data/lib/prosemirror/nodes/table_header.rb +2 -0
  49. data/lib/prosemirror/nodes/table_row.rb +1 -0
  50. data/lib/prosemirror/parse_rule.rb +50 -0
  51. data/lib/prosemirror/schema.rb +87 -15
  52. data/lib/prosemirror/type_definition.rb +34 -4
  53. data/lib/prosemirror/utf8.rb +16 -0
  54. data/lib/prosemirror/version.rb +1 -1
  55. data/lib/prosemirror.rb +22 -1
  56. metadata +15 -1
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module HTML
5
+ # The CSS selectors parse rules need, matched against one element: lists of compound selectors,
6
+ # with a type (`p`, `*`), `#id`, `.class`, attribute selectors (`[href]`, `[a=b]`, `[a^=b]`,
7
+ # `[a$=b]`, `[a*=b]`, `[a~=b]`, `[a|=b]`) and `:not(…)`. Combinators aren't supported.
8
+ class Selector
9
+ IDENTIFIER = /-?[_a-zA-Z][-\w]*/
10
+ ATTRIBUTE = /\[\s*(#{IDENTIFIER})\s*(?:([~|^$*]?=)\s*(?:"([^"]*)"|'([^']*)'|(#{IDENTIFIER}))\s*)?\]/
11
+
12
+ def self.parse(source)
13
+ (@cache ||= {})[source] ||= new(source)
14
+ end
15
+
16
+ def initialize(source)
17
+ @source = source
18
+ @compounds = source.split(",").map { |part| parse_compound(part.strip) }
19
+ end
20
+
21
+ def match?(element)
22
+ @compounds.any? { |tests| tests.all? { |test| test.call(element) } }
23
+ end
24
+
25
+ private
26
+
27
+ def parse_compound(part)
28
+ invalid! if part.empty?
29
+
30
+ tests = []
31
+ rest = part
32
+ if (type = rest[/\A(?:\*|#{IDENTIFIER})/o])
33
+ tests << ->(element) { element.name == type.downcase } unless type == "*"
34
+ rest = rest[type.length..]
35
+ end
36
+ until rest.empty?
37
+ test, length = parse_simple(rest)
38
+ tests << test
39
+ rest = rest[length..]
40
+ end
41
+ tests
42
+ end
43
+
44
+ # A simple selector at the start of `rest`, as a test and the length it takes.
45
+ def parse_simple(rest)
46
+ if (match = /\A#(#{IDENTIFIER})/o.match(rest))
47
+ [id_test(match[1]), match[0].length]
48
+ elsif (match = /\A\.(#{IDENTIFIER})/o.match(rest))
49
+ [class_test(match[1]), match[0].length]
50
+ elsif (match = /\A#{ATTRIBUTE}/o.match(rest))
51
+ [attribute_test(*match.captures), match[0].length]
52
+ elsif (match = /\A:not\(([^()]*)\)/.match(rest))
53
+ [not_test(parse_compound(match[1].strip)), match[0].length]
54
+ else
55
+ invalid!
56
+ end
57
+ end
58
+
59
+ def id_test(id)
60
+ ->(element) { element["id"] == id }
61
+ end
62
+
63
+ def class_test(name)
64
+ ->(element) { element.classes.include?(name) }
65
+ end
66
+
67
+ def attribute_test(name, operator, double_quoted, single_quoted, bare)
68
+ name = name.downcase
69
+ expected = double_quoted || single_quoted || bare
70
+ lambda do |element|
71
+ value = element[name]
72
+ next false if value.nil?
73
+
74
+ case operator
75
+ when nil then true
76
+ when "=" then value == expected
77
+ when "^=" then !expected.empty? && value.start_with?(expected)
78
+ when "$=" then !expected.empty? && value.end_with?(expected)
79
+ when "*=" then !expected.empty? && value.include?(expected)
80
+ when "~=" then value.split.include?(expected)
81
+ when "|=" then value == expected || value.start_with?("#{expected}-")
82
+ end
83
+ end
84
+ end
85
+
86
+ def not_test(tests)
87
+ ->(element) { !tests.all? { |test| test.call(element) } }
88
+ end
89
+
90
+ def invalid!
91
+ raise ArgumentError, "unsupported selector #{@source.inspect}: only compound selectors, without combinators"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Prosemirror
6
+ module HTML
7
+ # Splits HTML into pieces of about `size` bytes, between top-level elements, so Herb can parse
8
+ # them one at a time: its memory grows to hundreds of times the size of the input it parses
9
+ # (https://github.com/marcoroth/herb/issues/2839). Once Herb fixes it, this class can probably be removed.
10
+ #
11
+ # It only cuts where it can follow the structure: when a closing tag doesn't match the last open
12
+ # element, as with unclosed or implicitly closed elements, the rest stays in one piece. HTML with
13
+ # ERB tags isn't split, since Herb groups ERB blocks with the HTML between them.
14
+ class Splitter
15
+ # As Herb's lists.
16
+ VOID_TAGS = %w[
17
+ area base basefont bgsound br col embed frame hr img input keygen link meta param source track wbr
18
+ ].to_set.freeze
19
+ RAW_TEXT_TAGS = %w[script style iframe xmp noembed noframes textarea title].to_set.freeze
20
+
21
+ NAME = /[a-zA-Z][^\s\/>]*/
22
+ # The rest of an opening tag: attributes, with quoted values that can hold `>`.
23
+ TAG_REST = /(?:[^>"']|"[^"]*"|'[^']*')*>/
24
+
25
+ def initialize(size)
26
+ @size = size
27
+ end
28
+
29
+ def call(html)
30
+ return [html] if html.bytesize <= @size || html.include?("<%")
31
+
32
+ pieces = []
33
+ start = 0
34
+ open = []
35
+ scanner = StringScanner.new(html)
36
+ until scanner.eos?
37
+ break unless scanner.skip_until(/</)
38
+
39
+ if scanner.skip(/!--/)
40
+ break unless scanner.skip_until(/-->/)
41
+ elsif scanner.skip(/[!?]/)
42
+ break unless scanner.skip_until(/>/)
43
+ elsif scanner.skip(%r{/(#{NAME})\s*>}o)
44
+ break unless open.pop == scanner[1].downcase
45
+ elsif scanner.skip(/(#{NAME})/o)
46
+ name = scanner[1].downcase
47
+ break unless (rest = scanner.scan(TAG_REST))
48
+
49
+ if RAW_TEXT_TAGS.include?(name)
50
+ break unless scanner.skip_until(%r{</#{Regexp.escape(name)}\s*>}i)
51
+ elsif !VOID_TAGS.include?(name) && !rest.end_with?("/>")
52
+ open << name
53
+ end
54
+ else
55
+ next
56
+ end
57
+
58
+ if open.empty? && scanner.pos - start >= @size
59
+ pieces << html.byteslice(start...scanner.pos)
60
+ start = scanner.pos
61
+ end
62
+ end
63
+ pieces << html.byteslice(start..) if start < html.bytesize
64
+ pieces
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module HTML
5
+ # A text node, with its character references decoded.
6
+ class Text
7
+ attr_reader :value
8
+ attr_accessor :parent
9
+
10
+ def initialize(value)
11
+ @value = value
12
+ end
13
+
14
+ def text_content
15
+ @value
16
+ end
17
+
18
+ def element?
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,503 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ # Builds a document from HTML with the parse rules of the schema's node and mark types, as ProseMirror's
5
+ # DOMParser does, from which this is ported: https://prosemirror.net/docs/ref/#model.DOMParser
6
+ #
7
+ # - whitespace is collapsed, except in nodes whose whitespace is preserved, like code blocks;
8
+ # - content that doesn't fit where it is gets wrapped (text in a list becomes a list item with a
9
+ # paragraph), or moves up to a parent that accepts it;
10
+ # - required content is filled in (an empty list item gets a paragraph);
11
+ # - elements without a rule are unwrapped, and `head`, `noscript`, `object`, `script`, `style` and `title`
12
+ # are ignored.
13
+ class HTMLParser
14
+ BLOCK_TAGS = %w[
15
+ address article aside blockquote canvas dd div dl fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6
16
+ header hgroup hr li noscript ol output p pre section table tfoot ul
17
+ ].to_set.freeze
18
+ IGNORE_TAGS = %w[head noscript object script style title].to_set.freeze
19
+ LIST_TAGS = %w[ol ul].to_set.freeze
20
+
21
+ PRESERVE_WS = 1
22
+ PRESERVE_WS_FULL = 2
23
+
24
+ WHITESPACE = /[ \t\r\n\f]/
25
+ WHITESPACE_RUN = /[ \t\r\n\f]+/
26
+
27
+ # A rule that matched an element: the rule, its index, the attributes it gives, and the schema's type.
28
+ Match = Data.define(:rule, :index, :attrs, :type)
29
+
30
+ attr_reader :schema, :normalize_lists, :matched_styles
31
+
32
+ def initialize(schema)
33
+ @schema = schema
34
+ rules = (schema.marks + schema.nodes).flat_map(&:parse_rules)
35
+ rules = rules.each_with_index.sort_by { |rule, index| [-rule.priority, index] }.map(&:first)
36
+ @tags = rules.select(&:tag)
37
+ @styles = rules.select(&:style)
38
+ @matched_styles = @styles.map(&:style_property).uniq
39
+ @normalize_lists = @tags.none? { |rule| lists_contain_themselves?(rule) }
40
+ end
41
+
42
+ # The document parsed from the HTML, as its top node.
43
+ def parse(html)
44
+ parse_element(HTML::Builder.new.call(html))
45
+ end
46
+
47
+ # The document for an Element's content, like the one HTML::Builder gives.
48
+ def parse_element(root)
49
+ context = Context.new(self)
50
+ context.add_all(root, [])
51
+ context.finish
52
+ end
53
+
54
+ # The first tag rule after `after` that matches the element in this context.
55
+ def match_tag(element, context, after = nil)
56
+ ((after ? after + 1 : 0)...@tags.size).each do |index|
57
+ rule = @tags[index]
58
+ next unless HTML::Selector.parse(rule.tag).match?(element)
59
+ next if rule.context && !context.matches_context?(rule.context)
60
+
61
+ match = build_match(rule, index, element) { |get_attrs| call_get_attrs(get_attrs, element) }
62
+ return match if match
63
+ end
64
+ nil
65
+ end
66
+
67
+ # The first style rule after `after` that matches the property and value in this context.
68
+ def match_style(property, value, context, after = nil)
69
+ ((after ? after + 1 : 0)...@styles.size).each do |index|
70
+ rule = @styles[index]
71
+ next unless rule.style_matches?(property, value)
72
+ next if rule.context && !context.matches_context?(rule.context)
73
+
74
+ match = build_match(rule, index, nil) { |get_attrs| get_attrs.call(value) }
75
+ return match if match
76
+ end
77
+ nil
78
+ end
79
+
80
+ # The schema's type for a rule: a subclass registered in the schema inherits its parent's rules.
81
+ def type_for(rule)
82
+ rule.node? ? @schema.node_type(rule.type.type_name) : @schema.mark_type(rule.type.type_name)
83
+ end
84
+
85
+ private
86
+
87
+ def build_match(rule, index, element)
88
+ type = type_for(rule)
89
+ return nil unless type
90
+
91
+ given = rule.attrs
92
+ if rule.get_attrs
93
+ result = yield(rule.get_attrs)
94
+ return nil if result == false
95
+
96
+ given = result.is_a?(Hash) ? result : nil
97
+ end
98
+ return Match.new(rule:, index:, attrs: nil, type:) if rule.ignore || rule.skip || rule.close_parent
99
+
100
+ attrs = build_attrs(type, given, element)
101
+ attrs && Match.new(rule:, index:, attrs:, type:)
102
+ end
103
+
104
+ # The rule's attributes, with the ones read from the element, built with the type's properties.
105
+ def build_attrs(type, given, element)
106
+ given = (given || {}).transform_keys(&:to_s)
107
+ given = given.merge(type.attrs_from_html(element)) if element
108
+ type.build_attrs(given, report: ->(message) { @schema.report(message, path: "html") })
109
+ end
110
+
111
+ def call_get_attrs(get_attrs, element)
112
+ (get_attrs.arity == 2) ? get_attrs.call(element, @schema) : get_attrs.call(element)
113
+ end
114
+
115
+ def lists_contain_themselves?(rule)
116
+ return false unless rule.tag.match?(/\A(ul|ol)\b/) && rule.node?
117
+
118
+ type = type_for(rule)
119
+ type && @schema.content_match(type).match_type(type)
120
+ end
121
+
122
+ # A node being built: its type, attributes and marks, its content so far, and its content match state.
123
+ class NodeContext
124
+ attr_reader :type, :attrs, :marks, :solid, :content
125
+ attr_accessor :match, :options
126
+
127
+ def initialize(type, attrs, marks, solid, options, schema)
128
+ @type = type
129
+ @attrs = attrs
130
+ @marks = marks
131
+ @solid = solid
132
+ @options = options
133
+ @schema = schema
134
+ @match = schema.content_match(type)
135
+ @content = []
136
+ end
137
+
138
+ def find_wrapping(type)
139
+ @match&.find_wrapping(type, @schema)
140
+ end
141
+
142
+ def finish
143
+ strip_trailing_whitespace unless @options.anybits?(PRESERVE_WS)
144
+ fill = @match&.fill_before([], @schema, to_end: true) || []
145
+ content = Node.join_text(@content + fill.filter_map { |type| @schema.create_and_fill(type) })
146
+ Node.new(type: @type.type_name, attrs: @attrs, content:, marks: @marks)
147
+ end
148
+
149
+ def inline_context?
150
+ @schema.content_match(@type).inline_content?
151
+ end
152
+
153
+ private
154
+
155
+ def strip_trailing_whitespace
156
+ last = @content.last
157
+ return unless last&.text? && (trailing = last.text[/#{WHITESPACE_RUN}\z/o])
158
+
159
+ if trailing.length == last.text.length
160
+ @content.pop
161
+ else
162
+ @content[-1] = last.with(text: last.text[0...-trailing.length])
163
+ end
164
+ end
165
+ end
166
+
167
+ # The state of one parse, as ProseMirror's ParseContext.
168
+ class Context
169
+ def initialize(parser)
170
+ @parser = parser
171
+ @schema = parser.schema
172
+ @nodes = [NodeContext.new(@schema.node_type(@schema.top_node), @schema.node_type(@schema.top_node).default_attrs,
173
+ [], true, 0, @schema)]
174
+ @open = 0
175
+ @needs_block = false
176
+ @local_preserve_ws = false
177
+ @linebreak = @schema.nodes.find(&:linebreak_replacement?)
178
+ end
179
+
180
+ def top
181
+ @nodes[@open]
182
+ end
183
+
184
+ def add_all(parent, marks)
185
+ parent.children.each { |child| add_dom(child, marks) }
186
+ end
187
+
188
+ def finish
189
+ @open = 0
190
+ close_extra
191
+ @nodes.first.finish
192
+ end
193
+
194
+ # Whether the parents match a rule's context, as `"list_item/"`, `"blockquote//"` or `"a/|b/"`.
195
+ def matches_context?(context)
196
+ return context.split(/\s*\|\s*/).any? { |option| matches_context?(option) } if context.include?("|")
197
+
198
+ parts = context.split("/", -1)
199
+ context_matches?(parts, parts.size - 1, @open)
200
+ end
201
+
202
+ private
203
+
204
+ def context_matches?(parts, index, depth)
205
+ while index >= 0
206
+ part = parts[index]
207
+ if part.empty?
208
+ if index == parts.size - 1 || index.zero?
209
+ index -= 1
210
+ next
211
+ end
212
+ return depth.downto(0).any? { |deeper| context_matches?(parts, index - 1, deeper) }
213
+ end
214
+
215
+ type = (depth >= 0) ? @nodes[depth].type : nil
216
+ return false if type.nil? || (type.type_name.to_s != part && !type.group.include?(part.to_sym))
217
+
218
+ depth -= 1
219
+ index -= 1
220
+ end
221
+ true
222
+ end
223
+
224
+ def add_dom(dom, marks)
225
+ dom.element? ? add_element(dom, marks) : add_text_node(dom, marks)
226
+ end
227
+
228
+ def add_text_node(dom, marks)
229
+ value = dom.value
230
+ top = self.top
231
+ preserve = if top.options.anybits?(PRESERVE_WS_FULL)
232
+ :full
233
+ else
234
+ @local_preserve_ws || top.options.anybits?(PRESERVE_WS)
235
+ end
236
+ return unless preserve == :full || top.inline_context? || value.match?(/[^ \t\r\n\f]/)
237
+
238
+ if !preserve
239
+ value = value.gsub(WHITESPACE_RUN, " ")
240
+ value = value[1..] if value.start_with?(" ") && @open == @nodes.size - 1 && strip_leading_space?(dom, top)
241
+ elsif preserve == :full
242
+ value = value.gsub(/\r\n?/, "\n")
243
+ elsif @linebreak && value.match?(/[\r\n]/) && top.find_wrapping(@linebreak)
244
+ value.split(/\r?\n|\r/, -1).each_with_index do |line, index|
245
+ insert_node(Node.new(type: @linebreak.type_name, attrs: @linebreak.default_attrs), marks, true) if index.positive?
246
+ insert_node(text_node(line), marks, !line.match?(/\S/)) unless line.empty?
247
+ end
248
+ value = ""
249
+ else
250
+ value = value.gsub(/\r?\n|\r/, " ")
251
+ end
252
+ insert_node(text_node(value), marks, !value.match?(/\S/)) unless value.empty?
253
+ end
254
+
255
+ # A leading space is dropped at the start of a node, after a line break, and after a space.
256
+ def strip_leading_space?(dom, top)
257
+ before = top.content.last
258
+ previous = dom.parent&.previous_sibling(dom)
259
+ before.nil? || (previous&.element? && previous.name == "br") || (before.text? && before.text.match?(/#{WHITESPACE}\z/o))
260
+ end
261
+
262
+ def add_element(dom, marks, match_after = nil)
263
+ outer_ws = @local_preserve_ws
264
+ top = self.top
265
+ @local_preserve_ws = true if dom.name == "pre" || dom.style["white-space"].to_s.include?("pre")
266
+ normalize_list(dom) if LIST_TAGS.include?(dom.name) && @parser.normalize_lists
267
+ match = @parser.match_tag(dom, self, match_after)
268
+ rule = match&.rule
269
+
270
+ if rule ? rule.ignore : IGNORE_TAGS.include?(dom.name)
271
+ ignore_fallback(dom, marks)
272
+ elsif !rule || rule.skip || rule.close_parent
273
+ add_element_without_rule(dom, marks, rule, top)
274
+ else
275
+ inner_marks = read_styles(dom, marks)
276
+ add_element_by_rule(dom, match, inner_marks, rule.consuming ? nil : match.index) if inner_marks
277
+ end
278
+ @local_preserve_ws = outer_ws
279
+ end
280
+
281
+ def add_element_without_rule(dom, marks, rule, top)
282
+ @open = [0, @open - 1].max if rule&.close_parent
283
+ sync = false
284
+ old_needs_block = @needs_block
285
+ if BLOCK_TAGS.include?(dom.name)
286
+ if top.content.any? && inline?(top.content.first) && @open.positive?
287
+ @open -= 1
288
+ top = self.top
289
+ end
290
+ sync = true
291
+ @needs_block = true unless top.type
292
+ elsif dom.children.empty?
293
+ return leaf_fallback(dom, marks)
294
+ end
295
+ inner_marks = rule&.skip ? marks : read_styles(dom, marks)
296
+ add_all(dom, inner_marks) if inner_marks
297
+ self.sync(top) if sync
298
+ @needs_block = old_needs_block
299
+ end
300
+
301
+ # A `br` without a rule becomes a newline, in nodes with inline content.
302
+ def leaf_fallback(dom, marks)
303
+ add_text_node(HTML::Text.new("\n"), marks) if dom.name == "br" && top.inline_context?
304
+ end
305
+
306
+ # An ignored `br` still starts inline content.
307
+ def ignore_fallback(dom, marks)
308
+ find_place(@schema.node_type(:text), marks, true) if dom.name == "br" && !top.inline_context?
309
+ end
310
+
311
+ # The marks with the ones from the element's styles, or nil when a style rule ignores the element.
312
+ def read_styles(dom, marks)
313
+ return marks if dom.style.empty?
314
+
315
+ @parser.matched_styles.each do |property|
316
+ value = dom.style[property]
317
+ next if value.nil? || value.empty?
318
+
319
+ after = nil
320
+ while (match = @parser.match_style(property, value, self, after))
321
+ return nil if match.rule.ignore
322
+
323
+ marks = if match.rule.clear_mark
324
+ marks.reject { |mark| clears?(match.rule.clear_mark, match.type, mark) }
325
+ else
326
+ marks + [Mark.new(type: match.type.type_name, attrs: match.attrs)]
327
+ end
328
+ break if match.rule.consuming
329
+
330
+ after = match.index
331
+ end
332
+ end
333
+ marks
334
+ end
335
+
336
+ def clears?(clear_mark, type, mark)
337
+ (clear_mark == true) ? mark.type == type.type_name : clear_mark.call(mark)
338
+ end
339
+
340
+ def add_element_by_rule(dom, match, marks, continue_after)
341
+ rule = match.rule
342
+ type = match.type
343
+ sync = false
344
+ if rule.node?
345
+ if !type.leaf?
346
+ inner = enter(type, match.attrs, marks, rule.preserve_whitespace)
347
+ if inner
348
+ sync = true
349
+ marks = inner
350
+ end
351
+ elsif !insert_node(Node.new(type: type.type_name, attrs: match.attrs), marks, dom.name == "br")
352
+ leaf_fallback(dom, marks)
353
+ end
354
+ else
355
+ marks += [Mark.new(type: type.type_name, attrs: match.attrs)]
356
+ end
357
+ start_in = top
358
+
359
+ if rule.node? && type.leaf?
360
+ # Leaves have no content to parse.
361
+ elsif continue_after
362
+ add_element(dom, marks, continue_after)
363
+ elsif rule.get_content
364
+ Array(rule.get_content.call(dom, @schema)).each { |node| insert_node(node, marks, false) }
365
+ else
366
+ add_all(content_element(dom, rule) || dom, marks)
367
+ end
368
+ @open -= 1 if sync && self.sync(start_in)
369
+ end
370
+
371
+ def content_element(dom, rule)
372
+ case rule.content_element
373
+ when String then dom.query(rule.content_element)
374
+ when Proc then rule.content_element.call(dom)
375
+ end
376
+ end
377
+
378
+ # Finds where a node of this type fits: in the open nodes, from the innermost, possibly with wrappers.
379
+ # Returns the marks left to apply, or nil when it fits nowhere.
380
+ def find_place(type, marks, cautious)
381
+ route = nil
382
+ sync_to = nil
383
+ penalty = 0
384
+ @open.downto(0) do |depth|
385
+ context = @nodes[depth]
386
+ found = context.find_wrapping(type)
387
+ if found && (route.nil? || route.size > found.size + penalty)
388
+ route = found
389
+ sync_to = context
390
+ break if found.empty?
391
+ end
392
+ next unless context.solid
393
+ break if cautious
394
+
395
+ penalty += 2
396
+ end
397
+ return nil unless route
398
+
399
+ sync(sync_to)
400
+ route.each { |wrapper| marks = enter_inner(wrapper, wrapper.default_attrs, marks, false) }
401
+ marks
402
+ end
403
+
404
+ def insert_node(node, marks, cautious)
405
+ type = node_type(node)
406
+ inner_marks = find_place(type, marks, cautious)
407
+ return false unless inner_marks
408
+
409
+ close_extra
410
+ top = self.top
411
+ top.match &&= top.match.match_type(type)
412
+ node_marks = (inner_marks + node.marks).each_with_object([]) do |mark, set|
413
+ mark_type = @schema.mark_type(mark.type)
414
+ set.replace(@schema.add_mark(mark, set)) if mark_type && top.type.allows_mark?(mark_type, @schema)
415
+ end
416
+ top.content << node.with(marks: node_marks)
417
+ true
418
+ end
419
+
420
+ def enter(type, attrs, marks, preserve_ws)
421
+ inner_marks = find_place(type, marks, false)
422
+ inner_marks && enter_inner(type, attrs, inner_marks, true, preserve_ws)
423
+ end
424
+
425
+ # Opens a node of this type. Returns the marks that don't apply to it, which apply to its content.
426
+ def enter_inner(type, attrs, marks, solid, preserve_ws = nil)
427
+ close_extra
428
+ top = self.top
429
+ top.match &&= top.match.match_type(type)
430
+ options = ws_options(type, preserve_ws, top.options)
431
+ applied = []
432
+ remaining = marks.reject do |mark|
433
+ mark_type = @schema.mark_type(mark.type)
434
+ next false unless mark_type && top.type.allows_mark?(mark_type, @schema)
435
+
436
+ applied = @schema.add_mark(mark, applied)
437
+ true
438
+ end
439
+ @nodes << NodeContext.new(type, attrs, applied, solid, options, @schema)
440
+ @open += 1
441
+ remaining
442
+ end
443
+
444
+ def ws_options(type, preserve_ws, base)
445
+ unless preserve_ws.nil?
446
+ return (preserve_ws ? PRESERVE_WS : 0) | ((preserve_ws == :full) ? PRESERVE_WS_FULL : 0)
447
+ end
448
+
449
+ (type.whitespace == :pre) ? (PRESERVE_WS | PRESERVE_WS_FULL) : base
450
+ end
451
+
452
+ # Finishes the nodes above the open one, and adds them to their parents.
453
+ def close_extra
454
+ (@nodes.size - 1).downto(@open + 1) { |index| @nodes[index - 1].content << @nodes[index].finish }
455
+ @nodes.slice!((@open + 1)..)
456
+ end
457
+
458
+ def sync(to)
459
+ @open.downto(0) do |index|
460
+ if @nodes[index].equal?(to)
461
+ @open = index
462
+ return true
463
+ elsif @local_preserve_ws
464
+ @nodes[index].options |= PRESERVE_WS
465
+ end
466
+ end
467
+ false
468
+ end
469
+
470
+ # Moves lists that directly follow a list item into it, as some tools produce them.
471
+ def normalize_list(dom)
472
+ previous_item = nil
473
+ kept = []
474
+ dom.children.each do |child|
475
+ name = child.element? ? child.name : nil
476
+ if name && LIST_TAGS.include?(name) && previous_item
477
+ previous_item.append(child)
478
+ else
479
+ kept << child
480
+ if name == "li"
481
+ previous_item = child
482
+ elsif name
483
+ previous_item = nil
484
+ end
485
+ end
486
+ end
487
+ dom.children.replace(kept)
488
+ end
489
+
490
+ def text_node(value)
491
+ Node.new(type: :text, text: value)
492
+ end
493
+
494
+ def node_type(node)
495
+ @schema.node_type(node.type)
496
+ end
497
+
498
+ def inline?(node)
499
+ node.text? || node_type(node)&.inline?
500
+ end
501
+ end
502
+ end
503
+ end