UNPKG

idyll-document

Version:

The Idyll runtime, implemented as a React component.

1 lines 105 kB
{"version":3,"file":"index.mjs","sources":["../../src/utils/schema2element.js","../../src/components/placeholder.js","../../src/components/author-tool.js","../../src/utils/index.js","../../src/runtime.js","../../src/index.js"],"sourcesContent":["import React, { createElement } from 'react';\nimport DOM from 'react-dom-factories';\nimport { paramCase, pascalCase } from 'change-case';\n\nconst _componentMap = new WeakMap();\n\nclass ReactJsonSchema {\n constructor(componentMap) {\n if (componentMap) this.setComponentMap(componentMap);\n }\n\n parseSchema(schema) {\n let element = null;\n let elements = null;\n if (Array.isArray(schema)) {\n elements = this.parseSubSchemas(schema);\n } else {\n element = this.createComponent(schema);\n }\n return element || elements;\n }\n\n parseSubSchemas(subSchemas = []) {\n const Components = [];\n let index = 0;\n for (const subSchema of subSchemas) {\n if (typeof subSchema === 'string') {\n Components.push(subSchema);\n } else {\n subSchema.key =\n typeof subSchema.key !== 'undefined' ? subSchema.key : index;\n Components.push(this.parseSchema(subSchema));\n index++;\n }\n }\n return Components;\n }\n\n createComponent(schema) {\n if (schema.type) {\n if (schema.type === 'textnode') return schema.value;\n }\n const { component, children, ...rest } = schema;\n const Component = this.resolveComponent(schema);\n const Children = this.resolveComponentChildren(schema);\n return createElement(Component, rest, Children);\n }\n\n resolveComponent(schema) {\n const componentMap = this.getComponentMap();\n let Component;\n // bail early if there is no component name\n if (!schema.hasOwnProperty('component')) {\n throw new Error(\n 'ReactJsonSchema could not resolve a component due to a missing component attribute in the schema.'\n );\n }\n\n // if it's already a ref bail early\n if (schema.component === Object(schema.component)) {\n return schema.component;\n }\n\n const [name, ...subs] = schema.component.split('.');\n\n // find the def in the provided map\n if (componentMap) {\n Component = componentMap[name];\n if (!Component) Component = componentMap[paramCase(name)];\n if (!Component) Component = componentMap[pascalCase(name)];\n\n for (let i = 0; i < subs.length; i++) {\n Component = Component.default[subs[i]] || Component[subs[i]];\n }\n }\n\n // if still nothing found it's a native DOM component or an error\n if (!Component) {\n if (DOM.hasOwnProperty(name)) {\n Component = schema.component;\n } else {\n console.warn(\n `Could not find an implementation for: ${schema.component}`\n );\n return () => (\n <div style={{ color: 'black', border: 'solid 1px red' }}>\n <pre>Could not find an implementation for: {schema.component}</pre>\n </div>\n );\n }\n }\n\n // if there is a default prop (CommonJS) return that\n return Component.default || Component;\n }\n\n resolveComponentChildren(schema) {\n const children = schema.hasOwnProperty('children')\n ? this.parseSchema(schema.children)\n : [];\n return children.length ? children : undefined;\n }\n\n getComponentMap() {\n return _componentMap.get(this);\n }\n\n setComponentMap(componentMap) {\n _componentMap.set(this, componentMap);\n }\n}\n\nexport default ReactJsonSchema;\n","import React from 'react';\n\nexport const generatePlaceholder = (name) => {\n return class extends React.PureComponent {\n constructor(props) {\n super(props);\n console.warn(`Warning: attempting to use component named ${name}, but it wasn't found`);\n }\n\n render() {\n const { idyll, updateProps, hasError, ...props } = this.props;\n return <div {...props} />;\n }\n }\n}\n","import React from 'react';\nimport ReactTooltip from 'react-tooltip';\n\nclass AuthorTool extends React.PureComponent {\n constructor(props) {\n super(props);\n this.state = {\n isAuthorView: false,\n debugHeight: 0,\n componentHeight: 0,\n hasPressedButton: false\n };\n this.handleClick = this.handleClick.bind(this);\n }\n\n // For all available props in metaValues, display them\n // If runtimeValues has a value for given prop, display it\n // Returns this in a single table row <tr>\n handleTableValues(metaValues, runtimeValues) {\n return metaValues.props.map(prop => {\n const runtimeValue = runtimeValues.props[prop.name];\n let currentPropValue = null;\n if (runtimeValue !== undefined) {\n if (\n runtimeValue &&\n {}.toString.call(runtimeValue) === '[object Function]'\n ) {\n currentPropValue = <em>function</em>;\n } else {\n currentPropValue = runtimeValue;\n }\n }\n return (\n <tr key={JSON.stringify(prop)} className=\"props-table-row\">\n <td>{prop.name}</td>\n <td className=\"props-table-type\">{prop.type}</td>\n <td>{prop.example}</td>\n <td>{currentPropValue}</td>\n </tr>\n );\n });\n }\n\n // Returns authoring information for the prop values in table format\n // and includes a link to the docs page at the bottom\n handleFormatComponent(runtimeValues) {\n const metaValues = runtimeValues.type._idyll;\n const componentName = metaValues.name;\n\n // Docs use lowercase component name for link\n const componentLowerCase =\n componentName.charAt(0).toLowerCase() + componentName.slice(1);\n const componentDocsLink =\n 'https://idyll-lang.org/docs/components/default/' + componentLowerCase;\n\n const showProps = this.handleTableValues(metaValues, runtimeValues);\n const { isAuthorView, debugHeight, componentHeight } = this.state;\n const currentDebugHeight = isAuthorView ? debugHeight : 0;\n const marginToGive = isAuthorView ? 15 : 0;\n // If a component's height is too small, button will overlap with table\n // so add margin to get a minimal height (40px seems fine)\n const marginAboveTable =\n componentHeight < 40 && isAuthorView ? 40 - componentHeight : 0;\n return (\n <div\n className=\"debug-collapse\"\n style={{\n height: currentDebugHeight + 'px',\n marginBottom: marginToGive + 'px',\n marginTop: marginAboveTable + 'px'\n }}\n >\n <div\n className=\"author-component-view\"\n ref={inner => (this.innerHeight = inner)}\n >\n <table className=\"props-table\">\n <tbody>\n <tr className=\"props-table-row\">\n <th>Prop</th>\n <th>Type</th>\n <th>Example</th>\n <th>Current Value</th>\n </tr>\n {showProps}\n </tbody>\n </table>\n <div className=\"icon-links\">\n <a className=\"icon-link\" href={componentDocsLink}>\n <img\n className=\"icon-link-image\"\n src=\"https://raw.githubusercontent.com/google/material-design-icons/master/action/svg/design/ic_description_24px.svg?sanitize=true\"\n />\n </a>\n <a className=\"icon-link\" href={componentDocsLink}>\n <span\n style={{\n fontFamily: 'courier',\n fontSize: '12px',\n marginTop: '8px'\n }}\n >\n docs\n </span>\n </a>\n </div>\n </div>\n </div>\n );\n }\n\n // Flips between whether we are in the author view of a component\n handleClick() {\n this.setState(prevState => ({\n isAuthorView: !prevState.isAuthorView,\n debugHeight: this.innerHeight.getBoundingClientRect().height\n }));\n if (!this.state.hasPressedButton) {\n this.setState({\n componentHeight: this._refContainer.getBoundingClientRect().height,\n hasPressedButton: true\n });\n }\n }\n\n // Returns an entire author view, including the component itself,\n // a quill icon to indicate whether we're hovering in the component,\n // and debugging information when the icon is pressed\n render() {\n const { idyll, updateProps, hasError, ...props } = this.props;\n const addBorder = this.state.isAuthorView\n ? {\n boxShadow: '5px 5px 10px 1px lightGray',\n transition: 'box-shadow 0.35s linear',\n padding: '0px 10px 10px',\n margin: '0px -10px 20px'\n }\n : null;\n const putButtonBack = this.state.isAuthorView\n ? {\n right: '10px',\n top: '3px'\n }\n : null;\n\n return (\n <div\n className=\"component-debug-view\"\n style={addBorder}\n ref={ref => (this._refContainer = ref)}\n >\n {props.component}\n <button\n className=\"author-view-button\"\n style={putButtonBack}\n onClick={this.handleClick}\n data-tip\n data-for={props.uniqueKey}\n />\n <ReactTooltip\n className=\"button-tooltip\"\n id={props.uniqueKey}\n type=\"info\"\n effect=\"solid\"\n place=\"bottom\" // TODO not showing up ?\n disable={this.state.isAuthorView}\n >\n <div className=\"tooltip-header\">\n {props.authorComponent.type._idyll.name} Component\n </div>\n <div className=\"tooltip-subtitle\">Click for more info</div>\n </ReactTooltip>\n {this.handleFormatComponent(props.authorComponent)}\n </div>\n );\n }\n}\n\nexport default AuthorTool;\n","import falafel from 'falafel';\nimport { parse } from 'csv-parse/sync';\n\nconst {\n cloneNode,\n getChildren,\n getNodeName,\n getNodeType,\n getProperties,\n isTextNode,\n removeNodes\n} = require('idyll-ast');\n\nconst isPropertyAccess = node => {\n const index = node.parent.source().indexOf(`.${node.name}`);\n if (index === -1) {\n return false;\n }\n const proxyString = '__idyllStateProxy';\n if (index >= proxyString.length) {\n if (\n node.parent\n .source()\n .substr(index - proxyString.length, proxyString.length) === proxyString\n ) {\n return false;\n }\n }\n return true;\n};\n\nconst isObjectKey = node => {\n return node.parent.type === 'Property' && node.parent.key === node;\n};\n\nexport const buildExpression = (acc, expr, isEventHandler) => {\n let identifiers = [];\n let modifiedExpression = '';\n\n try {\n modifiedExpression = falafel(\n isEventHandler ? expr : `var __idyllReturnValue = ${expr || 'undefined'}`,\n node => {\n switch (node.type) {\n case 'Identifier':\n const skip = isPropertyAccess(node) || isObjectKey(node);\n if (Object.keys(acc).indexOf(node.name) > -1) {\n identifiers.push(node.name);\n if (!skip) {\n node.update('__idyllStateProxy.' + node.source());\n }\n }\n break;\n }\n }\n );\n } catch (e) {\n console.error(e);\n }\n\n if (!isEventHandler) {\n return `\n ((context) => {\n var __idyllStateProxy = new Proxy({}, {\n get: (_, prop) => {\n return context[prop];\n },\n set: (_, prop, value) => {\n console.warn('Warning, trying to set a value in a property expression.');\n }\n });\n ${modifiedExpression};\n return __idyllReturnValue;\n })(this)`;\n }\n\n return `\n ((context) => {\n var __idyllExpressionExecuted = false;\n var __idyllStateProxy = new Proxy({\n ${identifiers\n .map(key => {\n return `${key}: ${\n key !== 'refs'\n ? `context.__idyllCopy(context['${key}'])`\n : `context['${key}']`\n }`;\n })\n .join(', ')}\n }, {\n get: (target, prop) => {\n return target[prop];\n },\n set: (target, prop, value) => {\n if (__idyllExpressionExecuted) {\n var newState = {};\n newState[prop] = value;\n context.__idyllUpdate(newState);\n }\n target[prop] = value;\n return true;\n }\n });\n ${modifiedExpression};\n context.__idyllUpdate({\n ${identifiers\n .filter(key => key !== 'refs')\n .map(key => {\n return `${key}: __idyllStateProxy['${key}']`;\n })\n .join(', ')}\n });\n __idyllExpressionExecuted = true;\n })(this)\n `;\n};\n\nexport const evalExpression = (acc, expr, key, context) => {\n const isEventHandler =\n key && (key.match(/^on[A-Z].*/) || key.match(/^handle[A-Z].*/));\n let e = buildExpression(acc, expr, isEventHandler);\n if (isEventHandler) {\n return function() {\n eval(e);\n }.bind(\n Object.assign({}, acc, context || {}, {\n __idyllCopy: function copy(o) {\n if (typeof o !== 'object') return o;\n var output, v, key;\n output = Array.isArray(o) ? [] : {};\n for (key in o) {\n v = o[key];\n output[key] = typeof v === 'object' ? copy(v) : v;\n }\n return output;\n }\n })\n );\n }\n\n try {\n return function(evalString) {\n try {\n return eval('(' + evalString + ')');\n } catch (err) {\n console.warn('Error occurred in Idyll expression');\n }\n }.call(Object.assign({}, acc), e);\n } catch (err) {}\n};\n\nexport const getVars = (arr, context = {}) => {\n const formatAccumulatedValues = acc => {\n const ret = {};\n Object.keys(acc).forEach(key => {\n const accVal = acc[key];\n if (\n typeof accVal.update !== 'undefined' &&\n typeof accVal.value !== 'undefined'\n ) {\n ret[key] = accVal.value;\n } else {\n ret[key] = accVal;\n }\n });\n return ret;\n };\n\n const pluck = (acc, val) => {\n const variableType = getNodeType(val);\n const attrs = getProperties(val) || {};\n\n if (!attrs.name || !attrs.value) return attrs;\n\n const nameValue = attrs.name.value;\n const valueType = attrs.value.type;\n const valueValue = attrs.value.value;\n\n switch (valueType) {\n case 'value':\n acc[nameValue] = valueValue;\n break;\n case 'variable':\n if (context.hasOwnProperty(valueValue)) {\n acc[nameValue] = context[valueValue];\n } else {\n acc[nameValue] = evalExpression(context, expr);\n }\n break;\n case 'expression':\n const expr = valueValue;\n if (variableType === 'var') {\n acc[nameValue] = evalExpression(\n Object.assign({}, context, formatAccumulatedValues(acc)),\n expr\n );\n } else {\n acc[nameValue] = {\n value: evalExpression(\n Object.assign({}, context, formatAccumulatedValues(acc)),\n expr\n ),\n update: (newState, oldState, context = {}) => {\n return evalExpression(\n Object.assign({}, oldState, newState, context),\n expr\n );\n }\n };\n }\n }\n return acc;\n };\n\n return arr.reduce(pluck, {});\n};\n\nexport const filterIdyllProps = (props, filterInjected) => {\n const {\n __vars__,\n __expr__,\n idyllASTNode,\n hasHook,\n initialState,\n isHTMLNode,\n refName,\n onEnterViewFully,\n onEnterView,\n onExitViewFully,\n onExitView,\n fullWidth,\n ...rest\n } = props;\n if (filterInjected) {\n const { idyll, hasError, updateProps, ...ret } = rest;\n return ret;\n }\n return rest;\n};\nexport const getData = (arr, datasets = {}) => {\n const pluck = (acc, val) => {\n const nameValue = getProperties(val).name.value;\n const sourceValue = getProperties(val).source.value;\n const async = getProperties(val).async\n ? getProperties(val).async.value\n : false;\n if (async) {\n const initialValue = getProperties(val).initialValue\n ? JSON.parse(getProperties(val).initialValue.value)\n : [];\n\n let dataPromise = new Promise(res => res(initialValue));\n\n if (typeof fetch !== 'undefined') {\n dataPromise = fetch(sourceValue)\n .then(res => {\n if (res.status >= 400) {\n throw new Error(\n `Error Status ${\n res.status\n } 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.`\n );\n }\n if (sourceValue.endsWith('.csv')) {\n return res\n .text()\n .then(resString =>\n parse(resString, {\n cast: true,\n columns: true,\n skip_empty_lines: true,\n ltrim: true,\n rtrim: true\n })\n )\n .catch(e => {\n console.error(`Error while parsing csv: ${e}`);\n });\n }\n return res.json().catch(e => console.error(e));\n })\n .catch(e => {\n console.error(e);\n });\n } else if (typeof window !== 'undefined') {\n console.warn('Could not find fetch.');\n }\n acc.asyncData[nameValue] = {\n initialValue,\n dataPromise\n };\n } else {\n acc.syncData[nameValue] = datasets[nameValue];\n }\n\n return acc;\n };\n\n return arr.reduce(pluck, { syncData: {}, asyncData: {} });\n};\n\nexport const splitAST = ast => {\n const state = {\n vars: [],\n derived: [],\n data: [],\n elements: []\n };\n\n const handleNode = storeElements => {\n return node => {\n const type = getNodeType(node);\n const children = getChildren(node);\n if (type === 'var') {\n state.vars.push(node);\n } else if (state[type]) {\n state[type].push(node);\n } else if (storeElements) {\n state.elements.push(node);\n }\n if (!children || (children.length === 1 && isTextNode(children[0]))) {\n return;\n }\n children.forEach(handleNode(false));\n };\n };\n\n ast.forEach(handleNode(true));\n return state;\n};\n\n//Properties that add logic to components for callbacks.\nexport const hooks = [\n 'onEnterView',\n 'onEnterViewFully',\n 'onExitView',\n 'onExitViewFully'\n];\n\nexport const scrollMonitorEvents = {\n onEnterView: 'enterViewport',\n onEnterViewFully: 'fullyEnterViewport',\n onExitView: 'partiallyExitViewport',\n onExitViewFully: 'exitViewport'\n};\n\nexport const translate = ast => {\n const attrConvert = (props, node) => {\n let reducedProps = {\n idyllASTNode: node\n };\n for (let propName in props) {\n const name = propName;\n const type = props[propName].type;\n const value = props[propName].value;\n if (type == 'variable') {\n if (!reducedProps.__vars__) {\n reducedProps.__vars__ = {};\n }\n reducedProps.__vars__[name] = value;\n }\n if (type == 'expression') {\n if (!reducedProps.__expr__) {\n reducedProps.__expr__ = {};\n }\n reducedProps.__expr__[name] = value;\n }\n\n if (hooks.includes(name)) {\n reducedProps.hasHook = true;\n }\n\n reducedProps[name] = value;\n }\n return reducedProps;\n };\n const tNode = node => {\n if (isTextNode(node)) return node;\n\n let name = getNodeName(node);\n\n let attrs = getProperties(node);\n if (!attrs) {\n attrs = {};\n }\n const children = getChildren(node);\n return {\n component: name,\n ...attrConvert(attrs, node),\n children: children.map(tNode)\n };\n };\n\n return splitAST(getChildren(ast)).elements.map(tNode);\n};\n\nexport const mapTree = (tree, mapFn, filterFn = () => true, depth = 0) => {\n const walkFn = depth => (acc, node) => {\n //To check for textnodes\n if (node.component) {\n //To check for children\n if (node.children) {\n node.children = node.children.reduce(walkFn(depth + 1), []);\n }\n }\n\n if (filterFn(node)) {\n acc.push(mapFn(node, depth));\n }\n return acc;\n };\n let value = tree.reduce(walkFn(depth), []);\n return value;\n};\n\nexport const filterASTForDocument = ast => {\n return removeNodes(cloneNode(ast), node => getNodeName(node) === 'meta');\n};\n\nexport const findWrapTargets = (schema, state, components) => {\n //Custom components\n const targets = [];\n //Name of custom components\n const componentNames = Object.keys(components);\n\n componentNames.forEach((component, i) => {\n let words = component.split('-');\n for (let i = 0; i < words.length; i++) {\n words[i] = words[i].charAt(0).toUpperCase() + words[i].substring(1);\n }\n componentNames[i] = words.join('').toLowerCase();\n });\n\n //Array of keys for the runtime state passed.\n const stateKeys = Object.keys(state);\n\n //Populating target with the custom components\n //Walk the whole tree, collect and return the nodes\n //for wrapping\n mapTree(schema, node => {\n if (node.component === 'textnode') {\n return node;\n }\n\n //Custom components will have hooks attached to them\n if (node.hasHook) {\n targets.push(node);\n return node;\n }\n\n if (node.component) {\n const checkName = node.component\n .toLowerCase()\n .split('-')\n .join('');\n if (componentNames.includes(checkName)) {\n targets.push(node);\n return node;\n }\n }\n\n const { component, children, __vars__, __expr__, ...props } = node;\n\n const expressions = Object.keys(__expr__ || {});\n const variables = Object.keys(__vars__ || {});\n\n for (let prop in props) {\n if (variables.includes(prop) || expressions.includes(prop)) {\n targets.push(node);\n return node;\n }\n }\n return node;\n });\n\n return targets;\n};\n","import React from 'react';\nimport ReactDOM from 'react-dom';\nimport scrollparent from 'scrollparent';\nimport scrollMonitor from 'scrollmonitor';\nimport ReactJsonSchema from './utils/schema2element';\nimport entries from 'object.entries';\nimport values from 'object.values';\nimport { generatePlaceholder } from './components/placeholder';\nimport AuthorTool from './components/author-tool';\nimport { getChildren } from 'idyll-ast';\nimport equal from 'fast-deep-equal';\n\nimport * as layouts from 'idyll-layouts';\nimport * as themes from 'idyll-themes';\n\nimport {\n getData,\n getVars,\n filterASTForDocument,\n splitAST,\n translate,\n findWrapTargets,\n filterIdyllProps,\n mapTree,\n evalExpression,\n hooks,\n scrollMonitorEvents\n} from './utils';\n\nconst updatePropsCallbacks = [];\nconst updateRefsCallbacks = [];\nconst scrollWatchers = [];\nconst scrollOffsets = {};\nconst refCache = {};\nconst evalContext = {};\nlet scrollContainer;\n\nconst getLayout = layout => {\n return layouts[layout.trim()] || {};\n};\n\nconst getTheme = theme => {\n return themes[theme.trim()] || {};\n};\n\nconst getRefs = () => {\n const refs = {};\n if (!scrollContainer) {\n return refCache;\n }\n\n scrollWatchers.forEach(watcher => {\n // left and right props assume no horizontal scrolling\n const {\n watchItem,\n callbacks,\n container,\n recalculateLocation,\n offsets,\n ...watcherProps\n } = watcher;\n refs[watchItem.dataset.ref] = {\n ...watcherProps,\n ...refCache[watchItem.dataset.ref],\n domNode: watchItem\n };\n });\n\n return { ...refCache, ...refs };\n};\n\nlet wrapperKey = 0;\nconst createWrapper = ({\n theme,\n layout,\n authorView,\n textEditComponent,\n userViewComponent,\n userInlineViewComponent,\n wrapTextComponents\n}) => {\n return class Wrapper extends React.PureComponent {\n constructor(props) {\n super(props);\n\n this.key = wrapperKey++;\n this.ref = {};\n this.onUpdateRefs = this.onUpdateRefs.bind(this);\n this.onUpdateProps = this.onUpdateProps.bind(this);\n\n const vars = values(props.__vars__);\n const exps = values(props.__expr__);\n\n this.usesRefs = exps.some(v => v && v.includes('refs.'));\n\n this.state = { hasError: false, error: null };\n\n // listen for props updates IF we care about them\n if (vars.length || exps.length) {\n // called with new doc state\n // when any component calls updateProps()\n updatePropsCallbacks.push(this.onUpdateProps);\n this.state = this.onUpdateProps(\n props.initialState,\n Object.keys(props),\n true\n );\n }\n\n // listen for ref updates IF we care about them\n if (props.hasHook || this.usesRefs) {\n updateRefsCallbacks.push(this.onUpdateRefs);\n }\n }\n\n componentDidCatch(error, info) {\n this.setState({ hasError: true, error: error });\n }\n\n onUpdateProps(newState, changedKeys, initialRender) {\n const { __vars__, __expr__ } = this.props;\n\n // were there changes to any vars we track?\n // or vars our expressions reference?\n const shouldUpdate =\n initialRender ||\n changedKeys.some(k => {\n return (\n values(__vars__).includes(k) ||\n values(__expr__).some(expr => expr.includes(k))\n );\n });\n // if nothing we care about changed bail out and don't re-render\n if (!shouldUpdate) return;\n\n // update this component's state\n const nextState = {};\n // pull in the latest value for any tracked vars\n Object.keys(__vars__).forEach(key => {\n nextState[key] = newState[__vars__[key]];\n });\n // re-run this component's expressions using the latest doc state\n Object.keys(__expr__).forEach(key => {\n nextState[key] = evalExpression(\n { ...newState, refs: getRefs() },\n __expr__[key],\n key,\n evalContext\n );\n });\n\n if (initialRender) {\n return Object.assign({ hasError: false }, nextState);\n }\n // trigger a re-render of this component\n // and more importantly, its wrapped component\n this.setState(Object.assign({ hasError: false, error: null }, nextState));\n }\n\n onUpdateRefs(newState) {\n const { __expr__ } = this.props;\n\n if (this.usesRefs) {\n const nextState = { refs: newState.refs };\n entries(__expr__).forEach(([key, val]) => {\n if (!val.includes('refs.')) {\n return;\n }\n nextState[key] = evalExpression(newState, val, key, evalContext);\n });\n\n // trigger a render with latest state\n this.setState(nextState);\n }\n }\n\n componentWillUnmount() {\n const propsIndex = updatePropsCallbacks.indexOf(this.onUpdateProps);\n if (propsIndex > -1) updatePropsCallbacks.splice(propsIndex, 1);\n\n const refsIndex = updateRefsCallbacks.indexOf(this.onUpdateRefs);\n if (refsIndex > -1) updateRefsCallbacks.splice(refsIndex, 1);\n }\n\n render() {\n const state = filterIdyllProps(this.state, this.props.isHTMLNode);\n const { children, ...passThruProps } = filterIdyllProps(\n this.props,\n this.props.isHTMLNode\n );\n let childComponent = null;\n let uniqueKey = `${this.key}-help`;\n let returnComponent = React.Children.map(children, (c, i) => {\n childComponent = c;\n return React.cloneElement(c, {\n key: `${this.key}-${i}`,\n idyll: {\n theme: getTheme(theme),\n layout: getLayout(layout),\n authorView: authorView\n },\n ...state,\n ...passThruProps\n });\n });\n if (this.state.hasError) {\n returnComponent = (\n <div style={{ border: 'solid red 1px', padding: 10 }}>\n {this.state.error.message}\n </div>\n );\n }\n const metaData = childComponent.type._idyll;\n if (authorView) {\n // ensure inline elements do not have this overlay\n if (\n (metaData && metaData.name === 'TextContainer') ||\n ['TextContainer', 'DragDropContainer'].includes(\n childComponent.type.name\n )\n ) {\n return returnComponent;\n } else if (\n textEditComponent &&\n metaData &&\n wrapTextComponents.includes(metaData.name.toLowerCase())\n ) {\n const ViewComponent = textEditComponent;\n return (\n <ViewComponent idyllASTNode={this.props.idyllASTNode}>\n {childComponent}\n </ViewComponent>\n );\n } else if (\n !metaData ||\n metaData.displayType === undefined ||\n metaData.displayType !== 'inline'\n ) {\n const ViewComponent = userViewComponent || AuthorTool;\n return (\n <ViewComponent\n idyllASTNode={this.props.idyllASTNode}\n component={returnComponent}\n authorComponent={childComponent}\n uniqueKey={uniqueKey}\n />\n );\n } else if (metaData.displayType === 'inline') {\n const InlineViewComponent =\n userInlineViewComponent || userViewComponent || AuthorTool;\n return (\n <InlineViewComponent\n idyllASTNode={this.props.idyllASTNode}\n component={returnComponent}\n authorComponent={childComponent}\n uniqueKey={uniqueKey}\n />\n );\n }\n }\n return returnComponent;\n }\n };\n};\n\nconst getDerivedValues = dVars => {\n const o = {};\n Object.keys(dVars).forEach(key => (o[key] = dVars[key].value));\n return o;\n};\n\nclass IdyllRuntime extends React.PureComponent {\n constructor(props) {\n super(props);\n this.state = {};\n this.scrollListener = this.scrollListener.bind(this);\n this.initScrollListener = this.initScrollListener.bind(this);\n const ast = filterASTForDocument(props.ast);\n\n const { vars, derived, data, elements } = splitAST(getChildren(ast));\n const Wrapper = createWrapper({\n theme: props.theme,\n layout: props.layout,\n authorView: props.authorView,\n textEditComponent: props.textEditComponent,\n userViewComponent: props.userViewComponent,\n userInlineViewComponent: props.userInlineViewComponent,\n wrapTextComponents: props.wrapTextComponents\n });\n\n let hasInitialized = false;\n let initialContext = {};\n // Initialize a custom context\n let _initializeCallbacks = [];\n let _mountCallbacks = [];\n let _updateCallbacks = [];\n\n this._onInitializeState = () => {\n _initializeCallbacks.forEach(cb => {\n cb();\n });\n };\n this._onMount = () => {\n _mountCallbacks.forEach(cb => {\n cb();\n });\n };\n this._onUpdateState = newData => {\n _updateCallbacks.forEach(cb => {\n cb(newData);\n });\n };\n if (typeof props.context === 'function') {\n props.context({\n update: newState => {\n if (!hasInitialized) {\n initialContext = Object.assign(initialContext, newState);\n } else {\n this.updateState(newState);\n }\n },\n data: () => {\n return this.state;\n },\n onInitialize: cb => {\n _initializeCallbacks.push(cb);\n },\n onMount: cb => {\n _mountCallbacks.push(cb);\n },\n onUpdate: cb => {\n _updateCallbacks.push(cb);\n }\n });\n }\n\n const dataStore = getData(data, props.datasets);\n const initialState = Object.assign(\n {},\n {\n ...getVars(vars, initialContext),\n ...dataStore.syncData\n },\n initialContext,\n props.initialState ? props.initialState : {}\n );\n\n const { asyncData: asyncDataStore } = dataStore;\n const asyncDataStoreKeys = Object.keys(asyncDataStore);\n asyncDataStoreKeys.forEach(key => {\n this.state[key] = asyncDataStore[key].initialValue;\n });\n\n asyncDataStoreKeys.map(key => {\n asyncDataStore[key].dataPromise\n .then(res => {\n this.updateState({\n ...this.state,\n [key]: res\n });\n })\n .catch(e => console.error('Error while resolving the data' + e));\n });\n const derivedVars = (this.derivedVars = getVars(derived, initialState));\n\n let state = (this.state = {\n ...this.state,\n ...initialState,\n ...getDerivedValues(derivedVars)\n });\n\n this.updateState = newState => {\n // merge new doc state with old\n const newMergedState = { ...this.state, ...newState };\n // update derived values\n const newDerivedValues = getDerivedValues(\n getVars(derived, newMergedState)\n );\n const nextState = { ...newMergedState, ...newDerivedValues };\n\n const changedMap = {};\n const changedKeys = Object.keys(state).reduce((acc, k) => {\n if (!equal(state[k], nextState[k])) {\n acc.push(k);\n changedMap[k] = nextState[k] || state[k];\n }\n return acc;\n }, []);\n\n // Update doc state reference.\n // We re-use the same object here so that\n // IdyllRuntime.state can be accurately checked in tests\n state = Object.assign(state, nextState);\n // pass the new doc state to all listeners aka component wrappers\n updatePropsCallbacks.forEach(f => f(state, changedKeys));\n\n changedKeys.length &&\n this._onUpdateState &&\n this._onUpdateState(changedMap);\n };\n\n evalContext.__idyllUpdate = this.updateState;\n hasInitialized = true;\n this._onInitializeState && this._onInitializeState();\n\n // Put these in to avoid hard errors if people are on the latest\n // CLI but haven't updated their local default components\n const fallbackComponents = {\n 'text-container': generatePlaceholder('TextContainer'),\n 'full-width': generatePlaceholder('FullWidth')\n };\n\n // Components that the Document needs to function properly\n const internalComponents = {\n Wrapper\n };\n\n Object.keys(internalComponents).forEach(key => {\n if (props.components[key]) {\n console.warn(\n `Warning! You are including a component named ${key}, but this is a reserved Idyll component. Please rename your component.`\n );\n }\n });\n\n const components = Object.assign(\n fallbackComponents,\n props.components,\n internalComponents\n );\n\n const rjs = new ReactJsonSchema(components);\n const schema = translate(ast);\n const wrapTargets = findWrapTargets(schema, this.state, props.components);\n let refCounter = 0;\n const transformedSchema = mapTree(schema, (node, depth) => {\n // console.log('mapoing ', node.component || node.type);\n if (!node.component) {\n if (node.type && node.type === 'textnode') return node.value;\n }\n\n // transform refs from strings to functions and store them\n if (node.ref || node.hasHook) {\n node.refName = node.ref || node.component + (refCounter++).toString();\n node.ref = el => {\n if (!el) return;\n const domNode = ReactDOM.findDOMNode(el);\n domNode.dataset.ref = node.refName;\n scrollOffsets[node.refName] = node.scrollOffset || 0;\n refCache[node.refName] = {\n props: node,\n domNode: domNode,\n component: el\n };\n };\n refCache[node.refName] = {\n props: node,\n domNode: null\n };\n }\n //Inspect for isHTMLNode props and to check for dynamic components.\n if (!wrapTargets.includes(node)) {\n if (\n this.props.wrapTextComponents.indexOf(node.component) > -1 &&\n this.props.textEditComponent\n ) {\n const { idyllASTNode, ...rest } = node;\n return {\n component: this.props.textEditComponent,\n idyllASTNode: idyllASTNode,\n children: [rest]\n };\n }\n\n // Don't include the AST node reference on unwrapped components\n const { idyllASTNode, ...rest } = node;\n return rest;\n }\n let {\n component,\n children,\n idyllASTNode,\n key,\n __vars__,\n __expr__,\n ...props // actual component props\n } = node;\n __vars__ = __vars__ || {};\n __expr__ = __expr__ || {};\n\n // assign the initial values for tracked vars and expressions\n Object.keys(props).forEach(k => {\n if (__vars__[k]) {\n node[k] = state[__vars__[k]];\n }\n if (__expr__[k] !== undefined) {\n if (hooks.indexOf(k) > -1) {\n return;\n }\n node[k] = evalExpression(\n { ...state, refs: getRefs() },\n __expr__[k],\n k,\n evalContext\n );\n }\n });\n const resolvedComponent = rjs.resolveComponent(node);\n const isHTMLNode = typeof resolvedComponent === 'string';\n\n return {\n component: Wrapper,\n __vars__,\n __expr__,\n idyllASTNode,\n isHTMLNode: isHTMLNode,\n hasHook: node.hasHook,\n refName: node.refName,\n initialState: this.state,\n updateProps: newProps => {\n // init new doc state object\n const newState = {};\n // iterate over passed in updates\n Object.keys(newProps).forEach(k => {\n // if a tracked var was updated get its new value\n if (__vars__[k]) {\n newState[__vars__[k]] = newProps[k];\n }\n });\n this.updateState(newState);\n },\n children: [filterIdyllProps(node, isHTMLNode)]\n };\n });\n\n this.kids = rjs.parseSchema(transformedSchema);\n }\n\n scrollListener() {\n const refs = getRefs();\n updateRefsCallbacks.forEach(f => f({ ...this.state, refs }));\n }\n\n initScrollListener(el) {\n if (!el) return;\n\n let scroller = scrollparent(el);\n if (\n scroller === document.documentElement ||\n scroller === document.body ||\n scroller === window\n ) {\n scroller = window;\n scrollContainer = scrollMonitor;\n } else {\n scrollContainer = scrollMonitor.createContainer(scroller);\n }\n Object.keys(refCache).forEach(key => {\n const { props, domNode } = refCache[key];\n const watcher = scrollContainer.create(domNode, scrollOffsets[key]);\n hooks.forEach(hook => {\n if (props[hook]) {\n watcher[scrollMonitorEvents[hook]](() => {\n evalExpression(\n { ...this.state, refs: getRefs() },\n props[hook],\n hook,\n evalContext\n )();\n });\n }\n });\n scrollWatchers.push(watcher);\n });\n scroller.addEventListener('scroll', this.scrollListener);\n }\n\n updateDerivedVars(newState) {\n const context = {};\n Object.keys(this.derivedVars).forEach(dv => {\n this.derivedVars[dv].value = this.derivedVars[dv].update(\n newState,\n this.state,\n context\n );\n context[dv] = this.derivedVars[dv].value;\n });\n }\n\n getDerivedVars() {\n let dvs = {};\n Object.keys(this.derivedVars).forEach(dv => {\n dvs[dv] = this.derivedVars[dv].value;\n });\n return dvs;\n }\n\n componentDidMount() {\n const refs = getRefs();\n updateRefsCallbacks.forEach(f => f({ ...this.state, refs }));\n this._onMount && this._onMount();\n }\n\n render() {\n return (\n <div className=\"idyll-root\" ref={this.initScrollListener}>\n {this.kids}\n </div>\n );\n }\n}\n\nIdyllRuntime.defaultProps = {\n layout: 'blog',\n theme: 'github',\n authorView: false,\n insertStyles: false,\n wrapTextComponents: [\n 'p',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'ul',\n 'ol',\n 'pre',\n 'CodeHighlight'\n ]\n};\n\nexport default IdyllRuntime;\n","import React from 'react';\nimport Runtime from './runtime';\nimport compile from 'idyll-compiler';\nimport * as layouts from 'idyll-layouts';\nimport * as themes from 'idyll-themes';\n\nconst getLayout = layout => {\n return layouts[layout.trim()] || {};\n};\n\nconst getTheme = theme => {\n return themes[theme.trim()] || {};\n};\n\nlet layoutNode;\nlet themeNode;\n\nconst defaultAST = {\n id: 0,\n type: 'component',\n name: 'root'\n};\n\nconst hashCode = str => {\n var hash = 0,\n i,\n chr;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n chr = str.charCodeAt(i);\n hash = (hash << 5) - hash + chr;\n hash |= 0; // Convert to 32bit integer\n }\n return hash;\n};\n\nclass IdyllDocument extends React.Component {\n constructor(props) {\n super(props);\n this.state = {\n ast: props.ast || defaultAST,\n previousAST: props.ast || defaultAST,\n hash: props.ast ? JSON.stringify(props.ast) : '',\n error: null\n };\n }\n\n createStyleNode(str) {\n var node = document.createElement('style');\n node.innerHTML = str;\n document.body.appendChild(node);\n return node;\n }\n\n componentDidMount() {\n if (!this.props.ast && this.props.markup) {\n compile(this.props.markup, this.props.compilerOptions).then(ast => {\n this.setState({ ast, hash: hashCode(this.props.markup), error: null });\n });\n }\n\n if (this.props.injectThemeCSS) {\n if (themeNode) {\n themeNode.innerHTML = getTheme(this.props.theme).styles;\n } else {\n themeNode = this.createStyleNode(getTheme(this.props.theme).styles);\n }\n }\n if (this.props.injectLayoutCSS) {\n if (layoutNode) {\n layoutNode.innerHTML = getLayout(this.props.layout).styles;\n } else {\n layoutNode = this.createStyleNode(getLayout(this.props.layout).styles);\n }\n }\n }\n\n componentDidCatch(error, info) {\n this.props.onError && this.props.onError(error);\n this.setState({ error: error.message });\n }\n\n UNSAFE_componentWillReceiveProps(newProps) {\n if (newProps.theme !== this.props.theme && newProps.injectThemeCSS) {\n if (themeNode) {\n themeNode.innerHTML = getTheme(newProps.theme).styles;\n } else {\n themeNode = this.createStyleNode(getTheme(newProps.theme).styles);\n }\n }\n if (newProps.layout !== this.props.layout && newProps.injectLayoutCSS) {\n if (layoutNode) {\n layoutNode.innerHTML = getLayout(newProps.layout).styles;\n } else {\n layoutNode = this.createStyleNode(getLayout(newProps.layout).styles);\n }\n }\n\n if (newProps.ast) {\n this.setState({ hash: JSON.stringify(newProps.ast) });\n return;\n }\n\n const hash = hashCode(newProps.markup);\n if (hash !== this.state.hash) {\n this.setState({ previousAST: this.state.ast });\n compile(newProps.markup, newProps.compilerOptions)\n .then(ast => {\n this.setState({ previousAST: ast, ast, hash, error: null });\n })\n .catch(this.componentDidCatch.bind(this));\n }\n }\n\n getErrorComponent() {\n if (!this.state.error) {\n return null;\n }\n return React.createElement(\n this.props.errorComponent || 'pre',\n {\n className: 'idyll-document-error'\n },\n this.state.error\n );\n }\n\n render() {\n return (\n <div>\n <Runtime\n {...this.props}\n key={this.state.hash}\n context={context => {\n this.idyllContext = context;\n typeof this.props.context === 'function' &&\n this.props.context(context);\n }}\n initialState={\n this.props.initialState ||\n (this.idyllContext ? this.idyllContext.data() : {})\n }\n ast={this.props.ast || this.state.ast}\n />\n {this.getErrorComponent()}\n </div>\n );\n }\n}\n\nexport default IdyllDocument;\n"],"names":["_componentMap","WeakMap","ReactJsonSchema","componentMap","setComponentMap","schema","element","elements","Array","isArray","parseSubSchemas","createComponent","subSchemas","Components","index","subSchema","push","key","parseSchema","type","value","component","children","rest","_excluded","Component","resolveComponent","Children","resolveComponentChildren","createElement","getComponentMap","hasOwnProperty","Error","Object","split","name","subs","paramCase","pascalCase","i","length","DOM","console","warn","color","border","undefined","get","set","generatePlaceholder","_createSuper","props","idyll","updateProps","hasError","React","PureComponent","AuthorTool","state","isAuthorView","debugHeight","componentHeight","hasPressedButton","handleClick","bind","metaValues","runtimeValues","map","prop","runtimeValue","currentPropValue","toString","call","JSON","stringify","example","_idyll","componentName","componentLowerCase","charAt","toLowerCase","slice","componentDocsLink","showProps","handleTableValues","currentDebugHeight","marginToGive","marginAboveTable","height","marginBottom","marginTop","inner","innerHeight","fontFamily","fontSize","setState","prevState","getBoundingClientRect","_refContainer","addBorder","boxShadow","transition","padding","margin","putButtonBack","right","top","ref","uniqueKey","authorComponent","handleFormatComponent","require","cloneNode","getChildren","getNodeName","getNodeType","getProperties","isTextNode","removeNodes","isPropertyAccess","node","parent","source","indexOf","proxyString","substr","isObjectKey","buildExpression","acc","expr","isEventHandler","identifiers","modifiedExpression","falafel","skip","keys","update","e","error","join","filter","evalExpression","context","match","eval","assign","__idyllCopy","copy","o","output","v","evalString","err","getVars","arr","formatAccumulatedValues","ret","forEach","accVal","pluck","val","variableType","attrs","nameValue","valueType","valueValue","newState","oldState","reduce","filterIdyllProps","filterInjected","__vars__","__expr__","idyllASTNode","hasHook","initialState","isHTMLNode","refName","onEnterViewFully","onEnterView","onExitViewFully","onExitView","fullWidth","_excluded2","getData","datasets","sourceValue","async","initialValue","parse","dataPromise","Promise","res","fetch","then","status","endsWith","text","resString","cast","columns","skip_empty_lines","ltrim","rtrim","json","window","asyncData","syncData","splitAST","ast","vars","derived","data","handleNode","storeElements","hooks","scrollMonitorEvents","translate","attrConvert","reducedProps","propName","includes","tNode","_objectSpread","mapTree","tree","mapFn","filterFn","depth","walkFn","filterASTForDocument","findWrapTargets","components","targets","componentNames","words","toUpperCase","substring","checkName","_excluded3","expressions","variables","updatePropsCallbacks","updateRefsCallbacks","scrollWatchers","scrollOffsets","refCache","evalContext","scrollContainer","getLayout","layout","layouts","trim","getTheme","theme","themes","getRefs","refs","watcher","watchItem","callbacks","container","recalculateLocation","offsets","watcherProps","dataset","domNode","wrapperKey","createWrapper","authorView","textEditComponent","userViewComponent","userInlineViewComponent","wrapTextComponents","onUpdateRefs","onUpdateProps","values","exps","usesRefs","some","info","changedKeys","initialRender","shouldUpdate","k","nextState","entries","propsIndex","splice","refsIndex","passThruProps","childComponent","returnComponent","c","cloneElement","message","metaData","ViewComponent","displayType","InlineViewComponent","getDerivedValues","dVars","IdyllRuntime","scrollListener","initScrollListener","Wrapper","hasInitialized","initialContext","_initializeCallbacks","_mountCallbacks","_updateCallbacks","_onInitializeState","cb","_onMount","_onUpdateState","newData","updateState","onInitialize","onMount","onUpdate","dataStore","asyncDataStore","asyncDataStoreKeys","derivedVars","newMergedState","newDerivedValues","changedMap","equal","f","__idyllUpdate","fallbackComponents","internalComponents","rjs","wrapTargets","refCounter","transformedSchema","el","ReactDOM","findDOMNode","scrollOffset","resolvedComponent","newProps","kids","scroller","scrollparent","document","documentElement","body","scrollMonitor","createContainer","create","hook","addEventListener","dv","dvs","defaultProps","insertStyles","layoutNode","themeNode","defaultAST","id","hashCode","str","hash","chr","charCodeAt","IdyllDocument","previousAST","innerHTML","appendChild","markup","compile","compilerOptions","injectThemeCSS","styles","createStyleNode","injectLayoutCSS","onError","componentDidCatch","errorComponent","className","Runtime","idyllContext","getErrorComponent"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,IAAMA,aAAa,GAAG,IAAIC,OAAJ,EAAtB,CAAA;;IAEMC;AACJ,EAAA