mdom 0.2.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 +378 -163
- 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 +58 -7
- metadata +40 -7
- data/lib/mdom/nodes.rb +0 -214
- data/lib/mdom/serializer.rb +0 -165
|
@@ -0,0 +1,332 @@
|
|
|
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
|
+
# (3)/(5) AST → CSM 源(CSM §6;docs/IMPLEMENTATION.md §6)。
|
|
9
|
+
#
|
|
10
|
+
# 同一个方法服务两种输入状态:
|
|
11
|
+
#
|
|
12
|
+
# | 输入 | 嵌入节点 | 输出 | 保真 |
|
|
13
|
+
# |------|----------|------|------|
|
|
14
|
+
# | AST1 | 源码原文 | 原样回吐源码 | **保真往返** |
|
|
15
|
+
# | AST2 | 已被结果替换 | 只有结果 | **降级**(无法还原成 Ruby) |
|
|
16
|
+
#
|
|
17
|
+
# MDOM::MarkdownSerializer.serialize(MDOM::Parser.parse("# 标题\n"))
|
|
18
|
+
# # => "# 标题"
|
|
19
|
+
#
|
|
20
|
+
# 序列化**从节点结构重建**,不读 +attributes[:text]+ 那类原文缓存——树被改过之后
|
|
21
|
+
# 输出反映改动。唯一的例外是嵌入节点:它本来就承载 +source+ 原文,AST1 因此保真。
|
|
22
|
+
#
|
|
23
|
+
# == 缩进
|
|
24
|
+
#
|
|
25
|
+
# 输出统一用 4 个空格作 1 个缩进单位(CSM §1.1:1 个制表符或 4 个空格)。嵌入块
|
|
26
|
+
# 在 AST 里只存「已剥离块首缩进」的字符串(CSM §3.6),回吐时按**节点在树中的
|
|
27
|
+
# 深度**重新缩进——层级由树位置决定,不由字符串承载。
|
|
28
|
+
#
|
|
29
|
+
# == 转义
|
|
30
|
+
#
|
|
31
|
+
# 行内文本采取**保守转义**:凡是可能重新触发解析的字符(`\` `` ` `` `*` `~` `[`
|
|
32
|
+
# `]` `$`)以及插值起始的 `#{` 一律转义(CSM §3.11 的 `\` 规则)。代价是输出比
|
|
33
|
+
# 手写的 CSM 更谨慎,换来的是「不会被自己的内容重新解析」这一稳定性质。
|
|
34
|
+
#
|
|
35
|
+
# == 依赖边界
|
|
36
|
+
#
|
|
37
|
+
# 本文件属 **builder 侧**(docs/IMPLEMENTATION.md §1.1):零 gem 依赖、不
|
|
38
|
+
# `require` prism、不使用正则字面量,因此 mruby / WASM 也能跑。
|
|
39
|
+
class MarkdownSerializer
|
|
40
|
+
# 一个缩进单位(CSM §1.1)。
|
|
41
|
+
INDENT = " "
|
|
42
|
+
|
|
43
|
+
# 行内文本的单字符转义表(保守策略)。
|
|
44
|
+
TEXT_ESCAPES = {
|
|
45
|
+
"\\" => "\\\\",
|
|
46
|
+
"`" => "\\`",
|
|
47
|
+
"*" => "\\*",
|
|
48
|
+
"~" => "\\~",
|
|
49
|
+
"[" => "\\[",
|
|
50
|
+
"]" => "\\]",
|
|
51
|
+
"$" => "\\$"
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# 分隔线(CSM §1.6:3–72 个 `-`)。
|
|
55
|
+
HRULE = "---"
|
|
56
|
+
|
|
57
|
+
# 行内节点的类型名,用来区分 #serialize 收到的是一棵行内子树还是块。
|
|
58
|
+
INLINE_TYPES = %i[
|
|
59
|
+
text emphasis strong code_span link inline_quote image wiki_link mention
|
|
60
|
+
del ins hard_break meter progress math interpolation
|
|
61
|
+
].freeze
|
|
62
|
+
|
|
63
|
+
def self.serialize(node)
|
|
64
|
+
new.serialize(node)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 序列化一棵树(+Node::Document+ / 任意块级节点 / 任意行内节点),返回 CSM 源码。
|
|
68
|
+
# 块级输出**不带**末尾换行。
|
|
69
|
+
def serialize(node)
|
|
70
|
+
case node.type
|
|
71
|
+
when :document then blocks(node.children, 0).join("\n")
|
|
72
|
+
when *INLINE_TYPES then inline(node)
|
|
73
|
+
else block(node, 0).join("\n")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# -- 块级 -----------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
# 一串同级块,行数组。块之间空一行;**节块**(+Div+,CSM §1.1 的 `\n\n\n\n`)
|
|
82
|
+
# 与邻居之间空三行,这样往返时 +Div+ 还能重建出来。
|
|
83
|
+
def blocks(children, depth)
|
|
84
|
+
buffer = []
|
|
85
|
+
previous = nil
|
|
86
|
+
children.each do |child|
|
|
87
|
+
lines = block(child, depth)
|
|
88
|
+
next if lines.empty?
|
|
89
|
+
|
|
90
|
+
buffer.concat(gap_for(previous, child.type)) if previous
|
|
91
|
+
buffer.concat(lines)
|
|
92
|
+
previous = child.type
|
|
93
|
+
end
|
|
94
|
+
buffer
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def gap_for(previous, current)
|
|
98
|
+
previous == :div || current == :div ? ["", "", ""] : [""]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# 单个块 → 行数组(每行已含 +depth+ 级缩进)。
|
|
102
|
+
def block(node, depth)
|
|
103
|
+
pad = INDENT * depth
|
|
104
|
+
case node.type
|
|
105
|
+
when :paragraph then indented_text(pad, inline_children(node))
|
|
106
|
+
when :heading then ["#{pad}#{heading_text(node)}"]
|
|
107
|
+
when :hrule then ["#{pad}#{HRULE}"]
|
|
108
|
+
when :code_block then code_block(node, pad)
|
|
109
|
+
when :math_block then math_block(node, pad)
|
|
110
|
+
when :list then list(node, depth)
|
|
111
|
+
when :definition_list then definition_list(node, depth)
|
|
112
|
+
when :blockquote then blockquote(node, depth)
|
|
113
|
+
when :table then table(node, pad)
|
|
114
|
+
when :div then blocks(node.children, depth)
|
|
115
|
+
when :ruby_embed then embed(node, pad)
|
|
116
|
+
when :end_marker then end_marker(node)
|
|
117
|
+
else indented_text(pad, inline_children(node))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# 段落内容可能因 #HardBreak 而断开,所以按行铺开。
|
|
122
|
+
def indented_text(pad, content)
|
|
123
|
+
"#{pad}#{content}".split("\n", -1)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def heading_text(node)
|
|
127
|
+
hashes = "#" * node.level.to_i
|
|
128
|
+
line = "#{hashes} #{inline_children(node)}"
|
|
129
|
+
node.closed? ? "#{line} #{hashes}" : line
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def code_block(node, pad)
|
|
133
|
+
fence = "`" * (node.fence || 3).to_i
|
|
134
|
+
header = [node.lang, node.params].compact.reject(&:empty?).join(" ")
|
|
135
|
+
opening = header.empty? ? fence : "#{fence}#{header}"
|
|
136
|
+
body = node.code.to_s.split("\n", -1).map { |line| line.empty? ? "" : "#{pad}#{line}" }
|
|
137
|
+
["#{pad}#{opening}", *body, "#{pad}#{fence}"]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# 块级公式:单行形式(`$$…$$`)能装下就用单行,否则退回 `$$` 独占一行的包围式。
|
|
141
|
+
def math_block(node, pad)
|
|
142
|
+
tex = node.tex.to_s
|
|
143
|
+
return ["#{pad}$$#{tex}$$"] unless tex.include?("\n")
|
|
144
|
+
|
|
145
|
+
["#{pad}$$", *tex.split("\n", -1).map { |line| line.empty? ? "" : "#{pad}#{line}" }, "#{pad}$$"]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# -- 列表 -----------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def list(node, depth)
|
|
151
|
+
node.children.flat_map.with_index do |item, index|
|
|
152
|
+
list_item(item, depth, marker_for(node, item, index))
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def marker_for(list_node, item, index)
|
|
157
|
+
return "#{index + 1}. " if list_node.ordered?
|
|
158
|
+
return "- [#{item.checked ? "x" : " "}] " if list_node.task?
|
|
159
|
+
|
|
160
|
+
"#{item.marker || "-"} "
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# 列表项的首个子块接在标记后面,其余子块整块缩进一级。
|
|
164
|
+
def list_item(item, depth, marker)
|
|
165
|
+
pad = INDENT * depth
|
|
166
|
+
body = item.children.flat_map { |child| block(child, depth + 1) }
|
|
167
|
+
return ["#{pad}#{marker.strip}"] if body.empty?
|
|
168
|
+
|
|
169
|
+
["#{pad}#{marker}#{body.first.strip}", *body.drop(1)]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def definition_list(node, depth)
|
|
173
|
+
pad = INDENT * depth
|
|
174
|
+
node.children.flat_map do |child|
|
|
175
|
+
if child.type == :term
|
|
176
|
+
["#{pad}#{inline_children(child)}"]
|
|
177
|
+
else
|
|
178
|
+
body = child.children.flat_map { |grandchild| block(grandchild, depth) }
|
|
179
|
+
first = body.first || ""
|
|
180
|
+
["#{pad}: #{first.strip}", *body.drop(1)]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# -- 引用 -----------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
# 正文各行加 `> ` 前缀;最后一行以 `>-` 收尾时提取为 `cite`(CSM §1.4)。
|
|
188
|
+
def blockquote(node, depth)
|
|
189
|
+
pad = INDENT * depth
|
|
190
|
+
body = node.children.reject { |child| child.type == :cite }
|
|
191
|
+
quoted = body.flat_map { |child| block(child, depth) }.map { |line| quote(line, pad) }
|
|
192
|
+
cite = node.children.find { |child| child.type == :cite }
|
|
193
|
+
quoted << "#{pad}>- #{inline_children(cite)}" if cite
|
|
194
|
+
quoted
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# 空行保留成 `>`,其余行剥掉公共缩进后加 `> `(不重复缩进)。
|
|
198
|
+
def quote(line, pad)
|
|
199
|
+
rest = line.start_with?(pad) ? line[pad.length..] : line
|
|
200
|
+
rest.empty? ? "#{pad}>" : "#{pad}> #{rest}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# -- 表格 -----------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def table(node, pad)
|
|
206
|
+
rows = node.children.reject { |row| row.children.empty? }
|
|
207
|
+
return [] if rows.empty?
|
|
208
|
+
|
|
209
|
+
lines = [table_row(rows.first, pad)]
|
|
210
|
+
lines << separator_row(rows.first, pad) if header_row?(rows.first)
|
|
211
|
+
rows.drop(1).each { |row| lines << table_row(row, pad) }
|
|
212
|
+
lines
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def header_row?(row)
|
|
216
|
+
row.children.all? { |cell| cell.type == :table_header_cell }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def table_row(row, pad)
|
|
220
|
+
cells = row.children.map { |cell| " #{inline_children(cell)} " }
|
|
221
|
+
"#{pad}|#{cells.join("|")}|"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def separator_row(row, pad)
|
|
225
|
+
cells = row.children.map { |cell| separator_cell(cell.align) }
|
|
226
|
+
"#{pad}|#{cells.join("|")}|"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def separator_cell(align)
|
|
230
|
+
case align
|
|
231
|
+
when :left then ":--"
|
|
232
|
+
when :right then "--:"
|
|
233
|
+
when :center then ":-:"
|
|
234
|
+
else "---"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# -- 嵌入与元数据 ---------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
# 回吐嵌入源码(AST1 保真);按节点深度重新缩进(CSM §3.6)。`~` 形式每行都
|
|
241
|
+
# 要重新加 `~ ` 前缀(CSM §3.4)。
|
|
242
|
+
def embed(node, pad)
|
|
243
|
+
lines = node.source.to_s.split("\n", -1)
|
|
244
|
+
return lines.map { |line| line.empty? ? "" : "#{pad}~ #{line}" } if node.form == :tilde
|
|
245
|
+
|
|
246
|
+
lines.map { |line| line.empty? ? "" : "#{pad}#{line}" }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def end_marker(node)
|
|
250
|
+
data = node.data.to_s
|
|
251
|
+
return ["__END__"] if data.empty?
|
|
252
|
+
|
|
253
|
+
["__END__", *data.split("\n", -1)]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# -- 行内 -----------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
def inline_children(node)
|
|
259
|
+
node.children.map { |child| inline(child) }.join
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def inline(node)
|
|
263
|
+
case node.type
|
|
264
|
+
when :text then escape_text(node.text)
|
|
265
|
+
when :emphasis then "*#{inline_children(node)}*"
|
|
266
|
+
when :strong then "**#{inline_children(node)}**"
|
|
267
|
+
when :code_span then code_span(node)
|
|
268
|
+
when :link then "[#{inline_children(node)}](#{node.uri}#{title_suffix(node.title)})"
|
|
269
|
+
when :inline_quote then "[#{inline_children(node)}](@#{node.uri})"
|
|
270
|
+
when :image then "})"
|
|
271
|
+
when :wiki_link then wiki_link(node)
|
|
272
|
+
when :mention then mention(node)
|
|
273
|
+
when :del then "~~#{inline_children(node)}~~"
|
|
274
|
+
when :ins then "~+#{inline_children(node)}+~"
|
|
275
|
+
when :hard_break then "\\\n"
|
|
276
|
+
when :meter then "[=#{node.value}/#{node.max}]"
|
|
277
|
+
when :progress then "[->#{node.value}/#{node.max}]"
|
|
278
|
+
when :math then "$#{node.tex}$"
|
|
279
|
+
when :interpolation then "\#{#{node.expr}}"
|
|
280
|
+
else ""
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def code_span(node)
|
|
285
|
+
code = node.code.to_s
|
|
286
|
+
fence = code.include?("`") ? "``" : "`"
|
|
287
|
+
"#{fence}#{code}#{fence}"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def wiki_link(node)
|
|
291
|
+
label = node.label
|
|
292
|
+
return "[[#{node.target}]]" if label.nil? || label.to_s.empty?
|
|
293
|
+
|
|
294
|
+
"[[#{node.target}|#{label}]]"
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def mention(node)
|
|
298
|
+
kind = node.kind
|
|
299
|
+
kind.nil? || kind.to_s.empty? ? "@#{node.name}" : "@#{kind}:#{node.name}"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# `[文本](uri "title")` 的可选 title 部分。
|
|
303
|
+
def title_suffix(title)
|
|
304
|
+
title.nil? || title.to_s.empty? ? "" : " \"#{title}\""
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# 保守转义:单字符查表 + `#{` 的起始 `#`(CSM §3.11、§3.13)。
|
|
308
|
+
def escape_text(text)
|
|
309
|
+
characters = text.to_s.chars
|
|
310
|
+
out = +""
|
|
311
|
+
characters.each_with_index do |char, index|
|
|
312
|
+
out << if char == "#" && characters[index + 1] == "{"
|
|
313
|
+
"\\#"
|
|
314
|
+
else
|
|
315
|
+
TEXT_ESCAPES[char] || char
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
out
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
module MDOM
|
|
324
|
+
module Node
|
|
325
|
+
# 便捷入口:+node.to_markdown+ ≡ +MDOM::MarkdownSerializer.serialize(node)+。
|
|
326
|
+
class BaseNode
|
|
327
|
+
def to_markdown
|
|
328
|
+
MarkdownSerializer.serialize(self)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../node"
|
|
4
|
+
|
|
5
|
+
# 行内节点:由 +InlineParser+ 从一段行内文本产出,只出现在行内容器
|
|
6
|
+
# (段落、标题、列表项、引用、定义……)的 +children+ 里。
|
|
7
|
+
#
|
|
8
|
+
# 与块级节点一样,每个类只固定自己的 +type+ 与语义属性。产出的是**未求值**的
|
|
9
|
+
# 节点:`#{}` 只保存表达式原文,公式只保存 TeX 原文。
|
|
10
|
+
module MDOM
|
|
11
|
+
module Node
|
|
12
|
+
# 行内节点的标记模块,便于与块级节点区分。
|
|
13
|
+
module Inline; end
|
|
14
|
+
|
|
15
|
+
# 行内纯文本。
|
|
16
|
+
class Text < BaseNode
|
|
17
|
+
include Inline
|
|
18
|
+
|
|
19
|
+
def initialize(text = "", attributes: {}, children: [])
|
|
20
|
+
super(:text, attributes: { text: text }.merge(attributes), children: children)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def text
|
|
24
|
+
attributes[:text]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def text=(value)
|
|
28
|
+
attributes[:text] = value
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# `*文本*` → `<em>`。
|
|
33
|
+
class Emphasis < BaseNode
|
|
34
|
+
include Inline
|
|
35
|
+
|
|
36
|
+
def initialize(children: [], attributes: {})
|
|
37
|
+
super(:emphasis, attributes: attributes, children: children)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# `**文本**` → `<strong>`。
|
|
42
|
+
class Strong < BaseNode
|
|
43
|
+
include Inline
|
|
44
|
+
|
|
45
|
+
def initialize(children: [], attributes: {})
|
|
46
|
+
super(:strong, attributes: attributes, children: children)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# `` `代码` `` → `<code>`。内容按字面处理,不再解析。
|
|
51
|
+
class CodeSpan < BaseNode
|
|
52
|
+
include Inline
|
|
53
|
+
|
|
54
|
+
def initialize(code = "", attributes: {}, children: [])
|
|
55
|
+
super(:code_span, attributes: { code: code }.merge(attributes), children: children)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def code
|
|
59
|
+
attributes[:code]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# `[文本](uri "title")`、`<uri>` → `<a>`。
|
|
64
|
+
class Link < BaseNode
|
|
65
|
+
include Inline
|
|
66
|
+
|
|
67
|
+
def initialize(uri, title: nil, children: [], attributes: {})
|
|
68
|
+
super(:link, attributes: { uri: uri, title: title }.merge(attributes), children: children)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def uri
|
|
72
|
+
attributes[:uri]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def title
|
|
76
|
+
attributes[:title]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# `[文本](@uri)` → `<q cite="uri">`。
|
|
81
|
+
class InlineQuote < BaseNode
|
|
82
|
+
include Inline
|
|
83
|
+
|
|
84
|
+
def initialize(uri, children: [], attributes: {})
|
|
85
|
+
super(:inline_quote, attributes: { uri: uri }.merge(attributes), children: children)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def uri
|
|
89
|
+
attributes[:uri]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# `` → `<img>`(空元素)。+alt+ 保持原文,+children+ 是它
|
|
94
|
+
# 解析出的行内节点(含 `#{}` 插值)——`<img>` 没有子内容,这里只承载替代文本。
|
|
95
|
+
class Image < BaseNode
|
|
96
|
+
include Inline
|
|
97
|
+
|
|
98
|
+
def initialize(uri, alt: "", title: nil, children: [], attributes: {})
|
|
99
|
+
super(:image, attributes: { uri: uri, alt: alt, title: title }.merge(attributes), children: children)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def uri
|
|
103
|
+
attributes[:uri]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def alt
|
|
107
|
+
attributes[:alt]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def title
|
|
111
|
+
attributes[:title]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `[[target|label]]` → `<a>`。+label+ 省略时用 +target+ 作显示文本。
|
|
116
|
+
# +children+ 是显示文本解析出的行内节点(含 `#{}` 插值)。
|
|
117
|
+
class WikiLink < BaseNode
|
|
118
|
+
include Inline
|
|
119
|
+
|
|
120
|
+
def initialize(target, label: nil, children: [], attributes: {})
|
|
121
|
+
super(:wiki_link, attributes: { target: target, label: label }.merge(attributes), children: children)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def target
|
|
125
|
+
attributes[:target]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def label
|
|
129
|
+
attributes[:label]
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# `@user` / `@project:x` / `@tag:kw` → `<a>`。+kind+ 是 `:` 前的作用域。
|
|
134
|
+
class Mention < BaseNode
|
|
135
|
+
include Inline
|
|
136
|
+
|
|
137
|
+
def initialize(name, kind: nil, attributes: {})
|
|
138
|
+
super(:mention, attributes: { name: name, kind: kind }.merge(attributes))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def name
|
|
142
|
+
attributes[:name]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def kind
|
|
146
|
+
attributes[:kind]
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# `~~文本~~` → `<del>`。
|
|
151
|
+
class Del < BaseNode
|
|
152
|
+
include Inline
|
|
153
|
+
|
|
154
|
+
def initialize(children: [], attributes: {})
|
|
155
|
+
super(:del, attributes: attributes, children: children)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# `~+文本+~` → `<ins>`。
|
|
160
|
+
class Ins < BaseNode
|
|
161
|
+
include Inline
|
|
162
|
+
|
|
163
|
+
def initialize(children: [], attributes: {})
|
|
164
|
+
super(:ins, attributes: attributes, children: children)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# 行尾 `\` → `<br>`(空元素)。
|
|
169
|
+
class HardBreak < BaseNode
|
|
170
|
+
include Inline
|
|
171
|
+
|
|
172
|
+
def initialize(attributes: {})
|
|
173
|
+
super(:hard_break, attributes: attributes)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# `[=m/n]` → `<meter value="m" max="n">`(空元素)。+value+ / +max+ 保持原文,
|
|
178
|
+
# +children+ 是 `m/n` 解析出的行内节点(含 `#{}` 插值)。
|
|
179
|
+
class Meter < BaseNode
|
|
180
|
+
include Inline
|
|
181
|
+
|
|
182
|
+
def initialize(value, max, children: [], attributes: {})
|
|
183
|
+
super(:meter, attributes: { value: value, max: max }.merge(attributes), children: children)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def value
|
|
187
|
+
attributes[:value]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def max
|
|
191
|
+
attributes[:max]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# `[->m/n]` → `<progress value="m" max="n">`(空元素)。语义同 +Meter+。
|
|
196
|
+
class Progress < BaseNode
|
|
197
|
+
include Inline
|
|
198
|
+
|
|
199
|
+
def initialize(value, max, children: [], attributes: {})
|
|
200
|
+
super(:progress, attributes: { value: value, max: max }.merge(attributes), children: children)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def value
|
|
204
|
+
attributes[:value]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def max
|
|
208
|
+
attributes[:max]
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# `$\TeX$` → 行内 `<span class="math">`(TeX 原文,不渲染成 SVG)。+tex+ 保持
|
|
213
|
+
# 原文,+children+ 是 TeX 解析出的行内节点(含 `#{}` 插值)。
|
|
214
|
+
class Math < BaseNode
|
|
215
|
+
include Inline
|
|
216
|
+
|
|
217
|
+
def initialize(tex = "", children: [], attributes: {})
|
|
218
|
+
super(:math, attributes: { tex: tex }.merge(attributes), children: children)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def tex
|
|
222
|
+
attributes[:tex]
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# `#{expr}`:交给 Ruby 求值,此处只保存表达式原文、不求值。
|
|
227
|
+
class Interpolation < BaseNode
|
|
228
|
+
include Inline
|
|
229
|
+
|
|
230
|
+
def initialize(expr = "", attributes: {}, children: [])
|
|
231
|
+
super(:interpolation, attributes: { expr: expr }.merge(attributes), children: children)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def expr
|
|
235
|
+
attributes[:expr]
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|