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,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ # A state of a node type's content expression, compiled into a deterministic automaton, as in
5
+ # ProseMirror's ContentMatch: https://prosemirror.net/docs/ref/#model.ContentMatch
6
+ #
7
+ # Edges go from one state to the next for a node type. Their order matters: the first generatable
8
+ # type is used to fill required content.
9
+ class ContentMatch
10
+ attr_reader :edges
11
+
12
+ def initialize(valid_end)
13
+ @valid_end = valid_end
14
+ @edges = []
15
+ @wrappings = {}
16
+ end
17
+
18
+ # Compiles a content expression for a schema. Returns EMPTY for leaf types.
19
+ def self.compile(expression, schema)
20
+ return EMPTY if expression.nil?
21
+
22
+ Compiler.new(expression, schema).call
23
+ end
24
+
25
+ # Whether the content can end here.
26
+ def valid_end?
27
+ @valid_end
28
+ end
29
+
30
+ # The state after a node of this type, or nil when it can't come here.
31
+ def match_type(type)
32
+ @edges.find { |edge_type, _| edge_type == type }&.last
33
+ end
34
+
35
+ # The state after these node types, or nil when they don't match.
36
+ def match_types(types)
37
+ types.reduce(self) { |match, type| match&.match_type(type) }
38
+ end
39
+
40
+ def inline_content?
41
+ !@edges.empty? && @edges.first.first.inline?
42
+ end
43
+
44
+ # The first type that can come here and can be created empty, or nil.
45
+ def default_type(schema)
46
+ @edges.map(&:first).find { |type| generatable?(type, schema) }
47
+ end
48
+
49
+ # The node types to insert before `types` so that they match, possibly none, or nil when no
50
+ # insertion works. With `to_end`, the content must also be able to end after them.
51
+ def fill_before(types, schema, to_end: false)
52
+ seen = [self]
53
+ search = lambda do |match, filled|
54
+ finished = match.match_types(types)
55
+ return filled if finished && (!to_end || finished.valid_end?)
56
+
57
+ match.edges.each do |type, following|
58
+ next if !generatable?(type, schema) || seen.include?(following)
59
+
60
+ seen << following
61
+ found = search.call(following, filled + [type])
62
+ return found if found
63
+ end
64
+ nil
65
+ end
66
+ search.call(self, [])
67
+ end
68
+
69
+ # The wrapper types that let a node of this type come here, possibly none, or nil when none do.
70
+ def find_wrapping(target, schema)
71
+ return @wrappings[target] if @wrappings.key?(target)
72
+
73
+ @wrappings[target] = compute_wrapping(target, schema)
74
+ end
75
+
76
+ def generatable?(type, schema)
77
+ type.type_name != :text && !type.required_attrs?
78
+ end
79
+
80
+ private
81
+
82
+ def compute_wrapping(target, schema)
83
+ seen = {}
84
+ active = [[self, nil, nil]]
85
+ until active.empty?
86
+ current = active.shift
87
+ match, type, = current
88
+ return wrapping_path(current) if match.match_type(target)
89
+
90
+ match.edges.each do |next_type, following|
91
+ next if next_type.leaf? || next_type.required_attrs? || seen[next_type]
92
+ next if type && !following.valid_end?
93
+
94
+ active << [schema.content_match(next_type), next_type, current]
95
+ seen[next_type] = true
96
+ end
97
+ end
98
+ nil
99
+ end
100
+
101
+ def wrapping_path(step)
102
+ path = []
103
+ while step && step[1]
104
+ path.unshift(step[1])
105
+ step = step[2]
106
+ end
107
+ path
108
+ end
109
+
110
+ EMPTY = new(true)
111
+
112
+ # Compiles an expression tree into a non-deterministic automaton, then a deterministic one.
113
+ class Compiler
114
+ def initialize(expression, schema)
115
+ @expression = expression
116
+ @schema = schema
117
+ @nfa = [[]]
118
+ end
119
+
120
+ def call
121
+ connect(compile(@expression.tree, 0), new_state)
122
+ explore(null_from(0), {})
123
+ end
124
+
125
+ private
126
+
127
+ Edge = Struct.new(:term, :to)
128
+
129
+ def new_state
130
+ @nfa << []
131
+ @nfa.size - 1
132
+ end
133
+
134
+ def edge(from, to = nil, term = nil)
135
+ Edge.new(term, to).tap { |created| @nfa[from] << created }
136
+ end
137
+
138
+ def connect(edges, to)
139
+ edges.each { |created| created.to = to }
140
+ end
141
+
142
+ def compile(tree, from)
143
+ case tree.first
144
+ when :choice then tree[1].flat_map { |branch| compile(branch, from) }
145
+ when :sequence then compile_sequence(tree[1], from)
146
+ when :repeat then compile_repeat(*tree.drop(1), from)
147
+ when :name then resolve(tree[1]).map { |type| edge(from, nil, type) }
148
+ end
149
+ end
150
+
151
+ def compile_sequence(parts, from)
152
+ parts.each_with_index do |part, index|
153
+ edges = compile(part, from)
154
+ return edges if index == parts.size - 1
155
+
156
+ connect(edges, from = new_state)
157
+ end
158
+ end
159
+
160
+ def compile_repeat(tree, min, max, from)
161
+ if min.zero? && max.nil? # *
162
+ loop_state = new_state
163
+ edge(from, loop_state)
164
+ connect(compile(tree, loop_state), loop_state)
165
+ [edge(loop_state)]
166
+ elsif min == 1 && max.nil? # +
167
+ loop_state = new_state
168
+ connect(compile(tree, from), loop_state)
169
+ connect(compile(tree, loop_state), loop_state)
170
+ [edge(loop_state)]
171
+ elsif min.zero? && max == 1 # ?
172
+ [edge(from)] + compile(tree, from)
173
+ else
174
+ compile_range(tree, min, max, from)
175
+ end
176
+ end
177
+
178
+ def compile_range(tree, min, max, from)
179
+ current = from
180
+ min.times do
181
+ following = new_state
182
+ connect(compile(tree, current), following)
183
+ current = following
184
+ end
185
+ if max.nil?
186
+ connect(compile(tree, current), current)
187
+ else
188
+ (max - min).times do
189
+ following = new_state
190
+ edge(current, following)
191
+ connect(compile(tree, current), following)
192
+ current = following
193
+ end
194
+ end
195
+ [edge(current)]
196
+ end
197
+
198
+ def resolve(name)
199
+ types = @schema.node_types_named(name)
200
+ raise ArgumentError, "no node type or group #{name} in content expression #{@expression.source.inspect}" if types.empty?
201
+
202
+ types
203
+ end
204
+
205
+ # The states reachable from `state` without consuming a node, sorted.
206
+ def null_from(state)
207
+ result = []
208
+ scan = lambda do |current|
209
+ edges = @nfa[current]
210
+ return scan.call(edges.first.to) if edges.size == 1 && edges.first.term.nil?
211
+
212
+ result << current
213
+ edges.each { |created| scan.call(created.to) if created.term.nil? && !result.include?(created.to) }
214
+ end
215
+ scan.call(state)
216
+ result.sort.reverse
217
+ end
218
+
219
+ def explore(states, labeled)
220
+ out = []
221
+ states.each do |state|
222
+ @nfa[state].each do |created|
223
+ next unless created.term
224
+
225
+ set = out.find { |type, _| type == created.term }&.last
226
+ null_from(created.to).each do |reached|
227
+ out << [created.term, set = []] unless set
228
+ set << reached unless set.include?(reached)
229
+ end
230
+ end
231
+ end
232
+ match = labeled[states.join(",")] = ContentMatch.new(states.include?(@nfa.size - 1))
233
+ out.each do |type, set|
234
+ set = set.sort.reverse
235
+ match.edges << [type, labeled[set.join(",")] || explore(set, labeled)]
236
+ end
237
+ match
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module HTML
5
+ # Builds Elements and Texts from HTML, with Herb's parser (https://herb-tools.dev), which never raises
6
+ # on malformed HTML. It also does what Herb leaves to its users:
7
+ # - decodes character references, in text and attribute values;
8
+ # - keeps ERB tags (`<% %>`) as text;
9
+ # - applies an opening tag without a closing tag to the rest of its parent, as browsers do;
10
+ # - drops comments (including Word's `<![if …]>`), doctypes and CDATA;
11
+ # - walks the tree without recursion, and flattens elements nested deeper than MAX_DEPTH.
12
+ class Builder
13
+ MAX_DEPTH = 256
14
+ # The size of the pieces Herb parses, in bytes. See Splitter.
15
+ PIECE_SIZE = 65_536
16
+ # What browsers read as a comment, and Herb leaves in text, like Word's `<![if !supportLists]>`.
17
+ BOGUS_COMMENT = /<![^>]*>/
18
+
19
+ def self.load_herb
20
+ require "herb"
21
+ rescue LoadError
22
+ raise Error, 'HTML import needs the herb gem: add `gem "herb"` to your Gemfile'
23
+ end
24
+
25
+ # Whether the herb gem is installed.
26
+ def self.herb?
27
+ load_herb
28
+ true
29
+ rescue Error
30
+ false
31
+ end
32
+
33
+ # A root Element holding the parsed content.
34
+ def call(html)
35
+ self.class.load_herb
36
+ root = Element.new("#document")
37
+ Splitter.new(PIECE_SIZE).call(UTF8.string(html)).each do |piece|
38
+ # Without `timeout: 0`, Herb stops after a second and returns what it parsed so far.
39
+ build(Herb.parse(piece, track_locations: false, strict: false, timeout: 0).value.children, root)
40
+ end
41
+ root
42
+ end
43
+
44
+ private
45
+
46
+ # Each item of the stack is a list of Herb nodes, the position in it, the element to add them to,
47
+ # and its depth.
48
+ def build(nodes, root)
49
+ stack = [[nodes, 0, root, 0]]
50
+ until stack.empty?
51
+ frame = stack.last
52
+ list, index, target, depth = frame
53
+ if index >= list.size
54
+ stack.pop
55
+ next
56
+ end
57
+ frame[1] += 1
58
+ child = add(list[index], target, depth)
59
+ next unless child
60
+
61
+ element, children = child
62
+ if element.equal?(target)
63
+ stack << [children, 0, target, depth]
64
+ elsif children.nil? # an opening tag without a closing tag: the rest of the list goes in it
65
+ frame[2] = element
66
+ frame[3] = depth + 1
67
+ else
68
+ stack << [children, 0, element, depth + 1]
69
+ end
70
+ end
71
+ end
72
+
73
+ # Adds a Herb node to the target. Returns the element to fill and the Herb nodes to fill it with,
74
+ # when the node has children.
75
+ def add(node, target, depth)
76
+ case node
77
+ when Herb::AST::HTMLElementNode then add_element(node, target, depth)
78
+ when Herb::AST::HTMLOpenTagNode then add_open_tag(node, target, depth)
79
+ when Herb::AST::HTMLTextNode then add_text(Entities.decode(node.content.to_s.gsub(BOGUS_COMMENT, "")), target)
80
+ when Herb::AST::LiteralNode then add_text(node.content.to_s, target)
81
+ when Herb::AST::WhitespaceNode then add_text(node.value&.value.to_s, target)
82
+ when Herb::AST::HTMLCommentNode, Herb::AST::HTMLDoctypeNode, Herb::AST::XMLDeclarationNode,
83
+ Herb::AST::XMLProcessingInstructionNode, Herb::AST::CDATANode, Herb::AST::HTMLCloseTagNode
84
+ nil
85
+ else add_other(node, target)
86
+ end
87
+ end
88
+
89
+ def add_element(node, target, depth)
90
+ open_tag = node.open_tag
91
+ return [target, node.body] unless open_tag.respond_to?(:tag_name) && open_tag.tag_name
92
+
93
+ element = new_element(open_tag)
94
+ return [target, node.body] if depth >= MAX_DEPTH
95
+
96
+ target.append(element)
97
+ [element, node.body]
98
+ end
99
+
100
+ def add_open_tag(node, target, depth)
101
+ element = new_element(node)
102
+ return nil if depth >= MAX_DEPTH
103
+
104
+ target.append(element)
105
+ node.is_void ? nil : [element, nil]
106
+ end
107
+
108
+ # ERB tags become text. Other nodes give their children, if any.
109
+ def add_other(node, target)
110
+ if node.respond_to?(:tag_opening) && node.respond_to?(:content) && node.respond_to?(:tag_closing)
111
+ add_text([node.tag_opening, node.content, node.tag_closing].map { |token| token&.value.to_s }.join, target)
112
+ end
113
+ children = node.respond_to?(:child_nodes) ? node.child_nodes.compact : []
114
+ children.empty? ? nil : [target, children]
115
+ end
116
+
117
+ def add_text(value, target)
118
+ return nil if value.empty?
119
+
120
+ last = target.children.last
121
+ if last.is_a?(Text)
122
+ target.children[-1] = Text.new(last.value + value).tap { |text| text.parent = target }
123
+ else
124
+ target.append(Text.new(value))
125
+ end
126
+ nil
127
+ end
128
+
129
+ def new_element(open_tag)
130
+ Element.new(open_tag.tag_name.value.downcase, attributes(open_tag.children))
131
+ end
132
+
133
+ # The first value wins when an attribute is repeated, as in browsers.
134
+ def attributes(nodes)
135
+ nodes.grep(Herb::AST::HTMLAttributeNode).each_with_object({}) do |attribute, result|
136
+ name = text_of(attribute.name&.children).downcase
137
+ next if name.empty? || result.key?(name)
138
+
139
+ result[name] = Entities.decode(text_of(attribute.value&.children), attribute: true)
140
+ end
141
+ end
142
+
143
+ def text_of(nodes)
144
+ Array(nodes).map do |node|
145
+ if node.respond_to?(:tag_opening) && node.respond_to?(:tag_closing)
146
+ [node.tag_opening, node.content, node.tag_closing].map { |token| token&.value.to_s }.join
147
+ else
148
+ node.content.to_s
149
+ end
150
+ end.join
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module HTML
5
+ # An element, with a lowercase name, attributes with lowercase names and decoded values, and
6
+ # children. The import works on these rather than on the parser's own tree.
7
+ class Element
8
+ attr_reader :name, :attributes, :children
9
+ attr_accessor :parent
10
+
11
+ def initialize(name, attributes = {})
12
+ @name = name
13
+ @attributes = attributes
14
+ @children = []
15
+ end
16
+
17
+ def append(child)
18
+ child.parent = self
19
+ @children << child
20
+ child
21
+ end
22
+
23
+ def element?
24
+ true
25
+ end
26
+
27
+ def [](attribute)
28
+ @attributes[attribute]
29
+ end
30
+
31
+ def key?(attribute)
32
+ @attributes.key?(attribute)
33
+ end
34
+
35
+ def classes
36
+ self["class"].to_s.split
37
+ end
38
+
39
+ # The declarations of the `style` attribute, with lowercase properties: `{"font-weight" => "bold"}`.
40
+ def style
41
+ @style ||= self["style"].to_s.split(";").each_with_object({}) do |declaration, styles|
42
+ property, value = declaration.split(":", 2)
43
+ next if value.nil? || property.strip.empty?
44
+
45
+ styles[property.strip.downcase] = value.strip.sub(/\s*!important\z/i, "")
46
+ end
47
+ end
48
+
49
+ def text_content
50
+ @children.map(&:text_content).join
51
+ end
52
+
53
+ def element_children
54
+ @children.select(&:element?)
55
+ end
56
+
57
+ def first_element_child
58
+ @children.find(&:element?)
59
+ end
60
+
61
+ def previous_sibling(child)
62
+ index = @children.index { |candidate| candidate.equal?(child) }
63
+ @children[index - 1] if index&.positive?
64
+ end
65
+
66
+ def matches?(selector)
67
+ Selector.parse(selector).match?(self)
68
+ end
69
+
70
+ # The first descendant matching the selector, depth first.
71
+ def query(selector)
72
+ compiled = Selector.parse(selector)
73
+ stack = element_children.reverse
74
+ until stack.empty?
75
+ element = stack.pop
76
+ return element if compiled.match?(element)
77
+
78
+ stack.concat(element.element_children.reverse)
79
+ end
80
+ nil
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosemirror
4
+ module HTML
5
+ # Decodes character references (`&amp;`, `&eacute;`, `&#233;`, `&#xE9;`) as HTML5 does, with the
6
+ # WHATWG list of named references: https://html.spec.whatwg.org/multipage/named-characters.html
7
+ module Entities
8
+ # Code points 0x80 to 0x9F, read as Windows-1252, as browsers do.
9
+ WINDOWS_1252 = {
10
+ 0x80 => 0x20AC, 0x82 => 0x201A, 0x83 => 0x0192, 0x84 => 0x201E, 0x85 => 0x2026, 0x86 => 0x2020,
11
+ 0x87 => 0x2021, 0x88 => 0x02C6, 0x89 => 0x2030, 0x8A => 0x0160, 0x8B => 0x2039, 0x8C => 0x0152,
12
+ 0x8E => 0x017D, 0x91 => 0x2018, 0x92 => 0x2019, 0x93 => 0x201C, 0x94 => 0x201D, 0x95 => 0x2022,
13
+ 0x96 => 0x2013, 0x97 => 0x2014, 0x98 => 0x02DC, 0x99 => 0x2122, 0x9A => 0x0161, 0x9B => 0x203A,
14
+ 0x9C => 0x0153, 0x9E => 0x017E, 0x9F => 0x0178
15
+ }.freeze
16
+
17
+ # The length of the longest reference without `;`, like `middot`.
18
+ LONGEST_LEGACY_NAME = 6
19
+ REFERENCE = /&(?:#(\d+);?|#[xX](\h+);?|([A-Za-z][A-Za-z0-9]*;?))/
20
+
21
+ module_function
22
+
23
+ # In an attribute, a named reference without a semicolon followed by a letter, a digit or `=`
24
+ # stays as it is, as in browsers (`?a=1&copy=2` keeps `&copy`).
25
+ def decode(string, attribute: false)
26
+ return string unless string.include?("&")
27
+
28
+ string.gsub(REFERENCE) do
29
+ if (decimal = Regexp.last_match(1))
30
+ character(decimal.to_i)
31
+ elsif (hexadecimal = Regexp.last_match(2))
32
+ character(hexadecimal.to_i(16))
33
+ else
34
+ named(Regexp.last_match(3), Regexp.last_match.post_match, attribute) || Regexp.last_match(0)
35
+ end
36
+ end
37
+ end
38
+
39
+ # The characters of a named reference ending with `;`, or nil. `entities.txt` has one reference per
40
+ # line, with its code points in hexadecimal (`nvlt; 3C,20D2`). It is kept as one string, searched with
41
+ # `index`, which takes a tenth of the memory of a Hash. The references found are cached, but not the
42
+ # unknown names, which documents could make up without limit.
43
+ def lookup(name)
44
+ @found ||= {}
45
+ @found[name] ||= begin
46
+ start = list.index("\n#{name} ") or return nil
47
+ characters(list[start + name.length + 2...list.index("\n", start + 1)])
48
+ end
49
+ end
50
+
51
+ # The references without `;`, which browsers also read at the start of a longer name (`&copy2`).
52
+ def legacy
53
+ @legacy ||= list.scan(/^([A-Za-z0-9]+) ([\h,]+)$/).to_h { |name, codes| [name, characters(codes)] }.freeze
54
+ end
55
+
56
+ def list
57
+ @list ||= "\n#{File.read(File.join(__dir__, "entities.txt"))}".freeze
58
+ end
59
+
60
+ def characters(codes)
61
+ codes.split(",").map { |code| code.to_i(16) }.pack("U*").freeze
62
+ end
63
+
64
+ def character(code)
65
+ return "�" if code.zero? || code > 0x10FFFF || (0xD800..0xDFFF).cover?(code)
66
+
67
+ [WINDOWS_1252.fetch(code, code)].pack("U")
68
+ end
69
+
70
+ # The longest reference starting the name, followed by the rest of the name. A `;` can only end the
71
+ # name, so a shorter reference is one without `;`.
72
+ def named(name, following, attribute)
73
+ return lookup(name) if name.end_with?(";") && lookup(name)
74
+
75
+ [name.length, LONGEST_LEGACY_NAME].min.downto(1) do |length|
76
+ next unless (characters = legacy[name[0, length]])
77
+
78
+ rest = name[length..]
79
+ return nil if attribute && "#{rest}#{following}".match?(/\A[A-Za-z0-9=]/)
80
+
81
+ return characters + rest
82
+ end
83
+ nil
84
+ end
85
+ end
86
+ end
87
+ end