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
|
@@ -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
|
+
# 调用,嵌入节点(RubyEmbed / 行内插值)原样注入。产出的只是源码字符串,不
|
|
12
|
+
# 绑定任何执行方式(docs/IMPLEMENTATION.md §6.2)——`eval`、落盘后 `require`、
|
|
13
|
+
# 交给 mruby 都可以。
|
|
14
|
+
#
|
|
15
|
+
# MDOM::Translator.to_ruby(MDOM::Parser.parse("# 标题\n"))
|
|
16
|
+
# # => "builder = MDOM::Builder.new\nbuilder.heading(1, \"标题\")\nbuilder.to_document"
|
|
17
|
+
#
|
|
18
|
+
# 两侧的结构约束(docs/IMPLEMENTATION.md §6.3):排版段一律经由 +builder.*+
|
|
19
|
+
# 接收者,形式可辨识;嵌入段保持原样,边界明确(Ruby 块原文 / 字符串字面量里的
|
|
20
|
+
# `#{}`)。这样宿主才有可能判断一段产物是否能在 mruby 上跑。
|
|
21
|
+
#
|
|
22
|
+
# == 安全闸门(CSM §3.14)
|
|
23
|
+
#
|
|
24
|
+
# +unsafe+ 是**翻译期**参数,默认 +false+(安全):嵌入节点不写进产物,所以
|
|
25
|
+
# 产物静态上不含用户代码——块级 `RubyEmbed` 只留一行注释占位,行内插值退化为
|
|
26
|
+
# 字面 `#{expr}`。+unsafe: true+ 才把嵌入原样注入。
|
|
27
|
+
#
|
|
28
|
+
# 安全模式下宿主不需要(也不应该)拿产物去 `eval`:未求值的嵌入由 (6) 直接吃
|
|
29
|
+
# AST1 就能按独立样式输出。求值本身只是宿主的一行 `eval`,mdom 不含求值器。
|
|
30
|
+
#
|
|
31
|
+
# 本类**不依赖 prism**:行内边界已由 InlineParser 判好,这里只做序列化。它只
|
|
32
|
+
# +require+ 节点定义(builder 侧)。
|
|
33
|
+
#
|
|
34
|
+
# == 行内内容为什么重新生成标记
|
|
35
|
+
#
|
|
36
|
+
# 行内容器(段落、标题……)在产物里以**字符串参数**交给 builder,而 builder
|
|
37
|
+
# 会用 InlineParser 重新解析这些字符串。因此本类要把 AST1 的行内子树重新序列化
|
|
38
|
+
# 成 CSM 标记文本(`**粗**`、`[标签](uri)`……)。
|
|
39
|
+
#
|
|
40
|
+
# 插值单独处理:+unsafe: true+ 时 `#{}` 在产物里是**真正的 Ruby 插值**(CSM §3.13),
|
|
41
|
+
# 由 Ruby 求值;安全模式与字面 `#{` 都写成字面。字面 `#{` 会被转义成 `\#{`。两者在 AST1 里已经分开——真插值是
|
|
42
|
+
# +Node::Interpolation+,字面的 `#{` 落在 +Node::Text+ 里——所以边界不必再扫描
|
|
43
|
+
# (CSM §3.12:配对与屏蔽交给 prism)。
|
|
44
|
+
#
|
|
45
|
+
# == 两个转义层
|
|
46
|
+
#
|
|
47
|
+
# 产物里的纯文本要过两层转义,顺序不能颠倒:
|
|
48
|
+
#
|
|
49
|
+
# 1. #escape_markup:让 InlineParser 把纯文本当**字面**(`*星*` 必须写成
|
|
50
|
+
# `\*星\*`,否则重新解析会得到紧强调);
|
|
51
|
+
# 2. #escape_ruby:把该文本编码进 Ruby 双引号字面量(`\` → `\\`、`"` → `\"`、
|
|
52
|
+
# `#` → `\#` 以免与插值混淆)。
|
|
53
|
+
#
|
|
54
|
+
# 已由节点结构提供的标记(`**`、`[`……)只过第 2 层。
|
|
55
|
+
class Translator
|
|
56
|
+
# 会被 InlineParser 当作语法开启的字符(CSM §1.5–§1.11、§2、§3.13)。纯文本
|
|
57
|
+
# 里出现它们时必须转义;`/`、`:`、`-` 等只在特定上下文里才有意义,不在表内。
|
|
58
|
+
MARKUP_CHARS = ["\\", "`", "*", "~", "[", "]", "!", "<", "$", "#", "@"].freeze
|
|
59
|
+
|
|
60
|
+
# Ruby 双引号字面量的转义表。`#` 也转义,避免 `\#{` 里的 `#` 与随后的 `{`
|
|
61
|
+
# 被 Ruby 重新拼成插值。
|
|
62
|
+
RUBY_ESCAPES = { "\\" => "\\\\", "\"" => "\\\"", "#" => "\\#", "\n" => "\\n", "\t" => "\\t" }.freeze
|
|
63
|
+
|
|
64
|
+
# 行内代码的定界符上限(CSM §1.5:最多 2 个反引号)。
|
|
65
|
+
MAX_CODE_TICKS = 2
|
|
66
|
+
|
|
67
|
+
# 没有行内内容时传给 builder 的空字符串参数。
|
|
68
|
+
EMPTY_ARGUMENT = '""'
|
|
69
|
+
|
|
70
|
+
# 安全模式下块级嵌入的占位注释(CSM §3.14)。
|
|
71
|
+
EMBED_PLACEHOLDER = "# [csm] ruby 嵌入已省略(unsafe: false)"
|
|
72
|
+
|
|
73
|
+
def self.to_ruby(node, **options)
|
|
74
|
+
new(**options).to_ruby(node)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# +indent+ 是产物每层嵌套的缩进宽度;+builder+ 是产物里 builder 变量的名字。
|
|
78
|
+
# +unsafe+ 为 +false+(默认)时嵌入节点不写进产物(CSM §3.14)。
|
|
79
|
+
def initialize(indent: 2, builder: "builder", unsafe: false)
|
|
80
|
+
@indent = " " * indent
|
|
81
|
+
@builder = builder
|
|
82
|
+
@unsafe = unsafe
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 翻译一棵树(+Node::Document+ 或任意子节点),返回 Ruby 源码字符串。
|
|
86
|
+
#
|
|
87
|
+
# 第一个 `__END__` 必须排在 `to_document` **之后**:Ruby 的 `__END__` 会截断
|
|
88
|
+
# 其后的程序文本,排在它后面的语句不会执行(CSM §3.1、§4)。
|
|
89
|
+
def to_ruby(node)
|
|
90
|
+
parts = [assignment] + statements(node, 0) + [finish]
|
|
91
|
+
marker = end_marker_of(node)
|
|
92
|
+
parts.concat(end_marker_statement(marker)) if marker
|
|
93
|
+
parts.join("\n")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def assignment = "#{@builder} = MDOM::Builder.new"
|
|
99
|
+
|
|
100
|
+
def finish = "#{@builder}.to_document"
|
|
101
|
+
|
|
102
|
+
def children_of(node)
|
|
103
|
+
node.type == :document ? node.children : [node]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def end_marker_of(node)
|
|
107
|
+
children_of(node).find { |child| child.type == :end_marker }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# -- 骨架 -----------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def statements(node, depth)
|
|
113
|
+
children_of(node).reject { |child| child.type == :end_marker }
|
|
114
|
+
.flat_map { |child| statement(child, depth) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def line(depth, text) = (@indent * depth) + text
|
|
118
|
+
|
|
119
|
+
# 逐节点分派。返回语句行数组(已含缩进)。
|
|
120
|
+
def statement(node, depth)
|
|
121
|
+
case node.type
|
|
122
|
+
when :heading then [line(depth, heading_call(node))]
|
|
123
|
+
when :paragraph then [line(depth, paragraph_call(node))]
|
|
124
|
+
when :hrule then [line(depth, "#{@builder}.hrule")]
|
|
125
|
+
when :code_block then [line(depth, code_block_call(node))]
|
|
126
|
+
when :math_block then [line(depth, math_block_call(node))]
|
|
127
|
+
when :table then block_statement(node, depth, "#{@builder}.table")
|
|
128
|
+
when :table_row then block_statement(node, depth, "#{@builder}.row")
|
|
129
|
+
when :table_header_cell then [line(depth, cell_call(node, "header_cell"))]
|
|
130
|
+
when :table_cell then [line(depth, cell_call(node, "cell"))]
|
|
131
|
+
when :term then [line(depth, term_call(node))]
|
|
132
|
+
when :cite then [line(depth, cite_call(node))]
|
|
133
|
+
when :definition then block_statement(node, depth, "#{@builder}.definition")
|
|
134
|
+
when :list_item then block_statement(node, depth, item_head(node))
|
|
135
|
+
when :blockquote then block_statement(node, depth, "#{@builder}.blockquote")
|
|
136
|
+
when :list then block_statement(node, depth, list_head(node))
|
|
137
|
+
when :definition_list then block_statement(node, depth, "#{@builder}.definition_list")
|
|
138
|
+
when :div then block_statement(node, depth, "#{@builder}.division")
|
|
139
|
+
when :ruby_embed then ruby_embed_statement(node, depth)
|
|
140
|
+
when :end_marker then end_marker_statement(node)
|
|
141
|
+
else raise Error, "cannot translate #{node.type.inspect} to Ruby"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# 块级调用:无子节点时退化为单行,有子节点时用 `do ... end` 包住。
|
|
146
|
+
# 内容策略为 FLOW 的容器只收块级子节点(CSM 元素表),这里原样递归它们。
|
|
147
|
+
def block_statement(node, depth, head)
|
|
148
|
+
body = node.children.flat_map { |child| statement(child, depth + 1) }
|
|
149
|
+
return [line(depth, head)] if body.empty?
|
|
150
|
+
|
|
151
|
+
[line(depth, "#{head} do"), *body, line(depth, "end")]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# -- 块级节点 -------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def heading_call(node)
|
|
157
|
+
args = [node.level.to_s]
|
|
158
|
+
argument = inline_argument(node)
|
|
159
|
+
args << argument if argument
|
|
160
|
+
args << "closed: true" if node.closed?
|
|
161
|
+
call(@builder, "heading", args)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# 引用来源(`>-`,PHRASING)。现在它是 Blockquote 的普通子节点。
|
|
165
|
+
def cite_call(node)
|
|
166
|
+
"#{@builder}.cite(#{inline_argument(node) || EMPTY_ARGUMENT})"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def paragraph_call(node)
|
|
170
|
+
"#{@builder}.paragraph(#{inline_argument(node) || EMPTY_ARGUMENT})"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def term_call(node)
|
|
174
|
+
"#{@builder}.term(#{inline_argument(node) || EMPTY_ARGUMENT})"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def item_head(node)
|
|
178
|
+
call(@builder, "item", item_arguments(node))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def item_arguments(node)
|
|
182
|
+
args = ["marker: #{ruby_literal(node.marker)}"]
|
|
183
|
+
args << "task: true" if node.task?
|
|
184
|
+
args << "checked: #{node.checked.inspect}" unless node.checked.nil?
|
|
185
|
+
args
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def list_head(node)
|
|
189
|
+
args = [
|
|
190
|
+
"ordered: #{node.ordered?}",
|
|
191
|
+
"task: #{node.task?}",
|
|
192
|
+
"marker: #{ruby_literal(node.attributes[:marker])}"
|
|
193
|
+
]
|
|
194
|
+
call(@builder, "list", args)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def code_block_call(node)
|
|
198
|
+
args = [ruby_literal(node.code)]
|
|
199
|
+
args << "lang: #{ruby_literal(node.lang)}" if node.lang
|
|
200
|
+
args << "params: #{ruby_literal(node.params)}" if node.params
|
|
201
|
+
fence = node.attributes[:fence]
|
|
202
|
+
args << "fence: #{fence}" unless fence.nil? || fence == 3
|
|
203
|
+
call(@builder, "code_block", args)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# 块级公式:+tex+ 按字面(CSM §1.11)。
|
|
207
|
+
def math_block_call(node)
|
|
208
|
+
"#{@builder}.math_block(#{ruby_literal(node.tex)})"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# 表格单元格:正文(PHRASING)作字符串参数,+align+ 按需带上。
|
|
212
|
+
def cell_call(node, name)
|
|
213
|
+
args = [inline_argument(node) || EMPTY_ARGUMENT]
|
|
214
|
+
args << "align: #{node.align.inspect}" if node.align
|
|
215
|
+
call(@builder, name, args)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def call(receiver, name, args)
|
|
219
|
+
args.empty? ? "#{receiver}.#{name}" : "#{receiver}.#{name}(#{args.join(", ")})"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Ruby 嵌入块:源码原文(已剥离 N 层 CSM 缩进)按所在层级重新缩进后原样注入
|
|
223
|
+
# (CSM §3.6)。`begin`/`end` 标志与 `.render` 收尾都在原文里,不再改动。
|
|
224
|
+
# 安全模式下不写源码,只留一行注释占位(CSM §3.14)。
|
|
225
|
+
def ruby_embed_statement(node, depth)
|
|
226
|
+
return [line(depth, EMBED_PLACEHOLDER)] unless @unsafe
|
|
227
|
+
|
|
228
|
+
prefix = @indent * depth
|
|
229
|
+
node.source.split("\n", -1).map { |text| text.empty? ? text : prefix + text }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# 第一个 `__END__` 原样写入,其后内容交给 Ruby 的 `DATA`(CSM §4)。
|
|
233
|
+
def end_marker_statement(node)
|
|
234
|
+
data = node.attributes[:data]
|
|
235
|
+
return ["__END__"] if data.nil? || data.empty?
|
|
236
|
+
|
|
237
|
+
["__END__", *data.split("\n", -1)]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# -- 行内内容 -------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
def inline?(node) = node.is_a?(Node::Inline)
|
|
243
|
+
|
|
244
|
+
def inline_nodes(node)
|
|
245
|
+
node.children.select { |child| inline?(child) }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# 行内内容的 Ruby 字符串字面量;没有行内内容时返回 +nil+。
|
|
249
|
+
#
|
|
250
|
+
# 兜底:以 +inline: false+ 解析出的 AST 没有行内节点,此时原文本身就是 CSM
|
|
251
|
+
# 源码,直接交给 builder 重新解析。
|
|
252
|
+
def inline_argument(node)
|
|
253
|
+
nodes = inline_nodes(node)
|
|
254
|
+
return "\"#{inline_fragments(nodes)}\"" unless nodes.empty?
|
|
255
|
+
|
|
256
|
+
text = node.attributes[:text].to_s
|
|
257
|
+
return nil if text.empty?
|
|
258
|
+
|
|
259
|
+
"\"#{escape_ruby(text)}\""
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# 行内节点序列化:返回**Ruby 字面量体**(已转义,插值保持为裸 `#{}`)。
|
|
263
|
+
#
|
|
264
|
+
# 标记(`**`、`[`……)由节点结构给出,只过 Ruby 转义;纯文本额外过一层 CSM
|
|
265
|
+
# 转义,让 InlineParser 重新解析时把它当字面。
|
|
266
|
+
def inline_fragments(nodes)
|
|
267
|
+
nodes.map { |node| inline_fragment(node) }.join
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def inline_fragment(node)
|
|
271
|
+
case node.type
|
|
272
|
+
when :text then escape_ruby(escape_markup(node.text))
|
|
273
|
+
when :interpolation then interpolation_fragment(node)
|
|
274
|
+
when :emphasis then wrap_markup(node, "*")
|
|
275
|
+
when :strong then wrap_markup(node, "**")
|
|
276
|
+
when :del then wrap_markup(node, "~~")
|
|
277
|
+
when :ins then wrap_markup(node, "~+", "+~")
|
|
278
|
+
when :code_span then code_span_fragment(node.code)
|
|
279
|
+
when :link then link_fragment(node, "")
|
|
280
|
+
when :image then link_fragment(node, "!")
|
|
281
|
+
when :inline_quote then inline_quote_fragment(node)
|
|
282
|
+
when :wiki_link then wiki_link_fragment(node)
|
|
283
|
+
when :mention then mention_fragment(node)
|
|
284
|
+
when :meter then bar_fragment(node, "[=")
|
|
285
|
+
when :progress then bar_fragment(node, "[->")
|
|
286
|
+
when :math then wrap_markup(node, "$")
|
|
287
|
+
when :hard_break then escape_ruby("\\\n")
|
|
288
|
+
else raise Error, "cannot translate inline #{node.type.inspect} to Ruby"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# 插值:不安全时是真正的 Ruby 插值(交 Ruby 求值);安全时退化为**字面**
|
|
293
|
+
# `#{expr}`——求值后它落回 CSM 源码,由 builder 重新解析成未求值的
|
|
294
|
+
# Interpolation 节点,供 (6) 按独立样式输出(CSM §3.14)。
|
|
295
|
+
def interpolation_fragment(node)
|
|
296
|
+
literal = "\#{#{node.expr}}"
|
|
297
|
+
@unsafe ? literal : escape_ruby(literal)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def wrap_markup(node, opener, closer = opener)
|
|
301
|
+
"#{escape_ruby(opener)}#{inline_fragments(node.children)}#{escape_ruby(closer)}"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# `[标签](uri "title")` / ``。
|
|
305
|
+
def link_fragment(node, sigil)
|
|
306
|
+
label = escape_ruby("#{sigil}[")
|
|
307
|
+
body = inline_fragments(node.children)
|
|
308
|
+
target = escape_ruby("](#{destination(node)})")
|
|
309
|
+
"#{label}#{body}#{target}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# `[文本](@uri)` → `<q cite="uri">`。
|
|
313
|
+
def inline_quote_fragment(node)
|
|
314
|
+
label = escape_ruby("[")
|
|
315
|
+
body = inline_fragments(node.children)
|
|
316
|
+
target = escape_ruby("](@#{node.uri})")
|
|
317
|
+
"#{label}#{body}#{target}"
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# `[[target|label]]`;省略 label 时用 +[[target]]+。
|
|
321
|
+
def wiki_link_fragment(node)
|
|
322
|
+
target = node.attributes[:target]
|
|
323
|
+
return escape_ruby("[[#{target}]]") if node.attributes[:label].nil?
|
|
324
|
+
|
|
325
|
+
"#{escape_ruby("[[#{target}|")}#{inline_fragments(node.children)}#{escape_ruby("]]")}"
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def mention_fragment(node)
|
|
329
|
+
scope = node.attributes[:kind]
|
|
330
|
+
escape_ruby("@#{scope ? "#{scope}:" : ""}#{node.attributes[:name]}")
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def bar_fragment(node, opener)
|
|
334
|
+
"#{escape_ruby(opener)}#{inline_fragments(node.children)}#{escape_ruby("]")}"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# 链接/图片的目的地(`uri "title"`)。uri 与 title 是字面,不做 CSM 转义。
|
|
338
|
+
def destination(node)
|
|
339
|
+
title = node.attributes[:title]
|
|
340
|
+
title ? "#{node.uri} \"#{title}\"" : node.uri.to_s
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# 行内代码:反引号数量要与内容里的反引号错开(CSM §1.5),内容按字面。
|
|
344
|
+
def code_span_fragment(code)
|
|
345
|
+
ticks = "`" * (code.include?("`") ? MAX_CODE_TICKS : 1)
|
|
346
|
+
escape_ruby("#{ticks}#{code}#{ticks}")
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# -- 转义 -----------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
# 第 1 层:纯文本 → 其 CSM 源码形式(给每个标记字符加 `\`)。
|
|
352
|
+
def escape_markup(text)
|
|
353
|
+
text.each_char.map { |char| MARKUP_CHARS.include?(char) ? "\\#{char}" : char }.join
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# 第 2 层:CSM 源码 → Ruby 双引号字面量的体。
|
|
357
|
+
def escape_ruby(text)
|
|
358
|
+
text.each_char.map { |char| RUBY_ESCAPES.fetch(char, char) }.join
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# 值 → Ruby 字面量(含引号)。
|
|
362
|
+
def ruby_literal(value)
|
|
363
|
+
return "nil" if value.nil?
|
|
364
|
+
|
|
365
|
+
"\"#{escape_ruby(value.to_s)}\""
|
|
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,15 +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/
|
|
7
|
+
require_relative "mdom/translator"
|
|
8
|
+
require_relative "mdom/builder"
|
|
9
|
+
require_relative "mdom/html_serializer"
|
|
10
|
+
require_relative "mdom/markdown_serializer"
|
|
9
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`。
|
|
10
20
|
module MDOM
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
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, []))
|
|
14
65
|
end
|
|
66
|
+
private_class_method :run_hooks
|
|
15
67
|
end
|
metadata
CHANGED
|
@@ -1,26 +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.3.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 parses CSM documents into a document tree (AST) and translates
|
|
33
|
+
it to Ruby source (safe by default: no embedded code is emitted) or to HTML. Markdown
|
|
34
|
+
is treated as syntactic sugar for Ruby, so a `.rb.md` document is both readable
|
|
35
|
+
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
|
|
46
|
+
- lib/mdom/builder.rb
|
|
47
|
+
- lib/mdom/error.rb
|
|
48
|
+
- lib/mdom/hooks.rb
|
|
49
|
+
- lib/mdom/html_serializer.rb
|
|
19
50
|
- lib/mdom/inline_parser.rb
|
|
51
|
+
- lib/mdom/markdown_serializer.rb
|
|
20
52
|
- lib/mdom/node.rb
|
|
21
|
-
- lib/mdom/
|
|
53
|
+
- lib/mdom/node/inline_nodes.rb
|
|
54
|
+
- lib/mdom/node/nodes.rb
|
|
22
55
|
- lib/mdom/parser.rb
|
|
23
|
-
- lib/mdom/
|
|
56
|
+
- lib/mdom/tokenizer.rb
|
|
57
|
+
- lib/mdom/translator.rb
|
|
24
58
|
- lib/mdom/version.rb
|
|
25
59
|
licenses:
|
|
26
60
|
- MIT
|
|
@@ -42,5 +76,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
42
76
|
requirements: []
|
|
43
77
|
rubygems_version: 3.6.7
|
|
44
78
|
specification_version: 4
|
|
45
|
-
summary:
|
|
79
|
+
summary: CSM (Complex Simple Markup) toolchain for Ruby.
|
|
46
80
|
test_files: []
|