├── .babelrc ├── .env ├── src ├── aspects │ ├── function │ │ ├── application-styles.scss │ │ ├── style.scss │ │ ├── application-view.js │ │ ├── function-test.js │ │ ├── view.js │ │ └── data.js │ ├── module │ │ ├── index.js │ │ ├── module-test.js │ │ ├── style.scss │ │ ├── data.js │ │ └── view.js │ ├── record │ │ ├── index.js │ │ ├── style.scss │ │ ├── view.js │ │ ├── record-test.js │ │ └── data.js │ ├── variant │ │ ├── style.scss │ │ ├── view.js │ │ └── data.js │ ├── row │ │ ├── style.scss │ │ ├── view.js │ │ └── data.js │ ├── label │ │ ├── view.js │ │ └── data.js │ ├── equality.js │ ├── external-test.js │ ├── external.js │ └── transit.js ├── containers │ ├── LoginSuccess.scss │ ├── logo.jpg │ ├── logo.png │ ├── index.js │ ├── Login.scss │ ├── NotFound.js │ ├── RequireLogin.js │ ├── LoginSuccess.js │ ├── App.scss │ ├── Login.js │ ├── App.js │ └── Module.js ├── theory │ ├── .flowconfig │ ├── context.js │ ├── registry.js │ ├── __test__ │ │ ├── typelevels-test.js │ │ ├── unify-test.js │ │ ├── eval-test.js │ │ ├── ref-test.js │ │ └── subst-test.js │ ├── definitionalEquality.js │ ├── eval.js │ ├── type.js │ ├── evaluation.js │ ├── hole.js │ ├── conflict.js │ ├── relation.js │ ├── unify.js │ ├── ref.js │ ├── transit.js │ ├── edit.js │ ├── tm.js │ └── scope.js ├── style │ ├── color.scss │ └── header.scss ├── ducks │ ├── reducer.js │ ├── relation.js │ ├── auth.js │ └── module.js ├── components │ ├── Autocomplete.scss │ ├── Expression │ │ ├── Name.scss │ │ ├── Hole.scss │ │ ├── Name.js │ │ └── Hole.js │ ├── Expression.scss │ ├── Avatar.js │ ├── Autocomplete.js │ ├── __tests__ │ │ └── InfoBar-test.js │ └── Expression.js ├── testutil │ ├── expectImmutableIs.js │ └── examples.js ├── childjoin.js ├── validation │ ├── surveyValidation.js │ ├── widgetValidation.js │ └── validation.js ├── commands │ ├── addEntry.js │ └── pokeHole.js ├── util │ ├── isPrefix.js │ └── halfzip.js ├── config.js ├── routes.js ├── redux │ ├── clientMiddleware.js │ ├── transit.js │ └── create.js ├── transit-immutable-js.js ├── helpers │ ├── ApiClient.js │ ├── universalRouter.js │ └── Html.js ├── client.js └── server.js ├── .eslintignore ├── static ├── logo.jpg ├── favicon.ico └── favicon.png ├── compiler.js ├── tests.webpack.js ├── api ├── actions │ ├── loadDevelopment.js │ ├── loadAuth.js │ ├── login.js │ ├── loadInfo.js │ ├── logout.js │ ├── index.js │ ├── updateWidget.js │ └── loadWidgets.js ├── config.js └── api.js ├── .travis.yml ├── .gitignore ├── .editorconfig ├── .flowconfig ├── bin ├── api.js └── server.js ├── server.babel.js ├── webpack ├── webpack-dev-server.js ├── webpack-isomorphic-tools.js ├── prod.config.js └── dev.config.js ├── LICENSE ├── .eslintrc ├── karma.conf.js ├── docs └── InlineStyles.md ├── README.md └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_PATH=./src 2 | NODE_ENV=production 3 | PORT=80 4 | -------------------------------------------------------------------------------- /src/aspects/function/application-styles.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | } 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack/* 2 | karma.conf.js 3 | tests.webpack.js 4 | -------------------------------------------------------------------------------- /src/containers/LoginSuccess.scss: -------------------------------------------------------------------------------- 1 | .loginSuccess { 2 | margin-left: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /static/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelburget/pigment/HEAD/static/logo.jpg -------------------------------------------------------------------------------- /src/theory/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelburget/pigment/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelburget/pigment/HEAD/static/favicon.png -------------------------------------------------------------------------------- /compiler.js: -------------------------------------------------------------------------------- 1 | // enables ES6 support 2 | require('babel/register')({ 3 | stage: 0, 4 | }); 5 | -------------------------------------------------------------------------------- /src/containers/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelburget/pigment/HEAD/src/containers/logo.jpg -------------------------------------------------------------------------------- /src/containers/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelburget/pigment/HEAD/src/containers/logo.png -------------------------------------------------------------------------------- /src/theory/context.js: -------------------------------------------------------------------------------- 1 | import type { Tm } from './tm'; 2 | 3 | 4 | export type Context = Map; 5 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | var context = require.context('./src', true, /-test\.js$/); 2 | context.keys().forEach(context); 3 | -------------------------------------------------------------------------------- /src/aspects/module/index.js: -------------------------------------------------------------------------------- 1 | // export { * as data } from './controller'; 2 | export { default as view } from './view'; 3 | -------------------------------------------------------------------------------- /src/aspects/record/index.js: -------------------------------------------------------------------------------- 1 | // export { default as view } from './view'; 2 | export { default as data } from './data'; 3 | -------------------------------------------------------------------------------- /api/actions/loadDevelopment.js: -------------------------------------------------------------------------------- 1 | export default function(req) { 2 | return Promise.resolve(req.session.user || null); 3 | } 4 | -------------------------------------------------------------------------------- /api/actions/loadAuth.js: -------------------------------------------------------------------------------- 1 | export default function loadAuth(req) { 2 | return Promise.resolve(req.session.user || null); 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | before_script: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | *.iml 5 | webpack-stats.json 6 | npm-debug.log 7 | .DS_Store 8 | *.agda 9 | *.agdai 10 | *.hs 11 | -------------------------------------------------------------------------------- /api/actions/login.js: -------------------------------------------------------------------------------- 1 | export default function login(req) { 2 | const user = { 3 | name: req.body.name 4 | }; 5 | req.session.user = user; 6 | return Promise.resolve(user); 7 | } 8 | -------------------------------------------------------------------------------- /src/aspects/record/style.scss: -------------------------------------------------------------------------------- 1 | .recList { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .recRow { 7 | display: flex; 8 | flex-display: row; 9 | } 10 | -------------------------------------------------------------------------------- /src/aspects/variant/style.scss: -------------------------------------------------------------------------------- 1 | .variant { 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .variantRow { 7 | display: flex; 8 | flex-display: column; 9 | } 10 | -------------------------------------------------------------------------------- /src/style/color.scss: -------------------------------------------------------------------------------- 1 | $hole: #FCF474; 2 | $conflict: #FC8374; 3 | $history: #7FD0D4; 4 | 5 | $margin-links: #424242; 6 | $margin-background: #fcfcfb; 7 | 8 | $expression-highlight: rgba(112, 182, 255, 0.38); 9 | -------------------------------------------------------------------------------- /src/style/header.scss: -------------------------------------------------------------------------------- 1 | .material-header { 2 | font-weight: 400; 3 | line-height: 24px; 4 | font-size: 16px; 5 | letter-spacing: .04em; 6 | font-family: "Roboto","Helvetica","Arial",sans-serif; 7 | } 8 | -------------------------------------------------------------------------------- /src/ducks/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import auth from './auth'; 4 | import module from './module'; 5 | 6 | export default combineReducers({ 7 | auth, 8 | module, 9 | }); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /api/actions/loadInfo.js: -------------------------------------------------------------------------------- 1 | export default function loadInfo() { 2 | return new Promise((resolve) => { 3 | resolve({ 4 | message: 'This came from the api server', 5 | time: Date.now() 6 | }); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /api/actions/logout.js: -------------------------------------------------------------------------------- 1 | export default function logout(req) { 2 | return new Promise((resolve) => { 3 | req.session.destroy(() => { 4 | req.session = null; 5 | return resolve(null); 6 | }); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | suppress_comment= \\(.\\|\n\\)*\\$flowstatic 9 | suppress_comment= \\(.\\|\n\\)*\\$flow-type-in-type 10 | suppress_comment= \\(.\\|\n\\)*\\$flow-exception 11 | -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | export App from './App'; 2 | export RequireLogin from './RequireLogin'; 3 | export Login from './Login'; 4 | export LoginSuccess from './LoginSuccess'; 5 | export Module from './Module'; 6 | export NotFound from './NotFound'; 7 | -------------------------------------------------------------------------------- /src/aspects/row/style.scss: -------------------------------------------------------------------------------- 1 | .rowList { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .rowRow { // ... 🎶 row your boat 🎶 7 | display: flex; 8 | flex-direction: row; // 🎶 gently down the screeeeeen 🎶 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Autocomplete.scss: -------------------------------------------------------------------------------- 1 | $bg: #2A2F3A; 2 | $hlbg: $bg - #222; 3 | 4 | .menu { 5 | color: white; 6 | } 7 | 8 | .item { 9 | background-color: #2A2F3A; 10 | } 11 | 12 | .highlightedItem { 13 | background-color: $hlbg; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Expression/Name.scss: -------------------------------------------------------------------------------- 1 | .name { 2 | margin: 0 5px; 3 | // cursor: pointer; 4 | 5 | .nameAbs { 6 | position: absolute; 7 | } 8 | } 9 | 10 | .nameContextMenu { 11 | background: white; 12 | list-style-type: none; 13 | } 14 | -------------------------------------------------------------------------------- /api/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | development: { 3 | isProduction: false, 4 | apiPort: process.env.APIPORT, 5 | }, 6 | production: { 7 | isProduction: true, 8 | apiPort: process.env.APIPORT, 9 | } 10 | }[process.env.NODE_ENV || 'development']; 11 | -------------------------------------------------------------------------------- /src/testutil/expectImmutableIs.js: -------------------------------------------------------------------------------- 1 | /* eslint id-length: 0 */ 2 | import expect from 'expect'; 3 | import { is } from 'immutable'; 4 | 5 | export default function expectImmutableIs(a, b) { 6 | const msg = JSON.stringify(a) + " isn't " + JSON.stringify(b); 7 | return expect(is(a, b)).toBe(true, msg); 8 | } 9 | -------------------------------------------------------------------------------- /src/containers/Login.scss: -------------------------------------------------------------------------------- 1 | .loginPage { 2 | margin-left: 20px; 3 | 4 | input { 5 | padding: 5px 10px; 6 | border-radius: 5px; 7 | border: 1px solid #ccc; 8 | } 9 | 10 | form { 11 | margin: 30px 0; 12 | :global(.btn) { 13 | margin-left: 10px; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/aspects/function/style.scss: -------------------------------------------------------------------------------- 1 | .lambda { 2 | display: inline-flex; 3 | flex-direction: row; 4 | } 5 | 6 | .expandContainer { 7 | position: relative; 8 | } 9 | 10 | .expand { 11 | position: absolute; 12 | background-color: white; 13 | width: 100px; 14 | display: flex; 15 | } 16 | -------------------------------------------------------------------------------- /src/childjoin.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function childJoin(children, joiner) { 4 | const result = []; 5 | React.Children.map(children, child => { 6 | result.push(child); 7 | result.push(React.cloneElement(joiner)); 8 | }); 9 | result.pop(); 10 | return result; 11 | } 12 | -------------------------------------------------------------------------------- /bin/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | if (process.env.NODE_ENV !== 'production') { 3 | if (!require('piping')({ 4 | hook: true, 5 | ignore: /(\/\.|~$|\.json$)/i 6 | })) { 7 | return; 8 | } 9 | } 10 | require('../server.babel'); // babel registration (runtime transpilation for node) 11 | require('../api/api'); 12 | -------------------------------------------------------------------------------- /src/containers/NotFound.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | export default class NotFound extends Component { 4 | render() { 5 | return ( 6 |
7 |

Doh! 404!

8 |

These are not the droids you are looking for!

9 |
10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/theory/registry.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import Immutable from 'immutable'; 4 | 5 | import type { Tm } from './tm'; 6 | 7 | 8 | const registry = {}; 9 | 10 | 11 | export function read(): Map { 12 | return Immutable.fromJS(registry); 13 | } 14 | 15 | 16 | export function register(name: string, cls: any): void { 17 | registry[name] = cls; 18 | } 19 | -------------------------------------------------------------------------------- /server.babel.js: -------------------------------------------------------------------------------- 1 | // enable runtime transpilation to use ES6/7 in node 2 | 3 | var fs = require('fs'); 4 | 5 | var babelrc = fs.readFileSync('./.babelrc'); 6 | var config; 7 | 8 | try { 9 | config = JSON.parse(babelrc); 10 | } catch (err) { 11 | console.error('==> ERROR: Error parsing your .babelrc.'); 12 | console.error(err); 13 | } 14 | 15 | require('babel/register')(config); 16 | -------------------------------------------------------------------------------- /src/components/Expression.scss: -------------------------------------------------------------------------------- 1 | @import '../style/color'; 2 | 3 | .expression { 4 | padding: 0 5px; 5 | cursor: pointer; 6 | 7 | &:hover { 8 | background-color: $expression-highlight; 9 | } 10 | } 11 | 12 | .highlighted { 13 | box-sizing: border-box; 14 | border-bottom: 2px solid rgb(112, 182, 255); 15 | } 16 | 17 | .conflict { 18 | background-color: $conflict; 19 | } 20 | -------------------------------------------------------------------------------- /api/actions/index.js: -------------------------------------------------------------------------------- 1 | export loadInfo from './loadInfo'; 2 | export loadWidgets from './loadWidgets'; 3 | export loadAuth from './loadAuth'; 4 | export login from './login'; 5 | export logout from './logout'; 6 | export updateWidget from './updateWidget'; 7 | 8 | // export loadModule from './loadModule'; 9 | // export saveModule from './saveModule'; 10 | 11 | // export searchType from './searchType'; 12 | -------------------------------------------------------------------------------- /src/validation/surveyValidation.js: -------------------------------------------------------------------------------- 1 | import memoize from 'lru-memoize'; 2 | import {createValidator, required, maxLength, email} from './validation'; 3 | 4 | const surveyValidation = createValidator({ 5 | name: [required, maxLength(10)], 6 | email: [required, email], 7 | occupation: maxLength(20) // single rules don't have to be in an array 8 | }); 9 | export default memoize(10)(surveyValidation); 10 | -------------------------------------------------------------------------------- /src/aspects/label/view.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import LabelData from './data'; 4 | 5 | 6 | export default class Label extends Component { 7 | static propTypes = { 8 | children: PropTypes.instanceOf(LabelData).isRequired, 9 | }; 10 | 11 | render() { 12 | return ( 13 |
14 | {this.props.children.name} 15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/addEntry.js: -------------------------------------------------------------------------------- 1 | export const ADD_ENTRY = 'ADD_ENTRY'; 2 | 3 | export const addEntry = { 4 | id: ADD_ENTRY, 5 | title: 'add entry', 6 | }; 7 | 8 | export function makeLabel(values) { 9 | const labelPrefix = 'new entry'; 10 | let label = labelPrefix; 11 | let ix = 0; 12 | while (values.has(label)) { 13 | ix += 1; 14 | label = labelPrefix + ' ' + ix; 15 | } 16 | 17 | return label; 18 | } 19 | -------------------------------------------------------------------------------- /src/validation/widgetValidation.js: -------------------------------------------------------------------------------- 1 | import {createValidator, required, maxLength, integer, oneOf} from './validation'; 2 | 3 | export const colors = ['Blue', 'Fuchsia', 'Green', 'Orange', 'Red', 'Taupe']; 4 | 5 | const widgetValidation = createValidator({ 6 | color: [required, oneOf(colors)], 7 | sprocketCount: [required, integer], 8 | owner: [required, maxLength(30)] 9 | }); 10 | export default widgetValidation; 11 | -------------------------------------------------------------------------------- /src/util/isPrefix.js: -------------------------------------------------------------------------------- 1 | /* eslint no-else-return: 0 */ 2 | import { is } from 'immutable'; 3 | import type { List } from 'immutable'; 4 | 5 | export default function isPrefix(xs: List, ys: List) { 6 | if (xs.size === 0) { 7 | return true; 8 | } else if (ys.size === 0) { 9 | return false; 10 | } else { 11 | return is(xs.first(), ys.first()) && 12 | isPrefix(xs.shift(), ys.shift()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/containers/RequireLogin.js: -------------------------------------------------------------------------------- 1 | import {Component} from 'react'; 2 | 3 | export default class RequireLogin extends Component { 4 | static onEnter(store) { 5 | return (nextState, transition) => { 6 | const { auth: { user }} = store.getState(); 7 | if (!user) { 8 | // oops, not logged in, so can't be here! 9 | transition.to('/'); 10 | } 11 | }; 12 | } 13 | 14 | render() { 15 | return this.props.children; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | development: { 3 | isProduction: false, 4 | port: process.env.PORT, 5 | apiPort: process.env.APIPORT, 6 | app: { 7 | name: 'React Redux Example Development' 8 | } 9 | }, 10 | production: { 11 | isProduction: true, 12 | port: process.env.PORT, 13 | apiPort: process.env.APIPORT, 14 | app: { 15 | name: 'React Redux Example Production' 16 | } 17 | } 18 | }[process.env.NODE_ENV || 'development']; 19 | -------------------------------------------------------------------------------- /src/theory/__test__/typelevels-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | 3 | import { Type } from '../tm'; 4 | 5 | describe('typelevels', () => { 6 | const type = Type.singleton; 7 | 8 | it('knows the types of basics', () => { 9 | expect(type.type).toBe(type); 10 | }); 11 | 12 | // it('knows sigmas', () => { 13 | // // XXX what should the second part of a sigma be? is it a lambda? 14 | // expect(new Sigma(type, new Lam('x', type)).type) 15 | // .toBe(type); 16 | // }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/aspects/label/data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Record } from 'immutable'; 4 | 5 | import { register } from '../../theory/registry'; 6 | 7 | 8 | // formation 9 | 10 | export const LabelShape = Record({ 11 | name: null, // string 12 | }, 'label'); 13 | 14 | 15 | // TODO - decide to drop this or actually build it 16 | const signature = { 17 | formation: null, 18 | intros: null, 19 | elims: null, 20 | 21 | searchAliases: ['label', 'name'], 22 | }; 23 | 24 | export default signature; 25 | 26 | 27 | register('label', signature); 28 | -------------------------------------------------------------------------------- /src/theory/definitionalEquality.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import invariant from 'invariant'; 4 | 5 | import { CANONICAL } from './tm'; 6 | 7 | 8 | export default function definitionallyEqual(tm1, tm2) { 9 | // TODO - this assumes the invariant that they're both canonical forms. 10 | // Is that a valid assumption? Is it strong enough? 11 | invariant( 12 | tm1.form === CANONICAL && tm2.form === CANONICAL, 13 | 'both terms passed to `definitionallyEqual` must be canonical' 14 | ); 15 | 16 | return tm1.form === tm2.form && tm1.form.equals(tm1, tm2); 17 | } 18 | -------------------------------------------------------------------------------- /src/testutil/examples.js: -------------------------------------------------------------------------------- 1 | /* eslint id-length: 0 */ 2 | import { Type, Var } from '../theory/tm'; 3 | import { mkBound } from '../theory/ref'; 4 | import { mkFunction } from '../aspects/function/data'; 5 | 6 | const type = Type.singleton; 7 | 8 | export const id = mkFunction( 9 | 'x', 10 | type, 11 | new Var({ ref: mkBound('..', 'binder'), type }), 12 | type, 13 | ); 14 | 15 | 16 | export const k = mkFunction( 17 | 'x', 18 | type, 19 | mkFunction( 20 | 'y', 21 | type, 22 | new Var({ ref: mkBound('..', '..', 'binder'), type }), 23 | type 24 | ), 25 | type 26 | ); 27 | -------------------------------------------------------------------------------- /src/commands/pokeHole.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { List } from 'immutable'; 4 | 5 | import { openNewEdit } from '../theory/edit'; 6 | import { Hole } from '../theory/tm'; // XXX this loops 7 | 8 | import type Edit from '../theory/edit'; 9 | import type { Tm } from '../theory/tm'; 10 | 11 | export const POKE_HOLE = 'POKE_HOLE'; 12 | 13 | export const pokeHole = { 14 | id: POKE_HOLE, 15 | title: 'poke hole', 16 | }; 17 | 18 | export function doPokeHole(here: Tm): Edit { 19 | return openNewEdit( 20 | POKE_HOLE, 21 | here, 22 | new Hole({ type: here.type }), 23 | List() 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/theory/eval.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // 3 | // What type of evaluation is this? Not sure yet. Concerns: 4 | // 5 | // * Allowing / tracking effects 6 | // * What type of result do we return? 7 | // - normal form 8 | // - head normal 9 | // - weak head normal 10 | // 11 | // Evaluation order: I think this is simpler, and rather neat. Computation 12 | // really only happens at eliminators, so they define evaluation order through 13 | // their step functions. 14 | 15 | import type { Tm } from './tm'; 16 | import type { Context } from './context'; 17 | 18 | export default function eval(tm: Tm, ctx: Context) { 19 | } 20 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route} from 'react-router'; 3 | 4 | import { 5 | Module, App, RequireLogin, Login, LoginSuccess, NotFound 6 | } from './containers'; 7 | 8 | export default function(store) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/redux/clientMiddleware.js: -------------------------------------------------------------------------------- 1 | export default function clientMiddleware(client) { 2 | return ({dispatch, getState}) => { 3 | return next => action => { 4 | if (typeof action === 'function') { 5 | return action(dispatch, getState); 6 | } 7 | 8 | const { promise, types, ...rest } = action; 9 | if (!promise) { 10 | return next(action); 11 | } 12 | 13 | const [REQUEST, SUCCESS, FAILURE] = types; 14 | next({...rest, type: REQUEST}); 15 | return promise(client).then( 16 | (result) => next({...rest, result, type: SUCCESS}), 17 | (error) => next({...rest, error, type: FAILURE}) 18 | ); 19 | }; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /api/actions/updateWidget.js: -------------------------------------------------------------------------------- 1 | import {getWidgets} from './loadWidgets'; 2 | 3 | export default function updateWidget(req) { 4 | return new Promise((resolve, reject) => { 5 | // write to database 6 | setTimeout(() => { 7 | if (Math.floor(Math.random() * 5) === 0) { 8 | reject('Oh no! Widget save fails 20% of the time. Try again.'); 9 | } else { 10 | const widgets = getWidgets(req); 11 | const widget = req.body; 12 | if (widget.id) { 13 | widgets[widget.id - 1] = widget; // id is 1-based. please don't code like this in production! :-) 14 | } 15 | resolve(widget); 16 | } 17 | }, 2000); // simulate async db write 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/theory/__test__/unify-test.js: -------------------------------------------------------------------------------- 1 | import { Type, Hole, Var } from '../tm'; 2 | import { mkBound } from '../ref'; 3 | 4 | import expectImmutableIs from '../../testutil/expectImmutableIs'; 5 | 6 | 7 | describe('unification', () => { 8 | 9 | it('unifies type', () => { 10 | expectImmutableIs(Type.unify(Type), Type); 11 | }); 12 | 13 | it('holes unify with everything', () => { 14 | const hole = new Hole({ name: 'hole', Type }); 15 | expectImmutableIs(hole.unify(Type), Type); 16 | }); 17 | 18 | it('vars unify with everything', () => { 19 | const v = new Var(mkBound('..', 'binder'), Type); // eslint-disable-line id-length 20 | expectImmutableIs(v.unify(Type), Type); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/aspects/function/application-view.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import React, { Component, PropTypes } from 'react'; 3 | 4 | import { expr } from '../../components/Expression'; 5 | import AppData from './data'; 6 | import styles from './styles.scss'; 7 | 8 | export default class App extends Component { 9 | static propTypes = { 10 | children: PropTypes.instanceOf(AppData).isRequired, 11 | path: PropTypes.instanceOf(List).isRequired, 12 | }; 13 | 14 | render() { 15 | const { path, children: item } = this.props; 16 | 17 | return ( 18 |
19 | {expr(item, path, 'func')} 20 | {expr(item, path, 'arg')} 21 |
22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/theory/type.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // import { POKE_HOLE, pokeHole, doPokeHole } from '../commands/pokeHole'; 4 | 5 | 6 | // Do we need a formation rule for Type? 7 | const Type = { 8 | // TODO 9 | }; 10 | 11 | export default Type; 12 | 13 | // XXX - do we need to find a home for this action/edit pair? We want to be 14 | // able to poke holes in Type. Is it a special case? I guess you can just poke 15 | // a hole in anything by default? 16 | // actions(): List { 17 | // return List([pokeHole]); 18 | // } 19 | 20 | // performEdit(id: string): Edit { 21 | // invariant( 22 | // id === POKE_HOLE, 23 | // 'Type.edit only knows of POKE_HOLE' 24 | // ); 25 | 26 | // return doPokeHole(this); 27 | // } 28 | -------------------------------------------------------------------------------- /src/theory/evaluation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Record } from 'immutable'; 4 | 5 | 6 | export class EvaluationResult extends Record({ 7 | type: null, // string 8 | value: null, // X 9 | }, 'evaluationresult') {} 10 | 11 | 12 | export function mkSuccess(value: any): EvaluationResult { 13 | return new EvaluationResult({ 14 | type: 'success', 15 | value, 16 | }); 17 | } 18 | 19 | 20 | export function mkStuck(value: any): EvaluationResult { 21 | return new EvaluationResult({ 22 | type: 'stuck', 23 | value, 24 | }); 25 | } 26 | 27 | 28 | export function bind( 29 | result: EvaluationResult, 30 | fun: (value: any) => EvaluationResult): EvaluationResult { 31 | return result.type === 'success' ? fun(result.value) : result; 32 | } 33 | -------------------------------------------------------------------------------- /src/transit-immutable-js.js: -------------------------------------------------------------------------------- 1 | import transit from 'transit-js'; 2 | import { List, Set, OrderedMap, Map } from 'immutable'; 3 | 4 | export const writeHandlers = [ 5 | List, transit.makeWriteHandler({ 6 | tag: () => 'iL', 7 | rep: vec => vec.toArray(), 8 | }), 9 | Set, transit.makeWriteHandler({ 10 | tag: () => 'iS', 11 | rep: vec => vec.toArray(), 12 | }), 13 | OrderedMap, transit.makeWriteHandler({ 14 | tag: () => 'iO', 15 | rep: vec => vec.toArray(), 16 | }), 17 | Map, transit.makeWriteHandler({ 18 | tag: () => 'iM', 19 | rep: vec => vec.toArray(), 20 | }), 21 | ]; 22 | 23 | export const readHandlers = { 24 | 'iL': rep => List(rep), 25 | 'iS': rep => Set(rep), 26 | 'iO': rep => OrderedMap(rep), 27 | 'iM': rep => Map(rep), 28 | }; 29 | -------------------------------------------------------------------------------- /src/aspects/function/function-test.js: -------------------------------------------------------------------------------- 1 | import { Type } from '../../theory/tm'; 2 | import { mkSuccess } from '../../theory/evaluation'; 3 | 4 | import { id, k } from '../../testutil/examples'; 5 | import expectImmutableIs from '../../testutil/expectImmutableIs'; 6 | 7 | import { mkApplication } from './data'; 8 | 9 | 10 | describe('functions', () => { 11 | 12 | it('steps id', () => { 13 | const app = mkApplication(id, Type); 14 | 15 | expectImmutableIs( 16 | app.step(), 17 | mkSuccess(Type) 18 | ); 19 | }); 20 | 21 | it('steps k', () => { 22 | const app = mkApplication( 23 | mkApplication(k, Type), 24 | Type 25 | ); 26 | 27 | expectImmutableIs( 28 | app.step(), 29 | mkSuccess(Type) 30 | ); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /src/theory/hole.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { List, Record } from 'immutable'; 3 | 4 | import { mkStuck } from './evaluation'; 5 | 6 | import type { Action } from './edit'; 7 | import type { EvaluationResult } from './evaluation'; 8 | import type { Ref, FreeVar } from './ref'; 9 | import type { Tm } from './tm'; 10 | 11 | 12 | const HoleShape = Record({ 13 | name: null, 14 | type: null, 15 | }, 'hole'); 16 | 17 | 18 | export default class Hole extends HoleShape { 19 | 20 | step(): EvaluationResult { 21 | return mkStuck(this); 22 | } 23 | 24 | subst(root: FreeVar, ref: Ref, value: Tm): Tm { 25 | return ref.is(this.ref, root) ? value : this; 26 | } 27 | 28 | actions(): List { 29 | return List(); 30 | } 31 | 32 | // TODO 33 | // * term 34 | // * type 35 | // * getPieces 36 | // * performEdit 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Avatar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class Avatar extends Component { 4 | static propTypes = { 5 | size: PropTypes.number, 6 | round: PropTypes.bool, 7 | // fbId: PropTypes.string.isRequired, 8 | }; 9 | 10 | render() { 11 | const size = this.props.size || 50; 12 | const width = size; 13 | const height = size; 14 | const style = { 15 | borderRadius: this.props.round ? size : 0, 16 | marginTop: -10, 17 | marginRight: 20, 18 | boxSizing: 'border-box', 19 | border: '1px solid #ddd', 20 | }; 21 | // const fbId = this.props.fbId; 22 | const src = 'http://joelburget.com/media/img/profile.jpg'; 23 | 24 | return ( 25 | 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ducks/relation.js: -------------------------------------------------------------------------------- 1 | import { Set } from 'immutable'; 2 | 3 | import type Relation from '../theory/relation'; 4 | 5 | 6 | const initialState = Set(); 7 | 8 | 9 | const INSERT_RELATION = 'pigment/relation/INSERT_RELATION'; 10 | 11 | 12 | export default function reducer(state = initialState, action = {}) { 13 | switch (action.type) { 14 | case INSERT_RELATION: 15 | return state.add(action.relation); 16 | 17 | default: 18 | return state; 19 | } 20 | } 21 | 22 | 23 | // query the relation store for all relations with either endpoint at this 24 | // location 25 | export function query(state, path) { 26 | return state.filter(({ subject, object }) => { 27 | return subject === path || object === path; 28 | }); 29 | } 30 | 31 | 32 | export function insertRelation(relation: Relation) { 33 | return { 34 | type: INSERT_RELATION, 35 | relation, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /api/actions/loadWidgets.js: -------------------------------------------------------------------------------- 1 | const initialWidgets = [ 2 | {id: 1, color: 'Red', sprocketCount: 7, owner: 'John'}, 3 | {id: 2, color: 'Taupe', sprocketCount: 1, owner: 'George'}, 4 | {id: 3, color: 'Green', sprocketCount: 8, owner: 'Ringo'}, 5 | {id: 4, color: 'Blue', sprocketCount: 2, owner: 'Paul'} 6 | ]; 7 | 8 | export function getWidgets(req) { 9 | let widgets = req.session.widgets; 10 | if (!widgets) { 11 | widgets = initialWidgets; 12 | req.session.widgets = widgets; 13 | } 14 | return widgets; 15 | } 16 | 17 | export default function loadWidgets(req) { 18 | return new Promise((resolve, reject) => { 19 | // make async call to database 20 | setTimeout(() => { 21 | if (Math.floor(Math.random() * 3) === 0) { 22 | reject('Widget load fails 33% of the time. You were unlucky.'); 23 | } else { 24 | resolve(getWidgets(req)); 25 | } 26 | }, 1000); // simulate async load 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/aspects/module/module-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { List } from 'immutable'; 3 | 4 | import { Module, Note, Definition, MODULE_PUBLIC } from '../module/data'; 5 | 6 | import expectImmutableIs from '../../testutil/expectImmutableIs'; 7 | import { id } from '../../testutil/examples'; 8 | 9 | 10 | const testModule = new Module({ 11 | name: 'test module', 12 | contents: List([ 13 | new Note({ 14 | name: 'note', 15 | defn: 'just a note!', 16 | }), 17 | new Definition({ 18 | name: 'f', 19 | defn: id, 20 | visibility: MODULE_PUBLIC, 21 | }), 22 | ]), 23 | }); 24 | 25 | describe('modules', () => { 26 | it('is named', () => { 27 | expect(testModule.name).toBe('test module'); 28 | }); 29 | 30 | it('accesses members', () => { 31 | expect(testModule.contents.get(1).name).toBe('f'); 32 | expectImmutableIs( 33 | testModule.contents.get(1).defn, 34 | id 35 | ); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/theory/conflict.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { List, Record } from 'immutable'; 3 | import invariant from 'invariant'; 4 | 5 | import type Edit, { Action } from './edit'; 6 | 7 | 8 | const ConflictShape = Record({ 9 | left: null, 10 | right: null, 11 | }); 12 | 13 | 14 | const TAKE_LEFT = 'TAKE_LEFT'; 15 | const TAKE_RIGHT = 'TAKE_RIGHT'; 16 | 17 | 18 | export default class Conflict extends ConflictShape { 19 | 20 | actions(): List { 21 | return List([ 22 | { 23 | title: 'take left', 24 | id: TAKE_LEFT, 25 | }, 26 | { 27 | title: 'take right', 28 | id: TAKE_RIGHT, 29 | }, 30 | ]); 31 | } 32 | 33 | performEdit(id: string): Edit { 34 | invariant( 35 | id === TAKE_LEFT || id === TAKE_RIGHT, 36 | 'Conflict only knows TAKE_LEFT and TAKE_RIGHT' 37 | ); 38 | 39 | return { 40 | TAKE_LEFT: this.left, 41 | TAKE_RIGHT: this.right, 42 | }[id]; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Expression/Hole.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/color'; 2 | 3 | $gray: #ddd; 4 | 5 | .hole { 6 | padding: 0 5px; 7 | display: flex; 8 | flex-direction: row; 9 | 10 | input { 11 | background-color: $hole; 12 | // background-color: rgb(255, 195, 158); 13 | border: none; 14 | width: 50px; 15 | padding: 4px 5px; 16 | outline: none; 17 | } 18 | 19 | // This is gross -- target the autocomplete overlay so it appears above 20 | // other page elements. 21 | > div > div:nth-child(2) { 22 | z-index: 1; 23 | } 24 | } 25 | 26 | .ascription { 27 | margin: 0 2px; 28 | } 29 | 30 | .completionHl { 31 | background-color: $hole; 32 | } 33 | 34 | .completion { 35 | display: flex; 36 | flex-direction: row; 37 | } 38 | 39 | .completionCategory { 40 | padding: 5px 10px; 41 | border-right: 1px solid $gray; 42 | width: 100px; 43 | } 44 | 45 | .completionName { 46 | padding: 5px 10px; 47 | } 48 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../server.babel'); // babel registration (runtime transpilation for node) 3 | var path = require('path'); 4 | var rootDir = path.resolve(__dirname, '..'); 5 | /** 6 | * Define isomorphic constants. 7 | */ 8 | global.__CLIENT__ = false; 9 | global.__SERVER__ = true; 10 | global.__DISABLE_SSR__ = true; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING 11 | global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production'; 12 | 13 | if (__DEVELOPMENT__) { 14 | if (!require('piping')({ 15 | hook: true, 16 | ignore: /(\/\.|~$|\.json|\.scss$)/i 17 | })) { 18 | return; 19 | } 20 | } 21 | 22 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools 23 | var WebpackIsomorphicTools = require('webpack-isomorphic-tools'); 24 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools')) 25 | .development(__DEVELOPMENT__) 26 | .server(rootDir, function() { 27 | require('../src/server'); 28 | }); 29 | -------------------------------------------------------------------------------- /webpack/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | var Express = require('express'); 2 | var webpack = require('webpack'); 3 | 4 | var config = require('../src/config'); 5 | var webpackConfig = require('./dev.config'); 6 | var compiler = webpack(webpackConfig); 7 | 8 | var host = process.env.HOST || 'localhost'; 9 | var port = parseInt(config.port, 10) + 1 || 3001; 10 | var serverOptions = { 11 | contentBase: 'http://' + host + ':' + port, 12 | quiet: true, 13 | noInfo: true, 14 | hot: true, 15 | inline: true, 16 | lazy: false, 17 | publicPath: webpackConfig.output.publicPath, 18 | headers: {'Access-Control-Allow-Origin': '*'}, 19 | stats: {colors: true} 20 | }; 21 | 22 | var app = new Express(); 23 | 24 | app.use(require('webpack-dev-middleware')(compiler, serverOptions)); 25 | app.use(require('webpack-hot-middleware')(compiler)); 26 | 27 | app.listen(port, function onAppListening(err) { 28 | if (err) { 29 | console.error(err); 30 | } else { 31 | console.info('==> 🚧 Webpack development server listening on port %s', port); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /src/theory/relation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // The notion of a relation between two terms (by which I mean terms, types, 4 | // kinds, etc). 5 | // 6 | // * x "is of type" y 7 | // * x "accepts term" y 8 | // - This is really the same as the first: y "is of type" x 9 | // - Is it though? The relationship is directional, only telling us about x. 10 | // * x "is" y 11 | // - perhaps this is subtype-aware? 12 | // 13 | // I'm not yet sure if these are all the relations we care about, but they're 14 | // all I can think of at the moment. 15 | 16 | import { Record } from 'immutable'; 17 | 18 | // Subject and object both need to point to locations -- definitely not a term. 19 | // Because the term is immutable and it can change, we can't point right at it! 20 | 21 | 22 | export const IS_TYPE = 'IS_TYPE'; 23 | export const ACCEPTS_TERM = 'ACCEPTS_TERM'; 24 | export const MATCHES = 'MATCHES'; 25 | 26 | 27 | const Relation = Record({ 28 | type: MATCHES, // x _ y 29 | subject: null, // _ matches y 30 | object: null, // x matches _ 31 | }); 32 | 33 | 34 | export default Relation; 35 | -------------------------------------------------------------------------------- /src/redux/transit.js: -------------------------------------------------------------------------------- 1 | import transit from 'transit-js'; 2 | 3 | import { writeHandlers as writeHandlers1, 4 | readHandlers as readHandlers1 } from '../transit-immutable-js'; 5 | import { writeHandlers as writeHandlers2, 6 | readHandlers as readHandlers2 } from '../theory/transit'; 7 | import { writeHandlers as writeHandlers3, 8 | readHandlers as readHandlers3 } from '../aspects/transit'; 9 | import { writeHandlers as writeHandlers4, 10 | readHandlers as readHandlers4 } from '../ducks/module'; 11 | 12 | const FORMAT = 'json-verbose'; 13 | 14 | const writeHandlers = transit.map( 15 | [].concat( 16 | writeHandlers1, 17 | writeHandlers2, 18 | writeHandlers3, 19 | writeHandlers4 20 | ) 21 | ); 22 | 23 | const readHandlers = { 24 | ...readHandlers1, 25 | ...readHandlers2, 26 | ...readHandlers3, 27 | ...readHandlers4, 28 | }; 29 | 30 | export const writer = transit.writer(FORMAT, { handlers: writeHandlers }); 31 | 32 | export const reader = transit.reader(FORMAT, { handlers: readHandlers }); 33 | 34 | export const decoder = transit.decoder({ handlers: readHandlers }); 35 | -------------------------------------------------------------------------------- /src/aspects/record/view.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import React, { Component, PropTypes } from 'react'; 3 | 4 | import RecData from './data'; 5 | import Expression from '../../components/Expression'; 6 | import style from './style.scss'; 7 | 8 | 9 | export default class Rec extends Component { 10 | static propTypes = { 11 | children: PropTypes.instanceOf(RecData).isRequired, 12 | path: PropTypes.instanceOf(List).isRequired, 13 | }; 14 | 15 | render() { 16 | const children = this.props.children.values 17 | .entrySeq() 18 | .toArray() 19 | .map(([name, value]) => { 20 | return ( 21 |
22 | {name} : 23 | 24 | {value} 25 | 26 |
27 | ); 28 | }); 29 | 30 | return ( 31 |
32 |
Record {'{'}
33 |
34 | {children} 35 |
36 |
{'}'}
37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Autocomplete.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Rackt from 'react-autocomplete'; 3 | 4 | // TODO - should this be removed? I can't remember where styles come from 5 | // import styles from './Autocomplete.scss'; 6 | 7 | // Synchronous loading component -- no "loading..." 8 | export default class Autocomplete extends Component { 9 | static propTypes = { 10 | items: PropTypes.arrayOf( 11 | PropTypes.shape({ 12 | category: PropTypes.string.isRequired, 13 | 14 | // really, an instance of Tm, which isn't a real class 15 | item: PropTypes.func.isRequired, 16 | }) 17 | ), 18 | renderItem: PropTypes.func.isRequired, 19 | onSelect: PropTypes.func.isRequired, 20 | onChange: PropTypes.func.isRequired, 21 | }; 22 | 23 | render() { 24 | return ( 25 | item.item.name} 28 | onChange={(event, value) => this.props.onChange(value)} 29 | onSelect={this.props.onSelect} 30 | renderItem={this.props.renderItem} /> 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/aspects/variant/view.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import React, { Component, PropTypes } from 'react'; 3 | 4 | import Expression from '../../components/Expression'; 5 | import VariantData from './data'; 6 | import style from './style.scss'; 7 | 8 | 9 | export default class Variant extends Component { 10 | static propTypes = { 11 | children: PropTypes.instanceOf(VariantData).isRequired, 12 | path: PropTypes.instanceOf(List).isRequired, 13 | }; 14 | 15 | render() { 16 | const children = this.props.children.values 17 | .entrySeq() 18 | .toArray() 19 | .map(([name, value]) => { 20 | return ( 21 |
22 | {name} : 23 | 24 | {value} 25 | 26 |
27 | ); 28 | }); 29 | 30 | return ( 31 |
32 |
{'{'}
33 |
34 | {children} 35 |
36 |
{'}'}
37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Erik Rasmussen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true, 7 | }, 8 | "rules": { 9 | // immutable violates this rule 10 | "new-cap": 0, 11 | // allow `== null` 12 | "eqeqeq": [2, "smart"], 13 | "react/jsx-uses-react": 2, 14 | "react/jsx-uses-vars": 2, 15 | "react/react-in-jsx-scope": 2, 16 | "react/jsx-quotes": 0, 17 | "jsx-quotes": [2, "prefer-single"], 18 | // Temporarirly disabled due to a possible bug in babel-eslint (todomvc example) 19 | "block-scoped-var": 0, 20 | // Temporarily disabled for test/* until babel/babel-eslint#33 is resolved 21 | "padded-blocks": 0, 22 | "comma-dangle": 0, // not sure why airbnb turned this on. gross! 23 | "indent": [2, 2, {"SwitchCase": 1}], 24 | "no-console": 0, 25 | "no-else-return": 0 26 | }, 27 | "plugins": [ 28 | "react" 29 | ], 30 | "globals": { 31 | "__DEVELOPMENT__": true, 32 | "__CLIENT__": true, 33 | "__SERVER__": true, 34 | "__DISABLE_SSR__": true, 35 | "__DEVTOOLS__": true, 36 | "socket": true, 37 | "webpackIsomorphicTools": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/aspects/equality.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Record } from 'immutable'; 3 | 4 | import { Type } from '../theory/tm'; 5 | import { EvaluationResult } from '../theory/evaluation'; 6 | 7 | 8 | const EqualityShape = Record({ 9 | of: null, // Tm 10 | type: Type.singleton, 11 | }, 'equality'); 12 | 13 | 14 | // Propositional Equality type 15 | // 16 | // * Should this be entirely defined in userland? 17 | // * How does the reflection rule work? 18 | export class Equality extends EqualityShape { 19 | static arity = [0]; 20 | 21 | map(): Equality { 22 | throw new Error('unimplemented - Equality.map'); 23 | } 24 | 25 | step(): EvaluationResult { 26 | throw new Error('unimplemented - Equality.step'); 27 | } 28 | } 29 | 30 | 31 | const ReflShape = Record({ 32 | left: null, // Tm 33 | right: null, // Tm 34 | type: Type.singleton, 35 | }, 'refl'); 36 | 37 | 38 | // TODO come up with appropriate name for this 39 | export class Refl extends ReflShape { 40 | static arity = [0, 0]; 41 | 42 | map(): Equality { 43 | throw new Error('unimplemented - Refl.map'); 44 | } 45 | 46 | step(): EvaluationResult { 47 | throw new Error('unimplemented - Refl.step'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/util/halfzip.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // 3 | // halfZip is like unification, but only one level at a time. useful? 4 | import type { KeyedIterable } from 'immutable'; 5 | 6 | // would be really slick if i could do: 7 | // 8 | 9 | for (let slot of as) { 10 | if (!bs.has(slot)) { 11 | halt; 12 | } else { 13 | const aVal = as.getIn(slot); 14 | const bVal = bs.getIn(slot); 15 | 16 | if (atomic(aVal) && atomic(bVal)) { 17 | if (defnEqual(aVal, bVal)) { 18 | yield aVal; 19 | } else { 20 | halt; 21 | } 22 | } else if (!atomic(aVal) && !atomic(bVal)) { 23 | const unified = await(unify( 24 | } 25 | } 26 | } 27 | 28 | export default function halfZip( 29 | as: KeyedIterable, 30 | bs: KeyedIterable): ?KeyedIterable { 31 | 32 | if (as.count() !== bs.count()) { 33 | return null; 34 | } 35 | 36 | try { 37 | return as.map((aVal, aKey) => { 38 | const bVal = bs.get(aKey); 39 | if (!bVal) { 40 | // TODO find more idiomatic way 41 | throw new Error('stop iteration!'); 42 | } else { 43 | return [aVal, bVal]; 44 | } 45 | }); 46 | } catch (stop) { 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/theory/unify.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // I think unification should join variable binding among the pantheon of 4 | // things we provide as infrastructure. Done in a simple, easy to understand 5 | // way. What other pieces of infrastructure are fundamental, simple, and 6 | // powerful? 7 | // 8 | // * pattern matching (SICP draws a connection between unification and pattern 9 | // matching) 10 | // - unification is a generalization of pattern matching in which both the 11 | // pattern and datum may contain variables 12 | // * evaluation 13 | // * editing / history / persistence 14 | 15 | import defnEq from './definitionalEquality'; 16 | 17 | import type Context from './context'; 18 | import type { Tm } from './tm'; 19 | 20 | 21 | export default function unify(pat1: Tm, pat2: Tm, frame: Context): ?Tm { 22 | // interesting case: holes are not symmetrical -- something else taking the 23 | // lead in unification will reject a hole, but a hole will accept anything. 24 | 25 | // TODO account for variables and holes! Are these special cased? How do we 26 | // specify that they're really just big slots? 27 | // 28 | // Use definitional equality for now, then figure out how to do something 29 | // more sophisticated later. 30 | 31 | // XXX need types to be equal, not terms! 32 | return defnEq(ty1, ty2) ? 33 | pat1.form.unify(pat1, pat2) : 34 | null; 35 | } 36 | -------------------------------------------------------------------------------- /src/aspects/module/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/color'; 2 | @import '../../style/header'; 3 | 4 | $padding: 15px; 5 | $bigMargin: 40px; 6 | $vBorder: 1px solid #e0e0e0; 7 | 8 | .module { 9 | @extend .material-header; 10 | margin-left: $bigMargin; 11 | 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | .moduleHeader { 17 | border-bottom: 2px solid $margin-links; 18 | } 19 | 20 | .moduleTable { 21 | display: flex; 22 | flex-direction: column; 23 | } 24 | 25 | .definitionRow { 26 | margin-bottom: $bigMargin; 27 | border-bottom: $vBorder; 28 | 29 | .rowContent { 30 | display: flex; 31 | flex-direction: row; 32 | } 33 | 34 | .rowSpacer { 35 | height: $padding; 36 | } 37 | } 38 | 39 | .itemLabel { 40 | display: flex; 41 | flex-direction: column; 42 | 43 | padding: $padding 0; 44 | width: 200px; 45 | 46 | // TODO - figure out a better place to put this move handle 47 | // cursor: move; 48 | } 49 | 50 | .itemContent { 51 | display: flex; 52 | flex-direction: column; 53 | flex: 1; 54 | 55 | vertical-align: top; 56 | border-left: $vBorder; 57 | padding: $padding 0 $padding $bigMargin; 58 | } 59 | 60 | .itemNotes { 61 | padding: $padding; 62 | border-left: $vBorder; 63 | } 64 | 65 | .itemType { 66 | font-size: 10px; 67 | font-weight: bold; 68 | } 69 | 70 | .newItem { 71 | padding-top: 10px; 72 | } 73 | -------------------------------------------------------------------------------- /src/aspects/external-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | 3 | import { mkSuccess, bind } from '../theory/tm'; 4 | import Arr from './function/data'; 5 | import { JsBoolean, JsNumber, JsApp, JsFunction } from './external'; 6 | 7 | const disable = () => {}; 8 | 9 | disable('externals', () => { 10 | it('does booleans', () => { 11 | expect(new JsBoolean(true).external) 12 | .toBe(true); 13 | 14 | expect(new JsBoolean(false).external) 15 | .toBe(false); 16 | }); 17 | 18 | it('does (increment) functions', () => { 19 | const numTy = JsNumber.type; 20 | const fun = new JsFunction( 21 | x => x + 1, // eslint-disable-line id-length 22 | new Arr(numTy, numTy) 23 | ); 24 | const num = new JsNumber(0); 25 | 26 | expect(new JsApp(fun, num).step([])) 27 | .toEqual(mkSuccess(new JsNumber(1))); 28 | }); 29 | 30 | it('does (curried) functions', () => { 31 | const numTy = JsNumber.type; 32 | const fun = new JsFunction( 33 | (x, y) => x + y, // eslint-disable-line id-length 34 | new Arr(numTy, Arr(numTy, numTy)) 35 | ); 36 | const num0 = new JsNumber(0); 37 | const num1 = new JsNumber(1); 38 | 39 | const computation = bind( 40 | new JsApp(fun, num0).step([]), 41 | fun_ => new JsApp(fun_, num1).step([]) 42 | ); 43 | 44 | expect(computation).toEqual(mkSuccess(num1)); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/redux/create.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | 3 | import { decoder } from './transit'; 4 | import createMiddleware from './clientMiddleware'; 5 | 6 | export default function createApiClientStore(client, data) { 7 | const middleware = createMiddleware(client); 8 | let finalCreateStore; 9 | if (__DEVELOPMENT__ && __CLIENT__ && __DEVTOOLS__) { 10 | const { devTools, persistState } = require('redux-devtools'); 11 | finalCreateStore = compose( 12 | applyMiddleware(middleware), 13 | devTools(), 14 | persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)), 15 | )(createStore); 16 | } else { 17 | finalCreateStore = applyMiddleware(middleware)(createStore); 18 | } 19 | 20 | // data is undefined on the server, 21 | // encoded transit on the client 22 | // 23 | // ... that's the idea, anyway. 24 | // HACK HACK HACK 25 | let hydrated; 26 | if (data == null) { 27 | hydrated = undefined; 28 | } else { 29 | hydrated = Object.assign({}, data); 30 | hydrated.module = decoder.decode(hydrated.module); 31 | } 32 | 33 | const reducer = require('../ducks/reducer'); 34 | const store = finalCreateStore(reducer, hydrated); 35 | store.client = client; 36 | 37 | if (__DEVELOPMENT__ && module.hot) { 38 | module.hot.accept('../ducks/reducer', () => { 39 | store.replaceReducer(require('../ducks/reducer')); 40 | }); 41 | } 42 | 43 | return store; 44 | } 45 | -------------------------------------------------------------------------------- /src/aspects/row/view.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import React, { Component, PropTypes } from 'react'; 3 | 4 | import RowData from './data'; 5 | import Expression from '../../components/Expression'; 6 | import style from './style.scss'; 7 | 8 | 9 | export default class Row extends Component { 10 | static propTypes = { 11 | children: PropTypes.instanceOf(RowData).isRequired, 12 | path: PropTypes.instanceOf(List).isRequired, 13 | }; 14 | 15 | render() { 16 | const children = this.props.children.entries 17 | .entrySeq() 18 | .toArray() 19 | .map(([name, value]) => { 20 | // TODO I think you should really be able to click on any of these rows 21 | // -- right now you can only click on the expression. 22 | // 23 | // Maybe it doesn't make sense to select the row, since it's not really 24 | // an expression, but you need to be able to rename and remove items, 25 | // at least. 26 | return ( 27 |
28 | {name} : 29 | 30 | {value} 31 | 32 |
33 | ); 34 | }); 35 | 36 | return ( 37 |
38 |
Row {'{'}
39 |
40 | {children} 41 |
42 |
{'}'}
43 |
44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/theory/ref.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // 3 | // global reference 4 | 5 | import { List, Record, is } from 'immutable'; 6 | import ImmutablePropTypes from 'react-immutable-proptypes'; 7 | import { PropTypes } from 'react'; 8 | 9 | 10 | export class BoundVar extends Record({ index: 0, name: null }) { 11 | is(ref: Ref, root: FreeVar): boolean { 12 | return ref instanceOf BoundVar ? 13 | ref.index === this.index : // XXX also disambiguate name 14 | XXX; 15 | } 16 | } 17 | 18 | 19 | export class FreeVar extends Record({ path: null }) { 20 | extend(ref: BoundVar): FreeVar { 21 | const path = this.path.concat(ref.path); 22 | return new FreeVar({ path }); 23 | } 24 | 25 | is(ref: Ref, root: FreeVar): boolean { 26 | return ref instanceof FreeVar ? 27 | is(this, ref) : 28 | XXX; 29 | } 30 | } 31 | 32 | 33 | export type Ref = BoundVar | FreeVar; 34 | 35 | 36 | export function mkBound(index: number, ?name: string): BoundVar { 37 | return new BoundVar({ index, name }); 38 | } 39 | 40 | 41 | // export function mkFree(...parts: Array): FreeVar { 42 | // const path = List(parts); 43 | export function mkFree(): FreeVar { 44 | const path = List(arguments); 45 | return new FreeVar({ path }); 46 | } 47 | 48 | 49 | export const PropTypesPath = ImmutablePropTypes.listOf(PropTypes.string); 50 | export const PropTypesBoundVar = PropTypes.instanceOf(BoundVar); 51 | export const PropTypesFreeVar = PropTypes.instanceOf(FreeVar); 52 | export const PropTypesRef = PropTypes.oneOfType([ 53 | PropTypesBoundVar, 54 | PropTypesFreeVar, 55 | ]); 56 | -------------------------------------------------------------------------------- /src/containers/LoginSuccess.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {bindActionCreators} from 'redux'; 3 | import {connect} from 'react-redux'; 4 | import {isLoaded as isAuthLoaded, load as loadAuth} from '../ducks/auth'; 5 | import * as authActions from '../ducks/auth'; 6 | 7 | import styles from './LoginSuccess.scss'; 8 | 9 | @connect( 10 | state => ({user: state.auth.user}), 11 | dispatch => bindActionCreators(authActions, dispatch) 12 | ) 13 | export default 14 | class LoginSuccess extends Component { 15 | static propTypes = { 16 | user: PropTypes.object, 17 | logout: PropTypes.func 18 | } 19 | 20 | static fetchData(store) { 21 | if (!isAuthLoaded(store.getState())) { 22 | return store.dispatch(loadAuth()); 23 | } 24 | } 25 | 26 | render() { 27 | const {user, logout} = this.props; 28 | 29 | return ( 30 |
31 |
Login Success
32 | 33 |
34 |

Hi, {user.name}. You have just successfully logged in, and were forwarded here 35 | by componentWillReceiveProps() in App.js, which is listening to 36 | the auth reducer via redux @connect. How exciting! 37 |

38 | 39 |

40 | The same function will forward you to / should you chose to log out. The choice is yours... 41 |

42 | 43 |
44 | 45 |
46 |
47 |
48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/__tests__/InfoBar-test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react/addons'; 2 | // import {expect} from 'chai'; 3 | // import InfoBar from '../InfoBar'; 4 | // import { Provider } from 'react-redux'; 5 | // import createStore from 'redux/create'; 6 | // import ApiClient from 'ApiClient'; 7 | // const { TestUtils } = React.addons; 8 | // const client = new ApiClient(); 9 | 10 | // describe('InfoBar', () => { 11 | // const mockStore = { 12 | // info: { 13 | // load: () => {}, 14 | // loaded: true, 15 | // loading: false, 16 | // data: { 17 | // message: 'This came from the api server', 18 | // time: Date.now() 19 | // } 20 | // } 21 | // }; 22 | 23 | // const store = createStore(client, mockStore); 24 | // const renderer = TestUtils.renderIntoDocument( 25 | // 26 | // {() => } 27 | // 28 | // ); 29 | // const dom = renderer; 30 | 31 | // it('should render correctly', () => { 32 | // expect(renderer).to.be.ok; 33 | // }); 34 | 35 | // it('should render with correct value', () => { 36 | // const text = dom.getElementsByTagName('strong')[0].textContent; 37 | // expect(text).to.equal(mockStore.info.data.message); 38 | // }); 39 | 40 | // it('should render with a reload button', () => { 41 | // const text = dom.getElementsByTagName('button')[0].textContent; 42 | // expect(text).to.be.a('string'); 43 | // }); 44 | 45 | // it('should render the correct className', () => { 46 | // const styles = require('../InfoBar.scss'); 47 | // expect(styles.infoBar).to.be.a('string'); 48 | // expect(dom.className).to.include(styles.infoBar); 49 | // }); 50 | 51 | // }); 52 | -------------------------------------------------------------------------------- /src/theory/transit.js: -------------------------------------------------------------------------------- 1 | /* eslint id-length: 0 */ 2 | import transit from 'transit-js'; 3 | 4 | import { FreeVar, BoundVar } from './ref'; 5 | import { Type, Var, Hole } from './tm'; 6 | import Edit from './edit'; 7 | import Relation from './relation'; 8 | 9 | export const writeHandlers = [ 10 | 11 | Relation, transit.makeWriteHandler({ 12 | tag: () => 'relation', 13 | rep: v => [v.type, v.subject, v.object], 14 | }), 15 | 16 | Edit, transit.makeWriteHandler({ 17 | tag: () => 'edit', 18 | rep: v => [v.status, v.world, v.catalyst, v.closure], 19 | }), 20 | 21 | // tm 22 | Type, transit.makeWriteHandler({ 23 | tag: () => 'type', 24 | rep: () => [], 25 | stringRep: 'ty', 26 | }), 27 | Hole, transit.makeWriteHandler({ 28 | tag: () => 'hole', 29 | rep: v => [v.name, v.type], 30 | }), 31 | Var, transit.makeWriteHandler({ 32 | tag: () => 'var', 33 | rep: v => [v.ref, v.type], 34 | }), 35 | 36 | // ref 37 | FreeVar, transit.makeWriteHandler({ 38 | tag: () => 'absref', 39 | rep: v => v.path, 40 | }), 41 | BoundVar, transit.makeWriteHandler({ 42 | tag: () => 'relref', 43 | rep: v => v.path, 44 | }), 45 | 46 | ]; 47 | 48 | 49 | export const readHandlers = { 50 | 51 | 'relation': ([type, subject, object]) => 52 | new Relation({ type, subject, object }), 53 | 54 | 'edit': ([status, world, catalyst, closure]) => 55 | new Edit({ status, world, catalyst, closure }), 56 | 57 | // tm 58 | 'type': () => Type.singleton, 59 | 'hole': ([name, type]) => new Hole({ name, type }), 60 | 'var': ([ref, type]) => new Var({ ref, type }), 61 | 62 | // ref 63 | 'absref': path => new FreeVar({ path }), 64 | 'relref': path => new BoundVar({ path }), 65 | 66 | }; 67 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | 6 | browsers: [ process.env.CONTINUOUS_INTEGRATION ? 'Firefox' : 'Chrome' ], 7 | 8 | singleRun: false, 9 | 10 | frameworks: [ 'mocha' ], 11 | 12 | files: [ 13 | 'tests.webpack.js' 14 | ], 15 | 16 | preprocessors: { 17 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ] 18 | }, 19 | 20 | reporters: [ 'mocha' ], 21 | 22 | webpack: { 23 | devtool: 'inline-source-map', 24 | module: { 25 | loaders: [ 26 | { test: /\.(jpe?g|png|gif|svg)$/, loader: 'url', query: {limit: 10240} }, 27 | { test: /\.js$/, exclude: /node_modules/, loaders: ['react-hot', 'babel?stage=0&optional=runtime']}, 28 | { test: /\.json$/, loader: 'json-loader' }, 29 | { test: /\.scss$/, loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap' } 30 | ] 31 | }, 32 | resolve: { 33 | modulesDirectories: [ 34 | 'src', 35 | 'node_modules' 36 | ], 37 | extensions: ['', '.json', '.js'] 38 | }, 39 | plugins: [ 40 | // hot reload 41 | new webpack.HotModuleReplacementPlugin(), 42 | new webpack.IgnorePlugin(/\.json$/), 43 | new webpack.NoErrorsPlugin(), 44 | new webpack.DefinePlugin({ 45 | __CLIENT__: true, 46 | __SERVER__: false, 47 | __DEVELOPMENT__: true, 48 | __DEVTOOLS__: false // <-------- DISABLE redux-devtools HERE 49 | }) 50 | ] 51 | }, 52 | 53 | webpackServer: { 54 | noInfo: true 55 | } 56 | 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /src/validation/validation.js: -------------------------------------------------------------------------------- 1 | const isEmpty = value => value === undefined || value === null || value === ''; 2 | const join = (rules) => value => rules.map(rule => rule(value)).filter(error => !!error)[0 /* first error */]; 3 | 4 | export function email(value) { 5 | // Let's not start a debate on email regex. This is just for an example app! 6 | if (!isEmpty(value) && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) { 7 | return 'Invalid email address'; 8 | } 9 | } 10 | 11 | export function required(value) { 12 | if (isEmpty(value)) { 13 | return 'Required'; 14 | } 15 | } 16 | 17 | export function minLength(min) { 18 | return value => { 19 | if (!isEmpty(value) && value.length < min) { 20 | return `Must be at least ${min} characters`; 21 | } 22 | }; 23 | } 24 | 25 | export function maxLength(max) { 26 | return value => { 27 | if (!isEmpty(value) && value.length > max) { 28 | return `Must be no more than ${max} characters`; 29 | } 30 | }; 31 | } 32 | 33 | export function integer(value) { 34 | if (!Number.isInteger(Number(value))) { 35 | return 'Must be an integer'; 36 | } 37 | } 38 | 39 | export function oneOf(enumeration) { 40 | return value => { 41 | if (!~enumeration.indexOf(value)) { 42 | return `Must be one of: ${enumeration.join(', ')}`; 43 | } 44 | }; 45 | } 46 | 47 | export function createValidator(rules) { 48 | return (data = {}) => { 49 | const errors = {valid: true}; 50 | Object.keys(rules).forEach((key) => { 51 | const rule = join([].concat(rules[key])); // concat enables both functions and arrays of functions 52 | const error = rule(data[key]); 53 | if (error) { 54 | errors[key] = error; 55 | errors.valid = false; 56 | } 57 | }); 58 | return errors; 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/aspects/row/data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import invariant from 'invariant'; 4 | import { Record, List } from 'immutable'; 5 | 6 | import { Hole, Type } from '../../theory/tm'; 7 | import { register } from '../../theory/registry'; 8 | import { openNewEdit } from '../../theory/edit'; 9 | import { ADD_ENTRY, addEntry, makeLabel } from '../../commands/addEntry'; 10 | import { POKE_HOLE, pokeHole, doPokeHole } from '../../commands/pokeHole'; 11 | 12 | import type { EvaluationResult } from '../../theory/evaluation'; 13 | import type { Tm } from '../../theory/tm'; 14 | import type Edit, { Action } from '../../theory/edit'; 15 | 16 | 17 | // formation 18 | 19 | const Row = Record({ 20 | entries: null, // Map 21 | }, 'row'); 22 | 23 | 24 | export const mkRow = entries => new Tm( 25 | ['Row', 'formation'], new Row({ entries }) 26 | ); 27 | 28 | 29 | const formation = { 30 | term: mkRow(XXX), 31 | type: Type, 32 | 33 | actions: () => List([addEntry, pokeHole]), 34 | 35 | performEdit: (id: string) => { 36 | invariant( 37 | id === ADD_ENTRY || id === POKE_HOLE, 38 | 'Row.performEdit only knows of ADD_ENTRY and POKE_HOLE' 39 | ); 40 | 41 | if (id === ADD_ENTRY) { 42 | const entries = this.entries; 43 | 44 | const label = makeLabel(entries); 45 | const val = new Hole({ type: Type.singleton }); 46 | const newRow = new Row(entries.set(label, val)); 47 | 48 | return openNewEdit(id, this, newRow, new List()); 49 | } else { // id === POKE_HOLE 50 | return doPokeHole(this); 51 | } 52 | } 53 | }; 54 | 55 | 56 | const signature = { 57 | formation, 58 | 59 | // no introduction / elimination! (for now?) 60 | intros: [], 61 | elims: [], 62 | 63 | searchAliases: ['row'], 64 | }; 65 | 66 | 67 | export default signature; 68 | 69 | 70 | register('row', signature); 71 | -------------------------------------------------------------------------------- /src/aspects/function/view.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { List } from 'immutable'; 4 | import React, { Component, PropTypes } from 'react'; 5 | 6 | import Expression, { expr } from '../../components/Expression'; 7 | 8 | import styles from './style.scss'; 9 | 10 | 11 | export class Binder extends Component { 12 | static propTypes = { 13 | path: PropTypes.instanceOf(List).isRequired, 14 | name: PropTypes.string.isRequired, 15 | type: PropTypes.object.isRequired, 16 | }; 17 | 18 | state = { 19 | expanded: false, 20 | }; 21 | 22 | toggle() { 23 | this.setState({ expanded: !this.state.expanded }); 24 | } 25 | 26 | renderExpanded() { 27 | const { name, type, path } = this.props; 28 | 29 | return ( 30 |
31 |
{name}
32 |
:
33 | {type} 34 |
35 | ); 36 | } 37 | 38 | render() { 39 | const expanded = this.state.expanded && this.renderExpanded(); 40 | 41 | return ( 42 | 43 |
{expanded}
44 | {this.props.name} 45 |
46 | ); 47 | } 48 | } 49 | 50 | 51 | export function Arrow({ children: item, path }) { 52 | return ( 53 |
54 | {expr(item, path, 'domain')} 55 | -> 56 | {expr(item, path, 'codomain')} 57 |
58 | ); 59 | } 60 | 61 | 62 | export function Lambda(props) { 63 | const { name, domain, body } = props.children; 64 | const { path } = props; 65 | 66 | return ( 67 |
68 | 69 |
70 | 71 |
72 | {body} 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/helpers/ApiClient.js: -------------------------------------------------------------------------------- 1 | import superagent from 'superagent'; 2 | import config from 'config'; 3 | 4 | /* 5 | * This silly underscore is here to avoid a mysterious "ReferenceError: ApiClient is not defined" error. 6 | * See Issue #14. https://github.com/erikras/react-redux-universal-hot-example/issues/14 7 | * 8 | * Remove it at your own risk. 9 | */ 10 | class ApiClient_ { 11 | constructor(req) { 12 | ['get', 'post', 'put', 'patch', 'del']. 13 | forEach((method) => { 14 | this[method] = (path, options) => { 15 | return new Promise((resolve, reject) => { 16 | const request = superagent[method](this.formatUrl(path)); 17 | if (options && options.params) { 18 | request.query(options.params); 19 | } 20 | if (__SERVER__) { 21 | if (req.get('cookie')) { 22 | request.set('cookie', req.get('cookie')); 23 | } 24 | } 25 | if (options && options.data) { 26 | request.send(options.data); 27 | } 28 | request.end((err, res) => { 29 | if (err) { 30 | reject(res.body || err); 31 | } else { 32 | resolve(res.body); 33 | } 34 | }); 35 | }); 36 | }; 37 | }); 38 | } 39 | 40 | /* This was originally a standalone function outside of this class, but babel kept breaking, and this fixes it */ 41 | formatUrl(path) { 42 | const adjustedPath = path[0] !== '/' ? '/' + path : path; 43 | if (__SERVER__) { 44 | // Prepend host and port of the API server to the path. 45 | return 'http://localhost:' + config.apiPort + adjustedPath; 46 | } 47 | // Prepend `/api` to relative URL, to proxy to API server. 48 | return '/api' + adjustedPath; 49 | } 50 | } 51 | const ApiClient = ApiClient_; 52 | 53 | export default ApiClient; 54 | -------------------------------------------------------------------------------- /src/helpers/universalRouter.js: -------------------------------------------------------------------------------- 1 | import qs from 'query-string'; 2 | import React from 'react'; 3 | import {match, RoutingContext} from 'react-router'; 4 | import createRoutes from '../routes'; 5 | import { Provider } from 'react-redux'; 6 | 7 | const getFetchData = (component = {}) => { 8 | return component.WrappedComponent ? 9 | getFetchData(component.WrappedComponent) : 10 | component.fetchData; 11 | }; 12 | 13 | 14 | const fetchDataForContainers = (containers, store, params, query) => { 15 | const promises = containers 16 | .filter((component) => getFetchData(component)) // only look at ones with a static fetchData() 17 | .map(getFetchData) // pull out fetch data methods 18 | .map(fetchData => fetchData(store, params, query || {})); // call fetch data methods and save promises 19 | 20 | return Promise.all(promises); 21 | }; 22 | 23 | 24 | export default function universalRouter(location, history, store, preload) { 25 | const routes = createRoutes(store); 26 | 27 | return new Promise((resolve, reject) => { 28 | match({routes, history, location}, (error, redirectLocation, renderProps) => { 29 | if (error) { 30 | return reject(error); 31 | } 32 | 33 | if (redirectLocation) { 34 | return resolve({ redirectLocation }); 35 | } 36 | 37 | const component = ( 38 | 39 | 40 | 41 | ); 42 | 43 | if (preload) { 44 | fetchDataForContainers( 45 | renderProps.components, 46 | store, 47 | renderProps.params, 48 | qs.parse(renderProps.location.search) 49 | ) 50 | .then( 51 | () => { resolve({ component }); }, 52 | err => reject(err) 53 | ); 54 | } else { 55 | resolve({ component }); 56 | } 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/theory/__test__/eval-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { Map } from 'immutable'; 3 | 4 | import { Type, Hole } from '../tm'; 5 | import { mkSuccess, mkStuck } from '../evaluation'; 6 | import { mkFree } from '../ref'; 7 | 8 | import { mkFunction, mkApplication } from '../../aspects/function/data'; 9 | 10 | 11 | const emptyCtx = Map(); 12 | 13 | describe('eval', () => { 14 | const type = Type.singleton; 15 | 16 | it('steps type', () => { 17 | // start with an empty context; 18 | expect(type.step(emptyCtx.bind(mkFree()))) 19 | .toEqual(mkSuccess(type)); 20 | }); 21 | 22 | it('gets stuck on holes', () => { 23 | const hole = new Hole({ name: 'hole', type }); 24 | expect(hole.step(emptyCtx.bind(mkFree()))) 25 | .toEqual(mkStuck(hole)); 26 | 27 | const returningHole = mkApplication({ 28 | func: mkFunction( 29 | null, 30 | type, 31 | hole, 32 | type 33 | ), 34 | arg: type, 35 | type, 36 | }); 37 | expect(returningHole.step(emptyCtx.bind(mkFree()))) 38 | .toEqual(mkStuck(hole)); 39 | }); 40 | 41 | describe('function', () => { 42 | // TODO would be awesome for this to be parametric 43 | const ctx = Map({'x': type}); 44 | 45 | it('works with var', () => { 46 | const tm = mkApplication({ 47 | func: mkFunction( 48 | 'x', 49 | type, 50 | type, 51 | type, 52 | ), 53 | arg: type, 54 | type, 55 | }); 56 | 57 | expect(tm.step(mkFree(), ctx)) 58 | .toEqual(mkSuccess(type)); 59 | }); 60 | 61 | it('works with wildcards', () => { 62 | const tm = mkApplication({ 63 | func: mkFunction( 64 | null, 65 | type, 66 | type, 67 | type 68 | ), 69 | arg: type, 70 | type, 71 | }); 72 | 73 | expect(tm.step(mkFree(), ctx)) 74 | .toEqual(mkSuccess(type)); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/theory/__test__/ref-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { List } from 'immutable'; 3 | 4 | import { mkBound, mkFree, BoundVar, FreeVar } from '../ref'; 5 | 6 | import expectImmutableIs from '../../testutil/expectImmutableIs'; 7 | 8 | 9 | describe('ref', () => { 10 | it('mkBound', () => { 11 | const rel = mkBound(1, 'arg'); 12 | const expected = new BoundVar({ index: 1, name: 'arg' }); 13 | 14 | expectImmutableIs(rel, expected); 15 | }); 16 | 17 | it('mkFree', () => { 18 | const abs = mkFree('module', 'function'); 19 | const expected = new FreeVar({ path: List(['module', 'function']) }); 20 | 21 | expectImmutableIs(abs, expected); 22 | }); 23 | 24 | it('extends abs refs', () => { 25 | expectImmutableIs( 26 | mkFree('a', 'b').extend(mkBound('c', 'd')), 27 | mkFree('a', 'b', 'c', 'd') 28 | ); 29 | }); 30 | 31 | it('compares abs refs', () => { 32 | expect( 33 | mkFree('a', 'b', 'c').is(mkFree('a', 'b', 'c'), mkFree()) 34 | ).toBe(true); 35 | 36 | expect( 37 | mkFree('a', 'b', 'c').is(mkFree('a', 'b', 'c', 'd', '..'), mkFree()) 38 | ).toBe(true); 39 | }); 40 | 41 | it('compares rel refs', () => { 42 | // TODO - do we want to support this? 43 | expect( 44 | mkBound('a', 'b', 'c').is(mkBound('a', 'b', 'c'), mkFree()) 45 | ).toBe(true); 46 | 47 | expect( 48 | mkBound('a', 'b', 'c').is(mkBound('a', 'b', 'c', 'd', '..'), mkFree()) 49 | ).toBe(true); 50 | }); 51 | 52 | it('compares mixed refs', () => { 53 | expect( 54 | mkFree('a', 'b', 'c').is(mkBound('a', 'b', 'c'), mkFree()) 55 | ).toBe(true); 56 | 57 | expect( 58 | mkBound('a', 'b', 'c').is(mkFree('a', 'b', 'c', 'd', '..'), mkFree()) 59 | ).toBe(true); 60 | 61 | expect( 62 | mkBound('..', '..').is(mkFree('a'), mkFree('a', 'b', 'c')) 63 | ).toBe(true); 64 | 65 | expect( 66 | mkFree('a').is(mkBound('..', '..'), mkFree('a', 'b', 'c')) 67 | ).toBe(true); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/containers/App.scss: -------------------------------------------------------------------------------- 1 | @import '../style/color'; 2 | 3 | // TODO probably simpler to just make this a list, no? 4 | .margin { 5 | margin: 40px 0 0 40px; 6 | color: $margin-links; 7 | text-decoration: none; 8 | font-family: "Roboto","Helvetica","Arial",sans-serif; 9 | } 10 | 11 | .columnar { 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | .row { 17 | display: flex; 18 | flex-direction: row; 19 | } 20 | 21 | body { 22 | font-family: "Helvetica Neue", Helvetica, sans-serif; 23 | } 24 | 25 | .app { 26 | @extend .row; 27 | 28 | nav { 29 | width: 200px; 30 | background-color: $margin-background; 31 | 32 | @extend .columnar; 33 | 34 | .title { 35 | @extend .margin; 36 | 37 | display: block; 38 | font-size: 20px; 39 | font-weight: 400; 40 | } 41 | 42 | .module { 43 | @extend .margin; 44 | padding-left: 0; 45 | 46 | .hole { 47 | margin: 10px 0; 48 | border-bottom: 4px solid $hole; 49 | } 50 | 51 | .conflict { 52 | margin: 10px 0; 53 | border-bottom: 4px solid $conflict; 54 | } 55 | 56 | .history { 57 | margin: 10px 0; 58 | border-bottom: 4px solid $history; 59 | } 60 | } 61 | 62 | .subModule { 63 | padding-left: 10px; 64 | } 65 | 66 | li { 67 | list-style-type: none; 68 | margin-left: 0; 69 | } 70 | 71 | .login { 72 | @extend .row; 73 | @extend .margin; 74 | 75 | a { 76 | color: $margin-links; 77 | text-decoration: none; 78 | font-weight: 500; 79 | font-size: 13px; 80 | margin: 0; 81 | } 82 | } 83 | } 84 | 85 | main { 86 | flex: 1; 87 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 88 | } 89 | } 90 | 91 | .appContent { 92 | margin: 50px 0; // for fixed navbar 93 | } 94 | -------------------------------------------------------------------------------- /webpack/webpack-isomorphic-tools.js: -------------------------------------------------------------------------------- 1 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); 2 | 3 | // see this link for more info on what all of this means 4 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools 5 | module.exports = { 6 | webpack_assets_file_path: 'webpack-stats.json', 7 | 8 | assets: { 9 | images: { 10 | extensions: [ 11 | 'jpeg', 12 | 'jpg', 13 | 'png', 14 | 'gif', 15 | 'svg' 16 | ], 17 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser 18 | }, 19 | style_modules: { 20 | extension: 'scss', 21 | development: true, 22 | filter: function(m, regex, options) { 23 | if (options.development) { 24 | return regex.test(m.name); 25 | } 26 | //filter by modules with '.scss' inside name string, that also have name and moduleName that end with 'ss'(allows for css, less, sass, and scss extensions) 27 | //this ensures that the proper scss module is returned, so that namePrefix variable is no longer needed 28 | return (regex.test(m.name) && m.name.slice(-2) === 'ss' && m.reasons[0].moduleName.slice(-2) === 'ss'); 29 | }, 30 | naming: function(m) { 31 | //find index of '/src' inside the module name, slice it and resolve path 32 | var srcIndex = m.name.indexOf('/src'); 33 | var name = '.' + m.name.slice(srcIndex); 34 | if (name) { 35 | // Resolve the e.g.: "C:\" issue on windows 36 | const i = name.indexOf(':'); 37 | if (i >= 0) { 38 | name = name.slice(i + 1); 39 | } 40 | } 41 | return name; 42 | }, 43 | parser: function(m, options) { 44 | if (m.source) { 45 | var regex = options.development ? 46 | /exports\.locals = ((.|\n)+);/ : 47 | /module\.exports = ((.|\n)+);/; 48 | var match = m.source.match(regex); 49 | return match ? JSON.parse(match[1]) : {}; 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/containers/Login.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {bindActionCreators} from 'redux'; 3 | import {connect} from 'react-redux'; 4 | import DocumentMeta from 'react-document-meta'; 5 | import * as authActions from '../ducks/auth'; 6 | import {isLoaded as isAuthLoaded, load as loadAuth} from '../ducks/auth'; 7 | 8 | @connect( 9 | state => ({user: state.auth.user}), 10 | dispatch => bindActionCreators(authActions, dispatch) 11 | ) 12 | export default class Login extends Component { 13 | static propTypes = { 14 | user: PropTypes.object, 15 | login: PropTypes.func, 16 | logout: PropTypes.func 17 | } 18 | 19 | static fetchData(store) { 20 | if (!isAuthLoaded(store.getState())) { 21 | return store.dispatch(loadAuth()); 22 | } 23 | } 24 | 25 | handleSubmit(event) { 26 | event.preventDefault(); 27 | const input = this.refs.username; 28 | this.props.login(input.value); 29 | input.value = ''; 30 | } 31 | 32 | render() { 33 | const {user, logout} = this.props; 34 | const styles = require('./Login.scss'); 35 | return ( 36 |
37 | 38 |
Login
39 | {!user && 40 |
41 |
42 | 43 | 45 | 46 |

This will 'log you in' as this user, storing the username in the session of the API server.

47 |
48 | } 49 | {user && 50 |
51 |

You are currently logged in as {user.name}.

52 | 53 |
54 | 55 |
56 |
57 | } 58 |
59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/aspects/module/data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { List, Record } from 'immutable'; 4 | import invariant from 'invariant'; 5 | 6 | import { register } from '../../theory/registry'; 7 | import { ADD_ENTRY, addEntry } from '../../commands/addEntry'; 8 | 9 | import type { Tm } from '../../theory/tm'; 10 | import type { EvaluationResult } from '../../theory/evaluation'; 11 | import type Edit, { Action } from '../../theory/edit'; 12 | 13 | 14 | export const MODULE_PUBLIC = 'module/MODULE_PUBLIC'; 15 | export const MODULE_PRIVATE = 'module/MODULE_PRIVATE'; 16 | // idris also has abstract, though i'm not sure when it's useful 17 | 18 | 19 | export type Visibility = MODULE_PUBLIC | MODULE_PRIVATE; 20 | 21 | 22 | const ModuleShape = Record({ 23 | name: null, // string 24 | contents: null, // List 25 | scratch: null, // Note | Definition | Property | Example 26 | }, 'module'); 27 | 28 | 29 | export default class Module extends ModuleShape { 30 | step(): EvaluationResult { 31 | throw new Error('unimplemented: Module.step'); 32 | } 33 | 34 | subst(): Tm { 35 | throw new Error('unimplemented: Module.subst'); 36 | } 37 | 38 | unify(): ?Tm { 39 | throw new Error('unimplemented: Module.unify'); 40 | } 41 | 42 | actions(): List { 43 | return List([addEntry]); 44 | } 45 | 46 | performEdit(id: string): Edit { 47 | invariant( 48 | id === ADD_ENTRY, 49 | 'Module only konws ADD_ENTRY' 50 | ); 51 | 52 | throw new Error('Module.performEdit -- not implemented yet'); 53 | } 54 | } 55 | 56 | export class Note extends Record({ 57 | name: null, // string; 58 | defn: null, // string; 59 | }, 'note') {} 60 | 61 | export class Definition extends Record({ 62 | name: null, // string; 63 | defn: null, // Tm; 64 | visibility: null, // Visibility; 65 | }, 'definition') {} 66 | 67 | export class Property extends Record({ 68 | name: null, // string; 69 | defn: null, // Tm; 70 | }, 'property') {} 71 | 72 | export class Example extends Record({ 73 | name: null, // string; 74 | defn: null, // Tm; 75 | }, 'example') {} 76 | 77 | register('module', Module); 78 | -------------------------------------------------------------------------------- /src/helpers/Html.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import ReactDOM from 'react-dom/server'; 3 | import { writer } from '../redux/transit'; 4 | import DocumentMeta from 'react-document-meta'; 5 | 6 | /** 7 | * Wrapper component containing HTML metadata and boilerplate tags. 8 | * Used in server-side code only to wrap the string output of the 9 | * rendered route component. 10 | * 11 | * The only thing this component doesn't (and can't) include is the 12 | * HTML doctype declaration, which is added to the rendered output 13 | * by the server.js file. 14 | */ 15 | export default class Html extends Component { 16 | static propTypes = { 17 | assets: PropTypes.object, 18 | component: PropTypes.object, 19 | store: PropTypes.object 20 | } 21 | 22 | render() { 23 | const {assets, component, store} = this.props; 24 | return ( 25 | 26 | 27 | 28 | {/* Auth0 asked to include this viewport */} 29 | 30 | {DocumentMeta.renderAsReact()} 31 | 32 | 33 | 34 | 35 | 36 | {/* styles (will be present only in production with webpack extract text plugin) */} 37 | {Object.keys(assets.styles).map((style, key) => 38 | 40 | )} 41 | 42 | 43 |
44 |