├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── __mocks__ ├── styleMock.js └── svgMock.js ├── __tests__ ├── __snapshots__ │ ├── lexer.test.ts.snap │ └── parser.test.ts.snap ├── compat.test.ts ├── evalute.test.ts ├── filter.test.ts ├── fomula.test.ts ├── jest.setup.js ├── lexer.test.ts ├── parser.test.ts └── tokenize.test.ts ├── package.json ├── rollup.config.js ├── scripts ├── genDoc.ts └── lib.ts ├── src ├── evalutor.ts ├── filter.ts ├── index.ts ├── lexer.ts ├── parser.ts └── util.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .DS_Store 107 | 108 | package-lock.json 109 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | /configs 3 | /__mocks__ 4 | /__tests__ 5 | tsconfig.json 6 | /coverage 7 | /scripts 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "semi": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "quoteProps": "consistent", 10 | "arrowParens": "avoid", 11 | "jsxBracketSameLine": false, 12 | "overrides": [ 13 | { 14 | "files": "src/locale/*.ts", 15 | "options": { 16 | "printWidth": 800 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 百度智能云爱速搭 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # amis-formula 2 | 3 | 负责 amis 里面的表达式、公式及模板的实现 4 | 5 | 属于 [amis](https://github.com/baidu/amis) 子项目 6 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * 4 | * 用于 jest 加载 css 的 mock 5 | */ 6 | module.exports = {}; 7 | -------------------------------------------------------------------------------- /__mocks__/svgMock.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | module.exports = React.forwardRef((props, ref) => 4 | React.createElement('icon-mock', {ref, ...props}) 5 | ); 6 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/lexer.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`lexer:filter 1`] = ` 4 | Array [ 5 | " $abc is ", 6 | " \${", 7 | " abc", 8 | " |", 9 | " ", 10 | " date", 11 | " :", 12 | " ", 13 | " YYYY-MM-DD", 14 | " ", 15 | " HH:mm:ss", 16 | " }", 17 | " undefined", 18 | ] 19 | `; 20 | 21 | exports[`lexer:simple 1`] = ` 22 | Array [ 23 | " expression result is ", 24 | " \${", 25 | " a", 26 | " +", 27 | " b", 28 | " }", 29 | " undefined", 30 | ] 31 | `; 32 | -------------------------------------------------------------------------------- /__tests__/compat.test.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import {resolveVariable, resolveVariableAndFilter} from '../src'; 3 | 4 | const filters = [ 5 | { 6 | type: 'raw1', 7 | data: { 8 | value: 1 9 | }, 10 | path: '${value}', 11 | filter: '| raw', 12 | expectValue: 1 13 | }, 14 | { 15 | type: 'raw2', 16 | data: { 17 | value: '2' 18 | }, 19 | path: '${value}', 20 | filter: '| raw', 21 | expectValue: '2' 22 | }, 23 | { 24 | type: 'raw3', 25 | data: { 26 | a: 1, 27 | b: '2', 28 | c: { 29 | '1': 'first', 30 | '2': 'second' 31 | } 32 | }, 33 | path: '${c.${a}}', 34 | filter: '| raw', 35 | expectValue: 'first' 36 | }, 37 | { 38 | type: 'raw4', 39 | data: { 40 | a: 1, 41 | b: '2', 42 | c: { 43 | '1': 'first', 44 | '2': 'second' 45 | } 46 | }, 47 | path: '${c.${b}}', 48 | filter: '| raw', 49 | expectValue: 'second' 50 | }, 51 | { 52 | type: 'raw5', 53 | data: { 54 | a: 1 55 | }, 56 | path: '', 57 | filter: '| raw', 58 | expectValue: undefined 59 | }, 60 | { 61 | type: 'raw6', 62 | data: { 63 | a: 1 64 | }, 65 | path: '$$', 66 | filter: 'raw', 67 | expectValue: {a: 1} 68 | }, 69 | { 70 | type: 'raw7', 71 | data: { 72 | a: 1 73 | }, 74 | path: '$a', 75 | filter: '| raw', 76 | expectValue: 1 77 | }, 78 | { 79 | type: 'json', 80 | data: { 81 | value: { 82 | a: 'a', 83 | b: 'b' 84 | } 85 | }, 86 | path: '${value | json:0}', 87 | filter: '', 88 | expectValue: '{"a":"a","b":"b"}' 89 | }, 90 | { 91 | type: 'toJson', 92 | data: { 93 | value: '{"a":"a","b":"b"}' 94 | }, 95 | path: '${value | toJson}', 96 | filter: '', 97 | expectValue: { 98 | a: 'a', 99 | b: 'b' 100 | } 101 | }, 102 | { 103 | type: 'date', 104 | data: { 105 | value: 1559649981 106 | }, 107 | path: '${value | date}', 108 | filter: '', 109 | expectValue: moment(1559649981, 'X').format('LLL') 110 | }, 111 | { 112 | type: 'number', 113 | data: { 114 | value: 9999 115 | }, 116 | path: '${value| number}', 117 | filter: '', 118 | expectValue: '9,999' 119 | }, 120 | { 121 | type: 'trim', 122 | data: { 123 | value: ' abc ' 124 | }, 125 | path: '${value| trim}', 126 | filter: '', 127 | expectValue: 'abc' 128 | }, 129 | { 130 | type: 'percent', 131 | data: { 132 | value: 0.8232343 133 | }, 134 | path: '${value| percent}', 135 | filter: '', 136 | expectValue: '82%' 137 | }, 138 | // duration 139 | { 140 | type: 'duration1', 141 | data: { 142 | value: 1 143 | }, 144 | path: '${value| duration}', 145 | filter: '', 146 | expectValue: '1秒' 147 | }, 148 | { 149 | type: 'duration2', 150 | data: { 151 | value: 61 152 | }, 153 | path: '${value| duration}', 154 | filter: '', 155 | expectValue: '1分1秒' 156 | }, 157 | { 158 | type: 'duration3', 159 | data: { 160 | value: 233233 161 | }, 162 | path: '${value| duration}', 163 | filter: '', 164 | expectValue: '2天16时47分13秒' 165 | }, 166 | // bytes 167 | { 168 | type: 'bytes1', 169 | data: { 170 | value: 1024 171 | }, 172 | path: '${value| bytes}', 173 | filter: '', 174 | expectValue: '1.02 KB' 175 | }, 176 | { 177 | type: 'bytes2', 178 | data: { 179 | value: 1024000 180 | }, 181 | path: '${value| bytes}', 182 | filter: '', 183 | expectValue: '1.02 MB' 184 | }, 185 | { 186 | type: 'bytes3', 187 | data: { 188 | value: -1024 189 | }, 190 | path: '${value| bytes}', 191 | filter: '', 192 | expectValue: '-1.02 KB' 193 | }, 194 | { 195 | type: 'bytes4', 196 | data: { 197 | value: 0.5 198 | }, 199 | path: '${value| bytes}', 200 | filter: '', 201 | expectValue: '0.5 B' 202 | }, 203 | // round 204 | { 205 | type: 'round1', 206 | data: { 207 | value: '啥啊' 208 | }, 209 | path: '${value| round}', 210 | filter: '', 211 | expectValue: 0 212 | }, 213 | { 214 | type: 'round2', 215 | data: { 216 | value: 1.22 217 | }, 218 | path: '${value| round:1}', 219 | filter: '', 220 | expectValue: '1.2' 221 | }, 222 | { 223 | type: 'round3', 224 | data: { 225 | value: 1.26 226 | }, 227 | path: '${value| round:1}', 228 | filter: '', 229 | expectValue: '1.3' 230 | }, 231 | { 232 | type: 'truncate1', 233 | data: { 234 | value: 'this is a very loooooong sentence.' 235 | }, 236 | path: '${value| truncate:10}', 237 | filter: '', 238 | expectValue: 'this is a ...' 239 | }, 240 | { 241 | type: 'truncate2', 242 | data: { 243 | value: 'this is a very loooooong sentence.' 244 | }, 245 | path: '${value| truncate:null}', 246 | filter: '', 247 | expectValue: 'this is a very loooooong sentence.' 248 | }, 249 | { 250 | type: 'url_encode', 251 | data: { 252 | value: 'http://www.baidu.com?query=123' 253 | }, 254 | path: '${value| url_encode}', 255 | filter: '', 256 | expectValue: 'http%3A%2F%2Fwww.baidu.com%3Fquery%3D123' 257 | }, 258 | { 259 | type: 'url_decode', 260 | data: { 261 | value: 'http%3A%2F%2Fwww.baidu.com%3Fquery%3D123' 262 | }, 263 | path: '${value| url_decode:10}', 264 | filter: '', 265 | expectValue: 'http://www.baidu.com?query=123' 266 | }, 267 | { 268 | type: 'default1', 269 | data: { 270 | value: '' 271 | }, 272 | path: '${value| default}', 273 | filter: '', 274 | expectValue: undefined 275 | }, 276 | { 277 | type: 'default2', 278 | data: { 279 | value: '' 280 | }, 281 | path: '${value| default:-}', 282 | filter: '', 283 | expectValue: '-' 284 | }, 285 | { 286 | type: 'join', 287 | data: { 288 | value: ['a', 'b', 'c'] 289 | }, 290 | path: '${value| join:,}', 291 | filter: '', 292 | expectValue: 'a,b,c' 293 | }, 294 | { 295 | type: 'split', 296 | data: { 297 | value: 'a,b,c' 298 | }, 299 | path: '${value| split}', 300 | filter: '', 301 | expectValue: ['a', 'b', 'c'] 302 | }, 303 | { 304 | type: 'first', 305 | data: { 306 | value: ['a', 'b', 'c'] 307 | }, 308 | path: '${value| first}', 309 | filter: '', 310 | expectValue: 'a' 311 | }, 312 | { 313 | type: 'nth', 314 | data: { 315 | value: ['a', 'b', 'c'] 316 | }, 317 | path: '${value| nth:1}', 318 | filter: '', 319 | expectValue: 'b' 320 | }, 321 | { 322 | type: 'last', 323 | data: { 324 | value: ['a', 'b', 'c'] 325 | }, 326 | path: '${value| last}', 327 | filter: '', 328 | expectValue: 'c' 329 | }, 330 | { 331 | type: 'minus', 332 | data: { 333 | value: 5 334 | }, 335 | path: '${value| minus:1}', 336 | filter: '', 337 | expectValue: 4 338 | }, 339 | { 340 | type: 'plus', 341 | data: { 342 | value: 5 343 | }, 344 | path: '${value| plus:1}', 345 | filter: '', 346 | expectValue: 6 347 | }, 348 | { 349 | type: 'pick1', 350 | data: { 351 | value: { 352 | a: '1', 353 | b: '2' 354 | } 355 | }, 356 | path: '${value| pick:a}', 357 | filter: '', 358 | expectValue: '1' 359 | }, 360 | { 361 | type: 'pick2', 362 | data: { 363 | value: [ 364 | { 365 | label: 'A', 366 | value: 'a' 367 | }, 368 | { 369 | label: 'B', 370 | value: 'b' 371 | }, 372 | { 373 | label: 'C', 374 | value: 'c' 375 | } 376 | ] 377 | }, 378 | path: '${value| pick:value}', 379 | filter: '', 380 | expectValue: ['a', 'b', 'c'] 381 | }, 382 | { 383 | type: 'pick_if_exist', 384 | data: { 385 | value: [ 386 | { 387 | label: 'A', 388 | value: 'a' 389 | }, 390 | { 391 | label: 'B', 392 | value: 'b' 393 | }, 394 | { 395 | label: 'C', 396 | value: 'c' 397 | } 398 | ] 399 | }, 400 | path: '${value| pick_if_exist:value}', 401 | filter: '', 402 | expectValue: ['a', 'b', 'c'] 403 | }, 404 | { 405 | type: 'str2date', 406 | data: { 407 | value: '1559649981' 408 | }, 409 | path: '${value| str2date:X:YYYY-MM-DD HH-mm-ss}', 410 | filter: '', 411 | expectValue: moment('1559649981', 'X').format('YYYY-MM-DD HH-mm-ss') 412 | }, 413 | { 414 | type: 'asArray', 415 | data: { 416 | value: 'a' 417 | }, 418 | path: '${value| asArray}', 419 | filter: '', 420 | expectValue: ['a'] 421 | }, 422 | { 423 | type: 'base64Encode', 424 | data: { 425 | value: 'I love amis' 426 | }, 427 | path: '${value| base64Encode}', 428 | filter: '', 429 | expectValue: 'SSBsb3ZlIGFtaXM=' 430 | }, 431 | { 432 | type: 'base64Decode', 433 | data: { 434 | value: 'SSBsb3ZlIGFtaXM=' 435 | }, 436 | path: '${value| base64Decode}', 437 | filter: '', 438 | expectValue: 'I love amis' 439 | }, 440 | { 441 | type: 'lowerCase', 442 | data: { 443 | value: 'AbC' 444 | }, 445 | path: '${value| lowerCase}', 446 | filter: '', 447 | expectValue: 'abc' 448 | }, 449 | { 450 | type: 'upperCase', 451 | data: { 452 | value: 'aBc' 453 | }, 454 | path: '${value| upperCase}', 455 | filter: '', 456 | expectValue: 'ABC' 457 | } 458 | ]; 459 | 460 | filters.forEach(f => { 461 | test(`compat:${f.type}`, () => { 462 | const result = resolveVariableAndFilter(f.path, f.data, f.filter); 463 | expect(result).toEqual(f.expectValue); 464 | }); 465 | }); 466 | 467 | test(`compat:filter`, () => { 468 | expect( 469 | resolveVariableAndFilter( 470 | '${rows | filter:engine:match:keywords}', 471 | { 472 | rows: [ 473 | { 474 | engine: 'a' 475 | }, 476 | { 477 | engine: 'b' 478 | }, 479 | { 480 | engine: 'c' 481 | } 482 | ] 483 | }, 484 | '| raw' 485 | ) 486 | ).toMatchObject([ 487 | { 488 | engine: 'a' 489 | }, 490 | { 491 | engine: 'b' 492 | }, 493 | { 494 | engine: 'c' 495 | } 496 | ]); 497 | 498 | expect( 499 | resolveVariableAndFilter( 500 | '${rows | filter:engine:match:keywords}', 501 | { 502 | keywords: 'a', 503 | rows: [ 504 | { 505 | engine: 'a' 506 | }, 507 | { 508 | engine: 'b' 509 | }, 510 | { 511 | engine: 'c' 512 | } 513 | ] 514 | }, 515 | '| raw' 516 | ) 517 | ).toMatchObject([ 518 | { 519 | engine: 'a' 520 | } 521 | ]); 522 | }); 523 | 524 | test(`compat:&`, () => { 525 | expect( 526 | resolveVariableAndFilter( 527 | '${& | json:0}', 528 | { 529 | a: 1, 530 | b: 2 531 | }, 532 | '| raw' 533 | ) 534 | ).toBe('{"a":1,"b":2}'); 535 | }); 536 | 537 | test(`compat:filter-default`, () => { 538 | expect( 539 | resolveVariableAndFilter( 540 | '${a | default:undefined}', 541 | { 542 | a: 1 543 | }, 544 | '| raw' 545 | ) 546 | ).toBe(1); 547 | 548 | expect( 549 | resolveVariableAndFilter( 550 | '${a | default:undefined}', 551 | { 552 | a: [1, 2, 3] 553 | }, 554 | '| raw' 555 | ) 556 | ).toMatchObject([1, 2, 3]); 557 | 558 | expect( 559 | resolveVariableAndFilter( 560 | '${b | default:undefined}', 561 | { 562 | a: 1 563 | }, 564 | '| raw' 565 | ) 566 | ).toBe(undefined); 567 | expect( 568 | resolveVariableAndFilter( 569 | '${b | default:-}', 570 | { 571 | a: 1 572 | }, 573 | '| raw' 574 | ) 575 | ).toBe('-'); 576 | 577 | expect( 578 | resolveVariableAndFilter( 579 | '${b | default:undefined}', 580 | { 581 | a: 1 582 | }, 583 | '| raw', 584 | () => '' 585 | ) 586 | ).toBe(undefined); 587 | expect( 588 | resolveVariableAndFilter( 589 | '${b}', 590 | { 591 | a: 1 592 | }, 593 | '| raw', 594 | () => '' 595 | ) 596 | ).toBe(''); 597 | }); 598 | 599 | test(`compat:numberVariable`, () => { 600 | expect( 601 | resolveVariableAndFilter( 602 | 'a $1 ', 603 | { 604 | '1': 233 605 | }, 606 | '| raw' 607 | ) 608 | ).toEqual('a 233 '); 609 | 610 | expect( 611 | resolveVariableAndFilter( 612 | 'a $1', 613 | { 614 | '1': 233 615 | }, 616 | '| raw' 617 | ) 618 | ).toEqual('a 233'); 619 | }); 620 | 621 | test(`compat:test`, () => { 622 | const result = resolveVariableAndFilter('', {}, '| raw'); 623 | expect(result).toEqual(undefined); 624 | }); 625 | 626 | test(`compat:test2`, () => { 627 | const data = { 628 | '123': 123, 629 | '123.123': 123, 630 | '中文': 123, 631 | 'obj': { 632 | x: 123 633 | } 634 | }; 635 | expect(resolveVariable('123', data)).toEqual(123); 636 | expect(resolveVariable('123.123', data)).toEqual(123); 637 | expect(resolveVariable('中文', data)).toEqual(123); 638 | expect(resolveVariable('obj.x', data)).toEqual(123); 639 | }); 640 | -------------------------------------------------------------------------------- /__tests__/evalute.test.ts: -------------------------------------------------------------------------------- 1 | import {evaluate, parse} from '../src'; 2 | 3 | test('evalute:simple', () => { 4 | expect( 5 | evaluate('a is ${a}', { 6 | a: 123 7 | }) 8 | ).toBe('a is 123'); 9 | }); 10 | 11 | test('evalute:filter', () => { 12 | expect( 13 | evaluate( 14 | 'a is ${a | abc}', 15 | { 16 | a: 123 17 | }, 18 | { 19 | filters: { 20 | abc(input: any) { 21 | return `${input}456`; 22 | } 23 | } 24 | } 25 | ) 26 | ).toBe('a is 123456'); 27 | 28 | expect( 29 | evaluate( 30 | 'a is ${a | concat:233}', 31 | { 32 | a: 123 33 | }, 34 | { 35 | filters: { 36 | concat(input: any, arg: string) { 37 | return `${input}${arg}`; 38 | } 39 | } 40 | } 41 | ) 42 | ).toBe('a is 123233'); 43 | 44 | expect( 45 | evaluate( 46 | 'a is ${concat(a, a)}', 47 | { 48 | a: 123 49 | }, 50 | { 51 | filters: { 52 | concat(input: any, arg: string) { 53 | return `${input}${arg}`; 54 | } 55 | } 56 | } 57 | ) 58 | ).toBe('a is 123123'); 59 | }); 60 | 61 | test('evalute:filter2', () => { 62 | expect( 63 | evaluate( 64 | 'a is ${[1, 2, 3] | concat:4 | join}', 65 | {}, 66 | { 67 | filters: { 68 | concat(input: any, ...args: Array) { 69 | return input.concat.apply(input, args); 70 | }, 71 | join(input: any) { 72 | return input.join(','); 73 | } 74 | } 75 | } 76 | ) 77 | ).toBe('a is 1,2,3,4'); 78 | }); 79 | 80 | test('evalute:filter3', () => { 81 | expect( 82 | evaluate( 83 | 'a is ${[1, 2, 3] | concat:"4" | join}', 84 | {}, 85 | { 86 | filters: { 87 | concat(input: any, ...args: Array) { 88 | return input.concat.apply(input, args); 89 | }, 90 | join(input: any) { 91 | return input.join(','); 92 | } 93 | } 94 | } 95 | ) 96 | ).toBe('a is 1,2,3,4'); 97 | }); 98 | 99 | test('evalute:filter4', () => { 100 | expect( 101 | evaluate( 102 | 'a is ${[1, 2, 3] | concat:${a + 3} | join}', 103 | { 104 | a: 4 105 | }, 106 | { 107 | filters: { 108 | concat(input: any, ...args: Array) { 109 | return input.concat.apply(input, args); 110 | }, 111 | join(input: any) { 112 | return input.join(','); 113 | } 114 | } 115 | } 116 | ) 117 | ).toBe('a is 1,2,3,7'); 118 | }); 119 | 120 | test('evalute:oldVariable', () => { 121 | expect( 122 | evaluate('a is $a', { 123 | a: 4 124 | }) 125 | ).toBe('a is 4'); 126 | 127 | expect( 128 | evaluate('b is $b', { 129 | a: 4 130 | }) 131 | ).toBe('b is '); 132 | 133 | expect( 134 | evaluate('a.b is $a.b', { 135 | a: { 136 | b: 233 137 | } 138 | }) 139 | ).toBe('a.b is 233'); 140 | }); 141 | 142 | test('evalute:ariable2', () => { 143 | expect( 144 | evaluate('a is $$', { 145 | a: 4 146 | }) 147 | ).toBe('a is [object Object]'); 148 | }); 149 | 150 | test('evalute:ariable3', () => { 151 | expect( 152 | evaluate( 153 | '$$', 154 | { 155 | a: 4 156 | }, 157 | { 158 | defaultFilter: 'raw' 159 | } 160 | ) 161 | ).toMatchObject({ 162 | a: 4 163 | }); 164 | }); 165 | 166 | test('evalute:conditional', () => { 167 | expect( 168 | evaluate( 169 | '${a | isTrue: true : false}', 170 | { 171 | a: 4 172 | }, 173 | { 174 | defaultFilter: 'raw' 175 | } 176 | ) 177 | ).toBe(true); 178 | 179 | expect( 180 | evaluate( 181 | '${a | isTrue: b : false}', 182 | { 183 | a: 4, 184 | b: 5 185 | }, 186 | { 187 | defaultFilter: 'raw' 188 | } 189 | ) 190 | ).toBe(5); 191 | 192 | expect( 193 | evaluate( 194 | '${a | isTrue: b : false}', 195 | { 196 | a: null, 197 | b: 5 198 | }, 199 | { 200 | defaultFilter: 'raw' 201 | } 202 | ) 203 | ).toBe(false); 204 | 205 | expect( 206 | evaluate( 207 | '${a | isEquals: 1 : "1" |isEquals: 2 : "2" | isEquals: 3 : "3" }', 208 | { 209 | a: 3 210 | }, 211 | { 212 | defaultFilter: 'raw' 213 | } 214 | ) 215 | ).toBe('3'); 216 | 217 | expect( 218 | evaluate( 219 | '${a | isEquals: 1 : "1" |isEquals: 1 : "2" | isEquals: 1 : "3" }', 220 | { 221 | a: 1 222 | }, 223 | { 224 | defaultFilter: 'raw' 225 | } 226 | ) 227 | ).toBe('1'); 228 | 229 | expect( 230 | evaluate( 231 | '${a | isEquals: 1 : "1" : "12" |isEquals: 2 : "2" | isEquals: 3 : "3" }', 232 | { 233 | a: 2 234 | }, 235 | { 236 | defaultFilter: 'raw' 237 | } 238 | ) 239 | ).toBe('12'); 240 | }); 241 | 242 | test('evalute:object-variable', () => { 243 | const data = { 244 | key: 'x', 245 | obj: { 246 | x: 1, 247 | y: 2 248 | } 249 | }; 250 | 251 | expect(evaluate('a is ${obj.x}', data)).toBe('a is 1'); 252 | expect(evaluate('a is ${obj[x]}', data)).toBe('a is 1'); 253 | expect(evaluate('a is ${obj[`x`]}', data)).toBe('a is 1'); 254 | expect(evaluate('a is ${obj["x"]}', data)).toBe('a is 1'); 255 | expect(evaluate('a is ${obj[key]}', data)).toBe('a is 1'); 256 | expect(evaluate('a is ${obj[`${key}`]}', data)).toBe('a is 1'); 257 | expect(evaluate('a is ${obj[${key}]}', data)).toBe('a is 1'); 258 | }); 259 | 260 | test('evalute:literal-variable', () => { 261 | const data = { 262 | key: 'x', 263 | index: 0, 264 | obj: { 265 | x: 1, 266 | y: 2 267 | } 268 | }; 269 | 270 | expect(evaluate('a is ${({x: 1})["x"]}', data)).toBe('a is 1'); 271 | expect(evaluate('a is ${({x: 1}).x}', data)).toBe('a is 1'); 272 | expect(evaluate('a is ${(["a", "b"])[index]}', data)).toBe('a is a'); 273 | expect(evaluate('a is ${(["a", "b"])[1]}', data)).toBe('a is b'); 274 | expect(evaluate('a is ${(["a", "b"]).0}', data)).toBe('a is a'); 275 | }); 276 | 277 | test('evalute:tempalte', () => { 278 | const data = { 279 | key: 'x' 280 | }; 281 | 282 | expect(evaluate('abc${`11${3}22`}xyz', data)).toBe('abc11322xyz'); 283 | expect(evaluate('abc${`${3}22`}xyz', data)).toBe('abc322xyz'); 284 | expect(evaluate('abc${`11${3}`}xyz', data)).toBe('abc113xyz'); 285 | expect(evaluate('abc${`${3}`}xyz', data)).toBe('abc3xyz'); 286 | expect(evaluate('abc${`${key}`}xyz', data)).toBe('abcxxyz'); 287 | }); 288 | 289 | test('evalute:literal', () => { 290 | const data = { 291 | dynamicKey: 'alpha' 292 | }; 293 | 294 | expect( 295 | evaluate('${{a: 1, 0: 2, "3": 3}}', data, { 296 | defaultFilter: 'raw' 297 | }) 298 | ).toMatchObject({ 299 | a: 1, 300 | 0: 2, 301 | 3: 3 302 | }); 303 | 304 | expect( 305 | evaluate('${{a: 1, 0: 2, "3": 3, [`4`]: 4}}', data, { 306 | defaultFilter: 'raw' 307 | }) 308 | ).toMatchObject({ 309 | a: 1, 310 | 0: 2, 311 | 3: 3, 312 | 4: 4 313 | }); 314 | 315 | expect( 316 | evaluate('${{a: 1, 0: 2, "3": 3, [`${dynamicKey}233`]: 4}}', data, { 317 | defaultFilter: 'raw' 318 | }) 319 | ).toMatchObject({ 320 | a: 1, 321 | 0: 2, 322 | 3: 3, 323 | alpha233: 4 324 | }); 325 | 326 | expect( 327 | evaluate('${[1, 2, `2${dynamicKey}2`, {a: 1, 0: 2, [`2`]: "3"}]}', data, { 328 | defaultFilter: 'raw' 329 | }) 330 | ).toMatchObject([1, 2, `2alpha2`, {a: 1, 0: 2, [`2`]: '3'}]); 331 | }); 332 | 333 | test('evalute:variableName', () => { 334 | const data = { 335 | 'a-b': 'c', 336 | '222': 10222, 337 | '222_221': 233, 338 | '222_abcde': 'abcde', 339 | '222-221': 333 340 | }; 341 | 342 | expect(evaluate('${a-b}', data)).toBe('c'); 343 | expect(evaluate('${222}', data)).toBe(222); 344 | expect(evaluate('${222_221}', data)).toBe('233'); 345 | expect(evaluate('${222-221}', data)).toBe(1); 346 | expect(evaluate('${222_abcde}', data)).toBe('abcde'); 347 | expect( 348 | evaluate('${&["222-221"]}', data, { 349 | defaultFilter: 'raw' 350 | }) 351 | ).toBe(333); 352 | expect( 353 | evaluate('222', data, { 354 | variableMode: true 355 | }) 356 | ).toBe(10222); 357 | }); 358 | 359 | test('evalute:3-1', () => { 360 | const data = {}; 361 | 362 | expect(evaluate('${3-1}', data)).toBe(2); 363 | expect(evaluate('${-1 + 2.5 + 3}', data)).toBe(4.5); 364 | expect(evaluate('${-1 + -1}', data)).toBe(-2); 365 | expect(evaluate('${3 * -1}', data)).toBe(-3); 366 | 367 | expect(evaluate('${3 + +1}', data)).toBe(4); 368 | }); 369 | 370 | test('evalate:0.1+0.2', () => { 371 | expect(evaluate('${0.1 + 0.2}', {})).toBe(0.3); 372 | }); 373 | 374 | test('evalute:variable:com.xxx.xx', () => { 375 | const data = { 376 | 'com.xxx.xx': 'abc', 377 | 'com xxx%xx': 'cde', 378 | 'com[xxx]': 'eee' 379 | }; 380 | 381 | expect(evaluate('${com\\.xxx\\.xx}', data)).toBe('abc'); 382 | expect(evaluate('${com\\ xxx\\%xx}', data)).toBe('cde'); 383 | expect(evaluate('${com\\[xxx\\]}', data)).toBe('eee'); 384 | }); 385 | 386 | test('evalute:anonymous:function', () => { 387 | const data = { 388 | arr: [1, 2, 3], 389 | arr2: [ 390 | { 391 | a: 1 392 | }, 393 | { 394 | a: 2 395 | }, 396 | { 397 | a: 3 398 | } 399 | ], 400 | outter: 4 401 | }; 402 | 403 | expect(evaluate('${() => 233}', data)).toMatchObject({ 404 | args: [], 405 | return: {type: 'literal', value: 233}, 406 | type: 'anonymous_function' 407 | }); 408 | 409 | expect(evaluate('${ARRAYMAP(arr, () => 1)}', data)).toMatchObject([1, 1, 1]); 410 | expect(evaluate('${ARRAYMAP(arr, item => item)}', data)).toMatchObject([ 411 | 1, 2, 3 412 | ]); 413 | expect(evaluate('${ARRAYMAP(arr, item => item * 2)}', data)).toMatchObject([ 414 | 2, 4, 6 415 | ]); 416 | expect( 417 | evaluate('${ARRAYMAP(arr2, (item, index) => `a${item.a}${index}`)}', data) 418 | ).toMatchObject(['a10', 'a21', 'a32']); 419 | expect( 420 | evaluate( 421 | '${ARRAYMAP(arr2, (item, index) => `a${item.a}${index}${outter}`)}', 422 | data 423 | ) 424 | ).toMatchObject(['a104', 'a214', 'a324']); 425 | expect( 426 | evaluate( 427 | '${ARRAYMAP(arr2, (item, index) => {x: item.a, index: index})}', 428 | data 429 | ) 430 | ).toMatchObject([ 431 | { 432 | x: 1, 433 | index: 0 434 | }, 435 | { 436 | x: 2, 437 | index: 1 438 | }, 439 | { 440 | x: 3, 441 | index: 2 442 | } 443 | ]); 444 | }); 445 | 446 | test('evalute:anonymous:function2', () => { 447 | const data = { 448 | arr: [1, 2, 3], 449 | arr2: [ 450 | { 451 | x: 1, 452 | y: [ 453 | { 454 | z: 1 455 | }, 456 | { 457 | z: 1 458 | } 459 | ] 460 | }, 461 | { 462 | x: 2, 463 | y: [ 464 | { 465 | z: 2 466 | }, 467 | { 468 | z: 2 469 | } 470 | ] 471 | } 472 | ] 473 | }; 474 | 475 | expect( 476 | evaluate( 477 | '${ARRAYMAP(ARRAYMAP(arr, item => item * 2), item => item + 2)}', 478 | data 479 | ) 480 | ).toMatchObject([4, 6, 8]); 481 | 482 | expect( 483 | evaluate('${ARRAYMAP(arr2, item => ARRAYMAP(item.y, i => i.z))}', data) 484 | ).toMatchObject([ 485 | [1, 1], 486 | [2, 2] 487 | ]); 488 | }); 489 | 490 | test('evalute:array:func', () => { 491 | const data = { 492 | arr1: [0, 1, false, 2, '', 3], 493 | arr2: ['a', 'b', 'c'], 494 | arr3: [1, 2, 3], 495 | arr4: [2, 4, 6] 496 | }; 497 | 498 | expect(evaluate('${COMPACT(arr1)}', data)).toMatchObject([1, 2, 3]); 499 | 500 | expect(evaluate("${COMPACT([0, 1, false, 2, '', 3])}", data)).toMatchObject([ 501 | 1, 2, 3 502 | ]); 503 | 504 | expect(evaluate('${JOIN(arr2, "~")}', data)).toMatch('a~b~c'); 505 | 506 | expect(evaluate('${SUM(arr3)}', data)).toBe(6); 507 | 508 | expect(evaluate('${AVG(arr4)}', data)).toBe(4); 509 | 510 | expect(evaluate('${MIN(arr4)}', data)).toBe(2); 511 | 512 | expect(evaluate('${MAX(arr4)}', data)).toBe(6); 513 | }); 514 | -------------------------------------------------------------------------------- /__tests__/filter.test.ts: -------------------------------------------------------------------------------- 1 | import {resolveVariableAndFilter} from '../src'; 2 | 3 | test(`filter:map`, () => { 4 | expect( 5 | resolveVariableAndFilter('${a | map: toInt}', { 6 | a: ['123', '3434'] 7 | }) 8 | ).toMatchObject([123, 3434]); 9 | }); 10 | test(`filter:html`, () => { 11 | expect( 12 | resolveVariableAndFilter('${a}', { 13 | a: '' 14 | }) 15 | ).toEqual('<html>'); 16 | }); 17 | 18 | test(`filter:complex`, () => { 19 | expect( 20 | resolveVariableAndFilter('${`${a}`}', { 21 | a: '' 22 | }) 23 | ).toEqual(''); 24 | 25 | expect( 26 | resolveVariableAndFilter('${a ? a : a}', { 27 | a: '' 28 | }) 29 | ).toEqual(''); 30 | 31 | expect( 32 | resolveVariableAndFilter('${b.a}', { 33 | a: '', 34 | b: { 35 | a: '
' 36 | } 37 | }) 38 | ).toEqual('<br />'); 39 | }); 40 | 41 | test(`filter:json`, () => { 42 | expect( 43 | resolveVariableAndFilter('${a | json : 0}', { 44 | a: {a: 1} 45 | }) 46 | ).toEqual('{"a":1}'); 47 | 48 | expect( 49 | resolveVariableAndFilter('${a | json : 2}', { 50 | a: {a: 1} 51 | }) 52 | ).toEqual('{\n "a": 1\n}'); 53 | }); 54 | 55 | test(`filter:toJson`, () => { 56 | expect( 57 | resolveVariableAndFilter('${a|toJson}', { 58 | a: '{"a":1}' 59 | }) 60 | ).toMatchObject({a: 1}); 61 | }); 62 | 63 | test(`filter:toInt`, () => { 64 | expect( 65 | resolveVariableAndFilter('${a|toInt}', { 66 | a: '233' 67 | }) 68 | ).toBe(233); 69 | }); 70 | 71 | test(`filter:toFloat`, () => { 72 | expect( 73 | resolveVariableAndFilter('${a|toFloat}', { 74 | a: '233.233' 75 | }) 76 | ).toBe(233.233); 77 | }); 78 | 79 | test(`filter:toDate`, () => { 80 | expect( 81 | resolveVariableAndFilter('${a|toDate:x|date: YYYY-MM-DD}', { 82 | a: 1638028267226 83 | }) 84 | ).toBe('2021-11-27'); 85 | }); 86 | 87 | test(`filter:fromNow`, () => { 88 | expect( 89 | resolveVariableAndFilter('${a|toDate:x|fromNow}', { 90 | a: Date.now() - 2 * 60 * 1000 91 | }) 92 | ).toBe('2 minutes ago'); 93 | }); 94 | 95 | test(`filter:dateModify`, () => { 96 | expect( 97 | resolveVariableAndFilter('${a|toDate:x|dateModify:subtract:2:m|fromNow}', { 98 | a: Date.now() 99 | }) 100 | ).toBe('2 minutes ago'); 101 | }); 102 | 103 | test(`filter:number`, () => { 104 | expect( 105 | resolveVariableAndFilter('${a|number}', { 106 | a: 1234 107 | }) 108 | ).toBe('1,234'); 109 | }); 110 | 111 | test(`filter:trim`, () => { 112 | expect( 113 | resolveVariableAndFilter('${a|trim}', { 114 | a: ' ab ' 115 | }) 116 | ).toBe('ab'); 117 | }); 118 | 119 | test(`filter:duration`, () => { 120 | expect( 121 | resolveVariableAndFilter('${a|duration}', { 122 | a: 234343 123 | }) 124 | ).toBe('2天17时5分43秒'); 125 | }); 126 | 127 | test(`filter:bytes`, () => { 128 | expect( 129 | resolveVariableAndFilter('${a|bytes}', { 130 | a: 234343 131 | }) 132 | ).toBe('234 KB'); 133 | }); 134 | test(`filter:round`, () => { 135 | expect( 136 | resolveVariableAndFilter('${a|round}', { 137 | a: 23.234 138 | }) 139 | ).toBe('23.23'); 140 | }); 141 | 142 | test(`filter:truncate`, () => { 143 | expect( 144 | resolveVariableAndFilter('${a|truncate:5}', { 145 | a: 'abcdefghijklmnopqrst' 146 | }) 147 | ).toBe('abcde...'); 148 | }); 149 | 150 | test(`filter:url_encode`, () => { 151 | expect( 152 | resolveVariableAndFilter('${a|url_encode}', { 153 | a: '=' 154 | }) 155 | ).toBe('%3D'); 156 | }); 157 | 158 | test(`filter:url_encode`, () => { 159 | expect( 160 | resolveVariableAndFilter('${a|url_decode}', { 161 | a: '%3D' 162 | }) 163 | ).toBe('='); 164 | }); 165 | 166 | test(`filter:url_encode`, () => { 167 | expect( 168 | resolveVariableAndFilter('${a|default:-}', { 169 | a: '' 170 | }) 171 | ).toBe('-'); 172 | }); 173 | 174 | test(`filter:join`, () => { 175 | expect( 176 | resolveVariableAndFilter('${a|join:-}', { 177 | a: [1, 2, 3] 178 | }) 179 | ).toBe('1-2-3'); 180 | }); 181 | 182 | test(`filter:split`, () => { 183 | expect( 184 | resolveVariableAndFilter('${a|split:-}', { 185 | a: '1-2-3' 186 | }) 187 | ).toMatchObject(['1', '2', '3']); 188 | }); 189 | 190 | test(`filter:sortBy`, () => { 191 | expect( 192 | resolveVariableAndFilter('${a|sortBy:&|join}', { 193 | a: ['b', 'c', 'a'] 194 | }) 195 | ).toBe('a,b,c'); 196 | 197 | expect( 198 | resolveVariableAndFilter('${a|sortBy:&:numerical|join}', { 199 | a: ['023', '20', '44'] 200 | }) 201 | ).toBe('20,023,44'); 202 | }); 203 | 204 | test(`filter:objectToArray`, () => { 205 | expect( 206 | resolveVariableAndFilter('${a|objectToArray}', { 207 | a: { 208 | a: 1, 209 | b: 2, 210 | done: 'Done' 211 | } 212 | }) 213 | ).toMatchObject([ 214 | { 215 | value: 'a', 216 | label: 1 217 | }, 218 | { 219 | value: 'b', 220 | label: 2 221 | }, 222 | { 223 | value: 'done', 224 | label: 'Done' 225 | } 226 | ]); 227 | }); 228 | 229 | test(`filter:substring`, () => { 230 | expect( 231 | resolveVariableAndFilter('${a|substring:0:2}', { 232 | a: 'abcdefg' 233 | }) 234 | ).toBe('ab'); 235 | expect( 236 | resolveVariableAndFilter('${a|substring:1:3}', { 237 | a: 'abcdefg' 238 | }) 239 | ).toBe('bc'); 240 | }); 241 | 242 | test(`filter:variableInVariable`, () => { 243 | expect( 244 | resolveVariableAndFilter('${a}', { 245 | a: 'abc$0defg' 246 | }) 247 | ).toBe('abc$0defg'); 248 | }); 249 | 250 | test('filter:isMatch', () => { 251 | expect( 252 | resolveVariableAndFilter('${status | isMatch:2:1|isMatch:5:1:4}', { 253 | status: 2 254 | }) 255 | ).toBe(1); 256 | }); 257 | 258 | test('filter:filter:isMatch', () => { 259 | expect( 260 | resolveVariableAndFilter('${items|filter:text:match:"ab"}', { 261 | items: [ 262 | { 263 | text: 'abc' 264 | }, 265 | { 266 | text: 'bcd' 267 | }, 268 | { 269 | text: 'cde' 270 | } 271 | ] 272 | }) 273 | ).toMatchObject([ 274 | { 275 | text: 'abc' 276 | } 277 | ]); 278 | }); 279 | -------------------------------------------------------------------------------- /__tests__/fomula.test.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import {evaluate, parse} from '../src'; 3 | 4 | const defaultContext = { 5 | a: 1, 6 | b: 2, 7 | c: 3, 8 | d: 4, 9 | e: 5 10 | }; 11 | 12 | function evalFormual(expression: string, data: any = defaultContext) { 13 | return evaluate(expression, data, { 14 | evalMode: true 15 | }); 16 | } 17 | 18 | test('formula:expression', () => { 19 | expect(evalFormual('a + 3')).toBe(4); 20 | expect(evalFormual('b * 3')).toBe(6); 21 | expect(evalFormual('b * 3 + 4')).toBe(10); 22 | expect(evalFormual('c * (3 + 4)')).toBe(21); 23 | expect(evalFormual('d / (a + 1)')).toBe(2); 24 | expect(evalFormual('5 % 3')).toBe(2); 25 | expect(evalFormual('3 | 4')).toBe(7); 26 | expect(evalFormual('4 ^ 4')).toBe(0); 27 | expect(evalFormual('4 ^ 4')).toBe(0); 28 | expect(evalFormual('4 & 4')).toBe(4); 29 | expect(evalFormual('4 & 3')).toBe(0); 30 | expect(evalFormual('~-1')).toBe(0); 31 | expect(evalFormual('!!1')).toBe(true); 32 | expect(evalFormual('!!""')).toBe(false); 33 | expect(evalFormual('1 || 2')).toBe(1); 34 | expect(evalFormual('1 && 2')).toBe(2); 35 | expect(evalFormual('1 && 2 || 3')).toBe(2); 36 | expect(evalFormual('1 || 2 || 3')).toBe(1); 37 | expect(evalFormual('1 || 2 && 3')).toBe(1); 38 | expect(evalFormual('(1 || 2) && 3')).toBe(3); 39 | expect(evalFormual('1 == "1"')).toBe(true); 40 | expect(evalFormual('1 === "1"')).toBe(false); 41 | expect(evalFormual('1 < 1')).toBe(false); 42 | expect(evalFormual('1 <= 1')).toBe(true); 43 | expect(evalFormual('1 > 1')).toBe(false); 44 | expect(evalFormual('1 >= 1')).toBe(true); 45 | expect(evalFormual('3 >> 1')).toBe(1); 46 | expect(evalFormual('3 << 1')).toBe(6); 47 | expect(evalFormual('10 ** 3')).toBe(1000); 48 | 49 | expect(evalFormual('10 ? 3 : 2')).toBe(3); 50 | expect(evalFormual('0 ? 3 : 2')).toBe(2); 51 | }); 52 | 53 | test('formula:expression2', () => { 54 | expect(evalFormual('a[0]', {a: [1, 2, 3]})).toBe(1); 55 | expect(evalFormual('a[b]', {a: [1, 2, 3], b: 1})).toBe(2); 56 | expect(evalFormual('a[b - 1]', {a: [1, 2, 3], b: 1})).toBe(1); 57 | expect(evalFormual('a[b ? 1 : 2]', {a: [1, 2, 3], b: 1})).toBe(2); 58 | expect(evalFormual('a[c ? 1 : 2]', {a: [1, 2, 3], b: 1})).toBe(3); 59 | }); 60 | 61 | test('formula:if', () => { 62 | expect(evalFormual('IF(true, 2, 3)')).toBe(2); 63 | expect(evalFormual('IF(false, 2, 3)')).toBe(3); 64 | expect(evalFormual('IF(false, 2, IF(true, 3, 4))')).toBe(3); 65 | }); 66 | 67 | test('formula:and', () => { 68 | expect(!!evalFormual('AND(0, 1)')).toBe(false); 69 | expect(!!evalFormual('AND(1, 1)')).toBe(true); 70 | expect(!!evalFormual('AND(1, 1, 1, 0)')).toBe(false); 71 | }); 72 | 73 | test('formula:or', () => { 74 | expect(!!evalFormual('OR(0, 1)')).toBe(true); 75 | expect(!!evalFormual('OR(1, 1)')).toBe(true); 76 | expect(!!evalFormual('OR(1, 1, 1, 0)')).toBe(true); 77 | expect(!!evalFormual('OR(0, 0, 0, 0)')).toBe(false); 78 | }); 79 | 80 | test('formula:xor', () => { 81 | expect(evalFormual('XOR(0, 1)')).toBe(false); 82 | expect(evalFormual('XOR(1, 0)')).toBe(false); 83 | expect(evalFormual('XOR(1, 1)')).toBe(true); 84 | expect(evalFormual('XOR(0, 0)')).toBe(true); 85 | }); 86 | 87 | test('formula:ifs', () => { 88 | expect(!!evalFormual('IFS(0, 1, 2)')).toBe(true); 89 | expect(!!evalFormual('IFS(0, 1, 2, 2, 3)')).toBe(true); 90 | expect(!!evalFormual('IFS(0, 1, 0, 2, 0)')).toBe(false); 91 | }); 92 | test('formula:math', () => { 93 | expect(evalFormual('ABS(1)')).toBe(1); 94 | expect(evalFormual('ABS(-1)')).toBe(1); 95 | expect(evalFormual('ABS(0)')).toBe(0); 96 | 97 | expect(evalFormual('MAX(1, -1, 2, 3, 5, -9)')).toBe(5); 98 | expect(evalFormual('MIN(1, -1, 2, 3, 5, -9)')).toBe(-9); 99 | 100 | expect(evalFormual('MOD(3, 2)')).toBe(1); 101 | 102 | expect(evalFormual('PI()')).toBe(Math.PI); 103 | 104 | expect(evalFormual('ROUND(3.5)')).toBe(4); 105 | expect(evalFormual('ROUND(3.4)')).toBe(3); 106 | 107 | expect(evalFormual('ROUND(3.456789, 2)')).toBe(3.46); 108 | expect(evalFormual('CEIL(3.456789)')).toBe(4); 109 | expect(evalFormual('FLOOR(3.456789)')).toBe(3); 110 | 111 | expect(evalFormual('SQRT(4)')).toBe(2); 112 | expect(evalFormual('AVG(4, 6, 10, 10, 10)')).toBe(8); 113 | 114 | // 示例来自 https://support.microsoft.com/zh-cn/office/devsq-%E5%87%BD%E6%95%B0-8b739616-8376-4df5-8bd0-cfe0a6caf444 115 | expect(evalFormual('DEVSQ(4,5,8,7,11,4,3)')).toBe(48); 116 | // 示例来自 https://support.microsoft.com/zh-cn/office/avedev-%E5%87%BD%E6%95%B0-58fe8d65-2a84-4dc7-8052-f3f87b5c6639 117 | expect(evalFormual('ROUND(AVEDEV(4,5,6,7,5,4,3), 2)')).toBe(1.02); 118 | // 示例来自 https://support.microsoft.com/zh-cn/office/harmean-%E5%87%BD%E6%95%B0-5efd9184-fab5-42f9-b1d3-57883a1d3bc6 119 | expect(evalFormual('ROUND(HARMEAN(4,5,8,7,11,4,3), 3)')).toBe(5.028); 120 | 121 | expect(evalFormual('LARGE([1,3,5,4,7,6], 3)')).toBe(5); 122 | expect(evalFormual('LARGE([1,3,5,4,7,6], 1)')).toBe(7); 123 | 124 | expect(evalFormual('UPPERMONEY(7682.01)')).toBe('柒仟陆佰捌拾贰元壹分'); 125 | expect(evalFormual('UPPERMONEY(7682)')).toBe('柒仟陆佰捌拾贰元整'); 126 | 127 | // 非数字类型转换是否正常? 128 | expect(evalFormual('"3" + "3"')).toBe(6); 129 | expect(evalFormual('"3" - "3"')).toBe(0); 130 | expect(evalFormual('AVG(4, "6", "10", 10, 10)')).toBe(8); 131 | }); 132 | 133 | test('formula:text', () => { 134 | expect(evalFormual('LEFT("abcdefg", 2)')).toBe('ab'); 135 | expect(evalFormual('RIGHT("abcdefg", 2)')).toBe('fg'); 136 | expect(evalFormual('LENGTH("abcdefg")')).toBe(7); 137 | expect(evalFormual('LEN("abcdefg")')).toBe(7); 138 | expect(evalFormual('ISEMPTY("abcdefg")')).toBe(false); 139 | expect(evalFormual('ISEMPTY("")')).toBe(true); 140 | expect(evalFormual('CONCATENATE("a", "b", "c", "d")')).toBe('abcd'); 141 | expect(evalFormual('CHAR(97)')).toBe('a'); 142 | expect(evalFormual('LOWER("AB")')).toBe('ab'); 143 | expect(evalFormual('UPPER("ab")')).toBe('AB'); 144 | expect(evalFormual('SPLIT("a,b,c")')).toMatchObject(['a', 'b', 'c']); 145 | expect(evalFormual('TRIM(" ab ")')).toBe('ab'); 146 | expect(evalFormual('STARTSWITH("xab", "ab")')).toBe(false); 147 | expect(evalFormual('STARTSWITH("xab", "x")')).toBe(true); 148 | expect(evalFormual('ENDSWITH("xab", "x")')).toBe(false); 149 | expect(evalFormual('ENDSWITH("xab", "b")')).toBe(true); 150 | expect(evalFormual('UPPERFIRST("xab")')).toBe('Xab'); 151 | expect(evalFormual('PADSTART("5", 3, "0")')).toBe('005'); 152 | expect(evalFormual('PADSTART(5, 3, 0)')).toBe('005'); 153 | expect(evalFormual('CAPITALIZE("star")')).toBe('Star'); 154 | expect(evalFormual('ESCAPE("&")')).toBe('&'); 155 | expect(evalFormual('TRUNCATE("amis.baidu.com", 7)')).toBe('amis...'); 156 | expect(evalFormual('BEFORELAST("amis.baidu.com", ".")')).toBe('amis.baidu'); 157 | expect(evalFormual('BEFORELAST("amis", ".")')).toBe('amis'); 158 | expect(evalFormual('STRIPTAG("amis")')).toBe('amis'); 159 | expect(evalFormual('LINEBREAK("am\nis")')).toBe('am
is'); 160 | expect(evalFormual('CONTAINS("xab", "x")')).toBe(true); 161 | expect(evalFormual('CONTAINS("xab", "b")')).toBe(true); 162 | expect(evalFormual('REPLACE("xabab", "ab", "cd")')).toBe('xcdcd'); 163 | expect(evalFormual('SEARCH("xabab", "ab")')).toBe(1); 164 | expect(evalFormual('SEARCH("xabab", "cd")')).toBe(-1); 165 | expect(evalFormual('SEARCH("xabab", "ab", 2)')).toBe(3); 166 | expect(evalFormual('MID("xabab", 2, 2)')).toBe('ba'); 167 | }); 168 | 169 | test('formula:date', () => { 170 | expect(evalFormual('TIMESTAMP(DATE(2021, 11, 21, 0, 0, 0), "x")')).toBe( 171 | new Date(2021, 11, 21, 0, 0, 0).getTime() 172 | ); 173 | expect( 174 | evalFormual('DATETOSTR(DATE(2021, 11, 21, 0, 0, 0), "YYYY-MM-DD")') 175 | ).toBe('2021-12-21'); 176 | expect(evalFormual('DATETOSTR(DATE("2021-12-21"), "YYYY-MM-DD")')).toBe( 177 | '2021-12-21' 178 | ); 179 | expect(evalFormual('DATETOSTR(TODAY(), "YYYY-MM-DD")')).toBe( 180 | moment().format('YYYY-MM-DD') 181 | ); 182 | expect(evalFormual('DATETOSTR(NOW(), "YYYY-MM-DD")')).toBe( 183 | moment().format('YYYY-MM-DD') 184 | ); 185 | expect(evalFormual('YEAR(STRTODATE("2021-10-24 10:10:10"))')).toBe(2021); 186 | }); 187 | 188 | test('formula:last', () => { 189 | expect(evalFormual('LAST([1, 2, 3])')).toBe(3); 190 | }); 191 | 192 | test('formula:basename', () => { 193 | expect(evalFormual('BASENAME("/home/amis/a.json")')).toBe('a.json'); 194 | }); 195 | -------------------------------------------------------------------------------- /__tests__/jest.setup.js: -------------------------------------------------------------------------------- 1 | const originalWarn = console.warn.bind(console.warn); 2 | 3 | require('moment-timezone'); 4 | const moment = require('moment'); 5 | moment.tz.setDefault('Asia/Shanghai'); 6 | 7 | global.beforeAll(() => { 8 | console.warn = msg => { 9 | // warning 先关了,实在太吵。 10 | // const str = msg.toString(); 11 | // if ( 12 | // str.includes('componentWillMount') || 13 | // str.includes('componentWillReceiveProps') 14 | // ) { 15 | // return; 16 | // } 17 | // originalWarn(msg); 18 | }; 19 | }); 20 | global.afterAll(() => { 21 | console.warn = originalWarn; 22 | }); 23 | -------------------------------------------------------------------------------- /__tests__/lexer.test.ts: -------------------------------------------------------------------------------- 1 | import {lexer as createLexer, parse} from '../src'; 2 | 3 | function getTokens(input: string, options?: any) { 4 | const lexer = createLexer(input, options); 5 | const tokens: Array = []; 6 | 7 | while (true) { 8 | const token = lexer.next(); 9 | if (token) { 10 | tokens.push(token); 11 | 12 | if (token.type === 'EOF') { 13 | break; 14 | } 15 | } else { 16 | break; 17 | } 18 | } 19 | 20 | return tokens.map(token => `<${token.type}> ${token.value}`); 21 | } 22 | 23 | test('lexer:simple', () => { 24 | expect( 25 | getTokens('expression result is ${a + b}', { 26 | evalMode: false 27 | }) 28 | ).toMatchSnapshot(); 29 | }); 30 | 31 | test('lexer:filter', () => { 32 | expect( 33 | getTokens('\\$abc is ${abc | date: YYYY-MM-DD HH\\:mm\\:ss}', { 34 | evalMode: false 35 | }) 36 | ).toMatchSnapshot(); 37 | }); 38 | 39 | // test('lexer:test', () => { 40 | // console.log(getTokens("{a: 1, 'b': 2, `c`: 3, d: {}}", {evalMode: true})); 41 | // }); 42 | 43 | test('lexer:exception', () => { 44 | expect(() => 45 | getTokens('\\aabc is ', { 46 | evalMode: false 47 | }) 48 | ).toThrow('Unexpected token a in 1:3'); 49 | 50 | expect(() => 51 | getTokens('${a | filter: \\x2}', { 52 | evalMode: false 53 | }) 54 | ).toThrow('Unexpected token x in 1:17'); 55 | }); 56 | -------------------------------------------------------------------------------- /__tests__/parser.test.ts: -------------------------------------------------------------------------------- 1 | import {parse} from '../src/index'; 2 | test('parser:simple', () => { 3 | expect( 4 | parse('expression result is ${a + b}', { 5 | evalMode: false 6 | }) 7 | ).toMatchSnapshot(); 8 | }); 9 | 10 | test('parser:complex', () => { 11 | expect( 12 | parse('raw content ${`es tempalte ${`deeper${a + 3}`}`}', { 13 | evalMode: false 14 | }) 15 | ).toMatchSnapshot(); 16 | }); 17 | 18 | test('parser:evalMode', () => { 19 | expect( 20 | parse('a + b', { 21 | evalMode: true 22 | }) 23 | ).toMatchSnapshot(); 24 | }); 25 | 26 | test('parser:template', () => { 27 | expect( 28 | parse('`abc${a + b}`', { 29 | evalMode: true 30 | }) 31 | ).toMatchSnapshot(); 32 | }); 33 | 34 | test('parser:string', () => { 35 | expect( 36 | parse('"string literall, escape \\""', { 37 | evalMode: true 38 | }) 39 | ).toMatchSnapshot(); 40 | }); 41 | 42 | test('parser:number', () => { 43 | expect( 44 | parse('-1 + 2.5 + 3', { 45 | evalMode: true 46 | }) 47 | ).toMatchSnapshot(); 48 | }); 49 | 50 | test('parser:single-string', () => { 51 | expect( 52 | parse("'string'", { 53 | evalMode: true 54 | }) 55 | ).toMatchSnapshot(); 56 | }); 57 | 58 | test('parser:object-literall', () => { 59 | expect( 60 | parse("{a: 1, 'b': 2, [`c`]: 3, d: {}}", { 61 | evalMode: true 62 | }) 63 | ).toMatchSnapshot(); 64 | }); 65 | 66 | test('parser:array-literall', () => { 67 | expect( 68 | parse('[a, b, 1, 2, {a: 1}]', { 69 | evalMode: true 70 | }) 71 | ).toMatchSnapshot(); 72 | }); 73 | 74 | test('parser:variable-geter', () => { 75 | expect( 76 | parse('doAction(a.b, a[b], a["c"], a[`d`])', { 77 | evalMode: true 78 | }) 79 | ).toMatchSnapshot(); 80 | }); 81 | 82 | test('parser:variable-geter2', () => { 83 | expect( 84 | parse('a[b]["c"][d][`x`]', { 85 | evalMode: true 86 | }) 87 | ).toMatchSnapshot(); 88 | 89 | expect( 90 | parse('a[b]["c"].d[`x`]', { 91 | evalMode: true 92 | }) 93 | ).toMatchSnapshot(); 94 | }); 95 | test('parser:multi-expression', () => { 96 | expect( 97 | parse('(a.b, a[b], a["c"], a[`d`])', { 98 | evalMode: true 99 | }) 100 | ).toMatchSnapshot(); 101 | }); 102 | 103 | test('parser:functionCall', () => { 104 | expect( 105 | parse('doAction(a, doAction(b))', { 106 | evalMode: true 107 | }) 108 | ).toMatchSnapshot(); 109 | }); 110 | 111 | test('parser:filter', () => { 112 | expect( 113 | parse('\\$abc is ${abc | html}', { 114 | evalMode: false 115 | }) 116 | ).toMatchSnapshot(); 117 | }); 118 | 119 | test('parser:filter-escape', () => { 120 | expect( 121 | parse('\\$abc is ${abc | date: YYYY-MM-DD HH\\:mm\\:ss}', { 122 | evalMode: false 123 | }) 124 | ).toMatchSnapshot(); 125 | }); 126 | 127 | test('parser:conditional', () => { 128 | expect( 129 | parse('a ? b : c', { 130 | evalMode: true 131 | }) 132 | ).toMatchSnapshot(); 133 | }); 134 | 135 | test('parser:binary-expression', () => { 136 | expect( 137 | parse('a && b && c', { 138 | evalMode: true 139 | }) 140 | ).toMatchSnapshot(); 141 | 142 | expect( 143 | parse('a && b || c', { 144 | evalMode: true 145 | }) 146 | ).toMatchSnapshot(); 147 | 148 | expect( 149 | parse('a || b && c', { 150 | evalMode: true 151 | }) 152 | ).toMatchSnapshot(); 153 | 154 | expect( 155 | parse('a !== b === c', { 156 | evalMode: true 157 | }) 158 | ).toMatchSnapshot(); 159 | }); 160 | 161 | test('parser:group-expression', () => { 162 | expect( 163 | parse('a && (b && c)', { 164 | evalMode: true 165 | }) 166 | ).toMatchSnapshot(); 167 | }); 168 | 169 | test('parser:unary-expression', () => { 170 | expect( 171 | parse('!!a', { 172 | evalMode: true 173 | }) 174 | ).toMatchSnapshot(); 175 | }); 176 | 177 | test('parser:anonymous-function', () => { 178 | expect( 179 | parse('() => 1', { 180 | evalMode: true 181 | }) 182 | ).toMatchSnapshot(); 183 | 184 | expect( 185 | parse('() => "string"', { 186 | evalMode: true 187 | }) 188 | ).toMatchSnapshot(); 189 | 190 | expect( 191 | parse('(a) => `${a.a}---${a.b}`', { 192 | evalMode: true 193 | }) 194 | ).toMatchSnapshot(); 195 | }); 196 | 197 | // test('parser:test', () => { 198 | // console.log(JSON.stringify(parse('ARRAYMAP(arr, (item) => item.abc)', { 199 | // evalMode: true 200 | // }), null, 2)); 201 | // }); 202 | -------------------------------------------------------------------------------- /__tests__/tokenize.test.ts: -------------------------------------------------------------------------------- 1 | import {tokenize} from '../src'; 2 | 3 | test(`tokenize:null`, () => { 4 | expect( 5 | tokenize('abc${a}', { 6 | a: '' 7 | }) 8 | ).toBe('abc'); 9 | expect( 10 | tokenize('abc${a}', { 11 | a: null 12 | }) 13 | ).toBe('abc'); 14 | 15 | expect( 16 | tokenize('abc${a}', { 17 | a: undefined 18 | }) 19 | ).toBe('abc'); 20 | 21 | expect( 22 | tokenize('abc${a}', { 23 | a: 0 24 | }) 25 | ).toBe('abc0'); 26 | }); 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amis-formula", 3 | "version": "1.3.15", 4 | "description": "负责 amis 里面的表达式实现,内置公式,编辑器等", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "npm run clean-dist && NODE_ENV=production rollup -c && npm run declaration && npm run genDoc", 8 | "lib": "npm run clean-dist && NODE_ENV=lib rollup -c", 9 | "clean-dist": "rimraf dist/*", 10 | "declaration": "tsc --allowJs --declaration --emitDeclarationOnly --declarationDir ./dist --rootDir ./src", 11 | "test": "jest", 12 | "coverage": "jest --coverage", 13 | "genDoc": "ts-node ./scripts/genDoc.ts" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/aisuda/amis-tpl.git" 18 | }, 19 | "keywords": [ 20 | "amis", 21 | "tpl", 22 | "parser", 23 | "formula" 24 | ], 25 | "author": "fex", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/aisuda/amis-tpl/issues" 29 | }, 30 | "homepage": "https://github.com/aisuda/amis-tpl#readme", 31 | "dependencies": { 32 | "lodash": "^4.17.15", 33 | "moment": "^2.29.1", 34 | "tslib": "^2.3.1" 35 | }, 36 | "devDependencies": { 37 | "@types/doctrine": "0.0.5", 38 | "@types/jest": "^27.0.2", 39 | "@types/lodash": "^4.14.175", 40 | "doctrine": "^3.0.0", 41 | "jest": "^27.2.1", 42 | "jest-canvas-mock": "^2.3.0", 43 | "mini-css-extract-plugin": "^2.4.5", 44 | "moment-timezone": "^0.5.33", 45 | "rimraf": "^3.0.2", 46 | "rollup": "^2.60.2", 47 | "rollup-plugin-commonjs": "^10.1.0", 48 | "rollup-plugin-json": "^4.0.0", 49 | "rollup-plugin-license": "^2.6.0", 50 | "rollup-plugin-node-resolve": "^5.2.0", 51 | "rollup-plugin-terser": "^7.0.2", 52 | "rollup-plugin-typescript": "^1.0.1", 53 | "sass": "^1.36.0", 54 | "sass-loader": "^12.1.0", 55 | "style-loader": "^3.2.1", 56 | "stylelint": "^13.0.0", 57 | "ts-jest": "^27.0.5", 58 | "ts-loader": "^9.2.3", 59 | "ts-node": "^10.4.0", 60 | "typescript": "^4.3.5" 61 | }, 62 | "browserslist": "IE >= 11", 63 | "jest": { 64 | "testEnvironment": "jsdom", 65 | "collectCoverageFrom": [ 66 | "src/**/*" 67 | ], 68 | "moduleFileExtensions": [ 69 | "ts", 70 | "tsx", 71 | "js" 72 | ], 73 | "transform": { 74 | "\\.(ts|tsx)$": "ts-jest" 75 | }, 76 | "setupFiles": [ 77 | "jest-canvas-mock" 78 | ], 79 | "testRegex": "/.*\\.test\\.(ts|tsx|js)$", 80 | "moduleNameMapper": { 81 | "\\.(css|less|sass|scss)$": "/__mocks__/styleMock.js", 82 | "\\.(svg)$": "/__mocks__/svgMock.js" 83 | }, 84 | "setupFilesAfterEnv": [ 85 | "/__tests__/jest.setup.js" 86 | ], 87 | "globals": { 88 | "ts-jest": { 89 | "diagnostics": false, 90 | "tsconfig": { 91 | "module": "commonjs", 92 | "target": "es5", 93 | "lib": [ 94 | "es6", 95 | "dom", 96 | "ES2015" 97 | ], 98 | "sourceMap": true, 99 | "jsx": "react", 100 | "moduleResolution": "node", 101 | "rootDir": ".", 102 | "importHelpers": true, 103 | "esModuleInterop": true, 104 | "allowSyntheticDefaultImports": true, 105 | "sourceRoot": ".", 106 | "noImplicitReturns": true, 107 | "noImplicitThis": true, 108 | "noImplicitAny": true, 109 | "strictNullChecks": true, 110 | "experimentalDecorators": true, 111 | "emitDecoratorMetadata": false, 112 | "typeRoots": [ 113 | "./node_modules/@types", 114 | "./types" 115 | ], 116 | "skipLibCheck": true 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import json from 'rollup-plugin-json'; 4 | import resolve from 'rollup-plugin-node-resolve'; 5 | import typescript from 'rollup-plugin-typescript'; 6 | import {terser} from 'rollup-plugin-terser'; 7 | import license from 'rollup-plugin-license'; 8 | import {name, version, main, module, browser, author} from './package.json'; 9 | 10 | const isProduction = process.env.NODE_ENV === 'production'; 11 | const isForLib = process.env.NODE_ENV === 'lib'; 12 | 13 | 14 | const settings = { 15 | globals: { 16 | lodash: 'lodash', 17 | moment: 'moment', 18 | tslib: 'tslib' 19 | } 20 | }; 21 | 22 | export default { 23 | input: isForLib ? './scripts/lib.ts' : './src/index.ts', 24 | output: [ 25 | { 26 | file: isForLib ? 'dist/formula.js' : main, 27 | name: isForLib ? 'formula' : main, 28 | ...settings, 29 | format: isForLib ? 'iife' : 'cjs', 30 | plugins: [ 31 | isForLib && terser() 32 | ], 33 | strict: !isForLib, 34 | footer: isForLib ? `var evaluate = formula.evaluate; 35 | var momentFormat = formula.momentFormat; 36 | var parse = formula.parse;` : '', 37 | } 38 | // { 39 | // file: module, 40 | // ...settings, 41 | // name: name, 42 | // format: 'es' 43 | // }, 44 | // { 45 | // file: browser, 46 | // ...settings, 47 | // name: name, 48 | // format: 'umd' 49 | // } 50 | ], 51 | external: isForLib ? [] : [ 52 | 'lodash', 53 | 'lodash/transform', 54 | 'lodash/groupBy', 55 | 'lodash/uniqBy', 56 | 'lodash/uniq', 57 | 'lodash/isPlainObject', 58 | 'lodash/padStart', 59 | 'lodash/upperFirst', 60 | 'lodash/capitalize', 61 | 'lodash/escape', 62 | 'lodash/truncate', 63 | 'moment', 64 | 'tslib' 65 | ], 66 | 67 | plugins: [ 68 | json(), 69 | resolve({ 70 | jsnext: true, 71 | main: true, 72 | browser: true 73 | }), 74 | typescript({ 75 | typescript: require('typescript') 76 | }), 77 | commonjs({ 78 | include: 'node_modules/**', 79 | extensions: ['.js'], 80 | ignoreGlobal: false, 81 | sourceMap: false 82 | }), 83 | license({ 84 | banner: ` 85 | ${name} v${version} 86 | Copyright 2021<%= moment().format('YYYY') > 2021 ? '-' + moment().format('YYYY') : null %> ${author} 87 | ` 88 | }) 89 | ] 90 | }; 91 | -------------------------------------------------------------------------------- /scripts/genDoc.ts: -------------------------------------------------------------------------------- 1 | import path = require('path'); 2 | import fs = require('fs'); 3 | import doctrine = require('doctrine'); 4 | 5 | const workDir = path.resolve(path.dirname(__dirname)); 6 | const jsFile = path.join(workDir, 'src/evalutor.ts'); 7 | const outputFile = path.join(workDir, 'dist/doc.js'); 8 | const outputMD = path.join(workDir, 'dist/doc.md'); 9 | 10 | function getFormulaComments(contents: string) { 11 | const comments: Array<{ 12 | fnName: string; 13 | comments: string; 14 | }> = []; 15 | 16 | contents.replace(/\/\*[\s\S]+?\*\//g, (_, index, input) => { 17 | const pos = index + _.length; 18 | const following = input.substring(pos, pos + 200); 19 | 20 | if (/^\s*fn(\w+)\(/.test(following)) { 21 | comments.push({ 22 | fnName: RegExp.$1, 23 | comments: _ 24 | }); 25 | } 26 | 27 | return _; 28 | }); 29 | 30 | return comments; 31 | } 32 | 33 | function formatType(tag: any): string { 34 | // console.log(tag); 35 | if (tag.type === 'RestType') { 36 | return `...${formatType(tag.expression)}`; 37 | } else if (tag.type === 'TypeApplication') { 38 | return `Array<${tag.applications 39 | .map((item: any) => formatType(item)) 40 | .join(',')}>`; 41 | } 42 | 43 | return tag.name; 44 | } 45 | 46 | async function main(...params: Array) { 47 | const contents = fs.readFileSync(jsFile, 'utf8'); 48 | 49 | const comments = getFormulaComments(contents); 50 | const result = comments.map(item => { 51 | const ast = doctrine.parse(item.comments, { 52 | unwrap: true 53 | }); 54 | const result: any = { 55 | name: item.fnName, 56 | description: ast.description 57 | }; 58 | 59 | let example = ''; 60 | let params: Array = []; 61 | let returns: any = undefined; 62 | let namespace = ''; 63 | ast.tags.forEach(tag => { 64 | if (tag.title === 'example') { 65 | example = tag.description!; 66 | } else if (tag.title === 'namespace') { 67 | namespace = tag.name!; 68 | } else if (tag.title === 'param') { 69 | params.push({ 70 | type: formatType(tag.type), 71 | name: tag.name, 72 | description: tag.description 73 | }); 74 | } else if (tag.title === 'returns') { 75 | returns = { 76 | type: formatType(tag.type), 77 | description: tag.description 78 | }; 79 | } 80 | }); 81 | 82 | result.example = example; 83 | result.params = params; 84 | result.returns = returns; 85 | result.namespace = namespace; 86 | 87 | return result; 88 | }); 89 | 90 | fs.writeFileSync( 91 | outputFile, 92 | `/**\n * 公式文档\n */\nexports.doc = ${JSON.stringify( 93 | result, 94 | null, 95 | 2 96 | )}`.replace(/\"(\w+)\"\:/g, (_, key) => `${key}:`), 97 | 'utf8' 98 | ); 99 | console.log(`公式文档生成 > ${outputFile}`); 100 | fs.writeFileSync( 101 | outputFile.replace(/\.js$/, '.d.ts'), 102 | // 可以通过以下命令生成 103 | // tsc --declaration --emitDeclarationOnly --allowJs doc.js 104 | [ 105 | `export var doc: {`, 106 | ` name: string;`, 107 | ` description: string;`, 108 | ` example: string;`, 109 | ` params: {`, 110 | ` type: string;`, 111 | ` name: string;`, 112 | ` description: string;`, 113 | ` }[];`, 114 | ` returns: {`, 115 | ` type: string;`, 116 | ` description: string;`, 117 | ` };`, 118 | ` namespace: string;`, 119 | `}[];` 120 | ].join('\n'), 121 | 'utf8' 122 | ); 123 | 124 | const grouped: any = {}; 125 | result.forEach((item: any) => { 126 | const scope = item.namespace || '其他'; 127 | const arr = grouped[scope] || (grouped[scope] = []); 128 | 129 | arr.push(item); 130 | }); 131 | 132 | let md = ''; 133 | Object.keys(grouped).forEach(key => { 134 | md += `## ${key}\n\n`; 135 | 136 | grouped[key].forEach((item: any) => { 137 | md += `### ${item.name}\n\n`; 138 | 139 | md += `用法:\`${item.example}\`\n\n`; 140 | 141 | if (item.params.length) { 142 | // md += `参数:\n`; 143 | 144 | item.params.forEach((param: any) => { 145 | md += ` * \`${param.name}:${param.type}\` ${param.description}\n`; 146 | }); 147 | 148 | if (item.returns) { 149 | md += `\n返回:\`${item.returns.type}\` ${ 150 | item.returns.description || '' 151 | }\n\n`; 152 | } 153 | } 154 | 155 | md += `${item.description}\n\n`; 156 | }); 157 | }); 158 | fs.writeFileSync(outputMD, md, 'utf8'); 159 | console.log(`公式md生成 > ${outputMD}`); 160 | } 161 | 162 | main().catch(e => console.error(e)); 163 | -------------------------------------------------------------------------------- /scripts/lib.ts: -------------------------------------------------------------------------------- 1 | import {parse, evaluate} from '../src'; 2 | import moment from 'moment'; 3 | 4 | // 来自 https://vanillajstoolkit.com/polyfills/arrayfind/ 5 | if (!Array.prototype.find) { 6 | Array.prototype.find = function (callback) { 7 | // 1. Let O be ? ToObject(this value). 8 | if (this == null) { 9 | throw new TypeError('"this" is null or not defined'); 10 | } 11 | 12 | var o = Object(this); 13 | 14 | // 2. Let len be ? ToLength(? Get(O, "length")). 15 | var len = o.length >>> 0; 16 | 17 | // 3. If IsCallable(callback) is false, throw a TypeError exception. 18 | if (typeof callback !== 'function') { 19 | throw new TypeError('callback must be a function'); 20 | } 21 | 22 | // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. 23 | var thisArg = arguments[1]; 24 | 25 | // 5. Let k be 0. 26 | var k = 0; 27 | 28 | // 6. Repeat, while k < len 29 | while (k < len) { 30 | // a. Let Pk be ! ToString(k). 31 | // b. Let kValue be ? Get(O, Pk). 32 | // c. Let testResult be ToBoolean(? Call(callback, T, « kValue, k, O »)). 33 | // d. If testResult is true, return kValue. 34 | var kValue = o[k]; 35 | if (callback.call(thisArg, kValue, k, o)) { 36 | return kValue; 37 | } 38 | // e. Increase k by 1. 39 | k++; 40 | } 41 | 42 | // 7. Return undefined. 43 | return undefined; 44 | }; 45 | } 46 | 47 | export function momentFormat( 48 | input: any, 49 | inputFormat: string, 50 | outputFormat: string 51 | ) { 52 | return moment(input, inputFormat).format(outputFormat); 53 | } 54 | 55 | export {parse, evaluate, moment}; 56 | -------------------------------------------------------------------------------- /src/evalutor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 公式内置函数 3 | */ 4 | 5 | import moment from 'moment'; 6 | import upperFirst from 'lodash/upperFirst'; 7 | import padStart from 'lodash/padStart'; 8 | import capitalize from 'lodash/capitalize'; 9 | import escape from 'lodash/escape'; 10 | import truncate from 'lodash/truncate'; 11 | import {createObject, stripNumber} from './util'; 12 | 13 | export interface FilterMap { 14 | [propName: string]: (this: FilterContext, input: any, ...args: any[]) => any; 15 | } 16 | 17 | export interface FunctionMap { 18 | [propName: string]: (this: Evaluator, ast: Object, data: any) => any; 19 | } 20 | 21 | export interface FilterContext { 22 | data: Object; 23 | filter?: { 24 | name: string; 25 | args: Array; 26 | }; 27 | restFilters: Array<{ 28 | name: string; 29 | args: Array; 30 | }>; 31 | } 32 | 33 | export interface EvaluatorOptions { 34 | /** 35 | * 可以外部传入 ast 节点处理器,定制或者扩充自定义函数 36 | */ 37 | functions?: FunctionMap; 38 | 39 | /** 40 | * 可以外部扩充 filter 41 | */ 42 | filters?: FilterMap; 43 | 44 | defaultFilter?: string; 45 | } 46 | 47 | export class Evaluator { 48 | readonly filters: FilterMap; 49 | readonly functions: FunctionMap = {}; 50 | readonly context: { 51 | [propName: string]: any; 52 | }; 53 | contextStack: Array<(varname: string) => any> = []; 54 | 55 | static defaultFilters: FilterMap = {}; 56 | static setDefaultFilters(filters: FilterMap) { 57 | Evaluator.defaultFilters = { 58 | ...Evaluator.defaultFilters, 59 | ...filters 60 | }; 61 | } 62 | 63 | constructor( 64 | context: { 65 | [propName: string]: any; 66 | }, 67 | readonly options: EvaluatorOptions = { 68 | defaultFilter: 'html' 69 | } 70 | ) { 71 | this.context = context; 72 | this.contextStack.push((varname: string) => 73 | varname === '&' ? context : context?.[varname] 74 | ); 75 | 76 | this.filters = { 77 | ...Evaluator.defaultFilters, 78 | ...this.filters, 79 | ...options?.filters 80 | }; 81 | this.functions = { 82 | ...this.functions, 83 | ...options?.functions 84 | }; 85 | } 86 | 87 | // 主入口 88 | evalute(ast: any) { 89 | if (ast && ast.type) { 90 | const name = (ast.type as string).replace(/(?:_|\-)(\w)/g, (_, l) => 91 | l.toUpperCase() 92 | ); 93 | const fn = this.functions[name] || (this as any)[name]; 94 | 95 | if (!fn) { 96 | throw new Error(`${ast.type} unkown.`); 97 | } 98 | 99 | return fn.call(this, ast); 100 | } else { 101 | return ast; 102 | } 103 | } 104 | 105 | document(ast: {type: 'document'; body: Array}) { 106 | if (!ast.body.length) { 107 | return undefined; 108 | } 109 | const isString = ast.body.length > 1; 110 | const content = ast.body.map(item => { 111 | let result = this.evalute(item); 112 | 113 | if (isString && result == null) { 114 | // 不要出现 undefined, null 之类的文案 115 | return ''; 116 | } 117 | 118 | return result; 119 | }); 120 | return content.length === 1 ? content[0] : content.join(''); 121 | } 122 | 123 | filter(ast: { 124 | type: 'filter'; 125 | input: any; 126 | filters: Array<{name: string; args: Array}>; 127 | }) { 128 | let input = this.evalute(ast.input); 129 | const filters = ast.filters.concat(); 130 | const context: FilterContext = { 131 | filter: undefined, 132 | data: this.context, 133 | restFilters: filters 134 | }; 135 | 136 | while (filters.length) { 137 | const filter = filters.shift()!; 138 | const fn = this.filters[filter.name]; 139 | if (!fn) { 140 | throw new Error(`filter \`${filter.name}\` not exits`); 141 | } 142 | context.filter = filter; 143 | input = fn.apply( 144 | context, 145 | [input].concat( 146 | filter.args.map((item: any) => { 147 | if (item?.type === 'mixed') { 148 | return item.body 149 | .map((item: any) => 150 | typeof item === 'string' ? item : this.evalute(item) 151 | ) 152 | .join(''); 153 | } else if (item.type) { 154 | return this.evalute(item); 155 | } 156 | return item; 157 | }) 158 | ) 159 | ); 160 | } 161 | return input; 162 | } 163 | 164 | raw(ast: {type: 'raw'; value: string}) { 165 | return ast.value; 166 | } 167 | 168 | script(ast: {type: 'script'; body: any}) { 169 | const defaultFilter = this.options.defaultFilter; 170 | 171 | // 只给简单的变量取值用法自动补fitler 172 | if (defaultFilter && ~['getter', 'variable'].indexOf(ast.body?.type)) { 173 | ast.body = { 174 | type: 'filter', 175 | input: ast.body, 176 | filters: [ 177 | { 178 | name: defaultFilter.replace(/^\s*\|\s*/, ''), 179 | args: [] 180 | } 181 | ] 182 | }; 183 | } 184 | 185 | return this.evalute(ast.body); 186 | } 187 | 188 | expressionList(ast: {type: 'expression-list'; body: Array}) { 189 | return ast.body.reduce((prev, current) => this.evalute(current)); 190 | } 191 | 192 | template(ast: {type: 'template'; body: Array}) { 193 | return ast.body.map(arg => this.evalute(arg)).join(''); 194 | } 195 | 196 | templateRaw(ast: {type: 'template_raw'; value: any}) { 197 | return ast.value; 198 | } 199 | 200 | // 下标获取 201 | getter(ast: {host: any; key: any}) { 202 | const host = this.evalute(ast.host); 203 | let key = this.evalute(ast.key); 204 | if (typeof key === 'undefined' && ast.key?.type === 'variable') { 205 | key = ast.key.name; 206 | } 207 | return host?.[key]; 208 | } 209 | 210 | // 位操作如 +2 ~3 ! 211 | unary(ast: {op: '+' | '-' | '~' | '!'; value: any}) { 212 | let value = this.evalute(ast.value); 213 | 214 | switch (ast.op) { 215 | case '+': 216 | return +value; 217 | case '-': 218 | return -value; 219 | case '~': 220 | return ~value; 221 | case '!': 222 | return !value; 223 | } 224 | } 225 | 226 | formatNumber(value: any, int = false) { 227 | const typeName = typeof value; 228 | if (typeName === 'string') { 229 | return (int ? parseInt(value, 10) : parseFloat(value)) || 0; 230 | } else if (typeName === 'number' && int) { 231 | return Math.round(value); 232 | } 233 | 234 | return value ?? 0; 235 | } 236 | 237 | power(ast: {left: any; right: any}) { 238 | const left = this.evalute(ast.left); 239 | const right = this.evalute(ast.right); 240 | return Math.pow(this.formatNumber(left), this.formatNumber(right)); 241 | } 242 | 243 | multiply(ast: {left: any; right: any}) { 244 | const left = this.evalute(ast.left); 245 | const right = this.evalute(ast.right); 246 | return stripNumber(this.formatNumber(left) * this.formatNumber(right)); 247 | } 248 | 249 | divide(ast: {left: any; right: any}) { 250 | const left = this.evalute(ast.left); 251 | const right = this.evalute(ast.right); 252 | return stripNumber(this.formatNumber(left) / this.formatNumber(right)); 253 | } 254 | 255 | remainder(ast: {left: any; right: any}) { 256 | const left = this.evalute(ast.left); 257 | const right = this.evalute(ast.right); 258 | return this.formatNumber(left) % this.formatNumber(right); 259 | } 260 | 261 | add(ast: {left: any; right: any}) { 262 | const left = this.evalute(ast.left); 263 | const right = this.evalute(ast.right); 264 | return stripNumber(this.formatNumber(left) + this.formatNumber(right)); 265 | } 266 | 267 | minus(ast: {left: any; right: any}) { 268 | const left = this.evalute(ast.left); 269 | const right = this.evalute(ast.right); 270 | return stripNumber(this.formatNumber(left) - this.formatNumber(right)); 271 | } 272 | 273 | shift(ast: {op: '<<' | '>>' | '>>>'; left: any; right: any}) { 274 | const left = this.evalute(ast.left); 275 | const right = this.formatNumber(this.evalute(ast.right), true); 276 | 277 | if (ast.op === '<<') { 278 | return left << right; 279 | } else if (ast.op == '>>') { 280 | return left >> right; 281 | } else { 282 | return left >>> right; 283 | } 284 | } 285 | 286 | lt(ast: {left: any; right: any}) { 287 | const left = this.evalute(ast.left); 288 | const right = this.evalute(ast.right); 289 | 290 | // todo 如果是日期的对比,这个地方可以优化一下。 291 | 292 | return left < right; 293 | } 294 | 295 | gt(ast: {left: any; right: any}) { 296 | const left = this.evalute(ast.left); 297 | const right = this.evalute(ast.right); 298 | 299 | // todo 如果是日期的对比,这个地方可以优化一下。 300 | return left > right; 301 | } 302 | 303 | le(ast: {left: any; right: any}) { 304 | const left = this.evalute(ast.left); 305 | const right = this.evalute(ast.right); 306 | 307 | // todo 如果是日期的对比,这个地方可以优化一下。 308 | 309 | return left <= right; 310 | } 311 | 312 | ge(ast: {left: any; right: any}) { 313 | const left = this.evalute(ast.left); 314 | const right = this.evalute(ast.right); 315 | 316 | // todo 如果是日期的对比,这个地方可以优化一下。 317 | 318 | return left >= right; 319 | } 320 | 321 | eq(ast: {left: any; right: any}) { 322 | const left = this.evalute(ast.left); 323 | const right = this.evalute(ast.right); 324 | 325 | // todo 如果是日期的对比,这个地方可以优化一下。 326 | 327 | return left == right; 328 | } 329 | 330 | ne(ast: {left: any; right: any}) { 331 | const left = this.evalute(ast.left); 332 | const right = this.evalute(ast.right); 333 | 334 | // todo 如果是日期的对比,这个地方可以优化一下。 335 | 336 | return left != right; 337 | } 338 | 339 | streq(ast: {left: any; right: any}) { 340 | const left = this.evalute(ast.left); 341 | const right = this.evalute(ast.right); 342 | 343 | // todo 如果是日期的对比,这个地方可以优化一下。 344 | 345 | return left === right; 346 | } 347 | 348 | strneq(ast: {left: any; right: any}) { 349 | const left = this.evalute(ast.left); 350 | const right = this.evalute(ast.right); 351 | 352 | // todo 如果是日期的对比,这个地方可以优化一下。 353 | 354 | return left !== right; 355 | } 356 | 357 | binary(ast: {op: '&' | '^' | '|'; left: any; right: any}) { 358 | const left = this.evalute(ast.left); 359 | const right = this.evalute(ast.right); 360 | 361 | if (ast.op === '&') { 362 | return left & right; 363 | } else if (ast.op === '^') { 364 | return left ^ right; 365 | } else { 366 | return left | right; 367 | } 368 | } 369 | 370 | and(ast: {left: any; right: any}) { 371 | const left = this.evalute(ast.left); 372 | return left && this.evalute(ast.right); 373 | } 374 | 375 | or(ast: {left: any; right: any}) { 376 | const left = this.evalute(ast.left); 377 | return left || this.evalute(ast.right); 378 | } 379 | 380 | number(ast: {value: any; raw: string}) { 381 | // todo 以后可以在这支持大数字。 382 | return ast.value; 383 | } 384 | 385 | nsVariable(ast: {namespace: string; body: any}) { 386 | if (ast.namespace === 'window') { 387 | this.contextStack.push((name: string) => 388 | name === '&' ? window : (window as any)[name] 389 | ); 390 | } else if (ast.namespace === 'cookie') { 391 | this.contextStack.push((name: string) => { 392 | return getCookie(name); 393 | }); 394 | } else if (ast.namespace === 'ls' || ast.namespace === 'ss') { 395 | const ns = ast.namespace; 396 | this.contextStack.push((name: string) => { 397 | const raw = 398 | ns === 'ss' 399 | ? sessionStorage.getItem(name) 400 | : localStorage.getItem(name); 401 | 402 | if (typeof raw === 'string') { 403 | // 判断字符串是否一个纯数字字符串,如果是,则对比parse后的值和原值是否相同, 404 | // 如果不同则返回原值,因为原值如果是一个很长的纯数字字符串,则 parse 后可能会丢失精度 405 | if (/^\d+$/.test(raw)) { 406 | const parsed = JSON.parse(raw); 407 | return `${parsed}` === raw ? parsed : raw; 408 | } 409 | 410 | return parseJson(raw, raw); 411 | } 412 | 413 | return undefined; 414 | }); 415 | } else { 416 | throw new Error('Unsupported namespace: ' + ast.namespace); 417 | } 418 | 419 | const result = this.evalute(ast.body); 420 | this.contextStack.pop(); 421 | return result; 422 | } 423 | 424 | variable(ast: {name: string}) { 425 | const contextGetter = this.contextStack[this.contextStack.length - 1]; 426 | return contextGetter(ast.name); 427 | } 428 | 429 | identifier(ast: {name: string}) { 430 | return ast.name; 431 | } 432 | 433 | array(ast: {type: 'array'; members: Array}) { 434 | return ast.members.map(member => this.evalute(member)); 435 | } 436 | 437 | literal(ast: {type: 'literal'; value: any}) { 438 | return ast.value; 439 | } 440 | 441 | string(ast: {type: 'string'; value: string}) { 442 | return ast.value; 443 | } 444 | 445 | object(ast: {members: Array<{key: string; value: any}>}) { 446 | let object: any = {}; 447 | ast.members.forEach(({key, value}) => { 448 | object[this.evalute(key)] = this.evalute(value); 449 | }); 450 | return object; 451 | } 452 | 453 | conditional(ast: { 454 | type: 'conditional'; 455 | test: any; 456 | consequent: any; 457 | alternate: any; 458 | }) { 459 | return this.evalute(ast.test) 460 | ? this.evalute(ast.consequent) 461 | : this.evalute(ast.alternate); 462 | } 463 | 464 | funcCall(this: any, ast: {identifier: string; args: Array}) { 465 | const fnName = `fn${ast.identifier}`; 466 | const fn = 467 | this.functions[fnName] || this[fnName] || this.filters[ast.identifier]; 468 | 469 | if (!fn) { 470 | throw new Error(`${ast.identifier}函数没有定义`); 471 | } 472 | 473 | let args: Array = ast.args; 474 | 475 | // 逻辑函数特殊处理,因为有时候有些运算是可以跳过的。 476 | if (~['IF', 'AND', 'OR', 'XOR', 'IFS'].indexOf(ast.identifier)) { 477 | args = args.map(a => () => this.evalute(a)); 478 | } else { 479 | args = args.map(a => this.evalute(a)); 480 | } 481 | 482 | return fn.apply(this, args); 483 | } 484 | 485 | anonymousFunction(ast: any) { 486 | return ast; 487 | } 488 | 489 | callAnonymousFunction( 490 | ast: { 491 | args: any[]; 492 | return: any; 493 | }, 494 | args: Array 495 | ) { 496 | const ctx: any = createObject( 497 | this.contextStack[this.contextStack.length - 1]('&') || {}, 498 | {} 499 | ); 500 | ast.args.forEach((arg: any) => { 501 | if (arg.type !== 'variable') { 502 | throw new Error('expected a variable as argument'); 503 | } 504 | ctx[arg.name] = args.shift(); 505 | }); 506 | this.contextStack.push((varName: string) => 507 | varName === '&' ? ctx : ctx[varName] 508 | ); 509 | const result = this.evalute(ast.return); 510 | this.contextStack.pop(); 511 | return result; 512 | } 513 | 514 | /** 515 | * 示例:IF(A, B, C) 516 | * 517 | * 如果满足条件A,则返回B,否则返回C,支持多层嵌套IF函数。 518 | * 519 | * 也可以用表达式如:A ? B : C 520 | * 521 | * @example IF(condition, consequent, alternate) 522 | * @param {expression} condition - 条件表达式. 523 | * @param {any} consequent 条件判断通过的返回结果 524 | * @param {any} alternate 条件判断不通过的返回结果 525 | * @namespace 逻辑函数 526 | * 527 | * @returns {any} 根据条件返回不同的结果 528 | */ 529 | fnIF(condition: () => any, trueValue: () => any, falseValue: () => any) { 530 | return condition() ? trueValue() : falseValue(); 531 | } 532 | 533 | /** 534 | * 条件全部符合,返回 true,否则返回 false 535 | * 536 | * 示例:AND(语文成绩>80, 数学成绩>80) 537 | * 538 | * 语文成绩和数学成绩都大于 80,则返回 true,否则返回 false 539 | * 540 | * 也可以直接用表达式如:语文成绩>80 && 数学成绩>80 541 | * 542 | * @example AND(expression1, expression2, ...expressionN) 543 | * @param {...expression} conditions - 条件表达式. 544 | * @namespace 逻辑函数 545 | * 546 | * @returns {boolean} 547 | */ 548 | fnAND(...condtions: Array<() => any>) { 549 | return condtions.every(c => c()); 550 | } 551 | 552 | /** 553 | * 条件任意一个满足条件,返回 true,否则返回 false 554 | * 555 | * 示例:OR(语文成绩>80, 数学成绩>80) 556 | * 557 | * 语文成绩和数学成绩任意一个大于 80,则返回 true,否则返回 false 558 | * 559 | * 也可以直接用表达式如:语文成绩>80 || 数学成绩>80 560 | * 561 | * @example OR(expression1, expression2, ...expressionN) 562 | * @param {...expression} conditions - 条件表达式. 563 | * @namespace 逻辑函数 564 | * 565 | * @returns {boolean} 566 | */ 567 | fnOR(...condtions: Array<() => any>) { 568 | return condtions.some(c => c()); 569 | } 570 | 571 | /** 572 | * 异或处理,两个表达式同时为「真」,或者同时为「假」,则结果返回为「真」 573 | * 574 | * @example XOR(condition1, condition2) 575 | * @param {expression} condition1 - 条件表达式1 576 | * @param {expression} condition2 - 条件表达式2 577 | * @namespace 逻辑函数 578 | * 579 | * @returns {boolean} 580 | */ 581 | fnXOR(c1: () => any, c2: () => any) { 582 | return !!c1() === !!c2(); 583 | } 584 | 585 | /** 586 | * 判断函数集合,相当于多个 else if 合并成一个。 587 | * 588 | * 示例:IFS(语文成绩 > 80, "优秀", 语文成绩 > 60, "良", "继续努力") 589 | * 590 | * 如果语文成绩大于 80,则返回优秀,否则判断大于 60 分,则返回良,否则返回继续努力。 591 | * 592 | * @example IFS(condition1, result1, condition2, result2,...conditionN, resultN) 593 | * @param {...any} args - 条件,返回值集合 594 | * @namespace 逻辑函数 595 | * @returns {any} 第一个满足条件的结果,没有命中的返回 false。 596 | */ 597 | fnIFS(...args: Array<() => any>) { 598 | if (args.length % 2) { 599 | args.splice(args.length - 1, 0, () => true); 600 | } 601 | 602 | while (args.length) { 603 | const c = args.shift()!; 604 | const v = args.shift()!; 605 | 606 | if (c()) { 607 | return v(); 608 | } 609 | } 610 | return; 611 | } 612 | 613 | /** 614 | * 返回传入数字的绝对值 615 | * 616 | * @example ABS(num) 617 | * @param {number} num - 数值 618 | * @namespace 数学函数 619 | * 620 | * @returns {number} 传入数值的绝对值 621 | */ 622 | fnABS(a: number) { 623 | a = this.formatNumber(a); 624 | return Math.abs(a); 625 | } 626 | 627 | /** 628 | * 获取最大值,如果只有一个参数且是数组,则计算这个数组内的值 629 | * 630 | * @example MAX(num1, num2, ...numN) 631 | * @param {...number} num - 数值 632 | * @namespace 数学函数 633 | * 634 | * @returns {number} 所有传入值中最大的那个 635 | */ 636 | fnMAX(...args: Array) { 637 | let arr = args; 638 | if (args.length === 1 && Array.isArray(args[0])) { 639 | arr = args[0]; 640 | } 641 | return Math.max.apply( 642 | Math, 643 | arr.map(item => this.formatNumber(item)) 644 | ); 645 | } 646 | 647 | /** 648 | * 获取最小值,如果只有一个参数且是数组,则计算这个数组内的值 649 | * 650 | * @example MIN(num1, num2, ...numN) 651 | * @param {...number} num - 数值 652 | * @namespace 数学函数 653 | * 654 | * @returns {number} 所有传入值中最小的那个 655 | */ 656 | fnMIN(...args: Array) { 657 | let arr = args; 658 | if (args.length === 1 && Array.isArray(args[0])) { 659 | arr = args[0]; 660 | } 661 | return Math.min.apply( 662 | Math, 663 | arr.map(item => this.formatNumber(item)) 664 | ); 665 | } 666 | 667 | /** 668 | * 求和,如果只有一个参数且是数组,则计算这个数组内的值 669 | * 670 | * @example SUM(num1, num2, ...numN) 671 | * @param {...number} num - 数值 672 | * @namespace 数学函数 673 | * 674 | * @returns {number} 所有传入数值的总和 675 | */ 676 | fnSUM(...args: Array) { 677 | let arr = args; 678 | if (args.length === 1 && Array.isArray(args[0])) { 679 | arr = args[0]; 680 | } 681 | return arr.reduce((sum, a) => sum + this.formatNumber(a) || 0, 0); 682 | } 683 | 684 | /** 685 | * 将数值向下取整为最接近的整数 686 | * 687 | * @example INT(num) 688 | * @param {number} num - 数值 689 | * @namespace 数学函数 690 | * 691 | * @returns {number} 数值对应的整形 692 | */ 693 | fnINT(n: number) { 694 | return Math.floor(this.formatNumber(n)); 695 | } 696 | 697 | /** 698 | * 返回两数相除的余数,参数 number 是被除数,divisor 是除数 699 | * 700 | * @example MOD(num, divisor) 701 | * @param {number} num - 被除数 702 | * @param {number} divisor - 除数 703 | * @namespace 数学函数 704 | * 705 | * @returns {number} 两数相除的余数 706 | */ 707 | fnMOD(a: number, b: number) { 708 | return this.formatNumber(a) % this.formatNumber(b); 709 | } 710 | 711 | /** 712 | * 圆周率 3.1415... 713 | * 714 | * @example PI() 715 | * @namespace 数学函数 716 | * 717 | * @returns {number} 圆周率数值 718 | */ 719 | fnPI() { 720 | return Math.PI; 721 | } 722 | 723 | /** 724 | * 将数字四舍五入到指定的位数,可以设置小数位。 725 | * 726 | * @example ROUND(num[, numDigits = 2]) 727 | * @param {number} num - 要处理的数字 728 | * @param {number} numDigits - 小数位数 729 | * @namespace 数学函数 730 | * 731 | * @returns {number} 传入数值四舍五入后的结果 732 | */ 733 | fnROUND(a: number, b: number) { 734 | a = this.formatNumber(a); 735 | b = this.formatNumber(b); 736 | const bResult = Math.round(b); 737 | 738 | if (bResult) { 739 | const c = Math.pow(10, bResult); 740 | return Math.round(a * c) / c; 741 | } 742 | 743 | return Math.round(a); 744 | } 745 | 746 | /** 747 | * 将数字向下取整到指定的位数,可以设置小数位。 748 | * 749 | * @example FLOOR(num[, numDigits=2]) 750 | * @param {number} num - 要处理的数字 751 | * @param {number} numDigits - 小数位数 752 | * @namespace 数学函数 753 | * 754 | * @returns {number} 传入数值向下取整后的结果 755 | */ 756 | fnFLOOR(a: number, b: number) { 757 | a = this.formatNumber(a); 758 | b = this.formatNumber(b); 759 | const bResult = Math.round(b); 760 | 761 | if (bResult) { 762 | const c = Math.pow(10, bResult); 763 | return Math.floor(a * c) / c; 764 | } 765 | 766 | return Math.floor(a); 767 | } 768 | 769 | /** 770 | * 将数字向上取整到指定的位数,可以设置小数位。 771 | * 772 | * @example CEIL(num[, numDigits=2]) 773 | * @param {number} num - 要处理的数字 774 | * @param {number} numDigits - 小数位数 775 | * @namespace 数学函数 776 | * 777 | * @returns {number} 传入数值向上取整后的结果 778 | */ 779 | fnCEIL(a: number, b: number) { 780 | a = this.formatNumber(a); 781 | b = this.formatNumber(b); 782 | const bResult = Math.round(b); 783 | 784 | if (bResult) { 785 | const c = Math.pow(10, bResult); 786 | return Math.ceil(a * c) / c; 787 | } 788 | 789 | return Math.ceil(a); 790 | } 791 | 792 | /** 793 | * 开平方,参数 number 为非负数 794 | * 795 | * @example SQRT(num) 796 | * @param {number} num - 要处理的数字 797 | * @namespace 数学函数 798 | * 799 | * @returns {number} 开平方的结果 800 | */ 801 | fnSQRT(n: number) { 802 | return Math.sqrt(this.formatNumber(n)); 803 | } 804 | 805 | /** 806 | * 返回所有参数的平均值,如果只有一个参数且是数组,则计算这个数组内的值 807 | * 808 | * @example AVG(num1, num2, ...numN) 809 | * @param {...number} num - 要处理的数字 810 | * @namespace 数学函数 811 | * 812 | * @returns {number} 所有数值的平均值 813 | */ 814 | fnAVG(...args: Array) { 815 | let arr = args; 816 | if (args.length === 1 && Array.isArray(args[0])) { 817 | arr = args[0]; 818 | } 819 | return ( 820 | this.fnSUM.apply( 821 | this, 822 | arr.map(item => this.formatNumber(item)) 823 | ) / arr.length 824 | ); 825 | } 826 | 827 | /** 828 | * 返回数据点与数据均值点之差(数据偏差)的平方和,如果只有一个参数且是数组,则计算这个数组内的值 829 | * 830 | * @example DEVSQ(num1, num2, ...numN) 831 | * @param {...number} num - 要处理的数字 832 | * @namespace 数学函数 833 | * 834 | * @returns {number} 所有数值的平均值 835 | */ 836 | fnDEVSQ(...args: Array) { 837 | if (args.length === 0) { 838 | return null; 839 | } 840 | let arr = args; 841 | if (args.length === 1 && Array.isArray(args[0])) { 842 | arr = args[0]; 843 | } 844 | 845 | const nums = arr.map(item => this.formatNumber(item)); 846 | const sum = nums.reduce((sum, a) => sum + a || 0, 0); 847 | const mean = sum / nums.length; 848 | let result = 0; 849 | for (const num of nums) { 850 | result += Math.pow(num - mean, 2); 851 | } 852 | return result; 853 | } 854 | 855 | /** 856 | * 数据点到其算术平均值的绝对偏差的平均值 857 | * 858 | * @example AVEDEV(num1, num2, ...numN) 859 | * @param {...number} num - 要处理的数字 860 | * @namespace 数学函数 861 | * 862 | * @returns {number} 所有数值的平均值 863 | */ 864 | fnAVEDEV(...args: Array) { 865 | if (args.length === 0) { 866 | return null; 867 | } 868 | let arr = args; 869 | if (args.length === 1 && Array.isArray(args[0])) { 870 | arr = args[0]; 871 | } 872 | const nums = arr.map(item => this.formatNumber(item)); 873 | const sum = nums.reduce((sum, a) => sum + a || 0, 0); 874 | const mean = sum / nums.length; 875 | let result = 0; 876 | for (const num of nums) { 877 | result += Math.abs(num - mean); 878 | } 879 | return result / nums.length; 880 | } 881 | 882 | /** 883 | * 数据点的调和平均值,如果只有一个参数且是数组,则计算这个数组内的值 884 | * 885 | * @example HARMEAN(num1, num2, ...numN) 886 | * @param {...number} num - 要处理的数字 887 | * @namespace 数学函数 888 | * 889 | * @returns {number} 所有数值的平均值 890 | */ 891 | fnHARMEAN(...args: Array) { 892 | if (args.length === 0) { 893 | return null; 894 | } 895 | let arr = args; 896 | if (args.length === 1 && Array.isArray(args[0])) { 897 | arr = args[0]; 898 | } 899 | const nums = arr.map(item => this.formatNumber(item)); 900 | let den = 0; 901 | for (const num of nums) { 902 | den += 1 / num; 903 | } 904 | return nums.length / den; 905 | } 906 | 907 | /** 908 | * 数据集中第 k 个最大值 909 | * 910 | * @example LARGE(array, k) 911 | * @param {array} nums - 要处理的数字 912 | * @param {number} k - 第几大 913 | * @namespace 数学函数 914 | * 915 | * @returns {number} 所有数值的平均值 916 | */ 917 | fnLARGE(nums: Array, k: number) { 918 | if (nums.length === 0) { 919 | return null; 920 | } 921 | const numsFormat = nums.map(item => this.formatNumber(item)); 922 | if (k < 0 || numsFormat.length < k) { 923 | return null; 924 | } 925 | return numsFormat.sort(function (a, b) { 926 | return b - a; 927 | })[k - 1]; 928 | } 929 | 930 | /** 931 | * 将数值转为中文大写金额 932 | * 933 | * @example UPPERMONEY(num) 934 | * @param {number} num - 要处理的数字 935 | * @namespace 数学函数 936 | * 937 | * @returns {string} 数值中文大写字符 938 | */ 939 | fnUPPERMONEY(n: number) { 940 | n = this.formatNumber(n); 941 | const fraction = ['角', '分']; 942 | const digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']; 943 | const unit = [ 944 | ['元', '万', '亿'], 945 | ['', '拾', '佰', '仟'] 946 | ]; 947 | const head = n < 0 ? '欠' : ''; 948 | n = Math.abs(n); 949 | let s = ''; 950 | for (let i = 0; i < fraction.length; i++) { 951 | s += ( 952 | digit[Math.floor(n * 10 * Math.pow(10, i)) % 10] + fraction[i] 953 | ).replace(/零./, ''); 954 | } 955 | s = s || '整'; 956 | n = Math.floor(n); 957 | for (let i = 0; i < unit[0].length && n > 0; i++) { 958 | let p = ''; 959 | for (let j = 0; j < unit[1].length && n > 0; j++) { 960 | p = digit[n % 10] + unit[1][j] + p; 961 | n = Math.floor(n / 10); 962 | } 963 | s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s; 964 | } 965 | return ( 966 | head + 967 | s 968 | .replace(/(零.)*零元/, '元') 969 | .replace(/(零.)+/g, '零') 970 | .replace(/^整$/, '零元整') 971 | ); 972 | } 973 | 974 | /** 975 | * 返回大于等于 0 且小于 1 的均匀分布随机实数。每一次触发计算都会变化。 976 | * 977 | * 示例:`RAND()*100` 978 | * 979 | * 返回 0-100 之间的随机数 980 | * 981 | * @example RAND() 982 | * @namespace 数学函数 983 | * 984 | * @returns {number} 随机数 985 | */ 986 | fnRAND() { 987 | return Math.random(); 988 | } 989 | 990 | /** 991 | * 取数据最后一个 992 | * 993 | * @example LAST(array) 994 | * @param {...number} arr - 要处理的数组 995 | * @namespace 数学函数 996 | * 997 | * @returns {any} 最后一个值 998 | */ 999 | fnLAST(arr: Array) { 1000 | return arr.length ? arr[arr.length - 1] : null; 1001 | } 1002 | 1003 | // 文本函数 1004 | 1005 | normalizeText(raw: any) { 1006 | if (raw instanceof Date) { 1007 | return moment(raw).format(); 1008 | } 1009 | 1010 | return `${raw}`; 1011 | } 1012 | 1013 | /** 1014 | * 返回传入文本左侧的指定长度字符串。 1015 | * 1016 | * @example LEFT(text, len) 1017 | * @param {string} text - 要处理的文本 1018 | * @param {number} len - 要处理的长度 1019 | * @namespace 文本函数 1020 | * 1021 | * @returns {string} 对应字符串 1022 | */ 1023 | fnLEFT(text: string, len: number) { 1024 | text = this.normalizeText(text); 1025 | return text.substring(0, len); 1026 | } 1027 | 1028 | /** 1029 | * 返回传入文本右侧的指定长度字符串。 1030 | * 1031 | * @example RIGHT(text, len) 1032 | * @param {string} text - 要处理的文本 1033 | * @param {number} len - 要处理的长度 1034 | * @namespace 文本函数 1035 | * 1036 | * @returns {string} 对应字符串 1037 | */ 1038 | fnRIGHT(text: string, len: number) { 1039 | text = this.normalizeText(text); 1040 | return text.substring(text.length - len, text.length); 1041 | } 1042 | 1043 | /** 1044 | * 计算文本的长度 1045 | * 1046 | * @example LEN(text) 1047 | * @param {string} text - 要处理的文本 1048 | * @namespace 文本函数 1049 | * 1050 | * @returns {number} 长度 1051 | */ 1052 | fnLEN(text: string) { 1053 | text = this.normalizeText(text); 1054 | return text?.length; 1055 | } 1056 | 1057 | /** 1058 | * 计算文本集合中所有文本的长度 1059 | * 1060 | * @example LENGTH(textArr) 1061 | * @param {string[]} textArr - 要处理的文本集合 1062 | * @namespace 文本函数 1063 | * 1064 | * @returns {number[]} 长度集合 1065 | */ 1066 | fnLENGTH(...args: any[]) { 1067 | return this.fnLEN.call(this, args); 1068 | } 1069 | 1070 | /** 1071 | * 判断文本是否为空 1072 | * 1073 | * @example ISEMPTY(text) 1074 | * @param {string} text - 要处理的文本 1075 | * @namespace 文本函数 1076 | * 1077 | * @returns {boolean} 判断结果 1078 | */ 1079 | fnISEMPTY(text: string) { 1080 | return !text || !String(text).trim(); 1081 | } 1082 | 1083 | /** 1084 | * 将多个传入值连接成文本 1085 | * 1086 | * @example CONCATENATE(text1, text2, ...textN) 1087 | * @param {...string} text - 文本集合 1088 | * @namespace 文本函数 1089 | * 1090 | * @returns {string} 连接后的文本 1091 | */ 1092 | fnCONCATENATE(...args: Array) { 1093 | return args.join(''); 1094 | } 1095 | 1096 | /** 1097 | * 返回计算机字符集的数字代码所对应的字符。 1098 | * 1099 | * `CHAR(97)` 等价于 "a" 1100 | * 1101 | * @example CHAR(code) 1102 | * @param {number} code - 编码值 1103 | * @namespace 文本函数 1104 | * 1105 | * @returns {string} 指定位置的字符 1106 | */ 1107 | fnCHAR(code: number) { 1108 | return String.fromCharCode(code); 1109 | } 1110 | 1111 | /** 1112 | * 将传入文本转成小写 1113 | * 1114 | * @example LOWER(text) 1115 | * @param {string} text - 文本 1116 | * @namespace 文本函数 1117 | * 1118 | * @returns {string} 结果文本 1119 | */ 1120 | fnLOWER(text: string) { 1121 | text = this.normalizeText(text); 1122 | return text.toLowerCase(); 1123 | } 1124 | 1125 | /** 1126 | * 将传入文本转成大写 1127 | * 1128 | * @example UPPER(text) 1129 | * @param {string} text - 文本 1130 | * @namespace 文本函数 1131 | * 1132 | * @returns {string} 结果文本 1133 | */ 1134 | fnUPPER(text: string) { 1135 | text = this.normalizeText(text); 1136 | return text.toUpperCase(); 1137 | } 1138 | 1139 | /** 1140 | * 将传入文本首字母转成大写 1141 | * 1142 | * @example UPPERFIRST(text) 1143 | * @param {string} text - 文本 1144 | * @namespace 文本函数 1145 | * 1146 | * @returns {string} 结果文本 1147 | */ 1148 | fnUPPERFIRST(text: string) { 1149 | text = this.normalizeText(text); 1150 | return upperFirst(text); 1151 | } 1152 | 1153 | /** 1154 | * 向前补齐文本长度 1155 | * 1156 | * 示例 `PADSTART("6", 2, "0")` 1157 | * 1158 | * 返回 `06` 1159 | * 1160 | * @example PADSTART(text) 1161 | * @param {string} text - 文本 1162 | * @param {number} num - 目标长度 1163 | * @param {string} pad - 用于补齐的文本 1164 | * @namespace 文本函数 1165 | * 1166 | * @returns {string} 结果文本 1167 | */ 1168 | fnPADSTART(text: string, num: number, pad: string): string { 1169 | text = this.normalizeText(text); 1170 | return padStart(text, num, pad); 1171 | } 1172 | 1173 | /** 1174 | * 将文本转成标题 1175 | * 1176 | * 示例 `CAPITALIZE("star")` 1177 | * 1178 | * 返回 `Star` 1179 | * 1180 | * @example CAPITALIZE(text) 1181 | * @param {string} text - 文本 1182 | * @namespace 文本函数 1183 | * 1184 | * @returns {string} 结果文本 1185 | */ 1186 | fnCAPITALIZE(text: string): string { 1187 | text = this.normalizeText(text); 1188 | return capitalize(text); 1189 | } 1190 | 1191 | /** 1192 | * 对文本进行 HTML 转义 1193 | * 1194 | * 示例 `ESCAPE("star")` 1195 | * 1196 | * 返回 `Star` 1197 | * 1198 | * @example ESCAPE(text) 1199 | * @param {string} text - 文本 1200 | * @namespace 文本函数 1201 | * 1202 | * @returns {string} 结果文本 1203 | */ 1204 | fnESCAPE(text: string): string { 1205 | text = this.normalizeText(text); 1206 | return escape(text); 1207 | } 1208 | 1209 | /** 1210 | * 对文本长度进行截断 1211 | * 1212 | * 示例 `TRUNCATE("amis.baidu.com", 6)` 1213 | * 1214 | * 返回 `amis...` 1215 | * 1216 | * @example TRUNCATE(text, 6) 1217 | * @param {string} text - 文本 1218 | * @param {number} text - 最长长度 1219 | * @namespace 文本函数 1220 | * 1221 | * @returns {string} 结果文本 1222 | */ 1223 | fnTRUNCATE(text: string, length: number): string { 1224 | text = this.normalizeText(text); 1225 | return truncate(text, {length}); 1226 | } 1227 | 1228 | /** 1229 | * 取在某个分隔符之前的所有字符串 1230 | * 1231 | * @example BEFORELAST(text, '.') 1232 | * @param {string} text - 文本 1233 | * @param {string} delimiter - 结束文本 1234 | * @namespace 文本函数 1235 | * 1236 | * @returns {string} 判断结果 1237 | */ 1238 | fnBEFORELAST(text: string, delimiter: string = '.') { 1239 | text = this.normalizeText(text); 1240 | return text.split(delimiter).slice(0, -1).join(delimiter) || text + ''; 1241 | } 1242 | 1243 | /** 1244 | * 将文本根据指定片段分割成数组 1245 | * 1246 | * 示例:`SPLIT("a,b,c", ",")` 1247 | * 1248 | * 返回 `["a", "b", "c"]` 1249 | * 1250 | * @example SPLIT(text, ',') 1251 | * @param {string} text - 文本 1252 | * @param {string} delimiter - 文本片段 1253 | * @namespace 文本函数 1254 | * 1255 | * @returns {Array} 文本集 1256 | */ 1257 | fnSPLIT(text: string, sep: string = ',') { 1258 | text = this.normalizeText(text); 1259 | return text.split(sep); 1260 | } 1261 | 1262 | /** 1263 | * 将文本去除前后空格 1264 | * 1265 | * @example TRIM(text) 1266 | * @param {string} text - 文本 1267 | * @namespace 文本函数 1268 | * 1269 | * @returns {string} 处理后的文本 1270 | */ 1271 | fnTRIM(text: string) { 1272 | text = this.normalizeText(text); 1273 | return text.trim(); 1274 | } 1275 | 1276 | /** 1277 | * 去除文本中的 HTML 标签 1278 | * 1279 | * 示例:`STRIPTAG("amis")` 1280 | * 1281 | * 返回:`amis` 1282 | * 1283 | * @example STRIPTAG(text) 1284 | * @param {string} text - 文本 1285 | * @namespace 文本函数 1286 | * 1287 | * @returns {string} 处理后的文本 1288 | */ 1289 | fnSTRIPTAG(text: string) { 1290 | text = this.normalizeText(text); 1291 | return text.replace(/<\/?[^>]+(>|$)/g, ''); 1292 | } 1293 | 1294 | /** 1295 | * 将字符串中的换行转成 HTML `
`,用于简单换行的场景 1296 | * 1297 | * 示例:`LINEBREAK("\n")` 1298 | * 1299 | * 返回:`
` 1300 | * 1301 | * @example LINEBREAK(text) 1302 | * @param {string} text - 文本 1303 | * @namespace 文本函数 1304 | * 1305 | * @returns {string} 处理后的文本 1306 | */ 1307 | fnLINEBREAK(text: string) { 1308 | text = this.normalizeText(text); 1309 | return text.replace(/(?:\r\n|\r|\n)/g, '
'); 1310 | } 1311 | 1312 | /** 1313 | * 判断字符串(text)是否以特定字符串(startString)开始,是则返回 True,否则返回 False 1314 | * 1315 | * @example STARTSWITH(text, '片段') 1316 | * @param {string} text - 文本 1317 | * @param {string} startString - 起始文本 1318 | * @namespace 文本函数 1319 | * 1320 | * @returns {string} 判断结果 1321 | */ 1322 | fnSTARTSWITH(text: string, search: string) { 1323 | if (!search) { 1324 | return false; 1325 | } 1326 | 1327 | text = this.normalizeText(text); 1328 | return text.indexOf(search) === 0; 1329 | } 1330 | 1331 | /** 1332 | * 判断字符串(text)是否以特定字符串(endString)结束,是则返回 True,否则返回 False 1333 | * 1334 | * @example ENDSWITH(text, '片段') 1335 | * @param {string} text - 文本 1336 | * @param {string} endString - 结束文本 1337 | * @namespace 文本函数 1338 | * 1339 | * @returns {string} 判断结果 1340 | */ 1341 | fnENDSWITH(text: string, search: string) { 1342 | if (!search) { 1343 | return false; 1344 | } 1345 | 1346 | text = this.normalizeText(text); 1347 | return text.indexOf(search, text.length - search.length) !== -1; 1348 | } 1349 | 1350 | /** 1351 | * 判断参数 1 中的文本是否包含参数 2 中的文本。 1352 | * 1353 | * @example CONTAINS(text, searchText) 1354 | * @param {string} text - 文本 1355 | * @param {string} searchText - 搜索文本 1356 | * @namespace 文本函数 1357 | * 1358 | * @returns {string} 判断结果 1359 | */ 1360 | fnCONTAINS(text: string, search: string) { 1361 | if (!search) { 1362 | return false; 1363 | } 1364 | 1365 | text = this.normalizeText(text); 1366 | return !!~text.indexOf(search); 1367 | } 1368 | 1369 | /** 1370 | * 对文本进行全量替换。 1371 | * 1372 | * @example REPLACE(text, search, replace) 1373 | * @param {string} text - 要处理的文本 1374 | * @param {string} search - 要被替换的文本 1375 | * @param {string} replace - 要替换的文本 1376 | * @namespace 文本函数 1377 | * 1378 | * @returns {string} 处理结果 1379 | */ 1380 | fnREPLACE(text: string, search: string, replace: string) { 1381 | text = this.normalizeText(text); 1382 | let result = text; 1383 | 1384 | while (true) { 1385 | const idx = result.indexOf(search); 1386 | 1387 | if (!~idx) { 1388 | break; 1389 | } 1390 | 1391 | result = 1392 | result.substring(0, idx) + 1393 | replace + 1394 | result.substring(idx + search.length); 1395 | } 1396 | 1397 | return result; 1398 | } 1399 | 1400 | /** 1401 | * 对文本进行搜索,返回命中的位置 1402 | * 1403 | * @example SEARCH(text, search, 0) 1404 | * @param {string} text - 要处理的文本 1405 | * @param {string} search - 用来搜索的文本 1406 | * @param {number} start - 起始位置 1407 | * @namespace 文本函数 1408 | * 1409 | * @returns {number} 命中的位置 1410 | */ 1411 | fnSEARCH(text: string, search: string, start: number = 0) { 1412 | text = this.normalizeText(text); 1413 | start = this.formatNumber(start); 1414 | 1415 | const idx = text.indexOf(search, start); 1416 | if (~idx) { 1417 | return idx; 1418 | } 1419 | 1420 | return -1; 1421 | } 1422 | 1423 | /** 1424 | * 返回文本字符串中从指定位置开始的特定数目的字符 1425 | * 1426 | * @example MID(text, from, len) 1427 | * @param {string} text - 要处理的文本 1428 | * @param {number} from - 起始位置 1429 | * @param {number} len - 处理长度 1430 | * @namespace 文本函数 1431 | * 1432 | * @returns {number} 命中的位置 1433 | */ 1434 | fnMID(text: string, from: number, len: number) { 1435 | text = this.normalizeText(text); 1436 | return text.substring(from, from + len); 1437 | } 1438 | 1439 | /** 1440 | * 返回路径中的文件名 1441 | * 1442 | * 示例:`/home/amis/a.json` 1443 | * 1444 | * 返回:a.json` 1445 | * 1446 | * @example BASENAME(text) 1447 | * @param {string} text - 要处理的文本 1448 | * @namespace 文本函数 1449 | * 1450 | * @returns {string} 文件名 1451 | */ 1452 | fnBASENAME(text: string) { 1453 | text = this.normalizeText(text); 1454 | return text.split(/[\\/]/).pop(); 1455 | } 1456 | 1457 | // 日期函数 1458 | 1459 | /** 1460 | * 创建日期对象,可以通过特定格式的字符串,或者数值。 1461 | * 1462 | * 需要注意的是,其中月份的数值是从0开始的,也就是说, 1463 | * 如果是12月份,你应该传入数值11。 1464 | * 1465 | * @example DATE(2021, 11, 6, 8, 20, 0) 1466 | * @example DATE('2021-12-06 08:20:00') 1467 | * @namespace 日期函数 1468 | * 1469 | * @returns {Date} 日期对象 1470 | */ 1471 | fnDATE( 1472 | year: number, 1473 | month: number, 1474 | day: number, 1475 | hour: number, 1476 | minute: number, 1477 | second: number 1478 | ) { 1479 | if (month === undefined) { 1480 | return new Date(year); 1481 | } 1482 | 1483 | return new Date(year, month, day, hour, minute, second); 1484 | } 1485 | 1486 | /** 1487 | * 返回时间的时间戳 1488 | * 1489 | * @example TIMESTAMP(date[, format = "X"]) 1490 | * @namespace 日期函数 1491 | * @param {date} date 日期对象 1492 | * @param {string} format 时间戳格式,带毫秒传入 'x'。默认为 'X' 不带毫秒的。 1493 | * 1494 | * @returns {number} 时间戳 1495 | */ 1496 | fnTIMESTAMP(date: Date, format?: 'x' | 'X') { 1497 | return parseInt(moment(date).format(format === 'x' ? 'x' : 'X'), 10); 1498 | } 1499 | 1500 | /** 1501 | * 返回今天的日期 1502 | * 1503 | * @example TODAY() 1504 | * @namespace 日期函数 1505 | * 1506 | * @returns {number} 日期 1507 | */ 1508 | fnTODAY() { 1509 | return new Date(); 1510 | } 1511 | 1512 | /** 1513 | * 返回现在的日期 1514 | * 1515 | * @example NOW() 1516 | * @namespace 日期函数 1517 | * 1518 | * @returns {number} 日期 1519 | */ 1520 | fnNOW() { 1521 | return new Date(); 1522 | } 1523 | 1524 | /** 1525 | * 将日期转成日期字符串 1526 | * 1527 | * @example DATETOSTR(date[, format="YYYY-MM-DD HH:mm:ss"]) 1528 | * @namespace 日期函数 1529 | * @param {date} date 日期对象 1530 | * @param {string} format 日期格式,默认为 "YYYY-MM-DD HH:mm:ss" 1531 | * 1532 | * @returns {number} 日期字符串 1533 | */ 1534 | fnDATETOSTR(date: Date, format = 'YYYY-MM-DD HH:mm:ss') { 1535 | return moment(date).format(format); 1536 | } 1537 | 1538 | /** 1539 | * 返回日期的指定范围的开端 1540 | * 1541 | * @namespace 日期函数 1542 | * @example STARTOF(date[unit = "day"]) 1543 | * @param {date} date 日期对象 1544 | * @param {string} unit 比如可以传入 'day'、'month'、'year' 或者 `week` 等等 1545 | * @returns {date} 新的日期对象 1546 | */ 1547 | fnSTARTOF(date: Date, unit?: any) { 1548 | return moment(date) 1549 | .startOf(unit || 'day') 1550 | .toDate(); 1551 | } 1552 | 1553 | /** 1554 | * 返回日期的指定范围的末尾 1555 | * @namespace 日期函数 1556 | * @example ENDOF(date[unit = "day"]) 1557 | * @param {date} date 日期对象 1558 | * @param {string} unit 比如可以传入 'day'、'month'、'year' 或者 `week` 等等 1559 | * @returns {date} 新的日期对象 1560 | */ 1561 | fnENDOF(date: Date, unit?: any) { 1562 | return moment(date) 1563 | .endOf(unit || 'day') 1564 | .toDate(); 1565 | } 1566 | 1567 | normalizeDate(raw: any): Date { 1568 | if (typeof raw === 'string') { 1569 | const formats = ['', 'YYYY-MM-DD HH:mm:ss']; 1570 | 1571 | while (formats.length) { 1572 | const format = formats.shift()!; 1573 | const date = moment(raw, format); 1574 | 1575 | if (date.isValid()) { 1576 | return date.toDate(); 1577 | } 1578 | } 1579 | } else if (typeof raw === 'number') { 1580 | return new Date(raw); 1581 | } 1582 | 1583 | return raw; 1584 | } 1585 | 1586 | /** 1587 | * 返回日期的年份 1588 | * @namespace 日期函数 1589 | * @example YEAR(date) 1590 | * @param {date} date 日期对象 1591 | * @returns {number} 数值 1592 | */ 1593 | fnYEAR(date: Date) { 1594 | date = this.normalizeDate(date); 1595 | return date.getFullYear(); 1596 | } 1597 | 1598 | /** 1599 | * 返回日期的月份,这里就是自然月份。 1600 | * 1601 | * @namespace 日期函数 1602 | * @example MONTH(date) 1603 | * @param {date} date 日期对象 1604 | * @returns {number} 数值 1605 | */ 1606 | fnMONTH(date: Date) { 1607 | date = this.normalizeDate(date); 1608 | return date.getMonth() + 1; 1609 | } 1610 | 1611 | /** 1612 | * 返回日期的天 1613 | * @namespace 日期函数 1614 | * @example DAY(date) 1615 | * @param {date} date 日期对象 1616 | * @returns {number} 数值 1617 | */ 1618 | fnDAY(date: Date) { 1619 | date = this.normalizeDate(date); 1620 | return date.getDate(); 1621 | } 1622 | 1623 | /** 1624 | * 返回日期的小时 1625 | * @param {date} date 日期对象 1626 | * @namespace 日期函数 1627 | * @example HOUR(date) 1628 | * @returns {number} 数值 1629 | */ 1630 | fnHOUR(date: Date) { 1631 | date = this.normalizeDate(date); 1632 | return date.getHours(); 1633 | } 1634 | 1635 | /** 1636 | * 返回日期的分 1637 | * @param {date} date 日期对象 1638 | * @namespace 日期函数 1639 | * @example MINUTE(date) 1640 | * @returns {number} 数值 1641 | */ 1642 | fnMINUTE(date: Date) { 1643 | date = this.normalizeDate(date); 1644 | return date.getMinutes(); 1645 | } 1646 | 1647 | /** 1648 | * 返回日期的秒 1649 | * @param {date} date 日期对象 1650 | * @namespace 日期函数 1651 | * @example SECOND(date) 1652 | * @returns {number} 数值 1653 | */ 1654 | fnSECOND(date: Date) { 1655 | date = this.normalizeDate(date); 1656 | return date.getSeconds(); 1657 | } 1658 | 1659 | /** 1660 | * 返回两个日期相差多少年 1661 | * @param {date} endDate 日期对象 1662 | * @param {date} startDate 日期对象 1663 | * @namespace 日期函数 1664 | * @example YEARS(endDate, startDate) 1665 | * @returns {number} 数值 1666 | */ 1667 | fnYEARS(endDate: Date, startDate: Date) { 1668 | endDate = this.normalizeDate(endDate); 1669 | startDate = this.normalizeDate(startDate); 1670 | return moment(endDate).diff(moment(startDate), 'year'); 1671 | } 1672 | 1673 | /** 1674 | * 返回两个日期相差多少分钟 1675 | * @param {date} endDate 日期对象 1676 | * @param {date} startDate 日期对象 1677 | * @namespace 日期函数 1678 | * @example MINUTES(endDate, startDate) 1679 | * @returns {number} 数值 1680 | */ 1681 | fnMINUTES(endDate: Date, startDate: Date) { 1682 | endDate = this.normalizeDate(endDate); 1683 | startDate = this.normalizeDate(startDate); 1684 | return moment(endDate).diff(moment(startDate), 'minutes'); 1685 | } 1686 | 1687 | /** 1688 | * 返回两个日期相差多少天 1689 | * @param {date} endDate 日期对象 1690 | * @param {date} startDate 日期对象 1691 | * @namespace 日期函数 1692 | * @example DAYS(endDate, startDate) 1693 | * @returns {number} 数值 1694 | */ 1695 | fnDAYS(endDate: Date, startDate: Date) { 1696 | endDate = this.normalizeDate(endDate); 1697 | startDate = this.normalizeDate(startDate); 1698 | return moment(endDate).diff(moment(startDate), 'days'); 1699 | } 1700 | 1701 | /** 1702 | * 返回两个日期相差多少小时 1703 | * @param {date} endDate 日期对象 1704 | * @param {date} startDate 日期对象 1705 | * @namespace 日期函数 1706 | * @example HOURS(endDate, startDate) 1707 | * @returns {number} 数值 1708 | */ 1709 | fnHOURS(endDate: Date, startDate: Date) { 1710 | endDate = this.normalizeDate(endDate); 1711 | startDate = this.normalizeDate(startDate); 1712 | return moment(endDate).diff(moment(startDate), 'hour'); 1713 | } 1714 | 1715 | /** 1716 | * 修改日期,对日期进行加减天、月份、年等操作 1717 | * 1718 | * 示例: 1719 | * 1720 | * DATEMODIFY(A, -2, 'month') 1721 | * 1722 | * 对日期 A 进行往前减2月的操作。 1723 | * 1724 | * @param {date} date 日期对象 1725 | * @param {number} num 数值 1726 | * @param {string} unit 单位:支持年、月、天等等 1727 | * @namespace 日期函数 1728 | * @example DATEMODIFY(date, 2, 'days') 1729 | * @returns {date} 日期对象 1730 | */ 1731 | fnDATEMODIFY(date: Date, num: number, format: any) { 1732 | date = this.normalizeDate(date); 1733 | return moment(date).add(num, format).toDate(); 1734 | } 1735 | 1736 | /** 1737 | * 将字符日期转成日期对象,可以指定日期格式。 1738 | * 1739 | * 示例:STRTODATE('2021/12/6', 'YYYY/MM/DD') 1740 | * 1741 | * @param {string} value 日期字符 1742 | * @param {string} format 日期格式 1743 | * @namespace 日期函数 1744 | * @example STRTODATE(value[, format=""]) 1745 | * @returns {date} 日期对象 1746 | */ 1747 | fnSTRTODATE(value: any, format: string = '') { 1748 | return moment(value, format).toDate(); 1749 | } 1750 | 1751 | /** 1752 | * 判断两个日期,是否第一个日期在第二个日期的前面 1753 | * 1754 | * @param {date} a 第一个日期 1755 | * @param {date} b 第二个日期 1756 | * @param {string} unit 单位,默认是 'day', 即之比较到天 1757 | * @namespace 日期函数 1758 | * @example ISBEFORE(a, b) 1759 | * @returns {boolean} 判断结果 1760 | */ 1761 | fnISBEFORE(a: Date, b: Date, unit: any = 'day') { 1762 | a = this.normalizeDate(a); 1763 | b = this.normalizeDate(b); 1764 | return moment(a).isBefore(moment(b), unit); 1765 | } 1766 | 1767 | /** 1768 | * 判断两个日期,是否第一个日期在第二个日期的后面 1769 | * 1770 | * @param {date} a 第一个日期 1771 | * @param {date} b 第二个日期 1772 | * @param {string} unit 单位,默认是 'day', 即之比较到天 1773 | * @namespace 日期函数 1774 | * @example ISAFTER(a, b) 1775 | * @returns {boolean} 判断结果 1776 | */ 1777 | fnISAFTER(a: Date, b: Date, unit: any = 'day') { 1778 | a = this.normalizeDate(a); 1779 | b = this.normalizeDate(b); 1780 | return moment(a).isAfter(moment(b), unit); 1781 | } 1782 | 1783 | /** 1784 | * 判断两个日期,是否第一个日期在第二个日期的前面或者相等 1785 | * 1786 | * @param {date} a 第一个日期 1787 | * @param {date} b 第二个日期 1788 | * @param {string} unit 单位,默认是 'day', 即之比较到天 1789 | * @namespace 日期函数 1790 | * @example ISSAMEORBEFORE(a, b) 1791 | * @returns {boolean} 判断结果 1792 | */ 1793 | fnISSAMEORBEFORE(a: Date, b: Date, unit: any = 'day') { 1794 | a = this.normalizeDate(a); 1795 | b = this.normalizeDate(b); 1796 | return moment(a).isSameOrBefore(moment(b), unit); 1797 | } 1798 | 1799 | /** 1800 | * 判断两个日期,是否第一个日期在第二个日期的后面或者相等 1801 | * 1802 | * @param {date} a 第一个日期 1803 | * @param {date} b 第二个日期 1804 | * @param {string} unit 单位,默认是 'day', 即之比较到天 1805 | * @namespace 日期函数 1806 | * @example ISSAMEORAFTER(a, b) 1807 | * @returns {boolean} 判断结果 1808 | */ 1809 | fnISSAMEORAFTER(a: Date, b: Date, unit: any = 'day') { 1810 | a = this.normalizeDate(a); 1811 | b = this.normalizeDate(b); 1812 | return moment(a).isSameOrAfter(moment(b), unit); 1813 | } 1814 | 1815 | /** 1816 | * 返回数组的长度 1817 | * 1818 | * @param {Array} arr 数组 1819 | * @namespace 数组 1820 | * @example COUNT(arr) 1821 | * @returns {boolean} 结果 1822 | */ 1823 | fnCOUNT(value: any) { 1824 | return Array.isArray(value) ? value.length : value ? 1 : 0; 1825 | } 1826 | 1827 | /** 1828 | * 数组做数据转换,需要搭配箭头函数一起使用,注意箭头函数只支持单表达式用法。 1829 | * 1830 | * @param {Array} arr 数组 1831 | * @param {Function} iterator 箭头函数 1832 | * @namespace 数组 1833 | * @example ARRAYMAP(arr, item => item) 1834 | * @returns {boolean} 结果 1835 | */ 1836 | fnARRAYMAP(value: any, iterator: any) { 1837 | if (!iterator || iterator.type !== 'anonymous_function') { 1838 | throw new Error('expected an anonymous function get ' + iterator); 1839 | } 1840 | 1841 | return (Array.isArray(value) ? value : []).map((item, index) => 1842 | this.callAnonymousFunction(iterator, [item, index]) 1843 | ); 1844 | } 1845 | 1846 | /** 1847 | * 数组过滤掉 false、null、0 和 "" 1848 | * 1849 | * 示例: 1850 | * 1851 | * COMPACT([0, 1, false, 2, '', 3]) 得到 [1, 2, 3] 1852 | * 1853 | * @param {Array} arr 数组 1854 | * @namespace 数组 1855 | * @example COMPACT(arr) 1856 | * @returns {Array} 结果 1857 | */ 1858 | fnCOMPACT(arr: any[]) { 1859 | if (Array.isArray(arr)) { 1860 | let resIndex = 0; 1861 | const result = []; 1862 | for (const item of arr) { 1863 | if (item) { 1864 | result[resIndex++] = item; 1865 | } 1866 | } 1867 | return result; 1868 | } else { 1869 | return []; 1870 | } 1871 | } 1872 | 1873 | /** 1874 | * 数组转成字符串 1875 | * 1876 | * 示例: 1877 | * 1878 | * JOIN(['a', 'b', 'c'], '~') 得到 'a~b~c' 1879 | * 1880 | * @param {Array} arr 数组 1881 | * @param { String} separator 分隔符 1882 | * @namespace 数组 1883 | * @example JOIN(arr, string) 1884 | * @returns {String} 结果 1885 | */ 1886 | fnJOIN(arr: any[], separator = '') { 1887 | if (Array.isArray(arr)) { 1888 | return arr.join(separator); 1889 | } else { 1890 | return ''; 1891 | } 1892 | } 1893 | } 1894 | 1895 | function getCookie(name: string) { 1896 | const value = `; ${document.cookie}`; 1897 | const parts = value.split(`; ${name}=`); 1898 | if (parts.length === 2) { 1899 | return parts.pop()!.split(';').shift(); 1900 | } 1901 | return undefined; 1902 | } 1903 | 1904 | function parseJson(str: string, defaultValue?: any) { 1905 | try { 1906 | return JSON.parse(str); 1907 | } catch (e) { 1908 | return defaultValue; 1909 | } 1910 | } 1911 | -------------------------------------------------------------------------------- /src/filter.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import transform from 'lodash/transform'; 3 | import groupBy from 'lodash/groupBy'; 4 | import uniqBy from 'lodash/uniqBy'; 5 | import uniq from 'lodash/uniq'; 6 | import { 7 | createObject, 8 | escapeHtml, 9 | filterDate, 10 | formatDuration, 11 | pickValues, 12 | prettyBytes, 13 | resolveVariable, 14 | string2regExp, 15 | stripNumber 16 | } from './util'; 17 | import {Evaluator} from './evalutor'; 18 | import type {FilterContext, FilterMap} from './evalutor'; 19 | 20 | function makeSorter( 21 | key: string, 22 | method?: 'alpha' | 'numerical', 23 | order?: 'desc' | 'asc' 24 | ) { 25 | return function (a: any, b: any) { 26 | if (!a || !b) { 27 | return 0; 28 | } 29 | 30 | const va = resolveVariable(key, a); 31 | const vb = resolveVariable(key, b); 32 | let result = 0; 33 | 34 | if (method === 'numerical') { 35 | result = (parseFloat(va) || 0) - (parseFloat(vb) || 0); 36 | } else { 37 | result = String(va).localeCompare(String(vb)); 38 | } 39 | 40 | return result * (order === 'desc' ? -1 : 1); 41 | }; 42 | } 43 | 44 | export const filters: FilterMap = { 45 | map(input: Array, fn: string, ...arg: any) { 46 | return Array.isArray(input) && filters[fn] 47 | ? input.map(item => filters[fn].call(this, item, ...arg)) 48 | : input 49 | }, 50 | html: (input: string) => { 51 | if (input == null) { 52 | return input; 53 | } 54 | return escapeHtml(input); 55 | }, 56 | json: (input, tabSize: number | string = 2) => 57 | tabSize 58 | ? JSON.stringify(input, null, parseInt(tabSize as string, 10)) 59 | : JSON.stringify(input), 60 | toJson: input => { 61 | let ret; 62 | try { 63 | ret = JSON.parse(input); 64 | } catch (e) { 65 | ret = null; 66 | } 67 | return ret; 68 | }, 69 | toInt: input => (typeof input === 'string' ? parseInt(input, 10) : input), 70 | toFloat: input => (typeof input === 'string' ? parseFloat(input) : input), 71 | raw: input => input, 72 | now: () => new Date(), 73 | toDate: (input: any, inputFormat = '') => { 74 | const date = moment(input, inputFormat); 75 | return date.isValid() ? date.toDate() : undefined; 76 | }, 77 | fromNow: (input: any, inputFormat = '') => 78 | moment(input, inputFormat).fromNow(), 79 | dateModify: ( 80 | input: any, 81 | modifier: 'add' | 'subtract' | 'endOf' | 'startOf' = 'add', 82 | amount = 0, 83 | unit = 'days' 84 | ) => { 85 | if (!(input instanceof Date)) { 86 | input = new Date(); 87 | } 88 | 89 | if (modifier === 'endOf' || modifier === 'startOf') { 90 | return moment(input) 91 | [modifier === 'endOf' ? 'endOf' : 'startOf'](amount || 'day') 92 | .toDate(); 93 | } 94 | 95 | return moment(input) 96 | [modifier === 'add' ? 'add' : 'subtract'](parseInt(amount, 10) || 0, unit) 97 | .toDate(); 98 | }, 99 | date: (input, format = 'LLL', inputFormat = 'X') => 100 | moment(input, inputFormat).format(format), 101 | number: input => { 102 | let parts = String(input).split('.'); 103 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); 104 | return parts.join('.'); 105 | }, 106 | trim: input => (typeof input === 'string' ? input.trim() : input), 107 | percent: (input, decimals = 0) => { 108 | input = parseFloat(input) || 0; 109 | decimals = parseInt(decimals, 10) || 0; 110 | 111 | let whole = input * 100; 112 | let multiplier = Math.pow(10, decimals); 113 | 114 | return ( 115 | (Math.round(whole * multiplier) / multiplier).toFixed(decimals) + '%' 116 | ); 117 | }, 118 | duration: input => (input ? formatDuration(input) : input), 119 | bytes: input => (input ? prettyBytes(parseFloat(input)) : input), 120 | round: (input, decimals = 2) => { 121 | if (isNaN(input)) { 122 | return 0; 123 | } 124 | 125 | decimals = parseInt(decimals, 10) ?? 2; 126 | 127 | let multiplier = Math.pow(10, decimals); 128 | return (Math.round(input * multiplier) / multiplier).toFixed(decimals); 129 | }, 130 | truncate: (input, length, end) => { 131 | if (typeof input !== 'string') { 132 | return input; 133 | } 134 | 135 | end = end || '...'; 136 | 137 | if (length == null) { 138 | return input; 139 | } 140 | 141 | length = parseInt(length, 10) || 200; 142 | 143 | return input.substring(0, length) + (input.length > length ? end : ''); 144 | }, 145 | url_encode: input => { 146 | if (input == null) { 147 | return ''; 148 | } 149 | return encodeURIComponent(input); 150 | }, 151 | url_decode: input => decodeURIComponent(input), 152 | default: (input, defaultValue, strict = false) => 153 | (strict ? input : input ? input : undefined) ?? 154 | (() => { 155 | try { 156 | if (defaultValue === 'undefined') { 157 | return undefined; 158 | } 159 | 160 | return JSON.parse(defaultValue); 161 | } catch (e) { 162 | return defaultValue; 163 | } 164 | })(), 165 | join: (input, glue) => (input && input.join ? input.join(glue) : input), 166 | split: (input, delimiter = ',') => 167 | typeof input === 'string' ? input.split(delimiter) : input, 168 | sortBy: ( 169 | input: any, 170 | key: string = '&', 171 | method: 'alpha' | 'numerical' = 'alpha', 172 | order?: 'asc' | 'desc' 173 | ) => 174 | Array.isArray(input) ? input.sort(makeSorter(key, method, order)) : input, 175 | objectToArray: ( 176 | input: any, 177 | label: string = 'label', 178 | value: string = 'value' 179 | ) => 180 | transform( 181 | input, 182 | (result: any, v, k) => { 183 | (result || (result = [])).push({ 184 | [label]: v, 185 | [value]: k 186 | }); 187 | }, 188 | [] 189 | ), 190 | unique: (input: any, key?: string) => 191 | Array.isArray(input) ? (key ? uniqBy(input, key) : uniq(input)) : input, 192 | topAndOther: ( 193 | input: any, 194 | len: number = 10, 195 | labelField: string = 'name', 196 | restLabel = '其他' 197 | ) => { 198 | if (Array.isArray(input) && len) { 199 | const grouped = groupBy(input, (item: any) => { 200 | const index = input.indexOf(item) + 1; 201 | return index >= len ? len : index; 202 | }); 203 | 204 | return Object.keys(grouped).map((key, index) => { 205 | const group = grouped[key]; 206 | const obj = group.reduce((obj, item) => { 207 | Object.keys(item).forEach(key => { 208 | if (!obj.hasOwnProperty(key) || key === 'labelField') { 209 | obj[key] = item[key]; 210 | } else if ( 211 | typeof item[key] === 'number' && 212 | typeof obj[key] === 'number' 213 | ) { 214 | obj[key] += item[key]; 215 | } else if ( 216 | typeof item[key] === 'string' && 217 | /^(?:\-|\.)\d/.test(item[key]) && 218 | typeof obj[key] === 'number' 219 | ) { 220 | obj[key] += parseFloat(item[key]) || 0; 221 | } else if ( 222 | typeof item[key] === 'string' && 223 | typeof obj[key] === 'string' 224 | ) { 225 | obj[key] += `, ${item[key]}`; 226 | } else { 227 | obj[key] = item[key]; 228 | } 229 | }); 230 | 231 | return obj; 232 | }, {}); 233 | 234 | if (index === len - 1) { 235 | obj[labelField] = restLabel || '其他'; 236 | } 237 | return obj; 238 | }); 239 | } 240 | return input; 241 | }, 242 | first: input => input && input[0], 243 | nth: (input, nth = 0) => input && input[nth], 244 | last: input => input && (input.length ? input[input.length - 1] : null), 245 | minus(input, step = 1) { 246 | return stripNumber( 247 | (Number(input) || 0) - 248 | Number(getStrOrVariable(step, this.data, this.filter?.args[0])) 249 | ); 250 | }, 251 | plus(input, step = 1) { 252 | return stripNumber( 253 | (Number(input) || 0) + 254 | Number(getStrOrVariable(step, this.data, this.filter?.args[0])) 255 | ); 256 | }, 257 | times(input, step = 1) { 258 | return stripNumber( 259 | (Number(input) || 0) * 260 | Number(getStrOrVariable(step, this.data, this.filter?.args[0])) 261 | ); 262 | }, 263 | division(input, step = 1) { 264 | return stripNumber( 265 | (Number(input) || 0) / 266 | Number(getStrOrVariable(step, this.data, this.filter?.args[0])) 267 | ); 268 | }, 269 | count: (input: any) => 270 | Array.isArray(input) || typeof input === 'string' ? input.length : 0, 271 | sum: (input, field) => { 272 | if (!Array.isArray(input)) { 273 | return input; 274 | } 275 | const restult = input.reduce( 276 | (sum, item) => 277 | sum + (parseFloat(field ? pickValues(field, item) : item) || 0), 278 | 0 279 | ); 280 | return stripNumber(restult); 281 | }, 282 | abs: (input: any) => (typeof input === 'number' ? Math.abs(input) : input), 283 | pick: (input, path = '&') => 284 | Array.isArray(input) && !/^\d+$/.test(path) 285 | ? input.map((item, index) => 286 | pickValues(path, createObject({index}, item)) 287 | ) 288 | : pickValues(path, input), 289 | pick_if_exist: (input, path = '&') => 290 | Array.isArray(input) 291 | ? input.map(item => resolveVariable(path, item) || item) 292 | : resolveVariable(path, input) || input, 293 | str2date: function (input, inputFormat = 'X', outputFormat = 'X') { 294 | return input 295 | ? filterDate(input, this.data, inputFormat).format(outputFormat) 296 | : ''; 297 | }, 298 | asArray: input => (Array.isArray(input) ? input : input ? [input] : input), 299 | concat(input, ...args: any[]) { 300 | return Array.isArray(input) 301 | ? input.concat( 302 | ...args.map((arg, index) => 303 | getStrOrVariable(arg, this.data, this.filter?.args[index]) 304 | ) 305 | ) 306 | : input; 307 | }, 308 | filter: function (input, keys, expOrDirective, arg1) { 309 | if (!Array.isArray(input) || !keys || !expOrDirective) { 310 | return input; 311 | } 312 | 313 | let directive = expOrDirective; 314 | let fn: (value: any, key: string, item: any) => boolean = () => true; 315 | 316 | if (directive === 'isTrue') { 317 | fn = value => !!value; 318 | } else if (directive === 'isFalse') { 319 | fn = value => !value; 320 | } else if (directive === 'isExists') { 321 | fn = value => typeof value !== 'undefined'; 322 | } else if (directive === 'equals' || directive === 'equal') { 323 | arg1 = arg1 324 | ? getStrOrVariable(arg1, this.data, this.filter?.args[2]) 325 | : ''; 326 | fn = value => arg1 == value; 327 | } else if (directive === 'isIn') { 328 | let list: any = arg1 329 | ? getStrOrVariable(arg1, this.data, this.filter?.args[2]) 330 | : []; 331 | 332 | list = str2array(list); 333 | list = Array.isArray(list) ? list : list ? [list] : []; 334 | fn = value => (list.length ? !!~list.indexOf(value) : true); 335 | } else if (directive === 'notIn') { 336 | let list: Array = arg1 337 | ? getStrOrVariable(arg1, this.data, this.filter?.args[2]) 338 | : []; 339 | list = str2array(list); 340 | list = Array.isArray(list) ? list : list ? [list] : []; 341 | fn = value => !~list.indexOf(value); 342 | } else { 343 | if (directive !== 'match') { 344 | directive = 'match'; 345 | arg1 = expOrDirective; 346 | } 347 | arg1 = arg1 348 | ? getStrOrVariable(arg1, this.data, this.filter?.args[2]) 349 | : ''; 350 | 351 | // 比对的值是空时直接返回。 352 | if (!arg1) { 353 | return input; 354 | } 355 | 356 | let reg = string2regExp(`${arg1}`, false); 357 | fn = value => reg.test(String(value)); 358 | } 359 | 360 | // 判断keys是否为* 361 | const isAsterisk = /\s*\*\s*/.test(keys); 362 | keys = keys.split(/\s*,\s*/); 363 | return input.filter((item: any) => 364 | // 当keys为*时从item中获取key 365 | (isAsterisk ? Object.keys(item) : keys).some((key: string) => 366 | fn(resolveVariable(key, item), key, item) 367 | ) 368 | ); 369 | }, 370 | base64Encode(str) { 371 | return btoa( 372 | encodeURIComponent(str).replace( 373 | /%([0-9A-F]{2})/g, 374 | function toSolidBytes(match, p1) { 375 | return String.fromCharCode(('0x' + p1) as any); 376 | } 377 | ) 378 | ); 379 | }, 380 | 381 | base64Decode(str) { 382 | return decodeURIComponent( 383 | atob(str) 384 | .split('') 385 | .map(function (c) { 386 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 387 | }) 388 | .join('') 389 | ); 390 | }, 391 | 392 | substring: (input, start, end) => { 393 | return input && typeof input === 'string' 394 | ? input.substring(start, end) 395 | : input; 396 | }, 397 | 398 | lowerCase: input => 399 | input && typeof input === 'string' ? input.toLowerCase() : input, 400 | upperCase: input => 401 | input && typeof input === 'string' ? input.toUpperCase() : input, 402 | 403 | isTrue(input, trueValue, falseValue) { 404 | const hasAlternate = arguments.length > 2; 405 | return conditionalFilter( 406 | input, 407 | hasAlternate, 408 | this, 409 | !!input, 410 | trueValue, 411 | falseValue 412 | ); 413 | }, 414 | isFalse(input, trueValue, falseValue) { 415 | const hasAlternate = arguments.length > 2; 416 | return conditionalFilter( 417 | input, 418 | hasAlternate, 419 | this, 420 | !input, 421 | trueValue, 422 | falseValue 423 | ); 424 | }, 425 | isMatch(input, matchArg, trueValue, falseValue) { 426 | const hasAlternate = arguments.length > 3; 427 | matchArg = 428 | getStrOrVariable(matchArg, this.data as any, this.filter?.args[0]) ?? 429 | matchArg; 430 | return conditionalFilter( 431 | input, 432 | hasAlternate, 433 | this, 434 | matchArg && string2regExp(`${matchArg}`, false).test(String(input)), 435 | trueValue, 436 | falseValue 437 | ); 438 | }, 439 | notMatch(input, matchArg, trueValue, falseValue) { 440 | const hasAlternate = arguments.length > 3; 441 | matchArg = 442 | getStrOrVariable(matchArg, this.data as any, this.filter?.args[0]) ?? 443 | matchArg; 444 | return conditionalFilter( 445 | input, 446 | hasAlternate, 447 | this, 448 | matchArg && !string2regExp(`${matchArg}`, false).test(String(input)), 449 | trueValue, 450 | falseValue 451 | ); 452 | }, 453 | isEquals(input, equalsValue, trueValue, falseValue) { 454 | equalsValue = 455 | getStrOrVariable(equalsValue, this.data as any, this.filter?.args[0]) ?? 456 | equalsValue; 457 | 458 | const hasAlternate = arguments.length > 3; 459 | return conditionalFilter( 460 | input, 461 | hasAlternate, 462 | this, 463 | input === equalsValue, 464 | trueValue, 465 | falseValue 466 | ); 467 | }, 468 | notEquals(input, equalsValue, trueValue, falseValue) { 469 | equalsValue = 470 | getStrOrVariable(equalsValue, this.data as any, this.filter?.args[0]) ?? 471 | equalsValue; 472 | 473 | const hasAlternate = arguments.length > 3; 474 | return conditionalFilter( 475 | input, 476 | hasAlternate, 477 | this, 478 | input !== equalsValue, 479 | trueValue, 480 | falseValue 481 | ); 482 | } 483 | }; 484 | 485 | function conditionalFilter( 486 | input: any, 487 | hasAlternate: boolean, 488 | filterContext: FilterContext, 489 | test: any, 490 | trueValue: any, 491 | falseValue: any 492 | ) { 493 | (hasAlternate || test) && skipRestTest(filterContext.restFilters); 494 | const result = test ? trueValue : falseValue; 495 | const ast = test 496 | ? filterContext.filter?.args[1] 497 | : filterContext.filter?.args[2]; 498 | 499 | return test || hasAlternate 500 | ? getStrOrVariable(result, filterContext.data, ast) ?? result 501 | : input; 502 | } 503 | 504 | /** 505 | * 如果当前传入字符为:'xxx'或者"xxx",则返回字符xxx 506 | * 否则去数据域中,获取变量xxx 507 | * 508 | * @param value 传入字符 509 | * @param data 数据域 510 | */ 511 | function getStrOrVariable(value: any, data: any, ast?: any) { 512 | // 通过读取 ast 来判断,只有 literal 才可能是变量,也可能是字符串 513 | // 其他的直接返回值即可。 514 | if (ast?.type && ast.type !== 'literal') { 515 | return value; 516 | } 517 | 518 | return typeof value === 'string' && /,/.test(value) 519 | ? value.split(/\s*,\s*/).filter(item => item) 520 | : typeof value === 'string' 521 | ? resolveVariable(value, data) 522 | : value; 523 | } 524 | 525 | function str2array(list: any) { 526 | if (list && typeof list === 'string') { 527 | if (/^\[.*\]$/.test(list)) { 528 | return list 529 | .substring(1, list.length - 1) 530 | .split(/\s*,\s*/) 531 | .filter(item => item); 532 | } else { 533 | return list.split(/\s*,\s*/).filter(item => item); 534 | } 535 | } 536 | return list; 537 | } 538 | 539 | function skipRestTest(restFilters: Array<{name: string}>) { 540 | while ( 541 | ~[ 542 | 'isTrue', 543 | 'isFalse', 544 | 'isMatch', 545 | 'isEquals', 546 | 'notMatch', 547 | 'notEquals' 548 | ].indexOf(restFilters[0]?.name) 549 | ) { 550 | restFilters.shift(); 551 | } 552 | } 553 | 554 | export function registerFilter( 555 | name: string, 556 | fn: (input: any, ...args: any[]) => any 557 | ): void { 558 | filters[name] = fn; 559 | Evaluator.setDefaultFilters(filters); 560 | } 561 | 562 | export function getFilters() { 563 | return filters; 564 | } 565 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {Evaluator, EvaluatorOptions} from './evalutor'; 2 | import {ASTNode, parse, ParserOptions} from './parser'; 3 | import {lexer} from './lexer'; 4 | import {registerFilter, filters, getFilters} from './filter'; 5 | export {parse, lexer, Evaluator, filters, getFilters, registerFilter}; 6 | export * from './util'; 7 | 8 | export function evaluate( 9 | astOrString: string | ASTNode, 10 | data: any, 11 | options?: ParserOptions & EvaluatorOptions 12 | ) { 13 | let ast: ASTNode = astOrString as ASTNode; 14 | if (typeof astOrString === 'string') { 15 | ast = parse(astOrString, options); 16 | } 17 | 18 | return new Evaluator(data, options).evalute(ast); 19 | } 20 | 21 | Evaluator.setDefaultFilters(getFilters()); 22 | -------------------------------------------------------------------------------- /src/lexer.ts: -------------------------------------------------------------------------------- 1 | export interface LexerOptions { 2 | /** 3 | * 直接是运算表达式?还是从模板开始 ${} 里面才算运算表达式 4 | */ 5 | evalMode?: boolean; 6 | 7 | /** 8 | * 只支持取变量。 9 | */ 10 | variableMode?: boolean; 11 | 12 | /** 13 | * 是否允许 filter 语法,比如: 14 | * 15 | * ${abc | html} 16 | */ 17 | allowFilter?: boolean; 18 | } 19 | 20 | export const enum TokenEnum { 21 | BooleanLiteral = 1, 22 | RAW, 23 | Variable, 24 | OpenScript, 25 | CloseScript, 26 | EOF, 27 | Identifier, 28 | Literal, 29 | NumericLiteral, 30 | Punctuator, 31 | StringLiteral, 32 | RegularExpression, 33 | TemplateRaw, 34 | TemplateLeftBrace, 35 | TemplateRightBrace, 36 | OpenFilter, 37 | Char 38 | } 39 | 40 | export type TokenTypeName = 41 | | 'Boolean' 42 | | 'Raw' 43 | | 'Variable' 44 | | 'OpenScript' 45 | | 'CloseScript' 46 | | 'EOF' 47 | | 'Identifier' 48 | | 'Literal' 49 | | 'Numeric' 50 | | 'Punctuator' 51 | | 'String' 52 | | 'RegularExpression' 53 | | 'TemplateRaw' 54 | | 'TemplateLeftBrace' 55 | | 'TemplateRightBrace' 56 | | 'OpenFilter' 57 | | 'Char'; 58 | 59 | export const TokenName: { 60 | [propName: string]: TokenTypeName; 61 | } = {}; 62 | TokenName[TokenEnum.BooleanLiteral] = 'Boolean'; 63 | TokenName[TokenEnum.RAW] = 'Raw'; 64 | TokenName[TokenEnum.Variable] = 'Variable'; 65 | TokenName[TokenEnum.OpenScript] = 'OpenScript'; 66 | TokenName[TokenEnum.CloseScript] = 'CloseScript'; 67 | TokenName[TokenEnum.EOF] = 'EOF'; 68 | TokenName[TokenEnum.Identifier] = 'Identifier'; 69 | TokenName[TokenEnum.Literal] = 'Literal'; 70 | TokenName[TokenEnum.NumericLiteral] = 'Numeric'; 71 | TokenName[TokenEnum.Punctuator] = 'Punctuator'; 72 | TokenName[TokenEnum.StringLiteral] = 'String'; 73 | TokenName[TokenEnum.RegularExpression] = 'RegularExpression'; 74 | TokenName[TokenEnum.TemplateRaw] = 'TemplateRaw'; 75 | TokenName[TokenEnum.TemplateLeftBrace] = 'TemplateLeftBrace'; 76 | TokenName[TokenEnum.TemplateRightBrace] = 'TemplateRightBrace'; 77 | TokenName[TokenEnum.OpenFilter] = 'OpenFilter'; 78 | TokenName[TokenEnum.Char] = 'Char'; 79 | 80 | export interface Position { 81 | index: number; 82 | line: number; 83 | column: number; 84 | } 85 | 86 | export interface Token { 87 | type: TokenTypeName; 88 | value: any; 89 | raw?: string; 90 | start: Position; 91 | end: Position; 92 | } 93 | 94 | const mainStates = { 95 | START: 0, 96 | SCRIPT: 1, 97 | EXPRESSION: 2, 98 | BLOCK: 3, 99 | Template: 4, 100 | Filter: 5 101 | }; 102 | 103 | const rawStates = { 104 | START: 0, 105 | ESCAPE: 1 106 | }; 107 | 108 | const numberStates = { 109 | START: 0, 110 | ZERO: 1, 111 | DIGIT: 2, 112 | POINT: 3, 113 | DIGIT_FRACTION: 4, 114 | EXP: 5 115 | }; 116 | 117 | const stringStates = { 118 | START: 0, 119 | START_QUOTE_OR_CHAR: 1, 120 | ESCAPE: 2 121 | }; 122 | 123 | const filterStates = { 124 | START: 0, 125 | Func: 1, 126 | SEP: 2, 127 | ESCAPE: 3 128 | }; 129 | 130 | const punctuatorList = [ 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 | const escapes = { 177 | '"': 0, // Quotation mask 178 | '\\': 1, // Reverse solidus 179 | '/': 2, // Solidus 180 | 'b': 3, // Backspace 181 | 'f': 4, // Form feed 182 | 'n': 5, // New line 183 | 'r': 6, // Carriage return 184 | 't': 7, // Horizontal tab 185 | 'u': 8 // 4 hexadecimal digits 186 | }; 187 | 188 | function isDigit1to9(char: string) { 189 | return char >= '1' && char <= '9'; 190 | } 191 | 192 | function isDigit(char: string) { 193 | return char >= '0' && char <= '9'; 194 | } 195 | 196 | function isExp(char: string) { 197 | return char === 'e' || char === 'E'; 198 | } 199 | 200 | function escapeString(text: string, allowedLetter: Array = []) { 201 | return text.replace(/\\(.)/g, function (_, text) { 202 | return text === 'b' 203 | ? '\b' 204 | : text === 'f' 205 | ? '\f' 206 | : text === 'n' 207 | ? '\n' 208 | : text === 'r' 209 | ? '\r' 210 | : text === 't' 211 | ? '\t' 212 | : text === 'v' 213 | ? '\v' 214 | : ~allowedLetter.indexOf(text) 215 | ? text 216 | : _; 217 | }); 218 | } 219 | 220 | function formatNumber(value: string) { 221 | return Number(value); 222 | } 223 | 224 | export function lexer(input: string, options?: LexerOptions) { 225 | let line = 1; 226 | let column = 1; 227 | let index = 0; 228 | let mainState = mainStates.START; 229 | const states: Array = [mainState]; 230 | let tokenCache: Array = []; 231 | const allowFilter = options?.allowFilter !== false; 232 | 233 | if (options?.evalMode || options?.variableMode) { 234 | pushState(mainStates.EXPRESSION); 235 | } 236 | 237 | function pushState(state: any) { 238 | states.push((mainState = state)); 239 | } 240 | function popState() { 241 | states.pop(); 242 | mainState = states[states.length - 1]; 243 | } 244 | 245 | function position(value?: string) { 246 | if (value && typeof value === 'string') { 247 | const lines = value.split(/[\r\n]+/); 248 | return { 249 | index: index + value.length, 250 | line: line + lines.length - 1, 251 | column: column + lines[lines.length - 1].length 252 | }; 253 | } 254 | 255 | return {index: index, line, column}; 256 | } 257 | 258 | function eof(): Token | void | null { 259 | if (index >= input.length) { 260 | return { 261 | type: TokenName[TokenEnum.EOF], 262 | value: undefined, 263 | start: position(), 264 | end: position() 265 | }; 266 | } 267 | } 268 | 269 | function raw(): Token | void | null { 270 | if (mainState !== mainStates.START) { 271 | return null; 272 | } 273 | 274 | let buffer = ''; 275 | let state = rawStates.START; 276 | let i = index; 277 | 278 | while (i < input.length) { 279 | const ch = input[i]; 280 | 281 | if (state === rawStates.ESCAPE) { 282 | if (escapes.hasOwnProperty(ch) || ch === '$') { 283 | buffer += ch; 284 | i++; 285 | state = rawStates.START; 286 | } else { 287 | const pos = position(buffer + ch); 288 | throw new SyntaxError( 289 | `Unexpected token ${ch} in ${pos.line}:${pos.column}` 290 | ); 291 | } 292 | } else { 293 | if (ch === '\\') { 294 | buffer += ch; 295 | i++; 296 | state = rawStates.ESCAPE; 297 | continue; 298 | } else if (ch === '$') { 299 | const nextCh = input[i + 1]; 300 | if (nextCh === '{') { 301 | break; 302 | } else if (nextCh === '$') { 303 | // $$ 用法兼容 304 | tokenCache.push({ 305 | type: TokenName[TokenEnum.Variable], 306 | value: '&', 307 | raw: '$$', 308 | start: position(input.substring(index, i)), 309 | end: position(input.substring(index, i + 2)) 310 | }); 311 | break; 312 | } else { 313 | // 支持旧的 $varName 的取值方法 314 | const match = /^[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*/.exec( 315 | input.substring(i + 1) 316 | ); 317 | if (match) { 318 | tokenCache.push({ 319 | type: TokenName[TokenEnum.Variable], 320 | value: match[0], 321 | raw: match[0], 322 | start: position(input.substring(index, i)), 323 | end: position(input.substring(index, i + 1 + match[0].length)) 324 | }); 325 | break; 326 | } 327 | } 328 | } 329 | i++; 330 | buffer += ch; 331 | } 332 | } 333 | 334 | if (i > index) { 335 | return { 336 | type: TokenName[TokenEnum.RAW], 337 | value: escapeString(buffer, ['`', '$']), 338 | raw: buffer, 339 | start: position(), 340 | end: position(buffer) 341 | }; 342 | } 343 | return tokenCache.length ? tokenCache.shift() : null; 344 | } 345 | 346 | function openScript() { 347 | if (mainState === mainStates.Template) { 348 | return null; 349 | } 350 | 351 | const ch = input[index]; 352 | if (ch === '$') { 353 | const nextCh = input[index + 1]; 354 | if (nextCh === '{') { 355 | pushState(mainStates.SCRIPT); 356 | const value = input.substring(index, index + 2); 357 | return { 358 | type: TokenName[TokenEnum.OpenScript], 359 | value, 360 | start: position(), 361 | end: position(value) 362 | }; 363 | } 364 | } 365 | return null; 366 | } 367 | 368 | function expression() { 369 | if ( 370 | mainState !== mainStates.SCRIPT && 371 | mainState !== mainStates.EXPRESSION && 372 | mainState !== mainStates.BLOCK && 373 | mainState !== mainStates.Filter 374 | ) { 375 | return null; 376 | } 377 | 378 | const token = 379 | literal() || 380 | identifier() || 381 | numberLiteral() || 382 | stringLiteral() || 383 | punctuator() || 384 | char(); 385 | 386 | if (token?.value === '{') { 387 | pushState(mainStates.BLOCK); 388 | } else if (token?.value === '}') { 389 | if (mainState === mainStates.Filter) { 390 | popState(); 391 | } 392 | 393 | const prevState = mainState; 394 | popState(); 395 | 396 | if ( 397 | prevState === mainStates.SCRIPT || 398 | prevState === mainStates.EXPRESSION 399 | ) { 400 | return { 401 | type: TokenName[ 402 | prevState === mainStates.EXPRESSION 403 | ? TokenEnum.TemplateRightBrace 404 | : TokenEnum.CloseScript 405 | ], 406 | value: token!.value, 407 | start: position(), 408 | end: position(token!.value) 409 | }; 410 | } 411 | } 412 | 413 | // filter 过滤器部分需要特殊处理 414 | if ( 415 | mainState === mainStates.SCRIPT && 416 | token?.value === '|' && 417 | allowFilter 418 | ) { 419 | pushState(mainStates.Filter); 420 | return { 421 | type: TokenName[TokenEnum.OpenFilter], 422 | value: '|', 423 | start: position(), 424 | end: position('|') 425 | }; 426 | } else if (mainState === mainStates.Filter && token?.value === '|') { 427 | return { 428 | type: TokenName[TokenEnum.OpenFilter], 429 | value: '|', 430 | start: position(), 431 | end: position('|') 432 | }; 433 | } 434 | 435 | if (!token && input[index] === '`') { 436 | pushState(mainStates.Template); 437 | return { 438 | type: TokenName[TokenEnum.Punctuator], 439 | value: '`', 440 | start: position(), 441 | end: position('`') 442 | }; 443 | } 444 | 445 | return token; 446 | } 447 | 448 | function char() { 449 | if (mainState !== mainStates.Filter) { 450 | return null; 451 | } 452 | 453 | let i = index; 454 | let ch = input[i]; 455 | if (ch === '\\') { 456 | const nextCh = input[i + 1]; 457 | 458 | if ( 459 | nextCh === '$' || 460 | ~punctuatorList.indexOf(nextCh) || 461 | escapes.hasOwnProperty(nextCh) 462 | ) { 463 | i++; 464 | ch = 465 | nextCh === 'b' 466 | ? '\b' 467 | : nextCh === 'f' 468 | ? '\f' 469 | : nextCh === 'n' 470 | ? '\n' 471 | : nextCh === 'r' 472 | ? '\r' 473 | : nextCh === 't' 474 | ? '\t' 475 | : nextCh === 'v' 476 | ? '\v' 477 | : nextCh; 478 | } else { 479 | const pos = position(input.substring(index, index + 2)); 480 | throw new SyntaxError( 481 | `Unexpected token ${nextCh} in ${pos.line}:${pos.column}` 482 | ); 483 | } 484 | } 485 | const token = { 486 | type: TokenName[TokenEnum.Char], 487 | value: ch, 488 | start: position(), 489 | end: position(input.substring(index, i + 1)) 490 | }; 491 | return token; 492 | } 493 | 494 | function template(): Token | void | null { 495 | if (mainState !== mainStates.Template) { 496 | return null; 497 | } 498 | let state = stringStates.START; 499 | let i = index; 500 | while (i < input.length) { 501 | const ch = input[i]; 502 | 503 | if (state === stringStates.ESCAPE) { 504 | if (escapes.hasOwnProperty(ch) || ch === '`' || ch === '$') { 505 | i++; 506 | state = stringStates.START_QUOTE_OR_CHAR; 507 | } else { 508 | const pos = position(input.substring(index, i + 1)); 509 | throw new SyntaxError( 510 | `Unexpected token ${ch} in ${pos.line}:${pos.column}` 511 | ); 512 | } 513 | } else if (ch === '\\') { 514 | i++; 515 | state = stringStates.ESCAPE; 516 | } else if (ch === '`') { 517 | popState(); 518 | tokenCache.push({ 519 | type: TokenName[TokenEnum.Punctuator], 520 | value: '`', 521 | start: position(input.substring(index, i)), 522 | end: position(input.substring(index, i + 1)) 523 | }); 524 | break; 525 | } else if (ch === '$') { 526 | const nextCh = input[i + 1]; 527 | if (nextCh === '{') { 528 | pushState(mainStates.EXPRESSION); 529 | tokenCache.push({ 530 | type: TokenName[TokenEnum.TemplateLeftBrace], 531 | value: '${', 532 | start: position(input.substring(index, i)), 533 | end: position(input.substring(index, i + 2)) 534 | }); 535 | break; 536 | } 537 | i++; 538 | } else { 539 | i++; 540 | } 541 | } 542 | if (i > index) { 543 | const value = input.substring(index, i); 544 | return { 545 | type: TokenName[TokenEnum.TemplateRaw], 546 | value: escapeString(value, ['`', '$']), 547 | raw: value, 548 | start: position(), 549 | end: position(value) 550 | }; 551 | } 552 | return tokenCache.length ? tokenCache.shift() : null; 553 | } 554 | 555 | function skipWhiteSpace() { 556 | while (index < input.length) { 557 | const ch = input[index]; 558 | if (ch === '\r') { 559 | // CR (Unix) 560 | index++; 561 | line++; 562 | column = 1; 563 | if (input.charAt(index) === '\n') { 564 | // CRLF (Windows) 565 | index++; 566 | } 567 | } else if (ch === '\n') { 568 | // LF (MacOS) 569 | index++; 570 | line++; 571 | column = 1; 572 | } else if (ch === '\t' || ch === ' ') { 573 | index++; 574 | column++; 575 | } else { 576 | break; 577 | } 578 | } 579 | } 580 | 581 | function punctuator() { 582 | const find = punctuatorList.find( 583 | punctuator => 584 | input.substring(index, index + punctuator.length) === punctuator 585 | ); 586 | if (find) { 587 | return { 588 | type: TokenName[TokenEnum.Punctuator], 589 | value: find, 590 | start: position(), 591 | end: position(find) 592 | }; 593 | } 594 | return null; 595 | } 596 | 597 | function literal() { 598 | let keyword = input.substring(index, index + 4).toLowerCase(); 599 | let value: any = keyword; 600 | let isLiteral = false; 601 | if (keyword === 'true' || keyword === 'null') { 602 | isLiteral = true; 603 | value = keyword === 'true' ? true : null; 604 | } else if ( 605 | (keyword = input.substring(index, index + 5).toLowerCase()) === 'false' 606 | ) { 607 | isLiteral = true; 608 | value = false; 609 | } else if ( 610 | (keyword = input.substring(index, index + 9).toLowerCase()) === 611 | 'undefined' 612 | ) { 613 | isLiteral = true; 614 | value = undefined; 615 | } 616 | 617 | if (isLiteral) { 618 | return { 619 | type: 620 | value === true || value === false 621 | ? TokenName[TokenEnum.BooleanLiteral] 622 | : TokenName[TokenEnum.Literal], 623 | value, 624 | raw: keyword, 625 | start: position(), 626 | end: position(keyword) 627 | }; 628 | } 629 | return null; 630 | } 631 | 632 | function numberLiteral() { 633 | let i = index; 634 | 635 | let passedValueIndex = i; 636 | let state = numberStates.START; 637 | 638 | iterator: while (i < input.length) { 639 | const char = input.charAt(i); 640 | 641 | switch (state) { 642 | case numberStates.START: { 643 | if (char === '0') { 644 | passedValueIndex = i + 1; 645 | state = numberStates.ZERO; 646 | } else if (isDigit1to9(char)) { 647 | passedValueIndex = i + 1; 648 | state = numberStates.DIGIT; 649 | } else { 650 | return null; 651 | } 652 | break; 653 | } 654 | 655 | case numberStates.ZERO: { 656 | if (char === '.') { 657 | state = numberStates.POINT; 658 | } else if (isExp(char)) { 659 | state = numberStates.EXP; 660 | } else { 661 | break iterator; 662 | } 663 | break; 664 | } 665 | 666 | case numberStates.DIGIT: { 667 | if (isDigit(char)) { 668 | passedValueIndex = i + 1; 669 | } else if (char === '.') { 670 | state = numberStates.POINT; 671 | } else if (isExp(char)) { 672 | state = numberStates.EXP; 673 | } else { 674 | break iterator; 675 | } 676 | break; 677 | } 678 | 679 | case numberStates.POINT: { 680 | if (isDigit(char)) { 681 | passedValueIndex = i + 1; 682 | state = numberStates.DIGIT_FRACTION; 683 | } else { 684 | break iterator; 685 | } 686 | break; 687 | } 688 | 689 | case numberStates.DIGIT_FRACTION: { 690 | if (isDigit(char)) { 691 | passedValueIndex = i + 1; 692 | } else if (isExp(char)) { 693 | state = numberStates.EXP; 694 | } else { 695 | break iterator; 696 | } 697 | break; 698 | } 699 | } 700 | 701 | i++; 702 | } 703 | 704 | if (passedValueIndex > 0) { 705 | const value = input.slice(index, passedValueIndex); 706 | return { 707 | type: TokenName[TokenEnum.NumericLiteral], 708 | value: formatNumber(value), 709 | raw: value, 710 | start: position(), 711 | end: position(value) 712 | }; 713 | } 714 | 715 | return null; 716 | } 717 | 718 | function stringLiteral() { 719 | let startQuote = '"'; 720 | let state = stringStates.START; 721 | let i = index; 722 | while (i < input.length) { 723 | const ch = input[i]; 724 | 725 | if (state === stringStates.START) { 726 | if (ch === '"' || ch === "'") { 727 | startQuote = ch; 728 | i++; 729 | state = stringStates.START_QUOTE_OR_CHAR; 730 | } else { 731 | break; 732 | } 733 | } else if (state === stringStates.ESCAPE) { 734 | if (escapes.hasOwnProperty(ch) || ch === startQuote) { 735 | i++; 736 | state = stringStates.START_QUOTE_OR_CHAR; 737 | } else { 738 | const pos = position(input.substring(index, i + 1)); 739 | throw new SyntaxError( 740 | `Unexpected token ${ch} in ${pos.line}:${pos.column}` 741 | ); 742 | } 743 | } else if (ch === '\\') { 744 | i++; 745 | state = stringStates.ESCAPE; 746 | } else if (ch === startQuote) { 747 | i++; 748 | break; 749 | } else { 750 | i++; 751 | } 752 | } 753 | if (i > index) { 754 | const value = input.substring(index, i); 755 | return { 756 | type: TokenName[TokenEnum.StringLiteral], 757 | value: escapeString(value.substring(1, value.length - 1), [startQuote]), 758 | raw: value, 759 | start: position(), 760 | end: position(value) 761 | }; 762 | } 763 | return null; 764 | } 765 | 766 | function identifier() { 767 | // 变量模式是 resolveVariable 的时候使用的 768 | // 这个纯变量获取模式,不支持其他什么表达式 769 | // 仅仅支持 xxx.xxx 或者 xxx[ exression ] 这类语法 770 | // 所以纯变量模式支持纯数字作为变量名 771 | const reg = options?.variableMode 772 | ? /^[\u4e00-\u9fa5A-Za-z0-9_$@][\u4e00-\u9fa5A-Za-z0-9_\-$@]*/ 773 | : /^(?:[\u4e00-\u9fa5A-Za-z_$@]([\u4e00-\u9fa5A-Za-z0-9_\-$@]|\\(?:\.|\[|\]|\(|\)|\{|\}|\s|=|!|>|<|\||&|\+|-|\*|\/|\^|~|%|&|\?|:|;|,))*|\d+[\u4e00-\u9fa5A-Za-z_$@](?:[\u4e00-\u9fa5A-Za-z0-9_\-$@]|\\(?:\.|\[|\]|\(|\)|\{|\}|\s|=|!|>|<|\||&|\+|-|\*|\/|\^|~|%|&|\?|:|;|,))*)/; 774 | 775 | const match = reg.exec( 776 | input.substring(index, index + 256) // 变量长度不能超过 256 777 | ); 778 | if (match) { 779 | return { 780 | type: TokenName[TokenEnum.Identifier], 781 | value: match[0].replace( 782 | /\\(\.|\[|\]|\(|\)|\{|\}|\s|=|!|>|<|\||&|\+|-|\*|\/|\^|~|%|&|\?|:|;|,)/g, 783 | (_, v) => v 784 | ), 785 | start: position(), 786 | end: position(match[0]) 787 | }; 788 | } 789 | return null; 790 | } 791 | 792 | function getNextToken(): Token | void | null { 793 | if (tokenCache.length) { 794 | return tokenCache.shift()!; 795 | } 796 | 797 | if ( 798 | mainState === mainStates.SCRIPT || 799 | mainState === mainStates.EXPRESSION || 800 | mainState === mainStates.BLOCK 801 | ) { 802 | skipWhiteSpace(); 803 | } 804 | 805 | return eof() || raw() || openScript() || expression() || template(); 806 | } 807 | 808 | return { 809 | next: function () { 810 | const token = getNextToken(); 811 | 812 | if (token) { 813 | index = token.end.index; 814 | line = token.end.line; 815 | column = token.end.column; 816 | return token; 817 | } 818 | 819 | const pos = position(); 820 | throw new SyntaxError( 821 | `unexpected character "${input[index]}" at ${pos.line}:${pos.column}` 822 | ); 823 | } 824 | }; 825 | } 826 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | lexer as createLexer, 3 | Position, 4 | Token, 5 | TokenEnum, 6 | TokenName, 7 | TokenTypeName 8 | } from './lexer'; 9 | 10 | export type NodeType = 'content' | 'raw' | 'conditional'; 11 | 12 | export interface ParserOptions { 13 | /** 14 | * 直接是运算表达式?还是从模板开始 ${} 里面才算运算表达式 15 | */ 16 | evalMode?: boolean; 17 | 18 | /** 19 | * 只支持取变量。 20 | */ 21 | variableMode?: boolean; 22 | 23 | /** 24 | * 是否允许 filter 语法,比如: 25 | * 26 | * ${abc | html} 27 | */ 28 | allowFilter?: boolean; 29 | 30 | variableNamespaces?: Array; 31 | } 32 | 33 | export interface ASTNode { 34 | type: string; 35 | start: Position; 36 | end: Position; 37 | [propname: string]: any; 38 | } 39 | 40 | export type ASTNodeOrNull = ASTNode | null; 41 | 42 | const argListStates = { 43 | START: 0, 44 | COMMA: 1, 45 | SET: 2 46 | }; 47 | 48 | const tempalteStates = { 49 | START: 0, 50 | SCRIPTING: 1 51 | }; 52 | 53 | const objectStates = { 54 | START: 0, 55 | KEY: 1, 56 | COLON: 2, 57 | VALUE: 3, 58 | COMMA: 4 59 | }; 60 | 61 | export function parse(input: string, options?: ParserOptions): ASTNode { 62 | let token: Token; 63 | const lexer = createLexer(input, options); 64 | const tokens: Array = []; 65 | const tokenChunk: Array = []; 66 | 67 | // 允许的变量名字空间 68 | let variableNamespaces: Array = options?.variableNamespaces ?? [ 69 | 'window', 70 | 'cookie', 71 | 'ls', 72 | 'ss' 73 | ]; 74 | if (!Array.isArray(variableNamespaces)) { 75 | variableNamespaces = []; 76 | } 77 | 78 | function next() { 79 | token = tokenChunk.length ? tokenChunk.shift()! : lexer.next(); 80 | 81 | if (!token) { 82 | throw new TypeError('next token is undefined'); 83 | } 84 | tokens.push(token); 85 | } 86 | 87 | function back() { 88 | tokenChunk.unshift(tokens.pop()!); 89 | token = tokens[tokens.length - 1]; 90 | } 91 | 92 | function matchPunctuator(operator: string | Array) { 93 | return ( 94 | token.type === TokenName[TokenEnum.Punctuator] && 95 | (Array.isArray(operator) 96 | ? ~operator.indexOf(token.value!) 97 | : token.value === operator) 98 | ); 99 | } 100 | 101 | function fatal() { 102 | throw TypeError( 103 | `Unexpected token ${token!.value} in ${token!.start.line}:${ 104 | token!.start.column 105 | }` 106 | ); 107 | } 108 | 109 | function assert(result: any) { 110 | if (!result) { 111 | fatal(); 112 | } 113 | return result; 114 | } 115 | 116 | function expression(): ASTNodeOrNull { 117 | return assignmentExpression(); 118 | } 119 | 120 | function skipWhiteSpaceChar() { 121 | while ( 122 | token.type === TokenName[TokenEnum.Char] && 123 | /^\s+$/m.test(token.value) 124 | ) { 125 | next(); 126 | } 127 | } 128 | 129 | function collectFilterArg() { 130 | const arg: Array = []; 131 | while ( 132 | !matchPunctuator(':') && 133 | token.type !== TokenName[TokenEnum.OpenFilter] && 134 | token.type !== TokenName[TokenEnum.CloseScript] 135 | ) { 136 | const item = 137 | literal() || 138 | numberLiteral() || 139 | stringLiteral() || 140 | template() || 141 | arrayLiteral() || 142 | rawScript() || 143 | objectLiteral(); 144 | 145 | if (item) { 146 | arg.push(item); 147 | } else { 148 | assert( 149 | ~[ 150 | TokenName[TokenEnum.Identifier], 151 | TokenName[TokenEnum.Punctuator], 152 | TokenName[TokenEnum.Char] 153 | ].indexOf(token.type) 154 | ); 155 | 156 | // 其他的都当字符处理 157 | if (arg.length && typeof arg[arg.length - 1] === 'string') { 158 | arg[arg.length - 1] += token.raw || token.value; 159 | } else { 160 | arg.push(token.raw || token.value); 161 | } 162 | next(); 163 | } 164 | } 165 | if (arg.length && typeof arg[arg.length - 1] === 'string') { 166 | arg[arg.length - 1] = arg[arg.length - 1].replace(/\s+$/, ''); 167 | if (!arg[arg.length - 1]) { 168 | arg.pop(); 169 | } 170 | } 171 | return arg; 172 | } 173 | 174 | function complexExpression(): ASTNodeOrNull { 175 | let ast = expression(); 176 | 177 | const filters: Array = []; 178 | while (token.type === TokenName[TokenEnum.OpenFilter]) { 179 | next(); 180 | 181 | skipWhiteSpaceChar(); 182 | const name = assert(identifier()); 183 | const fnName = name.name; 184 | const args = []; 185 | 186 | skipWhiteSpaceChar(); 187 | while (matchPunctuator(':')) { 188 | next(); 189 | skipWhiteSpaceChar(); 190 | 191 | let argContents: any = collectFilterArg(); 192 | if (argContents.length === 1) { 193 | argContents = argContents[0]; 194 | } else if (!argContents.length) { 195 | argContents = ''; 196 | } 197 | 198 | args.push( 199 | Array.isArray(argContents) 200 | ? { 201 | type: 'mixed', 202 | body: argContents 203 | } 204 | : argContents 205 | ); 206 | } 207 | filters.push({ 208 | name: fnName, 209 | args 210 | }); 211 | } 212 | 213 | if (filters.length) { 214 | ast = { 215 | type: 'filter', 216 | input: ast, 217 | filters, 218 | start: ast!.start, 219 | end: filters[filters.length - 1].end 220 | }; 221 | } 222 | 223 | return ast; 224 | } 225 | 226 | function arrowFunction(): ASTNodeOrNull { 227 | let ast: any = argList() || variable(); 228 | let args: Array = []; 229 | let start: Position; 230 | 231 | if (ast?.type === 'variable') { 232 | args = [ast]; 233 | start = ast.start; 234 | } else if (ast?.type === 'arg-list') { 235 | start = ast.start; 236 | args = ast.body; 237 | } 238 | 239 | if (Array.isArray(args) && matchPunctuator('=')) { 240 | next(); 241 | if (matchPunctuator('>')) { 242 | next(); 243 | const body = assert(expression()); 244 | return { 245 | type: 'anonymous_function', 246 | args: args, 247 | return: body, 248 | start: start!, 249 | end: body.end 250 | }; 251 | } else { 252 | back(); 253 | } 254 | } 255 | 256 | return ast; 257 | } 258 | 259 | function conditionalExpression(): ASTNodeOrNull { 260 | const ast = logicalOrExpression(); 261 | 262 | if (!ast) { 263 | return null; 264 | } 265 | 266 | if (matchPunctuator('?')) { 267 | next(); 268 | let consequent = assignmentExpression(); 269 | assert(consequent); 270 | assert(matchPunctuator(':')); 271 | 272 | next(); 273 | let alternate = assignmentExpression(); 274 | assert(alternate); 275 | 276 | return { 277 | type: 'conditional', 278 | test: ast, 279 | consequent: consequent, 280 | alternate: alternate, 281 | start: ast.start, 282 | end: alternate!.end 283 | }; 284 | } 285 | 286 | return ast; 287 | } 288 | 289 | function binaryExpressionParser( 290 | type: string, 291 | operator: string, 292 | parseFunction: () => any, 293 | rightParseFunction = parseFunction, 294 | leftKey = 'left', 295 | rightKey = 'right' 296 | ) { 297 | let ast = parseFunction(); 298 | if (!ast) { 299 | return null; 300 | } 301 | 302 | if (matchPunctuator(operator)) { 303 | while (matchPunctuator(operator)) { 304 | next(); 305 | const right = assert(rightParseFunction()); 306 | 307 | ast = { 308 | type: type, 309 | op: operator, 310 | [leftKey]: ast, 311 | [rightKey]: right, 312 | start: ast.start, 313 | end: right.end 314 | }; 315 | } 316 | } 317 | 318 | return ast; 319 | } 320 | 321 | function logicalOrExpression(): ASTNodeOrNull { 322 | return binaryExpressionParser('or', '||', logicalAndExpression); 323 | } 324 | 325 | function logicalAndExpression(): ASTNodeOrNull { 326 | return binaryExpressionParser('and', '&&', bitwiseOrExpression); 327 | } 328 | 329 | function bitwiseOrExpression(): ASTNodeOrNull { 330 | return binaryExpressionParser('binary', '|', bitwiseXOrExpression); 331 | } 332 | 333 | function bitwiseXOrExpression(): ASTNodeOrNull { 334 | return binaryExpressionParser('binary', '^', bitwiseAndExpression); 335 | } 336 | 337 | function bitwiseAndExpression(): ASTNodeOrNull { 338 | return binaryExpressionParser('binary', '&', equalityExpression); 339 | } 340 | 341 | function equalityExpression(): ASTNodeOrNull { 342 | return binaryExpressionParser('eq', '==', () => 343 | binaryExpressionParser('ne', '!=', () => 344 | binaryExpressionParser('streq', '===', () => 345 | binaryExpressionParser('strneq', '!==', relationalExpression) 346 | ) 347 | ) 348 | ); 349 | } 350 | 351 | function relationalExpression(): ASTNodeOrNull { 352 | return binaryExpressionParser('lt', '<', () => 353 | binaryExpressionParser('gt', '>', () => 354 | binaryExpressionParser('le', '<=', () => 355 | binaryExpressionParser('ge', '>=', shiftExpression) 356 | ) 357 | ) 358 | ); 359 | } 360 | 361 | function shiftExpression(): ASTNodeOrNull { 362 | return binaryExpressionParser('shift', '<<', () => 363 | binaryExpressionParser('shift', '>>', () => 364 | binaryExpressionParser('shift', '>>>', additiveExpression) 365 | ) 366 | ); 367 | } 368 | 369 | function additiveExpression(): ASTNodeOrNull { 370 | return binaryExpressionParser('add', '+', () => 371 | binaryExpressionParser('minus', '-', multiplicativeExpression) 372 | ); 373 | } 374 | 375 | function multiplicativeExpression(): ASTNodeOrNull { 376 | return binaryExpressionParser('multiply', '*', () => 377 | binaryExpressionParser('divide', '/', () => 378 | binaryExpressionParser('remainder', '%', powerExpression) 379 | ) 380 | ); 381 | } 382 | 383 | function powerExpression(): ASTNodeOrNull { 384 | return binaryExpressionParser('power', '**', unaryExpression); 385 | } 386 | 387 | function unaryExpression(): ASTNodeOrNull { 388 | const unaryOperators = ['+', '-', '~', '!']; 389 | const stack: Array = []; 390 | while (matchPunctuator(unaryOperators)) { 391 | stack.push(token); 392 | next(); 393 | } 394 | let ast: any = postfixExpression(); 395 | assert(!stack.length || ast); 396 | while (stack.length) { 397 | const op = stack.pop(); 398 | 399 | ast = { 400 | type: 'unary', 401 | op: op.value, 402 | value: ast, 403 | start: op.start, 404 | end: op.end, 405 | }; 406 | } 407 | return ast; 408 | } 409 | 410 | function postfixExpression( 411 | parseFunction: () => any = leftHandSideExpression 412 | ): ASTNodeOrNull { 413 | let ast = parseFunction(); 414 | if (!ast) { 415 | return null; 416 | } 417 | 418 | while (matchPunctuator('[') || matchPunctuator('.')) { 419 | const isDot = matchPunctuator('.'); 420 | next(); 421 | const right = assert( 422 | isDot ? identifier() || numberLiteral() || rawScript() : expression() 423 | ); 424 | 425 | if (!isDot) { 426 | assert(matchPunctuator(']')); 427 | next(); 428 | } 429 | 430 | ast = { 431 | type: 'getter', 432 | host: ast, 433 | key: right, 434 | start: ast.start, 435 | end: right.end 436 | }; 437 | } 438 | 439 | return ast; 440 | } 441 | 442 | function leftHandSideExpression(): ASTNodeOrNull { 443 | return functionCall() || arrowFunction() || primaryExpression(); 444 | } 445 | 446 | function varibleKey(allowVariable = false, inObject = false): ASTNodeOrNull { 447 | return ( 448 | (allowVariable ? variable() : identifier()) || 449 | stringLiteral() || 450 | numberLiteral() || 451 | (inObject ? objectTemplateKey() : template()) 452 | ); 453 | } 454 | 455 | function objectTemplateKey(): ASTNodeOrNull { 456 | if (matchPunctuator('[')) { 457 | next(); 458 | const key = assert(template()); 459 | assert(matchPunctuator(']')); 460 | next(); 461 | return key; 462 | } 463 | return null; 464 | } 465 | 466 | function stringLiteral(): ASTNodeOrNull { 467 | if (token.type === TokenName[TokenEnum.StringLiteral]) { 468 | const cToken = token; 469 | next(); 470 | return { 471 | type: 'string', 472 | value: cToken.value, 473 | start: cToken.start, 474 | end: cToken.end 475 | }; 476 | } 477 | return null; 478 | } 479 | 480 | function numberLiteral(): ASTNodeOrNull { 481 | if (token.type === TokenName[TokenEnum.NumericLiteral]) { 482 | const value = token.value; 483 | const cToken = token; 484 | next(); 485 | return { 486 | type: 'literal', 487 | value: value, 488 | start: cToken.start, 489 | end: cToken.end 490 | }; 491 | } 492 | 493 | return null; 494 | } 495 | 496 | function template(): ASTNodeOrNull { 497 | if (matchPunctuator('`')) { 498 | const start = token; 499 | let end = start; 500 | next(); 501 | let state = tempalteStates.START; 502 | const ast: ASTNode = { 503 | type: 'template', 504 | body: [], 505 | start: start.start, 506 | end: start.end 507 | }; 508 | while (true) { 509 | if (state === tempalteStates.SCRIPTING) { 510 | const exp = assert(expression()); 511 | ast.body.push(exp); 512 | assert(token.type === TokenName[TokenEnum.TemplateRightBrace]); 513 | next(); 514 | state = tempalteStates.START; 515 | } else { 516 | if (matchPunctuator('`')) { 517 | end = token; 518 | next(); 519 | break; 520 | } else if (token.type === TokenName[TokenEnum.TemplateLeftBrace]) { 521 | next(); 522 | state = tempalteStates.SCRIPTING; 523 | } else if (token.type === TokenName[TokenEnum.TemplateRaw]) { 524 | ast.body.push({ 525 | type: 'template_raw', 526 | value: token.value, 527 | start: token.start, 528 | end: token.end 529 | }); 530 | next(); 531 | } else { 532 | fatal(); 533 | } 534 | } 535 | } 536 | 537 | ast.end = end.end; 538 | return ast; 539 | } 540 | return null; 541 | } 542 | 543 | function identifier(): ASTNodeOrNull { 544 | if (token.type === TokenName[TokenEnum.Identifier]) { 545 | const cToken = token; 546 | next(); 547 | return { 548 | type: 'identifier', 549 | name: cToken.value, 550 | start: cToken.start, 551 | end: cToken.end 552 | }; 553 | } 554 | return null; 555 | } 556 | 557 | function primaryExpression(): ASTNodeOrNull { 558 | return ( 559 | variable() || 560 | literal() || 561 | numberLiteral() || 562 | stringLiteral() || 563 | template() || 564 | arrayLiteral() || 565 | objectLiteral() || 566 | (() => { 567 | const ast = expressionList(); 568 | 569 | if (ast?.body.length === 1) { 570 | return ast.body[0]; 571 | } 572 | 573 | return ast; 574 | })() || 575 | rawScript() 576 | ); 577 | } 578 | 579 | function literal(): ASTNodeOrNull { 580 | if ( 581 | token.type === TokenName[TokenEnum.Literal] || 582 | token.type === TokenName[TokenEnum.BooleanLiteral] 583 | ) { 584 | const value = token.value; 585 | const cToken = token; 586 | next(); 587 | return { 588 | type: 'literal', 589 | value: value, 590 | start: cToken.start, 591 | end: cToken.end 592 | }; 593 | } 594 | 595 | return null; 596 | } 597 | 598 | function functionCall(): ASTNodeOrNull { 599 | if (token.type === TokenName[TokenEnum.Identifier]) { 600 | const id = token; 601 | next(); 602 | if (matchPunctuator('(')) { 603 | const argList = expressionList(); 604 | assert(argList); 605 | return { 606 | type: 'func_call', 607 | identifier: id.value, 608 | args: argList?.body, 609 | start: id.start, 610 | end: argList!.end 611 | }; 612 | } else { 613 | back(); 614 | } 615 | } 616 | return null; 617 | } 618 | 619 | function arrayLiteral(): ASTNodeOrNull { 620 | if (matchPunctuator('[')) { 621 | const argList = expressionList('[', ']'); 622 | assert(argList); 623 | return { 624 | type: 'array', 625 | members: argList?.body, 626 | start: argList!.start, 627 | end: argList!.end 628 | }; 629 | } 630 | return null; 631 | } 632 | 633 | function expressionList(startOP = '(', endOp = ')'): ASTNodeOrNull { 634 | if (matchPunctuator(startOP)) { 635 | const start = token; 636 | let end: Token; 637 | next(); 638 | const args: Array = []; 639 | let state = argListStates.START; 640 | 641 | while (true) { 642 | if (state === argListStates.COMMA || !matchPunctuator(endOp)) { 643 | const arg = assert(expression()); 644 | args.push(arg); 645 | state = argListStates.START; 646 | 647 | if (matchPunctuator(',')) { 648 | next(); 649 | state = argListStates.COMMA; 650 | } 651 | } else if (matchPunctuator(endOp)) { 652 | end = token; 653 | next(); 654 | break; 655 | } 656 | } 657 | return { 658 | type: 'expression-list', 659 | body: args, 660 | start: start.start, 661 | end: end!.end 662 | }; 663 | } 664 | return null; 665 | } 666 | 667 | function argList(startOP = '(', endOp = ')'): ASTNodeOrNull { 668 | let count = 0; 669 | let rollback = () => { 670 | while (count-- > 0) { 671 | back(); 672 | } 673 | return null; 674 | }; 675 | if (matchPunctuator(startOP)) { 676 | const start = token; 677 | let end: Token = start; 678 | next(); 679 | count++; 680 | const args: Array = []; 681 | let state = argListStates.START; 682 | 683 | while (!matchPunctuator(endOp)) { 684 | if (state === argListStates.COMMA || state === argListStates.START) { 685 | const arg = variable(false); 686 | 687 | if (!arg) { 688 | return rollback(); 689 | } 690 | 691 | count++; 692 | args.push(arg); 693 | state = argListStates.SET; 694 | } else if (state === argListStates.SET && matchPunctuator(',')) { 695 | next(); 696 | count++; 697 | state = argListStates.COMMA; 698 | } else { 699 | return rollback(); 700 | } 701 | } 702 | 703 | if (matchPunctuator(endOp)) { 704 | end = token; 705 | next(); 706 | return { 707 | type: 'arg-list', 708 | body: args, 709 | start: start.start, 710 | end: end.end 711 | }; 712 | } else { 713 | return rollback(); 714 | } 715 | } 716 | return null; 717 | } 718 | 719 | function objectLiteral(): ASTNodeOrNull { 720 | if (matchPunctuator('{')) { 721 | const start = token; 722 | let end = start; 723 | next(); 724 | let ast: ASTNode = { 725 | type: 'object', 726 | members: [], 727 | start: start.start, 728 | end: start.end 729 | }; 730 | let state = objectStates.START; 731 | let key: any, value: any; 732 | while (true) { 733 | if (state === objectStates.KEY) { 734 | assert(matchPunctuator(':')); 735 | next(); 736 | state = objectStates.COLON; 737 | } else if (state === objectStates.COLON) { 738 | value = assert(expression()); 739 | ast.members.push({ 740 | key, 741 | value 742 | }); 743 | state = objectStates.VALUE; 744 | } else if (state === objectStates.VALUE) { 745 | if (matchPunctuator(',')) { 746 | next(); 747 | state = objectStates.COMMA; 748 | } else if (matchPunctuator('}')) { 749 | end = token; 750 | next(); 751 | break; 752 | } else { 753 | fatal(); 754 | } 755 | } else { 756 | if (state != objectStates.COMMA && matchPunctuator('}')) { 757 | end = token; 758 | next(); 759 | break; 760 | } 761 | 762 | key = assert(varibleKey(false, true)); 763 | state = objectStates.KEY; 764 | } 765 | } 766 | 767 | ast.end = end.end; 768 | return ast; 769 | } 770 | return null; 771 | } 772 | 773 | function assignmentExpression(): ASTNodeOrNull { 774 | return conditionalExpression(); 775 | } 776 | 777 | function contents(): ASTNodeOrNull { 778 | const node: ASTNode = { 779 | type: 'document', 780 | body: [], 781 | start: token.start, 782 | end: token.end 783 | }; 784 | while (token.type !== TokenName[TokenEnum.EOF]) { 785 | const ast = raw() || rawScript() || oldVariable(); 786 | 787 | if (!ast) { 788 | break; 789 | } 790 | node.body.push(ast); 791 | } 792 | if (node.body.length) { 793 | node.end = node.body[node.body.length - 1].end; 794 | } 795 | return node; 796 | } 797 | 798 | function raw(): ASTNodeOrNull { 799 | if (token.type !== TokenName[TokenEnum.RAW]) { 800 | return null; 801 | } 802 | 803 | const cToken = token; 804 | next(); 805 | return { 806 | type: 'raw', 807 | value: cToken.value, 808 | start: cToken.start, 809 | end: cToken.end 810 | }; 811 | } 812 | 813 | function rawScript(): ASTNodeOrNull { 814 | if (token.type !== TokenName[TokenEnum.OpenScript]) { 815 | return null; 816 | } 817 | 818 | const start = token; 819 | let end = start; 820 | next(); 821 | const exp = assert(complexExpression()); 822 | assert(token.type === TokenName[TokenEnum.CloseScript]); 823 | end = token; 824 | next(); 825 | 826 | return { 827 | type: 'script', 828 | body: exp, 829 | start: start.start, 830 | end: end.end 831 | }; 832 | } 833 | 834 | function variable(allowNameSpace = true): ASTNodeOrNull { 835 | if (token.type === TokenName[TokenEnum.Identifier]) { 836 | const cToken = token; 837 | next(); 838 | 839 | if ( 840 | allowNameSpace && 841 | matchPunctuator(':') && 842 | ~variableNamespaces.indexOf(cToken.value) 843 | ) { 844 | next(); 845 | const body = assert(postfixExpression()); 846 | return { 847 | type: 'ns-variable', 848 | namespace: cToken.value, 849 | body, 850 | start: cToken.start, 851 | end: body.end 852 | }; 853 | } 854 | 855 | return { 856 | type: 'variable', 857 | name: cToken.value, 858 | start: cToken.start, 859 | end: cToken.end 860 | }; 861 | } else if (matchPunctuator('&')) { 862 | const v = token; 863 | next(); 864 | return { 865 | type: 'variable', 866 | name: '&', 867 | start: v.start, 868 | end: v.end 869 | }; 870 | } 871 | return null; 872 | } 873 | 874 | function oldVariable(): ASTNodeOrNull { 875 | if (token.type !== TokenName[TokenEnum.Variable]) { 876 | return null; 877 | } 878 | const prevToken = token; 879 | next(); 880 | return { 881 | type: 'script', 882 | body: prevToken.value.split('.').reduce((prev: any, key: string) => { 883 | return prev 884 | ? { 885 | type: 'getter', 886 | host: prev, 887 | key, 888 | start: prevToken.start, 889 | end: prevToken.end 890 | } 891 | : { 892 | type: 'variable', 893 | name: key, 894 | start: prevToken.start, 895 | end: prevToken.end 896 | }; 897 | }, null), 898 | start: prevToken.start, 899 | end: prevToken.end 900 | }; 901 | } 902 | 903 | next(); 904 | const ast = options?.variableMode 905 | ? postfixExpression(variable) 906 | : options?.evalMode 907 | ? expression() 908 | : contents(); 909 | 910 | assert(token!?.type === TokenName[TokenEnum.EOF]); 911 | 912 | return ast!; 913 | } 914 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'lodash/isPlainObject'; 2 | import {Evaluator} from './evalutor'; 3 | import {parse} from './parser'; 4 | import moment from 'moment'; 5 | 6 | // 方便取值的时候能够把上层的取到,但是获取的时候不会全部把所有的数据获取到。 7 | export function createObject( 8 | superProps?: {[propName: string]: any}, 9 | props?: {[propName: string]: any}, 10 | properties?: any 11 | ): object { 12 | if (superProps && Object.isFrozen(superProps)) { 13 | superProps = cloneObject(superProps); 14 | } 15 | 16 | const obj = superProps 17 | ? Object.create(superProps, { 18 | ...properties, 19 | __super: { 20 | value: superProps, 21 | writable: false, 22 | enumerable: false 23 | } 24 | }) 25 | : Object.create(Object.prototype, properties); 26 | 27 | props && 28 | isObject(props) && 29 | Object.keys(props).forEach(key => (obj[key] = props[key])); 30 | 31 | return obj; 32 | } 33 | 34 | export function cloneObject(target: any, persistOwnProps: boolean = true) { 35 | const obj = 36 | target && target.__super 37 | ? Object.create(target.__super, { 38 | __super: { 39 | value: target.__super, 40 | writable: false, 41 | enumerable: false 42 | } 43 | }) 44 | : Object.create(Object.prototype); 45 | persistOwnProps && 46 | target && 47 | Object.keys(target).forEach(key => (obj[key] = target[key])); 48 | return obj; 49 | } 50 | 51 | export function isObject(obj: any) { 52 | const typename = typeof obj; 53 | return ( 54 | obj && 55 | typename !== 'string' && 56 | typename !== 'number' && 57 | typename !== 'boolean' && 58 | typename !== 'function' && 59 | !Array.isArray(obj) 60 | ); 61 | } 62 | 63 | export function string2regExp(value: string, caseSensitive = false) { 64 | if (typeof value !== 'string') { 65 | throw new TypeError('Expected a string'); 66 | } 67 | 68 | return new RegExp( 69 | value.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d'), 70 | !caseSensitive ? 'i' : '' 71 | ); 72 | } 73 | 74 | export function getVariable( 75 | data: {[propName: string]: any}, 76 | key: string | undefined, 77 | canAccessSuper: boolean = true 78 | ): any { 79 | if (!data || !key) { 80 | return undefined; 81 | } else if (canAccessSuper ? key in data : data.hasOwnProperty(key)) { 82 | return data[key]; 83 | } 84 | 85 | return keyToPath(key).reduce( 86 | (obj, key) => 87 | obj && 88 | typeof obj === 'object' && 89 | (canAccessSuper ? key in obj : obj.hasOwnProperty(key)) 90 | ? obj[key] 91 | : undefined, 92 | data 93 | ); 94 | } 95 | 96 | export function setVariable( 97 | data: {[propName: string]: any}, 98 | key: string, 99 | value: any, 100 | convertKeyToPath?: boolean 101 | ) { 102 | data = data || {}; 103 | 104 | if (key in data) { 105 | data[key] = value; 106 | return; 107 | } 108 | 109 | const parts = convertKeyToPath !== false ? keyToPath(key) : [key]; 110 | const last = parts.pop() as string; 111 | 112 | while (parts.length) { 113 | let key = parts.shift() as string; 114 | if (isPlainObject(data[key])) { 115 | data = data[key] = { 116 | ...data[key] 117 | }; 118 | } else if (Array.isArray(data[key])) { 119 | data[key] = data[key].concat(); 120 | data = data[key]; 121 | } else if (data[key]) { 122 | // throw new Error(`目标路径不是纯对象,不能覆盖`); 123 | // 强行转成对象 124 | data[key] = {}; 125 | data = data[key]; 126 | } else { 127 | data[key] = {}; 128 | data = data[key]; 129 | } 130 | } 131 | 132 | data[last] = value; 133 | } 134 | 135 | export function deleteVariable(data: {[propName: string]: any}, key: string) { 136 | if (!data) { 137 | return; 138 | } else if (data.hasOwnProperty(key)) { 139 | delete data[key]; 140 | return; 141 | } 142 | 143 | const parts = keyToPath(key); 144 | const last = parts.pop() as string; 145 | 146 | while (parts.length) { 147 | let key = parts.shift() as string; 148 | if (isPlainObject(data[key])) { 149 | data = data[key] = { 150 | ...data[key] 151 | }; 152 | } else if (data[key]) { 153 | throw new Error(`目标路径不是纯对象,不能修改`); 154 | } else { 155 | break; 156 | } 157 | } 158 | 159 | if (data && data.hasOwnProperty && data.hasOwnProperty(last)) { 160 | delete data[last]; 161 | } 162 | } 163 | 164 | /** 165 | * 将例如像 a.b.c 或 a[1].b 的字符串转换为路径数组 166 | * 167 | * @param string 要转换的字符串 168 | */ 169 | export const keyToPath = (string: string) => { 170 | const result = []; 171 | 172 | if (string.charCodeAt(0) === '.'.charCodeAt(0)) { 173 | result.push(''); 174 | } 175 | 176 | string.replace( 177 | new RegExp( 178 | '[^.[\\]]+|\\[(?:([^"\'][^[]*)|(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))', 179 | 'g' 180 | ), 181 | (match, expression, quote, subString) => { 182 | let key = match; 183 | if (quote) { 184 | key = subString.replace(/\\(\\)?/g, '$1'); 185 | } else if (expression) { 186 | key = expression.trim(); 187 | } 188 | result.push(key); 189 | return ''; 190 | } 191 | ); 192 | 193 | return result; 194 | }; 195 | 196 | export const tokenize = ( 197 | str: string, 198 | data: object, 199 | defaultFilter: string = '| html' 200 | ) => { 201 | if (!str || typeof str !== 'string') { 202 | return str; 203 | } 204 | 205 | try { 206 | const ast = parse(str, { 207 | evalMode: false, 208 | allowFilter: true 209 | }); 210 | const result = new Evaluator(data, { 211 | defaultFilter 212 | }).evalute(ast); 213 | 214 | return `${result == null ? '' : result}`; 215 | } catch (e) { 216 | console.warn(e); 217 | return str; 218 | } 219 | }; 220 | 221 | const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 222 | 223 | export const prettyBytes = (num: number) => { 224 | if (!Number.isFinite(num)) { 225 | throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`); 226 | } 227 | 228 | const neg = num < 0; 229 | 230 | if (neg) { 231 | num = -num; 232 | } 233 | 234 | if (num < 1) { 235 | return (neg ? '-' : '') + num + ' B'; 236 | } 237 | 238 | const exponent = Math.min( 239 | Math.floor(Math.log(num) / Math.log(1000)), 240 | UNITS.length - 1 241 | ); 242 | const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3)); 243 | const unit = UNITS[exponent]; 244 | 245 | return (neg ? '-' : '') + numStr + ' ' + unit; 246 | }; 247 | 248 | const entityMap: { 249 | [propName: string]: string; 250 | } = { 251 | '&': '&', 252 | '<': '<', 253 | '>': '>', 254 | '"': '"', 255 | "'": ''', 256 | '/': '/' 257 | }; 258 | export const escapeHtml = (str: string) => 259 | String(str).replace(/[&<>"'\/]/g, function (s) { 260 | return entityMap[s]; 261 | }); 262 | 263 | export function formatDuration(value: number): string { 264 | const unit = ['秒', '分', '时', '天', '月', '季', '年']; 265 | const steps = [1, 60, 3600, 86400, 2592000, 7776000, 31104000]; 266 | let len = steps.length; 267 | const parts = []; 268 | 269 | while (len--) { 270 | if (steps[len] && value >= steps[len]) { 271 | parts.push(Math.floor(value / steps[len]) + unit[len]); 272 | value %= steps[len]; 273 | } else if (len === 0 && value) { 274 | parts.push((value.toFixed ? value.toFixed(2) : '0') + unit[0]); 275 | } 276 | } 277 | 278 | return parts.join(''); 279 | } 280 | 281 | const timeUnitMap: { 282 | [propName: string]: string; 283 | } = { 284 | year: 'Y', 285 | month: 'M', 286 | week: 'w', 287 | weekday: 'W', 288 | day: 'd', 289 | hour: 'h', 290 | minute: 'm', 291 | min: 'm', 292 | second: 's', 293 | millisecond: 'ms' 294 | }; 295 | 296 | export const relativeValueRe = 297 | /^(.+)?(\+|-)(\d+)(minute|min|hour|day|week|month|year|weekday|second|millisecond)s?$/i; 298 | export const filterDate = ( 299 | value: string, 300 | data: object = {}, 301 | format = 'X', 302 | utc: boolean = false 303 | ): moment.Moment => { 304 | let m, 305 | mm = utc ? moment.utc : moment; 306 | 307 | if (typeof value === 'string') { 308 | value = value.trim(); 309 | } 310 | 311 | // todo 312 | const date = new Date(); 313 | value = tokenize(value, createObject(data, { 314 | now: mm().toDate(), 315 | today: mm([date.getFullYear(), date.getMonth(), date.getDate()]) 316 | }), '| raw'); 317 | 318 | if (value && typeof value === 'string' && (m = relativeValueRe.exec(value))) { 319 | const date = new Date(); 320 | const step = parseInt(m[3], 10); 321 | const from = m[1] 322 | ? filterDate(m[1], data, format, utc) 323 | : mm( 324 | /(minute|min|hour|second)s?/.test(m[4]) 325 | ? [ 326 | date.getFullYear(), 327 | date.getMonth(), 328 | date.getDate(), 329 | date.getHours(), 330 | date.getMinutes(), 331 | date.getSeconds() 332 | ] 333 | : [date.getFullYear(), date.getMonth(), date.getDate()] 334 | ); 335 | 336 | return m[2] === '-' 337 | ? from.subtract(step, timeUnitMap[m[4]] as moment.DurationInputArg2) 338 | : from.add(step, timeUnitMap[m[4]] as moment.DurationInputArg2); 339 | // return from[m[2] === '-' ? 'subtract' : 'add'](step, mapping[m[4]] || m[4]); 340 | } else if (value === 'now') { 341 | return mm(); 342 | } else if (value === 'today') { 343 | const date = new Date(); 344 | return mm([date.getFullYear(), date.getMonth(), date.getDate()]); 345 | } else { 346 | const result = mm(value); 347 | return result.isValid() ? result : mm(value, format); 348 | } 349 | }; 350 | 351 | export function parseDuration(str: string): moment.Duration | undefined { 352 | const matches = 353 | /^((?:\-|\+)?(?:\d*\.)?\d+)(minute|min|hour|day|week|month|quarter|year|weekday|second|millisecond)s?$/.exec( 354 | str 355 | ); 356 | 357 | if (matches) { 358 | const duration = moment.duration(parseFloat(matches[1]), matches[2] as any); 359 | 360 | if (moment.isDuration(duration)) { 361 | return duration; 362 | } 363 | } 364 | 365 | return; 366 | } 367 | 368 | // 主要用于解决 0.1+0.2 结果的精度问题导致太长 369 | export function stripNumber(number: number) { 370 | if (typeof number === 'number') { 371 | return parseFloat(number.toPrecision(12)); 372 | } else { 373 | return number; 374 | } 375 | } 376 | 377 | export function pickValues(names: string, data: object) { 378 | let arr: Array; 379 | if (!names || ((arr = names.split(',')) && arr.length < 2)) { 380 | let idx = names.indexOf('~'); 381 | if (~idx) { 382 | let key = names.substring(0, idx); 383 | let target = names.substring(idx + 1); 384 | return { 385 | [key]: resolveVariable(target, data) 386 | }; 387 | } 388 | return resolveVariable(names, data); 389 | } 390 | 391 | let ret: any = {}; 392 | arr.forEach(name => { 393 | let idx = name.indexOf('~'); 394 | let target = name; 395 | 396 | if (~idx) { 397 | target = name.substring(idx + 1); 398 | name = name.substring(0, idx); 399 | } 400 | 401 | setVariable(ret, name, resolveVariable(target, data)); 402 | }); 403 | return ret; 404 | } 405 | 406 | export function resolveVariable(path?: string, data: any = {}): any { 407 | if (path === '&' || path == '$$') { 408 | return data; 409 | } else if (!path || typeof path !== 'string') { 410 | return undefined; 411 | } else if (!~path.indexOf(':')) { 412 | // 简单用法直接用 getVariable 413 | return getVariable(data, path[0] === '$' ? path.substring(1) : path); 414 | } 415 | 416 | // window:xxx ls:xxx.xxx 417 | // 带 namespace 的用公式 418 | // 主要是用公式会严格点,不能出现奇怪的变量名 419 | try { 420 | return new Evaluator(data).evalute( 421 | parse(path, { 422 | variableMode: true, 423 | allowFilter: false 424 | }) 425 | ); 426 | } catch (e) { 427 | return undefined; 428 | } 429 | } 430 | 431 | export function isPureVariable(path?: any): path is string { 432 | return typeof path === 'string' 433 | ? /^\$(?:((?:\w+\:)?[a-z0-9_.][a-z0-9_.\[\]]*)|{[^}{]+})$/i.test(path) 434 | : false; 435 | } 436 | 437 | export const resolveVariableAndFilter = ( 438 | path?: string, 439 | data: object = {}, 440 | defaultFilter: string = '| html', 441 | fallbackValue = (value: any) => value 442 | ) => { 443 | if (!path || typeof path !== 'string') { 444 | return undefined; 445 | } 446 | 447 | try { 448 | const ast = parse(path, { 449 | evalMode: false, 450 | allowFilter: true 451 | }); 452 | 453 | const ret = new Evaluator(data, { 454 | defaultFilter 455 | }).evalute(ast); 456 | 457 | return ret == null && !~path.indexOf('default') && !~path.indexOf('now') 458 | ? fallbackValue(ret) 459 | : ret; 460 | } catch (e) { 461 | console.warn(e); 462 | return undefined; 463 | } 464 | }; 465 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist/", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": ["es6", "dom", "ES2015"], 7 | "sourceMap": true, 8 | "jsx": "react", 9 | "moduleResolution": "node", 10 | "rootDir": "", 11 | "importHelpers": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "sourceRoot": "src", 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noImplicitAny": true, 18 | "strictNullChecks": true, 19 | "experimentalDecorators": true, 20 | "emitDecoratorMetadata": true, 21 | "skipLibCheck": true, 22 | "skipDefaultLibCheck": true, 23 | "allowJs": true 24 | }, 25 | "include": ["src/**/*"], 26 | "exclude": [ 27 | "node_modules", 28 | "build", 29 | "scripts", 30 | "acceptance-tests", 31 | "webpack", 32 | "jest", 33 | "src/setupTests.ts" 34 | ], 35 | "types": ["typePatches"] 36 | } 37 | --------------------------------------------------------------------------------