-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathyii2docbot.js
290 lines (268 loc) · 10.2 KB
/
yii2docbot.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
'use strict';
/**
* @file yii2docbot.js
* @license GPL 3.0
* @copyright 2015 Tom Worster [email protected]
*
* Copyright 2015 Tom Worster
*
* This file is part of yii2docbot.
*
* yii2docbot is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* yii2docbot is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with yii2docbot. If not, see <http://www.gnu.org/licenses/>.
*/
var getBot,
optionsRe,
config = {},
options = {
repl: false,
test: false,
server: 'irc.libera.chat',
channel: '#yii2docbot',
nick: 'yii2docbot',
pass: undefined,
realName: 'Documentation bot for Yii 2',
types: undefined,
botPath: './bot.js'
},
/**
* Reads JSON output from the yii2-apidoc docuemntation gernator, processes it into a
* search index and writes that to docs.json in the CWD.
*
* @see http://www.yiiframework.com/doc-2.0/ext-apidoc-index.html
* @see https://github.com/tom--/yii2-apidoc
*
* @param {String} types The output JSON document from yii2-apidoc.
*/
indexTypes = function (types) {
/**
* The index is an object of search keys, each mapping to an array of one or more leaves.
*
* The search key is the type name if the API item is a type (i.e. class, trait or interface)
* or the member name of the API item is a type member (i.e. property, method or constant).
* The key is stripped of special chars and underscores and in lower case.
*
* Each leaf is an object with properties:
* - name: The item's fully-qualified name, which depends on the kind of API item:
* - "name\space\TypeName" for a class, trait or interface
* - "name\space\TypeName::$property" for a property (note the $ after the ::)
* - "name\space\TypeName::method()" for a method (note the dog's bollox at end)
* - "name\space\TypeName::CONST" for a constant (no special chars, name may have underscores)
* - desc: The short description from the PHP docbock
* and if the item is a member:
* - definedBy: The fully-qualified name of the class that defines the item
*
* Thus, for example, one of the entries in index might look like:
{
...
"query": [
{"name": "yii\\db\\Query",
"desc": "Query represents a SELECT SQL statement in a way that is independent of DBMS."},
{"name": "yii\\data\\ActiveDataProvider::$query",
"desc": "The query that is used to fetch data models and [[totalCount]]\nif it is not explicitly set.",
"definedBy": "yii\\data\\ActiveDataProvider"},
{"name": "yii\\db\\Command::query()",
"desc": "Executes the SQL statement and returns query result.",
"definedBy": "yii\\db\\Command"}
],
...
}
*
*/
var index = {},
/**
* Adds a leaf to index (i.e. the search tree) and (if needed) a key node.
* @param {String} kind What kind of API item to add "t" = type, "m" = method, also "p" and "c"
* @param {Object} typeName Fully-qualified name of the type the item appears in
* @param {Object} item The phpdoc object for the API item to add
*/
addLeaf = function (kind, typeName, item) {
if (!item.name) {
return;
}
var keyword = item.name.match(/\w+$/)[0].replace(/_/g, '').toLowerCase(),
leaf = {
name: typeName,
desc: item.shortDescription
};
if (kind !== 't') {
leaf.name += '::' + item.name;
if (kind === 'm') {
leaf.name += '()';
}
leaf.definedBy = item.definedBy;
}
if (!index.hasOwnProperty(keyword)) {
index[keyword] = [];
}
index[keyword].push(leaf);
};
// Iterate over the types in the JSON file.
Object.keys(types).map(function (name) {
var type = types[name],
kinds = ['methods', 'properties', 'constants'];
addLeaf('t', type.name, type);
// Look for the three kinds of member in each type object.
kinds.map(function (kind) {
if (type.hasOwnProperty(kind) && type[kind]) {
// Iterate over each member adding it to the index.
Object.keys(type[kind]).map(function (key) {
addLeaf(kind[0], type.name, type[kind][key]);
});
}
});
});
// Write the documentation index to a JSON file.
require('fs').writeFile('./docs.json', JSON.stringify(index, null, ' '), function (err) {
if (err) {
console.error('Error:', err);
}
console.log('Saved index to: ./docs.json')
});
},
/**
* Start a node REPL using our own eval function that calls the Yii 2 doc bot.
*/
replBot = function () {
require('repl').start({
"prompt": 'bot> ',
"eval": function (cmd, context, filename, callback) {
var answers;
try {
console.log(Date.millinow() + "\n");
answers = getBot().bot('nick', cmd, '');
} catch (err) {
console.error('Error:', err);
}
if (answers) {
callback(undefined, answers.join(" ... "));
}
}
});
},
/**
* Start an IRC client and add a listener that calls the Yii 2 doc bot.
* @param {Object} options The main script options object.
*/
ircBot = function (options) {
var irc = require('irc'),
client,
reply = function (to) {
return function (from, message) {
var answers;
answers = getBot().bot(from, message, to || '');
if (answers && answers.length > 0) {
answers.map(function (answer) {
client.say(to || from, answer);
console.log(Date.millinow() + ' ' + options.nick + ': ' + answer);
});
}
};
};
if (!options.pass) {
throw "Can't connect to IRC without a password, use --pass option.";
}
client = new irc.Client(
options.server,
options.nick,
{
server: options.server,
nick: options.nick,
channels: [options.channel],
userName: options.nick,
password: options.pass,
realName: options.realName,
sasl: false,
port: 6697,
secure: true,
autoConnect: true
}
);
client
.addListener('error', function (message) {
console.log('error: ', message);
})
.addListener('message' + options.channel, reply(options.channel))
.addListener('message' + options.channel, function (from, message) {
console.log(Date.millinow() + ' ' + from + ': ' + message);
})
.addListener('pm', reply());
};
/**
* Add a method to Date that returns logging timestamps.
* @returns {string}
*/
Date.millinow = function () {
var now = new Date();
function pad(number) {
if (number < 10) {
return '0' + number;
}
return number;
}
return pad(now.getUTCHours()) +
':' + pad(now.getUTCMinutes()) +
':' + pad(now.getUTCSeconds()) +
'.' +
(now.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) +
'Z';
};
// look for --config=path in args and load the (JSON) file into config object
process.argv.some(function (arg) {
var matches = arg.match(/^--config=(.+)$/);
if (matches) {
config = require(matches[1]);
return true;
}
});
// config file options override the hard-coded defaults
Object.keys(config).map(function (key) {
if (options.hasOwnProperty(key)) {
options[key] = config[key];
}
});
// finally, each option can overridden from command line
// Note: not escaping the option names.
optionsRe = new RegExp('^--(' + Object.keys(options).join('|') + ')(?:=(.+))?$');
process.argv.map(function (arg) {
var matches = arg.match(optionsRe);
if (matches) {
options[matches[1]] = matches[2] || true;
}
});
// The supplied path input can be relative but we need the absolute path because that's
// what the require cache uses for its key, which we need to delete the cache entry if
// --test was specified.
options.botPath = require.resolve(options.botPath);
// Create a a bot-getter function that returns the bot function, reloading
// the bot module each time it is called if the --test option was set.
getBot = (function (options) {
var bot;
return function () {
if (!bot || options.test) {
delete require.cache[options.botPath];
bot = require(options.botPath);
}
return bot;
};
}(options));
// Update the search index if requested.
if (options.types) {
indexTypes(require(options.types));
}
// Start the bot either in a REPL or as an IRC client.
if (options.repl) {
replBot();
} else {
ircBot(options);
}