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.
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
- # A builder (DSL) for constructing MDOM trees programmatically, as an
5
- # alternative to parsing Markdown text.
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
- # Every method both appends the constructed node to the node currently being
8
- # built and returns the builder itself, so calls chain. Blocks nest: while a
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.h1 "Title"
14
- # b.p "A paragraph with *emphasis*."
15
- # b.list do
16
- # b.item "one"
17
- # b.item "two"
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
- # String arguments to block- and inline-level methods are run through the
23
- # inline parser, so *emphasis*, **strong**, `code`, [links](url) and
24
- # ![images](src) are recognised. Use the explicit inline methods
25
- # (+em+, +strong+, +code_span+, +link+, +image+) when you need an exact
26
- # structure; their string arguments are treated as literal text unless you
27
- # nest an explicit method inside a block.
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
- # Build a document with +block+ (or a nested block passed to an explicit
30
- # +builder+) and return it. See MDOM.build.
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
- def initialize
37
- @document = Document.new
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
- # -- block level ---------------------------------------------------------
98
+ # -- 结果 -----------------------------------------------------------------
99
+
100
+ # 正在构造的文档(根节点)。同时把它从「当前文档」栈上退出。
101
+ def to_document
102
+ Builder.leave(self)
103
+ @document
104
+ end
105
+ alias finish to_document
42
106
 
43
- # Build a document (the root node). With a block, the block is evaluated
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
- push(@document) { instance_eval(&block) } if block
109
+ within(@document, &block) if block
48
110
  self
49
111
  end
50
112
 
51
- # An ATX heading. +level+ defaults to 1; the remaining arguments are the
52
- # heading's inline content.
53
- def heading(level = 1, *parts, **attributes)
54
- node = Heading.new(level, children: inline_nodes(parts), attributes: attributes)
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, **attributes) = heading(1, *parts, **attributes)
59
- def h2(*parts, **attributes) = heading(2, *parts, **attributes)
60
- def h3(*parts, **attributes) = heading(3, *parts, **attributes)
61
- def h4(*parts, **attributes) = heading(4, *parts, **attributes)
62
- def h5(*parts, **attributes) = heading(5, *parts, **attributes)
63
- def h6(*parts, **attributes) = heading(6, *parts, **attributes)
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
- # A paragraph. With a block, the block's inline nodes become its children.
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
- # A list. +ordered: true+ produces an ordered list; +start:+ sets its first
74
- # number. Without a block, +items+ are wrapped into single-paragraph list
75
- # items.
76
- def list(*items, ordered: false, tight: true, start: nil, &block)
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
- push(node) { instance_eval(&block) }
145
+ within(node, &block)
80
146
  else
81
- items.each { node.append(ListItem.new(children: [Paragraph.new])) }
82
- refill_items(node, items)
147
+ item_marker = node.attributes[:marker]
148
+ within(node) { items.each { |text| item(text, marker: item_marker) } }
83
149
  end
84
- append(node)
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
- # An ordered list, as a shorthand for +list(ordered: true)+.
90
- def ol(*items, start: nil, &block) = list(*items, ordered: true, start: start, &block)
91
-
92
- # A list item. Without a block, +parts+ become a single paragraph.
93
- def item(*parts, task: nil, checked: false, &block)
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
- # A task list item (+checked+ marks it done).
166
+ # 任务列表项。
109
167
  def task(checked, *parts, &block)
110
168
  item(*parts, task: true, checked: checked, &block)
111
169
  end
112
170
 
113
- # A blockquote. +parts+/+block+ are parsed as block content.
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
- # A fenced code block. +lang+ sets the info string.
122
- def code_block(literal, lang: nil, fenced: true)
123
- append(CodeBlock.new(literal.to_s, lang: lang, fenced: fenced))
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
- # A horizontal rule.
127
- def hrule(*, **)
128
- append(Hrule.new)
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
- # -- inline --------------------------------------------------------------
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
- # Emphasis (*x*). With a block, nested explicit inline methods are used;
135
- # otherwise string arguments are parsed as inline content. Appends to the
136
- # current target and returns the node.
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
- # Strong (**x**).
146
- def strong(*parts, &block)
147
- node = Strong.new
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
- # A code span (`x`). The argument is literal.
154
- def code_span(value)
155
- append(Code.new(value.to_s))
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
- # A link. Without a block the label is the literal +*label+ parts; with a
159
- # block, nested explicit inline methods build the label.
160
- def link(destination, *label, title: nil, &block)
161
- node = Link.new(destination, title: title)
162
- build_inline(node, label, &block)
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
- # An image.
168
- def image(destination, alt: nil, title: nil)
169
- append(Image.new(destination, alt: alt, title: title))
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
- # A soft line break.
173
- def softbreak
174
- append(Softbreak.new)
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
- # A hard line break (two trailing spaces).
178
- def hardbreak
179
- append(Hardbreak.new)
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
- # -- results -------------------------------------------------------------
268
+ # -- 行内 -----------------------------------------------------------------
183
269
 
184
- # The document built so far.
185
- def to_document
186
- @document
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
- # Serialize the document built so far to Markdown.
191
- def to_markdown
192
- @document.to_markdown
291
+ # `![alt](uri "title")` → `<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
- # A human-readable tree rendering of the document built so far.
196
- def inspect
197
- to_document.inspect
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
- # Append +node+ to the current target and return the builder, so both
203
- # +b.p "x"+ chains and top-level calls accumulate in the document.
204
- def append(node)
205
- @stack.last.append(node)
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
- # Make +node+ the current target while the block runs, then restore.
210
- def push(node)
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
- # Fill +node+ with inline content: a block plus direct parts, or the parts
218
- # alone. With a block, parts are rejected (there is only one content site).
219
- def build_inline(node, parts, &block)
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
- push(node) { instance_eval(&block) }
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
- # Fill +node+ with block content: a block plus direct parts (paragraphs),
230
- # or the parts alone.
231
- def build_blocks(node, parts, &block)
232
- parts.each { |part| node.append(Paragraph.new(children: inline_nodes([part]))) }
233
- push(node) { instance_eval(&block) } if block
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
- # Build the item paragraphs for a block-less +list(*items)+ call.
237
- def refill_items(list_node, items)
238
- list_node.children.each_with_index do |item_node, idx|
239
- item_node.children.first.append_parts(inline_nodes([items[idx]]))
240
- end
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
- # Convert arguments into inline nodes: strings are parsed as inline
244
- # Markdown, existing nodes pass through (and are detached from any parent).
245
- def inline_nodes(parts)
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
- if part.is_a?(Node)
248
- part.remove
249
- part
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
- def self.build(&block)
258
- Builder.build(&block)
259
- end
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
- # Adds a batch-append helper to the block/inline nodes that carry inline
262
- # content, so the builder can attach parsed parts in one call.
263
- module InlineContainer
264
- def append_parts(nodes)
265
- nodes.each { |node| append(node) }
266
- self
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
- [Paragraph, Emphasis, Strong, Link].each { |klass| klass.include(InlineContainer) }
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