{
- "ecmaFeatures": {
- "jsx": true,
- "modules": true
- },
"env": {
- "browser": true,
- "node": true
+ "browser": true
},
"parser": "babel-eslint",
- "rules": {
- "quotes": [2, "single"],
- "strict": [2, "never"],
- "react/jsx-uses-react": 2,
- "react/jsx-uses-vars": 2,
- "react/react-in-jsx-scope": 2
- },
"plugins": [
"react"
- ]
+ ],
+ "rules": {
+ // key: 0 = allow, 1 = warn, 2 = error
+ "comma-dangle": [0, "always-multiline"],
+ "no-process-exit": [0],
+ "no-underscore-dangle": [0],
+ "no-use-before-define": [0],
+ "quotes": [1, "single", "avoid-escape"],
+ "strict": [2, "global"]
+ }
}
The MIT License (MIT)
-Copyright (c) 2014 Dan Abramov
+Copyright (c) 2015-present Greg Hurrell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+// Copyright 2015-present Greg Hurrell. All rights reserved.
+// Licensed under the terms of the MIT license.
+
import WebpackDevServer from 'webpack-dev-server';
import config from './webpack.config.js';
+import eslint from 'gulp-eslint';
+import flow from 'gulp-flowtype';
import gulp from 'gulp';
import gutil from 'gulp-util';
+import mocha from 'gulp-spawn-mocha';
import webpack from 'webpack';
+let watching = false;
+
+/**
+ * Ring the terminal bell.
+ */
+function ringBell() {
+ process.stderr.write('\x07');
+}
+
+/**
+ * Wrap a stream in an error-handler (until Gulp 4, needed to prevent "watch"
+ * task from dying on error).
+ */
+function wrap(stream) {
+ stream.on('error', error => {
+ gutil.log(gutil.colors.red(error.message));
+ gutil.log(error.stack);
+ if (watching) {
+ gutil.log(gutil.colors.yellow('[aborting]'));
+ stream.end();
+ } else {
+ gutil.log(gutil.colors.yellow('[exiting]'));
+ process.exit(1);
+ }
+ ringBell();
+ });
+ return stream;
+}
+
gulp.task('default', ['webpack-dev-server']);
gulp.task('build', ['webpack:build']);
+gulp.task('flow', ['typecheck']);
+
+gulp.task('js', ['build', 'lint', 'test', 'typecheck']);
+
+gulp.task('lint', () => (
+ gulp.src('src/**/*.js')
+ .pipe(eslint())
+ .pipe(eslint.format())
+));
+
+gulp.task('typecheck', () => {
+ return gulp.src('src/**/*.js', {read: false})
+ .pipe(gutil.noop());
+
+ // TODO: enable this once Flow groks ES2015/ES2016 features.
+ return gulp.src('src/**/*.js')
+ .pipe(wrap(flow()))
+});
+
+gulp.task('test', () => (
+ gulp.src(
+ [
+ 'src/**/__mocks__/*.js',
+ 'src/**/__tests__/*-test.js',
+ ],
+ {read: false}
+ )
+ .pipe(wrap(mocha({
+ opts: 'mocha/mocha.opts',
+ reporter: watching ? 'mocha/watch-reporter' : 'list',
+ })))
+));
+
+gulp.task('watch', () => {
+ watching = true;
+ gulp.watch('src/**/*.js', ['js']);
+});
+
gulp.task('webpack:build', callback => {
const myConfig = {
...config,
webpack(myConfig, (err, stats) => {
if (err) {
+ ringBell();
throw new gutil.PluginError('webpack:build', err);
}
- gutil.log('[webpack:build]', stats.toString({
- colors: true
- }));
+ if (stats.compilation.errors) {
+ ringBell();
+ }
+ if (!watching) {
+ gutil.log('[webpack:build]', stats.toString({
+ colors: true
+ }));
+ }
callback();
});
});
gulp.task('webpack-dev-server', callback => {
- var myConfig = {
+ const myConfig = {
...config,
debug: true,
};
},
}).listen(3000, 'localhost', err => {
if (err) {
+ ringBell();
throw new gutil.PluginError('webpack-dev-server', err);
}
gutil.log('[webpack-dev-server]', 'http://localhost:3000');
<html>
<head>
- <title>Sample App</title>
+ <title>Hextrapolate</title>
</head>
<body>
<div id='root'>
"eslint-plugin-react": "^2.3.0",
"expect": "^1.8.0",
"gulp": "^3.9.0",
+ "gulp-eslint": "^0.15.0",
+ "gulp-flowtype": "^0.4.7",
+ "gulp-spawn-mocha": "^2.2.1",
"gulp-util": "^3.0.6",
"mocha": "^2.2.5",
"react-hot-loader": "^1.2.7",
-import React, { Component } from 'react';
+/**
+ * Copyright 2015-present Greg Hurrell. All rights reserved.
+ * Licensed under the terms of the MIT license.
+ *
+ * @flow
+ */
+
+'use strict';
+
+import React from 'react';
+import Field from './Field.react';
+
+export default class App extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ value: '0',
+ };
+ }
+
+ _onValueChange = (value: string) => {
+ this.setState({value});
+ }
-export default class App extends Component {
render() {
return (
- <h1>Hello, world.</h1>
+ <div>
+ <h1>Hextrapolate</h1>
+ <label>
+ Hexadecimal
+ <Field
+ base={16}
+ onValueChange={this._onValueChange}
+ value={this.state.value}
+ />
+ </label>
+ <label>
+ Decimal
+ <Field
+ onValueChange={this._onValueChange}
+ value={this.state.value}
+ />
+ </label>
+ <label>
+ Octal
+ <Field
+ base={8}
+ onValueChange={this._onValueChange}
+ value={this.state.value}
+ />
+ </label>
+ <label>
+ Binary
+ <Field
+ base={2}
+ onValueChange={this._onValueChange}
+ value={this.state.value}
+ />
+ </label>
+ </div>
);
}
}
--- /dev/null
+/**
+ * Copyright 2015-present Greg Hurrell. All rights reserved.
+ * Licensed under the terms of the MIT license.
+ *
+ * @flow
+ */
+
+'use strict';
+
+import React from 'react';
+
+const DIGITS = '0123456789abcdef';
+
+/**
+ * Convert from canonical (hexadecimal) value to `base`.
+ */
+function fromValue(value: string, base: number): string {
+ if (base === 16) {
+ return value;
+ }
+ for (let i = 0; i < value.length - 1; i++) {
+ const digit = value[value.length - 1 - i]; // eslint-disable-line no-unused-vars
+ // TODO: finish this
+ }
+ return value;
+}
+
+/**
+ * Convert from `base` value to canonical (hexadecimal) value.
+ */
+function toValue(value: string, base: number): string {
+ if (base === 16) {
+ return value;
+ }
+ return value;
+}
+
+export default class Field extends React.Component {
+ static propTypes = {
+ base: React.PropTypes.number,
+ onValueChange: React.PropTypes.func.required,
+ value: React.PropTypes.string.required,
+ };
+ static defaultProps = {
+ base: 10,
+ };
+
+ constructor(props) {
+ super(props);
+ if (props.base < 2 || props.base > DIGITS.length) {
+ throw new Error(
+ `base prop must be between 2..${DIGITS.length}`
+ );
+ }
+ }
+
+ _isValid(value: string): boolean {
+ const validator = new RegExp(
+ `^[${DIGITS.slice(0, this.props.base)}]*$`
+ );
+ return validator.test(value.trim().toLowerCase());
+ }
+
+ _onChange = event => {
+ const value = event.currentTarget.value;
+ if (this._isValid(value)) {
+ this.props.onValueChange(toValue(value, this.props.base));
+ }
+ }
+
+ render() {
+ return (
+ <input
+ onChange={this._onChange}
+ type="text"
+ value={fromValue(this.props.value, this.props.base)}
+ />
+ );
+ }
+}
--- /dev/null
+{
+ "env": {
+ "jasmine": true,
+ "node": true
+ },
+ "globals": {
+ "sinon": false
+ }
+}
--- /dev/null
+{
+ "env": {
+ "jasmine": true,
+ "node": true
+ },
+ "globals": {
+ "sinon": false
+ }
+}
+++ /dev/null
-// TODO: replace with actual test files
* @flow
*/
+'use strict';
+
/**
* Strips the leading prefix from `number` in `base` and returns the remaining
* part of the string.
+/**
+ * Copyright 2015-present Greg Hurrell. All rights reserved.
+ * Licensed under the terms of the MIT license.
+ *
+ * @flow
+ */
+
+'use strict';
+
import React from 'react';
import App from './App';
+/**
+ * Copyright 2015-present Greg Hurrell. All rights reserved.
+ * Licensed under the terms of the MIT license.
+ */
+
+'use strict';
+
var path = require('path');
var webpack = require('webpack');