This repository has been archived by the owner on Feb 18, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
476 lines (397 loc) · 16 KB
/
index.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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
var generator = require('mock-generator');
var Random = require('mock-random');
var _ = require('lodash').runInContext();
var Handlebars = require('handlebars');
_.extend(_, {
type: function (obj) {
return (obj === null || obj === undefined) ? String(obj) : Object.prototype.toString.call(obj).match(/\[object (\w+)\]/)[1].toLowerCase();
}
});
var Mock4Tpl = {
version: '0.0.1'
}
module.exports = Mock4Tpl
/*
Mock4Tpl.mock(input)
Mock4Tpl.mock(input, options)
Mock4Tpl.mock(input, options, helpers)
Mock4Tpl.mock(input, options, helpers, partials)
*/
Mock4Tpl.mock = function(input, options, helpers, partials) {
helpers = helpers ? _.extend({}, helpers, Handlebars.helpers) :
Handlebars.helpers
partials = partials ? _.extend({}, partials, Handlebars.partials) :
Handlebars.partials
return Handle.gen(input, null, options, helpers, partials)
}
var Handle = {
debug: Mock4Tpl.debug || false,
extend: _.extend
}
/*
Handle.gen(input, context, options, helpers, partials)
Handle.gen(ast, context, options, helpers, partials)
Handle.gen(node, context, options, helpers, partials)
input HTML 模板
ast HTML 模板
node
context
options
helpers
partials
## 构造路径
* 对于对象
{
a: {
b: {
c: d
}
}
}
→
[ a, b, c, d]
* 对于数组
{
a: [{
b: [{
c: d
}
]
}
]
}
->
[ a, [], b, [], c ]
*/
Handle.gen = function(node, context, options, helpers, partials) {
if (_.isString(node)) {
var ast = Handlebars.parse(node)
options = Handle.parseOptions(node, options)
var data = Handle.gen(ast, context, options, helpers, partials)
return data
}
context = context || [{}]
options = options || {}
if (this[node.type] === _.noop) return
options.__path = options.__path || []
if (Mock4Tpl.debug || Handle.debug) {
console.log()
console.group('[' + node.type + ']', JSON.stringify(node))
// console.log('[context]', context.length, JSON.stringify(context))
console.log('[options]', options.__path.length, JSON.stringify(options))
}
var preLength = options.__path.length
this[node.type](node, context, options, helpers, partials)
options.__path.splice(preLength)
if (Mock4Tpl.debug || Handle.debug) {
// console.log('[context]', context.length, JSON.stringify(context))
console.groupEnd()
}
return context[context.length - 1]
}
Handle.parseOptions = function(input, options) {
var rComment = /<!--\s*\n*Mock\s*\n*([\w\W]+?)\s*\n*-->/g;
var comments = input.match(rComment),
ret = {}, i, ma, option;
for (i = 0; comments && i < comments.length; i++) {
rComment.lastIndex = 0
ma = rComment.exec(comments[i])
if (ma) {
/*jslint evil: true */
option = new Function('return ' + ma[1])
option = option()
_.extend(ret, option)
}
}
return _.extend(ret, options)
}
/*
name 字符串,属性名
options 字符串或对象,数据模板
context 父节点,任意值
def 默认值
*/
Handle.val = function(name, options, context, def) {
if (name !== options.__path[options.__path.length - 1]) throw new Error(name + '!==' + options.__path)
if (Mock4Tpl.debug || Handle.debug) console.log('[options]', name, options.__path);
if (def !== undefined) def = generator(def)
if (options) {
var mocked = generator(options)
if (_.isString(mocked)) return mocked
if (name in mocked) {
return mocked[name]
}
}
if (_.isArray(context[0])) return {}
return def !== undefined ? def : (name) || Random.word()
}
/*
AST
*/
Handle.program = function(node, context, options, helpers, partials) {
for (var i = 0; i < node.statements.length; i++) {
this.gen(node.statements[i], context, options, helpers, partials)
}
// TODO node.inverse
}
Handle.mustache = function(node, context, options, helpers, partials) { // string id params
var i,
currentContext = context[0],
contextLength = context.length;
if (_.type(currentContext) === 'array') {
currentContext.push({})
currentContext = currentContext[currentContext.length - 1]
context.unshift(currentContext)
}
// "isHelper": 1
// 为何要明确的 isHelper?因为 eligibleHelper 实在不可靠!
if (node.isHelper || helpers && helpers[node.id.string]) {
// node.params
if (node.params.length === 0) {
// TODO test_helper_this_with_register_and_holder
} else {
for (i = 0; i < node.params.length; i++) {
this.gen(node.params[i], context, options, helpers, partials)
}
}
// node.hash
if (node.hash) this.gen(node.hash, context, options, helpers, partials)
} else {
// node.id
this.gen(node.id, context, options, helpers, partials)
/*
node.id.type === 'DATA'
eg @index,放到 DATA 中处理 TODO
*/
}
if (context.length > contextLength) context.splice(0, context.length - contextLength)
}
Handle.block = function(node, context, options, helpers, partials) { // mustache program inverse
var parts = node.mustache.id.parts,
i, len, cur, val, type,
currentContext = context[0],
contextLength = context.length;
if (node.inverse) {} // TODO
/*
## 关于自定义 block
{{#block}}{{...}}{{/block}}
| helper | type | 模板引擎的行为 | 模拟数据 |
| ------ | -------- | ----------------------------------------- | ------------ |
| Y | function | 由 helper 负责返回最后的结果 | 不处理 |
| N | array | 遍历该数组,渲染包含的 statements | 默认为对象 |
| N | object | 把该对象作为新 context,渲染包含的 statements | 默认为对象 |
| N | boolean | 用当前 context 渲染包含的 statements | 默认为对象 |
### 为什么默认为对象
无论默认为对象或数组,当真实数据的类型与默认数据不匹配时,模拟数据的渲染结果都会与预期不符合。
而把模拟数据的默认值设置为对象,则可以在渲染 object、boolean 时大致符合预期。
更直观(易读)的做法是
1. 明确指定数据的类型:数组 []、对象 {}、布尔 true/false。
2. 遍历数组时,用 each helper。
3. 为数据属性设置 Mock 参数,例如 `arr|5-10`: []。
然而,目前开发过程中遇到的更多的是因此数组,这不是一个好习惯,因为在理解上会造成模棱两可的印象。
我希望 Mock 是一个可用,并且好用的工具,除了这两个原则之外,任何固有的原则都可以放弃,
因此如果有任何感受和建议,请反馈给我,如果可以贡献代码就更好了。
### 另外,为什么称为上下文 context,而不是作用域 scope 呢?
在 Mock4Tpl 的模拟过程中,以及模板引擎的渲染过程中,只是在某个对象或数组上设置或设置属性,是上下文的概念,根本没有“作用域”的概念。
虽然从理解的角度,这两个过程与作用域极为“相似”,但是如果描述成“相似”,其实是对本质和概念的误导。
但是。。。好吧,确实“作用域”要更形象,更易于被不了解内部原理的人所接受。
*/
// block.mustache
if (node.mustache.isHelper ||
helpers && helpers[node.mustache.id.string]) {
type = parts[0] // helper: each if unless with log
// 指定 Handle 为上下文是为了使用 Handle 的方法
val = (Helpers[type] || Helpers.custom).apply(this, arguments)
currentContext = context[0]
} else {
for (i = 0; i < parts.length; i++) {
options.__path.push(parts[i])
cur = parts[i]
val = this.val(cur, options, context, {})
currentContext[cur] = _.isArray(val) && [] || val
type = _.type(currentContext[cur])
if (type === 'object' || type === 'array') {
currentContext = currentContext[cur]
context.unshift(currentContext)
}
}
}
// block.program
if (node.program) {
if (_.type(currentContext) === 'array') {
len = val.length || Random.integer(3, 7)
// Handle.program() 可以自己解析和生成数据,但是不知道该重复几次,所以这里需要循环调用
for (i = 0; i < len; i++) {
currentContext.push(typeof val[i] !== 'undefined' ? val[i] : {})
options.__path.push('[]')
context.unshift(currentContext[currentContext.length - 1])
this.gen(node.program, context, options, helpers, partials)
options.__path.pop()
context.shift()
}
} else this.gen(node.program, context, options, helpers, partials)
}
if (context.length > contextLength) context.splice(0, context.length - contextLength)
}
Handle.hash = function(node, context, options, helpers, partials) {
var pairs = node.pairs,
pair, i, j;
for (i = 0; i < pairs.length; i++) {
pair = pairs[i]
for (j = 1; j < pair.length; j++) {
this.gen(pair[j], context, options, helpers, partials)
}
}
}
Handle.ID = function(node, context, options) { // , helpers, partials
var parts = node.parts,
i, len, cur, prev, def, val, type, valType, preOptions,
currentContext = context[node.depth], // back path, eg {{../permalink}}
contextLength = context.length;
if (_.isArray(currentContext)) currentContext = context[node.depth + 1]
if (!parts.length) {
// TODO 修正父节点的类型
} else {
for (i = 0, len = parts.length; i < len; i++) {
options.__path.push(parts[i])
cur = parts[i]
prev = parts[i - 1]
preOptions = options[prev]
def = i === len - 1 ? currentContext[cur] : {}
val = this.val(cur, /*preOptions && preOptions[cur] ? preOptions :*/ options, context, def)
type = _.type(currentContext[cur])
valType = _.type(val)
if (type === 'undefined') {
// 如果不是最后一个属性,并且当前值不是 [] 或 {},则修正为 [] 或 {}
if (i < len - 1 && valType !== 'object' && valType !== 'array') {
currentContext[cur] = {}
} else {
currentContext[cur] = _.isArray(val) && [] || val
}
} else {
// 已有值
// 如果不是最后一个属性,并且不是 [] 或 {},则修正为 [] 或 {}
if (i < len - 1 && type !== 'object' && type !== 'array') {
currentContext[cur] = _.isArray(val) && [] || {}
}
}
type = _.type(currentContext[cur])
if (type === 'object' || type === 'array') {
currentContext = currentContext[cur]
context.unshift(currentContext)
}
}
}
if (context.length > contextLength) context.splice(0, context.length - contextLength)
}
Handle.partial = function(node, context, options, helpers, partials) {
var name = node.partialName.name,
partial = partials && partials[name],
contextLength = context.length;
if (partial) Handle.gen(partial, context, options, helpers, partials)
if (context.length > contextLength) context.splice(0, context.length - contextLength)
}
Handle.content = _.noop
Handle.PARTIAL_NAME = _.noop
Handle.DATA = _.noop
Handle.STRING = _.noop
Handle.INTEGER = _.noop
Handle.BOOLEAN = _.noop
Handle.comment = _.noop
var Helpers = {}
Helpers.each = function(node, context, options) {
var i, len, cur, val, parts, def, type,
currentContext = context[0];
parts = node.mustache.params[0].parts // each 只需要处理第一个参数,更多的参数由 each 自己处理
for (i = 0, len = parts.length; i < len; i++) {
options.__path.push(parts[i])
cur = parts[i]
def = i === len - 1 ? [] : {}
val = this.val(cur, options, context, def)
currentContext[cur] = _.isArray(val) && [] || val
type = _.type(currentContext[cur])
if (type === 'object' || type === 'array') {
currentContext = currentContext[cur]
context.unshift(currentContext)
}
}
return val
}
Helpers['if'] = Helpers.unless = function(node, context, options) {
var params = node.mustache.params,
i, j, cur, val, parts, def, type,
currentContext = context[0];
for (i = 0; i < params.length; i++) {
parts = params[i].parts
for (j = 0; j < parts.length; j++) {
if (i === 0) options.__path.push(parts[j])
cur = parts[j]
def = j === parts.length - 1 ? '@BOOL(2,1,true)' : {}
val = this.val(cur, options, context, def)
if (j === parts.length - 1) {
val = val === 'true' ? true :
val === 'false' ? false : val
}
currentContext[cur] = _.isArray(val) ? [] : val
type = _.type(currentContext[cur])
if (type === 'object' || type === 'array') {
currentContext = currentContext[cur]
context.unshift(currentContext)
}
}
}
return val
}
Helpers['with'] = function(node, context, options) {
var i, cur, val, parts, def,
currentContext = context[0];
parts = node.mustache.params[0].parts
for (i = 0; i < parts.length; i++) {
options.__path.push(parts[i])
cur = parts[i]
def = {}
val = this.val(cur, options, context, def)
currentContext = currentContext[cur] = val
context.unshift(currentContext)
}
return val
}
Helpers.log = function() {
// {{log "Look at me!"}}
}
Helpers.custom = function(node, context, options) {
var i, len, cur, val, parts, def, type,
currentContext = context[0];
// custom helper
// 如果 helper 没有参数,则认为是在当前上下文中判断 helper 是否为 true
if (node.mustache.params.length === 0) {
return
// 按理说,Mock4Tpl 不需要也不应该模拟 helper 的行为(返回值),只需要处理 helper 的 params 和 statements。
// 之所以保留下面的代码,是为了以防需要时扩展,也就是说,如果连 helper 也需要模拟的话!
options.__path.push(node.mustache.id.string)
cur = node.mustache.id.string
def = '@BOOL(2,1,true)'
val = this.val(cur, options, context, def)
currentContext[cur] = _.isArray(val) && [] || val
type = _.type(currentContext[cur])
if (type === 'object' || type === 'array') {
currentContext = currentContext[cur]
context.unshift(currentContext)
}
} else {
parts = node.mustache.params[0].parts
for (i = 0, len = parts.length; i < len; i++) {
options.__path.push(parts[i])
cur = parts[i]
def = i === len - 1 ? [] : {} // 默认值也可以是 [],如果有必要的话
val = this.val(cur, options, context, def)
currentContext[cur] = _.isArray(val) && [] || val
type = _.type(currentContext[cur])
if (type === 'object' || type === 'array') {
currentContext = currentContext[cur]
context.unshift(currentContext)
}
}
}
return val
}