bobe 0.0.35 → 0.0.37
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.
- package/dist/bobe.cjs.js +135 -15
- package/dist/bobe.cjs.js.map +1 -1
- package/dist/bobe.compiler.cjs.js +135 -15
- package/dist/bobe.compiler.cjs.js.map +1 -1
- package/dist/bobe.compiler.esm.js +135 -16
- package/dist/bobe.compiler.esm.js.map +1 -1
- package/dist/bobe.esm.js +135 -16
- package/dist/bobe.esm.js.map +1 -1
- package/dist/index.d.ts +35 -2
- package/dist/index.umd.js +135 -15
- package/dist/index.umd.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -174,6 +174,33 @@ type TerpConf = Partial<Pick<Interpreter, 'createNode' | 'setProp' | 'insertAfte
|
|
|
174
174
|
type CustomRenderConf = Pick<TerpConf, 'createNode' | 'setProp' | 'insertAfter' | 'remove' | 'createAnchor' | 'firstChild' | 'nextSib'>;
|
|
175
175
|
type Hook = (props: HookProps) => any;
|
|
176
176
|
type HookType = 'dynamic' | 'static';
|
|
177
|
+
declare enum ParseErrorCode {
|
|
178
|
+
UNCLOSED_BRACE = 9001,
|
|
179
|
+
UNCLOSED_STRING = 9002,
|
|
180
|
+
UNCLOSED_STATIC_INS = 9003,
|
|
181
|
+
INCONSISTENT_INDENT = 9004,
|
|
182
|
+
INDENT_MISMATCH = 9005,
|
|
183
|
+
MISSING_ASSIGN = 9006,
|
|
184
|
+
INVALID_TAG_NAME = 9007,
|
|
185
|
+
ELSE_WITHOUT_IF = 9008,
|
|
186
|
+
EMPTY_IF_BODY = 9009,
|
|
187
|
+
EMPTY_FOR_BODY = 9010,
|
|
188
|
+
MISSING_FOR_COLLECTION = 9011,
|
|
189
|
+
MISSING_FOR_SEMICOLON = 9012,
|
|
190
|
+
MISSING_FOR_ITEM = 9013,
|
|
191
|
+
PIPE_IN_WRONG_CONTEXT = 9014
|
|
192
|
+
}
|
|
193
|
+
type ParseError = {
|
|
194
|
+
code: ParseErrorCode;
|
|
195
|
+
message: string;
|
|
196
|
+
loc: SourceLocation;
|
|
197
|
+
};
|
|
198
|
+
/** tokenizer 抛出的带位置信息的语法错误 */
|
|
199
|
+
declare class ParseSyntaxError extends SyntaxError {
|
|
200
|
+
code: ParseErrorCode;
|
|
201
|
+
loc: SourceLocation;
|
|
202
|
+
constructor(code: ParseErrorCode, message: string, loc: SourceLocation);
|
|
203
|
+
}
|
|
177
204
|
type ProgramCtx = {
|
|
178
205
|
stack: MultiTypeStack<any>;
|
|
179
206
|
prevSibling: any;
|
|
@@ -271,6 +298,9 @@ declare class Tokenizer {
|
|
|
271
298
|
constructor(hook: Hook, useDedentAsEof: boolean);
|
|
272
299
|
private next;
|
|
273
300
|
getCurrentPos(): Position;
|
|
301
|
+
/** 构造从当前扫描起始位置到模板结尾的 SourceLocation,用于未闭合错误 */
|
|
302
|
+
private unclosedLoc;
|
|
303
|
+
private throwUnclosed;
|
|
274
304
|
resume(_snapshot: ReturnType<Tokenizer['snapshot']>): void;
|
|
275
305
|
snapshot(keys?: (keyof Tokenizer)[]): Partial<Tokenizer>;
|
|
276
306
|
skip(): string;
|
|
@@ -295,6 +325,7 @@ declare class Tokenizer {
|
|
|
295
325
|
private brace;
|
|
296
326
|
private newLine;
|
|
297
327
|
private getDentValue;
|
|
328
|
+
emptyLoc(): SourceLocation;
|
|
298
329
|
private dent;
|
|
299
330
|
private shorterThanBaseDentEof;
|
|
300
331
|
private identifier;
|
|
@@ -395,7 +426,9 @@ interface FragmentNode extends BaseNode {
|
|
|
395
426
|
declare class Compiler {
|
|
396
427
|
tokenizer: Tokenizer;
|
|
397
428
|
hooks: ParseHooks;
|
|
429
|
+
errors: ParseError[];
|
|
398
430
|
constructor(tokenizer: Tokenizer, hooks?: ParseHooks);
|
|
431
|
+
private addError;
|
|
399
432
|
/**
|
|
400
433
|
* 编译程序入口,生成AST
|
|
401
434
|
*/
|
|
@@ -458,5 +491,5 @@ type ParseHooks = Partial<{
|
|
|
458
491
|
declare function bobe(fragments: TemplateStringsArray, ...values: any[]): BobeUI;
|
|
459
492
|
declare function customRender(option: CustomRenderConf): <T>(Ctor: typeof Store, root: any) => (ComponentNode$1 | Store)[];
|
|
460
493
|
|
|
461
|
-
export { Compiler, NodeType, Tokenizer, bobe, customRender };
|
|
462
|
-
export type { ASTNodeType, BaseNode, ComponentNode, ConditionalNode, DynamicValue, ElementNode, FragmentNode, InterpolationNode, LoopNode, Program, Property, PropertyKeyNode, PropertyValue, SourceLocation, StaticValue, TemplateNode, TextNode };
|
|
494
|
+
export { Compiler, NodeType, ParseErrorCode, ParseSyntaxError, Tokenizer, bobe, customRender };
|
|
495
|
+
export type { ASTNodeType, BaseNode, ComponentNode, ConditionalNode, DynamicValue, ElementNode, FragmentNode, InterpolationNode, LoopNode, ParseError, Program, Property, PropertyKeyNode, PropertyValue, SourceLocation, StaticValue, TemplateNode, TextNode };
|
package/dist/index.umd.js
CHANGED
|
@@ -44,6 +44,30 @@
|
|
|
44
44
|
TerpEvt["HandledComponentNode"] = "handled-component-node";
|
|
45
45
|
return TerpEvt;
|
|
46
46
|
})({});
|
|
47
|
+
let ParseErrorCode = function (ParseErrorCode) {
|
|
48
|
+
ParseErrorCode[ParseErrorCode["UNCLOSED_BRACE"] = 9001] = "UNCLOSED_BRACE";
|
|
49
|
+
ParseErrorCode[ParseErrorCode["UNCLOSED_STRING"] = 9002] = "UNCLOSED_STRING";
|
|
50
|
+
ParseErrorCode[ParseErrorCode["UNCLOSED_STATIC_INS"] = 9003] = "UNCLOSED_STATIC_INS";
|
|
51
|
+
ParseErrorCode[ParseErrorCode["INCONSISTENT_INDENT"] = 9004] = "INCONSISTENT_INDENT";
|
|
52
|
+
ParseErrorCode[ParseErrorCode["INDENT_MISMATCH"] = 9005] = "INDENT_MISMATCH";
|
|
53
|
+
ParseErrorCode[ParseErrorCode["MISSING_ASSIGN"] = 9006] = "MISSING_ASSIGN";
|
|
54
|
+
ParseErrorCode[ParseErrorCode["INVALID_TAG_NAME"] = 9007] = "INVALID_TAG_NAME";
|
|
55
|
+
ParseErrorCode[ParseErrorCode["ELSE_WITHOUT_IF"] = 9008] = "ELSE_WITHOUT_IF";
|
|
56
|
+
ParseErrorCode[ParseErrorCode["EMPTY_IF_BODY"] = 9009] = "EMPTY_IF_BODY";
|
|
57
|
+
ParseErrorCode[ParseErrorCode["EMPTY_FOR_BODY"] = 9010] = "EMPTY_FOR_BODY";
|
|
58
|
+
ParseErrorCode[ParseErrorCode["MISSING_FOR_COLLECTION"] = 9011] = "MISSING_FOR_COLLECTION";
|
|
59
|
+
ParseErrorCode[ParseErrorCode["MISSING_FOR_SEMICOLON"] = 9012] = "MISSING_FOR_SEMICOLON";
|
|
60
|
+
ParseErrorCode[ParseErrorCode["MISSING_FOR_ITEM"] = 9013] = "MISSING_FOR_ITEM";
|
|
61
|
+
ParseErrorCode[ParseErrorCode["PIPE_IN_WRONG_CONTEXT"] = 9014] = "PIPE_IN_WRONG_CONTEXT";
|
|
62
|
+
return ParseErrorCode;
|
|
63
|
+
}({});
|
|
64
|
+
class ParseSyntaxError extends SyntaxError {
|
|
65
|
+
constructor(code, message, loc) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.code = code;
|
|
68
|
+
this.loc = loc;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
47
71
|
|
|
48
72
|
class Tokenizer {
|
|
49
73
|
TabSize = 2;
|
|
@@ -82,6 +106,25 @@
|
|
|
82
106
|
column: this.column
|
|
83
107
|
};
|
|
84
108
|
}
|
|
109
|
+
unclosedLoc(startOffset, startLine, startCol) {
|
|
110
|
+
const end = this.code.length - 1;
|
|
111
|
+
return {
|
|
112
|
+
start: {
|
|
113
|
+
offset: startOffset,
|
|
114
|
+
line: startLine,
|
|
115
|
+
column: startCol
|
|
116
|
+
},
|
|
117
|
+
end: {
|
|
118
|
+
offset: end,
|
|
119
|
+
line: this.line,
|
|
120
|
+
column: this.column
|
|
121
|
+
},
|
|
122
|
+
source: this.code.slice(startOffset, end)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
throwUnclosed(code, message, startOffset, startLine, startCol) {
|
|
126
|
+
throw new ParseSyntaxError(code, message, this.unclosedLoc(startOffset, startLine, startCol));
|
|
127
|
+
}
|
|
85
128
|
resume(_snapshot) {
|
|
86
129
|
this.token = undefined;
|
|
87
130
|
this.needIndent = false;
|
|
@@ -135,7 +178,7 @@
|
|
|
135
178
|
const expLen = this.dentStack[i];
|
|
136
179
|
if (currLen === expLen) break;
|
|
137
180
|
if (currLen > expLen) {
|
|
138
|
-
throw
|
|
181
|
+
throw new ParseSyntaxError(ParseErrorCode.INCONSISTENT_INDENT, '缩进大小不统一', this.emptyLoc());
|
|
139
182
|
}
|
|
140
183
|
if (this.shorterThanBaseDentEof()) {
|
|
141
184
|
break;
|
|
@@ -250,8 +293,7 @@
|
|
|
250
293
|
}
|
|
251
294
|
return this.token;
|
|
252
295
|
} catch (error) {
|
|
253
|
-
|
|
254
|
-
return this.token;
|
|
296
|
+
throw error;
|
|
255
297
|
} finally {
|
|
256
298
|
this.handledTokens.push(this.token);
|
|
257
299
|
}
|
|
@@ -314,6 +356,9 @@
|
|
|
314
356
|
this.setToken(TokenType.Pipe, '|');
|
|
315
357
|
}
|
|
316
358
|
staticIns() {
|
|
359
|
+
const startOffset = this.preI,
|
|
360
|
+
startLine = this.line,
|
|
361
|
+
startCol = this.preCol;
|
|
317
362
|
let nextC = this.code[this.i + 1];
|
|
318
363
|
if (nextC !== '{') {
|
|
319
364
|
return false;
|
|
@@ -323,6 +368,9 @@
|
|
|
323
368
|
let innerBrace = 0;
|
|
324
369
|
while (1) {
|
|
325
370
|
nextC = this.code[this.i + 1];
|
|
371
|
+
if (nextC === undefined) {
|
|
372
|
+
this.throwUnclosed(ParseErrorCode.UNCLOSED_STATIC_INS, '未闭合的 "${...}"', startOffset, startLine, startCol);
|
|
373
|
+
}
|
|
326
374
|
value += nextC;
|
|
327
375
|
this.next();
|
|
328
376
|
if (nextC === '{') {
|
|
@@ -339,6 +387,9 @@
|
|
|
339
387
|
return true;
|
|
340
388
|
}
|
|
341
389
|
brace() {
|
|
390
|
+
const startOffset = this.preI,
|
|
391
|
+
startLine = this.line,
|
|
392
|
+
startCol = this.preCol;
|
|
342
393
|
let inComment,
|
|
343
394
|
inString,
|
|
344
395
|
count = 0,
|
|
@@ -346,6 +397,9 @@
|
|
|
346
397
|
backslashCount = 0;
|
|
347
398
|
while (1) {
|
|
348
399
|
const char = this.code[this.i];
|
|
400
|
+
if (char === undefined) {
|
|
401
|
+
this.throwUnclosed(ParseErrorCode.UNCLOSED_BRACE, '未闭合的 "{"', startOffset, startLine, startCol);
|
|
402
|
+
}
|
|
349
403
|
const nextChar = this.code[this.i + 1];
|
|
350
404
|
if (inComment === 'single' && char === '\n') {
|
|
351
405
|
inComment = null;
|
|
@@ -433,6 +487,18 @@
|
|
|
433
487
|
isEmptyLine
|
|
434
488
|
};
|
|
435
489
|
}
|
|
490
|
+
emptyLoc() {
|
|
491
|
+
const pos = this.getCurrentPos();
|
|
492
|
+
return {
|
|
493
|
+
start: pos,
|
|
494
|
+
end: {
|
|
495
|
+
offset: pos.offset + 1,
|
|
496
|
+
line: pos.line,
|
|
497
|
+
column: pos.column + 1
|
|
498
|
+
},
|
|
499
|
+
source: ' '
|
|
500
|
+
};
|
|
501
|
+
}
|
|
436
502
|
dent() {
|
|
437
503
|
const _this$getDentValue2 = this.getDentValue(),
|
|
438
504
|
value = _this$getDentValue2.value,
|
|
@@ -459,7 +525,7 @@
|
|
|
459
525
|
const expLen = this.dentStack[i];
|
|
460
526
|
if (currLen === expLen) break;
|
|
461
527
|
if (currLen > expLen) {
|
|
462
|
-
throw
|
|
528
|
+
throw new ParseSyntaxError(ParseErrorCode.INCONSISTENT_INDENT, '缩进大小不统一', this.emptyLoc());
|
|
463
529
|
}
|
|
464
530
|
if (this.shorterThanBaseDentEof()) {
|
|
465
531
|
return;
|
|
@@ -528,11 +594,17 @@
|
|
|
528
594
|
this.setToken(TokenType.Identifier, realValue);
|
|
529
595
|
}
|
|
530
596
|
str(char) {
|
|
597
|
+
const startOffset = this.preI,
|
|
598
|
+
startLine = this.line,
|
|
599
|
+
startCol = this.preCol;
|
|
531
600
|
let value = '';
|
|
532
601
|
let nextC;
|
|
533
602
|
let continuousBackslashCount = 0;
|
|
534
603
|
while (1) {
|
|
535
604
|
nextC = this.code[this.i + 1];
|
|
605
|
+
if (nextC === undefined) {
|
|
606
|
+
this.throwUnclosed(ParseErrorCode.UNCLOSED_STRING, '未闭合的字符串字面量', startOffset, startLine, startCol);
|
|
607
|
+
}
|
|
536
608
|
const memoCount = continuousBackslashCount;
|
|
537
609
|
if (nextC === '\\') {
|
|
538
610
|
continuousBackslashCount++;
|
|
@@ -840,18 +912,33 @@
|
|
|
840
912
|
var _applyDecs$e = _slicedToArray(_applyDecs2311(this, [], [[NodeHook, 2, "parseProgram"], [[NodeHook, NodeLoc], 2, "parseComponentNode"], [[NodeHook, NodeLoc], 2, "parseElementNode"], [[NodeHook, NodeLoc], 2, "parseConditionalNode"], [[NodeHook, NodeLoc], 2, "parseLoopNode"], [NodeHook, 2, "parseProperty"], [[NodeHook, TokenLoc], 2, "parsePropertyKey"], [[NodeHook, TokenLoc], 2, "parsePropertyValue"], [[NodeHook, TokenLoc], 2, "parseName"]]).e, 1);
|
|
841
913
|
_initProto = _applyDecs$e[0];
|
|
842
914
|
}
|
|
915
|
+
errors = (_initProto(this), []);
|
|
843
916
|
constructor(tokenizer, hooks = {}) {
|
|
844
917
|
this.tokenizer = tokenizer;
|
|
845
918
|
this.hooks = hooks;
|
|
846
|
-
|
|
919
|
+
}
|
|
920
|
+
addError(code, message, loc) {
|
|
921
|
+
this.errors.push({
|
|
922
|
+
code,
|
|
923
|
+
message,
|
|
924
|
+
loc
|
|
925
|
+
});
|
|
847
926
|
}
|
|
848
927
|
parseProgram() {
|
|
849
|
-
this.tokenizer.nextToken();
|
|
850
928
|
const body = [];
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
929
|
+
try {
|
|
930
|
+
this.tokenizer.nextToken();
|
|
931
|
+
while (!this.tokenizer.isEof()) {
|
|
932
|
+
const node = this.templateNode(body);
|
|
933
|
+
if (node) {
|
|
934
|
+
body.push(node);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
} catch (error) {
|
|
938
|
+
if (error instanceof ParseSyntaxError) {
|
|
939
|
+
this.addError(error.code, error.message, error.loc);
|
|
940
|
+
} else {
|
|
941
|
+
this.addError(error.toString(), '未知错误', this.tokenizer.emptyLoc());
|
|
855
942
|
}
|
|
856
943
|
}
|
|
857
944
|
return {
|
|
@@ -877,7 +964,7 @@
|
|
|
877
964
|
if (this.tokenizer.token.type & TokenType.Indent) {
|
|
878
965
|
this.tokenizer.nextToken();
|
|
879
966
|
while (!(this.tokenizer.token.type & TokenType.Dedent) && !this.tokenizer.isEof()) {
|
|
880
|
-
const child = this.templateNode();
|
|
967
|
+
const child = this.templateNode(children);
|
|
881
968
|
if (child) {
|
|
882
969
|
children.push(child);
|
|
883
970
|
}
|
|
@@ -888,13 +975,26 @@
|
|
|
888
975
|
}
|
|
889
976
|
return children;
|
|
890
977
|
}
|
|
891
|
-
templateNode() {
|
|
892
|
-
this.tokenizer.token;
|
|
978
|
+
templateNode(siblings) {
|
|
979
|
+
const token = this.tokenizer.token;
|
|
980
|
+
if (token.type & TokenType.Pipe) {
|
|
981
|
+
this.addError(ParseErrorCode.PIPE_IN_WRONG_CONTEXT, '"|" 只能出现在元素属性扩展行中', token.loc ?? this.tokenizer.emptyLoc());
|
|
982
|
+
this.tokenizer.nextToken();
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
893
985
|
const _this$tokenizer$_hook = this.tokenizer._hook({}),
|
|
894
986
|
_this$tokenizer$_hook2 = _slicedToArray(_this$tokenizer$_hook, 2),
|
|
895
987
|
hookType = _this$tokenizer$_hook2[0],
|
|
896
988
|
value = _this$tokenizer$_hook2[1];
|
|
897
|
-
|
|
989
|
+
const isElseOrFail = value === 'else' || value === 'fail';
|
|
990
|
+
if (value === 'if' || isElseOrFail) {
|
|
991
|
+
if (isElseOrFail) {
|
|
992
|
+
const lastSibling = siblings[siblings.length - 1];
|
|
993
|
+
const lastType = lastSibling?.type;
|
|
994
|
+
if (lastType !== NodeType.If && lastType !== NodeType.Else && lastType !== NodeType.Fail) {
|
|
995
|
+
this.addError(ParseErrorCode.ELSE_WITHOUT_IF, `"${value}" 前必须有 "if" 或 "else" 节点`, token.loc ?? this.tokenizer.emptyLoc());
|
|
996
|
+
}
|
|
997
|
+
}
|
|
898
998
|
return this.parseConditionalNode();
|
|
899
999
|
}
|
|
900
1000
|
if (value === 'for') {
|
|
@@ -919,6 +1019,13 @@
|
|
|
919
1019
|
}
|
|
920
1020
|
parseElementNode(node) {
|
|
921
1021
|
const tagToken = this.tokenizer.token;
|
|
1022
|
+
if (!(tagToken.type & TokenType.Identifier)) {
|
|
1023
|
+
this.addError(ParseErrorCode.INVALID_TAG_NAME, `无效的标签名,期望标识符但得到 "${tagToken.value}"`, tagToken.loc ?? this.tokenizer.emptyLoc());
|
|
1024
|
+
while (!(this.tokenizer.token.type & TokenType.NewLine) && !this.tokenizer.isEof()) {
|
|
1025
|
+
this.tokenizer.nextToken();
|
|
1026
|
+
}
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
922
1029
|
const tagName = tagToken.value;
|
|
923
1030
|
this.tokenizer.nextToken();
|
|
924
1031
|
const props = this.headerLineAndExtensions();
|
|
@@ -944,15 +1051,25 @@
|
|
|
944
1051
|
return node;
|
|
945
1052
|
}
|
|
946
1053
|
parseLoopNode(node) {
|
|
1054
|
+
const forLoc = this.tokenizer.token.loc ?? this.tokenizer.emptyLoc();
|
|
947
1055
|
this.tokenizer.nextToken();
|
|
948
1056
|
const collection = this.parsePropertyValue();
|
|
949
|
-
|
|
1057
|
+
if (!collection.value && collection.value !== 0) {
|
|
1058
|
+
this.addError(ParseErrorCode.MISSING_FOR_COLLECTION, '"for" 缺少集合表达式', forLoc);
|
|
1059
|
+
}
|
|
1060
|
+
const semicolonToken = this.tokenizer.nextToken();
|
|
1061
|
+
if (!(semicolonToken.type & TokenType.Semicolon)) {
|
|
1062
|
+
this.addError(ParseErrorCode.MISSING_FOR_SEMICOLON, '"for" 语法:for <集合>; <item> [index][; key],缺少第一个 ";"', semicolonToken.loc ?? this.tokenizer.emptyLoc());
|
|
1063
|
+
}
|
|
950
1064
|
const itemToken = this.tokenizer.nextToken();
|
|
951
1065
|
const isDestruct = itemToken.type === TokenType.InsertionExp;
|
|
952
1066
|
if (isDestruct) {
|
|
953
1067
|
itemToken.value = '{' + itemToken.value + '}';
|
|
954
1068
|
}
|
|
955
1069
|
const item = this.parsePropertyValue();
|
|
1070
|
+
if (!item.value && item.value !== 0) {
|
|
1071
|
+
this.addError(ParseErrorCode.MISSING_FOR_ITEM, '"for" 缺少 item 变量名', itemToken.loc ?? this.tokenizer.emptyLoc());
|
|
1072
|
+
}
|
|
956
1073
|
let char = this.tokenizer.peekChar(),
|
|
957
1074
|
key,
|
|
958
1075
|
index;
|
|
@@ -1015,6 +1132,8 @@
|
|
|
1015
1132
|
this.tokenizer.nextToken();
|
|
1016
1133
|
node.value = this.parsePropertyValue();
|
|
1017
1134
|
this.tokenizer.nextToken();
|
|
1135
|
+
} else {
|
|
1136
|
+
this.addError(ParseErrorCode.MISSING_ASSIGN, `属性 "${node.key.key}" 缺少 "=" 赋值符号`, node.key.loc ?? this.tokenizer.emptyLoc());
|
|
1018
1137
|
}
|
|
1019
1138
|
node.loc.start = node.key.loc.start;
|
|
1020
1139
|
node.loc.end = node.value ? node.value.loc.end : node.key.loc.end;
|
|
@@ -2014,6 +2133,7 @@
|
|
|
2014
2133
|
|
|
2015
2134
|
exports.Compiler = Compiler;
|
|
2016
2135
|
exports.NodeType = NodeType;
|
|
2136
|
+
exports.ParseSyntaxError = ParseSyntaxError;
|
|
2017
2137
|
exports.Tokenizer = Tokenizer;
|
|
2018
2138
|
exports.bobe = bobe;
|
|
2019
2139
|
exports.customRender = customRender;
|