bobe 0.0.35 → 0.0.36

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/index.d.ts CHANGED
@@ -174,6 +174,18 @@ 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
+ type ParseErrorCode = 'UNCLOSED_BRACE' | 'UNCLOSED_STRING' | 'UNCLOSED_STATIC_INS' | 'INCONSISTENT_INDENT' | 'INDENT_MISMATCH' | 'MISSING_ASSIGN' | 'INVALID_TAG_NAME' | 'ELSE_WITHOUT_IF' | 'EMPTY_IF_BODY' | 'EMPTY_FOR_BODY' | 'MISSING_FOR_COLLECTION' | 'MISSING_FOR_SEMICOLON' | 'MISSING_FOR_ITEM' | 'PIPE_IN_WRONG_CONTEXT';
178
+ type ParseError = {
179
+ code: ParseErrorCode;
180
+ message: string;
181
+ loc: SourceLocation;
182
+ };
183
+ /** tokenizer 抛出的带位置信息的语法错误 */
184
+ declare class ParseSyntaxError extends SyntaxError {
185
+ code: ParseErrorCode;
186
+ loc: SourceLocation;
187
+ constructor(code: ParseErrorCode, message: string, loc: SourceLocation);
188
+ }
177
189
  type ProgramCtx = {
178
190
  stack: MultiTypeStack<any>;
179
191
  prevSibling: any;
@@ -271,6 +283,9 @@ declare class Tokenizer {
271
283
  constructor(hook: Hook, useDedentAsEof: boolean);
272
284
  private next;
273
285
  getCurrentPos(): Position;
286
+ /** 构造从当前扫描起始位置到模板结尾的 SourceLocation,用于未闭合错误 */
287
+ private unclosedLoc;
288
+ private throwUnclosed;
274
289
  resume(_snapshot: ReturnType<Tokenizer['snapshot']>): void;
275
290
  snapshot(keys?: (keyof Tokenizer)[]): Partial<Tokenizer>;
276
291
  skip(): string;
@@ -395,7 +410,10 @@ interface FragmentNode extends BaseNode {
395
410
  declare class Compiler {
396
411
  tokenizer: Tokenizer;
397
412
  hooks: ParseHooks;
413
+ errors: ParseError[];
398
414
  constructor(tokenizer: Tokenizer, hooks?: ParseHooks);
415
+ private addError;
416
+ private emptyLoc;
399
417
  /**
400
418
  * 编译程序入口,生成AST
401
419
  */
@@ -458,5 +476,5 @@ type ParseHooks = Partial<{
458
476
  declare function bobe(fragments: TemplateStringsArray, ...values: any[]): BobeUI;
459
477
  declare function customRender(option: CustomRenderConf): <T>(Ctor: typeof Store, root: any) => (ComponentNode$1 | Store)[];
460
478
 
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 };
479
+ export { Compiler, NodeType, ParseSyntaxError, Tokenizer, bobe, customRender };
480
+ export type { ASTNodeType, BaseNode, ComponentNode, ConditionalNode, DynamicValue, ElementNode, FragmentNode, InterpolationNode, LoopNode, ParseError, ParseErrorCode, Program, Property, PropertyKeyNode, PropertyValue, SourceLocation, StaticValue, TemplateNode, TextNode };
package/dist/index.umd.js CHANGED
@@ -44,6 +44,13 @@
44
44
  TerpEvt["HandledComponentNode"] = "handled-component-node";
45
45
  return TerpEvt;
46
46
  })({});
47
+ class ParseSyntaxError extends SyntaxError {
48
+ constructor(code, message, loc) {
49
+ super(message);
50
+ this.code = code;
51
+ this.loc = loc;
52
+ }
53
+ }
47
54
 
48
55
  class Tokenizer {
49
56
  TabSize = 2;
@@ -82,6 +89,25 @@
82
89
  column: this.column
83
90
  };
84
91
  }
92
+ unclosedLoc(startOffset, startLine, startCol) {
93
+ const end = this.code.length - 1;
94
+ return {
95
+ start: {
96
+ offset: startOffset,
97
+ line: startLine,
98
+ column: startCol
99
+ },
100
+ end: {
101
+ offset: end,
102
+ line: this.line,
103
+ column: this.column
104
+ },
105
+ source: this.code.slice(startOffset, end)
106
+ };
107
+ }
108
+ throwUnclosed(code, message, startOffset, startLine, startCol) {
109
+ throw new ParseSyntaxError(code, message, this.unclosedLoc(startOffset, startLine, startCol));
110
+ }
85
111
  resume(_snapshot) {
86
112
  this.token = undefined;
87
113
  this.needIndent = false;
@@ -250,8 +276,7 @@
250
276
  }
251
277
  return this.token;
252
278
  } catch (error) {
253
- console.error(error);
254
- return this.token;
279
+ throw error;
255
280
  } finally {
256
281
  this.handledTokens.push(this.token);
257
282
  }
@@ -314,6 +339,9 @@
314
339
  this.setToken(TokenType.Pipe, '|');
315
340
  }
316
341
  staticIns() {
342
+ const startOffset = this.preI,
343
+ startLine = this.line,
344
+ startCol = this.preCol;
317
345
  let nextC = this.code[this.i + 1];
318
346
  if (nextC !== '{') {
319
347
  return false;
@@ -323,6 +351,9 @@
323
351
  let innerBrace = 0;
324
352
  while (1) {
325
353
  nextC = this.code[this.i + 1];
354
+ if (nextC === undefined) {
355
+ this.throwUnclosed('UNCLOSED_STATIC_INS', '未闭合的 "${...}"', startOffset, startLine, startCol);
356
+ }
326
357
  value += nextC;
327
358
  this.next();
328
359
  if (nextC === '{') {
@@ -339,6 +370,9 @@
339
370
  return true;
340
371
  }
341
372
  brace() {
373
+ const startOffset = this.preI,
374
+ startLine = this.line,
375
+ startCol = this.preCol;
342
376
  let inComment,
343
377
  inString,
344
378
  count = 0,
@@ -346,6 +380,9 @@
346
380
  backslashCount = 0;
347
381
  while (1) {
348
382
  const char = this.code[this.i];
383
+ if (char === undefined) {
384
+ this.throwUnclosed('UNCLOSED_BRACE', '未闭合的 "{"', startOffset, startLine, startCol);
385
+ }
349
386
  const nextChar = this.code[this.i + 1];
350
387
  if (inComment === 'single' && char === '\n') {
351
388
  inComment = null;
@@ -528,11 +565,17 @@
528
565
  this.setToken(TokenType.Identifier, realValue);
529
566
  }
530
567
  str(char) {
568
+ const startOffset = this.preI,
569
+ startLine = this.line,
570
+ startCol = this.preCol;
531
571
  let value = '';
532
572
  let nextC;
533
573
  let continuousBackslashCount = 0;
534
574
  while (1) {
535
575
  nextC = this.code[this.i + 1];
576
+ if (nextC === undefined) {
577
+ this.throwUnclosed('UNCLOSED_STRING', '未闭合的字符串字面量', startOffset, startLine, startCol);
578
+ }
536
579
  const memoCount = continuousBackslashCount;
537
580
  if (nextC === '\\') {
538
581
  continuousBackslashCount++;
@@ -840,18 +883,47 @@
840
883
  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
884
  _initProto = _applyDecs$e[0];
842
885
  }
886
+ errors = (_initProto(this), []);
843
887
  constructor(tokenizer, hooks = {}) {
844
888
  this.tokenizer = tokenizer;
845
889
  this.hooks = hooks;
846
- _initProto(this);
890
+ }
891
+ addError(code, message, loc) {
892
+ this.errors.push({
893
+ code,
894
+ message,
895
+ loc
896
+ });
897
+ }
898
+ emptyLoc() {
899
+ const pos = this.tokenizer.getCurrentPos();
900
+ return {
901
+ start: pos,
902
+ end: {
903
+ offset: pos.offset + 1,
904
+ line: pos.line,
905
+ column: pos.column + 1
906
+ },
907
+ source: ' '
908
+ };
847
909
  }
848
910
  parseProgram() {
849
- this.tokenizer.nextToken();
850
911
  const body = [];
851
- while (!this.tokenizer.isEof()) {
852
- const node = this.templateNode();
853
- if (node) {
854
- body.push(node);
912
+ try {
913
+ this.tokenizer.nextToken();
914
+ while (!this.tokenizer.isEof()) {
915
+ const node = this.templateNode(body);
916
+ if (node) {
917
+ body.push(node);
918
+ }
919
+ }
920
+ } catch (error) {
921
+ if (error instanceof ParseSyntaxError) {
922
+ this.addError(error.code, error.message, error.loc);
923
+ } else if (error instanceof SyntaxError) {
924
+ const knownCodes = ['INCONSISTENT_INDENT', 'INDENT_MISMATCH'];
925
+ const code = knownCodes.includes(error.message) ? error.message : 'INCONSISTENT_INDENT';
926
+ this.addError(code, error.message, this.emptyLoc());
855
927
  }
856
928
  }
857
929
  return {
@@ -877,7 +949,7 @@
877
949
  if (this.tokenizer.token.type & TokenType.Indent) {
878
950
  this.tokenizer.nextToken();
879
951
  while (!(this.tokenizer.token.type & TokenType.Dedent) && !this.tokenizer.isEof()) {
880
- const child = this.templateNode();
952
+ const child = this.templateNode(children);
881
953
  if (child) {
882
954
  children.push(child);
883
955
  }
@@ -888,13 +960,25 @@
888
960
  }
889
961
  return children;
890
962
  }
891
- templateNode() {
892
- this.tokenizer.token;
963
+ templateNode(siblings) {
964
+ const token = this.tokenizer.token;
965
+ if (token.type & TokenType.Pipe) {
966
+ this.addError('PIPE_IN_WRONG_CONTEXT', '"|" 只能出现在元素属性扩展行中', token.loc ?? this.emptyLoc());
967
+ this.tokenizer.nextToken();
968
+ return null;
969
+ }
893
970
  const _this$tokenizer$_hook = this.tokenizer._hook({}),
894
971
  _this$tokenizer$_hook2 = _slicedToArray(_this$tokenizer$_hook, 2),
895
972
  hookType = _this$tokenizer$_hook2[0],
896
973
  value = _this$tokenizer$_hook2[1];
897
974
  if (value === 'if' || value === 'else' || value === 'fail') {
975
+ if (value === 'else' || value === 'fail') {
976
+ const lastSibling = siblings[siblings.length - 1];
977
+ const lastType = lastSibling?.type;
978
+ if (lastType !== NodeType.If && lastType !== NodeType.Else && lastType !== NodeType.Fail) {
979
+ this.addError('ELSE_WITHOUT_IF', `"${value}" 前必须有 "if" 或 "else" 节点`, token.loc ?? this.emptyLoc());
980
+ }
981
+ }
898
982
  return this.parseConditionalNode();
899
983
  }
900
984
  if (value === 'for') {
@@ -919,6 +1003,13 @@
919
1003
  }
920
1004
  parseElementNode(node) {
921
1005
  const tagToken = this.tokenizer.token;
1006
+ if (!(tagToken.type & TokenType.Identifier)) {
1007
+ this.addError('INVALID_TAG_NAME', `无效的标签名,期望标识符但得到 "${tagToken.value}"`, tagToken.loc ?? this.emptyLoc());
1008
+ while (!(this.tokenizer.token.type & TokenType.NewLine) && !this.tokenizer.isEof()) {
1009
+ this.tokenizer.nextToken();
1010
+ }
1011
+ return null;
1012
+ }
922
1013
  const tagName = tagToken.value;
923
1014
  this.tokenizer.nextToken();
924
1015
  const props = this.headerLineAndExtensions();
@@ -944,15 +1035,25 @@
944
1035
  return node;
945
1036
  }
946
1037
  parseLoopNode(node) {
1038
+ const forLoc = this.tokenizer.token.loc ?? this.emptyLoc();
947
1039
  this.tokenizer.nextToken();
948
1040
  const collection = this.parsePropertyValue();
949
- this.tokenizer.nextToken();
1041
+ if (!collection.value && collection.value !== 0) {
1042
+ this.addError('MISSING_FOR_COLLECTION', '"for" 缺少集合表达式', forLoc);
1043
+ }
1044
+ const semicolonToken = this.tokenizer.nextToken();
1045
+ if (!(semicolonToken.type & TokenType.Semicolon)) {
1046
+ this.addError('MISSING_FOR_SEMICOLON', '"for" 语法:for <集合>; <item> [index][; key],缺少第一个 ";"', semicolonToken.loc ?? this.emptyLoc());
1047
+ }
950
1048
  const itemToken = this.tokenizer.nextToken();
951
1049
  const isDestruct = itemToken.type === TokenType.InsertionExp;
952
1050
  if (isDestruct) {
953
1051
  itemToken.value = '{' + itemToken.value + '}';
954
1052
  }
955
1053
  const item = this.parsePropertyValue();
1054
+ if (!item.value && item.value !== 0) {
1055
+ this.addError('MISSING_FOR_ITEM', '"for" 缺少 item 变量名', itemToken.loc ?? this.emptyLoc());
1056
+ }
956
1057
  let char = this.tokenizer.peekChar(),
957
1058
  key,
958
1059
  index;
@@ -1015,6 +1116,8 @@
1015
1116
  this.tokenizer.nextToken();
1016
1117
  node.value = this.parsePropertyValue();
1017
1118
  this.tokenizer.nextToken();
1119
+ } else {
1120
+ this.addError('MISSING_ASSIGN', `属性 "${node.key.key}" 缺少 "=" 赋值符号`, node.key.loc ?? this.emptyLoc());
1018
1121
  }
1019
1122
  node.loc.start = node.key.loc.start;
1020
1123
  node.loc.end = node.value ? node.value.loc.end : node.key.loc.end;
@@ -2014,6 +2117,7 @@
2014
2117
 
2015
2118
  exports.Compiler = Compiler;
2016
2119
  exports.NodeType = NodeType;
2120
+ exports.ParseSyntaxError = ParseSyntaxError;
2017
2121
  exports.Tokenizer = Tokenizer;
2018
2122
  exports.bobe = bobe;
2019
2123
  exports.customRender = customRender;