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.
- 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 +378 -163
- 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 +58 -7
- metadata +40 -7
- data/lib/mdom/nodes.rb +0 -214
- data/lib/mdom/serializer.rb +0 -165
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c46bd566d0a5adabb0b78754b949f0bed6dfa1bf1e1dd09b03a0d3f36ab1e63
|
|
4
|
+
data.tar.gz: 563befb0a98299ded7d5036d2ce18681a2433bba8a7f213175242c3dd206fa04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92de7744eea2e3304013ea24576bdd718df6ee63ce251ce03dce5e675754fca90f6f2f9228e5018d4c830da79d460e018ea9c32ba04603814af1275a5608aa60
|
|
7
|
+
data.tar.gz: 643519a175794000d681d1efad3cc7a684b6aa8be4983054a0384f16e842942399e1b13c7ec6a63b195cdd72b7fe8db7e0a8786453d9bd26d29f8362ed36b2a0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.3.0] - 2026-09-26
|
|
4
|
+
|
|
5
|
+
从 0.2.0 的「Markdown DOM」重写为「CSM 工具链」:(1) 解析、(2) 翻译、(3)(5)
|
|
6
|
+
`to_markdown`、(6) HTML 序列化四段可用;(4) 执行归宿主一行 `eval`。
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `MDOM::Hooks` + `Document.register_hook`:钩子 / 回调系统(CSM §5.3)——五个点
|
|
11
|
+
`:after_parse` / `:before_compile` / `:after_compile` / `:before_serialize` /
|
|
12
|
+
`:after_serialize`,类级全局与调用时临时两种作用域。
|
|
13
|
+
- `MDOM.compile(source, ...)`:统一入口——(1)(2)(6) 全链路并触发钩子,返回
|
|
14
|
+
`MDOM::Compiled`(`document` / `ruby` / `html` / `meta`);`MDOM.parse` 一并接线。
|
|
15
|
+
- `MDOM::Node::Document#meta`:文档级元数据。
|
|
16
|
+
- `MDOM::HtmlSerializer`:(6) AST → HTML(零 gem 依赖)——含 CSM §1.2 的
|
|
17
|
+
article/section 自动包裹、`hgroup`、WikiLink 的 `known_targets`、未求值嵌入的
|
|
18
|
+
`csm-ruby` 样式;公式输出 `math` 类元素(TeX 原文,不渲染 SVG)。
|
|
19
|
+
- `MDOM::MarkdownSerializer`:(3)(5) AST → CSM 源——同一方法服务 AST1(保真往返)
|
|
20
|
+
与 AST2(降级还原);从节点结构重建、保守转义、按节点深度重建缩进。`node.to_markdown`
|
|
21
|
+
是便捷入口。`MDOM.compile` 暂不产出 Markdown。
|
|
22
|
+
- `USAGE.md`:使用指南(各阶段 API、可运行示例、配方,含程序化生成目录)。
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- 解析与翻译按 CSM 重写:`Tokenizer`(表格、块级公式)、`BlockDetector`(Prism)、
|
|
27
|
+
`Parser`(FLOW / PHRASING 内容策略)、`InlineParser`、`Translator`(`unsafe:` 默认
|
|
28
|
+
安全,安全时不写嵌入段)、`Builder`(游标栈 + `render` / `值.render` + 「当前文档」)。
|
|
29
|
+
- `lib/mdom.rb` 从脚手架空壳改为真正的入口。
|
|
30
|
+
- `mdom.gemspec`:摘要与描述改为 CSM 工具链(原先还是 0.2.0 的「Markdown DOM」,
|
|
31
|
+
且声称能序列化回 Markdown);补上 `prism` 运行时依赖。`Gemfile` 补 `rubocop`
|
|
32
|
+
(`Rakefile` 一直在 require 它)。
|
|
33
|
+
- `.rubocop.yml`:指标类 cop 改为非闸门(lib 侧是刻意的宽分类器与扫描循环,见该文件
|
|
34
|
+
注释);`TargetRubyVersion` 与 gemspec 的 `required_ruby_version` 统一为 3.0。
|
|
35
|
+
- `rake` 默认任务(`test` + `rubocop`)现在全绿。
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
- `MDOM.serialize`:它指向已被移除的 `Serializer`,等 (3) `to_markdown` 落地再恢复。
|
|
40
|
+
- `lib/mdom/scanner.rb`(没有消费者的空壳,块配对与插值边界早已交给 Prism)、
|
|
41
|
+
`lib/mdom/node.rb` 里零使用的 `ALLOWED_CHILD_TYPES`、空目录 `sig/`。
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- 表格:`| a | b |` 行 + 分隔行 → `Table > TableRow > TableHeaderCell` / `TableCell`
|
|
46
|
+
(`align` 记在单元格上)。
|
|
47
|
+
- 块级公式:单行 `$$…$$` 与 `$$` 独占一行的多行包围式两种写法。
|
|
48
|
+
- 标题收尾的 `#` 只有与引导 `#` **等长**才算闭合。
|
|
49
|
+
- heredoc(`<<~SQL` / `<<-SQL` 等)现在按 CSM §3.12 / 附录 E **明确报错**
|
|
50
|
+
(`MDOM::UnsupportedHeredocError`),不再被当作普通 Ruby 块收下;左移 `<<`
|
|
51
|
+
不受影响。
|
|
52
|
+
- 标题锚点与 WikiLink 目标的 slug 统一为**小写**(CSM §2.1 要求 WikiLink 目标不区分
|
|
53
|
+
大小写),因此 `[[笔记#锚点]]` 能对上标题的 `id`。
|
|
54
|
+
- `InlineParser::ESCAPABLE`:`%w[` * …]` 的首个元素 `\`` 在 `%w` 里其实是**两个
|
|
55
|
+
字符**(`\` + `` ` ``),于是集合里根本没有反引号,取而代之一个永远匹配不上的双字符
|
|
56
|
+
项——CSM §3.11 明列可转义的 `` \` `` 一直转不了。已改为 `` ` ``。
|
|
57
|
+
|
|
58
|
+
## [0.2.0] - 2026-09-10
|
|
59
|
+
|
|
60
|
+
- Add `MDOM::Builder` and the `MDOM.build` DSL for constructing trees
|
|
61
|
+
programmatically, with block nesting, chaining and whole-tree return.
|
|
62
|
+
- Fix `ArgumentError: missing keyword: :marker` when serializing a `ListItem`
|
|
63
|
+
outside of its `List`; `Serializer#serialize` and `Serializer.serialize` now
|
|
64
|
+
accept a `marker:` keyword (default `"-"`).
|
|
65
|
+
- Fill in real RBS signatures in `sig/mdom.rbs` (previously `module Mdom`
|
|
66
|
+
scaffolding only).
|
|
67
|
+
- Fix the scaffold test referencing `Mdom::VERSION` instead of `MDOM::VERSION`.
|
|
68
|
+
|
|
69
|
+
## [0.1.0] - 2026-09-07
|
|
70
|
+
|
|
71
|
+
- Initial release
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TODO: Write your name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# mdom
|
|
2
|
+
|
|
3
|
+
**mdom** 是 [CSM](docs/CSM.md)(Complex Simple Markup)的 Ruby 工具链:把 CSM 文档解析成一棵文档树(AST),再翻译为 Ruby 源码、HTML 或 Markdown。
|
|
4
|
+
|
|
5
|
+
CSM 把 Markdown 视为 **Ruby 的语法糖**——一份 `.rb.md` 文档既是可读的 Markdown,也是一段可执行的 Ruby 程序:
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
# 销售报表
|
|
9
|
+
|
|
10
|
+
~ total = orders.sum(&:amount)
|
|
11
|
+
|
|
12
|
+
本月合计 #{total} 元。
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> **仓库状态**:已从 0.2.0 的「Markdown DOM」重写为「CSM 工具链」。(1) 解析、(2) 翻译、(3)(5) `to_markdown`、(6) HTML 序列化四段可用,入口 `MDOM.parse` / `MDOM.compile` 已接线;(4) 执行归宿主一行 `eval`。见[当前状态](#当前状态)。
|
|
16
|
+
|
|
17
|
+
## 处理模型
|
|
18
|
+
|
|
19
|
+
CSM 的处理由六个转换关系定义,围绕**一棵树的两种状态**展开:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
(1) parse (2) to_ruby
|
|
23
|
+
CSM 源 ───────────> AST1 ──────────────────> Ruby 源
|
|
24
|
+
│ │
|
|
25
|
+
(3) to_markdown (4) eval
|
|
26
|
+
│ │
|
|
27
|
+
v v
|
|
28
|
+
CSM 源 AST2 ──(6) to_html──> HTML
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| # | 转换 | 说明 | 状态 |
|
|
32
|
+
|---|------|------|------|
|
|
33
|
+
| (1) | CSM → AST1 | 解析。嵌入部分以**源码原文 + 位置**存于树中,尚未求值 | ✅ 已实现 |
|
|
34
|
+
| (2) | AST1 → Ruby | 翻译。排版变 builder 调用;嵌入块在 `unsafe: true` 下保留为 Ruby | ✅ 已实现 |
|
|
35
|
+
| (3) | AST1 → CSM | `to_markdown`,不执行代码,嵌入部分原样回吐 → 保真往返 | ✅ 已实现 |
|
|
36
|
+
| (4) | Ruby → AST2 | 执行(`eval`)。嵌入部分被替换为求值产出的子树 | ➖ 由宿主 `eval`,mdom 不含求值器 |
|
|
37
|
+
| (5) | AST2 → CSM | `to_markdown`,已无源码,**降级还原** | ✅ 已实现(降级是设计后果) |
|
|
38
|
+
| (6) | AST2 → HTML | `to_html` 序列化(AST1 / AST2 均可) | ✅ 已实现 |
|
|
39
|
+
|
|
40
|
+
**AST1 与 AST2 是同一棵树类型**,区别只在于是否还残留未求值的嵌入节点。对一棵 AST1 直接 `to_html` 也是允许的——未求值的嵌入部分会以独立的嵌入样式输出。
|
|
41
|
+
|
|
42
|
+
## 当前状态
|
|
43
|
+
|
|
44
|
+
| 模块 | 职责 | 状态 |
|
|
45
|
+
|------|------|------|
|
|
46
|
+
| `lib/mdom/tokenizer.rb` | 逐行分类:围栏 → 表格 → 块级公式 → `\` 转义 → `~` → 候选嵌入 → 排版 | ✅ |
|
|
47
|
+
| `lib/mdom/block_detector.rb` | 用 Prism 增量 parse 判定 Ruby 块(A/B 两类) | ✅ |
|
|
48
|
+
| `lib/mdom/parser.rb` | Token Stream → AST1(按 CSM 缩进建层级;按内容策略区分 FLOW / PHRASING 容器) | ✅ |
|
|
49
|
+
| `lib/mdom/inline_parser.rb` | 行内解析:强调/代码/链接/增删/进度条/公式/插值/转义/换行 | ✅ |
|
|
50
|
+
| `lib/mdom/node.rb`、`node/nodes.rb`、`node/inline_nodes.rb` | 块级与行内节点定义、树操作(挂载/遍历/导航/深度) | ✅ |
|
|
51
|
+
| `lib/mdom/error.rb` | 解析错误(含文档行号) | ✅ |
|
|
52
|
+
| `lib/mdom/builder.rb` | 文档构造 DSL + 游标栈 + `render` / `值.render` + 「当前文档」栈(零 gem 依赖) | ✅ |
|
|
53
|
+
| `lib/mdom/translator.rb` | AST1 → Ruby 源码(`unsafe:` 默认安全,安全时不写嵌入段) | ✅ |
|
|
54
|
+
| `lib/mdom/html_serializer.rb` | AST → HTML(零 gem 依赖,安全模式下嵌入按独立样式输出) | ✅ |
|
|
55
|
+
| `lib/mdom/hooks.rb` | 钩子 / 回调注册表:五个点、类级全局 + 调用时临时(CSM §5.3) | ✅ |
|
|
56
|
+
| `lib/mdom.rb` | 总入口:`MDOM.parse` / `MDOM.compile`((1)(2)(6) + 钩子) | ✅ |
|
|
57
|
+
| `lib/mdom/markdown_serializer.rb` | AST → CSM((3)(5);零 gem 依赖,保守转义 + 按深度重建缩进) | ✅ |
|
|
58
|
+
| `test/` | minitest 用例(`rake test`,覆盖 tokenizer / block_detector / parser / inline_parser / builder / translator / html_serializer / markdown_serializer / node / hooks / compile) | ✅ |
|
|
59
|
+
|
|
60
|
+
`require "mdom"` 是一个可用入口:`MDOM.parse` 给 AST1,`MDOM.compile` 走完 (1)(2)(6) 并在五个点上触发钩子,返回 `MDOM::Compiled`(`document` / `ruby` / `html` / `meta`)。回写 CSM 用 `node.to_markdown`(≡ `MDOM::MarkdownSerializer.serialize`)——AST1 保真往返,AST2 只能降级。旧版 `MDOM.serialize` 未恢复,`MDOM.compile` 也暂不产出 Markdown。
|
|
61
|
+
|
|
62
|
+
`rake`(默认任务 = `test` + `rubocop`)全绿:343 例、0 skip、32 个文件无 rubocop 告警。指标类 cop 在 `.rubocop.yml` 里关掉了,理由见该文件的注释。
|
|
63
|
+
|
|
64
|
+
## 用法
|
|
65
|
+
|
|
66
|
+
各阶段可以单独 `require`;想一次走完 (1)(2)(6)(并触发钩子)用总入口 `MDOM.compile`,细节见 [USAGE.md](USAGE.md)。下面按阶段演示:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
require "mdom/parser"
|
|
70
|
+
|
|
71
|
+
doc = MDOM::Parser.parse(<<~CSM)
|
|
72
|
+
# 标题
|
|
73
|
+
|
|
74
|
+
- 一
|
|
75
|
+
- 二
|
|
76
|
+
|
|
77
|
+
~ total = 1 + 2
|
|
78
|
+
CSM
|
|
79
|
+
|
|
80
|
+
doc.children.map(&:type) # => [:heading, :list, :ruby_embed]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
正文按 CSM 元素表的内容策略落在 `children` 上:**PHRASING** 容器(段落、标题、定义术语)直接收行内节点;**FLOW** 容器(引用、列表项、定义)只收块级节点,正文先成为一个段落。原文仍保留在 `attributes[:text]`:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
heading = doc.children.first
|
|
87
|
+
heading.level # => 1
|
|
88
|
+
heading.text # => "标题"
|
|
89
|
+
heading.children.map(&:type) # => [:text]
|
|
90
|
+
|
|
91
|
+
paragraph = MDOM::Parser.parse("**粗** 与 *斜* 与 `code`\n").children.first
|
|
92
|
+
paragraph.children.map(&:type) # => [:strong, :text, :emphasis, :text, :code_span]
|
|
93
|
+
paragraph.text # => "**粗** 与 *斜* 与 `code`"
|
|
94
|
+
|
|
95
|
+
quote = MDOM::Parser.parse("> 引用 **粗**\n>- 出处\n").children.first
|
|
96
|
+
quote.children.map(&:type) # => [:paragraph, :cite]
|
|
97
|
+
quote.children.first.children.map(&:type) # => [:text, :strong]
|
|
98
|
+
quote.cite # => "出处" (`>-` 收尾,内容策略 PHRASING)
|
|
99
|
+
|
|
100
|
+
table = MDOM::Parser.parse("| a | b |\n|:--|--:|\n| 1 | 2 |\n").children.first
|
|
101
|
+
table.children.map(&:type) # => [:table_row, :table_row]
|
|
102
|
+
table.children.first.children.map(&:type) # => [:table_header_cell, :table_header_cell]
|
|
103
|
+
table.children.first.children.map(&:align) # => [:left, :right]
|
|
104
|
+
|
|
105
|
+
MDOM::Parser.parse("$$x^2$$\n").children.first.type # => :math_block
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
行内节点覆盖 CSM 的全部行内语法,`MDOM::InlineParser.parse` 可单独调用:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
MDOM::InlineParser.parse(%q{[文本](@note) #{Date.today}}).map(&:type)
|
|
112
|
+
# => [:inline_quote, :text, :interpolation]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Ruby 嵌入节点保存源码原文、文档行号区间、CSM 缩进与形态:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
embed = MDOM::Parser.parse("list.each do |item|\n puts item\nend.render\n").children.first
|
|
119
|
+
|
|
120
|
+
embed.type # => :ruby_embed
|
|
121
|
+
embed.form # => :free (:begin / :free / :tilde)
|
|
122
|
+
embed.render? # => true (以 `.render` 收尾)
|
|
123
|
+
embed.lines # => 1..3
|
|
124
|
+
embed.source # => "list.each do |item|\n puts item\nend.render"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
块判定的失败方式遵循 CSM §3.5.3——散文不会被误判为代码:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
MDOM::Parser.parse("time flies\n").children.first.type # => :paragraph
|
|
131
|
+
|
|
132
|
+
MDOM::Parser.parse(" list.each do |x|\n y\n end\n")
|
|
133
|
+
# => MDOM::CrossLevelError: ruby block opens with 4 space(s) of indentation (line 1)
|
|
134
|
+
# but closes with 2 (line 3)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 翻译为 Ruby
|
|
138
|
+
|
|
139
|
+
`MDOM::Translator` 实现 (2):把 AST1 翻成一段顶层顺序执行的 Ruby 程序。排版节点
|
|
140
|
+
变成 `builder.*` 调用——产物不绑定执行方式,`eval`、落盘 `require`、交给 mruby
|
|
141
|
+
都行。
|
|
142
|
+
|
|
143
|
+
**`unsafe` 是翻译期参数,默认安全**(CSM §3.14)。`to_ruby(node)` 默认
|
|
144
|
+
`unsafe: false`:嵌入段**不写进产物**——块级嵌入只留一行注释占位,行内插值退化为
|
|
145
|
+
字面 `#{expr}`,因此产物静态上不含用户代码。`to_ruby(node, unsafe: true)` 才把
|
|
146
|
+
嵌入块(`begin...end` / `~` 行)原样注入、把行内插值交 Ruby 求值。
|
|
147
|
+
|
|
148
|
+
安全渲染不需要拿产物去执行:未求值的嵌入还留在 AST1 里,直接交给 `to_html` 即可。
|
|
149
|
+
(执行本身只是宿主的一行 `eval`,mdom 不含求值器。)
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
require "mdom/parser"
|
|
153
|
+
require "mdom/translator"
|
|
154
|
+
|
|
155
|
+
puts MDOM::Translator.to_ruby(MDOM::Parser.parse("- 一\n- 二\n"))
|
|
156
|
+
# builder = MDOM::Builder.new
|
|
157
|
+
# builder.list(ordered: false, task: false, marker: "-") do
|
|
158
|
+
# builder.item("一", marker: "-")
|
|
159
|
+
# builder.item("二", marker: "-")
|
|
160
|
+
# end
|
|
161
|
+
# builder.to_document
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`Translator.to_ruby(node, **options)` 与 `node.to_ruby(**options)` 等价。
|
|
165
|
+
|
|
166
|
+
### 序列化为 HTML
|
|
167
|
+
|
|
168
|
+
`MDOM::HtmlSerializer` 实现 (6)。AST1 与 AST2 都能喂给它——未求值的嵌入(块级
|
|
169
|
+
`RubyEmbed` / 行内插值)按独立的嵌入样式输出(与代码块同构,另加 `csm-ruby`
|
|
170
|
+
class),所以「安全渲染」与「不安全渲染」的差别只在于嵌入代码是否执行。
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
require "mdom/parser"
|
|
174
|
+
require "mdom/html_serializer"
|
|
175
|
+
|
|
176
|
+
puts MDOM::HtmlSerializer.serialize(MDOM::Parser.parse("## 标题\n\n正文 **粗**。\n"))
|
|
177
|
+
# <section>
|
|
178
|
+
# <h2 id="标题">标题</h2>
|
|
179
|
+
# <p>正文 <strong>粗</strong>。</p>
|
|
180
|
+
# </section>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`HtmlSerializer.serialize(node)` 与 `node.to_html` 等价;给 `known_targets:` 时,
|
|
184
|
+
不在其中的 WikiLink 会带上 `orphan-link` class。
|
|
185
|
+
|
|
186
|
+
### 序列化为 CSM
|
|
187
|
+
|
|
188
|
+
`MDOM::MarkdownSerializer` 实现 (3)/(5),把树写回 CSM 源。同一个方法服务两种输入
|
|
189
|
+
状态:**AST1**(嵌入还留着源码)逐字保真往返;**AST2**(嵌入已被求值结果替换)只能
|
|
190
|
+
降级——源码位置只剩结果,无法还原成 Ruby。
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
require "mdom/parser"
|
|
194
|
+
require "mdom/markdown_serializer"
|
|
195
|
+
|
|
196
|
+
MDOM::MarkdownSerializer.serialize(MDOM::Parser.parse("# 标题\n"))
|
|
197
|
+
# => "# 标题"
|
|
198
|
+
|
|
199
|
+
MDOM::Parser.parse("**粗** 与 `code`\n").to_markdown
|
|
200
|
+
# => "**粗** 与 `code`"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
序列化**从节点结构重建**(不读 `attributes[:text]` 那类原文缓存,所以改树之后输出
|
|
204
|
+
反映改动),并采取**保守转义**:文本里凡是可能重新触发解析的字符都会被转义,代价是
|
|
205
|
+
输出比手写的 CSM 更谨慎,换来的是「内容不会被自己的标记重新解析」。
|
|
206
|
+
|
|
207
|
+
### 用 DSL 构造
|
|
208
|
+
|
|
209
|
+
除了解析,也可以用 builder 从零构造文档。节点挂到「当前容器」;块参数就是 builder 自身(显式传参,不用 `instance_eval`):
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
require "mdom/parser" # 字符串参数要走行内解析,需要解析侧
|
|
213
|
+
require "mdom/builder"
|
|
214
|
+
|
|
215
|
+
doc = MDOM::Builder.build do |b|
|
|
216
|
+
b.heading(1, "标题")
|
|
217
|
+
b.paragraph("正文 *强调*。")
|
|
218
|
+
b.list do |l|
|
|
219
|
+
%w[一 二].each { |item| l.item { |i| i.render(item) } } # render 是唯一的注入通道
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`render(value, parse: false)` 把值挂到调用位置:字符串按**当前容器**包装(行内容器 → 文本节点,块级容器 → 段落),`Array` / `Enumerable` 逐项递归,节点直接插入;`parse: true` 时字符串按 CSM 递归解析。
|
|
225
|
+
|
|
226
|
+
builder 属**零 gem 依赖**的一侧:它不 `require` 任何东西,字符串参数的行内解析与 `render(parse: true)` 的 CSM 解析都通过构造参数注入(默认为 `MDOM::InlineParser` / `MDOM::Parser`)。
|
|
227
|
+
|
|
228
|
+
## 文档
|
|
229
|
+
|
|
230
|
+
| 文件 | 内容 |
|
|
231
|
+
|------|------|
|
|
232
|
+
| [USAGE.md](USAGE.md) | **使用指南**:各阶段的 API、可运行示例、常见配方 |
|
|
233
|
+
| [docs/CSM.md](docs/CSM.md) | **语言规格**:语法、嵌入机制、处理模型(`docs/CSM.md` 自身就是一份合法 CSM 文档) |
|
|
234
|
+
| [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md) | 实现方案:管线、模块划分、算法要点、风险清单、测试用例清单 |
|
|
235
|
+
| [docs/GFM-NOTES.md](docs/GFM-NOTES.md) | GitHub Flavored Markdown 规范阅读笔记(CSM 语法的来源之一) |
|
|
236
|
+
| [docs/STRICT-GFM.md](docs/STRICT-GFM.md) | SMD:基于 GFM 裁剪的确定性优先标记语言 |
|
|
237
|
+
| [docs/ROBUSTNESS.md](docs/ROBUSTNESS.md) | 元数据索引鲁棒性的纸面推演(非实测) |
|
|
238
|
+
| [CHANGELOG.md](CHANGELOG.md) | 版本记录 |
|
|
239
|
+
|
|
240
|
+
## 开发
|
|
241
|
+
|
|
242
|
+
需要 **Ruby 3.0+**;解析侧硬依赖 [`prism`](https://github.com/ruby/prism)(Ruby 3.3 起随发行版作为 default gem 提供)。builder 侧不依赖 prism,目标是 mruby 子集。
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
rake test # 运行全部测试
|
|
246
|
+
ruby -Ilib test/parser_test.rb # 运行单个测试文件
|
|
247
|
+
ruby -Ilib -e 'require "mdom/parser"; p MDOM::Parser.parse("# T\n").children'
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## 目录结构
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
lib/
|
|
254
|
+
mdom.rb # 入口:MDOM.parse / MDOM.compile
|
|
255
|
+
mdom/
|
|
256
|
+
parser.rb # Token Stream → AST1 (解析侧)
|
|
257
|
+
tokenizer.rb # 逐行分类 (解析侧)
|
|
258
|
+
block_detector.rb # Prism 块判定 (解析侧)
|
|
259
|
+
inline_parser.rb # 行内解析 (解析侧)
|
|
260
|
+
translator.rb # AST1 → Ruby 源码 (解析侧)
|
|
261
|
+
error.rb # 解析错误
|
|
262
|
+
node.rb # 节点基类与树操作
|
|
263
|
+
node/nodes.rb # 块级节点定义
|
|
264
|
+
node/inline_nodes.rb # 行内节点定义
|
|
265
|
+
builder.rb # 构建 DSL + 游标栈 + render
|
|
266
|
+
hooks.rb # 钩子 / 回调注册表(CSM §5.3)
|
|
267
|
+
html_serializer.rb # AST → HTML(零 gem 依赖)
|
|
268
|
+
markdown_serializer.rb # AST → CSM 源((3)(5),零 gem 依赖)
|
|
269
|
+
version.rb
|
|
270
|
+
test/ # minitest 用例
|
|
271
|
+
docs/ # 语言规格与实现文档
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## 设计边界
|
|
275
|
+
|
|
276
|
+
- **两个目标平台**。builder 侧(节点、builder、钩子、HTML 序列化)只用 mruby 子集,可在 WASM 等受限环境运行;解析侧真正硬依赖 `prism` 的是 `block_detector`(块判定)与 `inline_parser`(行内),`tokenizer` / `parser` 经由它们间接依赖,只在 CRuby 上运行。两侧没有硬调用关系——builder 要重解析字符串参数时靠 `MDOM::InlineParser` 的**依赖注入**,本侧不 require 它。
|
|
277
|
+
- **`prism` 只在解析期使用**。块判定必须交给 Ruby 原装解析器:`do` / `end` 在英语里也是常见单词,纯词法无法区分「Ruby 块」与「英文散文」。AST1 一旦产出,后续阶段不再需要词法能力。
|
|
278
|
+
- **注入文档树的唯一通道是 `render`**。裸 `end` 与不带 `render` 的 `~` 行都只是执行,不产生输出;默认**安全渲染**——`to_ruby` 的 `unsafe` 默认 `false`,此时产物不含嵌入段(CSM §3.14)。
|
|
279
|
+
- **「当前文档」是类级状态**。`值.render` 与裸 `render` 靠 `MDOM::Builder.current`(文档栈)定位目标文档,所以并行请用 **Ractor**(类实例变量天然隔离),**`Thread` 并行不支持**。`render` 内部持互斥锁,无 `Mutex` 的平台(mruby 默认构建)退化为无锁。
|
|
280
|
+
|
|
281
|
+
细节见 [docs/CSM.md](docs/CSM.md) 与 [docs/IMPLEMENTATION.md](docs/IMPLEMENTATION.md)。
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT
|