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
data/lib/mdom/parser.rb
CHANGED
|
@@ -1,285 +1,307 @@
|
|
|
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
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return [] if s.empty?
|
|
24
|
-
|
|
25
|
-
s.split("\n", -1).map { |line| expand_leading_tabs(line) }
|
|
34
|
+
def initialize(detector: BlockDetector.new, inline: true)
|
|
35
|
+
@tokenizer = Tokenizer.new(detector: detector)
|
|
36
|
+
@inline = inline
|
|
26
37
|
end
|
|
27
38
|
|
|
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..]
|
|
39
|
+
def parse(source)
|
|
40
|
+
root = Node::Document.new
|
|
41
|
+
stack = [Frame.new(indent: -1, node: root, kind: :document)]
|
|
42
|
+
@previous = nil
|
|
43
|
+
|
|
44
|
+
@tokenizer.tokenize(source).each do |token|
|
|
45
|
+
if token.type == :end_marker
|
|
46
|
+
stack.pop while stack.size > 1
|
|
47
|
+
root << Node::EndMarker.new(token.value[:line], token.value[:data])
|
|
51
48
|
break
|
|
52
49
|
end
|
|
53
|
-
end
|
|
54
|
-
out
|
|
55
|
-
end
|
|
56
50
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class Reader
|
|
60
|
-
def initialize(lines)
|
|
61
|
-
@lines = lines
|
|
62
|
-
@pos = 0
|
|
51
|
+
dispatch(token, root, stack)
|
|
52
|
+
@previous = token
|
|
63
53
|
end
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
drop_empty_divisions(root)
|
|
56
|
+
resolve_inline(root) if @inline
|
|
57
|
+
root
|
|
58
|
+
end
|
|
68
59
|
|
|
69
|
-
|
|
70
|
-
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def dispatch(token, root, stack)
|
|
63
|
+
case token.type
|
|
64
|
+
when :blank
|
|
65
|
+
handle_blank(token, root, stack)
|
|
66
|
+
when :text
|
|
67
|
+
handle_text(token, stack)
|
|
68
|
+
when :heading
|
|
69
|
+
place_leaf(token, stack, build_heading(token))
|
|
70
|
+
when :hrule
|
|
71
|
+
place_leaf(token, stack, Node::Hrule.new)
|
|
72
|
+
when :fence
|
|
73
|
+
place_leaf(token, stack, build_code_block(token))
|
|
74
|
+
when :math_block
|
|
75
|
+
place_leaf(token, stack, Node::MathBlock.new(token.value[:tex]))
|
|
76
|
+
when :table
|
|
77
|
+
place_leaf(token, stack, build_table(token))
|
|
78
|
+
when :ruby_block
|
|
79
|
+
place_leaf(token, stack, build_embed(token))
|
|
80
|
+
when :blockquote
|
|
81
|
+
handle_blockquote(token, stack)
|
|
82
|
+
when :cite
|
|
83
|
+
handle_cite(token, stack)
|
|
84
|
+
when :list_item
|
|
85
|
+
handle_list_item(token, stack)
|
|
86
|
+
when :definition
|
|
87
|
+
handle_definition(token, stack)
|
|
88
|
+
else
|
|
89
|
+
raise ParseError, "unknown token type #{token.type.inspect} (line #{token.line})"
|
|
71
90
|
end
|
|
91
|
+
end
|
|
72
92
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
# 段落:连续的排版文本行合并为一个段落。软换行的折叠(CSM §1.1)与行尾
|
|
94
|
+
# `\` 的硬换行由 InlineParser 处理,这里只把原始行用 `\n` 连接。
|
|
95
|
+
def handle_text(token, stack)
|
|
96
|
+
parent = pop_to_parent(stack, token.indent)
|
|
97
|
+
last = parent.children.last
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
if @previous&.type == :text && @previous.indent == token.indent && last.is_a?(Node::Paragraph)
|
|
100
|
+
last.text = "#{last.text}\n#{token.value[:text]}"
|
|
101
|
+
else
|
|
102
|
+
parent << Node::Paragraph.new(token.value[:text])
|
|
81
103
|
end
|
|
104
|
+
end
|
|
82
105
|
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
# 块引用(内容策略 FLOW):相邻的引用行合并进**同一个段落**(软换行语义,
|
|
107
|
+
# CSM §1.1);更深的缩进开启块级子节点。
|
|
108
|
+
def handle_blockquote(token, stack)
|
|
109
|
+
indent = token.indent
|
|
110
|
+
pop_deeper(stack, indent)
|
|
111
|
+
stack.pop while stack.size > 1 && stack.last.indent == indent && stack.last.kind != :quote
|
|
112
|
+
|
|
113
|
+
if stack.last.kind == :quote && stack.last.indent == indent
|
|
114
|
+
quote = stack.last.node
|
|
115
|
+
else
|
|
116
|
+
quote = Node::Blockquote.new
|
|
117
|
+
stack.last.node << quote
|
|
118
|
+
stack.push(Frame.new(indent: indent, node: quote, kind: :quote))
|
|
85
119
|
end
|
|
86
120
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
append_body_line(quote, token.value[:text])
|
|
122
|
+
quote
|
|
123
|
+
end
|
|
90
124
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@pos += 1
|
|
96
|
-
next
|
|
97
|
-
end
|
|
125
|
+
# 把一行正文并进 FLOW 容器的最后一个段落(软换行折叠交给 InlineParser);
|
|
126
|
+
# 上一个子节点不是段落(如嵌套块)时另起一个段落。空行不产生内容。
|
|
127
|
+
def append_body_line(node, text)
|
|
128
|
+
return if text.empty?
|
|
98
129
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
paragraph = node.children.last
|
|
131
|
+
if paragraph.is_a?(Node::Paragraph)
|
|
132
|
+
paragraph.text = "#{paragraph.text}\n#{text}"
|
|
133
|
+
else
|
|
134
|
+
node << Node::Paragraph.new(text)
|
|
103
135
|
end
|
|
136
|
+
end
|
|
104
137
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if HRULE_RE.match(line)
|
|
120
|
-
@pos += 1
|
|
121
|
-
return Hrule.new
|
|
122
|
-
end
|
|
138
|
+
# `>-` 收尾:作为 +Node::Cite+ 子节点挂到同级引用的末尾(内容策略
|
|
139
|
+
# PHRASING,正文会被行内解析,CSM §1.4)。
|
|
140
|
+
def handle_cite(token, stack)
|
|
141
|
+
indent = token.indent
|
|
142
|
+
pop_deeper(stack, indent)
|
|
143
|
+
|
|
144
|
+
if stack.last.kind == :quote && stack.last.indent == indent
|
|
145
|
+
quote = stack.last.node
|
|
146
|
+
else
|
|
147
|
+
stack.pop while stack.size > 1 && stack.last.indent == indent
|
|
148
|
+
quote = Node::Blockquote.new
|
|
149
|
+
stack.last.node << quote
|
|
150
|
+
end
|
|
123
151
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
end
|
|
152
|
+
quote << Node::Cite.new(token.value[:text])
|
|
153
|
+
end
|
|
127
154
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
155
|
+
# 列表项(内容策略 FLOW):同缩进的连续项复用同一个 +Node::List+,更深缩进则
|
|
156
|
+
# 嵌入到上一个列表项之下(CSM §1.3 的「嵌套通过缩进控制」)。正文成为段落。
|
|
157
|
+
def handle_list_item(token, stack)
|
|
158
|
+
indent = token.indent
|
|
159
|
+
pop_deeper(stack, indent)
|
|
160
|
+
stack.pop while stack.size > 1 && stack.last.indent == indent && stack.last.kind != :list
|
|
161
|
+
|
|
162
|
+
if stack.last.kind == :list && stack.last.indent == indent
|
|
163
|
+
list = stack.last.node
|
|
164
|
+
else
|
|
165
|
+
list = Node::List.new(ordered: token.value[:ordered], task: token.value[:task],
|
|
166
|
+
marker: token.value[:marker])
|
|
167
|
+
stack.last.node << list
|
|
168
|
+
stack.push(Frame.new(indent: indent, node: list, kind: :list))
|
|
169
|
+
end
|
|
133
170
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
171
|
+
list.attributes[:task] = true if token.value[:task]
|
|
172
|
+
item = Node::ListItem.new(marker: token.value[:marker], task: token.value[:task],
|
|
173
|
+
checked: token.value[:checked])
|
|
174
|
+
item << Node::Paragraph.new(token.value[:text])
|
|
175
|
+
list << item
|
|
176
|
+
stack.push(Frame.new(indent: indent, node: item, kind: :item))
|
|
177
|
+
item
|
|
178
|
+
end
|
|
137
179
|
|
|
138
|
-
|
|
180
|
+
# 定义列表(CSM §1.3):`term` 行 + 其后的 `: 定义` 行。第一个 `:` 行把前一个
|
|
181
|
+
# 同级段落提升为定义术语,再建 +Node::DefinitionList+。
|
|
182
|
+
#
|
|
183
|
+
# +dd+ 的内容策略是 FLOW,所以正文成为段落;并且它自己成为容器栈的一层——
|
|
184
|
+
# 缩进的子块因此挂在**这个** +Definition+ 下(而不是 `dl` 下)。
|
|
185
|
+
def handle_definition(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 != :deflist
|
|
189
|
+
|
|
190
|
+
if stack.last.kind == :deflist && stack.last.indent == indent
|
|
191
|
+
list = stack.last.node
|
|
192
|
+
else
|
|
193
|
+
list = promote_to_definition_list(stack.last.node)
|
|
194
|
+
stack.push(Frame.new(indent: indent, node: list, kind: :deflist))
|
|
139
195
|
end
|
|
140
196
|
|
|
141
|
-
|
|
142
|
-
|
|
197
|
+
definition = Node::Definition.new
|
|
198
|
+
definition << Node::Paragraph.new(token.value[:text])
|
|
199
|
+
list << definition
|
|
200
|
+
stack.push(Frame.new(indent: indent, node: definition, kind: :definition))
|
|
201
|
+
definition
|
|
202
|
+
end
|
|
143
203
|
|
|
144
|
-
|
|
204
|
+
# 空行终止当前块(CSM §1.1)。连续四个 `\n` 另起一个节块(`<div>`)。
|
|
205
|
+
def handle_blank(token, root, stack)
|
|
206
|
+
if token.value[:count] >= 3
|
|
207
|
+
stack.pop while stack.size > 1
|
|
208
|
+
division = Node::Div.new
|
|
209
|
+
root << division
|
|
210
|
+
stack.push(Frame.new(indent: -1, node: division, kind: :div))
|
|
211
|
+
else
|
|
212
|
+
stack.pop while stack.size > 1 && stack.last.kind != :div
|
|
145
213
|
end
|
|
214
|
+
end
|
|
146
215
|
|
|
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
|
|
216
|
+
# 叶子块:挂到「缩进严格小于自己」的最近容器下。
|
|
217
|
+
def place_leaf(token, stack, node)
|
|
218
|
+
parent = pop_to_parent(stack, token.indent)
|
|
219
|
+
parent << node
|
|
220
|
+
node
|
|
221
|
+
end
|
|
167
222
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
223
|
+
# 弹出所有「缩进不小于 +indent+」的容器,返回合适的父容器。
|
|
224
|
+
def pop_to_parent(stack, indent)
|
|
225
|
+
stack.pop while stack.size > 1 && stack.last.indent >= indent
|
|
226
|
+
stack.last.node
|
|
227
|
+
end
|
|
171
228
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Heading.new(level, children: inline_children(text))
|
|
177
|
-
end
|
|
229
|
+
# 只弹出「缩进严格大于 +indent+」的容器,保留同级的可复用容器。
|
|
230
|
+
def pop_deeper(stack, indent)
|
|
231
|
+
stack.pop while stack.size > 1 && stack.last.indent > indent
|
|
232
|
+
end
|
|
178
233
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
234
|
+
# 把容器里最后一个同级段落提升为定义术语。
|
|
235
|
+
def promote_to_definition_list(parent)
|
|
236
|
+
list = Node::DefinitionList.new
|
|
237
|
+
last = parent.children.last
|
|
238
|
+
|
|
239
|
+
if last.is_a?(Node::Paragraph)
|
|
240
|
+
list << Node::Term.new(last.text)
|
|
241
|
+
parent.replace(last, list)
|
|
242
|
+
else
|
|
243
|
+
parent << list
|
|
186
244
|
end
|
|
245
|
+
list
|
|
246
|
+
end
|
|
187
247
|
|
|
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
|
|
248
|
+
def build_heading(token)
|
|
249
|
+
Node::Heading.new(token.value[:level], token.value[:text], closed: token.value[:closed])
|
|
250
|
+
end
|
|
224
251
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
252
|
+
def build_code_block(token)
|
|
253
|
+
Node::CodeBlock.new(
|
|
254
|
+
token.value[:code],
|
|
255
|
+
lang: token.value[:lang],
|
|
256
|
+
params: token.value[:params],
|
|
257
|
+
fence: token.value[:fence]
|
|
258
|
+
)
|
|
259
|
+
end
|
|
229
260
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
261
|
+
# 表格(CSM §1.12):行 → 单元格,表头行用 +TableHeaderCell+,其余用
|
|
262
|
+
# +TableCell+;对齐逐列取自该行生效的分隔行。
|
|
263
|
+
def build_table(token)
|
|
264
|
+
Node::Table.new(children: token.value[:rows].map { |row| build_table_row(row) })
|
|
265
|
+
end
|
|
235
266
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
Paragraph.new(children: inline_children(collected.join("\n")))
|
|
243
|
-
end
|
|
267
|
+
def build_table_row(row)
|
|
268
|
+
klass = row[:header] ? Node::TableHeaderCell : Node::TableCell
|
|
269
|
+
align = row[:align] || []
|
|
270
|
+
cells = row[:cells].each_with_index.map { |text, column| klass.new(text, align: align[column]) }
|
|
271
|
+
Node::TableRow.new(children: cells)
|
|
272
|
+
end
|
|
244
273
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
274
|
+
def build_embed(token)
|
|
275
|
+
Node::RubyEmbed.new(
|
|
276
|
+
source: token.value[:source],
|
|
277
|
+
form: token.value[:form],
|
|
278
|
+
lines: token.value[:lines],
|
|
279
|
+
indent: token.value[:indent],
|
|
280
|
+
render: token.value[:render]
|
|
281
|
+
)
|
|
282
|
+
end
|
|
251
283
|
|
|
252
|
-
|
|
253
|
-
|
|
284
|
+
# 把行内容器(内容策略 PHRASING)的 +text+ 原文解析为行内节点,插到
|
|
285
|
+
# +children+ 之前。
|
|
286
|
+
#
|
|
287
|
+
# +attributes[:text]+ 保留原文(供保真回吐与调试),行内节点才是向下游交付的
|
|
288
|
+
# 结构。FLOW 容器不直接承载行内内容,所以这里不碰它们——它们的正文是段落,
|
|
289
|
+
# 段落自己会被递归解析到。
|
|
290
|
+
def resolve_inline(node)
|
|
291
|
+
node.children.each { |child| resolve_inline(child) }
|
|
292
|
+
return unless PHRASING_CONTAINERS.include?(node.type)
|
|
254
293
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return [] if text.to_s.empty?
|
|
294
|
+
text = node.attributes[:text]
|
|
295
|
+
return if text.nil? || text.empty?
|
|
258
296
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
end
|
|
297
|
+
InlineParser.parse(text).reverse_each { |inline| node.prepend(inline) }
|
|
298
|
+
end
|
|
262
299
|
|
|
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
|
|
300
|
+
# 文档末尾由空行开出的空节块没有意义,解析完清掉。
|
|
301
|
+
def drop_empty_divisions(root)
|
|
302
|
+
root.children.dup.each do |child|
|
|
303
|
+
root.remove(child) if child.is_a?(Node::Div) && child.children.empty?
|
|
277
304
|
end
|
|
278
305
|
end
|
|
279
306
|
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
307
|
end
|