Support of help and clear commands. Add view models

This commit is contained in:
Borys_Levytskyi
2016-11-20 23:01:20 +02:00
parent 3144cf2418
commit e35b87f668
14 changed files with 570 additions and 80 deletions

View File

@@ -19,7 +19,9 @@
"devDependencies": { "devDependencies": {
"babel-cli": "^6.18.0", "babel-cli": "^6.18.0",
"babel-plugin-syntax-jsx": "^6.18.0", "babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-class-properties": "^6.19.0",
"babel-plugin-transform-react-jsx": "^6.8.0", "babel-plugin-transform-react-jsx": "^6.8.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0", "babel-preset-react": "^6.16.0",
"grunt-contrib-clean": "latest", "grunt-contrib-clean": "latest",
"grunt-contrib-copy": "latest", "grunt-contrib-copy": "latest",

View File

@@ -6,12 +6,13 @@ app.set('cmd', function() {
var cmdController = app.controller('cmdController'); var cmdController = app.controller('cmdController');
return { return {
debugMode: true,
execute: function(rawInput) { execute: function(rawInput) {
var input = rawInput.trim().toLowerCase(); var input = rawInput.trim().toLowerCase();
var handler = findHandler(input); var handler = findHandler(input);
if(handler != null) { if(handler != null) {
if(app.debugMode) { if(this.debugMode) {
invokeHandler(input, handler); invokeHandler(input, handler);
} else { } else {
try { try {
@@ -52,13 +53,12 @@ app.set('cmd', function() {
handlers.push(h); handlers.push(h);
}, },
clear: function() { clear: function() {
cmdController.clear(); console.error('[displayCommandError] not implemented');
} }
}; };
function displayCommandError(input, message) { function displayCommandError(input, message) {
var error = new app.models.ErrorResult(message); console.error('[displayCommandError] not implemented');
cmdController.display(new app.models.DisplayResult(input, error));
} }
function invokeHandler (input, handler) { function invokeHandler (input, handler) {

View File

@@ -6,42 +6,6 @@ app.run(function() {
var rootView = app.get('rootView'); var rootView = app.get('rootView');
var expression = app.get('expression'); var expression = app.get('expression');
cmd.commands({
'help': function() {
var helpResult = document.querySelector('.result .helpResultTpl');
if(helpResult != null) {
moveResultUp(helpResult);
return;
}
return new app.models.ViewResult('helpResultTpl');
},
'clear': function() {
cmd.clear();
},
'em': function() {
cmdConfig.emphasizeBytes = !cmdConfig.emphasizeBytes;
},
'dark': function() {
cmdConfig.theme = 'dark';
},
light: function () {
cmdConfig.theme = 'light';
},
about: function() {
var aboutResult = document.querySelector('.result .aboutTpl');
if(aboutResult != null) {
moveResultUp(aboutResult);
return;
}
return new app.models.ViewResult('aboutTpl');
},
'-debug': function() {
app.debugMode = true;
console.log('debug is on');
},
'-notrack': function () {}
});
// TODO: Make as function // TODO: Make as function
cmd.command({ cmd.command({
canHandle: function(input) { return app.get('expression').canParse(input); }, canHandle: function(input) { return app.get('expression').canParse(input); },

35
src/app/appState.js Normal file
View File

@@ -0,0 +1,35 @@
class AppState {
constructor() {
this.emphasizeBytes = true;
this.commandResults = [];
this.handlers = [];
}
addCommandResult(result) {
this.commandResults.unshift(result);
this.triggerChanged();
}
clearCommmandResults() {
this.commandResults = [];
this.triggerChanged();
}
toggleEmphasizeBytes() {
this.emphasizeBytes = !this.emphasizeBytes;
this.triggerChanged();
}
onChange(handler) {
this.handlers.push(handler);
}
triggerChanged() {
for(var h of this.handlers) {
h();
}
}
};
var appState = new AppState();
export default appState;

91
src/app/cmd.js Normal file
View File

@@ -0,0 +1,91 @@
import is from './is';
var handlers = [];
var cmd = {
debugMode: true,
execute: function(rawInput) {
var input = rawInput.trim().toLowerCase();
var handler = findHandler(input);
if(handler != null) {
if(this.debugMode) {
invokeHandler(input, handler);
} else {
try {
invokeHandler(input, handler);
} catch (e) {
displayCommandError(input, "Error: " + e);
}
}
}
else {
displayCommandError(input, "Unsupported expression: " + input.trim());
}
},
commands: function(catalog) {
for(var key in catalog) {
if(catalog.hasOwnProperty(key)) {
this.command(key, catalog[key]);
}
}
},
command: function(cmd, handler) {
var h = createHandler(cmd, handler);
if(h == null){
console.warn('unexpected set of arguments: ', Array.prototype.splice.call(arguments));
return;
}
if(!is.aFunction(h.canHandle)) {
console.warn('handler is missing "canHandle" function. registration denied.');
return;
}
if(!is.aFunction(h.handle)) {
console.warn('handler is missing "handle" function. registration denied.');
return;
}
handlers.push(h);
},
clear: function() {
console.log('clear');
}
};
function displayCommandError(input, message) {
var error = new app.models.ErrorResult(message);
cmdController.display(new app.models.DisplayResult(input, error));
}
function invokeHandler (input, handler) {
console.log('[invokeHandler]: ' + input);
var cmdResult = handler.handle(input);
if(cmdResult != null) {
console.log(cmdResult);
}
}
function createHandler (cmd, handler) {
if(is.plainObject(cmd)) {
return cmd;
}
if(is.string(cmd)) {
return { canHandle: function (input) { return input === cmd; }, handle: handler };
}
return null;
}
function findHandler (input) {
var i= 0;
for(i;i<handlers.length; i++) {
if(handlers[i].canHandle(input)) {
return handlers[i];
}
}
};
export default cmd;

44
src/app/commands.js Normal file
View File

@@ -0,0 +1,44 @@
import appState from './appState';
import * as result from './result/result';
var cmdConfig = {};
export default {
initialize (cmd) {
cmd.commands({
'help': function() {
var helpResult = document.querySelector('.result .helpResultTpl');
if(helpResult != null) {
moveResultUp(helpResult);
return;
}
appState.addCommandResult(new result.HelpResult());
},
'clear': function() {
appState.clearCommmandResults();
},
'em': function() {
cmdConfig.emphasizeBytes = !cmdConfig.emphasizeBytes;
},
'dark': function() {
cmdConfig.theme = 'dark';
},
'light': function () {
cmdConfig.theme = 'light';
},
'about': function() {
var aboutResult = document.querySelector('.result .aboutTpl');
if(aboutResult != null) {
moveResultUp(aboutResult);
return;
}
return new app.models.ViewResult('aboutTpl');
},
'-debug': function() {
app.debugMode = true;
console.log('debug is on');
},
'-notrack': function () {}
});
}
}

View File

@@ -0,0 +1,55 @@
import React from 'react';
import InputBox from './InputBox';
import * as results from '../result/result';
import HelpView from './HelpView';
export default class AppRoot extends React.Component {
componentWillMount() {
this.refresh();
this.props.appState.onChange(() => this.refresh());
}
refresh() {
console.log(this.props.appState.commandResults);
this.setState(this.props.appState);
}
render() {
var results = this.state.commandResults.map((r, i) => this.findResultComponent(r, i));
return <div>
<div className="header">
<h1>Bitwise<span style={{color: "#c5c5c5"}}>Cmd</span></h1>
<div>State: <span>{JSON.stringify(this.state)}</span></div>
<ul className="top-links">
<li>
<a href="https://github.com/BorisLevitskiy/BitwiseCmd"><i className="icon github">&nbsp;</i><span className="link-text">Project on GitHub</span></a>
</li>
<li>
<a href="https://twitter.com/BitwiseCmd"><i className="icon twitter">&nbsp;</i><span className="link-text">Twitter</span></a>
</li>
<li>
<a href="mailto:&#098;&#105;&#116;&#119;&#105;&#115;&#101;&#099;&#109;&#100;&#064;&#103;&#109;&#097;&#105;&#108;&#046;&#099;&#111;&#109;?subject=Feedback"><i className="icon feedback">&nbsp;</i><span className="link-text">Send Feedback</span></a>
</li>
</ul>
</div>
<div className="expressionInput-container">
<InputBox />
<span className="configPnl">
<span id="emphasizeBytes" data-cmd="em" className="indicator on" title="Emphasize Bytes">[em]</span>
</span>
</div>
<div id="output">
{results}
</div>
</div>;
}
findResultComponent(result, key) {
if(result instanceof results.HelpResult) {
return <HelpView key={key} />
}
return <span>Unknown result {typeof result}</span>
}
}

View File

@@ -0,0 +1,45 @@
import React from 'react'
export default class HelpView extends React.Component {
render() {
return <div className="help helpResultTpl">
<div style={{overflow: "hidden"}}>
<div style={{float: "left", "marginRight": "20px"}}>
<p className="section">
<strong>Supported Commands</strong>
<ul>
<li><code>23 ^ 34</code> type bitwise expression to see result in binary (only positive integers are supported now)</li>
<li><code>23 34</code> type one or more numbers to see their binary representations</li>
<li><code>clear</code> clear output pane</li>
<li><code>help</code> display this help</li>
<li><code>em</code> turn On/Off Emphasize Bytes</li>
<li><code>dark</code> set Dark theme</li>
<li><code>light</code> set Light theme</li>
<li><code>about</code> about the app</li>
</ul>
</p>
</div>
<div style={{"float":"left"}}>
<p className="section">
<strong>Supported Bitwise Operations</strong><br/>
<small>
<a href="https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators">
as implemented in JavaScript engine
</a>
</small>
<ul>
<li><code>&amp;</code> bitwise AND</li>
<li><code>|</code> bitwise inclusive OR</li>
<li><code>^</code> bitwise exclusive XOR</li>
<li><code>~</code> bitwise NOT</li>
<li><code>&lt;&lt;</code> left shift</li>
<li><code>&gt;&gt;</code> sign propagating right shift</li>
<li><code>&gt;&gt;&gt;</code> zero-fill right shift</li>
</ul>
</p>
</div>
</div>
</div>;
}
}

View File

@@ -1,8 +1,13 @@
import React from 'react'; import React from 'react';
import cmd from '../cmd';
export default class InputBox extends React.Component { export default class InputBox extends React.Component {
history = []; constructor() {
historyIndex = 0; super();
this.history = [];
this.historyIndex = 0;
}
render() { render() {
return <input type="text" return <input type="text"
onKeyUp={e => this.onKeyUp(e)} onKeyUp={e => this.onKeyUp(e)}
@@ -18,10 +23,10 @@ export default class InputBox extends React.Component {
} }
var value = input.value; var value = input.value;
console.log(value);
this.history.unshift(value); this.history.unshift(value);
input.value = '';
console.log(this.history); input.value = '';
cmd.execute(value);
} }
onKeyDown(args) { onKeyDown(args) {

237
src/app/expression.js Normal file
View File

@@ -0,0 +1,237 @@
var expression = {
factories:[],
canParse: function(string) {
var trimmed = string.replace(/^\s+|\s+$/, '');
var i = this.factories.length-1;
for(;i>=0;i--) {
if(this.factories[i].canCreate(trimmed) === true){
return true;
}
}
return false;
},
parse: function(string) {
var trimmed = string.replace(/^\s+|\s+$/, '');
var i = 0, l = this.factories.length, factory;
for(;i<l;i++) {
factory = this.factories[i];
if(factory.canCreate(trimmed) == true){
return factory.create(trimmed);
}
}
return null;
},
parseOperand: function(input) {
return new Operand(input);
},
createOperand: function(number, kind) {
return Operand.create(number, kind);
},
addFactory: function(factory) {
this.factories.push(factory);
},
Operand:Operand,
TwoOperandExpression: TwoOperandExpression,
SingleOperandExpression: SingleOperandExpression,
ListOfNumbersExpression: ListOfNumbersExpression,
MultipleOperandsExpression: MultipleOperandsExpression
};
// List of numbers
expression.addFactory({
regex: /^(-?(?:\d+|0x[\d,a-f]+)\s?)+$/,
canCreate: function(string) {
return this.regex.test(string);
},
create: function (string) {
var matches = this.regex.exec(string),
numbers = [],
input = matches.input;
input.split(' ').forEach(function(n){
if(n.trim().length > 0) {
numbers.push(new Operand(n.trim()));
}
});
return new ListOfNumbersExpression(input, numbers);
}
});
// Not Expression
expression.addFactory({
regex: /^(~)(-?(?:\d+|0x[\d,a-f]+))$/,
canCreate: function(string) {
return this.regex.test(string);
},
create: function (string) {
var matches = this.regex.exec(string),
operand = new Operand(matches[2]);
return new SingleOperandExpression(matches.input, operand, matches[1]);
}
});
// Multiple operands expression
expression.addFactory({
fullRegex: /^((<<|>>|>>>|\||\&|\^)?(-?((?:\d+(?!x))|(?:0x[\d,a-f]+))))+$/,
regex: /(<<|>>|>>>|\||\&|\^)?(-?((?:\d+(?!x))|(?:0x[\d,a-f]+)))/g,
canCreate: function(string) {
this.fullRegex.lastIndex = 0;
return this.fullRegex.test(this.normalizeString(string));
},
create: function (string) {
var m, operands = [],
normalizedString = this.normalizeString(string);
while ((m = this.regex.exec(normalizedString)) != null) {
operands.push(this.parseMatch(m));
}
return new MultipleOperandsExpression(normalizedString, operands)
},
parseMatch: function (m) {
var input = m[0],
sign = m[1],
num = m[2];
if(sign == null) {
return new Operand(num);
} else {
return new SingleOperandExpression(input, new Operand(num), sign);
}
},
normalizeString: function (string) {
return string.replace(/\s+/g,'');
}
});
export class Operand {
constructor(input) {
this.input = input;
this.value = parseInt(input);
this.hex = Operand.toHexString(this.value.toString(16));
this.dec = this.value.toString(10);
// >>> 0 makes negative numbers like -1 to be displayed as '11111111111111111111111111111111' in binary instead of -1
this.bin = this.value < 0 ? (this.value >>> 0).toString(2) : this.value.toString(2);
this.kind = this.input.indexOf('0x') > -1 ? 'hex' : 'dec';
this.other = this.kind == 'dec' ? this.hex : this.dec;
}
toHexString (hex) {
return hex.indexOf('-') == 0 ? '-0x' + hex.substr(1) : '0x' + hex;
};
getLengthInBits() {
if(this.value < 0) {
return 32;
}
return Math.floor(Math.log(this.value) / Math.log(2)) + 1;
};
toKindString(value, kind) {
switch(kind) {
case 'hex':
var hexVal = Math.abs(value).toString(16);
return value >= 0 ? '0x' + hexVal : '-0x' + hexVal;
case 'bin':
return (value>>>0).toString(2);
case 'dec':
return value.toString(10);
default:
throw new Error("Unexpected kind: " + kind)
}
};
getOtherKind(kind) {
switch(kind) {
case 'dec': return 'hex';
case 'hex': return 'dec';
default : throw new Error(kind + " kind doesn't have opposite kind")
}
};
static getBase(kind){
switch (kind){
case 'bin': return 2;
case 'hex': return 16;
case 'dec': return 10;
}
};
static create(number, kind) {
var str = number.toString(Operand.getBase(kind));
if(kind == 'hex') {
str = Operand.toHexString(str);
}
return new Operand(str);
};
toString() {
return this.input;
}
}
export class SingleOperandExpression {
constructor(expressionString, operand, sign) {
this.expressionString = expressionString;
this.operand1 = operand;
this.sign = sign;
}
apply(value) {
var str = '';
if(this.sign == '~'){
str = '~' + this.operand1.value;
} else {
str = value + this.sign + this.operand1.value
}
return Operand.create(eval(str), this.operand1.kind);
};
isShiftExpression() {
return this.sign.indexOf('<') >= 0 || this.sign.indexOf('>')>= 0;
};
toString() {
return this.sign + this.operand1.toString();
}
}
export class TwoOperandExpression {
constructor(expressionString, operand1, operand2, sign) {
this.expressionString = expressionString;
this.operand1 = operand1;
this.operand2 = operand2;
this.sign = sign;
}
}
export class MultipleOperandsExpression {
constructor(expressionString, expressions) {
this.expressionString = expressionString;
this.expressions = expressions;
}
}
export class ListOfNumbersExpression {
constructor(expressionString, numbers) {
this.expressionString = expressionString;
this.numbers = numbers;
}
}
export class Expression {
toString() {
return this.expressionString ? "Expression: " + this.expressionString : this.toString();
};
}
export default expression;

View File

@@ -1,35 +1,12 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import InputBox from './components/InputBox.jsx'; import InputBox from './components/InputBox.jsx';
var rootView = <div> import appState from './appState';
<div className="header"> import cmd from './cmd';
<h1>Bitwise<span style={{color: "#c5c5c5"}}>Cmd</span></h1> import commands from './commands';
import AppRoot from './components/AppRoot';
<ul className="top-links"> commands.initialize(cmd);
<li>
<a href="https://github.com/BorisLevitskiy/BitwiseCmd"><i className="icon github">&nbsp;</i><span className="link-text">Project on GitHub</span></a>
</li>
<li>
<a href="https://twitter.com/BitwiseCmd"><i className="icon twitter">&nbsp;</i><span className="link-text">Twitter</span></a>
</li>
<li>
<a href="mailto:&#098;&#105;&#116;&#119;&#105;&#115;&#101;&#099;&#109;&#100;&#064;&#103;&#109;&#097;&#105;&#108;&#046;&#099;&#111;&#109;?subject=Feedback"><i className="icon feedback">&nbsp;</i><span className="link-text">Send Feedback</span></a>
</li>
</ul>
</div>
<div className="expressionInput-container"> var root = <AppRoot appState={appState} />;
<InputBox /> ReactDOM.render(root, document.getElementById('root'));
<span className="configPnl">
<span id="emphasizeBytes" data-cmd="em" className="indicator on" title="Emphasize Bytes">[em]</span>
</span>
</div>
<div id="output">
</div>
</div>;
ReactDOM.render(rootView, document.getElementById('root'));

33
src/app/is.js Normal file
View File

@@ -0,0 +1,33 @@
export default {
plainObject: function(obj) {
return typeof obj == "object" && obj instanceof Object;
},
aFunction: function (obj) {
return typeof obj == "function";
},
string: function (obj) {
return typeof obj == "string";
},
regex: function (obj) {
return typeof obj == "object" && this.constructedFrom(RegExp);
},
constructedFrom: function (obj, ctor) {
return obj instanceof ctor;
},
htmlElement: function(obj) {
return obj instanceof HtmlElement;
},
array: function(obj) {
return obj instanceof Array;
},
number: function(num) {
return typeof num == "number" && !isNaN(num)
}
}

2
src/app/result/result.js Normal file
View File

@@ -0,0 +1,2 @@
export class HelpResult {
}

View File

@@ -8,14 +8,14 @@ module.exports = {
devtool: 'source-maps', devtool: 'source-maps',
module: { module: {
loaders: [{ loaders: [{
test: /\.jsx$/, test: /\.jsx?$/,
loader: 'babel', loader: 'babel',
//exclude: /node-modules/, exclude: /node-modules/,
output: { path: __dirname + '/src', filename: 'bundle.js' }, output: { path: __dirname + '/src', filename: 'bundle.js' },
query: { query: {
presets: ['es2015', 'react'], presets: [require.resolve('babel-preset-es2015'), require.resolve('babel-preset-react')],
plugins: ['transform-class-properties'] plugins: [require.resolve('babel-plugin-transform-class-properties')]
} },
}] }]
}, },
// externals: { // externals: {