coradoc-markdown 1.0.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/lib/coradoc/markdown/errors.rb +28 -0
  4. data/lib/coradoc/markdown/model/abbreviation.rb +27 -0
  5. data/lib/coradoc/markdown/model/attribute_list.rb +98 -0
  6. data/lib/coradoc/markdown/model/base.rb +86 -0
  7. data/lib/coradoc/markdown/model/blockquote.rb +21 -0
  8. data/lib/coradoc/markdown/model/code.rb +11 -0
  9. data/lib/coradoc/markdown/model/code_block.rb +24 -0
  10. data/lib/coradoc/markdown/model/definition_item.rb +24 -0
  11. data/lib/coradoc/markdown/model/definition_list.rb +47 -0
  12. data/lib/coradoc/markdown/model/definition_term.rb +21 -0
  13. data/lib/coradoc/markdown/model/document.rb +39 -0
  14. data/lib/coradoc/markdown/model/emphasis.rb +11 -0
  15. data/lib/coradoc/markdown/model/extension.rb +92 -0
  16. data/lib/coradoc/markdown/model/footnote.rb +31 -0
  17. data/lib/coradoc/markdown/model/footnote_reference.rb +22 -0
  18. data/lib/coradoc/markdown/model/heading.rb +44 -0
  19. data/lib/coradoc/markdown/model/highlight.rb +18 -0
  20. data/lib/coradoc/markdown/model/horizontal_rule.rb +16 -0
  21. data/lib/coradoc/markdown/model/image.rb +19 -0
  22. data/lib/coradoc/markdown/model/link.rb +19 -0
  23. data/lib/coradoc/markdown/model/list.rb +22 -0
  24. data/lib/coradoc/markdown/model/list_item.rb +29 -0
  25. data/lib/coradoc/markdown/model/math.rb +50 -0
  26. data/lib/coradoc/markdown/model/paragraph.rb +28 -0
  27. data/lib/coradoc/markdown/model/strikethrough.rb +18 -0
  28. data/lib/coradoc/markdown/model/strong.rb +11 -0
  29. data/lib/coradoc/markdown/model/table.rb +13 -0
  30. data/lib/coradoc/markdown/model/text.rb +15 -0
  31. data/lib/coradoc/markdown/parser/ast_processor.rb +543 -0
  32. data/lib/coradoc/markdown/parser/block_parser.rb +745 -0
  33. data/lib/coradoc/markdown/parser/html_entities.rb +2149 -0
  34. data/lib/coradoc/markdown/parser/inline_parser.rb +274 -0
  35. data/lib/coradoc/markdown/parser/parslet_extras.rb +215 -0
  36. data/lib/coradoc/markdown/parser.rb +11 -0
  37. data/lib/coradoc/markdown/parser_util.rb +90 -0
  38. data/lib/coradoc/markdown/serializer.rb +199 -0
  39. data/lib/coradoc/markdown/toc_generator.rb +215 -0
  40. data/lib/coradoc/markdown/transform/from_core_model.rb +325 -0
  41. data/lib/coradoc/markdown/transform/text_extraction.rb +19 -0
  42. data/lib/coradoc/markdown/transform/to_core_model.rb +287 -0
  43. data/lib/coradoc/markdown/transformer.rb +463 -0
  44. data/lib/coradoc/markdown/version.rb +7 -0
  45. data/lib/coradoc/markdown.rb +190 -0
  46. metadata +173 -0
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ module Parser
6
+ autoload :ParsletExtras, "#{__dir__}/parslet_extras"
7
+ autoload :HtmlEntities, "#{__dir__}/html_entities"
8
+
9
+ class InlineParser < Parslet::Parser
10
+ using ParsletExtras
11
+
12
+ rule(:line_ending) { (str("\n") | str("\r\n") | str("\r")).ignore }
13
+ rule(:line_ending_or_eof) { line_ending | any.absent? }
14
+ rule(:whitespace) { match[" \t"] }
15
+ rule(:unicode_whitespace) { match["\\p{Zs}\t\r\n\f"] | any.absent? }
16
+ rule(:unicode_punctuation) { match['\\p{P}\\p{S}'] }
17
+
18
+ def unicode_codepoint(base, s)
19
+ i = s.to_s.to_i(base)
20
+ return "\uFFFD" if i.zero?
21
+
22
+ i.chr(Encoding::UTF_8)
23
+ rescue RangeError
24
+ "\uFFFD"
25
+ end
26
+
27
+ def unicode_dec(s)
28
+ unicode_codepoint(10, s)
29
+ end
30
+
31
+ def unicode_hex(s)
32
+ unicode_codepoint(16, s)
33
+ end
34
+
35
+ def lookup_entity(s)
36
+ HTML_ENTITIES[s.to_s] || "&#{s};"
37
+ end
38
+
39
+ def process_code(s)
40
+ s = s.to_s
41
+ s.tr!("\n", ' ')
42
+ return s.slice(1, s.length - 2) if s.length > 2 && s.start_with?(' ') && s.end_with?(' ')
43
+
44
+ s
45
+ end
46
+
47
+ rule(:escape) { str('\\').ignore >> match["!\"#$%&'\\(\\)*+,\\-./:;<=>?@\\[\\\\\\]\\^_`\\{\\|\\}~"] }
48
+ rule(:dec_entity) do
49
+ str('&#').ignore >> match['0-9'].repeat(1, 7).dynamic_output(method(:unicode_dec)) >> str(';').ignore
50
+ end
51
+ rule(:hex_entity) do
52
+ str('&#').ignore >> match['xX'].ignore >> match['A-Fa-f0-9'].repeat(1,
53
+ 6).dynamic_output(method(:unicode_hex)) >> str(';').ignore
54
+ end
55
+ rule(:entity) do
56
+ str('&').ignore >> match['A-Za-z0-9'].repeat(1).dynamic_output(method(:lookup_entity)) >> str(';').ignore
57
+ end
58
+ rule(:nul_byte) { str("\0").output("\uFFFD") }
59
+ rule(:special_char) { escape | dec_entity | hex_entity | entity | nul_byte }
60
+
61
+ rule(:text) do
62
+ (special_char | (element.absent? >> any)).repeat(1).as(:text)
63
+ end
64
+
65
+ rule(:code_span) do
66
+ str('`').does_not_precede? >>
67
+ str('`').repeat(1).capture(:code_opener).ignore >>
68
+ dynamic do |_src, ctx|
69
+ ending = (str('`').does_not_precede? >> str(ctx.captures[:code_opener]).ignore >> str('`').absent?)
70
+ (ending.absent? >> any).repeat(1).dynamic_output(method(:process_code)).as(:code) >> ending
71
+ end
72
+ end
73
+
74
+ rule(:delimiter_run) do
75
+ str('*').repeat(1) | str('_').repeat(1)
76
+ end
77
+
78
+ rule(:both_flanking_delimiter_run) do
79
+ any.precedes? >>
80
+ unicode_whitespace.does_not_precede? >> (
81
+ (
82
+ unicode_punctuation.precedes? >>
83
+ delimiter_run.as(:bfdr) >>
84
+ unicode_punctuation.present?
85
+ ) | (
86
+ unicode_punctuation.does_not_precede? >>
87
+ delimiter_run.as(:bfdr) >>
88
+ unicode_punctuation.absent?
89
+ )
90
+ ) >> unicode_whitespace.absent?
91
+ end
92
+
93
+ rule(:left_flanking_delimiter_run) do
94
+ (
95
+ (
96
+ delimiter_run.as(:lfdr) >>
97
+ unicode_punctuation.absent?
98
+ ) | (
99
+ ((unicode_whitespace | unicode_punctuation).precedes? | any.does_not_precede?) >>
100
+ delimiter_run.as(:lfdr)
101
+ )
102
+ ) >> unicode_whitespace.absent?
103
+ end
104
+
105
+ rule(:right_flanking_delimiter_run) do
106
+ any.precedes? >>
107
+ unicode_whitespace.does_not_precede? >> (
108
+ (
109
+ unicode_punctuation.precedes? >>
110
+ delimiter_run.as(:rfdr) >>
111
+ (unicode_whitespace | unicode_punctuation).present?
112
+ ) | (
113
+ unicode_punctuation.does_not_precede? >>
114
+ delimiter_run.as(:rfdr)
115
+ )
116
+ )
117
+ end
118
+
119
+ rule(:non_flanking_delimiter_run) do
120
+ left_flanking_delimiter_run.absent? >> left_flanking_delimiter_run.absent? >> delimiter_run.as(:nfdr)
121
+ end
122
+
123
+ rule(:flanking_delimiter_run) do
124
+ both_flanking_delimiter_run | left_flanking_delimiter_run | right_flanking_delimiter_run | non_flanking_delimiter_run
125
+ end
126
+
127
+ rule(:run_surrounded_by_punctuation) do
128
+ (unicode_punctuation.precedes? >> flanking_delimiter_run >> unicode_punctuation.present?).as(:rsp)
129
+ end
130
+
131
+ rule(:run_preceded_by_punctuation) do
132
+ (unicode_punctuation.precedes? >> flanking_delimiter_run).as(:rpp)
133
+ end
134
+
135
+ rule(:run_followed_by_punctuation) do
136
+ (flanking_delimiter_run >> unicode_punctuation.present?).as(:rfp)
137
+ end
138
+
139
+ rule(:checked_delimiter_run) do
140
+ run_surrounded_by_punctuation | run_preceded_by_punctuation | run_followed_by_punctuation | flanking_delimiter_run
141
+ end
142
+
143
+ rule(:element) { code_span | checked_delimiter_run }
144
+
145
+ rule(:inline) { (text | element).repeat }
146
+
147
+ root :inline
148
+
149
+ def can_open_emphasis(elem)
150
+ return false unless elem[:left_flanking]
151
+ return true unless elem[:char] == '_'
152
+
153
+ !elem[:right_flanking] || (elem[:right_flanking] && elem[:preceded_by_punc])
154
+ end
155
+
156
+ def can_close_emphasis(elem)
157
+ return false unless elem[:right_flanking]
158
+ return true unless elem[:char] == '_'
159
+
160
+ !elem[:left_flanking] || (elem[:left_flanking] && elem[:followed_by_punc])
161
+ end
162
+
163
+ def rule_of_three(opener, closer)
164
+ return true unless (can_open_emphasis(opener) && can_close_emphasis(opener)) ||
165
+ (can_open_emphasis(closer) && can_close_emphasis(closer))
166
+
167
+ ((opener[:length] % 3).zero? && (closer[:length] % 3).zero?) ||
168
+ (opener[:length] + closer[:length]) % 3 != 0
169
+ end
170
+
171
+ def used_delims_to_text(elems)
172
+ elems.map do |elem|
173
+ if elem.key?(:char)
174
+ next if elem[:length] < 1
175
+
176
+ { text: elem[:char] * elem[:length] }
177
+ else
178
+ elem
179
+ end
180
+ end.compact
181
+ end
182
+
183
+ def build_delim_stack(tree)
184
+ delim_stack = []
185
+ tree.each_with_index do |elem, idx|
186
+ next unless elem.is_a?(Hash) || elem.length != 1
187
+
188
+ key = elem.first.first
189
+ if %i[rsp rpp rfp].include?(key)
190
+ outer_key = key
191
+ tree[idx] = elem = elem[key]
192
+ key = elem.first.first
193
+ end
194
+ next unless %i[bfdr lfdr rfdr nfdr].include?(key)
195
+
196
+ delim_stack << idx
197
+ # pp elem
198
+ elem[:char] = elem[key].to_s[0]
199
+ elem[:length] = elem[key].length
200
+ elem[:left_flanking] = %i[bfdr lfdr].include?(key)
201
+ elem[:right_flanking] = %i[bfdr rfdr].include?(key)
202
+ elem[:preceded_by_punc] = %i[rsp rpp].include?(outer_key)
203
+ elem[:followed_by_punc] = %i[rsp rfp].include?(outer_key)
204
+ # elem[:active] = true
205
+ end
206
+ # pp delim_stack
207
+ delim_stack
208
+ end
209
+
210
+ def process_emphasis(tree)
211
+ delim_stack = build_delim_stack(tree)
212
+ cur_pos = 0
213
+ openers_bottom = { '*' => 0, '_' => 0 }
214
+ while (closer_offset = delim_stack[cur_pos..].index { |i| can_close_emphasis(tree[i]) })
215
+ # puts "-----"
216
+ # pp tree
217
+ # puts "clofset #{closer_offset} -> cur_pos #{closer_offset + cur_pos}"
218
+ cur_pos += closer_offset
219
+ closer = tree[closer_idx = delim_stack[cur_pos]]
220
+ # puts "closer:#{cur_pos}, #{closer}"
221
+ # look back - in reverse?
222
+ opener_bottom = openers_bottom[closer[:char]]
223
+ opener_to_cur = delim_stack.slice(opener_bottom, cur_pos - opener_bottom)
224
+ # puts "obottom #{opener_bottom} len #{cur_pos - opener_bottom} -> opener_to_cur #{opener_to_cur}"
225
+ opener_offset = (opener_to_cur || []).rindex do |i|
226
+ can_open_emphasis(tree[i]) && tree[i][:char] == closer[:char] && rule_of_three(tree[i], closer)
227
+ end
228
+ # puts "opener:#{opener_offset}"
229
+ if opener_offset
230
+ opener = tree[opener_idx = delim_stack[opener_bottom + opener_offset]]
231
+ strong = opener[:length] > 1 && closer[:length] > 1
232
+ # pp opener
233
+ contents_range = (opener_idx + 1)..(closer_idx - 1)
234
+ # puts "crange #{contents_range} size #{contents_range.size}"
235
+ contents = used_delims_to_text(tree.slice!(contents_range))
236
+ tree.insert(opener_idx + 1, { (strong ? :strong : :emph) => contents })
237
+ middle = (opener_bottom + opener_offset + 1)..(cur_pos - 1)
238
+ delim_stack.slice!(middle)
239
+ # puts "slice middle #{middle} -> #{delim_stack}"
240
+ delim_stack.map! { |i| i <= opener_idx ? i : (i - contents_range.size + 1) }
241
+ # puts "slice map <dstack.map!> #{delim_stack}"
242
+ cur_pos -= middle.size
243
+ if (opener[:length] -= strong ? 2 : 1).zero?
244
+ delim_stack.slice!(opener_bottom + opener_offset)
245
+ cur_pos -= 1
246
+ # puts "slice opener #{opener_bottom + opener_offset} -> #{delim_stack} @#{cur_pos}"
247
+ end
248
+ if (closer[:length] -= strong ? 2 : 1).zero?
249
+ delim_stack.slice!(cur_pos)
250
+ # puts "slice closer #{cur_pos} -> #{delim_stack}"
251
+ end
252
+ else
253
+ openers_bottom[closer[:chr]] = cur_pos - 1
254
+ if can_open_emphasis(closer)
255
+ cur_pos += 1
256
+ else
257
+ delim_stack.slice!(cur_pos)
258
+ # puts "nopener slice #{cur_pos} -> #{delim_stack}"
259
+ end
260
+ end
261
+ end
262
+ # puts "----------"
263
+ used_delims_to_text(tree)
264
+ # puts "----------"
265
+ # pp x
266
+ end
267
+
268
+ def parse(io, options = {})
269
+ process_emphasis(super(io, options))
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parslet'
4
+ require 'parslet/convenience'
5
+
6
+ module Coradoc
7
+ module Markdown
8
+ module Parser
9
+ module ParsletExtras
10
+ refine Parslet::Source do
11
+ def rewind(nchars)
12
+ # https://github.com/ruby/strscan/issues/122
13
+ self.charpos = @str.charpos - nchars
14
+ end
15
+
16
+ def charpos=(pos)
17
+ @str.reset
18
+ @str.getch while @str.charpos < pos
19
+ end
20
+
21
+ def charpos
22
+ @str.charpos
23
+ end
24
+
25
+ def peek_byte
26
+ @str.peek(1)
27
+ end
28
+ end
29
+
30
+ refine Parslet::Scope do
31
+ attr_reader :current
32
+
33
+ def key?(...)
34
+ @current.key?(...)
35
+ end
36
+
37
+ def has_key?(...)
38
+ @current.key?(...)
39
+ end
40
+
41
+ def root
42
+ scope = current
43
+ scope = scope.parent while scope.parent
44
+ scope
45
+ end
46
+ end
47
+
48
+ refine Parslet::Scope::Binding do
49
+ def key?(...)
50
+ @hash.key?(...)
51
+ end
52
+
53
+ def has_key?(...)
54
+ @hash.key?(...)
55
+ end
56
+
57
+ def initialize_copy(original)
58
+ super
59
+ @hash = @hash.clone
60
+ end
61
+ end
62
+
63
+ # like Named but returning other things
64
+ class Output < Parslet::Atoms::Base
65
+ attr_reader :parslet, :value
66
+
67
+ def initialize(parslet, value)
68
+ super()
69
+
70
+ @parslet = parslet
71
+ @value = value
72
+ end
73
+
74
+ def apply(source, context, consume_all)
75
+ success, = result = parslet.apply(source, context, consume_all)
76
+
77
+ return result unless success
78
+
79
+ succ(@value)
80
+ end
81
+
82
+ def to_s_inner(prec)
83
+ "#{value}:#{parslet.to_s(prec)}"
84
+ end
85
+ end
86
+
87
+ class DynamicOutput < Parslet::Atoms::Base
88
+ attr_reader :parslet, :callable
89
+
90
+ def initialize(parslet, callable)
91
+ super()
92
+
93
+ @parslet = parslet
94
+ @callable = callable
95
+ end
96
+
97
+ def apply(source, context, consume_all)
98
+ success, value = result = parslet.apply(source, context, consume_all)
99
+
100
+ return result unless success
101
+
102
+ succ(@callable.call(flatten(value)))
103
+ end
104
+
105
+ def to_s_inner(prec)
106
+ "#{callable}:#{parslet.to_s(prec)}"
107
+ end
108
+ end
109
+
110
+ class Lookbehind < Parslet::Atoms::Base
111
+ using ParsletExtras
112
+ attr_reader :positive
113
+ attr_reader :number, :bound_parslet
114
+
115
+ def initialize(bound_parslet, number, positive: true)
116
+ super()
117
+
118
+ # Model positive and negative lookbehind by testing this flag.
119
+ @positive = positive
120
+ @number = number
121
+ @bound_parslet = bound_parslet
122
+ end
123
+
124
+ def error_msgs
125
+ @error_msgs ||= {
126
+ positive: ['Input should be preceded by ', bound_parslet],
127
+ negative: ['Input should not be preceded by ', bound_parslet]
128
+ }
129
+ end
130
+
131
+ def try(source, context, consume_all)
132
+ rewind_pos = source.bytepos
133
+ if source.bytepos.zero?
134
+ return succ(nil) unless positive
135
+
136
+ return context.err_at(self, source, error_msgs[:positive], source.pos)
137
+ end
138
+ source.rewind(number)
139
+ error_pos = source.pos
140
+
141
+ success, = bound_parslet.apply(source, context, consume_all)
142
+
143
+ if positive
144
+ return succ(nil) if success
145
+
146
+ context.err_at(self, source, error_msgs[:positive], error_pos)
147
+ else
148
+ return succ(nil) unless success
149
+
150
+ context.err_at(self, source, error_msgs[:negative], error_pos)
151
+ end
152
+ ensure
153
+ source.bytepos = rewind_pos
154
+ end
155
+
156
+ def to_s_inner(prec)
157
+ @char = positive ? '&' : '!'
158
+ "<#{@char}<#{number}<#{bound_parslet.to_s(prec)}"
159
+ end
160
+ end
161
+
162
+ # Like Dynamic but does not return a further parslet, just a reject/accept boolean
163
+ module ::Parslet
164
+ module Atoms
165
+ class Check < ::Parslet::Atoms::Base
166
+ attr_reader :block
167
+
168
+ def initialize(block)
169
+ super()
170
+ @block = block
171
+ end
172
+
173
+ def cached?
174
+ false
175
+ end
176
+
177
+ def try(source, context, _consume_all)
178
+ [block.call(source, context), nil]
179
+ end
180
+
181
+ def to_s_inner(_prec)
182
+ 'check { ... }'
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ refine ::Parslet do
189
+ def check(&block)
190
+ ::Parslet::Atoms::Check.new(block)
191
+ end
192
+ module_function :check
193
+ end
194
+
195
+ refine ::Parslet::Atoms::DSL do
196
+ def output(value)
197
+ Output.new(self, value)
198
+ end
199
+
200
+ def dynamic_output(value)
201
+ DynamicOutput.new(self, value)
202
+ end
203
+
204
+ def precedes?(num = 1)
205
+ Lookbehind.new(self, num, positive: true)
206
+ end
207
+
208
+ def does_not_precede?(num = 1)
209
+ Lookbehind.new(self, num, positive: false)
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ module Parser
6
+ autoload :BlockParser, "#{__dir__}/block_parser"
7
+ autoload :InlineParser, "#{__dir__}/inline_parser"
8
+ autoload :AstProcessor, "#{__dir__}/ast_processor"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ # Shared parser utilities for Markdown processing
8
+ module ParserUtil
9
+ # Parser for IAL (Inline Attribute List) syntax
10
+ #
11
+ # IAL syntax: {:.class #id key="value"}
12
+ # Supports:
13
+ # - Classes: .classname or .-classname
14
+ # - IDs: #idname
15
+ # - Key-value pairs: key="value", key='value', or key=value
16
+ #
17
+ module IalParser
18
+ # Tokenize an IAL string into its components
19
+ # @param content [String] The IAL content (without braces)
20
+ # @return [Array<Hash>] Array of tokens with :type and :value
21
+ def self.tokenize(content)
22
+ tokens = []
23
+ scanner = StringScanner.new(content.to_s)
24
+
25
+ until scanner.eos?
26
+ scanner.skip(/\s+/)
27
+ break if scanner.eos?
28
+
29
+ if scanner.scan(/\.(-?\w[\w-]*)/)
30
+ tokens << { type: :class, value: scanner[1] }
31
+ elsif scanner.scan(/#(\w[\w-]*)/)
32
+ tokens << { type: :id, value: scanner[1] }
33
+ elsif scanner.scan(/(\w[\w-]*)\s*=\s*/)
34
+ key = scanner[1]
35
+ value = extract_quoted_value(scanner, handle_escapes: true)
36
+ tokens << { type: :attribute, key: key, value: value }
37
+ elsif scanner.scan(/\S+/)
38
+ # Skip unknown tokens
39
+ end
40
+ end
41
+
42
+ tokens
43
+ end
44
+
45
+ # Parse IAL content into a hash
46
+ # @param content [String] The IAL content
47
+ # @return [Hash] Parsed result with :id, :classes, :attributes keys
48
+ def self.parse_to_hash(content)
49
+ result = { id: nil, classes: [], attributes: {} }
50
+ return result if content.nil? || content.empty?
51
+
52
+ tokens = tokenize(content)
53
+ tokens.each do |token|
54
+ case token[:type]
55
+ when :class
56
+ result[:classes] << token[:value]
57
+ when :id
58
+ result[:id] = token[:value]
59
+ when :attribute
60
+ result[:attributes][token[:key]] = token[:value]
61
+ end
62
+ end
63
+
64
+ result
65
+ end
66
+
67
+ # Extract a quoted value from the scanner
68
+ # @param scanner [StringScanner]
69
+ # @param handle_escapes [Boolean] Whether to unescape \\" and \\'
70
+ # @return [String] The extracted value
71
+ def self.extract_quoted_value(scanner, handle_escapes: false)
72
+ if scanner.scan(/"([^"\\]*(?:\\.[^"\\]*)*)"/)
73
+ value = scanner[1]
74
+ value = value.gsub(/\\"/, '"') if handle_escapes
75
+ value
76
+ elsif scanner.scan(/'([^'\\]*(?:\\.[^'\\]*)*)'/)
77
+ value = scanner[1]
78
+ value = value.gsub(/\\'/, "'") if handle_escapes
79
+ value
80
+ elsif scanner.scan(/(\S+)/)
81
+ scanner[1]
82
+ else
83
+ ''
84
+ end
85
+ end
86
+ private_class_method :extract_quoted_value
87
+ end
88
+ end
89
+ end
90
+ end