@player-ui/player
Version:
326 lines (265 loc) • 6.85 kB
text/typescript
import type {
Parser,
AnyNode,
PathNode,
ConcatenatedNode,
ValueNode,
QueryNode,
ExpressionNode,
} from "../ast";
import {
toValue,
toPath,
toConcatenatedNode,
toQuery,
toExpression,
} from "../ast";
const SEGMENT_SEPARATOR = ".";
const OPEN_CURL = "{";
const CLOSE_CURL = "}";
const OPEN_BRACKET = "[";
const CLOSE_BRACKET = "]";
const EQUALS = "=";
const SINGLE_QUOTE = "'";
const DOUBLE_QUOTE = '"';
const BACK_TICK = "`";
// const IDENTIFIER_REGEX = /[\w\-@]+/;
/** A _faster_ way to match chars instead of a regex. */
const isIdentifierChar = (char?: string): boolean => {
if (!char) {
return false;
}
const charCode = char.charCodeAt(0);
const matches =
charCode === 32 || // ' '
charCode === 34 || // "
charCode === 39 || // '
charCode === 40 || // (
charCode === 41 || // )
charCode === 42 || // *
charCode === 46 || // .
charCode === 61 || // =
charCode === 91 || // [
charCode === 93 || // ]
charCode === 96 || // `
charCode === 123 || // {
charCode === 125; // }
return !matches;
};
/** Parse out a binding AST from a path */
export const parse: Parser = (path) => {
let index = 1;
let ch = path.charAt(0);
/** get the next char in the string */
const next = (expected?: string) => {
if (expected && ch !== expected) {
throw new Error(`Expected char: ${expected} but got: ${ch}`);
}
ch = path.charAt(index);
index += 1;
return ch;
};
/** gobble all whitespace */
const whitespace = () => {
while (ch === " ") {
next();
}
};
/** get an identifier if you can */
const identifier = (allowBoolValue = false): ValueNode | undefined => {
if (!isIdentifierChar(ch)) {
return;
}
let value: string | number = ch;
while (next()) {
if (!isIdentifierChar(ch)) {
break;
}
value += ch;
}
if (allowBoolValue) {
if (value === "true") {
return toValue(true);
}
if (value === "false") {
return toValue(false);
}
}
if (value) {
const maybeNumber = Number(value);
value = isNaN(maybeNumber) ? value : maybeNumber;
return toValue(value);
}
};
/** get an expression node if you can */
const expression = (): ExpressionNode | undefined => {
if (ch === BACK_TICK) {
next(BACK_TICK);
let exp = ch;
while (next()) {
if (ch === BACK_TICK) {
break;
}
exp += ch;
}
next(BACK_TICK);
if (exp) {
return toExpression(exp);
}
}
};
/** Grab a value using a regex */
const regex = (match: RegExp): ValueNode | undefined => {
if (!ch?.match(match)) {
return;
}
let value = ch;
while (next()) {
if (!ch?.match(match)) {
break;
}
value += ch;
}
if (value) {
return toValue(value);
}
};
/** parse out a nestedPath if you can */
const nestedPath = (): PathNode | undefined => {
if (ch === OPEN_CURL) {
next(OPEN_CURL);
next(OPEN_CURL);
const modelRef = parsePath();
next(CLOSE_CURL);
next(CLOSE_CURL);
return modelRef;
}
};
/** get a simple segment node */
const simpleSegment = (allowBoolValue = false) =>
nestedPath() ?? expression() ?? identifier(allowBoolValue);
/** Parse a segment */
const segment = ():
| ConcatenatedNode
| PathNode
| ValueNode
| ExpressionNode
| undefined => {
// Either a string, modelRef, or concatenated version (both)
const segments: Array<ValueNode | PathNode | ExpressionNode> = [];
let nextSegment = simpleSegment();
while (nextSegment !== undefined) {
segments.push(nextSegment);
nextSegment = simpleSegment();
}
if (segments.length === 0) {
return undefined;
}
return toConcatenatedNode(segments);
};
/** get an optionally quoted block */
const optionallyQuotedSegment = (
allowBoolValue = false,
): ValueNode | PathNode | ExpressionNode | undefined => {
whitespace();
// see if we have a quote
if (ch === SINGLE_QUOTE || ch === DOUBLE_QUOTE) {
const singleQuote = ch === SINGLE_QUOTE;
next(singleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE);
const id = regex(/[^'"]+/);
next(singleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE);
return id;
}
return simpleSegment(allowBoolValue);
};
/** eat equals signs */
const equals = (): boolean => {
if (ch !== EQUALS) {
return false;
}
while (ch === EQUALS) {
next();
}
return true;
};
/** Parse out a bracket */
const parseBracket = ():
| ValueNode
| QueryNode
| PathNode
| ExpressionNode
| undefined => {
if (ch === OPEN_BRACKET) {
next(OPEN_BRACKET);
whitespace();
let value: ValueNode | QueryNode | PathNode | ExpressionNode | undefined =
optionallyQuotedSegment();
if (value) {
whitespace();
if (equals()) {
whitespace();
const second = optionallyQuotedSegment(true);
value = toQuery(value, second);
whitespace();
}
} else {
throw new Error(`Expected identifier`);
}
if (value) {
next(CLOSE_BRACKET);
}
return value;
}
};
/** Parse a segment and any number of brackets following it */
const parseSegmentAndBrackets = (): Array<AnyNode> => {
// try to parse a segment first
const parsed: Array<AnyNode> = [];
const firstSegment = segment();
if (firstSegment) {
parsed.push(firstSegment);
let bracketSegment = parseBracket();
if (bracketSegment?.name === "Value") {
const maybeNumber = Number(bracketSegment.value);
bracketSegment.value =
isNaN(maybeNumber) || String(maybeNumber) !== bracketSegment.value
? bracketSegment.value
: maybeNumber;
}
while (bracketSegment !== undefined) {
parsed.push(bracketSegment);
bracketSegment = parseBracket();
}
}
return parsed;
};
/** Parse out a path segment */
const parsePath = (): PathNode => {
const parts: AnyNode[] = [];
let nextSegment = parseSegmentAndBrackets();
while (nextSegment !== undefined) {
parts.push(...nextSegment);
if (!ch || ch === CLOSE_CURL) {
break;
}
if (nextSegment.length === 0 && ch) {
throw new Error(`Unexpected character: ${ch}`);
}
next(SEGMENT_SEPARATOR);
nextSegment = parseSegmentAndBrackets();
}
return toPath(parts);
};
try {
const result = parsePath();
return {
status: true,
path: result,
};
} catch (e: any) {
return {
status: false,
error: e.message,
};
}
};