mdom 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.
- 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 +486 -0
- 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 +59 -7
- metadata +41 -7
- data/lib/mdom/nodes.rb +0 -214
- data/lib/mdom/serializer.rb +0 -165
data/lib/mdom/inline_parser.rb
CHANGED
|
@@ -1,185 +1,584 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "node/inline_nodes"
|
|
6
|
+
|
|
3
7
|
module MDOM
|
|
4
|
-
#
|
|
5
|
-
#
|
|
8
|
+
# 行内解析器:把一段**行内文本**解析为行内节点数组(docs/IMPLEMENTATION.md §2.4)。
|
|
9
|
+
#
|
|
10
|
+
# 覆盖 CSM 的行内语法:行内代码(§1.5)、强调(§1.7)、增删(§1.8)、行内引用
|
|
11
|
+
# (§1.9)、进度与度量(§1.10)、公式(§1.11)、WikiLink(§2.1)、提及(§2.2)、
|
|
12
|
+
# 链接与图片(§2.3)、行首转义与行内转义(§3.11)、插值(§3.13),以及软换行
|
|
13
|
+
# 折叠与硬换行(§1.1)。
|
|
6
14
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
# 产出的是**未求值**的节点:`#{}` 只保存表达式原文,公式只保存 TeX 原文。
|
|
16
|
+
# 块级结构(围栏、块级公式、透明 Ruby……)不属于本类。
|
|
17
|
+
#
|
|
18
|
+
# MDOM::InlineParser.parse("**粗** 与 *斜* 与 `code`")
|
|
19
|
+
class InlineParser
|
|
20
|
+
# 可以被 `\` 转义的字符(CommonMark 的 ASCII 标点,外加 CSM 用到的 `$`、`@`)。
|
|
21
|
+
ESCAPABLE = %w[` * _ { } [ ] ( ) # + - . ! " $ % & ' , / : ; < = > ? @ ^ | ~ \\].freeze
|
|
22
|
+
|
|
23
|
+
# 东亚(CJK)标点(CSM §1.1:`\n` 前是它们时不插入空格)。
|
|
24
|
+
CJK_PUNCTUATION = /
|
|
25
|
+
[\u3000-\u303F] # CJK 符号和标点(、。〈〉《》「」【】…)
|
|
26
|
+
|[\uFF01-\uFF0F] # 全角标点(! ~ /)
|
|
27
|
+
|[\uFF1A-\uFF20] # 全角标点(: ~ @)
|
|
28
|
+
|[\uFF3B-\uFF40] # 全角标点([ ~ `)
|
|
29
|
+
|[\uFF5B-\uFF65] # 全角标点({ ~ ・)
|
|
30
|
+
|[\u2013\u2014] # – —
|
|
31
|
+
|[\u2018-\u201D] # ‘ ’ “ ”
|
|
32
|
+
|[\u2026\u00B7\u30FB]
|
|
33
|
+
/x
|
|
14
34
|
|
|
15
|
-
|
|
35
|
+
MENTION_RE = /\A@([A-Za-z0-9_][\w-]*)(?::([A-Za-z0-9_][\w-]*))?/
|
|
36
|
+
AUTOLINK_RE = /\A(?:[A-Za-z][A-Za-z0-9+.-]*:[^\s<>]*|[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+)\z/
|
|
37
|
+
DESTINATION_RE = /\A(\S*?)(?:\s+"([^"]*)")?\z/
|
|
38
|
+
RATIO_RE = %r{\A(\S+?)/(\S+)\z}
|
|
39
|
+
|
|
40
|
+
# 行内代码最多 2 个反引号(CSM §1.5)。
|
|
41
|
+
MAX_CODE_TICKS = 2
|
|
42
|
+
|
|
43
|
+
# 链接 `[]` 内允许的单个样式标签(CSM §2.3):开标记、闭标记、节点类。
|
|
44
|
+
# 长标记在前——`**` 要先于 `*` 试。
|
|
45
|
+
STYLE_MARKERS = [
|
|
46
|
+
["**", "**", Node::Strong],
|
|
47
|
+
["~~", "~~", Node::Del],
|
|
48
|
+
["~+", "+~", Node::Ins],
|
|
49
|
+
["*", "*", Node::Emphasis]
|
|
50
|
+
].freeze
|
|
51
|
+
|
|
52
|
+
# 解析行内文本,返回行内节点数组。
|
|
53
|
+
#
|
|
54
|
+
# * +styles:+ 为 +false+ 时不再识别样式标签(`*` `**` `~~` `~+`)——样式不得
|
|
55
|
+
# 嵌套(CSM §1.7);
|
|
56
|
+
# * +literal:+ 为 +true+ 时只识别转义与 `#{}` 插值,其余一律按字面,用于
|
|
57
|
+
# 链接的 `[]` 内容(CSM §2.3)。
|
|
58
|
+
def self.parse(text, styles: true, literal: false)
|
|
59
|
+
new(styles: styles, literal: literal).parse(text)
|
|
60
|
+
end
|
|
16
61
|
|
|
17
|
-
def
|
|
18
|
-
|
|
62
|
+
def initialize(styles: true, literal: false)
|
|
63
|
+
@styles = styles
|
|
64
|
+
@literal = literal
|
|
19
65
|
end
|
|
20
66
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
67
|
+
def parse(text)
|
|
68
|
+
@text = text.to_s
|
|
69
|
+
@pos = 0
|
|
70
|
+
@nodes = []
|
|
71
|
+
buffer = +""
|
|
72
|
+
|
|
73
|
+
while @pos < @text.length
|
|
74
|
+
char = @text[@pos]
|
|
75
|
+
next if handle(char, buffer)
|
|
76
|
+
|
|
77
|
+
buffer << char
|
|
78
|
+
@pos += 1
|
|
26
79
|
end
|
|
27
80
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
81
|
+
flush(buffer)
|
|
82
|
+
@nodes
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# 按当前模式分派一个字符,返回是否已消费。
|
|
88
|
+
def handle(char, buffer)
|
|
89
|
+
return literal_char(char, buffer) if @literal
|
|
90
|
+
|
|
91
|
+
case char
|
|
92
|
+
when "\\" then escape(buffer)
|
|
93
|
+
when "`" then code_span(buffer)
|
|
94
|
+
when "*" then emphasis(buffer)
|
|
95
|
+
when "~" then tilde_mark(buffer)
|
|
96
|
+
when "[" then bracket(buffer)
|
|
97
|
+
when "!" then image(buffer)
|
|
98
|
+
when "<" then autolink(buffer)
|
|
99
|
+
when "$" then math(buffer)
|
|
100
|
+
when "#" then interpolation(buffer)
|
|
101
|
+
when "@" then mention(buffer)
|
|
102
|
+
when "\n" then line_break(buffer)
|
|
35
103
|
end
|
|
104
|
+
end
|
|
36
105
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
when "!" then read_link_or_image
|
|
44
|
-
when "[" then read_link_or_image
|
|
45
|
-
when "\n" then read_break
|
|
46
|
-
when "*" then read_emphasis
|
|
47
|
-
when "_" then read_emphasis
|
|
48
|
-
else read_plain_run
|
|
49
|
-
end
|
|
106
|
+
# 字面模式:只认转义、插值与换行折叠。
|
|
107
|
+
def literal_char(char, buffer)
|
|
108
|
+
case char
|
|
109
|
+
when "\\" then escape(buffer)
|
|
110
|
+
when "#" then interpolation(buffer)
|
|
111
|
+
when "\n" then line_break(buffer)
|
|
50
112
|
end
|
|
113
|
+
end
|
|
51
114
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
115
|
+
# 行内文本里的 `\`:行尾是硬换行,否则按 Markdown 转义语义(CSM §3.11)。
|
|
116
|
+
def escape(buffer)
|
|
117
|
+
following = @text[@pos + 1]
|
|
118
|
+
return false if following.nil?
|
|
119
|
+
|
|
120
|
+
if following == "\n"
|
|
121
|
+
@pos += 2
|
|
122
|
+
emit(buffer, Node::HardBreak.new)
|
|
123
|
+
return true
|
|
61
124
|
end
|
|
62
125
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
126
|
+
return false unless ESCAPABLE.include?(following)
|
|
127
|
+
|
|
128
|
+
buffer << following
|
|
129
|
+
@pos += 2
|
|
130
|
+
true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# 软换行折叠(CSM §1.1):前一个字符是 CJK 标点则直接相连,否则折叠为一个空格。
|
|
134
|
+
def line_break(buffer)
|
|
135
|
+
previous = @pos.zero? ? nil : @text[@pos - 1]
|
|
136
|
+
@pos += 1
|
|
137
|
+
buffer << " " unless cjk_punctuation?(previous)
|
|
138
|
+
true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# `` `代码` `` / ``` ``代码`` ```(CSM §1.5)。内容按字面处理。
|
|
142
|
+
def code_span(buffer)
|
|
143
|
+
ticks = run_length("`")
|
|
144
|
+
return false if ticks > MAX_CODE_TICKS
|
|
145
|
+
|
|
146
|
+
after = skip_code_span(@pos, ticks)
|
|
147
|
+
return false if after.nil?
|
|
148
|
+
|
|
149
|
+
code = @text[(@pos + ticks)...(after - ticks)]
|
|
150
|
+
@pos = after
|
|
151
|
+
emit(buffer, Node::CodeSpan.new(code))
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# `*斜体*` / `**加粗**`(CSM §1.7)。
|
|
156
|
+
#
|
|
157
|
+
# 三个条件同时满足才成立:定界符数量相同(`**` 只配 `**`)、开 `*` 的右边与
|
|
158
|
+
# 闭 `*` 的左边都是非空白、且强调不嵌套(内部不再识别 `*`)。任一不满足就把
|
|
159
|
+
# 整个定界符串按字面收下,不退化成更短的定界符。
|
|
160
|
+
def emphasis(buffer)
|
|
161
|
+
count = run_length("*")
|
|
162
|
+
return literal_emphasis(buffer, count) unless @styles && count <= 2
|
|
163
|
+
return literal_emphasis(buffer, count) if blank?(@text[@pos + count])
|
|
164
|
+
|
|
165
|
+
closing = find_emphasis_close(count)
|
|
166
|
+
return literal_emphasis(buffer, count) if closing.nil?
|
|
167
|
+
|
|
168
|
+
inner = @text[(@pos + count)...closing]
|
|
169
|
+
@pos = closing + count
|
|
170
|
+
klass = count == 2 ? Node::Strong : Node::Emphasis
|
|
171
|
+
emit(buffer, klass.new(children: InlineParser.parse(inner, styles: false)))
|
|
172
|
+
true
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def literal_emphasis(buffer, count)
|
|
176
|
+
buffer << @text[@pos, count]
|
|
177
|
+
@pos += count
|
|
178
|
+
true
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# `~~删除~~`(`<del>`)与 `~+增加+~`(`<ins>`)(CSM §1.8)。
|
|
182
|
+
def tilde_mark(buffer)
|
|
183
|
+
return false unless @styles
|
|
184
|
+
|
|
185
|
+
if @text[@pos, 2] == "~~"
|
|
186
|
+
closing = find_sequence("~~", @pos + 2)
|
|
187
|
+
return false if closing.nil?
|
|
188
|
+
|
|
189
|
+
inner = @text[(@pos + 2)...closing]
|
|
190
|
+
@pos = closing + 2
|
|
191
|
+
emit(buffer, Node::Del.new(children: InlineParser.parse(inner)))
|
|
192
|
+
true
|
|
193
|
+
elsif @text[@pos, 2] == "~+"
|
|
194
|
+
closing = find_sequence("+~", @pos + 2)
|
|
195
|
+
return false if closing.nil?
|
|
196
|
+
|
|
197
|
+
inner = @text[(@pos + 2)...closing]
|
|
198
|
+
@pos = closing + 2
|
|
199
|
+
emit(buffer, Node::Ins.new(children: InlineParser.parse(inner)))
|
|
200
|
+
true
|
|
201
|
+
else
|
|
202
|
+
false
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# `[` 家族:WikiLink、进度条、度量、链接/行内引用。
|
|
207
|
+
def bracket(buffer)
|
|
208
|
+
if @text[@pos, 2] == "[["
|
|
209
|
+
wiki_link(buffer)
|
|
210
|
+
elsif @text[@pos, 3] == "[->"
|
|
211
|
+
bar(buffer, Node::Progress)
|
|
212
|
+
elsif @text[@pos, 2] == "[="
|
|
213
|
+
bar(buffer, Node::Meter)
|
|
214
|
+
else
|
|
215
|
+
link(buffer)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def wiki_link(buffer)
|
|
220
|
+
closing = find_sequence("]]", @pos + 2)
|
|
221
|
+
return false if closing.nil?
|
|
222
|
+
|
|
223
|
+
inner = @text[(@pos + 2)...closing]
|
|
224
|
+
@pos = closing + 2
|
|
225
|
+
target, _, label = inner.partition("|")
|
|
226
|
+
label = nil if label.empty?
|
|
227
|
+
# label 的策略与一般链接的文本相同:最多一个紧邻的样式标签,其余按字面
|
|
228
|
+
# (CSM §2.1、§2.3)。
|
|
229
|
+
emit(buffer, Node::WikiLink.new(target, label: label, children: link_label_nodes(label || target)))
|
|
230
|
+
true
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# `[=m/n]` → `<meter>`,`[->m/n]` → `<progress>`(CSM §1.10)。
|
|
234
|
+
def bar(buffer, klass)
|
|
235
|
+
offset = klass == Node::Meter ? 2 : 3
|
|
236
|
+
closing = @text.index("]", @pos + offset)
|
|
237
|
+
return false if closing.nil?
|
|
238
|
+
|
|
239
|
+
ratio = @text[(@pos + offset)...closing]
|
|
240
|
+
match = RATIO_RE.match(ratio)
|
|
241
|
+
return false if match.nil?
|
|
242
|
+
|
|
243
|
+
@pos = closing + 1
|
|
244
|
+
emit(buffer, klass.new(match[1], match[2], children: literal_nodes(ratio)))
|
|
245
|
+
true
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# `[文本](uri "title")` → `<a>`;`[文本](@uri)` → `<q>`(CSM §1.9、§2.3)。
|
|
249
|
+
def link(buffer)
|
|
250
|
+
closing = matching_bracket(@pos)
|
|
251
|
+
return false if closing.nil?
|
|
252
|
+
return false unless @text[closing + 1] == "("
|
|
253
|
+
|
|
254
|
+
paren = matching_paren(closing + 1)
|
|
255
|
+
return false if paren.nil?
|
|
256
|
+
|
|
257
|
+
label = @text[(@pos + 1)...closing]
|
|
258
|
+
destination = @text[(closing + 2)...paren]
|
|
259
|
+
@pos = paren + 1
|
|
260
|
+
emit(buffer, build_link(destination, label))
|
|
261
|
+
true
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# `` → `<img>`(CSM §2.3)。
|
|
265
|
+
def image(buffer)
|
|
266
|
+
return false unless @text[@pos + 1] == "["
|
|
267
|
+
|
|
268
|
+
closing = matching_bracket(@pos + 1)
|
|
269
|
+
return false if closing.nil?
|
|
270
|
+
return false unless @text[closing + 1] == "("
|
|
271
|
+
|
|
272
|
+
paren = matching_paren(closing + 1)
|
|
273
|
+
return false if paren.nil?
|
|
274
|
+
|
|
275
|
+
alt = @text[(@pos + 2)...closing]
|
|
276
|
+
destination = @text[(closing + 2)...paren]
|
|
277
|
+
uri, title = split_destination(destination)
|
|
278
|
+
@pos = paren + 1
|
|
279
|
+
emit(buffer, Node::Image.new(uri, alt: alt, title: title, children: literal_nodes(alt)))
|
|
280
|
+
true
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# `<uri>` / `<mailto:a@b.c>` → `<a>`(CSM §2.3)。
|
|
284
|
+
def autolink(buffer)
|
|
285
|
+
closing = @text.index(">", @pos + 1)
|
|
286
|
+
return false if closing.nil?
|
|
287
|
+
|
|
288
|
+
inner = @text[(@pos + 1)...closing]
|
|
289
|
+
return false if inner.empty? || !AUTOLINK_RE.match?(inner)
|
|
290
|
+
|
|
291
|
+
@pos = closing + 1
|
|
292
|
+
emit(buffer, Node::Link.new(inner, children: literal_nodes(inner)))
|
|
293
|
+
true
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# `$\TeX$` → 行内 `<span class="math">`(CSM §1.11)。`$$` 属于块级公式,这里按字面处理。
|
|
297
|
+
def math(buffer)
|
|
298
|
+
return false if @text[@pos + 1] == "$"
|
|
299
|
+
|
|
300
|
+
closing = find_sequence("$", @pos + 1)
|
|
301
|
+
return false if closing.nil?
|
|
302
|
+
|
|
303
|
+
tex = @text[(@pos + 1)...closing]
|
|
304
|
+
return false if tex.empty?
|
|
305
|
+
|
|
306
|
+
@pos = closing + 1
|
|
307
|
+
emit(buffer, Node::Math.new(tex, children: literal_nodes(tex)))
|
|
308
|
+
true
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# `#{expr}` → 插值节点(CSM §3.13)。只保存表达式原文,不求值。
|
|
312
|
+
#
|
|
313
|
+
# 边界交给 Prism 判定:把从 `#{` 起的剩余文本包进一个双引号字符串字面量,
|
|
314
|
+
# Prism 给出的插值节点 location 就是 `#{}` 的范围。嵌套 `{}`、字符串与注释
|
|
315
|
+
# 里的 `}` 由 Ruby 自己的词法处理,不需要自研扫描器(CSM §3.12)。
|
|
316
|
+
def interpolation(buffer)
|
|
317
|
+
return false unless @text[@pos + 1] == "{"
|
|
318
|
+
|
|
319
|
+
closing = interpolation_end
|
|
320
|
+
return false if closing.nil?
|
|
321
|
+
|
|
322
|
+
expr = @text[(@pos + 2)...closing]
|
|
323
|
+
@pos = closing + 1
|
|
324
|
+
emit(buffer, Node::Interpolation.new(expr))
|
|
325
|
+
true
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# `@user` / `@project:x` / `@tag:kw`(CSM §2.2)。`:` 前是作用域,其后是名字。
|
|
329
|
+
def mention(buffer)
|
|
330
|
+
return false if @pos.positive? && @text[@pos - 1].match?(/\w/)
|
|
331
|
+
|
|
332
|
+
match = MENTION_RE.match(@text[@pos..])
|
|
333
|
+
return false if match.nil?
|
|
334
|
+
|
|
335
|
+
@pos += match[0].length
|
|
336
|
+
scope = match[1]
|
|
337
|
+
name = match[2]
|
|
338
|
+
emit(buffer, Node::Mention.new(name || scope, kind: name ? scope : nil))
|
|
339
|
+
true
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# 链接与行内引用的构造(CSM §1.9、§2.3)。
|
|
343
|
+
#
|
|
344
|
+
# `[]` 内容最多含**一个**样式标签,且必须紧邻方括号(`[*strong link*](url)`);
|
|
345
|
+
# 其余按字面(`#{}` 插值始终解析)。链接本身处在样式标签内部时,`[]` 内容
|
|
346
|
+
# 一律按字面——样式与链接不得互相嵌套。
|
|
347
|
+
def build_link(destination, label)
|
|
348
|
+
uri, title = split_destination(destination)
|
|
349
|
+
children = link_label_nodes(label)
|
|
350
|
+
|
|
351
|
+
if uri&.start_with?("@")
|
|
352
|
+
Node::InlineQuote.new(uri[1..], children: children)
|
|
353
|
+
else
|
|
354
|
+
Node::Link.new(uri, title: title, children: children)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def split_destination(destination)
|
|
359
|
+
match = DESTINATION_RE.match(destination.strip)
|
|
360
|
+
return [destination.strip, nil] if match.nil?
|
|
361
|
+
|
|
362
|
+
[match[1], match[2]]
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# `[]` 内容的节点:至多一个紧邻方括号的样式标签,其余按字面。
|
|
366
|
+
def link_label_nodes(label)
|
|
367
|
+
wrapped = @styles ? wrapping_style(label) : nil
|
|
368
|
+
return literal_nodes(label) if wrapped.nil?
|
|
369
|
+
|
|
370
|
+
klass, inner = wrapped
|
|
371
|
+
[klass.new(children: literal_nodes(inner))]
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# 整个 +label+ 恰好被一个样式标签包裹时返回 +[节点类, 内文]+,否则 +nil+。
|
|
375
|
+
def wrapping_style(label)
|
|
376
|
+
STYLE_MARKERS.each do |opener, closer, klass|
|
|
377
|
+
next if label.length <= opener.length + closer.length
|
|
378
|
+
next unless label.start_with?(opener) && label.end_with?(closer)
|
|
379
|
+
|
|
380
|
+
inner = label[opener.length, label.length - opener.length - closer.length]
|
|
381
|
+
next if blank?(inner[0]) || blank?(inner[-1])
|
|
382
|
+
# 内文不得再出现同族定界符(那会是第二个样式标签)
|
|
383
|
+
next if unescaped?(inner, opener[0])
|
|
384
|
+
|
|
385
|
+
return [klass, inner]
|
|
386
|
+
end
|
|
387
|
+
nil
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def literal_nodes(text)
|
|
391
|
+
InlineParser.parse(text, literal: true)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# +text+ 里是否存在未被 `\` 转义的 +char+。
|
|
395
|
+
def unescaped?(text, char)
|
|
396
|
+
cursor = 0
|
|
397
|
+
while cursor < text.length
|
|
398
|
+
if text[cursor] == "\\"
|
|
399
|
+
cursor += 2
|
|
400
|
+
elsif text[cursor] == char
|
|
401
|
+
return true
|
|
72
402
|
else
|
|
73
|
-
|
|
74
|
-
@pos = closing + count
|
|
75
|
-
Code.new(content)
|
|
403
|
+
cursor += 1
|
|
76
404
|
end
|
|
77
405
|
end
|
|
406
|
+
false
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def flush(buffer)
|
|
410
|
+
return if buffer.empty?
|
|
411
|
+
|
|
412
|
+
@nodes << Node::Text.new(buffer.dup)
|
|
413
|
+
buffer.clear
|
|
414
|
+
end
|
|
78
415
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
416
|
+
def emit(buffer, node)
|
|
417
|
+
flush(buffer)
|
|
418
|
+
@nodes << node
|
|
419
|
+
end
|
|
83
420
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
421
|
+
# 从 +@pos+ 起连续 +char+ 的个数。
|
|
422
|
+
def run_length(char)
|
|
423
|
+
run_length_at(@pos, char)
|
|
424
|
+
end
|
|
87
425
|
|
|
88
|
-
|
|
89
|
-
|
|
426
|
+
def run_length_at(pos, char)
|
|
427
|
+
cursor = pos
|
|
428
|
+
cursor += 1 while cursor < @text.length && @text[cursor] == char
|
|
429
|
+
cursor - pos
|
|
430
|
+
end
|
|
90
431
|
|
|
91
|
-
|
|
92
|
-
|
|
432
|
+
# 从 +pos+(一个反引号序列的起点)跳到配对的结束反引号之后;找不到返回 nil。
|
|
433
|
+
def skip_code_span(pos, ticks)
|
|
434
|
+
cursor = pos + ticks
|
|
435
|
+
while cursor < @text.length
|
|
436
|
+
if @text[cursor] == "`"
|
|
437
|
+
run = run_length_at(cursor, "`")
|
|
438
|
+
return cursor + run if run == ticks
|
|
93
439
|
|
|
94
|
-
|
|
95
|
-
@pos = dest_end + 1
|
|
96
|
-
if image
|
|
97
|
-
Image.new(dest, alt: label, title: title)
|
|
440
|
+
cursor += run
|
|
98
441
|
else
|
|
99
|
-
|
|
442
|
+
cursor += 1
|
|
100
443
|
end
|
|
101
444
|
end
|
|
445
|
+
nil
|
|
446
|
+
end
|
|
102
447
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
448
|
+
# 找可以充当闭定界符的 `*`:长度必须与 +count+ 相同,且左侧是非空白
|
|
449
|
+
# (CSM §1.7)。跳过转义、行内代码与链接——它们的 `*` 不算定界符。
|
|
450
|
+
def find_emphasis_close(count)
|
|
451
|
+
cursor = @pos + count
|
|
452
|
+
while cursor < @text.length
|
|
453
|
+
case @text[cursor]
|
|
454
|
+
when "\\" then cursor += 2
|
|
455
|
+
when "`" then cursor = skip_code_span(cursor, run_length_at(cursor, "`")) || @text.length
|
|
456
|
+
when "[", "!"
|
|
457
|
+
cursor = link_extent(cursor) || cursor + 1
|
|
458
|
+
when "*"
|
|
459
|
+
run = run_length_at(cursor, "*")
|
|
460
|
+
return cursor if run == count && !blank?(@text[cursor - 1])
|
|
461
|
+
|
|
462
|
+
cursor += run
|
|
463
|
+
else cursor += 1
|
|
111
464
|
end
|
|
112
|
-
[body, title]
|
|
113
465
|
end
|
|
466
|
+
nil
|
|
467
|
+
end
|
|
114
468
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
469
|
+
# 找下一个未被转义的 +sequence+。
|
|
470
|
+
def find_sequence(sequence, from)
|
|
471
|
+
cursor = from
|
|
472
|
+
while (found = @text.index(sequence, cursor))
|
|
473
|
+
return found if found.zero? || @text[found - 1] != "\\"
|
|
474
|
+
|
|
475
|
+
cursor = found + 1
|
|
122
476
|
end
|
|
477
|
+
nil
|
|
478
|
+
end
|
|
123
479
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
# A single marker must close on a lone delimiter, not part of "**".
|
|
141
|
-
closer = find_closer(idx, open_ch, 1, skip_double: true)
|
|
142
|
-
if closer
|
|
143
|
-
inner = @text[idx...closer]
|
|
144
|
-
@pos = closer + 1
|
|
145
|
-
Emphasis.new(children: InlineParser.parse(inner))
|
|
146
|
-
else
|
|
147
|
-
read_plain_run
|
|
148
|
-
end
|
|
480
|
+
# 从 `[` 的位置找配对的 `]`,跳过嵌套、转义与行内代码。
|
|
481
|
+
def matching_bracket(open)
|
|
482
|
+
depth = 0
|
|
483
|
+
cursor = open
|
|
484
|
+
while cursor < @text.length
|
|
485
|
+
case @text[cursor]
|
|
486
|
+
when "\\" then cursor += 2
|
|
487
|
+
when "`" then cursor = skip_code_span(cursor, run_length_at(cursor, "`")) || @text.length
|
|
488
|
+
when "[" then depth += 1
|
|
489
|
+
cursor += 1
|
|
490
|
+
when "]"
|
|
491
|
+
depth -= 1
|
|
492
|
+
return cursor if depth.zero?
|
|
493
|
+
|
|
494
|
+
cursor += 1
|
|
495
|
+
else cursor += 1
|
|
149
496
|
end
|
|
150
497
|
end
|
|
498
|
+
nil
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def matching_paren(open)
|
|
502
|
+
matching_pair(open, "(", ")")
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# 若 +pos+(`[` 或 `!`)处是一个链接/图片,返回它之后的下标;否则 +nil+。
|
|
506
|
+
def link_extent(pos)
|
|
507
|
+
open = @text[pos] == "!" ? pos + 1 : pos
|
|
508
|
+
return nil unless @text[open] == "["
|
|
509
|
+
|
|
510
|
+
closing = matching_bracket(open)
|
|
511
|
+
return nil if closing.nil? || @text[closing + 1] != "("
|
|
151
512
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
513
|
+
paren = matching_paren(closing + 1)
|
|
514
|
+
paren && paren + 1
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# 从 +open+(开括号的位置)找配对闭括号;跳过嵌套、反斜杠转义与引号内的括号。
|
|
518
|
+
def matching_pair(open, opener, closer)
|
|
519
|
+
depth = 0
|
|
520
|
+
quote = nil
|
|
521
|
+
cursor = open
|
|
522
|
+
while cursor < @text.length
|
|
523
|
+
char = @text[cursor]
|
|
524
|
+
if char == "\\"
|
|
525
|
+
cursor += 2
|
|
526
|
+
next
|
|
527
|
+
end
|
|
528
|
+
if quote
|
|
529
|
+
quote = nil if char == quote
|
|
530
|
+
cursor += 1
|
|
531
|
+
next
|
|
169
532
|
end
|
|
170
|
-
nil
|
|
171
|
-
end
|
|
172
533
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
534
|
+
case char
|
|
535
|
+
when "\"", "'" then quote = char
|
|
536
|
+
when opener then depth += 1
|
|
537
|
+
when closer
|
|
538
|
+
depth -= 1
|
|
539
|
+
return cursor if depth.zero?
|
|
177
540
|
end
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
541
|
+
cursor += 1
|
|
542
|
+
end
|
|
543
|
+
nil
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def cjk_punctuation?(char)
|
|
547
|
+
!char.nil? && CJK_PUNCTUATION.match?(char)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# 空格或换行(含制表符)。强调定界符的内侧不能是它们(CSM §1.7)。
|
|
551
|
+
def blank?(char)
|
|
552
|
+
char.nil? || char.match?(/\s/)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# 用 Prism 找出与当前位置的 `#{` 配对的 `}` 的下标;不是插值则返回 nil。
|
|
556
|
+
#
|
|
557
|
+
# 构造出的字面量里,插值之后若出现裸 `"`,整段就不再是合法 Ruby——Prism
|
|
558
|
+
# 会报错,但错误恢复仍给出插值节点,边界照常可读。代价是每个插值解析一次
|
|
559
|
+
# 剩余文本(各次解析互不影响)。
|
|
560
|
+
def interpolation_end
|
|
561
|
+
result = Prism.parse("\"#{@text[@pos..]}\"")
|
|
562
|
+
segment = first_embedded_statement(result.value)
|
|
563
|
+
return nil if segment.nil?
|
|
564
|
+
|
|
565
|
+
slice = segment.location.slice
|
|
566
|
+
# 未闭合的 `#{` 会一直吞到字符串结尾,此时的 slice 不以 `}` 收尾。
|
|
567
|
+
return nil unless slice.end_with?("}")
|
|
568
|
+
|
|
569
|
+
@pos + slice.length - 1
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# 先序遍历,取第一个字符串插值节点。
|
|
573
|
+
def first_embedded_statement(node)
|
|
574
|
+
return nil unless node
|
|
575
|
+
return node if node.is_a?(Prism::EmbeddedStatementsNode)
|
|
576
|
+
|
|
577
|
+
node.compact_child_nodes.each do |child|
|
|
578
|
+
found = first_embedded_statement(child)
|
|
579
|
+
return found if found
|
|
182
580
|
end
|
|
581
|
+
nil
|
|
183
582
|
end
|
|
184
583
|
end
|
|
185
584
|
end
|