├── .npmignore ├── .babelrc ├── .gitignore ├── .eslintrc ├── src ├── contexts.ts ├── utils.ts ├── types.ts ├── destructor.ts ├── tokens.ts ├── matcher.ts ├── tokenizer.ts ├── lru.ts ├── parser.ts └── index.ts ├── tsconfig.json ├── jest.config.js ├── package.json ├── README.md └── test ├── match.spec.ts ├── accessor.spec.ts └── parser.spec.ts /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "loose": true }], "@babel/preset-react"], 3 | "plugins":[ 4 | "@babel/plugin-proposal-class-properties" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | node_modules 5 | *.log 6 | TODOs.md 7 | TODO.md 8 | explorations 9 | test/e2e/reports 10 | test/e2e/screenshots 11 | coverage 12 | dist 13 | lib -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "globals": { 6 | "sleep": true, 7 | "prettyFormat": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 10, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/contexts.ts: -------------------------------------------------------------------------------- 1 | export type Context = { 2 | flag : string, 3 | [key : string] : any 4 | } 5 | 6 | const ContextType = (flag: string, props?: any) : Context => { 7 | return { 8 | flag, 9 | ...props 10 | } 11 | } 12 | 13 | export const bracketContext = ContextType("[]") 14 | 15 | export const bracketArrayContext = ContextType("[\\d]") 16 | 17 | export const bracketDContext = ContextType("[[]]") 18 | 19 | export const parenContext = ContextType("()") 20 | 21 | export const braceContext = ContextType("{}") 22 | 23 | export const destructorContext = ContextType("{x}") 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "esModuleInterop": true, 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "module": "commonjs", 8 | "target": "es5", 9 | "lib": ["es2015", "es2017", "dom"], 10 | "allowJs": false, 11 | "noUnusedLocals": true, 12 | "removeComments": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": false, 15 | "declaration": true, 16 | "baseUrl": "." 17 | }, 18 | "include": [ 19 | "./src/**/*.ts", 20 | "./src/**/*.tsx" 21 | ], 22 | "exclude": [ 23 | "./src/__tests__/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | verbose: true, 4 | testEnvironment: 'jsdom', 5 | transform: { 6 | '^.+\\.jsx?$': 'babel-jest' 7 | }, 8 | preset: 'ts-jest', 9 | globals: { 10 | 'ts-jest': { 11 | babelConfig: true, 12 | diagnostics: true, 13 | tsConfig: './tsconfig.json' 14 | } 15 | }, 16 | //watchPlugins: ['jest-watch-lerna-packages'], 17 | coveragePathIgnorePatterns: [ 18 | '/node_modules/', 19 | 'package.json', 20 | '/demo/', 21 | '/packages/builder/src/__tests__/', 22 | '/packages/builder/src/components/', 23 | '/packages/builder/src/configs/', 24 | 'package-lock.json' 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "depath", 3 | "version": "1.0.5", 4 | "description": "Path Matcher/Getter/Setter for Object/Array", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": " cross-env NODE_ENV=test jest", 8 | "build": "tsc --declaration", 9 | "precommit": "lint-staged && npm run test && npm run build && git add -A" 10 | }, 11 | "types": "lib/index.d.ts", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/janryWang/depath.git" 15 | }, 16 | "author": "janryWang", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/janryWang/depath/issues" 20 | }, 21 | "homepage": "https://github.com/janryWang/depath#readme", 22 | "devDependencies": { 23 | "@types/jest": "^24.0.18", 24 | "babel-jest": "^24.9.0", 25 | "cross-env": "^5.1.4", 26 | "husky": "^0.14.3", 27 | "jest": "^24.9.0", 28 | "jest-codemods": "^0.20.0", 29 | "lint-staged": "^4.3.0", 30 | "prettier": "^1.11.x", 31 | "ts-jest": "^24.0.2", 32 | "typescript": "^3.6.2" 33 | }, 34 | "lint-staged": { 35 | "src/*.js": [ 36 | "prettier --write --tab-width 2 --no-semi", 37 | "git add" 38 | ], 39 | "dist/*.js": [ 40 | "prettier --write --tab-width 2 --no-semi", 41 | "git add" 42 | ], 43 | "test.js": [ 44 | "prettier --write --tab-width 2 --no-semi", 45 | "git add" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | const isType = (type: string) => (obj: unknown): obj is T => 2 | obj != null && Object.prototype.toString.call(obj) === `[object ${type}]` 3 | export const isFn = isType<(...args: any[]) => any>('Function') 4 | export const isArr = Array.isArray || isType('Array') 5 | export const isPlainObj = isType('Object') 6 | export const isStr = isType('String') 7 | export const isBool = isType('Boolean') 8 | export const isNum = isType('Number') 9 | export const isObj = (val: unknown): val is object => typeof val === 'object' 10 | export const isRegExp = isType('RegExp') 11 | export const isNumberLike = (t: any) => { 12 | return isNum(t) || /^(\d+)(\.\d+)?$/.test(t) 13 | } 14 | const isArray = isArr 15 | const keyList = Object.keys 16 | const hasProp = Object.prototype.hasOwnProperty 17 | 18 | export const toArray = (val: T | T[]): T[] => 19 | Array.isArray(val) ? val : val !== undefined ? [val] : [] 20 | 21 | export const isEqual = (a: any, b: any) => { 22 | if (a === b) { 23 | return true 24 | } 25 | if (a && b && typeof a === 'object' && typeof b === 'object') { 26 | const arrA = isArray(a) 27 | const arrB = isArray(b) 28 | let i 29 | let length 30 | let key 31 | 32 | if (arrA && arrB) { 33 | length = a.length 34 | if (length !== b.length) { 35 | return false 36 | } 37 | for (i = length; i-- !== 0; ) { 38 | if (!isEqual(a[i], b[i])) { 39 | return false 40 | } 41 | } 42 | return true 43 | } 44 | 45 | if (arrA !== arrB) { 46 | return false 47 | } 48 | 49 | const keys = keyList(a) 50 | length = keys.length 51 | 52 | if (length !== keyList(b).length) { 53 | return false 54 | } 55 | 56 | for (i = length; i-- !== 0; ) { 57 | if (!hasProp.call(b, keys[i])) { 58 | return false 59 | } 60 | } 61 | for (i = length; i-- !== 0; ) { 62 | key = keys[i] 63 | if (!isEqual(a[key], b[key])) { 64 | return false 65 | } 66 | } 67 | 68 | return true 69 | } 70 | return a !== a && b !== b 71 | } 72 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Path } from './index' 2 | interface INode { 3 | type?: string 4 | after?: Node 5 | depth?: number 6 | } 7 | 8 | export type Node = 9 | | IdentifierNode 10 | | WildcardOperatorNode 11 | | GroupExpressionNode 12 | | RangeExpressionNode 13 | | DestructorExpressionNode 14 | | ObjectPatternNode 15 | | ArrayPatternNode 16 | | DotOperatorNode 17 | | ExpandOperatorNode 18 | | INode 19 | 20 | export type IdentifierNode = { 21 | type: 'Identifier' 22 | value: string 23 | arrayIndex?: boolean 24 | } & INode 25 | 26 | export type IgnoreExpressionNode = { 27 | type: 'IgnoreExpression' 28 | value: string 29 | } & INode 30 | 31 | export type DotOperatorNode = { 32 | type: 'DotOperator' 33 | } & INode 34 | 35 | export type WildcardOperatorNode = { 36 | type: 'WildcardOperator' 37 | filter?: GroupExpressionNode | RangeExpressionNode 38 | } & INode 39 | 40 | export type ExpandOperatorNode = { 41 | type: 'ExpandOperator' 42 | } & INode 43 | 44 | export type GroupExpressionNode = { 45 | type: 'GroupExpression' 46 | value: Node[] 47 | isExclude?: boolean 48 | } & INode 49 | 50 | export type RangeExpressionNode = { 51 | type: 'RangeExpression' 52 | start?: IdentifierNode 53 | end?: IdentifierNode 54 | } & INode 55 | 56 | export type DestructorExpressionNode = { 57 | type: 'DestructorExpression' 58 | value?: ObjectPatternNode | ArrayPatternNode 59 | source?: string 60 | } & INode 61 | 62 | export type ObjectPatternNode = { 63 | type: 'ObjectPattern' 64 | properties: ObjectPatternPropertyNode[] 65 | } & INode 66 | 67 | export type ObjectPatternPropertyNode = { 68 | type: 'ObjectPatternProperty' 69 | key: IdentifierNode 70 | value?: ObjectPatternNode[] | ArrayPatternNode[] | IdentifierNode 71 | } & INode 72 | 73 | export type ArrayPatternNode = { 74 | type: 'ArrayPattern' 75 | elements: ObjectPatternNode[] | ArrayPatternNode[] | IdentifierNode[] 76 | } & INode 77 | 78 | export type DestrcutorRule = { 79 | key?: string | number 80 | path?: Array 81 | } 82 | 83 | export type MatcherFunction = ((path: Segments) => boolean) & { 84 | path: Path 85 | } 86 | 87 | export type Pattern = 88 | | string 89 | | number 90 | | Path 91 | | Segments 92 | | MatcherFunction 93 | | RegExp 94 | 95 | export type DestrcutorRules = DestrcutorRule[] 96 | 97 | export type Segments = Array 98 | 99 | export const isType = (type: string) => (obj: any): obj is T => { 100 | return obj && obj.type === type 101 | } 102 | 103 | export const isIdentifier = isType('Identifier') 104 | 105 | export const isIgnoreExpression = isType( 106 | 'IgnoreExpression' 107 | ) 108 | 109 | export const isDotOperator = isType('DotOperator') 110 | 111 | export const isWildcardOperator = isType( 112 | 'WildcardOperator' 113 | ) 114 | 115 | export const isExpandOperator = isType('ExpandOperator') 116 | 117 | export const isGroupExpression = isType('GroupExpression') 118 | 119 | export const isRangeExpression = isType('RangeExpression') 120 | 121 | export const isDestructorExpression = isType( 122 | 'DestructorExpression' 123 | ) 124 | 125 | export const isObjectPattern = isType('ObjectPattern') 126 | 127 | export const isObjectPatternProperty = isType( 128 | 'ObjectPatternProperty' 129 | ) 130 | 131 | export const isArrayPattern = isType('ArrayPattern') 132 | 133 | export type KeyType = string | number | symbol 134 | 135 | export type IAccessors = { 136 | get?: (source: any, key: KeyType) => any 137 | set?: (source: any, key: KeyType, value: any) => any 138 | has?: (source: any, key: KeyType) => boolean 139 | delete?: (source: any, key: KeyType) => any 140 | } 141 | 142 | export type IRegistry = { 143 | accessors?: IAccessors 144 | } 145 | -------------------------------------------------------------------------------- /src/destructor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Segments, 3 | Node, 4 | DestrcutorRules, 5 | isArrayPattern, 6 | isObjectPattern, 7 | isIdentifier, 8 | isDestructorExpression 9 | } from './types' 10 | import { isNum } from './utils' 11 | 12 | type Mutatators = { 13 | getIn: (segments: Segments, source: any) => any 14 | setIn: (segments: Segments, source: any, value: any) => void 15 | deleteIn?: (segments: Segments, source: any) => any 16 | existIn?: (segments: Segments, source: any, start: number) => boolean 17 | } 18 | 19 | const DestrcutorCache = new Map() 20 | 21 | const isValid = (val: any) => val !== undefined && val !== null 22 | 23 | export const getDestructor = (source: string) => { 24 | return DestrcutorCache.get(source) 25 | } 26 | 27 | export const setDestructor = (source: string, rules: DestrcutorRules) => { 28 | DestrcutorCache.set(source, rules) 29 | } 30 | 31 | export const parseDestructorRules = (node: Node): DestrcutorRules => { 32 | let rules = [] 33 | if (isObjectPattern(node)) { 34 | let index = 0 35 | node.properties.forEach(child => { 36 | rules[index] = { 37 | path: [] 38 | } 39 | rules[index].key = child.key.value 40 | rules[index].path.push(child.key.value) 41 | if (isIdentifier(child.value)) { 42 | rules[index].key = child.value.value 43 | } 44 | const basePath = rules[index].path 45 | const childRules = parseDestructorRules(child.value as Node) 46 | let k = index 47 | childRules.forEach(rule => { 48 | if (rules[k]) { 49 | rules[k].key = rule.key 50 | rules[k].path = basePath.concat(rule.path) 51 | } else { 52 | rules[k] = { 53 | key: rule.key, 54 | path: basePath.concat(rule.path) 55 | } 56 | } 57 | k++ 58 | }) 59 | if (k > index) { 60 | index = k 61 | } else { 62 | index++ 63 | } 64 | }) 65 | return rules 66 | } else if (isArrayPattern(node)) { 67 | let index = 0 68 | node.elements.forEach((child, key) => { 69 | rules[index] = { 70 | path: [] 71 | } 72 | rules[index].key = key 73 | rules[index].path.push(key) 74 | if (isIdentifier(child)) { 75 | rules[index].key = child.value 76 | } 77 | const basePath = rules[index].path 78 | const childRules = parseDestructorRules(child as Node) 79 | let k = index 80 | childRules.forEach(rule => { 81 | if (rules[k]) { 82 | rules[k].key = rule.key 83 | rules[k].path = basePath.concat(rule.path) 84 | } else { 85 | rules[k] = { 86 | key: rule.key, 87 | path: basePath.concat(rule.path) 88 | } 89 | } 90 | k++ 91 | }) 92 | if (k > index) { 93 | index = k 94 | } else { 95 | index++ 96 | } 97 | }) 98 | return rules 99 | } 100 | if (isDestructorExpression(node)) { 101 | return parseDestructorRules(node.value) 102 | } 103 | return rules 104 | } 105 | 106 | export const setInByDestructor = ( 107 | source: any, 108 | rules: DestrcutorRules, 109 | value: any, 110 | mutators: Mutatators 111 | ) => { 112 | rules.forEach(({ key, path }) => { 113 | mutators.setIn([key], source, mutators.getIn(path, value)) 114 | }) 115 | } 116 | 117 | export const getInByDestructor = ( 118 | source: any, 119 | rules: DestrcutorRules, 120 | mutators: Mutatators 121 | ) => { 122 | let response = {} 123 | if (rules.length) { 124 | if (isNum(rules[0].path[0])) { 125 | response = [] 126 | } 127 | } 128 | source = isValid(source) ? source : {} 129 | rules.forEach(({ key, path }) => { 130 | mutators.setIn(path, response, source[key]) 131 | }) 132 | return response 133 | } 134 | 135 | export const deleteInByDestructor = ( 136 | source: any, 137 | rules: DestrcutorRules, 138 | mutators: Mutatators 139 | ) => { 140 | rules.forEach(({ key }) => { 141 | mutators.deleteIn([key], source) 142 | }) 143 | } 144 | 145 | export const existInByDestructor = ( 146 | source: any, 147 | rules: DestrcutorRules, 148 | start: number, 149 | mutators: Mutatators 150 | ) => { 151 | return rules.every(({ key }) => { 152 | return mutators.existIn([key], source, start) 153 | }) 154 | } 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Depath 2 | 3 | > Path utils for match/getter/setter 4 | 5 | 6 | 7 | ### Usage 8 | 9 | ``` 10 | import { Path } from "depath" 11 | 12 | const path = new Path("a.b.*") 13 | 14 | path.match(["a","b","c"]) // true 15 | 16 | 17 | ``` 18 | 19 | 20 | 21 | ### Install 22 | 23 | ``` 24 | npm install --save depath 25 | ``` 26 | 27 | ### API 28 | 29 | - Constructor 30 | 31 | - `new Path(pattern : string | number | Path | Array)` 32 | 33 | - Methods/Properties 34 | 35 | - `concat(...args: Array)` 36 | 37 | - `slice(start?: number, end?: number)` 38 | 39 | - `push(item: string | number)` 40 | 41 | - `pop()` 42 | 43 | - `splice(start: number,deleteCount?: number,...items: Array)` 44 | 45 | - `forEach(callback: (key: string | number)` 46 | 47 | - `map(callback: (key: string | number)` 48 | 49 | - `reduce(callback: (buf: T, item: string | number, index: number) => T, initial: T) : T` 50 | 51 | - `parent()` 52 | 53 | - `includes(pattern: Pattern)` 54 | 55 | - `transform(regexp: string | RegExp,callback: (...args: string[]) => T) : T` 56 | 57 | - `match(pattern: Pattern)` 58 | 59 | - `getIn(source?: any)` 60 | 61 | - `setIn(source?: any, value?: any)` 62 | 63 | - `deleteIn(source?: any)` 64 | 65 | - `existIn(source?: any,start?:number | Path)` 66 | 67 | - Static Methods 68 | 69 | - `parse(pattern: Pattern)` 70 | 71 | - `getIn(source: any, pattern: Pattern)` 72 | 73 | - `setIn(source: any, pattern: Pattern, value: any)` 74 | 75 | - `deleteIn(source: any, pattern: Pattern)` 76 | 77 | - `existIn(source: any,pattern: Pattern,start?:number | Path)` 78 | 79 | - `transform(pattern: Pattern,regexp: string | RegExp,callback: (...args: string[]) => T):T` 80 | 81 | ### Getter/Setter Destructor Syntax 82 | 83 | - Object Pattern 84 | 85 | ```javascript 86 | Path.setIn({},'a.b.c.{aaa,bbb}',{aaa:123,bbb:321}) 87 | ==> 88 | {a:{b:{c:{aaa:123,bbb:321}}}} 89 | Path.getIn({a:{b:{c:{aaa:123,bbb:321}}}},'a.b.c.{aaa,bbb}') 90 | ==> 91 | {aaa:123,bbb:321} 92 | 93 | Path.setIn({a:{b:{c:{kkk:'ddd'}}}},'a.b.c.{aaa,bbb}',{aaa:123,bbb:321}) 94 | ==> 95 | {a:{b:{c:{aaa:123,bbb:321,kkk:'ddd'}}}} 96 | Path.getIn({a:{b:{c:{aaa:123,bbb:321,kkk:'ddd'}}}},'a.b.c.{aaa,bbb}') 97 | ==> 98 | {aaa:123,bbb:321} 99 | 100 | Path.setIn({a:{b:{c:{kkk:'ddd'}}}},'a.b.c.{aaa:ooo,bbb}',{aaa:123,bbb:321}) 101 | ==> 102 | {a:{b:{c:{ooo:123,bbb:321,kkk:'ddd'}}}} 103 | Path.getIn({a:{b:{c:{ooo:123,bbb:321,kkk:'ddd'}}}},'a.b.c.{aaa:ooo,bbb}') 104 | ==> 105 | {aaa:123,bbb:321} 106 | ``` 107 | 108 | 109 | - Array Pattern 110 | 111 | ```javascript 112 | Path.setIn({},'a.b.c.[aaa,bbb]',[123,321]) 113 | ==> 114 | {a:{b:{c:{aaa:123,bbb:321}}}} 115 | Path.getIn({a:{b:{c:{aaa:123,bbb:321}}}},'a.b.c.[aaa,bbb]') 116 | ==> 117 | [123,321] 118 | 119 | Path.setIn({a:{b:{c:{kkk:'ddd'}}}},'a.b.c.[aaa,bbb]',[123,321]) 120 | ==> 121 | {a:{b:{c:{aaa:123,bbb:321,kkk:'ddd'}}}} 122 | Path.getIn({a:{b:{c:{aaa:123,bbb:321,kkk:'ddd'}}}},'a.b.c.[aaa,bbb]') 123 | ==> 124 | [123,321] 125 | ``` 126 | 127 | - Nested Array and Object Pattern 128 | 129 | ```javascript 130 | Path.setIn({},'a.b.c.[{ddd,kkk:mmm},bbb]',[{ddd:123,kkk:'hhhh'},321]) 131 | ==> 132 | {a:{b:{c:{ddd:123,bbb:321,mmm:'hhh'}}}} 133 | Path.getIn({a:{b:{c:{ddd:123,bbb:321,mmm:'hhh'}}}},'a.b.c.[{ddd,kkk:mmm},bbb]') 134 | ==> 135 | [{ddd:123,kkk:'hhh'},321] 136 | 137 | Path.setIn({a:{b:{c:{kkk:'ddd'}}}},'a.b.c.{aaa:ooo,bbb:[ccc,ddd]}',{aaa:123,bbb:[123,321]}) 138 | ==> 139 | {a:{b:{c:{ooo:123,ccc:123,ddd:321,kkk:'ddd'}}}} 140 | Path.getIn({a:{b:{c:{ooo:123,ccc:123,ddd:321,kkk:'ddd'}}}},'a.b.c.{aaa:ooo,bbb:[ccc,ddd]}') 141 | ==> 142 | {aaa:123,bbb:[123,321]} 143 | ``` 144 | 145 | 146 | 147 | 148 | ### Path Match Pattern Syntax 149 | 150 | 151 | 152 | **Wildcard** 153 | 154 | ``` 155 | "*" 156 | ``` 157 | 158 | **Expand String** 159 | 160 | ``` 161 | "aaa~" or "~" or "aaa~.bbb.cc" 162 | ``` 163 | 164 | **Part Wildcard** 165 | 166 | ``` 167 | "a.b.*.c.*" 168 | ``` 169 | 170 | 171 | 172 | **Wildcard With Group Filter** 173 | 174 | ``` 175 | "a.b.*(aa.bb.dd,cc,mm)" 176 | or 177 | "a.b.*(!aa.bb.dd,cc,mm)" 178 | ``` 179 | 180 | 181 | 182 | **Wildcard With Nest Group Filter** 183 | 184 | ``` 185 | "a.b.*(aa.bb.*(aa.b,c),cc,mm)" 186 | or 187 | "a.b.*(!aa.bb.*(aa.b,c),cc,mm)" 188 | ``` 189 | 190 | 191 | 192 | **Wildcard With Range Filter** 193 | 194 | ``` 195 | "a.b.*[10:100]" 196 | or 197 | "a.b.*[10:]" 198 | or 199 | "a.b.*[:100]" 200 | ``` 201 | 202 | **Ignore Key Word** 203 | 204 | ``` 205 | "a.b.[[cc.uu()sss*\\[1222\\]]]" 206 | ``` 207 | 208 | 209 | 210 | 211 | ### LICENSE 212 | 213 | The MIT License (MIT) 214 | 215 | Copyright (c) 2018 JanryWang 216 | 217 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 218 | 219 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 220 | 221 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/tokens.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bracketContext, 3 | parenContext, 4 | bracketArrayContext, 5 | bracketDContext, 6 | braceContext, 7 | destructorContext, 8 | } from './contexts' 9 | 10 | interface ITokenProps { 11 | expectNext?: (next?: Token) => boolean 12 | expectPrev?: (prev?: Token) => boolean 13 | updateContext?: (prev?: Token) => void 14 | } 15 | 16 | export type Token = ITokenProps & { 17 | flag: string 18 | } 19 | 20 | const TokenType = (flag: string, props?: ITokenProps): Token => { 21 | return { 22 | flag, 23 | ...props, 24 | } 25 | } 26 | 27 | export const nameTok = TokenType('name', { 28 | expectNext(next) { 29 | if (this.includesContext(destructorContext)) { 30 | return ( 31 | next === nameTok || 32 | next === commaTok || 33 | next === bracketRTok || 34 | next === braceRTok || 35 | next === colonTok 36 | ) 37 | } 38 | return ( 39 | next === dotTok || 40 | next === commaTok || 41 | next === eofTok || 42 | next === bracketRTok || 43 | next === parenRTok || 44 | next === colonTok || 45 | next === expandTok || 46 | next === bracketLTok 47 | ) 48 | }, 49 | }) 50 | export const starTok = TokenType('*', { 51 | expectNext(next) { 52 | return ( 53 | next === dotTok || 54 | next === starTok || 55 | next === parenLTok || 56 | next === bracketLTok || 57 | next === eofTok || 58 | next === commaTok || 59 | next === parenRTok 60 | ) 61 | }, 62 | }) 63 | export const dotTok = TokenType('.', { 64 | expectNext(next) { 65 | return ( 66 | next === dotTok || 67 | next === nameTok || 68 | next === bracketDLTok || 69 | next === starTok || 70 | next === bracketLTok || 71 | next === braceLTok || 72 | next === eofTok 73 | ) 74 | }, 75 | expectPrev(prev) { 76 | return ( 77 | prev === dotTok || 78 | prev === nameTok || 79 | prev === bracketDRTok || 80 | prev === starTok || 81 | prev === parenRTok || 82 | prev === bracketRTok || 83 | prev === expandTok || 84 | prev === braceRTok 85 | ) 86 | }, 87 | }) 88 | export const bangTok = TokenType('!', { 89 | expectNext(next) { 90 | return next === nameTok || next === bracketDLTok 91 | }, 92 | }) 93 | export const colonTok = TokenType(':', { 94 | expectNext(next) { 95 | if (this.includesContext(destructorContext)) { 96 | return next === nameTok || next === braceLTok || next === bracketLTok 97 | } 98 | return next === nameTok || next === bracketDLTok || next === bracketRTok 99 | }, 100 | }) 101 | 102 | export const braceLTok = TokenType('{', { 103 | expectNext(next) { 104 | return next === nameTok 105 | }, 106 | expectPrev(prev) { 107 | if (this.includesContext(destructorContext)) { 108 | return prev === colonTok || prev === commaTok || prev === bracketLTok 109 | } 110 | return prev === dotTok || prev === colonTok 111 | }, 112 | updateContext(prev) { 113 | this.state.context.push(braceContext) 114 | }, 115 | }) 116 | 117 | export const braceRTok = TokenType('}', { 118 | expectNext(next) { 119 | if (this.includesContext(destructorContext)) { 120 | return ( 121 | next === commaTok || 122 | next === braceRTok || 123 | next === eofTok || 124 | next === bracketRTok 125 | ) 126 | } 127 | return next === dotTok || next === eofTok 128 | }, 129 | expectPrev(prev) { 130 | return prev === nameTok || prev === braceRTok || prev === bracketRTok 131 | }, 132 | updateContext() { 133 | this.state.context.pop(braceContext) 134 | }, 135 | }) 136 | 137 | export const bracketLTok = TokenType('[', { 138 | expectNext(next) { 139 | if (this.includesContext(destructorContext)) { 140 | return ( 141 | next === nameTok || 142 | next === bracketLTok || 143 | next === braceLTok || 144 | next === bracketRTok 145 | ) 146 | } 147 | return ( 148 | next === nameTok || 149 | next === bracketDLTok || 150 | next === colonTok || 151 | next === bracketLTok || 152 | next === ignoreTok || 153 | next === bracketRTok 154 | ) 155 | }, 156 | expectPrev(prev) { 157 | if (this.includesContext(destructorContext)) { 158 | return prev === colonTok || prev === commaTok || prev === bracketLTok 159 | } 160 | return ( 161 | prev === starTok || 162 | prev === bracketLTok || 163 | prev === dotTok || 164 | prev === nameTok || 165 | prev === parenLTok || 166 | prev == commaTok 167 | ) 168 | }, 169 | updateContext(prev) { 170 | this.state.context.push(bracketContext) 171 | }, 172 | }) 173 | 174 | export const bracketRTok = TokenType(']', { 175 | expectNext(next) { 176 | if (this.includesContext(destructorContext)) { 177 | return ( 178 | next === commaTok || 179 | next === braceRTok || 180 | next === bracketRTok || 181 | next === eofTok 182 | ) 183 | } 184 | return ( 185 | next === dotTok || 186 | next === eofTok || 187 | next === commaTok || 188 | next === parenRTok || 189 | next === bracketRTok 190 | ) 191 | }, 192 | updateContext(prev) { 193 | if (this.includesContext(bracketArrayContext)) return 194 | if (!this.includesContext(bracketContext)) throw this.unexpect() 195 | this.state.context.pop() 196 | }, 197 | }) 198 | 199 | export const bracketDLTok = TokenType('[[', { 200 | updateContext() { 201 | this.state.context.push(bracketDContext) 202 | }, 203 | }) 204 | 205 | export const bracketDRTok = TokenType(']]', { 206 | updateContext() { 207 | if (this.curContext() !== bracketDContext) throw this.unexpect() 208 | this.state.context.pop() 209 | }, 210 | }) 211 | 212 | export const parenLTok = TokenType('(', { 213 | expectNext(next) { 214 | return ( 215 | next === nameTok || 216 | next === bracketDLTok || 217 | next === bangTok || 218 | next === bracketLTok 219 | ) 220 | }, 221 | expectPrev(prev) { 222 | return prev === starTok 223 | }, 224 | updateContext(prev) { 225 | this.state.context.push(parenContext) 226 | }, 227 | }) 228 | export const parenRTok = TokenType(')', { 229 | expectNext(next) { 230 | return ( 231 | next === dotTok || 232 | next === eofTok || 233 | next === commaTok || 234 | next === parenRTok 235 | ) 236 | }, 237 | updateContext() { 238 | if (this.curContext() !== parenContext) throw this.unexpect() 239 | this.state.context.pop() 240 | }, 241 | }) 242 | 243 | export const commaTok = TokenType(',', { 244 | expectNext(next) { 245 | return ( 246 | next === nameTok || 247 | next === bracketDLTok || 248 | next === bracketLTok || 249 | next === braceLTok 250 | ) 251 | }, 252 | }) 253 | export const ignoreTok = TokenType('ignore', { 254 | expectNext(next) { 255 | return next === bracketDRTok 256 | }, 257 | expectPrev(prev) { 258 | return prev == bracketDLTok 259 | }, 260 | }) 261 | 262 | export const expandTok = TokenType('expandTok', { 263 | expectNext(next) { 264 | return ( 265 | next === dotTok || 266 | next === eofTok || 267 | next === commaTok || 268 | next === parenRTok 269 | ) 270 | }, 271 | }) 272 | 273 | export const eofTok = TokenType('eof') 274 | -------------------------------------------------------------------------------- /test/match.spec.ts: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import { Path } from '../src' 3 | 4 | const match = (obj) => { 5 | for (let name in obj) { 6 | test('test match ' + name, () => { 7 | const path = new Path(name) 8 | if (Array.isArray(obj[name]) && Array.isArray(obj[name][0])) { 9 | obj[name].forEach((_path) => { 10 | expect(path.match(_path)).toBeTruthy() 11 | }) 12 | } else { 13 | expect(path.match(obj[name])).toBeTruthy() 14 | } 15 | }) 16 | } 17 | } 18 | 19 | const unmatch = (obj) => { 20 | for (let name in obj) { 21 | test('test unmatch ' + name, () => { 22 | const path = new Path(name) 23 | if (Array.isArray(obj[name]) && Array.isArray(obj[name][0])) { 24 | obj[name].forEach((_path) => { 25 | expect(path.match(_path)).toBeFalsy() 26 | }) 27 | } else { 28 | expect(path.match(obj[name])).toBeFalsy() 29 | } 30 | }) 31 | } 32 | } 33 | 34 | test('test matchGroup', () => { 35 | const pattern = new Path('*(aa,bb,cc)') 36 | expect(pattern.matchAliasGroup('aa', 'bb')).toEqual(true) 37 | const excludePattern = new Path('aa.bb.*(11,22,33).*(!aa,bb,cc)') 38 | expect( 39 | excludePattern.matchAliasGroup('aa.bb.11.mm', 'aa.cc.dd.bb.11.mm') 40 | ).toEqual(true) 41 | expect(excludePattern.matchAliasGroup('aa.cc', 'aa.kk.cc')).toEqual(false) 42 | expect(new Path('aa.*(!bb)').matchAliasGroup('kk.mm.aa.bb', 'aa.bb')).toEqual( 43 | false 44 | ) 45 | expect( 46 | new Path('aa.*(!bb)').matchAliasGroup('kk.mm.aa.bb.cc', 'kk.mm.aa') 47 | ).toEqual(false) 48 | expect(new Path('aa.*(!bb,oo)').matchAliasGroup('kk.mm', 'aa')).toEqual(false) 49 | expect(new Path('aa.*(!bb.*)').matchAliasGroup('kk.mm', 'aa')).toEqual(false) 50 | expect(new Path('aa.*(!bb)').matchAliasGroup('kk.mm.aa.cc', 'aa.cc')).toEqual( 51 | true 52 | ) 53 | const patttern2 = Path.parse('*(array)') 54 | expect(patttern2.matchAliasGroup(['array', 0], ['array', 0])).toEqual(false) 55 | }) 56 | 57 | test('exclude match', () => { 58 | //路径长度相等 59 | expect(Path.parse('*(!aaa)').match('ggg')).toBeTruthy() 60 | expect(Path.parse('*(!aaa)').match('aaa')).toBeFalsy() 61 | expect(Path.parse('*(!aaa.bbb)').match('ggg.ddd')).toBeTruthy() 62 | expect(Path.parse('*(!aaa.ccc)').match('aaa.ccc')).toBeFalsy() 63 | //长路径匹配短路径 64 | expect(Path.parse('*(!aaa.bbb)').match('ggg')).toBeTruthy() 65 | expect(Path.parse('*(!aaa.bbb)').match('aaa')).toBeFalsy() 66 | //短路径匹配长路径 67 | expect(Path.parse('*(!aaa)').match('aaa.bbb')).toBeTruthy() 68 | expect(Path.parse('*(!aaa)').match('aaa.ccc')).toBeTruthy() 69 | expect(Path.parse('*(!aaa)').match('bbb.ccc')).toBeTruthy() 70 | 71 | expect(Path.parse('*(!aaa,bbb)').match('bbb')).toBeFalsy() 72 | expect(Path.parse('*(!aaa.bbb)').match('aaa.ccc')).toBeTruthy() 73 | expect(Path.parse('*(!basic.name,versionTag)').match('basic.id')).toBeTruthy() 74 | expect(Path.parse('*(!basic.name,versionTag)').match('basic')).toBeFalsy() 75 | expect( 76 | Path.parse('*(!basic.name,versionTag)').match('isExecutable') 77 | ).toBeTruthy() 78 | expect( 79 | Path.parse('*(!basic.name,versionTag)').match('versionTag') 80 | ).toBeFalsy() 81 | expect( 82 | Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name') 83 | ).toBeFalsy() 84 | expect( 85 | Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name.kkk') 86 | ).toBeFalsy() 87 | expect(Path.parse('aa.*(!bb)').match('kk.mm.aa.bb.cc')).toBeFalsy() 88 | expect(Path.parse('aa.*(!bb)').match('aa')).toBeFalsy() 89 | expect(Path.parse('aa.*(!bb.*)').match('aa')).toBeFalsy() 90 | expect(Path.parse('aa.*(!bb,cc)').match('aa')).toBeFalsy() 91 | expect(Path.parse('aa.*(!bb,cc)').match('aa.dd')).toBeTruthy() 92 | expect(Path.parse('aa.*(!bb,cc)').match('aa.kk')).toBeTruthy() 93 | }) 94 | 95 | test('match regexp', () => { 96 | expect(Path.parse(/^\d+$/).match('212')).toBeTruthy() 97 | expect(Path.parse(/^\d+$/).match('212dd')).toBeFalsy() 98 | }) 99 | 100 | test('test zero', () => { 101 | expect(Path.parse('t.0.value~').match(['t', 0, 'value_list'])).toEqual(true) 102 | }) 103 | 104 | test('test expand', () => { 105 | expect( 106 | Path.parse('t.0.value~').match(['t', 0, 'value_list', 'hello']) 107 | ).toEqual(false) 108 | }) 109 | 110 | test('test multi expand', () => { 111 | expect(Path.parse('*(aa~,bb~).*').match(['aa12323', 'asdasd'])).toEqual(true) 112 | }) 113 | 114 | test('test group', () => { 115 | const node = Path.parse('*(phases.*.type,phases.*.steps.*.type)') 116 | expect(node.match('phases.0.steps.1.type')).toBeTruthy() 117 | }) 118 | 119 | match({ 120 | '*': [[], ['aa'], ['aa', 'bb', 'cc'], ['aa', 'dd', 'gg']], 121 | '*.a.b': [ 122 | ['c', 'a', 'b'], 123 | ['k', 'a', 'b'], 124 | ['m', 'a', 'b'], 125 | ], 126 | 'a.*.k': [ 127 | ['a', 'b', 'k'], 128 | ['a', 'd', 'k'], 129 | ['a', 'c', 'k'], 130 | ], 131 | 'a.*(b,d,m).k': [ 132 | ['a', 'b', 'k'], 133 | ['a', 'd', 'k'], 134 | ['a', 'm', 'k'], 135 | ], 136 | 'a.*(!b,d,m).*(!a,b)': [ 137 | ['a', 'o', 'k'], 138 | ['a', 'q', 'k'], 139 | ['a', 'c', 'k'], 140 | ], 141 | 'a.*(b.c.d,d,m).k': [ 142 | ['a', 'b', 'c', 'd', 'k'], 143 | ['a', 'd', 'k'], 144 | ['a', 'm', 'k'], 145 | ], 146 | 'a.*(b.*(c,k).d,d,m).k': [ 147 | ['a', 'b', 'c', 'd', 'k'], 148 | ['a', 'b', 'k', 'd', 'k'], 149 | ['a', 'd', 'k'], 150 | ['a', 'm', 'k'], 151 | ], 152 | 'a.b.*': [ 153 | ['a', 'b', 'c', 'd'], 154 | ['a', 'b', 'c'], 155 | ['a', 'b', 2, 'aaa', 3, 'bbb'], 156 | ], 157 | '*(step1,step2).*': [ 158 | ['step1', 'aa', 'bb'], 159 | ['step1', 'aa', 'bb', 'ccc', 'ddd'], 160 | ], 161 | 'dyanmic.*(!dynamic-1)': [ 162 | ['dyanmic', 'dynamic-2'], 163 | ['dyanmic', 'dynamic-3'], 164 | ], 165 | 't.0.value~': [['t', '0', 'value']], 166 | 'a.*[10:50].*(!a,b)': [ 167 | ['a', 49, 's'], 168 | ['a', 10, 's'], 169 | ['a', 50, 's'], 170 | ], 171 | 'a.*[:50].*(!a,b)': [ 172 | ['a', 49, 's'], 173 | ['a', 10, 's'], 174 | ['a', 50, 's'], 175 | ], 176 | 'a.*([[a.b.c]],[[c.b.d~]])': [ 177 | ['a', '[[a.b.c]]'], 178 | ['a', 'c.b.d~'], 179 | ], 180 | 'a.*(!k,d,m).k': [ 181 | ['a', 'u', 'k'], 182 | ['a', 'o', 'k'], 183 | ['a', 'p', 'k'], 184 | ], 185 | 'a\\.\\*\\[1\\]': [['a.*[1]']], 186 | '[[\\[aa,bb\\]]]': [['[aa,bb]']], 187 | '[[\\[aa,bb\\] ]]': [['[aa,bb] ']], 188 | '[[ \\[aa,bb~\\] ]]': [[' [aa,bb~] ']], 189 | 'aa.bb.*': [['aa', 'bb', 'ccc']], 190 | 'a.*': [ 191 | ['a', 'b'], 192 | ['a', 'b', 'c'], 193 | ], 194 | 'aaa.products.0.*': [['aaa', 'products', '0', 'aaa']], 195 | 'aa~.ccc': [ 196 | ['aa', 'ccc'], 197 | ['aa12', 'ccc'], 198 | ], 199 | '*(aa~,bb~).*': [ 200 | ['aa12323', 'asdasd'], 201 | ['bb12222', 'asd'], 202 | ], 203 | '*(aa,bb,bb.aa)': [['bb', 'aa']], 204 | '*(!aa,bb,bb.aa)': [['xx'], ['yyy']], 205 | '*(!aaa)': [['bbb']], 206 | '*(!aaa,bbb)': [['ccc'], ['ggg']], 207 | }) 208 | 209 | unmatch({ 210 | 'a.*': [['a'], ['b']], 211 | '*(array)': [['array', '0']], 212 | 'aa.bb.*': [['aa', 'bb']], 213 | 'a.*.b': [['a', 'k', 'b', 'd']], 214 | '*(!aaa)': [['aaa']], 215 | 'dyanmic.*(!dynamic-1)': [['dyanmic', 'dynamic-1']], 216 | 'dyanmic.*(!dynamic-1.*)': [['dyanmic', 'dynamic-1', 'ccc']], 217 | a: [['c', 'b']], 218 | 'aa~.ccc': [['a', 'ccc'], ['aa'], ['aaasdd']], 219 | bb: [['bb', 'cc']], 220 | 'aa.*(cc,bb).*.aa': [['aa', 'cc', '0', 'bb']], 221 | }) 222 | -------------------------------------------------------------------------------- /src/matcher.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Segments, 3 | Node, 4 | isIdentifier, 5 | isExpandOperator, 6 | isWildcardOperator, 7 | isGroupExpression, 8 | isRangeExpression, 9 | isIgnoreExpression, 10 | isDotOperator, 11 | isDestructorExpression, 12 | IdentifierNode, 13 | IgnoreExpressionNode, 14 | DestructorExpressionNode, 15 | ExpandOperatorNode, 16 | WildcardOperatorNode, 17 | GroupExpressionNode, 18 | RangeExpressionNode, 19 | DotOperatorNode 20 | } from './types' 21 | import { isEqual, toArray } from './utils' 22 | 23 | const isValid = val => val !== undefined && val !== null && val !== '' 24 | 25 | export class Matcher { 26 | private tree: Node 27 | 28 | private pos: number 29 | 30 | private tail: Node 31 | 32 | private stack: any[] 33 | 34 | private excluding: boolean 35 | 36 | private record: any 37 | 38 | constructor(tree: Node, record?: any) { 39 | this.tree = tree 40 | this.pos = 0 41 | this.excluding = false 42 | this.record = record 43 | this.stack = [] 44 | } 45 | 46 | currentElement(path: Segments) { 47 | return String(path[this.pos] || '').replace(/\s*/g, '') 48 | } 49 | 50 | matchNext = (node: any, path: any) => { 51 | return node.after 52 | ? this.matchAtom(path, node.after) 53 | : isValid(path[this.pos]) 54 | } 55 | 56 | recordMatch(match: () => boolean) { 57 | return () => { 58 | const result = match() 59 | if (result) { 60 | if (this.record && this.record.score !== undefined) { 61 | this.record.score++ 62 | } 63 | } 64 | return result 65 | } 66 | } 67 | 68 | matchIdentifier(path: Segments, node: IdentifierNode) { 69 | this.tail = node 70 | if (isValid(path[this.pos + 1]) && !node.after) { 71 | if (this.stack.length) { 72 | for (let i = this.stack.length - 1; i >= 0; i--) { 73 | if (!this.stack[i].after || !this.stack[i].filter) { 74 | return false 75 | } 76 | } 77 | } else { 78 | return false 79 | } 80 | } 81 | let current: any 82 | const next = () => { 83 | return this.matchNext(node, path) 84 | } 85 | 86 | if (isExpandOperator(node.after)) { 87 | current = this.recordMatch( 88 | () => 89 | node.value === String(path[this.pos]).substring(0, node.value.length) 90 | ) 91 | } else { 92 | current = this.recordMatch(() => 93 | isEqual(String(node.value), String(path[this.pos])) 94 | ) 95 | } 96 | 97 | if (this.excluding) { 98 | if (node.after) { 99 | if (this.pos < path.length) { 100 | return current() && next() 101 | } else { 102 | if (node.after && isWildcardOperator(node.after.after)) { 103 | return true 104 | } 105 | return false 106 | } 107 | } else { 108 | if (this.pos >= path.length) { 109 | return true 110 | } 111 | return current() 112 | } 113 | } 114 | 115 | return current() && next() 116 | } 117 | 118 | matchIgnoreExpression(path: Segments, node: IgnoreExpressionNode) { 119 | return ( 120 | isEqual(node.value, this.currentElement(path)) && 121 | this.matchNext(node, path) 122 | ) 123 | } 124 | 125 | matchDestructorExpression(path: Segments, node: DestructorExpressionNode) { 126 | return ( 127 | isEqual(node.source, this.currentElement(path)) && 128 | this.matchNext(node, path) 129 | ) 130 | } 131 | 132 | matchExpandOperator(path: Segments, node: ExpandOperatorNode) { 133 | return this.matchAtom(path, node.after) 134 | } 135 | 136 | matchWildcardOperator(path: Segments, node: WildcardOperatorNode) { 137 | this.tail = node 138 | this.stack.push(node) 139 | let matched = false 140 | if (node.filter) { 141 | if (node.after) { 142 | matched = 143 | this.matchAtom(path, node.filter) && this.matchAtom(path, node.after) 144 | } else { 145 | matched = this.matchAtom(path, node.filter) 146 | } 147 | } else { 148 | matched = this.matchNext(node, path) 149 | } 150 | this.stack.pop() 151 | return matched 152 | } 153 | 154 | matchGroupExpression(path: Segments, node: GroupExpressionNode) { 155 | const current = this.pos 156 | this.excluding = !!node.isExclude 157 | const method = this.excluding ? 'every' : 'some' 158 | const result = toArray(node.value)[method](_node => { 159 | this.pos = current 160 | return this.excluding 161 | ? !this.matchAtom(path, _node) 162 | : this.matchAtom(path, _node) 163 | }) 164 | this.excluding = false 165 | return result 166 | } 167 | 168 | matchRangeExpression(path: Segments, node: RangeExpressionNode) { 169 | if (node.start) { 170 | if (node.end) { 171 | return ( 172 | path[this.pos] >= parseInt(node.start.value) && 173 | path[this.pos] <= parseInt(node.end.value) 174 | ) 175 | } else { 176 | return path[this.pos] >= parseInt(node.start.value) 177 | } 178 | } else { 179 | if (node.end) { 180 | return path[this.pos] <= parseInt(node.end.value) 181 | } else { 182 | return true 183 | } 184 | } 185 | } 186 | 187 | matchDotOperator(path: Segments, node: DotOperatorNode) { 188 | this.pos++ 189 | return this.matchNext(node, path) 190 | } 191 | 192 | matchAtom(path: Segments, node: Node) { 193 | if (!node) { 194 | if (this.stack.length > 0) return true 195 | if (isValid(path[this.pos + 1])) return false 196 | if (this.pos == path.length - 1) return true 197 | } 198 | if (isIdentifier(node)) { 199 | return this.matchIdentifier(path, node) 200 | } else if (isIgnoreExpression(node)) { 201 | return this.matchIgnoreExpression(path, node) 202 | } else if (isDestructorExpression(node)) { 203 | return this.matchDestructorExpression(path, node) 204 | } else if (isExpandOperator(node)) { 205 | return this.matchExpandOperator(path, node) 206 | } else if (isWildcardOperator(node)) { 207 | return this.matchWildcardOperator(path, node) 208 | } else if (isGroupExpression(node)) { 209 | return this.matchGroupExpression(path, node) 210 | } else if (isRangeExpression(node)) { 211 | return this.matchRangeExpression(path, node) 212 | } else if (isDotOperator(node)) { 213 | return this.matchDotOperator(path, node) 214 | } 215 | 216 | return true 217 | } 218 | 219 | match(path: Segments) { 220 | const matched = this.matchAtom(path, this.tree) 221 | if (!this.tail) return { matched: false } 222 | if (this.tail == this.tree && isWildcardOperator(this.tail)) { 223 | return { matched: true } 224 | } 225 | 226 | return { matched, record: this.record } 227 | } 228 | 229 | static matchSegments(source: Segments, target: Segments, record?: any) { 230 | let pos = 0 231 | if (source.length !== target.length) return false 232 | const match = (pos: number) => { 233 | const current = () => { 234 | const res = isEqual(source[pos], target[pos]) 235 | if (record && record.score !== undefined) { 236 | record.score++ 237 | } 238 | return res 239 | } 240 | const next = () => (pos < source.length - 1 ? match(pos + 1) : true) 241 | return current() && next() 242 | } 243 | 244 | return { matched: match(pos), record } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /test/accessor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Path } from '../src' 2 | 3 | const { getIn, setIn } = Path 4 | 5 | test('test getIn and setIn', () => { 6 | const value = { a: { b: { c: 2, d: 333 } } } 7 | expect(getIn(value, 'a.b.c')).toEqual(2) 8 | setIn(value, 'a.b.c', 1111) 9 | expect(getIn(value, 'a.b.c')).toEqual(1111) 10 | }) 11 | 12 | test('test getIn with destructor', () => { 13 | const value = { array: [{ aa: 123, bb: 321 }] } 14 | expect(getIn(value, 'array.0.[aa,bb]')).toEqual([123, 321]) 15 | }) 16 | 17 | test('test setIn auto create array', () => { 18 | const value = { array: null } 19 | setIn(value, 'array[0].bb[2]', 'hello world') 20 | expect(value).toEqual({ 21 | array: [ 22 | { 23 | bb: [undefined, undefined, 'hello world'], 24 | }, 25 | ], 26 | }) 27 | expect(getIn(undefined, 'aa.bb.cc')).toEqual(undefined) 28 | setIn(undefined, 'aa.bb.cc', 123) 29 | }) 30 | 31 | test('map', () => { 32 | const value = { map: new Map() } 33 | setIn(value, 'map.aa.bb.cc', 123) 34 | expect(getIn(value, 'map.aa.bb.cc')).toEqual(123) 35 | }) 36 | 37 | test('test setIn array properties', () => { 38 | const value = { array: [] } 39 | setIn(value, 'array.xxx', 'hello world') 40 | expect(value).toEqual({ array: [] }) 41 | }) 42 | 43 | test('test setIn dose not affect other items', () => { 44 | const value = { 45 | aa: [ 46 | { 47 | dd: [ 48 | { 49 | ee: '是', 50 | }, 51 | ], 52 | cc: '1111', 53 | }, 54 | ], 55 | } 56 | 57 | setIn(value, 'aa.1.dd.0.ee', '否') 58 | expect(value.aa[0]).toEqual({ 59 | dd: [ 60 | { 61 | ee: '是', 62 | }, 63 | ], 64 | cc: '1111', 65 | }) 66 | }) 67 | 68 | test('destruct getIn', () => { 69 | // getIn 通过解构表达式从扁平数据转为复合嵌套数据 70 | const value = { a: { b: { c: 2, d: 333 } } } 71 | expect(getIn({ a: { b: { kk: 2, mm: 333 } } }, 'a.b.{c:kk,d:mm}')).toEqual({ 72 | c: 2, 73 | d: 333, 74 | }) 75 | 76 | expect( 77 | getIn( 78 | { kk: 2, mm: 333 }, 79 | `{ 80 | a : { 81 | b : { 82 | c : kk, 83 | d : mm 84 | } 85 | } 86 | }` 87 | ) 88 | ).toEqual(value) 89 | expect(getIn({ bb: undefined, dd: undefined }, `[{aa:bb,cc:dd}]`)).toEqual([]) 90 | expect( 91 | getIn( 92 | { kk: undefined, mm: undefined }, 93 | `{ 94 | a : { 95 | b : { 96 | c : kk, 97 | d : mm 98 | } 99 | } 100 | }` 101 | ) 102 | ).toEqual({}) 103 | }) 104 | 105 | test('destruct setIn', () => { 106 | const value = { a: { b: { c: 2, d: 333 } } } 107 | // setIn 从复杂嵌套结构中解构数据出来对其做赋值处理 108 | expect( 109 | setIn( 110 | {}, 111 | `{ 112 | a : { 113 | b : { 114 | c, 115 | d 116 | } 117 | } 118 | }`, 119 | value 120 | ) 121 | ).toEqual({ c: 2, d: 333 }) 122 | 123 | expect( 124 | setIn( 125 | {}, 126 | ` 127 | [aa,bb] 128 | `, 129 | [123, 444] 130 | ) 131 | ).toEqual({ aa: 123, bb: 444 }) 132 | expect(setIn({}, 'aa.bb.ddd.[aa,bb]', [123, 444])).toEqual({ 133 | aa: { bb: { ddd: { aa: 123, bb: 444 } } }, 134 | }) 135 | 136 | expect(setIn({}, 'aa.bb.ddd.[{cc:aa,bb}]', [{ cc: 123, bb: 444 }])).toEqual({ 137 | aa: { bb: { ddd: { aa: 123, bb: 444 } } }, 138 | }) 139 | }) 140 | 141 | test('setIn with a.b.c.{aaa,bbb}', () => { 142 | expect(Path.setIn({}, 'a.b.c.{aaa,bbb}', { aaa: 123, bbb: 321 })).toEqual({ 143 | a: { b: { c: { aaa: 123, bbb: 321 } } }, 144 | }) 145 | }) 146 | 147 | test('getIn with a.b.c.{aaa,bbb}', () => { 148 | expect( 149 | Path.getIn({ a: { b: { c: { aaa: 123, bbb: 321 } } } }, 'a.b.c.{aaa,bbb}') 150 | ).toEqual({ aaa: 123, bbb: 321 }) 151 | }) 152 | 153 | test('setIn with a.b.c.{aaa,bbb} source has extra property', () => { 154 | expect( 155 | Path.setIn({ a: { b: { c: { kkk: 'ddd' } } } }, 'a.b.c.{aaa,bbb}', { 156 | aaa: 123, 157 | bbb: 321, 158 | }) 159 | ).toEqual({ a: { b: { c: { aaa: 123, bbb: 321, kkk: 'ddd' } } } }) 160 | }) 161 | 162 | test('getIn with a.b.c.{aaa,bbb} source has extra property', () => { 163 | expect( 164 | Path.getIn( 165 | { a: { b: { c: { aaa: 123, bbb: 321, kkk: 'ddd' } } } }, 166 | 'a.b.c.{aaa,bbb}' 167 | ) 168 | ).toEqual({ aaa: 123, bbb: 321 }) 169 | }) 170 | 171 | test('setIn with a.b.c.{aaa:ooo,bbb}', () => { 172 | expect( 173 | Path.setIn({ a: { b: { c: { kkk: 'ddd' } } } }, 'a.b.c.{aaa:ooo,bbb}', { 174 | aaa: 123, 175 | bbb: 321, 176 | }) 177 | ).toEqual({ a: { b: { c: { ooo: 123, bbb: 321, kkk: 'ddd' } } } }) 178 | }) 179 | 180 | test('getIn with a.b.c.{aaa:ooo,bbb}', () => { 181 | expect( 182 | Path.getIn( 183 | { a: { b: { c: { ooo: 123, bbb: 321, kkk: 'ddd' } } } }, 184 | 'a.b.c.{aaa:ooo,bbb}' 185 | ) 186 | ).toEqual({ aaa: 123, bbb: 321 }) 187 | }) 188 | 189 | test('setIn with a.b.c.[aaa,bbb]', () => { 190 | expect(Path.setIn({}, 'a.b.c.[aaa,bbb]', [123, 321])).toEqual({ 191 | a: { b: { c: { aaa: 123, bbb: 321 } } }, 192 | }) 193 | }) 194 | 195 | test('getIn with a.b.c.[aaa,bbb]', () => { 196 | expect( 197 | Path.getIn({ a: { b: { c: { aaa: 123, bbb: 321 } } } }, 'a.b.c.[aaa,bbb]') 198 | ).toEqual([123, 321]) 199 | }) 200 | 201 | test('setIn with a.b.c.[aaa,bbb] source has extra property', () => { 202 | expect( 203 | Path.setIn({ a: { b: { c: { kkk: 'ddd' } } } }, 'a.b.c.[aaa,bbb]', [ 204 | 123, 205 | 321, 206 | ]) 207 | ).toEqual({ a: { b: { c: { aaa: 123, bbb: 321, kkk: 'ddd' } } } }) 208 | }) 209 | 210 | test('getIn with a.b.c.[aaa,bbb] source has extra property', () => { 211 | expect( 212 | Path.getIn( 213 | { a: { b: { c: { aaa: 123, bbb: 321, kkk: 'ddd' } } } }, 214 | 'a.b.c.[aaa,bbb]' 215 | ) 216 | ).toEqual([123, 321]) 217 | }) 218 | 219 | test('setIn with a.b.c.[{ddd,kkk:mmm},bbb]', () => { 220 | expect( 221 | Path.setIn({}, 'a.b.c.[{ddd,kkk:mmm},bbb]', [{ ddd: 123, kkk: 'hhh' }, 321]) 222 | ).toEqual({ a: { b: { c: { ddd: 123, bbb: 321, mmm: 'hhh' } } } }) 223 | }) 224 | 225 | test('getIn with a.b.c.[{ddd,kkk:mmm},bbb]', () => { 226 | expect( 227 | Path.getIn( 228 | { a: { b: { c: { ddd: 123, bbb: 321, mmm: 'hhh' } } } }, 229 | 'a.b.c.[{ddd,kkk:mmm},bbb]' 230 | ) 231 | ).toEqual([{ ddd: 123, kkk: 'hhh' }, 321]) 232 | }) 233 | 234 | test('setIn with a.b.c.{aaa:ooo,bbb:[ccc,ddd]}', () => { 235 | expect( 236 | Path.setIn( 237 | { a: { b: { c: { kkk: 'ddd' } } } }, 238 | 'a.b.c.{aaa:ooo,bbb:[ccc,ddd]}', 239 | { aaa: 123, bbb: [123, 321] } 240 | ) 241 | ).toEqual({ a: { b: { c: { ooo: 123, ccc: 123, ddd: 321, kkk: 'ddd' } } } }) 242 | }) 243 | 244 | test('getIn with a.b.c.{aaa:ooo,bbb:[ccc,ddd]}', () => { 245 | expect( 246 | Path.getIn( 247 | { a: { b: { c: { ooo: 123, ccc: 123, ddd: 321, kkk: 'ddd' } } } }, 248 | 'a.b.c.{aaa:ooo,bbb:[ccc,ddd]}' 249 | ) 250 | ).toEqual({ aaa: 123, bbb: [123, 321] }) 251 | }) 252 | 253 | test('existIn with a.b.c', () => { 254 | expect(Path.existIn({ a: { b: { c: 123123 } } }, 'a.b.c')).toEqual(true) 255 | expect(Path.existIn({ a: { b: { c: 123123 } } }, 'a.b.c.d')).toEqual(false) 256 | expect(Path.existIn({ a: 123 }, 'a.b.c.d')).toEqual(false) 257 | expect( 258 | Path.existIn( 259 | { a: { b: { c: { ooo: 123, ccc: 123, ddd: 321, kkk: 'ddd' } } } }, 260 | 'a.b.c.{aaa:ooo,bbb:[ccc,ddd]}' 261 | ) 262 | ).toEqual(true) 263 | expect( 264 | Path.existIn( 265 | { a: { b: { c: { ooo: 123, ccc: 123, kkk: 'ddd' } } } }, 266 | 'a.b.c.{aaa:ooo,bbb:[ccc,ddd]}' 267 | ) 268 | ).toEqual(false) 269 | expect(Path.existIn({ a: [{}] }, 'a.0')).toEqual(true) 270 | }) 271 | 272 | test('complex destructing', () => { 273 | expect( 274 | Path.setIn( 275 | {}, 276 | '{aa:{bb:{cc:destructor1,dd:[destructor2,destructor3],ee}}}', 277 | { 278 | aa: { 279 | bb: { 280 | cc: 123, 281 | dd: [333, 444], 282 | ee: 'abcde', 283 | }, 284 | }, 285 | } 286 | ) 287 | ).toEqual({ 288 | destructor1: 123, 289 | destructor2: 333, 290 | destructor3: 444, 291 | ee: 'abcde', 292 | }) 293 | expect( 294 | Path.getIn( 295 | { 296 | destructor1: 123, 297 | destructor2: 333, 298 | destructor3: 444, 299 | ee: 'abcde', 300 | }, 301 | '{aa:{bb:{cc:destructor1,dd:[destructor2,destructor3],ee}}}' 302 | ) 303 | ).toEqual({ 304 | aa: { 305 | bb: { 306 | cc: 123, 307 | dd: [333, 444], 308 | ee: 'abcde', 309 | }, 310 | }, 311 | }) 312 | }) 313 | -------------------------------------------------------------------------------- /src/tokenizer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Token, 3 | nameTok, 4 | colonTok, 5 | dotTok, 6 | starTok, 7 | bangTok, 8 | bracketLTok, 9 | bracketRTok, 10 | bracketDRTok, 11 | expandTok, 12 | parenLTok, 13 | parenRTok, 14 | commaTok, 15 | eofTok, 16 | ignoreTok, 17 | braceLTok, 18 | braceRTok, 19 | bracketDLTok, 20 | } from './tokens' 21 | import { bracketDContext, Context } from './contexts' 22 | 23 | const nonASCIIwhitespace = /[\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]/ 24 | 25 | const fullCharCodeAtPos = (input: string, pos: number) => { 26 | const code = input.charCodeAt(pos) 27 | if (code <= 0xd7ff || code >= 0xe000) return code 28 | 29 | const next = input.charCodeAt(pos + 1) 30 | return (code << 10) + next - 0x35fdc00 31 | } 32 | 33 | const isRewordCode = (code: number) => 34 | code === 42 || 35 | code === 46 || 36 | code === 33 || 37 | code === 91 || 38 | code === 93 || 39 | code === 40 || 40 | code === 41 || 41 | code === 44 || 42 | code === 58 || 43 | code === 126 || 44 | code === 123 || 45 | code === 125 46 | 47 | const getError = (message?: string, props?: any) => { 48 | const err = new Error(message) 49 | Object.assign(err, props) 50 | return err 51 | } 52 | 53 | const slice = (string: string, start: number, end: number) => { 54 | let str = '' 55 | for (let i = start; i < end; i++) { 56 | let ch = string.charAt(i) 57 | if (ch !== '\\') { 58 | str += ch 59 | } 60 | } 61 | return str 62 | } 63 | 64 | export class Tokenizer { 65 | public input: string 66 | public state: { 67 | context: Context[] 68 | type: Token 69 | pos: number 70 | value?: any 71 | } 72 | public type_: Token 73 | constructor(input: string) { 74 | this.input = input 75 | this.state = { 76 | context: [], 77 | type: null, 78 | pos: 0, 79 | } 80 | this.type_ = null 81 | } 82 | 83 | curContext() { 84 | return this.state.context[this.state.context.length - 1] 85 | } 86 | 87 | includesContext(context: Context) { 88 | for (let len = this.state.context.length - 1; len >= 0; len--) { 89 | if (this.state.context[len] === context) { 90 | return true 91 | } 92 | } 93 | return false 94 | } 95 | 96 | unexpect(type?: Token) { 97 | type = type || this.state.type 98 | return getError( 99 | `Unexpect token "${type.flag}" in ${this.state.pos} char.`, 100 | { 101 | pos: this.state.pos, 102 | } 103 | ) 104 | } 105 | 106 | expectNext(type?: Token, next?: Token) { 107 | if (type && type.expectNext) { 108 | if (next && !type.expectNext.call(this, next)) { 109 | throw getError( 110 | `Unexpect token "${next.flag}" token should not be behind "${type.flag}" token.(${this.state.pos}th char)`, 111 | { 112 | pos: this.state.pos, 113 | } 114 | ) 115 | } 116 | } 117 | } 118 | 119 | expectPrev(type?: Token, prev?: Token) { 120 | if (type && type.expectPrev) { 121 | if (prev && !type.expectPrev.call(this, prev)) { 122 | throw getError( 123 | `Unexpect token "${type.flag}" should not be behind "${prev.flag}"(${this.state.pos}th char).`, 124 | { 125 | pos: this.state.pos, 126 | } 127 | ) 128 | } 129 | } 130 | } 131 | 132 | match(type?: Token) { 133 | return this.state.type === type 134 | } 135 | 136 | skipSpace() { 137 | if (this.curContext() === bracketDContext) return 138 | loop: while (this.state.pos < this.input.length) { 139 | const ch = this.input.charCodeAt(this.state.pos) 140 | switch (ch) { 141 | case 32: 142 | case 160: 143 | ++this.state.pos 144 | break 145 | 146 | case 13: 147 | if (this.input.charCodeAt(this.state.pos + 1) === 10) { 148 | ++this.state.pos 149 | } 150 | 151 | case 10: 152 | case 8232: 153 | case 8233: 154 | ++this.state.pos 155 | break 156 | default: 157 | if ( 158 | (ch > 8 && ch < 14) || 159 | (ch >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(ch))) 160 | ) { 161 | ++this.state.pos 162 | } else { 163 | break loop 164 | } 165 | } 166 | } 167 | } 168 | 169 | next() { 170 | this.type_ = this.state.type 171 | if (this.input.length <= this.state.pos) { 172 | return this.finishToken(eofTok) 173 | } 174 | this.skipSpace() 175 | this.readToken( 176 | this.getCode(), 177 | this.state.pos > 0 ? this.getCode(this.state.pos - 1) : -Infinity 178 | ) 179 | } 180 | 181 | getCode(pos = this.state.pos) { 182 | return fullCharCodeAtPos(this.input, pos) 183 | } 184 | 185 | eat(type) { 186 | if (this.match(type)) { 187 | this.next() 188 | return true 189 | } else { 190 | return false 191 | } 192 | } 193 | 194 | readKeyWord() { 195 | let startPos = this.state.pos, 196 | string = '' 197 | while (true) { 198 | let code = this.getCode() 199 | let prevCode = this.getCode(this.state.pos - 1) 200 | if (this.input.length === this.state.pos) { 201 | string = slice(this.input, startPos, this.state.pos + 1) 202 | break 203 | } 204 | if (!isRewordCode(code) || prevCode === 92) { 205 | if ( 206 | code === 32 || 207 | code === 160 || 208 | code === 10 || 209 | code === 8232 || 210 | code === 8233 211 | ) { 212 | string = slice(this.input, startPos, this.state.pos) 213 | break 214 | } 215 | if (code === 13 && this.input.charCodeAt(this.state.pos + 1) === 10) { 216 | string = slice(this.input, startPos, this.state.pos) 217 | break 218 | } 219 | if ( 220 | (code > 8 && code < 14) || 221 | (code >= 5760 && nonASCIIwhitespace.test(String.fromCharCode(code))) 222 | ) { 223 | string = slice(this.input, startPos, this.state.pos) 224 | break 225 | } 226 | this.state.pos++ 227 | } else { 228 | string = slice(this.input, startPos, this.state.pos) 229 | break 230 | } 231 | } 232 | 233 | this.finishToken(nameTok, string) 234 | } 235 | 236 | readIngoreString() { 237 | let startPos = this.state.pos, 238 | prevCode, 239 | string = '' 240 | while (true) { 241 | let code = this.getCode() 242 | if (this.state.pos >= this.input.length) break 243 | if ((code === 91 || code === 93) && prevCode === 92) { 244 | this.state.pos++ 245 | prevCode = '' 246 | } else if (code == 93 && prevCode === 93) { 247 | string = this.input 248 | .slice(startPos, this.state.pos - 1) 249 | .replace(/\\([\[\]])/g, '$1') 250 | this.state.pos++ 251 | break 252 | } else { 253 | this.state.pos++ 254 | prevCode = code 255 | } 256 | } 257 | 258 | this.finishToken(ignoreTok, string) 259 | this.finishToken(bracketDRTok) 260 | } 261 | 262 | finishToken(type: Token, value?: any) { 263 | const preType = this.state.type 264 | this.state.type = type 265 | if (value !== undefined) this.state.value = value 266 | this.expectNext(preType, type) 267 | this.expectPrev(type, preType) 268 | if (type.updateContext) { 269 | type.updateContext.call(this, preType) 270 | } 271 | } 272 | 273 | readToken(code: number, prevCode: number) { 274 | if (prevCode === 92) { 275 | return this.readKeyWord() 276 | } 277 | if (this.input.length <= this.state.pos) { 278 | this.finishToken(eofTok) 279 | } else if (this.curContext() === bracketDContext) { 280 | this.readIngoreString() 281 | } else if (code === 123) { 282 | this.state.pos++ 283 | this.finishToken(braceLTok) 284 | } else if (code === 125) { 285 | this.state.pos++ 286 | this.finishToken(braceRTok) 287 | } else if (code === 42) { 288 | this.state.pos++ 289 | this.finishToken(starTok) 290 | } else if (code === 33) { 291 | this.state.pos++ 292 | this.finishToken(bangTok) 293 | } else if (code === 46) { 294 | this.state.pos++ 295 | this.finishToken(dotTok) 296 | } else if (code === 91) { 297 | this.state.pos++ 298 | if (this.getCode() === 91) { 299 | this.state.pos++ 300 | return this.finishToken(bracketDLTok) 301 | } 302 | this.finishToken(bracketLTok) 303 | } else if (code === 126) { 304 | this.state.pos++ 305 | this.finishToken(expandTok) 306 | } else if (code === 93) { 307 | this.state.pos++ 308 | this.finishToken(bracketRTok) 309 | } else if (code === 40) { 310 | this.state.pos++ 311 | this.finishToken(parenLTok) 312 | } else if (code === 41) { 313 | this.state.pos++ 314 | this.finishToken(parenRTok) 315 | } else if (code === 44) { 316 | this.state.pos++ 317 | this.finishToken(commaTok) 318 | } else if (code === 58) { 319 | this.state.pos++ 320 | this.finishToken(colonTok) 321 | } else { 322 | this.readKeyWord() 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/lru.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A doubly linked list-based Least Recently Used (LRU) cache. Will keep most 3 | * recently used items while discarding least recently used items when its limit 4 | * is reached. 5 | * 6 | * Licensed under MIT. Copyright (c) 2010 Rasmus Andersson 7 | * See README.md for details. 8 | * 9 | * Illustration of the design: 10 | * 11 | * entry entry entry entry 12 | * ______ ______ ______ ______ 13 | * | head |.newer => | |.newer => | |.newer => | tail | 14 | * | A | | B | | C | | D | 15 | * |______| <= older.|______| <= older.|______| <= older.|______| 16 | * 17 | * removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added 18 | */ 19 | /* eslint-disable */ 20 | 21 | const NEWER = Symbol('newer') 22 | const OLDER = Symbol('older') 23 | 24 | export function LRUMap(limit: number, entries?: any) { 25 | if (typeof limit !== 'number') { 26 | // called as (entries) 27 | entries = limit 28 | limit = 0 29 | } 30 | 31 | this.size = 0 32 | this.limit = limit 33 | this.oldest = this.newest = undefined 34 | this._keymap = new Map() 35 | 36 | if (entries) { 37 | this.assign(entries) 38 | if (limit < 1) { 39 | this.limit = this.size 40 | } 41 | } 42 | } 43 | 44 | function Entry(key: any, value: any) { 45 | this.key = key 46 | this.value = value 47 | this[NEWER] = undefined 48 | this[OLDER] = undefined 49 | } 50 | 51 | LRUMap.prototype._markEntryAsUsed = function(entry: any) { 52 | if (entry === this.newest) { 53 | // Already the most recenlty used entry, so no need to update the list 54 | return 55 | } 56 | // HEAD--------------TAIL 57 | // <.older .newer> 58 | // <--- add direction -- 59 | // A B C E 60 | if (entry[NEWER]) { 61 | if (entry === this.oldest) { 62 | this.oldest = entry[NEWER] 63 | } 64 | entry[NEWER][OLDER] = entry[OLDER] // C <-- E. 65 | } 66 | if (entry[OLDER]) { 67 | entry[OLDER][NEWER] = entry[NEWER] // C. --> E 68 | } 69 | entry[NEWER] = undefined // D --x 70 | entry[OLDER] = this.newest // D. --> E 71 | if (this.newest) { 72 | this.newest[NEWER] = entry // E. <-- D 73 | } 74 | this.newest = entry 75 | } 76 | 77 | LRUMap.prototype.assign = function(entries: any) { 78 | let entry: any 79 | let limit = this.limit || Number.MAX_VALUE 80 | this._keymap.clear() 81 | const it = entries[Symbol.iterator]() 82 | for (let itv = it.next(); !itv.done; itv = it.next()) { 83 | const e = new Entry(itv.value[0], itv.value[1]) 84 | this._keymap.set(e.key, e) 85 | if (!entry) { 86 | this.oldest = e 87 | } else { 88 | entry[NEWER] = e 89 | e[OLDER] = entry 90 | } 91 | entry = e 92 | if (limit-- === 0) { 93 | throw new Error('overflow') 94 | } 95 | } 96 | this.newest = entry 97 | this.size = this._keymap.size 98 | } 99 | 100 | LRUMap.prototype.get = function(key: any) { 101 | // First, find our cache entry 102 | const entry = this._keymap.get(key) 103 | if (!entry) { 104 | return 105 | } // Not cached. Sorry. 106 | // As was found in the cache, register it as being requested recently 107 | this._markEntryAsUsed(entry) 108 | return entry.value 109 | } 110 | 111 | LRUMap.prototype.set = function(key: any, value: any) { 112 | let entry = this._keymap.get(key) 113 | 114 | if (entry) { 115 | // update existing 116 | entry.value = value 117 | this._markEntryAsUsed(entry) 118 | return this 119 | } 120 | 121 | // new entry 122 | this._keymap.set(key, (entry = new Entry(key, value))) 123 | 124 | if (this.newest) { 125 | // link previous tail to the new tail (entry) 126 | this.newest[NEWER] = entry 127 | entry[OLDER] = this.newest 128 | } else { 129 | // we're first in -- yay 130 | this.oldest = entry 131 | } 132 | 133 | // add new entry to the end of the linked list -- it's now the freshest entry. 134 | this.newest = entry 135 | ++this.size 136 | if (this.size > this.limit) { 137 | // we hit the limit -- remove the head 138 | this.shift() 139 | } 140 | 141 | return this 142 | } 143 | 144 | LRUMap.prototype.shift = function() { 145 | // todo: handle special case when limit == 1 146 | const entry = this.oldest 147 | if (entry) { 148 | if (this.oldest[NEWER]) { 149 | // advance the list 150 | this.oldest = this.oldest[NEWER] 151 | this.oldest[OLDER] = undefined 152 | } else { 153 | // the cache is exhausted 154 | this.oldest = undefined 155 | this.newest = undefined 156 | } 157 | // Remove last strong reference to and remove links from the purged 158 | // entry being returned: 159 | entry[NEWER] = entry[OLDER] = undefined 160 | this._keymap.delete(entry.key) 161 | --this.size 162 | return [entry.key, entry.value] 163 | } 164 | } 165 | 166 | // ---------------------------------------------------------------------------- 167 | // Following code is optional and can be removed without breaking the core 168 | // functionality. 169 | 170 | LRUMap.prototype.find = function(key: any) { 171 | const e = this._keymap.get(key) 172 | return e ? e.value : undefined 173 | } 174 | 175 | LRUMap.prototype.has = function(key: any) { 176 | return this._keymap.has(key) 177 | } 178 | 179 | LRUMap.prototype.delete = function(key: any) { 180 | const entry = this._keymap.get(key) 181 | if (!entry) { 182 | return 183 | } 184 | this._keymap.delete(entry.key) 185 | if (entry[NEWER] && entry[OLDER]) { 186 | // relink the older entry with the newer entry 187 | entry[OLDER][NEWER] = entry[NEWER] 188 | entry[NEWER][OLDER] = entry[OLDER] 189 | } else if (entry[NEWER]) { 190 | // remove the link to us 191 | entry[NEWER][OLDER] = undefined 192 | // link the newer entry to head 193 | this.oldest = entry[NEWER] 194 | } else if (entry[OLDER]) { 195 | // remove the link to us 196 | entry[OLDER][NEWER] = undefined 197 | // link the newer entry to head 198 | this.newest = entry[OLDER] 199 | } else { 200 | // if(entry[OLDER] === undefined && entry.newer === undefined) { 201 | this.oldest = this.newest = undefined 202 | } 203 | 204 | this.size-- 205 | return entry.value 206 | } 207 | 208 | LRUMap.prototype.clear = function() { 209 | // Not clearing links should be safe, as we don't expose live links to user 210 | this.oldest = this.newest = undefined 211 | this.size = 0 212 | this._keymap.clear() 213 | } 214 | 215 | function EntryIterator(oldestEntry: any) { 216 | this.entry = oldestEntry 217 | } 218 | EntryIterator.prototype[Symbol.iterator] = function() { 219 | return this 220 | } 221 | EntryIterator.prototype.next = function() { 222 | const ent = this.entry 223 | if (ent) { 224 | this.entry = ent[NEWER] 225 | return { done: false, value: [ent.key, ent.value] } 226 | } else { 227 | return { done: true, value: undefined } 228 | } 229 | } 230 | 231 | function KeyIterator(oldestEntry) { 232 | this.entry = oldestEntry 233 | } 234 | KeyIterator.prototype[Symbol.iterator] = function() { 235 | return this 236 | } 237 | KeyIterator.prototype.next = function() { 238 | const ent = this.entry 239 | if (ent) { 240 | this.entry = ent[NEWER] 241 | return { done: false, value: ent.key } 242 | } else { 243 | return { done: true, value: undefined } 244 | } 245 | } 246 | 247 | function ValueIterator(oldestEntry) { 248 | this.entry = oldestEntry 249 | } 250 | ValueIterator.prototype[Symbol.iterator] = function() { 251 | return this 252 | } 253 | ValueIterator.prototype.next = function() { 254 | const ent = this.entry 255 | if (ent) { 256 | this.entry = ent[NEWER] 257 | return { done: false, value: ent.value } 258 | } else { 259 | return { done: true, value: undefined } 260 | } 261 | } 262 | 263 | LRUMap.prototype.keys = function() { 264 | return new KeyIterator(this.oldest) 265 | } 266 | 267 | LRUMap.prototype.values = function() { 268 | return new ValueIterator(this.oldest) 269 | } 270 | 271 | LRUMap.prototype.entries = function() { 272 | return this 273 | } 274 | 275 | LRUMap.prototype[Symbol.iterator] = function() { 276 | return new EntryIterator(this.oldest) 277 | } 278 | 279 | LRUMap.prototype.forEach = function( 280 | fun: (value: any, key: any, ctx: object) => void, 281 | thisObj: any 282 | ) { 283 | if (typeof thisObj !== 'object') { 284 | thisObj = this 285 | } 286 | let entry = this.oldest 287 | while (entry) { 288 | fun.call(thisObj, entry.value, entry.key, this) 289 | entry = entry[NEWER] 290 | } 291 | } 292 | 293 | /** Returns a JSON (array) representation */ 294 | LRUMap.prototype.toJSON = function() { 295 | const s = new Array(this.size) 296 | let i = 0 297 | let entry = this.oldest 298 | while (entry) { 299 | s[i++] = { key: entry.key, value: entry.value } 300 | entry = entry[NEWER] 301 | } 302 | return s 303 | } 304 | 305 | /** Returns a String representation */ 306 | LRUMap.prototype.toString = function() { 307 | let s = '' 308 | let entry = this.oldest 309 | while (entry) { 310 | s += String(entry.key) + ':' + entry.value 311 | entry = entry[NEWER] 312 | if (entry) { 313 | s += ' < ' 314 | } 315 | } 316 | return s 317 | } 318 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import { Tokenizer } from './tokenizer' 2 | import { 3 | Token, 4 | nameTok, 5 | colonTok, 6 | dotTok, 7 | starTok, 8 | bangTok, 9 | bracketLTok, 10 | bracketRTok, 11 | braceLTok, 12 | braceRTok, 13 | bracketDLTok, 14 | parenLTok, 15 | parenRTok, 16 | commaTok, 17 | expandTok, 18 | eofTok, 19 | } from './tokens' 20 | import { bracketArrayContext, destructorContext } from './contexts' 21 | import { 22 | IdentifierNode, 23 | ExpandOperatorNode, 24 | WildcardOperatorNode, 25 | RangeExpressionNode, 26 | GroupExpressionNode, 27 | DotOperatorNode, 28 | IgnoreExpressionNode, 29 | DestructorExpressionNode, 30 | ObjectPatternNode, 31 | ObjectPatternPropertyNode, 32 | ArrayPatternNode, 33 | Node, 34 | Segments, 35 | } from './types' 36 | import { parseDestructorRules, setDestructor } from './destructor' 37 | import { isNumberLike } from './utils' 38 | import Path from './index' 39 | 40 | const createTreeBySegments = (segments: Segments = [], afterNode?: Node) => { 41 | const segLen = segments.length 42 | const build = (start = 0) => { 43 | const after = start < segLen - 1 ? build(start + 1) : afterNode 44 | const dot = after && { 45 | type: 'DotOperator', 46 | after, 47 | } 48 | return { 49 | type: 'Identifier', 50 | value: segments[start], 51 | after: dot, 52 | } 53 | } 54 | return build() 55 | } 56 | 57 | const calculate = ( 58 | a: string | number, 59 | b: string | number, 60 | operator: string 61 | ) => { 62 | if (isNumberLike(a) && isNumberLike(b)) { 63 | if (operator === '+') return String(Number(a) + Number(b)) 64 | if (operator === '-') return String(Number(a) - Number(b)) 65 | if (operator === '*') return String(Number(a) * Number(b)) 66 | if (operator === '/') return String(Number(a) / Number(b)) 67 | } else { 68 | if (operator === '+') return String(a) + String(b) 69 | if (operator === '-') return 'NaN' 70 | if (operator === '*') return 'NaN' 71 | if (operator === '/') return 'NaN' 72 | } 73 | return String(Number(b)) 74 | } 75 | 76 | export class Parser extends Tokenizer { 77 | public isMatchPattern: boolean 78 | 79 | public isWildMatchPattern: boolean 80 | 81 | public haveExcludePattern: boolean 82 | 83 | public base: Path 84 | 85 | public relative: string | number 86 | 87 | public data: { 88 | segments: Segments 89 | tree?: Node 90 | } 91 | 92 | constructor(input: string, base?: Path) { 93 | super(input) 94 | this.base = base 95 | } 96 | 97 | parse() { 98 | let node: Node 99 | this.data = { 100 | segments: [], 101 | } 102 | if (!this.eat(eofTok)) { 103 | this.next() 104 | node = this.parseAtom(this.state.type) 105 | } 106 | this.data.tree = node 107 | 108 | return node 109 | } 110 | 111 | append(parent: Node, node: Node) { 112 | if (parent && node) { 113 | parent.after = node 114 | } 115 | } 116 | 117 | parseAtom(type: Token): Node { 118 | switch (type) { 119 | case braceLTok: 120 | case bracketLTok: 121 | if (this.includesContext(destructorContext)) { 122 | if (type === braceLTok) { 123 | return this.parseObjectPattern() 124 | } else { 125 | return this.parseArrayPattern() 126 | } 127 | } 128 | return this.parseDestructorExpression() 129 | case nameTok: 130 | return this.parseIdentifier() 131 | case expandTok: 132 | return this.parseExpandOperator() 133 | case starTok: 134 | return this.parseWildcardOperator() 135 | case bracketDLTok: 136 | return this.parseIgnoreExpression() 137 | case dotTok: 138 | return this.parseDotOperator() 139 | } 140 | } 141 | 142 | pushSegments(key: string | number) { 143 | this.data.segments.push(key) 144 | } 145 | 146 | parseIdentifier() { 147 | const node: IdentifierNode = { 148 | type: 'Identifier', 149 | value: this.state.value, 150 | } 151 | const hasNotInDestructor = 152 | !this.includesContext(destructorContext) && 153 | !this.isMatchPattern && 154 | !this.isWildMatchPattern 155 | 156 | this.next() 157 | if (this.includesContext(bracketArrayContext)) { 158 | if (this.state.type !== bracketRTok) { 159 | throw this.unexpect() 160 | } else { 161 | this.state.context.pop() 162 | this.next() 163 | } 164 | } else if (hasNotInDestructor) { 165 | this.pushSegments(node.value) 166 | } 167 | if (this.state.type === bracketLTok) { 168 | this.next() 169 | if (this.state.type !== nameTok) { 170 | throw this.unexpect() 171 | } 172 | this.state.context.push(bracketArrayContext) 173 | let isNumberKey = false 174 | if (/^\d+$/.test(this.state.value)) { 175 | isNumberKey = true 176 | } 177 | const value = this.state.value 178 | this.pushSegments(isNumberKey ? Number(value) : value) 179 | const after = this.parseAtom(this.state.type) as IdentifierNode 180 | if (isNumberKey) { 181 | after.arrayIndex = true 182 | } 183 | this.append(node, after) 184 | } else { 185 | this.append(node, this.parseAtom(this.state.type)) 186 | } 187 | 188 | return node 189 | } 190 | 191 | parseExpandOperator() { 192 | const node: ExpandOperatorNode = { 193 | type: 'ExpandOperator', 194 | } 195 | 196 | this.isMatchPattern = true 197 | this.isWildMatchPattern = true 198 | this.data.segments = [] 199 | 200 | this.next() 201 | 202 | this.append(node, this.parseAtom(this.state.type)) 203 | 204 | return node 205 | } 206 | 207 | parseWildcardOperator(): WildcardOperatorNode { 208 | const node: WildcardOperatorNode = { 209 | type: 'WildcardOperator', 210 | } 211 | 212 | this.isMatchPattern = true 213 | this.isWildMatchPattern = true 214 | this.data.segments = [] 215 | 216 | this.next() 217 | 218 | if (this.state.type === parenLTok) { 219 | node.filter = this.parseGroupExpression(node) 220 | } else if (this.state.type === bracketLTok) { 221 | node.filter = this.parseRangeExpression(node) 222 | } 223 | 224 | this.append(node, this.parseAtom(this.state.type)) 225 | 226 | return node 227 | } 228 | 229 | parseDestructorExpression(): DestructorExpressionNode { 230 | const node: DestructorExpressionNode = { 231 | type: 'DestructorExpression', 232 | } 233 | this.state.context.push(destructorContext) 234 | const startPos = this.state.pos - 1 235 | node.value = 236 | this.state.type === braceLTok 237 | ? this.parseObjectPattern() 238 | : this.parseArrayPattern() 239 | const endPos = this.state.pos 240 | this.state.context.pop() 241 | this.next() 242 | node.source = this.input 243 | .substring(startPos, endPos) 244 | .replace( 245 | /\[\s*([\+\-\*\/])?\s*([^,\]\s]*)\s*\]/, 246 | (match, operator, target) => { 247 | if (this.relative !== undefined) 248 | return calculate(target || 1, this.relative, operator) 249 | return match 250 | } 251 | ) 252 | .replace(/\s*\.\s*/g, '') 253 | .replace(/\s*/g, '') 254 | if (this.relative === undefined) { 255 | setDestructor(node.source, parseDestructorRules(node)) 256 | } 257 | this.relative = undefined 258 | this.pushSegments(node.source) 259 | this.append(node, this.parseAtom(this.state.type)) 260 | return node 261 | } 262 | 263 | parseArrayPattern(): ArrayPatternNode { 264 | const node: ArrayPatternNode = { 265 | type: 'ArrayPattern', 266 | elements: [], 267 | } 268 | this.next() 269 | node.elements = this.parseArrayPatternElements() 270 | return node 271 | } 272 | 273 | parseArrayPatternElements() { 274 | const nodes = [] 275 | while (this.state.type !== bracketRTok && this.state.type !== eofTok) { 276 | nodes.push(this.parseAtom(this.state.type)) 277 | if (this.state.type === bracketRTok) { 278 | this.next() 279 | break 280 | } 281 | this.next() 282 | } 283 | return nodes 284 | } 285 | 286 | parseObjectPattern(): ObjectPatternNode { 287 | const node: ObjectPatternNode = { 288 | type: 'ObjectPattern', 289 | properties: [], 290 | } 291 | this.next() 292 | node.properties = this.parseObjectProperties() 293 | return node 294 | } 295 | 296 | parseObjectProperties(): ObjectPatternPropertyNode[] { 297 | const nodes = [] 298 | while (this.state.type !== braceRTok && this.state.type !== eofTok) { 299 | const node: ObjectPatternPropertyNode = { 300 | type: 'ObjectPatternProperty', 301 | key: this.parseAtom(this.state.type) as IdentifierNode, 302 | } 303 | nodes.push(node) 304 | if (this.state.type === colonTok) { 305 | this.next() 306 | node.value = this.parseAtom(this.state.type) as 307 | | IdentifierNode 308 | | ObjectPatternNode[] 309 | | ArrayPatternNode[] 310 | } 311 | if (this.state.type === braceRTok) { 312 | this.next() 313 | break 314 | } 315 | this.next() 316 | } 317 | return nodes 318 | } 319 | 320 | parseDotOperator(): Node { 321 | const node: DotOperatorNode = { 322 | type: 'DotOperator', 323 | } 324 | 325 | const prevToken = this.type_ 326 | if (!prevToken && this.base) { 327 | if (this.base.isMatchPattern) { 328 | throw new Error('Base path must be an absolute path.') 329 | } 330 | this.data.segments = this.base.toArray() 331 | while (this.state.type === dotTok) { 332 | this.relative = this.data.segments.pop() 333 | this.next() 334 | } 335 | return createTreeBySegments( 336 | this.data.segments.slice(), 337 | this.parseAtom(this.state.type) 338 | ) 339 | } else { 340 | this.next() 341 | } 342 | 343 | this.append(node, this.parseAtom(this.state.type)) 344 | 345 | return node 346 | } 347 | 348 | parseIgnoreExpression() { 349 | this.next() 350 | 351 | const value = String(this.state.value).replace(/\s*/g, '') 352 | 353 | const node: IgnoreExpressionNode = { 354 | type: 'IgnoreExpression', 355 | value: value, 356 | } 357 | 358 | this.pushSegments(value) 359 | 360 | this.next() 361 | 362 | this.append(node, this.parseAtom(this.state.type)) 363 | 364 | this.next() 365 | 366 | return node 367 | } 368 | 369 | parseGroupExpression(parent: Node) { 370 | const node: GroupExpressionNode = { 371 | type: 'GroupExpression', 372 | value: [], 373 | } 374 | 375 | this.isMatchPattern = true 376 | this.data.segments = [] 377 | 378 | this.next() 379 | 380 | loop: while (true) { 381 | switch (this.state.type) { 382 | case commaTok: 383 | this.next() 384 | break 385 | case bangTok: 386 | node.isExclude = true 387 | this.haveExcludePattern = true 388 | this.next() 389 | break 390 | case eofTok: 391 | break loop 392 | case parenRTok: 393 | break loop 394 | default: 395 | node.value.push(this.parseAtom(this.state.type)) 396 | } 397 | } 398 | 399 | this.next() 400 | 401 | this.append(parent, this.parseAtom(this.state.type)) 402 | 403 | return node 404 | } 405 | 406 | parseRangeExpression(parent: Node) { 407 | const node: RangeExpressionNode = { 408 | type: 'RangeExpression', 409 | } 410 | 411 | this.next() 412 | 413 | this.isMatchPattern = true 414 | this.data.segments = [] 415 | 416 | let start = false, 417 | hasColon = false 418 | 419 | loop: while (true) { 420 | switch (this.state.type) { 421 | case colonTok: 422 | hasColon = true 423 | start = true 424 | this.next() 425 | break 426 | case bracketRTok: 427 | if (!hasColon && !node.end) { 428 | node.end = node.start 429 | } 430 | break loop 431 | case commaTok: 432 | throw this.unexpect() 433 | case eofTok: 434 | break loop 435 | default: 436 | if (!start) { 437 | node.start = this.parseAtom(this.state.type) as IdentifierNode 438 | } else { 439 | node.end = this.parseAtom(this.state.type) as IdentifierNode 440 | } 441 | } 442 | } 443 | 444 | this.next() 445 | 446 | this.append(parent, this.parseAtom(this.state.type)) 447 | 448 | return node 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /test/parser.spec.ts: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import { Parser } from '../src/parser' 3 | import { Path } from '../src/index' 4 | const parse = (string: string, json: any, index: number) => { 5 | test('test ' + string + ` : ${index}`, () => { 6 | const parser = new Parser(string) 7 | expect(parser.parse()).toEqual(json) 8 | }) 9 | } 10 | 11 | const batchTest = (obj) => { 12 | let i = 0 13 | for (let key in obj) { 14 | i++ 15 | parse(key, obj[key], i) 16 | } 17 | } 18 | 19 | test('relative', () => { 20 | const parser = new Parser('..[ + 1 ].dd.bb', new Path(['aa', '1', 'cc'])) 21 | const parser2 = new Parser('.ee', new Path(['aa', '1', 'cc'])) 22 | const parser3 = new Parser('.', new Path(['aa', '1', 'cc'])) 23 | const parser4 = new Parser('..', new Path(['aa', '1', 'cc'])) 24 | const parser5 = new Parser('.[].dd', new Path(['aa', '1'])) 25 | const parser6 = new Parser('.[].[]', new Path(['aa', '1'])) 26 | const parser7 = new Parser('.[].[aa]', new Path(['aa', '1'])) 27 | parser2.parse() 28 | parser3.parse() 29 | parser4.parse() 30 | parser5.parse() 31 | parser6.parse() 32 | parser7.parse() 33 | expect(parser.parse()).toEqual({ 34 | type: 'Identifier', 35 | value: 'aa', 36 | after: { 37 | type: 'DotOperator', 38 | after: { 39 | type: 'DestructorExpression', 40 | value: { 41 | type: 'ArrayPattern', 42 | elements: [ 43 | { 44 | type: 'Identifier', 45 | value: '+', 46 | after: { 47 | type: 'Identifier', 48 | value: '1', 49 | }, 50 | }, 51 | ], 52 | }, 53 | source: '2', 54 | after: { 55 | type: 'Identifier', 56 | value: 'dd', 57 | after: { 58 | type: 'DotOperator', 59 | after: { 60 | type: 'Identifier', 61 | value: 'bb', 62 | }, 63 | }, 64 | }, 65 | }, 66 | }, 67 | }) 68 | expect(parser.data.segments).toEqual(['aa', '2', 'dd', 'bb']) 69 | expect(parser2.data.segments).toEqual(['aa', '1', 'ee']) 70 | expect(parser3.data.segments).toEqual(['aa', '1']) 71 | expect(parser4.data.segments).toEqual(['aa']) 72 | expect(parser5.data.segments).toEqual(['aa', '1', 'dd']) 73 | expect(parser6.data.segments).toEqual(['aa', '1', '[]']) 74 | expect(parser7.data.segments).toEqual(['aa', '1', '[aa]']) 75 | }) 76 | 77 | batchTest({ 78 | '*': { 79 | type: 'WildcardOperator', 80 | }, 81 | 'a.b.c': { 82 | type: 'Identifier', 83 | value: 'a', 84 | after: { 85 | type: 'DotOperator', 86 | after: { 87 | type: 'Identifier', 88 | value: 'b', 89 | after: { 90 | type: 'DotOperator', 91 | after: { 92 | type: 'Identifier', 93 | value: 'c', 94 | }, 95 | }, 96 | }, 97 | }, 98 | }, 99 | 'a.b.*': { 100 | type: 'Identifier', 101 | value: 'a', 102 | after: { 103 | type: 'DotOperator', 104 | after: { 105 | type: 'Identifier', 106 | value: 'b', 107 | after: { 108 | type: 'DotOperator', 109 | after: { 110 | type: 'WildcardOperator', 111 | }, 112 | }, 113 | }, 114 | }, 115 | }, 116 | 'a.b.*(111,222,aaa)': { 117 | type: 'Identifier', 118 | value: 'a', 119 | after: { 120 | type: 'DotOperator', 121 | after: { 122 | type: 'Identifier', 123 | value: 'b', 124 | after: { 125 | type: 'DotOperator', 126 | after: { 127 | type: 'WildcardOperator', 128 | filter: { 129 | type: 'GroupExpression', 130 | value: [ 131 | { 132 | type: 'Identifier', 133 | value: '111', 134 | }, 135 | { 136 | type: 'Identifier', 137 | value: '222', 138 | }, 139 | { 140 | type: 'Identifier', 141 | value: 'aaa', 142 | }, 143 | ], 144 | }, 145 | }, 146 | }, 147 | }, 148 | }, 149 | }, 150 | 'a.b.*(!111,222,aaa)': { 151 | type: 'Identifier', 152 | value: 'a', 153 | after: { 154 | type: 'DotOperator', 155 | after: { 156 | type: 'Identifier', 157 | value: 'b', 158 | after: { 159 | type: 'DotOperator', 160 | after: { 161 | type: 'WildcardOperator', 162 | filter: { 163 | type: 'GroupExpression', 164 | isExclude: true, 165 | value: [ 166 | { 167 | type: 'Identifier', 168 | value: '111', 169 | }, 170 | { 171 | type: 'Identifier', 172 | value: '222', 173 | }, 174 | { 175 | type: 'Identifier', 176 | value: 'aaa', 177 | }, 178 | ], 179 | }, 180 | }, 181 | }, 182 | }, 183 | }, 184 | }, 185 | 'a.b. * [ 11 : 22 ]': { 186 | type: 'Identifier', 187 | value: 'a', 188 | after: { 189 | type: 'DotOperator', 190 | after: { 191 | type: 'Identifier', 192 | value: 'b', 193 | after: { 194 | type: 'DotOperator', 195 | after: { 196 | type: 'WildcardOperator', 197 | filter: { 198 | type: 'RangeExpression', 199 | start: { 200 | type: 'Identifier', 201 | value: '11', 202 | }, 203 | end: { 204 | type: 'Identifier', 205 | value: '22', 206 | }, 207 | }, 208 | }, 209 | }, 210 | }, 211 | }, 212 | }, 213 | 'a.b.*([[123123!,()]],[[aaa]])': { 214 | type: 'Identifier', 215 | value: 'a', 216 | after: { 217 | type: 'DotOperator', 218 | after: { 219 | type: 'Identifier', 220 | value: 'b', 221 | after: { 222 | type: 'DotOperator', 223 | after: { 224 | type: 'WildcardOperator', 225 | filter: { 226 | type: 'GroupExpression', 227 | value: [ 228 | { 229 | type: 'IgnoreExpression', 230 | value: '123123!,()', 231 | }, 232 | { 233 | type: 'IgnoreExpression', 234 | value: 'aaa', 235 | }, 236 | ], 237 | }, 238 | }, 239 | }, 240 | }, 241 | }, 242 | }, 243 | 'a.b.*([[123123!,()]],aaa)': { 244 | type: 'Identifier', 245 | value: 'a', 246 | after: { 247 | type: 'DotOperator', 248 | after: { 249 | type: 'Identifier', 250 | value: 'b', 251 | after: { 252 | type: 'DotOperator', 253 | after: { 254 | type: 'WildcardOperator', 255 | filter: { 256 | type: 'GroupExpression', 257 | value: [ 258 | { 259 | type: 'IgnoreExpression', 260 | value: '123123!,()', 261 | }, 262 | { 263 | type: 'Identifier', 264 | value: 'aaa', 265 | }, 266 | ], 267 | }, 268 | }, 269 | }, 270 | }, 271 | }, 272 | }, 273 | 'a.b.*(![[123123!,()]],aaa)': { 274 | type: 'Identifier', 275 | value: 'a', 276 | after: { 277 | type: 'DotOperator', 278 | after: { 279 | type: 'Identifier', 280 | value: 'b', 281 | after: { 282 | type: 'DotOperator', 283 | after: { 284 | type: 'WildcardOperator', 285 | filter: { 286 | type: 'GroupExpression', 287 | value: [ 288 | { 289 | type: 'IgnoreExpression', 290 | value: '123123!,()', 291 | }, 292 | { 293 | type: 'Identifier', 294 | value: 'aaa', 295 | }, 296 | ], 297 | isExclude: true, 298 | }, 299 | }, 300 | }, 301 | }, 302 | }, 303 | }, 304 | 'a.b . * (![[123123!,()]],aaa,bbb)': { 305 | type: 'Identifier', 306 | value: 'a', 307 | after: { 308 | type: 'DotOperator', 309 | after: { 310 | type: 'Identifier', 311 | value: 'b', 312 | after: { 313 | type: 'DotOperator', 314 | after: { 315 | type: 'WildcardOperator', 316 | filter: { 317 | type: 'GroupExpression', 318 | value: [ 319 | { 320 | type: 'IgnoreExpression', 321 | value: '123123!,()', 322 | }, 323 | { 324 | type: 'Identifier', 325 | value: 'aaa', 326 | }, 327 | { 328 | type: 'Identifier', 329 | value: 'bbb', 330 | }, 331 | ], 332 | isExclude: true, 333 | }, 334 | }, 335 | }, 336 | }, 337 | }, 338 | }, 339 | 'a.b.[[123123!,()]] ': { 340 | type: 'Identifier', 341 | value: 'a', 342 | after: { 343 | type: 'DotOperator', 344 | after: { 345 | type: 'Identifier', 346 | value: 'b', 347 | after: { 348 | type: 'DotOperator', 349 | after: { 350 | type: 'IgnoreExpression', 351 | value: '123123!,()', 352 | }, 353 | }, 354 | }, 355 | }, 356 | }, 357 | [`a . 358 | b . 359 | [[123123!,()]] 360 | 361 | .aaaa`]: { 362 | type: 'Identifier', 363 | value: 'a', 364 | after: { 365 | type: 'DotOperator', 366 | after: { 367 | type: 'Identifier', 368 | value: 'b', 369 | after: { 370 | type: 'DotOperator', 371 | after: { 372 | type: 'IgnoreExpression', 373 | value: '123123!,()', 374 | after: { 375 | type: 'DotOperator', 376 | after: { 377 | type: 'Identifier', 378 | value: 'aaaa', 379 | }, 380 | }, 381 | }, 382 | }, 383 | }, 384 | }, 385 | }, 386 | 'a.*(aaa.d.*(!sss),ddd,bbb).c.b': { 387 | type: 'Identifier', 388 | value: 'a', 389 | after: { 390 | type: 'DotOperator', 391 | after: { 392 | type: 'WildcardOperator', 393 | filter: { 394 | type: 'GroupExpression', 395 | value: [ 396 | { 397 | type: 'Identifier', 398 | value: 'aaa', 399 | after: { 400 | type: 'DotOperator', 401 | after: { 402 | type: 'Identifier', 403 | value: 'd', 404 | after: { 405 | type: 'DotOperator', 406 | after: { 407 | type: 'WildcardOperator', 408 | filter: { 409 | type: 'GroupExpression', 410 | isExclude: true, 411 | value: [ 412 | { 413 | type: 'Identifier', 414 | value: 'sss', 415 | }, 416 | ], 417 | }, 418 | }, 419 | }, 420 | }, 421 | }, 422 | }, 423 | { 424 | type: 'Identifier', 425 | value: 'ddd', 426 | }, 427 | { 428 | type: 'Identifier', 429 | value: 'bbb', 430 | }, 431 | ], 432 | }, 433 | after: { 434 | type: 'DotOperator', 435 | after: { 436 | type: 'Identifier', 437 | value: 'c', 438 | after: { 439 | type: 'DotOperator', 440 | after: { 441 | type: 'Identifier', 442 | value: 'b', 443 | }, 444 | }, 445 | }, 446 | }, 447 | }, 448 | }, 449 | }, 450 | 'aa.bb.cc.{aa,bb,cc:kk}': { 451 | type: 'Identifier', 452 | value: 'aa', 453 | after: { 454 | type: 'DotOperator', 455 | after: { 456 | type: 'Identifier', 457 | value: 'bb', 458 | after: { 459 | type: 'DotOperator', 460 | after: { 461 | type: 'Identifier', 462 | value: 'cc', 463 | after: { 464 | type: 'DotOperator', 465 | after: { 466 | type: 'DestructorExpression', 467 | value: { 468 | type: 'ObjectPattern', 469 | properties: [ 470 | { 471 | type: 'ObjectPatternProperty', 472 | key: { type: 'Identifier', value: 'aa' }, 473 | }, 474 | { 475 | type: 'ObjectPatternProperty', 476 | key: { type: 'Identifier', value: 'bb' }, 477 | }, 478 | { 479 | type: 'ObjectPatternProperty', 480 | key: { type: 'Identifier', value: 'cc' }, 481 | value: { type: 'Identifier', value: 'kk' }, 482 | }, 483 | ], 484 | }, 485 | source: '{aa,bb,cc:kk}', 486 | }, 487 | }, 488 | }, 489 | }, 490 | }, 491 | }, 492 | }, 493 | 'aa.bb.cc.[ [aa,bb,cc,[ [{aa:bb}] ]] ]': { 494 | type: 'Identifier', 495 | value: 'aa', 496 | after: { 497 | type: 'DotOperator', 498 | after: { 499 | type: 'Identifier', 500 | value: 'bb', 501 | after: { 502 | type: 'DotOperator', 503 | after: { 504 | type: 'Identifier', 505 | value: 'cc', 506 | after: { 507 | type: 'DotOperator', 508 | after: { 509 | type: 'DestructorExpression', 510 | value: { 511 | type: 'ArrayPattern', 512 | elements: [ 513 | { 514 | type: 'ArrayPattern', 515 | elements: [ 516 | { 517 | type: 'Identifier', 518 | value: 'aa', 519 | }, 520 | { 521 | type: 'Identifier', 522 | value: 'bb', 523 | }, 524 | { 525 | type: 'Identifier', 526 | value: 'cc', 527 | }, 528 | { 529 | type: 'ArrayPattern', 530 | elements: [ 531 | { 532 | type: 'ArrayPattern', 533 | elements: [ 534 | { 535 | type: 'ObjectPattern', 536 | properties: [ 537 | { 538 | type: 'ObjectPatternProperty', 539 | key: { 540 | type: 'Identifier', 541 | value: 'aa', 542 | }, 543 | value: { 544 | type: 'Identifier', 545 | value: 'bb', 546 | }, 547 | }, 548 | ], 549 | }, 550 | ], 551 | }, 552 | ], 553 | }, 554 | ], 555 | }, 556 | ], 557 | }, 558 | source: '[[aa,bb,cc,[[{aa:bb}]]]]', 559 | }, 560 | }, 561 | }, 562 | }, 563 | }, 564 | }, 565 | }, 566 | }) 567 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from './parser' 2 | import { 3 | isStr, 4 | isArr, 5 | isFn, 6 | isEqual, 7 | isObj, 8 | isNum, 9 | isRegExp, 10 | isPlainObj, 11 | } from './utils' 12 | import { 13 | getDestructor, 14 | getInByDestructor, 15 | setInByDestructor, 16 | deleteInByDestructor, 17 | existInByDestructor, 18 | } from './destructor' 19 | import { Segments, Node, Pattern, IRegistry, IAccessors } from './types' 20 | export * from './types' 21 | import { LRUMap } from './lru' 22 | import { Matcher } from './matcher' 23 | 24 | const REGISTRY: IRegistry = { 25 | accessors: { 26 | get(source: any, key: number | string | symbol) { 27 | if (typeof source !== 'object') return source 28 | return Reflect.get(source, key) 29 | }, 30 | set(source: any, key: number | string | symbol, value: any) { 31 | if (typeof source !== 'object') return 32 | return Reflect.set(source, key, value) 33 | }, 34 | has(source: any, key: number | string | symbol) { 35 | if (typeof source !== 'object') return 36 | return Reflect.has(source, key) 37 | }, 38 | delete(source: any, key: number | string | symbol) { 39 | if (typeof source !== 'object') return 40 | if (Array.isArray(source) && isNumberIndex(key)) { 41 | source.splice(Number(key), 1) 42 | return true 43 | } 44 | return Reflect.deleteProperty(source, key) 45 | }, 46 | }, 47 | } 48 | 49 | const pathCache = new LRUMap(1000) 50 | 51 | const isMatcher = Symbol('PATH_MATCHER') 52 | 53 | const isValid = (val: any) => val !== undefined && val !== null 54 | 55 | const isNumberIndex = (val: any) => 56 | isStr(val) ? /^\d+$/.test(val) : isNum(val) 57 | 58 | const arrayExist = (obj: any, key: string | number) => { 59 | if (Array.isArray(obj)) { 60 | const index = Number(key) 61 | if (index < 0 || index > obj.length - 1) return false 62 | } 63 | return true 64 | } 65 | 66 | const getIn = (segments: Segments, source: any) => { 67 | for (let i = 0; i < segments.length; i++) { 68 | let index = segments[i] 69 | const rules = getDestructor(index as string) 70 | if (!rules) { 71 | if (!isValid(source)) { 72 | if (i !== segments.length - 1) { 73 | return source 74 | } 75 | break 76 | } 77 | if (arrayExist(source, index)) { 78 | source = REGISTRY.accessors.get(source, index) 79 | } else { 80 | return 81 | } 82 | } else { 83 | source = getInByDestructor(source, rules, { setIn, getIn }) 84 | break 85 | } 86 | } 87 | return source 88 | } 89 | 90 | const setIn = (segments: Segments, source: any, value: any) => { 91 | for (let i = 0; i < segments.length; i++) { 92 | const index = segments[i] 93 | const rules = getDestructor(index as string) 94 | if (!rules) { 95 | if (!isValid(source)) return 96 | if (isArr(source) && !isNumberIndex(index)) { 97 | return 98 | } 99 | if (!arrayExist(source, index)) { 100 | if (!isValid(value)) { 101 | return 102 | } 103 | if (i < segments.length - 1) { 104 | REGISTRY.accessors.set( 105 | source, 106 | index, 107 | isNum(segments[i + 1]) ? [] : {} 108 | ) 109 | } 110 | } else if (!isValid(source[index])) { 111 | if (!isValid(value)) { 112 | return 113 | } 114 | if (i < segments.length - 1) { 115 | REGISTRY.accessors.set( 116 | source, 117 | index, 118 | isNum(segments[i + 1]) ? [] : {} 119 | ) 120 | } 121 | } 122 | if (i === segments.length - 1) { 123 | REGISTRY.accessors.set(source, index, value) 124 | } 125 | if (arrayExist(source, index)) { 126 | source = REGISTRY.accessors.get(source, index) 127 | } 128 | } else { 129 | setInByDestructor(source, rules, value, { setIn, getIn }) 130 | break 131 | } 132 | } 133 | } 134 | 135 | const deleteIn = (segments: Segments, source: any) => { 136 | for (let i = 0; i < segments.length; i++) { 137 | let index = segments[i] 138 | const rules = getDestructor(index as string) 139 | if (!rules) { 140 | if (i === segments.length - 1 && isValid(source)) { 141 | REGISTRY.accessors.delete(source, index) 142 | return 143 | } 144 | 145 | if (!isValid(source)) return 146 | if (arrayExist(source, index)) { 147 | source = REGISTRY.accessors.get(source, index) 148 | } else { 149 | return 150 | } 151 | 152 | if (!isObj(source)) { 153 | return 154 | } 155 | } else { 156 | deleteInByDestructor(source, rules, { 157 | setIn, 158 | getIn, 159 | deleteIn, 160 | }) 161 | break 162 | } 163 | } 164 | } 165 | 166 | const existIn = (segments: Segments, source: any, start: number | Path) => { 167 | if (start instanceof Path) { 168 | start = start.length 169 | } 170 | for (let i = start; i < segments.length; i++) { 171 | let index = segments[i] 172 | const rules = getDestructor(index as string) 173 | if (!rules) { 174 | if (i === segments.length - 1) { 175 | return REGISTRY.accessors.has(source, index) 176 | } 177 | 178 | if (!isValid(source)) return false 179 | if (arrayExist(source, index)) { 180 | source = REGISTRY.accessors.get(source, index) 181 | } else { 182 | return false 183 | } 184 | 185 | if (!isObj(source)) { 186 | return false 187 | } 188 | } else { 189 | return existInByDestructor(source, rules, start, { 190 | setIn, 191 | getIn, 192 | deleteIn, 193 | existIn, 194 | }) 195 | } 196 | } 197 | } 198 | 199 | const parse = (pattern: Pattern, base?: Pattern) => { 200 | if (pattern instanceof Path) { 201 | return { 202 | entire: pattern.entire, 203 | segments: pattern.segments.slice(), 204 | isRegExp: false, 205 | isWildMatchPattern: pattern.isWildMatchPattern, 206 | isMatchPattern: pattern.isMatchPattern, 207 | haveExcludePattern: pattern.haveExcludePattern, 208 | tree: pattern.tree, 209 | } 210 | } else if (isStr(pattern)) { 211 | if (!pattern) 212 | return { 213 | entire: '', 214 | segments: [], 215 | isRegExp: false, 216 | isWildMatchPattern: false, 217 | haveExcludePattern: false, 218 | isMatchPattern: false, 219 | } 220 | const parser = new Parser(pattern, Path.parse(base)) 221 | const tree = parser.parse() 222 | if (!parser.isMatchPattern) { 223 | const segments = parser.data.segments 224 | return { 225 | entire: segments.join('.'), 226 | segments, 227 | tree, 228 | isRegExp: false, 229 | isWildMatchPattern: false, 230 | haveExcludePattern: false, 231 | isMatchPattern: false, 232 | } 233 | } else { 234 | return { 235 | entire: pattern, 236 | segments: [], 237 | isRegExp: false, 238 | isWildMatchPattern: parser.isWildMatchPattern, 239 | haveExcludePattern: parser.haveExcludePattern, 240 | isMatchPattern: true, 241 | tree, 242 | } 243 | } 244 | } else if (isFn(pattern) && pattern[isMatcher]) { 245 | return parse(pattern['path']) 246 | } else if (isArr(pattern)) { 247 | return { 248 | entire: pattern.join('.'), 249 | segments: pattern.reduce((buf, key) => { 250 | return buf.concat(parseString(key)) 251 | }, []), 252 | isRegExp: false, 253 | isWildMatchPattern: false, 254 | haveExcludePattern: false, 255 | isMatchPattern: false, 256 | } 257 | } else if (isRegExp(pattern)) { 258 | return { 259 | entire: pattern, 260 | segments: [], 261 | isRegExp: true, 262 | isWildMatchPattern: false, 263 | haveExcludePattern: false, 264 | isMatchPattern: true, 265 | } 266 | } else { 267 | return { 268 | entire: '', 269 | isRegExp: false, 270 | segments: pattern !== undefined ? [pattern] : [], 271 | isWildMatchPattern: false, 272 | haveExcludePattern: false, 273 | isMatchPattern: false, 274 | } 275 | } 276 | } 277 | 278 | const parseString = (source: any) => { 279 | if (isStr(source)) { 280 | source = source.replace(/\s*/g, '') 281 | try { 282 | const { segments, isMatchPattern } = parse(source) 283 | return !isMatchPattern ? segments : source 284 | } catch (e) { 285 | return source 286 | } 287 | } else if (source instanceof Path) { 288 | return source.segments 289 | } 290 | return source 291 | } 292 | 293 | export class Path { 294 | public entire: string | RegExp 295 | public segments: Segments 296 | public isMatchPattern: boolean 297 | public isWildMatchPattern: boolean 298 | public isRegExp: boolean 299 | public haveExcludePattern: boolean 300 | public matchScore: number 301 | public tree: Node 302 | private matchCache: any 303 | private includesCache: any 304 | 305 | constructor(input: Pattern, base?: Pattern) { 306 | const { 307 | tree, 308 | segments, 309 | entire, 310 | isRegExp, 311 | isMatchPattern, 312 | isWildMatchPattern, 313 | haveExcludePattern, 314 | } = parse(input, base) 315 | this.entire = entire 316 | this.segments = segments 317 | this.isMatchPattern = isMatchPattern 318 | this.isWildMatchPattern = isWildMatchPattern 319 | this.isRegExp = isRegExp 320 | this.haveExcludePattern = haveExcludePattern 321 | this.tree = tree as Node 322 | this.matchCache = new LRUMap(200) 323 | this.includesCache = new LRUMap(200) 324 | } 325 | 326 | toString() { 327 | return this.entire?.toString() 328 | } 329 | 330 | toArray() { 331 | return this.segments?.slice() 332 | } 333 | 334 | get length() { 335 | return this.segments.length 336 | } 337 | 338 | concat = (...args: Pattern[]) => { 339 | if (this.isMatchPattern || this.isRegExp) { 340 | throw new Error(`${this.entire} cannot be concat`) 341 | } 342 | const path = new Path('') 343 | path.segments = this.segments.concat(...args.map((s) => parseString(s))) 344 | path.entire = path.segments.join('.') 345 | return path 346 | } 347 | 348 | slice = (start?: number, end?: number) => { 349 | if (this.isMatchPattern || this.isRegExp) { 350 | throw new Error(`${this.entire} cannot be slice`) 351 | } 352 | const path = new Path('') 353 | path.segments = this.segments.slice(start, end) 354 | path.entire = path.segments.join('.') 355 | return path 356 | } 357 | 358 | push = (...items: Pattern[]) => { 359 | return this.concat(...items) 360 | } 361 | 362 | pop = () => { 363 | if (this.isMatchPattern || this.isRegExp) { 364 | throw new Error(`${this.entire} cannot be pop`) 365 | } 366 | return new Path(this.segments.slice(0, this.segments.length - 1)) 367 | } 368 | 369 | splice = ( 370 | start: number, 371 | deleteCount?: number, 372 | ...items: Array 373 | ) => { 374 | if (this.isMatchPattern || this.isRegExp) { 375 | throw new Error(`${this.entire} cannot be splice`) 376 | } 377 | items = items.reduce((buf, item) => buf.concat(parseString(item)), []) 378 | const segments_ = this.segments.slice() 379 | segments_.splice(start, deleteCount, ...items) 380 | return new Path(segments_) 381 | } 382 | 383 | forEach = (callback: (key: string | number) => any) => { 384 | if (this.isMatchPattern || this.isRegExp) { 385 | throw new Error(`${this.entire} cannot be each`) 386 | } 387 | this.segments.forEach(callback) 388 | } 389 | 390 | map = (callback: (key: string | number) => any) => { 391 | if (this.isMatchPattern || this.isRegExp) { 392 | throw new Error(`${this.entire} cannot be map`) 393 | } 394 | return this.segments.map(callback) 395 | } 396 | 397 | reduce = ( 398 | callback: (buf: T, item: string | number, index: number) => T, 399 | initial: T 400 | ): T => { 401 | if (this.isMatchPattern || this.isRegExp) { 402 | throw new Error(`${this.entire} cannot be reduce`) 403 | } 404 | return this.segments.reduce(callback, initial) 405 | } 406 | 407 | parent = () => { 408 | return this.slice(0, this.length - 1) 409 | } 410 | 411 | includes = (pattern: Pattern) => { 412 | const { entire, segments, isMatchPattern } = Path.parse(pattern) 413 | const cache = this.includesCache.get(entire) 414 | if (cache !== undefined) return cache 415 | const cacheWith = (value: boolean): boolean => { 416 | this.includesCache.set(entire, value) 417 | return value 418 | } 419 | if (this.isMatchPattern) { 420 | if (!isMatchPattern) { 421 | return cacheWith(this.match(segments)) 422 | } else { 423 | throw new Error(`${this.entire} cannot be used to match ${entire}`) 424 | } 425 | } 426 | if (isMatchPattern) { 427 | throw new Error(`${this.entire} cannot be used to match ${entire}`) 428 | } 429 | if (segments.length > this.segments.length) return cacheWith(false) 430 | for (let i = 0; i < segments.length; i++) { 431 | if (!isEqual(String(segments[i]), String(this.segments[i]))) { 432 | return cacheWith(false) 433 | } 434 | } 435 | return cacheWith(true) 436 | } 437 | 438 | transform = ( 439 | regexp: string | RegExp, 440 | callback: (...args: string[]) => T 441 | ): T | string => { 442 | if (!isFn(callback)) return '' 443 | if (this.isMatchPattern) { 444 | throw new Error(`${this.entire} cannot be transformed`) 445 | } 446 | const args = this.segments.reduce((buf, key) => { 447 | return new RegExp(regexp).test(key as string) ? buf.concat(key) : buf 448 | }, []) 449 | return callback(...args) 450 | } 451 | 452 | match = (pattern: Pattern): boolean => { 453 | const path = Path.parse(pattern) 454 | const cache = this.matchCache.get(path.entire) 455 | if (cache !== undefined) { 456 | if (cache.record && cache.record.score !== undefined) { 457 | this.matchScore = cache.record.score 458 | } 459 | return cache.matched 460 | } 461 | const cacheWith = (value: any) => { 462 | this.matchCache.set(path.entire, value) 463 | return value 464 | } 465 | if (path.isMatchPattern) { 466 | if (this.isMatchPattern) { 467 | throw new Error(`${path.entire} cannot match ${this.entire}`) 468 | } else { 469 | this.matchScore = 0 470 | return cacheWith(path.match(this.segments)) 471 | } 472 | } else { 473 | if (this.isMatchPattern) { 474 | if (this.isRegExp) { 475 | return this['entire']?.['test']?.(path.entire) 476 | } 477 | const record = { 478 | score: 0, 479 | } 480 | const result = cacheWith( 481 | new Matcher(this.tree, record).match(path.segments) 482 | ) 483 | this.matchScore = record.score 484 | return result.matched 485 | } else { 486 | const record = { 487 | score: 0, 488 | } 489 | const result = cacheWith( 490 | Matcher.matchSegments(this.segments, path.segments, record) 491 | ) 492 | this.matchScore = record.score 493 | return result.matched 494 | } 495 | } 496 | } 497 | 498 | //别名组匹配 499 | matchAliasGroup = (name: Pattern, alias: Pattern) => { 500 | const namePath = Path.parse(name) 501 | const aliasPath = Path.parse(alias) 502 | const nameMatched = this.match(namePath) 503 | const nameMatchedScore = this.matchScore 504 | const aliasMatched = this.match(aliasPath) 505 | const aliasMatchedScore = this.matchScore 506 | if (this.haveExcludePattern) { 507 | if (nameMatchedScore >= aliasMatchedScore) { 508 | return nameMatched 509 | } else { 510 | return aliasMatched 511 | } 512 | } else { 513 | return nameMatched || aliasMatched 514 | } 515 | } 516 | 517 | existIn = (source?: any, start: number | Path = 0) => { 518 | return existIn(this.segments, source, start) 519 | } 520 | 521 | getIn = (source?: any) => { 522 | return getIn(this.segments, source) 523 | } 524 | 525 | setIn = (source?: any, value?: any) => { 526 | setIn(this.segments, source, value) 527 | return source 528 | } 529 | 530 | deleteIn = (source?: any) => { 531 | deleteIn(this.segments, source) 532 | return source 533 | } 534 | 535 | ensureIn = (source?: any, defaults?: any) => { 536 | const results = this.getIn(source) 537 | if (results === undefined) { 538 | this.setIn(source, defaults) 539 | return this.getIn(source) 540 | } 541 | return results 542 | } 543 | 544 | static match(pattern: Pattern) { 545 | const path = Path.parse(pattern) 546 | const matcher = (target) => { 547 | return path.match(target) 548 | } 549 | matcher[isMatcher] = true 550 | matcher.path = path 551 | return matcher 552 | } 553 | 554 | static transform( 555 | pattern: Pattern, 556 | regexp: string | RegExp, 557 | callback: (...args: string[]) => T 558 | ): any { 559 | return Path.parse(pattern).transform(regexp, callback) 560 | } 561 | 562 | static parse(path: Pattern = '', base?: Pattern): Path { 563 | if (path instanceof Path) { 564 | const found = pathCache.get(path.entire) 565 | if (found) { 566 | return found 567 | } else { 568 | pathCache.set(path.entire, path) 569 | return path 570 | } 571 | } else if (path && path[isMatcher]) { 572 | return Path.parse(path['path']) 573 | } else { 574 | const key_ = base ? Path.parse(base) : '' 575 | const key = `${path}:${key_}` 576 | const found = pathCache.get(key) 577 | if (found) { 578 | return found 579 | } else { 580 | path = new Path(path, base) 581 | pathCache.set(key, path) 582 | return path 583 | } 584 | } 585 | } 586 | 587 | static getIn = (source: any, pattern: Pattern) => { 588 | const path = Path.parse(pattern) 589 | return path.getIn(source) 590 | } 591 | 592 | static setIn = (source: any, pattern: Pattern, value: any) => { 593 | const path = Path.parse(pattern) 594 | return path.setIn(source, value) 595 | } 596 | 597 | static deleteIn = (source: any, pattern: Pattern) => { 598 | const path = Path.parse(pattern) 599 | return path.deleteIn(source) 600 | } 601 | 602 | static existIn = (source: any, pattern: Pattern, start?: number | Path) => { 603 | const path = Path.parse(pattern) 604 | return path.existIn(source, start) 605 | } 606 | 607 | static ensureIn = (source: any, pattern: Pattern, defaultValue?: any) => { 608 | const path = Path.parse(pattern) 609 | return path.ensureIn(source, defaultValue) 610 | } 611 | 612 | static registerAccessors = (accessors: IAccessors) => { 613 | if (isPlainObj(accessors)) { 614 | for (let name in accessors) { 615 | if (isFn(accessors[name])) { 616 | REGISTRY.accessors[name] = accessors[name] 617 | } 618 | } 619 | } 620 | } 621 | } 622 | 623 | export default Path 624 | --------------------------------------------------------------------------------