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