3 Sequencer syntax highlighting and operation functions
5 Created by Zaher Salman on September 16th, 2023
10 Some of these functions, although specific to the midas sequencer,
11 can be used for general syntax highlighting and editing.
13 All files created and read from subdirectories within
14 experiment_directory/userfiles/sequencer. The path defined in ODB
15 /Sequencer/State/Path is added to this folder.
17 Generate highlighted script text:
18 syntax_msl(seqLines,keywordGroups)
19 - seqLines - is the text of the script (or array of lines).
20 - keywordGroups - (optional) object defining the command groups for
21 highlighting. If not provided, a msl default will
24 Highlight the current line:
25 hlLine(lineNum,color,editor,msgs)
26 - lineNums- the line number to be highlighted (or an array of numbers)
27 - color - (optional) background color
28 - editor - (optional) if provided, scroll to highlighted line in editor
29 - msgs - (optional) if provided, use as title/s for the highlighted line/s
32 - syntax validate ODBSET, ODBGET etc. have a valid ODB parameter
35// Using Solarized color scheme - https://ethanschoonover.com/solarized/
37// Default msl definitions, keywords, indentation and file name
38var SeqODB = "/Sequencer";
42 if (SeqODB === "/PySequencer") {
46 {pattern: String.raw`(?<!(?:''|\s))'(?:(?!').)*?(?<!')'|(?<!(?:""|\s))(\s*)"(?:(?!").)*?(?<!")"`, flags: "g" }
49 mslclass: "msl_string"
53 "def", "return", "if", "elif", "else", "pass", "yield", "with", "as", "lambda",
54 "assert", "global", "nonlocal", "del", "await", "async"
56 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
58 mslclass: "msl_data_management"
62 { "pattern": "\\$\\w+", "flags": "g" },
63 { "pattern": "^(\\s*)(\\w+)(?=\\s*=)", "flags": "gm" }
67 mslclass: "msl_variable",
70 keywords: ["for", "while", "break", "continue"],
71 kwregex: ["\\b(", ")\\b", "g"],
72 color: "#d33682", // magenta
73 mslclass: "msl_control_flow"
76 keywords: ["try", "except", "raise", "finally"],
77 kwregex: ["\\b(", ")\\b", "g"],
78 color: "#cb4b16", // orange
79 mslclass: "msl_exceptions"
82 keywords: ["import", "from", "as"],
83 kwregex: ["\\b(", ")\\b", "g"],
84 color: "#6c71c4", // purple
85 mslclass: "msl_imports"
88 keywords: ["in", "is", "not", "and", "or"],
89 kwregex: ["\\b(", ")\\b", "g"],
90 color: "#2aa198", // teal
91 mslclass: "msl_operators"
94 keywords: ["int", "float", "str", "list", "dict", "set", "tuple", "bool", "complex"],
95 kwregex: ["\\b(", ")\\b", "g"],
96 color: "#859900", // green
97 mslclass: "msl_datatypes"
101 "print", "len", "range", "input", "open", "enumerate", "zip",
102 "map", "filter", "any", "all", "sum", "min", "max", "type", "dir"
104 kwregex: ["\\b(", ")\\b", "g"],
105 color: "#b58900", // yellow
106 mslclass: "msl_builtins"
109 keywords: ["True", "False", "None"],
110 kwregex: ["\\b(", ")\\b", "g"],
116 { pattern: "\\b\\d+(\\.\\d+)?([eE][-+]?\\d+)?\\b", flags: "g" }
119 mslclass: "msl_number"
123 pattern: String.raw`^(\s*)("""|''')([^]*?)\2`,
127 fontWeight: "italic",
128 mslclass: "msl_comment",
132 { pattern: "#.*?$", flags: "gm" }
135 fontWeight: "italic",
136 mslclass: "msl_comment"
143 { "pattern": "([\"'])(.*?)(?<!\\\\)\\1", "flags": "g" } // supports escaped quotes
147 mslclass: "msl_string",
151 { "pattern": "\\$\\w+", "flags": "g" },
152 { "pattern": "^(\\s*)(\\w+)(?=\\s*=)", "flags": "gm" }
154 keywords: ["PARAM","CAT","SET"],
155 kwregex: ["^(\\s*)(?:", ")\\s+(\\w+)\\b", "gim"],
158 mslclass: "msl_variable",
161 keywords: ["ODBSET", "ODBGET", "ODBCREATE", "ODBDELETE", "ODBINC", "ODBLOAD", "ODBSAVE", "PARAM", "SET", "CAT"],
162 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
163 color: "#2aa198", // cyan
165 mslclass: "msl_data_management",
168 keywords: ["UNIT8", "INT8", "UNIT16", "INT16", "UNIT32", "INT32", "BOOL", "FLOAT", "DOUBLE", "STRING"],
169 kwregex: ["(?<=,\\s*)\\b(",")\\b","gim"],
170 color: "#859900", // green
172 mslclass: "msl_data_types",
175 keywords: ["BREAK", "LOOP", "ENDLOOP", "ODBSUBDIR", "ENDODBSUBDIR"],
176 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
177 color: "#d33682", // magenta
179 mslclass: "msl_loops",
182 keywords: ["GOTO", "CALL", "SCRIPT", "SUBROUTINE", "ENDSUBROUTINE", "TRANSITION", "INCLUDE", "EXIT"],
183 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
184 color: "#268bd2", // blue
186 mslclass: "msl_control_flow",
189 keywords: ["RUNDESCRIPTION", "LIBRARY", "MESSAGE", "MSG"],
190 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
191 color: "#6c71c4", // violet
193 mslclass: "msl_info",
196 keywords: ["IF", "ELSE", "ENDIF", "WAIT"],
197 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
198 color: "#c577f6", // pink (not solarized)
200 mslclass: "msl_cond",
203 keywords: ["SECONDS", "EVENTS", "ODBVALUE"],
204 kwregex: ["\\b(",")\\b","gi"],
205 color: "#cb4b16", // orange
207 mslclass: "msl_units",
210 keywords: ["start", "stop", "pause", "resume"],
211 kwregex: ["\\b(", ")\\b", "gi"], //["\\b(",")\\b(\\s*)$","gim"],
212 color: "#dc322f", // red
214 mslclass: "msl_actions",
217 //regex: [/\b(\d+(\.\d+)?([eE][-+]?\d+)?)\b/g],
219 { "pattern": "\\b\\d+(\\.\\d+)?([eE][-+]?\\d+)?\\b", "flags": "g" }
221 color: "#b58900", // yellow
223 mslclass: "msl_number",
226 keywords: ["true","false"],
227 kwregex: ["\\b(",")\\b","gim"],
230 mslclass: "msl_bool",
234 { pattern: "(^\\s*COMMENT\\b.*?$|#.*?$)", flags: "gim" }
237 fontWeight: "italic",
238 mslclass: "msl_comment"
245 if (SeqODB === "/PySequencer") {
247 indentplus: ["def", "for", "if", "elif", "else", "try", "except", "finally", "with", "while"],
248 indentminos: ["elif", "else", "except", "finally"],
253 indentplus: ["IF","LOOP","ELSE","SUBROUTINE","ODBSUBDIR"],
254 indentminos: ["ENDIF","ENDLOOP","ELSE","ENDSUBROUTINE","ENDODBSUBDIR"],
263 next : `${SeqODB}/State/Next Filename`,
264 ext : SeqODB === "/PySequencer" ? "*.py" : "*.msl"
285 background-color: #f0f0f0;
291 padding: 5px 5px 20px 5px;
292 box-sizing: border-box;
294 display: inline-block;
296 font-family: monospace;
298 -moz-user-select: text;
299 pointer-events: none;
309 width: calc(100% - 3em);
310 max-width: calc(100% - 3em);
312 /* resize: horizontal; */
313 background-color:white;
315 padding: 5px 5px 20px 5px;
316 box-sizing: border-box;
318 display: inline-block;
320 border-top: 1px solid gray;
321 font-family: monospace;
323 -moz-user-select: text;
327 display: inline-block;
330 display: inline-block;
333.msl_comment, .msl_comment * {
334 all: revert !important;
335 color: #839496 !important;
336 font-weight: italic !important;
337 background: none !important;
341 background-color: #FFFF00;
344.msl_current_line:hover {
348 background-color: #FF0000;
352 background-color: #FF8800;
356 background-color: #11FF11;
360 background-color: #FFFF00;
365 /*resize: horizontal;*/
372 border: 1px solid black;
377 line-height: 30px; /* since modbvalue forces resize */
382.infotable tr:first-child th:first-child,
383.infotable tr:first-child td:first-child {
384 border-top-left-radius: 5px;
386.infotable tr:first-child th:last-child,
387.infotable tr:first-child td:last-child {
388 border-top-right-radius: 5px;
390.infotable tr:last-child td:first-child {
391 border-bottom-left-radius: 5px;
393.infotable tr:last-child td:last-child {
394 border-bottom-right-radius: 5px;
401 display: inline-block;
405 width: calc(100% - 2px);
410/* Dropdown button styles*/
412 background-color: Transparent;
414 font-family: verdana,tahoma,sans-serif;
423 background-color: #C0D0D0;
426/* Style the dropdown content (hidden by default) */
429 display: inline-block;
434 background-color: #f9f9f9;
436 box-shadow: 0 8px 16px rgba(0,0,0,0.2);
447.dropdown-content div{
449 justify-content: space-between;
453.dropdown-content a:hover {
454 background-color: #C0D0D0;
456.dropdown-content div:hover {
457 background-color: #C0D0D0;
459.dropdown:hover .dropdown-content {
472 -webkit-overflow-scrolling: touch;
476 background-color: #D0D0D0;
478 margin: 4px 2px 0px 2px;
479 border-top-left-radius: 10px;
480 border-top-right-radius: 10px;
482 border-top: 1px solid Transparent;
483 border-right: 1px solid Transparent;
484 border-left: 1px solid Transparent;
486 padding: 3px 5px 3px 5px;
489 background-color: #FFFFFF;
490 border-bottom: 5px solid #FFFFFF;
492.etab button.edt_active:hover {
493 background-color: #FFFFFF;
494 border-bottom: 5px solid #FFFFFF;
496.etab button.edt_active {
497 background-color: white;/*Transparent;*/
498 border-top: 1px solid gray;
499 border-right: 1px solid gray;
500 border-left: 1px solid gray;
501 border-bottom: 5px solid white;
511 background-color: #f0f0f0;
512 border: 1px solid black;
515 padding: 0px 2px 0px 2px;
519 background-color: #C0D0D0;
522 font-family: verdana,tahoma,sans-serif;
530 background-color: #FD5E59;
538 border: 1px solid black;
539 box-shadow: 6px 6px 10px 4px rgba(0,0,0,0.2);
546 border: 2px dashed #6bb28c;
547 background-color: white;
550 background-color: gray;
555 text-overflow: ellipsis;
556 max-width: calc(10em - 30px);
558#nextFNContainer img {
561 vertical-align: center;
565// line connector string
567// revisions array, maximum nRevisions
568const previousRevisions = {};
569const nRevisions = 20;
570var revisionIndex = {};
571var saveRevision = {};
572// Meta combo keydown flag
573var MetaCombo = false;
574// Deal with Chrome issues
575var isChrome = browserType();
576// Make current state global
577var stateText = "Unknown";
579// -- Sequencer specific functions --
580// Implement colors and styles from KeywordsGroups in CSS
582 for (const group in mslDefs.groups) {
583 const { mslclass, color, fontWeight } = mslDefs.groups[group];
586 seq_css += `.${mslclass} { color: ${color}; font-weight: ${fontWeight}; }\n`;
588 seq_css += `.${mslclass} { color: ${color};}\n`;
592 const seqStyle = document.createElement('style');
593 seqStyle.textContent = seq_css;
594 document.head.appendChild(seqStyle);
597// Setup the correct sequencer state visually
598function seqState(funcCall) {
600 funcCall - (optional) a function to be called when the state is set (with the state text)
602 stateText = "Stopped";
603 // Check sequence state
604 mjsonrpc_db_get_values([`${SeqODB}/State/Running`,`${SeqODB}/State/Paused`,`${SeqODB}/State/Finished`,`${SeqODB}/State/Debug`,`${SeqODB}/State/Stop after run`]).then(function(rpc) {
605 if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2] && !rpc.result.data[4]) {
606 stateText = "Running";
607 } else if (rpc.result.data[1] && rpc.result.data[0] && !rpc.result.data[2] && !rpc.result.data[4]) {
608 stateText = "Paused";
609 } else if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2] && rpc.result.data[4]) {
610 // Check if stop after run is set and running
611 stateText = "Running.StopAfter"
612 } else if (rpc.result.data[0] && rpc.result.data[1] && !rpc.result.data[2] && rpc.result.data[4]) {
613 // Check if stop after run is set and paused
614 stateText = "Paused.StopAfter"
616 stateText = "Stopped";
623 }).catch (function (error) {
624 console.error(error);
629// Ask user to edit current sequence
630function askToEdit(flag,event) {
632 openETab(document.getElementById("etab1-btn"));
633 const [lineNumbers,editor,btnLabel,label] = editorElements();
634 // make editable and add event listeners
635 //editor.contentEditable = true;
636 addETab(document.getElementById("addETab"));
637 seqOpen(label.title.split("\n")[0]);
638 event.stopPropagation();
641 const message = "To edit the sequence it must be opened in an editor tab.<br>Would you like to proceed?";
642 dlgConfirm(message,function(resp) {
644 const label = editorElements()[3];
645 addETab(document.getElementById("addETab"));
646 seqOpen(label.title.split("\n")[0]);
651// Enable editing of sequence
652function editorEventListeners() {
653 let [lineNumbers,editor] = editorElements();
654 editor.contentEditable = true;
655 // Attached syntax highlight event editor
656 editor.addEventListener("keydown",checkSyntaxEventDown);
657 editor.addEventListener("keyup",checkSyntaxEventUp);
658 editor.addEventListener("paste", checkSyntaxEventPaste,true);
660 editor.addEventListener("input", function() {
663 document.addEventListener("selectionchange", function(event) {
664 if (event.target.activeElement === editor) markCurrLineNum();
666 // Synchronize the scroll position of lineNumbers with editor
667 editor.addEventListener("scroll", function() {
668 lineNumbers.scrollTop = editor.scrollTop;
670 if (isChrome === 1) {
671 editor.addEventListener("keydown", arrowKeysChrome);
675// apply changes of filename in the ODB (triggers reload)
676function seqChange(filename) {
678 filename - full file name with path to change
680 if (!filename) return;
681 const lastIndex = filename.lastIndexOf('/');
682 const path = filename.substring(0, lastIndex).replace(new RegExp('^' + mslDefs.filename.path),"").replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
683 const file = filename.substring(lastIndex + 1);
684 // set path and filename, wait for completion and return
685 mjsonrpc_db_paste([`${SeqODB}/State/Path`,`${SeqODB}/State/Filename`],[path,file]).then(function (rpc1) {
686 sessionStorage.removeItem("depthDir");
687 if (rpc1.result.status[0] === 1 && rpc1.result.status[1] === 1) {
688 mjsonrpc_db_paste([`${SeqODB}/Command/Load new file`],[true]).then(function (rpc2) {
689 if (rpc2.result.status[0] === 1) {
692 }).catch(function (error) {console.error(error);});
694 dlgAlert("Something went wrong, I could not set the filename!");
696 }).catch(function (error) {console.error(error);});
699// Save sequence text in filename.
700function seqSave(filename) {
702 filename (opt) - save to provided filename with path. If undefined save to original
703 filename and if empty trigger file_picker.
705 let [lineNumbers,editor,label] = editorElements();
706 let text = editor.innerText;
707 let ext = mslDefs.filename.ext.replace("*","");
708 if (editor.id !== "editorTab1") {
709 if (filename === undefined) {
710 // take name from button title
711 filename = label.title;
712 if (filename.endsWith(ext)) {
713 file_save_ascii_overwrite(filename,text);
718 } else if (filename === "") {
719 filename = label.title;
720 let file = filename.split("/").pop();
721 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
722 // If file/path are empty start with default value
724 path = sessionStorage.getItem("pathName") ? sessionStorage.getItem("pathName") : mslDefs.filename.path + "/";
726 file = sessionStorage.getItem("fileName") ? sessionStorage.getItem("fileName") : "filename" + ext;
727 file_picker(path,mslDefs.filename.ext,seqSave,true,{},true);
729 file_save_ascii_overwrite(filename,text);
730 let file = filename.split("/").pop();
731 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
732 label.title = filename;
733 label.innerText = file;
734 sessionStorage.setItem("fileName",file);
735 sessionStorage.setItem("pathName",path);
738 // Check if filename is currently in editorTab1 and reload
739 let currFilename = document.getElementById("etab1-btn").title;
740 if (filename == currFilename) {
741 modbset(`${SeqODB}/Command/Load new file`,true);
748function seqOpen(filename) {
750 filename - file name to open (empty trigger file_picker)
752 // if a full filename is provided, open and return
753 if (filename && filename !== "") {
754 // Identify active tab
755 let [lineNumbers,editor,label] = editorElements();
756 // Check the option to open in new tab, also open in new tab if sequence is running
757 if ((document.getElementById("inNewTab").checked && (label.title !== "" || editor.id !== "editorTab1")) ||
758 (editor.id === "editorTab1" && stateText === "Running")) {
759 addETab(document.getElementById("addETab"));
760 [lineNumbers,editor,label] = editorElements();
762 let file = filename.split("/").pop();
763 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
764 label.title = filename.replaceAll(/\/+/g, '/');
765 label.innerText = file;
766 sessionStorage.setItem("fileName",file);
767 sessionStorage.setItem("pathName",path);
768 if (editor.id === "editorTab1") {
771 file_load_ascii(filename, function(text) {
772 editor.innerHTML = syntax_msl(text).join(lc).slice(0,-1);
773 updateLineNumbers(lineNumbers,editor);
774 // Change state to not edited
777 saveState(text,editor);
782 // empty or undefined file name
783 mjsonrpc_db_get_values([`${SeqODB}/State/Path`]).then(function(rpc) {
784 let path = mslDefs.filename.path + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
785 sessionStorage.setItem("pathName",path);
786 file_picker(path,mslDefs.filename.ext,seqOpen,false);
787 }).catch(function (error) {
788 mjsonrpc_error_alert(error);
793// Show/hide buttons according to sequencer state
794function updateBtns(state) {
796 state - the state of the sequencer
800 color: "var(--mgreen)",
803 color: "var(--mred)",
806 color: "var(--mred)",
809 color: "var(--myellow)",
812 const color = seqState[state.split(".")[0]].color;
813 const nclass = state.split(".").length;
814 const seqStateSpans = document.querySelectorAll('.seqstate');
815 seqStateSpans.forEach(e => {e.style.backgroundColor = color;});
817 const hideBtns = document.querySelectorAll('.seqbtn');
818 hideBtns.forEach(button => {
819 button.style.display = "none";
821 // then show only those belonging to the current state
822 const showBtns = document.querySelectorAll('.seqbtn.' + state);
823 showBtns.forEach(button => {
824 if (button.tagName === "IMG") {
825 button.style.display = "inline-block";
827 button.style.display = "flex";
830 // Hide exclusive buttons
832 const exclBtns = document.querySelectorAll('.seqbtn.Exclusive');
833 exclBtns.forEach(button => {
834 button.style.display = "none";
837 // Hide progress modal when stopped
838 const hideProgress = document.getElementById("Progress");
839 if (state === "Stopped" && hideProgress) hideProgress.style.display = "none";
842// Show sequencer messages if present
843function mslMessage(message) {
844 // Empty message, return
845 if (!message) return;
846 // Check message and message wait
847 mjsonrpc_db_get_values([`${SeqODB}/State/Message`,`${SeqODB}/State/Message Wait`]).then(function(rpc) {
848 const message = rpc.result.data[0];
849 const hold = rpc.result.data[1];
851 dlgMessage("Message", message, true, false,clrMessage);
855 }).catch (function (error) {
856 console.error(error);
860// Clear sequencer messages
861function clrMessage() {
862 mjsonrpc_db_paste([`${SeqODB}/State/Message`], [""]).then(function (rpc) {
864 }).catch(function (error) {
865 console.error(error);
869// Adjust size of sequencer editor according to browser window size
870function windowResize() {
871 const [lineNumbers, editor] = editorElements();
872 const seqTable = document.getElementById("seqTable");
873 const infoColumn = document.getElementById("infoColumn");
875 // Calculate scrollbar width
876 const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
878 // Visible width of the window accounting for scrollbar
879 let winWidth = Math.max(document.documentElement.clientWidth, window.innerWidth) - scrollbarWidth;
881 // Visible height of the window
882 let winHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
884 // Set seqTable height and width to fit the remaining space in the viewport
885 seqTable.style.width = winWidth - seqTable.getBoundingClientRect().left + "px";
886 seqTable.style.height = winHeight - seqTable.getBoundingClientRect().top - 20 + "px";
888 // Adjust editor dimensions
889 const editorTop = editor.getBoundingClientRect().top;
890 editor.style.height = winHeight - editorTop - 20 + "px";
891 editor.style.width = winWidth - editor.getBoundingClientRect().left - infoColumn.getBoundingClientRect().width - 10 + "px";
892 editor.style.maxWidth = editor.style.width;
894 // Adjust infoColumn and lineNumbers heights to match the editor height
895 infoColumn.style.height = editor.style.height;
896 lineNumbers.style.height = editor.style.height;
899// Load the current sequence from ODB (only on main tab)
901 const editor = document.getElementById("editorTab1");
902 const btn = document.getElementById("etab1-btn");
903 mjsonrpc_db_get_values([`${SeqODB}/Script/Lines`,`${SeqODB}/State/Running`,`${SeqODB}/State/SCurrent line number`,`${SeqODB}/State/Filename`,`${SeqODB}/State/SFilename`,`${SeqODB}/State/Path`]).then(function(rpc) {
904 let seqLines = rpc.result.data[0];
905 let seqState = rpc.result.data[1];
906 let currLine = rpc.result.data[2];
907 let filename = rpc.result.data[3];
908 let sfilename = rpc.result.data[4] ? rpc.result.data[4].split('userfiles/sequencer/')[1] : "";
909 filename = (mslDefs.filename.path + "/" + rpc.result.data[5] + "/" + filename).replace("//","/");
912 editor.innerHTML = syntax_msl(seqLines).join(lc);
913 updateLineNumbers(editor.previousElementSibling,editor);
914 if (seqState) hlLine(currLine);
916 let file = filename.split("/").pop();
917 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
918 sessionStorage.setItem("fileName",file);
919 sessionStorage.setItem("pathName",path);
920 // change button title to add sfilename if present
921 btn.title = (sfilename && sfilename !== filename) ? filename + "\n" + sfilename : filename;
922 // also change validation icon to defaule
923 let vldBtn = document.getElementById("vldBtn");
924 vldBtn.src = "icons/validate-syntax.svg";
925 vldBtn.style.backgroundColor = "";
926 }).catch (function (error) {
927 console.error(error);
931// Highlight (background color) and scroll to current line
932function hlLine(lineNums,color,editor,msgs = []) {
934 lineNums- the line number to be highlighted (or an array of numbers)
935 color - (optional) background color
936 editor - (optional) if provided, scroll to highlighted line in editor
937 msgs - (optional) if provided, use as title/s for the highlighted line/s
940 // Remove highlight from all lines with the class "msl_current_line"
941 const highlightedLines = document.querySelectorAll(".msl_current_line");
942 highlightedLines.forEach((line) => line.classList.remove("msl_current_line"));
944 // If single value make an array
945 lineNums = Array.isArray(lineNums) ? lineNums : [lineNums];
946 msgs = Array.isArray(msgs) ? msgs : [msgs];
948 lineNums.forEach(lineNum => {
949 const lineId = "sline" + lineNum;
950 const lineHTML = (editor) ? editor.querySelector(`#${lineId}`) : document.getElementById(lineId);
953 lineHTML.classList.add("msl_current_line");
954 if (color) lineHTML.style.backgroundColor = color;
955 if (msgs[counter]) lineHTML.title = msgs[counter];
956 // Scroll to the highlighted line if the checkbox is checked
957 const scrollToCurrCheckbox = document.getElementById("scrollToCurr");
958 if (((scrollToCurrCheckbox && scrollToCurrCheckbox.checked) || editor) && (counter == 0)) {
959 lineHTML.scrollIntoView({ block: "center" });
966// Scroll to make line appear in the center of editor
967function scrollToCurr(scrToCur) {
968 if (scrToCur.checked) {
969 localStorage.setItem("scrollToCurr",true);
970 const currLine = document.querySelector(".msl_current_line");
972 currLine.scrollIntoView({ block: "center" });
975 localStorage.removeItem("scrollToCurr",true);
979// Open files in new tabs
980function toggleCheck(e) {
982 localStorage.setItem(e.id,true);
984 localStorage.removeItem(e.id);
988// shortcut event handling to overtake default behaviour
989function shortCutEvent(event) {
990 const parEditor = editorElements();
991 const notFirstTab = (parEditor[1].id !== "editorTab1");
993 // Check these only for editors
994 if (event.altKey && event.key === 's') {
995 event.preventDefault();
996 //save as with file_picker
998 event.preventDefault();
999 } else if ((event.ctrlKey || event.metaKey) && event.key === 's') {
1000 event.preventDefault();
1001 //save to the same filename
1003 event.preventDefault();
1004 } else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
1005 event.preventDefault();
1006 undoEdit(event.target);
1007 event.preventDefault();
1008 } else if ((event.ctrlKey || event.metaKey) && event.key === 'r') {
1009 event.preventDefault();
1010 redoEdit(event.target);
1011 event.preventDefault();
1015 // Check these only for first tab
1016 if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
1017 // open new tab and load current sequence
1018 event.preventDefault();
1019 addETab(document.getElementById("addETab"));
1020 seqOpen(parEditor[3].title.split("\n")[0]);
1021 event.preventDefault();
1024 // Check these for all tabs
1025 if (event.altKey && event.key === 'n') {
1027 event.preventDefault();
1028 addETab(document.getElementById("addETab"));
1029 event.preventDefault();
1030 } else if (event.altKey && event.key === 'o') {
1031 event.preventDefault();
1033 event.preventDefault();
1038// Trigger syntax highlighting on keyup events
1039function checkSyntaxEventUp(event) {
1040 if (event.ctrlKey || event.altKey || event.metaKey || MetaCombo) return;
1041 if (event.keyCode >= 0x30 || event.key === ' '
1042 || event.key === 'Backspace' || event.key === 'Delete'
1043 || event.key === 'Enter'
1046 const e = event.target;
1047 let caretPos = getCurrentCursorPosition(e);
1048 let currText = e.innerText;
1049 // Indentation keywords
1050 const defIndent = mslDefs.defIndent;
1051 const nIndent = defIndent.nIndent || 3;
1052 // save current revision for undo
1053 if (event.key === ' ' || event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
1054 saveState(currText,e);
1058 // Indent according to previous line
1059 if (event.key === 'Enter') {
1060 event.preventDefault();
1061 // get previous and current line elements (before and after enter)
1062 let pline = whichLine(e,-1);
1063 let cline = whichLine(e);
1064 let plineText = (pline) ? pline.innerText : "";
1065 let clineText = (cline) ? cline.innerText : "";
1066 let indentLevel = 0;
1068 let indentString = "";
1070 // indent line according to the previous line text
1071 // if, loop, else, subroutine - increase indentation
1072 const indentPlus = defIndent.indentplus.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1073 // else, endif, endloop, endsubroutine - decrease indentation
1074 const indentMinos = defIndent.indentminos.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1075 /* (indentMinos/indentPlus)
1076 true/false - pline indent -1, cline indent 0
1077 fale/true - pline indent 0, cline indent +1
1078 true/true - pline indent -1, cline indent +1
1079 false/false- pline indent 0, cline indent 0
1081 // Count number of white spaces at begenning of pline
1082 preSpace = plineText.replace("\n","").search(/\S|$/);
1083 pPreSpace = preSpace - indentMinos * nIndent;
1084 if (pPreSpace < 0) pPreSpace = 0;
1085 cPreSpace = pPreSpace + indentPlus * nIndent;
1086 // Calculate and insert indentation
1087 pIndentString = " ".repeat(pPreSpace);
1088 cIndentString = " ".repeat(cPreSpace);
1089 cline.innerText = cIndentString + clineText.trimStart();
1090 caretPos += cline.innerText.length - clineText.length;
1091 pline.innerText = pIndentString + plineText.trimStart();
1092 caretPos += pline.innerText.length - plineText.length;
1094 event.preventDefault();
1096 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
1097 setCurrentCursorPosition(e, caretPos);
1098 updateLineNumbers(e.previousElementSibling,e);
1104// Handle arrow up/down keys in Chrome
1105function arrowKeysChrome(event) {
1106 // Skip combos with special keys
1107 if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
1108 if ((event.key === "ArrowUp" || event.key === "ArrowDown") && event.shiftKey ) {
1109 event.preventDefault();
1110 startChromeSelecting(event.target);
1111 } else if ((event.key === "ArrowUp" || event.key === "ArrowDown") && !event.shiftKey ) {
1112 event.preventDefault();
1113 const e = event.target;
1114 let caretPos = getCurrentCursorPosition(e);
1115 let ncaretPos = caretPos;
1116 let currText = e.innerText;
1117 let lines = currText.split("\n");
1118 // Determine the current line number (0-based index)
1119 let clineNum = getLinesInSelection(e)[0] - 1;
1120 // Determine the direction: -1 for up, +1 for down
1121 let addLine = (event.key === "ArrowDown") ? 1 : -1;
1122 // Determine the target line number
1123 let nlineNum = clineNum + addLine;
1124 if (lines[nlineNum] !== undefined) {
1125 // line exists can move cursor
1126 let clineStart = currText.split("\n").slice(0, clineNum).join("\n").length + (clineNum > 0 ? 1 : 0);
1127 let caretInLine = caretPos - clineStart;
1129 let nlength = lines[nlineNum].length;
1130 let nlineStart = currText.split("\n").slice(0, nlineNum).join("\n").length + (nlineNum > 0 ? 1 : 0);
1131 if (nlength >= caretInLine && caretInLine >= 0) {
1132 // If the target line is longer or equal to the caret's position in the current line
1133 ncaretPos = nlineStart + caretInLine;
1134 } else if (caretInLine < 0) {
1135 ncaretPos = nlineStart + nlength + 1;
1137 // If the target line is shorter than the caret's position in the current line
1138 ncaretPos = nlineStart + nlength;
1140 setCurrentCursorPosition(e, ncaretPos);
1146// Trigger syntax highlighting on keydown events
1147function checkSyntaxEventDown(event) {
1148 sessionStorage.setItem("keydown",event.target.innerText);
1149 // take care of Mac odd keyup behaviour
1150 if (event.metaKey && (/^[a-z]$/.test(event.key) || event.shiftKey || event.altKey)) {
1156 if (event.ctrlKey || event.altKey || event.metaKey) return;
1157 // Quickly return for anything but these keys
1158 if (event.key !== 'Tab' && event.key !== 'Escape' && event.key !== 'Backspace' && event.key !== 'Delete' && event.key !== 'Enter') return;
1159 //if (event.key !== 'Tab' && event.key !== 'Escape' && event.key !== 'Enter') return;
1160 const e = event.target;
1161 event.preventDefault();
1162 const textSelected = isTextSelected(e);
1163 if (textSelected && event.key !== 'Tab' && event.key !== 'Escape') {
1164 deleteSelectedText(e);
1166 let caretPos = getCurrentCursorPosition(e);
1167 let currText = e.innerText;
1169 if (event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
1170 //if (event.key === 'Enter') {
1171 // Handle Backspace, Delete and Enter manually for better compatibility and control
1172 if (event.key === 'Enter') {
1173 currText = currText.substring(0,caretPos) + "\n" + currText.substring(caretPos);
1174 caretPos = caretPos + 1 <= currText.length ? caretPos + 1 : caretPos;
1175 if (currText.substring(caretPos) == "" && isChrome === 1) currText = currText + "\n";
1176 } else if (event.key === 'Backspace') {
1177 currText = currText.substring(0,caretPos).slice(0,-1) + currText.substring(caretPos);
1178 caretPos = caretPos - 1 >= 0 ? caretPos - 1 : caretPos;
1179 } else if (event.key === 'Delete') {
1181 currText = currText.substring(0,caretPos) + currText.substring(caretPos).slice(0);
1183 currText = currText.substring(0,caretPos) + currText.substring(caretPos).slice(1);
1186 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
1187 setCurrentCursorPosition(e, caretPos);
1188 keepCaretVisible(e);
1190 } else if (event.key === 'Tab' || event.key === 'Escape') {
1191 let lines = getLinesInSelection(e);
1192 if (event.shiftKey && event.key === 'Tab') {
1193 indent_msl(lines,-1);
1194 } else if (event.key === 'Tab') {
1195 indent_msl(lines,+1);
1196 } else if (event.key === 'Escape') {
1199 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
1200 let newText = e.innerText;
1201 setCurrentCursorPosition(e, caretPos + newText.length - currText.length);
1202 if (lines[0] !== lines[1]) selectLines(lines,e);
1207// Trigger syntax highlighting when you paste text
1208function checkSyntaxEventPaste(event) {
1209 // set time out to allow default to go first
1211 let e = event.target;
1212 // make sure you paste in the editor area
1213 if (e.tagName !== 'PRE') e = e.parentElement;
1214 let caretPos = getCurrentCursorPosition(e);
1215 let currText = e.innerText;
1216 // save current revision for undo
1217 saveState(currText,e);
1218 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
1219 setCurrentCursorPosition(e, caretPos);
1220 updateLineNumbers(e.previousElementSibling,e);
1226// Find on which line is the current carret position in e
1227// This assumes each line has an id="sline#" where # is the line number.
1228function whichLine(e,offset = 0) {
1229 // offset allows to pick previous line (after enter)
1230 let pos = getCurrentCursorPosition(e);
1232 let lineNum = e.innerText.substring(0,pos).split("\n").length + offset;
1233 let sline = e.querySelector("#sline" + lineNum.toString());
1237// Return an array with the first and last line numbers of the selected region
1238/* This assumes that the lines are in a <pre> element and that
1239 each line has an id="sline#" where # is the line number.
1240 When the caret in in an empty line, the anchorNode is the <pre> element.
1242function getLinesInSelection(e) {
1243 const selection = window.getSelection();
1244 if (selection.rangeCount === 0) return [0,0];
1245 // is it a single line?
1246 const singleLine = selection.isCollapsed;
1248 const line = whichLine(e);
1250 const startLine = parseInt(line.id.replace("sline",""));
1251 return [startLine,startLine];
1256 const anchorNode = selection.anchorNode;
1257 const range = selection.getRangeAt(0);
1258 let startNode,endNode;
1259 if (anchorNode.tagName === 'PRE') {
1260 let startOffset = range.startOffset;
1261 let endOffset = range.endOffset;
1262 startNode = range.startContainer.childNodes[startOffset];
1263 endNode = range.startContainer.childNodes[endOffset-1];
1265 startNode = (range.startContainer && range.startContainer.parentElement.tagName !== 'PRE') ? range.startContainer : range.startContainer.nextSibling;
1266 if (startNode && startNode.tagName === 'PRE') startNode = startNode.firstChild;
1267 endNode = (range.endContainer && range.endContainer.parentElement.tagName !== 'PRE') ? range.endContainer : range.endContainer.previousSibling;
1268 if (endNode && endNode.tagName === 'PRE') endNode = endNode.lastChild;
1270 let startID = (startNode && startNode.id) ? startNode.id : "";
1271 let endID = (endNode && endNode.id) ? endNode.id : "";
1273 while (startNode && !startID.startsWith("sline") && startNode.tagName !== 'PRE') {
1274 startNode = (startNode.parentNode.tagName !== 'PRE') ? startNode.parentNode : startNode.nextSibling;
1275 startID = (startNode && startNode.id) ? startNode.id : "";
1278 while (endNode && !endID.startsWith("sline") && endNode.tagName !== 'PRE') {
1279 endNode = (endNode.parentNode.tagName !== 'PRE') ? endNode.parentNode : endNode.previousSibling;
1280 endID = (endNode && endNode.id) ? endNode.id : "";
1282 let startLine = (startNode && startNode.id) ? parseInt(startNode.id.replace("sline","")) : 0;
1283 let endLine = (endNode && endNode.id) ? parseInt(endNode.id.replace("sline","")) : 0;
1285 startLine = endLine = Math.min(startLine, endLine);
1287 return [startLine,endLine];
1290// get current caret position in chars within element parent
1291function getCurrentCursorPosition(parent) {
1292 let sel = window.getSelection();
1293 if (!sel.focusNode || !parent) return;
1294 const range = sel.getRangeAt(0);
1295 const prefix = range.cloneRange();
1296 prefix.selectNodeContents(parent);
1297 prefix.setEnd(range.endContainer, range.endOffset);
1298 return prefix.toString().length;
1301// set current caret position at pos within element parent
1302function setCurrentCursorPosition(parent,pos) {
1303 for (const node of parent.childNodes) {
1304 if (node.nodeType === Node.TEXT_NODE) {
1305 if (node.length >= pos) {
1306 const range = document.createRange();
1307 const sel = window.getSelection();
1308 range.setStart(node, pos);
1309 range.collapse(true);
1310 sel.removeAllRanges();
1311 sel.addRange(range);
1314 pos = pos - node.length;
1317 pos = setCurrentCursorPosition(node, pos);
1326// Update line numbers in lineNumbers div
1327function updateLineNumbers(lineNumbers,editor) {
1328 if (lineNumbers === undefined || editor === undefined)
1329 [lineNumbers,editor] = editorElements();
1330 // Clear existing line numbers
1331 lineNumbers.innerHTML = "";
1332 // Get the number of lines accurately
1333 let lineCount = editor.querySelectorAll('[id^="sline"]').length;
1334 let lineTextCount = editor.innerText.split("\n").length;
1335 lineCount = (lineTextCount - lineCount) < 2 ? lineTextCount : lineTextCount - 1;
1336 // Add line numbers to lineNumbers
1337 for (let i = 1; i <= lineCount; i++) {
1338 const lineNumber = document.createElement('span');
1339 lineNumber.id = "lNum" + i.toString();
1340 lineNumber.textContent = i;
1341 lineNumbers.appendChild(lineNumber);
1343 lineNumbers.scrollTop = editor.scrollTop;
1347// Utility function to escape special characters in a string for use in a regular expression
1348function escapeRegExp(s) {
1350 return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
1353function syntax_msl(seqLines, keywordGroups) {
1354 // Use default if none provided
1355 if (!keywordGroups) {
1356 keywordGroups = mslDefs.groups;
1360 let mslText = Array.isArray(seqLines) ? seqLines.join("\n") : seqLines;
1361 mslText = escapeSpecialCharacters(mslText);
1362 const seqLines_org = mslText.split(/\n/);
1364 // Loop over all keyword groups
1365 for (const [groupName, group] of Object.entries(keywordGroups)) {
1366 const className = group.mslclass;
1367 const spanStart = `<span class='${className}'>`;
1368 const spanEnd = `</span>`;
1370 // If kwregex and keywords are defined
1371 if (group.keywords && group.kwregex && group.kwregex.length === 3) {
1372 const [prefix, suffix, flags] = group.kwregex;
1373 const regexStr = prefix + group.keywords.join("|") + suffix;
1374 const regex = new RegExp(regexStr, flags);
1376 mslText = mslText.replace(regex, (...args) => {
1377 const match = args[0];
1378 const captureGroups = args.slice(1, -2); // remove offset & input
1379 if (captureGroups.length === 0) {
1380 return `${spanStart}${match}${spanEnd}`;
1382 // Wrap last capturing group
1387 for (let i = 0; i < captureGroups.length; i++) {
1388 const part = captureGroups[i];
1389 if (part === undefined) continue;
1390 const idx = raw.indexOf(part, lastIndex);
1391 result += raw.slice(lastIndex, idx);
1392 if (i === captureGroups.length - 1) {
1393 result += `${spanStart}${part}${spanEnd}`;
1397 lastIndex = idx + part.length;
1399 result += raw.slice(lastIndex);
1404 // If explicit regex patterns are provided
1406 group.regex.forEach(r => {
1408 const flags = r.flags || "g";
1409 const regex = new RegExp(r.pattern, flags);
1410 if (groupName === 'multilineComments') {
1411 mslText = mslText.replace(regex, (fullMatch) => {
1412 // Split the full match by lines
1413 const lines = fullMatch.split('\n');
1414 // Process each line individually
1415 const processedLines = lines.map(line => {
1416 return `${spanStart}${line}${spanEnd}`;
1418 // Join back with newlines
1419 return processedLines.join('\n');
1422 mslText = mslText.replace(regex, `${spanStart}$&${spanEnd}`);
1429 // Split processed lines
1430 seqLines = mslText.split("\n");
1432 // Firefox / Chrome handling
1433 const emptyClass = (typeof isChrome !== 'undefined' && isChrome === 1) ? "esline" : "";
1435 for (let j = 0; j < seqLines_org.length; j++) {
1436 const line = seqLines_org[j];
1437 // Add span wrapper for editor tracking
1439 const id = `sline${j + 1}`;
1440 seqLines[j] = `<span class='${emptyClass}' id='${id}'></span>`;
1442 const id = `sline${j + 1}`;
1443 seqLines[j] = `<span class='sline' id='${id}'>${seqLines[j]}</span>`;
1450// Syntax highlight any text according to provided rules
1451function syntax_msl2(seqLines,keywordGroups) {
1452 // If not provided use the default msl keywords and groups
1453 if (!keywordGroups) {
1455 keywordGroups = mslDefs.groups;
1458 // Make full text if you get an array of lines
1459 let mslText = Array.isArray(seqLines) ? seqLines.join("\n") : seqLines;
1460 // Make some cleanup of illegal characters
1461 mslText = escapeSpecialCharacters(mslText);
1462 // Keep original sequence lines (as array)
1463 let seqLines_org = mslText.split(/\n/);
1464 let nLines = (mslText.match(/\n/g) || []).length;
1466 // These can be done on the text in one go
1468 let reg = /(["'])(.*?)\1/g;
1469 mslText = mslText.replace(reg,'<span class="msl_string">$1$2$1</span>');
1472 //reg = /^(COMMENT|#.*?)(.*)$/gim;
1473 //mslText = mslText.replace(reg,'<span class="msl_comment">$&</span>');
1476 reg = /(?:\$[\w]+|^\b\w+(?=\s*=))/gm; // starting with $ or something =
1477 mslText = mslText.replace(reg,'<span class="msl_variable">$&</span>');
1478 reg = new RegExp("(^(?:\\s*)\\b(" + keywordGroups.variables.keywords.join("|") + ")\\s+)(\\w+)\\b", "gim"); // after PARAM, CAT and SET
1479 mslText = mslText.replace(reg,'$1<span class="msl_variable">$3</span>');
1481 // Data Management group excluding variables (must be after variables)
1482 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1483 mslText = mslText.replace(reg, "$1<span class='msl_data_management'>$2</span>");
1485 // Data Type group (must have comma before the keyword)
1486 reg = new RegExp("(?<=,\\s*)\\b(" + keywordGroups.dataTypes.keywords.join("|") + ")\\b", "gim");
1487 mslText = mslText.replace(reg, "<span class='msl_data_types'>$1</span>");
1489 // Loops group (must be at the begenning of the line)
1490 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.loops.keywords.join("|") + ")\\b", "gim");
1491 mslText = mslText.replace(reg, "$1<span class='msl_loops'>$2</span>");
1493 // Control Flow group
1494 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.controlFlow.keywords.join("|") + ")\\b", "gim");
1495 mslText = mslText.replace(reg, "$1<span class='msl_control_flow'>$2</span>");
1497 // Data Management group
1498 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1499 mslText = mslText.replace(reg, "$1<span class='msl_data_managemen'>$2</span>");
1501 // Information group
1502 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.info.keywords.join("|") + ")\\b", "gim");
1503 mslText = mslText.replace(reg, "$1<span class='msl_info'>$2</span>");
1505 // Conditional group
1506 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.cond.keywords.join("|") + ")\\b", "gim");
1507 mslText = mslText.replace(reg,"$1<span class='msl_cond'>$2</span>");
1510 reg = new RegExp("\\b(" + keywordGroups.units.keywords.join("|") + ")\\b", "gi");
1511 mslText = mslText.replace(reg, "<span class='msl_units'>$1</span>");
1514 reg = new RegExp("\\b(" + keywordGroups.actions.keywords.join("|") + ")\\b(\\s*)$", "gim");
1515 mslText = mslText.replace(reg, "<span class='msl_actions'>$1</span>$2");
1517 // Numbers/boolean group
1518 reg = /\b(\d+(\.\d+)?([eE][-+]?\d+)?)\b/g;
1519 mslText = mslText.replace(reg, '<span class="msl_number">$1</span>');
1520 reg = new RegExp("\\b(" + keywordGroups.bool.keywords.join("|") + ")\\b", "gi");
1521 //reg = /\b(true|false)\b/gi;
1522 mslText = mslText.replace(reg, '<span class="msl_bool">$1</span>');
1524 // Break lines and handle one by one
1525 seqLines = mslText.split("\n");
1527 // This is important for Firefox
1528 let emptyClass = "";
1529 if (isChrome === 1) emptyClass = "esline";
1530 // Loop and restore comment lines and empty lines
1531 for (let j = 0; j < seqLines_org.length ; j++) {
1532 let line = seqLines_org[j];
1533 let inlineComment = mslDefs.groups.comments.keywords[0];
1534 commentIndex = line.indexOf(inlineComment);
1535 if (line.trim().startsWith(inlineComment) || line.trim().toLowerCase().startsWith("comment")) {
1536 // Restore comment lines without highlighting
1537 seqLines[j] = `<span class='msl_comment'>${line}</span>`;
1538 } else if (commentIndex > 0) {
1539 // Restore comment section at end of line
1540 const comment = line.slice(commentIndex);
1541 seqLines[j] = seqLines[j].slice(0, seqLines[j].indexOf("#")) + `</span><span class='msl_comment'>${comment}</span>`;
1544 // empty class is needed for cursor movement in Firefox
1545 // for Chrome empty lines are skipped with arrow up??
1547 if ((j === seqLines_org.length - 1) && (isChrome === 1)) {
1548 seqLines[j] = "<span class=' ' id='sline" + (j+1).toString() + "'></span>";
1549 } else if (isChrome === 1) {
1550 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1552 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1555 seqLines[j] = "<span class='sline' id='sline" + (j+1).toString() + "'>" + seqLines[j] + "</span>";
1561// Adjust indentation of a selection of lines
1562function indent_msl(lines,addTab) {
1564 lines - an array of two elements, first and last line numbers
1565 addTab - (opt) +/-1 to add/subtract three spaces to selected lines
1567 let indentLevel = 0;
1568 let singleLine = false;
1569 let editor = editorElements()[1];
1570 // Indentation keywords
1571 const defIndent = mslDefs.defIndent;
1572 const nIndent = defIndent.nIndent || 3;
1574 // Avoid issues of begenning of single line
1575 if (lines[0] > lines[1] || lines[0] == lines[1]) {
1576 lines[0] = lines[0] > 0 ? lines[0] : 1;
1577 lines[1] = lines[0];
1580 for (let j = lines[0]; j <= lines[1] ; j++) {
1581 let lineId = "#sline" + j.toString();
1582 let prevLineId = "#sline" + (j-1).toString();
1583 let lineEl = editor.querySelector(lineId);
1585 if (lineEl) line = lineEl.innerText;
1587 let indentString = " ".repeat(nIndent);
1588 lineEl.innerText = indentString + line;
1589 } else if (addTab === -1) {
1590 lineEl.innerText = line.replace(/^\s{1,nIndent}/, '');
1591 } else if (singleLine && editor.querySelector(prevLineId)) {
1592 let prevLineEl = editor.querySelector(prevLineId);
1593 let prevLine = prevLineEl.innerText;
1594 const indentMinos = defIndent.indentminos.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1595 const indentPlus = defIndent.indentplus.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1598 } else if (indentPlus) {
1601 let preSpace = prevLine.search(/\S|$/) + (indentLevel * nIndent);
1602 if (preSpace < 0) preSpace = 0;
1603 let indentString = " ".repeat(preSpace);
1604 lineEl.innerText = indentString + line.trimStart();
1606 const indentMinos = defIndent.indentminos.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1607 if (indentMinos && indentLevel > 0) indentLevel--;
1608 let indentString = " ".repeat(indentLevel * nIndent);
1609 if (line !== "" || indentString !== "") {
1610 lineEl.innerText = indentString + line.trimStart();
1612 const indentPlus = defIndent.indentplus.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1613 if (indentPlus) indentLevel++;
1618// Prepare the parameters/variables (if present) from the ODB as an html table
1619// Also return default ODB Paths and their respective values
1620function varTable(id,odbTreeVar) {
1622 id - ID of div to fill with the table of variables
1623 odbTreeVar - values of ${SeqODB}/Variables
1626 let e = document.getElementById(id);
1628 dlgAlert("Container ID was not give.");
1632 let old_var_names = [];
1633 let new_var_names = [];
1634 document.querySelectorAll(".varname").forEach(el => old_var_names.push(el.innerText));
1636 // If ${SeqODB}/Variables are empty return empty
1637 if (!odbTreeVar || (odbTreeVar && Object.keys(odbTreeVar).length === 0)) {
1638 // Clear container row
1643 let html = "<table id='varTable' class='mtable infotable'>\n";
1644 html += "<tr><th style='min-width: 120px'>Variable </th><th>Current value </th></tr>\n";
1646 // Go over all variables in ODB and count them
1647 for (let key in odbTreeVar) {
1648 const match = key.match(/([^/]+)\/name$/);
1650 const name = match[1];
1651 const value = odbTreeVar[name];
1652 new_var_names.push(name);
1653 let isBool = (typeof(value) === "boolean");
1655 html += `<tr><td class="varname">${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="${SeqODB}/Variables/${name}"></span></td></tr>\n`;
1657 html += `<tr><td class="varname">${name}</td><td><span class="modbvalue" data-odb-path="${SeqODB}/Variables/${name}"></span></td></tr>\n`;
1662 let have_new_var = false;
1663 if (old_var_names.length !== new_var_names.length) {
1664 have_new_var = true;
1666 for (let i = 0; i < old_var_names.length; i++) {
1667 if (old_var_names[i] != new_var_names[i]) {
1668 have_new_var = true;
1678// Prepare the parameters/variables (if present) from the ODB as an object with
1679// html table, default ODB Paths, their respective values and their number
1680function parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq) {
1682 odbTree... - Objects of ODB values
1685 let odbDefPaths = [];
1686 let odbDefValues = [];
1689 html += "<table id='paramTable' class='mtable infotable'>";
1690 html += "<tr><th>Parameter </th><th>Initial value </th><th>Comment</th></tr>";
1692 const processParam = (name, value, isBool, defValue, optValue, comment) => {
1693 let parLine = `<tr><td>${name}</td>`;
1696 // if not given the default is the first option
1697 if (defValue === undefined || defValue === "") defValue = optValue[0];
1698 const optionsHtml = optValue.map(option => `<option value="${option}" ${option === defValue ? 'selected' : ''}>${option}</option>`).join('');
1699 inParLine += `<select class="modbselect" data-odb-path="${SeqODB}/Param/Value/${name}" data-odb-editable="1">${optionsHtml}</select>`;
1700 } else if (isBool) {
1701 inParLine += `<input type="checkbox" class="modbcheckbox" data-odb-path="${SeqODB}/Param/Value/${name}" data-odb-editable="1"></input>`;
1703 inParLine += `<span class="modbvalue" data-odb-path="${SeqODB}/Param/Value/${name}" data-odb-editable="1" data-input="1"></span>`;
1705 if (defValue !== undefined) {
1707 odbDefPaths.push(`${SeqODB}/Param/Value/${name}`);
1708 odbDefValues.push(defValue);
1711 parLine += `<td>${inParLine}</td>`;
1712 parLine += `<td>${comment}</td></tr>`;
1716 // Go over all parameters in ODB
1717 for (let key in odbTreeV) {
1718 const match = key.match(/([^/]+)\/name$/);
1720 const name = match[1];
1721 // if variable is found use its value
1722 let value = (odbTreeVar && odbTreeVar[name]) ? odbTreeVar[name] : odbTreeV[name];
1723 let isBool = (typeof(value) === "boolean");
1724 let defValue = (value !== null && value !== undefined && value !== '') ? value : (odbTreeD && odbTreeD[name]) || value;
1725 let optValue = odbTreeO ? odbTreeO[name] : undefined;
1726 let comment = odbTreeC[name] || '';
1727 if (typeof value !== "object") {
1728 processParam(name, value, isBool, defValue, optValue, comment);
1733 // Go over Edit on sequence links
1734 for (let key in editOnSeq) {
1735 const match = key.match(/([^/]+)\/name$/);
1737 const name = match[1];
1738 const value = editOnSeq[name];
1739 let isBool = (typeof(value) === "boolean");
1741 html += `<tr><td>${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="/Experiment/Edit on sequence/${name}" data-odb-editable="1"></input></td><td></td></tr>\n`;
1743 html += `<tr><td>${name}</td><td><span class="modbvalue" data-odb-path="/Experiment/Edit on sequence/${name}" data-odb-editable="1" data-input="1"></span></td><td></td></tr>\n`;
1752 values: odbDefValues,
1757function dlgParam(debugFlag) {
1759 debugFlag - (opt) true/false run in debug/normal mode
1762 // Ensure we read the latest Params from the ODB, in case we just loaded a new file
1763 let paths = [`${SeqODB}/Param`, `${SeqODB}/Variables`];
1765 mjsonrpc_db_get_values(paths).then(function (rpc) {
1766 // Try to use the very latest values from ODB when building
1767 // the parameters table.
1768 // `dlgParamMain()` reads from sessionStorage, so we need to write
1769 // the latest values there.
1770 // Normally sessionStorage is updated by `mhttpd_refresh()` (when it
1771 // updates "modbvalue" spans in the HTML), but we don't want to hang
1772 // around waiting for the next update to happen, so we just update it
1775 const status = rpc?.result?.status ?? [];
1776 const data = rpc?.result?.data ?? [];
1778 if (status[0] === 1) {
1779 sessionStorage.setItem("parameters", JSON.stringify(data[0]));
1782 if (status[1] === 1) {
1783 sessionStorage.setItem("variables", JSON.stringify(data[1]));
1785 dlgParamMain(debugFlag);
1786 }).catch(function (error) {
1787 console.error(error);
1791// Prepare the parameters/variables (if present) from the ODB as a table
1792function dlgParamMain(debugFlag) {
1794 debugFlag - (opt) true/false run in debug/normal mode
1797 let odbTree = JSON.parse(sessionStorage.getItem('parameters') !== 'undefined' ? sessionStorage.getItem('parameters') : "null");
1798 const editOnSeq = JSON.parse(sessionStorage.getItem('editonseq') !== 'undefined' ? sessionStorage.getItem('editonseq') : "null");
1799 // If ${SeqODB}/Param are empty, start and return
1800 if ((odbTree === null || Object.keys(odbTree.value).length === 0) && editOnSeq === null ) {
1801 //if ((odbTree === null || Object.keys(odbTree).length) && (editOnSeq === null || Object.keys(editOnSeq).length)) {
1803 modbset(`${SeqODB}/Command/Debug script`,true);
1805 modbset(`${SeqODB}/Command/Start script`,true);
1810 let odbTreeV = null;
1811 let odbTreeC = null;
1812 let odbTreeD = null;
1813 let odbTreeO = null;
1814 let odbTreeVar = null;
1817 odbTreeV = odbTree.value;
1818 odbTreeC = odbTree.comment;
1819 odbTreeD = odbTree.defaults;
1820 odbTreeO = odbTree.options;
1821 odbTreeVar = JSON.parse(sessionStorage.getItem('variables'));
1824 // Go over all parameters in ODB
1825 let seqParTable = parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq);
1826 let html = seqParTable.html;
1827 // set all default values and once finished produce dialog
1828 // Collect paths where values start with "/"
1829 let valuesLinkODB = seqParTable.values.map((valueLinkODB, indexLinkODB) => {
1830 if (valueLinkODB !== undefined && typeof(valueLinkODB) === "string") {
1831 if (valueLinkODB.startsWith("/")) {
1832 return seqParTable.values[indexLinkODB];
1836 }).filter(path => path !== null);
1837 mjsonrpc_db_get_values(valuesLinkODB).then(function (rpc) {
1838 if (rpc.result.status.every(status => status === 1)) {
1839 // substitute values
1840 rpc.result.data.forEach((newData, index) => {
1841 let pathIndex = seqParTable.values.indexOf(valuesLinkODB[index]);
1842 seqParTable.values[pathIndex] = `${newData}`; // Update corresponding value
1844 mjsonrpc_db_paste(seqParTable.paths,seqParTable.values).then(function (rpc) {
1845 if ((rpc.result.status.every(status => status === 1)) || seqParTable.values.length === 0) {
1846 // if parContainer not given produce a dialog
1847 let htmlDlg = `${html}<br><button class="dlgButtonDefault" id="dlgParamStart" type="button">Start</button><button class="dlgButton" id="dlgParamCancel" type="button">Cancel</button><br>`;
1848 let d = dlgGeneral({html: htmlDlg,iddiv: "Parameters",minWidth:500});
1849 let e = document.getElementById("parContainer");
1850 // Append the table to a container
1851 let startBtn = document.getElementById("dlgParamStart");
1852 let cancelBtn = document.getElementById("dlgParamCancel");
1853 cancelBtn.addEventListener("click", function () {d.remove();});
1854 startBtn.addEventListener("click", function () {
1857 modbset(`${SeqODB}/Command/Debug script`,true);
1859 modbset(`${SeqODB}/Command/Start script`,true);
1863 // refresh immediately modbvalue elements
1866 dlgAlert("Something went wrong. Please try again!");
1868 }).catch(function (error) {
1869 console.error(error);
1872 let message = `ODB "${valuesLinkODB}" was not found.<br>Cannot start sequence!`;
1875 }).catch(function (error) {
1876 console.error(error);
1881// helper debug function
1882function debugSeq(parContainer) {
1883 startSeq(parContainer,true);
1886// helper start function
1887function startSeq(parContainer,debugFlag) {
1888 const [lineNumbers,editor,label] = editorElements();
1889 if (!debugFlag) debugFlag = false;
1890 if (editor.id !== "editorTab1" && parContainer === undefined) {
1891 let filename = label.title;
1893 dlgAlert("Please give the file a name first (Save as).");
1896 const message = debugFlag ? `Save and debug ${filename}?` : `Save and start ${filename}?`;
1897 dlgConfirm(message,function(resp) {
1900 openETab(document.getElementById("etab1-btn"));
1902 // Make sure to load file and reset parameters
1903 mjsonrpc_db_paste([`${SeqODB}/Command/Load new file`],[true]).then(function (rpc) {
1904 if (rpc.result.status[0] === 1) {
1905 // Wait for Load new file to turn false
1906 checkODBValue(`${SeqODB}/Command/Load new file`,false,dlgParam,debugFlag);
1912 // make sure to load file first
1913 mjsonrpc_db_paste([`${SeqODB}/Command/Load new file`],[true]).then(function (rpc) {
1914 if (rpc.result.status[0] === 1) {
1915 // Wait for Load new file to turn false
1916 checkODBValue(`${SeqODB}/Command/Load new file`,false,dlgParam,debugFlag);
1922// Helper function to add the current file to next files queue
1923function setAsNext() {
1924 const [lineNumbers,editor,label] = editorElements();
1925 // This is the addAsNext button cell
1926 const e = document.getElementById("addAsNext");
1927 let filename = label.title.split("\n")[0].replace(/^sequencer\//,'');
1928 let message = `Save and put ${filename} in the next file queue?`;
1929 dlgConfirm(message,function(resp) {
1931 let order = chngNextFilename(e,filename);
1932 if (order !== -1 && order !== undefined) {
1934 dlgAlert(`File saved and placed in position ${order} in the queue.`);
1940// helper stop function
1942 const message = `Are you sure you want to stop the sequence?`;
1943 dlgConfirm(message,function(resp) {
1945 modbset(`${SeqODB}/Command/Stop immediately`,true);
1950// Show or hide parameters table
1951function showParTable(varContainer) {
1952 let e = document.getElementById(varContainer);
1953 let varTable = document.getElementById("varTable");
1954 let vis = document.getElementById("showParTable").checked;
1955 let visNF = (document.getElementById("nextFNContainer").style.display === "none");
1957 e.style.display = "flex";
1958 e.parentElement.style.width = "285px";
1960 e.style.display = "none";
1962 e.parentElement.style.width = "0px";
1968// Show or hide next file list
1969function showFNTable(nextFNContainer) {
1970 let e = document.getElementById(nextFNContainer);
1971 let vis = document.getElementById("showNextFile").checked;
1972 let visVar = document.getElementById("varContainer") ? (document.getElementById("varContainer").style.display === "none") : false;
1974 let addFileRow = "";
1976 let odbpath = mslDefs.filename.next;
1977 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
1978 if (rpc.result.status[0] !== 1) return;
1979 let fList_org = rpc.result.data[0];
1980 // Move full slots to the begenning of the array
1981 fList = fList_org.filter(Boolean);
1982 fList = [...fList, ...new Array(10 - fList.length).fill("")];
1983 // If the cleaned up not equal to original update ODB
1984 if (fList !== fList_org) {
1985 mjsonrpc_db_paste([odbpath],[fList]).then(function (rpc2) {
1986 if (rpc2.result.status[0] !== 1) {
1987 dlgAlert(`${odbpath} needs manual cleanup!<br>Please check.`);
1989 }).catch(function (error) {
1990 console.error(error);
1993 for (let i = 0; i < fList.length; i++) {
1994 if (fList[i] && fList[i].trim() !== "") {
1995 html += `<tr class="dragrow" draggable="true"><td style="cursor: all-scroll;"><img draggable="false" style="cursor: all-scroll;" src="icons/menu.svg" title="Drag and drop to reorder"></td><td><img draggable="false" src="icons/folder-open.svg" title="Change file" onclick="chngNextFilename(this.parentElement);"></td><td style="cursor: all-scroll;" title="Drag and drop to reorder" ondblclick="chngNextFilename(this);">${fList[i]}</td><td><img draggable="false" title="Remove file from list" onclick="remNextFilename(this.parentElement);" src="icons/trash-2.svg"></td></tr>`;
1999 if (vis && html !== "") {
2000 e.style.display = "flex";
2001 e.parentElement.style.width = "285px";
2003 disableMenuItems("SetAsNext",false);
2004 addFileRow = `<tr><td id="addAsNext"><img src="icons/file-plus.svg" title="Add file" onclick="chngNextFilename(this.parentElement);"></td><td></td><td></td><td></td></tr>`;
2006 disableMenuItems("SetAsNext",true);
2008 e.innerHTML = `<table class="mtable infotable"><tr><th style="width:10px;"></th><th style="width:10px;"></th><th>Next files</th><th style="width:10px;"></th></tr>${html}${addFileRow}</table>`;
2011 e.style.display = "none";
2013 e.parentElement.style.width = "0px";
2017 activateDragDrop(e);
2018 }).catch (function (error) {
2019 console.error(error);
2023// Show extra rows for wait and loop
2024function extraRows(e) {
2026 e - triggering element to identify wait or loop
2028 // get current row, table and dialog
2029 let rIndex = e.parentElement.parentElement.rowIndex;
2030 let table = e.parentElement.parentElement.parentElement;
2031 let progressDlg = table.parentElement.parentElement.parentElement.parentElement.parentElement;
2032 // check if there is a wait or loop commands (if non-zero)
2035 if (e.id === "waitTrig") {
2036 // Make sure there is only one wait row
2037 const waitTRs = document.querySelectorAll('.waitTR');
2038 const waitFormula = (e.value === "Seconds" || e.value === "ClientsRunning" || e.value === "ODBValueStability") ? 'data-formula="x/1000"' : '';
2039 if (waitTRs.length) waitTRs.forEach(element => element.remove());
2041 let tr = table.insertRow(rIndex+1);
2042 tr.className = "waitTR";
2043 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">
2044 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: lightgreen;" data-odb-path="${SeqODB}/State/Wait value" ${waitFormula} id="mwaitProgress"></span>
2045 <span class="waitlooptxt">
2046 Wait: [<span class="modbvalue" data-odb-path="${SeqODB}/State/Wait value" ${waitFormula}></span>/<span class="modbvalue" data-odb-path="${SeqODB}/State/Wait limit" ${waitFormula} onchange="if (document.getElementById('mwaitProgress')) document.getElementById('mwaitProgress').dataset.maxValue = this.value;"></span>] <span class="modbvalue" data-odb-path="${SeqODB}/State/Wait type"></span>
2050 } else if (e.id === "loopTrig") {
2051 mjsonrpc_db_get_values([`${SeqODB}/State/Loop n`]).then(function(rpc) {
2052 let loopArray = rpc.result.data[0];
2053 for (let i = 0; i < loopArray.length; i++) {
2054 if (loopArray[i] === 0) break;
2055 let tr = table.insertRow(rIndex+1);
2056 tr.className = "loopTR";
2057 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">
2058 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: #CBC3E3;" data-odb-path="${SeqODB}/State/Loop counter[${i}]" id="mloopProgress${i}"></span>
2059 <span class="waitlooptxt">
2060 Loop ${i}: [<span class="modbvalue" data-odb-path="${SeqODB}/State/Loop counter[${i}]"></span>/<span class="modbvalue" data-odb-path="${SeqODB}/State/Loop n[${i}]" onchange="if (document.getElementById('mloopProgress${i}')) document.getElementById('mloopProgress${i}').dataset.maxValue = this.value;"></span>]
2065 }).catch (function (error) {
2066 console.error(error);
2071 if (e.id === "waitTrig") document.querySelectorAll('.waitTR').forEach(element => element.remove());
2072 if (e.id === "loopTrig") document.querySelectorAll('.loopTR').forEach(element => element.remove());
2073 // hide progress div
2074 //dlgHide(progressDlg);
2079// Helper function to identify browser, 1 FF, 2 Chrome, 3, other
2080function browserType() {
2081 if (navigator.userAgent.indexOf("Chrome") !== -1) {
2083 } else if (navigator.userAgent.indexOf("Firefox") !== -1) {
2090// make visual hint that file is changes
2091function seqIsChanged(flag) {
2092 // flag - true is change, false is saved
2093 let fileChangedId = "filechanged" + editorElements()[0].id.replace("lineNumbers","");
2094 let filechanged = document.getElementById(fileChangedId);
2097 filechanged.innerHTML = " ✎";
2098 } else if (flag === undefined) {
2099 // true if text has changed false if not
2100 return (filechanged.innerHTML !== "");
2102 filechanged.innerHTML = "";
2105 // also change validation icon to defaule
2106 let vldBtn = document.getElementById("vldBtn");
2107 vldBtn.src = "icons/validate-syntax.svg";
2108 vldBtn.style.backgroundColor = "";
2111// save history of edits in element editor
2112function saveState(mslText,editor) {
2113 editor = (editor) ? editor : editorElements()[1];
2114 const editorId = editor.id;
2115 if (saveRevision[editorId] === false) {
2117 } else if (saveRevision[editorId] === undefined){
2118 saveRevision[editorId] = true;
2121 if (!previousRevisions[editorId]) {
2122 previousRevisions[editorId] = [];
2123 revisionIndex[editorId] = -1;
2126 // Add one more revision, and trim array if we had some undos
2127 revisionIndex[editorId]++;
2128 if (revisionIndex[editorId] < previousRevisions[editorId].length - 1) {
2129 previousRevisions[editorId].splice(revisionIndex[editorId] + 1);
2131 // Push new revision and keep only nRevisions revisions
2132 previousRevisions[editorId].push(mslText)
2133 if (previousRevisions[editorId].length > nRevisions) {
2134 previousRevisions[editorId].shift();
2136 revisionIndex[editorId] = previousRevisions[editorId].length - 1;
2140function undoEdit(editor) {
2141 editor = (editor) ? editor : editorElements()[1];
2142 const editorId = editor.id;
2143 if (revisionIndex[editorId] === 0) {
2144 // disable menu item
2145 disableMenuItems("undoMenu",true);
2146 seqIsChanged(false);
2150 disableMenuItems("undoMenu",false);
2151 revisionIndex[editorId]--;
2154 disableMenuItems("redoMenu",false);
2156 let caretPos = getCurrentCursorPosition(editor);
2157 let currText = editor.innerText;
2158 saveRevision[editorId] = false;
2159 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
2160 updateLineNumbers(editor.previousElementSibling,editor);
2161 saveRevision[editorId] = true;
2162 // calculate change in caret position based on length
2163 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
2164 setCurrentCursorPosition(editor, caretPos);
2168function redoEdit(editor) {
2169 editor = (editor) ? editor : editorElements()[1];
2170 const editorId = editor.id;
2171 if (revisionIndex[editorId] >= previousRevisions[editorId].length - 1) {
2172 // disable menu item
2173 disableMenuItems("redoMenu",true);
2177 disableMenuItems("redoMenu",false);
2178 revisionIndex[editorId]++;
2181 disableMenuItems("undoMenu",false);
2184 let caretPos = getCurrentCursorPosition(editor);
2185 let currText = editor.innerText;
2186 saveRevision[editorId] = false;
2187 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
2188 updateLineNumbers(editor.previousElementSibling,editor);
2189 saveRevision[editorId] = true;
2190 // calculate change in caret position based on length
2191 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
2192 setCurrentCursorPosition(editor, caretPos);
2195// Select slines from startLine to endLine
2196function selectLines([startLine, endLine],e) {
2197 const selection = window.getSelection();
2198 // Remove existing selections
2199 selection.removeAllRanges();
2200 let startElementId = '#sline' + startLine;
2201 let endElementId = '#sline' + endLine;
2202 let startElement = null, endElement = null;
2203 if (e.querySelector(startElementId)) startElement = e.querySelector(startElementId).firstChild;
2204 if (e.querySelector(endElementId)) endElement = e.querySelector(endElementId).lastChild;
2205 // we need startElement and endElement with first/lastChild
2206 // the following prevents loosing selection but not ideal
2207 while (startElement === null && startLine <= endLine) {
2209 startElementId = '#sline' + startLine;
2210 startElement = e.querySelector(startElementId).firstChild;
2212 while (endElement === null && endLine > 0) {
2214 endElementId = '#sline' + endLine;
2215 endElement = e.querySelector(endElementId).lastChild;
2217 if (startElement && endElement) {
2218 const range = document.createRange();
2219 // Set the start of the range to the startElement at offset 0
2220 range.setStart(startElement, 0);
2221 // Set the end of the range to the endElement at its length
2222 range.setEnd(endElement, endElement.childNodes.length);
2223 // Add the range to the selection
2224 selection.addRange(range);
2228// switch between dark and light modes on request
2229function lightToDark(lToDcheck) {
2230 if (!lToDcheck) return;
2231 const edt_areas = document.querySelectorAll('.edt_area');
2232 if (lToDcheck.checked) {
2233 localStorage.setItem("darkMode", true);
2234 edt_areas.forEach(area => {
2235 area.style.backgroundColor = "black";
2236 area.style.color = "white";
2238 updateCSSRule(".etab button:hover","background-color","black");
2239 updateCSSRule(".etab button:hover","color","white");
2240 updateCSSRule(".etab button:hover","border-bottom","5px solid black");
2241 updateCSSRule(".etab button.edt_active","background-color","black");
2242 updateCSSRule(".etab button.edt_active","color","white");
2243 updateCSSRule(".etab button.edt_active","border-bottom","5px solid black");
2244 updateCSSRule(".etab button.edt_active:hover","background-color","black");
2245 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid black");
2247 localStorage.removeItem("darkMode");
2248 edt_areas.forEach(area => {
2249 area.style.backgroundColor = "white";
2250 area.style.color = "black";
2252 updateCSSRule(".etab button:hover","background-color","white");
2253 updateCSSRule(".etab button:hover","color","black");
2254 updateCSSRule(".etab button:hover","border-bottom","5px solid white");
2255 updateCSSRule(".etab button.edt_active","background-color","white");
2256 updateCSSRule(".etab button.edt_active","color","black");
2257 updateCSSRule(".etab button.edt_active","border-bottom","5px solid white");
2258 updateCSSRule(".etab button.edt_active:hover","background-color","white");
2259 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid white");
2263function updateCSSRule(selector, property, value) {
2264 for (let i = 0; i < document.styleSheets.length; i++) {
2265 let styleSheet = document.styleSheets[i];
2266 let rules = styleSheet.cssRules || styleSheet.rules;
2267 if (!rules) continue;
2268 for (let j = 0; j < rules.length; j++) {
2269 let rule = rules[j];
2270 if (rule.selectorText === selector) {
2271 rule.style[property] = value;
2278// show/hide wait and loop progress
2279function showProgress(e) {
2280 //const progressDiv = document.getElementById("progressDiv");
2281 const progressDiv = document.getElementById("Progress");
2282 if (e === undefined) e = document.getElementById("showProgressBars");
2284 localStorage.setItem("showProgress",true);
2285 progressDiv.style.display = "block";
2286 //dlgShow(progressDiv);
2288 localStorage.removeItem("showProgress");
2289 progressDiv.style.display = "none";
2290 //dlgHide(progressDiv);
2294// Mark the current line number
2295function markCurrLineNum() {
2296 const [lineNumbers,editor] = editorElements();
2297 const currLines = lineNumbers.querySelectorAll(".edt_linenum_curr");
2298 const [startLine,endLine] = getLinesInSelection(editor);
2299 if (startLine === 0 && endLine === 0) return;
2300 currLines.forEach((line) => line.classList.remove("edt_linenum_curr"));
2301 for (let i = startLine; i <= endLine; i++) {
2302 let lineNumId = "#lNum" + i.toString();
2303 let lineNum = lineNumbers.querySelector(lineNumId);
2305 lineNum.className = "edt_linenum_curr";
2310// Check if program is progName is running.
2311// If not, try to get it going
2312function checkProgram(progName = "Sequencer") {
2313 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc1) {
2314 mjsonrpc_db_get_values([`/Programs/${progName}/Start command`]).then(function(rpc2) {
2315 let isRunning = (rpc1.result.status === 1);
2316 let isDefined = ((rpc2.result.data[0] !== null) && (rpc2.result.data[0] !== ""));
2317 if (isRunning && isDefined) return;
2318 // progName is not running or not defined, stop it just in case and check the reason
2319 mjsonrpc_stop_program(progName);
2322 message = `${progName} program is not running.<br>Should I start it?`
2324 message = `${progName} program is not configured and not running.<br>Should I try to start it anyway?`
2326 dlgConfirm(message,function(resp) {
2327 // Guess the name of the actual binary
2328 let binName = (progName === "Sequencer") ? "msequencer" : progName.toLowerCase();
2331 // assume that progName is in path and create a start command, sleep 2s,
2332 // set value to "progName -D", sleep 2s, start program
2333 mjsonrpc_db_create([{"path" : `/Programs/${progName}/Start command`, "type" : TID_STRING}]).then(function (rpc3) {
2334 setTimeout(function(){
2335 mjsonrpc_db_paste([`/Programs/${progName}/Start command`],[`${binName} -D`]).then(function (rpc4) {
2336 if (rpc4.result.status[0] === 1) {
2337 mjsonrpc_start_program(progName);
2339 }).catch(function (error) {
2340 console.error(error);
2343 }).catch(function (error) {
2344 console.error(error);
2347 mjsonrpc_start_program(progName);
2349 // take 3 seconds and check that it actually started
2350 setTimeout(function(){
2351 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc5) {
2352 if (rpc5.result.status === 1) {
2353 dlgAlert(`${progName} started successfully.`);
2355 dlgAlert(`Failed to start ${progName}!<br>Try to start it manually (${binName} -D)`);
2361 }).catch (function (error) {
2362 console.error(error);
2364 }).catch(function (error) {
2365 console.error(error);
2369// Python programs seem to be different from standard midas - to be checked
2370function checkPyProgram(progName = "PySequencer") {
2371 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc1) {
2372 mjsonrpc_db_get_values([`/Programs/${progName}/Start command`]).then(function(rpc2) {
2373 let isRunning = (rpc1.result.status === 1);
2374 let isDefined = ((rpc2.result.data[0] !== null) && (rpc2.result.data[0] !== ""));
2375 if (isRunning && isDefined) return;
2376 // progName is not running or not defined, stop it just in case and check the reason
2377 mjsonrpc_stop_program(progName);
2380 message = `${progName} program is not running.<br>Should I start it?`
2382 message = `${progName} program is not configured and not running.<br>Should I try to start it anyway?`
2384 dlgConfirm(message,function(resp) {
2385 // Guess the name of the actual binary
2386 let binName = (progName === "PySequencer") ? "python3 $MIDASSYS/python/midas/sequencer.py" : "python3 $MIDASSYS/python/midas/" + progName.toLowerCase() + ".py";
2389 // assume that progName is in path and create a start command, sleep 2s,
2390 // set value to "progName -D", sleep 2s, start program
2391 mjsonrpc_db_create([{"path" : `/Programs/${progName}/Start command`, "type" : TID_STRING}]).then(function (rpc3) {
2392 setTimeout(function(){
2393 mjsonrpc_db_paste([`/Programs/${progName}/Start command`],[`${binName} -D`]).then(function (rpc4) {
2394 if (rpc4.result.status[0] === 1) {
2395 mjsonrpc_start_program(progName);
2397 }).catch(function (error) {
2398 console.error(error);
2401 }).catch(function (error) {
2402 console.error(error);
2405 mjsonrpc_start_program(progName);
2407 // take 3 seconds and check that it actually started
2408 setTimeout(function(){
2409 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc5) {
2410 if (rpc5.result.status === 1) {
2411 dlgAlert(`${progName} started successfully.`);
2413 dlgAlert(`Failed to start ${progName}!<br>Try to start it manually (${binName} -D)`);
2419 }).catch (function (error) {
2420 console.error(error);
2422 }).catch(function (error) {
2423 console.error(error);
2427function captureSelected() {
2428 if (window.getSelection) {
2429 let selection = window.getSelection();
2430 let text = selection.toString();
2431 let range = selection.getRangeAt(0);
2432 if (text && range) {
2434 startPath: range.startContainer,
2435 startOffset: range.startOffset,
2436 endPath: range.endContainer,
2437 endOffset: range.endOffset
2439 sessionStorage.setItem("tempSelText", text);
2440 sessionStorage.setItem("tempSelRange", JSON.stringify(rangeData));
2445function editMenu(action) {
2446 let text = sessionStorage.getItem("tempSelText") ?? "";
2447 let storedRange = sessionStorage.getItem("tempSelRange") ?? "";
2450 let rangeData = JSON.parse(storedRange);
2451 let startContainer = nodeFromPath(rangeData.startPath);
2452 let endContainer = nodeFromPath(rangeData.endPath);
2454 // Create a new range
2455 let newRange = new Range();
2456 newRange.setStart(startContainer, rangeData.startOffset);
2457 newRange.setEnd(endContainer, rangeData.endOffset);
2459 // Select the new range
2460 let selection = window.getSelection();
2461 selection.removeAllRanges();
2462 selection.addRange(newRange);
2465 if (action === "Copy") {
2467 sessionStorage.setItem("copiedText",text);
2469 } else if (action === "Paste") {
2470 const copiedText = sessionStorage.getItem("copiedText");
2472 newRange.deleteContents();
2473 newRange.insertNode(document.createTextNode(copiedText));
2475 } else if (action === "Cut") {
2477 sessionStorage.setItem("copyText",text);
2478 //document.execCommand("cut");
2479 newRange.deleteContents();
2481 } else if (action === "Undo") {
2483 } else if (action === "Redo") {
2488// Switch to the clicked tab
2489function openETab(btn) {
2490 const tabcontent = document.querySelectorAll(".etabcontent");
2491 const tablinks = document.querySelectorAll(".etablinks");
2492 const tab = btn ? btn : document.querySelectorAll(".edt_active")[0];
2493 const tabID = tab.id.replace("-btn","")
2494 tabcontent.forEach(content => {
2495 content.style.display = "none";
2497 tablinks.forEach(link => {
2498 link.classList.remove("edt_active");
2500 tab.className += " edt_active";
2501 //document.getElementById(tabID).style.display = "inline-flex";
2502 document.getElementById(tabID).style.display = "flex";
2503 // For the main sequence tab disable Save and Save as...
2504 if (tabID === "etab1") {
2505 disableMenuItems("noteditor",true);
2507 disableMenuItems("noteditor",false);
2509 // Change validation icon to defaule
2510 let vldBtn = document.getElementById("vldBtn");
2511 vldBtn.src = "icons/validate-syntax.svg";
2512 vldBtn.style.backgroundColor = "";
2514 // Adjust height of active editor
2518// Close the clicked tab
2519function closeTab(tab,event) {
2520 const tablinks = document.querySelectorAll(".etablinks");
2521 if (tablinks.length < 3) return;
2522 const tabCount = parseInt(tab.parentElement.id.replace("-btn","").replace("etab",""));
2523 const tabBtn = document.getElementById(`etab${tabCount}-btn`);
2524 const tabContent = document.getElementById(`etab${tabCount}`);
2525 if (seqIsChanged()) {
2526 dlgConfirm("File was changed, close anyway?", function(resp) {
2529 tabContent.remove();
2530 // switch to previous tab
2531 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2536 tabContent.remove();
2537 // switch to previous tab
2538 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2540 // need this since the close button is inside the tab button
2541 event.stopPropagation();
2544// Create and add a new editor tab
2545function addETab(btn) {
2546 // Create tab button
2547 const tabBtn = document.createElement("button");
2548 tabBtn.className = "etablinks";
2549 const tabCount = (btn.previousElementSibling) ? parseInt(btn.previousElementSibling.id.replace("-btn","").replace("etab","")) + 1 : 1;
2550 tabBtn.id = "etab" + tabCount + "-btn";
2551 tabBtn.innerHTML = `<span id="etab${tabCount}-lbl">new file${tabCount}</span><span style="color: var(--mred);" id="filechanged${tabCount}"></span><span onclick="closeTab(this,event);" class="closebtn">×</span>`;
2552 btn.parentNode.insertBefore(tabBtn,btn);
2553 tabBtn.onclick = function () { openETab(this);};
2555 // Create editor area
2556 const tabContent = document.createElement("div");
2557 tabContent.id = "etab" + tabCount;
2558 tabContent.className = "etabcontent";
2560 if (localStorage.getItem("darkMode")) makeDark = "style='background-color: black; color: white;'";
2562 `<pre id="lineNumbers${tabCount}" class="edt_linenum"></pre><pre id="editorTab${tabCount}" ${makeDark} class="edt_area" spellcheck="false" contenteditable="false"></pre>`;
2563 tabContent.innerHTML = html;
2564 const lastETab = document.getElementById("lastETab");
2565 lastETab.parentNode.insertBefore(tabContent,lastETab);
2568 // Add event listeners
2569 editorEventListeners();
2572// Return the pre of lineNumbers, editor, tab label and tab button element of the active tab
2573function editorElements() {
2574 const btn = (document.querySelectorAll(".edt_active")[0]) ? document.querySelectorAll(".edt_active")[0] : document.getElementById("etab1-btn");
2575 const tab = document.getElementById(btn.id.replace("-btn",""));
2576 const [lineNumbers,editor] = tab.children;
2577 const btnLabel = (btn.id !== "etab1-btn") ? btn.children[0] : btn.children[1];
2578 return [lineNumbers,editor,btnLabel,btn];
2581// disable and enable clicking on menu item
2582function disableMenuItems(className,flag) {
2584 className - the class name of the item
2585 flag - true/false to enable/disable item
2587 const els = document.querySelectorAll(`.${className}`);
2590 e.style.opacity = 0.5;
2591 e.style.pointerEvents = "none";
2593 e.style.opacity = "";
2594 e.style.pointerEvents = "";
2599// Function to replace some special characters in the text
2600function escapeSpecialCharacters(text) {
2601 return text.replace(/&/g, "&")
2602 .replace(/</g, "<")
2603 .replace(/>/g, ">")
2604 .replace(/\r\n|\n\r/g, '\n')
2605 //.replace(/"/g, """)
2606 //.replace(/'/g, "'")
2607 .replace(/\t/g, " ")
2608 // and illegal ascii characters
2609 .replace(/[^\x00-\x7F]/g, " ");
2612// Produce a help window
2615<span style='text-align: left;'>
2616<b>Hidden features of the sequencer editor</b>
2617<ul style='white-space: pre;font-family: monospace;'>
2618<li>Double click on the edit area of the first (main) tab to edit the currently loaded sequence.</li>
2619<li>Tab - Indent selected lines.</li>
2620<li>Shift+Tab - Unindent selected lines.</li>
2621<li>Escape - Autoindent selected lines according to syntax rules.</li>
2622<li>Ctrl+C - Copy selected text.</li>
2623<li>Ctrl+V - Paste selected text.</li>
2624<li>Ctrl+A - Select all text.</li>
2625<li>Ctrl+Z - Undo last change.</li>
2626<li>Ctrl+R - Redo last undo.</li>
2630 const d = dlgMessage("Editor help",help, false, false);
2631 const btn = d.querySelectorAll('.dlgButton')[0];
2632 btn.className = "dlgButtonDefault";
2637// Activate drag and drop events on next files table
2638function activateDragDrop(table) {
2640 table - The table element containing the list of next files.
2642 // collect all rows with class dragrow
2643 const rows = table.querySelectorAll('.dragrow');
2644 let dragStartIndex,dragEndIndex;
2646 // add event listeners
2647 rows.forEach(row => {
2648 row.addEventListener('dragstart', dragStart);
2649 row.addEventListener('dragover', dragOver);
2650 row.addEventListener('dragend', dragEnd);
2653 function dragStart(e) {
2654 dragStartIndex = Array.from(rows).indexOf(this);
2655 rows.forEach(row => row.classList.remove('dragstart'));
2656 this.classList.add('dragstart');
2658 function dragOver(e) {
2660 dragEndIndex = Array.from(rows).indexOf(this);
2661 // Create or update the empty row element
2663 emptyRow = document.createElement('tr');
2664 emptyRow.innerHTML = "<td colspan=4> </td>";
2665 emptyRow.classList.add('empty-dragrow');
2667 // Insert the empty row element at the appropriate position
2668 if (dragEndIndex > dragStartIndex) {
2669 this.parentNode.insertBefore(emptyRow, this.nextSibling);
2670 } else if (dragEndIndex < dragStartIndex) {
2671 this.parentNode.insertBefore(emptyRow, this);
2674 function dragEnd(e) {
2679 reorderNextFilenames(dragStartIndex,dragEndIndex);
2680 rows.forEach(row => {
2681 row.classList.remove('dragstart');
2686// Move next filename from dragStarIndex to dragEndIndex position
2687function reorderNextFilenames(dragStarIndex,dragEndIndex) {
2689 dragStarIndex - move file from this indext.
2690 dragEndIndex - destination index of the file.
2692 if (dragStarIndex === dragEndIndex) return;
2693 let odbpath = mslDefs.filename.next;
2694 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2695 if (rpc.result.status[0] !== 1) return;
2696 let fList = rpc.result.data[0];
2698 // Remove the draggedFile from the starting index
2699 const draggedFile = fList.splice(dragStarIndex, 1)[0];
2700 // Insert the draggedFile at the new index
2701 fList.splice(dragEndIndex, 0, draggedFile);
2702 // Check that the list does not contain more than 10 files
2703 if (fList.length > 10) {
2704 dlgAlert("There are more than 10 files. Aborting!.");
2707 // Move full slots to the begenning of the array
2708 fList = fList.filter(Boolean);
2709 fList = [...fList, ...new Array(10 - fList.length).fill("")];
2710 // Update values in ODB
2711 mjsonrpc_db_paste([odbpath],[fList]).then(function (rpc2) {
2712 if (rpc2.result.status[0] !== 1) {
2713 dlgAlert("Failed to move the file!<br>Please check.");
2715 }).catch(function (error) {
2716 console.error(error);
2718 }).catch (function (error) {
2719 console.error(error);
2723// Change the next file name in the clicked row
2724function chngNextFilename(e,filename) {
2726 e - (optional) cell element on the same row of "next file" to be changed.
2727 If last row (or undefined) add a file to the end of the queue.
2728 filename - (optional) The file name to add to the end of the queue.
2730 e = (e) ? e : document.getElementById("addAsNext");
2731 //if (e === undefined) e = document.getElementById("addAsNext");
2732 // file index from table row index
2733 let index = e ? e.parentElement.rowIndex - 1 : 0;
2734 // Only 10 files are allowed
2736 dlgAlert("Maximum number (10) of next files reached!");
2740 let odbpath = mslDefs.filename.next;
2741 mjsonrpc_db_get_values([`${SeqODB}/State/Path`]).then(function(rpc) {
2742 let path = mslDefs.filename.path + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
2743 sessionStorage.setItem("pathName",path);
2745 odbpath = odbpath + "[" + index.toString() + "]";
2746 modbset(odbpath,filename);
2748 file_picker(path,mslDefs.filename.ext,function(filename) {
2749 filename = filename.replace(/^sequencer\//,'').replace(/^\//,'');
2750 odbpath = odbpath + "[" + index.toString() + "]";
2752 modbset(odbpath,filename);
2757 }).catch(function (error) {
2758 mjsonrpc_error_alert(error);
2762// Reomve the next file name from the queue
2763function remNextFilename(e) {
2765 e - cell element on the same row of "next file" to be removed
2767 // file index from table row index
2768 let index = e.parentElement.rowIndex - 1;
2769 let odbpath = mslDefs.filename.next;
2770 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2771 if (rpc.result.status[0] !== 1) return;
2772 let fList = rpc.result.data[0];
2775 // Move full slots to the begenning of the array
2776 fList = fList.filter(Boolean);
2777 fList = [...fList, ...new Array(10 - fList.length).fill("")];
2778 mjsonrpc_db_paste([odbpath],[fList]).then(function (rpc2) {
2779 if (rpc2.result.status[0] !== 1) {
2780 dlgAlert("Failed to remove file from list!<br>Please check.");
2782 }).catch(function (error) {
2783 console.error(error);
2785 }).catch (function (error) {
2786 console.error(error);
2791// Wait for ODB in path to have value
2792// If value is not reached, give up after 10s
2793function checkODBValue(path,value,funcCall,args) {
2795 path - ODB path to monitor for value
2796 value - the value to be reached and return success
2797 funcCall - function name to call when value is reached
2798 args - argument to pass to funcCall
2800 // Call the mjsonrpc_db_get_values function
2801 mjsonrpc_db_get_values([path]).then(function(rpc) {
2802 if (rpc.result.status[0] === 1 && rpc.result.data[0] !== value) {
2803 console.log("Value not reached yet", NcheckValue);
2805 if (NcheckValue < 100) {
2806 // Wait 0.1 second and then call checkODBValue again
2807 // Time out after 10 s
2809 checkODBValue(path,value,funcCall,args);
2813 if (funcCall) funcCall(args);
2814 console.log("Value reached, proceeding...");
2818 }).catch(function(error) {
2819 console.error(error);
2823function isTextSelected(element) {
2824 const selection = window.getSelection();
2825 if (!selection.rangeCount) return false;
2827 const range = selection.getRangeAt(0);
2828 const selectedText = selection.toString();
2830 // Check if the selection is within the specified element
2831 if (!element.contains(range.commonAncestorContainer)) return false;
2833 // Check if there is any selected text
2834 return selectedText.length > 0;
2837function deleteSelectedText(element) {
2838 const selection = window.getSelection();
2839 if (!selection.rangeCount) return; // No selection exists
2840 const range = selection.getRangeAt(0);
2841 // Ensure the selection is within the target element
2842 if (!element.contains(range.commonAncestorContainer)) return;
2843 // Delete the selected text
2844 range.deleteContents();
2847// Make sure that the caret stays visible
2848function keepCaretVisible(editor) {
2849 const selection = window.getSelection();
2850 const caretPosition = selection.getRangeAt(0).getBoundingClientRect();
2851 const containerRect = editor.getBoundingClientRect();
2852 const caretTop = caretPosition.top - containerRect.top;
2853 const caretBottom = caretPosition.bottom - containerRect.top;
2854 const containerHeight = containerRect.height;
2855 // Caclulate font size
2856 const emInPixels = parseFloat(window.getComputedStyle(editor).fontSize) * 1.5;
2858 // If the caret is near the bottom, scroll the container down with 1.5 fontSize
2859 if (caretBottom > containerHeight) {
2860 editor.scrollTop += caretBottom - containerHeight + emInPixels;
2863 // If the caret is near the top, scroll the container up
2865 editor.scrollTop -= Math.abs(caretTop);
2870// commands object for syntax validation
2871const mslCommands = {
2873 patterns: [/^INCLUDE\s+.+(\.msl)?$/i],
2874 description: "Include another MSL file.",
2878 patterns: [/^BREAK$/i],
2879 description: "Break (finish prematurely) a loop.",
2883 patterns: [/^CALL\s+\w+(\s*,\s*.+)*$/i],
2884 description: "Call a subroutine with optional parameters.",
2888 patterns: [/^CAT\s+\w+(\s*,\s*.+)*$/i],
2889 description: "Concatenate strings into a single variable.",
2893 patterns: [/^(?:COMMENT\s+.*|#.*)$/i],
2894 description: "A comment line starting with COMMENT or #.",
2898 patterns: [/^EXIT$/i],
2899 description: "Exit the script immediately.",
2903 patterns: [/^GOTO\s+\d+$/i],
2904 description: "Jump to a specific line in the script.",
2908 patterns: [/^IF\s*\(.+\)$/i],
2909 description: "Conditional execution of code blocks.",
2910 blockStart: true, // Marks this as a block start command
2914 patterns: [/^ELSE$/i],
2915 description: "Alternative block for an IF statement.",
2919 patterns: [/^ENDIF$/i],
2920 description: "Ends an IF block.",
2921 blockEnd: "IF", // Marks this as a block end command for IF
2925 patterns: [/^LIBRARY\s+.+$/i],
2926 description: "Declare the file as a library.",
2931 /^LOOP\s+(\d+|INFINITE|\$\w+)$/i, // LOOP 5, LOOP INFINITE, or LOOP $varName
2932 /^LOOP\s+\w+\s*,\s*.+$/i, // LOOP i, 1:10
2933 // /^LOOP\s+\w+\s*,\s*(?:\d+:\d+|\S+)$/i, // LOOP i, 1:10
2935 description: "Execute a loop a fixed number of times or with values.",
2936 blockStart: true, // Marks this as a block start command
2938 //argcond1: /^(\d+|INFINITE|\$\w+)$/, // First argument can be a number, INFINITE, or a variable
2941 patterns: [/^ENDLOOP$/i],
2942 description: "Ends a LOOP block.",
2943 blockEnd: "LOOP", // Marks this as a block end command for LOOP
2947 patterns: [/^MESSAGE\s+.+(\s*,\s*1)?$/i],
2948 description: "Display a message in the browser.",
2952 patterns: [/^MSG\s+.+(\s*,\s*(ERROR|INFO|DEBUG|LOG|TALK))?$/i],
2953 description: "Send a message to the MIDAS buffer.",
2957 patterns: [/^ODBCREATE\s+.+,\s+\w+(\s*,\s*\d+)?$/i],
2958 description: "Create an ODB key.",
2962 patterns: [/^ODBDELETE\s+.+$/i],
2963 description: "Delete an ODB key or subdirectory.",
2967 patterns: [/^ODBGET\s+".+",\s+\w+$/i],
2968 description: "Retrieve a value from the ODB.",
2970 argcond: [/^\/.+$/, // First argument must start with /
2971 /^.+$/], // Second argument can be anything
2974 patterns: [/^ODBINC\s+.+(\s*,\s*[+-]?\d+)?$/i],
2975 description: "Increment an ODB value.",
2979 patterns: [/^ODBLOOKUP\s+".+",\s+".+",\s+\w+$/i],
2980 description: "Lookup a string in the ODB array.",
2984 //patterns: [/^ODBSET\s+("[^"]*"|\/[^,]+),\s*("[^"]*"|[^,]+)(\s*,\s*[01])?$/i,],
2985 patterns: [/^ODBSET\s+.+,\s+.+(\s*,\s*[01])?$/i],
2986 description: "Set a value in the ODB.",
2989 /^.+$/, // First argument must start with / or be a quoted string
2990 /^.+$/, // Second argument can be a quoted string or unquoted value
2991 /^[01]$/, // Third argument (optional) must be 0 or 1
2995 patterns: [/^ODBLOAD\s+.+(\s*,\s*.+)?$/i],
2996 description: "Load an external file into the ODB.",
3000 patterns: [/^ODBSAVE\s+.+,\s+.+$/i],
3001 description: "Save part of the ODB to a file.",
3005 patterns: [/^ODBSUBDIR\s+("\/[^"]*"|\/[^,]+)$/i],
3006 description: "Set a base path for ODB commands.",
3007 blockStart: true, // Marks this as a block start command
3011 patterns: [/^ENDODBSUBDIR$/i],
3012 description: "Ends an ODBSUBDIR block.",
3013 blockEnd: "ODBSUBDIR", // Marks this as a block end command for ODBSUBDIR
3017 patterns: [/^PARAM\s+\w+(\s*,\s*.+)*$/i],
3018 description: "Define script parameters.",
3022 patterns: [/^RUNDESCRIPTION\s+.+$/i],
3023 description: "Set a run description.",
3027 patterns: [/^SCRIPT\s+.+(\s*,\s*.+)*$/i],
3028 description: "Call a server-side script with optional parameters.",
3032 patterns: [/^SET\s+\w+\s*,\s*.+$/i],
3033 description: "Set a variable to a value.",
3037 patterns: [/^SUBROUTINE\s+\w+$/i],
3038 description: "Start a subroutine block.",
3039 blockStart: true, // Marks this as a block start command
3043 patterns: [/^ENDSUBROUTINE$/i],
3044 description: "Ends a subroutine block.",
3045 blockEnd: "SUBROUTINE", // Marks this as a block end command for SUBROUTINE
3049 patterns: [/^TRANSITION\s+(start|stop|pause|resume)$/i],
3050 description: "Trigger a run state transition.",
3055 /^WAIT\s+seconds,\s*(\d+|\$\w+)$/i, // WAIT seconds,10
3056 /^WAIT\s+events,\s*(\d+|\$\w+)$/i, // WAIT events,10
3057 /^WAIT\s+ODBvalue,\s*\/.+,\s*[<>=!]+,\s*(\d+|\$\w+)$/i, // WAIT ODBvalue, /path, <, 100
3059 description: "Wait for a condition, time, or event.",
3063 patterns: [/^[a-zA-Z_]\w*\s*=\s*([0-9]+(\.\d+)?|\$[a-zA-Z_]\w*|[\d\w\$\s\+\-\*\/\^\(\)]+|[\w\$]+\(.*\)|".*"|'.*')$/i],
3064 description: "Variable assignment or equation.",
3069const pythonCommands = {
3070 // Comment lines starting with "#" are ignored.
3073 description: "Comment line. Lines starting with '#' are ignored.",
3076 // Multi line comments between triple brackets are ignored
3078 patterns: [/("""[\s\S]*?""")|('''[\s\S]*?''')/g],
3079 description: "Multi line comments between tripple quotes.",
3082 // Function definition
3084 patterns: [/^\s*def\s+[A-Za-z_]\w*\s*\([^)]*\)\s*:$/],
3085 description: "Function definition. Syntax: def name(params):",
3090 patterns: [/^\s*class\s+[A-Za-z_]\w*(?:\s*\([^)]*\))?\s*:$/],
3091 description: "Class definition. Syntax: class Name(Base1, Base2):",
3094 // Import statements
3097 /^\s*import\s+[A-Za-z_]\w*(?:\.\w+)*(?:\s*,\s*[A-Za-z_]\w*(?:\.\w+)*)*\s*$/,
3098 /^\s*from\s+[A-Za-z_]\w*(?:\.\w+)*\s+import\s+(?:\*\s*|[A-Za-z_]\w*(?:\s+as\s+[A-Za-z_]\w*)?(?:\s*,\s*[A-Za-z_]\w*(?:\s+as\s+[A-Za-z_]\w*)?)*)\s*$/
3100 description: "Import statements. Syntax: import module[, module2]\n or: from pkg.mod import name[, name2]",
3103 // Conditional blocks
3105 patterns: [/^\s*if\s+.+\s*:$/],
3106 description: "If statement. Syntax: if condition:",
3111 patterns: [/^\s*elif\s+.+\s*:$/],
3112 description: "Elif statement. Syntax: elif condition:",
3116 patterns: [/^\s*else\s*:$/],
3117 description: "Else statement. Syntax: else:",
3122 patterns: [/^\s*for\s+[A-Za-z_]\w*\s+in\s+.+\s*:$/],
3123 description: "For loop. Syntax: for var in iterable:",
3128 patterns: [/^\s*while\s+.+\s*:$/],
3129 description: "While loop. Syntax: while condition:",
3133 // Try/Except/Finally
3135 patterns: [/^\s*try\s*:$/],
3136 description: "Try block. Syntax: try:",
3141 patterns: [/^\s*except(?:\s+[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)?(?:\s+as\s+[A-Za-z_]\w*)?\s*:$/],
3142 description: "Except block. Syntax: except ExceptionType as name:",
3146 patterns: [/^\s*finally\s*:$/],
3147 description: "Finally block. Syntax: finally:",
3150 // With context manager
3152 patterns: [/^\s*with\s+.+\s+as\s+[A-Za-z_]\w*\s*:$/],
3153 description: "With statement. Syntax: with expr as var:",
3157 // Simple statements
3160 /^\s*return(?:\s+.+)?$/
3162 description: "Return statement. Syntax: return [expr]",
3166 patterns: [/^\s*pass\s*$/],
3167 description: "Pass statement. Does nothing.",
3171 patterns: [/^\s*break\s*$/],
3172 description: "Break out of the nearest loop.",
3176 patterns: [/^\s*continue\s*$/],
3177 description: "Continue to next iteration of the nearest loop.",
3181 patterns: [/^\s*raise(?:\s+[A-Za-z_]\w*(?:\s*\(.*\))?)?(?:\s+from\s+[A-Za-z_]\w*(?:\s*\(.*\))?)?\s*$/],
3182 description: "Raise an exception. Syntax: raise ExceptionType(args) [from other]",
3186 patterns: [/^\s*assert\s+.+$/],
3187 description: "Assert statement. Syntax: assert condition[, message]",
3192 patterns: [/^\s*@\w+(?:\.\w+)*(?:\s*\(.*\))?$/],
3193 description: "Decorator. Syntax: @decorator or @decorator(args)",
3196 // Assignment (simple)
3198 patterns: [/^\s*[A-Za-z_]\w*\s*=.*$/],
3199 description: "Variable assignment. Syntax: name = expression",
3202 // Bare function or method call, possibly with args
3204 patterns: [/^\s*[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\s*\(.*\)\s*$/],
3205 description: "Expression statement (e.g. function or method call).",
3210// Validation function to validate script content
3211function checkEditorContent(e, commands) {
3212 // If not provided take appropriate default values
3213 if (commands === undefined) {
3214 commands = (SeqODB === "/PySequencer") ? pythonCommands : mslCommands;
3217 // Get the content from the current editor
3218 let [lineNumbers,editor] = editorElements();
3219 const editorContent = editor.innerText;
3221 // Validate the script content
3222 const errors = validateScript(editorContent,commands);
3223 if (errors.length > 0) {
3224 // Format and display errors
3225 let vldErrors = errors.map(err => {
3226 if (typeof err === "string") {
3227 return err; // Handle string errors directly
3228 } else if (err.line && err.message) {
3229 return `Line ${err.line}: ${err.message}`; // Format object errors
3231 return "Unknown error"; // Fallback for unexpected error formats
3234 // highlight error lines and scroll to first one
3235 hlLine(errors.map(error => error.line),"var(--mred)",editor,errors.map(error => error.message));
3236 setCurrentCursorPosition(editor, errors[0].line - 1 + editorContent.split("\n").slice(0,errors[0].line).reduce((sum, line) => sum + line.length, 0));
3239 // Show the error message
3240 //dlgMessage("Validation errors",vldErrors,false,true);
3241 console.error("Validation errors:\n",vldErrors);
3242 e.src = "icons/validate-syntax-red.svg";
3243 e.style.backgroundColor = "var(--mred)";
3245 //dlgAlert("The syntax of the sequence is valid.");
3246 e.src = "icons/validate-syntax-green.svg";
3247 e.style.backgroundColor = "var(--mgreen)";
3251// Function to validate the script against commands object
3252function validateScript(script, commands) {
3255 // Stack to track nested blocks
3256 const blockStack = [];
3257 const lineNums = [];
3258 // Stack to track delimiters
3259 const delimiterStack = [];
3260 const delimiterLineNums = [];
3262 // Delimiters and their matching pairs
3263 const delimiters = {
3271 // Remove multiline comments
3272 if (commands.MULTICOMM.patterns[0]) {
3273 const multiCommRegex = commands.MULTICOMM.patterns[0];
3275 // replace each triple-quote block with the same count of blank lines
3276 script = script.replace(
3277 new RegExp(multiCommRegex.source, "g"),
3279 // count the lines in the matched block
3280 const lines = match.split("\n");
3281 // return that many lines, each a single space
3282 return lines.map(() => " ").join("\n");
3287 // Regex to match and replace all comments by space
3288 const commentRegex = new RegExp(commands.COMMENT.patterns.map(pattern => pattern.source).join("|"), "gim");
3289 script = script.replace(commentRegex, "");
3290 const lines = script.split("\n");
3292 lines.forEach((line, index) => {
3293 const trimmedLine = line.trim();
3294 if (!trimmedLine) return; // Skip empty lines
3295 let isValid = false;
3297 // Check if the line matches any command pattern
3298 const cmdKey = Object.keys(commands).find(cmd => {
3299 const cmdData = commands[cmd];
3300 const cmdPattern = cmdData.patterns || null;
3301 if (Array.isArray(cmdPattern)) {
3302 isValid = cmdPattern.some(pattern => pattern.test(trimmedLine));
3304 // Handle block start commands
3305 if (isValid && cmdData.blockStart) {
3306 blockStack.push(cmd); // Push the block type to the stack
3307 lineNums.push(index + 1);
3310 // Handle block end commands
3311 if (isValid && cmdData.blockEnd) {
3312 const expectedBlock = cmdData.blockEnd;
3313 const lastBlock = blockStack.pop();
3316 if (lastBlock !== expectedBlock) {
3317 errors.push({ line: index + 1, message: `"${cmd}" without matching "${expectedBlock}".` });
3321 // Validate arguments if argcond is defined
3322 if (isValid && cmdData.argcond) {
3323 // Remove the command keyword from the line and split remaining arguments by comma
3324 const args = splitArguments(trimmedLine.replace(new RegExp(`^${cmd}`, "i"), "").trim());
3325 for (let i = 0; i < args.length; i++) {
3326 const arg = args[i].trim().replace(/^"(.*)"$/, "$1"); // Remove surrounding quotes
3327 const argCond = cmdData.argcond[i]; // Get the condition for this argument
3328 if (argCond && !argCond.test(arg)) {
3329 errors.push({ line: index + 1, message: `Argument ${i + 1} of "${cmd}" is invalid.` });
3336 return cmdPattern && cmdPattern.test(trimmedLine);
3340 // Valid command or comment, no error
3343 errors.push({ line: index + 1, message: `Syntax error or unknown command.` });
3346 // Delimiter validation
3347 for (let i = 0; i < trimmedLine.length; i++) {
3348 const char = trimmedLine[i];
3349 if (delimiters[char]) {
3350 // Opening delimiter
3351 delimiterStack.push({ char, line: index + 1, pos: i + 1 });
3352 } else if (Object.values(delimiters).includes(char)) {
3353 // Closing delimiter
3354 if (delimiterStack.length === 0) {
3355 errors.push({ line: index + 1, message: `Mismatched "${char}" at line ${index + 1}, column ${i + 1}.` });
3357 const lastDelimiter = delimiterStack.pop();
3358 if (delimiters[lastDelimiter.char] !== char) {
3359 errors.push({ line: index + 1, message: `Mismatched "${char}" at line ${index + 1}, column ${i + 1}. Expected "${delimiters[lastDelimiter.char]}".` });
3366 // Check for unclosed blocks at the end of the script
3367 if (blockStack.length > 0) {
3368 errors.push({ line: lineNums[0], message: `Unclosed "${blockStack[0]}" block starting at line ${lineNums[0]}.` });
3371 // Check for unclosed delimiters at the end of the script
3372 if (delimiterStack.length > 0) {
3373 errors.push({ line: delimiterLineNums[0], message: `Unclosed "${delimiterStack[0].char}" delimiter starting at line ${delimiterLineNums[0]}, column ${delimiterStack[0].pos}.` });
3379// Helper function to split arguments with quoted strings by commas
3380function splitArguments(input) {
3381 // Regex to match quoted strings
3382 const regex = /(["'])(.*?)\1/g;
3386 // Step 1: Find all quoted strings and split the input around them
3388 while ((match = regex.exec(input)) !== null) {
3389 // Add unquoted arguments before the current quoted string
3390 const beforeQuoted = input.slice(lastIndex, match.index).trim();
3392 args.push(...beforeQuoted.split(",").map(arg => arg.trim()).filter(arg => arg.length > 0));
3395 // Add the quoted argument
3396 args.push(match[2]); // Capture the content inside the quotes
3398 // Update lastIndex to the end of the current match
3399 lastIndex = regex.lastIndex;
3402 // Step 2: Add any remaining unquoted arguments after the last quoted string
3403 const remaining = input.slice(lastIndex).trim();
3405 args.push(...remaining.split(",").map(arg => arg.trim()).filter(arg => arg.length > 0));