caset 1.0.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.
@@ -0,0 +1,345 @@
1
+ #coding:utf-8
2
+
3
+
4
+ #------------------------------------------------------------------------------
5
+ # 代码块执行引擎与语言适配器
6
+ #------------------------------------------------------------------------------
7
+ module CasetDown
8
+ module_function
9
+
10
+ # 将 HTML 实体转义还原为原始字符。
11
+ #
12
+ # 处理 &gt; → > 和 &lt; → < 的转换。
13
+ #
14
+ # @param text [String] 包含 &gt;、&lt; 的文本
15
+ # @return [String] 还原后的原始文本
16
+ def textchange text
17
+ text.gsub('&gt;','>').gsub('&lt;','<')
18
+ end
19
+
20
+ # 将输出文本包装为 Markdown 代码块格式。
21
+ #
22
+ # 生成格式:```shell\n#output>id\ntext\n```
23
+ #
24
+ # @param text [String] 输出内容
25
+ # @param id [String] 输出标识符
26
+ # @param tag [String] 标签前缀,默认为 'output'
27
+ # @return [String] 包装后的 Markdown 代码块字符串
28
+ def wrap_output text, id, tag='output'
29
+ output_start = "```shell\n##{tag}>#{id}"
30
+ output_finish= "```"
31
+ return [output_start, text, output_finish].join("\n")
32
+ end
33
+
34
+ # 在文本中搜索特定输出标记段落。
35
+ #
36
+ # 匹配 ```shell\n#output>id ... ``` 格式的代码块。
37
+ # 注意:TODO 存在 #output>path 和 #output> path 的识别问题。
38
+ #
39
+ # @param text [String] 完整文档文本
40
+ # @param id [String] 输出标识符
41
+ # @param tag [String] 标签前缀,默认为 'output'
42
+ # @return [Array<String>] 匹配到的输出内容列表
43
+ def search_output text, id, tag='output'
44
+ # [TODO] REFACTORING for recognition between '#output>path' and '#output> path'
45
+ output_start = "```shell\n##{tag}>#{id}"
46
+ output_finish= "```"
47
+ match = TextAbstract.match_paragraph text, output_start, output_finish
48
+ return match.map{|m|[output_start, m, output_finish].join}
49
+ end
50
+
51
+ # 从代码块头部魔法注释提取元数据。
52
+ #
53
+ # 支持的魔法注释及其含义:
54
+ # #src> / #script> → interpreter(解释器类型)
55
+ # #ns> / #namespace> → namespace(命名空间)
56
+ # #conf> / #config> → config(配置文件)
57
+ # #dst> / #target> → target(目标文件)
58
+ # #flow> / #step> → flow(流程标识)
59
+ # #seg> / #segment> → segment(分段标识)
60
+ # #in> / #input> → input(输入文件)
61
+ # #out> / #output> → output(输出文件)
62
+ # #row> → rows(行范围,支持 ~ 展开为 ..)
63
+ # #col> → cols(列选择,支持 ~ 展开为 ..)
64
+ #
65
+ # @param code [Hash] 代码块结构,必须包含 :code 和 :class 键
66
+ # @option code [String] :code 代码文本内容
67
+ # @option code [String, Symbol] :class 代码语言类,默认 :default
68
+ # @return [Array] 元数据数组
69
+ # [0] interpreter [String, Symbol] 解释器类型
70
+ # [1] namespace [String, Symbol] 命名空间
71
+ # [2] config [String, Symbol] 配置文件
72
+ # [3] target [String, Symbol] 目标文件
73
+ # [4] flow [String, Symbol] 流程标识
74
+ # [5] segment [String, Symbol] 分段标识
75
+ # [6] input [String, Symbol] 输入文件
76
+ # [7] output [String, Symbol] 输出文件
77
+ # [8] rows [Array, Symbol] 行范围数组或 :rows
78
+ # [9] cols [Array, Symbol] 列选择数组或 :cols
79
+ def magic_head code
80
+ states = textchange(code[:code]).split("\n")
81
+ interpreter = code[:class]
82
+ script = states.find{|s|s[0..4]=='#src>'||s[0..7]=='#script>'}.to_s.split('>')[1]
83
+ interpreter = script if script # :default
84
+
85
+ namespace = states.find{|s|s[0..3]=='#ns>'||s[0..10]=='#namespace>'}.to_s.split('>')[1] || :ns
86
+ config = states.find{|s|s[0..5]=='#conf>'||s[0..7]=='#config>'}.to_s.split('>')[1] || :conf
87
+ target = states.find{|s|s[0..4]=='#dst>'||s[0..7]=='#target>'}.to_s.split('>')[1]
88
+
89
+ flow = states.find{|s|s[0..5]=='#flow>'||s[0..5]=='#step>'}.to_s.split('>')[1] || :flow
90
+ segment = states.find{|s|s[0..4]=='#seg>'||s[0..8]=='#segment>'}.to_s.split('>')[1] || :seg
91
+
92
+ input = states.find{|s|s[0..3]=='#in>'||s[0..6]=='#input>'}.to_s.split('>')[1] || :in
93
+ output = states.find{|s|s[0..4]=='#out>'||s[0..7]=='#output>'}.to_s.split('>')[1] || :out
94
+
95
+ [interpreter, namespace, target, flow, segment, input, output].each{|tag|tag.strip! if tag.instance_of?(String)}
96
+
97
+ rows = if row = states.find{|s|s[0..4]=='#row>'}.to_s.split('>')[1]
98
+ row.gsub('~','..').split(',').map{|r|eval(r)}
99
+ else
100
+ :rows
101
+ end
102
+ cols = if col = states.find{|s|s[0..4]=='#col>'}.to_s.split('>')[1]
103
+ col.split(',').map{|c|
104
+ c.split('').inject(true){|f,chr|
105
+ f&&(('0'..'9').to_a+['-','~']).include?(chr)
106
+ } ? eval(c.gsub('~','..')) : c.strip
107
+ }
108
+ else
109
+ :cols
110
+ end
111
+
112
+ return interpreter, namespace, config, target, flow, segment, input, output, rows, cols
113
+ end
114
+
115
+ # 从文档中提取所有代码块,按元数据分组为哈希表。
116
+ #
117
+ # 过滤掉魔法注释行,只保留实际代码内容。
118
+ #
119
+ # @param cds [Hash] 解析结果,包含文档结构
120
+ # @return [Hash] 代码块映射表
121
+ # 键:[object_id, interpreter, namespace, config, target, [rows, cols], flow, segment, input, output]
122
+ # 值:String 代码内容(已过滤魔法注释)
123
+ def search_codes cds
124
+ codes = CasetDown::Check.all(cds).select{|cd|cd[:name]=='code'}
125
+
126
+ table = codes.inject({}) do|table, code|
127
+ interpreter, namespace, config, target, flow, segment, input, output, rows, cols = magic_head code
128
+
129
+ content = textchange(code[:code]).split("\n")[1..-1].select{|s|
130
+ !['#ns>','#in>'].include?(s[0..3]) &&
131
+ !['#out>','#src>','#dst>','#seg>','#row>','#col>'].include?(s[0..4]) &&
132
+ !['#flow>','#step>','#conf>'].include?(s[0..5]) &&
133
+ !['#input>'].include?(s[0..6]) &&
134
+ !['#script>','#target>','#output>','#config>'].include?(s[0..7]) &&
135
+ !['#segment>'].include?(s[0..8]) &&
136
+ !['#namespace>'].include?(s[0..10])
137
+ }.join("\n")
138
+ table[[code.object_id, interpreter, namespace, config, target, [rows,cols], flow, segment, input, output]] = content
139
+ table
140
+ end
141
+ return table
142
+ end
143
+
144
+ # 执行文档中所有代码块,支持多种编程语言。
145
+ #
146
+ # 支持解释器:shell/bash, ruby, erlang/erl, python3/python, lms, lmx
147
+ # 全局配置按代码块顺序累积生效($_tmp_global_)。
148
+ #
149
+ # @param cds [Hash] 解析结果
150
+ # @return [Hash] 执行结果映射
151
+ # 键:input [String, Symbol] 输入标识
152
+ # 值:running_output [String] 代码执行输出
153
+ # @note Windows 平台(mingw)shell 执行暂为空实现
154
+ def run cds
155
+ table = CasetDown.search_codes(cds)
156
+ $_tmp_global_ = {}
157
+ $_scr_head_ = ['#coding:utf-8','require "casetdown"'].join("\n")
158
+ result = {}
159
+ table.each do|key, text|
160
+ oid, interpreter, ns, conf, target, range, flow, seg, input, output = key
161
+ unless conf==:conf # 注意全局配置累积生效,所以有先后顺序
162
+ $_tmp_global_.merge!(
163
+ (File.exist?(conf) ? YAML.load(File.read conf) : {}) ||
164
+ (File.exist?("#{conf}.yml") ? YAML.load(File.read "#{conf}.yml") : {}) ||
165
+ (File.exist?("#{conf}.yaml") ? YAML.load(File.read "#{conf}.yaml") : {})
166
+ )
167
+ $_scr_tail_ = "\n__END__\n[global@endata ~]$ruby global\n#{$_tmp_global_.to_s}"
168
+ $_erl_tail_ = $_tmp_global_.empty? ? '' : ToErlang.fact('global', (ToErlang.hash($_tmp_global_)) )
169
+ end
170
+ if interpreter=='shell' || interpreter==:default
171
+ if RUBY_PLATFORM.include?('mingw')
172
+ else
173
+ File.write "./~tmp.sh",text
174
+ running = `bash ./~tmp.sh`
175
+ File.delete "./~tmp.sh"
176
+ end
177
+ elsif interpreter=='ruby'
178
+ File.write "./~tmp.rb","#{$_scr_head_}\n\n#{text}\n\n#{$_scr_tail_}"
179
+ running = `ruby -I"#{CasetDown.home}" ./~tmp.rb`
180
+ File.delete "./~tmp.rb"
181
+ elsif interpreter=='erlang' || interpreter=='erl'
182
+ File.write "./_tmp.erl","-module(_tmp).\n-export([main/1]).\n\n#{text}\n\n#{$_erl_tail_}\n\nfact(UnknownMessage)->\n io:format(\"~p\",[UnknownMessage])."
183
+ running = `escript ./_tmp.erl`
184
+ File.delete "./_tmp.erl"
185
+ elsif interpreter=='python3' || interpreter=='python'
186
+ File.write "./~tmp.py",text
187
+ running = `python ./~tmp.py`
188
+ File.delete "./~tmp.py"
189
+ elsif interpreter=='lms'
190
+ running = `lms "#{text}"`
191
+ elsif interpreter=='lmx'
192
+ running = `lmx "#{text}"`
193
+ else
194
+ end
195
+ result[input] = running
196
+ end
197
+ return result
198
+ end
199
+
200
+ # 执行代码块并将结果回写到 Markdown 文档。
201
+ #
202
+ # 支持将执行结果替换回原始文档中的 #output 标记位置。
203
+ #
204
+ # @param cds [Hash] 解析结果
205
+ # @param option [Hash] 控制选项
206
+ # @option option [String] :stream 覆盖原始文档文本
207
+ # @option option [String] :data 附加到 Ruby 脚本尾部($_scr_tail_)
208
+ # @option option [String] :fact 附加到 Erlang 脚本尾部($_erl_tail_)
209
+ # @option option [Array<Symbol>] :file 文件控制标志
210
+ # - :input 保存输入脚本到文件
211
+ # - :output 保存执行输出到文件
212
+ # - :all 保存输入和输出
213
+ # - :debug 保留临时文件不删除
214
+ # - :rendfile 保存为带时间戳的新文件
215
+ # - :preview 保存为 preview 版本文件
216
+ # @return [String] 回写后的完整文档文本
217
+ # @note :file 为 [] 时只执行不保存文件
218
+ def run_to_file cds,option={stream: nil, data: nil, fact: nil,file:[]} # empty, :input, :output, :all
219
+ $_tmp_global_ = {}
220
+ $_scr_head_ = ['#coding:utf-8','require "casetdown"'].join("\n")
221
+ $_scr_tail_ = option[:data] ? option[:data] : ''
222
+ $_erl_tail_ = option[:fact] ? option[:fact] : ''
223
+
224
+ table = CasetDown.search_codes(cds)
225
+ result = {}
226
+ table.each do|key, text|
227
+ oid, interpreter, ns, conf, target, range, flow, seg, input, output = key
228
+ unless conf==:conf # 注意全局配置累积生效,所以有先后顺序
229
+ $_tmp_global_.merge!(
230
+ (File.exist?(conf) ? YAML.load(File.read conf) : {}) ||
231
+ (File.exist?("#{conf}.yml") ? YAML.load(File.read "#{conf}.yml") : {}) ||
232
+ (File.exist?("#{conf}.yaml") ? YAML.load(File.read "#{conf}.yaml") : {})
233
+ )
234
+ $_scr_tail_ = $_scr_tail_.empty? ? "\n__END__\n[global@endata ~]$ruby global\n#{$_tmp_global_.to_s}" : "#{$_scr_tail_}\n[global@endata ~]$ruby global\n#{$_tmp_global_.to_s}"
235
+ $_erl_tail_ = $_tmp_global_.empty? ? $_erl_tail_ : $_erl_tail_+"\n\n"+ToErlang.fact('global', (ToErlang.hash($_tmp_global_)) )
236
+ end
237
+ if interpreter==:default
238
+ # NOTHING
239
+ elsif interpreter=='shell' || interpreter=='bash'
240
+ if RUBY_PLATFORM.include?('mingw')
241
+ else
242
+ File.write "./~tmp.sh",text
243
+ running = `bash ./~tmp.sh`
244
+ File.delete "./~tmp.sh" unless option[:file].include?(:debug)
245
+ end
246
+ elsif interpreter=='ruby'
247
+ File.write "./~tmp.rb","#{$_scr_head_}\n\n#{text}\n\n#{$_scr_tail_}"
248
+ running = `ruby -I"#{CasetDown.home}" ./~tmp.rb`
249
+ File.delete "./~tmp.rb" unless option[:file].include?(:debug)
250
+ elsif interpreter=='erlang' || interpreter=='erl'
251
+ File.write "./_tmp.erl","-module(_tmp).\n-export([main/1]).\n\n#{text}\n\n#{$_erl_tail_}\n\nfact(UnknownMessage)->\n io:format(\"~p\",[UnknownMessage])."
252
+ running = `escript ./_tmp.erl`
253
+ File.delete "./_tmp.erl" unless option[:file].include?(:debug)
254
+ elsif interpreter=='python3' || interpreter=='python'
255
+ File.write "./~tmp.py",text
256
+ running = `python ./~tmp.py`
257
+ File.delete "./~tmp.py" unless option[:file].include?(:debug)
258
+ elsif interpreter=='lms'
259
+ running = `lms "#{text}"`
260
+ elsif interpreter=='lmx'
261
+ running = `lmx "#{text}"`
262
+ else
263
+ end
264
+ result[input] = running
265
+ File.write input, text if !(option[:file] & [:input, :all]).empty? && input.instance_of?(String)
266
+ end
267
+
268
+ origin_text = option[:stream] || cds[:doc].text
269
+ result.each do|input, running| # input == output
270
+ File.write input, running if !(option[:file] & [:output, :all]).empty? && input.instance_of?(String)
271
+ origins = CasetDown.search_output(origin_text, input)
272
+ wrapped = CasetDown.wrap_output(running,input)
273
+ if origins.empty?
274
+ origins = CasetDown.search_output(origin_text, input, 'out')
275
+ wrapped = CasetDown.wrap_output(running,input, 'out')
276
+ end
277
+ origins.each do|origin|
278
+ # [TODO] 无法解决不同后缀的前置匹配替换问题,待修复
279
+ origin_text = origin_text.gsub(origin, wrapped)
280
+ end
281
+ end
282
+
283
+ if option[:file].include?(:rendfile)
284
+ if option[:file].include?(:preview)
285
+ prvw = cds[:path].split('.')
286
+ prvw.insert(-2,'preview')
287
+ File.write prvw.join('.'), origin_text
288
+ else
289
+ pazz = cds[:path].split('.')
290
+ pazz.insert(-2,Time.new.strftime("%Y%m%d%H%M%S"))
291
+ File.write pazz.join('.'), origin_text
292
+ end
293
+ end
294
+ return origin_text
295
+ end
296
+ end
297
+
298
+ # Erlang 数据格式生成模块
299
+ module ToErlang
300
+ module_function
301
+
302
+ # 将二维数据转换为 Erlang 列表记录格式。
303
+ #
304
+ # 第一行作为字段名,后续行作为数据。
305
+ # 每个字段生成 {"field", "value"} 元组,每行生成 {..., ...} 元组。
306
+ #
307
+ # @param data [Array<Array>] 二维数据,第一行为字段名
308
+ # @return [String] Erlang 列表字符串
309
+ # @example
310
+ # ToErlang.table([["name", "age"], ["Alice", "30"]])
311
+ # # => "[{{\"name\", \"Alice\"}, {\"age\", \"30\"}}]"
312
+ def table data
313
+ fields = data.first
314
+ erlang_data = data.drop(1).map do |row|
315
+ row_data = fields.zip(row).map do |field, value|
316
+ "{\"#{field}\", \"#{value}\"}"
317
+ end.join(",")
318
+ "{#{row_data}}"
319
+ end
320
+ "[#{erlang_data.join(", ")}]"
321
+ end
322
+
323
+ # 将 Hash 转换为 Erlang 键值列表格式。
324
+ #
325
+ # @param data [Hash] 键值对数据
326
+ # @return [String] Erlang 键值列表字符串
327
+ # @example
328
+ # ToErlang.hash({name: "Alice", age: 30})
329
+ # # => "{{\"name\", \"Alice\"}, {\"age\", \"30\"}}"
330
+ def hash data
331
+ "{"+data.map{|k,v|"{\"#{k}\", \"#{v}\"}"}.join(",")+"}"
332
+ end
333
+
334
+ # 生成 Erlang fact 函数子句。
335
+ #
336
+ # @param name [String, Symbol] 函数名
337
+ # @param body [String] 函数体
338
+ # @return [String] Erlang 函数子句代码
339
+ # @example
340
+ # ToErlang.fact("global", "{\"key\", \"value\"}")
341
+ # # => "fact(\"global\")->\n {\"key\", \"value\"};"
342
+ def fact name, body
343
+ "fact(\"#{name.to_s}\")->\n #{body};"
344
+ end
345
+ end
@@ -0,0 +1,241 @@
1
+ #coding:utf-8
2
+
3
+ #------------------------------------------------------------------------------
4
+ # 文档结构检查与解析
5
+ #------------------------------------------------------------------------------
6
+ module CasetDown
7
+ module Check
8
+ module_function
9
+
10
+ # 遍历文档所有节点,按类型分发到各检查方法,收集并返回结果。
11
+ #
12
+ # 会依次调用 head, table, para, pre, list, quote 等方法处理每个节点。
13
+ #
14
+ # @param cds [Hash] 解析结果,必须包含 :src 键(Script 实例)
15
+ # @return [Array<Hash>] 所有节点解析后的结构化数据列表
16
+ # @example
17
+ # cds = CasetDown.load("example.md")
18
+ # elements = CasetDown::Check.all(cds)
19
+ def all cds
20
+ epool = cds[:src].stack.inject([]) do|epool, cd|
21
+ res = nil
22
+ [:head, :table, :para, :pre, :list, :quote].each do|tag|
23
+ res ||= CasetDown::Check.send tag, cd
24
+ end
25
+ epool += res.instance_of?(Array) ? res : [res]
26
+ end.compact
27
+ end
28
+
29
+ # 解析标题节点(h1 ~ h6)。
30
+ #
31
+ # 支持内联链接元素(<a> 标签)的提取。
32
+ #
33
+ # @param cd [Node] 标题 DOM 节点
34
+ # @return [Hash, nil] 解析结果
35
+ # - :name [String] 标签名 'h1' ~ 'h6'
36
+ # - :text [String] 标题文本
37
+ # - :inline [Array<Hash>] 内联链接元素(可选)
38
+ # @return [nil] 如果节点不是标题类型
39
+ def head cd
40
+ if %w{h1 h2 h3 h4 h5 h6}.include? cd.name
41
+ unless cd.elements.empty?
42
+ ha = {name: cd.name, text: cd.attributes[:text], inline: [] }
43
+ cd.elements.each{|sub|ha[:inline]<<(sub.name=='a' ? a(sub) : {})}
44
+ ha
45
+ else
46
+ {name: cd.name, text: cd.attributes[:text] }
47
+ end
48
+ else
49
+ nil
50
+ end
51
+ end
52
+
53
+ # 解析表格节点。
54
+ #
55
+ # 将表格转换为 head(表头)和 body(数据行)结构。
56
+ #
57
+ # @param cd [Node] 表格 DOM 节点
58
+ # @return [Hash, nil] 解析结果
59
+ # - :name [String] 'table'
60
+ # - :head [Array<String>] 表头列名数组
61
+ # - :body [Array<Array<String>>] 数据行二维数组
62
+ # @return [nil] 如果节点不是表格类型
63
+ def table cd
64
+ if cd.name=='table'
65
+ hd = cd.to_doc['table'][1]['thead'][1]['tr'][1..-1].map{|th|th['th'][0][:text].strip}
66
+ bd = cd.to_doc['table'][2]['tbody'][1..-1].map{|tr|tr['tr'][1..-1].map{|td|td['td'][0][:text].strip}}
67
+ {name: 'table', head: hd, body: bd}
68
+ else
69
+ nil
70
+ end
71
+ end
72
+
73
+ # 解析段落节点,支持内联代码、图片、链接元素。
74
+ #
75
+ # 段落文本存储在 attributes[:text] 中,内联元素通过子节点遍历提取。
76
+ #
77
+ # @param cd [Node] 段落 DOM 节点
78
+ # @return [Array<Hash>, nil] 段落内容及其内联元素数组
79
+ # - 首元素通常为 {name: 'p', text: String}
80
+ # - 后续元素为内联的 code, img, a 等
81
+ # @return [nil] 如果节点不是段落类型
82
+ def para cd
83
+ if cd.name=='p'
84
+ cur = []
85
+ cur << (cd.attributes[:text] ? {name: 'p', text: cd.attributes[:text]||cd.attributes["text"]} : nil)
86
+ cd.elements.each do|cs|
87
+ cur << code(cs)
88
+ cur << img(cs)
89
+ cur << a(cs)
90
+ end
91
+ return cur
92
+ else
93
+ nil
94
+ end
95
+ end
96
+
97
+ # 解析图片节点。
98
+ #
99
+ # @param cd [Node] 图片 DOM 节点
100
+ # @return [Hash, nil] 解析结果
101
+ # - :name [String] 'img'
102
+ # - :src [String] 图片 URL
103
+ # - :alt [String] 替代文本
104
+ # @return [nil] 如果节点不是图片类型
105
+ def img cd
106
+ if cd.name=='img'
107
+ {
108
+ name: 'img',
109
+ src: (cd.attributes['src'] || cd.attributes[:src]),
110
+ alt: (cd.attributes['alt'] || cd.attributes[:alt])
111
+ }
112
+ else
113
+ nil
114
+ end
115
+ end
116
+
117
+ # 解析链接节点。
118
+ #
119
+ # @param cd [Node] 链接 DOM 节点
120
+ # @return [Hash, nil] 解析结果
121
+ # - :name [String] 'a'
122
+ # - :href [String] 链接目标 URL
123
+ # - :text [String] 链接显示文本
124
+ # @return [nil] 如果节点不是链接类型
125
+ def a cd
126
+ if cd.name=='a'
127
+ {
128
+ name: 'a',
129
+ href: (cd.attributes['href'] || cd.attributes[:href]),
130
+ text: cd.attributes[:text]
131
+ }
132
+ else
133
+ nil
134
+ end
135
+ end
136
+
137
+ # 解析代码块容器 <pre> 节点。
138
+ #
139
+ # 实际调用 code 方法处理内部 <code> 子节点。
140
+ #
141
+ # @param cd [Node] <pre> DOM 节点
142
+ # @return [Hash, nil] code 方法的返回结果
143
+ # @return [nil] 如果节点不是 <pre> 类型
144
+ def pre cd
145
+ if cd.name=='pre'
146
+ return {
147
+ name: 'pre',
148
+ text: cd.to_doc['pre'][0][:text]
149
+ } if cd.to_doc['pre'][0][:text]
150
+ return code(cd.elements.first) if cd.elements.first.name=='code'
151
+ else
152
+ nil
153
+ end
154
+ end
155
+
156
+ # 解析内联/块级代码节点。
157
+ #
158
+ # @param cd [Node] <code> DOM 节点
159
+ # @return [Hash, nil] 解析结果
160
+ # - :name [String] 'code'
161
+ # - :class [String, Symbol] 代码语言类,默认 :default
162
+ # - :code [String] 代码文本内容
163
+ # @return [nil] 如果节点不是 <code> 类型
164
+ def code cd
165
+ if cd.name=='code'
166
+ {
167
+ name: 'code',
168
+ class: (
169
+ cd.attributes['class'] ? cd.attributes['class'] :
170
+ cd.attributes[:class] ? cd.attributes[:class] :
171
+ :default
172
+ ),
173
+ code: cd.attributes[:text].strip
174
+ }
175
+ else
176
+ nil
177
+ end
178
+ end
179
+
180
+ # 解析无序列表节点 <ul> 和 有序列表节点 <ol>。
181
+ #
182
+ # 注意:当前仅支持顶层列表,子列表递归处理待实现。
183
+ # 列表项支持内联的 blockquote、code、img、a 等元素。
184
+ #
185
+ # @param cd [Node] <ul> <ol> DOM 节点
186
+ # @return [Hash, nil] 解析结果
187
+ # - :name [String] 'ul' 'ol'
188
+ # - :list [Array<Hash>] 列表项数组,每项包含 :name, :text, :inline
189
+ # @return [nil] 如果节点不是 <ul> <ol> 类型
190
+ def list cd
191
+ # TODO: ONLY TOP LEVEL, PENDING TO RECURSION
192
+ if cd.name=='ul'
193
+ subs = cd.elements.inject([]) do|subs, li|
194
+ sub = {name: 'li', text: (li.attributes[:text] ? li.attributes[:text].strip : li.attributes['text'] ? li.attributes['text'].strip : nil)}
195
+ li.elements.each do|subsub|
196
+ sub.merge!(inline: quote(subsub)) if subsub.name=='blockquote'
197
+ sub.merge!(inline: code(subsub)) if subsub.name=='code'
198
+ sub.merge!(inline: pre(subsub)) if subsub.name=='pre'
199
+ sub.merge!(inline: img(subsub)) if subsub.name=='img'
200
+ sub.merge!(inline: a(subsub)) if subsub.name=='a'
201
+ end
202
+ subs << sub
203
+ end
204
+ {name: 'ul', list: subs}
205
+ elsif cd.name=='ol'
206
+ subs = cd.elements.inject([]) do|subs, li|
207
+ sub = {name: 'li', text: (li.attributes[:text] ? li.attributes[:text].strip : li.attributes['text'] ? li.attributes['text'].strip : nil)}
208
+ li.elements.each do|subsub|
209
+ sub.merge!(inline: quote(subsub)) if subsub.name=='blockquote'
210
+ sub.merge!(inline: code(subsub)) if subsub.name=='code'
211
+ sub.merge!(inline: pre(subsub)) if subsub.name=='pre'
212
+ sub.merge!(inline: img(subsub)) if subsub.name=='img'
213
+ sub.merge!(inline: a(subsub)) if subsub.name=='a'
214
+ end
215
+ subs << sub
216
+ end
217
+ {name: 'ol', list: subs}
218
+ else
219
+ nil
220
+ end
221
+ end
222
+
223
+ # 解析引用块 <blockquote>。
224
+ #
225
+ # 注意:当前仅支持顶层引用,嵌套处理待实现。
226
+ #
227
+ # @param cd [Node] <blockquote> DOM 节点
228
+ # @return [Hash, nil] 解析结果
229
+ # - :name [String] 'blockquote'
230
+ # - :quote [String] 引用文本内容
231
+ # @return [nil] 如果节点不是 <blockquote> 类型
232
+ def quote cd
233
+ # TODO: ONLY TOP LEVEL, PENDING TO RECURSION
234
+ if cd.name=='blockquote'
235
+ {name: 'blockquote', quote: cd.to_doc['blockquote'][0][:text].to_s.strip}
236
+ else
237
+ nil
238
+ end
239
+ end
240
+ end
241
+ end