helpline 0.1.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,18 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ Node版GitHelp
4
+ -->
5
+ <html>
6
+ <head>
7
+ <meta charset="UTF-8">
8
+ <title>HelpLine</title>
9
+ <link rel="stylesheet" href="helpline.css" type="text/css">
10
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
11
+ <script src="helpline_browserify.js"></script>
12
+ </head>
13
+ <body>
14
+ <input id='query' type="text" style="width:60%;">
15
+ <div id="candidates" height='100%'>
16
+ </div>
17
+ </body>
18
+ </html>
@@ -0,0 +1,252 @@
1
+ //
2
+ // CommandLine Help レンダラ
3
+ //
4
+ const electron = window.require('electron');
5
+ //const electron = require('electron');
6
+ const remote = electron.remote;
7
+ const shell = electron.shell;
8
+ //const clipboard = electron.clipboard; // clipboard.writeText() でクリップボードに文字列が入る
9
+
10
+ const data = require("./data");
11
+ const Generator = require('re_expand');
12
+
13
+ var g; // ExpandHelp generator
14
+
15
+ // 実行時に取得する各種環境値
16
+ var param_files = '___';
17
+ var param_branches = remote.app.branches();
18
+ var param_params = 'param1|param2';
19
+ var param_numbers = '1|2|3';
20
+
21
+ const glossary = require("./glossary"); // 各種定義をいれておく
22
+ for(var e in glossary){
23
+ s = `param_${e} = '${glossary[e]}'`;
24
+ eval(s); // これが良くない! glossaryに「delete」なんてのが定義されてると "delete = 'abc'" みたいなのをevalしてしまう!
25
+ }
26
+
27
+ var commands = [];
28
+ var commandind = 0;
29
+ var selected = 0;
30
+ var g;
31
+
32
+ function generator(patterns){
33
+ g = new Generator();
34
+
35
+ param_files = remote.app.files(patterns).join('|'); // レンダラプロセスではコマンド起動できないようなのでメインプロセスを利用してファイルリストを取得
36
+ param_params = get_params(patterns);
37
+ param_numbers = get_numbers(patterns);
38
+
39
+ var lines = [];
40
+ for(var def of data.defs){
41
+ m = def.match(/^\s*\$\s*(.*)\s*$/);
42
+ if(m){
43
+ lines.push(m[1]);
44
+ }
45
+ m = def.match(/^\s*\%\s*(.*)\s*$/);
46
+ if(m){
47
+ var cmd = m[1];
48
+ for(var line of lines){
49
+ var desc = line.replace(/\s*{(\d+)}\s*$/,'');
50
+ desc = ("\`"+desc+"\`").replace(/#{/g,'${param_');
51
+ console.log(desc)
52
+ cmd = cmd.replace(/#{\$(\d+)}/g,"DOLLAR$1").replace(/DOLLAR/g,'$');
53
+ g.add(`${eval(desc)}`,cmd);
54
+ }
55
+ }
56
+ }
57
+ return g;
58
+ }
59
+
60
+ function sel(e){
61
+ selected = Number($(e.target).attr('id').match(/(\d+)$/)[1]);
62
+ show_selected();
63
+ $("html,body").animate({scrollTop:$(`#entry${selected}`).offset().top - 100});
64
+ $('#query').focus();
65
+ }
66
+
67
+ function get_params(patterns){
68
+ var a = new Set;
69
+ for(var pattern of patterns){
70
+ var m;
71
+ if(m = pattern.match(/^'(.*)'$/)){
72
+ a.add(m[1]);
73
+ }
74
+ if(m = pattern.match(/^"(.*)"$/)){
75
+ a.add(m[1]);
76
+ }
77
+ if(m = pattern.match(/^\[(.*)\]$/)){
78
+ a.add(m[1]);
79
+ }
80
+ }
81
+ a = Array.from(a);
82
+ if(a.length == 0){
83
+ a = ['param'];
84
+ }
85
+ return a.join('|');
86
+ }
87
+
88
+ function get_numbers(patterns){
89
+ var a = new Set;
90
+ for(var pattern of patterns){
91
+ if(pattern.match(/^\d+$/)){
92
+ a.add(pattern);
93
+ }
94
+ }
95
+ a = Array.from(a);
96
+ if(a.length == 0){
97
+ a = ['1'];
98
+ }
99
+ return a.join('|');
100
+ }
101
+
102
+ function addentry(a, cmd){ // 候補を整形してリストに追加
103
+ console.log(cmd);
104
+ var num = cmd.match(/\s*{(\d+)}$/,"$1")[1]; // 説明ページの番号を取得
105
+ cmd = cmd.replace(/\s*{(\d+)}$/,"");
106
+ if(commands.indexOf(cmd) >= 0) return;
107
+ commands[commandind] = cmd;
108
+ var entry = $('<div>')
109
+ .on('click',sel)
110
+ .attr('id',`entry${commandind}`)
111
+ .attr('class','entry')
112
+ .appendTo($('#candidates'));
113
+ var title = $('<span>')
114
+ .on('click',sel)
115
+ .attr('id',`title${commandind}`)
116
+ .attr('class','title')
117
+ .text(a[0])
118
+ .appendTo(entry);
119
+ var icon = $('<img>')
120
+ .attr('id',`icon${commandind}`)
121
+ .attr('src',"https://www.iconsdb.com/icons/preview/orange/info-xxl.png")
122
+ .attr('class','icon')
123
+ .attr('desc',num)
124
+ .appendTo(entry);
125
+ $('<br>').appendTo(entry);
126
+ icon.on('click',function(e){
127
+ // とてもよくわからないがこれで外部ブラウザを開ける
128
+ var t = data.pages[$(e.target).attr('desc')];
129
+ var url = `https://scrapbox.io/GitHelp/${t}`;
130
+ shell.openExternal(url);
131
+ });
132
+ var code = $('<code>')
133
+ .on('click',sel)
134
+ .attr('id',`code${commandind}`)
135
+ .text(cmd)
136
+ .appendTo(entry);
137
+
138
+ if(commandind == selected){
139
+ entry.css('background-color','#ccc');
140
+ title.css('background-color','#ccc');
141
+ }
142
+ else {
143
+ entry.css('background-color','#fff');
144
+ title.css('background-color','#fff');
145
+ }
146
+
147
+ commandind += 1;
148
+ }
149
+
150
+ var key_timeout = null;
151
+ var control = false;
152
+
153
+ function show_selected(){
154
+ for(var i=0; i<commands.length; i++){
155
+ if(selected == i){
156
+ $(`#entry${i}`).css('background-color','#ccc');
157
+ $(`#title${i}`).css('background-color','#ccc');
158
+ }
159
+ else {
160
+ $(`#entry${i}`).css('background-color','#fff');
161
+ $(`#title${i}`).css('background-color','#fff');
162
+ }
163
+ }
164
+ }
165
+
166
+ function init(){
167
+ // keypressだと日本語入力時のEnterキーが入らない
168
+ //$('#query').on('keypress', function(e){
169
+ $('body').on('keypress', function(e){
170
+ if(e.keyCode == 13){ // Enter
171
+ remote.app.finish(commands[selected]);
172
+ }
173
+ });
174
+
175
+ //$('#query').on('keydown', function(e){
176
+ $('body').on('keydown', function(e){
177
+ if(e.keyCode == 17){ // Control Key
178
+ control = true;
179
+ }
180
+ else if(control && (e.keyCode == 67 || e.keyCode == 71)){ // Ctrl-C, Ctrl-G
181
+ remote.app.finish(remote.app.argv()[1]); // 引数をもとに戻す
182
+ }
183
+ else if((e.keyCode == 78 && control) || e.keyCode == 40){ // Ctrl-N or ↓
184
+ if(selected < commands.length-1){
185
+ selected += 1;
186
+ show_selected();
187
+ $("html,body").animate({scrollTop:$(`#entry${selected}`).offset().top - 100});
188
+ }
189
+ }
190
+ else if((e.keyCode == 80 && control) || e.keyCode == 38){ // Ctrl-P or ↑
191
+ if(selected > 0){
192
+ selected -= 1;
193
+ show_selected();
194
+ $("html,body").animate({scrollTop:$(`#entry${selected}`).offset().top - 100});
195
+ }
196
+ }
197
+ });
198
+
199
+ //$('#query').on('keyup', function(e){
200
+ $('body').on('keyup', function(e){
201
+ if(e.keyCode == 17){
202
+ control = false;
203
+ }
204
+ else if(!control && e.keyCode != 40 && e.keyCode != 38){
205
+ $('#candidates').empty();
206
+ commandind = 0;
207
+ commands = [];
208
+ selected = 0;
209
+ show_selected();
210
+
211
+ // インクリメンタルにファイル名やパラメタも計算してマッチング
212
+ // したいところだがそれだとすごく遅い
213
+ clearTimeout(key_timeout);
214
+ setTimeout(function(){
215
+ var qstr = $('#query').val();
216
+ g = generator(qstr.split(/\s+/));
217
+ var pstr = qstr.replace(/'/g,'').replace(/"/g,'');
218
+ g.filter(` ${pstr} `, addentry, 0);
219
+ },500);
220
+ }
221
+ });
222
+
223
+
224
+ var arg = remote.app.argv()[1]
225
+ if(arg.match(/^git/)){
226
+ // var qstr = arg.replace(/^git\s*/,'');
227
+ qstr = qstr.replace(/(\d)([^\d])/g,"$1 $2") // 10分 => 10 分
228
+ $('#query').val(qstr);
229
+ g = generator(qstr.split(/\s+/));
230
+ var pstr = qstr.replace(/'/g,'').replace(/"/g,'');
231
+
232
+ g.filter(` ${pstr} `, addentry, 0);
233
+ }
234
+ else {
235
+ var qstr = arg
236
+ $('#query').val(qstr);
237
+ g = generator(qstr.split(/\s+/));
238
+ var pstr = qstr.replace(/'/g,'').replace(/"/g,'');
239
+ g.filter(` ${pstr} `, addentry, 0);
240
+
241
+ // g = generator([]);
242
+ // g.filter(' ', addentry, 0);
243
+ }
244
+
245
+ $('#query').focus();
246
+
247
+ }
248
+
249
+ $(function() {
250
+ init();
251
+ });
252
+
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- ruby -*-
3
+
4
+ require 'json'
5
+ require 'optparse'
6
+
7
+ require 'scrapbox'
8
+ require 're_expand'
9
+
10
+ require 'io/console'
11
+ require './curses'
12
+
13
+ puts
14
+
15
+ class HelpLine
16
+ LINES = 12
17
+
18
+ def datafile
19
+ File.expand_path("~/.helpline")
20
+ end
21
+
22
+ def initialize
23
+ @pagedata = {}
24
+ @project = Scrapbox::Project.new("HelpLine")
25
+ end
26
+
27
+ def getdata
28
+ #
29
+ # ページデータ取得
30
+ #
31
+ puts "-----------------ページデータを取得"
32
+ @project.pages.each { |title,page|
33
+ puts "...#{title}"
34
+ @pagedata[title] = page.text.split(/\n/)
35
+ }
36
+
37
+ dumpdata = {}
38
+ dumpdata['codes'] = []
39
+ dumpdata['defs'] = []
40
+ dumpdata['pages'] = []
41
+
42
+ #
43
+ # 関数/定数を評価"
44
+ #
45
+ puts "-----------------関数/定数を取得"
46
+ @pagedata.each { |title,pagedata|
47
+ puts "...#{title}"
48
+ pagedata. each { |line|
49
+ if line =~ /code:(.*\.rb)$/ then
50
+ src = $1
51
+ puts "=========== #{src}"
52
+ page = Scrapbox::Page.new(@project,title)
53
+ dumpdata['codes'] << page.code(src)
54
+ end
55
+ }
56
+ }
57
+ puts "-----------------HelpLineデータを検出"
58
+ @pagedata.each { |title,pagedata|
59
+ puts "...#{title}"
60
+ dumpdata['pages'] << title
61
+ processing_defs = false
62
+ codeindent = nil
63
+ pagedata.each { |line|
64
+ if !codeindent
65
+ if line =~ /^(\s*)code:/
66
+ codeindent = $1.length
67
+ next
68
+ end
69
+ else
70
+ line =~ /^(\s*)/
71
+ if line.length < codeindent
72
+ codeindent = nil
73
+ else
74
+ next
75
+ end
76
+ end
77
+ if line =~ /^\s*[\$\%]/
78
+ puts line
79
+ if line =~ /^\%/ && !processing_defs
80
+ puts "'$'で始まる用例定義なしでコマンドを定義しようとしています"
81
+ exit
82
+ end
83
+ dumpdata['defs'] << "#{line} {#{dumpdata['pages'].length-1}}"
84
+ processing_defs = true
85
+ else
86
+ processing_defs = false
87
+ end
88
+ }
89
+ }
90
+
91
+ File.open(datafile,"w"){ |f|
92
+ f.puts dumpdata.to_json
93
+ }
94
+ end
95
+
96
+ def disp(list,sel)
97
+ Curses.move(0,0)
98
+ lines = list.length
99
+ lines = LINES if lines > LINES
100
+ (0...lines).each { |i|
101
+ Curses.move(i,0)
102
+ s = "[#{i}] #{list[i][0]}"
103
+ if i == sel
104
+ Curses.print_inverse s
105
+ else
106
+ Curses.print s
107
+ end
108
+ }
109
+ Curses.down
110
+ Curses.tol
111
+ end
112
+
113
+ def helpline
114
+ data = JSON.parse(File.read(datafile))
115
+ unless data['pages'] # データ型式変換があったので
116
+ getdata
117
+ data = JSON.parse(File.read(datafile))
118
+ end
119
+
120
+ #
121
+ # 関数定義などをeval
122
+ #
123
+ data['codes'].each { |code|
124
+ eval code
125
+ }
126
+
127
+ g = ExpandRuby::Generator.new # re_expandのジェネレータ
128
+
129
+ #
130
+ # HelpLineエントリ
131
+ #
132
+ lines = []
133
+ data['defs'].each { |line|
134
+ if line =~ /^\s*\$\s*(.*)$/ # $....
135
+ lines << $1
136
+ elsif line =~ /^\s*\%\s*(.*)$/ # %....
137
+ cmd = $1
138
+ lines.each { |l|
139
+ desc = eval('"' + l + '"')
140
+ g.add desc.force_encoding('utf-8'), cmd.force_encoding('utf-8')
141
+ }
142
+ lines = []
143
+ end
144
+ }
145
+
146
+ # puts "GENERATE #{params.split('|').join(' ')} "
147
+
148
+ res = g.generate " #{ARGV.join(' ').sub(/\[/,'').sub(/\]/,'')} "
149
+
150
+ if res[0].length == 0
151
+ puts "ヘルプがみつかりません"
152
+ File.open("/tmp/helpline","w"){ |f|
153
+ f.puts ARGV.join(' ')
154
+ }
155
+ exit
156
+ end
157
+
158
+ listed = {}
159
+ list = res[0].find_all { |a| # 0 ambig
160
+ # a = ["現在の状況を表示する {56}", "git status {56}"], etc.
161
+ if listed[a[1]]
162
+ false
163
+ else
164
+ listed[a[1]] = true
165
+ end
166
+ }
167
+
168
+ #
169
+ # HelpLineメニュー表示し、カーソル移動で選択
170
+ #
171
+
172
+ help_number = {}
173
+ list.each_with_index { |entry,ind|
174
+ entry[0].sub!(/\s*{(\d*)}$/,'')
175
+ entry[1].sub!(/\s*{(\d*)}$/,'')
176
+ help_number[entry[0]] = $1.to_i
177
+ }
178
+
179
+ sel = 0
180
+ disp(list,sel)
181
+
182
+ inputchars = ''
183
+ while true
184
+ c = STDIN.getch
185
+ inputchars += c
186
+
187
+ if inputchars == "\e"
188
+ # process ESC
189
+ elsif inputchars[0] == "\e" && inputchars.length == 2
190
+ # 何もしない
191
+ elsif inputchars == "\x06" || inputchars == "\e[C"
192
+ # Curses.right
193
+ inputchars = ''
194
+ elsif inputchars == "\x02" || inputchars == "\e[D"
195
+ # Curses.left
196
+ inputchars = ''
197
+ elsif inputchars == "\x0e" || inputchars == "\e[B"
198
+ Curses.down
199
+ sel = (sel + 1) if sel < LINES-1
200
+ inputchars = ''
201
+ elsif inputchars == "\x10" || inputchars == "\e[A"
202
+ Curses.up
203
+ sel = sel - 1 if sel > 0
204
+ inputchars = ''
205
+ else
206
+ inputchars = ''
207
+ end
208
+ STDIN.flush
209
+ disp(list,sel)
210
+
211
+ exit if c== 'q' || c == "\x03"
212
+
213
+ if c == "\r" || c == "\n"
214
+ break
215
+ end
216
+ end
217
+
218
+ desc = list[sel.to_i][0]
219
+ cmd = list[sel][1]
220
+
221
+ Curses.print_inverse("「#{desc}」を実行")
222
+ puts " (ソース: http://scrapbox.io/HelpLine/#{data['pages'][help_number[desc]]})"
223
+ File.open("/tmp/helpline","w"){ |f|
224
+ f.puts cmd
225
+ }
226
+ end
227
+ end
228
+
229
+ # is_repository = system 'git rev-parse --git-dir > /dev/null >& /dev/null'
230
+ # unless is_repository
231
+ # STDERR.puts "Gitレポジトリで実行して下さい"
232
+ # exit
233
+ # end
234
+
235
+ options = ARGV.getopts('u')
236
+
237
+ helpline = HelpLine.new
238
+
239
+ if !File.exist?(helpline.datafile) && !options['u']
240
+ puts "#{helpline.datafile}を作成します..."
241
+ helpline.getdata
242
+ end
243
+
244
+ if options['u'] then
245
+ helpline.getdata
246
+ else
247
+ helpline.helpline
248
+ end