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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/lib/coradoc/markdown/errors.rb +28 -0
- data/lib/coradoc/markdown/model/abbreviation.rb +27 -0
- data/lib/coradoc/markdown/model/attribute_list.rb +98 -0
- data/lib/coradoc/markdown/model/base.rb +86 -0
- data/lib/coradoc/markdown/model/blockquote.rb +21 -0
- data/lib/coradoc/markdown/model/code.rb +11 -0
- data/lib/coradoc/markdown/model/code_block.rb +24 -0
- data/lib/coradoc/markdown/model/definition_item.rb +24 -0
- data/lib/coradoc/markdown/model/definition_list.rb +47 -0
- data/lib/coradoc/markdown/model/definition_term.rb +21 -0
- data/lib/coradoc/markdown/model/document.rb +39 -0
- data/lib/coradoc/markdown/model/emphasis.rb +11 -0
- data/lib/coradoc/markdown/model/extension.rb +92 -0
- data/lib/coradoc/markdown/model/footnote.rb +31 -0
- data/lib/coradoc/markdown/model/footnote_reference.rb +22 -0
- data/lib/coradoc/markdown/model/heading.rb +44 -0
- data/lib/coradoc/markdown/model/highlight.rb +18 -0
- data/lib/coradoc/markdown/model/horizontal_rule.rb +16 -0
- data/lib/coradoc/markdown/model/image.rb +19 -0
- data/lib/coradoc/markdown/model/link.rb +19 -0
- data/lib/coradoc/markdown/model/list.rb +22 -0
- data/lib/coradoc/markdown/model/list_item.rb +29 -0
- data/lib/coradoc/markdown/model/math.rb +50 -0
- data/lib/coradoc/markdown/model/paragraph.rb +28 -0
- data/lib/coradoc/markdown/model/strikethrough.rb +18 -0
- data/lib/coradoc/markdown/model/strong.rb +11 -0
- data/lib/coradoc/markdown/model/table.rb +13 -0
- data/lib/coradoc/markdown/model/text.rb +15 -0
- data/lib/coradoc/markdown/parser/ast_processor.rb +543 -0
- data/lib/coradoc/markdown/parser/block_parser.rb +745 -0
- data/lib/coradoc/markdown/parser/html_entities.rb +2149 -0
- data/lib/coradoc/markdown/parser/inline_parser.rb +274 -0
- data/lib/coradoc/markdown/parser/parslet_extras.rb +215 -0
- data/lib/coradoc/markdown/parser.rb +11 -0
- data/lib/coradoc/markdown/parser_util.rb +90 -0
- data/lib/coradoc/markdown/serializer.rb +199 -0
- data/lib/coradoc/markdown/toc_generator.rb +215 -0
- data/lib/coradoc/markdown/transform/from_core_model.rb +325 -0
- data/lib/coradoc/markdown/transform/text_extraction.rb +19 -0
- data/lib/coradoc/markdown/transform/to_core_model.rb +287 -0
- data/lib/coradoc/markdown/transformer.rb +463 -0
- data/lib/coradoc/markdown/version.rb +7 -0
- data/lib/coradoc/markdown.rb +190 -0
- 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
|