mdom 0.3.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 +165 -1
- data/README.md +50 -12
- data/USAGE.md +89 -14
- data/lib/mdom/block_detector.rb +293 -60
- data/lib/mdom/builder.rb +151 -27
- data/lib/mdom/error.rb +0 -4
- data/lib/mdom/html_serializer.rb +19 -1
- data/lib/mdom/inline_parser.rb +402 -169
- data/lib/mdom/markdown_serializer.rb +69 -11
- data/lib/mdom/node.rb +9 -1
- data/lib/mdom/parser.rb +30 -2
- data/lib/mdom/tokenizer.rb +64 -21
- data/lib/mdom/translator.rb +148 -148
- data/lib/mdom/version.rb +1 -1
- metadata +5 -5
data/lib/mdom/builder.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "error"
|
|
4
|
+
require_relative "markdown_serializer"
|
|
4
5
|
require_relative "node/inline_nodes"
|
|
5
6
|
require_relative "node/nodes"
|
|
6
7
|
|
|
@@ -35,9 +36,14 @@ module MDOM
|
|
|
35
36
|
# +instance_eval+,因此不依赖 metaprog。
|
|
36
37
|
#
|
|
37
38
|
# 依赖边界(docs/IMPLEMENTATION.md §1.1):本文件属 **builder 侧**——只用 mruby
|
|
38
|
-
# 子集、不 +require+
|
|
39
|
-
#
|
|
40
|
-
#
|
|
39
|
+
# 子集、不 +require+ 任何**外部 gem**。行内内容的**结构化构造**(节点参数 / 块)
|
|
40
|
+
# 完全不需要解析器,因此既不需要 prism 也不依赖宿主的配置;只有字符串参数这一
|
|
41
|
+
# 便利形式需要行内解析器,且它是**注入**的:默认在首次需要时读取
|
|
42
|
+
# +MDOM::InlineParser+ / +MDOM::Parser+ 常量,宿主可以替换成别的实现。
|
|
43
|
+
#
|
|
44
|
+
# 块形式的行内内容会用 +MDOM::MarkdownSerializer+ 重建 +attributes[:text]+ 原文
|
|
45
|
+
# 缓存(`text` / `cite` 这类读取靠它)。它与本文件同属 builder 侧、零 gem 依赖,
|
|
46
|
+
# 所以是本文件唯一的 +require+。
|
|
41
47
|
class Builder
|
|
42
48
|
# 能直接收下行内节点的容器:内容策略为 **PHRASING** 的块
|
|
43
49
|
# (`docs/CSM.md` 元素表,与 Parser 的边界一致)。
|
|
@@ -84,13 +90,14 @@ module MDOM
|
|
|
84
90
|
|
|
85
91
|
# +document+ 省略时新建一个空文档。
|
|
86
92
|
#
|
|
87
|
-
# +inline_parser+ / +parser+
|
|
88
|
-
# +MDOM::Parser+ 常量。
|
|
89
|
-
def initialize(document: nil, inline_parser: nil, parser: nil)
|
|
93
|
+
# +inline_parser+ / +parser+ / +text_serializer+ 省略时,在首次需要时读取
|
|
94
|
+
# +MDOM::InlineParser+ / +MDOM::Parser+ / +MDOM::MarkdownSerializer+ 常量。
|
|
95
|
+
def initialize(document: nil, inline_parser: nil, parser: nil, text_serializer: nil)
|
|
90
96
|
@document = document || Node::Document.new
|
|
91
97
|
@stack = [@document]
|
|
92
98
|
@inline_parser = inline_parser
|
|
93
99
|
@parser = parser
|
|
100
|
+
@text_serializer = text_serializer
|
|
94
101
|
@lock = LOCK_CLASS&.new
|
|
95
102
|
Builder.enter(self)
|
|
96
103
|
end
|
|
@@ -145,7 +152,9 @@ module MDOM
|
|
|
145
152
|
within(node, &block)
|
|
146
153
|
else
|
|
147
154
|
item_marker = node.attributes[:marker]
|
|
148
|
-
within(node)
|
|
155
|
+
within(node) do
|
|
156
|
+
items.each { |text| item(text, marker: item_marker, task: task, checked: task ? false : nil) }
|
|
157
|
+
end
|
|
149
158
|
end
|
|
150
159
|
self
|
|
151
160
|
end
|
|
@@ -154,8 +163,15 @@ module MDOM
|
|
|
154
163
|
def ol(*items, &block) = list(*items, ordered: true, &block)
|
|
155
164
|
|
|
156
165
|
# 列表项。+parts+ 各自成为一个段落(`li` 的内容策略是 FLOW,见 CSM 元素表);
|
|
157
|
-
# 有块时在段落之后继续嵌套。省略 +marker+
|
|
158
|
-
|
|
166
|
+
# 有块时在段落之后继续嵌套。省略 +marker+ 时沿用最近一层列表的标记形式;
|
|
167
|
+
# 省略 +task+ 时沿用最近一层列表的任务状态。
|
|
168
|
+
def item(*parts, marker: nil, task: nil, checked: nil, &block)
|
|
169
|
+
list = current_list
|
|
170
|
+
task = list.task? if task.nil? && list
|
|
171
|
+
task = false if task.nil?
|
|
172
|
+
list.attributes[:task] = true if task && list
|
|
173
|
+
checked = false if task && checked.nil?
|
|
174
|
+
|
|
159
175
|
node = Node::ListItem.new(marker: marker || current_list_marker, task: task, checked: checked)
|
|
160
176
|
append(node)
|
|
161
177
|
fill_flow(node, parts, &block)
|
|
@@ -215,13 +231,15 @@ module MDOM
|
|
|
215
231
|
end
|
|
216
232
|
|
|
217
233
|
# 表头单元格(`th`)。+align+ 为 +:left+ / +:center+ / +:right+,省略为默认对齐。
|
|
218
|
-
|
|
219
|
-
|
|
234
|
+
# 内容策略是 PHRASING(CSM 元素表),因此正文与 #paragraph 一样可以给节点
|
|
235
|
+
# 参数或块——`cell` 从来不只是「一个字符串」。
|
|
236
|
+
def header_cell(*parts, align: nil, &block)
|
|
237
|
+
append_cell(Node::TableHeaderCell, parts, align, &block)
|
|
220
238
|
end
|
|
221
239
|
|
|
222
240
|
# 数据单元格(`td`)。+align+ 同上。
|
|
223
|
-
def cell(
|
|
224
|
-
append_cell(Node::TableCell,
|
|
241
|
+
def cell(*parts, align: nil, &block)
|
|
242
|
+
append_cell(Node::TableCell, parts, align, &block)
|
|
225
243
|
end
|
|
226
244
|
|
|
227
245
|
# 分隔线。
|
|
@@ -266,6 +284,10 @@ module MDOM
|
|
|
266
284
|
end
|
|
267
285
|
|
|
268
286
|
# -- 行内 -----------------------------------------------------------------
|
|
287
|
+
#
|
|
288
|
+
# 每个 CSM 行内构造都有对应的构造方法。**结构化构造**是首选形式:内容给节点
|
|
289
|
+
# 参数或块,产物因此不依赖任何解析器;字符串参数是给手写用的便利形式,它需要
|
|
290
|
+
# 一个行内解析器(注入,或 `require "mdom/inline_parser"`)。
|
|
269
291
|
|
|
270
292
|
# `*斜体*` → `<em>`。
|
|
271
293
|
def em(*parts, &block)
|
|
@@ -283,17 +305,64 @@ module MDOM
|
|
|
283
305
|
inline_node(Node::CodeSpan.new(value.to_s))
|
|
284
306
|
end
|
|
285
307
|
|
|
308
|
+
# `~~文本~~` → `<del>`。
|
|
309
|
+
def del(*parts, &block)
|
|
310
|
+
inline_node(Node::Del.new, parts, &block)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# `~+文本+~` → `<ins>`。
|
|
314
|
+
def ins(*parts, &block)
|
|
315
|
+
inline_node(Node::Ins.new, parts, &block)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# 行内纯文本。参数按字面。
|
|
319
|
+
def text(value = "")
|
|
320
|
+
inline_node(Node::Text.new(value.to_s))
|
|
321
|
+
end
|
|
322
|
+
|
|
286
323
|
# `[标签](uri "title")` → `<a>`。
|
|
287
324
|
def link(uri, *label, title: nil, &block)
|
|
288
325
|
inline_node(Node::Link.new(uri, title: title), label, &block)
|
|
289
326
|
end
|
|
290
327
|
|
|
291
|
-
#
|
|
292
|
-
def
|
|
328
|
+
# `[文本](@uri)` → `<q>`。
|
|
329
|
+
def inline_quote(uri, *parts, &block)
|
|
330
|
+
inline_node(Node::InlineQuote.new(uri), parts, &block)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# `[[target|label]]` → `<a>`。+label+ 省略时显示 +target+,行内内容也因此取
|
|
334
|
+
# +target+(与解析侧一致)。
|
|
335
|
+
def wiki_link(target, label = nil, *parts, &block)
|
|
336
|
+
literal_inline(Node::WikiLink.new(target, label: label), parts, label || target, &block)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# `@user` / `@project:x` → `<a>`。+kind+ 是 `:` 前的作用域。
|
|
340
|
+
def mention(name, kind: nil)
|
|
341
|
+
inline_node(Node::Mention.new(name.to_s, kind: kind))
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# `` → `<img>`(空元素)。+alt+ 是替代文本原文,行内内容
|
|
345
|
+
# (含 `#{}`)由 +parts+ / 块给出;省略时按**字面**解析 +alt+(CSM §2.3)。
|
|
346
|
+
def image(uri, *parts, alt: nil, title: nil, &block)
|
|
293
347
|
alt = alt&.to_s
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
348
|
+
literal_inline(Node::Image.new(uri, alt: alt, title: title), parts, alt, &block)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# `[=m/n]` → `<meter>`(CSM §1.10)。+value+ / +max+ 保持原文,行内内容省略时
|
|
352
|
+
# 按**字面**解析 `m/n`。
|
|
353
|
+
def meter(value, max, *parts, &block)
|
|
354
|
+
literal_inline(Node::Meter.new(value.to_s, max.to_s), parts, "#{value}/#{max}", &block)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# `[->m/n]` → `<progress>`。语义同 #meter。
|
|
358
|
+
def progress(value, max, *parts, &block)
|
|
359
|
+
literal_inline(Node::Progress.new(value.to_s, max.to_s), parts, "#{value}/#{max}", &block)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# `$\TeX$` → 行内 `<span class="math">`(CSM §1.11)。+tex+ 保持原文,行内内容
|
|
363
|
+
# 省略时按**字面**解析 +tex+。
|
|
364
|
+
def math(tex = "", *parts, &block)
|
|
365
|
+
literal_inline(Node::Math.new(tex.to_s), parts, tex.to_s, &block)
|
|
297
366
|
end
|
|
298
367
|
|
|
299
368
|
# 行尾 `\` → `<br>`。
|
|
@@ -301,6 +370,15 @@ module MDOM
|
|
|
301
370
|
inline_node(Node::HardBreak.new)
|
|
302
371
|
end
|
|
303
372
|
|
|
373
|
+
# `#{expr}`(CSM §3.13)。无块时构造**未求值**的插值节点,保存表达式原文
|
|
374
|
+
# (安全渲染用);有块时块的值按 #render 的分派表插入当前位置(不安全渲染用,
|
|
375
|
+
# 此时表达式由 Ruby 求值)。
|
|
376
|
+
def interpolation(expr = nil, &block)
|
|
377
|
+
return render(block.call) if block
|
|
378
|
+
|
|
379
|
+
inline_node(Node::Interpolation.new(expr.to_s))
|
|
380
|
+
end
|
|
381
|
+
|
|
304
382
|
# -- 注入通道 -------------------------------------------------------------
|
|
305
383
|
|
|
306
384
|
# 嵌入代码进入文档树的唯一通道(CSM §3.3):值挂在**当前容器**,插入位置就是
|
|
@@ -338,11 +416,11 @@ module MDOM
|
|
|
338
416
|
fill_inline(source, [cite.to_s])
|
|
339
417
|
end
|
|
340
418
|
|
|
341
|
-
#
|
|
342
|
-
def append_cell(klass,
|
|
419
|
+
# 单元格:正文是行内内容(PHRASING),填充方式与段落一致。
|
|
420
|
+
def append_cell(klass, parts, align, &block)
|
|
343
421
|
node = klass.new("", align: align)
|
|
344
422
|
append(node)
|
|
345
|
-
fill_inline(node,
|
|
423
|
+
fill_inline(node, parts, &block)
|
|
346
424
|
self
|
|
347
425
|
end
|
|
348
426
|
|
|
@@ -385,9 +463,14 @@ module MDOM
|
|
|
385
463
|
node
|
|
386
464
|
end
|
|
387
465
|
|
|
466
|
+
# 最近一层列表,没有则 +nil+。
|
|
467
|
+
def current_list
|
|
468
|
+
@stack.reverse.find { |node| node.is_a?(Node::List) }
|
|
469
|
+
end
|
|
470
|
+
|
|
388
471
|
# 最近一层列表的标记形式(有序列表得到 `N.`),没有则 `-`。
|
|
389
472
|
def current_list_marker
|
|
390
|
-
list =
|
|
473
|
+
list = current_list
|
|
391
474
|
list ? list.attributes[:marker] : "-"
|
|
392
475
|
end
|
|
393
476
|
|
|
@@ -403,17 +486,48 @@ module MDOM
|
|
|
403
486
|
node
|
|
404
487
|
end
|
|
405
488
|
|
|
406
|
-
#
|
|
489
|
+
# 填充行内内容:给块就用块(进入节点作为容器),否则填充 +parts+(节点原样
|
|
490
|
+
# 收下,字符串交给行内解析器)。
|
|
407
491
|
def fill_inline(node, parts, &block)
|
|
408
492
|
if block
|
|
409
493
|
raise ArgumentError, "cannot combine inline arguments with a block" unless parts.empty?
|
|
410
494
|
|
|
411
|
-
|
|
495
|
+
within(node, &block)
|
|
496
|
+
return cache_text(node)
|
|
412
497
|
end
|
|
413
498
|
|
|
414
499
|
fill_inline_parts(node, parts)
|
|
415
500
|
end
|
|
416
501
|
|
|
502
|
+
# 行内子节点挂好后,用(可注入的)序列化器重建 +attributes[:text]+ 原文缓存。
|
|
503
|
+
#
|
|
504
|
+
# 块形式的内容没有原文可存(内容本身就是节点),所以从子节点重建——`text` /
|
|
505
|
+
# `cite` 这类读取因此对 builder 构造的节点同样成立。只对 PHRASING 容器做:
|
|
506
|
+
# 行内格式节点(`em` / `strong` …)不需要原文缓存。
|
|
507
|
+
def cache_text(node)
|
|
508
|
+
return node unless PHRASING_CONTAINERS.include?(node.type)
|
|
509
|
+
return node unless node.attributes[:text].to_s.empty? # 解析期原文优先
|
|
510
|
+
|
|
511
|
+
serializer = text_serializer.new
|
|
512
|
+
node.attributes[:text] = node.children.map { |child| serializer.serialize(child) }.join
|
|
513
|
+
node
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# 「字段 + 行内内容」型行内节点:内容给 +parts+ / 块,否则把 +literal+ 按字面
|
|
517
|
+
# 解析成子节点(与解析侧的策略一致)。
|
|
518
|
+
def literal_inline(node, parts, literal, &block)
|
|
519
|
+
append(node)
|
|
520
|
+
return fill_inline(node, parts, &block) if block || !parts.empty?
|
|
521
|
+
|
|
522
|
+
fill_inline(node, literal_nodes(literal)) if literal
|
|
523
|
+
node
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# 一段原文的**字面**行内节点(只认 `#{}`,不认强调等;CSM §2.3 的字面策略)。
|
|
527
|
+
def literal_nodes(text)
|
|
528
|
+
inline_parser.parse(text.to_s, literal: true)
|
|
529
|
+
end
|
|
530
|
+
|
|
417
531
|
# FLOW 容器的填充(CSM 元素表:`blockquote` / `li` / `dd` 的内容策略是 FLOW):
|
|
418
532
|
# +parts+ 各自成为一个段落,随后进入块(嵌套的块级子节点)。
|
|
419
533
|
def fill_flow(node, parts, &block)
|
|
@@ -466,6 +580,12 @@ module MDOM
|
|
|
466
580
|
raise ArgumentError,
|
|
467
581
|
"render(parse: true) needs a parser: require \"mdom/parser\" or pass parser:"
|
|
468
582
|
end
|
|
583
|
+
|
|
584
|
+
# 原文缓存(块形式的行内内容)用的序列化器:默认 +MDOM::MarkdownSerializer+。
|
|
585
|
+
# 它与本文件同属 builder 侧(零 gem 依赖),所以是唯一的直接依赖。
|
|
586
|
+
def text_serializer
|
|
587
|
+
@text_serializer ||= MarkdownSerializer
|
|
588
|
+
end
|
|
469
589
|
end
|
|
470
590
|
end
|
|
471
591
|
|
|
@@ -475,12 +595,16 @@ end
|
|
|
475
595
|
# `Array`、`nil`……)。代价是给宿主的每个对象都加了 `render`——docs/IMPLEMENTATION.md
|
|
476
596
|
# §5.2 已接受这一点,并说明为什么这是唯一可行的落点。
|
|
477
597
|
class Object
|
|
478
|
-
#
|
|
479
|
-
|
|
598
|
+
# 把值渲染进当前文档;没有正在渲染的文档时抛错。
|
|
599
|
+
#
|
|
600
|
+
# 两种写法走同一条路(CSM §3.3):**给了位置参数就用它,否则用接收者本身**——
|
|
601
|
+
# 于是块内裸 `render(value)` 与 `值.render` 等价。`render(nil)` 仍然是「渲染 nil」
|
|
602
|
+
# (按 §3.3 的值分派表被忽略),多余的位置参数由 Ruby 自己报错。
|
|
603
|
+
def render(value = self, **options)
|
|
480
604
|
builder = MDOM::Builder.current
|
|
481
605
|
raise MDOM::Error, "render outside of a document" if builder.nil?
|
|
482
606
|
|
|
483
|
-
builder.render(
|
|
607
|
+
builder.render(value, **options)
|
|
484
608
|
self
|
|
485
609
|
end
|
|
486
610
|
end
|
data/lib/mdom/error.rb
CHANGED
data/lib/mdom/html_serializer.rb
CHANGED
|
@@ -123,7 +123,7 @@ module MDOM
|
|
|
123
123
|
when :blockquote then blockquote_html(node)
|
|
124
124
|
when :code_block then code_block_html(node)
|
|
125
125
|
when :math_block then tag("div", escape(node.tex, TEXT_ESCAPES), class: MATH_CLASS)
|
|
126
|
-
when :table then
|
|
126
|
+
when :table then table_html(node)
|
|
127
127
|
when :table_row then element("tr", children_html(node))
|
|
128
128
|
when :table_header_cell then cell_html(node, "th")
|
|
129
129
|
when :table_cell then cell_html(node, "td")
|
|
@@ -178,6 +178,24 @@ module MDOM
|
|
|
178
178
|
tag(name, inline_children(node), attrs)
|
|
179
179
|
end
|
|
180
180
|
|
|
181
|
+
# 表格(CSM §1.12):开头的连续表头行包进 `<thead>`,其余行包进 `<tbody>`。
|
|
182
|
+
#
|
|
183
|
+
# 分隔线可以把**相邻的任意一行**提升为表头(§1.12:`|1|2|` 也可以是标题),而
|
|
184
|
+
# HTML 只允许 `<thead>` 位于表格最前——所以出现在中途的表头行留在 `<tbody>` 里
|
|
185
|
+
# (仍输出 `<th>`),行顺序因此不被重排。零数据行时没有 `<tbody>`(与 GFM 一致)。
|
|
186
|
+
def table_html(node)
|
|
187
|
+
headers = node.children.take_while { |row| header_row?(row) }
|
|
188
|
+
bodies = node.children.drop(headers.size)
|
|
189
|
+
sections = []
|
|
190
|
+
sections << element("thead", block_statements(headers)) unless headers.empty?
|
|
191
|
+
sections << element("tbody", block_statements(bodies)) unless bodies.empty?
|
|
192
|
+
element("table", sections.join("\n"))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def header_row?(node)
|
|
196
|
+
node.type == :table_row && node.children.any? { |cell| cell.type == :table_header_cell }
|
|
197
|
+
end
|
|
198
|
+
|
|
181
199
|
def list_html(node)
|
|
182
200
|
attrs = {}
|
|
183
201
|
attrs[:task] = "true" if node.task?
|