├── 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 |
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 |
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 |
28 | );
29 | }
30 | }
31 |
32 | StrRep.propTypes = {
33 | selectedData: PropTypes.object,
34 | }
35 |
36 | export default StrRep;
37 |
--------------------------------------------------------------------------------
/src/less/explainer/node/node__word__tile.less:
--------------------------------------------------------------------------------
1 | // Node Tile
2 |
3 | .node__word__tile {
4 | .fn-gradient(@color0);
5 | }
6 |
7 | .node__word__tile,
8 | .node__word__tile:after {
9 | .u-100;
10 | .u-tl;
11 | border: @node-word-tile-border-width solid @node-border-color;
12 | border-radius: @node-border-radius;
13 | box-shadow: @node-shadow-properties @node-box-shadow-color;
14 | box-sizing: border-box;
15 | position: absolute;
16 | }
17 |
18 | .node__word__tile:after {
19 | .u-pe;
20 | position: absolute;
21 | border: none;
22 | box-shadow: none;
23 | opacity: 0;
24 | }
25 |
26 | .node-tile-transition-props {
27 | transition: opacity @node-transition-duration @node-transition-ease,
28 | border-color @node-transition-duration @node-transition-ease,
29 | box-shadow @node-transition-duration @node-transition-ease;
30 | }
31 |
32 | .node:not(.node--focused) {
33 | & > .node__word > .node__word__tile {
34 | &:after {
35 | .node-tile-transition-props;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/module/pane/PanePanel.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 |
4 | class PanePanel extends React.Component {
5 | constructor() {
6 | super();
7 | this.state = {
8 | collapsed: false,
9 | };
10 | }
11 |
12 | render() {
13 | const { collapsed } = this.state;
14 | const { panelHeader, panelContent, padded } = this.props;
15 |
16 | return (
17 |
18 |
{this.setState({collapsed: !this.state.collapsed})}}>
19 | {panelHeader}
20 |
21 |
22 | {panelContent}
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | PanePanel.propTypes = {
30 | panelHeader: PropTypes.string,
31 | panelContent: PropTypes.object,
32 | padded: PropTypes.bool,
33 | }
34 |
35 | export default PanePanel;
36 |
--------------------------------------------------------------------------------
/src/less/explainer/node/node-children.less:
--------------------------------------------------------------------------------
1 | // Node Children
2 |
3 | .node-children-container {
4 | position: relative;
5 | margin-bottom: -@node-word-margin * 2;
6 | padding-bottom: @node-word-margin * 2;
7 | }
8 |
9 | .node-children-container,
10 | .ft--middle-children > .node-children-container {
11 | .flex-container-row;
12 | .flex-container-hcentered;
13 | }
14 |
15 | .ft--left-children > .node-children-container,
16 | .ft--right-children > .node-children-container {
17 | .flex-container-column;
18 |
19 | & > * {
20 | .flex-align-self(flex-start);
21 |
22 | & + * {
23 | margin-top: 2 * @node-word-margin;
24 | }
25 |
26 | &:nth-child(2) {
27 | margin-top: @node-word-margin;
28 | }
29 | }
30 | }
31 |
32 | .ft--left-children > .node-children-container {
33 | & > * {
34 | .flex-align-self(flex-end);
35 | }
36 | }
37 |
38 | .node-children-container-defocus-trigger {
39 | .u-child100;
40 | .u-mp0 !important;
41 | }
42 |
43 | .node.node-container--expanded[data-has-down-children="true"][data-has-side-children="false"] {
44 | padding-bottom: @node-word-margin;
45 | }
46 |
--------------------------------------------------------------------------------
/src/static/hierplane.js:
--------------------------------------------------------------------------------
1 | import { Tree } from '../module/index.js';
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 |
6 | /**
7 | * Renders a hierplane tree visualization from the provided tree.
8 | *
9 | * @param {Object} tree The tree to render.
10 | * @param {Object} [options] Optional command options.
11 | * @param {string} [options.target='body'] The element into which the tree should be rendered, this
12 | * defaults to document.body.
13 | * @param {string} [options.theme='dark'] The theme to use, can be "light" or undefined.
14 | * @returns {function} unmount A function which ummounts the resulting Component.
15 | */
16 | export function renderTree(tree, options = { target: 'body', theme: 'dark' }) {
17 | const node = document.querySelector(options.target)
18 | ReactDOM.render(
19 | ,
20 | node
21 | );
22 | return function unmount() {
23 | ReactDOM.unmountComponentAtNode(node)
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/src/less/explainer/parse-error.less:
--------------------------------------------------------------------------------
1 | .parse-error {
2 | text-align: center;
3 |
4 | * {
5 | color: @passage-text-color;
6 | }
7 | }
8 |
9 | .parse-error__icon {
10 | opacity: .17;
11 | margin-bottom: 22/@em;
12 | height: 88/@em;
13 | fill: @passage-text-color;
14 | }
15 |
16 | .parse-error__primary {
17 | .u-mp0;
18 | margin-bottom: 15/@em;
19 | display: block;
20 | opacity: .7;
21 | font-size: 16/@em;
22 |
23 | span {
24 | font-weight: normal;
25 | font-size: 30/@em;
26 | }
27 | }
28 |
29 | .parse-error__secondary,
30 | .parse-error__tertiary {
31 | .u-mp0;
32 | display: block;
33 | margin-top: 4/@em;
34 |
35 | strong {
36 | font-size: 16/@em;
37 | opacity: .5;
38 | }
39 |
40 | & > span {
41 | font-size: 14/@em;
42 | opacity: .4;
43 | }
44 |
45 | span.parse-error--key {
46 | .t-uppercase;
47 | vertical-align: top;
48 | margin: 3/@em;
49 | padding: 2/@em 4/@em;
50 | border: 1/@em solid fade(@passage-text-color, 75%);
51 | display: inline-block;
52 | font-size: 11/@em;
53 | border-radius: 3/@em;
54 | background: fade(@passage-text-color, 5%);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/less/explainer/pane/pane__handler.less:
--------------------------------------------------------------------------------
1 | // Resize Handler
2 |
3 | .pane__handler {
4 | width: 12/@em;
5 | height: 12/@em;
6 | border-top-width: 0;
7 | border-bottom-width: 0;
8 | position: absolute;
9 | z-index: 999;
10 | top: 0;
11 | left: 0;
12 |
13 | .pane__handler__thumb {
14 | background: @pane-panel-separator;
15 | width: 4/@em;
16 | height: 4/@em;
17 | margin: 4/@em;
18 | position: absolute;
19 |
20 | .pane__handler__thumb__highlight {
21 | transition: background-color @node-transition-duration @node-transition-ease;
22 | width: 2/@em;
23 | height: 2/@em;
24 | margin: 2/@em;
25 | }
26 | }
27 |
28 | &:hover {
29 | .pane__handler__thumb__highlight {
30 | background: @focused-ui;
31 | }
32 | }
33 | }
34 |
35 | .pane__handler--vertical {
36 | height: 100%;
37 | margin-left: -4/@em;
38 | margin-right: -4/@em;
39 | cursor: ew-resize;
40 | cursor: col-resize;
41 |
42 | .pane__handler__thumb {
43 | height: 100%;
44 | margin-top: 0;
45 | margin-bottom: 0;
46 |
47 | .pane__handler__thumb__highlight {
48 | height: 100%;
49 | margin-top: 0;
50 | margin-bottom: 0;
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/less/explainer/node/node__word__label.less:
--------------------------------------------------------------------------------
1 | // Node label
2 |
3 | .node__word__label {
4 | .flex-container;
5 | .flex-justify-content(center);
6 | .flex-grow(1);
7 | padding: @node-word-padding;
8 | min-height: @node-word-label-height;
9 | min-width: 16/@em;
10 | position: relative;
11 | white-space: nowrap;
12 |
13 | span {
14 | display: block;
15 | }
16 |
17 | .node__word__label__headword {
18 | color: @node-text-color;
19 | text-shadow: 0 0 20/@em @node-text-shadow-color;
20 | font-size: 18/@em;
21 | opacity: 1;
22 | transition: color @node-transition-duration @node-transition-ease, opacity .05s @node-transition-ease;
23 | }
24 |
25 | .node__word__label__rollup {
26 | font-size: 14/@em;
27 | display: none;
28 | font-weight: bold;
29 | color: fade(@node-text-color, 53%);
30 | text-shadow: none;
31 | margin-top: -27/@em;
32 | padding-top: 2/@em;
33 | transition: color @node-transition-duration @node-transition-ease;
34 |
35 | &:first-child {
36 | margin-top: auto;
37 | }
38 |
39 | strong {
40 | transition: color @node-transition-duration @node-transition-ease;
41 | color: @node-text-color;
42 | text-shadow: 0 0 20/@em fade(@node-text-shadow-color, 66%);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/less/explainer/pane/pane__toggle.less:
--------------------------------------------------------------------------------
1 | .pane__toggle {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | display: block;
6 | width: 34/@em;
7 | height: 34/@em;
8 | text-align: center;
9 | line-height: 32/@em;
10 | cursor: pointer;
11 | transition: opacity .5s @node-transition-ease, background-color @transition-properties;
12 |
13 | .pane__toggle__glyph {
14 | display: inline-block;
15 | fill: @white;
16 | opacity: .4;
17 | width: 10/@em;
18 | height: 10/@em;
19 | transition: opacity @transition-properties;
20 | }
21 |
22 | &:hover {
23 | .pane__toggle__glyph {
24 | opacity: 1;
25 | }
26 | }
27 |
28 | &:active {
29 | .pane__toggle__glyph {
30 | transition-duration: .05s;
31 | opacity: .3;
32 | }
33 | }
34 | }
35 |
36 | .pane__toggle--sidebar {
37 | z-index: -99;
38 | opacity: 0;
39 | transition-delay: 0s;
40 | transition-duration: 0s;
41 |
42 | .pane__toggle__glyph {
43 | width: 16/@em;
44 | height: 16/@em;
45 | }
46 | }
47 |
48 | .pane__toggle--sidebar.pane__toggle--sidebar-collapsed {
49 | z-index: 0;
50 | transition-delay: .0 1s;
51 | transition-duration: .5s;
52 | opacity: 1;
53 | }
54 |
55 | .pane--collapsed {
56 | .pane__toggle--close {
57 | transition: opacity .05s @node-transition-ease;
58 | opacity: 0;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/less/utils.less:
--------------------------------------------------------------------------------
1 | .u-mp0 {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | .u-100 {
7 | width: 100%;
8 | height: 100%;
9 | }
10 |
11 | .u-w100 {
12 | width: 100%;
13 | }
14 |
15 | .u-h100 {
16 | height: 100%;
17 | }
18 |
19 | .u-tl {
20 | top: 0;
21 | left: 0;
22 | }
23 |
24 | .u-pe {
25 | content: "";
26 | }
27 |
28 | .u-child100 {
29 | position: absolute;
30 | .u-100;
31 | .u-tl;
32 | }
33 |
34 | .u-pe100 {
35 | display: block;
36 | .u-child100;
37 | .u-pe;
38 | }
39 |
40 | .u-blocklist {
41 | list-style: none;
42 | display: block;
43 | .u-mp0;
44 | }
45 |
46 | .u-hidden {
47 | display: none;
48 | }
49 |
50 | .u-bg-clip {
51 | -webkit-background-clip: padding-box;
52 | -moz-background-clip: padding;
53 | background-clip: padding-box;
54 | }
55 |
56 | .u-select-none {
57 | -webkit-touch-callout: none;
58 | -webkit-user-select: none;
59 | -moz-user-select: none;
60 | -ms-user-select: none;
61 | user-select: none;
62 | .u-disable-touch-callout;
63 | }
64 |
65 | .u-appearance-none {
66 | -moz-appearance: none;
67 | -webkit-appearance: none;
68 | }
69 |
70 | .u-disable-touch-callout {
71 | -webkit-touch-callout: none;
72 | }
73 |
74 | .u-disable-tap-hilight {
75 | -webkit-tap-highlight-color: rgba(0,0,0,0);
76 | -webkit-tap-highlight-color: transparent;
77 | .u-select-none;
78 | }
79 |
80 | .u-nowrap {
81 | white-space: nowrap;
82 | }
83 |
--------------------------------------------------------------------------------
/src/less/explainer/meta-table.less:
--------------------------------------------------------------------------------
1 | .meta-table,
2 | .meta-table th
3 | .meta-table td {
4 | .u-mp0;
5 | }
6 |
7 | .meta-table {
8 | border-collapse: collapse;
9 | }
10 |
11 | .meta-table tr {
12 | &.meta-table__tr--section {
13 | th,
14 | td {
15 | padding-top: 18/@em;
16 | }
17 | }
18 | }
19 |
20 | .meta-table th,
21 | .meta-table td {
22 | text-align: left;
23 | font-weight: normal;
24 | vertical-align: top;
25 | }
26 |
27 | .meta-table th > span,
28 | .meta-table td > .meta-table-label > span {
29 | font-size: 13/@em;
30 | }
31 |
32 | .meta-table th {
33 | min-width: 124/@em;
34 |
35 | span {
36 | color: @meta-table-th;
37 | }
38 | }
39 |
40 | .meta-table td {
41 | .meta-table-label {
42 | display: table;
43 | table-layout: fixed;
44 | width: 100%;
45 | white-space: nowrap;
46 |
47 | & > span {
48 | display: table-cell;
49 | overflow: hidden;
50 | text-overflow: ellipsis;
51 | padding-top: 4/@em;
52 | }
53 |
54 | span {
55 | .t-smooth;
56 | color: @meta-table-td;
57 |
58 | &.meta-table-label--hero {
59 | padding-top: 0;
60 | font-size: 18/@em;
61 | color: @white;
62 | }
63 |
64 | &.meta-table-label--empty {
65 | opacity: .25;
66 | }
67 |
68 | &.meta-table-label--secondary {
69 | opacity: .5;
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/module/pane/SpanFragments.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 |
4 | class SpanFragments extends React.Component {
5 | render() {
6 | const { selectedData, text } = this.props;
7 |
8 | function getFragmentData({ alternateParseInfo }) {
9 | return alternateParseInfo && alternateParseInfo.spanAnnotations ? alternateParseInfo.spanAnnotations : null;
10 | }
11 |
12 | const fragmentData = getFragmentData(selectedData);
13 |
14 | return (
15 |
16 |
17 |
18 | {fragmentData ? fragmentData.map((item) => (
19 |
20 | {item.spanType === "ignored" ? "ignr." : item.spanType}
21 | {text.slice(item.lo, item.hi)}
22 |
23 | )) : (
24 |
No span fragments were served.
25 | )}
26 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | SpanFragments.propTypes = {
34 | selectedData: PropTypes.object,
35 | text: PropTypes.string,
36 | }
37 |
38 | export default SpanFragments;
39 |
--------------------------------------------------------------------------------
/src/less/explainer/pane/pane__alt-parse-nav.less:
--------------------------------------------------------------------------------
1 | .pane__alt-parse {
2 | min-height: 43/@em;
3 | }
4 |
5 | .pane__alt-parse,
6 | .pane__alt-parse__nav {
7 | .flex-container-row;
8 | }
9 |
10 | .pane__alt-parse__meta {
11 | .u-w100;
12 | .flex-container-column;
13 | flex: 2;
14 | }
15 |
16 | .pane__alt-parse__nav {
17 | margin-right: auto;
18 | max-width: 90/@em;
19 | }
20 |
21 | .pane__alt-parse__nav__trigger-prev,
22 | .pane__alt-parse__nav__trigger-next {
23 | cursor: pointer;
24 | padding: 6/@em 1/@em 3/@em 2/@em;
25 |
26 | svg {
27 | width: 30/@em;
28 | height: 31/@em;
29 | fill: @pane-panel-header-text;
30 | transition: fill @node-transition-duration @node-transition-ease;
31 | }
32 |
33 | &.pane__alt-parse__nav--hover {
34 | svg {
35 | fill: lighten(@pane-panel-header-text, 20%);
36 | }
37 | }
38 |
39 | &.pane__alt-parse__nav--active {
40 | svg {
41 | fill: fade(@pane-panel-header-text, 50%);
42 | transition-duration: 0s;
43 | }
44 | }
45 |
46 | &.pane__alt-parse__nav--disabled {
47 | cursor: default;
48 |
49 | svg {
50 | fill: fade(@pane-panel-header-text, 15%);
51 | }
52 |
53 | &.pane__alt-parse__nav--loading {
54 | svg {
55 | fill: fade(@pane-panel-header-text, 50%);
56 | }
57 | }
58 | }
59 | }
60 |
61 | .pane__alt-parse__empty {
62 | margin-top: 1/@em;
63 |
64 | span {
65 | .t-smooth;
66 | color: @meta-table-td;
67 | font-size: 13/@em;
68 | opacity: .25;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/module/TreeExpansionControl.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 |
4 | class TreeExpansionControl extends React.Component {
5 | render() {
6 | const { mode, onClick } = this.props;
7 |
8 | // Enforce valid values of mode property
9 | const validModes = new Set([
10 | "explode",
11 | "implode",
12 | ]);
13 | if (!validModes.has(mode)) {
14 | throw new Error(`Invalid value of property "mode". Expected ("${Array.from(validModes).join("\" or \"")}") but found "${mode}" instead.`);
15 | }
16 |
17 | return (
18 |
29 | );
30 | }
31 | }
32 |
33 | TreeExpansionControl.propTypes = {
34 | mode: PropTypes.string,
35 | onClick: PropTypes.func,
36 | }
37 |
38 | export default TreeExpansionControl;
39 |
--------------------------------------------------------------------------------
/EXAMPLES.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | ## In a web page:
4 |
5 | Here's an example, showing how the dependency parse for the sentence `"Sam likes boats"` can be
6 | rendered using `hierplane`:
7 |
8 | ```
9 |
10 |
11 | Hierplane!
12 |
13 |
14 |
15 |
16 |
58 |
59 |
60 | ```
61 |
--------------------------------------------------------------------------------
/src/module/ParseTreeToolbar.js:
--------------------------------------------------------------------------------
1 | import { collapseAllNodes, expandAllNodes } from './stores/modules/ui';
2 |
3 | import { connect } from 'react-redux';
4 | import PropTypes from 'prop-types';
5 | import React, { Component } from 'react';
6 |
7 | import TreeExpansionControl from './TreeExpansionControl.js';
8 |
9 | class ParseTreeToolbar extends Component {
10 | static get propTypes() {
11 | return {
12 | collapseAllNodes: PropTypes.func.isRequired,
13 | expandAllNodes: PropTypes.func.isRequired,
14 | disabled: PropTypes.bool,
15 | };
16 | }
17 |
18 | render() {
19 | const { collapseAllNodes, expandAllNodes, disabled } = this.props;
20 |
21 | return (
22 |
23 |
24 | { collapseAllNodes() }}/>
25 | Collapse all nodes
26 |
27 |
28 |
29 | { expandAllNodes() }} />
30 | Expand all nodes
31 |
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | const mapStateToProps = ({ ui }) => ({ exploded: ui.exploded });
39 |
40 | export default connect(mapStateToProps, { collapseAllNodes, expandAllNodes })(ParseTreeToolbar);
41 |
--------------------------------------------------------------------------------
/src/less/explainer/ft.less:
--------------------------------------------------------------------------------
1 | // Faux Table
2 |
3 | .ft {
4 | display: table;
5 | margin-top: @node-word-margin;
6 | height: 100%;
7 | }
8 |
9 | .ft__tr {
10 | display: table-row;
11 | height: 100%;
12 | }
13 |
14 | .ft__tr__td {
15 | display: table-cell;
16 | position: relative;
17 | vertical-align: top;
18 | text-align: center;
19 | height: 100%;
20 | }
21 |
22 | .ft--middle-parent {
23 | height: 0; // TODO(aarons): see if this hack can be removed if/when Chrome addresses their table cell content height bug
24 |
25 | & > .node {
26 | height: 100%;
27 | margin-top: 0;
28 | margin-bottom: 0;
29 |
30 | &[data-has-side-children="true"] > .node__word {
31 | height: 100%;
32 | }
33 | }
34 | }
35 |
36 | .ft--middle-children {
37 | padding: 0 @node-word-margin;
38 | }
39 |
40 | .ft--left-children,
41 | .ft--root-event > .ft__tr > .ft--middle-children,
42 | .ft--right-children {
43 | padding-bottom: @node-word-margin;
44 | }
45 |
46 | .ft--root-event + .ft--root-event {
47 | margin-left: @node-word-margin * 2;
48 | }
49 |
50 | // Preventing extra space around side children.
51 |
52 | .ft--no-left-children {
53 | margin-left: @node-word-margin;
54 |
55 | & > .ft__tr {
56 | & > .ft--middle-parent > .node {
57 | margin-left: 0;
58 | }
59 |
60 | & > .ft--middle-children {
61 | padding-left: 0;
62 | }
63 | }
64 | }
65 |
66 | .ft--no-right-children {
67 | margin-right: @node-word-margin;
68 |
69 | & > .ft__tr {
70 | & > .ft--middle-parent > .node {
71 | margin-right: 0;
72 | }
73 |
74 | & > .ft--middle-children {
75 | padding-right: 0;
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/module/Toolbar.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 |
4 | // `Toolbar` displays the Euclid tool buttons.
5 | class Toolbar extends React.Component {
6 | constructor() {
7 | super();
8 |
9 | this.handleClick = this.handleClick.bind(this);
10 | }
11 |
12 | handleClick() {
13 | const win = window.open("", "_blank");
14 | const data = this.props.selectedData;
15 | const hasParses = data.hasOwnProperty("alternateParseInfo") && data.alternateParseInfo.hasOwnProperty("currentParseIndex");
16 |
17 | if (win) {
18 | win.document.write("" + JSON.stringify(this.props.jsonData, null, 2) + " ");
19 | win.document.title = `${hasParses ? "Parse: " + data.alternateParseInfo.currentParseIndex + " | " : ""}Node: ${data.id}`;
20 | win.focus();
21 | } else {
22 | alert('Please allow popups for this website');
23 | }
24 | }
25 |
26 | render() {
27 | const { jsonUrl,
28 | serverEndPoint } = this.props;
29 |
30 | let jsonBtn;
31 | if (serverEndPoint) {
32 | jsonBtn = (JSON );
33 | } else {
34 | jsonBtn = (JSON
);
35 | }
36 |
37 | return (
38 |
39 |
40 | {jsonBtn}
41 |
42 |
43 | );
44 | }
45 | }
46 |
47 | Toolbar.propTypes = {
48 | jsonUrl: PropTypes.string,
49 | serverEndPoint: PropTypes.bool,
50 | jsonData: PropTypes.object,
51 | selectedData: PropTypes.object,
52 | }
53 |
54 | export default Toolbar;
55 |
--------------------------------------------------------------------------------
/src/less/explainer/node/node--segments-container.less:
--------------------------------------------------------------------------------
1 | .node.node--segments-container {
2 | margin-bottom: @node-word-margin * 2;
3 |
4 | .node__segments {
5 | .node-focus-trigger {
6 | z-index: auto;
7 | }
8 |
9 | .node__word__ui--parse-nav {
10 | .node__word__ui__glyph {
11 | border: none;
12 | }
13 |
14 | .node__word__ui__glyph--left {
15 | margin-right: 0;
16 | }
17 |
18 | .node__word__ui__glyph--right {
19 | margin-left: 0;
20 | }
21 |
22 | .node__word__ui__glyph__svg {
23 | opacity: 0;
24 | }
25 |
26 | .node__word__ui__glyph__svg--inverted {
27 | opacity: 1;
28 | fill: fade(@white, 27%);
29 | }
30 |
31 | .parse-nav-trigger-left:not(.node__word__ui--disabled),
32 | .parse-nav-trigger-right:not(.node__word__ui--disabled) {
33 | &:hover {
34 | .node__word__ui__glyph {
35 | background: none;
36 | border: none;
37 |
38 | .node__word__ui__glyph__svg--inverted {
39 | fill: fade(@white, 50%);
40 | }
41 | }
42 | }
43 |
44 | &:active {
45 | .node__word__ui__glyph {
46 | opacity: .25;
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
54 | .node.node--segments-container.node--has-alt-parses {
55 | position: relative;
56 | box-shadow: inset 0 0 0 2/@em fade(@white, 5.5%);
57 | padding: 30/@em 30/@em 40/@em 30/@em;
58 | margin: @node-word-margin (@node-word-margin * 2) (@node-word-margin * 2) (@node-word-margin * 2) !important;
59 |
60 | &.node--focused {
61 | box-shadow: inset 0 0 0 2/@em @focused-ui;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/dev/dev.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sample trees to be used while developing locally, using `dev/static.html`.
3 | */
4 | (function() {
5 | const sampleTrees = [];
6 |
7 | function renderSelectTreeUI(onTreeChanged = () => {}) {
8 | return (
9 | fetch('/api/samples')
10 | .then(res => res.json())
11 | .then(samples => {
12 | sampleTrees.push.apply(sampleTrees, samples);
13 |
14 | const div = document.createElement('div');
15 | div.classList.add('hpdev__select-tree');
16 |
17 | const select = document.createElement('select');
18 | select.addEventListener('change', onTreeChanged);
19 |
20 | sampleTrees.forEach((tree, idx) => {
21 | const option = document.createElement('option');
22 | option.setAttribute('value', idx);
23 | option.textContent = tree.text;
24 | select.appendChild(option);
25 | });
26 |
27 | div.appendChild(select);
28 |
29 | document.body.insertBefore(div, document.body.firstElementChild);
30 |
31 | renderTree(0);
32 | })
33 | .catch(err => {
34 | console.error('Error fetching sample trees:', err);
35 | })
36 | );
37 | };
38 |
39 | function getTreeAtIdx(idx) {
40 | if (idx < 0 || idx >= sampleTrees.length) {
41 | throw new Error(`No tree at index ${idx}`);
42 | }
43 | return sampleTrees[idx];
44 | };
45 |
46 | const containerId = 'tree';
47 | const container = document.getElementById(containerId);
48 | let unmount = null;
49 | function renderTree(idx) {
50 | if (typeof unmount == 'function') {
51 | unmount();
52 | unmount = null;
53 | }
54 | unmount = hierplane.renderTree(getTreeAtIdx(idx), { target: `#${containerId}` });
55 | }
56 |
57 | renderSelectTreeUI(event => { renderTree(event.target.value) })
58 | })();
59 |
--------------------------------------------------------------------------------
/dev/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const chalk = require('chalk');
4 | const express = require('express');
5 | const morgan = require('morgan');
6 | const fs = require('fs');
7 | const path = require('path');
8 |
9 | const app = express();
10 |
11 | app.use(morgan('combined'));
12 | app.use(express.static(__dirname));
13 |
14 | const dataDir = path.resolve(__dirname, 'data');
15 | app.get('/api/samples', (req, res) => {
16 | fs.readdir(dataDir, (err, files) => {
17 | // We load all of the files into a single response. If this ever gets too huge, we'll
18 | // need to send a dictionary back to the client and enable the actual JSON to be
19 | // queried on-demand. It'd also be better would be to stream things to the client, but that's
20 | // all overkill at this point.
21 | Promise.all(
22 | files.map(file => new Promise((resolve, reject) => {
23 | fs.readFile(
24 | path.resolve(__dirname, 'data', file),
25 | 'utf-8',
26 | (err, data) => {
27 | if (err) {
28 | reject(err);
29 | } else {
30 | try {
31 | resolve(JSON.parse(data));
32 | } catch (err) {
33 | reject({
34 | message: `error parsing ${file}`,
35 | cause: err.toString()
36 | });
37 | }
38 | }
39 | }
40 | );
41 | }))
42 | ).then(samples => {
43 | // It's kind of weird that we have to convert these from and back to JSON, but it allows
44 | // us to take advantage of express' built in `json()` API.
45 | res.json(samples);
46 | }).catch(err => {
47 | res.status(500).json(err);
48 | });
49 | });
50 | });
51 |
52 | const port = process.env.HIERPLANE_DEV_SERVER_PORT || 3000;
53 | app.listen(port, () => {
54 | console.log(`listening at ${chalk.blue(`http://localhost:${port}`)}`);
55 | });
56 |
--------------------------------------------------------------------------------
/src/module/node/UiParseNav.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import Icon from '../Icon.js';
4 |
5 | class UiParseNav extends React.Component {
6 | render() {
7 | const { readOnly,
8 | onPnMouseOver,
9 | onPnMouseOut,
10 | onPnMouseUp,
11 | data } = this.props;
12 |
13 | const altParseInfoExists = data.hasOwnProperty("alternateParseInfo") && data.alternateParseInfo !== undefined;
14 |
15 | const arrowIcons = (direction) => {
16 | return (
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | const createNavButton = (direction, target) => {
25 | return altParseInfoExists && data.alternateParseInfo.hasOwnProperty(`${target}Parse`) && !readOnly ? (
26 | { onPnMouseUp(data, target) }}>
30 | {arrowIcons(direction)}
31 |
32 | ) : (
33 |
34 | {arrowIcons(direction)}
35 |
36 | );
37 | }
38 |
39 | return (
40 |
41 | {createNavButton("left", "prev")}
42 | {createNavButton("right", "next")}
43 |
44 | );
45 | }
46 | }
47 |
48 | UiParseNav.propTypes = {
49 | readOnly: PropTypes.bool,
50 | data: PropTypes.object,
51 | onPnMouseOver: PropTypes.func,
52 | onPnMouseOut: PropTypes.func,
53 | onPnMouseUp: PropTypes.func,
54 | }
55 |
56 | export default UiParseNav;
57 |
--------------------------------------------------------------------------------
/src/less/explainer/node/node__word__link.less:
--------------------------------------------------------------------------------
1 | // Node Links
2 |
3 | .node__word__link {
4 | display: inline-block;
5 | position: relative;
6 | z-index: 9;
7 |
8 | &,
9 | & * {
10 | .fn-backface-visibility(hidden);
11 | }
12 |
13 | .node__word__link__tab {
14 | .flex-container-hcentered;
15 | position: relative;
16 | filter: drop-shadow(1/@em 2/@em 2/@em rgba(0,0,0,.15));
17 | }
18 |
19 | .node__word__link__tab__left-cap,
20 | .node__word__link__tab__right-cap {
21 | min-height: 21/@em;
22 | width: 14/@em;
23 | position: relative;
24 | }
25 |
26 | .node__word__link__tab__top-cap,
27 | .node__word__link__tab__bottom-cap {
28 | min-width: 21/@em;
29 | height: 14/@em;
30 | position: relative;
31 | }
32 |
33 | // This fixes a zoom rendering bug that displays annoying gaps between caps.
34 | .node__word__link__tab__left-cap {
35 | .fn-transform(translateX(0.5/@em));
36 | }
37 | .node__word__link__tab__right-cap {
38 | .fn-transform(translateX(-0.5/@em));
39 | }
40 | .node__word__link__tab__top-cap {
41 | .fn-transform(translateY(0.5/@em));
42 | }
43 | .node__word__link__tab__bottom-cap {
44 | .fn-transform(translateY(-0.5/@em));
45 | }
46 |
47 | .node__word__link__label {
48 | .flex-container-centered;
49 | background: @link-bg-color;
50 | border-bottom: 1/@em solid @link-border-color;
51 | min-width: @node-word-link-label-size;
52 | min-height: @node-word-link-label-size;
53 | text-align: center;
54 |
55 | span {
56 | .t-uppercase;
57 | .u-nowrap;
58 | color: @link-text-color;
59 | font-size: 10/@em;
60 | font-weight: bold;
61 | }
62 | }
63 |
64 | .node__word__link__tab svg {
65 | height: 100%;
66 | width: 100%;
67 | left: 0;
68 | top: 0;
69 | position: absolute;
70 | }
71 |
72 | .node__word__link__tab__svg__fill {
73 | fill: @link-bg-color;
74 | }
75 |
76 | .node__word__link__tab__svg__stroke {
77 | fill: none;
78 | stroke: @link-border-color;
79 | stroke-width: 1;
80 | stroke-miterlimit: 10;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/module/pane/AltParseNavToggle.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import Icon from '../Icon.js';
4 |
5 | class AltParseNavToggle extends React.Component {
6 | constructor() {
7 | super();
8 | this.state = {
9 | interaction: "idle", // idle, hover, active
10 | };
11 |
12 | this.handleMouseUp = this.handleMouseUp.bind(this);
13 | }
14 |
15 | handleMouseUp() {
16 | this.setState({
17 | interaction: "hover",
18 | });
19 | this.props.fetchAltParse(this.props.selectedData, this.props.direction);
20 | }
21 |
22 | render() {
23 | const { direction, keyInteraction, disabled, loading } = this.props;
24 | const icoArrow = ( );
25 |
26 | let altParseTrigger = null;
27 |
28 | if (disabled === false) {
29 | altParseTrigger = (
30 | {this.setState({interaction: "hover"})}}
34 | onMouseOut={() => {this.setState({interaction: "idle"})}}
35 | onMouseDown={() => {this.setState({interaction: "active"})}}
36 | onMouseUp={this.handleMouseUp}>
37 | {icoArrow}
38 |
39 | );
40 | } else {
41 | altParseTrigger = (
42 | {this.setState({interaction: "idle"})}}>
45 | {icoArrow}
46 |
47 | );
48 | }
49 |
50 | return altParseTrigger;
51 | }
52 | }
53 |
54 | AltParseNavToggle.propTypes = {
55 | direction: PropTypes.string,
56 | keyInteraction: PropTypes.string,
57 | fetchAltParse: PropTypes.func,
58 | selectedData: PropTypes.object,
59 | disabled: PropTypes.bool,
60 | loading: PropTypes.bool,
61 | }
62 |
63 | export default AltParseNavToggle;
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hierplane",
3 | "version": "0.2.2",
4 | "description": "A javascript library for visualizing hierarchical data, specifically tailored towards rendering dependency parses.",
5 | "files": [
6 | "dist/**/*.js",
7 | "dist/**/*.css"
8 | ],
9 | "main": "dist/module/index.js",
10 | "scripts": {
11 | "build": "node ./bin/build.js",
12 | "minify": "uglifyjs dist/static/hierplane.bundle.js --compress --mangle -o dist/static/hierplane.min.js",
13 | "prepare": "npm run clean && npm run build && npm run minify",
14 | "watch": "node ./bin/build.js --watch",
15 | "start": "node ./bin/build.js --server --watch",
16 | "test": "mocha --require @babel/register 'src/**/*.test.js'",
17 | "clean": "rm -rf dist/"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/allenai/hierplane.git"
22 | },
23 | "author": "The Euclid Team ",
24 | "license": "Apache-2.0",
25 | "bugs": {
26 | "url": "https://github.com/allenai/hierplane/issues"
27 | },
28 | "homepage": "https://github.com/allenai/hierplane#readme",
29 | "peerDependencies": {
30 | "react": ">=16"
31 | },
32 | "dependencies": {
33 | "classnames": "2.2.5",
34 | "immutable": "3.8.1",
35 | "less-plugin-autoprefix": "1.5.1",
36 | "merge": "^1.2.1",
37 | "react-redux": "6.0.1",
38 | "redux": "4.0.1",
39 | "superagent": "3.7.0"
40 | },
41 | "devDependencies": {
42 | "@babel/cli": "^7.5.5",
43 | "@babel/core": "^7.5.5",
44 | "@babel/plugin-proposal-export-default-from": "^7.5.2",
45 | "@babel/preset-env": "^7.5.5",
46 | "@babel/preset-react": "^7.0.0",
47 | "@babel/register": "^7.5.5",
48 | "babel-preset-env": "1.6.1",
49 | "browserify": "14.5.0",
50 | "chai": "3.5.0",
51 | "chalk": "2.3.0",
52 | "chokidar": "^3.0.2",
53 | "debounce": "1.1.0",
54 | "express": "4.16.2",
55 | "less": "2.7.2",
56 | "mocha": "^6.2.0",
57 | "morgan": "^1.9.1",
58 | "npm-which": "3.0.1",
59 | "prop-types": "15.7.2",
60 | "react": "16.8.6",
61 | "react-dom": "16.8.6",
62 | "uglify-js": "3.1.5",
63 | "watchify": "^3.11.1"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/less/explainer/main-stage.less:
--------------------------------------------------------------------------------
1 | #main-stage {
2 | .u-w100;
3 | .flex-container;
4 | .u-select-none;
5 | .flex-grow(1);
6 | .fn-backface-visibility(hidden);
7 | overflow: auto;
8 | background: @main-stage-bg-color;
9 | cursor: default;
10 | position: relative;
11 |
12 | .main-stage__loading-mask,
13 | .main-stage__defocus-trigger {
14 | .u-child100;
15 | }
16 |
17 | .main-stage__tree-container,
18 | .main-stage__error-container {
19 | padding: @node-word-margin;
20 | margin: auto;
21 | }
22 |
23 | .main-stage__tree-container {
24 | opacity: 0;
25 | padding: (@node-word-margin * 6) @node-word-margin;
26 | }
27 |
28 | .main-stage__error-container,
29 | .main-stage__tree-container.main-stage--rendered {
30 | animation: content-fade .3s ease-in-out 1 forwards;
31 | }
32 |
33 | @keyframes content-fade {
34 | 0% {opacity: 0;}
35 | 100% {opacity: 1;}
36 | }
37 |
38 | &.main-stage--loading {
39 | .main-stage__loading-mask {
40 | .flex-container-centered;
41 | z-index: 99999;
42 |
43 | .main-stage__loading-mask__spinbox {
44 | .u-child100;
45 | .flex-container-centered;
46 | animation: spinbox-fade .7s ease-in-out 1;
47 | }
48 |
49 | @keyframes spinbox-fade {
50 | 0% {opacity: 0;}
51 | 100% {opacity: 1;}
52 | }
53 | }
54 |
55 | &.main-stage--fade-delay {
56 | .main-stage__tree-container,
57 | .main-stage__error-container {
58 | animation: tree-fade-delay 4s ease-in-out 1 forwards;
59 | }
60 |
61 | @keyframes tree-fade-delay {
62 | 0% {opacity: 1;.fn-filter(blur(0/@em) contrast(1) brightness(1) grayscale(.5));}
63 | 15% {opacity: 1;.fn-filter(blur(1.5/@em) contrast(.8) brightness(.8) grayscale(.35));}
64 | 100% {opacity: .3;.fn-filter(blur(6/@em) contrast(.8) brightness(.8) grayscale(.35));}
65 | }
66 |
67 | .main-stage__loading-mask {
68 | .main-stage__loading-mask__spinbox {
69 | animation: spinbox-fade-slow 4s ease-in-out 1;
70 | }
71 |
72 | @keyframes spinbox-fade-slow {
73 | 0% {opacity: 0;}
74 | 15% {opacity: 0;}
75 | 100% {opacity: 1;}
76 | }
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/less/explainer/pane/pane__fragments.less:
--------------------------------------------------------------------------------
1 | .pane__fragments {
2 | min-height: 56/@em;
3 | padding: 2/@em 0;
4 | position: relative;
5 |
6 | &:after {
7 | .u-pe;
8 | position: absolute;
9 | display: block;
10 | height: 100%;
11 | width: 40/@em;
12 | z-index: 99999;
13 | right: 0;
14 | top: 0;
15 |
16 | background: -moz-linear-gradient(left, fade(@pane-bg, 0%) 0%, fade(@pane-bg, 100%) 100%);
17 | background: -webkit-linear-gradient(left, fade(@pane-bg, 0%) 0%,fade(@pane-bg, 100%) 100%);
18 | background: linear-gradient(to right, fade(@pane-bg, 0%) 0%,fade(@pane-bg, 100%) 100%);
19 | }
20 |
21 | .pane__fragments__nowrap-container {
22 | display: table;
23 | table-layout: fixed;
24 | width: 100%;
25 | white-space: nowrap;
26 | margin-bottom: -4/@em;
27 | }
28 |
29 | .pane__fragments__nowrap-container__truncation-container {
30 | display: table-cell;
31 | overflow: hidden;
32 | text-overflow: ellipsis;
33 | padding-top: 4/@em;
34 | }
35 |
36 | .fragment {
37 | .u-select-none;
38 | display: inline-block;
39 | border: 2/@em solid transparent;
40 | padding: 15/@em;
41 | position: relative;
42 | overflow: hidden;
43 | cursor: default;
44 |
45 | .fragment__type {
46 | .t-smooth;
47 | .t-uppercase;
48 | text-shadow: 0 0 5/@em rgba(0,0,0,.5);
49 | top: -2/@em;
50 | left: -2/@em;
51 | position: absolute;
52 | display: block;
53 | font-size: 9/@em;
54 | color: fade(@white, 80%);
55 | font-weight: bold;
56 | padding: 3.5/@em 7.5/@em;
57 | }
58 |
59 | .fragment__text {
60 | .t-smooth;
61 | color: fade(@white, 80%);
62 | font-size: 12/@em;
63 | }
64 |
65 | &.fragment--child {
66 | border-color: #5c8b9b;
67 | background: fade(#5c8b9b, 30%);
68 |
69 | .fragment__type {
70 | background: #5c8b9b;
71 | }
72 | }
73 |
74 | &.fragment--self {
75 | border-color: @focused-ui;
76 | background: fade(@focused-ui, 50%);
77 |
78 | .fragment__type {
79 | background: @focused-ui;
80 | }
81 |
82 | .fragment__text {
83 | color: @white;
84 | }
85 | }
86 |
87 | &.fragment--ignored {
88 | border-color: #797e85;
89 | background: fade(#797e85, 30%);
90 | opacity: .66;
91 |
92 | .fragment__type {
93 | background: #797e85;
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/less/functions.less:
--------------------------------------------------------------------------------
1 | .fn-transform(@v) {
2 | -webkit-transform: @v;
3 | -ms-transform: @v;
4 | transform: @v;
5 | }
6 |
7 | .fn-transform-origin(@v) {
8 | -ms-transform-origin: @v;
9 | -webkit-transform-origin: @v;
10 | transform-origin: @v;
11 | }
12 |
13 | .fn-transition-transform(@d:@sec, @e:@eas) {
14 | transition: -webkit-transform @d @e;
15 | transition: -ms-transform @d @e;
16 | transition: transform @d @e;
17 | }
18 |
19 | .fn-transition-transform-opacity(@d:@sec, @e:@eas) {
20 | transition: opacity @d @e, -webkit-transform @d @e;
21 | transition: opacity @d @e, -ms-transform @d @e;
22 | transition: opacity @d @e, transform @d @e;
23 | }
24 |
25 | .fn-user-select(@v) {
26 | -webkit-user-select: @v;
27 | -moz-user-select: @v;
28 | -ms-user-select: @v;
29 | user-select: @v;
30 | }
31 |
32 | .fn-backface-visibility(@v) {
33 | -webkit-backface-visibility: @v;
34 | backface-visibility: @v;
35 | }
36 |
37 | .fn-transition(@p:all, @d:@sec, @e:@eas) {
38 | transition: @p @d @e;
39 | }
40 |
41 | .fn-filter(@v) {
42 | -webkit-filter: @v;
43 | filter: @v;
44 | }
45 |
46 | .fn-gradient(@v) {
47 | .fn-subtle-gradient(@v);
48 |
49 | &:after {
50 | .fn-gradient-contrast(@v);
51 | }
52 | }
53 |
54 | .fn-subtle-gradient(@v) {
55 | background: @v;
56 | background: -moz-linear-gradient(top, lighten(@v, 8%) 0%, darken(@v, 8%) 100%);
57 | background: -webkit-linear-gradient(top, lighten(@v, 8%) 0%,darken(@v, 8%) 100%);
58 | background: linear-gradient(to bottom, lighten(@v, 8%) 0%,darken(@v, 8%) 100%);
59 | }
60 |
61 | .fn-gradient-contrast(@v) {
62 | background: @v;
63 | background: -moz-linear-gradient(top, lighten(@v, 18%) 0%, darken(@v, 2%) 100%);
64 | background: -webkit-linear-gradient(top, lighten(@v, 18%) 0%,darken(@v, 2%) 100%);
65 | background: linear-gradient(to bottom, lighten(@v, 18%) 0%,darken(@v, 2%) 100%);
66 | }
67 |
68 | .fn-trans-gradient(@v) {
69 | background: -moz-linear-gradient(top, fade(@v, 100%) 0%, fade(@v, 0%) 100%);
70 | background: -webkit-linear-gradient(top, fade(@v, 100%) 0%,fade(@v, 0%) 100%);
71 | background: linear-gradient(to bottom, fade(@v, 100%) 0%,fade(@v, 0%) 100%);
72 | }
73 |
74 | .fn-placeholder-color(@v) {
75 | ::-webkit-input-placeholder {
76 | color: @v;
77 | }
78 |
79 | :-moz-placeholder {
80 | color: @v;
81 | }
82 |
83 | ::-moz-placeholder {
84 | color: @v;
85 | }
86 |
87 | :-ms-input-placeholder {
88 | color: @v;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/less/explainer/parse-tree-toolbar.less:
--------------------------------------------------------------------------------
1 | .parse-tree-toolbar {
2 | .flex-container-align-right;
3 | .u-select-none;
4 | .u-mp0;
5 | width: 140/@em;
6 | position: absolute;
7 | min-height: 44/@em;
8 | top: 0;
9 | right: 0;
10 | z-index: 9;
11 | transition: opacity @node-transition-duration @node-transition-ease,
12 | width .15s linear .15s,
13 | z-index @node-transition-duration @node-transition-ease;
14 |
15 | &:before {
16 | .u-pe100;
17 | background: darken(@main-stage-bg-color, 4%);
18 | border-bottom: 2/@em solid @passage-border-color;
19 | box-shadow: 0 1/@em 2/@em fade(darken(@main-stage-bg-color, 10%), 50%),
20 | 0 0 100/@em fade(@main-stage-bg-color, 66%);
21 | opacity: 0;
22 | transition: opacity .15s @node-transition-ease;
23 | }
24 |
25 | &:after {
26 | .u-pe;
27 | .u-100;
28 | width: 180/@em;
29 | position: absolute;
30 | display: block;
31 | top: 0;
32 | right: 92/@em;
33 | }
34 |
35 | &,
36 | &.parse-tree-toolbar__item {
37 | list-style: none;
38 | }
39 |
40 | .parse-tree-toolbar__item {
41 | .u-mp0;
42 | transition: opacity .15s @node-transition-ease;
43 | display: block;
44 | min-width: 24/@em;
45 | min-height: 24/@em;
46 | opacity: .25;
47 |
48 | &:last-child {
49 | padding-right: 8/@em;
50 | }
51 |
52 | .parse-tree-toolbar__item__mask {
53 | .u-child100;
54 | z-index: 999;
55 | display: none;
56 | }
57 |
58 | .parse-tree-toolbar__item__label {
59 | .u-select-none;
60 | width: 130/@em;
61 | position: absolute;
62 | top: 25/@em;
63 | right: 130/@em;
64 | text-align: right;
65 | font-size: 13/@em;
66 | white-space: nowrap;
67 | z-index: 0;
68 | opacity: 0;
69 | transition: opacity .3s @node-transition-ease;
70 | cursor: default;
71 | }
72 |
73 | &:hover {
74 | .parse-tree-toolbar__item__label {
75 | opacity: .5;
76 | transition-duration: 1s;
77 | }
78 | }
79 | }
80 |
81 | &:hover {
82 | .u-w100;
83 | transition: width 0s linear 0s;
84 |
85 | &:before {
86 | opacity: .975;
87 | }
88 |
89 | .parse-tree-toolbar__item {
90 | opacity: 1;
91 | }
92 | }
93 |
94 | &.parse-tree-toolbar--disabled {
95 | opacity: 0;
96 | z-index: -9;
97 |
98 | .parse-tree-toolbar__item__mask {
99 | display: block;
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/less/colors.less:
--------------------------------------------------------------------------------
1 | // Console
2 |
3 | // General
4 | @white: #ffffff;
5 | @black: #000000;
6 |
7 | // AI2 Colors
8 |
9 | @blue-dark: #2f8cbb;
10 | @blue-light: #49affa;
11 | @blue-lighter: #29a1fd;
12 | @blue-light-muted: #8acde8;
13 | @blue-gray-light: #e2ecf2;
14 |
15 | @gray-slate: #27292a;
16 | @gray-dark: #3e4346;
17 | @gray-darker: #38393a;
18 | @gray-medium-dark: #717172;
19 | @gray-disabled: #959595;
20 | @gray-medium: #8c9296;
21 | @gray-medium-light: #929292;
22 | @gray-light: #e0e0e0;
23 | @gray-last-query: #3d3f40;
24 | @gray-historical-query: #313233;
25 | @gray-historical-query-hover: #333434;
26 | @working: #53cdde;
27 |
28 | @orange-dark: #df652d;
29 | @orange: #fdc936;
30 | @yellow: #fdea65;
31 |
32 | @green: #45b336;
33 | @purple: #863591;
34 |
35 | // Translucent
36 | @trans-black-light: fade(@black, 11%);
37 | @trans-blue-light: rgba(124,195,247,.2);
38 | @trans-white-faded: fade(@white, 4%);
39 | @trans-white-lighter: fade(@white, 7%);
40 |
41 | // Tree Rendering
42 |
43 | // Passage
44 | @passage-bg-color: #383c43;
45 | @passage-border-color: #31343b;
46 | @passage-text-color: #ccc;
47 | @passage-active-text-color: #d8d8d8;
48 | @passage-edit-bg: #26282a;
49 |
50 | // Tree
51 | @main-stage-bg-color: #3b4047;
52 |
53 | // Nodes
54 | @color0: #999a9d;
55 | @color1: #9ed084;
56 | @color2: #9ac5e2;
57 | @color3: #ef859c;
58 | @color4: #e5d594;
59 | @color5: #e991e8;
60 | @color6: #8bd6db;
61 | @color0-focused: fade(#a7a7a7, 50%);
62 | @color1-focused: #489b36;
63 | @color2-focused: @focused-node;
64 | @color3-focused: #d21e45;
65 | @color4-focused: #c2a400;
66 | @color5-focused: #cb21c9;
67 | @color6-focused: #0092a3;
68 |
69 | @placeholder-border: #c3c3c3;
70 | @placeholder-border-hover: #88d4fe;
71 | @node-text-color: @black;
72 | @node-border-color: @black;
73 | @node-text-shadow-color: fade(@white, 70%);
74 | @node-box-shadow-color: fade(@black, 26%);
75 | @node-focused-shadow-color: rgba(62,126,255,.38);
76 |
77 | // Faded Node
78 | @color-faded: fade(@white, 5%);
79 | @color-faded-text-color: fade(@white, 50%);
80 | @color-faded-border-color: fade(@white, 50%);
81 | @color-faded-text-shadow-color: fade(@white, 30%);
82 | @color-faded-box-shadow-color: fade(@black, 5%);
83 |
84 | // Attr
85 | @attr-text-color: fade(@white, 75%);
86 | @attr-bg-color: fade(@black, 40%);
87 |
88 | // Link
89 | @link-bg-color: #c2d0d3;
90 | @link-border-color: @black;
91 | @link-text-color: fade(@black, 75%);
92 |
93 | // Pane
94 | @pane-bg: #4a4f56;
95 | @pane-panel-separator: #31343b;
96 | @pane-empty-text: #838890;
97 | @pane-panel-header-bg: #383c43;
98 | @pane-panel-header-text: #989da6;
99 | @code-bg: #2a2e33;
100 | @code-text: #c6cfdc;
101 |
102 | @meta-table-th: #b6bcc6;
103 | @meta-table-td: #e3e4e4;
104 |
105 | // Active/Focused
106 | @blue: #0000f2;
107 | @focused-node: #167afb;
108 | @focused-ui: #3893fc;
109 |
--------------------------------------------------------------------------------
/src/less/explainer/node/node__word__ui.less:
--------------------------------------------------------------------------------
1 | .node__word__ui {
2 | position: absolute;
3 | text-align: center;
4 | top: 0;
5 | padding: 6/@em;
6 | z-index: 999999 !important;
7 | cursor: pointer;
8 |
9 | .node__word__ui__glyph {
10 | position: relative;
11 | width: 20/@em;
12 | height: 20/@em;
13 | border-radius: 3/@em;
14 | border: 1/@em solid fade(@black, 33%);
15 | box-sizing: border-box;
16 | transition: color @node-transition-duration @node-transition-ease,
17 | border-color @node-transition-duration @node-transition-ease,
18 | opacity @node-transition-duration @node-transition-ease;
19 |
20 | .node__word__ui__glyph__svg,
21 | .node__word__ui__glyph__svg--inverted {
22 | .u-tl;
23 | position: absolute;
24 | top: -1/@em;
25 | width: 20/@em;
26 | height: 20/@em;
27 | fill: fade(@black, 33%);
28 | transition: fill @node-transition-duration @node-transition-ease,
29 | opacity @node-transition-duration @node-transition-ease;
30 | }
31 |
32 | .node__word__ui__glyph__svg--inverted {
33 | opacity: 0;
34 | }
35 | }
36 | }
37 |
38 | .node__word__ui--toggle {
39 | left: 0;
40 |
41 | .node__word__ui__glyph {
42 | .node__word__ui__glyph__svg {
43 | left: -1/@em;
44 | }
45 |
46 | .node__word__ui__glyph__svg--expand {
47 | opacity: 0;
48 | }
49 |
50 | .node__word__ui__glyph__svg--collapse {
51 | opacity: 1;
52 | }
53 | }
54 | }
55 |
56 | .node__word__ui--parse-nav {
57 | .flex-container-row;
58 | right: 0;
59 | padding: 0;
60 | cursor: default;
61 |
62 | .parse-nav-trigger-left,
63 | .parse-nav-trigger-right {
64 | cursor: pointer;
65 |
66 | &.node__word__ui--disabled {
67 | cursor: default;
68 |
69 | .node__word__ui__glyph {
70 | opacity: .25;
71 | }
72 | }
73 | }
74 |
75 | .node__word__ui__glyph {
76 | margin: 6/@em;
77 | }
78 |
79 | .node__word__ui__glyph--left {
80 | margin-right: 0;
81 | border-top-right-radius: 0;
82 | border-bottom-right-radius: 0;
83 | border-top-left-radius: 3.5/@em;
84 | border-right-color: fade(@black, 15%);
85 | }
86 |
87 | .node__word__ui__glyph--right {
88 | border-top-left-radius: 0;
89 | border-bottom-left-radius: 0;
90 | border-left-color: fade(@black, 15%);
91 | margin-left: -1/@em;
92 | }
93 | }
94 |
95 | .node__word__ui--toggle:not(.node__word__ui--disabled) {
96 | &:hover {
97 | .node__word__ui__glyph {
98 | background: fade(@black, 8%);
99 | border-color: fade(@black, 50%);
100 | fill: fade(@black, 50%);
101 | }
102 | }
103 |
104 | &:active {
105 | .node__word__ui__glyph {
106 | opacity: .5;
107 | }
108 | }
109 | }
110 |
111 | .node__word__ui--parse-nav .parse-nav-trigger-left:not(.node__word__ui--disabled),
112 | .node__word__ui--parse-nav .parse-nav-trigger-right:not(.node__word__ui--disabled) {
113 | &:hover {
114 | .node__word__ui__glyph {
115 | background: fade(@black, 8%);
116 | border-color: @blue;
117 | color: @blue;
118 | }
119 | }
120 |
121 | &:active {
122 | .node__word__ui__glyph {
123 | opacity: .33;
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/module/node/Link.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import LinkSvg from './LinkSvg.js';
4 |
5 | class Link extends React.Component {
6 | render() {
7 | const { link, dataPos, layout, linkLabels, id } = this.props;
8 |
9 | let displayLink = null;
10 |
11 | // If a linkNameToLabel mapping exists, display that instead of node object's link value.
12 | if (linkLabels[link]) {
13 | displayLink = linkLabels[link];
14 | } else {
15 | displayLink = link;
16 | }
17 |
18 | const linkData = {
19 | left: {
20 | before: {
21 | capPos: "top",
22 | viewBox: "0 0 21 14",
23 | fillPoints: "21.3,14 0.5,14 0.5,13.7 21.3,0.4",
24 | strokePoints: "0.5,14 0.5,13.7 21.3,0.4",
25 | },
26 | after: {
27 | capPos: "bottom",
28 | viewBox: "0 0 21 14",
29 | fillPoints: "21.3,-0.1 0.5,-0.1 0.5,0.3 21.3,13.6",
30 | strokePoints: "0.5,0 0.5,0.3 21.3,13.6",
31 | },
32 | },
33 | right: {
34 | before: {
35 | capPos: "top",
36 | viewBox: "0 0 21 14",
37 | fillPoints: "-0.3,14 20.5,14 20.5,13.7 -0.3,0.4",
38 | strokePoints: "20.5,14 20.5,13.7 -0.3,0.4",
39 | },
40 | after: {
41 | capPos: "bottom",
42 | viewBox: "0 0 21 14",
43 | fillPoints: "-0.3,-0.1 20.5,-0.1 20.5,0.3 -0.3,13.6",
44 | strokePoints: "20.5,0 20.5,0.3 -0.3,13.6",
45 | },
46 | },
47 | inside: {
48 | before: {
49 | capPos: "top",
50 | viewBox: "0 0 34 11",
51 | fillPoints: "17,1.2 0.5,10.7 0.5,11 33.5,11 33.5,10.7",
52 | strokePoints: "33.5,11 33.5,10.7 17,1.2 0.5,10.7 0.5,11",
53 | },
54 | after: {
55 | capPos: "bottom",
56 | viewBox: "0 0 34 11",
57 | fillPoints: "17,9.8 33.5,0.3 33.5,0 0.5,0 0.5,0.3",
58 | strokePoints: "0.5,0 0.5,0.3 17,9.8 33.5,0.3 33.5,0",
59 | },
60 | },
61 | down: {
62 | before: {
63 | capPos: "left",
64 | viewBox: "0 0 14 21",
65 | fillPoints: "14.1,-0.3 14.1,20.5 13.7,20.5 0.4,-0.3",
66 | strokePoints: "14.1,20.5 13.7,20.5 0.4,-0.3",
67 | },
68 | after: {
69 | capPos: "right",
70 | viewBox: "0 0 14 21",
71 | fillPoints: "-0.1,-0.3 -0.1,20.5 0.3,20.5 13.6,-0.3",
72 | strokePoints: "0,20.5 0.3,20.5 13.6,-0.3",
73 | },
74 | },
75 | }
76 |
77 | return (
78 |
79 |
80 |
81 |
82 | {displayLink}
83 |
84 |
85 |
86 |
87 | );
88 | }
89 | }
90 |
91 | Link.propTypes = {
92 | link: PropTypes.string,
93 | id: PropTypes.string,
94 | dataPos: PropTypes.string,
95 | layout: PropTypes.string,
96 | linkLabels: PropTypes.object,
97 | }
98 |
99 | export default Link;
100 |
--------------------------------------------------------------------------------
/src/less/hierplane.less:
--------------------------------------------------------------------------------
1 | /** Global variables */
2 | @base-font-size: 16px;
3 | @em: @base-font-size*1em;
4 | @rem: @base-font-size*1rem;
5 | @assets-path: "../assets/";
6 | @transition-duration: .2s;
7 | @transition-timing-function: ease;
8 | @transition-properties: @transition-duration @transition-timing-function;
9 |
10 | /**
11 | * This scopes all hierplane styles so that they're rooted by `.hierplane`. This way we don't
12 | * collide with class names used in the parent document.
13 | *
14 | * This doesn't, however, mean that styles applied by the user can't mutate those dictated by
15 | * hierplane. If this provies to be an issue, we need to prefix each and every selector as to
16 | * ensure they're as unique as possible. I attempted to do this previously, but it proved
17 | * to be extremely cumbersome.
18 | */
19 | .hierplane {
20 | .t-crisp;
21 | display: flex;
22 | flex-direction: column;
23 | padding: 0;
24 | background: @gray-slate;
25 | font-size: @base-font-size;
26 | font-family: 'Helvetica Neue', Arial, sans-serif;
27 | color: @white;
28 |
29 | &,
30 | .pane-container {
31 | flex-grow: 1;
32 | }
33 |
34 | @import './functions.less';
35 | @import './text.less';
36 | @import './utils.less';
37 | @import './colors.less';
38 | @import './flex.less';
39 | @import './toolbar.less';
40 | @import './icons.less';
41 |
42 | // Explainer Modules
43 | @import './explainer/code.less';
44 | @import './explainer/ft.less';
45 | @import './explainer/loader.less';
46 | @import './explainer/main-stage.less';
47 | @import './explainer/meta-table.less';
48 | @import './explainer/parse-error.less';
49 | @import './explainer/parse-tree-toolbar.less';
50 | @import './explainer/passage.less';
51 | @import './explainer/tree-expansion-control.less';
52 |
53 | // Explainer / Node Modules
54 | @import './explainer/node/node_vars.less';
55 | @import './explainer/node/node.less';
56 | @import './explainer/node/node__word.less';
57 | @import './explainer/node/node__word__attrs.less';
58 | @import './explainer/node/node__word__label.less';
59 | @import './explainer/node/node__word__link.less';
60 | @import './explainer/node/node__word__tile.less';
61 | @import './explainer/node/node__word__ui.less';
62 | @import './explainer/node/node-children.less';
63 | @import './explainer/node/node-focus-trigger.less';
64 |
65 | // Explainer / Node Modifiers
66 | @import './explainer/node/node--segments-container.less';
67 | @import './explainer/node/node--seq.less';
68 | @import './explainer/node/node_mod.less';
69 | @import './explainer/node/node_pos.less';
70 |
71 | // Explainer / Pane Modules
72 | @import './explainer/pane/pane.less';
73 | @import './explainer/pane/pane__alt-parse-nav.less';
74 | @import './explainer/pane/pane__empty.less';
75 | @import './explainer/pane/pane__handler.less';
76 | @import './explainer/pane/pane__panel.less';
77 | @import './explainer/pane/pane__fragments.less';
78 | @import './explainer/pane/pane__toggle.less';
79 | @import './explainer/pane/pane-container.less';
80 |
81 | // Explainer / Pane Modifiers
82 | @import './explainer/pane/pane_mod.less';
83 | }
84 |
85 | // Themes
86 | // TODO (aarons @ codeviking):
87 | // Ideally, we wouldn't include all themes in the every CSS bundle,
88 | // and instead we'd produce separate CSS files that the end user
89 | // could include as appropriate. That way they'd get smaller static files.
90 | @import './theme/theme-light.less';
91 |
--------------------------------------------------------------------------------
/src/less/explainer/pane/pane__panel.less:
--------------------------------------------------------------------------------
1 | // Pane Content
2 |
3 | .pane__panels {
4 | .flex-container-column;
5 | .u-100;
6 | box-sizing: border-box;
7 | position: relative;
8 | }
9 |
10 | .pane__panel {
11 | .flex-container-column;
12 | margin-left: 4/@em;
13 | overflow: hidden;
14 | max-height: 600/@em;
15 | transition: max-height .04s @node-transition-ease;
16 | }
17 |
18 | .pane__panel__header {
19 | .u-select-none;
20 | .flex-container-vcentered;
21 | .t-truncate;
22 | height: 32/@em;
23 | min-height: 32/@em;
24 | padding-bottom: 2/@em;
25 | background: @pane-panel-header-bg;
26 | padding-left: 20/@em;
27 | cursor: pointer;
28 | position: relative;
29 | transition: background-color @transition-properties;
30 |
31 | &:before {
32 | .u-pe100;
33 | top: 15/@em;
34 | left: 7/@em;
35 | display: block;
36 | width: 0;
37 | height: 0;
38 | border-left: 4/@em solid transparent;
39 | border-right: 4/@em solid transparent;
40 | border-top: 4/@em solid darken(@pane-panel-header-text, 20%);
41 | transition: border-color @transition-properties, transform .06s @node-transition-ease, -webkit-transform .06s @node-transition-ease;
42 | }
43 |
44 | span {
45 | display: inline-block;
46 | font-size: 13/@em;
47 | font-weight: bold;
48 | color: @pane-panel-header-text;
49 | transition: color @transition-properties;
50 | }
51 |
52 | &:hover {
53 | background: lighten(@pane-panel-header-bg, 1%);
54 |
55 | &:before {
56 | border-top-color: darken(@pane-panel-header-text, 20%);
57 | }
58 |
59 | span {
60 | color: lighten(@pane-panel-header-text, 7%);
61 | }
62 | }
63 |
64 | &:active {
65 | background: darken(@pane-panel-header-bg, 1%);
66 | transition-duration: 0s;
67 |
68 | &:before {
69 | border-top-color: darken(@pane-panel-header-text, 25%);
70 | transition-duration: 0s;
71 | }
72 |
73 | span {
74 | color: darken(@pane-panel-header-text, 2%);
75 | transition-duration: 0s;
76 | }
77 | }
78 | }
79 |
80 | .pane__panel + .pane__panel {
81 | max-height: 1320/@em;
82 |
83 | .pane__panel__header {
84 | border-top: 4/@em solid @pane-panel-separator;
85 | }
86 | }
87 |
88 | .pane__panel:last-child {
89 | .flex-container-column;
90 | flex: 1;
91 | }
92 |
93 | .pane__panel__content {
94 | .u-h100;
95 | .flex-container-column;
96 | box-sizing: border-box;
97 | flex: 2;
98 | overflow: auto;
99 | max-height: 1440/@em;
100 | transition: opacity .06s @node-transition-ease, max-height .06s @node-transition-ease;
101 |
102 | &.pane__panel__content--padded {
103 | padding: 16/@em 20/@em 20/@em 20/@em;
104 | }
105 | }
106 |
107 | // Pane Panel Collapsed
108 |
109 | .pane__panel--collapsed {
110 | max-height: 34/@em;
111 |
112 | .pane__panel__header {
113 | &:before {
114 | .fn-transform(rotate(-90deg));
115 | border-top-color: darken(@pane-panel-header-text, 27%);
116 | }
117 |
118 | &:hover {
119 | &:before {
120 | border-top-color: darken(@pane-panel-header-text, 1%);
121 | }
122 | }
123 |
124 | &:active {
125 | &:before {
126 | border-top-color: darken(@pane-panel-header-text, 15%);
127 | }
128 | }
129 | }
130 |
131 | .pane__panel__content {
132 | max-height: 0;
133 | opacity: 0;
134 | }
135 | }
136 |
137 | .pane__panel + .pane__panel--collapsed {
138 | max-height: 38/@em;
139 | height: 38@em;
140 | }
141 |
--------------------------------------------------------------------------------
/src/module/pane/NodeProperties.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 |
4 | class NodeProperties extends React.Component {
5 | render() {
6 | const { selectedData, text } = this.props;
7 |
8 | const emptyProp = (
9 |
10 | none
11 |
12 | );
13 |
14 | // charNodeRoot is the field in the JSON node object that contains its span's
15 | // lo and hi values that let the UI extract a phrase from the original query.
16 | const hasSpan = selectedData.hasOwnProperty("alternateParseInfo") && selectedData.alternateParseInfo.hasOwnProperty("charNodeRoot");
17 | let spanData = null;
18 |
19 | if (hasSpan) {
20 | const spanField = selectedData.alternateParseInfo.charNodeRoot;
21 | spanData = text.slice(spanField.charLo, spanField.charHi);
22 | }
23 |
24 | return (
25 |
26 |
27 |
28 | Head Word:
29 | {selectedData.word ? (
30 |
31 | {selectedData.word}
32 |
33 |
34 | ) : emptyProp}
35 |
36 |
37 |
38 | JSON ID:
39 | {selectedData.id}
40 |
41 |
42 | Kind:
43 | {(selectedData.nodeType) ? (
44 |
45 | {selectedData.nodeType}
46 |
47 | ) : emptyProp}
48 |
49 |
50 |
51 | Link:
52 | {selectedData.link ? (
53 |
54 | {selectedData.link}
55 |
56 | ) : emptyProp}
57 |
58 |
59 |
60 | Children:
61 | {selectedData.children.length > 0 ? (
62 |
63 | {selectedData.children.length}
64 |
65 | ) : emptyProp}
66 |
67 |
68 |
69 | Attributes:
70 | {selectedData.attributes.length > 0 ? (
71 |
72 | {selectedData.attributes.join(", ")}
73 |
74 | ) : emptyProp}
75 |
76 |
77 |
78 | Span:
79 | {hasSpan ? (
80 |
81 | {spanData}
82 |
83 | ) : emptyProp}
84 |
85 |
86 |
87 |
88 | );
89 | }
90 | }
91 |
92 | NodeProperties.propTypes = {
93 | selectedData: PropTypes.object,
94 | text: PropTypes.string,
95 | }
96 |
97 | export default NodeProperties;
98 |
--------------------------------------------------------------------------------
/src/less/explainer/tree-expansion-control.less:
--------------------------------------------------------------------------------
1 | .tree-expansion-control {
2 | padding: 15/@em 8/@em;
3 | position: relative;
4 | z-index: 9;
5 |
6 | .tree-expansion-control__glyph,
7 | .tree-expansion-control__trigger {
8 | position: relative;
9 | width: 24/@em;
10 | height: 24/@em;
11 | opacity: .5;
12 | transition: opacity @node-transition-duration @node-transition-ease;
13 |
14 | .tree-expansion-control__glyph__triangle {
15 | position: absolute;
16 | width: 0;
17 | height: 0;
18 | border: 4.273/@em solid transparent;
19 | transition: border-color @transition-properties, transform @transition-properties, -webkit-transform @transition-properties;
20 |
21 | &.tree-expansion-control__glyph__triangle--down {
22 | border-bottom: none;
23 | border-top-color: @white;
24 | }
25 |
26 | &.tree-expansion-control__glyph__triangle--up {
27 | border-top: none;
28 | border-bottom-color: @white;
29 | }
30 |
31 | &.tree-expansion-control__glyph__triangle--left {
32 | border-left: none;
33 | border-right-color: @white;
34 | }
35 |
36 | &.tree-expansion-control__glyph__triangle--right {
37 | border-right: none;
38 | border-left-color: @white;
39 | }
40 | }
41 |
42 | &.tree-expansion-control__glyph--explode {
43 | .fn-transform(rotate(45deg));
44 |
45 | .tree-expansion-control__glyph__triangle--down {
46 | top: 20.727/@em;
47 | left: 7.727/@em;
48 | }
49 |
50 | .tree-expansion-control__glyph__triangle--up {
51 | top: -1/@em;
52 | left: 7.727/@em;
53 | }
54 |
55 | .tree-expansion-control__glyph__triangle--left {
56 | top: 7.727/@em;
57 | left: -1/@em;
58 | }
59 |
60 | .tree-expansion-control__glyph__triangle--right {
61 | top: 7.727/@em;
62 | left: 20.727/@em;
63 | }
64 | }
65 |
66 | &.tree-expansion-control__glyph--implode {
67 | .tree-expansion-control__glyph__triangle--down {
68 | top: 1.369/@em;
69 | left: 7.727/@em;
70 | }
71 |
72 | .tree-expansion-control__glyph__triangle--up {
73 | top: 18.358/@em;
74 | left: 7.727/@em;
75 | }
76 |
77 | .tree-expansion-control__glyph__triangle--left {
78 | top: 7.727/@em;
79 | left: 18.358/@em;
80 | }
81 |
82 | .tree-expansion-control__glyph__triangle--right {
83 | top: 7.727/@em;
84 | left: 1.369/@em;
85 | }
86 | }
87 | }
88 |
89 | &.tree-expansion-control:hover,
90 | &.tree-expansion-control:active {
91 | .tree-expansion-control__glyph {
92 | .tree-expansion-control__glyph__triangle--down {
93 | .fn-transform(translateY(2/@em));
94 | }
95 |
96 | .tree-expansion-control__glyph__triangle--up {
97 | .fn-transform(translateY(-2/@em));
98 | }
99 |
100 | .tree-expansion-control__glyph__triangle--left {
101 | .fn-transform(translateX(-2/@em));
102 | }
103 |
104 | .tree-expansion-control__glyph__triangle--right {
105 | .fn-transform(translateX(2/@em));
106 | }
107 | }
108 | }
109 |
110 | &.tree-expansion-control:hover {
111 | .tree-expansion-control__glyph {
112 | opacity: 1;
113 | }
114 | }
115 |
116 | &.tree-expansion-control:active {
117 | .tree-expansion-control__glyph {
118 | opacity: .2;
119 | transition-duration: 0s;
120 | }
121 | }
122 |
123 | .tree-expansion-control__trigger {
124 | .u-100;
125 | position: absolute;
126 | z-index: 9;
127 | top: 0/@em;
128 | right: 0/@em;
129 | cursor: pointer;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/module/MainStage.js:
--------------------------------------------------------------------------------
1 | import EmptyTree from './EmptyTree.js';
2 | import Icon from './Icon.js';
3 | import Node from './node/Node.js';
4 | import ParseError from './ParseError.js';
5 | import classNames from 'classnames/bind';
6 |
7 | import { isSingleSegment } from './helpers';
8 |
9 | import PropTypes from 'prop-types';
10 |
11 | import React from 'react';
12 |
13 | class MainStage extends React.Component {
14 | constructor() {
15 | super();
16 | this.state = {
17 | rendered: false,
18 | };
19 | }
20 |
21 | componentDidUpdate() {
22 | this.state = {
23 | rendered: true,
24 | };
25 | }
26 |
27 | render() {
28 | const { rendered } = this.state;
29 | const { readOnly,
30 | styles,
31 | positions,
32 | linkLabels,
33 | data,
34 | layout,
35 | text,
36 | selectedNodeId,
37 | hoverNodeId,
38 | focusNode,
39 | hoverNode,
40 | fetchAltParse,
41 | togglePane,
42 | loading,
43 | firstLoad,
44 | emptyQuery,
45 | errorState } = this.props;
46 |
47 | let mainsStageContent = null;
48 |
49 | if (emptyQuery) {
50 | mainsStageContent = ( );
51 | } else {
52 | if (data && !errorState) {
53 | // TODO: remove readOnly, execute componentDidUpdate automatically when readOnly is true
54 | mainsStageContent = (
55 |
56 |
{ focusNode("defocus") }}>
57 |
75 |
76 | );
77 | } else {
78 | mainsStageContent = ( );
79 | }
80 | }
81 |
82 | // mainStageConditionalClasses builds dynamic class lists for #main-stage:
83 | const mainStageConditionalClasses = classNames({
84 | [`${layout}`]: true,
85 | "main-stage--loading": loading,
86 | "main-stage--fade-delay": !firstLoad && !emptyQuery,
87 | });
88 |
89 | return (
90 |
91 | {loading ? (
92 |
97 | ) : null}
98 |
{ focusNode("defocus") }}>
99 | {mainsStageContent}
100 |
101 | );
102 | }
103 | }
104 |
105 | MainStage.propTypes = {
106 | readOnly: PropTypes.bool,
107 | styles: PropTypes.object.isRequired,
108 | positions: PropTypes.object.isRequired,
109 | linkLabels: PropTypes.object.isRequired,
110 | data: PropTypes.shape({
111 | id: PropTypes.string,
112 | kind: PropTypes.string,
113 | word: PropTypes.string,
114 | attributes: PropTypes.arrayOf(PropTypes.string.isRequired),
115 | children: PropTypes.arrayOf(PropTypes.object.isRequired),
116 | link: PropTypes.string,
117 | }),
118 | layout: PropTypes.string,
119 | text: PropTypes.string,
120 | selectedNodeId: PropTypes.string,
121 | hoverNodeId: PropTypes.string,
122 | focusNode: PropTypes.func,
123 | hoverNode: PropTypes.func,
124 | fetchAltParse: PropTypes.func,
125 | togglePane: PropTypes.func,
126 | loading: PropTypes.bool,
127 | firstLoad: PropTypes.bool,
128 | emptyQuery: PropTypes.bool,
129 | errorState: PropTypes.bool,
130 | }
131 |
132 | export default MainStage;
133 |
--------------------------------------------------------------------------------
/src/module/pane/SideBar.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import NodeProperties from './NodeProperties.js';
4 | import AltParseNav from './AltParseNav.js';
5 | import StrRep from './StrRep.js';
6 | import PanePanel from './PanePanel.js';
7 | import PaneToggle from './PaneToggle.js';
8 | import PaneHandler from './PaneHandler.js';
9 | import ReactDOM from 'react-dom';
10 |
11 | class SideBar extends React.Component {
12 | constructor() {
13 | super();
14 | this.state = {
15 | defaultWidth: 400,
16 | mode: "autosnap",
17 | handlerStartX: null,
18 | sideBarStartWidth: null,
19 | };
20 |
21 | this.handleMouseDown = this.handleMouseDown.bind(this);
22 | this.handleMouseMove = this.handleMouseMove.bind(this);
23 | this.handleMouseUp = this.handleMouseUp.bind(this);
24 | this.handleDoubleClick = this.handleDoubleClick.bind(this);
25 | }
26 |
27 | componentDidMount() {
28 | this.setSideBarWidth(this.state.defaultWidth);
29 | }
30 |
31 | setSideBarWidth(width) {
32 | ReactDOM.findDOMNode(this.refs.sideBar).style.width = width + 'px';
33 | }
34 |
35 | handleMouseDown(e) {
36 | this.setState({
37 | mode: "moving",
38 | handlerStartX: e.clientX,
39 | sideBarStartWidth: ReactDOM.findDOMNode(this.refs.sideBar).getBoundingClientRect().width,
40 | });
41 | window.addEventListener('mousemove', this.handleMouseMove);
42 | window.addEventListener('mouseup', this.handleMouseUp);
43 | }
44 |
45 | handleMouseMove(e) {
46 | let newWidth;
47 | if (this.state.mode === "moving") {
48 | newWidth = (this.state.sideBarStartWidth - e.clientX + this.state.handlerStartX);
49 | this.setSideBarWidth(this.state.width);
50 | }
51 | this.setState({
52 | width: newWidth,
53 | });
54 | }
55 |
56 | handleMouseUp() {
57 | this.setState({
58 | mode: "autosnap",
59 | });
60 | window.removeEventListener('mousemove', this.handleMouseMove);
61 | window.addEventListener('mouseup', this.handleMouseUp);
62 | }
63 |
64 | handleDoubleClick() {
65 | this.setSideBarWidth(this.state.defaultWidth);
66 | }
67 |
68 | render() {
69 | const { mode } = this.state;
70 | const { readOnly,
71 | text,
72 | selectedData,
73 | sideBarCollapsed,
74 | togglePane,
75 | fetchAltParse,
76 | loading } = this.props;
77 |
78 | const nodePropContent = ( ),
79 | altParseContent = ( ),
80 | strRepContent = ( );
81 |
82 | let paneContent = null;
83 |
84 | if (selectedData !== null) {
85 | paneContent = (
86 |
91 | );
92 | } else {
93 | paneContent = (
94 | Click a node to focus it and inspect its properties.
95 | );
96 | }
97 |
98 | return (
99 |
114 | );
115 | }
116 | }
117 |
118 | SideBar.propTypes = {
119 | readOnly: PropTypes.bool,
120 | selectedData: PropTypes.object,
121 | text: PropTypes.string,
122 | sideBarCollapsed: PropTypes.bool,
123 | togglePane: PropTypes.func,
124 | fetchAltParse: PropTypes.func,
125 | loading: PropTypes.bool,
126 | }
127 |
128 | export default SideBar;
129 |
--------------------------------------------------------------------------------
/dev/data/the-sum-of-three-consecutive-integers.json:
--------------------------------------------------------------------------------
1 | {
2 | "text": "The sum of three consecutive odd integers is 9. What is the largest integer?",
3 | "root": {
4 | "nodeType": "top-level-and",
5 | "word": "and",
6 | "children": [
7 | {
8 | "nodeType": "event",
9 | "word": "be",
10 | "spans": [
11 | {
12 | "start": 42,
13 | "end": 46
14 | },
15 | {
16 | "start": 46,
17 | "end": 47,
18 | "spanType": "ignored"
19 | }
20 | ],
21 | "children": [
22 | {
23 | "nodeType": "entity",
24 | "word": "sum",
25 | "spans": [
26 | {
27 | "start": 0,
28 | "end": 7
29 | }
30 | ],
31 | "attributes": ["the"],
32 | "link": "subj",
33 | "children": [
34 | {
35 | "nodeType": "detail",
36 | "word": "of",
37 | "spans": [
38 | {
39 | "start": 8,
40 | "end": 10
41 | }
42 | ],
43 | "link": "adj",
44 | "children": [
45 | {
46 | "nodeType": "entity",
47 | "word": "integer",
48 | "spans": [
49 | {
50 | "start": 11,
51 | "end": 16
52 | },
53 | {
54 | "start": 33,
55 | "end": 41
56 | }
57 | ],
58 | "attributes": [">1"],
59 | "link": "parg",
60 | "children": [
61 | {
62 | "nodeType": "numericstring",
63 | "word": "3",
64 | "link": "quant"
65 | },
66 | {
67 | "nodeType": "sequence",
68 | "word": "sequence",
69 | "link": "adj",
70 | "children": [
71 | {
72 | "nodeType": "detail",
73 | "word": "consecutive",
74 | "spans": [
75 | {
76 | "start": 17,
77 | "end": 28
78 | }
79 | ],
80 | "link": "seqChild"
81 | },
82 | {
83 | "nodeType": "detail",
84 | "word": "odd",
85 | "spans": [
86 | {
87 | "start": 29,
88 | "end": 32
89 | }
90 | ],
91 | "link": "seqChild"
92 | }
93 | ]
94 | }
95 | ]
96 | }
97 | ]
98 | }
99 | ]
100 | },
101 | {
102 | "nodeType": "numericstring",
103 | "word": "9",
104 | "link": "obj"
105 | }
106 | ]
107 | },
108 | {
109 | "nodeType": "event",
110 | "word": "be",
111 | "attributes": ["linking"],
112 | "link": "none",
113 | "spans": [
114 | {
115 | "start": 53,
116 | "end": 55
117 | }
118 | ],
119 | "children": [
120 | {
121 | "nodeType": "entity",
122 | "word": "what",
123 | "spans": [
124 | {
125 | "start": 48,
126 | "end": 52
127 | }
128 | ],
129 | "link": "subj"
130 | },
131 | {
132 | "nodeType": "detail",
133 | "word": "large",
134 | "spans": [
135 | {
136 | "start": 56,
137 | "end": 67
138 | }
139 | ],
140 | "attributes": ["superlative"],
141 | "link": "advx"
142 | },
143 | {
144 | "nodeType": "detail",
145 | "word": "Integer",
146 | "spans": [
147 | {
148 | "start": 68,
149 | "end": 75
150 | }
151 | ],
152 | "attributes": [],
153 | "link": "obj"
154 | }
155 | ]
156 | }
157 | ]
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/module/pane/AltParseNav.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import AltParseNavToggle from './AltParseNavToggle.js';
4 |
5 | class AltParseNav extends React.Component {
6 | constructor() {
7 | super();
8 | this.state = {
9 | keyInteraction: "idle", // idle, prev, next
10 | };
11 |
12 | this.handleKeydown = this.handleKeydown.bind(this);
13 | this.handleKeyup = this.handleKeyup.bind(this);
14 | this.handleKeyInput = this.handleKeyInput.bind(this);
15 | }
16 |
17 | componentDidMount() {
18 | window.addEventListener('keydown', this.handleKeydown);
19 | window.addEventListener('keyup', this.handleKeyup);
20 | }
21 |
22 | handleKeydown(e) {
23 | this.handleKeyInput(e, "down");
24 | }
25 |
26 | handleKeyup(e) {
27 | this.handleKeyInput(e, "up");
28 | }
29 |
30 |
31 | handleKeyInput(e, keyEvent) {
32 | const selectedData = this.props.selectedData;
33 | const altParseInfo = selectedData.alternateParseInfo;
34 | if (selectedData.hasOwnProperty("alternateParseInfo") && !this.props.readOnly && !this.props.loading) {
35 | if (e.keyCode === 219 && altParseInfo.hasOwnProperty("prevParse")) {
36 | if (keyEvent === "up") {
37 | this.props.fetchAltParse(selectedData, "prev");
38 | }
39 | this.setState({
40 | keyInteraction: keyEvent === "down" ? "prev" : "idle",
41 | });
42 | } else if (e.keyCode === 221 && altParseInfo.hasOwnProperty("nextParse")) {
43 | if (keyEvent === "up") {
44 | this.props.fetchAltParse(selectedData, "next");
45 | }
46 | this.setState({
47 | keyInteraction: keyEvent === "down" ? "next" : "idle",
48 | });
49 | }
50 | }
51 | }
52 |
53 | componentWillUnmount() {
54 | window.removeEventListener('keydown', this.handleKeydown);
55 | window.removeEventListener('keyup', this.handleKeyup);
56 | }
57 |
58 | render() {
59 | const { keyInteraction } = this.state;
60 | const { readOnly,
61 | selectedData,
62 | fetchAltParse,
63 | loading } = this.props;
64 |
65 | let altParseContent = null;
66 | let altParseInfoExists = selectedData.hasOwnProperty("alternateParseInfo") && selectedData.alternateParseInfo !== undefined;
67 |
68 | const altParseInfo = altParseInfoExists ? selectedData.alternateParseInfo : null;
69 | const hasPrevParse = altParseInfoExists && altParseInfo.hasOwnProperty("prevParse");
70 | const hasNextParse = altParseInfoExists && altParseInfo.hasOwnProperty("nextParse");
71 |
72 | const insertTrigger = (direction) => {
73 | const hasDirectionalParse = direction === "prev" ? hasPrevParse : hasNextParse;
74 | const disabled = altParseInfoExists && hasDirectionalParse && !loading && !readOnly ? false : true;
75 |
76 | return (
77 |
84 | );
85 | }
86 |
87 | if (altParseInfoExists) {
88 | altParseContent = (
89 |
90 |
91 |
92 |
93 |
94 |
95 | Current Parse:
96 |
97 |
98 |
99 |
100 | {altParseInfo.hasOwnProperty("currentParseIndex") ? (
101 | {altParseInfo.currentParseIndex + 1}
102 | ) : (
103 | unknown
104 | )}
105 | {altParseInfo.hasOwnProperty("numberOfParses") ? (
106 |
107 | of
108 | {altParseInfo.numberOfParses}
109 |
110 | ) : ("")}
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | {insertTrigger("prev")}
120 | {insertTrigger("next")}
121 |
122 |
123 | );
124 | } else {
125 | altParseContent = (
126 |
127 |
No alternate parse data was served.
128 |
129 | );
130 | }
131 |
132 | return altParseContent;
133 | }
134 | }
135 |
136 | AltParseNav.propTypes = {
137 | readOnly: PropTypes.bool,
138 | selectedData: PropTypes.object,
139 | fetchAltParse: PropTypes.func,
140 | loading: PropTypes.bool,
141 | }
142 |
143 | export default AltParseNav;
144 |
--------------------------------------------------------------------------------
/src/less/flex.less:
--------------------------------------------------------------------------------
1 | .flex-container {
2 | display: -webkit-box;
3 | display: -ms-flexbox;
4 | display: -webkit-flex;
5 | display: flex;
6 | }
7 |
8 | .flex-direction(@v) {
9 | -webkit-flex-direction: @v;
10 | -ms-flex-direction: @v;
11 | flex-direction: @v;
12 | // row (default): same as text direction
13 | // row-reverse: opposite to text direction
14 | // column: same as row but top to bottom
15 | // column-reverse: same as row-reverse top to bottom
16 | }
17 |
18 | .flex-align-items(@v) { // Vertical alignment of single line of items
19 | -webkit-align-items: @v;
20 | -ms-flex-align: @v;
21 | align-items: @v;
22 | // stretch (default): Items are stretched to fit the container
23 | // center: Items are positioned at the center of the container
24 | // flex-start: Items are positioned at the beginning of the container
25 | // flex-end: Items are positioned at the end of the container
26 | // baseline: Items are positioned at the baseline of the container
27 | }
28 |
29 | .flex-justify-content(@v) { // Horizontal alignment of children
30 | -webkit-justify-content: @v;
31 | -ms-justify-content: @v;
32 | justify-content: @v;
33 | // flex-start (default): items are packed toward the start line
34 | // flex-end: items are packed toward to end line
35 | // center: items are centered along the line
36 | // space-between: items are evenly distributed in the line; first item is on the start line, last item on the end line
37 | // space-around: items are evenly distributed in the line with equal space around them
38 | }
39 |
40 | .flex-wrap(@v) { // Row wrapping of children
41 | -webkit-flex-wrap: @v;
42 | flex-wrap: @v;
43 | // nowrap (default): single-line which may cause the container to overflow
44 | // wrap: multi-lines, direction is defined by flex-direction
45 | // wrap-reverse: multi-lines, opposite to direction defined by flex-direction
46 | }
47 |
48 | .flex-align-content(@v) { // Vertical alignment of multiple lines of items
49 | -webkit-align-content: @v;
50 | align-content: @v;
51 | // flex-start: lines packed to the start of the container
52 | // flex-end: lines packed to the end of the container
53 | // center: lines packed to the center of the container
54 | // space-between: lines evenly distributed; the first line is at the start of the container while the last one is at the end
55 | // space-around: lines evenly distributed with equal space between them
56 | // stretch (default): lines stretch to take up the remaining space
57 | }
58 |
59 | .flex-align-self(@v) { // Vertical alignment of self
60 | -webkit-align-self: @v;
61 | align-self: @v;
62 | // flex-start: cross-start margin edge of the item is placed on the cross-start line
63 | // flex-end: cross-end margin edge of the item is placed on the cross-end line
64 | // center: item is centered in the cross-axis
65 | // baseline: items are aligned such as their baseline are aligned
66 | // stretch (default): stretch to fill the container (still respect min-width/max-width)
67 | }
68 |
69 | .flex-basis(@v) { // Initial size of the flex item, before any available space is distributed
70 | -webkit-flex-basis: @v;
71 | flex-basis: @v;
72 | // dimension with unit (cannot be negative)
73 | }
74 |
75 | .flex-grow(@v) { // defines the ability for a flex item to grow if necessary, size proportion relative to an item's siblings
76 | -webkit-flex-grow: @v;
77 | flex-grow: @v;
78 | // number
79 | }
80 |
81 | .flex-shrink(@v) { // how much the flex item will shrink relative to the rest of the flex items in the flex container when there isn't enough space on the row.
82 | -webkit-flex-shrink: @v;
83 | flex-shrink: @v;
84 | // number
85 | }
86 |
87 | .flex-order(@v) { // manually set ordering of a flex item
88 | -webkit-order: @v;
89 | order: @v;
90 | // number
91 | }
92 |
93 | .flex(@g:0, @s:1, @b:auto) { // Shorthand for flex-grow, flex-shrink and flex-basis
94 | -webkit-flex: @g @s @b;
95 | -ms-flex: @g @s @b;
96 | -flex: @g @s @b;
97 | }
98 |
99 | .flex-none { // Same as setting flex-grow 0, flex-shrink 0 and flex-basis auto
100 | -webkit-flex: none;
101 | -ms-flex: none;
102 | -flex: none;
103 | }
104 |
105 | .flex-flow(@d:row, @w:nowrap) { // Shorthand for flex-direction and flex-wrap
106 | -webkit-flex-flow: @d @w;
107 | flex-flow: @d @w;
108 | }
109 |
110 | // Prefab Flex classes
111 |
112 | .flex-container-row {
113 | .flex-container;
114 | .flex-direction(row);
115 | }
116 |
117 | .flex-container-column {
118 | .flex-container;
119 | .flex-direction(column);
120 | }
121 |
122 | .flex-container-align-left {
123 | .flex-container-row;
124 | .flex-justify-content(flex-start);
125 | }
126 |
127 | .flex-container-align-right {
128 | .flex-container-row;
129 | .flex-justify-content(flex-end);
130 | }
131 |
132 | .flex-container-hcentered {
133 | .flex-container;
134 | .flex-justify-content(center); // Horizontally centered
135 | }
136 |
137 | .flex-container-vcentered {
138 | .flex-container;
139 | .flex-align-items(center); // Vertical centered
140 | }
141 |
142 | .flex-container-centered {
143 | .flex-container;
144 | .flex-align-items(center); // Vertical centered
145 | .flex-justify-content(center); // Horizontally centered
146 | }
147 |
148 | .flex-distribute-vertically {
149 | .flex-align-content(space-around);
150 | }
151 |
--------------------------------------------------------------------------------
/src/module/IconSprite.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconSprite = () => (
4 |
5 |
6 | Parse nav previous icon
7 |
8 |
9 |
10 |
11 | Inverted parse nav previous icon
12 |
13 |
14 |
15 |
16 | Parse nav next icon
17 |
18 |
19 |
20 |
21 | Inverted parse nav next icon
22 |
23 |
24 |
25 |
26 | Close (x) icon
27 |
28 |
29 |
30 |
31 | Node collapse icon
32 |
33 |
34 |
35 |
36 | Inverted node collapse icon
37 |
38 |
39 |
40 |
41 | Pencil edit icon
42 |
43 |
44 |
45 |
46 |
47 | Parse error icon
48 |
49 |
50 |
51 |
52 | Node expand icon
53 |
54 |
55 |
56 |
57 | Inverted node expand icon
58 |
59 |
60 |
61 |
62 | Keyboard icon
63 |
64 |
65 |
66 |
67 | Euclid logo
68 |
69 |
70 |
71 |
75 |
76 | );
77 |
78 | export default IconSprite;
79 |
--------------------------------------------------------------------------------
/src/module/Passage.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import PassageSpan from './PassageSpan.js';
5 | import Icon from './Icon.js';
6 | import classNames from 'classnames/bind';
7 |
8 | class Passage extends React.Component {
9 | constructor() {
10 | super();
11 | this.state = {
12 | passageActive: false,
13 | focused: false,
14 | autoFocus: null,
15 | };
16 | this.handleFocus = this.handleFocus.bind(this);
17 | this.handleBlur = this.handleBlur.bind(this);
18 | this.handleMouseOver = this.handleMouseOver.bind(this);
19 | this.handleMouseOut = this.handleMouseOut.bind(this);
20 | this.handleSpaceBar = this.handleSpaceBar.bind(this);
21 | this.handleEsc = this.handleEsc.bind(this);
22 | }
23 |
24 | componentDidMount() {
25 | window.addEventListener('keyup', this.handleSpaceBar);
26 | }
27 |
28 | componentDidUpdate() {
29 | this.handleEmpty();
30 | }
31 |
32 | handleEmpty() {
33 | if (this.props.emptyQuery === true) {
34 | switch (this.state.autoFocus) {
35 | case null:
36 | this.setState({
37 | autoFocus: true,
38 | });
39 | break;
40 | case true:
41 | this.handleFocus();
42 | this.setState({
43 | autoFocus: false,
44 | });
45 | break;
46 | }
47 | }
48 | }
49 |
50 | handleEsc(e) {
51 | if (e.keyCode === 27) {
52 | this.handleBlur();
53 | }
54 | }
55 |
56 | handleSpaceBar(e) {
57 | const { readOnly, loading } = this.props;
58 |
59 | if (!loading && !readOnly) {
60 | if (this.state.focused === false && e.keyCode === 32) {
61 | e.preventDefault();
62 | this.handleFocus();
63 | }
64 | }
65 |
66 | if (this.state.focused && e.key === 'Enter' && !readOnly) {
67 | this.handleBlur();
68 | }
69 | }
70 |
71 | handleFocus() {
72 | this.setState({
73 | focused: true,
74 | });
75 | ReactDOM.findDOMNode(this.refs.passageInput).focus();
76 | this.props.focusNode("defocus");
77 | }
78 |
79 | handleBlur() {
80 | if (this.props.emptyQuery === true) {
81 | this.handleFocus();
82 | } else {
83 | this.setState({
84 | focused: false,
85 | });
86 | ReactDOM.findDOMNode(this.refs.passageInput).blur();
87 | }
88 | }
89 |
90 | handleMouseOver() {
91 | this.setState({
92 | passageActive: true,
93 | });
94 | }
95 |
96 | handleMouseOut() {
97 | this.setState({
98 | passageActive: false,
99 | });
100 | }
101 |
102 | componentWillUnmount() {
103 | window.removeEventListener('keyup', this.handleSpaceBar);
104 | }
105 |
106 | render() {
107 |
108 | const { focused, passageActive } = this.state;
109 | const { readOnly,
110 | text,
111 | inputText,
112 | onKeyPress,
113 | onChange,
114 | loading,
115 | data,
116 | styles,
117 | selectedNodeId,
118 | hoverNodeId,
119 | hoverNode,
120 | focusNode,
121 | errorState } = this.props;
122 |
123 | const passageConditionalClasses = classNames({
124 | "passage--editing": focused,
125 | "passage--active": passageActive,
126 | "passage--loading": loading,
127 | });
128 |
129 | return (
130 |
131 |
{}}>
133 | {!readOnly ? (
134 |
144 | ) : null}
145 |
{}}>
146 |
147 | {loading || errorState ? text : (
148 |
157 | )}
158 | {!readOnly ? (
159 |
164 |
165 |
166 | ) : null}
167 |
168 |
169 |
170 |
171 | );
172 | }
173 | }
174 |
175 | Passage.propTypes = {
176 | readOnly: PropTypes.bool,
177 | text: PropTypes.string.isRequired,
178 | inputText: PropTypes.string,
179 | onKeyPress: PropTypes.func.isRequired,
180 | onChange: PropTypes.func.isRequired,
181 | focusNode: PropTypes.func,
182 | loading: PropTypes.bool,
183 | emptyQuery: PropTypes.bool,
184 | errorState: PropTypes.bool,
185 | data: PropTypes.object,
186 | styles: PropTypes.object,
187 | selectedNodeId: PropTypes.string,
188 | hoverNodeId: PropTypes.string,
189 | hoverNode: PropTypes.func,
190 | }
191 |
192 | export default Passage;
193 |
--------------------------------------------------------------------------------
/src/module/stores/modules/ui.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | /**
4 | * Action Type Constants
5 | */
6 | export const ADD_ALL_NODE_IDS = 'ADD_ALL_NODE_IDS';
7 | export const TOGGLE_NODE_STATE = 'TOGGLE_NODE_STATE';
8 | export const COLLAPSE_NODE = 'COLLAPSE_NODE';
9 | export const COLLAPSE_ALL_NODES = 'COLLAPSE_ALL_NODES';
10 | export const COLLAPSE_DESCENDANTS = 'COLLAPSE_DESCENDANTS';
11 | export const EXPAND_NODE = 'EXPAND_NODE';
12 | export const EXPAND_ALL_NODES = 'EXPAND_ALL_NODES';
13 | export const EXPAND_PATH_TO_NODE = 'EXPAND_PATH_TO_NODE';
14 |
15 | /**
16 | * Action Creators
17 | */
18 |
19 | /**
20 | * Adds all the node ids.
21 | *
22 | * @param {Immutable.Set} ids - the node ids to be added
23 | * @returns {object}
24 | */
25 | export function addAllNodeIds(ids) {
26 | return {
27 | ids,
28 | type: ADD_ALL_NODE_IDS,
29 | };
30 | }
31 |
32 | /**
33 | * Toggle the collapsed/expanded state of a node.
34 | *
35 | * @param {string} id - the id of the node to be toggled
36 | * @returns {object}
37 | */
38 | export function toggleNode(id) {
39 | return {
40 | id,
41 | type: TOGGLE_NODE_STATE,
42 | };
43 | }
44 |
45 | /**
46 | * Explicitly collapse a node.
47 | *
48 | * @param {string} id - the id of the node to be collapsed
49 | * @returns {object}
50 | */
51 | export function collapseNode(id) {
52 | return {
53 | id,
54 | type: COLLAPSE_NODE,
55 | };
56 | }
57 |
58 | /**
59 | * Explicitly expand a node.
60 | *
61 | * @param {string} id - the id of the node to be expanded
62 | * @returns {object}
63 | */
64 | export function expandNode(id) {
65 | return {
66 | id,
67 | type: EXPAND_NODE,
68 | };
69 | }
70 |
71 | /**
72 | * Collapse all nodes.
73 | *
74 | * @returns {object}
75 | */
76 | export function collapseAllNodes() {
77 | return {
78 | type: COLLAPSE_ALL_NODES,
79 | };
80 | }
81 |
82 | /**
83 | * Expand all nodes.
84 | *
85 | * @returns {object}
86 | */
87 | export function expandAllNodes() {
88 | return {
89 | type: EXPAND_ALL_NODES,
90 | };
91 | }
92 |
93 | /**
94 | * Collapse decendants for a given node id. When navigating between parses on a focused node, we
95 | * keep that node expanded (i.e. show its immediate children), but force-collapse all of its other
96 | * descendants.
97 | *
98 | * @param {string} id - The node id that we're fetching an alternate parse for.
99 | * @returns {object}
100 | */
101 | export function collapseDescendants(id) {
102 | return {
103 | id,
104 | type: COLLAPSE_DESCENDANTS,
105 | };
106 | }
107 |
108 | /**
109 | * Expand the path to the clicked node.
110 | *
111 | * @param {string} id - The node id that we're exposing the path to.
112 | * @returns {object}
113 | */
114 | export function expandPathToNode(id) {
115 | return {
116 | id,
117 | type: EXPAND_PATH_TO_NODE,
118 | };
119 | }
120 |
121 | /**
122 | * UI Reducer
123 | */
124 | const initialState = {
125 | expandableNodeIds: Immutable.Set(),
126 | expandedNodeIds: Immutable.Set(),
127 | exploded: false,
128 | };
129 |
130 | export default (state = initialState, action) => {
131 | switch(action.type) {
132 | case ADD_ALL_NODE_IDS:
133 | return {
134 | ...state,
135 | expandableNodeIds: action.ids,
136 | };
137 | case COLLAPSE_NODE:
138 | return {
139 | ...state,
140 | exploded: false,
141 | expandedNodeIds: state.expandedNodeIds.delete(action.id),
142 | };
143 | case COLLAPSE_ALL_NODES:
144 | return {
145 | ...state,
146 | exploded: false,
147 | expandedNodeIds: Immutable.Set(),
148 | };
149 | case COLLAPSE_DESCENDANTS:
150 | return {
151 | ...state,
152 | exploded: false,
153 | expandedNodeIds: state.expandedNodeIds.filterNot(isChildOf(action.id)),
154 | };
155 | case EXPAND_NODE:
156 | return (function() {
157 | const { expandedNodeIds, expandableNodeIds } = state;
158 | const newExpandedNodeIds = expandedNodeIds.add(action.id);
159 |
160 | return {
161 | ...state,
162 | exploded: newExpandedNodeIds.equals(expandableNodeIds),
163 | expandedNodeIds: newExpandedNodeIds,
164 | };
165 | })();
166 | case TOGGLE_NODE_STATE:
167 | return (function() {
168 | const { expandedNodeIds: prevIds, expandableNodeIds } = state;
169 | const id = action.id;
170 | const newExpandedNodeIds = prevIds.has(id) ? prevIds.delete(id) : prevIds.add(id);
171 |
172 | return {
173 | ...state,
174 | exploded: newExpandedNodeIds.equals(expandableNodeIds),
175 | expandedNodeIds: newExpandedNodeIds,
176 | };
177 | })();
178 | case EXPAND_ALL_NODES:
179 | return {
180 | ...state,
181 | exploded: true,
182 | expandedNodeIds: Immutable.Set(state.expandableNodeIds),
183 | };
184 | case EXPAND_PATH_TO_NODE:
185 | return {
186 | ...state,
187 | expandedNodeIds: state.expandedNodeIds.union(pathToNode(action.id)),
188 | };
189 | default:
190 | return state;
191 | }
192 | }
193 |
194 | function isChildOf(parseId) {
195 | return nodeId => nodeId.startsWith(`${parseId}.`);
196 | }
197 |
198 | /**
199 | * Returns an Immutable.Set of node ids. The ids are the path from root to that id. For example,
200 | * given the id '0.0.0', this function will return ['0', '0.0', '0.0.0'].
201 | *
202 | * @param {string} id - the node id to get the path from the root node to.
203 | * @returns {Immutable.Set}
204 | */
205 | function pathToNode(id) {
206 | if (!id) {
207 | return Immutable.Set();
208 | }
209 |
210 | // Recurse from child to parent by cutting off the last two characters, treating the shortened
211 | // string as a parent pointer.
212 | return id.length > 0 ? Immutable.Set([id]).union(pathToNode(id.slice(0, -2))) : [];
213 | }
214 |
--------------------------------------------------------------------------------
/src/less/explainer/node/node--seq.less:
--------------------------------------------------------------------------------
1 | .node--seq {
2 | .node-sequence-container {
3 | .flex-container-row;
4 | .flex-justify-content(center);
5 | position: relative;
6 | z-index: 9999 !important;
7 | padding: (@node-word-margin * .5) (@node-word-margin * .5) (@node-word-margin * 1.5) (@node-word-margin * 1.5);
8 | }
9 |
10 | .node-sequence-trigger {
11 | .u-child100;
12 | cursor: pointer;
13 | }
14 |
15 | & > .node__word {
16 | min-width: (@node-word-padding * 2) + 16/@em;
17 | }
18 |
19 | &[data-pos='left'],
20 | &[data-pos='right'] {
21 | & > .node__word {
22 | & > .node__word__link {
23 | margin-top: 25/@em;
24 | }
25 | }
26 | }
27 |
28 | &[data-pos='inside'] {
29 | & > .node__word {
30 | & > .node__word__link {
31 | margin-top: 32/@em;
32 | }
33 | }
34 | }
35 |
36 | &[data-collapsable="true"] {
37 | & > .node__word {
38 | & > .node__word__content > .node-sequence-container {
39 | padding-left: @node-word-margin * 3;
40 | }
41 | }
42 | }
43 |
44 | &[data-alt-parses="true"] {
45 | & > .node__word {
46 | & > .node__word__content > .node-sequence-container {
47 | padding-right: 39/@em;
48 | }
49 | }
50 | }
51 |
52 | &[data-pos='down'] {
53 | & > .node__word {
54 | & > .node__word__content > .node-sequence-container {
55 | margin-top: -(@node-word-margin * 3);
56 | }
57 | }
58 | }
59 |
60 | & > .node__word--has-attrs > .node__word__content > .node-sequence-container {
61 | padding-bottom: 15/@em;
62 | }
63 |
64 | &.node-container--expanded {
65 | .event-seq-child {
66 | transition: margin-top .05s @node-transition-ease;
67 | }
68 | }
69 | }
70 |
71 |
72 | .ft--right-children,
73 | .ft--left-children {
74 | & > .node-children-container > .encapsulated.event-seq-child--expanded {
75 | margin-bottom: -@node-word-margin;
76 | }
77 | }
78 |
79 | // Event Sequence Children and Side-hanging events
80 | .ft.ft--seq > .ft__tr > .ft__tr__td {
81 | &.ft--left-children > .node-children-container > .encapsulated[data-pos='left'] {
82 | & > .ft.ft--encapsulated:not(.node-container--expanded) {
83 | margin-left: 0;
84 | margin-right: -(@node-word-margin * 4);
85 |
86 | &.ft--no-left-children {
87 | margin-left: @node-word-margin;
88 | }
89 |
90 | &.ft--no-right-children {
91 | margin-left: 0;
92 | margin-right: -(@node-word-margin * 3);
93 | }
94 | }
95 | }
96 |
97 | &.ft--right-children > .node-children-container > .encapsulated[data-pos='right'] {
98 | & > .ft.ft--encapsulated:not(.node-container--expanded) {
99 | margin-left: -(@node-word-margin * 3);
100 |
101 | &.ft--no-right-children {
102 | margin-right: @node-word-margin;
103 | margin-left: -(@node-word-margin * 4);
104 | }
105 | }
106 | }
107 | }
108 |
109 | .ft.ft--encapsulated,
110 | .ft.ft--event-seq-child {
111 | &.node-container--expanded[data-has-children="true"] {
112 | padding: (@node-word-margin * 2.25) (@node-word-margin * 1.5);
113 | border-radius: @node-border-radius;
114 | position: relative;
115 |
116 | &:before {
117 | .u-child100;
118 | .u-pe;
119 | border-radius: @node-border-radius;
120 | }
121 |
122 | &.ft--no-left-children {
123 | padding-left: (@node-word-margin * 1.5) + @node-word-margin;
124 | }
125 |
126 | &.ft--no-right-children {
127 | padding-right: (@node-word-margin * 1.5) + @node-word-margin;
128 | }
129 | }
130 | }
131 |
132 | .encapsulated.event-seq-child {
133 | .u-100;
134 | }
135 |
136 | .ft--left-children,
137 | .ft--right-children {
138 | & > .node-children-container > .event-seq-child--expanded {
139 | padding-bottom: @node-word-margin !important;
140 | }
141 | }
142 |
143 | // Side-hanging nodes with directional children
144 | .encapsulated {
145 | &[data-pos='left'] {
146 | & > .ft.ft--encapsulated {
147 | margin-left: @node-word-margin;
148 | margin-right: -(@node-word-margin * 3);
149 |
150 | &.ft--event {
151 | margin-right: -(@node-word-margin * 4);
152 | }
153 |
154 | &.node-container--expanded.ft--event {
155 | margin-right: -(@node-word-margin * 3);
156 | }
157 | }
158 | }
159 |
160 | &[data-pos='right'] {
161 | & > .ft.ft--encapsulated {
162 | margin-right: 0;
163 | margin-left: -(@node-word-margin * 3);
164 |
165 | &.ft--event {
166 | margin-left: -(@node-word-margin * 4);
167 | }
168 |
169 | &.node-container--expanded {
170 | margin-right: @node-word-margin;
171 |
172 | &.ft--event {
173 | margin-left: -(@node-word-margin * 3);
174 | }
175 | }
176 | }
177 | }
178 | }
179 |
180 | .ft.ft--encapsulated {
181 | margin-left: -@node-word-margin;
182 | margin-top: 0;
183 |
184 | &.node-container--expanded[data-has-children="true"] {
185 | box-shadow: inset 0 0 0 2/@em @node-border-color, 2/@em 4/@em 24/@em rgba(0,0,0,.2);
186 |
187 | &:before {
188 | box-shadow: inset 0 0 0 2/@em fade(@node-border-color, 30%);
189 | }
190 | }
191 | }
192 |
193 | // Event sequence children
194 | .ft.ft--event-seq-child {
195 | margin-left: -@node-word-margin;
196 |
197 | &:not(.ft--no-left-children) > .ft__tr > .ft__tr__td > .node {
198 | margin-left: @node-word-margin;
199 | }
200 |
201 | &.node-container--collapsed {
202 | &.ft--no-left-children {
203 | margin-left: 0;
204 | }
205 | }
206 |
207 | &.node-container--expanded[data-has-children="true"] {
208 | margin: @node-word-margin @node-word-margin 0 0;
209 | box-shadow: inset 0 0 0 2/@em fade(@node-border-color, 70%), 2/@em 4/@em 24/@em rgba(0,0,0,.2);
210 | }
211 | }
212 |
213 | .event-seq-child.event-seq-child--expanded + .event-seq-child.event-seq-child--expanded {
214 | & > .node__word__link {
215 | margin-top: ((@node-word-padding * 2 + @node-strong-label-height) / 2) - (@chain-link-label-height / 2) + (@node-word-margin * 3) !important;
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/src/module/PassageSpan.js:
--------------------------------------------------------------------------------
1 | import { expandPathToNode } from './stores/modules/ui';
2 |
3 | import { connect } from 'react-redux';
4 | import PropTypes from 'prop-types';
5 | import React, { Component } from 'react';
6 | import classNames from 'classnames/bind';
7 |
8 | import { colorToString } from './helpers.js';
9 |
10 | class PassageSpan extends Component {
11 | constructor() {
12 | super();
13 | this.state = {
14 | active: null, // null, hover, pressed
15 | };
16 |
17 | this.handleMouseOver = this.handleMouseOver.bind(this);
18 | this.handleMouseOut = this.handleMouseOut.bind(this);
19 | this.handleMouseUp = this.handleMouseUp.bind(this);
20 | }
21 |
22 | handleMouseOver() {
23 | this.setState({
24 | active: "hover",
25 | }, () => { this.props.hoverNode(this.props.data.id) });
26 | }
27 |
28 | handleMouseOut() {
29 | this.setState({
30 | active: null,
31 | }, () => { this.props.hoverNode("none") });
32 | }
33 |
34 | handleMouseUp() {
35 | this.setState({
36 | active: "null",
37 | }, () => {
38 | const { data, expandPathToNode, focusNode } = this.props;
39 |
40 | expandPathToNode(data.id);
41 | focusNode(data);
42 | });
43 | }
44 |
45 | componentWillReceiveProps(nextProps) {
46 | this.setState({
47 | active: nextProps.hoverNodeId === this.props.data.id ? "hover" : null,
48 | });
49 | }
50 |
51 | render() {
52 | const { active } = this.state;
53 | const { text,
54 | data,
55 | styles,
56 | selectedNodeId,
57 | parentId,
58 | depth,
59 | hoverNodeId,
60 | hoverNode,
61 | focusNode } = this.props;
62 |
63 | // Shorthand consts for fragment data
64 | const segmentsContainer = data.nodeType === "top-level-and";
65 |
66 | function getFragmentData({ alternateParseInfo }) {
67 | return alternateParseInfo && alternateParseInfo.spanAnnotations ? alternateParseInfo.spanAnnotations : null;
68 | }
69 |
70 | const fragmentData = getFragmentData(data);
71 | const textHi = text.length + 1;
72 |
73 | const populateSpans = (children, lo, hi, fragments = true) => {
74 | return children.map((childNode) => {
75 | // Shorthand consts for span data
76 | const hasSpan = childNode.hasOwnProperty("alternateParseInfo") && childNode.alternateParseInfo.hasOwnProperty("charNodeRoot");
77 | const spanField = hasSpan ? childNode.alternateParseInfo.charNodeRoot : null;
78 | const spanLo = fragments && hasSpan ? spanField.charLo : 0;
79 | const spanHi = fragments && hasSpan ? spanField.charHi : textHi;
80 |
81 | // If the child node span fits inside the bounds of the child fragment that triggered this recursion:
82 | if (spanLo >= lo && spanHi <= hi) {
83 | return (
84 |
95 | );
96 | }
97 | });
98 | }
99 |
100 | let output = null;
101 | if (fragmentData) {
102 | output = fragmentData.map(item => {
103 | // If fragment is type child then trigger recursive rendering of children:
104 | if (item.spanType === "child") {
105 | return populateSpans(data.children, item.lo, item.hi);
106 | // Otherwise, render the fragment now:
107 | } else {
108 | return ({text.slice(item.lo, item.hi)} );
109 | }
110 | });
111 | } else {
112 | // If we don't have fragment data, just display the given text. This means that highlighting
113 | // won't work.
114 | output = text;
115 | }
116 |
117 | // Building list of conditional classes for span-slice
118 | const spanConditionalClasses = classNames({
119 | "span-slice--hover": active === "hover" || hoverNodeId === data.id,
120 | "span-slice--pressed": active === "pressed",
121 | "span-slice--focused": selectedNodeId === data.id,
122 | "span-slice--margin": depth === 0,
123 | [`span-slice--${colorToString(styles[data.nodeType])}`]: true,
124 | });
125 |
126 | const onMouseOver = !segmentsContainer ? this.handleMouseOver : null;
127 | const onMouseOut = !segmentsContainer ? this.handleMouseOut : null;
128 | const onMouseDown = !segmentsContainer ? () => {this.setState({active: "pressed"})} : null;
129 | const onMouseUp = !segmentsContainer ? this.handleMouseUp : null;
130 |
131 | return (
132 | 0) ? parentId : "null"}
135 | data-id={data.id}
136 | onMouseOver={onMouseOver}
137 | onMouseOut={onMouseOut}
138 | onMouseDown={onMouseDown}
139 | onMouseUp={onMouseUp}>
140 | {output}
141 |
142 | );
143 | }
144 | }
145 |
146 | PassageSpan.propTypes = {
147 | text: PropTypes.string.isRequired,
148 | data: PropTypes.object,
149 | styles: PropTypes.object,
150 | parentId: PropTypes.string,
151 | selectedNodeId: PropTypes.string,
152 | depth: PropTypes.number,
153 | hoverNodeId: PropTypes.string,
154 | hoverNode: PropTypes.func,
155 | focusNode: PropTypes.func,
156 | expandPathToNode: PropTypes.func.isRequired,
157 | }
158 |
159 | // We have no state to map to props, so we just return an empty object.
160 | const mapStateToProps = () => ({});
161 |
162 | // When PassageSpan is called recursively, it is using the local definition of the component and not the
163 | // exported, "wrapped with connect" definition, which is a higher-ordered component that has been
164 | // decorated with redux store state. The fix is to assign the wrapped version of Node to a new
165 | // variable here, export that, and call it when we recurse.
166 | const PassageSpanWrapper = connect(mapStateToProps, { expandPathToNode })(PassageSpan);
167 |
168 | export default PassageSpanWrapper;
169 |
--------------------------------------------------------------------------------
/bin/build.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | 'use strict';
3 |
4 | const cp = require('child_process');
5 | const fs = require('fs');
6 | const path = require('path');
7 | const chalk = require('chalk');
8 | const browserify = require('browserify');
9 | const watchify = require('watchify');
10 |
11 | // Whenever we incoke `cp.exec` or `cp.execSync` these args set the correct
12 | // working directory and ensure that stdout / sterr are streamed to the
13 | // current TTY
14 | const execArgs = { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' };
15 | const which = require('npm-which')(execArgs.cwd);
16 |
17 | // Argument parsing
18 | const args = new Set(process.argv.slice(2));
19 | if (args.has('-h') || args.has('--help')) {
20 | console.log('Usage: ./build.js [OPTIONS]')
21 | console.log('Options: ')
22 | console.log(' --watch -w rebuild UI artifacts with every change to webui/webapp/app/**/*');
23 | console.log(' --server -s run a local server for development purposes');
24 | console.log(' --skipInitial, -si if specified with --watch, the first build is skipped');
25 | process.exit(0);
26 | }
27 | const isWatchTarget = args.has('--watch') || args.has('-w');
28 | const shouldSkipFirstBuild = isWatchTarget && (args.has('--skipInitial') || args.has('-si'));
29 |
30 | // Build n' bundle the JS and CSS
31 | if (!shouldSkipFirstBuild) {
32 | compileJavascript();
33 | bundleJavascript();
34 | compileLess();
35 | } else {
36 | console.log(chalk.yellow('skipping first build, as --skipInitial was passed'));
37 | }
38 |
39 | // Local developers run the watch target, which watches the files in src/ for changes and updates
40 | // the build artifacts as they occur.
41 | if (isWatchTarget) {
42 | const chokidar = require('chokidar');
43 | const debounce = require('debounce');
44 |
45 | // Watch the css files for changes and recompile whenever one occurs
46 | const lessWatchPath = path.resolve(__dirname, '..', 'src', '**', '*.less');
47 | console.log(chalk.yellow(`watching ${chalk.magenta(path.relative(execArgs.cwd, lessWatchPath))}`));
48 | chokidar.watch(lessWatchPath, { ignoreInitial: true }).on('all', (event, filePath) => {
49 | console.log(chalk.yellow(
50 | `${chalk.magenta(path.relative(execArgs.cwd, filePath))} changed`
51 | ))
52 | try {
53 | compileLess();
54 | } catch(err) {
55 | // Swallow excpetions so the process stays alive. The `lessc` executable reports the exception
56 | // to stderr for us, so we don't need to output anything.
57 | }
58 | });
59 |
60 | // Watch for Javascript changes.
61 | const jsWatchPath = path.resolve(__dirname, '..', 'src', '**', '*.js');
62 | console.log(chalk.yellow(`watching ${chalk.magenta(path.relative(execArgs.cwd, jsWatchPath))}`));
63 | chokidar.watch(jsWatchPath, { ignoreInitial: true }).on('all', (event, filePath) => {
64 | const relativeFilePath = path.relative(execArgs.cwd, filePath);
65 | console.log(chalk.yellow(
66 | `${chalk.magenta(relativeFilePath)} changed`
67 | ))
68 | try {
69 | compileJavascript(relativeFilePath);
70 | } catch (err) {
71 | // Swallow excpetions so the process stays alive. The `babel` executable reports the exception
72 | // to stderr for us, so we don't need to output anything.
73 | }
74 | });
75 | }
76 |
77 | if (args.has('--server') || args.has('-s')) {
78 | runLocalServer();
79 | }
80 |
81 | /**
82 | * Compiles Javascript, using Babel, to a consistent runtime that isn't dependent on a JSX parser
83 | * or future ECMA targets.
84 | *
85 | * @param {String} [jsPath=src] Path to the file(s) to compile. If a directory is specified, all
86 | * *.js files in that directory are compiled.
87 | * @return {undefined}
88 | */
89 | function compileJavascript(filePath) {
90 | const jsPath = filePath || 'src';
91 | const babelPath = which.sync('babel');
92 | const outFile =
93 | jsPath.endsWith('.js')
94 | ? `--out-file ${jsPath.replace('src/', 'dist/')}`
95 | : `-d dist`;
96 | console.log(chalk.cyan(`compling javascript ${chalk.magenta(jsPath)}`));
97 | cp.execSync(`${babelPath} ${jsPath} ${outFile} --ignore test.js`, execArgs);
98 | console.log(chalk.green('babel compilation complete'));
99 | }
100 |
101 | /**
102 | * Compiles less to css.
103 | *
104 | * @return {undefined}
105 | */
106 | function compileLess() {
107 | console.log(chalk.cyan(`compiling ${chalk.magenta('src/less/hierplane.less')}`));
108 | cp.execSync(`${which.sync('lessc')} -x --autoprefix="last 2 versions" src/less/hierplane.less dist/static/hierplane.min.css`);
109 | console.log(chalk.green(`wrote ${chalk.magenta('dist/static/hierplane.min.css')}`));
110 | }
111 |
112 | /**
113 | * Compresses the static javascript bundle into a single file with all required dependencies so
114 | * that people can use it in their browser.
115 | *
116 | * @return {undefined}
117 | */
118 | function bundleJavascript() {
119 | const bundleEntryPath = path.resolve(__dirname, '..', 'dist', 'static', 'hierplane.js');
120 | const bundlePath = path.resolve(__dirname, '..', 'dist', 'static', 'hierplane.bundle.js');
121 |
122 | // Put together our "bundler", which uses browserify
123 | const browserifyOpts = {
124 | entries: [ bundleEntryPath ],
125 | standalone: 'hierplane'
126 | };
127 |
128 | // If we're watching or changes, enable watchify
129 | if (isWatchTarget) {
130 | // These are required for watchify
131 | browserifyOpts.packageCache = {};
132 | browserifyOpts.cache = {};
133 |
134 | // Enable the plugin
135 | browserifyOpts.plugin = [ watchify ];
136 | };
137 |
138 | // Construct the bundler
139 | const bundler = browserify(browserifyOpts);
140 |
141 | // Make an inline function which writes out the bundle, so that we can invoke it whenever
142 | // an update is detected if the watch target is being executed.
143 | const writeBundle = () => {
144 | console.log(chalk.cyan(`bundling ${chalk.magenta(path.relative(execArgs.cwd, bundleEntryPath))}`));
145 | bundler.bundle().pipe(fs.createWriteStream(bundlePath))
146 | console.log(chalk.green(`wrote ${chalk.magenta(path.relative(execArgs.cwd, bundlePath))}`));
147 | };
148 |
149 | // Write out the bundle once
150 | writeBundle();
151 |
152 | // If we're supposed to watch for changes, write out the bundle whenever they occur
153 | if (isWatchTarget) {
154 | bundler.on('update', writeBundle);
155 | }
156 | }
157 |
158 | function runLocalServer() {
159 | return cp.fork(path.resolve(__dirname, '..', 'dev', 'server.js'), execArgs);
160 | }
161 |
162 |
--------------------------------------------------------------------------------
/src/less/explainer/node/node_pos.less:
--------------------------------------------------------------------------------
1 | // Node and Link Positioning
2 |
3 | // Canonical Positioning
4 | .canonical .node[data-pos='left'],
5 | .canonical .node[data-pos='right'],
6 | .canonical .node[data-pos='inside'],
7 | .node[data-pos='down'] {
8 | & > .node__word {
9 | & > .node__word__content > .node__word__label {
10 | padding-top: 0;
11 | }
12 |
13 | & > .node__word__link {
14 | .fn-transform(translateY(-@node-word-margin - (@node-word-tile-border-width / 2)));
15 | margin-bottom: -((@node-word-margin - @node-word-padding) / 2);
16 | min-height: @node-word-margin;
17 |
18 | .node__word__link__tab {
19 | margin: 0 8/@em;
20 | }
21 |
22 | .node__word__link__label {
23 | height: @node-word-margin;
24 | padding: 0 6/@em;
25 | }
26 | }
27 | }
28 |
29 | &[data-collapsable="true"] {
30 | & > .node__word {
31 | & > .node__word__link {
32 | .node__word__link__tab {
33 | margin: 0 18/@em;
34 | }
35 | }
36 | }
37 | }
38 |
39 | &[data-alt-parses="true"] {
40 | & > .node__word {
41 | & > .node__word__link {
42 | .node__word__link__tab {
43 | margin: 0 38/@em;
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
50 | // Directional Positioning
51 | .default .node[data-pos='left'] > .node__word,
52 | .default .node[data-pos='right'] > .node__word,
53 | .default .node[data-pos='inside'] > .node__word,
54 | .event-seq-child,
55 | .encapsulated {
56 | .flex-container-row;
57 |
58 | & > .node__word__link {
59 | .flex-container;
60 | .flex-align-items(center);
61 |
62 | .node__word__link__tab {
63 | .flex-container-column;
64 | margin: -10/@em 0;
65 | }
66 |
67 | .node__word__link__label {
68 | min-width: @node-word-margin;
69 | padding: 2/@em 4/@em;
70 | border-bottom-width: 0;
71 | }
72 | }
73 | }
74 |
75 | // Left Positioning
76 | .default .node[data-pos='left']:not(.node--encapsulated) {
77 | margin-right: 0;
78 |
79 | & > .node__word {
80 | & > .node__word__content {
81 | margin-right: -31/@em;
82 | }
83 |
84 | & > .node__word__link {
85 | .fn-transform(translateX(@node-word-margin + (@node-word-tile-border-width / 2)));
86 | .flex-justify-content(flex-end);
87 | margin-left: -((@node-word-margin - @node-word-padding) + (@node-word-link-label-size) / 2);
88 |
89 | .node__word__link__tab {
90 | filter: drop-shadow(-2/@em 1/@em 2/@em rgba(0,0,0,.15));
91 | }
92 |
93 | .node__word__link__label {
94 | border-left: 1/@em solid @link-border-color;
95 | }
96 | }
97 | }
98 | }
99 |
100 | // Right Positioning
101 | .default .node[data-pos='right']:not(.node--encapsulated) {
102 | margin-left: 0;
103 |
104 | & > .node__word {
105 | & > .node__word__content {
106 | margin-left: -31/@em;
107 | }
108 |
109 | & > .node__word__link {
110 | .fn-transform(translateX(-@node-word-margin - (@node-word-tile-border-width / 2)));
111 | margin-right: -((@node-word-margin - @node-word-padding) + (@node-word-link-label-size) / 2);
112 |
113 | .node__word__link__tab {
114 | filter: drop-shadow(2/@em 1/@em 2/@em rgba(0,0,0,.15));
115 | }
116 |
117 | .node__word__link__label {
118 | border-right: 1/@em solid @link-border-color;
119 | }
120 | }
121 | }
122 | }
123 |
124 | // Sequence Directional Positioning
125 | .default .node.node--seq[data-pos='left'],
126 | .default .node.node--seq[data-pos='right'],
127 | .default .node.node--seq[data-pos='inside'] {
128 | & > .node__word {
129 | & > .node__word__link {
130 | .flex-align-items(flex-start);
131 | }
132 | }
133 | }
134 |
135 | // Inside Positioning
136 | .default .node[data-pos='inside'] > .node__word,
137 | .event-seq-child {
138 | & > .node__word__link {
139 | .flex-align-items(flex-start);
140 | .fn-transform(translateX(-@node-word-margin - 12/@em - (@node-word-tile-border-width / 2)));
141 | margin-top: floor(((@node-word-padding * 2 + @node-word-label-height) / 2) - (@chain-link-label-height / 2)) - 1/@em;
142 | margin-right: -((@node-word-margin - @node-word-padding) + (@node-word-link-label-size) / 2);
143 | margin-right: -(@node-word-margin + @node-word-padding - 4/@em);
144 |
145 | .node__word__link__tab {
146 | filter: drop-shadow(2/@em 1/@em 2/@em rgba(0,0,0,.15));
147 | .fn-backface-visibility(hidden);
148 | }
149 |
150 | .node__word__link__tab__top-cap,
151 | .node__word__link__tab__bottom-cap {
152 | min-width: 34/@em;
153 | height: 11/@em;
154 | position: relative;
155 | }
156 |
157 | .node__word__link__label {
158 | min-width: 34/@em;
159 | min-height: @chain-link-label-height;
160 | padding: 0;
161 | border-bottom-width: 0;
162 | border-right: 1/@em solid @link-border-color;
163 | border-left: 1/@em solid @link-border-color;
164 | }
165 | }
166 | }
167 |
168 | .default .node[data-pos='inside'] {
169 | margin-left: 0;
170 | }
171 |
172 | .event-seq-child,
173 | .event-seq-child.encapsulated {
174 | & > .node__word__link {
175 | margin-top: ((@node-word-padding * 2 + @node-strong-label-height) / 2) - (@chain-link-label-height / 2) + @node-word-margin;
176 | }
177 | }
178 |
179 | // Side-hanging Event Positioning
180 | .encapsulated {
181 | & > .node__word__link {
182 | .flex-align-items(flex-start);
183 | margin-top: @node-word-padding + (@node-strong-label-height / 2) - 26/@em;
184 | }
185 | }
186 |
187 | .encapsulated[data-pos='right']:not(.event-seq-child) > .node__word__link {
188 | .fn-transform(translateX(-@node-word-margin - (@node-word-tile-border-width / 2)));
189 | margin-right: -((@node-word-margin - @node-word-padding) + (@node-word-link-label-size) / 2);
190 |
191 | .node__word__link__tab {
192 | filter: drop-shadow(2/@em 1/@em 2/@em rgba(0,0,0,.15));
193 | }
194 |
195 | .node__word__link__label {
196 | border-right: 1/@em solid @link-border-color;
197 | }
198 | }
199 |
200 | .encapsulated[data-pos='left']:not(.event-seq-child) > .node__word__link {
201 | .fn-transform(translateX(@node-word-margin + (@node-word-tile-border-width / 2)));
202 | .flex-justify-content(flex-end);
203 | margin-left: -((@node-word-margin - @node-word-padding) + (@node-word-link-label-size) / 2);
204 |
205 | .node__word__link__tab {
206 | filter: drop-shadow(-2/@em 1/@em 2/@em rgba(0,0,0,.15));
207 | }
208 |
209 | .node__word__link__label {
210 | border-left: 1/@em solid @link-border-color;
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/module/node/NodeWord.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import Attributes from './Attributes.js';
4 | import Link from './Link.js';
5 | import UiToggle from './UiToggle.js';
6 | import UiParseNav from './UiParseNav.js';
7 |
8 | class NodeWord extends React.Component {
9 |
10 | render() {
11 | const { readOnly,
12 | hasInsideChildren,
13 | layout,
14 | dataPos,
15 | positions,
16 | linkLabels,
17 | data,
18 | text,
19 | onMouseOver,
20 | onMouseOut,
21 | onMouseDown,
22 | onMouseUp,
23 | onUiMouseOver,
24 | onUiMouseOut,
25 | onUiMouseUp,
26 | onPnMouseOver,
27 | onPnMouseOut,
28 | onPnMouseUp,
29 | dataCollapsable,
30 | altParses,
31 | rollups,
32 | isRoot,
33 | isEventRoot,
34 | togglePane,
35 | insideChildren,
36 | eventSeqChild,
37 | encapsulated,
38 | notFirstInsideChild } = this.props;
39 |
40 | // charNodeRoot is the field in the JSON node object that contains its span's
41 | // lo and hi values that let the UI extract a phrase from the original query.
42 | const hasFragments = data.hasOwnProperty("alternateParseInfo") && data.alternateParseInfo.hasOwnProperty("spanAnnotations");
43 | const hasRollup = rollups && dataCollapsable && hasFragments;
44 | const fragmentData = hasFragments ? data.alternateParseInfo.spanAnnotations : null;
45 |
46 | // Max rollup characters before node is forced to text wrap
47 | const maxRollupChars = 40;
48 | // Boolean that returns true if node span is more than maxRollupChars (used in conditional class of .node__word__label)
49 | const wideRollup = hasRollup && (data.alternateParseInfo.charNodeRoot.charHi - data.alternateParseInfo.charNodeRoot.charLo >= maxRollupChars);
50 |
51 | // Iterates through spanAnnotations to wrap head word ("self") in a tag
52 | // so it is visually distinct from the rest of the rollup text.
53 | const rollupText = hasFragments ? fragmentData.map((item, index) => {
54 | if (item.spanType === "self") {
55 | return (
56 | {text.slice(item.lo, item.hi)}
57 | );
58 | } else {
59 | return ` ${text.slice(item.lo, item.hi)} `;
60 | }
61 | }) : null;
62 |
63 | const toggle = (
64 |
68 | );
69 |
70 | const parseNav = (
71 |
77 | );
78 |
79 | const focusTrigger = (
80 | { togglePane("open") }}
85 | onMouseUp={() => { onMouseUp(data) }}>
86 |
87 | );
88 |
89 | return (
90 | !isRoot ? (
91 | 0 ? "node__word--has-attrs" : ""}
93 | ${hasRollup ? "node__word--has-rollup" : ""}`}>
94 | {/* Node Word Tile */}
95 |
96 | {/* Left / Top Link */}
97 | {((!isEventRoot && data.link && layout === "canonical") ||
98 | (!isEventRoot && data.link && layout === "default" && positions[data.link] !== "left" && notFirstInsideChild && !encapsulated && !eventSeqChild)) ?
99 |
: null}
100 |
101 | {/* Node Word Label */}
102 |
103 |
104 | {data.word}
105 | {hasRollup ? ({rollupText} ) : null}
106 |
107 |
108 | {hasInsideChildren ? insideChildren : null}
109 | {/* Attributes */}
110 |
111 |
112 | {/* Right Link */}
113 | {(!encapsulated && !eventSeqChild && data.link && layout === "default" && positions[data.link] === "left") ?
114 |
: null}
115 | {focusTrigger}
116 | {/* UI Toggle */}
117 | {(dataCollapsable) ? toggle : null}
118 | {(altParses) ? parseNav : null}
119 |
) : (
120 | (altParses) ? (
121 |
122 | {focusTrigger}
123 | {(altParses) ? parseNav : null}
124 |
125 | ) : null
126 | )
127 | );
128 | }
129 | }
130 |
131 | NodeWord.propTypes = {
132 | readOnly: PropTypes.bool,
133 | positions: PropTypes.object.isRequired,
134 | linkLabels: PropTypes.object.isRequired,
135 | data: PropTypes.shape({
136 | attributes: PropTypes.arrayOf(PropTypes.string.isRequired),
137 | }),
138 | text: PropTypes.string,
139 | layout: PropTypes.string.isRequired,
140 | dataPos: PropTypes.string.isRequired,
141 | hasInsideChildren: PropTypes.bool,
142 | dataCollapsable: PropTypes.bool.isRequired,
143 | altParses: PropTypes.bool.isRequired,
144 | rollups: PropTypes.bool.isRequired,
145 | isRoot: PropTypes.bool.isRequired,
146 | isEventRoot: PropTypes.bool.isRequired,
147 | onMouseOver: PropTypes.func.isRequired,
148 | onMouseOut: PropTypes.func.isRequired,
149 | onMouseDown: PropTypes.func.isRequired,
150 | onMouseUp: PropTypes.func.isRequired,
151 | onUiMouseOver: PropTypes.func.isRequired,
152 | onUiMouseOut: PropTypes.func.isRequired,
153 | onUiMouseUp: PropTypes.func.isRequired,
154 | onPnMouseOver: PropTypes.func.isRequired,
155 | onPnMouseOut: PropTypes.func.isRequired,
156 | onPnMouseUp: PropTypes.func.isRequired,
157 | togglePane: PropTypes.func,
158 | insideChildren: PropTypes.object,
159 | eventSeqChild: PropTypes.bool,
160 | encapsulated: PropTypes.bool,
161 | notFirstInsideChild: PropTypes.bool,
162 | }
163 |
164 | export default NodeWord;
165 |
--------------------------------------------------------------------------------
/src/module/helpers.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | assignNodeIds,
3 | findAllNodeTypes,
4 | getCollapsibleNodeIds,
5 | generateStylesForNodeTypes,
6 | isSingleSegment,
7 | translateSpans
8 | } from './helpers';
9 |
10 | import { expect } from 'chai';
11 | import Immutable from 'immutable';
12 |
13 | const singleSegmentTree = {
14 | children: [{
15 | children: [{
16 | children: [],
17 | id: '0.0.0',
18 | kind: 'symbol',
19 | }, {
20 | children: [{
21 | children: [{
22 | children: [],
23 | id: '0.0.1.0.0',
24 | kind: 'const',
25 | }, {
26 | children: [],
27 | id: '0.0.1.0.1',
28 | kind: 'const',
29 | }],
30 | id: '0.0.1.0',
31 | kind: 'sequence',
32 | }],
33 | id: '0.0.1',
34 | kind: 'detail',
35 | }],
36 | id: '0.0',
37 | kind: 'entity',
38 | }],
39 | id: '0',
40 | kind: 'event',
41 | };
42 |
43 | const multiSegmentTree = {
44 | children: [{
45 | children: [{
46 | children: [],
47 | id: '0.0.0',
48 | kind: 'entity',
49 | }, {
50 | children: [],
51 | id: '0.0.1',
52 | kind: 'detail',
53 | }],
54 | id: '0.0',
55 | kind: 'event',
56 | }, {
57 | children: [{
58 | children: [{
59 | children: [],
60 | id: '0.1.0.0',
61 | kind: 'symbol',
62 | }],
63 | id: '0.1.0',
64 | kind: 'entity',
65 | }, {
66 | children: [],
67 | id: '0.1.1',
68 | kind: 'detail',
69 | }],
70 | id: '0.1',
71 | kind: 'event',
72 | }],
73 | id: '0',
74 | kind: 'top-level-and',
75 | };
76 |
77 | describe('isSingleSegment', () => {
78 | it('returns false if the root node in a parse tree is either "top-level-and" or "and"', () => {
79 | expect(isSingleSegment("top-level-and")).to.be.false;
80 | expect(isSingleSegment("and")).to.be.false;
81 | });
82 |
83 | it('returns true if the root node in a parse tree is not "top-level-and" or "and"', () => {
84 | expect(isSingleSegment("event")).to.be.true;
85 | })
86 | });
87 |
88 | describe('getCollapsibleNodeIds', () => {
89 | it('only returns collapsible node ids for single segmented parse trees', () => {
90 | const expectedNodeIds = Immutable.Set(['0.0','0.0.1','0.0.1.0']);
91 | const collapsibleIds = getCollapsibleNodeIds(singleSegmentTree, true);
92 | expect(collapsibleIds.equals(expectedNodeIds)).to.be.true;
93 | });
94 |
95 | it('only returns collapsible node ids for multi-segmented parse trees', () => {
96 | const expectedNodeIds = Immutable.Set(['0.1.0']);
97 | const collapsibleIds = getCollapsibleNodeIds(multiSegmentTree, false);
98 | expect(collapsibleIds.equals(expectedNodeIds)).to.be.true;
99 | });
100 | });
101 |
102 | describe('assignNodeIds', () => {
103 | it('does not overwrite existing ids', () => {
104 | const tree = {
105 | id: 'hi',
106 | children: [ {}, { id: 'foo' } ]
107 | };
108 | const withIds = assignNodeIds(tree);
109 | expect(withIds.id).to.equal('hi');
110 | expect(withIds.children[0].id).to.equal('hi.0');
111 | expect(withIds.children[1].id).to.equal('foo');
112 | });
113 |
114 | it('does not mutate the original object', () => {
115 | const tree = {};
116 | const withIds = assignNodeIds(tree);
117 | expect(withIds.id).to.equal('0');
118 | expect(tree.id).to.be.undefined;
119 | });
120 |
121 | it('assigns ids as expected', () => {
122 | const tree = {
123 | children: [
124 | { children: [ {}, {} ] },
125 | { children: [ {} ] },
126 | {}
127 | ]
128 | };
129 | const expectedTree = {
130 | id: '0',
131 | children: [
132 | {
133 | id: '0.0',
134 | children: [
135 | { id: '0.0.0' },
136 | { id: '0.0.1' }
137 | ]
138 | },
139 | {
140 | id: '0.1',
141 | children: [
142 | { id: '0.1.0' }
143 | ]
144 | },
145 | { id: '0.2' }
146 | ]
147 | }
148 | expect(assignNodeIds(tree)).to.deep.equal(expectedTree);''
149 | });
150 |
151 | describe('findAllNodeTypes', () => {
152 | it('returns all node types in a given tree', () => {
153 | const tree = {
154 | nodeType: 1,
155 | children: [
156 | { nodeType: 2 },
157 | { nodeType: 2 },
158 | {
159 | nodeType: 3,
160 | children: [ { nodeType: 4 } ]
161 | },
162 | ]
163 | };
164 | expect(findAllNodeTypes(tree).toJS()).to.deep.equal([ 1, 2, 3, 4 ]);
165 | });
166 | });
167 |
168 | describe('generateStylesForNodeTypes', () => {
169 | it('returns a style for each node type', () => {
170 | const nodeTypes = Immutable.Set([ 1, 2, 3, 4, 5, 6, 7, 8 ]);
171 | const expectedStyles = {
172 | '1': [ 'color1' ],
173 | '2': [ 'color2' ],
174 | '3': [ 'color3' ],
175 | '4': [ 'color4' ],
176 | '5': [ 'color5' ],
177 | '6': [ 'color6' ],
178 | '7': [ 'color1' ],
179 | '8': [ 'color2' ]
180 | };
181 | expect(generateStylesForNodeTypes(nodeTypes)).to.deep.equal(expectedStyles);
182 | });
183 | });
184 |
185 | describe('translateSpans', () => {
186 | it('translates the `spans` protocol to `alternateParseInfo` as expected', () => {
187 | const treeWithSpans = {
188 | spans: [ { start: 10, end: 13 } ],
189 | children: [
190 | { spans: [ { start: 0, end: 10 } ] },
191 | { spans: [ { start: 13, end: 15 }, { start: 15, end: 17 } ] }
192 | ]
193 | };
194 | const expectedTranslatedTree = {
195 | spans: [ { start: 10, end: 13 } ],
196 | alternateParseInfo: {
197 | charNodeRoot: { charLo: 0, charHi: 17 },
198 | spanAnnotations: [
199 | { lo: 0, hi: 10, spanType: 'child' },
200 | { lo: 10, hi: 13, spanType: 'self' },
201 | { lo: 13, hi: 17, spanType: 'child' },
202 | ]
203 | },
204 | children: [
205 | {
206 | alternateParseInfo: {
207 | charNodeRoot: { charLo: 0, charHi: 10 },
208 | spanAnnotations: [ { lo: 0, hi: 10, spanType: 'self' } ]
209 | },
210 | spans: [ { start: 0, end: 10 } ]
211 | },
212 | {
213 | alternateParseInfo: {
214 | charNodeRoot: { charLo: 13, charHi: 17 },
215 | spanAnnotations: [ { lo: 13, hi: 15, spanType: 'self' }, { lo: 15, hi: 17, spanType: 'self' } ]
216 | },
217 | spans: [ { start: 13, end: 15 }, { start: 15, end: 17 } ]
218 | }
219 | ]
220 | };
221 | expect(translateSpans(treeWithSpans)).to.deep.equal(expectedTranslatedTree);
222 | });
223 |
224 | it('preserves the type of a user provided span', () => {
225 | const treeWithSpans = {
226 | spans: [
227 | { start: 10, end: 13 },
228 | { start: 14, end: 15, spanType: 'ignored' }
229 | ]
230 | };
231 | const { alternateParseInfo: { spanAnnotations } } = translateSpans(treeWithSpans);
232 | expect(spanAnnotations[0].spanType).to.equal("self");
233 | expect(spanAnnotations[1].spanType).to.equal("ignored");
234 | });
235 | })
236 | });
237 |
--------------------------------------------------------------------------------
/src/module/stores/modules/ui.test.js:
--------------------------------------------------------------------------------
1 | import uiReducer, {
2 | ADD_ALL_NODE_IDS, addAllNodeIds,
3 | TOGGLE_NODE_STATE, toggleNode,
4 | COLLAPSE_NODE, collapseNode,
5 | COLLAPSE_ALL_NODES, collapseAllNodes,
6 | COLLAPSE_DESCENDANTS, collapseDescendants,
7 | EXPAND_NODE, expandNode,
8 | EXPAND_ALL_NODES, expandAllNodes,
9 | EXPAND_PATH_TO_NODE, expandPathToNode,
10 | } from './ui';
11 |
12 | import { expect } from 'chai';
13 | import Immutable from 'immutable';
14 |
15 | describe('ui actions', () => {
16 | it('addAllNodeIds - should create an action to add node ids', () => {
17 | const ids = Immutable.Set(['1.1.2.3.5', '3.1.4.1.5']);
18 | const expectedAction = { ids, type: ADD_ALL_NODE_IDS };
19 |
20 | expect(addAllNodeIds(ids)).to.deep.equal(expectedAction);
21 | });
22 |
23 | it('toggleNode - should create an action to toggle a node by id', () => {
24 | const id = '1.2.3';
25 | const expectedAction = { id, type: TOGGLE_NODE_STATE };
26 |
27 | expect(toggleNode(id)).to.deep.equal(expectedAction);
28 | });
29 |
30 | it('collapseNode - should create an action to collapse a node by id', () => {
31 | const id = '1.2.3';
32 | const expectedAction = { id, type: COLLAPSE_NODE };
33 |
34 | expect(collapseNode(id)).to.deep.equal(expectedAction);
35 | });
36 |
37 | it('expandNode - should create an action to expand a node by id', () => {
38 | const id = '1.2.3';
39 | const expectedAction = { id, type: EXPAND_NODE };
40 |
41 | expect(expandNode(id)).to.deep.equal(expectedAction);
42 | });
43 |
44 | it('collapseAllNodes - should create an action to collapse all nodes', () => {
45 | const expectedAction = { type: COLLAPSE_ALL_NODES };
46 |
47 | expect(collapseAllNodes()).to.deep.equal(expectedAction);
48 | });
49 |
50 | it('expandAllNodes - should create an action to expand all nodes', () => {
51 | const expectedAction = { type: EXPAND_ALL_NODES };
52 |
53 | expect(expandAllNodes()).to.deep.equal(expectedAction);
54 | });
55 |
56 | it('collapseDescendants - should create an action to collapse all descendants for some node id', () => {
57 | const id = '0.0';
58 | const expectedAction = { id, type: COLLAPSE_DESCENDANTS };
59 |
60 | expect(collapseDescendants(id)).to.deep.equal(expectedAction);
61 | });
62 |
63 | it('expandPathToNode - should create an action to expand a path to a node id', () => {
64 | const id = '0.0';
65 | const expectedAction = { id, type: EXPAND_PATH_TO_NODE };
66 |
67 | expect(expandPathToNode(id)).to.deep.equal(expectedAction);
68 | });
69 | });
70 |
71 | describe('ui reducer', () => {
72 | it('should return the initial state', () => {
73 | expect(uiReducer(undefined, {})).to.deep.equal({
74 | expandableNodeIds: Immutable.Set(),
75 | expandedNodeIds: Immutable.Set(),
76 | exploded: false,
77 | });
78 | });
79 |
80 | it('should handle ADD_ALL_NODE_IDS', () => {
81 | const { expandableNodeIds } = uiReducer(
82 | { expandableNodeIds: Immutable.Set() },
83 | { type: ADD_ALL_NODE_IDS, ids: Immutable.Set(['2.4.6', '1.3.5']) }
84 | );
85 |
86 | expect(expandableNodeIds.size).to.equal(2);
87 | expect(expandableNodeIds.has('2.4.6')).to.be.true;
88 | expect(expandableNodeIds.has('1.3.5')).to.be.true;
89 | });
90 |
91 | describe('TOGGLE_NODE_STATE', () => {
92 | it('should collapse a node if it is expanded', () => {
93 | const { expandedNodeIds } = uiReducer(
94 | { expandedNodeIds: Immutable.Set(['1.2.3']) },
95 | { type: TOGGLE_NODE_STATE, id: '1.2.3' }
96 | );
97 |
98 | expect(expandedNodeIds.has('1.2.3')).to.be.false;
99 | });
100 |
101 | it('should expand a node if is collapsed', () => {
102 | const { expandedNodeIds } = uiReducer(
103 | { expandedNodeIds: Immutable.Set() },
104 | { type: TOGGLE_NODE_STATE, id: '1.2.3' }
105 | );
106 |
107 | expect(expandedNodeIds.has('1.2.3')).to.be.true;
108 | });
109 |
110 | it('should set exploded to false when collapsing a node', () => {
111 | const { exploded } = uiReducer(
112 | { expandedNodeIds: Immutable.Set(['1.2.3']), expandableNodeIds: Immutable.Set(['1.2.3']) },
113 | { type: TOGGLE_NODE_STATE, id: '1.2.3' }
114 | );
115 |
116 | expect(exploded).to.be.false;
117 | });
118 |
119 | it('should set exploded to true when expanding a node if it is the last expandable node', () => {
120 | const { exploded } = uiReducer(
121 | { expandedNodeIds: Immutable.Set([]), expandableNodeIds: Immutable.Set(['1.2.3']) },
122 | { type: TOGGLE_NODE_STATE, id: '1.2.3' }
123 | );
124 |
125 | expect(exploded).to.be.true;
126 | });
127 | });
128 |
129 | describe('expand logic', () => {
130 | it('should handle EXPAND_NODE', () => {
131 | const { expandedNodeIds } = uiReducer(
132 | { expandedNodeIds: Immutable.Set([]), expandableNodeIds: Immutable.Set([]) },
133 | { type: EXPAND_NODE, id: '1.2.3' }
134 | );
135 |
136 | expect(expandedNodeIds.has('1.2.3')).to.be.true;
137 | });
138 |
139 | it('should handle EXPAND_ALL_NODES', () => {
140 | const { expandedNodeIds, expandableNodeIds, exploded } = uiReducer(
141 | { expandableNodeIds: Immutable.Set(['1.2.3', '2.3.4', '3.4.5']) },
142 | { type: EXPAND_ALL_NODES }
143 | );
144 |
145 | expect(exploded).to.be.true;
146 | expect(expandedNodeIds).to.deep.equal(expandableNodeIds);
147 | });
148 |
149 | it('should explode state when the last expandable node is expanded', () => {
150 | const { exploded } = uiReducer(
151 | { expandableNodeIds: Immutable.Set(['0', '1', '2']), expandedNodeIds: Immutable.Set(['0', '1']) },
152 | { type: EXPAND_NODE, id: '2' }
153 | );
154 |
155 | expect(exploded).to.be.true;
156 | });
157 | });
158 |
159 | it('should handle COLLAPSE_NODE', () => {
160 | const { expandedNodeIds } = uiReducer(
161 | { expandedNodeIds: Immutable.Set(['1.2.3']) },
162 | { type: COLLAPSE_NODE, id: '1.2.3' }
163 | );
164 |
165 | expect(expandedNodeIds.has('1.2.3')).to.be.false;
166 | });
167 |
168 |
169 | it('should handle COLLAPSE_ALL_NODES', () => {
170 | const { expandedNodeIds } = uiReducer(
171 | { expandedNodeIds: Immutable.Set(['1.2.3', '2.3.4', '3.4.5']) },
172 | { type: COLLAPSE_ALL_NODES }
173 | );
174 |
175 | expect(expandedNodeIds.isEmpty()).to.be.true;
176 | });
177 |
178 | it('should handle COLLAPSE_DESCENDANTS', () => {
179 | const node = '0.0';
180 | const sibling = '0.1';
181 | const child1 = '0.0.0';
182 | const child2 = '0.0.1';
183 |
184 | const { expandedNodeIds } = uiReducer(
185 | { expandedNodeIds: Immutable.Set([ node, sibling, child1, child2 ]) },
186 | { id: node, type: COLLAPSE_DESCENDANTS }
187 | );
188 |
189 | expect(expandedNodeIds.has(node)).to.be.true;
190 | expect(expandedNodeIds.has(sibling)).to.be.true;
191 | expect(expandedNodeIds.has(child1)).to.be.false;
192 | expect(expandedNodeIds.has(child2)).to.be.false;
193 | });
194 |
195 | it('should handle EXPAND_PATH_TO_NODE', () => {
196 | const id = '0.0.0.0';
197 | const expectedIds = Immutable.Set(['0', '0.0', '0.0.0', '0.0.0.0']);
198 | const { expandedNodeIds } = uiReducer(undefined, { id, type: EXPAND_PATH_TO_NODE });
199 |
200 | expect(expandedNodeIds.equals(expectedIds)).to.be.true;
201 | });
202 | });
203 |
--------------------------------------------------------------------------------
/src/module/node/MiddleParent.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import NodeWord from './NodeWord.js';
4 | import classNames from 'classnames/bind';
5 |
6 | import { colorToString } from '../helpers.js';
7 |
8 | // Converts an array of classes to string.
9 | function stylesToString(arr = []) {
10 | return arr.reduce((str, style) => {
11 | return "node--" + style + " " + str;
12 | }, "");
13 | }
14 |
15 | class MiddleParent extends Component {
16 |
17 | // TODO: Try to pull as much business logic out of the render function as possible.
18 | render() {
19 | const { readOnly,
20 | canonicalChildren,
21 | hasChildren,
22 | hasSideChildren,
23 | hasInsideChildren,
24 | hasDownChildren,
25 | layout,
26 | positions,
27 | linkLabels,
28 | data,
29 | depth,
30 | styles,
31 | active,
32 | collapsed,
33 | nodeFocusing,
34 | dataCollapsable,
35 | rollups,
36 | isRoot,
37 | isSingleSegment,
38 | isEventRoot,
39 | onMouseOver,
40 | onMouseOut,
41 | onMouseDown,
42 | onMouseUp,
43 | onUiMouseOver,
44 | onUiMouseOut,
45 | onUiMouseUp,
46 | onPnMouseOver,
47 | onPnMouseOut,
48 | onPnMouseUp,
49 | text,
50 | parentId,
51 | togglePane,
52 | insideChildren,
53 | directionalChildIndex,
54 | dataPos,
55 | eventSeqChild,
56 | encapsulated,
57 | notFirstInsideChild,
58 | seqType,
59 | focused } = this.props;
60 |
61 | const { id, nodeType } = data;
62 |
63 | const altParseInfo = data.alternateParseInfo;
64 | const altParses = altParseInfo !== undefined && (altParseInfo.hasOwnProperty("prevParse") || altParseInfo.hasOwnProperty("nextParse"));
65 | const nodeCollapsed = dataCollapsable && collapsed && (!hasSideChildren || (hasSideChildren && hasInsideChildren)) && !isRoot && !isEventRoot;
66 |
67 | // nodeConditionalClasses builds dynamic class lists for .node blocks:
68 | const nodeConditionalClasses = classNames({
69 | "node--root": isRoot,
70 | "node--has-alt-parses": altParses,
71 | "node--hover": active === "hover",
72 | "node--toggle-ready": active === "toggle-ready",
73 | "node--focused": focused,
74 | "node--focusing": nodeFocusing,
75 | "node--encapsulated": encapsulated,
76 | "node-container--collapsed": nodeCollapsed,
77 | "node-container--expanded": !nodeCollapsed,
78 | "node-container--active": active !== null && hasChildren && !hasSideChildren,
79 | [`${stylesToString(styles[data.nodeType])}`]: true,
80 | [`node--${colorToString(styles[seqType])}`]: seqType !== null,
81 | });
82 |
83 | // Screen Output
84 | return (
85 |
86 | {/* Node */}
87 |
0) ? parentId : "null"}
90 | data-node-type={nodeType}
91 | data-pos={dataPos}
92 | data-is-root={isRoot}
93 | data-is-single-segment={isSingleSegment}
94 | data-is-event-root={isEventRoot}
95 | data-depth={depth}
96 | data-has-children={hasChildren}
97 | data-has-side-children={hasSideChildren}
98 | data-has-inside-children={hasInsideChildren}
99 | data-has-down-children={hasDownChildren}
100 | data-collapsable={dataCollapsable}
101 | data-directional-child-index={directionalChildIndex}
102 | data-alt-parses={altParses} >
103 | {/* Node Word */}
104 |
136 |
137 | {/* Canonical Children */}
138 | {canonicalChildren}
139 |
140 |
141 | );
142 | }
143 | }
144 |
145 | MiddleParent.propTypes = {
146 | readOnly: PropTypes.bool,
147 | styles: PropTypes.object.isRequired,
148 | positions: PropTypes.object.isRequired,
149 | linkLabels: PropTypes.object.isRequired,
150 | data: PropTypes.shape({
151 | id: PropTypes.string,
152 | kind: PropTypes.string,
153 | word: PropTypes.string,
154 | attributes: PropTypes.arrayOf(PropTypes.string.isRequired),
155 | children: PropTypes.arrayOf(PropTypes.object.isRequired),
156 | link: PropTypes.string,
157 | }),
158 | text: PropTypes.string,
159 | depth: PropTypes.number.isRequired,
160 | layout: PropTypes.string.isRequired,
161 | hasChildren: PropTypes.bool.isRequired,
162 | hasSideChildren: PropTypes.bool.isRequired,
163 | hasInsideChildren: PropTypes.bool,
164 | hasDownChildren: PropTypes.bool,
165 | canonicalChildren: PropTypes.object,
166 | active: PropTypes.string,
167 | collapsed: PropTypes.bool.isRequired,
168 | nodeFocusing: PropTypes.bool.isRequired,
169 | dataCollapsable: PropTypes.bool.isRequired,
170 | rollups: PropTypes.bool.isRequired,
171 | isRoot: PropTypes.bool.isRequired,
172 | isSingleSegment: PropTypes.bool,
173 | isEventRoot: PropTypes.bool.isRequired,
174 | onMouseOver: PropTypes.func.isRequired,
175 | onMouseOut: PropTypes.func.isRequired,
176 | onMouseDown: PropTypes.func.isRequired,
177 | onMouseUp: PropTypes.func.isRequired,
178 | onUiMouseOver: PropTypes.func.isRequired,
179 | onUiMouseOut: PropTypes.func.isRequired,
180 | onUiMouseUp: PropTypes.func.isRequired,
181 | onPnMouseOver: PropTypes.func.isRequired,
182 | onPnMouseOut: PropTypes.func.isRequired,
183 | onPnMouseUp: PropTypes.func.isRequired,
184 | togglePane: PropTypes.func,
185 | parentId: PropTypes.string,
186 | insideChildren: PropTypes.object,
187 | directionalChildIndex: PropTypes.number,
188 | dataPos: PropTypes.string,
189 | eventSeqChild: PropTypes.bool,
190 | encapsulated: PropTypes.bool,
191 | notFirstInsideChild: PropTypes.bool,
192 | seqType: PropTypes.string,
193 | focused: PropTypes.bool,
194 | }
195 |
196 | export default MiddleParent;
197 |
--------------------------------------------------------------------------------
/src/module/helpers.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import merge from 'merge';
3 |
4 | /**
5 | * Whether a parse tree is a single segment or not. This function is called at the top of the tree
6 | * and is passed down to all child nodes.
7 | *
8 | * @param {string} kind The type of root node.
9 | * @return {boolean}
10 | */
11 | export function isSingleSegment(kind) {
12 | return kind !== "top-level-and" && kind !== "and" ? true : false
13 | }
14 |
15 | /**
16 | * A recursive function for concatenating the ids of nodes that are collapsible in a depth
17 | * first manner.
18 | *
19 | * Setting children to [] indicates the base case where map is acting on an empty array, therefore
20 | * not recursing. A set with that leaf node's id is returned in this case.
21 | */
22 | export function getCollapsibleNodeIds({ id, children = [], kind }, singleSegment) {
23 | /*
24 | We only want to capture the ids of nodes that are collapsible, and, therefore, only nodes that
25 | a) have children and b) are not "root" nodes (as root nodes are not collapsible).
26 | A root node depends on whether a parse tree is a comprised of one or many "segments".
27 | If it has many segments, then nodes at both the '0', i.e., ids of length 1, and '0.x', i.e.,
28 | ids of length 3, levels are roots. Otherwise, it is a single segment, and just the '0' level
29 | is the root.
30 | */
31 |
32 | const hasChildren = children.length > 0;
33 | const isRoot = id.length === 1;
34 | const isEventRoot = (!singleSegment && id.length === 3) || (singleSegment && isRoot);
35 |
36 | const dataCollapsible = hasChildren && !isRoot && !isEventRoot;
37 | const nodeId = dataCollapsible ? [id] : [];
38 |
39 | return hasChildren
40 | ? Immutable.Set(nodeId).union(...children.map(child => getCollapsibleNodeIds(child, singleSegment)))
41 | : Immutable.Set();
42 | }
43 |
44 | // Filter color style out of the style object and return the value:
45 | export function colorToString(arr = []) {
46 | return arr.filter((item) => {
47 | return item.indexOf("color") === 0;
48 | }, "");
49 | }
50 |
51 | /**
52 | * Returns a copy of the node, where the node and all of it's descendants are assigned unique
53 | * identifiers. Uniqueness is only guaranteed within the scope of the provided tree.
54 | *
55 | * @param {Node} node
56 | * @param {String} [prefix=''] A prefix to append to generated identifiers.
57 | * @param {Number} [childIdx=0] The index of the node in it's parent.
58 | * @return {Node}
59 | */
60 | export function assignNodeIds(node, prefix = '', childIdx = 0) {
61 | const nodeCopy = merge.recursive(true, node);
62 | const isLeaf = !Array.isArray(nodeCopy.children) || nodeCopy.children.length === 0;
63 | if (!nodeCopy.id) {
64 | nodeCopy.id = `${prefix}${childIdx}`;
65 | }
66 | if (Array.isArray(nodeCopy.children)) {
67 | nodeCopy.children = nodeCopy.children.slice().map(
68 | (node, idx) => assignNodeIds(node, `${nodeCopy.id}.`, idx)
69 | );
70 | }
71 | return nodeCopy;
72 | }
73 |
74 | /**
75 | * Returns an Immutable.Set including all unique nodeTypes discovered in the tree and all of it's
76 | * descendants.
77 | *
78 | * @param {Node} node
79 | * @return {Immutable.Set} All unique nodeType values present in the tree.
80 | */
81 | export function findAllNodeTypes(node) {
82 | const nodeTypes = Immutable.Set([ node.nodeType ]);
83 | if (Array.isArray(node.children)) {
84 | return node.children.reduce((types, node) => types.concat(findAllNodeTypes(node)), nodeTypes);
85 | } else {
86 | return nodeTypes;
87 | }
88 | }
89 |
90 | /**
91 | * Generates a map of node types to styles, for the provided node types.
92 | *
93 | * @param {Immutable.Set} nodeTypes The set of all node types for which styles should be defined.
94 | * @return {object} A dictionary where each key is a nodeType and each value is a collection
95 | * of styles to be applied to that node.
96 | */
97 | export function generateStylesForNodeTypes(nodeTypes) {
98 | if (!(nodeTypes instanceof Immutable.Set)) {
99 | throw new Error('You must provide an Immutable.Set of node types.');
100 | }
101 | return nodeTypes.reduce((nodeTypeToStyle, nodeType) => {
102 | // We have colors 0 through 6. Dyanmically assign them.
103 | return nodeTypeToStyle.set(nodeType, [ `color${nodeTypeToStyle.size % 6 + 1}` ]);
104 | }, Immutable.Map()).toJS();
105 | }
106 |
107 | /**
108 | * Returns a copy fo the node and all of it's descendants, translating the generic `spans` interface
109 | * into `alternateParseInfo` as appropriate. This method was written to support translation from
110 | * a "public", easy to digest API into that which the existing UI / API expects.
111 | *
112 | * TODO (codeviking): In the long run we should remove this mechanism and use a more canonical API.
113 | *
114 | * @param {Node} origNode
115 | * @return {Node} node The same node, mutated.
116 | */
117 | export function translateSpans(origNode) {
118 | const node = merge.recursive(true, origNode);
119 |
120 | // First translate all of this node's children
121 | if (Array.isArray(node.children)) {
122 | node.children = node.children.map(translateSpans);
123 | }
124 |
125 | // If the property already exists, we assume it's data being delivered by Euclid's API, in which
126 | // case we shouldn't mutate the tree.
127 | if (!node.alternateParseInfo) {
128 | // First we build up alternateParseInfo.charNodeRoot, which is a single span that captures the
129 | // aggregate boundaries of the span and all of it's children.
130 | const boundaries = getSpanBoundaries(node);
131 | const charNodeRoot = (
132 | boundaries
133 | ? new CharNodeRoot(boundaries.start, boundaries.end)
134 | : undefined
135 | );
136 |
137 | // TODO (codeviking): The UI should really support it being `undefined`, rather that using
138 | // if node.hasOwnProperty('charNodeRoot'), as then we wouldn't have to have carefully
139 | // implemented logic like so.
140 | if (charNodeRoot) {
141 | node.alternateParseInfo = { charNodeRoot };
142 | }
143 |
144 | // Now let's build up spanAnnotations, which are the aggregate boundaries (charNodeRoot) of the
145 | // node's immediate children and the node's own spans.
146 | const spanAnnotations =
147 | (node.children || [])
148 | .filter(n => n.alternateParseInfo && n.alternateParseInfo.charNodeRoot)
149 | .map(n => new Span(
150 | /* lo = */ n.alternateParseInfo.charNodeRoot.charLo,
151 | /* hi = */ n.alternateParseInfo.charNodeRoot.charHi,
152 | /* spanType = */'child'
153 | ))
154 | .concat(
155 | (node.spans || []).map(span => new Span(
156 | /* lo = */ span.start,
157 | /* hi = */ span.end,
158 | /* spanType = */ span.spanType || 'self'
159 | ))
160 | ).sort((first, second) => first.lo - second.lo);
161 |
162 | // TODO (codeviking): Again, the UI should handle the "empty state" appropriately as to prevent
163 | // logic like this from being necessary.
164 | if (spanAnnotations.length > 0) {
165 | if (!node.alternateParseInfo) {
166 | node.alternateParseInfo = {};
167 | }
168 | node.alternateParseInfo.spanAnnotations = spanAnnotations;
169 | }
170 | }
171 |
172 | return node;
173 | }
174 |
175 | /**
176 | * Returns a single span where the the start / end values encompass the indices of the provided
177 | * node's spans and all of it's children's spans.
178 | *
179 | * For instance, if provided a node with the span [0, 1] and that node had two children,
180 | * [1, 3] and [4, 20], this function would return a single span, [0, 20].
181 | *
182 | * If the node or it's children don't have any spans, `undefined` is returned.
183 | *
184 | * @param {Node}
185 | * @return {Span|undefined} The encompassing span (the boundaries), or undefined.
186 | */
187 | function getSpanBoundaries(node) {
188 | const allSpans = getAllChildSpans(node).concat(node.spans || []);
189 | if (allSpans.length > 0) {
190 | const firstSpan = allSpans[0];
191 | return allSpans.reduce((boundaries, span) => {
192 | if (boundaries.start > span.start) {
193 | boundaries.start = span.start;
194 | }
195 | if (boundaries.end < span.end) {
196 | boundaries.end = span.end;
197 | }
198 | return boundaries;
199 | }, { start: firstSpan.start, end: firstSpan.end });
200 | } else {
201 | return undefined;
202 | }
203 | }
204 |
205 | /**
206 | * Returns all children of the provided node, including those that are descendents of the node's
207 | * children.
208 | *
209 | * @param {Node} node
210 | * @return {Span[]}
211 | */
212 | function getAllChildSpans(node) {
213 | return (
214 | Array.isArray(node.children)
215 | ? node.children
216 | .map(n => (n.spans || []).concat(getAllChildSpans(n)))
217 | .reduce((all, arr) => all.concat(arr))
218 | : []
219 | );
220 | }
221 |
222 | class Span {
223 | constructor(lo, hi, spanType) {
224 | this.lo = lo;
225 | this.hi = hi;
226 | this.spanType = spanType;
227 | }
228 | }
229 |
230 | class CharNodeRoot {
231 | constructor(charLo, charHi) {
232 | this.charLo = charLo;
233 | this.charHi = charHi;
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/src/less/explainer/passage.less:
--------------------------------------------------------------------------------
1 | #passage {
2 | .u-w100;
3 | .flex-container-centered;
4 | .u-mp0;
5 | z-index: 99;
6 | min-height: 126/@em;
7 | max-height: 200/@em;
8 | height: 15%;
9 | background: @passage-bg-color;
10 | border: 2/@em solid @passage-border-color;
11 | border-left-width: 0;
12 | border-right-width: 0;
13 | box-sizing: border-box;
14 | position: relative;
15 | overflow: auto;
16 | transition: background-color @node-transition-duration @node-transition-ease,
17 | border-color @node-transition-duration @node-transition-ease,
18 | font-size .05s @node-transition-ease;
19 |
20 | textarea,
21 | p {
22 | .u-mp0;
23 | display: block;
24 | box-sizing: border-box;
25 | text-align: center;
26 | color: @passage-text-color;
27 | max-width: 100%;
28 | padding: 20/@em 70/@em;
29 | background: transparent;
30 | margin: auto;
31 | }
32 |
33 | textarea,
34 | p .passage__readonly {
35 | line-height: 22/@em;
36 | font-size: 30/@em;
37 | transition: color @node-transition-duration @node-transition-ease;
38 | }
39 |
40 | p {
41 | .u-w100;
42 | .u-select-none;
43 | position: relative;
44 | cursor: default;
45 |
46 | span {
47 | display: inline-block;
48 | transition: color .05s @node-transition-ease,
49 | background-color .05s @node-transition-ease,
50 | opacity .05s @node-transition-ease;
51 | }
52 |
53 | .passage__readonly {
54 | &,
55 | .span-slice__self,
56 | .span-slice__ignored {
57 | color: @passage-text-color;
58 | }
59 |
60 | .span-slice__self {
61 | padding: 0 2.22/@em;
62 | }
63 |
64 | .span-slice__ignored {
65 | margin-left: -2/@em;
66 | margin-right: 2/@em;
67 | z-index: 9;
68 | position: relative;
69 | cursor: default;
70 | }
71 |
72 | .span-slice {
73 | display: inline;
74 | padding: 3px 0; // Inline diplsay requires hard pixel value.
75 | cursor: pointer;
76 |
77 | &.span-slice--hover {
78 | background: fade(lighten(@color0-focused, 15%), 20%);
79 |
80 | span {
81 | color: lighten(@passage-text-color, 20%);
82 | }
83 |
84 | & > span.span-slice__self {
85 | background: fade(@color0-focused, 50%);
86 | color: lighten(@passage-text-color, 60%);
87 | }
88 | }
89 |
90 | &.span-slice--pressed.span-slice--hover {
91 | background: darken(desaturate(@color0-focused, 82%), 31.5%);
92 |
93 | span {
94 | color: darken(@passage-text-color, 18%);
95 | }
96 |
97 | & > span.span-slice__self {
98 | background: darken(desaturate(@color0-focused, 60%), 25%);
99 | }
100 | }
101 |
102 | &.span-slice--focused {
103 | & > span.span-slice__self {
104 | background: @color0-focused;
105 | color: @white;
106 | box-shadow: 0 0 10/@em fade(@color0-focused, 40%);
107 | }
108 | }
109 |
110 | // Span slice color overrides
111 | &.span-slice--color1 {
112 | &.span-slice--hover {
113 | background: fade(lighten(@color1-focused, 15%), 20%);
114 | & > span.span-slice__self {
115 | background: fade(@color1-focused, 50%);
116 | }
117 | }
118 |
119 | &.span-slice--pressed.span-slice--hover {
120 | background: darken(desaturate(@color1-focused, 52%), 15%);
121 | & > span.span-slice__self {
122 | background: darken(desaturate(@color1-focused, 40%), 12%);
123 | }
124 | }
125 |
126 | &.span-slice--focused > span.span-slice__self {
127 | background: @color1-focused;
128 | box-shadow: 0 0 10/@em fade(@color1-focused, 40%);
129 | }
130 | }
131 |
132 | &.span-slice--color2 {
133 | &.span-slice--hover {
134 | background: fade(lighten(@color2-focused, 15%), 20%);
135 | & > span.span-slice__self {
136 | background: fade(@color2-focused, 50%);
137 | }
138 | }
139 |
140 | &.span-slice--pressed.span-slice--hover {
141 | background: darken(desaturate(@color2-focused, 82%), 31.5%);
142 | & > span.span-slice__self {
143 | background: darken(desaturate(@color2-focused, 60%), 25%);
144 | }
145 | }
146 |
147 | &.span-slice--focused > span.span-slice__self {
148 | background: @color2-focused;
149 | box-shadow: 0 0 10/@em fade(@color2-focused, 40%);
150 | }
151 | }
152 |
153 | &.span-slice--color3 {
154 | &.span-slice--hover {
155 | background: fade(lighten(@color3-focused, 15%), 20%);
156 | & > span.span-slice__self {
157 | background: fade(@color3-focused, 50%);
158 | }
159 | }
160 |
161 | &.span-slice--pressed.span-slice--hover {
162 | background: darken(desaturate(@color3-focused, 62%), 20%);
163 | & > span.span-slice__self {
164 | background: darken(desaturate(@color3-focused, 40%), 12%);
165 | }
166 | }
167 |
168 | &.span-slice--focused > span.span-slice__self {
169 | background: @color3-focused;
170 | box-shadow: 0 0 10/@em fade(@color3-focused, 40%);
171 | }
172 | }
173 |
174 | &.span-slice--color4 {
175 | &.span-slice--hover {
176 | background: fade(lighten(@color4-focused, 15%), 20%);
177 | & > span.span-slice__self {
178 | background: fade(@color4-focused, 50%);
179 | }
180 | }
181 |
182 | &.span-slice--pressed.span-slice--hover {
183 | background: darken(desaturate(@color4-focused, 62%), 20%);
184 | & > span.span-slice__self {
185 | background: darken(desaturate(@color4-focused, 40%), 12%);
186 | }
187 | }
188 |
189 | &.span-slice--focused > span.span-slice__self {
190 | background: @color4-focused;
191 | box-shadow: 0 0 10/@em fade(@color4-focused, 40%);
192 | }
193 | }
194 |
195 | &.span-slice--color5 {
196 | &.span-slice--hover {
197 | background: fade(lighten(@color5-focused, 15%), 20%);
198 | & > span.span-slice__self {
199 | background: fade(@color5-focused, 50%);
200 | }
201 | }
202 |
203 | &.span-slice--pressed.span-slice--hover {
204 | background: darken(desaturate(@color5-focused, 62%), 20%);
205 | & > span.span-slice__self {
206 | background: darken(desaturate(@color5-focused, 40%), 12%);
207 | }
208 | }
209 |
210 | &.span-slice--focused > span.span-slice__self {
211 | background: @color5-focused;
212 | box-shadow: 0 0 10/@em fade(@color5-focused, 40%);
213 | }
214 | }
215 |
216 | &.span-slice--color6 {
217 | &.span-slice--hover {
218 | background: fade(lighten(@color6-focused, 15%), 20%);
219 | & > span.span-slice__self {
220 | background: fade(@color6-focused, 50%);
221 | }
222 | }
223 |
224 | &.span-slice--pressed.span-slice--hover {
225 | background: darken(desaturate(@color6-focused, 62%), 20%);
226 | & > span.span-slice__self {
227 | background: darken(desaturate(@color6-focused, 40%), 12%);
228 | }
229 | }
230 |
231 | &.span-slice--focused > span.span-slice__self {
232 | background: @color6-focused;
233 | box-shadow: 0 0 10/@em fade(@color6-focused, 40%);
234 | }
235 | }
236 | }
237 | }
238 | }
239 |
240 | textarea {
241 | .u-100;
242 | .u-appearance-none;
243 | padding-left: 40/@em;
244 | padding-right: 40/@em;
245 | opacity: 0;
246 | z-index: -9;
247 | position: absolute;
248 | border: none;
249 | resize: none;
250 | cursor: text;
251 |
252 | &:focus {
253 | color: @white;
254 | outline: none;
255 | }
256 | }
257 |
258 | &.passage--active {
259 | background: darken(@passage-bg-color, .75%);
260 |
261 | p .passage__readonly {
262 | color: @passage-active-text-color;
263 | }
264 | }
265 |
266 | .passage__focus-trigger {
267 | .u-child100;
268 | z-index: 0;
269 | }
270 |
271 | .passage__edit {
272 | position: absolute;
273 | cursor: pointer;
274 | width: 16.8/@em; //56% of 30
275 | height: 16.8/@em; //56% of 30
276 | margin-left: 0/@em; //56% of 8
277 | margin-top: -0.56/@em;
278 | transition: z-index @node-transition-duration @node-transition-ease;
279 |
280 | &,
281 | & div {
282 | display: inline-block;
283 | }
284 |
285 | .passage__edit__trigger {
286 | transition: opacity @node-transition-duration @node-transition-ease;
287 | opacity: 0;
288 | width: 7.84/@em; //56% of 14
289 | height: 7.84/@em; //56% of 14
290 | fill: @white;
291 | }
292 |
293 | &:hover .passage__edit__trigger {
294 | opacity: 1;
295 | }
296 | }
297 |
298 | &:hover {
299 | .passage__edit {
300 | .passage__edit__trigger {
301 | opacity: .2;
302 | }
303 |
304 | &:hover .passage__edit__trigger {
305 | opacity: 1;
306 | }
307 | }
308 | }
309 |
310 | &.passage--editing {
311 | background: @passage-edit-bg;
312 | border-width: 2/@em;
313 | border-color: @focused-ui;
314 | cursor: text;
315 | z-index: 1;
316 |
317 | textarea {
318 | position: static;
319 | z-index: auto;
320 | opacity: 1;
321 | }
322 |
323 | p {
324 | opacity: 0;
325 | z-index: -9;
326 | position: absolute;
327 | }
328 |
329 | .passage__edit,
330 | .passage__focus-trigger {
331 | opacity: 0;
332 | z-index: -9;
333 | }
334 | }
335 |
336 | .passage__loading-mask {
337 | .u-child100;
338 | z-index: -9;
339 | opacity: 0;
340 | background: @passage-bg-color;
341 | transition: opacity .2s ease-in-out, z-index .2s;
342 | }
343 |
344 | &.passage--loading {
345 | .passage__loading-mask {
346 | z-index: 9999;
347 | opacity: .6;
348 | }
349 |
350 | .passage__edit {
351 | display: none;
352 | }
353 | }
354 | }
355 |
356 | @media screen and (max-width: 1366px) {
357 | #passage {
358 | font-size: 12/@em;
359 |
360 | p {
361 | .passage__readonly {
362 | .span-slice {
363 | padding: 2px 0;
364 | }
365 | }
366 | }
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hierplane
2 |
3 | A javascript library for visualizing hierarchical data, specifically tailored towards rendering
4 | dependency parses.
5 |
6 | ## Table of Contents
7 |
8 | * [Usage](#usage)
9 | * [Tree structure](#tree-structure)
10 | * [Style Maps](#maps)
11 | * [Contributing](#contributing)
12 | * [Publishing](#publishing)
13 |
14 | ## Usage
15 |
16 | There are two ways to use `hierplane`:
17 |
18 | * [In a web page, without dependencies](#web)
19 | * [In a web application that uses ReactJS](#web-react)
20 |
21 | ### In a web page:
22 |
23 | Add the following `
27 | ```
28 |
29 | Add the following styles to your web page, likely in the ` ` tag:
30 |
31 | ```
32 |
33 | ```
34 |
35 | Then invoke `hierplane.renderTree(tree[, options])` as is desired.
36 |
37 | - `tree` *object* the tree to visualize, see a detailed description of
38 | the tree structure.
39 | - `options` *object* optional overrides
40 | - `options.theme` *string* the theme to use, one of 'dark' or 'light',
41 | defaults to 'dark'.
42 | - `options.target` *string* a css selector targeting the element where
43 | the resulting DOM should be rendered, defaults to 'body'.
44 |
45 | The `renderTree()` method returns a function that will unmount the rendered
46 | content, if you want to remove the visualization:
47 |
48 | ```javascript
49 | const unmount = hierplane.renderTree(tree, { target: '#hierplane', theme: 'light' });
50 | // Don't do this
51 | const target = document.getElementById('hierplane');
52 | target.removeChild(target.firstElementChild);
53 | // Do this
54 | unmount();
55 | ```
56 |
57 | You can see a full example [here](./EXAMPLES.md).
58 |
59 | ### In a web application that uses ReactJS:
60 |
61 | Install the hierplane dependency:
62 |
63 | ```
64 | npm install --save hierplane
65 | ```
66 |
67 | Then, simply import the `Tree` component, and pass it the tree you'd like to render:
68 |
69 | ```
70 | import { Tree } from 'hierplane';
71 | import React from 'react';
72 |
73 | const aTree = { ... };
74 |
75 | class TreeContainer extends React.PureComponent {
76 | render() {
77 | return ;
78 | }
79 | }
80 | ```
81 |
82 | ## Tree Structure
83 |
84 | A `tree` is an `object` with the following structure:
85 |
86 | ```
87 | /**
88 | * @type object
89 | */
90 | Tree
91 | /**
92 | * The text being visualized.
93 | * @type string
94 | * @required
95 | */
96 | text: 'Sam likes eating hot dogs.'
97 | /**
98 | * Map used to apply node styles (see `Style Maps` section).
99 | * @type object
100 | * @optional
101 | */
102 | nodeTypeToStyle: { ... }
103 | /**
104 | * Map used to set node positioning (see `Style Maps` section).
105 | * @type object
106 | * @optional
107 | */
108 | linkToPosition: { ... }
109 | /**
110 | * Map used to override link labels (see `Style Maps` section).
111 | * @type object
112 | * @optional
113 | */
114 | linkNameToLabel: { ... }
115 | /**
116 | * The root node of the tree.
117 | * @type object
118 | * @required
119 | */
120 | root: Node { ... }
121 | ```
122 |
123 | The `root` property refers to the root node of the tree to be visualized. Each `node` has the following
124 | structure:
125 |
126 | ```
127 | /**
128 | * @type object
129 | */
130 | Node
131 | /**
132 | * The text content of the node
133 | * @type string
134 | * @required
135 | *
136 | * TODO: This will likely be migrated to be named `text` in a future version, as it's less specific.
137 | */
138 | word: 'eating'
139 | /**
140 | * A string specifying the "type" of node. This is used to determine it's color -- all nodes of
141 | * the same type will be assigned the same color.
142 | * @type string
143 | * @required
144 | */
145 | nodeType: 'verb'
146 | /**
147 | * A string specifying describing the relationship between the node and it's parent. This text
148 | * will be displayed on an element connecting the node and it's parent.
149 | * @type string
150 | * @optional
151 | */
152 | link: 'direct object'
153 | /**
154 | * An array of strings, which will be displayed on the node.
155 | * @type string[]
156 | * @optional
157 | */
158 | attributes: [ 'action', ... ]
159 | /**
160 | * An array of spans, where each span represents a series of characters in the `text` property (
161 | * of the Tree) that should be highlighted when the node is hovered.
162 | * @type object[]
163 | * @optional
164 | */
165 | spans: [ Span { ... }, ... ]
166 | /**
167 | * An array containing the children of the node.
168 | * @type object[]
169 | * @optional
170 | */
171 | children: [ Node, ... ]
172 | ```
173 |
174 | Each `span` refers to a sequence of characters in the original sentence (the `text` property of the
175 | `Tree`) that should be highlighted when the node and is hovered. Each `span` should have the
176 | following properties:
177 |
178 | ```
179 | Span
180 | /**
181 | * The index indicating where the span begins.
182 | * @type number
183 | * @required
184 | */
185 | start
186 | /**
187 | * The index (exclusive) where the span ends.
188 | * @type number
189 | * @required
190 | */
191 | end
192 | /**
193 | * An optional identifier indicating the type of span. As of now, the only value you'll likely
194 | * put here is "ignored", which indicates that the span shouldn't be emphasized when the node
195 | * is hovered.
196 | * @type string
197 | * @optional
198 | */
199 | spanType
200 | ```
201 |
202 | You can see a full example of a tree [here](dev/data/the-sum-of-three-consecutive-integers.json).
203 |
204 | ## Style Maps
205 |
206 | The Hierplane data format supports three optional style maps (objects containing a set of key-value pairs) that can be added to a `Tree` object:
207 |
208 | * [`nodeTypeToStyle`](#nodetypetostyle) applies specified styles to nodes with particular `nodeType` values.
209 | * [`linkToPosition`](#linktoposition) tells the app how to position nodes with particular `link` values.
210 | * [`linkNameToLabel`](#linknametolabel) translates particular `link` values into custom display labels.
211 |
212 | ### nodeTypeToStyle
213 |
214 | A `nodeTypeToStyle` mapping applies specified styles to nodes with particular `nodeType` values. In the following example, any node with a `nodeType` value of `"verb"` will have `"color1"` and `"strong"` styles applied. This gets rendered as CSS modifier classes.
215 |
216 | ```
217 | "nodeTypeToStyle": {
218 | "verb": ["color1", "strong"],
219 | "noun": ["color2"],
220 | "modifier": ["color3"],
221 | "sequence": ["seq"],
222 | "reference": ["placeholder"]
223 | }
224 | ```
225 |
226 | Note: Hierplane will automatically color-code nodes based on their `nodeType` values, so out-of-the-box, you do not need to worry about `nodeTypeToStyle` mapping. However, as soon as you add this map and introduce a custom style on any `nodeType`, you will need to manually apply all node styles, as the automatic styling will be disabled at that point.
227 |
228 | **Supported `nodeTypeToStyle` Keys:**
229 |
230 | Any potential `nodeType` value is a valid key, whether it's being used in the current tree or not.
231 |
232 | **Supported `nodeTypeToStyle` Values:**
233 |
234 | Valid values are arrays of strings. While you are free to apply any string as a style, only the following strings are supported by the built-in stylesheet:
235 |
236 | * `"color0"` colors node gray.
237 | * `"color1"` colors node green.
238 | * `"color2"` colors node blue.
239 | * `"color3"` colors node pink.
240 | * `"color4"` colors node yellow.
241 | * `"color5"` colors node purple.
242 | * `"color6"` colors node aqua.
243 | * `"strong"` makes node text larger and bold.
244 | * `"seq"` renders node as a sequence container. Note that this style is required to correctly render nodes that have at least one child node with a `nodeType` value of `"inside"`. Also note that a node with a `"seq"` style will have its default node `text` hidden to make room for its `"inside"` children.
245 | * `"placeholder"` renders node with a transparent background and light dotted outline (to communicate a placeholder status, recommended for certain linguistic concepts such as relative references).
246 |
247 | Note: at this time, the only supported colors are the 7 mentioned above.
248 |
249 | ### linkToPosition
250 |
251 | A `linkToPosition` mapping tells the app how to position nodes with particular `link` values. In the following example, any node with a link value of `"subj"` will be given a position of `"left"`, while nodes with link values of `"obj"` will be given a position of `"right"` and so on.
252 |
253 | ```
254 | "linkToPosition": {
255 | "subj": "left",
256 | "obj": "right",
257 | "seqChild": "inside"
258 | }
259 | ```
260 |
261 | **Supported `linkToPosition` Keys:**
262 |
263 | Any potential `link` value is a valid key, whether it's being used in the current tree or not.
264 |
265 | **Supported `linkToPosition` Values:**
266 |
267 | * `inside` - Positions node inside of its parent. This was added mainly to support linguistic sequences (e.g. "The land has trees, grass, and animals." where the object of the sentence is a sequence of nouns).
268 |
269 | * `left` - Positions a node to the left of its parent (well suited for subjects of a sentence).
270 |
271 | * `right` - Positions a node to the right of its parent (well suited for objects of a sentence).
272 |
273 | * `down` - Positions a node directly underneath its parent (we call this layout "canonical"). All nodes have a position of `down` by default, so it is not necessary to explicitly set this.
274 |
275 | ### linkNameToLabel
276 |
277 | A `linkNameToLabel` mapping translates particular `link` values into custom display labels. In the following example, any node with a `link` value of `"subj"` will be displayed as `"S"`. This is especially useful for nodes positioned `"left"` and `"right"`, as those configurations lose aesthetic value with long link labels.
278 |
279 | ```
280 | "linkNameToLabel": {
281 | "subj": "S",
282 | "obj": "O"
283 | }
284 | ```
285 |
286 | **Supported `linkNameToLabel` Keys:**
287 |
288 | Any potential `link` value is a valid key, whether it's being used in the current tree or not.
289 |
290 | **Supported `linkNameToLabel` Values:**
291 |
292 | Any string is a valid value.
293 |
294 | ## Contributing
295 |
296 | To run the code locally and verify your changes, follow these steps:
297 |
298 | 1. Clone the repository.
299 |
300 | ```
301 | $ git clone git@github.com:allenai/hierplane.git
302 | ```
303 |
304 | 2. Install [nodejs](https://nodejs.org/en/). This was built against version `v6.11.5`. You're free
305 | to try something more recent.
306 |
307 | 3. Install the dependencies:
308 |
309 | ```
310 | $ cd hierplane/
311 | $ npm install
312 | ```
313 |
314 | 4. Run the `watch` target:
315 |
316 | ```
317 | $ npm start
318 | ```
319 |
320 | 5. Open [http://localhost:3000](http://localhost:3000) in your browser of choice.
321 |
322 | If you want to change the port on which the webserver is bound, set the `HIERPLANE_DEV_SERVER_PORT`
323 | environment variable to one of your choosing.
324 |
325 | ## Publishing
326 |
327 | In order to publish, you will need to be a collaborator on the [Hierplane NPM project](https://www.npmjs.com/package/hierplane).
328 |
329 | 1. Make sure to increment the Hierplane version in `package.json`.
330 | 2. If you're not already logged in, from your `hierplane` project folder, enter `npm login` and log in with your NPM credentials.
331 | 3. Execute `node bin/publish.js`.
332 |
--------------------------------------------------------------------------------