refactor: type rendering

This commit is contained in:
cha0s 2020-06-21 00:50:33 -05:00
parent c8ddacd5dd
commit 0c8ec0b1d2
18 changed files with 264 additions and 243 deletions

View File

@ -1,10 +1,23 @@
import {hot} from 'react-hot-loader'; import {hot} from 'react-hot-loader';
import React from 'react'; import React, {useState} from 'react';
import TypeRenderersContext from '~/client/context/typeRenderers';
import {all as allTypeRenderers} from '~/client/type-renderers.scwp';
import Entity from './entity'; import Entity from './entity';
const Persea = () => ( const typeRenderMap = () => Object.values(allTypeRenderers()).reduce((r, M) => {
<Entity /> const {default: {type, Component}} = M;
); return {...r, [type]: Component};
}, {});
const Persea = () => {
const [typeRenderers] = useState(typeRenderMap());
return (
<TypeRenderersContext.Provider value={typeRenderers}>
<Entity />
</TypeRenderersContext.Provider>
);
};
export default hot(module)(Persea); export default hot(module)(Persea);

View File

@ -15,7 +15,7 @@ import {
import {createSelector} from '@reduxjs/toolkit'; import {createSelector} from '@reduxjs/toolkit';
import {deregisterHooks, registerHooks} from 'scwp'; import {deregisterHooks, registerHooks} from 'scwp';
import {all as allTypeRenderers} from './types/type-renderers.scwp'; import Value from './types/value.type-renderer';
const SCROLL_MAG = 80; const SCROLL_MAG = 80;
@ -43,20 +43,10 @@ const ensureTraitComponents = () => {
}, {}); }, {});
} }
}; };
let TypeRenderers;
const ensureTypeRenderers = () => {
if (!TypeRenderers) {
TypeRenderers = Object.values(allTypeRenderers()).reduce((r, M) => {
const {default: {type, Component}} = M;
return {...r, [type]: Component};
}, {});
}
};
const makeTabSelector = (context, type) => createSelector( const makeTabSelector = (context, type) => createSelector(
(_) => _, (_) => _,
(trait) => { (trait) => {
ensureTypeRenderers();
ensureTraitComponents(); ensureTraitComponents();
const {params: paramsRaw, state: stateRaw} = trait; const {params: paramsRaw, state: stateRaw} = trait;
const {[type]: TraitComponent} = TraitComponents; const {[type]: TraitComponent} = TraitComponents;
@ -68,27 +58,19 @@ const makeTabSelector = (context, type) => createSelector(
const renderComponents = (description, values) => ( const renderComponents = (description, values) => (
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
Object.values(mapObject(description, (description, key) => { Object.values(mapObject(description, (description, key) => {
const {label, options, type: componentType} = description; const {label, options} = description;
const Component = TypeRenderers[componentType];
return ( return (
<label> <label key={key}>
<span className="text"> <span className="text">
{label} {label}
: :
</span> </span>
<div className="invisible-separator" /> <div className="invisible-separator" />
{ <Value.Component
Component context={context}
? ( options={options}
<Component value={values[key]}
context={context} />
key={key}
options={options}
value={values[key]}
/>
)
: <div className="unrenderable" title="No renderer found!">?</div>
}
</label> </label>
); );
})) }))
@ -187,6 +169,7 @@ const Traits = (props) => {
}; };
Traits.propTypes = { Traits.propTypes = {
context: PropTypes.shape({}).isRequired,
traits: PropTypes.shape({}).isRequired, traits: PropTypes.shape({}).isRequired,
}; };

View File

@ -19,13 +19,10 @@ const Actions = ({
{ {
value && value.traversals.length > 0 && ( value && value.traversals.length > 0 && (
value.traversals.map( value.traversals.map(
(traversal) => ( (traversal, i) => (
<li> // eslint-disable-next-line react/no-array-index-key
<Value.Component <li key={i}>
context={context} <Value.Component context={context} value={traversal} />
type="any"
value={traversal}
/>
</li> </li>
), ),
) )

View File

@ -11,6 +11,6 @@
text-align-last: center; text-align-last: center;
} }
.operand .steps { .operand .traversal {
display: flex; display: flex;
} }

View File

@ -6,9 +6,9 @@ import React from 'react';
import propertyPropTypes from './property-prop-types'; import propertyPropTypes from './property-prop-types';
import Value from './value.type-renderer'; import Value from './value.type-renderer';
const renderOperand = (context, operand, type) => ( const renderOperand = (context, operand) => (
<span className="operand"> <span className="operand">
<Value.Component context={context} type={type} value={operand} /> <Value.Component context={context} value={operand} />
</span> </span>
); );
@ -39,7 +39,6 @@ const Condition = ({
case '>=': case '>=':
case '<': case '<':
case '<=': { case '<=': {
const type = -1 !== ['is', 'isnt'].indexOf(value.operator) ? 'any' : 'number';
return value.operands.slice(1).reduce( return value.operands.slice(1).reduce(
(r, operand) => ( (r, operand) => (
<> <>
@ -47,10 +46,10 @@ const Condition = ({
<select className="operator" readOnly value={value.operator}> <select className="operator" readOnly value={value.operator}>
{binaryOps.map(([k, v]) => <option value={k}>{v}</option>)} {binaryOps.map(([k, v]) => <option value={k}>{v}</option>)}
</select> </select>
{renderOperand(context, operand, type)} {renderOperand(context, operand)}
</> </>
), ),
renderOperand(context, value.operands[0], type), renderOperand(context, value.operands[0]),
); );
} }
default: default:

View File

@ -0,0 +1,9 @@
:scope {
display: flex;
align-items: center;
}
select {
margin-right: 0.25em;
}

View File

@ -0,0 +1,58 @@
import {compose} from '@avocado/core';
import contempo from 'contempo';
import React from 'react';
import useTypeRenderers from '~/client/hooks/useTypeRenderers';
import propertyPropTypes from './property-prop-types';
import {contextStepsList} from './steps-lists';
import {typeFromLiteral} from './typing';
const decorate = compose(
contempo(require('./literal.raw.scss')),
);
const Literal = ({
context,
value,
}) => {
const typeRenderers = useTypeRenderers();
const type = typeFromLiteral(value.value);
const stepsList = contextStepsList(context, type);
const tierOptions = Object.keys(stepsList.reduce((r, optionSteps) => {
if (!optionSteps[0] || !optionSteps[0].key) {
return r;
}
return {...r, [optionSteps[0].key]: true};
}, {}));
tierOptions.splice(tierOptions.indexOf('context'), 1);
tierOptions.push('<literal>');
return (
<div className="literal">
<select readOnly value="<literal>">
{
tierOptions
.sort((l, r) => (l.toLowerCase() < r.toLowerCase() ? -1 : 1))
.map((tierOption) => <option key={tierOption}>{tierOption}</option>)
}
</select>
{
(() => {
const Component = typeRenderers[type];
return Component
? <Component value={value.value} />
: null;
})()
}
</div>
);
};
Literal.propTypes = {
...propertyPropTypes,
};
export default {
type: 'literal',
Component: decorate(Literal),
};

View File

@ -1,5 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
export default { export default {
context: PropTypes.shape({}),
options: PropTypes.shape({}), options: PropTypes.shape({}),
value: PropTypes.any,
}; };

View File

@ -22,7 +22,7 @@ const Routines = ({
entries.length > 0 && ( entries.length > 0 && (
entries.map( entries.map(
([name, routine]) => ( ([name, routine]) => (
<li className="routine"> <li className="routine" key={name}>
<label> <label>
<span className="text">{name}</span> <span className="text">{name}</span>
<Actions.Component context={context} value={routine.routine} /> <Actions.Component context={context} value={routine.routine} />

View File

@ -1,105 +0,0 @@
import {compose} from '@avocado/core';
import contempo from 'contempo';
import React from 'react';
import {contextStepsList} from './steps-lists';
import {typeFromSteps, typeFromLiteral} from './typing';
import Value from './value.type-renderer';
const decorate = compose(
contempo(require('./steps.raw.scss')),
);
const Steps = (props) => {
const {
context,
steps,
type,
value,
} = props;
let stepsType;
if ('undefined' === typeof value) {
stepsType = type;
}
else if (value.steps) {
stepsType = typeFromSteps(context, value.steps);
}
else {
stepsType = typeFromLiteral(value);
}
const stepsList = contextStepsList(context, stepsType);
return (
<div className="steps">
{steps.map((step, i) => {
switch (step.type) {
case 'key': {
const tierOptions = Object.keys(stepsList.reduce((r, optionSteps) => {
if (!optionSteps[i] || !optionSteps[i].key) {
return r;
}
for (let j = 0; j < i; ++j) {
if (steps[j].key !== optionSteps[j].key) {
return r;
}
}
return {...r, [optionSteps[i].key]: true};
}, {}));
if (0 === i) {
tierOptions.push('<literal>');
}
return (
<span className="key">
<select readOnly value={steps[i].key}>
{
tierOptions
.sort((l, r) => (l.toLowerCase() < r.toLowerCase() ? -1 : 1))
.map((tierOption) => <option>{tierOption}</option>)
}
</select>
</span>
);
}
case 'invoke':
return (
<div className="invoke">
<span className="paren open">(</span>
{step.args.map((arg) => (
<div className="arg">
<label>
{arg.label}
<Value.Component
context={context}
type={
arg.steps
? typeFromSteps(context, arg.steps)
: typeFromLiteral(arg)
}
value={arg}
/>
</label>
</div>
))}
<span className="paren close">)</span>
</div>
);
default:
return null;
}
})}
{
value && (
<span className="assign">
<span className="op">=</span>
<Value.Component
context={context}
type={typeFromSteps(context, steps)}
value={value}
/>
</span>
)
}
</div>
);
};
export default decorate(Steps);

View File

@ -37,7 +37,7 @@
padding: 0.5em; padding: 0.5em;
} }
.arg .steps { .arg .traversal {
display: flex; display: flex;
} }

View File

@ -0,0 +1,96 @@
import {compose} from '@avocado/core';
import contempo from 'contempo';
import React from 'react';
import propertyPropTypes from './property-prop-types';
import {contextStepsList} from './steps-lists';
import {typeFromSteps} from './typing';
import Value from './value.type-renderer';
const decorate = compose(
contempo(require('./traversal.raw.scss')),
);
const Traversal = (props) => {
const {
context,
value: {steps, value},
} = props;
const stepsList = contextStepsList(context, typeFromSteps(context, steps));
return (
<div className="traversal">
{steps.map((step, i) => (
// eslint-disable-next-line react/no-array-index-key
<span className="step" key={i}>
{
(() => {
switch (step.type) {
case 'key': {
const tierOptions = Object.keys(stepsList.reduce((r, optionSteps) => {
if (!optionSteps[i] || !optionSteps[i].key) {
return r;
}
for (let j = 0; j < i; ++j) {
if (steps[j].key !== optionSteps[j].key) {
return r;
}
}
return {...r, [optionSteps[i].key]: true};
}, {}));
if (0 === i) {
tierOptions.push('<literal>');
}
return (
<span className="key">
<select readOnly value={steps[i].key}>
{
tierOptions
.sort((l, r) => (l.toLowerCase() < r.toLowerCase() ? -1 : 1))
.map((tierOption) => <option key={tierOption}>{tierOption}</option>)
}
</select>
</span>
);
}
case 'invoke':
return (
<div className="invoke">
<span className="paren open">(</span>
{step.args.map((arg) => (
<div className="arg" key={JSON.stringify(arg)}>
<label>
{arg.label}
<Value.Component context={context} value={arg} />
</label>
</div>
))}
<span className="paren close">)</span>
</div>
);
default:
return null;
}
})()
}
</span>
))}
{
value && (
<span className="assign">
<span className="op">=</span>
<Value.Component context={context} value={value} />
</span>
)
}
</div>
);
};
Traversal.propTypes = {
...propertyPropTypes,
};
export default {
type: 'traversal',
Component: decorate(Traversal),
};

View File

@ -1,33 +1,45 @@
import {Context} from '@avocado/behavior'; import {Context} from '@avocado/behavior';
export function typeFromLiteral(v) { export function typeFromLiteral(value) {
if ('literal' === v.type) { if ('undefined' === typeof value) {
const {value} = v; return 'undefined';
if ('undefined' === typeof value) {
return 'undefined';
}
if (null === value) {
return 'null';
}
if ('number' === typeof value) {
return 'number';
}
if ('string' === typeof value) {
return 'string';
}
if ('boolean' === typeof value) {
return 'bool';
}
if (value.length && 2 === value.length && value instanceof Array) {
return 'vector';
}
} }
if ('actions' === v.type && v.traversals) { if (null === value) {
return 'actions'; return 'null';
}
if ('number' === typeof value) {
return 'number';
}
if ('string' === typeof value) {
return 'string';
}
if ('boolean' === typeof value) {
return 'bool';
}
if (value.length && 2 === value.length && value instanceof Array) {
return 'vector';
} }
return 'object'; return 'object';
} }
export function typeFromValue(v) {
if ('object' === typeof v) {
if ('routines' === v.type) {
return 'routines';
}
if ('traversal' === v.type) {
return 'traversal';
}
if ('actions' === v.type && v.traversals) {
return 'actions';
}
if ('literal' === v.type) {
return 'literal';
}
}
return undefined;
}
export function typeFromSteps(context, steps) { export function typeFromSteps(context, steps) {
if (!steps || 0 === steps.length) { if (!steps || 0 === steps.length) {
return 'undefined'; return 'undefined';

View File

@ -1,12 +1,3 @@
.fn { .fn {
text-align-last: right; text-align-last: right;
} }
.literal {
display: flex;
align-items: center;
}
.literal select {
margin-right: 0.25em;
}

View File

@ -3,66 +3,10 @@ import contempo from 'contempo';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import useTypeRenderers from '~/client/hooks/useTypeRenderers';
import {typeFromLiteral, typeFromValue} from '~/client/components/types/typing';
import propertyPropTypes from './property-prop-types'; import propertyPropTypes from './property-prop-types';
import Steps from './steps';
import {contextStepsList} from './steps-lists';
import {typeFromLiteral} from './typing';
let TypeRenderers;
const ensureTypeRenderers = () => {
if (!TypeRenderers) {
const {all: allTypeRenderers} = require('./type-renderers.scwp');
TypeRenderers = Object.values(allTypeRenderers()).reduce((r, M) => {
const {default: {type, Component}} = M;
return {...r, [type]: Component};
}, {});
}
};
const renderValue = (context, type, value) => {
ensureTypeRenderers();
switch (value.type) {
case 'traversal': {
return <Steps context={context} steps={value.steps} type={type} value={value.value} />;
}
case 'actions': {
const Component = TypeRenderers.actions;
return <Component context={context} value={value} />;
}
case 'literal': {
const stepsList = contextStepsList(context, type);
const tierOptions = Object.keys(stepsList.reduce((r, optionSteps) => {
if (!optionSteps[0] || !optionSteps[0].key) {
return r;
}
return {...r, [optionSteps[0].key]: true};
}, {}));
tierOptions.splice(tierOptions.indexOf('context'), 1);
tierOptions.push('<literal>');
return (
<div className="literal">
<select readOnly value="<literal>">
{
tierOptions
.sort((l, r) => (l.toLowerCase() < r.toLowerCase() ? -1 : 1))
.map((tierOption) => <option>{tierOption}</option>)
}
</select>
{
(() => {
const Component = TypeRenderers['any' === type ? typeFromLiteral(value) : type];
return Component
? <Component value={value.value} />
: null;
})()
}
</div>
);
}
default:
return null;
}
};
const decorate = compose( const decorate = compose(
contempo(require('./value.raw.scss')), contempo(require('./value.raw.scss')),
@ -70,9 +14,23 @@ const decorate = compose(
const Value = ({ const Value = ({
context, context,
type, options,
value, value,
}) => <span className="value">{renderValue(context, type, value)}</span>; }) => {
const typeRenderers = useTypeRenderers();
const fromValue = typeFromValue(value);
const type = 'undefined' !== typeof fromValue ? fromValue : typeFromLiteral(value);
const Component = typeRenderers[type];
return (
<span className="value">
{
Component
? <Component context={context} options={options} value={value} />
: <div className="unrenderable" title={`No renderer for '${type}' found!`}>?</div>
}
</span>
);
};
Value.propTypes = { Value.propTypes = {
...propertyPropTypes, ...propertyPropTypes,

View File

@ -0,0 +1,3 @@
import {createContext} from 'react';
export default createContext({});

View File

@ -0,0 +1,5 @@
import {useContext} from 'react';
import TypeRenderersContext from '~/client/context/typeRenderers';
export default () => useContext(TypeRenderersContext);