@diplodoc/transform
Version:
A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML
166 lines (137 loc) • 4.89 kB
text/typescript
import getObject from '../getObject';
import {log} from '../log';
import * as lexical from './lexical';
import filters from './filters';
import {SkippedEvalError} from './errors';
type Scope = Record<string, unknown>;
type WithFilter = (l: string, filter: (s: string) => boolean, exp: string) => boolean;
type NoFilter = (l: string, r: string, exp: string) => boolean;
type DotOperator = (
l: Record<string, (...args: unknown[]) => unknown>,
r: string,
exp: string,
) => boolean;
const operators: Record<string, WithFilter | NoFilter | DotOperator> = {
'==': ((l, r) => l === r) as NoFilter,
'!=': ((l, r) => l !== r) as NoFilter,
'>': ((l, r) => l !== null && r !== null && l > r) as NoFilter,
'<': ((l, r) => l !== null && r !== null && l < r) as NoFilter,
'>=': ((l, r) => l !== null && r !== null && l >= r) as NoFilter,
'<=': ((l, r) => l !== null && r !== null && l <= r) as NoFilter,
contains: ((l, r) => l !== null && r !== null && l.includes(r)) as NoFilter,
and: ((l, r) => isTruthy(l) && isTruthy(r)) as NoFilter,
or: ((l, r) => isTruthy(l) || isTruthy(r)) as NoFilter,
'|': ((l, filter, exp) => {
try {
return filter(l);
} catch (e) {
if (!filter) {
throw new SkippedEvalError('Cannot apply an unsupported filter', exp);
}
throw new SkippedEvalError('There are some problems with the filter', exp);
}
}) as WithFilter,
'.': ((l, r, exp) => {
const parsed = lexical.getParsedMethod(r);
try {
if (!parsed) {
throw new Error();
}
const {name, args} = parsed;
return l[name](...args);
} catch (e) {
if (!l) {
throw new SkippedEvalError(
`Cannot apply the function '${name}' on an undefined variable`,
exp,
);
}
throw new SkippedEvalError('There are some problems with the function', exp);
}
}) as DotOperator,
};
export const NoValue = Symbol('NoValue');
function evalValue(originStr: string, scope: Scope, strict: boolean) {
const str = originStr && originStr.trim();
if (!str) {
return undefined;
}
if (lexical.isLiteral(str)) {
return lexical.parseLiteral(str);
}
if (lexical.isVariable(str)) {
return getObject(str, scope, strict ? NoValue : undefined);
}
throw new TypeError(`cannot eval '${str}' as value`);
}
function isTruthy(val: unknown) {
return !isFalsy(val);
}
function isFalsy(val: unknown) {
return val === false || undefined === val || val === null;
}
const operatorREs = lexical.operators.map(
(op) =>
new RegExp(
`^(${lexical.quoteBalanced.source})(${op.source})(${lexical.quoteBalanced.source})$`,
),
);
export function evalExp(
exp: string,
scope: Record<string, unknown>,
strict = false,
):
| string[]
| number[]
| boolean
| string
| symbol
| undefined
| ((input: string) => number | string) {
if (Object.getOwnPropertyNames(filters).includes(exp.trim())) {
return filters[exp.trim() as keyof typeof filters];
}
if (lexical.isSupportedMethod(exp)) {
return exp;
}
try {
for (let i = 0; i < operatorREs.length; i++) {
const operatorRE = operatorREs[i];
const match = exp.match(operatorRE);
if (match) {
const operator = match[2].trim();
if (operator === '.' && !lexical.isSupportedMethod(match[3].trim())) {
break;
}
const op = operators[operator];
const l = evalExp(match[1], scope, strict);
const r = evalExp(match[3], scope, strict);
if (l === NoValue || r === NoValue) {
return NoValue;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return op(l as any, r as any, exp);
}
}
const match = exp.match(lexical.rangeLine);
if (match) {
const low = Number(evalValue(match[1], scope, strict));
const high = Number(evalValue(match[2], scope, strict));
const range = [];
for (let j = low; j <= high; j++) {
range.push(j);
}
return range;
}
return evalValue(exp, scope, strict);
} catch (e) {
if (e instanceof SkippedEvalError) {
log.warn(`Skip error: ${e}`);
return undefined;
}
log.error(`Error: ${e}`);
}
return undefined;
}
export default (exp: string, scope: Record<string, unknown>, strict = false) =>
Boolean(evalExp(exp, scope, strict));