diff --git a/src/quadratic-curve.js b/src/quadratic-curve.js
new file mode 100644
index 0000000..90f368c
--- /dev/null
+++ b/src/quadratic-curve.js
@@ -0,0 +1,23 @@
+class QuadraticCurve {
+ constructor() {
+ this.points = [];
+ this.controlPoint = { x: 0, y: 0 };
+ }
+
+ addPoint({ x, y }) {
+ const { x: cx, y: cy } = this.controlPoint;
+ this.points.push({ x: x - cx, y: y - cy });
+
+ if (this.points.length % 2 === 0) {
+ this.controlPoint = { x, y };
+ }
+
+ return this;
+ }
+
+ toString() {
+ return `q${ this.points.map(({ x, y }) => `${ x },${ y }`).join(' ') }`;
+ }
+}
+
+export default QuadraticCurve;
diff --git a/src/rendering/VerticalLayout/index.js b/src/rendering/VerticalLayout/index.js
new file mode 100644
index 0000000..e4e74ee
--- /dev/null
+++ b/src/rendering/VerticalLayout/index.js
@@ -0,0 +1,157 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import QuadraticCurve from 'quadratic-curve';
+
+import * as style from 'rendering/style';
+
+const radius = 10;
+
+const VerticalLayout = ({
+ withConnectors,
+ connectorPath,
+ transforms,
+ children
+}) => <>
+ { withConnectors && }
+ { React.Children.map(children, (child, i) => (
+ { child }
+ )) }
+>;
+
+VerticalLayout.defaultProps = {
+ withConnectors: false,
+ spacing: 10
+};
+
+VerticalLayout.propTypes = {
+ spacing: PropTypes.number,
+ withConnectors: PropTypes.bool,
+ connectorPath: PropTypes.string,
+ transforms: PropTypes.arrayOf(PropTypes.string),
+ children: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.node),
+ PropTypes.node
+ ]).isRequired
+};
+
+const generateOffsetBoxes = (boxes, spacing, containerWidth) => {
+ let y = 0;
+ return boxes.map(box => {
+ try {
+ const x = (containerWidth - box.width) / 2;
+ return {
+ ...box,
+ x, y,
+ axisX1: x + box.axisX1,
+ axisX2: x + box.axisX2,
+ axisY: y + box.axisY
+ };
+ }
+ finally {
+ y += spacing + box.height;
+ }
+ });
+};
+const calculateChildTransforms = boxes =>
+ boxes.map(box => `translate(${ box.x } ${ box.y })`);
+const linear = (start, end, amount) => start * (1 - amount) + end * amount;
+const generateCurve = (start, end) => {
+ const curve = new QuadraticCurve();
+ const width = Math.abs(start.x - end.x);
+ const height = Math.abs(start.y - end.y);
+ const direction = {
+ x: start.x < end.x ? 1 : -1,
+ y: start.y < end.y ? 1 : -1
+ };
+ const compression = Math.max(0, 1 - height / (2 * radius));
+ const curveEnd = linear(radius * 2, width, compression);
+
+ curve.addPoint({
+ x: linear(radius, width * 0.25, compression) * direction.x,
+ y: 0
+ });
+
+ if (compression === 0) {
+ curve.addPoint({
+ x: radius * direction.x,
+ y: radius * direction.y
+ });
+ }
+
+ curve.addPoint({
+ x: linear(radius, width * 0.5, compression) * direction.x,
+ y: height / 2 * direction.y
+ });
+
+ if (compression === 0) {
+ curve.addPoint({
+ x: radius * direction.x,
+ y: (height - radius) * direction.y
+ });
+ }
+
+ curve
+ .addPoint({
+ x: linear(radius, width * 0.75, compression) * direction.x,
+ y: height * direction.y
+ })
+ .addPoint({
+ x: curveEnd * direction.x,
+ y: height * direction.y
+ });
+
+ if (width > curveEnd) {
+ curve
+ .addPoint({
+ x: (width + curveEnd) / 2 * direction.x,
+ y: height * direction.y
+ })
+ .addPoint({
+ x: width * direction.x,
+ y: height * direction.y
+ });
+ }
+
+ return `M ${ start.x },${ start.y } ${ curve.toString() }`;
+};
+const calculateConnectorPath = (boxes, { width, height }) => {
+ const connectorY = height / 2;
+ const start1 = { x: 0, y: connectorY };
+ const start2 = { x: width, y: connectorY };
+
+ return boxes.map(box => `
+ ${ generateCurve(start1, { x: box.axisX1, y: box.axisY }) }
+ ${ generateCurve(start2, { x: box.axisX2, y: box.axisY }) }
+ `).join('');
+};
+const layout = data => {
+ const { withConnectors, spacing } = {
+ ...VerticalLayout.defaultProps,
+ ...data.props
+ };
+
+ const childBoxes = data.children.map(child => child.box);
+ const curveAllowance = withConnectors ? radius * 2 : 0;
+
+ data.box = {
+ width: Math.max(...(childBoxes.map(box => box.width))) +
+ 2 * curveAllowance,
+ height: childBoxes.reduce((height, box) => height + box.height, 0) +
+ (childBoxes.length - 1) * spacing
+ };
+
+ const offsetBoxes = generateOffsetBoxes(childBoxes, spacing, data.box.width);
+ data.props = {
+ ...data.props,
+ transforms: calculateChildTransforms(offsetBoxes),
+ connectorPath: withConnectors
+ ? calculateConnectorPath(offsetBoxes, data.box)
+ : undefined
+ };
+
+ return data;
+};
+
+export default VerticalLayout;
+export { layout };
diff --git a/src/rendering/types.js b/src/rendering/types.js
index 709458f..6fa6d19 100644
--- a/src/rendering/types.js
+++ b/src/rendering/types.js
@@ -3,11 +3,13 @@ import * as Pin from 'rendering/Pin';
import * as Text from 'rendering/Text';
import * as Box from 'rendering/Box';
import * as HorizontalLayout from 'rendering/HorizontalLayout';
+import * as VerticalLayout from 'rendering/VerticalLayout';
export default {
SVG,
Pin,
Text,
Box,
- HorizontalLayout
+ HorizontalLayout,
+ VerticalLayout
};
diff --git a/src/syntax/js.js b/src/syntax/js.js
index c371b46..94e410c 100644
--- a/src/syntax/js.js
+++ b/src/syntax/js.js
@@ -1,6 +1,9 @@
import Render from 'components/Render';
import layout from 'layout';
+const type = 'JS';
+const description = 'JavaScript';
+
const parse = expr => {
return {
type: 'SVG',
@@ -29,15 +32,39 @@ const parse = expr => {
},
children: [
{
- type: 'Box',
+ type: 'VerticalLayout',
props: {
- theme: 'literal'
+ withConnectors: true
},
children: [
{
- type: 'Text',
+ type: 'Box',
+ props: {
+ theme: 'literal',
+ label: 'Type'
+ },
children: [
- 'JS'
+ {
+ type: 'Text',
+ children: [
+ type
+ ]
+ }
+ ]
+ },
+ {
+ type: 'Box',
+ props: {
+ theme: 'literal',
+ label: 'Description'
+ },
+ children: [
+ {
+ type: 'Text',
+ children: [
+ description
+ ]
+ }
]
}
]
diff --git a/src/syntax/pcre.js b/src/syntax/pcre.js
index 1ba9f6b..4bf294a 100644
--- a/src/syntax/pcre.js
+++ b/src/syntax/pcre.js
@@ -1,6 +1,9 @@
import Render from 'components/Render';
import layout from 'layout';
+const type = 'PCRE';
+const description = 'Perl-compatible Regular Expression';
+
const parse = expr => {
return {
type: 'SVG',
@@ -29,15 +32,39 @@ const parse = expr => {
},
children: [
{
- type: 'Box',
+ type: 'VerticalLayout',
props: {
- theme: 'literal'
+ withConnectors: true
},
children: [
{
- type: 'Text',
+ type: 'Box',
+ props: {
+ theme: 'literal',
+ label: 'Type'
+ },
children: [
- 'PCRE'
+ {
+ type: 'Text',
+ children: [
+ type
+ ]
+ }
+ ]
+ },
+ {
+ type: 'Box',
+ props: {
+ theme: 'literal',
+ label: 'Description'
+ },
+ children: [
+ {
+ type: 'Text',
+ children: [
+ description
+ ]
+ }
]
}
]