diff --git a/.eslintrc.json b/.eslintrc.json index 15d0a84..3599e84 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -21,6 +21,9 @@ "error", "always" ], + "no-useless-escape": [ + "off" + ], "no-unused-vars": [ "warn", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false } diff --git a/index.html b/index.html index 3ca63a4..d81f3cb 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@ - + @@ -86,8 +86,9 @@

Header

diff --git a/scripts/consts.js b/scripts/consts.js index 8e37856..3edcb6a 100644 --- a/scripts/consts.js +++ b/scripts/consts.js @@ -195,4 +195,7 @@ export const PLUS = '\u207A'; export const SIGMA = '\u03A3'; /** @constant {string} EMPTY_SET - empty set symbol for regular expressions */ -export const EMPTY_SET = '\u2205'; \ No newline at end of file +export const EMPTY_SET = '\u2205'; + +/** @constant {float} LATEX_ANGLE_SCALE - used to scale angels for curved edges in tikzpicture*/ +export const LATEX_ANGLE_SCALE = 8; diff --git a/scripts/index.js b/scripts/index.js index dfb1133..8658fdd 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -11,6 +11,7 @@ import * as permalink from './permalink.js'; import * as util from './util.js'; import * as ui_setup from './ui_setup.js'; import * as regex from './regex.js'; +import * as latex from './latex.js'; // if not in browser, don't run if (typeof document !== 'undefined') { @@ -389,6 +390,18 @@ function bind_permalink() { window.addEventListener('hashchange', hash_change_handler); } +/** button to generate latex text */ +function bind_latex() { + const latex_button = document.getElementById('latex'); + latex_button.addEventListener('click', () => { + const select = document.getElementById('select_machine'); + const latex_str = latex.serialize(select.value, graph); + navigator.clipboard.writeText(latex_str) + .then(() => alert('Latex text copied to clipboard')); + }); + return; +} + /** change cursor style when hovering over certain elements */ function bind_mousemove() { const canvas = drawing.get_canvas(); @@ -405,10 +418,14 @@ function bind_mousemove() { /** bind context menu for side nav bar and secondary side navbar */ function bind_context_menu_navbar(){ - const navbar = document.querySelector('.nav') - const secondBar = document.querySelector('#secondbar') - navbar.addEventListener('click', () => {menus.remove_context_menu()}) - secondBar.addEventListener('click', () => {menus.remove_context_menu()}) + const navbar = document.querySelector('.nav'); + const secondBar = document.querySelector('#secondbar'); + navbar.addEventListener('click', () => { + menus.remove_context_menu(); + }); + secondBar.addEventListener('click', () => { + menus.remove_context_menu(); + }); /* for(var btns of navbar){ btns.addEventListener('click', () => {remove_context_menu()}) @@ -422,7 +439,7 @@ function bind_regex() { const convert_to_nfa_btn = document.getElementById('convert_to_nfa'); convert_to_nfa_btn.addEventListener('click', () => { console.log(document.getElementById('regex_string').value); - let inputString = document.getElementById('regex_string').value + let inputString = document.getElementById('regex_string').value; inputString = inputString.replace(/\s/g, ''); if (regex.isValidRegex(inputString)) { graph = regex.process_string(inputString); @@ -431,14 +448,14 @@ function bind_regex() { drawing.draw(graph); // hist.push_history(graph); NEED TO IMPLEMENT HISTORY BEFORE UNCOMMENTING } else { - alert("Invalid regular expression.") + alert('Invalid regular expression.'); } }); const input_field = document.getElementById('regex_string'); input_field.addEventListener('keypress', (e) => { - if (e.key === "Enter") { + if (e.key === 'Enter') { console.log(document.getElementById('regex_string').value); - let inputString = document.getElementById('regex_string').value + let inputString = document.getElementById('regex_string').value; inputString = inputString.replace(/\s/g, ''); if (regex.isValidRegex(inputString)) { graph = regex.process_string(inputString); @@ -447,10 +464,10 @@ function bind_regex() { drawing.draw(graph); // hist.push_history(graph); NEED TO IMPLEMENT HISTORY BEFORE UNCOMMENTING } else { - alert("Invalid regular expression.") + alert('Invalid regular expression.'); } } - }) + }); } /** run after all the contents are loaded to hook up callbacks */ @@ -466,6 +483,7 @@ function init() { bind_scroll(); bind_dd(); bind_permalink(); + bind_latex(); bind_mousemove(); ui_setup.bind_plus_minus(); ui_setup.add_input_bar(); // called so one input bar appears on opening of homepage diff --git a/scripts/latex.js b/scripts/latex.js new file mode 100644 index 0000000..6cf5067 --- /dev/null +++ b/scripts/latex.js @@ -0,0 +1,190 @@ +/** @module latex */ + +// ------------------------------------------------------------- +// @author Meruzhan Sargsyan +// +// A module used to export the graph as text used in tikzpicture +// for easily exporting graphs into latex files +// ------------------------------------------------------------- + +//---------------------------------------------- +// Testing Notes: +// - clipboard only available in secure contexts +//---------------------------------------------- + +//---------------------------------------------- +// Current TODO: +// 1. Self loops only go above +// 2. Horizontally bend angles have label on the left +// 3. Fix more complicated state names +// 4. Overlapping labels for self loops +//---------------------------------------------- + +import * as consts from './consts.js'; +import * as linalg from './linalg.js'; + +let debug = false; // change this to enable/disable logging + +/** + * compresses graph to tikz space + * @param {String} type - type of graph (DFA, NFA, ...) + * @param {Array} states - the states of the graph + * @returns {Array} formatted positions of states + */ +function compressPlanar(states) { + const distance = 8; + + let centroidX = 0, centroidY = 0; + let n = states.length; + + let output = Array(n); + + for(let i = 0; i < n; i++) { + let state = states[i]; + centroidX += state.x; + centroidY += state.y; + output[i] = [state.x, state.y]; + } + if(debug) { + console.log(output); + } + + centroidX /= n; + centroidY /= n; + let center = [centroidX, centroidY]; + + let maxDist = Number.MIN_VALUE; + for(let i = 0; i < n; i++) { + output[i] = linalg.sub(output[i], center); + maxDist = Math.max(maxDist, linalg.vec_len(output[i])); + } + + let scaleFactor = distance / (2 * maxDist); + let formatted = output.map((v) => { + let scaled = linalg.scale(scaleFactor, v); + return `(${scaled[0].toFixed(2)},${-1 * scaled[1].toFixed(2)})`; + }); + + if(debug) { + console.log(formatted); + } + return formatted; +} + +/** + * Computes the type of a given state + * @param {Object} state + * @returns {String} tikz labels for the type of state + */ +function getStateType(state) { + let inner = 'state,'; + if(state.is_start) { + inner += 'initial,'; + } + if(state.is_final) { + inner += 'accepting,'; + } + + return inner; +} + +/** + * converts an edge to tikz string representation + * @param {String} type - type of graph (DFA, NFA, ...) + * @param {Object} edge - edge to convert to string + * @param {String} labelPos - where to position label on edge + * @returns {String} - tikz string representaiton of edge + */ +function edgeToString(type, edge, labelPos) { + if(debug) { + console.log(edge); + } + let bendAngle = Math.floor(edge.a2) * consts.LATEX_ANGLE_SCALE; + let inner = `bend right=${bendAngle}`; + let label = `${edge.transition}`; + + if(bendAngle > consts.LATEX_ANGLE_SCALE) { + labelPos = 'right'; + } else if(bendAngle < 0) { + labelPos = 'left'; + } + + if(edge.from === edge.to) { + inner = 'loop above'; + labelPos = 'above'; + } + + switch (type) { + case 'PDA': + label += `,${edge.pop_symbol} \\rightarrow ${edge.push_symbol}`.replaceAll('$', '\\$'); + break; + case 'Turing': + label += ` \\rightarrow ${edge.push_symbol}, ${edge.move}`.replaceAll('$', '\\$'); + break; + default: + break; + } + + + let output = `(${edge.from}) edge [${inner}] node[${labelPos}] {$${label}$} (${edge.to})\n`; + return output.replaceAll(consts.EMPTY_SYMBOL, '\\epsilon').replaceAll(consts.EMPTY_TAPE, '\\square'); +} + +/** + * @param {Object} graph - graph to be converted to latex + * @return {String} representation of graph in latex tikzpicture + */ +export function serialize(type, graph) { + // setup + let distance = 2; + + let output = `\\begin{tikzpicture}[->,>=stealth\',shorten >=1pt, auto, node distance=${distance}cm, semithick]\n`; + output += '\\tikzstyle{every state}=[text=black, fill=none]\n'; + + // initializing nodes + let states = Object.values(graph); + states.sort((a,b) => a.x - b.x); // sorts the states from left to right + + let statePositions = compressPlanar(states); + + let start = states[0]; + let inner = getStateType(start); + + for(let i = 0; i < states.length; i++) { + let current = states[i]; + inner = getStateType(current); + let position = statePositions[i]; + output += `\\node[${inner}] (${current.name}) at ${position} {$${current.name}$};\n`; + } + + output += '\n'; + output += '\\path\n'; + + for(let i = 0; i < states.length; i++) { + let current = states[i]; + let edges = current.out; // array of edges + + for(let j = 0; j < edges.length; j++) { + let edge = edges[j]; + let labelPosition = 'above'; + + let startState = graph[edge.from]; + let endState = graph[edge.to]; + let angle = linalg.angle([startState.x, startState.y], [endState.x, endState.y]); + + if(angle <= -80 && angle >= -110) { + labelPosition = 'left'; + } else if(angle >= 80 && angle <= 110) { + labelPosition = 'right'; + } + + output += edgeToString(type, edge, labelPosition); + } + } + output += ';\n'; + + output += '\\end{tikzpicture}'; + if(debug) { + console.log(output); + } +} \ No newline at end of file diff --git a/scripts/linalg.js b/scripts/linalg.js index ae4173b..3283f5e 100644 --- a/scripts/linalg.js +++ b/scripts/linalg.js @@ -120,3 +120,25 @@ export function inv(v1, v2) { const inv_det = 1/det(v1, v2); return [scale(inv_det, [v2[1], -v1[1]]), scale(inv_det, [-v2[0], v1[0]])]; } + +/** + * computes angle between two points in degrees with 0deg = x-axis, increasing counterclockwise + * @param {Array} pt1 - point to compute angle from + * @param {Array} pt2 - point to compute angle to + * @returns {float} angle between pt1, pt2 + */ +export function angle(pt1, pt2) { + let direction = [pt2[0] - pt1[0], pt2[1] - pt1[1]]; + let base = [1, 0]; // x axis + + let dotProd = dot(direction, base); + let mult = vec_len(direction); // |base| = 1 + + let angle = Math.acos(dotProd/mult) * (180 / Math.PI); + + // start is above + if(pt1[1] < pt2[1]) { + angle *= -1; + } + return angle; +} \ No newline at end of file diff --git a/scripts/regex.js b/scripts/regex.js index 4d76550..765a16f 100644 --- a/scripts/regex.js +++ b/scripts/regex.js @@ -376,19 +376,22 @@ export function create_buttons() { // 5. handle case with empty set // function taken from https://github.com/flapjs/webapp/blob/master/src/modules/re/machine/RegularExpression.js -export function areParenthesisBalanced(expressionString) -{ - let count = 0; - for (let i = 0; i < expressionString.length; i++) - { - let symbol = expressionString.charAt(i); +export function areParenthesisBalanced(expressionString) { + let count = 0; + for (let i = 0; i < expressionString.length; i++) { + let symbol = expressionString.charAt(i); - if (symbol === consts.OPEN) count++; - else if (symbol === consts.CLOSE) count--; + if (symbol === consts.OPEN) { + count++; + } else if (symbol === consts.CLOSE) { + count--; + } - if (count < 0) return false; + if (count < 0) { + return false; } - return count === 0; + } + return count === 0; } export function isValidRegex(string) { @@ -396,7 +399,7 @@ export function isValidRegex(string) { string = string.replace(/\s+/g, ''); // check for invalid parentheses and empty string - if (!areParenthesisBalanced(string) || string === "") { + if (!areParenthesisBalanced(string) || string === '') { return false; } let prev; @@ -411,9 +414,9 @@ export function isValidRegex(string) { if ((char === consts.UNION || char === consts.KLEENE || char === consts.CONCAT) && (prev === consts.UNION || prev === consts.KLEENE || prev === consts.CONCAT) - ) { - return false; - } + ) { + return false; + } prev = char; } @@ -426,5 +429,5 @@ export function process_string(string) { let postfix = shunting_yard(injectedConcat); let finalGraph = thompson(postfix); - return finalGraph + return finalGraph; } diff --git a/tests/FlapJS_test.pdf b/tests/FlapJS_test.pdf new file mode 100644 index 0000000..be712c5 Binary files /dev/null and b/tests/FlapJS_test.pdf differ