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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +235 -0
- data/LICENSE.txt +21 -0
- data/README.md +323 -0
- data/USAGE.md +864 -0
- data/lib/mdom/block_detector.rb +433 -0
- data/lib/mdom/builder.rb +500 -161
- data/lib/mdom/error.rb +18 -0
- data/lib/mdom/hooks.rb +81 -0
- data/lib/mdom/html_serializer.rb +348 -0
- data/lib/mdom/inline_parser.rb +777 -145
- data/lib/mdom/markdown_serializer.rb +390 -0
- data/lib/mdom/node/inline_nodes.rb +239 -0
- data/lib/mdom/node/nodes.rb +336 -0
- data/lib/mdom/node.rb +685 -199
- data/lib/mdom/parser.rb +284 -234
- data/lib/mdom/tokenizer.rb +458 -0
- data/lib/mdom/translator.rb +379 -0
- data/lib/mdom/version.rb +1 -1
- data/lib/mdom.rb +58 -7
- metadata +40 -7
- data/lib/mdom/nodes.rb +0 -214
- data/lib/mdom/serializer.rb +0 -165
data/lib/mdom/parser.rb
CHANGED
|
@@ -1,285 +1,335 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "error"
|
|
4
|
+
require_relative "inline_parser"
|
|
5
|
+
require_relative "node/nodes"
|
|
6
|
+
require_relative "tokenizer"
|
|
7
|
+
|
|
3
8
|
module MDOM
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
# Token Stream → AST1(docs/IMPLEMENTATION.md §2.4)。
|
|
10
|
+
#
|
|
11
|
+
# 只负责**块结构**:按 CSM 缩进(CSM §1.1)把 token 挂成树。段落、标题等的
|
|
12
|
+
# 正文以原文保存,行内解析留给后续的 InlineParser。嵌入 token 生成
|
|
13
|
+
# +Node::RubyEmbed+ 节点,存源码原文与位置,**不求值**。
|
|
14
|
+
#
|
|
15
|
+
# MDOM::Parser.parse("# 标题\n\n正文\n")
|
|
16
|
+
class Parser
|
|
17
|
+
# 容器栈的一层。+indent+ 是该容器所在行的 CSM 缩进(根与节块用 -1,
|
|
18
|
+
# 表示不会被同级或更深的块弹出)。
|
|
19
|
+
Frame = Struct.new(:indent, :node, :kind, keyword_init: true)
|
|
20
|
+
|
|
21
|
+
# 行内容器:内容策略为 **PHRASING** 的节点(`docs/CSM.md` 元素表),它们的
|
|
22
|
+
# +attributes[:text]+ 会被 InlineParser 解析成行内节点。
|
|
23
|
+
#
|
|
24
|
+
# 内容策略为 FLOW 的容器(`blockquote` / `li` / `dd`)**不在此列**——它们的正文
|
|
25
|
+
# 先成为一个段落,直接子节点一律是块级节点。表格单元格(`th` / `td`)也是
|
|
26
|
+
# PHRASING。
|
|
27
|
+
PHRASING_CONTAINERS = %i[paragraph heading term cite table_header_cell table_cell].freeze
|
|
28
|
+
|
|
29
|
+
# 解析 CSM 源码,返回 +Node::Document+。+inline: false+ 可跳过行内解析。
|
|
30
|
+
def self.parse(source, **options)
|
|
31
|
+
new(**options).parse(source)
|
|
18
32
|
end
|
|
19
33
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
# 参数:
|
|
35
|
+
# * +detector:+ —— 块判定器,默认 +BlockDetector.new+。**自定义判定器时,A 类的
|
|
36
|
+
# 开关与白名单要配在判定器自己身上**,不要再传下面两个选项(同时给出会报错)。
|
|
37
|
+
# * +inline:+ —— 是否做行内解析;+false+ 只做块结构。
|
|
38
|
+
# * +free_blocks:+ —— **A 类(自由块)判定开关**(CSM §3.5),默认 +true+。
|
|
39
|
+
# +false+ 时不做任何 A 类判定,命中行按**排版**处理(CSM §3.5.3,不报错);
|
|
40
|
+
# B 类(引导关键字)与 `~` 行不受影响。
|
|
41
|
+
# * +free_blocks_allowed:+ —— A 类判定的**白名单**,默认 +nil+(不限制)。给出后
|
|
42
|
+
# 只有行首标识符命中的行才尝试 A 类判定:+String+ 精确相等,+Regexp+ 对已剥离
|
|
43
|
+
# 缩进的整行匹配。空数组等价于 +free_blocks: false+。
|
|
44
|
+
#
|
|
45
|
+
# 后两个选项服务于英文散文文档:那里「以字母开头的行」比比皆是,逐一交给 Prism
|
|
46
|
+
# 增量 parse 会产生几百到上万次调用(docs/IMPLEMENTATION.md §2.2.4)。
|
|
47
|
+
def initialize(detector: nil, inline: true, free_blocks: true, free_blocks_allowed: nil)
|
|
48
|
+
@tokenizer = Tokenizer.new(detector: resolve_detector(detector, free_blocks, free_blocks_allowed))
|
|
49
|
+
@inline = inline
|
|
26
50
|
end
|
|
27
51
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
while i < line.length
|
|
38
|
-
ch = line[i]
|
|
39
|
-
case ch
|
|
40
|
-
when " "
|
|
41
|
-
out << " "
|
|
42
|
-
col += 1
|
|
43
|
-
i += 1
|
|
44
|
-
when "\t"
|
|
45
|
-
target = (col / 4 + 1) * 4
|
|
46
|
-
out << " " * (target - col)
|
|
47
|
-
col = target
|
|
48
|
-
i += 1
|
|
49
|
-
else
|
|
50
|
-
out << line[i..]
|
|
52
|
+
def parse(source)
|
|
53
|
+
root = Node::Document.new
|
|
54
|
+
stack = [Frame.new(indent: -1, node: root, kind: :document)]
|
|
55
|
+
@previous = nil
|
|
56
|
+
|
|
57
|
+
@tokenizer.tokenize(source).each do |token|
|
|
58
|
+
if token.type == :end_marker
|
|
59
|
+
stack.pop while stack.size > 1
|
|
60
|
+
root << Node::EndMarker.new(token.value[:line], token.value[:data])
|
|
51
61
|
break
|
|
52
62
|
end
|
|
53
|
-
end
|
|
54
|
-
out
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Line-oriented block scanner. parse_blocks returns a flat array of sibmling
|
|
58
|
-
# block nodes (and nothing else), so callers don't juggle an index.
|
|
59
|
-
class Reader
|
|
60
|
-
def initialize(lines)
|
|
61
|
-
@lines = lines
|
|
62
|
-
@pos = 0
|
|
63
|
-
end
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
dispatch(token, root, stack)
|
|
65
|
+
@previous = token
|
|
67
66
|
end
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
private
|
|
68
|
+
drop_empty_divisions(root)
|
|
69
|
+
resolve_inline(root) if @inline
|
|
70
|
+
root
|
|
71
|
+
end
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# 组装块判定器:没给 +detector+ 就把两个 A 类选项用在默认判定器上;给了就要求
|
|
76
|
+
# 那两个选项保持默认——配置的**单一来源**,避免「传了却不生效」的静默失效。
|
|
77
|
+
def resolve_detector(detector, free_blocks, free_blocks_allowed)
|
|
78
|
+
if detector.nil?
|
|
79
|
+
BlockDetector.new(free_blocks: free_blocks, free_blocks_allowed: free_blocks_allowed)
|
|
80
|
+
else
|
|
81
|
+
unless free_blocks == true && free_blocks_allowed.nil?
|
|
82
|
+
raise ArgumentError,
|
|
83
|
+
"`detector:` cannot be combined with `free_blocks:` / `free_blocks_allowed:` " \
|
|
84
|
+
"— configure the detector itself instead"
|
|
85
|
+
end
|
|
86
|
+
detector
|
|
77
87
|
end
|
|
88
|
+
end
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
def dispatch(token, root, stack)
|
|
91
|
+
case token.type
|
|
92
|
+
when :blank
|
|
93
|
+
handle_blank(token, root, stack)
|
|
94
|
+
when :text
|
|
95
|
+
handle_text(token, stack)
|
|
96
|
+
when :heading
|
|
97
|
+
place_leaf(token, stack, build_heading(token))
|
|
98
|
+
when :hrule
|
|
99
|
+
place_leaf(token, stack, Node::Hrule.new)
|
|
100
|
+
when :fence
|
|
101
|
+
place_leaf(token, stack, build_code_block(token))
|
|
102
|
+
when :math_block
|
|
103
|
+
place_leaf(token, stack, Node::MathBlock.new(token.value[:tex]))
|
|
104
|
+
when :table
|
|
105
|
+
place_leaf(token, stack, build_table(token))
|
|
106
|
+
when :ruby_block
|
|
107
|
+
place_leaf(token, stack, build_embed(token))
|
|
108
|
+
when :blockquote
|
|
109
|
+
handle_blockquote(token, stack)
|
|
110
|
+
when :cite
|
|
111
|
+
handle_cite(token, stack)
|
|
112
|
+
when :list_item
|
|
113
|
+
handle_list_item(token, stack)
|
|
114
|
+
when :definition
|
|
115
|
+
handle_definition(token, stack)
|
|
116
|
+
else
|
|
117
|
+
raise ParseError, "unknown token type #{token.type.inspect} (line #{token.line})"
|
|
81
118
|
end
|
|
119
|
+
end
|
|
82
120
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
121
|
+
# 段落:连续的排版文本行合并为一个段落。软换行的折叠(CSM §1.1)与行尾
|
|
122
|
+
# `\` 的硬换行由 InlineParser 处理,这里只把原始行用 `\n` 连接。
|
|
123
|
+
def handle_text(token, stack)
|
|
124
|
+
parent = pop_to_parent(stack, token.indent)
|
|
125
|
+
last = parent.children.last
|
|
86
126
|
|
|
87
|
-
|
|
88
|
-
|
|
127
|
+
if @previous&.type == :text && @previous.indent == token.indent && last.is_a?(Node::Paragraph)
|
|
128
|
+
last.text = "#{last.text}\n#{token.value[:text]}"
|
|
129
|
+
else
|
|
130
|
+
parent << Node::Paragraph.new(token.value[:text])
|
|
89
131
|
end
|
|
132
|
+
end
|
|
90
133
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
134
|
+
# 块引用(内容策略 FLOW):相邻的引用行合并进**同一个段落**(软换行语义,
|
|
135
|
+
# CSM §1.1);更深的缩进开启块级子节点。
|
|
136
|
+
def handle_blockquote(token, stack)
|
|
137
|
+
indent = token.indent
|
|
138
|
+
pop_deeper(stack, indent)
|
|
139
|
+
stack.pop while stack.size > 1 && stack.last.indent == indent && stack.last.kind != :quote
|
|
140
|
+
|
|
141
|
+
if stack.last.kind == :quote && stack.last.indent == indent
|
|
142
|
+
quote = stack.last.node
|
|
143
|
+
else
|
|
144
|
+
quote = Node::Blockquote.new
|
|
145
|
+
stack.last.node << quote
|
|
146
|
+
stack.push(Frame.new(indent: indent, node: quote, kind: :quote))
|
|
103
147
|
end
|
|
104
148
|
|
|
105
|
-
|
|
106
|
-
|
|
149
|
+
append_body_line(quote, token.value[:text])
|
|
150
|
+
quote
|
|
151
|
+
end
|
|
107
152
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
153
|
+
# 把一行正文并进 FLOW 容器的最后一个段落(软换行折叠交给 InlineParser);
|
|
154
|
+
# 上一个子节点不是段落(如嵌套块)时另起一个段落。空行不产生内容。
|
|
155
|
+
def append_body_line(node, text)
|
|
156
|
+
return if text.empty?
|
|
111
157
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
158
|
+
paragraph = node.children.last
|
|
159
|
+
if paragraph.is_a?(Node::Paragraph)
|
|
160
|
+
paragraph.text = "#{paragraph.text}\n#{text}"
|
|
161
|
+
else
|
|
162
|
+
node << Node::Paragraph.new(text)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
118
165
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
166
|
+
# `>-` 收尾:作为 +Node::Cite+ 子节点挂到同级引用的末尾(内容策略
|
|
167
|
+
# PHRASING,正文会被行内解析,CSM §1.4)。
|
|
168
|
+
def handle_cite(token, stack)
|
|
169
|
+
indent = token.indent
|
|
170
|
+
pop_deeper(stack, indent)
|
|
171
|
+
|
|
172
|
+
if stack.last.kind == :quote && stack.last.indent == indent
|
|
173
|
+
quote = stack.last.node
|
|
174
|
+
else
|
|
175
|
+
stack.pop while stack.size > 1 && stack.last.indent == indent
|
|
176
|
+
quote = Node::Blockquote.new
|
|
177
|
+
stack.last.node << quote
|
|
178
|
+
end
|
|
123
179
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
end
|
|
180
|
+
quote << Node::Cite.new(token.value[:text])
|
|
181
|
+
end
|
|
127
182
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
183
|
+
# 列表项(内容策略 FLOW):同缩进的连续项复用同一个 +Node::List+,更深缩进则
|
|
184
|
+
# 嵌入到上一个列表项之下(CSM §1.3 的「嵌套通过缩进控制」)。正文成为段落。
|
|
185
|
+
def handle_list_item(token, stack)
|
|
186
|
+
indent = token.indent
|
|
187
|
+
pop_deeper(stack, indent)
|
|
188
|
+
stack.pop while stack.size > 1 && stack.last.indent == indent && stack.last.kind != :list
|
|
189
|
+
|
|
190
|
+
if stack.last.kind == :list && stack.last.indent == indent
|
|
191
|
+
list = stack.last.node
|
|
192
|
+
else
|
|
193
|
+
list = Node::List.new(ordered: token.value[:ordered], task: token.value[:task],
|
|
194
|
+
marker: token.value[:marker])
|
|
195
|
+
stack.last.node << list
|
|
196
|
+
stack.push(Frame.new(indent: indent, node: list, kind: :list))
|
|
197
|
+
end
|
|
133
198
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
199
|
+
list.attributes[:task] = true if token.value[:task]
|
|
200
|
+
item = Node::ListItem.new(marker: token.value[:marker], task: token.value[:task],
|
|
201
|
+
checked: token.value[:checked])
|
|
202
|
+
item << Node::Paragraph.new(token.value[:text])
|
|
203
|
+
list << item
|
|
204
|
+
stack.push(Frame.new(indent: indent, node: item, kind: :item))
|
|
205
|
+
item
|
|
206
|
+
end
|
|
137
207
|
|
|
138
|
-
|
|
208
|
+
# 定义列表(CSM §1.3):`term` 行 + 其后的 `: 定义` 行。第一个 `:` 行把前一个
|
|
209
|
+
# 同级段落提升为定义术语,再建 +Node::DefinitionList+。
|
|
210
|
+
#
|
|
211
|
+
# +dd+ 的内容策略是 FLOW,所以正文成为段落;并且它自己成为容器栈的一层——
|
|
212
|
+
# 缩进的子块因此挂在**这个** +Definition+ 下(而不是 `dl` 下)。
|
|
213
|
+
def handle_definition(token, stack)
|
|
214
|
+
indent = token.indent
|
|
215
|
+
pop_deeper(stack, indent)
|
|
216
|
+
stack.pop while stack.size > 1 && stack.last.indent == indent && stack.last.kind != :deflist
|
|
217
|
+
|
|
218
|
+
if stack.last.kind == :deflist && stack.last.indent == indent
|
|
219
|
+
list = stack.last.node
|
|
220
|
+
else
|
|
221
|
+
list = promote_to_definition_list(stack.last.node)
|
|
222
|
+
stack.push(Frame.new(indent: indent, node: list, kind: :deflist))
|
|
139
223
|
end
|
|
140
224
|
|
|
141
|
-
|
|
142
|
-
|
|
225
|
+
definition = Node::Definition.new
|
|
226
|
+
definition << Node::Paragraph.new(token.value[:text])
|
|
227
|
+
list << definition
|
|
228
|
+
stack.push(Frame.new(indent: indent, node: definition, kind: :definition))
|
|
229
|
+
definition
|
|
230
|
+
end
|
|
143
231
|
|
|
144
|
-
|
|
232
|
+
# 空行终止当前块(CSM §1.1)。连续四个 `\n` 另起一个节块(`<div>`)。
|
|
233
|
+
def handle_blank(token, root, stack)
|
|
234
|
+
if token.value[:count] >= 3
|
|
235
|
+
stack.pop while stack.size > 1
|
|
236
|
+
division = Node::Div.new
|
|
237
|
+
root << division
|
|
238
|
+
stack.push(Frame.new(indent: -1, node: division, kind: :div))
|
|
239
|
+
else
|
|
240
|
+
stack.pop while stack.size > 1 && stack.last.kind != :div
|
|
145
241
|
end
|
|
242
|
+
end
|
|
146
243
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@pos += 1
|
|
155
|
-
content = []
|
|
156
|
-
while !eof?
|
|
157
|
-
line = current
|
|
158
|
-
if (cm = FENCE_RE.match(line)) && cm[2][0] == marker && cm[2].length >= fence_len && cm[1].length <= 3
|
|
159
|
-
@pos += 1
|
|
160
|
-
break
|
|
161
|
-
end
|
|
162
|
-
content << line[indent..].to_s
|
|
163
|
-
@pos += 1
|
|
164
|
-
end
|
|
165
|
-
CodeBlock.new(content.join("\n"), lang: lang, fenced: true)
|
|
166
|
-
end
|
|
244
|
+
# 叶子块:挂到「缩进严格小于自己」的最近容器下。
|
|
245
|
+
def place_leaf(token, stack, node)
|
|
246
|
+
parent = pop_to_parent(stack, token.indent)
|
|
247
|
+
parent << node
|
|
248
|
+
node
|
|
249
|
+
end
|
|
167
250
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
251
|
+
# 弹出所有「缩进不小于 +indent+」的容器,返回合适的父容器。
|
|
252
|
+
def pop_to_parent(stack, indent)
|
|
253
|
+
stack.pop while stack.size > 1 && stack.last.indent >= indent
|
|
254
|
+
stack.last.node
|
|
255
|
+
end
|
|
171
256
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Heading.new(level, children: inline_children(text))
|
|
177
|
-
end
|
|
257
|
+
# 只弹出「缩进严格大于 +indent+」的容器,保留同级的可复用容器。
|
|
258
|
+
def pop_deeper(stack, indent)
|
|
259
|
+
stack.pop while stack.size > 1 && stack.last.indent > indent
|
|
260
|
+
end
|
|
178
261
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
262
|
+
# 把容器里最后一个同级段落提升为定义术语。
|
|
263
|
+
def promote_to_definition_list(parent)
|
|
264
|
+
list = Node::DefinitionList.new
|
|
265
|
+
last = parent.children.last
|
|
266
|
+
|
|
267
|
+
if last.is_a?(Node::Paragraph)
|
|
268
|
+
list << Node::Term.new(last.text)
|
|
269
|
+
parent.replace(last, list)
|
|
270
|
+
else
|
|
271
|
+
parent << list
|
|
186
272
|
end
|
|
273
|
+
list
|
|
274
|
+
end
|
|
187
275
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
while !eof?
|
|
192
|
-
line = current
|
|
193
|
-
lm = ORDERED_RE.match(line) || LIST_RE.match(line)
|
|
194
|
-
break if lm.nil?
|
|
195
|
-
break if (ORDERED_RE.match(line).nil?) == ordered
|
|
196
|
-
|
|
197
|
-
marker_len = marker_width(lm)
|
|
198
|
-
content = lm[5] || lm[4]
|
|
199
|
-
task = false
|
|
200
|
-
checked = false
|
|
201
|
-
if (tm = TASK_RE.match(content))
|
|
202
|
-
task = true
|
|
203
|
-
checked = tm[1].downcase == "x"
|
|
204
|
-
content = tm[2]
|
|
205
|
-
end
|
|
206
|
-
@pos += 1
|
|
207
|
-
|
|
208
|
-
item_lines = [content]
|
|
209
|
-
while !eof? && !blank?(current)
|
|
210
|
-
l = current
|
|
211
|
-
child = ORDERED_RE.match(l) || LIST_RE.match(l)
|
|
212
|
-
break if child && marker_width(child) <= marker_len
|
|
213
|
-
|
|
214
|
-
item_lines << l
|
|
215
|
-
@pos += 1
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
li = ListItem.new
|
|
219
|
-
Reader.parse_lines(item_lines).each { |n| li.append(n) }
|
|
220
|
-
li.attributes[:task] = true if task
|
|
221
|
-
li.attributes[:checked] = checked if task
|
|
222
|
-
items << li
|
|
223
|
-
end
|
|
276
|
+
def build_heading(token)
|
|
277
|
+
Node::Heading.new(token.value[:level], token.value[:text], closed: token.value[:closed])
|
|
278
|
+
end
|
|
224
279
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
280
|
+
def build_code_block(token)
|
|
281
|
+
Node::CodeBlock.new(
|
|
282
|
+
token.value[:code],
|
|
283
|
+
lang: token.value[:lang],
|
|
284
|
+
params: token.value[:params],
|
|
285
|
+
fence: token.value[:fence]
|
|
286
|
+
)
|
|
287
|
+
end
|
|
229
288
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
289
|
+
# 表格(CSM §1.12):行 → 单元格,表头行用 +TableHeaderCell+,其余用
|
|
290
|
+
# +TableCell+;对齐逐列取自该行生效的分隔行。
|
|
291
|
+
def build_table(token)
|
|
292
|
+
Node::Table.new(children: token.value[:rows].map { |row| build_table_row(row) })
|
|
293
|
+
end
|
|
235
294
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
Paragraph.new(children: inline_children(collected.join("\n")))
|
|
243
|
-
end
|
|
295
|
+
def build_table_row(row)
|
|
296
|
+
klass = row[:header] ? Node::TableHeaderCell : Node::TableCell
|
|
297
|
+
align = row[:align] || []
|
|
298
|
+
cells = row[:cells].each_with_index.map { |text, column| klass.new(text, align: align[column]) }
|
|
299
|
+
Node::TableRow.new(children: cells)
|
|
300
|
+
end
|
|
244
301
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
302
|
+
def build_embed(token)
|
|
303
|
+
Node::RubyEmbed.new(
|
|
304
|
+
source: token.value[:source],
|
|
305
|
+
form: token.value[:form],
|
|
306
|
+
lines: token.value[:lines],
|
|
307
|
+
indent: token.value[:indent],
|
|
308
|
+
render: token.value[:render]
|
|
309
|
+
)
|
|
310
|
+
end
|
|
251
311
|
|
|
252
|
-
|
|
253
|
-
|
|
312
|
+
# 把行内容器(内容策略 PHRASING)的 +text+ 原文解析为行内节点,插到
|
|
313
|
+
# +children+ 之前。
|
|
314
|
+
#
|
|
315
|
+
# +attributes[:text]+ 保留原文(供保真回吐与调试),行内节点才是向下游交付的
|
|
316
|
+
# 结构。FLOW 容器不直接承载行内内容,所以这里不碰它们——它们的正文是段落,
|
|
317
|
+
# 段落自己会被递归解析到。
|
|
318
|
+
def resolve_inline(node)
|
|
319
|
+
node.children.each { |child| resolve_inline(child) }
|
|
320
|
+
return unless PHRASING_CONTAINERS.include?(node.type)
|
|
254
321
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return [] if text.to_s.empty?
|
|
322
|
+
text = node.attributes[:text]
|
|
323
|
+
return if text.nil? || text.empty?
|
|
258
324
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
end
|
|
325
|
+
InlineParser.parse(text).reverse_each { |inline| node.prepend(inline) }
|
|
326
|
+
end
|
|
262
327
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
out = []
|
|
268
|
-
nodes.each_with_index do |node, i|
|
|
269
|
-
if node.text? && nodes[i + 1]&.type == :hardbreak
|
|
270
|
-
value = node.value.sub(/ {2}\z/, "")
|
|
271
|
-
out << (value.empty? ? nil : Text.new(value))
|
|
272
|
-
else
|
|
273
|
-
out << node
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
out.compact
|
|
328
|
+
# 文档末尾由空行开出的空节块没有意义,解析完清掉。
|
|
329
|
+
def drop_empty_divisions(root)
|
|
330
|
+
root.children.dup.each do |child|
|
|
331
|
+
root.remove(child) if child.is_a?(Node::Div) && child.children.empty?
|
|
277
332
|
end
|
|
278
333
|
end
|
|
279
334
|
end
|
|
280
|
-
|
|
281
|
-
# Public entry point: parse +source+ into an MDOM::Document tree.
|
|
282
|
-
def self.parse(source)
|
|
283
|
-
Parser.read(source)
|
|
284
|
-
end
|
|
285
335
|
end
|