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.
- 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 +486 -0
- 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 +59 -7
- metadata +41 -7
- data/lib/mdom/nodes.rb +0 -214
- data/lib/mdom/serializer.rb +0 -165
|
@@ -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
|
data/lib/mdom/builder.rb
ADDED
|
@@ -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
|
+
# `` → `<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
|