mdom 0.2.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 +235 -0
- data/LICENSE.txt +21 -0
- data/README.md +323 -0
- data/USAGE.md +864 -0
- data/lib/mdom/block_detector.rb +433 -0
- data/lib/mdom/builder.rb +500 -161
- data/lib/mdom/error.rb +18 -0
- data/lib/mdom/hooks.rb +81 -0
- data/lib/mdom/html_serializer.rb +348 -0
- data/lib/mdom/inline_parser.rb +777 -145
- data/lib/mdom/markdown_serializer.rb +390 -0
- data/lib/mdom/node/inline_nodes.rb +239 -0
- data/lib/mdom/node/nodes.rb +336 -0
- data/lib/mdom/node.rb +685 -199
- data/lib/mdom/parser.rb +284 -234
- data/lib/mdom/tokenizer.rb +458 -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
data/lib/mdom/builder.rb
CHANGED
|
@@ -1,271 +1,610 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "error"
|
|
4
|
+
require_relative "markdown_serializer"
|
|
5
|
+
require_relative "node/inline_nodes"
|
|
6
|
+
require_relative "node/nodes"
|
|
7
|
+
|
|
3
8
|
module MDOM
|
|
4
|
-
#
|
|
5
|
-
#
|
|
9
|
+
# 文档构造 DSL(docs/IMPLEMENTATION.md §4.3、§4.4)。
|
|
10
|
+
#
|
|
11
|
+
# **排版**:节点挂到「当前容器」,也就是*游标栈*的栈顶。块方法进入一个新容器、
|
|
12
|
+
# 退出时恢复,嵌套调用因此自然形成层级。
|
|
13
|
+
#
|
|
14
|
+
# **嵌入**:+render+ 是嵌入代码进入文档树的唯一通道(CSM §3.3)。它的插入位置
|
|
15
|
+
# 就是调用位置——不需要槽位 id、回填,也不需要「0 个消失 / n 个展开」的规则。
|
|
6
16
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
17
|
+
# **当前文档**:`值.render` 与求值作用域里的裸 `render` 都要知道「往哪篇文档挂」。
|
|
18
|
+
# 载体是类级的 +Builder.current+(一个文档栈):+Builder.new+ 入栈、+to_document+
|
|
19
|
+
# 出栈,+Builder.with+ 提供显式作用域。类实例变量在 **Ractor** 之间天然隔离,所以
|
|
20
|
+
# CRuby 上的并行用 Ractor;`Thread` 并行会串上下文,不支持(docs/IMPLEMENTATION.md
|
|
21
|
+
# §5.1、§8.1 #5)。
|
|
11
22
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
23
|
+
# **并发**:+render+ 内部持一把互斥锁(CSM §3.3),只保证结构不被撕裂、不保证挂载
|
|
24
|
+
# 位置;mruby 的默认构建没有 `Mutex`,此时退化为无锁。
|
|
25
|
+
#
|
|
26
|
+
# doc = MDOM::Builder.build do |b|
|
|
27
|
+
# b.heading(1, "标题")
|
|
28
|
+
# b.paragraph("正文 *强调*。")
|
|
29
|
+
# b.list do |l|
|
|
30
|
+
# l.item "一"
|
|
31
|
+
# l.item "二"
|
|
18
32
|
# end
|
|
19
33
|
# end
|
|
20
|
-
# doc.to_markdown
|
|
21
34
|
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
35
|
+
# 块按**显式传参**取 builder(CSM §5.2.1):块参数就是 builder 自身,不用
|
|
36
|
+
# +instance_eval+,因此不依赖 metaprog。
|
|
37
|
+
#
|
|
38
|
+
# 依赖边界(docs/IMPLEMENTATION.md §1.1):本文件属 **builder 侧**——只用 mruby
|
|
39
|
+
# 子集、不 +require+ 任何**外部 gem**。行内内容的**结构化构造**(节点参数 / 块)
|
|
40
|
+
# 完全不需要解析器,因此既不需要 prism 也不依赖宿主的配置;只有字符串参数这一
|
|
41
|
+
# 便利形式需要行内解析器,且它是**注入**的:默认在首次需要时读取
|
|
42
|
+
# +MDOM::InlineParser+ / +MDOM::Parser+ 常量,宿主可以替换成别的实现。
|
|
43
|
+
#
|
|
44
|
+
# 块形式的行内内容会用 +MDOM::MarkdownSerializer+ 重建 +attributes[:text]+ 原文
|
|
45
|
+
# 缓存(`text` / `cite` 这类读取靠它)。它与本文件同属 builder 侧、零 gem 依赖,
|
|
46
|
+
# 所以是本文件唯一的 +require+。
|
|
28
47
|
class Builder
|
|
29
|
-
#
|
|
30
|
-
#
|
|
48
|
+
# 能直接收下行内节点的容器:内容策略为 **PHRASING** 的块
|
|
49
|
+
# (`docs/CSM.md` 元素表,与 Parser 的边界一致)。
|
|
50
|
+
PHRASING_CONTAINERS = %i[paragraph heading term cite table_header_cell table_cell].freeze
|
|
51
|
+
|
|
52
|
+
# +render+ 的互斥锁类(CSM §3.3)。mruby 的默认构建既没有 `Thread` 也没有
|
|
53
|
+
# `Mutex`(属外部 mrbgem),此时为 +nil+ ⇒ 退化为无锁。
|
|
54
|
+
LOCK_CLASS = defined?(Mutex) ? Mutex : nil
|
|
55
|
+
|
|
56
|
+
# 构造一个文档并返回它。块参数是 builder,见类说明。
|
|
31
57
|
def self.build(builder = new, &block)
|
|
32
58
|
builder.document(&block)
|
|
33
59
|
builder.to_document
|
|
34
60
|
end
|
|
35
61
|
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
# -- 当前文档 -------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
# 「当前文档」的 builder;没有正在渲染的文档时为 +nil+(CSM §3.3)。
|
|
65
|
+
def self.current
|
|
66
|
+
(@current ||= []).last
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 把 +builder+ 设为当前文档。+new+ 会自动入栈,一般不必直接调用。
|
|
70
|
+
def self.enter(builder)
|
|
71
|
+
(@current ||= []).push(builder)
|
|
72
|
+
builder
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 退出当前文档。只在栈顶是 +builder+ 时出栈——因此可以反复调用(幂等),
|
|
76
|
+
# +to_document+ 被读多次也不会把栈弄乱。
|
|
77
|
+
def self.leave(builder)
|
|
78
|
+
stack = @current
|
|
79
|
+
stack.pop if stack && stack.last.equal?(builder)
|
|
80
|
+
builder
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# 显式作用域:块内 +current+ 指向 +builder+,退出时恢复。
|
|
84
|
+
def self.with(builder)
|
|
85
|
+
enter(builder)
|
|
86
|
+
yield
|
|
87
|
+
ensure
|
|
88
|
+
leave(builder)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# +document+ 省略时新建一个空文档。
|
|
92
|
+
#
|
|
93
|
+
# +inline_parser+ / +parser+ / +text_serializer+ 省略时,在首次需要时读取
|
|
94
|
+
# +MDOM::InlineParser+ / +MDOM::Parser+ / +MDOM::MarkdownSerializer+ 常量。
|
|
95
|
+
def initialize(document: nil, inline_parser: nil, parser: nil, text_serializer: nil)
|
|
96
|
+
@document = document || Node::Document.new
|
|
38
97
|
@stack = [@document]
|
|
98
|
+
@inline_parser = inline_parser
|
|
99
|
+
@parser = parser
|
|
100
|
+
@text_serializer = text_serializer
|
|
101
|
+
@lock = LOCK_CLASS&.new
|
|
102
|
+
Builder.enter(self)
|
|
39
103
|
end
|
|
40
104
|
|
|
41
|
-
# --
|
|
105
|
+
# -- 结果 -----------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
# 正在构造的文档(根节点)。同时把它从「当前文档」栈上退出。
|
|
108
|
+
def to_document
|
|
109
|
+
Builder.leave(self)
|
|
110
|
+
@document
|
|
111
|
+
end
|
|
112
|
+
alias finish to_document
|
|
42
113
|
|
|
43
|
-
#
|
|
44
|
-
# against this builder and its content is added to the root document.
|
|
45
|
-
# Returns the builder; use +to_document+ / +finish+ for the tree.
|
|
114
|
+
# 把块的内容挂到文档根下。
|
|
46
115
|
def document(&block)
|
|
47
|
-
|
|
116
|
+
within(@document, &block) if block
|
|
48
117
|
self
|
|
49
118
|
end
|
|
50
119
|
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
120
|
+
# -- 块级 -----------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
# ATX 标题。+level+ 为 1–6,其余参数是行内内容。+closed+ 表示以等长 `#` 收尾
|
|
123
|
+
# (CSM §1.2 的 `<hgroup>` 用法)。
|
|
124
|
+
def heading(level = 1, *parts, closed: false, &block)
|
|
125
|
+
node = Node::Heading.new(level, closed: closed)
|
|
55
126
|
append(node)
|
|
127
|
+
fill_inline(node, parts, &block)
|
|
128
|
+
self
|
|
56
129
|
end
|
|
57
130
|
|
|
58
|
-
def h1(*parts,
|
|
59
|
-
def h2(*parts,
|
|
60
|
-
def h3(*parts,
|
|
61
|
-
def h4(*parts,
|
|
62
|
-
def h5(*parts,
|
|
63
|
-
def h6(*parts,
|
|
131
|
+
def h1(*parts, closed: false, &block) = heading(1, *parts, closed: closed, &block)
|
|
132
|
+
def h2(*parts, closed: false, &block) = heading(2, *parts, closed: closed, &block)
|
|
133
|
+
def h3(*parts, closed: false, &block) = heading(3, *parts, closed: closed, &block)
|
|
134
|
+
def h4(*parts, closed: false, &block) = heading(4, *parts, closed: closed, &block)
|
|
135
|
+
def h5(*parts, closed: false, &block) = heading(5, *parts, closed: closed, &block)
|
|
136
|
+
def h6(*parts, closed: false, &block) = heading(6, *parts, closed: closed, &block)
|
|
64
137
|
|
|
65
|
-
#
|
|
138
|
+
# 段落。字符串参数按 CSM 行内语法解析。
|
|
66
139
|
def paragraph(*parts, &block)
|
|
67
|
-
node = Paragraph.new
|
|
68
|
-
build_inline(node, parts, &block)
|
|
140
|
+
node = Node::Paragraph.new
|
|
69
141
|
append(node)
|
|
142
|
+
fill_inline(node, parts, &block)
|
|
143
|
+
self
|
|
70
144
|
end
|
|
71
145
|
alias p paragraph
|
|
72
146
|
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
node = List.new(ordered: ordered, tight: tight, start: start)
|
|
147
|
+
# 列表。+ordered+: 有序列表;+task+: 任务列表。无块时 +items+ 逐个成为列表项。
|
|
148
|
+
def list(*items, ordered: false, task: false, marker: nil, &block)
|
|
149
|
+
node = Node::List.new(ordered: ordered, task: task, marker: marker || (ordered ? "1." : "-"))
|
|
150
|
+
append(node)
|
|
78
151
|
if block
|
|
79
|
-
|
|
152
|
+
within(node, &block)
|
|
80
153
|
else
|
|
81
|
-
|
|
82
|
-
|
|
154
|
+
item_marker = node.attributes[:marker]
|
|
155
|
+
within(node) do
|
|
156
|
+
items.each { |text| item(text, marker: item_marker, task: task, checked: task ? false : nil) }
|
|
157
|
+
end
|
|
83
158
|
end
|
|
84
|
-
|
|
159
|
+
self
|
|
85
160
|
end
|
|
86
161
|
|
|
87
|
-
# An unordered list, as a shorthand for +list(ordered: false)+.
|
|
88
162
|
def ul(*items, &block) = list(*items, ordered: false, &block)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
node.append(Paragraph.new(children: inline_nodes(parts)))
|
|
103
|
-
end
|
|
163
|
+
def ol(*items, &block) = list(*items, ordered: true, &block)
|
|
164
|
+
|
|
165
|
+
# 列表项。+parts+ 各自成为一个段落(`li` 的内容策略是 FLOW,见 CSM 元素表);
|
|
166
|
+
# 有块时在段落之后继续嵌套。省略 +marker+ 时沿用最近一层列表的标记形式;
|
|
167
|
+
# 省略 +task+ 时沿用最近一层列表的任务状态。
|
|
168
|
+
def item(*parts, marker: nil, task: nil, checked: nil, &block)
|
|
169
|
+
list = current_list
|
|
170
|
+
task = list.task? if task.nil? && list
|
|
171
|
+
task = false if task.nil?
|
|
172
|
+
list.attributes[:task] = true if task && list
|
|
173
|
+
checked = false if task && checked.nil?
|
|
174
|
+
|
|
175
|
+
node = Node::ListItem.new(marker: marker || current_list_marker, task: task, checked: checked)
|
|
104
176
|
append(node)
|
|
177
|
+
fill_flow(node, parts, &block)
|
|
178
|
+
self
|
|
105
179
|
end
|
|
106
180
|
alias li item
|
|
107
181
|
|
|
108
|
-
#
|
|
182
|
+
# 任务列表项。
|
|
109
183
|
def task(checked, *parts, &block)
|
|
110
184
|
item(*parts, task: true, checked: checked, &block)
|
|
111
185
|
end
|
|
112
186
|
|
|
113
|
-
#
|
|
114
|
-
def blockquote(*parts, &block)
|
|
115
|
-
node = Blockquote.new
|
|
116
|
-
build_blocks(node, parts, &block)
|
|
187
|
+
# 块引用。+parts+ 各自成为一个段落(FLOW 内容);+cite+ 对应 `>-` 收尾。
|
|
188
|
+
def blockquote(*parts, cite: nil, &block)
|
|
189
|
+
node = Node::Blockquote.new
|
|
117
190
|
append(node)
|
|
191
|
+
fill_flow(node, parts, &block)
|
|
192
|
+
append_cite(node, cite)
|
|
193
|
+
self
|
|
118
194
|
end
|
|
119
195
|
alias quote blockquote
|
|
120
196
|
|
|
121
|
-
#
|
|
122
|
-
def
|
|
123
|
-
|
|
197
|
+
# 引用来源(`<cite>`)。字符串参数按 CSM 行内语法解析。
|
|
198
|
+
def cite(*parts, &block)
|
|
199
|
+
node = Node::Cite.new(parts.grep(String).join)
|
|
200
|
+
append(node)
|
|
201
|
+
fill_inline(node, parts, &block)
|
|
202
|
+
self
|
|
124
203
|
end
|
|
125
204
|
|
|
126
|
-
#
|
|
127
|
-
def
|
|
128
|
-
append(
|
|
205
|
+
# 围栏代码块。+code+ 按字面收下。
|
|
206
|
+
def code_block(code, lang: nil, params: nil, fence: 3)
|
|
207
|
+
append(Node::CodeBlock.new(code.to_s, lang: lang, params: params, fence: fence))
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# 块级公式(`$$…$$`)。+tex+ 按字面收下(CSM §1.11)。
|
|
212
|
+
def math_block(tex)
|
|
213
|
+
append(Node::MathBlock.new(tex.to_s))
|
|
214
|
+
self
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# 表格(纯结构,CSM §1.12):块内给出若干 #row。
|
|
218
|
+
def table(&block)
|
|
219
|
+
node = Node::Table.new
|
|
220
|
+
append(node)
|
|
221
|
+
within(node, &block) if block
|
|
222
|
+
self
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# 表格行:块内给出若干单元格。
|
|
226
|
+
def row(&block)
|
|
227
|
+
node = Node::TableRow.new
|
|
228
|
+
append(node)
|
|
229
|
+
within(node, &block) if block
|
|
230
|
+
self
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# 表头单元格(`th`)。+align+ 为 +:left+ / +:center+ / +:right+,省略为默认对齐。
|
|
234
|
+
# 内容策略是 PHRASING(CSM 元素表),因此正文与 #paragraph 一样可以给节点
|
|
235
|
+
# 参数或块——`cell` 从来不只是「一个字符串」。
|
|
236
|
+
def header_cell(*parts, align: nil, &block)
|
|
237
|
+
append_cell(Node::TableHeaderCell, parts, align, &block)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# 数据单元格(`td`)。+align+ 同上。
|
|
241
|
+
def cell(*parts, align: nil, &block)
|
|
242
|
+
append_cell(Node::TableCell, parts, align, &block)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# 分隔线。
|
|
246
|
+
def hrule
|
|
247
|
+
append(Node::Hrule.new)
|
|
248
|
+
self
|
|
129
249
|
end
|
|
130
250
|
alias hr hrule
|
|
131
251
|
|
|
132
|
-
#
|
|
252
|
+
# 节块(`<div>`)。
|
|
253
|
+
def division(&block)
|
|
254
|
+
node = Node::Div.new
|
|
255
|
+
append(node)
|
|
256
|
+
within(node, &block) if block
|
|
257
|
+
self
|
|
258
|
+
end
|
|
259
|
+
alias div division
|
|
133
260
|
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def em(*parts, &block)
|
|
138
|
-
node = Emphasis.new
|
|
139
|
-
build_inline(node, parts, &block)
|
|
261
|
+
# 定义列表:块内依次给出 #term 与 #definition。
|
|
262
|
+
def definition_list(&block)
|
|
263
|
+
node = Node::DefinitionList.new
|
|
140
264
|
append(node)
|
|
141
|
-
node
|
|
265
|
+
within(node, &block) if block
|
|
266
|
+
self
|
|
267
|
+
end
|
|
268
|
+
alias dl definition_list
|
|
269
|
+
|
|
270
|
+
# 定义术语(`<dt>`)。字符串参数按 CSM 行内语法解析。
|
|
271
|
+
def term(*parts, &block)
|
|
272
|
+
node = Node::Term.new(parts.grep(String).join)
|
|
273
|
+
append(node)
|
|
274
|
+
fill_inline(node, parts, &block)
|
|
275
|
+
self
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# 定义内容(`<dd>`)。+parts+ 各自成为一个段落(FLOW 内容)。
|
|
279
|
+
def definition(*parts, &block)
|
|
280
|
+
node = Node::Definition.new
|
|
281
|
+
append(node)
|
|
282
|
+
fill_flow(node, parts, &block)
|
|
283
|
+
self
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# -- 行内 -----------------------------------------------------------------
|
|
287
|
+
#
|
|
288
|
+
# 每个 CSM 行内构造都有对应的构造方法。**结构化构造**是首选形式:内容给节点
|
|
289
|
+
# 参数或块,产物因此不依赖任何解析器;字符串参数是给手写用的便利形式,它需要
|
|
290
|
+
# 一个行内解析器(注入,或 `require "mdom/inline_parser"`)。
|
|
291
|
+
|
|
292
|
+
# `*斜体*` → `<em>`。
|
|
293
|
+
def em(*parts, &block)
|
|
294
|
+
inline_node(Node::Emphasis.new, parts, &block)
|
|
142
295
|
end
|
|
143
296
|
alias emphasis em
|
|
144
297
|
|
|
145
|
-
#
|
|
298
|
+
# `**加粗**` → `<strong>`。
|
|
146
299
|
def strong(*parts, &block)
|
|
147
|
-
|
|
148
|
-
build_inline(node, parts, &block)
|
|
149
|
-
append(node)
|
|
150
|
-
node
|
|
300
|
+
inline_node(Node::Strong.new, parts, &block)
|
|
151
301
|
end
|
|
152
302
|
|
|
153
|
-
#
|
|
303
|
+
# `` `代码` `` → `<code>`。参数按字面。
|
|
154
304
|
def code_span(value)
|
|
155
|
-
|
|
305
|
+
inline_node(Node::CodeSpan.new(value.to_s))
|
|
156
306
|
end
|
|
157
307
|
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
node = Link.new(destination, title: title)
|
|
162
|
-
build_inline(node, label, &block)
|
|
163
|
-
append(node)
|
|
164
|
-
node
|
|
308
|
+
# `~~文本~~` → `<del>`。
|
|
309
|
+
def del(*parts, &block)
|
|
310
|
+
inline_node(Node::Del.new, parts, &block)
|
|
165
311
|
end
|
|
166
312
|
|
|
167
|
-
#
|
|
168
|
-
def
|
|
169
|
-
|
|
313
|
+
# `~+文本+~` → `<ins>`。
|
|
314
|
+
def ins(*parts, &block)
|
|
315
|
+
inline_node(Node::Ins.new, parts, &block)
|
|
170
316
|
end
|
|
171
317
|
|
|
172
|
-
#
|
|
173
|
-
def
|
|
174
|
-
|
|
318
|
+
# 行内纯文本。参数按字面。
|
|
319
|
+
def text(value = "")
|
|
320
|
+
inline_node(Node::Text.new(value.to_s))
|
|
175
321
|
end
|
|
176
322
|
|
|
177
|
-
#
|
|
178
|
-
def
|
|
179
|
-
|
|
323
|
+
# `[标签](uri "title")` → `<a>`。
|
|
324
|
+
def link(uri, *label, title: nil, &block)
|
|
325
|
+
inline_node(Node::Link.new(uri, title: title), label, &block)
|
|
180
326
|
end
|
|
181
327
|
|
|
182
|
-
#
|
|
328
|
+
# `[文本](@uri)` → `<q>`。
|
|
329
|
+
def inline_quote(uri, *parts, &block)
|
|
330
|
+
inline_node(Node::InlineQuote.new(uri), parts, &block)
|
|
331
|
+
end
|
|
183
332
|
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
|
|
333
|
+
# `[[target|label]]` → `<a>`。+label+ 省略时显示 +target+,行内内容也因此取
|
|
334
|
+
# +target+(与解析侧一致)。
|
|
335
|
+
def wiki_link(target, label = nil, *parts, &block)
|
|
336
|
+
literal_inline(Node::WikiLink.new(target, label: label), parts, label || target, &block)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# `@user` / `@project:x` → `<a>`。+kind+ 是 `:` 前的作用域。
|
|
340
|
+
def mention(name, kind: nil)
|
|
341
|
+
inline_node(Node::Mention.new(name.to_s, kind: kind))
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# `` → `<img>`(空元素)。+alt+ 是替代文本原文,行内内容
|
|
345
|
+
# (含 `#{}`)由 +parts+ / 块给出;省略时按**字面**解析 +alt+(CSM §2.3)。
|
|
346
|
+
def image(uri, *parts, alt: nil, title: nil, &block)
|
|
347
|
+
alt = alt&.to_s
|
|
348
|
+
literal_inline(Node::Image.new(uri, alt: alt, title: title), parts, alt, &block)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# `[=m/n]` → `<meter>`(CSM §1.10)。+value+ / +max+ 保持原文,行内内容省略时
|
|
352
|
+
# 按**字面**解析 `m/n`。
|
|
353
|
+
def meter(value, max, *parts, &block)
|
|
354
|
+
literal_inline(Node::Meter.new(value.to_s, max.to_s), parts, "#{value}/#{max}", &block)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# `[->m/n]` → `<progress>`。语义同 #meter。
|
|
358
|
+
def progress(value, max, *parts, &block)
|
|
359
|
+
literal_inline(Node::Progress.new(value.to_s, max.to_s), parts, "#{value}/#{max}", &block)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# `$\TeX$` → 行内 `<span class="math">`(CSM §1.11)。+tex+ 保持原文,行内内容
|
|
363
|
+
# 省略时按**字面**解析 +tex+。
|
|
364
|
+
def math(tex = "", *parts, &block)
|
|
365
|
+
literal_inline(Node::Math.new(tex.to_s), parts, tex.to_s, &block)
|
|
187
366
|
end
|
|
188
|
-
alias finish to_document
|
|
189
367
|
|
|
190
|
-
#
|
|
191
|
-
def
|
|
192
|
-
|
|
368
|
+
# 行尾 `\` → `<br>`。
|
|
369
|
+
def hardbreak
|
|
370
|
+
inline_node(Node::HardBreak.new)
|
|
193
371
|
end
|
|
194
372
|
|
|
195
|
-
#
|
|
196
|
-
|
|
197
|
-
|
|
373
|
+
# `#{expr}`(CSM §3.13)。无块时构造**未求值**的插值节点,保存表达式原文
|
|
374
|
+
# (安全渲染用);有块时块的值按 #render 的分派表插入当前位置(不安全渲染用,
|
|
375
|
+
# 此时表达式由 Ruby 求值)。
|
|
376
|
+
def interpolation(expr = nil, &block)
|
|
377
|
+
return render(block.call) if block
|
|
378
|
+
|
|
379
|
+
inline_node(Node::Interpolation.new(expr.to_s))
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# -- 注入通道 -------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
# 嵌入代码进入文档树的唯一通道(CSM §3.3):值挂在**当前容器**,插入位置就是
|
|
385
|
+
# 调用位置。`render` 是排版指令的一种,与 `heading` / `paragraph` / `list` 同级。
|
|
386
|
+
#
|
|
387
|
+
# 值按类型分派,**包装方式取决于当前容器**(行内容器收行内节点,块级容器收块):
|
|
388
|
+
#
|
|
389
|
+
# | 值 | 行内容器中 | 块级容器中 |
|
|
390
|
+
# |----|-----------|-----------|
|
|
391
|
+
# | `nil` | 忽略 | 忽略 |
|
|
392
|
+
# | 节点 | 直接插入(自动从原父脱离) | 同左 |
|
|
393
|
+
# | 字符串 | 文本节点 | 段落 |
|
|
394
|
+
# | `Array` / `Enumerable` | 逐项递归 | 逐项递归 |
|
|
395
|
+
# | 其它 | `to_s` 后同上 | `to_s` 后同上 |
|
|
396
|
+
#
|
|
397
|
+
# +parse: true+ 时字符串按 CSM 递归解析,解析出的**子节点**逐个插入——不再按
|
|
398
|
+
# 容器包装,因为解析结果自带块级结构。
|
|
399
|
+
def render(value, parse: false)
|
|
400
|
+
if @lock
|
|
401
|
+
@lock.synchronize { emit(value, parse) }
|
|
402
|
+
else
|
|
403
|
+
emit(value, parse)
|
|
404
|
+
end
|
|
405
|
+
self
|
|
198
406
|
end
|
|
199
407
|
|
|
200
408
|
private
|
|
201
409
|
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
410
|
+
# 把 +>-+ 的出处作为 +Node::Cite+ 挂到引用末尾;正文按行内语法解析。
|
|
411
|
+
def append_cite(node, cite)
|
|
412
|
+
return if cite.nil?
|
|
413
|
+
|
|
414
|
+
source = Node::Cite.new
|
|
415
|
+
node << source
|
|
416
|
+
fill_inline(source, [cite.to_s])
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# 单元格:正文是行内内容(PHRASING),填充方式与段落一致。
|
|
420
|
+
def append_cell(klass, parts, align, &block)
|
|
421
|
+
node = klass.new("", align: align)
|
|
422
|
+
append(node)
|
|
423
|
+
fill_inline(node, parts, &block)
|
|
206
424
|
self
|
|
207
425
|
end
|
|
208
426
|
|
|
209
|
-
#
|
|
210
|
-
def
|
|
427
|
+
# 分派一个值。见 #render 的说明表。
|
|
428
|
+
def emit(value, parse)
|
|
429
|
+
case value
|
|
430
|
+
when nil then nil
|
|
431
|
+
when Node::BaseNode then @stack.last << value
|
|
432
|
+
when String then emit_string(value, parse)
|
|
433
|
+
when Enumerable then value.each { |item| emit(item, parse) }
|
|
434
|
+
else emit_string(value.to_s, parse)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def emit_string(text, parse)
|
|
439
|
+
return attach_tree(text) if parse
|
|
440
|
+
return @stack.last << Node::Text.new(text) if inline_container?
|
|
441
|
+
|
|
442
|
+
# 块级容器收不下裸文本,包成段落。
|
|
443
|
+
paragraph = Node::Paragraph.new(text)
|
|
444
|
+
paragraph << Node::Text.new(text)
|
|
445
|
+
@stack.last << paragraph
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# 当前容器能否直接收下行内节点。
|
|
449
|
+
def inline_container?
|
|
450
|
+
PHRASING_CONTAINERS.include?(@stack.last.type)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# 进入 +node+ 作为当前容器,块结束后恢复。
|
|
454
|
+
def within(node)
|
|
211
455
|
@stack.push(node)
|
|
212
|
-
yield
|
|
456
|
+
yield self
|
|
213
457
|
ensure
|
|
214
458
|
@stack.pop
|
|
215
459
|
end
|
|
216
460
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
461
|
+
def append(node)
|
|
462
|
+
@stack.last << node
|
|
463
|
+
node
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# 最近一层列表,没有则 +nil+。
|
|
467
|
+
def current_list
|
|
468
|
+
@stack.reverse.find { |node| node.is_a?(Node::List) }
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# 最近一层列表的标记形式(有序列表得到 `N.`),没有则 `-`。
|
|
472
|
+
def current_list_marker
|
|
473
|
+
list = current_list
|
|
474
|
+
list ? list.attributes[:marker] : "-"
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# 解析 +source+,把结果的**子节点**逐个挂到当前容器(不是挂整个 Document)。
|
|
478
|
+
def attach_tree(source)
|
|
479
|
+
parser.parse(source).children.dup.each { |child| @stack.last << child }
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# 行内节点方法:挂载后返回节点本身,便于 `b.p { b.em "x" }` 这样的显式构造。
|
|
483
|
+
def inline_node(node, parts = [], &block)
|
|
484
|
+
append(node)
|
|
485
|
+
fill_inline(node, parts, &block)
|
|
486
|
+
node
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# 填充行内内容:给块就用块(进入节点作为容器),否则填充 +parts+(节点原样
|
|
490
|
+
# 收下,字符串交给行内解析器)。
|
|
491
|
+
def fill_inline(node, parts, &block)
|
|
220
492
|
if block
|
|
221
493
|
raise ArgumentError, "cannot combine inline arguments with a block" unless parts.empty?
|
|
222
494
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
node.append_parts(inline_nodes(parts))
|
|
495
|
+
within(node, &block)
|
|
496
|
+
return cache_text(node)
|
|
226
497
|
end
|
|
498
|
+
|
|
499
|
+
fill_inline_parts(node, parts)
|
|
227
500
|
end
|
|
228
501
|
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
502
|
+
# 行内子节点挂好后,用(可注入的)序列化器重建 +attributes[:text]+ 原文缓存。
|
|
503
|
+
#
|
|
504
|
+
# 块形式的内容没有原文可存(内容本身就是节点),所以从子节点重建——`text` /
|
|
505
|
+
# `cite` 这类读取因此对 builder 构造的节点同样成立。只对 PHRASING 容器做:
|
|
506
|
+
# 行内格式节点(`em` / `strong` …)不需要原文缓存。
|
|
507
|
+
def cache_text(node)
|
|
508
|
+
return node unless PHRASING_CONTAINERS.include?(node.type)
|
|
509
|
+
return node unless node.attributes[:text].to_s.empty? # 解析期原文优先
|
|
510
|
+
|
|
511
|
+
serializer = text_serializer.new
|
|
512
|
+
node.attributes[:text] = node.children.map { |child| serializer.serialize(child) }.join
|
|
513
|
+
node
|
|
234
514
|
end
|
|
235
515
|
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
516
|
+
# 「字段 + 行内内容」型行内节点:内容给 +parts+ / 块,否则把 +literal+ 按字面
|
|
517
|
+
# 解析成子节点(与解析侧的策略一致)。
|
|
518
|
+
def literal_inline(node, parts, literal, &block)
|
|
519
|
+
append(node)
|
|
520
|
+
return fill_inline(node, parts, &block) if block || !parts.empty?
|
|
521
|
+
|
|
522
|
+
fill_inline(node, literal_nodes(literal)) if literal
|
|
523
|
+
node
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# 一段原文的**字面**行内节点(只认 `#{}`,不认强调等;CSM §2.3 的字面策略)。
|
|
527
|
+
def literal_nodes(text)
|
|
528
|
+
inline_parser.parse(text.to_s, literal: true)
|
|
241
529
|
end
|
|
242
530
|
|
|
243
|
-
#
|
|
244
|
-
#
|
|
245
|
-
def
|
|
531
|
+
# FLOW 容器的填充(CSM 元素表:`blockquote` / `li` / `dd` 的内容策略是 FLOW):
|
|
532
|
+
# +parts+ 各自成为一个段落,随后进入块(嵌套的块级子节点)。
|
|
533
|
+
def fill_flow(node, parts, &block)
|
|
534
|
+
parts.each { |part| append_paragraph(node, part) }
|
|
535
|
+
within(node, &block) if block
|
|
536
|
+
node
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# 把一个 part 作为段落挂到 +node+ 下:字符串按 CSM 行内语法解析,节点原样收下。
|
|
540
|
+
def append_paragraph(node, part)
|
|
541
|
+
paragraph = Node::Paragraph.new
|
|
542
|
+
node << paragraph
|
|
543
|
+
return node << part if part.is_a?(Node::BaseNode)
|
|
544
|
+
|
|
545
|
+
fill_inline(paragraph, [part.to_s])
|
|
546
|
+
node
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# 把 +parts+ 解析成行内节点挂到 +node+。
|
|
550
|
+
def fill_inline_parts(node, parts)
|
|
551
|
+
# `text` 保留原文,与 Parser 构造出的节点一致;只有全字符串参数才成立。
|
|
552
|
+
node.attributes[:text] = parts.join if !parts.empty? && parts.all?(String)
|
|
553
|
+
inline_parts(parts).each { |part| node << part }
|
|
554
|
+
node
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# 把参数转成行内节点:字符串按 CSM 行内语法解析,节点原样收下。
|
|
558
|
+
def inline_parts(parts)
|
|
246
559
|
parts.flat_map do |part|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
else
|
|
251
|
-
InlineParser.parse(part.to_s)
|
|
560
|
+
case part
|
|
561
|
+
when Node::BaseNode then [part]
|
|
562
|
+
when String then inline_parser.parse(part)
|
|
563
|
+
else [Node::Text.new(part.to_s)]
|
|
252
564
|
end
|
|
253
565
|
end
|
|
254
566
|
end
|
|
255
|
-
end
|
|
256
567
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
568
|
+
# 行内解析器(用于字符串参数):默认取 +MDOM::InlineParser+,本文件不 require 它。
|
|
569
|
+
def inline_parser
|
|
570
|
+
@inline_parser ||= MDOM::InlineParser
|
|
571
|
+
rescue NameError
|
|
572
|
+
raise ArgumentError,
|
|
573
|
+
"string arguments need an inline parser: require \"mdom/inline_parser\" or pass inline_parser:"
|
|
574
|
+
end
|
|
260
575
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
576
|
+
# 块级解析器(用于 `render(parse: true)`):默认取 +MDOM::Parser+。
|
|
577
|
+
def parser
|
|
578
|
+
@parser ||= MDOM::Parser
|
|
579
|
+
rescue NameError
|
|
580
|
+
raise ArgumentError,
|
|
581
|
+
"render(parse: true) needs a parser: require \"mdom/parser\" or pass parser:"
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# 原文缓存(块形式的行内内容)用的序列化器:默认 +MDOM::MarkdownSerializer+。
|
|
585
|
+
# 它与本文件同属 builder 侧(零 gem 依赖),所以是唯一的直接依赖。
|
|
586
|
+
def text_serializer
|
|
587
|
+
@text_serializer ||= MarkdownSerializer
|
|
267
588
|
end
|
|
268
589
|
end
|
|
590
|
+
end
|
|
269
591
|
|
|
270
|
-
|
|
592
|
+
# `值.render`(CSM §3.3):把值交给**当前文档**的 builder。
|
|
593
|
+
#
|
|
594
|
+
# 定义在 `Object` 上,所以 `begin ... end.render` 对任意块值都成立(`String`、
|
|
595
|
+
# `Array`、`nil`……)。代价是给宿主的每个对象都加了 `render`——docs/IMPLEMENTATION.md
|
|
596
|
+
# §5.2 已接受这一点,并说明为什么这是唯一可行的落点。
|
|
597
|
+
class Object
|
|
598
|
+
# 把值渲染进当前文档;没有正在渲染的文档时抛错。
|
|
599
|
+
#
|
|
600
|
+
# 两种写法走同一条路(CSM §3.3):**给了位置参数就用它,否则用接收者本身**——
|
|
601
|
+
# 于是块内裸 `render(value)` 与 `值.render` 等价。`render(nil)` 仍然是「渲染 nil」
|
|
602
|
+
# (按 §3.3 的值分派表被忽略),多余的位置参数由 Ruby 自己报错。
|
|
603
|
+
def render(value = self, **options)
|
|
604
|
+
builder = MDOM::Builder.current
|
|
605
|
+
raise MDOM::Error, "render outside of a document" if builder.nil?
|
|
606
|
+
|
|
607
|
+
builder.render(value, **options)
|
|
608
|
+
self
|
|
609
|
+
end
|
|
271
610
|
end
|