mdom 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +71 -0
- data/LICENSE.txt +21 -0
- data/README.md +285 -0
- data/USAGE.md +789 -0
- data/lib/mdom/block_detector.rb +200 -0
- data/lib/mdom/builder.rb +378 -163
- data/lib/mdom/error.rb +22 -0
- data/lib/mdom/hooks.rb +81 -0
- data/lib/mdom/html_serializer.rb +330 -0
- data/lib/mdom/inline_parser.rb +541 -142
- data/lib/mdom/markdown_serializer.rb +332 -0
- data/lib/mdom/node/inline_nodes.rb +239 -0
- data/lib/mdom/node/nodes.rb +336 -0
- data/lib/mdom/node.rb +677 -199
- data/lib/mdom/parser.rb +257 -235
- data/lib/mdom/tokenizer.rb +415 -0
- data/lib/mdom/translator.rb +379 -0
- data/lib/mdom/version.rb +1 -1
- data/lib/mdom.rb +58 -7
- metadata +40 -7
- data/lib/mdom/nodes.rb +0 -214
- data/lib/mdom/serializer.rb +0 -165
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "error"
|
|
6
|
+
|
|
7
|
+
module MDOM
|
|
8
|
+
# 用 Ruby 官方解析器(Prism)判定「不带 `begin` 前缀的 Ruby 块」与
|
|
9
|
+
# 「引导块关键字段落」(CSM §3.5)。
|
|
10
|
+
#
|
|
11
|
+
# 之所以必须交给 Prism,是因为 `do` / `end` 在英语里也是常见单词,纯词法
|
|
12
|
+
# 无法区分「Ruby 块」与「英文散文」(CSM §3.5.1)。判定只在 CSM → AST1
|
|
13
|
+
# 这一侧发生(docs/IMPLEMENTATION.md §1.1);AST1 一旦产出,后续阶段不再
|
|
14
|
+
# 需要任何词法能力。
|
|
15
|
+
#
|
|
16
|
+
# 判定按候选块**增量 parse**(CSM §3.5.4):从候选行开始逐行增加,只解析
|
|
17
|
+
# 这一段,而不是整篇一次解析——文档里的英文散文可能构成语法错误,整篇解析
|
|
18
|
+
# 会让一处错误波及全篇。
|
|
19
|
+
class BlockDetector
|
|
20
|
+
# 会引导块的关键字(CSM §3.5.1 的 B 类)。大小写严格匹配。
|
|
21
|
+
GUIDING_KEYWORDS = %w[begin while until for if unless case def class module do].freeze
|
|
22
|
+
|
|
23
|
+
# 关键字 → Prism 节点类。
|
|
24
|
+
KEYWORD_NODES = {
|
|
25
|
+
"begin" => Prism::BeginNode,
|
|
26
|
+
"while" => Prism::WhileNode,
|
|
27
|
+
"until" => Prism::UntilNode,
|
|
28
|
+
"for" => Prism::ForNode,
|
|
29
|
+
"if" => Prism::IfNode,
|
|
30
|
+
"unless" => Prism::UnlessNode,
|
|
31
|
+
"case" => Prism::CaseNode,
|
|
32
|
+
"def" => Prism::DefNode,
|
|
33
|
+
"class" => Prism::ClassNode,
|
|
34
|
+
"module" => Prism::ModuleNode
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# 判定结果。
|
|
38
|
+
#
|
|
39
|
+
# * +end_index+ —— 块尾行在 +lines+ 中的下标(0 起);
|
|
40
|
+
# * +form+ —— +:begin+(B 类)/ +:free+(A 类);
|
|
41
|
+
# * +render+ —— 是否以 `.render` 收尾。
|
|
42
|
+
Detection = Struct.new(:end_index, :form, :render, keyword_init: true)
|
|
43
|
+
|
|
44
|
+
# 在 +lines+(已规范化:tab 展开、去 `\r`)中,从 +start_index+ 起判定候选块。
|
|
45
|
+
#
|
|
46
|
+
# +indent+ 是候选行的 CSM 缩进(前导空格数)。返回 +Detection+ 或 +nil+。
|
|
47
|
+
#
|
|
48
|
+
# 抛出 +CrossLevelError+,当 Prism 判定为块、但块尾行的缩进与块首行不同
|
|
49
|
+
# (CSM §3.5.2 条件 1)。
|
|
50
|
+
def detect(lines, start_index, indent)
|
|
51
|
+
first = drop_indent(lines[start_index], indent)
|
|
52
|
+
kind, keyword = classify(first)
|
|
53
|
+
return nil unless kind
|
|
54
|
+
|
|
55
|
+
# 增量 parse 只在**候选所在的 CSM 块范围内**进行(CSM §3.5.4):块由空行
|
|
56
|
+
# 或更浅的缩进收尾。逐行增加,直到解析成为以候选行起始的完整顶层语句,
|
|
57
|
+
# 立即给出结论——不做整篇一次解析,散文里的语法错误不会波及全篇。
|
|
58
|
+
(start_index..block_end(lines, start_index, indent)).each do |last|
|
|
59
|
+
line_count = last - start_index + 1
|
|
60
|
+
code = (start_index..last).map { |i| drop_indent(lines[i], indent) }.join("\n")
|
|
61
|
+
result = parse(code)
|
|
62
|
+
next unless result
|
|
63
|
+
|
|
64
|
+
node = result.value.statements.body.last
|
|
65
|
+
next if node.nil?
|
|
66
|
+
# 顶层语句必须恰好从候选行开始、到当前行结束;一旦成立就说明
|
|
67
|
+
# 「从候选行起的一个完整 Ruby 语句到此为止」,无论是否匹配块结构,
|
|
68
|
+
# 都不必再往下扩展。
|
|
69
|
+
next unless covers?(node, code, line_count)
|
|
70
|
+
|
|
71
|
+
match = kind == :keyword ? keyword_match(node, keyword) : call_match(node)
|
|
72
|
+
return nil unless match
|
|
73
|
+
|
|
74
|
+
assert_same_indent!(lines, start_index, last, indent)
|
|
75
|
+
assert_no_heredoc!(code, start_index, last)
|
|
76
|
+
return Detection.new(end_index: last, form: match[:form], render: match[:render])
|
|
77
|
+
end
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# 候选所在的 CSM 块的最后一行(CSM §1.1:块由空行或更浅的缩进收尾)。
|
|
84
|
+
#
|
|
85
|
+
# 更浅缩进的那一行**包含在内**——块尾缩进与块首不同时要能按 §3.5.2 报错,
|
|
86
|
+
# 而不是静默降级成排版。
|
|
87
|
+
def block_end(lines, start_index, indent)
|
|
88
|
+
(start_index + 1...lines.size).each do |i|
|
|
89
|
+
return i - 1 if lines[i].strip.empty?
|
|
90
|
+
return i if indent_width(lines[i]) < indent
|
|
91
|
+
end
|
|
92
|
+
lines.size - 1
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 收窄:行首词是引导块关键字 → B 类;否则行首是字母 → A 类;否则不判定。
|
|
96
|
+
def classify(first)
|
|
97
|
+
word = first[/\A[A-Za-z_][A-Za-z0-9_]*/]
|
|
98
|
+
return [nil, nil] unless word
|
|
99
|
+
|
|
100
|
+
if GUIDING_KEYWORDS.include?(word)
|
|
101
|
+
[:keyword, word]
|
|
102
|
+
elsif first.match?(/\A[A-Za-z]/)
|
|
103
|
+
[:call, nil]
|
|
104
|
+
else
|
|
105
|
+
[nil, nil]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse(code)
|
|
110
|
+
result = Prism.parse(code)
|
|
111
|
+
result.success? ? result : nil
|
|
112
|
+
rescue StandardError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# 顶层语句是否恰好覆盖「整段代码」。
|
|
117
|
+
def covers?(node, code, line_count)
|
|
118
|
+
loc = node.location
|
|
119
|
+
last_line = code.lines.last.to_s.rstrip
|
|
120
|
+
loc.start_line == 1 &&
|
|
121
|
+
loc.start_column.zero? &&
|
|
122
|
+
loc.end_line == line_count &&
|
|
123
|
+
loc.end_column == last_line.length
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# A 类:`CallNode`(一个或多个)→ 带 `closing_loc` 的 `BlockNode`。
|
|
127
|
+
#
|
|
128
|
+
# 允许链上出现多个 `CallNode`,这自然覆盖了 `end.render` 这类收尾。
|
|
129
|
+
def call_match(node)
|
|
130
|
+
render = false
|
|
131
|
+
if render_call?(node)
|
|
132
|
+
render = true
|
|
133
|
+
node = node.receiver
|
|
134
|
+
end
|
|
135
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
136
|
+
|
|
137
|
+
cursor = node
|
|
138
|
+
while cursor.is_a?(Prism::CallNode)
|
|
139
|
+
block = cursor.block
|
|
140
|
+
return { form: :free, render: render } if block.is_a?(Prism::BlockNode) && block.closing_loc
|
|
141
|
+
|
|
142
|
+
cursor = cursor.receiver
|
|
143
|
+
end
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# B 类:Prism 给出的节点类必须与首行关键字一致。
|
|
148
|
+
def keyword_match(node, keyword)
|
|
149
|
+
render = false
|
|
150
|
+
if render_call?(node)
|
|
151
|
+
render = true
|
|
152
|
+
node = node.receiver
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
expected = KEYWORD_NODES[keyword]
|
|
156
|
+
return nil unless expected && node.is_a?(expected)
|
|
157
|
+
|
|
158
|
+
{ form: :begin, render: render }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def render_call?(node)
|
|
162
|
+
node.is_a?(Prism::CallNode) && node.name == :render && node.block.nil?
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# CSM §3.5.2 条件 1:块首行与块尾行的缩进必须完全相同。
|
|
166
|
+
def assert_same_indent!(lines, start_index, last_index, indent)
|
|
167
|
+
found = indent_width(lines[last_index])
|
|
168
|
+
return if found == indent
|
|
169
|
+
|
|
170
|
+
raise CrossLevelError,
|
|
171
|
+
"ruby block opens with #{indent} space(s) of indentation " \
|
|
172
|
+
"(line #{start_index + 1}) but closes with #{found} (line #{last_index + 1})"
|
|
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" }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def indent_width(line)
|
|
191
|
+
line[/\A[ \t]*/].to_s.length
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def drop_indent(line, count)
|
|
195
|
+
index = 0
|
|
196
|
+
index += 1 while index < count && index < line.length && line[index] == " "
|
|
197
|
+
line[index..]
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|