manticore-smash 3.1.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 +7 -0
- data/LICENSE +661 -0
- data/README.md +492 -0
- data/lib/manticore.rb +24 -0
- data/lib/mdutils/rediscount.rb +871 -0
- data/lib/xmlutils/formatters.rb +91 -0
- data/lib/xmlutils/node.rb +585 -0
- data/lib/xmlutils/tokenizer.rb +282 -0
- data/lib/xmlutils/tree_parser.rb +161 -0
- data/lib/xmlutils/xml_doc.rb +273 -0
- data/lib/xmlutils/xpath.rb +103 -0
- metadata +48 -0
data/README.md
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# Manticore
|
|
2
|
+
|
|
3
|
+
## XMLUtils Ruby XML 工具库(REXML 精简重写版)
|
|
4
|
+
|
|
5
|
+
本项目是`imdoc/XMLUtils`的再编集,改名为 `xml_doc.rb` 兼容层,将 `XmlUtils` DOM 转换为自定义 `XmlNode` 结构。
|
|
6
|
+
并重新实现了 Ruby XML 处理库,不再依赖原版 `REXML` Gem,完全独立命名空间 `XmlUtils`,包含解析器、DOM、XPath 和格式化输出四层架构。
|
|
7
|
+
|
|
8
|
+
### 架构原理
|
|
9
|
+
|
|
10
|
+
原版 `REXML` 的实现核心在于**基于事件的拉取解析器(Pull Parser)**与**树形构建器(Tree Parser)**的分离。本重写遵循同样的分层设计:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Source Text
|
|
14
|
+
│
|
|
15
|
+
▼
|
|
16
|
+
┌─────────────────┐
|
|
17
|
+
│ Tokenizer │ ← 词法分析:将原始字符流切分为 Token 序列
|
|
18
|
+
│ (词法分析器) │
|
|
19
|
+
└─────────────────┘
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
┌─────────────────┐
|
|
23
|
+
│ TreeParser │ ← 语法分析:根据 Token 序列构建 DOM 树
|
|
24
|
+
│ (树形解析器) │
|
|
25
|
+
└─────────────────┘
|
|
26
|
+
│
|
|
27
|
+
▼
|
|
28
|
+
┌─────────────────┐
|
|
29
|
+
│ DOM (Node) │ ← 节点树:Document, Element, Text, Attribute...
|
|
30
|
+
│ (文档对象模型) │
|
|
31
|
+
└─────────────────┘
|
|
32
|
+
│
|
|
33
|
+
▼
|
|
34
|
+
┌─────────────────┐
|
|
35
|
+
│ XPath Engine │ ← 查询引擎:基于节点树的路径表达式匹配
|
|
36
|
+
│ (查询引擎) │
|
|
37
|
+
└─────────────────┘
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
┌─────────────────┐
|
|
41
|
+
│ Formatters │ ← 序列化:将 DOM 树输出为格式化的 XML 字符串
|
|
42
|
+
│ (格式化输出) │
|
|
43
|
+
└─────────────────┘
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 文件结构
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
lib/
|
|
50
|
+
├── manticore.rb # 引用入口,require 'manticore'
|
|
51
|
+
├── xmlutils/
|
|
52
|
+
│ ├── node.rb # 核心节点类(DOM)
|
|
53
|
+
│ ├── tokenizer.rb # 词法分析器(Token)
|
|
54
|
+
│ ├── tree_parser.rb # 树形解析器(Parser)
|
|
55
|
+
│ ├── xpath.rb # XPath 简化查询引擎
|
|
56
|
+
│ ├── formatters.rb # 序列化与格式化输出
|
|
57
|
+
│ └── xml_doc.rb # 模型转换:XmlUtils DOM → XmlNode
|
|
58
|
+
├── test/
|
|
59
|
+
│ ├── xml_doc_test.rb # xml_doc测试用例
|
|
60
|
+
│ └── xmlutils_test.rb # xmlutils测试用例
|
|
61
|
+
├── manticore.gemspec # Gem 打包配置
|
|
62
|
+
├── LICENSE # GNU AGPL-3.0 许可证
|
|
63
|
+
└── README.md
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 核心实现要点
|
|
67
|
+
|
|
68
|
+
#### 1. Tokenizer(词法分析器)
|
|
69
|
+
|
|
70
|
+
Tokenizer 将 XML 文本流拆分为语义化的 Token 序列。关键状态机逻辑:
|
|
71
|
+
|
|
72
|
+
- 遇到 `<` 时区分:起始标签 `<tag`、结束标签 `</tag>`、空标签 `<tag/>`、注释 `<!--`、CDATA `<![CDATA[`、DOCTYPE `<!DOCTYPE`、处理指令 `<?`
|
|
73
|
+
- 属性值支持单引号与双引号包裹
|
|
74
|
+
- 实体引用(`&` 等)在词法阶段即展开为对应字符
|
|
75
|
+
- 文本节点在遇到 `<` 或 `&` 时截断,保证文本与标记严格分离
|
|
76
|
+
|
|
77
|
+
核心方法:`read_tag`、`read_comment`、`read_cdata`、`read_doctype`、`read_processing_instruction`、`read_text`、`read_entity_ref`
|
|
78
|
+
|
|
79
|
+
#### 2. TreeParser(树形解析器)
|
|
80
|
+
|
|
81
|
+
TreeParser 接收 Token 序列,递归构建 DOM 树:
|
|
82
|
+
|
|
83
|
+
- 使用栈隐式结构:遇到 `start_tag` 创建 `Element`,将其加入当前父节点;遇到 `close_tag` 时回溯
|
|
84
|
+
- 支持自闭合标签(`empty_tag`)无需等待关闭标签
|
|
85
|
+
- XML 声明、DOCTYPE、注释、CDATA、处理指令均作为独立节点插入树中
|
|
86
|
+
- 空白文本节点默认过滤(符合 REXML 标准行为)
|
|
87
|
+
|
|
88
|
+
#### 3. DOM 节点类(Node 层)
|
|
89
|
+
|
|
90
|
+
继承体系:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Node
|
|
94
|
+
├── ChildNode
|
|
95
|
+
│ ├── Text
|
|
96
|
+
│ ├── CData (继承 Text,raw 模式)
|
|
97
|
+
│ ├── Comment
|
|
98
|
+
│ ├── ProcessingInstruction
|
|
99
|
+
│ ├── DocType
|
|
100
|
+
│ └── Element (核心容器节点)
|
|
101
|
+
│ └── 包含 Attribute 哈希表 + children 数组
|
|
102
|
+
├── XMLDecl (继承 ProcessingInstruction)
|
|
103
|
+
└── Document (根节点,容纳所有顶层节点)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`Element` 是最核心的容器:
|
|
107
|
+
- `attributes` 存储 `Attribute` 对象,支持 `[]`、`[]=`、`add_attribute`、`delete_attribute`
|
|
108
|
+
- `children` 存储子节点数组,支持 `add`、`delete`、`each_element`
|
|
109
|
+
- `text` 方法提取所有文本子节点拼接值
|
|
110
|
+
- `namespaces` 方法自动解析 `xmlns` 和 `xmlns:prefix` 属性
|
|
111
|
+
|
|
112
|
+
#### 4. XPath 引擎
|
|
113
|
+
|
|
114
|
+
实现为简化但功能完整的 XPath 子集:
|
|
115
|
+
|
|
116
|
+
- 支持 `/root/person/name` 路径导航
|
|
117
|
+
- 支持 `*` 通配符匹配任意元素
|
|
118
|
+
- 支持 `..` 父节点轴
|
|
119
|
+
- 支持 `[n]` 位置谓词(1-based)
|
|
120
|
+
- 支持 `[@attr]` 存在性谓词
|
|
121
|
+
- 支持 `[@attr='value']` 等值谓词
|
|
122
|
+
|
|
123
|
+
核心算法:`match_step` 逐层过滤候选节点,`apply_predicate` 应用谓词筛选。
|
|
124
|
+
|
|
125
|
+
#### 5. Formatters(序列化)
|
|
126
|
+
|
|
127
|
+
`Formatters::Default` 负责 DOM → XML 字符串:
|
|
128
|
+
|
|
129
|
+
- 自动判断元素是否只包含文本节点,若是则内联输出不换行
|
|
130
|
+
- 混合内容(子元素 + 文本)自动缩进
|
|
131
|
+
- 转义规则:`&` → `&`,`<` → `<`,`>` → `>`,`"` → `"`
|
|
132
|
+
|
|
133
|
+
#### 6. 设计取舍
|
|
134
|
+
|
|
135
|
+
- **Tokenizer 使用正则与字符串扫描** 而非原版复杂的 `Source` 类,实现更简洁,但大文件流式读取性能略逊。
|
|
136
|
+
- **XPath 使用朴素递归过滤** 而非原版编译为内部指令集,实现简单但复杂查询性能较低。
|
|
137
|
+
- **格式化输出基于递归判断** 是否仅含文本节点,与原版 `Formatters::Pretty` 的 `write` 策略一致。
|
|
138
|
+
- **Node 不提供 `previous_sibling`/`next_sibling` 的缓存优化**,每次通过父节点的 `children` 数组查找,符合大多数使用场景。
|
|
139
|
+
|
|
140
|
+
未实现的部分(原版 REXML 的高级功能):
|
|
141
|
+
|
|
142
|
+
- **PullParser / SAX 流式解析**:原版提供基于事件的流式解析,本版仅提供树形 DOM 解析。
|
|
143
|
+
- **DTD 内部子集解析**:如 `<!ENTITY>`、`<!ATTLIST>` 等声明未展开;仅支持外部声明行读取,忽略内部实体定义。
|
|
144
|
+
- **完整的 XPath 1.0**:未实现 `descendant-or-self::`、函数调用(除 `contains` 占位外)、复杂轴、`or/and` 逻辑谓词、字符串处理函数等。
|
|
145
|
+
- **UTF-16 / ISO-2022-JP 编码处理**:简化假设输入为 UTF-8 或系统兼容编码;XML 声明中的 encoding 字段仅读取不执行转码。
|
|
146
|
+
- **XML 命名空间前缀重写**:保留前缀但不验证 URI 一致性;不自动为元素添加默认命名空间声明。
|
|
147
|
+
- **实体声明外部解析**:不支持 SYSTEM 公共标识符指向的外部 DTD 实体;不解析参数实体。
|
|
148
|
+
- **XML 命名空间默认声明**:不支持 `xmlns="..."` 对无前缀元素的自动命名空间绑定。
|
|
149
|
+
- **CDATA 区块内的 `]]>` 拆分**:标准允许将 `]]>` 拆分为两个相邻 CDATA 节点,本版未实现。
|
|
150
|
+
- **ProcessingInstruction 目标名大小写敏感**:按原样保留,不做规范化处理。
|
|
151
|
+
- **文档片段(DocumentFragment)**:未提供 `REXML::DocumentFragment` 等价类。
|
|
152
|
+
- **XML 编码声明的自动检测**:不扫描 BOM 头,不根据前几个字节推断编码。
|
|
153
|
+
- **XML 规范化(C14N)**:未提供Canonical XML输出模式。
|
|
154
|
+
- **XML 数字签名相关**:不涉及 `Signature`、`SignedInfo` 等元素的特化处理。
|
|
155
|
+
|
|
156
|
+
功能对照表:
|
|
157
|
+
|
|
158
|
+
| 功能 | 原版 REXML | 本 XmlUtils | 状态 |
|
|
159
|
+
|------|-----------|-------------|------|
|
|
160
|
+
| `REXML::Document.new(string)` | ✅ | `XmlUtils.parse(source)` | 等价 |
|
|
161
|
+
| `doc.root` | ✅ | ✅ | 完全兼容 |
|
|
162
|
+
| `element['attr']` | ✅ | ✅ | 完全兼容 |
|
|
163
|
+
| `element.add_text` | ✅ | ✅ | 完全兼容 |
|
|
164
|
+
| `element.each_element` | ✅ | ✅ | 完全兼容 |
|
|
165
|
+
| `XPath.match` | ✅ | ✅ | 支持路径+谓词 |
|
|
166
|
+
| `XPath.first` | ✅ | ✅ | 完全兼容 |
|
|
167
|
+
| `Formatters::Pretty` | ✅ | ✅ | 简化实现 |
|
|
168
|
+
| `Parsers::PullParser` | ✅ | ❌ | 未实现(本版专注 TreeParser) |
|
|
169
|
+
| `Validation` | ✅ | ❌ | 未实现 |
|
|
170
|
+
| `StreamListener` (SAX) | ✅ | ❌ | 未实现 |
|
|
171
|
+
| `DTD` 完整解析 | ✅ | ⚠️ | 仅支持声明行,忽略内部实体定义 |
|
|
172
|
+
|
|
173
|
+
#### 7. xml_doc 移植问题
|
|
174
|
+
|
|
175
|
+
`lib/xmlutils/xml_doc.rb` 在适配原版 IMDOC/XMLUtils 的过程中,发现并修复了以下问题:
|
|
176
|
+
|
|
177
|
+
已修复问题:
|
|
178
|
+
|
|
179
|
+
| 问题 | 严重度 | 说明 |
|
|
180
|
+
|------|--------|------|
|
|
181
|
+
| `to_xml` 未转义属性和文本内容 | **高** | 原始实现直接拼接 `@attributes` 值到 XML 字符串,若值含 `<`、`&` 或 `"` 会生成非法 XML。已新增 `escape_xml` 和 `escape_xml_attr` 方法。 |
|
|
182
|
+
| `make_xml_from` 转义顺序错误 | **高** | 原始实现用数组 `zip` + `gsub!` 按 `['&','<','>',"'",'"']` 顺序替换,导致 `&` 先被替换为 `&`,后续 `<` 等被错误二次转义为 `&lt;`。已改为 `gsub` 链式调用,顺序修正为 `&` → `&`。 |
|
|
183
|
+
| `load` 使用 `open(filepath)` | 中 | `Kernel#open` 在 Ruby 3.x 中已被限制,可能调用 URI 解析而非本地文件读取。已改为 `File.read(filepath)`。 |
|
|
184
|
+
| `make_xml_from` / `make_str_from` 使用 `gsub!` | 中 | `gsub!` 直接修改传入字符串,产生副作用。已改为 `dup` + `gsub` 或 `gsub` 非破坏性调用。 |
|
|
185
|
+
| `self.copy` 浅拷贝属性 | 中 | `attributes: node.attributes` 是引用传递,修改副本属性会影响原节点。已改为 `node.attributes.dup`。 |
|
|
186
|
+
| `delete_elements` 空 block 时未定义变量 | 中 | 当未传入 block 时 `elems` 未定义,后续 `elems.each` 抛出 `NameError`。已加 `return [] unless block`。 |
|
|
187
|
+
| `pretty` 缺少非法 method 分支 | 低 | 传入 `:xml` 或 `:json` 以外的 method 会静默返回 `nil`。已加 `raise ArgumentError`。 |
|
|
188
|
+
| `to_xml` 字符串子元素未转义 | 中 | `@elements` 中若为 `String` 实例直接拼接,未做 XML 转义。已加 `XmlNode.escape_xml(e)`。 |
|
|
189
|
+
|
|
190
|
+
未修复的设计层面问题:
|
|
191
|
+
|
|
192
|
+
| 问题 | 说明 |
|
|
193
|
+
|------|------|
|
|
194
|
+
| `@prev` 语义混乱 | 构造函数将父节点放入 `@prev` 数组,但 `prev` 通常表示「前一个兄弟节点」。这导致双向链表遍历逻辑可能不符合预期。 |
|
|
195
|
+
| `to_obj` 同名覆盖 | 若两个子元素同名,`Hash#merge!` 会覆盖,只能保留最后一个。如需保留全部同名子元素,建议改用数组结构。 |
|
|
196
|
+
| `add_content` 与 `modify_content` 语义不一致 | `add_content` 追加到 `@elements` 数组;`modify_content` 先清空所有 `XmlNode` 子元素再添加。混合使用可能导致非文本子元素被意外删除。 |
|
|
197
|
+
| `pretty` 的 `indent` 参数失效 | 原 `REXML::Document.write(output, indent)` 支持自定义缩进宽度。`XmlUtils::Formatters::Default` 当前硬编码为 2 空格,`indent` 参数仅保留接口兼容,实际未生效。 |
|
|
198
|
+
|
|
199
|
+
### 使用示例
|
|
200
|
+
|
|
201
|
+
#### 1. 打包 和 测试
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
gem build manticore.gemspec
|
|
205
|
+
gem install --local manticore-3.0.0.gem
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
测试脚本覆盖以下场景:
|
|
209
|
+
|
|
210
|
+
1. 基本 XML 解析(多层嵌套元素)
|
|
211
|
+
2. XPath 查询(路径导航、位置谓词、属性筛选)
|
|
212
|
+
3. CDATA 与注释处理
|
|
213
|
+
4. 空元素与属性读写
|
|
214
|
+
5. 序列化与反序列化 round-trip
|
|
215
|
+
6. 程序化 DOM 构建
|
|
216
|
+
7. XML 声明与 DOCTYPE 解析
|
|
217
|
+
|
|
218
|
+
运行方式:
|
|
219
|
+
```bash
|
|
220
|
+
ruby -I lib test/xmlutils_test.rb
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### 2. DOM节点 和 XPath
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
require 'manticore'
|
|
227
|
+
|
|
228
|
+
# 解析 XML
|
|
229
|
+
xml = <<-XML
|
|
230
|
+
<?xml version="1.0"?>
|
|
231
|
+
<root>
|
|
232
|
+
<person id="1">
|
|
233
|
+
<name>Alice</name>
|
|
234
|
+
</person>
|
|
235
|
+
</root>
|
|
236
|
+
XML
|
|
237
|
+
|
|
238
|
+
doc = XmlUtils.parse(xml)
|
|
239
|
+
|
|
240
|
+
# XPath 查询
|
|
241
|
+
person = XmlUtils::XPath.first(doc.root, 'person')
|
|
242
|
+
name = XmlUtils::XPath.first(doc.root, 'person/name').text
|
|
243
|
+
puts name # => "Alice"
|
|
244
|
+
|
|
245
|
+
# 构造文档
|
|
246
|
+
doc2 = XmlUtils.new_document
|
|
247
|
+
root = XmlUtils::Element.new('catalog')
|
|
248
|
+
doc2.add(root)
|
|
249
|
+
item = XmlUtils::Element.new('item')
|
|
250
|
+
item['id'] = '99'
|
|
251
|
+
item.add_text('Widget')
|
|
252
|
+
root.add(item)
|
|
253
|
+
|
|
254
|
+
puts XmlUtils.to_xml_string(doc2)
|
|
255
|
+
# => <catalog>
|
|
256
|
+
# <item id="99">Widget</item>
|
|
257
|
+
# </catalog>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### 3. 模型转换兼容层
|
|
261
|
+
|
|
262
|
+
`xml_doc.rb` 提供 `XmlNode` 自定义树结构,适合需要更灵活遍历或双向链表关系的场景。内部使用 `XmlUtils` 解析,然后递归转换为 `XmlNode`:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
require 'manticore'
|
|
266
|
+
|
|
267
|
+
node = XmlParser.parse('<root><a>1</a><b id="2">text</b></root>')
|
|
268
|
+
puts node.to_xml #=> <root><a>1</a><b id="2">text</b></root>
|
|
269
|
+
|
|
270
|
+
# 格式化输出
|
|
271
|
+
puts node.pretty(:to_xml, :xml)
|
|
272
|
+
|
|
273
|
+
# 三元组 / 对象 / JSON 转换
|
|
274
|
+
p node.to_triad, node.to_obj
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
## ReDiscount Markdown 解析器(rdiscount API 兼容层)
|
|
282
|
+
|
|
283
|
+
`mdutils/rediscount` 是 Markdown 处理组件,提供 Markdown → HTML 文档解析转换,完全兼容 `rdiscount` Gem 的 API 接口,无需编译 C 扩展即可在任何 Ruby 3.0+ 环境中运行。
|
|
284
|
+
|
|
285
|
+
### 设计目标
|
|
286
|
+
|
|
287
|
+
- **零原生依赖**:摆脱 `rdiscount` 对 C 库 `libmarkdown` 的编译依赖,解决 Windows / 跨平台部署难题。
|
|
288
|
+
- **API 平替**:构造函数、标志位、方法名与 `RDiscount` 逐一对齐,现有项目只需 `s/RDiscount/ReDiscount/` 即可迁移。
|
|
289
|
+
- **功能对齐**:覆盖 `rdiscount` 的核心扩展特性(表格、脚注、目录、SmartyPants 等),并保留 BlueCloth 兼容别名。
|
|
290
|
+
|
|
291
|
+
### 文件结构
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
lib/
|
|
295
|
+
├── mdutils/
|
|
296
|
+
│ └── rediscount.rb # 纯 Ruby Markdown 解析器(ReDiscount + MarkdownParser)
|
|
297
|
+
├── test/
|
|
298
|
+
│ └── mdutils_test.rb # 与原生 rdiscount 的对比测试套件
|
|
299
|
+
└── manticore.rb # 统一入口,require 'manticore' 自动加载
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### 核心实现要点
|
|
305
|
+
|
|
306
|
+
#### 1. 双层架构
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
┌─────────────────┐
|
|
310
|
+
│ ReDiscount │ ← 公共 API:标志管理、入口方法 to_html / toc_content
|
|
311
|
+
│ (接口类) │
|
|
312
|
+
└─────────────────┘
|
|
313
|
+
│
|
|
314
|
+
▼
|
|
315
|
+
┌─────────────────┐
|
|
316
|
+
│ MarkdownParser │ ← 内部实现:分块 → 渲染 → 后处理
|
|
317
|
+
│ (解析引擎) │
|
|
318
|
+
└─────────────────┘
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- **ReDiscount** 负责接收原始 Markdown 文本与开关标志,提供 `to_html`、`toc_content` 及所有 `attr_accessor` 标志位。
|
|
322
|
+
- **MarkdownParser** 为内部无状态解析引擎,执行「预处理 → 块级分块 → 内联渲染 → 后处理」四阶段流水线。
|
|
323
|
+
|
|
324
|
+
#### 2. 预处理阶段
|
|
325
|
+
|
|
326
|
+
1. **换行标准化**:统一 `\r\n`、`\r` 为 `\n`。
|
|
327
|
+
2. **引用定义提取**:识别 `[^id]: url "title"` 形式的引用链接与脚注定义,从正文中剥离并建立查找表。
|
|
328
|
+
3. **脚注多行合并**:支持缩进续行的脚注内容收集。
|
|
329
|
+
|
|
330
|
+
#### 3. 块级解析(Block-level)
|
|
331
|
+
|
|
332
|
+
扫描器按优先级逐行识别以下块类型:
|
|
333
|
+
|
|
334
|
+
| 块类型 | 识别规则 | 说明 |
|
|
335
|
+
|--------|----------|------|
|
|
336
|
+
| 水平线 (`:hr`) | `^*{3,}` / `^-{3,}` / `^_{3,}` | 标准分隔线 |
|
|
337
|
+
| ATX 标题 (`:header`) | `^#{1,6}\s+...` | 1–6 级标题 |
|
|
338
|
+
| Setext 标题 (`:header`) | 下划线 `====` / `----` | 1–2 级标题 |
|
|
339
|
+
| 围栏代码 (`:code`) | \`\`\`lang … \`\`\` | GFM 风格代码块 |
|
|
340
|
+
| 缩进代码 (`:code`) | `^ ` 或 `^\t` | 经典 4 空格/Tab 缩进 |
|
|
341
|
+
| 引用块 (`:blockquote`) | `^>\s?...` | 支持嵌套 |
|
|
342
|
+
| 表格 (`:table`) | `\|...\|` + `\|:-:\|` | GFM / Discount 扩展 |
|
|
343
|
+
| 定义列表 (`:deflist`) | term + `^:\s+def` | Discount 扩展 |
|
|
344
|
+
| 无序列表 (`:ul`) | `* ` / `+ ` / `- ` | 支持嵌套与多行项 |
|
|
345
|
+
| 有序列表 (`:ol`) | `1. ` / `1) ` | 数字序号 |
|
|
346
|
+
| 字母列表 (`:ol_alpha`) | `a. ` / `a) ` | 需 `:md1compat` 标志 |
|
|
347
|
+
| HTML 块 (`:html`) | `<div>` / `<table>` 等 | 保留原始标签 |
|
|
348
|
+
| 段落 (`:paragraph`) | 默认兜底 | 连续非空行 |
|
|
349
|
+
|
|
350
|
+
#### 4. 内联渲染(Inline)
|
|
351
|
+
|
|
352
|
+
处理顺序经过精心设计,避免嵌套冲突:
|
|
353
|
+
|
|
354
|
+
1. **代码片段保护** `` `code` `` → 占位符,防止后续正则误匹配。
|
|
355
|
+
2. **图片** `` → 支持 Discount 尺寸扩展。
|
|
356
|
+
3. **链接** `[text](url "title")` 与引用链接 `[text][ref]`。
|
|
357
|
+
4. **自动链接** `<https://...>` / `<mail@...>`(需 `:autolink`)。
|
|
358
|
+
5. **删除线** `~~text~~` → `<del>`。
|
|
359
|
+
6. **上标** `^text^` → `<sup>`。
|
|
360
|
+
7. **强调** `**strong**` / `*em*` / `__strong__` / `_em_`。
|
|
361
|
+
8. **硬换行** 行尾两空格 → `<br />`。
|
|
362
|
+
9. **脚注引用** `[^id]` → 上标链接。
|
|
363
|
+
10. **恢复代码片段** 占位符还原为 `<code>`。
|
|
364
|
+
|
|
365
|
+
#### 5. 后处理阶段
|
|
366
|
+
|
|
367
|
+
- **Smartypants**(`:smart`): `"..."` → `“...”`,`'...'` → `‘...’`,`--` → `—`,`...` → `…`。
|
|
368
|
+
- **脚注列表生成**:在文档末尾追加 `<div class="footnotes">` 有序列表。
|
|
369
|
+
- **HTML 过滤**:`:filter_html` strip 全部标签;`:filter_styles` 移除 `<style>` 块。
|
|
370
|
+
|
|
371
|
+
### 支持标志位(Flags)
|
|
372
|
+
|
|
373
|
+
构造函数支持通过 Symbol 列表一键开启:
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
rd = ReDiscount.new(text, :smart, :footnotes, :autolink)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
| 标志 | 说明 | 默认 |
|
|
380
|
+
|------|------|------|
|
|
381
|
+
| `:smart` | 智能引号与排版符号(SmartyPants) | false |
|
|
382
|
+
| `:filter_html` | 过滤所有 HTML 标签 | false |
|
|
383
|
+
| `:filter_styles` | 过滤 `<style>` 块 | false |
|
|
384
|
+
| `:footnotes` | 启用脚注 `[^id]` | false |
|
|
385
|
+
| `:generate_toc` | 生成目录并插入标题锚点 | false |
|
|
386
|
+
| `:no_image` | 禁用图片解析 | false |
|
|
387
|
+
| `:no_links` | 禁用链接解析 | false |
|
|
388
|
+
| `:no_tables` | 禁用表格解析 | false |
|
|
389
|
+
| `:strict` | 严格模式 | false |
|
|
390
|
+
| `:autolink` | 自动识别 URL 与邮箱链接 | false |
|
|
391
|
+
| `:safelink` | 安全链接(仅允许 http/https/ftp/news) | false |
|
|
392
|
+
| `:no_pseudo_protocols` | 禁用伪协议链接 | false |
|
|
393
|
+
| `:no_superscript` | 禁用上标 | false |
|
|
394
|
+
| `:no_strikethrough` | 禁用删除线 | false |
|
|
395
|
+
| `:latex` | LaTeX 支持占位 | false |
|
|
396
|
+
| `:explicitlist` | 显式列表(多行项不嵌套 `<p>`) | false |
|
|
397
|
+
| `:md1compat` | Markdown 1.0 兼容(字母列表等) | false |
|
|
398
|
+
|
|
399
|
+
### 使用示例
|
|
400
|
+
|
|
401
|
+
#### 基础用法
|
|
402
|
+
|
|
403
|
+
```ruby
|
|
404
|
+
require 'mdutils/rediscount'
|
|
405
|
+
|
|
406
|
+
markdown = ReDiscount.new("Hello **World**!")
|
|
407
|
+
puts markdown.to_html
|
|
408
|
+
# => <p>Hello <strong>World</strong>!</p>
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
#### 生成目录
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
text = <<~MD
|
|
415
|
+
# 第一章
|
|
416
|
+
## 1.1 小节
|
|
417
|
+
# 第二章
|
|
418
|
+
MD
|
|
419
|
+
|
|
420
|
+
rd = ReDiscount.new(text, :generate_toc)
|
|
421
|
+
puts rd.toc_content
|
|
422
|
+
# => <ul>...<a href="#第一章">第一章</a>...</ul>
|
|
423
|
+
puts rd.to_html
|
|
424
|
+
# => 标题自动附带 <a name="第一章"></a> 锚点
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### 表格与对齐
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
text = <<~MD
|
|
431
|
+
| 左对齐 | 居中 | 右对齐 |
|
|
432
|
+
|:-------|:----:|-------:|
|
|
433
|
+
| A | B | C |
|
|
434
|
+
MD
|
|
435
|
+
|
|
436
|
+
puts ReDiscount.new(text).to_html
|
|
437
|
+
# => <table>...<th style="text-align:left;">...
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
#### 脚注
|
|
441
|
+
|
|
442
|
+
```ruby
|
|
443
|
+
text = <<~MD
|
|
444
|
+
这是一个脚注示例[^1]。
|
|
445
|
+
|
|
446
|
+
[^1]: 脚注内容支持多行
|
|
447
|
+
续行缩进。
|
|
448
|
+
MD
|
|
449
|
+
|
|
450
|
+
puts ReDiscount.new(text, :footnotes).to_html
|
|
451
|
+
# => <sup><a href="#fn1" id="ref1">1</a></sup>
|
|
452
|
+
# => <div class="footnotes">...</div>
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
#### BlueCloth 兼容
|
|
456
|
+
|
|
457
|
+
```ruby
|
|
458
|
+
require 'mdutils/rediscount'
|
|
459
|
+
# BlueCloth 自动别名为 ReDiscount
|
|
460
|
+
markdown = BlueCloth.new("Hello World!")
|
|
461
|
+
puts markdown.to_html
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### 与原生 rdiscount 的差异
|
|
465
|
+
|
|
466
|
+
本实现以「最大兼容」为目标,但纯 Ruby 解析与 C 扩展在边界处理上仍有细微差异,测试套件中已标记为已知差异(`assert_differs_from_rdiscount`):
|
|
467
|
+
|
|
468
|
+
| 差异点 | 说明 |
|
|
469
|
+
|--------|------|
|
|
470
|
+
| 引用块空白 | 嵌套引用块的内部 `<p>` 剥离策略与 `rdiscount` 不同,导致空白字符差异。 |
|
|
471
|
+
| 上标尾部 `^` | `x^2^` 中 `rdiscount` 会残留尾部 `^`,本实现完全移除。 |
|
|
472
|
+
| 相邻列表合并 | `rdiscount` 会将相邻同类型列表合并为一个 `<ul>`;本实现保持独立列表。 |
|
|
473
|
+
| `filter_html` 段落 | 当 HTML 块位于顶部时,`rdiscount` 会保留空 `<p></p>`,本实现可能将其一并过滤。 |
|
|
474
|
+
| `explicitlist` 多行项 | 多行列表项的换行与 `<p>` 嵌套策略不同。 |
|
|
475
|
+
|
|
476
|
+
> 注:以上差异不影响语义正确性,仅影响 HTML 空白与标签嵌套细节。常规文档(段落、标题、列表、表格、链接、图片、代码块)的输出与 `rdiscount` 完全一致。
|
|
477
|
+
|
|
478
|
+
### 测试
|
|
479
|
+
|
|
480
|
+
测试脚本与原生 `rdiscount` 进行交叉比对,覆盖块级元素、内联元素、扩展语法与全部标志位:
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
ruby -I lib test/mdutils_test.rb
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
运行时会自动检测系统中是否安装了原生 `rdiscount` Gem:
|
|
487
|
+
- 若已安装,则执行输出对比,标记已知差异。
|
|
488
|
+
- 若未安装,则跳过比对测试,仅运行纯 Ruby 断言。
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
data/lib/manticore.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# Copyright (C) 2024 Manticore Authors
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU Affero General Public License as published
|
|
7
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU Affero General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
require_relative 'xmlutils/node'
|
|
19
|
+
require_relative 'xmlutils/tokenizer'
|
|
20
|
+
require_relative 'xmlutils/tree_parser'
|
|
21
|
+
require_relative 'xmlutils/xpath'
|
|
22
|
+
require_relative 'xmlutils/formatters'
|
|
23
|
+
require_relative 'xmlutils/xml_doc'
|
|
24
|
+
require_relative 'mdutils/rediscount'
|