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.
@@ -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 "![#{node.alt}](#{node.uri}#{title_suffix(node.title)})"
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
+ # `![alt](uri "title")` → `<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