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.
- checksums.yaml +7 -0
- data/README.md +180 -0
- data/bin/cm +15 -0
- data/bin/cml +15 -0
- data/doc/MANUAL.md +100 -0
- data/doc/sample.csv +12 -0
- data/doc/sample.md +60 -0
- data/lib/caset/gen_bdd.rb +121 -0
- data/lib/caset/gen_caset.rb +41 -0
- data/lib/caset/gen_pytest.rb +211 -0
- data/lib/caset/gen_spec.rb +36 -0
- data/lib/caset/version.rb +3 -0
- data/lib/caset.rb +272 -0
- data/lib/casetdown/casetable.rb +121 -0
- data/lib/casetdown/casetcode.rb +345 -0
- data/lib/casetdown/casetdoc.rb +241 -0
- data/lib/casetdown/casetter.rb +141 -0
- data/lib/casetdown/cli.rb +129 -0
- data/lib/casetdown/live.rb +102 -0
- data/lib/casetdown/support/api_app.rb +127 -0
- data/lib/casetdown/support/endata.rb +197 -0
- data/lib/casetdown/support/simtab.rb +372 -0
- data/lib/casetdown/support/text_abstract.rb +124 -0
- data/lib/casetdown/support/tintext.rb +141 -0
- data/lib/casetdown.rb +48 -0
- metadata +79 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#coding:utf-8
|
|
2
|
+
require 'csv'
|
|
3
|
+
|
|
4
|
+
#------------------------------------------------------------------------------
|
|
5
|
+
# 二维表处理工具(SimTab)
|
|
6
|
+
#------------------------------------------------------------------------------
|
|
7
|
+
# 移植自 imdoc/Tabot/simtab,用于符合人类直觉的二维表处理,方便处理报表。
|
|
8
|
+
#
|
|
9
|
+
# 一般格式是 [[表头], [记录], ...],所有的操作处理都是针对记录、不针对表头的;
|
|
10
|
+
# 表头的格式由人主观决定,只提供绑定方法。
|
|
11
|
+
#
|
|
12
|
+
# 本模块通过扩展 Array 使任意二维数组获得表格操作能力,
|
|
13
|
+
# casetdown 的表格导入导出(casetable.rb)与脚本数据绑定(EnData)都依赖它。
|
|
14
|
+
#------------------------------------------------------------------------------
|
|
15
|
+
module SimTab
|
|
16
|
+
##################################################################
|
|
17
|
+
# 说明
|
|
18
|
+
# 这个模块用于符合人类直觉的二维表处理,方便处理报表
|
|
19
|
+
# 一般格式是 [[表头], [记录], ...]
|
|
20
|
+
# 所有的操作处理都是针对记录不针对表头的
|
|
21
|
+
# 表头的格式由人主观决定,只提供绑定方法
|
|
22
|
+
##################################################################
|
|
23
|
+
|
|
24
|
+
##################################################################
|
|
25
|
+
# 读取报表
|
|
26
|
+
##################################################################
|
|
27
|
+
|
|
28
|
+
# 读取分隔符文本报表为二维表。
|
|
29
|
+
#
|
|
30
|
+
# 默认按 Tab 分隔;UTF-8 读取失败时按 GBK 转码重读。
|
|
31
|
+
#
|
|
32
|
+
# @param 路径 [String] 报表文件路径
|
|
33
|
+
# @param 分隔符 [String] 列分隔符,默认 "\t"
|
|
34
|
+
# @return [Array<Array<String>>] 二维表
|
|
35
|
+
def self.读取报表 路径,分隔符="\t"
|
|
36
|
+
begin
|
|
37
|
+
File.read(路径).gsub("\r","").split("\n").map{|l|l.split(分隔符)}
|
|
38
|
+
rescue
|
|
39
|
+
File.read(路径).force_encoding('GBK').encode("UTF-8").gsub("\r","").split("\n").map{|l|l.split(分隔符)}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# 读取 CSV 报表为二维表(UTF-8 优先,GBK 兼容回退,自动剥离 BOM)。
|
|
44
|
+
#
|
|
45
|
+
# @param 路径 [String] CSV 文件路径
|
|
46
|
+
# @param format [Symbol] :raw 原始二维数组 / :csv 带表头解析
|
|
47
|
+
# @return [Array<Array>] 绑定表头后的二维表
|
|
48
|
+
def self.读取csv 路径,format=:raw
|
|
49
|
+
require 'csv'
|
|
50
|
+
raw = File.binread(路径)
|
|
51
|
+
raw = raw.sub(/\A\xEF\xBB\xBF/n, '') # 剥离 UTF-8 BOM
|
|
52
|
+
csv = raw.dup.force_encoding('UTF-8')
|
|
53
|
+
csv = raw.dup.force_encoding('GBK').encode('UTF-8') unless csv.valid_encoding?
|
|
54
|
+
content = CSV.parse(csv, headers:(format==:csv))
|
|
55
|
+
table = content.绑定表头 content[0]
|
|
56
|
+
return table
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# 读取 HTML 文档中的表格为二维表。
|
|
60
|
+
#
|
|
61
|
+
# 原版基于 Nokogiri;本移植为零依赖实现,使用宽松正则扫描
|
|
62
|
+
# <table>/<tr>/<td|th> 结构,适用于常规表格型 HTML(含 markdown 渲染产物)。
|
|
63
|
+
#
|
|
64
|
+
# @param 文档 [String] HTML 文档文本
|
|
65
|
+
# @return [Array<Array<String>>] 所有表格行拼接成的二维表
|
|
66
|
+
def self.读取html 文档
|
|
67
|
+
table = []
|
|
68
|
+
文档.scan(/<table[^>]*>(.*?)<\/table>/mi).each do|tmatch|
|
|
69
|
+
tbody = tmatch[0]
|
|
70
|
+
tbody.scan(/<tr[^>]*>(.*?)<\/tr>/mi).each do|rmatch|
|
|
71
|
+
tr = rmatch[0]
|
|
72
|
+
record = tr.scan(/<(?:td|th)[^>]*>(.*?)<\/(?:td|th)>/mi).map{|c|c[0].gsub(/<[^>]+>/,'').strip}
|
|
73
|
+
table << record
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
return table
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##################################################################
|
|
80
|
+
# 表头
|
|
81
|
+
# 如果已有表头,只需要'#绑定表头'指定表头即可
|
|
82
|
+
# 如果没有表头,可以'#表头'自动生成表头,也可以指定'#生成表头'再'#绑定表头'
|
|
83
|
+
##################################################################
|
|
84
|
+
|
|
85
|
+
# 按当前最大列数自动生成表头("0","1","2"...),并插入为首行。
|
|
86
|
+
#
|
|
87
|
+
# @param 前缀 [String] 表头前缀(历史遗留参数,原实现未使用,保持不动)
|
|
88
|
+
# @return [Array<String>] 生成的表头
|
|
89
|
+
def 生成表头 前缀=''
|
|
90
|
+
max = self.inject(0){|max,item|(max < item.size ? item.size : max)}
|
|
91
|
+
@表头 = Array.new(max){|i|i.to_s}
|
|
92
|
+
self.unshift @表头
|
|
93
|
+
@表头
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# 绑定表头到当前表,并递归绑定每条记录的内联表头。
|
|
97
|
+
#
|
|
98
|
+
# @param 表头 [Array] 表头数组;非数组入参将被忽略
|
|
99
|
+
# @return [Array] 当前表自身
|
|
100
|
+
def 绑定表头 表头
|
|
101
|
+
@表头 = 表头 if 表头.instance_of?(Array)
|
|
102
|
+
self.each do|记录|
|
|
103
|
+
记录.绑定表头 表头 if 记录.instance_of?(Array)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 读取表头;未绑定时自动生成并绑定(注意会修改当前表)。
|
|
108
|
+
#
|
|
109
|
+
# @return [Array] 表头
|
|
110
|
+
def 表头
|
|
111
|
+
@表头 ||= []
|
|
112
|
+
if !@表头 or @表头.empty?
|
|
113
|
+
自定义表头 = 生成表头
|
|
114
|
+
绑定表头 自定义表头
|
|
115
|
+
end
|
|
116
|
+
@表头
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 去掉形式上的表头(首行),记录的内联表头保留以便恢复。
|
|
120
|
+
#
|
|
121
|
+
# 如想彻底去掉每条记录的表头,可再绑定一个空表头。
|
|
122
|
+
#
|
|
123
|
+
# @return [Array] 当前表自身
|
|
124
|
+
def 去掉表头
|
|
125
|
+
@表头 = []
|
|
126
|
+
self.shift
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# 展示表头与列号的对应关系("00: 名称" 每行一条)。
|
|
131
|
+
#
|
|
132
|
+
# @return [String] 多行文本
|
|
133
|
+
def 展示表头
|
|
134
|
+
索引表 = []
|
|
135
|
+
@表头.each_with_index{|c,i|索引表 << "#{"%02s"%i}: #{c}"}
|
|
136
|
+
索引表.join("\n")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# 特殊的双层表头,计算时转化为单层(原单层不变)。
|
|
140
|
+
#
|
|
141
|
+
# 复合表头形如 ["上层#下层", ...],双层列表转为 "上层#下层" 单层数组。
|
|
142
|
+
#
|
|
143
|
+
# @param 列表 [Array<Array<String>>] 双层表头
|
|
144
|
+
# @return [Array] 单层复合表头
|
|
145
|
+
def self.解析双层表头 列表
|
|
146
|
+
return 列表 unless 列表[0].instance_of?(Array)
|
|
147
|
+
表头 = []
|
|
148
|
+
列表[0].each_with_index do|item,index|
|
|
149
|
+
item=='' and index==0 and head = ''
|
|
150
|
+
item=='' and index >0 and head = 表头[-1].split("#")[0]
|
|
151
|
+
item!='' and head = item
|
|
152
|
+
表头 << [head, 列表[1][index]].join("#")
|
|
153
|
+
end if 列表.size == 2 && 列表[0].instance_of?(Array)
|
|
154
|
+
if 列表[0].size < 列表[1].size
|
|
155
|
+
num = 列表[1].size - 列表[0].size
|
|
156
|
+
head = 表头[-1].split("#")[0]
|
|
157
|
+
表头 += 列表[1][-1*num..-1].map{|i|"#{head}##{i}"}
|
|
158
|
+
end
|
|
159
|
+
return 表头
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# 特殊的双层表头,输出前转化为多层(原单层不变)。
|
|
163
|
+
#
|
|
164
|
+
# @param 列表 [Array<String>] 单层复合表头
|
|
165
|
+
# @return [Array<Array<String>>] 双层表头
|
|
166
|
+
def self.生成双层表头 列表
|
|
167
|
+
表头 = 列表.map{|复合表头|复合表头.split("#")}
|
|
168
|
+
return [列表] if 表头[0].size==1
|
|
169
|
+
row1,row2 = [],[]
|
|
170
|
+
表头.each do|items|
|
|
171
|
+
row1 << items[0]
|
|
172
|
+
row2 << items[1]
|
|
173
|
+
end
|
|
174
|
+
return [row1,row2]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
##################################################################
|
|
178
|
+
# 查询(行)、选择(列)、排序
|
|
179
|
+
# 这里的计算结果都是生成新表
|
|
180
|
+
##################################################################
|
|
181
|
+
|
|
182
|
+
# 按字段值等值查询,生成新表。
|
|
183
|
+
#
|
|
184
|
+
# @param 字段 [String] 表头字段名
|
|
185
|
+
# @param 内容 [String] 期望的字段值
|
|
186
|
+
# @return [Array<Array>] 查询结果新表(携带表头)
|
|
187
|
+
def 字段查询 字段,内容
|
|
188
|
+
索引 = self.表头.index(字段)
|
|
189
|
+
新表 = [self.表头]+self.select{|c|c[索引]==内容}
|
|
190
|
+
新表.绑定表头 self.表头
|
|
191
|
+
新表
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# 按 block 条件查询记录,生成新表。
|
|
195
|
+
#
|
|
196
|
+
# @yield [记录] 对每条记录求值,真值保留
|
|
197
|
+
# @return [Array<Array>] 查询结果新表(携带表头)
|
|
198
|
+
def 查询
|
|
199
|
+
结果 = []
|
|
200
|
+
self[1..-1].each do|记录|
|
|
201
|
+
yield(记录) and (结果.push 记录)
|
|
202
|
+
end
|
|
203
|
+
新表 = [self.表头]+结果
|
|
204
|
+
新表.绑定表头 self.表头
|
|
205
|
+
新表
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# 按 block 生成的键自定义排序,生成新表。
|
|
209
|
+
#
|
|
210
|
+
# yield 返回 [键, 逆序标志],逆序标志为 :reverse 时结果取逆。
|
|
211
|
+
#
|
|
212
|
+
# @yield [记录] 返回 [排序键, :reverse或nil]
|
|
213
|
+
# @return [Array<Array>] 排序结果新表(携带表头)
|
|
214
|
+
def 自定义排序
|
|
215
|
+
字典,逆序,counter = {},nil,0
|
|
216
|
+
self[1..-1].each_with_index do|记录,索引|
|
|
217
|
+
键,逆序 = yield(记录)
|
|
218
|
+
字典[键+"_#{"%010d"%counter}"] = 索引+1
|
|
219
|
+
counter += 1
|
|
220
|
+
end
|
|
221
|
+
结果 = 字典.keys.sort.inject([]) do|结果,键|
|
|
222
|
+
结果 << self[字典[键]]
|
|
223
|
+
结果
|
|
224
|
+
end
|
|
225
|
+
逆序==:reverse and 结果.reverse!
|
|
226
|
+
新表 = [self[0]]+结果
|
|
227
|
+
新表.绑定表头 self.表头
|
|
228
|
+
新表
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# 按记录自然顺序排序,生成新表。
|
|
232
|
+
#
|
|
233
|
+
# @return [Array<Array>] 排序结果新表(携带表头)
|
|
234
|
+
def 排序
|
|
235
|
+
新表 = [self[0]]+self[1..-1].sort
|
|
236
|
+
新表.绑定表头 self.表头
|
|
237
|
+
新表
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# 记录逆序,生成新表。
|
|
241
|
+
#
|
|
242
|
+
# @return [Array<Array>] 逆序新表(携带表头)
|
|
243
|
+
def 逆序
|
|
244
|
+
新表 = [self[0]]+self[1..-1].reverse
|
|
245
|
+
新表.绑定表头 self.表头
|
|
246
|
+
新表
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# 可以使用“.字段(index|"column"|...)”查询,找不到查询名称的场合用名称做占位符。
|
|
250
|
+
#
|
|
251
|
+
# 提取若干列:按列号或表头名取列;找不到名称时该列输出字段名本身作占位。
|
|
252
|
+
# 单字段返回该列数组,多字段返回新表。
|
|
253
|
+
#
|
|
254
|
+
# @param 属性表 [Integer, String, ...] 列号或字段名
|
|
255
|
+
# @return [Array<Array>, Array] 多字段返回新表,单字段返回列数组
|
|
256
|
+
def 字段 *属性表
|
|
257
|
+
字段表,新表头 = [],[]
|
|
258
|
+
属性表.each do|属性|
|
|
259
|
+
if 索引 = self.表头.index(属性)
|
|
260
|
+
字段表 << self[索引]
|
|
261
|
+
新表头 << 属性
|
|
262
|
+
else
|
|
263
|
+
字段表 << 属性
|
|
264
|
+
新表头 << 属性.to_s
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
if 字段表.size>1
|
|
268
|
+
字段表.绑定表头 新表头
|
|
269
|
+
字段表
|
|
270
|
+
else
|
|
271
|
+
字段表[0]
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# 列投影:按列号或表头名选择若干列,生成新表(找不到名称的场合用名称做占位符)。
|
|
276
|
+
#
|
|
277
|
+
# select 在集合中指选择符合条件的某些元素,在SQL中指投影到具体的列上,
|
|
278
|
+
# 这里偏向后者,前者可以对应查询。
|
|
279
|
+
#
|
|
280
|
+
# @param 字段列表 [Integer, String, ...] 列号或表头名
|
|
281
|
+
# @return [Array<Array>] 投影结果新表(携带新表头)
|
|
282
|
+
def 选择 *字段列表 # col_num or 'col_name' or filler
|
|
283
|
+
字段索引 = 字段列表.map{|f|r = f.instance_of?(Integer) ? f : self.表头.index(f); r ? r : f }
|
|
284
|
+
新表头 = 字段列表.map{|f|r = f.instance_of?(Integer) ? self.表头[f] : f; r ? f : r }
|
|
285
|
+
新表 = [新表头]+self[1..-1].map{|记录|字段索引.map{|索引|索引.instance_of?(Integer) ? 记录[索引] : 索引}}
|
|
286
|
+
新表.绑定表头 新表头
|
|
287
|
+
新表
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# 笛卡尔拼接两张表(行对行叉积),生成新表。
|
|
291
|
+
#
|
|
292
|
+
# @param 另一个表 [Array<Array>] 右表
|
|
293
|
+
# @return [Array<Array>] 拼接结果新表(携带合并表头)
|
|
294
|
+
def 拼接 另一个表
|
|
295
|
+
新表头 = self.表头 + 另一个表.表头
|
|
296
|
+
新表 = [新表头]+self[1..-1].product(另一个表[1..-1]).map{|t1,t2|t1+t2}
|
|
297
|
+
新表.绑定表头(新表头)
|
|
298
|
+
return 新表
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
##################################################################
|
|
302
|
+
# 统计
|
|
303
|
+
##################################################################
|
|
304
|
+
|
|
305
|
+
# 按字段值分组统计,返回 字段值 => 记录数组 的哈希。
|
|
306
|
+
#
|
|
307
|
+
# @param 字段 [String] 表头字段名
|
|
308
|
+
# @return [Hash{String => Array<Array>}] 分组结果
|
|
309
|
+
def 统计 字段
|
|
310
|
+
结果 = {}
|
|
311
|
+
索引 = self[0].index(字段)
|
|
312
|
+
self[1..-1].each do|记录|
|
|
313
|
+
结果[ 记录[索引] ] ||= []
|
|
314
|
+
结果[ 记录[索引] ] << 记录
|
|
315
|
+
end if 索引
|
|
316
|
+
结果
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# 按字段做唯一值/条目数统计,并把结果写入文件。
|
|
320
|
+
#
|
|
321
|
+
# 生成 "<路径>/字段唯一统计.txt" 与 "<路径>/字段数值统计/<字段>(<n>).txt"。
|
|
322
|
+
#
|
|
323
|
+
# @param 路径 [String] 统计输出目录,默认 '.'
|
|
324
|
+
# @return [void]
|
|
325
|
+
def 字段条目数统计 路径='.'
|
|
326
|
+
字段唯一统计 = []
|
|
327
|
+
Dir.mkdir(路径) unless File.exist?(路径)
|
|
328
|
+
self[0].each do|字段|
|
|
329
|
+
统计表 = self.统计 字段
|
|
330
|
+
字段数值统计 = []
|
|
331
|
+
统计表.each do|字段值,条目|
|
|
332
|
+
字段数值统计 << "#{字段值}\t:\t#{条目.size}"
|
|
333
|
+
end
|
|
334
|
+
字段唯一统计 << "#{字段}\t:\t#{统计表.keys.size}"
|
|
335
|
+
Dir.mkdir("#{路径}/字段数值统计") unless File.exist?("#{路径}/字段数值统计")
|
|
336
|
+
File.write "#{路径}/字段数值统计/#{字段}(#{统计表.keys.size}).txt", 字段数值统计.join("\n")
|
|
337
|
+
end
|
|
338
|
+
File.write "#{路径}/字段唯一统计.txt", 字段唯一统计.join("\n")
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# 把表格档案化为对象数组(每条记录带 'row' 行号与各字段键值)。
|
|
342
|
+
#
|
|
343
|
+
# @return [Array<Hash>] 档案数组
|
|
344
|
+
def 建立档案
|
|
345
|
+
文档 = []
|
|
346
|
+
self[1..-1].each_with_index do|记录,记录索引|
|
|
347
|
+
record = {'row' => 记录索引+1 }
|
|
348
|
+
记录.each_with_index do|字段,字段索引|
|
|
349
|
+
record[ self.表头[字段索引] ] = 字段
|
|
350
|
+
end
|
|
351
|
+
文档 << record
|
|
352
|
+
end
|
|
353
|
+
return 文档
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
##################################################################
|
|
357
|
+
# 排版
|
|
358
|
+
##################################################################
|
|
359
|
+
|
|
360
|
+
# 以 Tab 连接各列、换行连接各行,展示表格。
|
|
361
|
+
#
|
|
362
|
+
# @return [String] 多行文本
|
|
363
|
+
def 展示列表
|
|
364
|
+
self.map{|记录|记录.join("\t")}.join("\n")
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
class Array
|
|
370
|
+
attr_reader :表头
|
|
371
|
+
include SimTab
|
|
372
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#coding:utf-8
|
|
2
|
+
|
|
3
|
+
#------------------------------------------------------------------------------
|
|
4
|
+
# 文本段落抽取工具(TextAbstract)
|
|
5
|
+
#------------------------------------------------------------------------------
|
|
6
|
+
# 移植自 imdoc/TextUtils/text_abstract,为 casetdown 提供标记段落定位能力:
|
|
7
|
+
# - search_output 依赖 match_paragraph 在文档中查找 "#output>xxx" 代码块
|
|
8
|
+
# - draw_fragments / match_cascade / match_html_tag 保留原模块完整功能
|
|
9
|
+
#
|
|
10
|
+
# 本文件为零依赖纯 Ruby 实现,随 caset gem 一起发布。
|
|
11
|
+
#------------------------------------------------------------------------------
|
|
12
|
+
module TextAbstract
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# 按前置/后置标记抽取文本片段(特定环境遗留方法,不推荐常用)。
|
|
16
|
+
#
|
|
17
|
+
# 遇到 pre_flag 匹配行时开始收集,遇到 post_flag 匹配行时结束;
|
|
18
|
+
# 标记行本身包含在片段内,未闭合的片段追加到上下文。
|
|
19
|
+
#
|
|
20
|
+
# @param text [String] 原始文本
|
|
21
|
+
# @param pre_flag [Regexp] 开始标记(行级正则)
|
|
22
|
+
# @param post_flag [Regexp] 结束标记(行级正则)
|
|
23
|
+
# @return [Array<String>] 二元组 [上下文文本, 片段数组]
|
|
24
|
+
def draw_fragments text,pre_flag,post_flag
|
|
25
|
+
context,fragments = [],[]
|
|
26
|
+
temp,flag = "",false
|
|
27
|
+
text.each_line do|line|
|
|
28
|
+
if pre_flag.match(line)
|
|
29
|
+
temp = String.new if flag == false
|
|
30
|
+
flag = true
|
|
31
|
+
end
|
|
32
|
+
if flag == true
|
|
33
|
+
temp << line # base on line!
|
|
34
|
+
else
|
|
35
|
+
context << line
|
|
36
|
+
end
|
|
37
|
+
if post_flag.match(line)
|
|
38
|
+
flag = false
|
|
39
|
+
fragments << temp
|
|
40
|
+
temp = String.new
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
context << temp if flag == true # exist pre_flag but no post_flag
|
|
44
|
+
fragments.delete("")
|
|
45
|
+
return context.join(),fragments
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# 严格顺序匹配开始/结束标记之间的段落。
|
|
49
|
+
#
|
|
50
|
+
# start 和 finish 有严格先后顺序:一段未结束不开启另一段。
|
|
51
|
+
# 返回匹配到的段落正文数组(不含首尾标记本身)。
|
|
52
|
+
#
|
|
53
|
+
# @param text [String] 完整文档文本
|
|
54
|
+
# @param start [String] 开始标记(严格字符串包含匹配)
|
|
55
|
+
# @param finish [String] 结束标记(严格字符串包含匹配)
|
|
56
|
+
# @return [Array<String>] 段落正文列表
|
|
57
|
+
# @example
|
|
58
|
+
# TextAbstract.match_paragraph("```shell\n#out>a\nX\n```", "```shell\n#out>a", "```")
|
|
59
|
+
# # => ["\nX\n"]
|
|
60
|
+
def match_paragraph text,start,finish
|
|
61
|
+
paragraphs,current_text = [],text.clone
|
|
62
|
+
until current_text==""
|
|
63
|
+
return paragraphs unless current_text.include?(start) # 严格匹配开始
|
|
64
|
+
start_split = current_text.split(start)
|
|
65
|
+
current_text = start_split[1..-1].join(start)
|
|
66
|
+
return paragraphs unless current_text.include?(finish) # 严格匹配结束
|
|
67
|
+
finish_split = current_text.split(finish)
|
|
68
|
+
context = finish_split[0]
|
|
69
|
+
current_text = finish_split[1..-1].join(finish)+(text[-1*finish.size..-1]==finish ? finish : "")
|
|
70
|
+
paragraphs << context
|
|
71
|
+
end
|
|
72
|
+
return paragraphs
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 可嵌套匹配开始/结束标记,返回树状段落结构。
|
|
76
|
+
#
|
|
77
|
+
# start 和 finish 可以嵌套(一个 start/finish 包含在另一个内部),
|
|
78
|
+
# 但全局上仍保持顺序。返回嵌套数组:字符串为内容,数组为下一层级。
|
|
79
|
+
#
|
|
80
|
+
# @param text [String] 完整文档文本
|
|
81
|
+
# @param start [String] 开始标记
|
|
82
|
+
# @param finish [String] 结束标记
|
|
83
|
+
# @return [Array] 树状段落结构
|
|
84
|
+
def match_cascade text,start,finish
|
|
85
|
+
paragraphs,current_text = [],text
|
|
86
|
+
return paragraphs unless text.include?(start) or text.include?(finish)
|
|
87
|
+
current_pos,parent_pos = paragraphs,[paragraphs]
|
|
88
|
+
until current_text==""
|
|
89
|
+
st_pos = (current_text.include?(start) ? current_text.split(start)[0] : current_text).size
|
|
90
|
+
fn_pos = (current_text.include?(finish) ? current_text.split(finish)[0] : current_text).size
|
|
91
|
+
if fn_pos==st_pos # 无标志或标志互包含
|
|
92
|
+
current_pos << current_text
|
|
93
|
+
current_text = ""
|
|
94
|
+
end
|
|
95
|
+
if fn_pos < st_pos # 结束在前
|
|
96
|
+
csp = current_text.split(finish)
|
|
97
|
+
current_pos << csp[0]
|
|
98
|
+
current_pos << finish # 本级完成
|
|
99
|
+
cur_parent = parent_pos.pop # 取回上一级
|
|
100
|
+
current_pos = cur_parent # 返回上一级
|
|
101
|
+
current_text = current_text.sub(csp[0],"").sub(finish,"")
|
|
102
|
+
end
|
|
103
|
+
if st_pos < fn_pos # 开始在前
|
|
104
|
+
csp = current_text.split(start)
|
|
105
|
+
current_pos << csp[0] unless csp[0]=="" # 本级内容加入
|
|
106
|
+
current_pos << [start] # 下一级内容加入
|
|
107
|
+
parent_pos << current_pos # 本级加入上下文
|
|
108
|
+
current_pos = current_pos[-1]
|
|
109
|
+
current_text = current_text.sub(csp[0],"").sub(start,"")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
return paragraphs
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# 匹配 HTML 标签的嵌套结构(<tag ...> ... </tag>)。
|
|
116
|
+
#
|
|
117
|
+
# @param html [String] HTML 文本
|
|
118
|
+
# @param tag [String] 标签名(如 "table"、"div")
|
|
119
|
+
# @return [Array] 树状段落结构
|
|
120
|
+
def match_html_tag html,tag
|
|
121
|
+
paragraphs = self.match_cascade(html,"<#{tag}","</#{tag}>")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#coding:utf-8
|
|
2
|
+
require 'erb'
|
|
3
|
+
|
|
4
|
+
#------------------------------------------------------------------------------
|
|
5
|
+
# 模板替换工具(TinText / Tum / Replacement)
|
|
6
|
+
#------------------------------------------------------------------------------
|
|
7
|
+
# 移植自 imdoc/TinText(tintext.rb + tin_text.rb + tum.rb + cache.rb),
|
|
8
|
+
# 为 casetdown 的执行脚本提供轻量模板展开能力:
|
|
9
|
+
#
|
|
10
|
+
# TinText.instance(raw) 展开两类标记:
|
|
11
|
+
# ((名称)) / ${名称} => 查 Replacement 字典,未命中输出 ((名称)) 原样
|
|
12
|
+
# <<..>> => <%=..%>,{{..}} => <%..%>,[[..]] => <%..%>..<%end%>
|
|
13
|
+
#
|
|
14
|
+
# Tum 文本展开方法(repeat / iterate)
|
|
15
|
+
# Replacement 轻量字典缓存(EnData::ApiRequest.bind 亦会使用)
|
|
16
|
+
#------------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
# 轻量字典缓存:名称 => 替换文本。
|
|
19
|
+
module Replacement
|
|
20
|
+
# 初始化(清空)字典。
|
|
21
|
+
def self.init
|
|
22
|
+
@dictionary = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 重置字典(等价 init)。
|
|
26
|
+
def self.reset
|
|
27
|
+
init
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 读取替换文本。
|
|
31
|
+
#
|
|
32
|
+
# @param index [Object] 名称
|
|
33
|
+
# @return [String, nil] 替换文本,未初始化时返回 nil
|
|
34
|
+
def self.[] index
|
|
35
|
+
@dictionary[index]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 写入替换文本。
|
|
39
|
+
#
|
|
40
|
+
# @param index [Object] 名称
|
|
41
|
+
# @param replacement [String] 替换文本
|
|
42
|
+
# @return [String] 写入的替换文本
|
|
43
|
+
def self.[]= index,replacement
|
|
44
|
+
@dictionary[index]= replacement
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 批量合并哈希到字典。
|
|
48
|
+
#
|
|
49
|
+
# @param hash [Hash{Object => String}] 名称 => 替换文本
|
|
50
|
+
# @return [Hash] 合并后的字典
|
|
51
|
+
def self.merge hash
|
|
52
|
+
@dictionary.merge! hash
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# TinText 模板展开。
|
|
57
|
+
module TinText
|
|
58
|
+
# 查字典翻译单个名称,未命中时返回 "((名称))" 原样占位。
|
|
59
|
+
#
|
|
60
|
+
# @param word [Object] 名称
|
|
61
|
+
# @return [String] 替换文本或原样占位
|
|
62
|
+
def self.translate word
|
|
63
|
+
unless Replacement[word]==nil
|
|
64
|
+
return Replacement[word]
|
|
65
|
+
else
|
|
66
|
+
return "((#{word}))"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 展开 ERB 扩展记法:
|
|
71
|
+
#
|
|
72
|
+
# <<..>> => <%=..%>
|
|
73
|
+
# {{..}} => <%..%>
|
|
74
|
+
# [[..\n....]] => <%..%>..<%end%>
|
|
75
|
+
#
|
|
76
|
+
# @param raw [String] 原始文本
|
|
77
|
+
# @return [String] ERB 源码文本
|
|
78
|
+
def self.unfolder raw
|
|
79
|
+
# <<..>> => <%=..%>
|
|
80
|
+
# {{..}} => <%..%>
|
|
81
|
+
# [[..\n....]] => <%..%>..<%end%>
|
|
82
|
+
raw.gsub(/\<\<\s*(.+?)\s*\>\>/,"<%=\\1%>")
|
|
83
|
+
.gsub(/\{\{\s*(.+?)\s*\}\}/,"<%\\1%>")
|
|
84
|
+
.gsub(/\{\{\s*(.+?)\s*/,"<%\\1").gsub(/\s*\}\}/,"\\1%>")
|
|
85
|
+
.gsub(/\[\[\s*(.+?)\s*\n/,"<%\\1%>").gsub(/\]\]/,"<%end%>")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 把 ((名称)) / ${名称} 翻译为 ERB 输出表达式(查 Replacement 字典)。
|
|
89
|
+
#
|
|
90
|
+
# @param raw [String] 原始文本
|
|
91
|
+
# @return [String] ERB 源码文本
|
|
92
|
+
def self.template raw
|
|
93
|
+
# ${..} ((...)) => <%=...%>
|
|
94
|
+
raw.gsub(/\(\(\s*(.+?)\s*\)\)/,"<%=TinText::translate('\\1')%>").gsub(/\$\{\s*(.+?)\s*\}/,"<%=TinText::translate('\\1')%>")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# 模板实例化:先模板替换再 ERB 渲染(tag=:pre),或反之(tag=:post)。
|
|
98
|
+
#
|
|
99
|
+
# @param raw [String] 原始文本
|
|
100
|
+
# @param tag [Symbol] :pre 先 ((..)) 后 <<..>>;:post 反序
|
|
101
|
+
# @return [String] 渲染结果文本
|
|
102
|
+
def self.instance raw,tag=:pre
|
|
103
|
+
if tag==:pre
|
|
104
|
+
a = self.template raw
|
|
105
|
+
b = ERB.new(a).result
|
|
106
|
+
c = self.unfolder b
|
|
107
|
+
ERB.new(c).result
|
|
108
|
+
else # :post
|
|
109
|
+
a = self.unfolder raw
|
|
110
|
+
b = ERB.new(a).result
|
|
111
|
+
c = self.template b
|
|
112
|
+
ERB.new(c).result
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# template 的短别名。
|
|
117
|
+
def self.tt raw;self.template raw;end
|
|
118
|
+
# instance 的短别名。
|
|
119
|
+
def self.ti fla;self.instance fla;end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Tum = Text Unfold Method,文本展开方法。
|
|
123
|
+
module Tum
|
|
124
|
+
# 把语句重复 times 次并逐行拼接。
|
|
125
|
+
#
|
|
126
|
+
# @param statement [String] 语句文本
|
|
127
|
+
# @param times [Integer] 重复次数
|
|
128
|
+
# @return [String] 多行文本
|
|
129
|
+
def self.repeat statement, times
|
|
130
|
+
Array.new(times,statement).join("\n")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# 把语句中的 @var 依次替换为 range 各项并逐行拼接。
|
|
134
|
+
#
|
|
135
|
+
# @param statement [String] 含 @var 占位的语句
|
|
136
|
+
# @param range [Enumerable] 取值范围
|
|
137
|
+
# @return [String] 多行文本
|
|
138
|
+
def self.iterate statement, range
|
|
139
|
+
range.collect{|item| statement.gsub("@var",item.to_s) }.join("\n")
|
|
140
|
+
end
|
|
141
|
+
end
|
data/lib/casetdown.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#coding:utf-8
|
|
2
|
+
|
|
3
|
+
#------------------------------------------------------------------------------
|
|
4
|
+
# casetdown.rb - 入口文件
|
|
5
|
+
#------------------------------------------------------------------------------
|
|
6
|
+
# casetdown:可执行 Markdown 文档框架(caset 的文档执行面)。
|
|
7
|
+
#
|
|
8
|
+
# 依赖说明:
|
|
9
|
+
# - markdown 解析 / XML 解析 / XLSX 读写 -> gem:manticore-smash (require 'manticore')
|
|
10
|
+
# (XMLUtils、Tabot(Excel)、rdiscount 的重写版,见 manticore 项目)
|
|
11
|
+
# - 二维表操作 / 文本段落抽取 / 内嵌数据绑定 / 模板替换
|
|
12
|
+
# -> lib/casetdown/support/*(随本 gem 自带,零第三方依赖)
|
|
13
|
+
#
|
|
14
|
+
# 引用方式:
|
|
15
|
+
# require 'casetdown' # cm / cml 与执行脚本统一走此入口
|
|
16
|
+
#
|
|
17
|
+
# 子模块地图:
|
|
18
|
+
# casetdown/casetter 文档加载与解析(ReDiscount -> XmlParser)
|
|
19
|
+
# casetdown/casetdoc 文档结构检查(CasetDown::Check)
|
|
20
|
+
# casetdown/casetcode 代码块提取与执行引擎(含 ToErlang)
|
|
21
|
+
# casetdown/casetable 表格导入导出与渲染
|
|
22
|
+
# casetdown/cli cm 命令实现
|
|
23
|
+
# casetdown/live cml 命令实现(文件监听渲染)
|
|
24
|
+
# casetdown/support TextAbstract / SimTab / EnData / TinText / ApiRequest
|
|
25
|
+
#------------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
require 'manticore'
|
|
28
|
+
|
|
29
|
+
[
|
|
30
|
+
'casetdown/support/text_abstract',
|
|
31
|
+
'casetdown/support/simtab',
|
|
32
|
+
'casetdown/support/endata',
|
|
33
|
+
'casetdown/support/tintext',
|
|
34
|
+
'casetdown/casetter',
|
|
35
|
+
'casetdown/casetdoc',
|
|
36
|
+
'casetdown/casetcode',
|
|
37
|
+
'casetdown/casetable',
|
|
38
|
+
'casetdown/cli',
|
|
39
|
+
'casetdown/live'
|
|
40
|
+
].each{|lib|require_relative lib}
|
|
41
|
+
|
|
42
|
+
begin
|
|
43
|
+
# EnData::ApiRequest(API-Request 扩展)依赖外部 gem webot(提供 weboot 接口)。
|
|
44
|
+
# 未安装 webot 时跳过,不影响 cm / cml 与数据绑定主功能。
|
|
45
|
+
require_relative 'casetdown/support/api_app'
|
|
46
|
+
rescue LoadError
|
|
47
|
+
# webot 未安装:EnData::ApiRequest 扩展不可用
|
|
48
|
+
end
|