caset 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,141 @@
1
+ #coding:utf-8
2
+
3
+ #------------------------------------------------------------------------------
4
+ # Markdown 解析器加载与包装器
5
+ #------------------------------------------------------------------------------
6
+ module CasetDown
7
+ module_function
8
+
9
+ # casetdown 库根目录(lib/)。
10
+ #
11
+ # 供代码执行引擎以 `ruby -I"..." ./~tmp.rb` 启动子进程时定位
12
+ # require "casetdown",使仓库直跑与 gem 安装两种方式下,
13
+ # 执行脚本中的 EnData / SimTab / TinText 数据绑定均可用。
14
+ #
15
+ # @return [String] lib 目录的绝对路径
16
+ def home
17
+ File.expand_path('..', __dir__)
18
+ end
19
+
20
+ # Loader
21
+ # 加载指定 Markdown 文件并解析为结构化数据。
22
+ #
23
+ # @param path [String] Markdown 文件路径
24
+ # @param option [Hash] 可选参数,会合并到解析参数中
25
+ # @option option [Class] :parser 解析器类,默认为 ReDiscount(manticore)
26
+ # @option option [Boolean] :doc 是否返回原始文档对象,默认为 true
27
+ # @option option [Boolean] :tree 是否返回树结构,默认为 true
28
+ # @option option [Boolean] :src 是否返回源节点列表,默认为 true
29
+ # @return [Hash] 解析结果,包含 :path, :src, :tree, :doc 等键
30
+ # @example
31
+ # result = CasetDown.load("README.md")
32
+ # result[:doc].text # 获取原始文本
33
+ def load path, option={}
34
+ args = {path: path}.merge(option)
35
+ text = (File.read path).gsub("\r","").split("\n").map{|line|line.rstrip}.join("\n")
36
+ return self.parse(text, args)
37
+ end
38
+
39
+ # InlineStructs
40
+ # 将 Markdown 文本字符串解析为结构化数据。
41
+ #
42
+ # @param text [String] Markdown 原始文本
43
+ # @param option [Hash] 可选参数
44
+ # @option option [Class] :parser 解析器类,默认为 RDiscount
45
+ # @option option [Boolean] :doc 是否返回原始文档对象,默认为 true
46
+ # @option option [Boolean] :tree 是否返回树结构,默认为 true
47
+ # @option option [Boolean] :src 是否返回源节点列表,默认为 true
48
+ # @option option [String] :path 文件路径标识,默认为 'tmp'
49
+ # @return [Hash] 解析结果
50
+ # - :path [String] 文件路径
51
+ # - :src [Script] 原始节点列表包装器
52
+ # - :tree [Tree] 按标签分组的节点树
53
+ # - :doc [RDiscount] 原始文档对象
54
+ def parse text, option={}
55
+ args = {
56
+ parser: ReDiscount, # parser := {Kramdown::Document | RDiscount} ★ Kramdown::Document无法处理好多重空行问题
57
+ doc: true,
58
+ tree: true,
59
+ src: true,
60
+ path: 'tmp'
61
+ }.merge(option)
62
+ document = args[:parser].new(text)
63
+ html = "<html>"+document.to_html+"</html>"
64
+ script, tree = [],{}
65
+ XmlParser.parse(html).elements.each do|node|
66
+ doc = node.to_a
67
+ script << node
68
+ tree[doc[0]] ||= []
69
+ tree[doc[0]] << node
70
+ end
71
+ result = {path: args[:path]}
72
+ result[:src] = Script.new(native: script) if args[:src]
73
+ result[:tree] = Tree.new(native: tree) if args[:tree]
74
+ result[:doc] = document if args[:doc]
75
+ return result
76
+ end
77
+
78
+ # 文档树结构包装器,按标签名分组存储 DOM 节点。
79
+ #
80
+ # 内部使用 @content 存储分组后的节点树,键为标签名,值为节点数组。
81
+ class Tree
82
+ # 初始化树结构。
83
+ #
84
+ # @param options [Hash] 初始化参数
85
+ # @option options [Hash] :native 原生树结构数据
86
+ # @option options [String] :json JSON 字符串,会解析为树结构
87
+ # @option options [String] :path 文件路径,通过 CasetDown.load 获取
88
+ # @option options [String] :doc Markdown 文本,通过 CasetDown.parse 获取
89
+ # @return [Tree] 树实例
90
+ def initialize options={}
91
+ @content ||= options[:native]
92
+ @content ||= JSON.parse(options[:json]) if options[:json]
93
+ @content ||= CasetDown.load(options[:path])[-1] if options[:path]
94
+ @content ||= CasetDown.parse(options[:doc])[-1] if options[:doc]
95
+ @content ||= {}
96
+ end
97
+
98
+ # 遍历并返回树内容,支持传入回调函数。
99
+ #
100
+ # @yield [content] 传入树内容,缺省返回内容本身
101
+ # @yieldparam content [Hash] 树结构数据
102
+ # @yieldreturn [Object] 回调处理结果
103
+ # @return [Object] 执行 block.call(@content) 的结果
104
+ def nodes &block
105
+ block = lambda{|content|content} unless block
106
+ block.call(@content)
107
+ end
108
+ end
109
+
110
+ # 文档节点顺序列表包装器,保持原始文档的节点顺序。
111
+ #
112
+ # 内部使用 @content 存储原始节点数组。
113
+ class Script
114
+ # 初始化节点列表。
115
+ #
116
+ # @param options [Hash] 初始化参数
117
+ # @option options [Array] :native 原生节点数组
118
+ # @option options [String] :json JSON 字符串,会解析为节点数组
119
+ # @option options [String] :path 文件路径,通过 CasetDown.load 获取
120
+ # @option options [String] :doc Markdown 文本,通过 CasetDown.parse 获取
121
+ # @return [Script] 脚本实例
122
+ def initialize options={}
123
+ @content ||= options[:native]
124
+ @content ||= JSON.parse(options[:json]) if options[:json]
125
+ @content ||= CasetDown.load(options[:path])[0] if options[:path]
126
+ @content ||= CasetDown.parse(options[:doc])[0] if options[:doc]
127
+ @content ||= []
128
+ end
129
+
130
+ # 遍历并返回节点列表,支持传入回调函数。
131
+ #
132
+ # @yield [content] 传入节点数组,缺省返回内容本身
133
+ # @yieldparam content [Array] 节点数组
134
+ # @yieldreturn [Object] 回调处理结果
135
+ # @return [Object] 执行 block.call(@content) 的结果
136
+ def stack &block
137
+ block = lambda{|content|content} unless block
138
+ block.call(@content)
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,129 @@
1
+ #coding:utf-8
2
+
3
+ #------------------------------------------------------------------------------
4
+ # cm 命令实现(CasetDown::CLI)
5
+ #------------------------------------------------------------------------------
6
+ # 对可执行 Markdown 文档做形式化执行与生成:
7
+ # 解析文档结构 -> 导入导出表格 -> 执行代码块 -> 结果回写渲染文件
8
+ #
9
+ # 命令行用法(bin/cm 薄启动器调用 CasetDown::CLI.run):
10
+ #
11
+ # cm [options] <filename>[.md]
12
+ #
13
+ # options:
14
+ # --help 查看帮助
15
+ # --check 检查文档结构
16
+ # --source 查看文档中的源代码
17
+ # --rtab 导入外部表格到文档中
18
+ # --wtab 导出内部表格到外部文件
19
+ # --input 导出源代码文件并渲染文档
20
+ # --output 导出代码运行结果
21
+ # --all 导出源代码和运行结果(input+output)
22
+ # --preview 预览渲染文档(不生成渲染结果)
23
+ # --debug 调试开关, 不删除中间代码
24
+ # --rendfile 渲染文档并生成一个时间戳(默认)
25
+ #------------------------------------------------------------------------------
26
+ module CasetDown
27
+ module CLI
28
+ module_function
29
+
30
+ # CLI 帮助文本。
31
+ #
32
+ # @return [String] 多行帮助信息
33
+ def help
34
+ %Q{cm [options] <filename>[.md]
35
+
36
+ options:
37
+ --help 查看帮助
38
+ --check 检查文档结构
39
+ --source 查看文档中的源代码
40
+ --rtab 导入外部表格到文档中
41
+ --wtab 导出内部表格到外部文件
42
+ --input 导出源代码文件并渲染文档
43
+ --output 导出代码运行结果
44
+ --all 导出源代码和运行结果(input+output)
45
+ --preview 预览渲染文档(不生成渲染结果)
46
+ --debug 调试开关, 不删除中间代码
47
+ --rendfile 渲染文档并生成一个时间戳(默认)
48
+ }
49
+ end
50
+
51
+ # 命令行入口:解析参数并执行对应动作。
52
+ #
53
+ # @param argv [Array<String>] 命令行参数(不含程序名)
54
+ # @return [Integer] 进程退出码
55
+ def run argv
56
+ pool = argv.dup
57
+ check, source = false, false
58
+
59
+ if pool.include?('-h') or pool.include?('--help')
60
+ puts help
61
+ return 0
62
+ end
63
+
64
+ if pool.include?('--check')
65
+ check = true
66
+ pool.delete '--check'
67
+ end
68
+ if pool.include?('--source')
69
+ source = true
70
+ pool.delete '--source'
71
+ end
72
+
73
+ table, file = [], []
74
+ ['--rtab','--wtab'].each do|tag|
75
+ if pool.include?(tag)
76
+ table << tag
77
+ pool.delete tag
78
+ end
79
+ end
80
+ ['--input','--output','--all','--preview','--debug'].each do|tag|
81
+ if pool.include?(tag)
82
+ file << tag
83
+ pool.delete tag
84
+ end
85
+ end
86
+
87
+ path = pool.last
88
+ path = Dir["./*.md"].last unless path
89
+ return 0 unless path
90
+
91
+ if File.exist?(path)
92
+ cds = CasetDown.load path
93
+ elsif File.exist?("#{path}.md")
94
+ cds = CasetDown.load "#{path}.md"
95
+ else
96
+ puts "File #{path} not exist."
97
+ return 1
98
+ end
99
+
100
+ if check
101
+ puts "checking>>>>"
102
+ pp CasetDown::Check.all(cds)
103
+ return 0
104
+ end
105
+ if source
106
+ puts "source>>>>"
107
+ pp CasetDown.search_codes(cds)
108
+ return 0
109
+ end
110
+
111
+ render_file cds, table: table.map{|i|i[2..-1].to_sym}, file: file.map{|i|i[2..-1].to_sym}+[:rendfile]
112
+ return 0
113
+ end
114
+
115
+ # 默认渲染流程:表格导入导出 + 代码执行 + 渲染文件生成。
116
+ #
117
+ # cm 主流程与 cml 监听渲染共用的入口。
118
+ #
119
+ # @param cds [Hash] CasetDown.load 的解析结果
120
+ # @param table [Array<Symbol>] 表格选项(:rtab / :wtab)
121
+ # @param file [Array<Symbol>] 文件选项(:input / :output / :all / :preview / :debug / :rendfile)
122
+ # @return [String] 回写后的完整文档文本
123
+ def render_file cds, table: [], file: [:rendfile]
124
+ tmp = CasetDown.render_table(cds, opt: table)
125
+ CasetDown.run_to_file cds, stream: tmp[:text], data: tmp[:data], fact: tmp[:fact], file: file
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,102 @@
1
+ #coding:utf-8
2
+ require 'digest'
3
+ require 'fileutils'
4
+
5
+ #------------------------------------------------------------------------------
6
+ # cml 命令实现(CasetDown::Live)
7
+ #------------------------------------------------------------------------------
8
+ # Markdown 文件监听渲染:持续监视指定目录下的 .md 文档,
9
+ # 内容发生变更时自动执行 cm 默认渲染流程(表格 + 代码 + 渲染文件),
10
+ # 并把最新渲染版本同步为 "<文件名>.latest.md"。
11
+ #
12
+ # 命令行用法(bin/cml 薄启动器调用 CasetDown::Live.run):
13
+ #
14
+ # cml # 监视当前目录所有 .md
15
+ # cml <dir> # 监视指定目录
16
+ # cml <dir> <glob> # 监视指定目录下匹配 glob 的文档(如 '*.md')
17
+ #
18
+ # 变更检测:mtime 触发 + MD5 摘要确认(原版为 atime 触发,
19
+ # Windows 卷常见关闭 atime 更新会导致漏检,故改用 mtime)。
20
+ #------------------------------------------------------------------------------
21
+ module CasetDown
22
+ module Live
23
+ module_function
24
+
25
+ # 命令行入口:解析参数并进入监听循环(正常情况下不返回)。
26
+ #
27
+ # @param argv [Array<String>] 命令行参数
28
+ # @return [Integer] 进程退出码(仅在参数校验失败时返回)
29
+ def run argv
30
+ if argv.empty?
31
+ cd = Dir.pwd
32
+ fs = '*'
33
+ else
34
+ cd = argv.first
35
+ fs = argv.size>1 ? argv.last : '*'
36
+ if Dir["#{cd}/#{fs}"].select{|p|p[-3..-1]=='.md'}.empty?
37
+ warn "No file(#{cd}/#{fs}) exist."
38
+ return 1
39
+ end
40
+ end
41
+
42
+ warn "DONT USE POSTFIX .latest.md, FILENAME.latest.md WONT BE UPDATED!"
43
+
44
+ table = {}
45
+ loop do
46
+ scan_once table, cd, fs
47
+ sleep 1
48
+ end
49
+ end
50
+
51
+ # 执行一轮扫描:登记新文件、检测内容变更并渲染。
52
+ #
53
+ # 供监听循环调用,独立暴露以便测试。
54
+ #
55
+ # @param table [Hash{String => Array}] 变更登记表
56
+ # 值为 [mtime, size, md5摘要] 三元组
57
+ # @param cd [String] 监视目录
58
+ # @param fs [String] 文件名匹配模式(glob)
59
+ # @return [Array<String>] 本轮发生变更并完成渲染的文件路径列表
60
+ def scan_once table, cd, fs
61
+ events = []
62
+ Dir["#{cd}/#{fs}"].each do|path|
63
+ next unless path[-3..-1]=='.md'
64
+ next if path[-10..-1]=='.latest.md'
65
+ unless table[path]
66
+ table[path] = [File.stat(path).mtime, File.stat(path).size, Digest::MD5.hexdigest(File.read(path))]
67
+ end
68
+ otime, osize, odigest = table[path]
69
+ mtime = File.stat(path).mtime
70
+ msize = File.stat(path).size
71
+ if mtime>otime # && (msize != osize || msize == osize)
72
+ adigest = Digest::MD5.hexdigest File.read(path)
73
+ if adigest!= odigest
74
+ text = render path
75
+ table[path] = [mtime, msize, adigest]
76
+ latpath = (Dir["#{path[0..-4]}.*.md"] - ["#{path[0..-4]}.latest.md"]).max
77
+ latest = "#{path[0..-4]}.latest.md"
78
+ File.delete(latest) if File.exist?(latest)
79
+ puts ">#{path} changed -> #{latpath || latest}(#{adigest}).";$stdout.flush
80
+ if latpath
81
+ FileUtils.copy_file(latpath, latest)
82
+ else
83
+ File.write(latest, text) # 兜底:无时间戳渲染文件时直接写入渲染文本
84
+ end
85
+ events << path
86
+ end
87
+ end
88
+ end
89
+ return events
90
+ end
91
+
92
+ # 渲染单个文档(等价于对该文件执行一次无参数 cm)。
93
+ #
94
+ # @param path [String] Markdown 文档路径
95
+ # @return [String, nil] 渲染回写后的完整文档文本
96
+ def render path
97
+ cds = CasetDown.load path
98
+ CasetDown::CLI.render_file cds
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,127 @@
1
+ #coding:utf-8
2
+
3
+ ##################################################################################################################
4
+ #
5
+ # EnData::ApiRequest —— WEBOOT API 调用应用层
6
+ # 移植自 imdoc/EnData/api-app(依赖外部 gem webot 提供的 weboot 接口)。
7
+ #
8
+ # 本文件为可选加载:casetdown 入口在 require 时做了 LoadError 兜底,
9
+ # 未安装 webot 的环境中 cm/cml 与数据绑定功能不受影响,
10
+ # 仅 EnData::ApiRequest API 调用扩展不可用。
11
+ #
12
+ # API: template1 # an example of templates
13
+ # API: bind # single bind
14
+ # API: send # single send
15
+ # API: make # batch bind
16
+ # API: handle # batch send
17
+ # API: run # make + handle
18
+ #
19
+ # Step1: EnData::ApiRequest.make
20
+ # CASE: table1 × table2 × ... >--(join)--> task_table + bind_table(ref,val) >--eval(binding)--> task_instances
21
+ # USE: EnData.join_table() |> EnData.make_ref |> EnData.make_doc |> EnData::ApiRequest.bind
22
+ #
23
+ # Step2: EnData::ApiRequest.handle
24
+ # CASE: task_instances >--(API CALLING)--> running_results
25
+ # USE: EnData::ApiRequest.send
26
+ #
27
+ ##################################################################################################################
28
+ require 'weboot'
29
+
30
+ # 便捷方法:把表名对应的数据体用分号拼接(API 命令序列)。
31
+ #
32
+ # @param tabname [String] 表名
33
+ # @return [String] 分号拼接的命令串
34
+ def cmds tabname
35
+ return EnData.get_body(tabname).join(";")
36
+ end
37
+
38
+ # 便捷方法:批量生成并执行 API 任务(make + handle)。
39
+ #
40
+ # @param tables [Array<Array>] 参与拼接的任务表
41
+ # @param bind_table [Hash{String => String}] 引用名 => 引用文本
42
+ # @return [Array] 执行结果列表
43
+ def run tables, bind_table
44
+ EnData::ApiRequest.run tables, bind_table
45
+ end
46
+
47
+ module EnData
48
+ # API 请求应用层。
49
+ module ApiRequest
50
+ module_function
51
+
52
+ # 示例请求模板(bind 的缺省模板)。
53
+ #
54
+ # @return [String] 请求模板文本
55
+ def template1
56
+ %Q|POST ${path} HTTP/1.1
57
+ Host: ${server}
58
+ Accept: */*
59
+ Content-Type: application/json
60
+
61
+ {
62
+ "protocol": "${protocol}",
63
+ "host": "${device}",
64
+ "user": "${access-key}",
65
+ "app-key": "${app-key}",
66
+ "cmds": "${operations}",
67
+ "method": "${processes}"
68
+ }|
69
+ end
70
+
71
+ # 把单条绑定数据填入请求模板。
72
+ #
73
+ # @param options [Hash{String => Object}] 模板字段取值
74
+ # @param template [String] 请求模板,缺省 template1
75
+ # @return [String] 填充后的请求文本
76
+ def bind options, template=EnData::ApiRequest.template1
77
+ Replacement.init
78
+ ['path','server','protocol','device','access-key','app-key','operations','processes'].each do|key|
79
+ Replacement[key] = options[key].to_s
80
+ end
81
+ return TinText.instance(template)
82
+ end
83
+
84
+ # 发送单条请求并解析响应。
85
+ #
86
+ # @param context [String] 请求文本
87
+ # @return [Object] 响应中的 content 字段
88
+ def send context
89
+ request = Weboot.request context
90
+ response = Weboot.construct request
91
+ report = JSON.parse(response['body'])['content']
92
+ end
93
+
94
+ # 批量绑定:拼接任务表、替换引用、生成任务实例。
95
+ #
96
+ # @param tables [Array<Array>] 任务表列表(依次笛卡尔拼接)
97
+ # @param bind_table [Hash{String => String}] 引用名 => 引用文本
98
+ # @return [Array<String>] 任务实例列表
99
+ def make tables, bind_table
100
+ # Please don't join too much tables
101
+ tasks = tables.shift
102
+ tasks = EnData.join_table(tasks, tables.shift) until tables.empty?
103
+ bind_table.each{|refname, reftext|tasks = EnData.make_ref(tasks, refname, reftext)}
104
+ binds = EnData.make_doc(tasks)
105
+ instances = binds.map{|binding|EnData::ApiRequest.bind binding} # with EnData::ApiRequest::template
106
+ end
107
+
108
+ # 批量发送:逐条执行任务实例。
109
+ #
110
+ # @param instances [Array<String>] 任务实例列表
111
+ # @return [Array] 执行结果列表
112
+ def handle instances
113
+ instances.map{|instance| EnData::ApiRequest.send instance}
114
+ end
115
+
116
+ # make + handle 一体化执行。
117
+ #
118
+ # @param tables [Array<Array>] 任务表列表
119
+ # @param bind_table [Hash{String => String}] 引用名 => 引用文本
120
+ # @return [Array] 执行结果列表
121
+ def run tables, bind_table
122
+ instances = EnData::ApiRequest.make tables, bind_table
123
+ EnData::ApiRequest.handle(instances)
124
+ end
125
+
126
+ end
127
+ end
@@ -0,0 +1,197 @@
1
+ #coding:utf-8
2
+
3
+ ['yaml','json'].each{|mod|require mod}
4
+
5
+ #------------------------------------------------------------------------------
6
+ # 脚本内嵌数据整理(EnData)
7
+ #------------------------------------------------------------------------------
8
+ # 移植自 imdoc/EnData/endata + endata-app,为 casetdown 的代码执行通道提供
9
+ # 数据绑定能力:表格与全局配置以 endata 格式附着在脚本 __END__ 之后,
10
+ # 执行中的脚本通过 EnData.table('表名') 取回二维表数据。
11
+ #
12
+ # endata 数据段格式(位于脚本 __END__ 之后,有顺序要求):
13
+ #
14
+ # [global@endata ~]$ruby global
15
+ # {"配置名"=>"配置值", ...}
16
+ #
17
+ # [table@endata ~]$ruby 表名
18
+ # [["字段1", "字段2"], ["值1", "值2"], ...]
19
+ #
20
+ # 行格式为 [用户@endata 路径]$解释器 名称 参数1# ...
21
+ # - 用户/路径:占位语义,可自由命名
22
+ # - 解释器:ruby / yaml / json / plaintext,决定 source 的解析方式
23
+ # - 名称:表的唯一标识(EnData.table 的检索键)
24
+ #
25
+ # 使用样例见 doc/MANUAL.md 第 5 节“数据绑定”。
26
+ #------------------------------------------------------------------------------
27
+ module EnData
28
+ extend self
29
+
30
+ # 读取当前脚本的 __END__ 数据段(DATA 常量),并做结果缓存。
31
+ #
32
+ # @note 仅在带 __END__ 段的脚本(cm 生成的执行脚本)中可用
33
+ # @return [String] 数据段原始文本
34
+ def load
35
+ @__EnData__ ||= DATA.read
36
+ return @__EnData__
37
+ end
38
+
39
+ # 解析 endata 数据段为 {名称 => 数据项} 哈希。
40
+ #
41
+ # 每个数据项包含:user/path/handler/name/arguments/context 七个键。
42
+ #
43
+ # @param data [String, nil] 数据段文本,缺省读取当前脚本 DATA
44
+ # @return [Hash{String => Hash}] 数据项列表(按名称索引)
45
+ # ['username'@EnData 'workpath']$ 'command' 'scriptname' 'argument1#' ...
46
+ def parse data=@__EnData__
47
+ @__datalist__ = {}
48
+ temp = {
49
+ 'user' => '',
50
+ 'path' => '',
51
+ 'handler' => '',
52
+ 'name' => '',
53
+ 'arguments' => [],
54
+ 'context' => []
55
+ }
56
+ data.split("\n").each do|line|
57
+ if matcher = /^\[[\w|\s]*\@endata[\w|\s|\/|\~]*\]\$/.match(line)
58
+ @__datalist__[temp['name']] = temp
59
+ front, back = line.split("@endata")
60
+ usr = front.sub("[",'').strip
61
+ pwd = back.split("]")[0].strip
62
+ cmd = back.split("]$")[-1].split(" ")
63
+ handler, name, arguments = cmd[0], cmd[1], cmd[2..-1]
64
+ context = ''
65
+ temp = {
66
+ 'user' => usr,
67
+ 'path' => pwd,
68
+ 'handler' => handler,
69
+ 'name' => name,
70
+ 'arguments' => arguments,
71
+ 'context' => []
72
+ }
73
+ else
74
+ temp['context'] << line
75
+ end
76
+ end
77
+ @__datalist__[temp['name']] = temp
78
+ return @__datalist__
79
+ end
80
+
81
+ # 按名称或属性选择数据项。
82
+ #
83
+ # @param options [Hash] 检索条件
84
+ # - :name 按名称精确选择
85
+ # - 其他键按数据项属性过滤(user/path/handler/...)
86
+ # @return [Hash, nil] 命中的数据项
87
+ def select options={}
88
+ if name = options[:name]
89
+ @__datalist__[name]
90
+ else
91
+ sets = @__datalist__.values
92
+ options.each do|key, val|
93
+ sets = sets.select{|s|s[key.to_s]==val.to_s}
94
+ end
95
+ sets.first
96
+ end
97
+ end
98
+
99
+ # 把数据项正文按解释器还原为 ruby / yaml / json / plaintext 对象。
100
+ #
101
+ # @param script [Hash, nil] parse 产生的数据项
102
+ # @return [Object, nil] 解析结果
103
+ def source script
104
+ return nil unless script && script['context']
105
+ context = script['context'].join("\n")
106
+ case script['handler']
107
+ when 'ruby'
108
+ context
109
+ when 'yaml'
110
+ YAML.load context
111
+ when 'json'
112
+ JSON.parse context
113
+ else # plaintext
114
+ context
115
+ end
116
+ end
117
+
118
+ # ruby inline:把数据段中的 ruby 文本在模块上下文中执行。
119
+ #
120
+ # @param scripts [String, nil] ruby 源码文本
121
+ # @yield [void] 无 scripts 时执行 block
122
+ # @return [Object] 执行结果
123
+ def run scripts=nil, &script_block
124
+ scripts ? self.module_eval(scripts) : self.module_eval(&script_block)
125
+ end
126
+ end
127
+
128
+ #------------------------------------------------------------------------------
129
+ # EnData 应用层(移植自 imdoc/EnData/endata-app)
130
+ #------------------------------------------------------------------------------
131
+ module EnData
132
+ module_function
133
+
134
+ # 取回指定名称的表格数据(执行脚本内最常用的入口)。
135
+ #
136
+ # [CUSTOM_YOUR_NAME@endata CUSTOM_YOUR_PATH]$ruby table
137
+ #
138
+ # @param name [String] 表名,缺省 'table';全局配置固定占用表名 'global'
139
+ # @return [Object] 表格数据(通常是二维数组)
140
+ def table name='table'
141
+ EnData.load()
142
+ EnData.parse()
143
+ EnData.run(EnData.source EnData.select name: name)
144
+ end
145
+
146
+ # 表1 × 表2 笛卡尔拼接(行对行叉积)。
147
+ #
148
+ # @param name1 [String] 左表名
149
+ # @param name2 [String] 右表名
150
+ # @return [Array<Array>] 拼接结果新表
151
+ def join_table name1, name2
152
+ table1 = EnData.table(name1)
153
+ table2 = EnData.table(name2)
154
+ table1.绑定表头 table1.first
155
+ table2.绑定表头 table2.first
156
+ return table1.拼接 table2
157
+ end
158
+
159
+ # 取表格的数据体(去表头)。
160
+ #
161
+ # @param name [String] 表名
162
+ # @return [Array<Array>] 数据行列表
163
+ def get_body name
164
+ EnData.table(name)[1..-1]
165
+ end
166
+
167
+ # 把 table[head, record...] 档案化为 {字段=>值} 对象数组。
168
+ #
169
+ # CASE: table[head=[field*], record=[value*]] >----> tree{field=>value, ...}
170
+ #
171
+ # @param table [Array<Array>] 二维表
172
+ # @return [Array<Hash>] 对象数组
173
+ def make_doc table
174
+ return table[1..-1].inject([]) do|doc, record|
175
+ rec = {}
176
+ record.each_with_index{|value, index|rec[table[0][index]] = value}
177
+ doc.push rec; doc
178
+ end
179
+ end
180
+
181
+ # 把表中 ${引用名} 占位替换为引用文本,生成新表。
182
+ #
183
+ # CASE: ${refname} >--(replace)--> reftext
184
+ #
185
+ # @param table [Array<Array>] 二维表
186
+ # @param refname [String] 引用名
187
+ # @param reftext [String] 引用文本
188
+ # @return [Array<Array>] 替换结果新表
189
+ def make_ref table, refname, reftext
190
+ return table.inject([]) do|newtab, record|
191
+ newrec = []
192
+ record.each{|field|newrec << (field=="${#{refname}}" ? reftext : field)}
193
+ newtab.push newrec; newtab
194
+ end
195
+ end
196
+
197
+ end