diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..bec2ed0 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + "env": { + "browser": true, + "es6": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "rules": { + "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }] + } +}; \ No newline at end of file diff --git a/.gitignore b/.gitignore index 18aead6..bae80c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .idea/ *.iml -.DS_Store \ No newline at end of file +.DS_Store +dist/ +node_modules/ \ No newline at end of file diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/cypress.json @@ -0,0 +1 @@ +{} diff --git a/cypress/.eslintrc.json b/cypress/.eslintrc.json new file mode 100644 index 0000000..9bf341d --- /dev/null +++ b/cypress/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "plugins": [ + "cypress" + ], + "extends": [ + "plugin:cypress/recommended" + ] +} \ No newline at end of file diff --git a/cypress/integration/console.spec.js b/cypress/integration/console.spec.js new file mode 100644 index 0000000..180e270 --- /dev/null +++ b/cypress/integration/console.spec.js @@ -0,0 +1,10 @@ +describe('draw a vector', () => { + it('adds the svg vector', () => { + cy.visit('http://localhost:8080'); + + cy.get('#command_input').type("a = vector(0,0,1,1){enter}"); + cy.get('#0').invoke('attr','d').should('eq','M550 350 L650 250'); + cy.get('#0').invoke('attr','class').should('eq','vector'); + cy.get('#0').invoke('attr','marker-end').should('eq','url(#arrow)'); + }) +}) diff --git a/dist/bundle.js b/dist/bundle.js new file mode 100644 index 0000000..1623590 --- /dev/null +++ b/dist/bundle.js @@ -0,0 +1,115 @@ +/* + * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). + * This devtool is neither made for production nor for readable output files. + * It uses "eval()" calls to create a separate source file in the browser devtools. + * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) + * or disable the default devtool with "devtool: false". + * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). + */ +/******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ({ + +/***/ "./src/js/console.js": +/*!***************************!*\ + !*** ./src/js/console.js ***! + \***************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"adjust_input_element_height\": () => (/* binding */ adjust_input_element_height)\n/* harmony export */ });\n/* harmony import */ var _scanner__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./scanner */ \"./src/js/scanner.js\");\n/* harmony import */ var _parser__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./parser */ \"./src/js/parser.js\");\n/* harmony import */ var _index__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./index */ \"./src/js/index.js\");\n\n\n\n\n/**\n * handles user input from the console div\n */\nconst state = {};\nconst command_input_element = document.getElementById('command_input');\nconst command_history_element = document.getElementById('command_history');\ncommand_input_element.value = '';\nlet command_history = [''];\nlet command_history_index = 0;\n\nconst adjust_input_element_height = function () {\n let num_lines = command_input_element.value.split(/\\n/).length;\n command_input_element.setAttribute('style', 'height: ' + num_lines + 'em');\n if (num_lines > 1) {\n command_input_element.setAttribute('class', 'multiline');\n } else {\n command_input_element.setAttribute('class', 'single_line');\n }\n}\n\ncommand_input_element.onkeyup = function handle_key_input(event) {\n adjust_input_element_height();\n if (event.key === 'ArrowUp') {\n if (command_history_index > -1) {\n command_input_element.value = command_history[command_history_index];\n if (command_history_index > 0) {\n command_history_index -= 1;\n }\n }\n }\n if (event.key === 'ArrowDown') {\n if (command_history_index < command_history.length - 1) {\n command_history_index += 1;\n command_input_element.value = command_history[command_history_index];\n } else {\n command_input_element.value = '';\n }\n }\n if (event.key === 'Enter') {\n let commands = command_input_element.value;\n command_input_element.value = '';\n adjust_input_element_height();\n let command_array = commands.split(/\\n/);\n for (let i = 0; i < command_array.length; i++) {\n let command = command_array[i];\n if (command.length > 0) {\n command_history_element.innerText += command + \"\\n\";\n command_input_element.value = '';\n command_history_index = command_history.length;\n let tokens = (0,_scanner__WEBPACK_IMPORTED_MODULE_0__.scan)(command);\n let statement = (0,_parser__WEBPACK_IMPORTED_MODULE_1__.parse)(tokens);\n let result;\n try {\n result = visit_expression(statement);\n if (result.description) {\n result = result.description;\n }\n } catch (e) {\n result = e.message;\n }\n command_history_element.innerText += result + \"\\n\";\n command_history.push(command);\n command_history_element.scrollTo(0, command_history_element.scrollHeight);\n }\n }\n }\n};\n\nlet visit_expression = function (expr) {\n switch (expr.type) {\n case 'declaration': {\n let value = visit_expression(expr.initializer);\n let existing_value = state[expr.var_name.value];\n if (existing_value) {\n if (existing_value.type === 'vector') {\n (0,_index__WEBPACK_IMPORTED_MODULE_2__.remove_vector)(existing_value.object); // remove from screen\n }\n }\n value.binding = expr.var_name.value;\n state[expr.var_name.value] = value;\n let description = state[expr.var_name.value].description;\n if (!description) {\n description = state[expr.var_name.value]; //questionable. use toString instead of message?\n }\n return {description: expr.var_name.value + ':' + description};\n }\n case 'group':\n return visit_expression(expr.expression);\n case 'unary': {\n let right_operand = visit_expression(expr.right);\n if (expr.operator === _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.MINUS) {\n return -right_operand;\n } else if (expr.operator === _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.NOT) {\n return !right_operand;\n } else {\n throw {message: 'illegal unary operator'};\n }\n }\n case 'binary': {\n let left = visit_expression(expr.left);\n let right = visit_expression(expr.right);\n switch (expr.operator) {\n case _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.MINUS:\n return left - right;\n case _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.PLUS:\n return addition(left, right);\n case _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.STAR:\n return multiplication(left, right);\n case _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.SLASH:\n return left / right;\n case _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.DOT:\n return method_call(left, expr.right);\n }\n throw {message: 'illegal binary operator'};\n }\n case 'identifier': {\n if (state[expr.name]) {\n return state[expr.name];\n } else {\n break;\n }\n }\n case 'literal':\n return expr.value;\n case 'call':\n return call(expr.name, expr.arguments);\n case 'lazy':\n console.log(expr.value);\n return visit_expression(expr.value);\n }\n}\n\nconst call = function (function_name, argument_exprs) {\n let arguments_list = [];\n for (let i = 0; i < argument_exprs.length; i++) {\n arguments_list.push(visit_expression(argument_exprs[i]));\n }\n if (functions[function_name]) {\n return functions[function_name](arguments_list);\n } else {\n let arg_list = '';\n for (let i = 0; i < argument_exprs.length; i++) {\n if (i > 0) {\n arg_list += ',';\n }\n arg_list += argument_exprs[i].value_type;\n }\n return 'unimplemented: ' + function_name + '(' + arg_list + ')';\n }\n}\n\nconst method_call = function (object_wrapper, method_or_property) {\n if (object_wrapper) {\n if (method_or_property.type === 'call') { // method\n if (typeof object_wrapper.object[method_or_property.name] !== 'function') {\n throw {message: `method ${method_or_property.name} not found on ${object_wrapper.type}`};\n }\n return object_wrapper.object[method_or_property.name].apply(object_wrapper, method_or_property.arguments);\n\n } else { // property\n if (!Object.prototype.hasOwnProperty.call(object_wrapper.object, method_or_property.name)) {\n throw {message: `property ${method_or_property.name} not found on ${object_wrapper.type}`};\n }\n return object_wrapper.object[method_or_property.name];\n }\n } else {\n throw {message: `not found: ${object_wrapper}`};\n }\n}\n\nconst functions = {\n help: () => help(),\n vector: (args) => (0,_index__WEBPACK_IMPORTED_MODULE_2__.add_vector)({x0: args[0], y0: args[1], x: args[2], y: args[3]}),\n remove: (args) => {\n if (Object.prototype.hasOwnProperty.call(args[0],'binding')){\n delete state[args[0].binding];\n return (0,_index__WEBPACK_IMPORTED_MODULE_2__.remove_vector)(args[0].object); // by binding value\n } else {\n return (0,_index__WEBPACK_IMPORTED_MODULE_2__.remove_vector)(args[0]); // by index (@...)\n }\n\n },\n}\n\nconst help = function () {\n return {\n description:\n `- vector(, , , ): draws a vector from x0,y0 to x,y\n - remove(|): removes an object, \n a ref is @n where n is the reference number asigned to the object`\n }\n}\n\nconst multiplication = function (left, right) {\n if (left.object && left.type === 'vector' && !right.object) {\n return left.object.multiply(right);\n }\n if (right.object && right.type === 'vector' && !left.object) {\n return right.object.multiply(left);\n }\n return left * right;\n}\n\nconst addition = function (left, right) {\n if (left.object && left.type === 'vector' && right.object && right.type === 'vector') {\n return left.object.add(right.object);\n }\n return left + right;\n}\n\n\n//# sourceURL=webpack://matrepl/./src/js/console.js?"); + +/***/ }), + +/***/ "./src/js/index.js": +/*!*************************!*\ + !*** ./src/js/index.js ***! + \*************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"add_vector\": () => (/* binding */ add_vector),\n/* harmony export */ \"remove_vector\": () => (/* binding */ remove_vector)\n/* harmony export */ });\n/* harmony import */ var _console_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./console.js */ \"./src/js/console.js\");\n/* harmony import */ var _scanner_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./scanner.js */ \"./src/js/scanner.js\");\n/* harmony import */ var _parser_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./parser.js */ \"./src/js/parser.js\");\n\n\n\n\nlet add_vector, remove_vector;\n\n/**\n * Main entry. draws the matrix\n */\nconst SVG_NS = 'http://www.w3.org/2000/svg'; // program needs these to create svg elements\nlet grid_size = 100; // this is the nr of pixels for the basis vector (1,0) (0,1)\nlet half_grid_size = grid_size >> 1; // used to position the grid lines\nlet vectors = []; // collection of added vectors\nlet moving_vector; // user can move vector arrows. when moving, this refers to the arrow\nlet width = window.innerWidth, height = window.innerHeight;\nlet origin_x = Math.floor((width / grid_size) / 2) * grid_size + half_grid_size,\n origin_y = Math.floor((height / grid_size) / 2) * grid_size + half_grid_size;\n/**\n * Creates an svg element\n * @param element_type path,g, etc\n * @returns SVG element\n */\nconst create = function (element_type) {\n return document.createElementNS(SVG_NS, element_type);\n}\n\n/**\n * creates the d attribute string\n * @param x0 start_x\n * @param y0 start_y\n * @param x1 end_x\n * @param y1 end y\n * @returns {string} to put in an SVG path\n */\nconst calculate_d = function (x0, y0, x1, y1) {\n return \"M\" + x0 + \" \" + y0 + \" L\" + x1 + \" \" + y1;\n}\n\n/**\n * creates a SVG line (path)\n * @param x0 start_x\n * @param y0 start_y\n * @param x1 end_x\n * @param y1 end_y\n * @param css_class the css class to make up the element\n * @returns an SVG path element\n */\nconst create_line = function (x0, y0, x1, y1, css_class) {\n let path = create('path');\n path.setAttribute('d', calculate_d(x0, y0, x1, y1));\n path.setAttribute('class', css_class);\n return path;\n}\n\n/**\n * creates the arrow path element\n * @param id attribute\n * @param x0 start_x\n * @param y0 start_y\n * @param x1 end_x\n * @param y1 end_y\n * @param css_class class attribute\n * @returns {SVGPathElement}\n */\nconst arrow = function (id, x0, y0, x1, y1, css_class) {\n let path = create('path');\n\n path.setAttribute('d', calculate_d(x0, y0, x1, y1));\n path.id = id;\n path.setAttribute('class', css_class);\n path.setAttribute('marker-end', 'url(#arrow)');\n return path;\n}\n\n/**\n * Draws the background grid of the space\n * @param css_class class for the lines that are 'multiples of the basis vector'\n * @param bg_css_class class for in between lines\n * @returns {SVGGElement}\n */\nconst create_grid = function (css_class, bg_css_class) {\n const group = create('g');\n group.setAttribute('id', 'grid');\n const horizontal = create('g');\n horizontal.setAttribute('id', 'horizontal');\n for (let y = 0; y < height; y += grid_size) {\n horizontal.appendChild(create_line(0, y + half_grid_size, width, y + half_grid_size, css_class));\n horizontal.appendChild(create_line(0, y, width, y, bg_css_class));\n }\n group.appendChild(horizontal);\n const vertical = create('g');\n vertical.setAttribute('id', 'vertical');\n for (let x = 0; x < width; x += grid_size) {\n vertical.appendChild(create_line(x + half_grid_size, 0, x + half_grid_size, height, css_class));\n vertical.appendChild(create_line(x, 0, x, height, bg_css_class));\n }\n group.appendChild(vertical);\n return group;\n}\n\n/**\n * removes child from element by id if found\n * @param element\n * @param child_id id to remove\n */\nconst remove_child = function (element, child_id) {\n let node = element.firstChild;\n while (node && child_id !== node.id) {\n node = node.nextSibling;\n }\n if (node) {\n element.removeChild(node);\n }\n}\n\n/**\n * removes the grid from the DOM and adds an updated one.\n */\nconst redraw_grid = function () {\n remove_child(svg, \"grid\");\n svg.appendChild(create_grid('grid', 'bg-grid'));\n svg.appendChild(create_axes());\n}\n\n/**\n * Adds a vector to the set.\n * @param vector\n */\nadd_vector = function (vector) {\n vector.id = vectors.length;\n vectors.push(vector);\n redraw();\n vector.add = (other) => add_vector({\n x0: vector.x0 + other.x0,\n y0: vector.x0 + other.x0,\n x: vector.x + other.x,\n y: vector.y + other.y\n });\n vector.multiply = (scalar) => add_vector({\n x0: vector.x0 * scalar,\n y0: vector.y0 * scalar,\n x: vector.x * scalar,\n y: vector.y * scalar\n });\n vector.is_vector = true;\n vector.type = () => 'vector';\n return { //object_wrapper\n type: 'vector',\n object: vector,\n description: `vector@${vector.id}{x0:${vector.x0},y0:${vector.y0} x:${vector.x},y:${vector.y}}`,\n };\n\n}\n\nremove_vector = function (vector_or_index) {\n let index;\n if (vector_or_index.is_vector) {\n for (let i = 0; i < vectors.length; i++) {\n if (vectors[i].id === vector_or_index.id) {\n index = i;\n break;\n }\n }\n } else {\n index = vector_or_index;\n }\n\n if (!vectors[index]) {\n throw {message: `vector@${index} not found`};\n }\n\n vectors.splice(index, 1);\n redraw();\n return {description: `vector@${index} removed`};\n}\n\n/**\n * The moving operation. Called by onmousemove on the svg ('canvas')\n * @param event\n */\nconst move_vector = function (event) {\n if (moving_vector) {\n let current_x = event.clientX;\n let current_y = event.clientY;\n vectors[moving_vector.id].x = (current_x - origin_x) / grid_size;\n vectors[moving_vector.id].y = (origin_y - current_y) / grid_size;\n moving_vector.setAttribute('d', calculate_d(origin_x, origin_y, current_x, current_y));\n }\n}\n\n/**\n * Draws all the vectors.\n *\n * vector {\n * x0,y0 origin\n * x,y coordinates\n * }\n */\nconst draw_vectors = function () {\n const vector_group = create(\"g\");\n vector_group.id = 'vectors';\n\n for (let i = 0; i < vectors.length; i++) {\n let vector_arrow = arrow(vectors[i].id,\n origin_x + vectors[i].x0 * grid_size,\n origin_y - vectors[i].y0 * grid_size,\n origin_x + vectors[i].x * grid_size,\n origin_y - vectors[i].y * grid_size,\n 'vector');\n vector_arrow.onmousedown = function start_moving_vector(event) {\n moving_vector = event.target;\n };\n vector_group.appendChild(vector_arrow);\n }\n svg.appendChild(vector_group);\n}\n\n/**\n * Removes all vectors in the svg and calls draw_vectors to draw updated versions.\n */\nconst redraw_vectors = function () {\n remove_child(svg, 'vectors');\n draw_vectors();\n}\n\n/**\n * (re)draws all\n */\nconst redraw = function () {\n redraw_grid();\n redraw_vectors();\n}\n\nconst create_axes = function () {\n let axes_group = create('g');\n let x = create_line(0, origin_y, width, origin_y, 'axis');\n x.id = 'x-axis';\n axes_group.appendChild(x);\n let y = create_line(origin_x, 0, origin_x, height, 'axis');\n y.id = 'y-axis';\n axes_group.appendChild(y);\n return axes_group;\n}\n\n/**\n * setup the arrow head for the vector\n * @returns {SVGDefsElement}\n */\nfunction create_defs() {\n let defs = create('defs');\n let marker = create('marker');\n marker.id = 'arrow';\n marker.setAttribute('orient', 'auto');\n marker.setAttribute('viewBox', '0 0 10 10');\n marker.setAttribute('markerWidth', '3');\n marker.setAttribute('markerHeight', '4');\n marker.setAttribute('markerUnits', 'strokeWidth');\n marker.setAttribute('refX', '6');\n marker.setAttribute('refY', '5');\n let polyline = create('polyline');\n polyline.setAttribute('points', '0,0 10,5 0,10 1,5');\n polyline.setAttribute('fill', 'yellow');\n marker.appendChild(polyline);\n defs.appendChild(marker);\n return defs;\n}\n\n/**\n * Creates the SVG\n * @returns {SVGElement}\n */\nconst create_svg = function () {\n let svg = create('svg');\n\n svg.onmousemove = move_vector();\n svg.onmouseup = function stop_moving_vector() {\n moving_vector = undefined;\n };\n\n let defs = create_defs();\n svg.appendChild(defs);\n return svg;\n}\n\ndocument.body.onresize = function recalculate_window_dimensions() {\n width = window.innerWidth;\n height = window.innerHeight;\n origin_x = Math.floor((width / grid_size) / 2) * grid_size + half_grid_size;\n origin_y = Math.floor((height / grid_size) / 2) * grid_size + half_grid_size;\n redraw();\n}\n\nconst svg = create_svg();\ndocument.body.appendChild(svg);\n\nsvg.appendChild(create_grid('grid', 'bg-grid'));\nsvg.appendChild(create_axes());\n\n\n//# sourceURL=webpack://matrepl/./src/js/index.js?"); + +/***/ }), + +/***/ "./src/js/parser.js": +/*!**************************!*\ + !*** ./src/js/parser.js ***! + \**************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"parse\": () => (/* binding */ parse)\n/* harmony export */ });\n/* harmony import */ var _scanner__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./scanner */ \"./src/js/scanner.js\");\n\n\n\nconst parse = function (tokens) {\n let token_index = 0;\n\n return statement();\n\n function statement() {\n if (check(_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.IDENTIFIER, token_index) && check(_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.EQUALS, token_index + 1)) {\n let var_name = current_token();\n advance();\n advance();\n return {type: 'declaration', var_name: var_name, initializer: expression()};\n } else {\n return expression();\n }\n }\n\n function expression() {\n return equality();\n }\n\n function equality() {\n let expr = comparison()\n\n while (match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.EQUALS_EQUALS, _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.NOT_EQUALS])) {\n let operator = previous_token();\n let right = unary();\n expr = {type: 'binary', left: expr, operator: operator, right: right};\n }\n\n return expr;\n }\n\n function comparison() {\n let expr = addition();\n\n while (match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.LESS, _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.LESS_OR_EQUAL, _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.GREATER, _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.GREATER_OR_EQUAL])) {\n let operator = previous_token();\n let right = addition();\n expr = {type: 'binary', left: expr, operator: operator, right: right};\n }\n\n return expr;\n }\n\n function addition() {\n let expr = multiplication();\n\n while (match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.MINUS, _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.PLUS])) {\n let operator = previous_token();\n let right = multiplication();\n expr = {type: 'binary', left: expr, operator: operator, right: right};\n }\n\n return expr;\n }\n\n function multiplication() {\n let expr = unary();\n\n while (match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.SLASH, _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.STAR, _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.DOT])) {\n let operator = previous_token();\n let right = unary();\n expr = {type: 'binary', left: expr, operator: operator, right: right};\n }\n\n return expr;\n }\n\n function unary() {\n if (match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.NOT, _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.MINUS])) {\n let operator = previous_token();\n let right = unary();\n return {type: 'unary', operator: operator, right: right};\n } else {\n return call();\n }\n }\n\n function call() {\n let expr = primary();\n\n for (;;){\n if (match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.LEFT_PAREN])) {\n expr = finish_call(expr.name);\n } else {\n break;\n }\n }\n\n return expr;\n }\n\n function finish_call(callee) {\n let arguments_list = [];\n if (!check(_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.RIGHT_PAREN, token_index)) {\n do {\n arguments_list.push(expression());\n } while (match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.COMMA]));\n }\n if (!match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.RIGHT_PAREN])) {\n throw {message: \"Expect ')' after arguments.\"};\n }\n\n return {type: 'call', name: callee, arguments: arguments_list};\n }\n\n\n function primary() {\n if (match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.NUMERIC, _scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.STRING])) {\n return {type: 'literal', value: previous_token().value, value_type: previous_token().type};\n } else if (match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.LAZY])) {\n let tokens = (0,_scanner__WEBPACK_IMPORTED_MODULE_0__.scan)(previous_token().expression);\n let expression = parse(tokens);\n return {type: 'lazy', value: expression};\n } else if (match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.LEFT_PAREN])) {\n let expr = expression();\n if (expr && match([_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.RIGHT_PAREN])) {\n return {type: 'group', expression: expr};\n } else {\n throw {message: 'expected expression or )'};\n }\n } else if (check(_scanner__WEBPACK_IMPORTED_MODULE_0__.token_types.IDENTIFIER, token_index)) {\n let identifier = {type: 'identifier', name: current_token().value};\n advance();\n return identifier;\n }\n }\n\n /**\n * matches token against array of tokens to check for equality (matching type)\n * @param tokens_to_match array of tokens\n * @returns {boolean}\n */\n function match(tokens_to_match) {\n for (let i = 0; i < tokens_to_match.length; i++) {\n if (are_same(tokens_to_match[i], current_token())) {\n advance()\n return true;\n }\n }\n return false;\n }\n\n /**\n * Checks if token at position index matches the given\n * @param token_to_check expected token type\n * @param index of token to check\n * @returns {boolean}\n */\n function check(token_to_check, index) {\n let token = tokens[index];\n if (!token) {\n return false;\n }\n return are_same(token_to_check, token);\n\n }\n\n /**\n * checks if 2 tokens have same type\n * @param token_1\n * @param token_2\n * @returns {boolean}\n */\n function are_same(token_1, token_2) {\n if (is_at_end()) {\n return false;\n } else {\n return token_1.type === token_2.type;\n }\n\n }\n\n function is_at_end() {\n return token_index >= tokens.length;\n }\n\n function advance() {\n token_index += 1;\n }\n\n function previous_token() {\n return tokens[token_index - 1];\n }\n\n function current_token() {\n return tokens[token_index];\n }\n}\n\n//# sourceURL=webpack://matrepl/./src/js/parser.js?"); + +/***/ }), + +/***/ "./src/js/scanner.js": +/*!***************************!*\ + !*** ./src/js/scanner.js ***! + \***************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"scan\": () => (/* binding */ scan),\n/* harmony export */ \"token_types\": () => (/* binding */ token_types)\n/* harmony export */ });\n/**\n * Creates an array of tokens from a line of input.\n *\n * @param command: string\n * @returns {token_type[]}\n */\nconst scan = function (command) {\n let current_index = 0, // current index of char to look at in the command string\n word_start_index = 0, // marker for start of a literal or identifier\n tokens = [];\n\n while (!is_at_end()) {\n word_start_index = current_index;\n let token = scan_token();\n if (token) { // undefined mostly means whitespace\n tokens.push(token);\n }\n }\n return tokens;\n\n function scan_token() {\n let next_char = advance();\n switch (next_char) {\n case '(':\n return token_types.LEFT_PAREN;\n case ')':\n return token_types.RIGHT_PAREN;\n case '[':\n return token_types.LEFT_BRACKET;\n case ']':\n return token_types.RIGHT_BRACKET;\n case ',':\n return token_types.COMMA;\n case '.':\n return token_types.DOT;\n case '-':\n return token_types.MINUS;\n case '+':\n return token_types.PLUS;\n case '*':\n return token_types.STAR;\n case '/':\n return token_types.SLASH;\n case '>':\n if (expect('=')) {\n return token_types.GREATER_OR_EQUAL;\n } else {\n return token_types.GREATER;\n }\n case '<':\n if (expect('=')) {\n return token_types.LESS_OR_EQUAL;\n } else {\n return token_types.LESS;\n }\n case '!':\n if (expect('=')) {\n return token_types.NOT_EQUALS;\n } else {\n return token_types.NOT;\n }\n case '=':\n if (expect('=')) {\n return token_types.EQUALS_EQUALS;\n } else {\n return token_types.EQUALS;\n }\n case '\\'':\n return string();\n case '\"':\n return lazy_expression();\n }\n if (is_digit(next_char)) {\n let token = Object.assign({}, token_types.NUMERIC);\n token.value = parse_number();\n return token;\n } else {\n if (is_alpha_or_underscore(next_char)) {\n let token = Object.assign({}, token_types.IDENTIFIER);\n token.value = parse_identifier();\n return token;\n }\n }\n }\n\n function expect(expected_char) {\n if (is_at_end()) {\n return false;\n }\n if (current_char() === expected_char) {\n advance();\n return true;\n } else {\n return false;\n }\n }\n\n function advance() {\n if (current_index < command.length) {\n current_index += 1;\n }\n return command[current_index - 1];\n }\n\n function is_at_end() {\n return current_index >= command.length;\n }\n\n function current_char() {\n return command[current_index];\n }\n\n function is_digit(char) {\n return char >= '0' && char <= '9';\n }\n\n function is_part_of_number(char) {\n return is_digit(char) || char === '.'; // no scientific notation for now\n }\n\n function parse_number() {\n while (is_part_of_number(current_char())) {\n advance();\n }\n let number_string = command.substring(word_start_index, current_index);\n return Number.parseFloat(number_string);\n }\n\n function is_alpha_or_underscore(char) {\n return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char === '_';\n }\n\n function is_alphanumeric_or_underscore(char) {\n return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || is_digit(char) || char === '_';\n }\n\n function parse_identifier() {\n while (is_alphanumeric_or_underscore(current_char())) {\n advance();\n }\n return command.substring(word_start_index, current_index);\n }\n\n function string() { // as of yet strings may not unclude escaped quotes that are also the start/end quote\n while (current_char() !== '\\'' && !is_at_end()) {\n advance();\n }\n if (is_at_end() && current_char() !== '\\'') {\n throw {message: 'unterminated string'}\n } else {\n let string_token = Object.assign({}, token_types.STRING);\n string_token.value = command.substring(word_start_index + 1, current_index);\n advance();\n return string_token;\n }\n }\n\n function lazy_expression() {\n while (current_char() !== '\"' && !is_at_end()) {\n advance();\n }\n if (is_at_end() && current_char() !== '\"') {\n throw {message: 'unterminated string'}\n } else {\n let lazy_token = Object.assign({}, token_types.LAZY);\n lazy_token.expression = command.substring(word_start_index + 1, current_index);\n advance();\n return lazy_token;\n }\n }\n};\n\nconst token_types = {\n LEFT_PAREN: {type: 'left_paren'},\n RIGHT_PAREN: {type: 'right_paren'},\n LEFT_BRACKET: {type: 'left_bracket'},\n RIGHT_BRACKET: {type: 'right_bracket'},\n COMMA: {type: 'comma'},\n DOT: {type: 'dot'},\n MINUS: {type: 'minus'},\n PLUS: {type: 'plus'},\n STAR: {type: 'star'},\n SLASH: {type: 'slash'},\n EQUALS: {type: 'equals'},\n EQUALS_EQUALS: {type: 'equals_equals'},\n NOT_EQUALS: {type: 'not_equals'},\n NOT: {type: 'not'},\n GREATER: {type: 'greater'},\n GREATER_OR_EQUAL: {type: 'greater_or_equal'},\n LESS: {type: 'less'},\n LESS_OR_EQUAL: {type: 'less_or_equal'},\n NUMERIC: {type: 'number', value: undefined},\n IDENTIFIER: {type: 'identifier', value: undefined},\n STRING: {type: 'string', value: undefined},\n LAZY: {type: 'lazy', expression: undefined, parsed_expression:undefined}\n};\n\n\n//# sourceURL=webpack://matrepl/./src/js/scanner.js?"); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ if(__webpack_module_cache__[moduleId]) { +/******/ return __webpack_module_cache__[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +/******/ +/******/ // startup +/******/ // Load entry module and return exports +/******/ // This entry module is referenced by other modules so it can't be inlined +/******/ var __webpack_exports__ = __webpack_require__("./src/js/index.js"); +/******/ +/******/ })() +; \ No newline at end of file diff --git a/src/index.html b/index.html similarity index 53% rename from src/index.html rename to index.html index f6af1a9..3350288 100644 --- a/src/index.html +++ b/index.html @@ -3,7 +3,7 @@ Interactive Linear Algebra - +
@@ -12,9 +12,6 @@
- - - - + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..a21247b --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "matrepl", + "version": "1.0.0", + "description": "MatRepl is a Matrix * and a REPL. The Print part does operations on vectors and matrices in a visual environment.", + "main": "src/js/index.js", + "scripts": { + "test": "jest --config=jest.config.js", + "build": "webpack build", + "watch": "webpack --watch", + "start": "webpack serve --open --host 0.0.0.0 --port 8080" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/shautvast/matrepl.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/shautvast/matrepl/issues" + }, + "homepage": "https://github.com/shautvast/matrepl#readme", + "devDependencies": { + "css-loader": "^5.0.2", + "cypress": "^6.4.0", + "eslint": "^7.20.0", + "eslint-loader": "^4.0.2", + "eslint-plugin-cypress": "^2.11.2", + "file-loader": "^6.2.0", + "style-loader": "^2.0.0", + "webpack": "^5.22.0", + "webpack-cli": "^4.5.0", + "webpack-dev-server": "^3.11.2" + } +} diff --git a/src/console.js b/src/console.js deleted file mode 100644 index 542190b..0000000 --- a/src/console.js +++ /dev/null @@ -1,201 +0,0 @@ -/** - * handles user input from the console div - */ -(function () { - const state = {}; - const command_input_element = document.getElementById('command_input'); - const command_history_element = document.getElementById('command_history'); - command_input_element.value = ''; - let command_history = ['']; - let command_history_index = 0; - - let adjust_input_element_height = function(){ - let num_lines=command_input_element.value.split(/\n/).length; - command_input_element.setAttribute('style', 'height: ' + num_lines + 'em'); - if (num_lines>1){ - command_input_element.setAttribute('class','multiline'); - } else { - command_input_element.setAttribute('class','single_line'); - } - } - - command_input_element.onkeyup = function handle_key_input(event) { - adjust_input_element_height(); - if (event.key === 'ArrowUp') { - if (command_history_index > -1) { - command_input_element.value = command_history[command_history_index]; - if (command_history_index > 0) { - command_history_index -= 1; - } - } - } - if (event.key === 'ArrowDown') { - if (command_history_index < command_history.length - 1) { - command_history_index += 1; - command_input_element.value = command_history[command_history_index]; - } else { - command_input_element.value = ''; - } - } - if (event.key === 'Enter') { - let commands = command_input_element.value; - command_input_element.value=''; - adjust_input_element_height(); - let command_array = commands.split(/\n/); - for (let i = 0; i < command_array.length; i++) { - let command = command_array[i]; - if (command.length > 0) { - command_history_element.innerText += command + "\n"; - command_input_element.value = ''; - command_history_index = command_history.length; - let tokens = scan(command); - let statement = parse(tokens); - let result; - try { - result = visit_expression(statement); - if (result.description) { - result = result.description; - } - } catch (e) { - result = e.message; - } - command_history_element.innerText += result + "\n"; - command_history.push(command); - command_history_element.scrollTo(0, command_history_element.scrollHeight); - } - } - } - }; - - let visit_expression = function (expr) { - switch (expr.type) { - case 'declaration': - let value = visit_expression(expr.initializer); - let existing_value = state[expr.var_name.value]; - if (existing_value) { - if (existing_value.type === 'vector') { - remove_vector(existing_value.object); // remove from screen - } - } - value.binding = expr.var_name.value; - state[expr.var_name.value] = value; - let description = state[expr.var_name.value].description; - if (!description) { - description = state[expr.var_name.value]; //questionable. use toString instead of message? - } - return {description: expr.var_name.value + ':' + description}; - case 'group': - return visit_expression(expr.expression); - case 'unary': - let right_operand = visit_expression(expr.right); - if (expr.operator === token_types.MINUS) { - return -right_operand; - } else if (expr.operator === token_types.NOT) { - return !right_operand; - } else { - throw {message: 'illegal unary operator'}; - } - case 'binary': - let left = visit_expression(expr.left); - let right = visit_expression(expr.right); - switch (expr.operator) { - case token_types.MINUS: - return left - right; - case token_types.PLUS: - return addition(left, right); - case token_types.STAR: - return multiplication(left, right); - case token_types.SLASH: - return left / right; - case token_types.DOT: - return method_call(left, expr.right); - } - throw {message: 'illegal binary operator'} - case 'identifier': { - if (state[expr.name]) { - return state[expr.name]; - } else { - break; - } - } - case 'literal': - return expr.value; - case 'call': - return call(expr.name, expr.arguments); - } - } - - const call = function (function_name, argument_exprs) { - let arguments = []; - for (let i = 0; i < argument_exprs.length; i++) { - arguments.push(visit_expression(argument_exprs[i])); - } - if (functions[function_name]) { - return functions[function_name](arguments); - } else { - let arg_list = ''; - for (let i = 0; i < argument_exprs.length; i++) { - if (i > 0) { - arg_list += ','; - } - arg_list += argument_exprs[i].value_type; - } - return 'unimplemented: ' + function_name + '(' + arg_list + ')'; - } - } - - const method_call = function (object_wrapper, method_or_property) { - if (object_wrapper) { - if (method_or_property.type === 'call') { // method - if (typeof object_wrapper.object[method_or_property.name] !== 'function') { - throw {message: `method ${method_or_property.name} not found on ${object_wrapper.type}`}; - } - return object_wrapper.object[method_or_property.name].apply(object_wrapper, method_or_property.arguments); - - } else { // property - if (!object_wrapper.object.hasOwnProperty(method_or_property.name)) { - throw {message: `property ${method_or_property.name} not found on ${object_wrapper.type}`}; - } - return object_wrapper.object[method_or_property.name]; - } - } else { - throw {message: `not found: ${object_wrapper}`}; - } - } - - const functions = { - help: () => help(), - vector: (args) => add_vector({x0: args[0], y0: args[1], x: args[2], y: args[3]}), - remove: (args) => { - if (args[0].hasOwnProperty('binding')) { - delete state[args[0].binding]; - return remove_vector(args[0].object); // by binding value - } else { - return remove_vector(args[0]); // by index (@...) - } - - }, - } - - const help = function () { - return {message: 'vector(x0, y0, x, y): draws a vector from x0,y0 to x,y'} - } - - const multiplication = function (left, right) { - if (left.object && left.type === 'vector' && !right.object) { - return left.object.multiply(right); - } - if (right.object && right.type === 'vector' && !left.object) { - return right.object.multiply(left); - } - return left * right; - } - - const addition = function (left, right) { - if (left.object && left.type === 'vector' && right.object && right.type === 'vector') { - return left.object.add(right.object); - } - return left + right; - } - } -)(); \ No newline at end of file diff --git a/src/app.css b/src/css/app.css similarity index 100% rename from src/app.css rename to src/css/app.css diff --git a/src/index.js b/src/index.js deleted file mode 100644 index c415c77..0000000 --- a/src/index.js +++ /dev/null @@ -1,297 +0,0 @@ -let add_vector, - remove_vector; - -/** - * Main entry. draws the matrix - */ -(function () { - const SVG_NS = 'http://www.w3.org/2000/svg'; // program needs these to create svg elements - let grid_size = 100; // this is the nr of pixels for the basis vector (1,0) (0,1) - let half_grid_size = grid_size >> 1; // used to position the grid lines - let vectors = []; // collection of added vectors - let moving_vector; // user can move vector arrows. when moving, this refers to the arrow - let width = window.innerWidth, height = window.innerHeight; - let origin_x = Math.floor((width / grid_size) / 2) * grid_size + half_grid_size, - origin_y = Math.floor((height / grid_size) / 2) * grid_size + half_grid_size; - /** - * Creates an svg element - * @param element_type path,g, etc - * @returns SVG element - */ - const create = function (element_type) { - return document.createElementNS(SVG_NS, element_type); - } - - /** - * creates the d attribute string - * @param x0 start_x - * @param y0 start_y - * @param x1 end_x - * @param y1 end y - * @returns {string} to put in an SVG path - */ - const calculate_d = function (x0, y0, x1, y1) { - return "M" + x0 + " " + y0 + " L" + x1 + " " + y1; - } - - /** - * creates a SVG line (path) - * @param x0 start_x - * @param y0 start_y - * @param x1 end_x - * @param y1 end_y - * @param css_class the css class to make up the element - * @returns an SVG path element - */ - const create_line = function (x0, y0, x1, y1, css_class) { - let path = create('path'); - path.setAttribute('d', calculate_d(x0, y0, x1, y1)); - path.setAttribute('class', css_class); - return path; - } - - /** - * creates the arrow path element - * @param id attribute - * @param x0 start_x - * @param y0 start_y - * @param x1 end_x - * @param y1 end_y - * @param css_class class attribute - * @returns {SVGPathElement} - */ - const arrow = function (id, x0, y0, x1, y1, css_class) { - let path = create('path'); - - path.setAttribute('d', calculate_d(x0, y0, x1, y1)); - path.id = id; - path.setAttribute('class', css_class); - path.setAttribute('marker-end', 'url(#arrow)'); - return path; - } - - /** - * Draws the background grid of the space - * @param css_class class for the lines that are 'multiples of the basis vector' - * @param bg_css_class class for in between lines - * @returns {SVGGElement} - */ - const create_grid = function (css_class, bg_css_class) { - const group = create('g'); - group.setAttribute('id', 'grid'); - const horizontal = create('g'); - horizontal.setAttribute('id', 'horizontal'); - for (let y = 0; y < height; y += grid_size) { - horizontal.appendChild(create_line(0, y + half_grid_size, width, y + half_grid_size, css_class)); - horizontal.appendChild(create_line(0, y, width, y, bg_css_class)); - } - group.appendChild(horizontal); - const vertical = create('g'); - vertical.setAttribute('id', 'vertical'); - for (let x = 0; x < width; x += grid_size) { - vertical.appendChild(create_line(x + half_grid_size, 0, x + half_grid_size, height, css_class)); - vertical.appendChild(create_line(x, 0, x, height, bg_css_class)); - } - group.appendChild(vertical); - return group; - } - - /** - * removes child from element by id if found - * @param element - * @param child_id id to remove - */ - const remove_child = function (element, child_id) { - let node = element.firstChild; - while (node && child_id !== node.id) { - node = node.nextSibling; - } - if (node) { - element.removeChild(node); - } - } - - /** - * removes the grid from the DOM and adds an updated one. - */ - const redraw_grid = function () { - remove_child(svg, "grid"); - svg.appendChild(create_grid('grid', 'bg-grid')); - svg.appendChild(create_axes()); - } - - /** - * Adds a vector to the set. - * @param vector - */ - add_vector = function (vector) { - vector.id = vectors.length; - vectors.push(vector); - redraw(); - vector.add = (other) => add_vector({ - x0: vector.x0 + other.x0, - y0: vector.x0 + other.x0, - x: vector.x + other.x, - y: vector.y + other.y - }); - vector.multiply = (scalar) => add_vector({ - x0: vector.x0 * scalar, - y0: vector.y0 * scalar, - x: vector.x * scalar, - y: vector.y * scalar - }); - vector.is_vector = true; - vector.type = () => 'vector'; - return { //object_wrapper - type: 'vector', - object: vector, - description: `vector@${vector.id}{x0:${vector.x0},y0:${vector.y0} x:${vector.x},y:${vector.y}}`, - }; - - } - - remove_vector = function (vector_or_index) { - let index; - if (vector_or_index.is_vector) { - for (let i = 0; i < vectors.length; i++) { - if (vectors[i].id === vector_or_index.id) { - index = i; - break; - } - } - } else { - index = vector_or_index; - } - - if (!vectors[index]) { - throw {message: `vector@${index} not found`}; - } - - vectors.splice(index, 1); - redraw(); - return {description: `vector@${index} removed`}; - } - - /** - * The moving operation. Called by onmousemove on the svg ('canvas') - * @param event - */ - const move = function (event) { - if (moving_vector) { - let current_x = event.clientX; - let current_y = event.clientY; - vectors[moving_vector.id].x = (current_x - origin_x) / grid_size; - vectors[moving_vector.id].y = (origin_y - current_y) / grid_size; - moving_vector.setAttribute('d', calculate_d(origin_x, origin_y, current_x, current_y)); - } - } - - /** - * Draws all the vectors. - * - * vector { - * x0,y0 origin - * x,y coordinates - * } - */ - const draw_vectors = function () { - const vector_group = create("g"); - vector_group.id = 'vectors'; - - for (let i = 0; i < vectors.length; i++) { - let vector_arrow = arrow(vectors[i].id, - origin_x + vectors[i].x0 * grid_size, - origin_y - vectors[i].y0 * grid_size, - origin_x + vectors[i].x * grid_size, - origin_y - vectors[i].y * grid_size, - 'vector'); - vector_arrow.onmousedown = function start_moving_vector(event) { - moving_vector = event.target; - }; - vector_group.appendChild(vector_arrow); - } - svg.appendChild(vector_group); - } - - /** - * Removes all vectors in the svg and calls draw_vectors to draw updated versions. - */ - const redraw_vectors = function () { - remove_child(svg, 'vectors'); - draw_vectors(); - } - - /** - * (re)draws all - */ - const redraw = function () { - redraw_grid(); - redraw_vectors(); - } - - const create_axes = function () { - let axes_group = create('g'); - let x = create_line(0, origin_y, width, origin_y, 'axis'); - x.id = 'x-axis'; - axes_group.appendChild(x); - let y = create_line(origin_x, 0, origin_x, height, 'axis'); - y.id = 'y-axis'; - axes_group.appendChild(y); - return axes_group; - } - - /** - * setup the arrow head for the vector - * @returns {SVGDefsElement} - */ - function create_defs() { - let defs = create('defs'); - let marker = create('marker'); - marker.id = 'arrow'; - marker.setAttribute('orient', 'auto'); - marker.setAttribute('viewBox', '0 0 10 10'); - marker.setAttribute('markerWidth', '3'); - marker.setAttribute('markerHeight', '4'); - marker.setAttribute('markerUnits', 'strokeWidth'); - marker.setAttribute('refX', '6'); - marker.setAttribute('refY', '5'); - let polyline = create('polyline'); - polyline.setAttribute('points', '0,0 10,5 0,10 1,5'); - polyline.setAttribute('fill', 'yellow'); - marker.appendChild(polyline); - defs.appendChild(marker); - return defs; - } - - /** - * Creates the SVG - * @returns {SVGElement} - */ - const create_svg = function () { - let svg = create('svg'); - - svg.onmousemove = move; - svg.onmouseup = function stop_moving_vector() { - moving_vector = undefined; - }; - - let defs = create_defs(); - svg.appendChild(defs); - return svg; - } - - document.body.onresize = function recalculate_window_dimensions() { - width = window.innerWidth; - height = window.innerHeight; - origin_x = Math.floor((width / grid_size) / 2) * grid_size + half_grid_size; - origin_y = Math.floor((height / grid_size) / 2) * grid_size + half_grid_size; - redraw(); - } - - const svg = create_svg(); - document.body.appendChild(svg); - - svg.appendChild(create_grid('grid', 'bg-grid')); - svg.appendChild(create_axes()); -}) -(); \ No newline at end of file diff --git a/src/js/console.js b/src/js/console.js new file mode 100644 index 0000000..1f67266 --- /dev/null +++ b/src/js/console.js @@ -0,0 +1,213 @@ +import {scan, token_types} from './scanner'; +import {parse} from './parser'; +import {add_vector, remove_vector} from "./index"; + +/** + * handles user input from the console div + */ +const state = {}; +const command_input_element = document.getElementById('command_input'); +const command_history_element = document.getElementById('command_history'); +command_input_element.value = ''; +let command_history = ['']; +let command_history_index = 0; + +export const adjust_input_element_height = function () { + let num_lines = command_input_element.value.split(/\n/).length; + command_input_element.setAttribute('style', 'height: ' + num_lines + 'em'); + if (num_lines > 1) { + command_input_element.setAttribute('class', 'multiline'); + } else { + command_input_element.setAttribute('class', 'single_line'); + } +} + +command_input_element.onkeyup = function handle_key_input(event) { + adjust_input_element_height(); + if (event.key === 'ArrowUp') { + if (command_history_index > -1) { + command_input_element.value = command_history[command_history_index]; + if (command_history_index > 0) { + command_history_index -= 1; + } + } + } + if (event.key === 'ArrowDown') { + if (command_history_index < command_history.length - 1) { + command_history_index += 1; + command_input_element.value = command_history[command_history_index]; + } else { + command_input_element.value = ''; + } + } + if (event.key === 'Enter') { + let commands = command_input_element.value; + command_input_element.value = ''; + adjust_input_element_height(); + let command_array = commands.split(/\n/); + for (let i = 0; i < command_array.length; i++) { + let command = command_array[i]; + if (command.length > 0) { + command_history_element.innerText += command + "\n"; + command_input_element.value = ''; + command_history_index = command_history.length; + let tokens = scan(command); + let statement = parse(tokens); + let result; + try { + result = visit_expression(statement); + if (result.description) { + result = result.description; + } + } catch (e) { + result = e.message; + } + command_history_element.innerText += result + "\n"; + command_history.push(command); + command_history_element.scrollTo(0, command_history_element.scrollHeight); + } + } + } +}; + +let visit_expression = function (expr) { + switch (expr.type) { + case 'declaration': { + let value = visit_expression(expr.initializer); + let existing_value = state[expr.var_name.value]; + if (existing_value) { + if (existing_value.type === 'vector') { + remove_vector(existing_value.object); // remove from screen + } + } + value.binding = expr.var_name.value; + state[expr.var_name.value] = value; + let description = state[expr.var_name.value].description; + if (!description) { + description = state[expr.var_name.value]; //questionable. use toString instead of message? + } + return {description: expr.var_name.value + ':' + description}; + } + case 'group': + return visit_expression(expr.expression); + case 'unary': { + let right_operand = visit_expression(expr.right); + if (expr.operator === token_types.MINUS) { + return -right_operand; + } else if (expr.operator === token_types.NOT) { + return !right_operand; + } else { + throw {message: 'illegal unary operator'}; + } + } + case 'binary': { + let left = visit_expression(expr.left); + let right = visit_expression(expr.right); + switch (expr.operator) { + case token_types.MINUS: + return left - right; + case token_types.PLUS: + return addition(left, right); + case token_types.STAR: + return multiplication(left, right); + case token_types.SLASH: + return left / right; + case token_types.DOT: + return method_call(left, expr.right); + } + throw {message: 'illegal binary operator'}; + } + case 'identifier': { + if (state[expr.name]) { + return state[expr.name]; + } else { + break; + } + } + case 'literal': + return expr.value; + case 'call': + return call(expr.name, expr.arguments); + case 'lazy': + console.log(expr.value); + return visit_expression(expr.value); + } +} + +const call = function (function_name, argument_exprs) { + let arguments_list = []; + for (let i = 0; i < argument_exprs.length; i++) { + arguments_list.push(visit_expression(argument_exprs[i])); + } + if (functions[function_name]) { + return functions[function_name](arguments_list); + } else { + let arg_list = ''; + for (let i = 0; i < argument_exprs.length; i++) { + if (i > 0) { + arg_list += ','; + } + arg_list += argument_exprs[i].value_type; + } + return 'unimplemented: ' + function_name + '(' + arg_list + ')'; + } +} + +const method_call = function (object_wrapper, method_or_property) { + if (object_wrapper) { + if (method_or_property.type === 'call') { // method + if (typeof object_wrapper.object[method_or_property.name] !== 'function') { + throw {message: `method ${method_or_property.name} not found on ${object_wrapper.type}`}; + } + return object_wrapper.object[method_or_property.name].apply(object_wrapper, method_or_property.arguments); + + } else { // property + if (!Object.prototype.hasOwnProperty.call(object_wrapper.object, method_or_property.name)) { + throw {message: `property ${method_or_property.name} not found on ${object_wrapper.type}`}; + } + return object_wrapper.object[method_or_property.name]; + } + } else { + throw {message: `not found: ${object_wrapper}`}; + } +} + +const functions = { + help: () => help(), + vector: (args) => add_vector({x0: args[0], y0: args[1], x: args[2], y: args[3]}), + remove: (args) => { + if (Object.prototype.hasOwnProperty.call(args[0],'binding')){ + delete state[args[0].binding]; + return remove_vector(args[0].object); // by binding value + } else { + return remove_vector(args[0]); // by index (@...) + } + + }, +} + +const help = function () { + return { + description: + `- vector(, , , ): draws a vector from x0,y0 to x,y + - remove(|): removes an object, + a ref is @n where n is the reference number asigned to the object` + } +} + +const multiplication = function (left, right) { + if (left.object && left.type === 'vector' && !right.object) { + return left.object.multiply(right); + } + if (right.object && right.type === 'vector' && !left.object) { + return right.object.multiply(left); + } + return left * right; +} + +const addition = function (left, right) { + if (left.object && left.type === 'vector' && right.object && right.type === 'vector') { + return left.object.add(right.object); + } + return left + right; +} diff --git a/src/js/index.js b/src/js/index.js new file mode 100644 index 0000000..9073daa --- /dev/null +++ b/src/js/index.js @@ -0,0 +1,297 @@ +import './console.js'; +import './scanner.js'; +import './parser.js'; + +export let add_vector, remove_vector; + +/** + * Main entry. draws the matrix + */ +const SVG_NS = 'http://www.w3.org/2000/svg'; // program needs these to create svg elements +let grid_size = 100; // this is the nr of pixels for the basis vector (1,0) (0,1) +let half_grid_size = grid_size >> 1; // used to position the grid lines +let vectors = []; // collection of added vectors +let moving_vector; // user can move vector arrows. when moving, this refers to the arrow +let width = window.innerWidth, height = window.innerHeight; +let origin_x = Math.floor((width / grid_size) / 2) * grid_size + half_grid_size, + origin_y = Math.floor((height / grid_size) / 2) * grid_size + half_grid_size; +/** + * Creates an svg element + * @param element_type path,g, etc + * @returns SVG element + */ +const create = function (element_type) { + return document.createElementNS(SVG_NS, element_type); +} + +/** + * creates the d attribute string + * @param x0 start_x + * @param y0 start_y + * @param x1 end_x + * @param y1 end y + * @returns {string} to put in an SVG path + */ +const calculate_d = function (x0, y0, x1, y1) { + return "M" + x0 + " " + y0 + " L" + x1 + " " + y1; +} + +/** + * creates a SVG line (path) + * @param x0 start_x + * @param y0 start_y + * @param x1 end_x + * @param y1 end_y + * @param css_class the css class to make up the element + * @returns an SVG path element + */ +const create_line = function (x0, y0, x1, y1, css_class) { + let path = create('path'); + path.setAttribute('d', calculate_d(x0, y0, x1, y1)); + path.setAttribute('class', css_class); + return path; +} + +/** + * creates the arrow path element + * @param id attribute + * @param x0 start_x + * @param y0 start_y + * @param x1 end_x + * @param y1 end_y + * @param css_class class attribute + * @returns {SVGPathElement} + */ +const arrow = function (id, x0, y0, x1, y1, css_class) { + let path = create('path'); + + path.setAttribute('d', calculate_d(x0, y0, x1, y1)); + path.id = id; + path.setAttribute('class', css_class); + path.setAttribute('marker-end', 'url(#arrow)'); + return path; +} + +/** + * Draws the background grid of the space + * @param css_class class for the lines that are 'multiples of the basis vector' + * @param bg_css_class class for in between lines + * @returns {SVGGElement} + */ +const create_grid = function (css_class, bg_css_class) { + const group = create('g'); + group.setAttribute('id', 'grid'); + const horizontal = create('g'); + horizontal.setAttribute('id', 'horizontal'); + for (let y = 0; y < height; y += grid_size) { + horizontal.appendChild(create_line(0, y + half_grid_size, width, y + half_grid_size, css_class)); + horizontal.appendChild(create_line(0, y, width, y, bg_css_class)); + } + group.appendChild(horizontal); + const vertical = create('g'); + vertical.setAttribute('id', 'vertical'); + for (let x = 0; x < width; x += grid_size) { + vertical.appendChild(create_line(x + half_grid_size, 0, x + half_grid_size, height, css_class)); + vertical.appendChild(create_line(x, 0, x, height, bg_css_class)); + } + group.appendChild(vertical); + return group; +} + +/** + * removes child from element by id if found + * @param element + * @param child_id id to remove + */ +const remove_child = function (element, child_id) { + let node = element.firstChild; + while (node && child_id !== node.id) { + node = node.nextSibling; + } + if (node) { + element.removeChild(node); + } +} + +/** + * removes the grid from the DOM and adds an updated one. + */ +const redraw_grid = function () { + remove_child(svg, "grid"); + svg.appendChild(create_grid('grid', 'bg-grid')); + svg.appendChild(create_axes()); +} + +/** + * Adds a vector to the set. + * @param vector + */ +add_vector = function (vector) { + vector.id = vectors.length; + vectors.push(vector); + redraw(); + vector.add = (other) => add_vector({ + x0: vector.x0 + other.x0, + y0: vector.x0 + other.x0, + x: vector.x + other.x, + y: vector.y + other.y + }); + vector.multiply = (scalar) => add_vector({ + x0: vector.x0 * scalar, + y0: vector.y0 * scalar, + x: vector.x * scalar, + y: vector.y * scalar + }); + vector.is_vector = true; + vector.type = () => 'vector'; + return { //object_wrapper + type: 'vector', + object: vector, + description: `vector@${vector.id}{x0:${vector.x0},y0:${vector.y0} x:${vector.x},y:${vector.y}}`, + }; + +} + +remove_vector = function (vector_or_index) { + let index; + if (vector_or_index.is_vector) { + for (let i = 0; i < vectors.length; i++) { + if (vectors[i].id === vector_or_index.id) { + index = i; + break; + } + } + } else { + index = vector_or_index; + } + + if (!vectors[index]) { + throw {message: `vector@${index} not found`}; + } + + vectors.splice(index, 1); + redraw(); + return {description: `vector@${index} removed`}; +} + +/** + * The moving operation. Called by onmousemove on the svg ('canvas') + * @param event + */ +const move_vector = function (event) { + if (moving_vector) { + let current_x = event.clientX; + let current_y = event.clientY; + vectors[moving_vector.id].x = (current_x - origin_x) / grid_size; + vectors[moving_vector.id].y = (origin_y - current_y) / grid_size; + moving_vector.setAttribute('d', calculate_d(origin_x, origin_y, current_x, current_y)); + } +} + +/** + * Draws all the vectors. + * + * vector { + * x0,y0 origin + * x,y coordinates + * } + */ +const draw_vectors = function () { + const vector_group = create("g"); + vector_group.id = 'vectors'; + + for (let i = 0; i < vectors.length; i++) { + let vector_arrow = arrow(vectors[i].id, + origin_x + vectors[i].x0 * grid_size, + origin_y - vectors[i].y0 * grid_size, + origin_x + vectors[i].x * grid_size, + origin_y - vectors[i].y * grid_size, + 'vector'); + vector_arrow.onmousedown = function start_moving_vector(event) { + moving_vector = event.target; + }; + vector_group.appendChild(vector_arrow); + } + svg.appendChild(vector_group); +} + +/** + * Removes all vectors in the svg and calls draw_vectors to draw updated versions. + */ +const redraw_vectors = function () { + remove_child(svg, 'vectors'); + draw_vectors(); +} + +/** + * (re)draws all + */ +const redraw = function () { + redraw_grid(); + redraw_vectors(); +} + +const create_axes = function () { + let axes_group = create('g'); + let x = create_line(0, origin_y, width, origin_y, 'axis'); + x.id = 'x-axis'; + axes_group.appendChild(x); + let y = create_line(origin_x, 0, origin_x, height, 'axis'); + y.id = 'y-axis'; + axes_group.appendChild(y); + return axes_group; +} + +/** + * setup the arrow head for the vector + * @returns {SVGDefsElement} + */ +function create_defs() { + let defs = create('defs'); + let marker = create('marker'); + marker.id = 'arrow'; + marker.setAttribute('orient', 'auto'); + marker.setAttribute('viewBox', '0 0 10 10'); + marker.setAttribute('markerWidth', '3'); + marker.setAttribute('markerHeight', '4'); + marker.setAttribute('markerUnits', 'strokeWidth'); + marker.setAttribute('refX', '6'); + marker.setAttribute('refY', '5'); + let polyline = create('polyline'); + polyline.setAttribute('points', '0,0 10,5 0,10 1,5'); + polyline.setAttribute('fill', 'yellow'); + marker.appendChild(polyline); + defs.appendChild(marker); + return defs; +} + +/** + * Creates the SVG + * @returns {SVGElement} + */ +const create_svg = function () { + let svg = create('svg'); + + svg.onmousemove = move_vector(); + svg.onmouseup = function stop_moving_vector() { + moving_vector = undefined; + }; + + let defs = create_defs(); + svg.appendChild(defs); + return svg; +} + +document.body.onresize = function recalculate_window_dimensions() { + width = window.innerWidth; + height = window.innerHeight; + origin_x = Math.floor((width / grid_size) / 2) * grid_size + half_grid_size; + origin_y = Math.floor((height / grid_size) / 2) * grid_size + half_grid_size; + redraw(); +} + +const svg = create_svg(); +document.body.appendChild(svg); + +svg.appendChild(create_grid('grid', 'bg-grid')); +svg.appendChild(create_axes()); diff --git a/src/parser.js b/src/js/parser.js similarity index 91% rename from src/parser.js rename to src/js/parser.js index a3f8d74..dd13b27 100644 --- a/src/parser.js +++ b/src/js/parser.js @@ -1,4 +1,7 @@ -const parse = function (tokens) { +import {token_types} from './scanner'; +import {scan} from './scanner'; + +export const parse = function (tokens) { let token_index = 0; return statement(); @@ -79,7 +82,7 @@ const parse = function (tokens) { function call() { let expr = primary(); - while (true) { + for (;;){ if (match([token_types.LEFT_PAREN])) { expr = finish_call(expr.name); } else { @@ -91,23 +94,27 @@ const parse = function (tokens) { } function finish_call(callee) { - let arguments = []; + let arguments_list = []; if (!check(token_types.RIGHT_PAREN, token_index)) { do { - arguments.push(expression()); + arguments_list.push(expression()); } while (match([token_types.COMMA])); } if (!match([token_types.RIGHT_PAREN])) { throw {message: "Expect ')' after arguments."}; } - return {type: 'call', name: callee, arguments: arguments}; + return {type: 'call', name: callee, arguments: arguments_list}; } function primary() { if (match([token_types.NUMERIC, token_types.STRING])) { return {type: 'literal', value: previous_token().value, value_type: previous_token().type}; + } else if (match([token_types.LAZY])) { + let tokens = scan(previous_token().expression); + let expression = parse(tokens); + return {type: 'lazy', value: expression}; } else if (match([token_types.LEFT_PAREN])) { let expr = expression(); if (expr && match([token_types.RIGHT_PAREN])) { diff --git a/src/scanner.js b/src/js/scanner.js similarity index 83% rename from src/scanner.js rename to src/js/scanner.js index 92be530..fcf7b72 100644 --- a/src/scanner.js +++ b/src/js/scanner.js @@ -2,9 +2,9 @@ * Creates an array of tokens from a line of input. * * @param command: string - * @returns {token_type[]} + * @returns {token_types[]} */ -const scan = function(command) { +export const scan = function (command) { let current_index = 0, // current index of char to look at in the command string word_start_index = 0, // marker for start of a literal or identifier tokens = []; @@ -66,9 +66,9 @@ const scan = function(command) { return token_types.EQUALS; } case '\'': - return string('\''); - case '\"': - return string('\"'); + return string(); + case '"': + return lazy_expression(); } if (is_digit(next_char)) { let token = Object.assign({}, token_types.NUMERIC); @@ -141,11 +141,11 @@ const scan = function(command) { return command.substring(word_start_index, current_index); } - function string(quote) { // as of yet strings may not unclude escaped quotes that are also the start/end quote - while (current_char() !== quote && !is_at_end()) { + function string() { // as of yet strings may not unclude escaped quotes that are also the start/end quote + while (current_char() !== '\'' && !is_at_end()) { advance(); } - if (is_at_end() && current_char() !== quote) { + if (is_at_end() && current_char() !== '\'') { throw {message: 'unterminated string'} } else { let string_token = Object.assign({}, token_types.STRING); @@ -154,9 +154,23 @@ const scan = function(command) { return string_token; } } + + function lazy_expression() { + while (current_char() !== '"' && !is_at_end()) { + advance(); + } + if (is_at_end() && current_char() !== '"') { + throw {message: 'unterminated string'} + } else { + let lazy_token = Object.assign({}, token_types.LAZY); + lazy_token.expression = command.substring(word_start_index + 1, current_index); + advance(); + return lazy_token; + } + } }; -const token_types = { +export const token_types = { LEFT_PAREN: {type: 'left_paren'}, RIGHT_PAREN: {type: 'right_paren'}, LEFT_BRACKET: {type: 'left_bracket'}, @@ -177,5 +191,6 @@ const token_types = { LESS_OR_EQUAL: {type: 'less_or_equal'}, NUMERIC: {type: 'number', value: undefined}, IDENTIFIER: {type: 'identifier', value: undefined}, - STRING: {type: 'string', value: undefined} + STRING: {type: 'string', value: undefined}, + LAZY: {type: 'lazy', expression: undefined, parsed_expression:undefined} }; diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..872e4a2 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,42 @@ +const path = require('path'); + +module.exports = { + mode: 'development', + entry: './src/js/index.js', + + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'eslint-loader', + options: { + emitError: true, + }, + }, + { + test: /\.css$/, + use: [ + 'style-loader', + 'css-loader', + ], + }, + { + test: /\.(png|svg|jpg|gif)$/, + use: [ + 'file-loader', + ] + }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/, + use: [ + 'file-loader', + ], + }, + ], + }, +}; \ No newline at end of file