cc 1.3.0 → 1.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.
- checksums.yaml +4 -4
- data/README.md +29 -3
- data/cc.rb +1 -0
- data/text-client.rb +297 -0
- data/text-server.rb +403 -0
- data/text-tools.rb +418 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 560df541d3d489827cf9873d8ad9b78efb20e454824728462dff70fd955b477a
|
|
4
|
+
data.tar.gz: 235e3d4bc81f79b5bc3eb72303ef4df9ae1f724753a7484f451fff75b9f7bb53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90a657521953f6e3a9b69b7aef92f41f2f10c5e87faa858717df4f6551cf06e7a83d16836c2fb5aab053254b48be0281908707ea6073acdc03e3c5de7b42f858
|
|
7
|
+
data.tar.gz: 2c250b3c62437a27116357cc331947cac3532691f2dcbb116d7814ff1c8e5fb9f585301498933e84f711471a1f8e34b6f9528b8448682622ce32903fded145cf
|
data/README.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
-
# custom-core
|
|
2
|
-
|
|
3
|
-
CC
|
|
1
|
+
# custom-core
|
|
2
|
+
|
|
3
|
+
CC
|
|
4
|
+
|
|
5
|
+
## textmind(text-tools / text-server / text-client)
|
|
6
|
+
|
|
7
|
+
移植自 imdoc/TextUtils 的文本直觉操作族,v1.4.0 起随 cc 自带,三个文件各司其职:
|
|
8
|
+
|
|
9
|
+
- **text-tools.rb** —— 操作库本体:`TextAbstract` 模块函数(draw_fragments / match_paragraph /
|
|
10
|
+
match_cascade / match_html_tag)+ String/Array/Hash 的直觉操作扩展
|
|
11
|
+
(`head?` `tail?` `matches` `cut` `-` `/` `exchange` `catch` `match` `sub` `get` `set` `+` …)。
|
|
12
|
+
加载方式:`require 'cc'; CC.use 'text-tools'`
|
|
13
|
+
|
|
14
|
+
- **text-server.rb** —— **S 端**:命令行启动解析服务(原 config.ru 的改造版)。
|
|
15
|
+
`ruby text-server.rb [--port 9292] [--host 0.0.0.0]`,
|
|
16
|
+
40 个 POST 端点(/type/* /text/* /list/* /tree/*),路由与参数名和原服务一致,
|
|
17
|
+
原 Postman 集合可继续使用;CORS 由 rack-cors 依赖改为手工响应头。
|
|
18
|
+
|
|
19
|
+
- **text-client.rb** —— **C 端**:命令行传入解析。
|
|
20
|
+
`ruby text-client.rb <ns> <op> [参数...]`,默认本地执行(不依赖服务),
|
|
21
|
+
`--remote URL` 时改走 S 端,本地/远程结果一致;数据走 `--text/--list/--tree` 或管道 stdin。
|
|
22
|
+
|
|
23
|
+
```shell
|
|
24
|
+
ruby text-client.rb type int --radix 2 --text 100 # => 4
|
|
25
|
+
echo "abedef" | ruby text-client.rb text exchange --oldtext e --newtext z
|
|
26
|
+
ruby text-client.rb list map --func "proc{|c| c * 2}" --list '[1,2,3]'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
测试:`ruby -I . tests/text_tools_test.rb`(另有 text_server_test / text_client_test)
|
data/cc.rb
CHANGED
data/text-client.rb
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#coding:utf-8
|
|
3
|
+
|
|
4
|
+
############################################################################################################
|
|
5
|
+
# text-client —— C 端:textmind 命令行传入解析 #
|
|
6
|
+
# #
|
|
7
|
+
# 移植改造自 imdoc/TextUtils 的 tm CLI 与 config.ru 使用方式: #
|
|
8
|
+
# - 默认本地执行(加载同目录 text-tools.rb,不依赖任何网络与服务) #
|
|
9
|
+
# - --remote URL 时走 S 端(text-server.rb)服务解析,本地/远程结果一致 #
|
|
10
|
+
# - 操作名与参数名和 S 端 URL 路由一一对应,Postman 集合里的用法可原样翻译成命令行 #
|
|
11
|
+
# #
|
|
12
|
+
# 用法: #
|
|
13
|
+
# ruby text-client.rb <namespace> <op> [参数...] #
|
|
14
|
+
# namespace: type | text | list | tree #
|
|
15
|
+
# op: 操作名(可含子路径,如 match/paragraph、draw/lines) #
|
|
16
|
+
# #
|
|
17
|
+
# 数据输入: #
|
|
18
|
+
# --text "字符串" 文本/类型操作输入(或 --file PATH,或管道 stdin) #
|
|
19
|
+
# --list '[1,2,3]' 列表操作输入(JSON 数组) #
|
|
20
|
+
# --tree '{"a":1}' 树操作输入(JSON 对象) #
|
|
21
|
+
# 布尔开关(如 --strip)出现即置 true;--patterns/--attaches/--oprands/--oprand 若以 [ 或 { 开头按 JSON #
|
|
22
|
+
# 解析,与 S 端 JSON 请求体语义对齐 #
|
|
23
|
+
# #
|
|
24
|
+
# 通用: #
|
|
25
|
+
# --remote http://127.0.0.1:9292 改走 S 端服务解析 #
|
|
26
|
+
# --help 帮助 #
|
|
27
|
+
# #
|
|
28
|
+
# 输出:JSON(与 S 端响应一致);用法/连接错误输出到 stderr 并返回 1 #
|
|
29
|
+
# #
|
|
30
|
+
# 示例: #
|
|
31
|
+
# ruby text-client.rb type int --radix 2 --text 100 # => 4 #
|
|
32
|
+
# ruby text-client.rb text head --prefix aa --text aabbcc # => true #
|
|
33
|
+
# ruby text-client.rb text match/paragraph --start a --finish b --text a1a2b3 #
|
|
34
|
+
# echo "abedef" | ruby text-client.rb text exchange --oldtext e --newtext z #
|
|
35
|
+
# ruby text-client.rb list map --func "proc{|c| c * 2}" --list '[1,2,3]' # => [2,4,6] #
|
|
36
|
+
# ruby text-client.rb tree + --oprand '[1,3,5]' --tree '{"a":1}' # => {"a":1,"0":1,"1":3,"2":5} #
|
|
37
|
+
# ruby text-client.rb --remote http://127.0.0.1:9292 text head --prefix aa --text aabbcc #
|
|
38
|
+
############################################################################################################
|
|
39
|
+
|
|
40
|
+
require_relative 'text-tools'
|
|
41
|
+
require 'json'
|
|
42
|
+
require 'net/http'
|
|
43
|
+
|
|
44
|
+
module TextClient
|
|
45
|
+
# 结构化参数键:这些键的值以 [ 或 { 开头时按 JSON 解析(与 S 端 JSON 请求体语义对齐),
|
|
46
|
+
# 其余一律保持字符串(数字/布尔由操作端自行 to_i/判断,与 S 端一致)。
|
|
47
|
+
STRUCTURED_KEYS = %w{list tree oprand oprands patterns attaches item val}
|
|
48
|
+
|
|
49
|
+
module_function
|
|
50
|
+
|
|
51
|
+
# 结构化参数解析:结构化键的值尝试按 JSON 解析(数字/布尔/数组/对象),解析失败保持原串。
|
|
52
|
+
#
|
|
53
|
+
# @param name [String] 参数名
|
|
54
|
+
# @param value [String] 命令行参数值
|
|
55
|
+
# @return [Object] 解析结果
|
|
56
|
+
def coerce name, value
|
|
57
|
+
return value unless STRUCTURED_KEYS.include?(name) && value.is_a?(String)
|
|
58
|
+
JSON.parse(value)
|
|
59
|
+
rescue JSON::ParserError
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 解析命令行参数:--key value 成对(也接受 --key=value 写法),
|
|
64
|
+
# 值缺失或下一个也是 --开关 时视为布尔 true。
|
|
65
|
+
#
|
|
66
|
+
# @param argv [Array<String>] 参数(不含 namespace/op)
|
|
67
|
+
# @return [Hash{String => Object}] 参数表
|
|
68
|
+
def parse_params argv
|
|
69
|
+
params = {}
|
|
70
|
+
i = 0
|
|
71
|
+
while i < argv.size
|
|
72
|
+
key = argv[i]
|
|
73
|
+
if key.start_with?('--')
|
|
74
|
+
name, inline = key[2..-1].split('=', 2)
|
|
75
|
+
value = inline || argv[i+1]
|
|
76
|
+
if value.nil? || (!inline && value.start_with?('--'))
|
|
77
|
+
params[name] = true
|
|
78
|
+
else
|
|
79
|
+
params[name] = coerce(name, value)
|
|
80
|
+
i += 1 unless inline
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
i += 1
|
|
84
|
+
end
|
|
85
|
+
params
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 取文本输入:--text > --file > 管道 stdin。
|
|
89
|
+
#
|
|
90
|
+
# @param params [Hash] 参数表
|
|
91
|
+
# @return [String, nil] 文本输入
|
|
92
|
+
def fetch_text params
|
|
93
|
+
return params['text'] if params['text']
|
|
94
|
+
return File.read(params['file']) if params['file']
|
|
95
|
+
return $stdin.read unless $stdin.tty?
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 本地执行:直接调用 text-tools 的操作符(与 S 端路由同参数同缺省)。
|
|
100
|
+
#
|
|
101
|
+
# @param ns [String] namespace(type/text/list/tree)
|
|
102
|
+
# @param op [String] 操作名(可含子路径)
|
|
103
|
+
# @param params [Hash] 参数表
|
|
104
|
+
# @return [Object] 计算结果
|
|
105
|
+
def dispatch_local ns, op, params
|
|
106
|
+
case "#{ns}/#{op}"
|
|
107
|
+
# ---- type ----
|
|
108
|
+
when 'type/int'
|
|
109
|
+
fetch_text(params).int(params['radix'] ? params['radix'].to_i : 10)
|
|
110
|
+
when 'type/float'
|
|
111
|
+
fetch_text(params).float(params['tail'] ? params['tail'].to_i : 2)
|
|
112
|
+
when 'type/precent'
|
|
113
|
+
fetch_text(params).precent(params['tail'] ? params['tail'].to_i : 2)
|
|
114
|
+
when 'type/text'
|
|
115
|
+
fetch_text(params).text((params['format'] || 'raw').to_sym)
|
|
116
|
+
when 'type/bool'
|
|
117
|
+
fetch_text(params).bool
|
|
118
|
+
# ---- text ----
|
|
119
|
+
when 'text/head'
|
|
120
|
+
fetch_text(params).head?(params['prefix'], params['strip'] || false)
|
|
121
|
+
when 'text/tail'
|
|
122
|
+
fetch_text(params).tail?(params['postfix'], params['strip'] || false)
|
|
123
|
+
when 'text/include'
|
|
124
|
+
fetch_text(params).include?(params['pattern'])
|
|
125
|
+
when 'text/empty'
|
|
126
|
+
fetch_text(params).empty?
|
|
127
|
+
when 'text/length'
|
|
128
|
+
fetch_text(params).length
|
|
129
|
+
when 'text/strip'
|
|
130
|
+
flag = (params['flag'] || 'both').to_s.downcase.to_sym
|
|
131
|
+
text = fetch_text(params)
|
|
132
|
+
flag==:both ? text.strip : flag==:left ? text.lstrip : flag==:right ? text.rstrip : text
|
|
133
|
+
when 'text/join'
|
|
134
|
+
fetch_text(params).join(params['attaches'] || [])
|
|
135
|
+
when 'text/+'
|
|
136
|
+
fetch_text(params) + (params['oprand'] || '')
|
|
137
|
+
when 'text/-'
|
|
138
|
+
fetch_text(params) - (params['oprand'] || '')
|
|
139
|
+
when 'text/*'
|
|
140
|
+
fetch_text(params) * (params['oprand'] || 1).to_s.to_i
|
|
141
|
+
when 'text/split'
|
|
142
|
+
fetch_text(params) / (params['oprand'] || ' ')
|
|
143
|
+
when 'text/cut'
|
|
144
|
+
fetch_text(params).cut(params['lidx'] ? params['lidx'].to_i : 1, params['ridx'] ? params['ridx'].to_i : 0)
|
|
145
|
+
when 'text/match'
|
|
146
|
+
text = fetch_text(params)
|
|
147
|
+
patterns = params['patterns'] || []
|
|
148
|
+
mode = (params['mode'] || 'one').to_s.downcase.to_sym
|
|
149
|
+
[:one, :all].include?(mode) ? (!patterns.first.to_s.empty? ? text.matches(patterns.first.to_s,mode) : text) :
|
|
150
|
+
text.matches(patterns,mode)
|
|
151
|
+
when 'text/exchange'
|
|
152
|
+
repeat = (params['repeat'] || 'all').to_s
|
|
153
|
+
repeat = repeat.include?('all') ? repeat.to_sym : repeat.to_i
|
|
154
|
+
fetch_text(params).exchange(params['oldtext'], params['newtext'], repeat)
|
|
155
|
+
when 'text/draw/lines'
|
|
156
|
+
rests, content = fetch_text(params).draw_lines(Regexp.new(params['preflag'].to_s), Regexp.new(params['postflag'].to_s))
|
|
157
|
+
[content, rests]
|
|
158
|
+
when 'text/match/paragraph'
|
|
159
|
+
fetch_text(params).match_paragraph(params['start'].to_s, params['finish'].to_s)
|
|
160
|
+
when 'text/match/cascade'
|
|
161
|
+
fetch_text(params).match_cascade(params['start'].to_s, params['finish'].to_s)
|
|
162
|
+
when 'text/match/xml'
|
|
163
|
+
fetch_text(params).match_xml(params['tag'].to_s)
|
|
164
|
+
# ---- list ----
|
|
165
|
+
when 'list/pop'
|
|
166
|
+
params['list'].pop
|
|
167
|
+
when 'list/push'
|
|
168
|
+
params['list'].push(params['item'])
|
|
169
|
+
when 'list/shift'
|
|
170
|
+
params['list'].shift
|
|
171
|
+
when 'list/unshift'
|
|
172
|
+
params['list'].unshift(params['item'])
|
|
173
|
+
when 'list/+'
|
|
174
|
+
params['list'] + (params['oprand'] || [])
|
|
175
|
+
when 'list/-'
|
|
176
|
+
params['list'] - (params['oprand'] || [])
|
|
177
|
+
when 'list/catch'
|
|
178
|
+
params['list'].catch(params['oprands'] || [])
|
|
179
|
+
when 'list/sub'
|
|
180
|
+
params['list'].sub(params['lidx'] ? params['lidx'].to_i : 1, params['ridx'] ? params['ridx'].to_i : 0)
|
|
181
|
+
when 'list/match'
|
|
182
|
+
repeat = params['repeat'] || 1
|
|
183
|
+
repeat = :all if repeat.to_s == 'all'
|
|
184
|
+
repeat = repeat.to_i unless repeat == :all
|
|
185
|
+
params['list'].match(repeat, &eval(params['func']))
|
|
186
|
+
when 'list/map'
|
|
187
|
+
params['list'].map(&eval(params['func']))
|
|
188
|
+
when 'list/reduce'
|
|
189
|
+
sectors = params['init'].to_s.split(":")
|
|
190
|
+
type = sectors.last.strip
|
|
191
|
+
init = sectors[0..-2].join(':')
|
|
192
|
+
iv = case type
|
|
193
|
+
when 'text'; init.strip.to_s
|
|
194
|
+
when 'integer'; init.strip.to_i
|
|
195
|
+
when 'float'; init.strip.to_f
|
|
196
|
+
when 'list'; eval(init)
|
|
197
|
+
when 'tree'; eval(init)
|
|
198
|
+
end
|
|
199
|
+
params['list'].reduce(iv, &eval(params['func']))
|
|
200
|
+
# ---- tree ----
|
|
201
|
+
when 'tree/keys'
|
|
202
|
+
params['tree'].keys
|
|
203
|
+
when 'tree/values'
|
|
204
|
+
params['tree'].values
|
|
205
|
+
when 'tree/get'
|
|
206
|
+
params['tree'][params['key'].to_s]
|
|
207
|
+
when 'tree/set'
|
|
208
|
+
params['tree'][params['key']] = params['val']
|
|
209
|
+
params['tree']
|
|
210
|
+
when 'tree/+'
|
|
211
|
+
params['tree'] + params['oprand']
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# 远程执行:POST JSON 参数到 S 端(路径与操作名一致)。
|
|
216
|
+
#
|
|
217
|
+
# @param base [String] S 端地址(如 http://127.0.0.1:9292)
|
|
218
|
+
# @param ns [String] namespace
|
|
219
|
+
# @param op [String] 操作名
|
|
220
|
+
# @param params [Hash] 参数表
|
|
221
|
+
# @return [Object] 解析结果
|
|
222
|
+
def dispatch_remote base, ns, op, params
|
|
223
|
+
uri = URI("#{base}/#{ns}/#{op}")
|
|
224
|
+
body = params.reject{|k,_| %w{remote help}.include?(k) }.to_json
|
|
225
|
+
response = Net::HTTP.post(uri, body, 'Content-Type' => 'application/json')
|
|
226
|
+
raise "S 端响应 #{response.code}" unless response.code == '200'
|
|
227
|
+
JSON.parse(response.body)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# 帮助文本。
|
|
231
|
+
#
|
|
232
|
+
# @return [String]
|
|
233
|
+
def help
|
|
234
|
+
<<~HELP
|
|
235
|
+
text-client —— textmind 命令行解析(C 端)
|
|
236
|
+
|
|
237
|
+
用法: ruby text-client.rb <namespace> <op> [参数...]
|
|
238
|
+
namespace: type | text | list | tree
|
|
239
|
+
op: 操作名(可含子路径,如 match/paragraph、draw/lines)
|
|
240
|
+
|
|
241
|
+
数据输入:
|
|
242
|
+
--text "字符串" 文本/类型操作输入(或 --file PATH,或管道 stdin)
|
|
243
|
+
--list '[...]' 列表操作输入(JSON 数组)
|
|
244
|
+
--tree '{...}' 树操作输入(JSON 对象)
|
|
245
|
+
|
|
246
|
+
通用:
|
|
247
|
+
--remote URL 改走 S 端服务解析(默认本地执行)
|
|
248
|
+
--help 本帮助
|
|
249
|
+
|
|
250
|
+
示例:
|
|
251
|
+
ruby text-client.rb type int --radix 2 --text 100
|
|
252
|
+
ruby text-client.rb text head --prefix aa --text aabbcc
|
|
253
|
+
ruby text-client.rb text exchange --oldtext e --newtext z --text abedef
|
|
254
|
+
ruby text-client.rb list map --func "proc{|c| c * 2}" --list '[1,2,3]'
|
|
255
|
+
ruby text-client.rb tree + --oprand '[1,3,5]' --tree '{"a":1}'
|
|
256
|
+
echo "a1a2b3" | ruby text-client.rb text match/paragraph --start a --finish b
|
|
257
|
+
ruby text-client.rb --remote http://127.0.0.1:9292 text head --prefix aa --text aabbcc
|
|
258
|
+
HELP
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# C 端命令行入口(可测试:传入 argv 与 io)。
|
|
262
|
+
#
|
|
263
|
+
# --remote 可出现在任意位置(--remote URL 或 --remote=URL 两种写法),
|
|
264
|
+
# 抽取后剩余参数的前两个位置量即 namespace 与 op。
|
|
265
|
+
#
|
|
266
|
+
# @param argv [Array<String>] 命令行参数
|
|
267
|
+
# @param io [IO] 结果输出流,默认 $stdout
|
|
268
|
+
# @return [Integer] 进程退出码
|
|
269
|
+
def run argv, io: $stdout
|
|
270
|
+
return (io.puts help; 0) if argv.empty? || argv.include?('--help')
|
|
271
|
+
|
|
272
|
+
# 归一化 --remote=URL 写法,再整对抽取 --remote,避免挤占 ns/op 位置
|
|
273
|
+
argv = argv.flat_map{|arg| arg.start_with?('--remote=') ? ['--remote', arg.split('=', 2)[1]] : [arg] }
|
|
274
|
+
remote = nil
|
|
275
|
+
if i = argv.index('--remote')
|
|
276
|
+
remote = argv[i+1]
|
|
277
|
+
argv = argv.dup.tap{|a| a.slice!(i, 2) }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
ns, op, *raw_params = argv
|
|
281
|
+
return (io.puts help; 1) unless ns && op && !ns.start_with?('--')
|
|
282
|
+
params = parse_params(raw_params)
|
|
283
|
+
|
|
284
|
+
begin
|
|
285
|
+
result = remote ? dispatch_remote(remote, ns, op, params) : dispatch_local(ns, op, params)
|
|
286
|
+
io.puts result.to_json
|
|
287
|
+
return 0
|
|
288
|
+
rescue => e
|
|
289
|
+
warn "text-client: #{e.class}: #{e.message}"
|
|
290
|
+
return 1
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
if $PROGRAM_NAME == __FILE__
|
|
296
|
+
exit(TextClient.run(ARGV))
|
|
297
|
+
end
|
data/text-server.rb
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#coding:utf-8
|
|
3
|
+
|
|
4
|
+
############################################################################################################
|
|
5
|
+
# text-server —— S 端:textmind 解析服务(命令行启动) #
|
|
6
|
+
# #
|
|
7
|
+
# 移植改造自 imdoc/TextUtils/config.ru: #
|
|
8
|
+
# - 原 `rackup config.ru` 部署方式 -> `ruby text-server.rb`(或安装后 require)命令行直接启动 #
|
|
9
|
+
# - 原依赖 gem:rack-cors -> 手工 CORS 响应头(放行规则与原配置一致,少一个依赖) #
|
|
10
|
+
# - 修正原 config.ru 三处失效缺省: #
|
|
11
|
+
# r.params['radix'].to_i || 10 (nil.to_i==0 恒真,缺省 10 从未生效 -> 缺省 10) #
|
|
12
|
+
# r.params['format'].to_sym (缺省会 nil.to_sym 崩溃 -> 缺省 :raw) #
|
|
13
|
+
# r.params['flag'].to_s.downcase.to_sym || :both(:"" 恒真 -> 缺省 :both;mode 同理缺省 :one) #
|
|
14
|
+
# - 路由路径与参数名与原服务完全一致,原 Postman 集合(imdoc/TextUtils/doc/)无需改动即可继续使用 #
|
|
15
|
+
# #
|
|
16
|
+
# 接口(JSON 请求体,POST): #
|
|
17
|
+
# /type/{int,float,precent,text,bool} #
|
|
18
|
+
# /text/{head,tail,include,empty,length,strip,join,+, -, *,split,cut,match,exchange,draw/lines, #
|
|
19
|
+
# match/paragraph, match/cascade, match/xml} #
|
|
20
|
+
# /list/{pop,push,shift,unshift,+, -,catch,sub,match,map,reduce} #
|
|
21
|
+
# /tree/{keys,values,get,set,+} #
|
|
22
|
+
# 注意:list 的 match/map/reduce 接收 ruby 代码串并 eval(内部信任工具,请勿暴露到不可信网络) #
|
|
23
|
+
# #
|
|
24
|
+
# 启动: #
|
|
25
|
+
# ruby text-server.rb # 127.0.0.1:9292 #
|
|
26
|
+
# ruby text-server.rb --port 9293 # 指定端口 #
|
|
27
|
+
# ruby text-server.rb --host 0.0.0.0 # 监听所有网卡(原 CORS 允许的 192.168.0.x 前端即此场景) #
|
|
28
|
+
# #
|
|
29
|
+
# 配套:C 端 text-client.rb(本地解析或 --remote 调用本服务) #
|
|
30
|
+
############################################################################################################
|
|
31
|
+
|
|
32
|
+
require_relative 'text-tools'
|
|
33
|
+
require 'roda'
|
|
34
|
+
|
|
35
|
+
module TextServer
|
|
36
|
+
# 原 rack-cors 放行来源(与 config.ru 一致):本地 9200 前端与 192.168.0.x 内网前端
|
|
37
|
+
ALLOWED_ORIGINS = ['localhost:9200', '127.0.0.1:9200']
|
|
38
|
+
ALLOWED_ORIGIN_PATTERN = /\Ahttp:\/\/192\.168\.0\.\d{1,3}(:\d+)?\z/
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# 判断 Origin 是否在 CORS 放行名单内。
|
|
43
|
+
#
|
|
44
|
+
# @param origin [String, nil] 请求 Origin 头
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def allowed_origin? origin
|
|
47
|
+
return false unless origin
|
|
48
|
+
ALLOWED_ORIGINS.include?(origin) || ALLOWED_ORIGIN_PATTERN.match?(origin)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# S 端启动入口:解析 --host/--port 并拉起 Rack 服务。
|
|
52
|
+
#
|
|
53
|
+
# @param argv [Array<String>] 命令行参数
|
|
54
|
+
# @return [void]
|
|
55
|
+
def run! argv
|
|
56
|
+
host, port = '127.0.0.1', 9292
|
|
57
|
+
argv.each_slice(2) do|key, value|
|
|
58
|
+
case key
|
|
59
|
+
when '--host' then host = value || host
|
|
60
|
+
when '--port' then port = (value || port).to_i
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
require 'rackup'
|
|
64
|
+
puts "textmind S 端启动: http://#{host}:#{port} (POST /type|/text|/list|/tree/*)"
|
|
65
|
+
Rackup::Server.start(app: App.freeze.app, Host: host, Port: port)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# textmind 解析服务(Roda 应用)。
|
|
70
|
+
class App < Roda
|
|
71
|
+
plugin :json_parser # 解析 JSON 请求体到 r.params
|
|
72
|
+
plugin :request_headers # 读取请求头(CORS Origin)
|
|
73
|
+
plugin :all_verbs # 全动词路由(GET/POST/PUT/…/OPTIONS)
|
|
74
|
+
|
|
75
|
+
route do |r|
|
|
76
|
+
# ---- 手工 CORS:按原 rack-cors 规则放行并回写响应头 ----
|
|
77
|
+
if origin = r.headers['Origin'] and TextServer.allowed_origin?(origin)
|
|
78
|
+
response['Access-Control-Allow-Origin'] = origin
|
|
79
|
+
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'
|
|
80
|
+
response['Access-Control-Allow-Headers'] = 'x-domain-token, Content-Type'
|
|
81
|
+
response['Access-Control-Expose-Headers'] = 'Some-Custom-Response-Header'
|
|
82
|
+
response['Access-Control-Max-Age'] = '600'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# CORS 预检(OPTIONS)直接放行
|
|
86
|
+
r.options do
|
|
87
|
+
''
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# ---- 类型操作:字符串 -> 目标类型 ----
|
|
91
|
+
r.on 'type' do
|
|
92
|
+
# POST /type/int radix=2, text="100" => 4
|
|
93
|
+
r.post 'int' do
|
|
94
|
+
text = r.params['text']
|
|
95
|
+
radix = r.params['radix'] ? r.params['radix'].to_i : 10
|
|
96
|
+
text.int(radix).to_json
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# POST /type/float tail=2, text="3.1415926" => 3.14
|
|
100
|
+
r.post 'float' do
|
|
101
|
+
text = r.params['text']
|
|
102
|
+
tail = r.params['tail'] ? r.params['tail'].to_i : 2
|
|
103
|
+
text.float(tail).to_json
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# POST /type/precent tail=2, text="0.8675309" => "86.75%"
|
|
107
|
+
r.post 'precent' do
|
|
108
|
+
text = r.params['text']
|
|
109
|
+
tail = r.params['tail'] ? r.params['tail'].to_i : 2
|
|
110
|
+
text.precent(tail).to_json
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# POST /type/text format=meta, text="hello world" => JSON 编码
|
|
114
|
+
r.post 'text' do
|
|
115
|
+
text = r.params['text']
|
|
116
|
+
format = (r.params['format'] || 'raw').to_sym
|
|
117
|
+
text.text(format).to_json
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# POST /type/bool text="true" => true
|
|
121
|
+
r.post 'bool' do
|
|
122
|
+
text = r.params['text']
|
|
123
|
+
text.bool.to_json
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# ---- 文本操作 ----
|
|
128
|
+
r.on 'text' do
|
|
129
|
+
# POST /text/head prefix="aa", strip=false, text="aabbcc"
|
|
130
|
+
r.post 'head' do
|
|
131
|
+
text = r.params['text']
|
|
132
|
+
prefix = r.params['prefix']
|
|
133
|
+
strip = r.params['strip'] || false
|
|
134
|
+
text.head?(prefix, strip).to_json
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# POST /text/tail postfix="cc", strip=false, text="aabbcc"
|
|
138
|
+
r.post 'tail' do
|
|
139
|
+
text = r.params['text']
|
|
140
|
+
postfix = r.params['postfix']
|
|
141
|
+
strip = r.params['strip'] || false
|
|
142
|
+
text.tail?(postfix, strip).to_json
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# POST /text/include pattern="bb", text="aabbcc"
|
|
146
|
+
r.post 'include' do
|
|
147
|
+
text = r.params['text']
|
|
148
|
+
pattern = r.params['pattern']
|
|
149
|
+
text.include?(pattern).to_json
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# POST /text/empty text=""
|
|
153
|
+
r.post 'empty' do
|
|
154
|
+
text = r.params['text']
|
|
155
|
+
text.empty?.to_json
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# POST /text/length text="aabbcc"
|
|
159
|
+
r.post 'length' do
|
|
160
|
+
text = r.params['text']
|
|
161
|
+
text.length.to_json
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# POST /text/strip flag=both, text=" aa "
|
|
165
|
+
r.post 'strip' do
|
|
166
|
+
text = r.params['text']
|
|
167
|
+
flag = (r.params['flag'] || 'both').to_s.downcase.to_sym
|
|
168
|
+
result = flag==:both ? text.strip : flag==:left ? text.lstrip : flag==:right ? text.rstrip : text
|
|
169
|
+
result.to_json
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# POST /text/join attaches=["22345678","32345678"], text="aa"
|
|
173
|
+
r.post 'join' do
|
|
174
|
+
text = r.params['text']
|
|
175
|
+
attaches = r.params['attaches'] || []
|
|
176
|
+
text.join(attaches).to_json
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# POST /text/+ oprand="bb", text="aa" => "aabb"
|
|
180
|
+
r.post '+' do
|
|
181
|
+
text = r.params['text']
|
|
182
|
+
oprand = r.params['oprand'] || ''
|
|
183
|
+
(text + oprand).to_json
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# POST /text/- oprand="bc", text="aabbcc" => "aa"(从首次出现处截断)
|
|
187
|
+
r.post '-' do
|
|
188
|
+
text = r.params['text']
|
|
189
|
+
oprand = r.params['oprand'] || ''
|
|
190
|
+
(text - oprand).to_json
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# POST /text/* oprand=3, text="ab" => "ababab"
|
|
194
|
+
r.post '*' do
|
|
195
|
+
text = r.params['text']
|
|
196
|
+
oprand = (r.params['oprand'] || 1).to_s.to_i
|
|
197
|
+
(text * oprand).to_json
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# POST /text/split oprand=" ", text="a b" (缺省 ' ',原文标注 DIFF: '' <=> ' ')
|
|
201
|
+
r.post 'split' do
|
|
202
|
+
text = r.params['text']
|
|
203
|
+
oprand = r.params['oprand'] || ' ' # DIFF: '' <=> ' '
|
|
204
|
+
(text / oprand).to_json
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# POST /text/cut lidx=1, ridx=0, text="abcdef"(1 起数闭区间,0 取到末尾)
|
|
208
|
+
r.post 'cut' do
|
|
209
|
+
text = r.params['text']
|
|
210
|
+
lidx = r.params['lidx'] || 1
|
|
211
|
+
ridx = r.params['ridx'] || 0
|
|
212
|
+
text.cut(lidx, ridx).to_json
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# POST /text/match patterns=["a\\d","c\\d"], mode=one|all|union, text="a1b2c3"
|
|
216
|
+
r.post 'match' do
|
|
217
|
+
text = r.params['text']
|
|
218
|
+
patterns = r.params['patterns'] || []
|
|
219
|
+
mode = (r.params['mode'] || 'one').to_s.downcase.to_sym
|
|
220
|
+
([:one, :all].include?(mode) ? (!patterns.first.to_s.empty? ? text.matches(patterns.first.to_s,mode) : text) :
|
|
221
|
+
text.matches(patterns,mode)).to_json
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# POST /text/exchange oldtext="e", newtext="z", repeat=all, text="abedef"
|
|
225
|
+
r.post 'exchange' do
|
|
226
|
+
text = r.params['text']
|
|
227
|
+
oldtext = r.params['oldtext']
|
|
228
|
+
newtext = r.params['newtext']
|
|
229
|
+
repeat = (r.params['repeat'] || 'all').to_s
|
|
230
|
+
repeat = repeat.include?('all') ? repeat.to_sym : repeat.to_i
|
|
231
|
+
text.exchange(oldtext, newtext, repeat).to_json
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# POST /text/draw/lines preflag="a", postflag="b", text="1111\na\n2222\nb\n3333"
|
|
235
|
+
r.post 'draw/lines' do
|
|
236
|
+
text = r.params['text']
|
|
237
|
+
preflag = Regexp.new r.params['preflag'].to_s
|
|
238
|
+
postflag = Regexp.new r.params['postflag'].to_s
|
|
239
|
+
rests, content = text.draw_lines(preflag, postflag)
|
|
240
|
+
[content, rests].to_json
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# POST /text/match/paragraph start="a", finish="b", text="a1a2b3a4b"
|
|
244
|
+
r.post 'match/paragraph' do
|
|
245
|
+
text = r.params['text']
|
|
246
|
+
start = r.params['start'].to_s
|
|
247
|
+
finish = r.params['finish'].to_s
|
|
248
|
+
text.match_paragraph(start, finish).to_json
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# POST /text/match/cascade start="a", finish="b", text="a1{a2}b3"
|
|
252
|
+
r.post 'match/cascade' do
|
|
253
|
+
text = r.params['text']
|
|
254
|
+
start = r.params['start'].to_s
|
|
255
|
+
finish = r.params['finish'].to_s
|
|
256
|
+
text.match_cascade(start, finish).to_json
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# POST /text/match/xml tag="a", text="<a>1<a>2</a>3</a>"
|
|
260
|
+
r.post 'match/xml' do
|
|
261
|
+
text = r.params['text']
|
|
262
|
+
tag = r.params['tag'].to_s
|
|
263
|
+
text.match_xml(tag).to_json
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# ---- 列表操作 ----
|
|
268
|
+
r.on 'list' do
|
|
269
|
+
# POST /list/pop list=[1,2,3]
|
|
270
|
+
r.post 'pop' do
|
|
271
|
+
list = r.params['list']
|
|
272
|
+
list.pop.to_json
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# POST /list/push item="x", list=[1,2,3]
|
|
276
|
+
r.post 'push' do
|
|
277
|
+
list = r.params['list']
|
|
278
|
+
item = r.params['item']
|
|
279
|
+
list.push(item).to_json
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# POST /list/shift list=[1,2,3]
|
|
283
|
+
r.post 'shift' do
|
|
284
|
+
list = r.params['list']
|
|
285
|
+
list.shift.to_json
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# POST /list/unshift item="x", list=[1,2,3]
|
|
289
|
+
r.post 'unshift' do
|
|
290
|
+
list = r.params['list']
|
|
291
|
+
item = r.params['item']
|
|
292
|
+
list.unshift(item).to_json
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# POST /list/+ oprand=[4,5], list=[1,2,3]
|
|
296
|
+
r.post '+' do
|
|
297
|
+
list = r.params['list']
|
|
298
|
+
alt = r.params['oprand'] || []
|
|
299
|
+
(list + alt).to_json
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# POST /list/- oprand=[2], list=[1,2,3]
|
|
303
|
+
r.post '-' do
|
|
304
|
+
list = r.params['list']
|
|
305
|
+
alt = r.params['oprand'] || []
|
|
306
|
+
(list - alt).to_json
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# POST /list/catch oprands=[["a","b"],["c"]], list=[1,2,3]
|
|
310
|
+
r.post 'catch' do
|
|
311
|
+
list = r.params['list']
|
|
312
|
+
alts = r.params['oprands'] || []
|
|
313
|
+
list.catch(alts).to_json
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# POST /list/sub lidx=1, ridx=0, list=[1,2,3]
|
|
317
|
+
r.post 'sub' do
|
|
318
|
+
list = r.params['list']
|
|
319
|
+
lidx = r.params['lidx'] || 1
|
|
320
|
+
ridx = r.params['ridx'] || 0
|
|
321
|
+
list.sub(lidx, ridx).to_json
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# POST /list/match func="proc{|c| c >= 'b'}", repeat=1, list=["a","b","c"]
|
|
325
|
+
# repeat: 0 只判存在 / 1 取首个 / "all" 取全部 / n 取前 n 个
|
|
326
|
+
r.post 'match' do
|
|
327
|
+
list = r.params['list']
|
|
328
|
+
repeat = r.params['repeat'] || 1
|
|
329
|
+
repeat = :all if repeat.to_s == 'all'
|
|
330
|
+
func = eval(r.params['func'])
|
|
331
|
+
list.match(repeat, &func).to_json
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# POST /list/map func="proc{|c| c * 2}", list=[1,2,3]
|
|
335
|
+
r.post 'map' do
|
|
336
|
+
list = r.params['list']
|
|
337
|
+
func = eval(r.params['func'])
|
|
338
|
+
list.map(&func).to_json
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# POST /list/reduce func="proc{|a,b| a+b}", init="0:integer", list=[1,2,3]
|
|
342
|
+
# init 形如 "初值:类型",类型支持 text/integer/float/list/tree
|
|
343
|
+
r.post 'reduce' do
|
|
344
|
+
list = r.params['list']
|
|
345
|
+
func = eval(r.params['func'])
|
|
346
|
+
sectors = r.params['init'].to_s.split(":")
|
|
347
|
+
type = sectors.last.strip
|
|
348
|
+
init = sectors[0..-2].join(':')
|
|
349
|
+
iv = case type
|
|
350
|
+
when 'text'; init.strip.to_s
|
|
351
|
+
when 'integer'; init.strip.to_i
|
|
352
|
+
when 'float'; init.strip.to_f
|
|
353
|
+
when 'list'; eval(init)
|
|
354
|
+
when 'tree'; eval(init)
|
|
355
|
+
end
|
|
356
|
+
list.reduce(iv,&func).to_json
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# ---- 树操作 ----
|
|
361
|
+
r.on 'tree' do
|
|
362
|
+
# POST /tree/keys tree={"a":1}
|
|
363
|
+
r.post 'keys' do
|
|
364
|
+
tree = r.params['tree']
|
|
365
|
+
tree.keys.to_json
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# POST /tree/values tree={"a":1}
|
|
369
|
+
r.post 'values' do
|
|
370
|
+
tree = r.params['tree']
|
|
371
|
+
tree.values.to_json
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# POST /tree/get key="a", tree={"a":1}
|
|
375
|
+
r.post 'get' do
|
|
376
|
+
tree = r.params['tree']
|
|
377
|
+
get_key = r.params['key'].to_s
|
|
378
|
+
tree[get_key].to_json
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# POST /tree/set key="a", val=2, tree={"a":1}
|
|
382
|
+
r.post 'set' do
|
|
383
|
+
tree = r.params['tree']
|
|
384
|
+
set_key = r.params['key']
|
|
385
|
+
set_val = r.params['val']
|
|
386
|
+
tree[set_key]=set_val
|
|
387
|
+
tree.to_json
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# POST /tree/+ oprand={"b":2}|[1,3,5]|"x", tree={"a":1}
|
|
391
|
+
r.post '+' do
|
|
392
|
+
tree = r.params['tree']
|
|
393
|
+
alt = r.params['oprand'] # DIFF: "num" <=> num
|
|
394
|
+
(tree + alt).to_json
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
if $PROGRAM_NAME == __FILE__
|
|
402
|
+
TextServer.run! ARGV
|
|
403
|
+
end
|
data/text-tools.rb
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
#coding:utf-8
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
############################################################################################################
|
|
5
|
+
# HOW TO USE #
|
|
6
|
+
############################################################################################################
|
|
7
|
+
|
|
8
|
+
=begin
|
|
9
|
+
require 'cc'
|
|
10
|
+
CC.use 'text-tools' # immediately when loading this file
|
|
11
|
+
|
|
12
|
+
# 类型操作
|
|
13
|
+
p "100".int(2), "3.14159".float(3), "0.86".precent, "true".bool
|
|
14
|
+
|
|
15
|
+
# 文本操作
|
|
16
|
+
p "aaabbb".head? "aa"
|
|
17
|
+
p "cccddd".tail? "dd"
|
|
18
|
+
p "abcdef" - "c"
|
|
19
|
+
p "abcdef" / "c"
|
|
20
|
+
p "abedef".exchange "e", 'z'
|
|
21
|
+
p "a11111a2222b3333b4444a5555b".match_paragraph('a','b')
|
|
22
|
+
p "<a>1<a><a>2</a>3</a>".match_xml('a')
|
|
23
|
+
|
|
24
|
+
# 列表操作
|
|
25
|
+
p ['a','b','c'].match(1){|c| c >= 'b'}
|
|
26
|
+
p ['a','b','c'].catch('d')
|
|
27
|
+
|
|
28
|
+
# 树操作
|
|
29
|
+
p ({a: 1, b: 2} + [1, 3, 5]).keys
|
|
30
|
+
p ({a: 1}).get :a
|
|
31
|
+
=end
|
|
32
|
+
|
|
33
|
+
############################################################################################################
|
|
34
|
+
# text-tools —— 文本/列表/树的人类直觉操作扩展(移植自 imdoc/TextUtils/text_mind + text_abstract) #
|
|
35
|
+
# #
|
|
36
|
+
# 组成(单一文件自带全部依赖,零第三方依赖): #
|
|
37
|
+
# TextAbstract(模块函数):draw_fragments / match_paragraph / match_cascade / match_html_tag #
|
|
38
|
+
# —— 复杂文本的特征字段摘取与采集(段落/级联/标签结构匹配) #
|
|
39
|
+
# TextAbstract::Type -> String 类型转换(int / float / precent / text / bool) #
|
|
40
|
+
# TextAbstract::TextOperator -> String 文本操作(head? / tail? / matches / join / cut / - / / …) #
|
|
41
|
+
# TextAbstract::ListOperator -> Array 列表操作(catch / match / sub) #
|
|
42
|
+
# TextAbstract::TreeOperator -> Hash 树操作(get / set / +) #
|
|
43
|
+
# #
|
|
44
|
+
# 配套工具: #
|
|
45
|
+
# text-server.rb —— S 端:命令行启动 HTTP 解析服务(本文件的 HTTP 包装) #
|
|
46
|
+
# text-client.rb —— C 端:命令行传入解析(本地执行本文件,或 --remote 走 S 端服务) #
|
|
47
|
+
############################################################################################################
|
|
48
|
+
|
|
49
|
+
module TextAbstract
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
# 按前置/后置标记抽取文本片段(特定环境遗留方法,不推荐常用)。
|
|
53
|
+
#
|
|
54
|
+
# 遇到 pre_flag 匹配行时开始收集,遇到 post_flag 匹配行时结束;
|
|
55
|
+
# 标记行本身包含在片段内,未闭合的片段追加到上下文。
|
|
56
|
+
#
|
|
57
|
+
# @param text [String] 原始文本
|
|
58
|
+
# @param pre_flag [Regexp] 开始标记(行级正则)
|
|
59
|
+
# @param post_flag [Regexp] 结束标记(行级正则)
|
|
60
|
+
# @return [Array<String>] 二元组 [上下文文本, 片段数组]
|
|
61
|
+
def draw_fragments text,pre_flag,post_flag
|
|
62
|
+
context,fragments = [],[]
|
|
63
|
+
temp,flag = "",false
|
|
64
|
+
text.each_line do|line|
|
|
65
|
+
if pre_flag.match(line)
|
|
66
|
+
temp = String.new if flag == false
|
|
67
|
+
flag = true
|
|
68
|
+
end
|
|
69
|
+
if flag == true
|
|
70
|
+
temp << line # base on line!
|
|
71
|
+
else
|
|
72
|
+
context << line
|
|
73
|
+
end
|
|
74
|
+
if post_flag.match(line)
|
|
75
|
+
flag = false
|
|
76
|
+
fragments << temp
|
|
77
|
+
temp = String.new
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
context << temp if flag == true # exist pre_flag but no post_flag
|
|
81
|
+
fragments.delete("")
|
|
82
|
+
return context.join(),fragments
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 严格顺序匹配开始/结束标记之间的段落。
|
|
86
|
+
#
|
|
87
|
+
# start 和 finish 有严格先后顺序:一段未结束不开启另一段。
|
|
88
|
+
# 返回匹配到的段落正文数组(不含首尾标记本身)。
|
|
89
|
+
#
|
|
90
|
+
# @param text [String] 完整文档文本
|
|
91
|
+
# @param start [String] 开始标记(严格字符串包含匹配)
|
|
92
|
+
# @param finish [String] 结束标记(严格字符串包含匹配)
|
|
93
|
+
# @return [Array<String>] 段落正文列表
|
|
94
|
+
def match_paragraph text,start,finish
|
|
95
|
+
paragraphs,current_text = [],text.clone
|
|
96
|
+
until current_text==""
|
|
97
|
+
return paragraphs unless current_text.include?(start) # 严格匹配开始
|
|
98
|
+
start_split = current_text.split(start)
|
|
99
|
+
current_text = start_split[1..-1].join(start)
|
|
100
|
+
return paragraphs unless current_text.include?(finish) # 严格匹配结束
|
|
101
|
+
finish_split = current_text.split(finish)
|
|
102
|
+
context = finish_split[0]
|
|
103
|
+
current_text = finish_split[1..-1].join(finish)+(text[-1*finish.size..-1]==finish ? finish : "")
|
|
104
|
+
paragraphs << context
|
|
105
|
+
end
|
|
106
|
+
return paragraphs
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 可嵌套匹配开始/结束标记,返回树状段落结构。
|
|
110
|
+
#
|
|
111
|
+
# start 和 finish 可以嵌套(一个 start/finish 包含在另一个内部),
|
|
112
|
+
# 但全局上仍保持顺序。返回嵌套数组:字符串为内容,数组为下一层级。
|
|
113
|
+
#
|
|
114
|
+
# @param text [String] 完整文档文本
|
|
115
|
+
# @param start [String] 开始标记
|
|
116
|
+
# @param finish [String] 结束标记
|
|
117
|
+
# @return [Array] 树状段落结构
|
|
118
|
+
def match_cascade text,start,finish
|
|
119
|
+
paragraphs,current_text = [],text
|
|
120
|
+
return paragraphs unless text.include?(start) or text.include?(finish)
|
|
121
|
+
current_pos,parent_pos = paragraphs,[paragraphs]
|
|
122
|
+
until current_text==""
|
|
123
|
+
st_pos = (current_text.include?(start) ? current_text.split(start)[0] : current_text).size
|
|
124
|
+
fn_pos = (current_text.include?(finish) ? current_text.split(finish)[0] : current_text).size
|
|
125
|
+
if fn_pos==st_pos # 无标志或标志互包含
|
|
126
|
+
current_pos << current_text
|
|
127
|
+
current_text = ""
|
|
128
|
+
end
|
|
129
|
+
if fn_pos < st_pos # 结束在前
|
|
130
|
+
csp = current_text.split(finish)
|
|
131
|
+
current_pos << csp[0]
|
|
132
|
+
current_pos << finish # 本级完成
|
|
133
|
+
cur_parent = parent_pos.pop # 取回上一级
|
|
134
|
+
current_pos = cur_parent # 返回上一级
|
|
135
|
+
current_text = current_text.sub(csp[0],"").sub(finish,"")
|
|
136
|
+
end
|
|
137
|
+
if st_pos < fn_pos # 开始在前
|
|
138
|
+
csp = current_text.split(start)
|
|
139
|
+
current_pos << csp[0] unless csp[0]=="" # 本级内容加入
|
|
140
|
+
current_pos << [start] # 下一级内容加入
|
|
141
|
+
parent_pos << current_pos # 本级加入上下文
|
|
142
|
+
current_pos = current_pos[-1]
|
|
143
|
+
current_text = current_text.sub(csp[0],"").sub(start,"")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
return paragraphs
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# 匹配 HTML 标签的嵌套结构(<tag ...> ... </tag>)。
|
|
150
|
+
#
|
|
151
|
+
# @param html [String] HTML 文本
|
|
152
|
+
# @param tag [String] 标签名(如 "table"、"div")
|
|
153
|
+
# @return [Array] 树状段落结构
|
|
154
|
+
def match_html_tag html,tag
|
|
155
|
+
paragraphs = self.match_cascade(html,"<#{tag}","</#{tag}>")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
module TextAbstract
|
|
161
|
+
|
|
162
|
+
# 类型操作模块:把字符串按目标类型转换/格式化。
|
|
163
|
+
module Type
|
|
164
|
+
# 按进制字符串转整数。
|
|
165
|
+
#
|
|
166
|
+
# @param radix [Integer] 进制,默认 10
|
|
167
|
+
# @return [Integer]
|
|
168
|
+
def int radix=10
|
|
169
|
+
self.to_i(radix)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# 转浮点并保留 tail 位小数。
|
|
173
|
+
#
|
|
174
|
+
# @param tail [Integer] 保留位数,默认 2
|
|
175
|
+
# @return [Float]
|
|
176
|
+
def float tail=2
|
|
177
|
+
self.to_f.round(tail)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# 转百分比字符串(乘 100 保留 tail 位 + "%")。
|
|
181
|
+
#
|
|
182
|
+
# @param tail [Integer] 保留位数,默认 2
|
|
183
|
+
# @return [String]
|
|
184
|
+
def precent tail=2
|
|
185
|
+
(self.to_f*100).round(tail).to_s+"%"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# 原样或 JSON 化输出。
|
|
189
|
+
#
|
|
190
|
+
# @param format [Symbol] :raw 原样 / :meta JSON 编码
|
|
191
|
+
# @return [String]
|
|
192
|
+
def text format=:raw
|
|
193
|
+
return self if format==:raw
|
|
194
|
+
return self.to_json if format==:meta
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# 转布尔:'true'/'false' 按字面,其余按是否为空。
|
|
198
|
+
#
|
|
199
|
+
# @return [Boolean]
|
|
200
|
+
def bool
|
|
201
|
+
return true if self.downcase=='true'
|
|
202
|
+
return false if self.downcase=='false'
|
|
203
|
+
return self.empty?
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# 文本操作模块:常见文本加工的直觉操作符。
|
|
208
|
+
module TextOperator
|
|
209
|
+
# 判断是否以 prefix 开头。
|
|
210
|
+
#
|
|
211
|
+
# @param prefix [String] 前缀
|
|
212
|
+
# @param strip [Boolean] 是否先去除两端空白
|
|
213
|
+
# @return [Boolean]
|
|
214
|
+
def head? prefix, strip=false #=> bool
|
|
215
|
+
left = strip ? self.strip : self
|
|
216
|
+
left[0..(prefix.length-1)]==prefix
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# 判断是否以 postfix 结尾。
|
|
220
|
+
#
|
|
221
|
+
# @param postfix [String] 后缀
|
|
222
|
+
# @param strip [Boolean] 是否先去除两端空白
|
|
223
|
+
# @return [Boolean]
|
|
224
|
+
def tail? postfix, strip=false #=> bool
|
|
225
|
+
left = strip ? self.strip : self
|
|
226
|
+
left[(-1*postfix.length)..-1]==postfix
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# include? / empty? 由 Ruby 核心提供,不再重复定义。
|
|
230
|
+
|
|
231
|
+
# 按模式匹配文本。
|
|
232
|
+
#
|
|
233
|
+
# @param pattern [String, Array<String>] 模式:单模式传字符串,多模式传数组
|
|
234
|
+
# @param mode [Symbol] :one 取首个匹配 / :all 全部匹配 / :union 多模式并集
|
|
235
|
+
# @return [MatchData, Array]
|
|
236
|
+
def matches pattern, mode=:one #=> match
|
|
237
|
+
return self.match(Regexp.new pattern) if mode==:one
|
|
238
|
+
return self.scan(Regexp.new pattern) if mode==:all
|
|
239
|
+
return self.scan(Regexp.union *pattern.map{|pat|Regexp.new(pat)}) if mode==:union
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# 按前置/后置标记抽行片段(draw_fragments 的字符串包装)。
|
|
243
|
+
#
|
|
244
|
+
# @param preflag [Regexp] 开始标记
|
|
245
|
+
# @param postflag [Regexp] 结束标记
|
|
246
|
+
# @return [Array<String>] 二元组 [上下文文本, 片段数组]
|
|
247
|
+
def draw_lines preflag, postflag #=> text, list
|
|
248
|
+
restext, contexts = TextAbstract.draw_fragments self, preflag, postflag
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# 严格顺序匹配段落(match_paragraph 的字符串包装)。
|
|
252
|
+
#
|
|
253
|
+
# @param start [String] 开始标记
|
|
254
|
+
# @param finish [String] 结束标记
|
|
255
|
+
# @return [Array<String>] 段落正文列表
|
|
256
|
+
def match_paragraph start, finish #=> list
|
|
257
|
+
paragraphs = TextAbstract.match_paragraph self, start, finish
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# 可嵌套匹配(match_cascade 的字符串包装)。
|
|
261
|
+
#
|
|
262
|
+
# @param start [String] 开始标记
|
|
263
|
+
# @param finish [String] 结束标记
|
|
264
|
+
# @return [Array] 树状段落结构
|
|
265
|
+
def match_cascade start, finish #=> list
|
|
266
|
+
paragraphs = TextAbstract.match_cascade self, start, finish
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# 匹配 HTML 标签嵌套结构(match_html_tag 的字符串包装)。
|
|
270
|
+
#
|
|
271
|
+
# @param tag [String] 标签名
|
|
272
|
+
# @return [Array] 树状段落结构
|
|
273
|
+
def match_xml tag #=> tree
|
|
274
|
+
document = TextAbstract.match_html_tag self,tag
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# length / size / strip / lstrip / rstrip 由 Ruby 核心提供。
|
|
278
|
+
|
|
279
|
+
# 把后续文本依次拼接在自身后面。
|
|
280
|
+
#
|
|
281
|
+
# @param texts [String...] 待拼接文本
|
|
282
|
+
# @return [String]
|
|
283
|
+
def join *texts #=> text
|
|
284
|
+
self+texts.map{|t|t}.join
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# 按人类直觉截取(1 起数的闭区间,ridx=0 表示取到末尾)。
|
|
288
|
+
#
|
|
289
|
+
# @param index1 [Integer] 起始序号(1 起)
|
|
290
|
+
# @param index2 [Integer] 结束序号,0 表示末尾
|
|
291
|
+
# @return [String]
|
|
292
|
+
def cut index1, index2=0 #=> text
|
|
293
|
+
self[(index1-1)..(index2-1)]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# +(拼接)与 *(重复)由 Ruby 核心提供。
|
|
297
|
+
|
|
298
|
+
# 从首次出现 text 的位置截断(含 text),未出现则原样返回。
|
|
299
|
+
#
|
|
300
|
+
# @param text [String] 截断标记
|
|
301
|
+
# @return [String]
|
|
302
|
+
def - text #=> text
|
|
303
|
+
self.include?(text) ? self.split(text).first : self
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# 按分隔符切分为列表(即 split 的直觉别名)。
|
|
307
|
+
#
|
|
308
|
+
# @param text [String] 分隔符
|
|
309
|
+
# @return [Array<String>]
|
|
310
|
+
def / text #=> list
|
|
311
|
+
self.split(text)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# 按模式替换:oldtext 匹配处替换为 newtext,最多 num 次。
|
|
315
|
+
#
|
|
316
|
+
# @param text [String] 匹配模式(正则源文本)
|
|
317
|
+
# @param newtext [String] 替换文本
|
|
318
|
+
# @param num [Integer, Symbol] 次数上限,:all 表示全部
|
|
319
|
+
# @return [String]
|
|
320
|
+
def exchange text, newtext, num=:all #=> text
|
|
321
|
+
num = 65535 if num==:all
|
|
322
|
+
count = 0
|
|
323
|
+
self.gsub(Regexp.new(text)) do |match|
|
|
324
|
+
count += 1
|
|
325
|
+
count <= num ? newtext : text
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# 列表操作模块:列表加工的直觉操作符。
|
|
331
|
+
#
|
|
332
|
+
# pop/push/shift/unshift/+/-/find/select/filter/map/collect/reduce/inject
|
|
333
|
+
# 均由 Ruby 核心提供,不再重复定义。
|
|
334
|
+
module ListOperator
|
|
335
|
+
# 把自身挂到列表头部,返回新列表。
|
|
336
|
+
#
|
|
337
|
+
# @param list [Array] 待挂接的元素序列
|
|
338
|
+
# @return [Array] 新列表
|
|
339
|
+
def catch *list #=> list(list)
|
|
340
|
+
list.unshift(self)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# 按条件块检索。
|
|
344
|
+
#
|
|
345
|
+
# @param num [Integer, Symbol] 0 只判存在 / 1 取首个 / :all 取全部 / n>1 取前 n 个
|
|
346
|
+
# @yield [item] 匹配条件
|
|
347
|
+
# @return [Boolean, Object, Array]
|
|
348
|
+
def match num=1, &block #=> bool | item | list
|
|
349
|
+
return self.find(&block).nil? if num ==0
|
|
350
|
+
return self.find(&block) if num ==1
|
|
351
|
+
return self.select(&block) if num ==:all
|
|
352
|
+
return self.select(&block)[0..(num-1)] if num > 1
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# 按人类直觉截取(1 起数的闭区间,index2=0 表示取到末尾)。
|
|
356
|
+
#
|
|
357
|
+
# @param index1 [Integer] 起始序号(1 起)
|
|
358
|
+
# @param index2 [Integer] 结束序号,0 表示末尾
|
|
359
|
+
# @return [Array]
|
|
360
|
+
def sub index1, index2=0 #=> list
|
|
361
|
+
self[(index1-1)..(index2-1)]
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# 树操作模块:散列表的直觉操作符。
|
|
366
|
+
#
|
|
367
|
+
# keys/values/[]/[]= 由 Ruby 核心提供,不再重复定义。
|
|
368
|
+
module TreeOperator
|
|
369
|
+
# 取键值([] 的直觉别名)。
|
|
370
|
+
#
|
|
371
|
+
# @param key [Object] 键
|
|
372
|
+
# @return [Object]
|
|
373
|
+
def get key #=> item
|
|
374
|
+
self[key]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# 写键值([]= 的直觉别名)。
|
|
378
|
+
#
|
|
379
|
+
# @param key [Object] 键
|
|
380
|
+
# @param value [Object] 值
|
|
381
|
+
# @return [Object] 写入的值
|
|
382
|
+
def set key, value #=> item
|
|
383
|
+
self[key] = value
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# 合并树/列表/单值为新树。
|
|
387
|
+
#
|
|
388
|
+
# Hash 深合并;Array 按现有最大整数键续排;单值追加为下一个整数键。
|
|
389
|
+
#
|
|
390
|
+
# @param oprand [Hash, Array, Object] 待合并对象
|
|
391
|
+
# @return [Hash] 合并结果新树
|
|
392
|
+
def + oprand #=> tree
|
|
393
|
+
index = self.keys.filter{|k|k.instance_of?(Integer)}.max
|
|
394
|
+
index = index ? index+1 : 0
|
|
395
|
+
if oprand.instance_of?(Hash)
|
|
396
|
+
self.merge(oprand)
|
|
397
|
+
elsif oprand.instance_of?(Array)
|
|
398
|
+
self.merge oprand.reduce({}){|tr,op|index+=1;tr.merge({(index-1)=>op})}
|
|
399
|
+
else
|
|
400
|
+
self.merge({index=>oprand})
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
class String
|
|
408
|
+
include TextAbstract::Type
|
|
409
|
+
include TextAbstract::TextOperator
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
class Array
|
|
413
|
+
include TextAbstract::ListOperator
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
class Hash
|
|
417
|
+
include TextAbstract::TreeOperator
|
|
418
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt
|
|
@@ -34,6 +34,9 @@ files:
|
|
|
34
34
|
- regexp.rb
|
|
35
35
|
- shell-tools.rb
|
|
36
36
|
- string.rb
|
|
37
|
+
- text-client.rb
|
|
38
|
+
- text-server.rb
|
|
39
|
+
- text-tools.rb
|
|
37
40
|
- trace.rb
|
|
38
41
|
- tree.rb
|
|
39
42
|
homepage: https://github.com/ChenMeng1365/custom-core
|
|
@@ -54,7 +57,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
54
57
|
- !ruby/object:Gem::Version
|
|
55
58
|
version: '0'
|
|
56
59
|
requirements: []
|
|
57
|
-
rubygems_version: 4.0.
|
|
60
|
+
rubygems_version: 4.0.19
|
|
58
61
|
specification_version: 4
|
|
59
62
|
summary: custom core
|
|
60
63
|
test_files: []
|