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.
Files changed (7) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -3
  3. data/cc.rb +1 -0
  4. data/text-client.rb +297 -0
  5. data/text-server.rb +403 -0
  6. data/text-tools.rb +418 -0
  7. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8386675ee1a8b68cb73c020b620f2b314b0778a15c22a852bc230955155275f
4
- data.tar.gz: 58d7146b6769792681d5454bb665fd9f7694c85b56ea2d2c4b91d6a519839173
3
+ metadata.gz: 560df541d3d489827cf9873d8ad9b78efb20e454824728462dff70fd955b477a
4
+ data.tar.gz: 235e3d4bc81f79b5bc3eb72303ef4df9ae1f724753a7484f451fff75b9f7bb53
5
5
  SHA512:
6
- metadata.gz: 946c2bc2b698b1a4e05e03224005038281f57712a228c82ff6d9ff635a3d34921741969ce058c75716dbd9ee9823d16f2224214b71bec7904ec8c8314ed481df
7
- data.tar.gz: c77d03c47b193c321a22f52b23841be5b5284b91839309294d93a48bc577bd1a31ca2375befba9576c9e8cbd91b212140e6a397a1c99c18c75722b51d1828a16
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
@@ -14,6 +14,7 @@ $modlist = [
14
14
  'number',
15
15
  'regexp',
16
16
  'string',
17
+ 'text-tools',
17
18
  'tree',
18
19
  'trace',
19
20
  'logic'
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.3.0
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.10
60
+ rubygems_version: 4.0.19
58
61
  specification_version: 4
59
62
  summary: custom core
60
63
  test_files: []