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
|
@@ -0,0 +1,379 @@
|
|
|
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
|
+
# (2) AST1 → Ruby 源码(CSM §3.1;docs/IMPLEMENTATION.md §3)。
|
|
9
|
+
#
|
|
10
|
+
# 把一棵 AST1 翻译成**一段顶层顺序执行的 Ruby 程序**:排版节点变成 builder
|
|
11
|
+
# 调用,嵌入节点(块级嵌入 / 行内插值)按 +unsafe+ 决定是否原样注入。产出的
|
|
12
|
+
# 只是源码字符串,不绑定任何执行方式(docs/IMPLEMENTATION.md §6.2)——`eval`、
|
|
13
|
+
# 落盘后 `require`、交给 mruby 都可以。
|
|
14
|
+
#
|
|
15
|
+
# puts MDOM::Translator.to_ruby(MDOM::Parser.parse("# 标题\n"))
|
|
16
|
+
# # builder = MDOM::Builder.new
|
|
17
|
+
# # builder.heading(1) do
|
|
18
|
+
# # builder.text("标题")
|
|
19
|
+
# # end
|
|
20
|
+
# # builder.to_document
|
|
21
|
+
#
|
|
22
|
+
# 两侧的结构约束(docs/IMPLEMENTATION.md §6.3):排版段一律经由 +builder.*+
|
|
23
|
+
# 接收者,形式可辨识;嵌入段保持原样,边界明确(Ruby 块原文 / `interpolation`
|
|
24
|
+
# 的块)。这样宿主才有可能判断一段产物是否能在 mruby 上跑。
|
|
25
|
+
#
|
|
26
|
+
# == 行内内容结构化,不回吐标记
|
|
27
|
+
#
|
|
28
|
+
# 行内容器(段落、标题、单元格……)在产物里是**块形式**的 builder 调用,每个
|
|
29
|
+
# 行内子节点翻译成对应的 builder 行内方法:
|
|
30
|
+
#
|
|
31
|
+
# builder.paragraph do
|
|
32
|
+
# builder.text("正文 ")
|
|
33
|
+
# builder.strong do
|
|
34
|
+
# builder.text("粗")
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# 产物因此**不经过 InlineParser**:翻译器不输出 CSM 标记文本,也就没有「产物
|
|
39
|
+
# 执行时再解析一次」这一步,排版段真正落在 mruby 子集里(CSM §3.1)。唯一仍
|
|
40
|
+
# 需要行内解析器的形态是 +inline: false+ 解析出的 AST——那时树里只有原文、没有
|
|
41
|
+
# 行内节点(见 #inline_block_statement)。
|
|
42
|
+
#
|
|
43
|
+
# == 插值
|
|
44
|
+
#
|
|
45
|
+
# +unsafe: true+ 时插值是 `builder.interpolation { expr }`:表达式由 Ruby 求值,
|
|
46
|
+
# 值按 #render 的分派表插入当前位置(CSM §3.3)。+unsafe: false+(默认)时是
|
|
47
|
+
# `builder.interpolation("expr")`,构造**未求值**的插值节点(CSM §3.14),交
|
|
48
|
+
# (6) 按独立样式输出。
|
|
49
|
+
#
|
|
50
|
+
# == 安全闸门(CSM §3.14)
|
|
51
|
+
#
|
|
52
|
+
# +unsafe+ 是**翻译期**参数,默认 +false+(安全):块级嵌入不写进产物(只留一
|
|
53
|
+
# 行注释占位),插值构造未求值节点,所以产物静态上不含用户代码。+unsafe: true+
|
|
54
|
+
# 才把嵌入原样注入。
|
|
55
|
+
#
|
|
56
|
+
# 安全模式下宿主不需要(也不应该)拿产物去 `eval`:未求值的嵌入由 (6) 直接吃
|
|
57
|
+
# AST1 就能按独立样式输出。求值本身只是宿主的一行 `eval`,mdom 不含求值器。
|
|
58
|
+
#
|
|
59
|
+
# 本类**不依赖 prism**:行内边界已由 InlineParser 判好,这里只做序列化。它只
|
|
60
|
+
# +require+ 节点定义(builder 侧)。
|
|
61
|
+
#
|
|
62
|
+
# == 转义
|
|
63
|
+
#
|
|
64
|
+
# 只有一层转义(#escape_ruby):把值编码进 Ruby 双引号字面量(`\` → `\\`、
|
|
65
|
+
# `"` → `\"`、`#` → `\#`)。旧版为「让 InlineParser 把纯文本当字面」而有的
|
|
66
|
+
# 第二层 CSM 转义(CSM §3.2 的 `\*`、`` \` ``)随行内结构化一起消失——产物里
|
|
67
|
+
# 不再有任何等待重新解析的 CSM 文本。
|
|
68
|
+
class Translator
|
|
69
|
+
# Ruby 双引号字面量的转义表。`#` 也转义,避免 `\#{` 里的 `#` 与随后的 `{`
|
|
70
|
+
# 被 Ruby 重新拼成插值。
|
|
71
|
+
RUBY_ESCAPES = { "\\" => "\\\\", "\"" => "\\\"", "#" => "\\#", "\n" => "\\n", "\t" => "\\t" }.freeze
|
|
72
|
+
|
|
73
|
+
# 安全模式下块级嵌入的占位注释(CSM §3.14)。
|
|
74
|
+
EMBED_PLACEHOLDER = "# [csm] ruby 嵌入已省略(unsafe: false)"
|
|
75
|
+
|
|
76
|
+
def self.to_ruby(node, **options)
|
|
77
|
+
new(**options).to_ruby(node)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# +indent+ 是产物每层嵌套的缩进宽度;+builder+ 是产物里 builder 变量的名字。
|
|
81
|
+
# +unsafe+ 为 +false+(默认)时嵌入节点不写进产物(CSM §3.14)。
|
|
82
|
+
def initialize(indent: 2, builder: "builder", unsafe: false)
|
|
83
|
+
@indent = " " * indent
|
|
84
|
+
@builder = builder
|
|
85
|
+
@unsafe = unsafe
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 翻译一棵树(+Node::Document+ 或任意子节点),返回 Ruby 源码字符串。
|
|
89
|
+
#
|
|
90
|
+
# 第一个 `__END__` 必须排在 `to_document` **之后**:Ruby 的 `__END__` 会截断
|
|
91
|
+
# 其后的程序文本,排在它后面的语句不会执行(CSM §3.1、§4)。
|
|
92
|
+
def to_ruby(node)
|
|
93
|
+
parts = [assignment] + statements(node, 0) + [finish]
|
|
94
|
+
marker = end_marker_of(node)
|
|
95
|
+
parts.concat(end_marker_statement(marker)) if marker
|
|
96
|
+
parts.join("\n")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def assignment = "#{@builder} = MDOM::Builder.new"
|
|
102
|
+
|
|
103
|
+
def finish = "#{@builder}.to_document"
|
|
104
|
+
|
|
105
|
+
def children_of(node)
|
|
106
|
+
node.type == :document ? node.children : [node]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def end_marker_of(node)
|
|
110
|
+
children_of(node).find { |child| child.type == :end_marker }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# -- 骨架 -----------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def statements(node, depth)
|
|
116
|
+
children_of(node).reject { |child| child.type == :end_marker }
|
|
117
|
+
.flat_map { |child| statement(child, depth) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def line(depth, text) = (@indent * depth) + text
|
|
121
|
+
|
|
122
|
+
# 逐节点分派。返回语句行数组(已含缩进)。
|
|
123
|
+
def statement(node, depth)
|
|
124
|
+
case node.type
|
|
125
|
+
when :heading then inline_block_statement(node, depth, "heading", heading_arguments(node))
|
|
126
|
+
when :paragraph then inline_block_statement(node, depth, "paragraph")
|
|
127
|
+
when :hrule then [line(depth, "#{@builder}.hrule")]
|
|
128
|
+
when :code_block then [line(depth, code_block_call(node))]
|
|
129
|
+
when :math_block then [line(depth, math_block_call(node))]
|
|
130
|
+
when :table then block_statement(node, depth, "#{@builder}.table")
|
|
131
|
+
when :table_row then block_statement(node, depth, "#{@builder}.row")
|
|
132
|
+
when :table_header_cell then inline_block_statement(node, depth, "header_cell", cell_arguments(node))
|
|
133
|
+
when :table_cell then inline_block_statement(node, depth, "cell", cell_arguments(node))
|
|
134
|
+
when :term then inline_block_statement(node, depth, "term")
|
|
135
|
+
when :cite then inline_block_statement(node, depth, "cite")
|
|
136
|
+
when :definition then block_statement(node, depth, "#{@builder}.definition")
|
|
137
|
+
when :list_item then block_statement(node, depth, item_head(node))
|
|
138
|
+
when :blockquote then block_statement(node, depth, "#{@builder}.blockquote")
|
|
139
|
+
when :list then block_statement(node, depth, list_head(node))
|
|
140
|
+
when :definition_list then block_statement(node, depth, "#{@builder}.definition_list")
|
|
141
|
+
when :div then block_statement(node, depth, "#{@builder}.division")
|
|
142
|
+
when :ruby_embed then ruby_embed_statement(node, depth)
|
|
143
|
+
when :end_marker then end_marker_statement(node)
|
|
144
|
+
else raise Error, "cannot translate #{node.type.inspect} to Ruby"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# 块级调用:无子节点时退化为单行,有子节点时用 `do ... end` 包住。
|
|
149
|
+
# 内容策略为 FLOW 的容器只收块级子节点(CSM 元素表),这里原样递归它们。
|
|
150
|
+
def block_statement(node, depth, head)
|
|
151
|
+
body = node.children.flat_map { |child| statement(child, depth + 1) }
|
|
152
|
+
return [line(depth, head)] if body.empty?
|
|
153
|
+
|
|
154
|
+
[line(depth, "#{head} do"), *body, line(depth, "end")]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# -- 块级节点 -------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def heading_arguments(node)
|
|
160
|
+
args = [node.level.to_s]
|
|
161
|
+
args << "closed: true" if node.closed?
|
|
162
|
+
args
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def item_head(node)
|
|
166
|
+
call(@builder, "item", item_arguments(node))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def item_arguments(node)
|
|
170
|
+
args = ["marker: #{ruby_literal(node.marker)}"]
|
|
171
|
+
args << "task: true" if node.task?
|
|
172
|
+
# 任务列表里的普通项:必须显式写 false,否则 builder 会从列表继承 task。
|
|
173
|
+
args << "task: false" if !node.task? && node.parent.is_a?(Node::List) && node.parent.task?
|
|
174
|
+
args << "checked: #{node.checked.inspect}" unless node.checked.nil?
|
|
175
|
+
args
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def list_head(node)
|
|
179
|
+
args = [
|
|
180
|
+
"ordered: #{node.ordered?}",
|
|
181
|
+
"task: #{node.task?}",
|
|
182
|
+
"marker: #{ruby_literal(node.attributes[:marker])}"
|
|
183
|
+
]
|
|
184
|
+
call(@builder, "list", args)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def code_block_call(node)
|
|
188
|
+
args = [ruby_literal(node.code)]
|
|
189
|
+
args << "lang: #{ruby_literal(node.lang)}" if node.lang
|
|
190
|
+
args << "params: #{ruby_literal(node.params)}" if node.params
|
|
191
|
+
fence = node.attributes[:fence]
|
|
192
|
+
args << "fence: #{fence}" unless fence.nil? || fence == 3
|
|
193
|
+
call(@builder, "code_block", args)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# 块级公式:+tex+ 按字面(CSM §1.11)。
|
|
197
|
+
def math_block_call(node)
|
|
198
|
+
"#{@builder}.math_block(#{ruby_literal(node.tex)})"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# 表格单元格:正文(PHRASING)由 #inline_block_statement 填充,+align+ 按需带上。
|
|
202
|
+
def cell_arguments(node)
|
|
203
|
+
node.align ? ["align: #{node.align.inspect}"] : []
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def call(receiver, name, args)
|
|
207
|
+
args.empty? ? "#{receiver}.#{name}" : "#{receiver}.#{name}(#{args.join(", ")})"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Ruby 嵌入块:源码原文(已剥离 N 层 CSM 缩进)按所在层级重新缩进后原样注入
|
|
211
|
+
# (CSM §3.6)。`begin`/`end` 标志与 `.render` 收尾都在原文里,不再改动。
|
|
212
|
+
# 安全模式下不写源码,只留一行注释占位(CSM §3.14)。
|
|
213
|
+
def ruby_embed_statement(node, depth)
|
|
214
|
+
return [line(depth, EMBED_PLACEHOLDER)] unless @unsafe
|
|
215
|
+
|
|
216
|
+
prefix = @indent * depth
|
|
217
|
+
node.source.split("\n", -1).map { |text| text.empty? ? text : prefix + text }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# 第一个 `__END__` 原样写入,其后内容交给 Ruby 的 `DATA`(CSM §4)。
|
|
221
|
+
def end_marker_statement(node)
|
|
222
|
+
data = node.attributes[:data]
|
|
223
|
+
return ["__END__"] if data.nil? || data.empty?
|
|
224
|
+
|
|
225
|
+
["__END__", *data.split("\n", -1)]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# -- 行内内容 -------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
def inline?(node) = node.is_a?(Node::Inline)
|
|
231
|
+
|
|
232
|
+
def inline_nodes(node)
|
|
233
|
+
node.children.select { |child| inline?(child) }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# 行内容器(段落、标题、单元格……)与带行内子节点的行内节点:有行内子节点时
|
|
237
|
+
# 用 `do ... end` 逐条构造,没有时退化为单行调用。
|
|
238
|
+
#
|
|
239
|
+
# 唯一的例外是 +inline: false+ 解析出的 AST:树里只有 +attributes[:text]+ 原文,
|
|
240
|
+
# 没有行内节点。此时把原文作为**字符串参数**追加到调用上交给 builder 重新解析
|
|
241
|
+
# ——这是产物里唯一仍需要行内解析器的形态。
|
|
242
|
+
def inline_block_statement(node, depth, name, args = [])
|
|
243
|
+
nodes = inline_nodes(node)
|
|
244
|
+
return inline_block_lines(nodes, depth, call(@builder, name, args)) unless nodes.empty?
|
|
245
|
+
|
|
246
|
+
text = node.attributes[:text].to_s
|
|
247
|
+
args = text.empty? ? args : insert_text_argument(args, ruby_literal(text))
|
|
248
|
+
[line(depth, call(@builder, name, args))]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# 文本是**位置参数**,必须排在关键字参数之前(`heading(1, "x", closed: true)`)。
|
|
252
|
+
def insert_text_argument(args, literal)
|
|
253
|
+
keyword = args.index { |arg| keyword_argument?(arg) }
|
|
254
|
+
return args + [literal] if keyword.nil?
|
|
255
|
+
|
|
256
|
+
args.dup.insert(keyword, literal)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# 形如 `name: value` 的产物片段(`closed: true` / `align: :left`)。
|
|
260
|
+
def keyword_argument?(argument)
|
|
261
|
+
colon = argument.index(": ")
|
|
262
|
+
return false if colon.nil? || colon.zero?
|
|
263
|
+
|
|
264
|
+
argument[0...colon].each_char.all? do |char|
|
|
265
|
+
char == "_" || (char >= "a" && char <= "z") || (char >= "0" && char <= "9")
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def inline_block_lines(nodes, depth, head)
|
|
270
|
+
[line(depth, "#{head} do"), *inline_statements(nodes, depth + 1), line(depth, "end")]
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def inline_statements(nodes, depth)
|
|
274
|
+
nodes.flat_map { |node| inline_statement(node, depth) }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# 行内节点 → builder 行内方法(CSM 元素表里的每一种行内构造)。
|
|
278
|
+
#
|
|
279
|
+
# 顺序与 +MDOM::InlineParser+ 的分派一致:叶子单行,带内容的走块;`interpolation`
|
|
280
|
+
# 是嵌入通道,单独处理(见 #interpolation_statement)。
|
|
281
|
+
def inline_statement(node, depth)
|
|
282
|
+
case node.type
|
|
283
|
+
when :text then [line(depth, "#{@builder}.text(#{ruby_literal(node.text)})")]
|
|
284
|
+
when :code_span then [line(depth, "#{@builder}.code_span(#{ruby_literal(node.code)})")]
|
|
285
|
+
when :hard_break then [line(depth, "#{@builder}.hardbreak")]
|
|
286
|
+
when :mention then mention_statement(node, depth)
|
|
287
|
+
when :emphasis then inline_block_statement(node, depth, "em")
|
|
288
|
+
when :strong then inline_block_statement(node, depth, "strong")
|
|
289
|
+
when :del then inline_block_statement(node, depth, "del")
|
|
290
|
+
when :ins then inline_block_statement(node, depth, "ins")
|
|
291
|
+
when :link then inline_block_statement(node, depth, "link", link_arguments(node))
|
|
292
|
+
when :image then inline_block_statement(node, depth, "image", image_arguments(node))
|
|
293
|
+
when :inline_quote then inline_block_statement(node, depth, "inline_quote", [ruby_literal(node.uri)])
|
|
294
|
+
when :wiki_link then inline_block_statement(node, depth, "wiki_link", wiki_link_arguments(node))
|
|
295
|
+
when :meter then inline_block_statement(node, depth, "meter", bar_arguments(node))
|
|
296
|
+
when :progress then inline_block_statement(node, depth, "progress", bar_arguments(node))
|
|
297
|
+
when :math then inline_block_statement(node, depth, "math", [ruby_literal(node.tex)])
|
|
298
|
+
when :interpolation then interpolation_statement(node, depth)
|
|
299
|
+
else raise Error, "cannot translate inline #{node.type.inspect} to Ruby"
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# `@user` / `@project:x` → `builder.mention("user", kind: "project")`。
|
|
304
|
+
def mention_statement(node, depth)
|
|
305
|
+
args = [ruby_literal(node.name)]
|
|
306
|
+
kind = node.attributes[:kind]
|
|
307
|
+
args << "kind: #{ruby_literal(kind)}" unless kind.nil?
|
|
308
|
+
[line(depth, call(@builder, "mention", args))]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# `[标签](uri "title")` / `<uri>`。
|
|
312
|
+
def link_arguments(node)
|
|
313
|
+
args = [ruby_literal(node.uri)]
|
|
314
|
+
args << "title: #{ruby_literal(node.title)}" unless node.title.nil?
|
|
315
|
+
args
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# ``:+alt+ 是替代文本原文,显示内容由子节点给出。
|
|
319
|
+
def image_arguments(node)
|
|
320
|
+
args = [ruby_literal(node.uri), "alt: #{ruby_literal(node.alt)}"]
|
|
321
|
+
args << "title: #{ruby_literal(node.title)}" unless node.title.nil?
|
|
322
|
+
args
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# `[[target|label]]`:+label+ 省略时不传。
|
|
326
|
+
def wiki_link_arguments(node)
|
|
327
|
+
args = [ruby_literal(node.attributes[:target])]
|
|
328
|
+
label = node.attributes[:label]
|
|
329
|
+
args << ruby_literal(label) unless label.nil?
|
|
330
|
+
args
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# `[=m/n]` / `[->m/n]`:+value+ / +max+ 按字面。
|
|
334
|
+
def bar_arguments(node)
|
|
335
|
+
[ruby_literal(node.value), ruby_literal(node.max)]
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# 插值(CSM §3.13):不安全时把表达式交给 Ruby 求值(结果按 #render 的分派表
|
|
339
|
+
# 插入当前位置),安全时构造**未求值**的插值节点(CSM §3.14)。
|
|
340
|
+
def interpolation_statement(node, depth)
|
|
341
|
+
return [line(depth, "#{@builder}.interpolation(#{ruby_literal(node.expr)})")] unless @unsafe
|
|
342
|
+
|
|
343
|
+
expression_statements(node.expr, depth)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# `builder.interpolation { 表达式 }`;表达式含换行时改用 `do ... end`。
|
|
347
|
+
def expression_statements(expr, depth)
|
|
348
|
+
return [line(depth, "#{@builder}.interpolation { #{expr} }")] unless expr.include?("\n")
|
|
349
|
+
|
|
350
|
+
body = expr.split("\n", -1).map { |text| line(depth + 1, text) }
|
|
351
|
+
[line(depth, "#{@builder}.interpolation do"), *body, line(depth, "end")]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# -- 转义 -----------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
# 值 → Ruby 字面量(含引号)。
|
|
357
|
+
def ruby_literal(value)
|
|
358
|
+
return "nil" if value.nil?
|
|
359
|
+
|
|
360
|
+
"\"#{escape_ruby(value.to_s)}\""
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# CSM 源码文本(或任意值)→ Ruby 双引号字面量的体。
|
|
364
|
+
def escape_ruby(text)
|
|
365
|
+
text.each_char.map { |char| RUBY_ESCAPES.fetch(char, char) }.join
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
module MDOM
|
|
371
|
+
module Node
|
|
372
|
+
# 便捷入口:+node.to_ruby(**options)+ ≡ +MDOM::Translator.to_ruby(node, **options)+。
|
|
373
|
+
class BaseNode
|
|
374
|
+
def to_ruby(**options)
|
|
375
|
+
Translator.to_ruby(self, **options)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
data/lib/mdom/version.rb
CHANGED
data/lib/mdom.rb
CHANGED
|
@@ -1,16 +1,67 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "mdom/error"
|
|
3
4
|
require_relative "mdom/version"
|
|
4
|
-
require_relative "mdom/
|
|
5
|
-
require_relative "mdom/nodes"
|
|
5
|
+
require_relative "mdom/hooks"
|
|
6
6
|
require_relative "mdom/parser"
|
|
7
|
-
require_relative "mdom/
|
|
8
|
-
require_relative "mdom/serializer"
|
|
7
|
+
require_relative "mdom/translator"
|
|
9
8
|
require_relative "mdom/builder"
|
|
9
|
+
require_relative "mdom/html_serializer"
|
|
10
|
+
require_relative "mdom/markdown_serializer"
|
|
10
11
|
|
|
12
|
+
# CSM(Complex Simple Markup)的 Ruby 工具链。
|
|
13
|
+
#
|
|
14
|
+
# `require "mdom"` 是**总入口**,会把两侧都加载进来(解析侧需要 `prism`)。只想在
|
|
15
|
+
# mruby/WASM 上构造文档时,直接 `require "mdom/builder"` 即可——那一侧零 gem 依赖。
|
|
16
|
+
#
|
|
17
|
+
# 六个转换关系见 `docs/CSM.md` §6;这里接的是其中已经实现的四段:
|
|
18
|
+
# (1) 解析、(2) 翻译、(3)(5) `to_markdown`、(6) HTML 序列化。(4) 执行不在 mdom
|
|
19
|
+
# 范围内——它就是宿主的一行 `eval`。
|
|
11
20
|
module MDOM
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
# 一次 #compile 的结果。
|
|
22
|
+
#
|
|
23
|
+
# 三个阶段的产物都在这里;钩子改写过的版本就是这里看到的版本。
|
|
24
|
+
Compiled = Struct.new(:document, :ruby, :html, :meta, keyword_init: true)
|
|
25
|
+
|
|
26
|
+
# (1):CSM 源码 → AST1。等价于 +MDOM::Parser.parse+。
|
|
27
|
+
def self.parse(source, **options)
|
|
28
|
+
Parser.parse(source, **options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# (1)(2)(6) 全链路,并在五个点上触发钩子(CSM §5.3)。
|
|
32
|
+
#
|
|
33
|
+
# 顺序:
|
|
34
|
+
#
|
|
35
|
+
# 1. `Parser.parse` → 触发 `:after_parse(doc)`
|
|
36
|
+
# 2. 触发 **`:before_compile(doc)`** —— 此时树仍可改写,程序化生成目录就放这里
|
|
37
|
+
# 3. `Translator.to_ruby` → 触发 `:after_compile(doc, ruby)`
|
|
38
|
+
# 4. 触发 `:before_serialize(doc)`
|
|
39
|
+
# 5. `HtmlSerializer.serialize` → 触发 `:after_serialize(html, meta)`
|
|
40
|
+
#
|
|
41
|
+
# 参数:
|
|
42
|
+
# * +hooks:+ —— 本次调用专用的临时钩子,形如 +{ before_compile: [proc] }+;
|
|
43
|
+
# 与类级注册表里的钩子一起执行,顺序是「全局 → 临时」;
|
|
44
|
+
# * +parser:+ / +translator:+ / +serializer:+ —— 分别透传给三个阶段。
|
|
45
|
+
#
|
|
46
|
+
# 返回值:+MDOM::Compiled+(含 +document+ / +ruby+ / +html+ / +meta+)。
|
|
47
|
+
def self.compile(source, hooks: {}, parser: {}, translator: {}, serializer: {})
|
|
48
|
+
document = Parser.parse(source, **parser)
|
|
49
|
+
run_hooks(:after_parse, hooks, document)
|
|
50
|
+
|
|
51
|
+
run_hooks(:before_compile, hooks, document)
|
|
52
|
+
ruby = Translator.to_ruby(document, **translator)
|
|
53
|
+
run_hooks(:after_compile, hooks, document, ruby)
|
|
54
|
+
|
|
55
|
+
run_hooks(:before_serialize, hooks, document)
|
|
56
|
+
html = HtmlSerializer.serialize(document, **serializer)
|
|
57
|
+
run_hooks(:after_serialize, hooks, html, document.meta)
|
|
58
|
+
|
|
59
|
+
Compiled.new(document: document, ruby: ruby, html: html, meta: document.meta)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# 触发一个钩子点:先全局注册表,再本次调用传入的临时钩子。
|
|
63
|
+
def self.run_hooks(point, hooks, *args)
|
|
64
|
+
Hooks.run(point, *args, extra: hooks.fetch(point, []))
|
|
15
65
|
end
|
|
66
|
+
private_class_method :run_hooks
|
|
16
67
|
end
|
metadata
CHANGED
|
@@ -1,27 +1,60 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mdom
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- mdom
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: prism
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.19'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '2.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '0.19'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '2.0'
|
|
32
|
+
description: MDOM is the Ruby toolchain for CSM (Complex Simple Markup). It parses
|
|
33
|
+
CSM documents into an AST, translates them to safe-by-default Ruby source, re-serializes
|
|
34
|
+
them to CSM, and renders HTML. CSM treats Markdown as syntactic sugar for Ruby,
|
|
35
|
+
so a `.rb.md` document is both readable Markdown and an executable Ruby program.
|
|
14
36
|
executables: []
|
|
15
37
|
extensions: []
|
|
16
38
|
extra_rdoc_files: []
|
|
17
39
|
files:
|
|
40
|
+
- CHANGELOG.md
|
|
41
|
+
- LICENSE.txt
|
|
42
|
+
- README.md
|
|
43
|
+
- USAGE.md
|
|
18
44
|
- lib/mdom.rb
|
|
45
|
+
- lib/mdom/block_detector.rb
|
|
19
46
|
- lib/mdom/builder.rb
|
|
47
|
+
- lib/mdom/error.rb
|
|
48
|
+
- lib/mdom/hooks.rb
|
|
49
|
+
- lib/mdom/html_serializer.rb
|
|
20
50
|
- lib/mdom/inline_parser.rb
|
|
51
|
+
- lib/mdom/markdown_serializer.rb
|
|
21
52
|
- lib/mdom/node.rb
|
|
22
|
-
- lib/mdom/
|
|
53
|
+
- lib/mdom/node/inline_nodes.rb
|
|
54
|
+
- lib/mdom/node/nodes.rb
|
|
23
55
|
- lib/mdom/parser.rb
|
|
24
|
-
- lib/mdom/
|
|
56
|
+
- lib/mdom/tokenizer.rb
|
|
57
|
+
- lib/mdom/translator.rb
|
|
25
58
|
- lib/mdom/version.rb
|
|
26
59
|
licenses:
|
|
27
60
|
- MIT
|
|
@@ -43,5 +76,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
43
76
|
requirements: []
|
|
44
77
|
rubygems_version: 3.6.7
|
|
45
78
|
specification_version: 4
|
|
46
|
-
summary:
|
|
79
|
+
summary: CSM (Complex Simple Markup) toolchain for Ruby.
|
|
47
80
|
test_files: []
|