├── dev ├── static ├── data │ ├── sam-likes-bananas.json │ └── the-sum-of-three-consecutive-integers.json ├── index.html ├── dev.js └── server.js ├── src ├── module │ ├── index.js │ ├── stores │ │ ├── modules │ │ │ ├── index.js │ │ │ ├── ui.js │ │ │ └── ui.test.js │ │ └── create.js │ ├── node │ │ ├── Attributes.js │ │ ├── LinkSvg.js │ │ ├── UiToggle.js │ │ ├── UiParseNav.js │ │ ├── Link.js │ │ ├── NodeWord.js │ │ └── MiddleParent.js │ ├── EmptyTree.js │ ├── Icon.js │ ├── pane │ │ ├── PaneToggle.js │ │ ├── PaneHandler.js │ │ ├── StrRep.js │ │ ├── PanePanel.js │ │ ├── SpanFragments.js │ │ ├── AltParseNavToggle.js │ │ ├── NodeProperties.js │ │ ├── SideBar.js │ │ └── AltParseNav.js │ ├── ParseError.js │ ├── TreeExpansionControl.js │ ├── ParseTreeToolbar.js │ ├── Toolbar.js │ ├── MainStage.js │ ├── IconSprite.js │ ├── Passage.js │ ├── PassageSpan.js │ ├── helpers.test.js │ └── helpers.js ├── less │ ├── explainer │ │ ├── pane │ │ │ ├── pane-container.less │ │ │ ├── pane.less │ │ │ ├── pane__empty.less │ │ │ ├── pane_mod.less │ │ │ ├── pane__handler.less │ │ │ ├── pane__toggle.less │ │ │ ├── pane__alt-parse-nav.less │ │ │ ├── pane__fragments.less │ │ │ └── pane__panel.less │ │ ├── node │ │ │ ├── node-focus-trigger.less │ │ │ ├── node.less │ │ │ ├── node_vars.less │ │ │ ├── node__word.less │ │ │ ├── node__word__attrs.less │ │ │ ├── node__word__tile.less │ │ │ ├── node-children.less │ │ │ ├── node__word__label.less │ │ │ ├── node--segments-container.less │ │ │ ├── node__word__link.less │ │ │ ├── node__word__ui.less │ │ │ ├── node--seq.less │ │ │ └── node_pos.less │ │ ├── loader.less │ │ ├── code.less │ │ ├── parse-error.less │ │ ├── meta-table.less │ │ ├── ft.less │ │ ├── main-stage.less │ │ ├── parse-tree-toolbar.less │ │ ├── tree-expansion-control.less │ │ └── passage.less │ ├── icons.less │ ├── toolbar.less │ ├── text.less │ ├── utils.less │ ├── functions.less │ ├── colors.less │ ├── hierplane.less │ └── flex.less └── static │ └── hierplane.js ├── .gitignore ├── .babelrc ├── bin ├── publish.js └── build.js ├── EXAMPLES.md ├── package.json └── README.md /dev/static: -------------------------------------------------------------------------------- 1 | ../dist/static/ -------------------------------------------------------------------------------- /src/module/index.js: -------------------------------------------------------------------------------- 1 | export Tree from './Tree.js'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | npm-debug.log 4 | .sass-cache 5 | _site 6 | -------------------------------------------------------------------------------- /src/less/explainer/pane/pane-container.less: -------------------------------------------------------------------------------- 1 | .pane-container { 2 | .flex-container-row; 3 | } 4 | -------------------------------------------------------------------------------- /src/less/explainer/pane/pane.less: -------------------------------------------------------------------------------- 1 | // Pane base 2 | .pane { 3 | background: @pane-bg; 4 | position: relative; 5 | } 6 | -------------------------------------------------------------------------------- /src/less/explainer/node/node-focus-trigger.less: -------------------------------------------------------------------------------- 1 | .node-focus-trigger { 2 | .u-child100; 3 | z-index: 99; 4 | cursor: pointer; 5 | } 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "@babel/preset-env", "@babel/preset-react" ], 3 | "plugins": ["@babel/plugin-proposal-export-default-from"] 4 | } 5 | -------------------------------------------------------------------------------- /src/module/stores/modules/index.js: -------------------------------------------------------------------------------- 1 | import ui from './ui'; 2 | 3 | import { combineReducers } from 'redux'; 4 | 5 | export default combineReducers({ ui }); 6 | -------------------------------------------------------------------------------- /src/less/explainer/node/node.less: -------------------------------------------------------------------------------- 1 | // Node base 2 | .node { 3 | .flex-align-self(flex-start); 4 | margin: @node-word-margin; 5 | margin-bottom: 0; 6 | padding-bottom: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/less/explainer/pane/pane__empty.less: -------------------------------------------------------------------------------- 1 | // Empty Pane treatment when nothing is focused 2 | 3 | .pane__empty { 4 | .u-100; 5 | .flex-container-centered; 6 | line-height: 19/@em; 7 | overflow: hidden; 8 | 9 | span { 10 | .u-nowrap; 11 | .u-select-none; 12 | cursor: default; 13 | color: @pane-empty-text; 14 | font-size: 13/@em; 15 | text-align: center; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/module/stores/create.js: -------------------------------------------------------------------------------- 1 | import rootReducer from './modules'; 2 | 3 | import { createStore } from 'redux'; 4 | 5 | // This is for integrating with the Redux Devtool Chrome/Firefix extension: 6 | // https://github.com/zalmoxisus/redux-devtools-extension 7 | const devToolExtension = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(); 8 | 9 | export default () => createStore(rootReducer, devToolExtension); 10 | -------------------------------------------------------------------------------- /src/less/icons.less: -------------------------------------------------------------------------------- 1 | // Icon Sprite container 2 | #icons { 3 | .u-tl; 4 | position: absolute; 5 | height: 0; 6 | width: 0; 7 | visibility: hidden; 8 | overflow: hidden; 9 | z-index: -999; 10 | } 11 | 12 | // Working icon svg gradient assignment 13 | .icon__working__gradient__stop { 14 | stop-color: @working; 15 | } 16 | 17 | .icon__working__gradient__stop--stop4, 18 | .icon__working__gradient__stop--stop5, 19 | .icon__working__gradient__stop--stop6 { 20 | stop-opacity: 0; 21 | } 22 | 23 | .icon__working__path { 24 | fill: url(#icon__working__gradient); 25 | } 26 | -------------------------------------------------------------------------------- /src/less/explainer/node/node_vars.less: -------------------------------------------------------------------------------- 1 | // Node Vars 2 | 3 | // Measurements 4 | @node-word-tile-border-width: 2/@em; 5 | @node-word-margin: 10/@em; 6 | @node-word-padding: 30/@em; 7 | @node-shadow-properties: 2/@em 4/@em 12/@em; 8 | @node-border-radius: 6/@em; 9 | @node-word-label-height: 22/@em; 10 | @node-strong-label-height: @node-word-label-height + (@node-word-margin * 2); 11 | @node-word-link-label-size: 20/@em; 12 | @chain-link-label-height: 17/@em; 13 | @node-label-max-width: 280/@em; 14 | 15 | // Transitions 16 | @node-transition-duration: .09s; 17 | @node-transition-ease: ease-in-out; 18 | -------------------------------------------------------------------------------- /src/less/explainer/node/node__word.less: -------------------------------------------------------------------------------- 1 | // Node Word 2 | 3 | .node__word { 4 | .flex-container-column; 5 | min-width: 30/@em; 6 | min-height: @node-word-label-height + (@node-word-padding * 2); 7 | text-align: center; 8 | position: relative; 9 | } 10 | 11 | .node[data-has-side-children="true"] > .node__word { 12 | min-width: 90/@em; 13 | } 14 | 15 | .node__word > * { 16 | z-index: 1; 17 | position: relative; 18 | } 19 | 20 | .node__word__content { 21 | .u-w100; 22 | .flex-container-column; 23 | } 24 | 25 | .ft.node-container--expanded > .ft__tr > .ft--middle-parent > .node:not(.node--seq) > .node__word > .node__word__content { 26 | .u-h100; 27 | } 28 | -------------------------------------------------------------------------------- /src/less/explainer/loader.less: -------------------------------------------------------------------------------- 1 | .loader { 2 | .fn-transform(translateZ(0)); 3 | width: 60/@em; 4 | height: 60/@em; 5 | fill: fade(@orange, 80%); 6 | -webkit-animation: euclid-spinner 1.1s infinite linear; 7 | animation: euclid-spinner 1.1s infinite linear; 8 | filter: drop-shadow(0 0 10/@em rgba(0,0,0,.7)); 9 | } 10 | 11 | @-webkit-keyframes euclid-spinner { 12 | 0% { 13 | -webkit-transform: rotate(0deg); 14 | } 15 | 16 | 100% { 17 | -webkit-transform: rotate(360deg); 18 | } 19 | } 20 | 21 | @keyframes euclid-spinner { 22 | 0% { 23 | .fn-transform(rotate(0deg)); 24 | } 25 | 26 | 100% { 27 | .fn-transform(rotate(360deg)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/module/node/Attributes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | class Attributes extends React.Component { 5 | render() { 6 | const { attrs, id } = this.props; 7 | 8 | return ( 9 |
10 | {attrs !== undefined && attrs.length > 0 ? attrs.map(attr => ( 11 |
{attr}
12 | )) : null} 13 |
14 | ); 15 | } 16 | } 17 | 18 | Attributes.propTypes = { 19 | attrs: PropTypes.array, 20 | id: PropTypes.string, 21 | } 22 | 23 | export default Attributes; 24 | -------------------------------------------------------------------------------- /src/module/EmptyTree.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from './Icon.js'; 3 | 4 | // `Toolbar` displays the Euclid tool buttons. 5 | class EmptyTree extends React.Component { 6 | render() { 7 | return ( 8 |
9 |
10 | 11 |

12 | Enter your query above 13 |

14 |

15 | Press enter to parse. 16 |

17 |
18 |
19 | ); 20 | } 21 | } 22 | 23 | export default EmptyTree; 24 | -------------------------------------------------------------------------------- /src/less/toolbar.less: -------------------------------------------------------------------------------- 1 | .toolbar, 2 | .toolbar__button { 3 | .u-blocklist; 4 | } 5 | 6 | .toolbar { 7 | .u-select-none; 8 | position: absolute; 9 | bottom: 0; 10 | right: 0; 11 | transition: padding .05s @node-transition-ease; 12 | z-index: 9; 13 | } 14 | 15 | .toolbar__button { 16 | .u-100; 17 | .flex-container-vcentered; 18 | } 19 | 20 | .toolbar__button__link { 21 | .t-link; 22 | .t-bold; 23 | color: fade(@white, 40%); 24 | padding: 20/@em; 25 | transition: color @node-transition-duration @node-transition-ease, font-size .05s @node-transition-ease; 26 | 27 | &:hover { 28 | color: @white; 29 | } 30 | } 31 | 32 | .toolbar__button--disabled { 33 | .toolbar__button__link, 34 | &:hover, 35 | &:active { 36 | color: fade(@white, 15%); 37 | cursor: default; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/module/Icon.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | // TODO (codeviking): This component is a duplicate of webui/webapp/app/components/Icon.jsx. When 5 | // migrating hierplane to it's own dependency, I had to move this too. Eventually we should 6 | // figure out a way tos hare these types of "common" components (or decide we're not going to, 7 | // and remove this TODO). 8 | class Icon extends React.Component { 9 | render() { 10 | const { symbol, wrapperClass } = this.props; 11 | 12 | return ( 13 | 14 | 15 | 16 | ); 17 | } 18 | } 19 | 20 | Icon.propTypes = { 21 | wrapperClass: PropTypes.string, 22 | symbol: PropTypes.string, 23 | } 24 | 25 | export default Icon; 26 | -------------------------------------------------------------------------------- /src/module/pane/PaneToggle.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Icon from '../Icon.js'; 4 | 5 | // `Toolbar` displays the Euclid tool buttons. 6 | class PaneToggle extends React.Component { 7 | render() { 8 | const { sideBarCollapsed, togglePane, icon, mode } = this.props; 9 | 10 | return ( 11 |
{ togglePane(mode) }}> 12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | PaneToggle.propTypes = { 19 | sideBarCollapsed: PropTypes.bool, 20 | togglePane: PropTypes.func, 21 | icon: PropTypes.string, 22 | mode: PropTypes.string, 23 | } 24 | 25 | export default PaneToggle; 26 | -------------------------------------------------------------------------------- /dev/data/sam-likes-bananas.json: -------------------------------------------------------------------------------- 1 | { 2 | "text": "Sam likes boats", 3 | "root": { 4 | "nodeType": "event", 5 | "word": "like", 6 | "spans": [ 7 | { 8 | "start": 4, 9 | "end": 9 10 | } 11 | ], 12 | "children": [ 13 | { 14 | "nodeType": "entity", 15 | "word": "Sam", 16 | "link": "subject", 17 | "attributes": [ "Person" ], 18 | "spans": [ 19 | { 20 | "start": 0, 21 | "end": 3 22 | } 23 | ] 24 | }, 25 | { 26 | "nodeType": "entity", 27 | "word": "boat", 28 | "link": "object", 29 | "attributes": [ ">1"], 30 | "spans": [ 31 | { 32 | "start": 10, 33 | "end": 17 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/module/node/LinkSvg.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | class LinkSvg extends React.Component { 5 | render() { 6 | const { capPos, viewBox, fillPoints, strokePoints } = this.props; 7 | 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | } 18 | 19 | LinkSvg.propTypes = { 20 | capPos: PropTypes.string, 21 | viewBox: PropTypes.string, 22 | fillPoints: PropTypes.string, 23 | strokePoints: PropTypes.string, 24 | } 25 | 26 | export default LinkSvg; 27 | -------------------------------------------------------------------------------- /src/module/pane/PaneHandler.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | // `Toolbar` displays the Euclid tool buttons. 5 | class PaneHandler extends React.Component { 6 | render() { 7 | const { onMouseDown, onDoubleClick, direction } = this.props; 8 | 9 | return ( 10 | 18 | ); 19 | } 20 | } 21 | 22 | PaneHandler.propTypes = { 23 | onMouseDown: PropTypes.func, 24 | onDoubleClick: PropTypes.func, 25 | direction: PropTypes.string, 26 | } 27 | 28 | export default PaneHandler; 29 | -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Static Hierplane 4 | 5 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /bin/publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | /** 4 | * NPM (version 3, at least) unfortunately has a bug where the `prepack`, and 5 | * `prepublishOnly` lifecycle hooks don't execute as expected. 6 | * 7 | * To work around this this script explicitly cleans and builds prior to the 8 | * `publish` action being executed. 9 | */ 10 | const cp = require('child_process'); 11 | const path = require('path'); 12 | const merge = require('merge'); 13 | 14 | // Whenever we incoke `cp.exec` or `cp.execSync` these args set the correct 15 | // working directory and ensure that stdout / sterr are streamed to the 16 | // current TTY 17 | const execArgs = { 18 | cwd: path.resolve(__dirname, '..'), 19 | stdio: 'inherit', 20 | env: merge(process.env, { 'NODE_ENV': 'production' }) 21 | }; 22 | 23 | cp.execSync('npm run prepare', execArgs); 24 | cp.execSync('npm publish', execArgs); 25 | -------------------------------------------------------------------------------- /src/module/ParseError.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from './Icon.js'; 3 | 4 | // `Toolbar` displays the Euclid tool buttons. 5 | class ParseError extends React.Component { 6 | render() { 7 | return ( 8 |
9 |
10 | 11 |

12 | Parsing error 13 |

14 |

15 | No parse trees were returned in the JSON. 16 |

17 |

18 | Press space bar to enter a new query. 19 |

20 |
21 |
22 | ); 23 | } 24 | } 25 | 26 | export default ParseError; 27 | -------------------------------------------------------------------------------- /src/less/explainer/node/node__word__attrs.less: -------------------------------------------------------------------------------- 1 | // Node Attributes 2 | 3 | .node__word__attrs { 4 | .u-w100; 5 | .flex-container-align-right; 6 | .flex-align-items(flex-end); 7 | min-height: 30/@em; 8 | margin-top: -30/@em; 9 | 10 | .node__word__attrs__item { 11 | .flex-container-centered; 12 | .flex-align-self(flex-end); 13 | border-radius: 11/@em; 14 | width: auto; 15 | height: auto; 16 | background: @attr-bg-color; 17 | padding: 2/@em 9/@em; 18 | margin: 0 7/@em 7/@em -2/@em; 19 | transition: opacity @node-transition-duration @node-transition-ease, background-color @node-transition-duration @node-transition-ease; 20 | 21 | &:first-child { 22 | margin-left: 7/@em; 23 | } 24 | 25 | span { 26 | .t-uppercase; 27 | font-size: 10/@em; 28 | font-weight: bold; 29 | color: @attr-text-color; 30 | .u-nowrap; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/less/text.less: -------------------------------------------------------------------------------- 1 | .t-crisp { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | .t-smooth { 7 | -webkit-font-smoothing: subpixel-antialiased; 8 | -moz-osx-font-smoothing: auto; 9 | } 10 | 11 | .t-kerning-default { 12 | letter-spacing: normal; 13 | } 14 | 15 | .t-underline { 16 | text-decoration: underline; 17 | } 18 | 19 | .t-no-underline { 20 | text-decoration: none; 21 | } 22 | 23 | .t-uppercase { 24 | text-transform: uppercase; 25 | } 26 | 27 | .t-case-normal { 28 | text-transform: none; 29 | } 30 | 31 | .t-bold { 32 | font-weight: bold; 33 | } 34 | 35 | .t-thin { 36 | font-weight: 100; 37 | } 38 | 39 | .t-caps { 40 | font-size: 14/@em; 41 | font-weight: bold; 42 | letter-spacing: .025em; 43 | .t-uppercase; 44 | } 45 | 46 | .t-link { 47 | text-decoration: none; 48 | cursor: pointer; 49 | } 50 | 51 | .t-truncate { 52 | .u-nowrap; 53 | overflow: hidden; 54 | text-overflow: ellipsis; 55 | } 56 | -------------------------------------------------------------------------------- /src/less/explainer/code.less: -------------------------------------------------------------------------------- 1 | .code { 2 | .flex-container-column; 3 | flex: 2; 4 | background: @code-bg; 5 | 6 | textarea.code__content { 7 | .u-appearance-none; 8 | .t-smooth; 9 | .u-100; 10 | display: block; 11 | background: transparent; 12 | white-space: pre; 13 | box-sizing: border-box; 14 | resize: none; 15 | border: 3/@em solid transparent; 16 | padding: 21/@em; 17 | line-height: 21/@em; 18 | font-size: 14/@em; 19 | color: @code-text; 20 | font-family: 'Ubuntu Mono', monospace !important; 21 | transition: border-color @node-transition-duration @node-transition-ease, background-color @node-transition-duration @node-transition-ease; 22 | cursor: text; 23 | 24 | &:hover { 25 | background: darken(@code-bg, 1%); 26 | } 27 | 28 | &:focus { 29 | background: @passage-edit-bg; 30 | outline: none; 31 | border: none; 32 | border: 3/@em solid @focused-ui; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/less/explainer/pane/pane_mod.less: -------------------------------------------------------------------------------- 1 | // MainStage Content 2 | .pane--fill { 3 | .u-w100; 4 | .flex-container-column; 5 | flex: 2; 6 | } 7 | 8 | // MainStage Container 9 | .pane--scroll { 10 | .pane--fill; 11 | overflow: auto; 12 | } 13 | 14 | // SideBar 15 | .pane--right { 16 | min-width: 280/@em; 17 | max-width: 85%; 18 | box-shadow: 0 0 10/@em rgba(0,0,0,.1); 19 | opacity: 1; 20 | } 21 | 22 | .pane--autosnap { 23 | transition: min-width .12s @node-transition-ease, width .12s @node-transition-ease, height .12s @node-transition-ease, opacity .06s @node-transition-ease; 24 | } 25 | 26 | .pane--collapsed { 27 | min-width: 0; 28 | min-height: 0; 29 | overflow: hidden; 30 | opacity: 0; 31 | } 32 | 33 | .pane--right.pane--collapsed { 34 | width: 0 !important; 35 | } 36 | 37 | .pane--moving { 38 | &, 39 | & * { 40 | .u-select-none !important; 41 | } 42 | .pane__handler__thumb__highlight { 43 | background-color: @focused-ui; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/module/node/UiToggle.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Icon from '../Icon.js'; 4 | 5 | class UiToggle extends React.Component { 6 | render() { 7 | const { onUiMouseOver, onUiMouseOut, onUiMouseUp } = this.props; 8 | 9 | return ( 10 |
14 |
15 | 16 | 17 |
18 |
19 | ); 20 | } 21 | } 22 | 23 | UiToggle.propTypes = { 24 | onUiMouseOver: PropTypes.func, 25 | onUiMouseOut: PropTypes.func, 26 | onUiMouseUp: PropTypes.func, 27 | } 28 | 29 | export default UiToggle; 30 | -------------------------------------------------------------------------------- /src/module/pane/StrRep.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | class StrRep extends React.Component { 5 | handleFocus() { 6 | // Make sure any selected text on the page is de-selected when textarea is focused. 7 | if (document.selection) { 8 | document.selection.empty(); 9 | } else if (window.getSelection) { 10 | window.getSelection().removeAllRanges(); 11 | } 12 | } 13 | 14 | render() { 15 | const { selectedData } = this.props; 16 | 17 | let parsedStr = null; 18 | if (selectedData.stringRepresentation) { 19 | parsedStr = (selectedData.stringRepresentation); 20 | } else { 21 | parsedStr = ""; 22 | } 23 | 24 | return ( 25 |
26 |