UNPKG

@sanity/mutator

Version:

A set of models to make it easier to utilize the powerful real time collaborative features of Sanity

316 lines (271 loc) • 7.48 kB
// Converts a string into an abstract syntax tree representation import {tokenize} from './tokenize' import { type AliasExpr, type AttributeExpr, type BooleanExpr, type ConstraintExpr, type IndexExpr, type NumberExpr, type PathExpr, type RangeExpr, type RecursiveExpr, type StringExpr, type Token, type UnionExpr, } from './types' // TODO: Support '*' class Parser { tokens: Token[] length: number i: number constructor(path: string) { this.tokens = tokenize(path) this.length = this.tokens.length this.i = 0 } parse() { return this.parsePath() } EOF() { return this.i >= this.length } // Look at upcoming token peek() { if (this.EOF()) { return null } return this.tokens[this.i] } consume() { const result = this.peek() this.i += 1 return result } // Return next token if it matches the pattern probe(pattern: Record<string, unknown>): Token | null { const token = this.peek() if (!token) { return null } const record = token as unknown as Record<string, unknown> const match = Object.keys(pattern).every((key) => { return key in token && pattern[key] === record[key] }) return match ? token : null } // Return and consume next token if it matches the pattern match(pattern: Partial<Token>): Token | null { return this.probe(pattern) ? this.consume() : null } parseAttribute(): AttributeExpr | null { const token = this.match({type: 'identifier'}) if (token && token.type === 'identifier') { return { type: 'attribute', name: token.name, } } const quoted = this.match({type: 'quoted', quote: 'single'}) if (quoted && quoted.type === 'quoted') { return { type: 'attribute', name: quoted.value || '', } } return null } parseAlias(): AliasExpr | null { if (this.match({type: 'keyword', symbol: '@'}) || this.match({type: 'keyword', symbol: '$'})) { return { type: 'alias', target: 'self', } } return null } parseNumber(): NumberExpr | null { const token = this.match({type: 'number'}) if (token && token.type === 'number') { return { type: 'number', value: token.value, } } return null } parseNumberValue(): number | null { const expr = this.parseNumber() if (expr) { return expr.value } return null } parseSliceSelector(): RangeExpr | IndexExpr | null { const start = this.i const rangeStart = this.parseNumberValue() const colon1 = this.match({type: 'operator', symbol: ':'}) if (!colon1) { if (rangeStart === null) { // Rewind, this was actually nothing this.i = start return null } // Unwrap, this was just a single index not followed by colon return {type: 'index', value: rangeStart} } const result: RangeExpr = { type: 'range', start: rangeStart, end: this.parseNumberValue(), } const colon2 = this.match({type: 'operator', symbol: ':'}) if (colon2) { result.step = this.parseNumberValue() } if (result.start === null && result.end === null) { // rewind, this wasnt' a slice selector this.i = start return null } return result } parseValueReference(): AttributeExpr | RangeExpr | IndexExpr | null { return this.parseAttribute() || this.parseSliceSelector() } parseLiteralValue(): StringExpr | BooleanExpr | NumberExpr | null { const literalString = this.match({type: 'quoted', quote: 'double'}) if (literalString && literalString.type === 'quoted') { return { type: 'string', value: literalString.value || '', } } const literalBoolean = this.match({type: 'boolean'}) if (literalBoolean && literalBoolean.type === 'boolean') { return { type: 'boolean', value: literalBoolean.symbol === 'true', } } return this.parseNumber() } // TODO: Reorder constraints so that literal value is always on rhs, and variable is always // on lhs. parseFilterExpression(): ConstraintExpr | null { const start = this.i const expr = this.parseAttribute() || this.parseAlias() if (!expr) { return null } if (this.match({type: 'operator', symbol: '?'})) { return { type: 'constraint', operator: '?', lhs: expr, } } const binOp = this.match({type: 'comparator'}) if (!binOp || binOp.type !== 'comparator') { // No expression, rewind! this.i = start return null } const lhs = expr const rhs = this.parseLiteralValue() if (!rhs) { throw new Error(`Operator ${binOp.symbol} needs a literal value at the right hand side`) } return { type: 'constraint', operator: binOp.symbol, lhs: lhs, rhs: rhs, } } parseExpression(): ConstraintExpr | AttributeExpr | RangeExpr | IndexExpr | null { return this.parseFilterExpression() || this.parseValueReference() } parseUnion(): UnionExpr | null { if (!this.match({type: 'paren', symbol: '['})) { return null } const terms = [] let expr = this.parseFilterExpression() || this.parsePath() || this.parseValueReference() while (expr) { terms.push(expr) // End of union? if (this.match({type: 'paren', symbol: ']'})) { break } if (!this.match({type: 'operator', symbol: ','})) { throw new Error('Expected ]') } expr = this.parseFilterExpression() || this.parsePath() || this.parseValueReference() if (!expr) { throw new Error("Expected expression following ','") } } return { type: 'union', nodes: terms, } } parseRecursive(): RecursiveExpr | null { if (!this.match({type: 'operator', symbol: '..'})) { return null } const subpath = this.parsePath() if (!subpath) { throw new Error("Expected path following '..' operator") } return { type: 'recursive', term: subpath, } } parsePath(): PathExpr | AttributeExpr | UnionExpr | RecursiveExpr | null { const nodes: (AttributeExpr | UnionExpr | RecursiveExpr)[] = [] const expr = this.parseAttribute() || this.parseUnion() || this.parseRecursive() if (!expr) { return null } nodes.push(expr) while (!this.EOF()) { if (this.match({type: 'operator', symbol: '.'})) { const attr = this.parseAttribute() if (!attr) { throw new Error("Expected attribute name following '.") } nodes.push(attr) continue } else if (this.probe({type: 'paren', symbol: '['})) { const union = this.parseUnion() if (!union) { throw new Error("Expected union following '['") } nodes.push(union) } else { const recursive = this.parseRecursive() if (recursive) { nodes.push(recursive) } break } } if (nodes.length === 1) { return nodes[0] } return { type: 'path', nodes: nodes, } } } export function parseJsonPath(path: string): PathExpr | AttributeExpr | UnionExpr | RecursiveExpr { const parsed = new Parser(path).parse() if (!parsed) { throw new Error(`Failed to parse JSON path "${path}"`) } return parsed }