@comyata/run
Version:
Simplify data workflows and management with data templates, for browser and server.
145 lines (141 loc) • 6.14 kB
JavaScript
import { DataNode, DataNodeObject } from '@comyata/run/DataNode';
import { NodeParserError } from '@comyata/run/Errors';
import jsonpointer from 'json-pointer';
function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
}
export class Parser {
constructor(nodesTypes = [], options = {}) {
this.nodes = nodesTypes;
this.options = {
...options,
paren: options.paren || ['{', '}']
};
// todo refactor matcher and extract-expr for a better usage outside of runtime,
// maybe add a `comyata-utils` for these universal field utils?
const tagPattern = new RegExp(`^(?<engine>${this.nodes.filter(tag => tag.engine).map(tag => escapeRegex(tag.engine)).join('|')})${escapeRegex(this.options.paren[0])}`);
const offsetParenStart = this.options.paren[0].length;
const offsetParenEnd = this.options.paren[1].length;
const getTagExpExtract = offsetParenEnd ? tag => {
const offsetStart = tag.length + offsetParenStart;
return value => {
return value.slice(offsetStart, -offsetParenEnd);
};
} : tag => {
const offsetStart = tag.length + offsetParenStart;
return value => {
return value.slice(offsetStart);
};
};
const nodesMap = this.nodes.reduce((nodesMap, nodeType) => {
if (nodeType.engine) {
nodesMap.set(nodeType.engine, [nodeType, getTagExpExtract(nodeType.engine)]);
}
return nodesMap;
}, new Map());
const matchText = text => {
const match = text.match(tagPattern);
if (match?.groups?.engine) {
const tagName = match.groups.engine;
return nodesMap.get(tagName);
}
return undefined;
};
if (this.options.paren[1] === '') {
this.matchNode = matchText;
} else {
this.matchNode = text => {
if (!text.endsWith(this.options.paren[1])) return undefined;
return matchText(text);
};
}
}
static dataNodeParsers = {
null: (currentValue, currentPath, parent) => {
return [new DataNode(parent, currentPath, 'null', currentValue).withHydrate(() => currentValue)];
},
array: (currentValue, currentPath, parent) => {
const valueLength = currentValue.length;
const dataNode = new DataNodeObject(parent, currentPath, 'array', currentValue).withHydrate(() => new Array(valueLength));
return [dataNode, currentValue];
},
object: (currentValue, currentPath, parent) => {
const dataNode = new DataNodeObject(parent, currentPath, 'object', currentValue).withHydrate(() => ({}));
return [dataNode, currentValue];
},
number: (currentValue, currentPath, parent) => {
return [new DataNode(parent, currentPath, 'number', currentValue).withHydrate(() => currentValue)];
},
string: (currentValue, currentPath, parent) => {
return [new DataNode(parent, currentPath, 'string', currentValue).withHydrate(() => currentValue)];
},
boolean: (currentValue, currentPath, parent) => {
return [new DataNode(parent, currentPath, 'boolean', currentValue).withHydrate(() => currentValue)];
},
undefined: (_currentValue, currentPath, parent) => {
return [new DataNode(parent, currentPath, 'undefined', undefined).withHydrate(() => undefined)];
}
};
parseData = (currentValue, currentPath, parent) => {
if (typeof currentValue === 'string') {
const nodeTag = this.matchNode(currentValue);
if (nodeTag) {
try {
return [new nodeTag[0](parent, currentPath, '', currentValue, nodeTag[1])];
} catch (e) {
if (e instanceof NodeParserError) throw e;
throw new NodeParserError(currentPath, parent, `Parse error` + ` at ${JSON.stringify(jsonpointer.compile(currentPath))}` + ` with ${JSON.stringify(nodeTag[0].engine)}.` + `${e instanceof Error ? '\n' + e.message : typeof e === 'object' && e && 'message' in e ? '\n' + e.message : ''}`, e);
}
}
return Parser.dataNodeParsers.string(currentValue, currentPath, parent);
} else if (typeof currentValue === 'object') {
if (currentValue === null) {
return Parser.dataNodeParsers.null(currentValue, currentPath, parent);
} else if (Array.isArray(currentValue)) {
return Parser.dataNodeParsers.array(currentValue, currentPath, parent);
}
return Parser.dataNodeParsers.object(currentValue, currentPath, parent);
}
const type = typeof currentValue;
if (type in Parser.dataNodeParsers) {
return Parser.dataNodeParsers[type](
// @ts-expect-error not possible to type guard value
currentValue, currentPath, parent);
}
throw new NodeParserError(currentPath, parent, `Parse error` + ` at ${JSON.stringify(jsonpointer.compile(currentPath))}` + ` unsupported value in data, no supported parser for type ${JSON.stringify(type)}.`);
};
/**
* @deprecated create an instance and use it instead
*/
/* istanbul ignore next */
static parse(objOrEval) {
return new Parser([]).parse(objOrEval);
}
parse(objOrEval) {
const parseComments = this.options.comments;
const [rootNode, nextObject] = this.parseData(objOrEval, [], undefined);
const openParser = nextObject ? [[rootNode, nextObject]] : [];
while (openParser.length) {
const [dataNode, currentObject] = openParser.pop();
if (Array.isArray(currentObject)) {
for (const [key, val] of currentObject.entries()) {
const [nextDataNode, nextObject] = this.parseData(val, [...dataNode.path, key], dataNode);
dataNode.append(key, nextDataNode);
if (nextObject) {
openParser.push([nextDataNode, nextObject]);
}
}
} else {
for (const [key, val] of Object.entries(currentObject)) {
if (parseComments && key.endsWith('!')) continue;
const [nextDataNode, nextObject] = this.parseData(val, [...dataNode.path, key], dataNode);
dataNode.append(key, nextDataNode);
if (nextObject) {
openParser.push([nextDataNode, nextObject]);
}
}
}
}
return rootNode;
}
}