mdom 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "error"
6
+
7
+ module MDOM
8
+ # 用 Ruby 官方解析器(Prism)判定「不带 `begin` 前缀的 Ruby 块」与
9
+ # 「引导块关键字段落」(CSM §3.5)。
10
+ #
11
+ # 之所以必须交给 Prism,是因为 `do` / `end` 在英语里也是常见单词,纯词法
12
+ # 无法区分「Ruby 块」与「英文散文」(CSM §3.5.1)。判定只在 CSM → AST1
13
+ # 这一侧发生(docs/IMPLEMENTATION.md §1.1);AST1 一旦产出,后续阶段不再
14
+ # 需要任何词法能力。
15
+ #
16
+ # 判定按候选块**增量 parse**(CSM §3.5.4):从候选行开始逐行增加,只解析
17
+ # 这一段,而不是整篇一次解析——文档里的英文散文可能构成语法错误,整篇解析
18
+ # 会让一处错误波及全篇。
19
+ class BlockDetector
20
+ # 会引导块的关键字(CSM §3.5.1 的 B 类)。大小写严格匹配。
21
+ GUIDING_KEYWORDS = %w[begin while until for if unless case def class module do].freeze
22
+
23
+ # 关键字 → Prism 节点类。
24
+ KEYWORD_NODES = {
25
+ "begin" => Prism::BeginNode,
26
+ "while" => Prism::WhileNode,
27
+ "until" => Prism::UntilNode,
28
+ "for" => Prism::ForNode,
29
+ "if" => Prism::IfNode,
30
+ "unless" => Prism::UnlessNode,
31
+ "case" => Prism::CaseNode,
32
+ "def" => Prism::DefNode,
33
+ "class" => Prism::ClassNode,
34
+ "module" => Prism::ModuleNode
35
+ }.freeze
36
+
37
+ # 判定结果。
38
+ #
39
+ # * +end_index+ —— 块尾行在 +lines+ 中的下标(0 起);
40
+ # * +form+ —— +:begin+(B 类)/ +:free+(A 类);
41
+ # * +render+ —— 是否以 `.render` 收尾。
42
+ Detection = Struct.new(:end_index, :form, :render, keyword_init: true)
43
+
44
+ # 在 +lines+(已规范化:tab 展开、去 `\r`)中,从 +start_index+ 起判定候选块。
45
+ #
46
+ # +indent+ 是候选行的 CSM 缩进(前导空格数)。返回 +Detection+ 或 +nil+。
47
+ #
48
+ # 抛出 +CrossLevelError+,当 Prism 判定为块、但块尾行的缩进与块首行不同
49
+ # (CSM §3.5.2 条件 1)。
50
+ def detect(lines, start_index, indent)
51
+ first = drop_indent(lines[start_index], indent)
52
+ kind, keyword = classify(first)
53
+ return nil unless kind
54
+
55
+ # 增量 parse 只在**候选所在的 CSM 块范围内**进行(CSM §3.5.4):块由空行
56
+ # 或更浅的缩进收尾。逐行增加,直到解析成为以候选行起始的完整顶层语句,
57
+ # 立即给出结论——不做整篇一次解析,散文里的语法错误不会波及全篇。
58
+ (start_index..block_end(lines, start_index, indent)).each do |last|
59
+ line_count = last - start_index + 1
60
+ code = (start_index..last).map { |i| drop_indent(lines[i], indent) }.join("\n")
61
+ result = parse(code)
62
+ next unless result
63
+
64
+ node = result.value.statements.body.last
65
+ next if node.nil?
66
+ # 顶层语句必须恰好从候选行开始、到当前行结束;一旦成立就说明
67
+ # 「从候选行起的一个完整 Ruby 语句到此为止」,无论是否匹配块结构,
68
+ # 都不必再往下扩展。
69
+ next unless covers?(node, code, line_count)
70
+
71
+ match = kind == :keyword ? keyword_match(node, keyword) : call_match(node)
72
+ return nil unless match
73
+
74
+ assert_same_indent!(lines, start_index, last, indent)
75
+ assert_no_heredoc!(code, start_index, last)
76
+ return Detection.new(end_index: last, form: match[:form], render: match[:render])
77
+ end
78
+ nil
79
+ end
80
+
81
+ private
82
+
83
+ # 候选所在的 CSM 块的最后一行(CSM §1.1:块由空行或更浅的缩进收尾)。
84
+ #
85
+ # 更浅缩进的那一行**包含在内**——块尾缩进与块首不同时要能按 §3.5.2 报错,
86
+ # 而不是静默降级成排版。
87
+ def block_end(lines, start_index, indent)
88
+ (start_index + 1...lines.size).each do |i|
89
+ return i - 1 if lines[i].strip.empty?
90
+ return i if indent_width(lines[i]) < indent
91
+ end
92
+ lines.size - 1
93
+ end
94
+
95
+ # 收窄:行首词是引导块关键字 → B 类;否则行首是字母 → A 类;否则不判定。
96
+ def classify(first)
97
+ word = first[/\A[A-Za-z_][A-Za-z0-9_]*/]
98
+ return [nil, nil] unless word
99
+
100
+ if GUIDING_KEYWORDS.include?(word)
101
+ [:keyword, word]
102
+ elsif first.match?(/\A[A-Za-z]/)
103
+ [:call, nil]
104
+ else
105
+ [nil, nil]
106
+ end
107
+ end
108
+
109
+ def parse(code)
110
+ result = Prism.parse(code)
111
+ result.success? ? result : nil
112
+ rescue StandardError
113
+ nil
114
+ end
115
+
116
+ # 顶层语句是否恰好覆盖「整段代码」。
117
+ def covers?(node, code, line_count)
118
+ loc = node.location
119
+ last_line = code.lines.last.to_s.rstrip
120
+ loc.start_line == 1 &&
121
+ loc.start_column.zero? &&
122
+ loc.end_line == line_count &&
123
+ loc.end_column == last_line.length
124
+ end
125
+
126
+ # A 类:`CallNode`(一个或多个)→ 带 `closing_loc` 的 `BlockNode`。
127
+ #
128
+ # 允许链上出现多个 `CallNode`,这自然覆盖了 `end.render` 这类收尾。
129
+ def call_match(node)
130
+ render = false
131
+ if render_call?(node)
132
+ render = true
133
+ node = node.receiver
134
+ end
135
+ return nil unless node.is_a?(Prism::CallNode)
136
+
137
+ cursor = node
138
+ while cursor.is_a?(Prism::CallNode)
139
+ block = cursor.block
140
+ return { form: :free, render: render } if block.is_a?(Prism::BlockNode) && block.closing_loc
141
+
142
+ cursor = cursor.receiver
143
+ end
144
+ nil
145
+ end
146
+
147
+ # B 类:Prism 给出的节点类必须与首行关键字一致。
148
+ def keyword_match(node, keyword)
149
+ render = false
150
+ if render_call?(node)
151
+ render = true
152
+ node = node.receiver
153
+ end
154
+
155
+ expected = KEYWORD_NODES[keyword]
156
+ return nil unless expected && node.is_a?(expected)
157
+
158
+ { form: :begin, render: render }
159
+ end
160
+
161
+ def render_call?(node)
162
+ node.is_a?(Prism::CallNode) && node.name == :render && node.block.nil?
163
+ end
164
+
165
+ # CSM §3.5.2 条件 1:块首行与块尾行的缩进必须完全相同。
166
+ def assert_same_indent!(lines, start_index, last_index, indent)
167
+ found = indent_width(lines[last_index])
168
+ return if found == indent
169
+
170
+ raise CrossLevelError,
171
+ "ruby block opens with #{indent} space(s) of indentation " \
172
+ "(line #{start_index + 1}) but closes with #{found} (line #{last_index + 1})"
173
+ end
174
+
175
+ # CSM §3.12 / 附录 E:heredoc(`<<~SQL` 等)**明确不支持**。既然块判定本来就
176
+ # 要交给 Prism,就顺便让它把 heredoc 也认出来——被判定为块的内容里出现
177
+ # +HEREDOC_START+ 就报错,而不是当普通块收下。
178
+ def assert_no_heredoc!(code, start_index, last_index)
179
+ return unless heredoc?(code)
180
+
181
+ raise UnsupportedHeredocError,
182
+ "heredoc is not supported (CSM §3.12): " \
183
+ "line #{start_index + 1}..#{last_index + 1}"
184
+ end
185
+
186
+ def heredoc?(code)
187
+ Prism.lex(code).value.any? { |token, _state| token.type.to_s == "HEREDOC_START" }
188
+ end
189
+
190
+ def indent_width(line)
191
+ line[/\A[ \t]*/].to_s.length
192
+ end
193
+
194
+ def drop_indent(line, count)
195
+ index = 0
196
+ index += 1 while index < count && index < line.length && line[index] == " "
197
+ line[index..]
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,486 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+ require_relative "node/inline_nodes"
5
+ require_relative "node/nodes"
6
+
7
+ module MDOM
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)。
21
+ #
22
+ # **并发**:+render+ 内部持一把互斥锁(CSM §3.3),只保证结构不被撕裂、不保证挂载
23
+ # 位置;mruby 的默认构建没有 `Mutex`,此时退化为无锁。
24
+ #
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 "二"
31
+ # end
32
+ # end
33
+ #
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+ 常量,宿主可以替换成别的实现。
41
+ class Builder
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,见类说明。
51
+ def self.build(builder = new, &block)
52
+ builder.document(&block)
53
+ builder.to_document
54
+ end
55
+
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
91
+ @stack = [@document]
92
+ @inline_parser = inline_parser
93
+ @parser = parser
94
+ @lock = LOCK_CLASS&.new
95
+ Builder.enter(self)
96
+ end
97
+
98
+ # -- 结果 -----------------------------------------------------------------
99
+
100
+ # 正在构造的文档(根节点)。同时把它从「当前文档」栈上退出。
101
+ def to_document
102
+ Builder.leave(self)
103
+ @document
104
+ end
105
+ alias finish to_document
106
+
107
+ # 把块的内容挂到文档根下。
108
+ def document(&block)
109
+ within(@document, &block) if block
110
+ self
111
+ end
112
+
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)
119
+ append(node)
120
+ fill_inline(node, parts, &block)
121
+ self
122
+ end
123
+
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)
130
+
131
+ # 段落。字符串参数按 CSM 行内语法解析。
132
+ def paragraph(*parts, &block)
133
+ node = Node::Paragraph.new
134
+ append(node)
135
+ fill_inline(node, parts, &block)
136
+ self
137
+ end
138
+ alias p paragraph
139
+
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)
144
+ if block
145
+ within(node, &block)
146
+ else
147
+ item_marker = node.attributes[:marker]
148
+ within(node) { items.each { |text| item(text, marker: item_marker) } }
149
+ end
150
+ self
151
+ end
152
+
153
+ def ul(*items, &block) = list(*items, ordered: false, &block)
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)
160
+ append(node)
161
+ fill_flow(node, parts, &block)
162
+ self
163
+ end
164
+ alias li item
165
+
166
+ # 任务列表项。
167
+ def task(checked, *parts, &block)
168
+ item(*parts, task: true, checked: checked, &block)
169
+ end
170
+
171
+ # 块引用。+parts+ 各自成为一个段落(FLOW 内容);+cite+ 对应 `>-` 收尾。
172
+ def blockquote(*parts, cite: nil, &block)
173
+ node = Node::Blockquote.new
174
+ append(node)
175
+ fill_flow(node, parts, &block)
176
+ append_cite(node, cite)
177
+ self
178
+ end
179
+ alias quote blockquote
180
+
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
187
+ end
188
+
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
193
+ end
194
+
195
+ # 块级公式(`$$…$$`)。+tex+ 按字面收下(CSM §1.11)。
196
+ def math_block(tex)
197
+ append(Node::MathBlock.new(tex.to_s))
198
+ self
199
+ end
200
+
201
+ # 表格(纯结构,CSM §1.12):块内给出若干 #row。
202
+ def table(&block)
203
+ node = Node::Table.new
204
+ append(node)
205
+ within(node, &block) if block
206
+ self
207
+ end
208
+
209
+ # 表格行:块内给出若干单元格。
210
+ def row(&block)
211
+ node = Node::TableRow.new
212
+ append(node)
213
+ within(node, &block) if block
214
+ self
215
+ end
216
+
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)
225
+ end
226
+
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
237
+ append(node)
238
+ within(node, &block) if block
239
+ self
240
+ end
241
+ alias div division
242
+
243
+ # 定义列表:块内依次给出 #term 与 #definition。
244
+ def definition_list(&block)
245
+ node = Node::DefinitionList.new
246
+ append(node)
247
+ within(node, &block) if block
248
+ self
249
+ end
250
+ alias dl definition_list
251
+
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
258
+ end
259
+
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
266
+ end
267
+
268
+ # -- 行内 -----------------------------------------------------------------
269
+
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)
289
+ end
290
+
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)
297
+ end
298
+
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
328
+ end
329
+
330
+ private
331
+
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])
346
+ self
347
+ end
348
+
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)
377
+ @stack.push(node)
378
+ yield self
379
+ ensure
380
+ @stack.pop
381
+ end
382
+
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)
408
+ if block
409
+ raise ArgumentError, "cannot combine inline arguments with a block" unless parts.empty?
410
+
411
+ return within(node, &block)
412
+ end
413
+
414
+ fill_inline_parts(node, parts)
415
+ end
416
+
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
423
+ end
424
+
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
433
+ end
434
+
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)
445
+ parts.flat_map do |part|
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)]
450
+ end
451
+ end
452
+ end
453
+
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
461
+
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:"
468
+ end
469
+ end
470
+ end
471
+
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
486
+ end
data/lib/mdom/error.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MDOM
4
+ # CSM 处理的错误基类。
5
+ class Error < StandardError; end
6
+
7
+ # CSM 源码不合法(解析期)。消息中带文档行号,便于定位。
8
+ class ParseError < Error; end
9
+
10
+ # 判定为 Ruby 块,但块首行与块尾行的 CSM 缩进不同(CSM §3.5.2 条件 1)。
11
+ class CrossLevelError < ParseError; end
12
+
13
+ # 围栏代码块没有找到对称的结束围栏(扫描到文档末尾)。
14
+ class UnclosedFenceError < ParseError; end
15
+
16
+ # 块级公式的 `$$` 包围式没有找到独占一行的结束标记(扫描到文档末尾)。
17
+ class UnclosedMathError < ParseError; end
18
+
19
+ # 块里出现 heredoc。CSM §3.12 明确**不支持** heredoc(附录 E),一旦出现就报错,
20
+ # 而不是把它当普通块收下。
21
+ class UnsupportedHeredocError < ParseError; end
22
+ end