mdom 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +165 -1
- data/README.md +50 -12
- data/USAGE.md +89 -14
- data/lib/mdom/block_detector.rb +293 -60
- data/lib/mdom/builder.rb +151 -27
- data/lib/mdom/error.rb +0 -4
- data/lib/mdom/html_serializer.rb +19 -1
- data/lib/mdom/inline_parser.rb +402 -169
- data/lib/mdom/markdown_serializer.rb +69 -11
- data/lib/mdom/node.rb +9 -1
- data/lib/mdom/parser.rb +30 -2
- data/lib/mdom/tokenizer.rb +64 -21
- data/lib/mdom/translator.rb +148 -148
- data/lib/mdom/version.rb +1 -1
- metadata +5 -5
data/lib/mdom/block_detector.rb
CHANGED
|
@@ -18,7 +18,7 @@ module MDOM
|
|
|
18
18
|
# 会让一处错误波及全篇。
|
|
19
19
|
class BlockDetector
|
|
20
20
|
# 会引导块的关键字(CSM §3.5.1 的 B 类)。大小写严格匹配。
|
|
21
|
-
GUIDING_KEYWORDS = %w[begin while until for if unless case def class module
|
|
21
|
+
GUIDING_KEYWORDS = %w[begin while until for if unless case def class module].freeze
|
|
22
22
|
|
|
23
23
|
# 关键字 → Prism 节点类。
|
|
24
24
|
KEYWORD_NODES = {
|
|
@@ -41,6 +41,48 @@ module MDOM
|
|
|
41
41
|
# * +render+ —— 是否以 `.render` 收尾。
|
|
42
42
|
Detection = Struct.new(:end_index, :form, :render, keyword_init: true)
|
|
43
43
|
|
|
44
|
+
# 候选开始后无条件尝试解析的行数(不论该行是否含 `end` / `}`)。散文候选通常
|
|
45
|
+
# 在第一行就是硬语法错误或完整非块语句,这一小段探针能让它立刻退出,避免把
|
|
46
|
+
# 整篇文档拼进 code、再到每个闭合行上重复 parse(见 docs/IMPLEMENTATION.md §2.2.4)。
|
|
47
|
+
EARLY_PROBE_LINES = 4
|
|
48
|
+
|
|
49
|
+
# C1-min 的 A 类候选开头。`[` 还要在 #classify 里排除 `[[`(WikiLink 优先)。
|
|
50
|
+
CANDIDATE_PREFIXES = ["(", "{", "%", "_", "::", "["].freeze
|
|
51
|
+
|
|
52
|
+
# 行首是否落在 A 类候选的预筛范围内(字母或 CANDIDATE_PREFIXES)。
|
|
53
|
+
def self.candidate_start?(content)
|
|
54
|
+
content.match?(/\A[A-Za-z]/) ||
|
|
55
|
+
CANDIDATE_PREFIXES.any? { |prefix| content.start_with?(prefix) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 判定器的配置。
|
|
59
|
+
#
|
|
60
|
+
# * +free_blocks:+ —— **A 类(自由块)判定的总开关**,默认 +true+(CSM §3.5)。
|
|
61
|
+
# 置 +false+ 时不做任何 A 类判定,命中行按**排版**处理(CSM §3.5.3,不报错)。
|
|
62
|
+
# B 类(引导关键字)与 `~` 行不受影响。
|
|
63
|
+
# * +free_blocks_allowed:+ —— A 类的**白名单**,默认 +nil+(不限制)。给出后,
|
|
64
|
+
# 只有行首标识符命中的行才尝试 A 类判定:+String+ 按标识符**精确相等**匹配
|
|
65
|
+
# (大小写严格),+Regexp+ 对**已剥离 CSM 缩进的整行**做 +match?+(要不要锚定
|
|
66
|
+
# 行首由自己写的 `\A` 决定)。空数组等价于 +free_blocks: false+。
|
|
67
|
+
#
|
|
68
|
+
# 之所以要这道闸门:英文散文里「以字母开头的行」比比皆是,每行都交给 Prism
|
|
69
|
+
# 增量 parse 会让一篇普通文档产生几百到上万次调用(docs/IMPLEMENTATION.md §2.2.4)。
|
|
70
|
+
def initialize(free_blocks: true, free_blocks_allowed: nil)
|
|
71
|
+
@free_blocks = free_blocks
|
|
72
|
+
@free_blocks_allowed = free_blocks_allowed
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 行首是否值得进入块判定——CSM §3.5.4 的「收窄」,外加 A 类门控。
|
|
76
|
+
#
|
|
77
|
+
# Tokenizer 用它做预筛,从而对注定不会被判定的行连 +detect+ 都不必调用。
|
|
78
|
+
# +first+ 是**已剥离 CSM 缩进**的首行内容。B 类与引导关键字一律值得判定。
|
|
79
|
+
def candidate?(first)
|
|
80
|
+
kind, = classify(first)
|
|
81
|
+
return false unless kind
|
|
82
|
+
|
|
83
|
+
kind == :keyword || free_block_candidate?(first)
|
|
84
|
+
end
|
|
85
|
+
|
|
44
86
|
# 在 +lines+(已规范化:tab 展开、去 `\r`)中,从 +start_index+ 起判定候选块。
|
|
45
87
|
#
|
|
46
88
|
# +indent+ 是候选行的 CSM 缩进(前导空格数)。返回 +Detection+ 或 +nil+。
|
|
@@ -51,76 +93,260 @@ module MDOM
|
|
|
51
93
|
first = drop_indent(lines[start_index], indent)
|
|
52
94
|
kind, keyword = classify(first)
|
|
53
95
|
return nil unless kind
|
|
96
|
+
# A 类被关掉或行首不在白名单里 → 按排版处理,且**在碰 Prism 之前**就收手。
|
|
97
|
+
return nil if kind == :call && !free_block_candidate?(first)
|
|
54
98
|
|
|
55
|
-
# 增量 parse
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
99
|
+
# 增量 parse 只在**候选行到同级块尾的范围内**进行(CSM §3.5.4):范围由更浅
|
|
100
|
+
# 的缩进收尾,**空行不中断**——块内的空行是 Ruby 的普通空行,交给 prism 决定
|
|
101
|
+
# 块的实际收尾。先无条件探前几行,让散文候选就地失败。
|
|
102
|
+
code = String.new
|
|
103
|
+
cursor = start_index
|
|
104
|
+
early_stop = [start_index + EARLY_PROBE_LINES - 1, lines.size - 1].min
|
|
105
|
+
|
|
106
|
+
while cursor <= early_stop
|
|
107
|
+
line = lines[cursor]
|
|
108
|
+
boundary = cursor > start_index && boundary_line?(line, indent)
|
|
109
|
+
code << drop_indent(line, indent) << "\n"
|
|
110
|
+
|
|
111
|
+
verdict = evaluate(code, lines, start_index, cursor, indent, kind, keyword,
|
|
112
|
+
closing: closing_line?(line))
|
|
113
|
+
return verdict unless verdict == :continue
|
|
114
|
+
return nil if boundary
|
|
115
|
+
|
|
116
|
+
cursor += 1
|
|
117
|
+
end
|
|
118
|
+
return nil if cursor >= lines.size
|
|
119
|
+
|
|
120
|
+
closers = closing_candidates(lines, cursor, indent)
|
|
121
|
+
return nil if closers.empty?
|
|
122
|
+
|
|
123
|
+
source, offsets = prefix_source(lines, start_index, closers.last, indent)
|
|
124
|
+
divide_and_conquer(source, offsets, lines, start_index, closers,
|
|
125
|
+
indent, kind, keyword)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
63
129
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
130
|
+
# 解析一个前缀并给出结论:+Detection+ / +nil+ / +:continue+。
|
|
131
|
+
# +closing+ 为 false 时,完整但非块的前缀还要检查下一行是否是 Ruby 的
|
|
132
|
+
# leading-dot 续行(`foo\n .each do ... end`)。
|
|
133
|
+
def evaluate(code, lines, start_index, last, indent, kind, keyword,
|
|
134
|
+
closing:, result: nil)
|
|
135
|
+
result ||= parse(code)
|
|
136
|
+
return nil if result.nil?
|
|
137
|
+
# 错误恢复可能把散文解析成 message_loc 为空(长度为 0 / 不存在)的
|
|
138
|
+
# CallNode,例如 `I do this till end.`;这种节点不能作为自由块。
|
|
139
|
+
return nil if kind == :call && result.failure? && empty_call_message?(result)
|
|
140
|
+
return :continue if result.failure? && incomplete?(result, code)
|
|
141
|
+
return nil unless result.success?
|
|
70
142
|
|
|
71
|
-
|
|
72
|
-
|
|
143
|
+
statements = result.value.statements.body
|
|
144
|
+
# 顶层已经不止一条语句:第一条既已闭合,再追加行也覆盖不到整段。
|
|
145
|
+
return nil if statements.size >= 2
|
|
73
146
|
|
|
147
|
+
node = statements.last
|
|
148
|
+
return :continue if node.nil?
|
|
149
|
+
|
|
150
|
+
line_count = last - start_index + 1
|
|
151
|
+
loc = node.location
|
|
152
|
+
last_line = code.lines.last.to_s.rstrip
|
|
153
|
+
covers = loc.start_line == 1 &&
|
|
154
|
+
loc.start_column.zero? &&
|
|
155
|
+
loc.end_line == line_count &&
|
|
156
|
+
loc.end_column == last_line.bytesize
|
|
157
|
+
return :continue unless covers
|
|
158
|
+
|
|
159
|
+
match = kind == :keyword ? keyword_match(node, keyword) : call_match(node)
|
|
160
|
+
if match
|
|
74
161
|
assert_same_indent!(lines, start_index, last, indent)
|
|
75
|
-
assert_no_heredoc!(code, start_index, last)
|
|
76
162
|
return Detection.new(end_index: last, form: match[:form], render: match[:render])
|
|
77
163
|
end
|
|
164
|
+
return :continue if !closing && leading_continuation?(lines, last)
|
|
165
|
+
|
|
78
166
|
nil
|
|
79
167
|
end
|
|
80
168
|
|
|
81
|
-
|
|
169
|
+
# 对闭合候选行做分治:找到**最早**一个不再 incomplete 的前缀。真实块在闭合前
|
|
170
|
+
# 始终 incomplete,闭合后才会成为完整语句,于是可以在候选下标上二分,而不是逐个
|
|
171
|
+
# 闭合行调用 Prism。
|
|
172
|
+
def divide_and_conquer(source, offsets, lines, start_index, closers,
|
|
173
|
+
indent, kind, keyword)
|
|
174
|
+
low = 0
|
|
175
|
+
high = closers.size - 1
|
|
176
|
+
first = nil
|
|
177
|
+
cached = nil
|
|
82
178
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return
|
|
90
|
-
|
|
179
|
+
while low <= high
|
|
180
|
+
mid = (low + high) / 2
|
|
181
|
+
last = closers[mid]
|
|
182
|
+
code = prefix(source, offsets, start_index, last)
|
|
183
|
+
result = parse(code)
|
|
184
|
+
return nil if result.nil?
|
|
185
|
+
return nil if kind == :call && result.failure? && empty_call_message?(result)
|
|
186
|
+
|
|
187
|
+
if result.failure? && incomplete?(result, code) && !first_statement_closed?(result)
|
|
188
|
+
low = mid + 1
|
|
189
|
+
else
|
|
190
|
+
first = mid
|
|
191
|
+
cached = [last, code, result]
|
|
192
|
+
high = mid - 1
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
return nil if first.nil?
|
|
196
|
+
|
|
197
|
+
last, code, result = cached
|
|
198
|
+
verdict = evaluate(code, lines, start_index, last, indent, kind, keyword,
|
|
199
|
+
closing: true, result: result)
|
|
200
|
+
return verdict unless verdict == :continue
|
|
201
|
+
|
|
202
|
+
(first + 1...closers.size).each do |index|
|
|
203
|
+
last = closers[index]
|
|
204
|
+
code = prefix(source, offsets, start_index, last)
|
|
205
|
+
verdict = evaluate(code, lines, start_index, last, indent, kind, keyword, closing: true)
|
|
206
|
+
return verdict unless verdict == :continue
|
|
91
207
|
end
|
|
92
|
-
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# 前缀末尾可能落在后续散文的未完成构造里,使整段 parse 报 incomplete;但第一个
|
|
212
|
+
# 顶层语句其实已经闭合。此时不能把它当成“块还没结束”而向右二分。
|
|
213
|
+
def first_statement_closed?(result)
|
|
214
|
+
node = result.value.statements.body.first
|
|
215
|
+
return false if node.nil?
|
|
216
|
+
|
|
217
|
+
location = node.location
|
|
218
|
+
return false unless location.start_line == 1 && location.start_column.zero?
|
|
219
|
+
|
|
220
|
+
result.errors.all? { |error| error.location.start_offset > location.end_offset }
|
|
93
221
|
end
|
|
94
222
|
|
|
95
|
-
|
|
223
|
+
def prefix(source, offsets, start_index, last)
|
|
224
|
+
source.byteslice(0, offsets[last - start_index])
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# 一次性构造候选到最后一个闭合行的源码,并记下每行结束的字节偏移;分治的各次
|
|
228
|
+
# 探测只需要 byteslice,不再反复 map/join。
|
|
229
|
+
def prefix_source(lines, start_index, last_index, indent)
|
|
230
|
+
parts = Array.new(last_index - start_index + 1)
|
|
231
|
+
offsets = Array.new(parts.size)
|
|
232
|
+
bytes = 0
|
|
233
|
+
|
|
234
|
+
parts.each_index do |index|
|
|
235
|
+
text = drop_indent(lines[start_index + index], indent)
|
|
236
|
+
parts[index] = text
|
|
237
|
+
bytes += text.bytesize + 1
|
|
238
|
+
offsets[index] = bytes
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
["#{parts.join("\n")}\n", offsets]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# 闭合行候选:含 `end` 或 `}`,且与旧实现使用同一套正则收窄。扫描到同级块尾
|
|
245
|
+
# (更浅的非空行,包含在内)为止。
|
|
246
|
+
def closing_candidates(lines, from, indent)
|
|
247
|
+
candidates = []
|
|
248
|
+
cursor = from
|
|
249
|
+
|
|
250
|
+
while cursor < lines.size
|
|
251
|
+
line = lines[cursor]
|
|
252
|
+
candidates << cursor if closing_line?(line)
|
|
253
|
+
break if boundary_line?(line, indent)
|
|
254
|
+
|
|
255
|
+
cursor += 1
|
|
256
|
+
end
|
|
257
|
+
candidates
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def closing_line?(line)
|
|
261
|
+
return false unless line.include?("end") || line.include?("}")
|
|
262
|
+
|
|
263
|
+
line =~ /(([^A-Za-z0-9_-]|^)end([^A-Za-z0-9_-]|$))|(})/
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# 非空且缩进比块首更浅的行是块尾边界;它**包含在候选范围内**,供跨级缩进校验。
|
|
267
|
+
def boundary_line?(line, indent)
|
|
268
|
+
!line.strip.empty? && indent_width(line) < indent
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Ruby 的 leading-dot 续行:`foo` 下一行以 `.` / `&.` 开头时仍属于同一表达式,
|
|
272
|
+
# 因此完整但非块的前缀不能立刻判死。空行与注释行跳过(保守处理)。
|
|
273
|
+
def leading_continuation?(lines, index)
|
|
274
|
+
cursor = index + 1
|
|
275
|
+
|
|
276
|
+
while cursor < lines.size
|
|
277
|
+
stripped = lines[cursor].strip
|
|
278
|
+
if stripped.empty? || stripped.start_with?("#")
|
|
279
|
+
cursor += 1
|
|
280
|
+
next
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
return stripped.start_with?(".") || stripped.start_with?("&.")
|
|
284
|
+
end
|
|
285
|
+
false
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# 收窄:行首词是引导块关键字 → B 类;否则字母或 C1-min 开头 → A 类。
|
|
289
|
+
# `[[` 一律按 WikiLink 处理,不进入 Ruby 候选;`__END__` 留给 Tokenizer。
|
|
96
290
|
def classify(first)
|
|
291
|
+
return [nil, nil] if first == "__END__" || first.start_with?("[[")
|
|
292
|
+
|
|
97
293
|
word = first[/\A[A-Za-z_][A-Za-z0-9_]*/]
|
|
98
|
-
return [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
294
|
+
return [:keyword, word] if word && GUIDING_KEYWORDS.include?(word)
|
|
295
|
+
return [:call, nil] if self.class.candidate_start?(first)
|
|
296
|
+
|
|
297
|
+
[nil, nil]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# A 类(自由块)的候选资格:总开关 + 白名单。
|
|
301
|
+
#
|
|
302
|
+
# 与 +classify+ 共用同一条「行首标识符」定义,所以白名单里的 +String+ 与
|
|
303
|
+
# B 类关键字一样是**精确相等**(`"list"` 不匹配 `lists.each do`),不做前缀模糊。
|
|
304
|
+
def free_block_candidate?(first)
|
|
305
|
+
return false unless @free_blocks
|
|
306
|
+
return true if @free_blocks_allowed.nil?
|
|
307
|
+
|
|
308
|
+
word = first[/\A[A-Za-z_][A-Za-z0-9_]*/]
|
|
309
|
+
@free_blocks_allowed.any? do |entry|
|
|
310
|
+
entry.is_a?(Regexp) ? entry.match?(first) : entry == word
|
|
106
311
|
end
|
|
107
312
|
end
|
|
108
313
|
|
|
109
314
|
def parse(code)
|
|
110
|
-
|
|
111
|
-
result.success? ? result : nil
|
|
315
|
+
Prism.parse(code)
|
|
112
316
|
rescue StandardError
|
|
113
317
|
nil
|
|
114
318
|
end
|
|
115
319
|
|
|
116
|
-
#
|
|
320
|
+
# 解析失败是否只是「还缺东西」:
|
|
321
|
+
#
|
|
322
|
+
# * prism 把错误报在**输入末尾**(`unexpected end-of-input`),或
|
|
323
|
+
# * 报成 +*_term+——缺 `end` / `)` / `]` 等收尾(`case x` 的 +case_term+ 属此类,
|
|
324
|
+
# 它的错误位置在构造起始处而非末尾,光看末尾看不出来)。
|
|
325
|
+
#
|
|
326
|
+
# 以上都值得再加一行试试,其余是行内已定的硬错误、加多少行也补不上。末尾空白
|
|
327
|
+
# (含换行)会被 prism 跳过,因此 EOF 位置取 +rstrip+ 之后的字节数。
|
|
328
|
+
# (+Prism::ParseError#type+ 是 prism 的内部符号,这里只当提示用,取不到就退回
|
|
329
|
+
# 「只看末尾」。)
|
|
330
|
+
def incomplete?(result, code)
|
|
331
|
+
eof = code.rstrip.bytesize
|
|
332
|
+
result.errors.any? { |error| error.location.start_offset >= eof || terminator?(error) }
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def terminator?(error)
|
|
336
|
+
# error.respond_to?(:type) && error.type.to_s.include?("_term")
|
|
337
|
+
error.type.to_s.include?("_term") # 有大概20%的性能提升,且未产生错误
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# 顶层语句是否恰好覆盖「整段代码」。prism 的行内列号(+end_column+)按**字节**
|
|
341
|
+
# 计,因此这里比的是 +bytesize+,不是字符长度——否则末行含 CJK 时会误判为不覆盖,
|
|
342
|
+
# 循着空行一路追加到文末。
|
|
117
343
|
def covers?(node, code, line_count)
|
|
118
344
|
loc = node.location
|
|
119
345
|
last_line = code.lines.last.to_s.rstrip
|
|
120
346
|
loc.start_line == 1 &&
|
|
121
347
|
loc.start_column.zero? &&
|
|
122
348
|
loc.end_line == line_count &&
|
|
123
|
-
loc.end_column == last_line.
|
|
349
|
+
loc.end_column == last_line.bytesize
|
|
124
350
|
end
|
|
125
351
|
|
|
126
352
|
# A 类:`CallNode`(一个或多个)→ 带 `closing_loc` 的 `BlockNode`。
|
|
@@ -134,14 +360,33 @@ module MDOM
|
|
|
134
360
|
end
|
|
135
361
|
return nil unless node.is_a?(Prism::CallNode)
|
|
136
362
|
|
|
363
|
+
match = nil
|
|
137
364
|
cursor = node
|
|
138
365
|
while cursor.is_a?(Prism::CallNode)
|
|
366
|
+
# 自由块必须是**有名字的**调用;`foo.() do ... end` 这种 message_loc 为空的
|
|
367
|
+
# 调用不算,避免把错误恢复出的空方法名当成 Ruby 块。
|
|
368
|
+
return nil if empty_message_loc?(cursor)
|
|
369
|
+
|
|
139
370
|
block = cursor.block
|
|
140
|
-
|
|
371
|
+
match ||= { form: :free, render: render } if block.is_a?(Prism::BlockNode) && block.closing_loc
|
|
141
372
|
|
|
142
373
|
cursor = cursor.receiver
|
|
143
374
|
end
|
|
144
|
-
|
|
375
|
+
match
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def empty_message_loc?(node)
|
|
379
|
+
location = node.message_loc
|
|
380
|
+
location.nil? || location.start_offset == location.end_offset
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# 错误恢复出的顶层语句可能是 message_loc 为空的 CallNode(如
|
|
384
|
+
# `I do this till end.`);只检查顶层语句,避免遍历整棵错误恢复 AST。
|
|
385
|
+
# 成功结果由 #call_match 沿 receiver 链检查。
|
|
386
|
+
def empty_call_message?(result)
|
|
387
|
+
result.value.statements.body.any? do |node|
|
|
388
|
+
node.is_a?(Prism::CallNode) && empty_message_loc?(node)
|
|
389
|
+
end
|
|
145
390
|
end
|
|
146
391
|
|
|
147
392
|
# B 类:Prism 给出的节点类必须与首行关键字一致。
|
|
@@ -169,29 +414,17 @@ module MDOM
|
|
|
169
414
|
|
|
170
415
|
raise CrossLevelError,
|
|
171
416
|
"ruby block opens with #{indent} space(s) of indentation " \
|
|
172
|
-
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# CSM §3.12 / 附录 E:heredoc(`<<~SQL` 等)**明确不支持**。既然块判定本来就
|
|
176
|
-
# 要交给 Prism,就顺便让它把 heredoc 也认出来——被判定为块的内容里出现
|
|
177
|
-
# +HEREDOC_START+ 就报错,而不是当普通块收下。
|
|
178
|
-
def assert_no_heredoc!(code, start_index, last_index)
|
|
179
|
-
return unless heredoc?(code)
|
|
180
|
-
|
|
181
|
-
raise UnsupportedHeredocError,
|
|
182
|
-
"heredoc is not supported (CSM §3.12): " \
|
|
183
|
-
"line #{start_index + 1}..#{last_index + 1}"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def heredoc?(code)
|
|
187
|
-
Prism.lex(code).value.any? { |token, _state| token.type.to_s == "HEREDOC_START" }
|
|
417
|
+
"(line #{start_index + 1}) but closes with #{found} (line #{last_index + 1})"
|
|
188
418
|
end
|
|
189
419
|
|
|
420
|
+
# 与 Tokenizer#indent_width 同一实现(见那里的说明)。
|
|
190
421
|
def indent_width(line)
|
|
191
|
-
line[
|
|
422
|
+
line.index(/[^ \t]/) || line.length
|
|
192
423
|
end
|
|
193
424
|
|
|
194
425
|
def drop_indent(line, count)
|
|
426
|
+
return line if count.zero?
|
|
427
|
+
|
|
195
428
|
index = 0
|
|
196
429
|
index += 1 while index < count && index < line.length && line[index] == " "
|
|
197
430
|
line[index..]
|