UNPKG

idyll-document

Version:

The Idyll runtime, implemented as a React component.

478 lines (433 loc) 12.3 kB
import falafel from 'falafel'; import { parse } from 'csv-parse/sync'; const { cloneNode, getChildren, getNodeName, getNodeType, getProperties, isTextNode, removeNodes } = require('idyll-ast'); const isPropertyAccess = node => { const index = node.parent.source().indexOf(`.${node.name}`); if (index === -1) { return false; } const proxyString = '__idyllStateProxy'; if (index >= proxyString.length) { if ( node.parent .source() .substr(index - proxyString.length, proxyString.length) === proxyString ) { return false; } } return true; }; const isObjectKey = node => { return node.parent.type === 'Property' && node.parent.key === node; }; export const buildExpression = (acc, expr, isEventHandler) => { let identifiers = []; let modifiedExpression = ''; try { modifiedExpression = falafel( isEventHandler ? expr : `var __idyllReturnValue = ${expr || 'undefined'}`, node => { switch (node.type) { case 'Identifier': const skip = isPropertyAccess(node) || isObjectKey(node); if (Object.keys(acc).indexOf(node.name) > -1) { identifiers.push(node.name); if (!skip) { node.update('__idyllStateProxy.' + node.source()); } } break; } } ); } catch (e) { console.error(e); } if (!isEventHandler) { return ` ((context) => { var __idyllStateProxy = new Proxy({}, { get: (_, prop) => { return context[prop]; }, set: (_, prop, value) => { console.warn('Warning, trying to set a value in a property expression.'); } }); ${modifiedExpression}; return __idyllReturnValue; })(this)`; } return ` ((context) => { var __idyllExpressionExecuted = false; var __idyllStateProxy = new Proxy({ ${identifiers .map(key => { return `${key}: ${ key !== 'refs' ? `context.__idyllCopy(context['${key}'])` : `context['${key}']` }`; }) .join(', ')} }, { get: (target, prop) => { return target[prop]; }, set: (target, prop, value) => { if (__idyllExpressionExecuted) { var newState = {}; newState[prop] = value; context.__idyllUpdate(newState); } target[prop] = value; return true; } }); ${modifiedExpression}; context.__idyllUpdate({ ${identifiers .filter(key => key !== 'refs') .map(key => { return `${key}: __idyllStateProxy['${key}']`; }) .join(', ')} }); __idyllExpressionExecuted = true; })(this) `; }; export const evalExpression = (acc, expr, key, context) => { const isEventHandler = key && (key.match(/^on[A-Z].*/) || key.match(/^handle[A-Z].*/)); let e = buildExpression(acc, expr, isEventHandler); if (isEventHandler) { return function() { eval(e); }.bind( Object.assign({}, acc, context || {}, { __idyllCopy: function copy(o) { if (typeof o !== 'object') return o; var output, v, key; output = Array.isArray(o) ? [] : {}; for (key in o) { v = o[key]; output[key] = typeof v === 'object' ? copy(v) : v; } return output; } }) ); } try { return function(evalString) { try { return eval('(' + evalString + ')'); } catch (err) { console.warn('Error occurred in Idyll expression'); } }.call(Object.assign({}, acc), e); } catch (err) {} }; export const getVars = (arr, context = {}) => { const formatAccumulatedValues = acc => { const ret = {}; Object.keys(acc).forEach(key => { const accVal = acc[key]; if ( typeof accVal.update !== 'undefined' && typeof accVal.value !== 'undefined' ) { ret[key] = accVal.value; } else { ret[key] = accVal; } }); return ret; }; const pluck = (acc, val) => { const variableType = getNodeType(val); const attrs = getProperties(val) || {}; if (!attrs.name || !attrs.value) return attrs; const nameValue = attrs.name.value; const valueType = attrs.value.type; const valueValue = attrs.value.value; switch (valueType) { case 'value': acc[nameValue] = valueValue; break; case 'variable': if (context.hasOwnProperty(valueValue)) { acc[nameValue] = context[valueValue]; } else { acc[nameValue] = evalExpression(context, expr); } break; case 'expression': const expr = valueValue; if (variableType === 'var') { acc[nameValue] = evalExpression( Object.assign({}, context, formatAccumulatedValues(acc)), expr ); } else { acc[nameValue] = { value: evalExpression( Object.assign({}, context, formatAccumulatedValues(acc)), expr ), update: (newState, oldState, context = {}) => { return evalExpression( Object.assign({}, oldState, newState, context), expr ); } }; } } return acc; }; return arr.reduce(pluck, {}); }; export const filterIdyllProps = (props, filterInjected) => { const { __vars__, __expr__, idyllASTNode, hasHook, initialState, isHTMLNode, refName, onEnterViewFully, onEnterView, onExitViewFully, onExitView, fullWidth, ...rest } = props; if (filterInjected) { const { idyll, hasError, updateProps, ...ret } = rest; return ret; } return rest; }; export const getData = (arr, datasets = {}) => { const pluck = (acc, val) => { const nameValue = getProperties(val).name.value; const sourceValue = getProperties(val).source.value; const async = getProperties(val).async ? getProperties(val).async.value : false; if (async) { const initialValue = getProperties(val).initialValue ? JSON.parse(getProperties(val).initialValue.value) : []; let dataPromise = new Promise(res => res(initialValue)); if (typeof fetch !== 'undefined') { dataPromise = fetch(sourceValue) .then(res => { if (res.status >= 400) { throw new Error( `Error Status ${ res.status } occurred while fetching data from ${sourceValue}. If you are using a file to load the data and not a url, make sure async is not set to true.` ); } if (sourceValue.endsWith('.csv')) { return res .text() .then(resString => parse(resString, { cast: true, columns: true, skip_empty_lines: true, ltrim: true, rtrim: true }) ) .catch(e => { console.error(`Error while parsing csv: ${e}`); }); } return res.json().catch(e => console.error(e)); }) .catch(e => { console.error(e); }); } else if (typeof window !== 'undefined') { console.warn('Could not find fetch.'); } acc.asyncData[nameValue] = { initialValue, dataPromise }; } else { acc.syncData[nameValue] = datasets[nameValue]; } return acc; }; return arr.reduce(pluck, { syncData: {}, asyncData: {} }); }; export const splitAST = ast => { const state = { vars: [], derived: [], data: [], elements: [] }; const handleNode = storeElements => { return node => { const type = getNodeType(node); const children = getChildren(node); if (type === 'var') { state.vars.push(node); } else if (state[type]) { state[type].push(node); } else if (storeElements) { state.elements.push(node); } if (!children || (children.length === 1 && isTextNode(children[0]))) { return; } children.forEach(handleNode(false)); }; }; ast.forEach(handleNode(true)); return state; }; //Properties that add logic to components for callbacks. export const hooks = [ 'onEnterView', 'onEnterViewFully', 'onExitView', 'onExitViewFully' ]; export const scrollMonitorEvents = { onEnterView: 'enterViewport', onEnterViewFully: 'fullyEnterViewport', onExitView: 'partiallyExitViewport', onExitViewFully: 'exitViewport' }; export const translate = ast => { const attrConvert = (props, node) => { let reducedProps = { idyllASTNode: node }; for (let propName in props) { const name = propName; const type = props[propName].type; const value = props[propName].value; if (type == 'variable') { if (!reducedProps.__vars__) { reducedProps.__vars__ = {}; } reducedProps.__vars__[name] = value; } if (type == 'expression') { if (!reducedProps.__expr__) { reducedProps.__expr__ = {}; } reducedProps.__expr__[name] = value; } if (hooks.includes(name)) { reducedProps.hasHook = true; } reducedProps[name] = value; } return reducedProps; }; const tNode = node => { if (isTextNode(node)) return node; let name = getNodeName(node); let attrs = getProperties(node); if (!attrs) { attrs = {}; } const children = getChildren(node); return { component: name, ...attrConvert(attrs, node), children: children.map(tNode) }; }; return splitAST(getChildren(ast)).elements.map(tNode); }; export const mapTree = (tree, mapFn, filterFn = () => true, depth = 0) => { const walkFn = depth => (acc, node) => { //To check for textnodes if (node.component) { //To check for children if (node.children) { node.children = node.children.reduce(walkFn(depth + 1), []); } } if (filterFn(node)) { acc.push(mapFn(node, depth)); } return acc; }; let value = tree.reduce(walkFn(depth), []); return value; }; export const filterASTForDocument = ast => { return removeNodes(cloneNode(ast), node => getNodeName(node) === 'meta'); }; export const findWrapTargets = (schema, state, components) => { //Custom components const targets = []; //Name of custom components const componentNames = Object.keys(components); componentNames.forEach((component, i) => { let words = component.split('-'); for (let i = 0; i < words.length; i++) { words[i] = words[i].charAt(0).toUpperCase() + words[i].substring(1); } componentNames[i] = words.join('').toLowerCase(); }); //Array of keys for the runtime state passed. const stateKeys = Object.keys(state); //Populating target with the custom components //Walk the whole tree, collect and return the nodes //for wrapping mapTree(schema, node => { if (node.component === 'textnode') { return node; } //Custom components will have hooks attached to them if (node.hasHook) { targets.push(node); return node; } if (node.component) { const checkName = node.component .toLowerCase() .split('-') .join(''); if (componentNames.includes(checkName)) { targets.push(node); return node; } } const { component, children, __vars__, __expr__, ...props } = node; const expressions = Object.keys(__expr__ || {}); const variables = Object.keys(__vars__ || {}); for (let prop in props) { if (variables.includes(prop) || expressions.includes(prop)) { targets.push(node); return node; } } return node; }); return targets; };