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.
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
- # A builder (DSL) for constructing MDOM trees programmatically, as an
5
- # alternative to parsing Markdown text.
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
- # 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.
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
- # 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"
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
- # 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.
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
- # Build a document with +block+ (or a nested block passed to an explicit
30
- # +builder+) and return it. See MDOM.build.
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
- def initialize
37
- @document = Document.new
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
- # -- block level ---------------------------------------------------------
105
+ # -- 结果 -----------------------------------------------------------------
106
+
107
+ # 正在构造的文档(根节点)。同时把它从「当前文档」栈上退出。
108
+ def to_document
109
+ Builder.leave(self)
110
+ @document
111
+ end
112
+ alias finish to_document
42
113
 
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.
114
+ # 把块的内容挂到文档根下。
46
115
  def document(&block)
47
- push(@document) { instance_eval(&block) } if block
116
+ within(@document, &block) if block
48
117
  self
49
118
  end
50
119
 
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)
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, **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)
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
- # A paragraph. With a block, the block's inline nodes become its children.
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
- # 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)
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
- push(node) { instance_eval(&block) }
152
+ within(node, &block)
80
153
  else
81
- items.each { node.append(ListItem.new(children: [Paragraph.new])) }
82
- refill_items(node, items)
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
- append(node)
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
- # 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
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
- # A task list item (+checked+ marks it done).
182
+ # 任务列表项。
109
183
  def task(checked, *parts, &block)
110
184
  item(*parts, task: true, checked: checked, &block)
111
185
  end
112
186
 
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)
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
- # 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))
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
- # A horizontal rule.
127
- def hrule(*, **)
128
- append(Hrule.new)
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
- # -- inline --------------------------------------------------------------
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
- # 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)
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
- # Strong (**x**).
298
+ # `**加粗**` → `<strong>`。
146
299
  def strong(*parts, &block)
147
- node = Strong.new
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
- # A code span (`x`). The argument is literal.
303
+ # `` `代码` `` → `<code>`。参数按字面。
154
304
  def code_span(value)
155
- append(Code.new(value.to_s))
305
+ inline_node(Node::CodeSpan.new(value.to_s))
156
306
  end
157
307
 
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)
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
- # An image.
168
- def image(destination, alt: nil, title: nil)
169
- append(Image.new(destination, alt: alt, title: title))
313
+ # `~+文本+~` → `<ins>`。
314
+ def ins(*parts, &block)
315
+ inline_node(Node::Ins.new, parts, &block)
170
316
  end
171
317
 
172
- # A soft line break.
173
- def softbreak
174
- append(Softbreak.new)
318
+ # 行内纯文本。参数按字面。
319
+ def text(value = "")
320
+ inline_node(Node::Text.new(value.to_s))
175
321
  end
176
322
 
177
- # A hard line break (two trailing spaces).
178
- def hardbreak
179
- append(Hardbreak.new)
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
- # -- results -------------------------------------------------------------
328
+ # `[文本](@uri)` → `<q>`。
329
+ def inline_quote(uri, *parts, &block)
330
+ inline_node(Node::InlineQuote.new(uri), parts, &block)
331
+ end
183
332
 
184
- # The document built so far.
185
- def to_document
186
- @document
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
+ # `![alt](uri "title")` → `<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
- # Serialize the document built so far to Markdown.
191
- def to_markdown
192
- @document.to_markdown
368
+ # 行尾 `\` → `<br>`。
369
+ def hardbreak
370
+ inline_node(Node::HardBreak.new)
193
371
  end
194
372
 
195
- # A human-readable tree rendering of the document built so far.
196
- def inspect
197
- to_document.inspect
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
- # 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)
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
- # Make +node+ the current target while the block runs, then restore.
210
- def push(node)
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
- # 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)
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
- push(node) { instance_eval(&block) }
224
- else
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
- # 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
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
- # 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
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
- # 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)
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
- if part.is_a?(Node)
248
- part.remove
249
- part
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
- def self.build(&block)
258
- Builder.build(&block)
259
- end
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
- # 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
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
- [Paragraph, Emphasis, Strong, Link].each { |klass| klass.include(InlineContainer) }
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