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.
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+ 任何外部 gem。字符串参数的行内解析与 `render(parse: true)`
39
- # 的 CSM 解析都通过**依赖注入**取得:默认在首次需要时读取 +MDOM::InlineParser+ /
40
- # +MDOM::Parser+ 常量,宿主可以替换成别的实现。
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+ 省略时,在首次需要时读取 +MDOM::InlineParser+ /
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) { items.each { |text| item(text, marker: item_marker) } }
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
- def item(*parts, marker: nil, task: false, checked: nil, &block)
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
- def header_cell(text = "", align: nil)
219
- append_cell(Node::TableHeaderCell, text, align)
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(text = "", align: nil)
224
- append_cell(Node::TableCell, text, align)
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
- # `![alt](uri "title")` → `<img>`。
292
- def image(uri, alt: nil, title: nil)
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
+ # `![alt](uri "title")` → `<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
- node = Node::Image.new(uri, alt: alt || "", title: title,
295
- children: alt.nil? ? [] : inline_parser.parse(alt, literal: true))
296
- inline_node(node)
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
- # 单元格:正文按 CSM 行内语法解析(PHRASING)。
342
- def append_cell(klass, text, align)
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, [text.to_s])
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 = @stack.reverse.find { |node| node.is_a?(Node::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
- # 填充行内内容:给块就用块(进入节点作为容器),否则解析 +parts+。
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
- return within(node, &block)
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
- def render(**options)
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(self, **options)
607
+ builder.render(value, **options)
484
608
  self
485
609
  end
486
610
  end
data/lib/mdom/error.rb CHANGED
@@ -15,8 +15,4 @@ module MDOM
15
15
 
16
16
  # 块级公式的 `$$` 包围式没有找到独占一行的结束标记(扫描到文档末尾)。
17
17
  class UnclosedMathError < ParseError; end
18
-
19
- # 块里出现 heredoc。CSM §3.12 明确**不支持** heredoc(附录 E),一旦出现就报错,
20
- # 而不是把它当普通块收下。
21
- class UnsupportedHeredocError < ParseError; end
22
18
  end
@@ -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 element("table", children_html(node))
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?