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 * Allow the editor to fill available vertical space.
328 * Scoped to the AutoRun editor container (.etabcontent) to avoid impacting
329 * any other <pre> usage on the page.
339.etabcontent .edt_linenum {
344.etabcontent .edt_area {
350.etabcontent .info_column {
357 display: inline-block;
360 display: inline-block;
363.msl_comment, .msl_comment * {
364 all: revert !important;
365 color: #839496 !important;
366 font-weight: italic !important;
367 background: none !important;
371 background-color: #FFFF00;
374.msl_current_line:hover {
378 background-color: #FF0000;
382 background-color: #FF8800;
386 background-color: #11FF11;
390 background-color: #FFFF00;
395 /*resize: horizontal;*/
402 border: 1px solid black;
407 line-height: 30px; /* since modbvalue forces resize */
412.infotable tr:first-child th:first-child,
413.infotable tr:first-child td:first-child {
414 border-top-left-radius: 5px;
416.infotable tr:first-child th:last-child,
417.infotable tr:first-child td:last-child {
418 border-top-right-radius: 5px;
420.infotable tr:last-child td:first-child {
421 border-bottom-left-radius: 5px;
423.infotable tr:last-child td:last-child {
424 border-bottom-right-radius: 5px;
431 display: inline-block;
435 width: calc(100% - 2px);
440/* Dropdown button styles*/
442 background-color: Transparent;
444 font-family: verdana,tahoma,sans-serif;
453 background-color: #C0D0D0;
456/* Style the dropdown content (hidden by default) */
459 display: inline-block;
464 background-color: #f9f9f9;
466 box-shadow: 0 8px 16px rgba(0,0,0,0.2);
477.dropdown-content div{
479 justify-content: space-between;
483.dropdown-content a:hover {
484 background-color: #C0D0D0;
486.dropdown-content div:hover {
487 background-color: #C0D0D0;
489.dropdown:hover .dropdown-content {
502 -webkit-overflow-scrolling: touch;
506 background-color: #D0D0D0;
508 margin: 4px 2px 0px 2px;
509 border-top-left-radius: 10px;
510 border-top-right-radius: 10px;
512 border-top: 1px solid Transparent;
513 border-right: 1px solid Transparent;
514 border-left: 1px solid Transparent;
516 padding: 3px 5px 3px 5px;
519 background-color: #FFFFFF;
520 border-bottom: 5px solid #FFFFFF;
522.etab button.edt_active:hover {
523 background-color: #FFFFFF;
524 border-bottom: 5px solid #FFFFFF;
526.etab button.edt_active {
527 background-color: white;/*Transparent;*/
528 border-top: 1px solid gray;
529 border-right: 1px solid gray;
530 border-left: 1px solid gray;
531 border-bottom: 5px solid white;
541 background-color: #f0f0f0;
542 border: 1px solid black;
545 padding: 0px 2px 0px 2px;
549 background-color: #C0D0D0;
552 font-family: verdana,tahoma,sans-serif;
560 background-color: #FD5E59;
570 flex-direction: column;
571 min-width: max-content;
572 min-height: max-content;
573 border: 1px solid black;
574 box-shadow: 6px 6px 10px 4px rgba(0,0,0,0.2);
581 border: 2px dashed #6bb28c;
582 background-color: white;
585 background-color: gray;
590 text-overflow: ellipsis;
591 max-width: calc(10em - 30px);
593#nextFNContainer img {
596 vertical-align: middle;
599const base_seq_css = seq_css;
601// line connector string
603// revisions array, maximum nRevisions
604const previousRevisions = {};
605const nRevisions = 20;
606var revisionIndex = {};
607var saveRevision = {};
608// Meta combo keydown flag
609var MetaCombo = false;
610// Deal with Chrome issues
611var isChrome = browserType();
612// Make current state global
613var stateText = "Unknown";
615// -- Sequencer specific functions --
616// Implement colors and styles from KeywordsGroups in CSS
619 for (const group in mslDefs.groups) {
620 const { mslclass, color, fontWeight } = mslDefs.groups[group];
623 syntaxCSS += `.${mslclass} { color: ${color}; font-weight: ${fontWeight}; }\n`;
625 syntaxCSS += `.${mslclass} { color: ${color};}\n`;
629 seq_css = base_seq_css + syntaxCSS;
630 let seqStyle = document.getElementById("seq-editor-style");
632 seqStyle = document.createElement("style");
633 seqStyle.id = "seq-editor-style";
634 document.head.appendChild(seqStyle);
636 seqStyle.textContent = seq_css;
640// Setup the correct sequencer state visually
641function seqState(funcCall) {
643 funcCall - (optional) a function to be called when the state is set (with the state text)
645 stateText = "Stopped";
646 // Check sequence state
647 mjsonrpc_db_get_values([`${SeqODB}/State/Running`,`${SeqODB}/State/Paused`,`${SeqODB}/State/Finished`,`${SeqODB}/State/Debug`,`${SeqODB}/State/Stop after run`]).then(function(rpc) {
648 if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2] && !rpc.result.data[4]) {
649 stateText = "Running";
650 } else if (rpc.result.data[1] && rpc.result.data[0] && !rpc.result.data[2] && !rpc.result.data[4]) {
651 stateText = "Paused";
652 } else if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2] && rpc.result.data[4]) {
653 // Check if stop after run is set and running
654 stateText = "Running.StopAfter"
655 } else if (rpc.result.data[0] && rpc.result.data[1] && !rpc.result.data[2] && rpc.result.data[4]) {
656 // Check if stop after run is set and paused
657 stateText = "Paused.StopAfter"
659 stateText = "Stopped";
666 }).catch (function (error) {
667 console.error(error);
672// Ask user to edit current sequence
673function askToEdit(flag,event) {
675 openETab(document.getElementById("etab1-btn"));
676 const [lineNumbers,editor,btnLabel,label] = editorElements();
677 // make editable and add event listeners
678 //editor.contentEditable = true;
679 addETab(document.getElementById("addETab"));
680 seqOpen(label.title.split("\n")[0]);
681 event.stopPropagation();
684 const message = "To edit the sequence it must be opened in a separate tab.<br>Would you like to proceed?";
685 dlgConfirm(message,function(resp) {
687 const label = editorElements()[3];
688 addETab(document.getElementById("addETab"));
689 seqOpen(label.title.split("\n")[0]);
695// Main tab editor, opens automatically when sequencer is stopped
696// Defauls button color is red, becomes white if someone start the sequencer
697function mainEditor() {
698 openETab(document.getElementById("etab1-btn"));
699 let [lineNumbers,mainTab,btnLabel,label] = editorElements();
700 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) {
701 let seqLines = rpc.result.data[0];
702 let seqState = rpc.result.data[1];
703 let currLine = rpc.result.data[2];
704 let filename = rpc.result.data[3] ? rpc.result.data[3] : "Untitled";
705 let sfilename = rpc.result.data[4] ? rpc.result.data[4].split('userfiles/sequencer/')[1] : "";
706 filename = (mslDefs.filename.path + "/" + rpc.result.data[5] + "/" + filename).replace("//","/");
709 lineNumbers.style.display = "none";
710 mainTab.style.display = "none";
711 btnLabel.parentElement.style.display = "none";
712 // Insert tab in second position
713 const btn = (document.querySelector(".edt_main")) ? document.querySelector(".edt_main") : addETab(document.getElementById("addETab"),2);
714 btn.querySelector(".closebtn").style.display = "none";
715 // Make sure you switch to main editor tab
717 [lineNumbers,editor,btnLabel,label] = editorElements();
718 btn.style.backgroundColor = "var(--mred)";
719 btn.className += " edt_main";
721 editor.innerHTML = syntax_msl(seqLines).join(lc);
722 updateLineNumbers(editor.previousElementSibling,editor);
724 let file = filename.split("/").pop();
725 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
726 btnLabel.title = filename.replaceAll(/\/+/g, '/');
727 btnLabel.innerText = file;
728 sessionStorage.setItem("fileName",file);
729 sessionStorage.setItem("pathName",path);
731 // Is there an editor already?
732 const btn = document.querySelector(".edt_main");
734 // Check if it is changed
736 if (seqIsChanged()) {
737 btn.classList.remove("edt_main");
738 btn.style.backgroundColor = "";
739 dlgAlert("Warning: Sequencer started from a different browser.<br>Make save you save your changes to a different file name!");
740 btn.querySelector(".closebtn").style.display = "";
741 btn.children[0].title = "";
742 btn.children[0].innerText = "Untitled";
744 btn.querySelector(".closebtn").click();
748 lineNumbers.style.display = "";
749 mainTab.style.display = "";
750 btnLabel.parentElement.style.display = "";
753 }).catch (function (error) {
754 console.error(error);
758// Enable editing of sequence
759function editorEventListeners() {
760 let [lineNumbers,editor] = editorElements();
761 editor.contentEditable = true;
762 // Attached syntax highlight event editor
763 editor.addEventListener("keydown",checkSyntaxEventDown);
764 editor.addEventListener("keyup",checkSyntaxEventUp);
765 editor.addEventListener("paste", checkSyntaxEventPaste,true);
767 editor.addEventListener("input", function() {
770 document.addEventListener("selectionchange", function(event) {
771 if (event.target.activeElement === editor) markCurrLineNum();
773 // Synchronize the scroll position of lineNumbers with editor
774 editor.addEventListener("scroll", function() {
775 lineNumbers.scrollTop = editor.scrollTop;
777 if (isChrome === 1) {
778 editor.addEventListener("keydown", arrowKeysChrome);
782// apply changes of filename in the ODB (triggers reload)
783function seqChange(filename) {
785 filename - full file name with path to change
787 if (!filename) return;
788 const lastIndex = filename.lastIndexOf('/');
789 const path = filename.substring(0, lastIndex).replace(new RegExp('^' + mslDefs.filename.path),"").replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
790 const file = filename.substring(lastIndex + 1);
791 // set path and filename, wait for completion and return
792 mjsonrpc_db_paste([`${SeqODB}/State/Path`,`${SeqODB}/State/Filename`],[path,file]).then(function (rpc1) {
793 sessionStorage.removeItem("depthDir");
794 if (rpc1.result.status[0] === 1 && rpc1.result.status[1] === 1) {
795 mjsonrpc_db_paste([`${SeqODB}/Command/Load new file`],[true]).then(function (rpc2) {
796 if (rpc2.result.status[0] === 1) {
799 }).catch(function (error) {console.error(error);});
801 dlgAlert("Something went wrong, I could not set the filename!");
803 }).catch(function (error) {console.error(error);});
806// Save sequence text in filename.
807function seqSave(filename) {
809 filename (opt) - save to provided filename with path. If undefined save to original
810 filename and if empty trigger file_picker.
812 let [lineNumbers,editor,label,btn] = editorElements();
813 let currFilename = document.getElementById("etab1-btn").title.split("\n")[0];
814 let text = editor.innerText;
815 let ext = mslDefs.filename.ext.replace("*","");
816 if (editor.id !== "editorTab1") {
817 if (filename === undefined) {
818 // take name from button title
819 filename = label.title;
820 if (filename.endsWith(ext)) {
821 if (filename == currFilename && stateText === "Running") {
822 const message = `Sequence ${filename} is currently running.<br>Are you sure you want to overwrite it?`
823 dlgConfirm(message,function(resp) {
825 file_save_ascii_overwrite(filename,text);
830 file_save_ascii_overwrite(filename,text);
836 } else if (filename === "") {
837 filename = label.title;
838 let file = filename.split("/").pop();
839 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
840 // If file/path are empty start with default value
842 path = sessionStorage.getItem("pathName") ? sessionStorage.getItem("pathName") : mslDefs.filename.path + "/";
844 file = sessionStorage.getItem("fileName") ? sessionStorage.getItem("fileName") : "filename" + ext;
845 file_picker(path,mslDefs.filename.ext,seqSave,true,{},true);
847 file_save_ascii_overwrite(filename,text);
848 let file = filename.split("/").pop();
849 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
850 label.title = filename;
851 label.innerText = file;
852 sessionStorage.setItem("fileName",file);
853 sessionStorage.setItem("pathName",path);
856 // Check if filename is currently in editorTab1 and reload
857 // If we are saving the stopped-state main editor, make the saved file
858 // the selected sequence as well. This makes Save As from the main editor
859 // behave like users expect: the visible main file remains the active one.
860 if (btn.classList.contains("edt_main") && stateText === "Stopped" && filename.endsWith(ext)) {
868function seqOpen(filename) {
870 filename - file name to open (empty trigger file_picker)
872 // if a full filename is provided, open and return
873 if (filename && filename !== "") {
874 // Identify active tab
875 let [lineNumbers,editor,label] = editorElements();
876 // Check the option to open in new tab, also open in new tab if sequence is running
877 if ((document.getElementById("inNewTab").checked && (label.title !== "" || editor.id !== "editorTab1")) ||
878 (editor.id === "editorTab1" && stateText === "Running")) {
879 addETab(document.getElementById("addETab"));
880 [lineNumbers,editor,label] = editorElements();
882 let file = filename.split("/").pop();
883 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
884 label.title = filename.replaceAll(/\/+/g, '/');
885 label.innerText = file;
886 sessionStorage.setItem("fileName",file);
887 sessionStorage.setItem("pathName",path);
888 if (editor.id === "editorTab1") {
891 file_load_ascii(filename, function(text) {
892 editor.innerHTML = syntax_msl(text).join(lc).slice(0,-1);
893 updateLineNumbers(lineNumbers,editor);
894 // Change state to not edited
897 saveState(text,editor);
902 // empty or undefined file name
903 mjsonrpc_db_get_values([`${SeqODB}/State/Path`]).then(function(rpc) {
904 let path = mslDefs.filename.path + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
905 sessionStorage.setItem("pathName",path);
906 file_picker(path,mslDefs.filename.ext,seqOpen,false);
907 }).catch(function (error) {
908 mjsonrpc_error_alert(error);
913// Show/hide buttons according to sequencer state
914function updateBtns(state) {
916 state - the state of the sequencer
920 color: "var(--mgreen)",
923 color: "var(--mred)",
926 color: "var(--mred)",
929 color: "var(--myellow)",
932 const color = seqState[state.split(".")[0]].color;
933 const nclass = state.split(".").length;
934 const seqStateSpans = document.querySelectorAll('.seqstate');
935 seqStateSpans.forEach(e => {e.style.backgroundColor = color;});
937 const hideBtns = document.querySelectorAll('.seqbtn');
938 hideBtns.forEach(button => {
939 button.style.display = "none";
941 // then show only those belonging to the current state
942 const showBtns = document.querySelectorAll('.seqbtn.' + state);
943 showBtns.forEach(button => {
944 if (button.tagName === "IMG") {
945 button.style.display = "inline-block";
947 button.style.display = "flex";
950 // Hide exclusive buttons
952 const exclBtns = document.querySelectorAll('.seqbtn.Exclusive');
953 exclBtns.forEach(button => {
954 button.style.display = "none";
957 // Hide progress modal when stopped
958 const hideProgress = document.getElementById("Progress");
959 if (state === "Stopped" && hideProgress) hideProgress.style.display = "none";
963// Show sequencer messages if present
964function mslMessage(message) {
965 // Empty message, return
966 if (!message) return; // Check message and message wait
967 mjsonrpc_db_get_values([`${SeqODB}/State/Message`,`${SeqODB}/State/Message Wait`]).then(function(rpc) {
968 const message = rpc.result.data[0];
969 const hold = rpc.result.data[1];
971 dlgMessage("Message", message, true, false,clrMessage);
975 }).catch (function (error) {
976 console.error(error);
980// Clear sequencer messages
981function clrMessage() {
982 mjsonrpc_db_paste([`${SeqODB}/State/Message`], [""]).then(function (rpc) {
984 }).catch(function (error) {
985 console.error(error);
989// Adjust size of sequencer editor according to browser window size
990function windowResize() {
991 const [lineNumbers, editor] = editorElements();
992 const seqTable = document.getElementById("seqTable");
993 const infoColumn = document.getElementById("infoColumn");
995 // Calculate scrollbar width
996 const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
998 // Visible width of the window accounting for scrollbar
999 let winWidth = Math.max(document.documentElement.clientWidth, window.innerWidth) - scrollbarWidth;
1001 // Visible height of the window
1002 let winHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
1004 // Set seqTable height and width to fit the remaining space in the viewport
1005 seqTable.style.width = winWidth - seqTable.getBoundingClientRect().left + "px";
1006 seqTable.style.height = winHeight - seqTable.getBoundingClientRect().top - 20 + "px";
1008 // Adjust editor dimensions
1009 const editorTop = editor.getBoundingClientRect().top;
1010 editor.style.height = winHeight - editorTop - 20 + "px";
1011 editor.style.width = winWidth - editor.getBoundingClientRect().left - infoColumn.getBoundingClientRect().width - 10 + "px";
1012 editor.style.maxWidth = editor.style.width;
1014 // Adjust infoColumn and lineNumbers heights to match the editor height
1015 infoColumn.style.height = editor.style.height;
1016 lineNumbers.style.height = editor.style.height;
1019// Load the current sequence from ODB (only on main tab)
1020function load_msl() {
1021 const editor = document.getElementById("editorTab1");
1022 const btn = document.getElementById("etab1-btn");
1023 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) {
1024 let seqLines = rpc.result.data[0];
1025 let seqState = rpc.result.data[1];
1026 let currLine = rpc.result.data[2];
1027 let filename = rpc.result.data[3];
1028 let sfilename = rpc.result.data[4] ? rpc.result.data[4].split('userfiles/sequencer/')[1] : "";
1029 filename = (mslDefs.filename.path + "/" + rpc.result.data[5] + "/" + filename).replace("//","/");
1032 editor.innerHTML = syntax_msl(seqLines).join(lc);
1033 updateLineNumbers(editor.previousElementSibling,editor);
1034 if (seqState) hlLine(currLine);
1036 let file = filename.split("/").pop();
1037 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
1038 sessionStorage.setItem("fileName",file);
1039 sessionStorage.setItem("pathName",path);
1040 // change button title to add sfilename if present
1041 btn.title = (sfilename && sfilename !== filename) ? filename + "\n" + sfilename : filename;
1042 // also change validation icon to defaule
1043 let vldBtn = document.getElementById("vldBtn");
1044 vldBtn.src = "icons/validate-syntax.svg";
1045 document.querySelectorAll('.seqbtn').forEach(btn => {
1046 btn.style.backgroundColor = "";
1048 }).catch (function (error) {
1049 console.error(error);
1053// Highlight (background color) and scroll to current line
1054function hlLine(lineNums,color,editor,msgs = []) {
1056 lineNums- the line number to be highlighted (or an array of numbers)
1057 color - (optional) background color
1058 editor - (optional) if provided, scroll to highlighted line in editor
1059 msgs - (optional) if provided, use as title/s for the highlighted line/s
1062 // Remove highlight from all lines with the class "msl_current_line"
1063 const highlightedLines = document.querySelectorAll(".msl_current_line");
1064 highlightedLines.forEach((line) => line.classList.remove("msl_current_line"));
1066 // If single value make an array
1067 lineNums = Array.isArray(lineNums) ? lineNums : [lineNums];
1068 msgs = Array.isArray(msgs) ? msgs : [msgs];
1070 lineNums.forEach(lineNum => {
1071 const lineId = "sline" + lineNum;
1072 const lineHTML = (editor) ? editor.querySelector(`#${lineId}`) : document.getElementById(lineId);
1075 lineHTML.classList.add("msl_current_line");
1076 if (color) lineHTML.style.backgroundColor = color;
1077 if (msgs[counter]) lineHTML.title = msgs[counter];
1078 // Scroll to the highlighted line if the checkbox is checked
1079 const scrollToCurrCheckbox = document.getElementById("scrollToCurr");
1080 if (((scrollToCurrCheckbox && scrollToCurrCheckbox.checked) || editor) && (counter == 0)) {
1081 lineHTML.scrollIntoView({ block: "center" });
1088// Scroll to make line appear in the center of editor
1089function scrollToCurr(scrToCur) {
1090 if (scrToCur.checked) {
1091 localStorage.setItem("scrollToCurr",true);
1092 const currLine = document.querySelector(".msl_current_line");
1094 currLine.scrollIntoView({ block: "center" });
1097 localStorage.removeItem("scrollToCurr",true);
1101// Open files in new tabs
1102function toggleCheck(e) {
1104 localStorage.setItem(e.id,true);
1106 localStorage.removeItem(e.id);
1110// shortcut event handling to overtake default behaviour
1111function shortCutEvent(event) {
1112 const parEditor = editorElements();
1113 const notFirstTab = (parEditor[1].id !== "editorTab1");
1115 // Check these only for editors
1116 if (event.altKey && event.key === 's') {
1117 event.preventDefault();
1118 //save as with file_picker
1120 event.preventDefault();
1121 } else if ((event.ctrlKey || event.metaKey) && event.key === 's') {
1122 event.preventDefault();
1123 //save to the same filename
1125 event.preventDefault();
1126 } else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
1127 event.preventDefault();
1128 undoEdit(event.target);
1129 event.preventDefault();
1130 } else if ((event.ctrlKey || event.metaKey) && event.key === 'r') {
1131 event.preventDefault();
1132 redoEdit(event.target);
1133 event.preventDefault();
1137 // Check these only for first tab
1138 if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
1139 // open new tab and load current sequence
1140 event.preventDefault();
1141 addETab(document.getElementById("addETab"));
1142 seqOpen(parEditor[3].title.split("\n")[0]);
1143 event.preventDefault();
1146 // Check these for all tabs
1147 if (event.altKey && event.key === 'n') {
1149 event.preventDefault();
1150 addETab(document.getElementById("addETab"));
1151 event.preventDefault();
1152 } else if (event.altKey && event.key === 'o') {
1153 event.preventDefault();
1155 event.preventDefault();
1160// Trigger syntax highlighting on keyup events
1161function checkSyntaxEventUp(event) {
1162 if (event.ctrlKey || event.altKey || event.metaKey || MetaCombo) return;
1163 if (event.keyCode >= 0x30 || event.key === ' '
1164 || event.key === 'Backspace' || event.key === 'Delete'
1165 || event.key === 'Enter'
1168 const e = event.target;
1169 let caretPos = getCurrentCursorPosition(e);
1170 let currText = e.innerText;
1171 // Indentation keywords
1172 const defIndent = mslDefs.defIndent;
1173 const nIndent = defIndent.nIndent || 3;
1174 // save current revision for undo
1175 if (event.key === ' ' || event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
1176 saveState(currText,e);
1180 // Indent according to previous line
1181 if (event.key === 'Enter') {
1182 event.preventDefault();
1183 // get previous and current line elements (before and after enter)
1184 let pline = whichLine(e,-1);
1185 let cline = whichLine(e);
1186 let plineText = (pline) ? pline.innerText : "";
1187 let clineText = (cline) ? cline.innerText : "";
1188 let indentLevel = 0;
1190 let indentString = "";
1192 // indent line according to the previous line text
1193 // if, loop, else, subroutine - increase indentation
1194 const indentPlus = defIndent.indentplus.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1195 // else, endif, endloop, endsubroutine - decrease indentation
1196 const indentMinos = defIndent.indentminos.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1197 /* (indentMinos/indentPlus)
1198 true/false - pline indent -1, cline indent 0
1199 fale/true - pline indent 0, cline indent +1
1200 true/true - pline indent -1, cline indent +1
1201 false/false- pline indent 0, cline indent 0
1203 // Count number of white spaces at begenning of pline
1204 preSpace = plineText.replace("\n","").search(/\S|$/);
1205 pPreSpace = preSpace - indentMinos * nIndent;
1206 if (pPreSpace < 0) pPreSpace = 0;
1207 cPreSpace = pPreSpace + indentPlus * nIndent;
1208 // Calculate and insert indentation
1209 pIndentString = " ".repeat(pPreSpace);
1210 cIndentString = " ".repeat(cPreSpace);
1211 cline.innerText = cIndentString + clineText.trimStart();
1212 caretPos += cline.innerText.length - clineText.length;
1213 pline.innerText = pIndentString + plineText.trimStart();
1214 caretPos += pline.innerText.length - plineText.length;
1216 event.preventDefault();
1218 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
1219 setCurrentCursorPosition(e, caretPos);
1220 updateLineNumbers(e.previousElementSibling,e);
1226// Handle arrow up/down keys in Chrome
1227function arrowKeysChrome(event) {
1228 // Skip combos with special keys
1229 if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
1230 if ((event.key === "ArrowUp" || event.key === "ArrowDown") && event.shiftKey ) {
1231 event.preventDefault();
1232 startChromeSelecting(event.target);
1233 } else if ((event.key === "ArrowUp" || event.key === "ArrowDown") && !event.shiftKey ) {
1234 event.preventDefault();
1235 const e = event.target;
1236 let caretPos = getCurrentCursorPosition(e);
1237 let ncaretPos = caretPos;
1238 let currText = e.innerText;
1239 let lines = currText.split("\n");
1240 // Determine the current line number (0-based index)
1241 let clineNum = getLinesInSelection(e)[0] - 1;
1242 // Determine the direction: -1 for up, +1 for down
1243 let addLine = (event.key === "ArrowDown") ? 1 : -1;
1244 // Determine the target line number
1245 let nlineNum = clineNum + addLine;
1246 if (lines[nlineNum] !== undefined) {
1247 // line exists can move cursor
1248 let clineStart = currText.split("\n").slice(0, clineNum).join("\n").length + (clineNum > 0 ? 1 : 0);
1249 let caretInLine = caretPos - clineStart;
1251 let nlength = lines[nlineNum].length;
1252 let nlineStart = currText.split("\n").slice(0, nlineNum).join("\n").length + (nlineNum > 0 ? 1 : 0);
1253 if (nlength >= caretInLine && caretInLine >= 0) {
1254 // If the target line is longer or equal to the caret's position in the current line
1255 ncaretPos = nlineStart + caretInLine;
1256 } else if (caretInLine < 0) {
1257 ncaretPos = nlineStart + nlength + 1;
1259 // If the target line is shorter than the caret's position in the current line
1260 ncaretPos = nlineStart + nlength;
1262 setCurrentCursorPosition(e, ncaretPos);
1268// Trigger syntax highlighting on keydown events
1269function checkSyntaxEventDown(event) {
1270 sessionStorage.setItem("keydown",event.target.innerText);
1271 // take care of Mac odd keyup behaviour
1272 if (event.metaKey && (/^[a-z]$/.test(event.key) || event.shiftKey || event.altKey)) {
1278 if (event.ctrlKey || event.altKey || event.metaKey) return;
1279 // Quickly return for anything but these keys
1280 if (event.key !== 'Tab' && event.key !== 'Escape' && event.key !== 'Backspace' && event.key !== 'Delete' && event.key !== 'Enter') return;
1281 const e = event.target;
1282 event.preventDefault();
1283 const textSelected = isTextSelected(e);
1284 if (textSelected && event.key !== 'Tab' && event.key !== 'Escape') {
1285 deleteSelectedText(e);
1287 let caretPos = getCurrentCursorPosition(e);
1288 let currText = e.innerText;
1290 if (event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
1291 //if (event.key === 'Enter') {
1292 // Handle Backspace, Delete and Enter manually for better compatibility and control
1293 if (event.key === 'Enter') {
1294 currText = currText.substring(0,caretPos) + "\n" + currText.substring(caretPos);
1295 caretPos = caretPos + 1 <= currText.length ? caretPos + 1 : caretPos;
1296 if (currText.substring(caretPos) == "" && isChrome === 1) currText = currText + "\n";
1297 } else if (event.key === 'Backspace') {
1298 currText = currText.substring(0,caretPos).slice(0,-1) + currText.substring(caretPos);
1299 caretPos = caretPos - 1 >= 0 ? caretPos - 1 : caretPos;
1300 } else if (event.key === 'Delete') {
1302 currText = currText.substring(0,caretPos) + currText.substring(caretPos).slice(0);
1304 currText = currText.substring(0,caretPos) + currText.substring(caretPos).slice(1);
1307 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
1308 setCurrentCursorPosition(e, caretPos);
1309 keepCaretVisible(e);
1311 } else if (event.key === 'Tab' || event.key === 'Escape') {
1312 let lines = getLinesInSelection(e);
1313 if (event.shiftKey && event.key === 'Tab') {
1314 indent_msl(lines,-1);
1315 } else if (event.key === 'Tab') {
1316 indent_msl(lines,+1);
1317 } else if (event.key === 'Escape') {
1320 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
1321 let newText = e.innerText;
1322 setCurrentCursorPosition(e, caretPos + newText.length - currText.length);
1323 if (lines[0] !== lines[1]) selectLines(lines,e);
1328// Trigger syntax highlighting when you paste text
1329function checkSyntaxEventPaste(event) {
1330 // set time out to allow default to go first
1332 let e = event.target;
1333 // make sure you paste in the editor area
1334 if (e.tagName !== 'PRE') e = e.parentElement;
1336 while (e && e.tagName !== 'PRE') e = e.parentElement;
1338 let caretPos = getCurrentCursorPosition(e);
1339 let currText = e.innerText;
1340 // save current revision for undo
1341 saveState(currText,e);
1342 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
1343 setCurrentCursorPosition(e, caretPos);
1344 updateLineNumbers(e.previousElementSibling,e);
1350// Find on which line is the current carret position in e
1351// This assumes each line has an id="sline#" where # is the line number.
1352function whichLine(e,offset = 0) {
1353 // offset allows to pick previous line (after enter)
1354 let pos = getCurrentCursorPosition(e);
1356 let lineNum = e.innerText.substring(0,pos).split("\n").length + offset;
1357 let sline = e.querySelector("#sline" + lineNum.toString());
1361// Return an array with the first and last line numbers of the selected region
1362/* This assumes that the lines are in a <pre> element and that
1363 each line has an id="sline#" where # is the line number.
1364 When the caret in in an empty line, the anchorNode is the <pre> element.
1366function getLinesInSelection(e) {
1367 const selection = window.getSelection();
1368 if (selection.rangeCount === 0) return [0,0];
1369 // is it a single line?
1370 const singleLine = selection.isCollapsed;
1372 const line = whichLine(e);
1374 const startLine = parseInt(line.id.replace("sline",""));
1375 return [startLine,startLine];
1380 const anchorNode = selection.anchorNode;
1381 const range = selection.getRangeAt(0);
1382 let startNode,endNode;
1383 if (anchorNode.tagName === 'PRE') {
1384 let startOffset = range.startOffset;
1385 let endOffset = range.endOffset;
1386 startNode = range.startContainer.childNodes[startOffset];
1387 endNode = range.startContainer.childNodes[endOffset-1];
1389 startNode = (range.startContainer && range.startContainer.parentElement.tagName !== 'PRE') ? range.startContainer : range.startContainer.nextSibling;
1390 if (startNode && startNode.tagName === 'PRE') startNode = startNode.firstChild;
1391 endNode = (range.endContainer && range.endContainer.parentElement.tagName !== 'PRE') ? range.endContainer : range.endContainer.previousSibling;
1392 if (endNode && endNode.tagName === 'PRE') endNode = endNode.lastChild;
1394 let startID = (startNode && startNode.id) ? startNode.id : "";
1395 let endID = (endNode && endNode.id) ? endNode.id : "";
1397 while (startNode && !startID.startsWith("sline") && startNode.tagName !== 'PRE') {
1398 startNode = (startNode.parentNode.tagName !== 'PRE') ? startNode.parentNode : startNode.nextSibling;
1399 startID = (startNode && startNode.id) ? startNode.id : "";
1402 while (endNode && !endID.startsWith("sline") && endNode.tagName !== 'PRE') {
1403 endNode = (endNode.parentNode.tagName !== 'PRE') ? endNode.parentNode : endNode.previousSibling;
1404 endID = (endNode && endNode.id) ? endNode.id : "";
1406 let startLine = (startNode && startNode.id) ? parseInt(startNode.id.replace("sline","")) : 0;
1407 let endLine = (endNode && endNode.id) ? parseInt(endNode.id.replace("sline","")) : 0;
1409 startLine = endLine = Math.min(startLine, endLine);
1411 return [startLine,endLine];
1414// get current caret position in chars within element parent
1415function getCurrentCursorPosition(parent) {
1416 let sel = window.getSelection();
1417 if (!sel.focusNode || !parent) return;
1418 const range = sel.getRangeAt(0);
1419 const prefix = range.cloneRange();
1420 prefix.selectNodeContents(parent);
1421 prefix.setEnd(range.endContainer, range.endOffset);
1422 return prefix.toString().length;
1425// set current caret position at pos within element parent
1426function setCurrentCursorPosition(parent,pos) {
1427 for (const node of parent.childNodes) {
1428 if (node.nodeType === Node.TEXT_NODE) {
1429 if (node.length >= pos) {
1430 const range = document.createRange();
1431 const sel = window.getSelection();
1432 range.setStart(node, pos);
1433 range.collapse(true);
1434 sel.removeAllRanges();
1435 sel.addRange(range);
1438 pos = pos - node.length;
1441 pos = setCurrentCursorPosition(node, pos);
1450// Update line numbers in lineNumbers div
1451function updateLineNumbers(lineNumbers,editor) {
1452 if (lineNumbers === undefined || editor === undefined)
1453 [lineNumbers,editor] = editorElements();
1454 // Clear existing line numbers
1455 lineNumbers.innerHTML = "";
1456 // Get the number of lines accurately
1457 let lineCount = editor.querySelectorAll('[id^="sline"]').length;
1458 let lineTextCount = editor.innerText.split("\n").length;
1459 lineCount = (lineTextCount - lineCount) < 2 ? lineTextCount : lineTextCount - 1;
1460 // Add line numbers to lineNumbers
1461 for (let i = 1; i <= lineCount; i++) {
1462 const lineNumber = document.createElement('span');
1463 lineNumber.id = "lNum" + i.toString();
1464 lineNumber.textContent = i;
1465 lineNumbers.appendChild(lineNumber);
1467 lineNumbers.scrollTop = editor.scrollTop;
1471// Utility function to escape special characters in a string for use in a regular expression
1472function escapeRegExp(s) {
1474 return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
1477function syntax_msl(seqLines,keywordGroups) {
1478 // Use default if none provided
1479 if (!keywordGroups) {
1480 keywordGroups = mslDefs.groups;
1484 let mslText = Array.isArray(seqLines) ? seqLines.join("\n") : seqLines;
1485 mslText = escapeSpecialCharacters(mslText);
1486 const seqLines_org = mslText.split(/\n/);
1488 // Loop over all keyword groups
1489 for (const [groupName, group] of Object.entries(keywordGroups)) {
1490 const className = group.mslclass;
1491 const spanStart = `<span class='${className}'>`;
1492 const spanEnd = `</span>`;
1494 // If kwregex and keywords are defined
1495 if (group.keywords && group.kwregex && group.kwregex.length === 3) {
1496 const [prefix, suffix, flags] = group.kwregex;
1497 const regexStr = prefix + group.keywords.join("|") + suffix;
1498 const regex = new RegExp(regexStr, flags);
1500 mslText = mslText.replace(regex, (...args) => {
1501 const match = args[0];
1502 const captureGroups = args.slice(1, -2); // remove offset & input
1503 if (captureGroups.length === 0) {
1504 return `${spanStart}${match}${spanEnd}`;
1506 // Wrap last capturing group
1511 for (let i = 0; i < captureGroups.length; i++) {
1512 const part = captureGroups[i];
1513 if (part === undefined) continue;
1514 const idx = raw.indexOf(part, lastIndex);
1515 result += raw.slice(lastIndex, idx);
1516 if (i === captureGroups.length - 1) {
1517 result += `${spanStart}${part}${spanEnd}`;
1521 lastIndex = idx + part.length;
1523 result += raw.slice(lastIndex);
1528 // If explicit regex patterns are provided
1530 group.regex.forEach(r => {
1532 const flags = r.flags || "g";
1533 const regex = new RegExp(r.pattern, flags);
1534 if (groupName === 'multilineComments') {
1535 mslText = mslText.replace(regex, (fullMatch) => {
1536 // Split the full match by lines
1537 const lines = fullMatch.split('\n');
1538 // Process each line individually
1539 const processedLines = lines.map(line => {
1540 return `${spanStart}${line}${spanEnd}`;
1542 // Join back with newlines
1543 return processedLines.join('\n');
1546 mslText = mslText.replace(regex, `${spanStart}$&${spanEnd}`);
1553 // Split processed lines
1554 seqLines = mslText.split("\n");
1556 // Firefox / Chrome handling
1557 const emptyClass = (typeof isChrome !== 'undefined' && isChrome === 1) ? "esline" : "";
1559 for (let j = 0; j < seqLines_org.length; j++) {
1560 const line = seqLines_org[j];
1561 // Add span wrapper for editor tracking
1563 const id = `sline${j + 1}`;
1564 seqLines[j] = `<span class='${emptyClass}' id='${id}'></span>`;
1566 const id = `sline${j + 1}`;
1567 seqLines[j] = `<span class='sline' id='${id}'>${seqLines[j]}</span>`;
1574// Syntax highlight any text according to provided rules
1575function syntax_msl2(seqLines,keywordGroups) {
1576 // If not provided use the default msl keywords and groups
1577 if (!keywordGroups) {
1579 keywordGroups = mslDefs.groups;
1582 // Make full text if you get an array of lines
1583 let mslText = Array.isArray(seqLines) ? seqLines.join("\n") : seqLines;
1584 // Make some cleanup of illegal characters
1585 mslText = escapeSpecialCharacters(mslText);
1586 // Keep original sequence lines (as array)
1587 let seqLines_org = mslText.split(/\n/);
1588 let nLines = (mslText.match(/\n/g) || []).length;
1590 // These can be done on the text in one go
1592 let reg = /(["'])(.*?)\1/g;
1593 mslText = mslText.replace(reg,'<span class="msl_string">$1$2$1</span>');
1596 //reg = /^(COMMENT|#.*?)(.*)$/gim;
1597 //mslText = mslText.replace(reg,'<span class="msl_comment">$&</span>');
1600 reg = /(?:\$[\w]+|^\b\w+(?=\s*=))/gm; // starting with $ or something =
1601 mslText = mslText.replace(reg,'<span class="msl_variable">$&</span>');
1602 reg = new RegExp("(^(?:\\s*)\\b(" + keywordGroups.variables.keywords.join("|") + ")\\s+)(\\w+)\\b", "gim"); // after PARAM, CAT and SET
1603 mslText = mslText.replace(reg,'$1<span class="msl_variable">$3</span>');
1605 // Data Management group excluding variables (must be after variables)
1606 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1607 mslText = mslText.replace(reg, "$1<span class='msl_data_management'>$2</span>");
1609 // Data Type group (must have comma before the keyword)
1610 reg = new RegExp("(?<=,\\s*)\\b(" + keywordGroups.dataTypes.keywords.join("|") + ")\\b", "gim");
1611 mslText = mslText.replace(reg, "<span class='msl_data_types'>$1</span>");
1613 // Loops group (must be at the begenning of the line)
1614 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.loops.keywords.join("|") + ")\\b", "gim");
1615 mslText = mslText.replace(reg, "$1<span class='msl_loops'>$2</span>");
1617 // Control Flow group
1618 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.controlFlow.keywords.join("|") + ")\\b", "gim");
1619 mslText = mslText.replace(reg, "$1<span class='msl_control_flow'>$2</span>");
1621 // Data Management group
1622 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1623 mslText = mslText.replace(reg, "$1<span class='msl_data_managemen'>$2</span>");
1625 // Information group
1626 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.info.keywords.join("|") + ")\\b", "gim");
1627 mslText = mslText.replace(reg, "$1<span class='msl_info'>$2</span>");
1629 // Conditional group
1630 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.cond.keywords.join("|") + ")\\b", "gim");
1631 mslText = mslText.replace(reg,"$1<span class='msl_cond'>$2</span>");
1634 reg = new RegExp("\\b(" + keywordGroups.units.keywords.join("|") + ")\\b", "gi");
1635 mslText = mslText.replace(reg, "<span class='msl_units'>$1</span>");
1638 reg = new RegExp("\\b(" + keywordGroups.actions.keywords.join("|") + ")\\b(\\s*)$", "gim");
1639 mslText = mslText.replace(reg, "<span class='msl_actions'>$1</span>$2");
1641 // Numbers/boolean group
1642 reg = /\b(\d+(\.\d+)?([eE][-+]?\d+)?)\b/g;
1643 mslText = mslText.replace(reg, '<span class="msl_number">$1</span>');
1644 reg = new RegExp("\\b(" + keywordGroups.bool.keywords.join("|") + ")\\b", "gi");
1645 //reg = /\b(true|false)\b/gi;
1646 mslText = mslText.replace(reg, '<span class="msl_bool">$1</span>');
1648 // Break lines and handle one by one
1649 seqLines = mslText.split("\n");
1651 // This is important for Firefox
1652 let emptyClass = "";
1653 if (isChrome === 1) emptyClass = "esline";
1654 // Loop and restore comment lines and empty lines
1655 for (let j = 0; j < seqLines_org.length ; j++) {
1656 let line = seqLines_org[j];
1657 let inlineComment = mslDefs.groups.comments.keywords[0];
1658 commentIndex = line.indexOf(inlineComment);
1659 if (line.trim().startsWith(inlineComment) || line.trim().toLowerCase().startsWith("comment")) {
1660 // Restore comment lines without highlighting
1661 seqLines[j] = `<span class='msl_comment'>${line}</span>`;
1662 } else if (commentIndex > 0) {
1663 // Restore comment section at end of line
1664 const comment = line.slice(commentIndex);
1665 seqLines[j] = seqLines[j].slice(0, seqLines[j].indexOf("#")) + `</span><span class='msl_comment'>${comment}</span>`;
1668 // empty class is needed for cursor movement in Firefox
1669 // for Chrome empty lines are skipped with arrow up??
1671 if ((j === seqLines_org.length - 1) && (isChrome === 1)) {
1672 seqLines[j] = "<span class=' ' id='sline" + (j+1).toString() + "'></span>";
1673 } else if (isChrome === 1) {
1674 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1676 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1679 seqLines[j] = "<span class='sline' id='sline" + (j+1).toString() + "'>" + seqLines[j] + "</span>";
1685// Adjust indentation of a selection of lines
1686function indent_msl(lines,addTab) {
1688 lines - an array of two elements, first and last line numbers
1689 addTab - (opt) +/-1 to add/subtract three spaces to selected lines
1691 let indentLevel = 0;
1692 let singleLine = false;
1693 let editor = editorElements()[1];
1694 // Indentation keywords
1695 const defIndent = mslDefs.defIndent;
1696 const nIndent = defIndent.nIndent || 3;
1698 // Avoid issues of begenning of single line
1699 if (lines[0] > lines[1] || lines[0] == lines[1]) {
1700 lines[0] = lines[0] > 0 ? lines[0] : 1;
1701 lines[1] = lines[0];
1704 for (let j = lines[0]; j <= lines[1] ; j++) {
1705 let lineId = "#sline" + j.toString();
1706 let prevLineId = "#sline" + (j-1).toString();
1707 let lineEl = editor.querySelector(lineId);
1709 if (lineEl) line = lineEl.innerText;
1711 let indentString = " ".repeat(nIndent);
1712 lineEl.innerText = indentString + line;
1713 } else if (addTab === -1) {
1714 lineEl.innerText = line.replace(new RegExp(`^ {1,${nIndent}}`), '');
1715 } else if (singleLine && editor.querySelector(prevLineId)) {
1716 let prevLineEl = editor.querySelector(prevLineId);
1717 let prevLine = prevLineEl.innerText;
1718 const indentMinos = defIndent.indentminos.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1719 const indentPlus = defIndent.indentplus.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1722 } else if (indentPlus) {
1725 let preSpace = prevLine.search(/\S|$/) + (indentLevel * nIndent);
1726 if (preSpace < 0) preSpace = 0;
1727 let indentString = " ".repeat(preSpace);
1728 lineEl.innerText = indentString + line.trimStart();
1730 const indentMinos = defIndent.indentminos.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1731 if (indentMinos && indentLevel > 0) indentLevel--;
1732 let indentString = " ".repeat(indentLevel * nIndent);
1733 if (line !== "" || indentString !== "") {
1734 lineEl.innerText = indentString + line.trimStart();
1736 const indentPlus = defIndent.indentplus.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1737 if (indentPlus) indentLevel++;
1742// Prepare the parameters/variables (if present) from the ODB as an html table
1743// Also return default ODB Paths and their respective values
1744function varTable(id,odbTreeVar) {
1746 id - ID of div to fill with the table of variables
1747 odbTreeVar - values of ${SeqODB}/Variables
1750 let e = document.getElementById(id);
1752 dlgAlert("Container ID was not give.");
1756 let old_var_names = [];
1757 let new_var_names = [];
1758 document.querySelectorAll(".varname").forEach(el => old_var_names.push(el.innerText));
1760 // If ${SeqODB}/Variables are empty return empty
1761 if (!odbTreeVar || (odbTreeVar && Object.keys(odbTreeVar).length === 0)) {
1762 // Clear container row
1767 let html = "<table id='varTable' class='mtable infotable'>\n";
1768 html += "<tr><th style='min-width: 120px'>Variable </th><th>Current value </th></tr>\n";
1770 // Go over all variables in ODB and count them
1771 for (let key in odbTreeVar) {
1772 const match = key.match(/([^/]+)\/name$/);
1774 const name = match[1];
1775 const value = odbTreeVar[name];
1776 new_var_names.push(name);
1777 let isBool = (typeof(value) === "boolean");
1779 html += `<tr><td class="varname">${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="${SeqODB}/Variables/${name}"></span></td></tr>\n`;
1781 html += `<tr><td class="varname">${name}</td><td><span class="modbvalue" data-odb-path="${SeqODB}/Variables/${name}"></span></td></tr>\n`;
1786 let have_new_var = false;
1787 if (old_var_names.length !== new_var_names.length) {
1788 have_new_var = true;
1790 for (let i = 0; i < old_var_names.length; i++) {
1791 if (old_var_names[i] != new_var_names[i]) {
1792 have_new_var = true;
1802// Prepare the parameters/variables (if present) from the ODB as an object with
1803// html table, default ODB Paths, their respective values and their number
1804function parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq) {
1806 odbTree... - Objects of ODB values
1809 let odbDefPaths = [];
1810 let odbDefValues = [];
1813 html += "<table id='paramTable' class='mtable infotable'>";
1814 html += "<tr><th>Parameter </th><th>Initial value </th><th>Comment</th></tr>";
1816 const processParam = (name, value, isBool, defValue, optValue, comment) => {
1817 let parLine = `<tr><td>${name}</td>`;
1820 // if not given the default is the first option
1821 if (defValue === undefined || defValue === "") defValue = optValue[0];
1822 const optionsHtml = optValue.map(option => `<option value="${option}" ${option === defValue ? 'selected' : ''}>${option}</option>`).join('');
1823 inParLine += `<select class="modbselect" data-odb-path="${SeqODB}/Param/Value/${name}" data-odb-editable="1">${optionsHtml}</select>`;
1824 } else if (isBool) {
1825 inParLine += `<input type="checkbox" class="modbcheckbox" data-odb-path="${SeqODB}/Param/Value/${name}" data-odb-editable="1"></input>`;
1827 inParLine += `<span class="modbvalue" data-odb-path="${SeqODB}/Param/Value/${name}" data-odb-editable="1" data-input="1"></span>`;
1829 if (defValue !== undefined) {
1831 odbDefPaths.push(`${SeqODB}/Param/Value/${name}`);
1832 odbDefValues.push(defValue);
1835 parLine += `<td>${inParLine}</td>`;
1836 parLine += `<td>${comment}</td></tr>`;
1840 // Go over all parameters in ODB
1841 for (let key in odbTreeV) {
1842 const match = key.match(/([^/]+)\/name$/);
1844 const name = match[1];
1845 // if variable is found use its value
1846 let value = (odbTreeVar && odbTreeVar[name]) ? odbTreeVar[name] : odbTreeV[name];
1847 let isBool = (typeof(value) === "boolean");
1848 let defValue = (value !== null && value !== undefined && value !== '') ? value : (odbTreeD && odbTreeD[name]) || value;
1849 let optValue = odbTreeO ? odbTreeO[name] : undefined;
1850 let comment = odbTreeC[name] || '';
1851 if (typeof value !== "object") {
1852 processParam(name, value, isBool, defValue, optValue, comment);
1857 // Go over Edit on sequence links
1858 for (let key in editOnSeq) {
1859 const match = key.match(/([^/]+)\/name$/);
1861 const name = match[1];
1862 const value = editOnSeq[name];
1863 let isBool = (typeof(value) === "boolean");
1865 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`;
1867 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`;
1876 values: odbDefValues,
1881function dlgParam(debugFlag) {
1883 debugFlag - (opt) true/false run in debug/normal mode
1886 // Ensure we read the latest Params from the ODB, in case we just loaded a new file
1887 let paths = [`${SeqODB}/Param`, `${SeqODB}/Variables`];
1889 mjsonrpc_db_get_values(paths).then(function (rpc) {
1890 // Try to use the very latest values from ODB when building
1891 // the parameters table.
1892 // `dlgParamMain()` reads from sessionStorage, so we need to write
1893 // the latest values there.
1894 // Normally sessionStorage is updated by `mhttpd_refresh()` (when it
1895 // updates "modbvalue" spans in the HTML), but we don't want to hang
1896 // around waiting for the next update to happen, so we just update it
1898 const status = rpc?.result?.status ?? [];
1899 const data = rpc?.result?.data ?? [];
1901 if (status[0] === 1) {
1902 sessionStorage.setItem("parameters", JSON.stringify(data[0]));
1905 if (status[1] === 1) {
1906 sessionStorage.setItem("variables", JSON.stringify(data[1]));
1908 dlgParamMain(debugFlag);
1909 }).catch(function (error) {
1910 console.error(error);
1914// Prepare the parameters/variables (if present) from the ODB as a table
1915function dlgParamMain(debugFlag) {
1917 debugFlag - (opt) true/false run in debug/normal mode
1920 let rawOdbTree = sessionStorage.getItem('parameters');
1921 let parsedOdbTree = (rawOdbTree !== null && rawOdbTree !== 'undefined') ? JSON.parse(rawOdbTree) : null;
1922 let odbTree = (parsedOdbTree && Object.keys(parsedOdbTree).length === 0) ? null : parsedOdbTree;
1924 const rawEditOnSeq = sessionStorage.getItem('editonseq');
1925 const parsedEditOnSeq = (rawEditOnSeq !== null && rawEditOnSeq !== 'undefined') ? JSON.parse(rawEditOnSeq) : null;
1926 const editOnSeq = (parsedEditOnSeq && Object.keys(parsedEditOnSeq).length === 0) ? null : parsedEditOnSeq;
1928 // If ${SeqODB}/Param are empty, start and return
1929 if ((odbTree === null || Object.keys(odbTree.value).length === 0) && editOnSeq === null ) {
1930 //if ((odbTree === null || Object.keys(odbTree).length) && (editOnSeq === null || Object.keys(editOnSeq).length)) {
1932 modbset(`${SeqODB}/Command/Debug script`,true);
1934 modbset(`${SeqODB}/Command/Start script`,true);
1939 let odbTreeV = null;
1940 let odbTreeC = null;
1941 let odbTreeD = null;
1942 let odbTreeO = null;
1943 let odbTreeVar = null;
1946 odbTreeV = odbTree.value;
1947 odbTreeC = odbTree.comment;
1948 odbTreeD = odbTree.defaults;
1949 odbTreeO = odbTree.options;
1950 odbTreeVar = JSON.parse(sessionStorage.getItem('variables'));
1953 // Go over all parameters in ODB
1954 let seqParTable = parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq);
1955 let html = seqParTable.html;
1956 // set all default values and once finished produce dialog
1957 // Collect paths where values start with "/"
1958 let valuesLinkODB = seqParTable.values.map((valueLinkODB, indexLinkODB) => {
1959 if (valueLinkODB !== undefined && typeof(valueLinkODB) === "string") {
1960 if (valueLinkODB.startsWith("/")) {
1961 return seqParTable.values[indexLinkODB];
1965 }).filter(path => path !== null);
1966 mjsonrpc_db_get_values(valuesLinkODB).then(function (rpc) {
1967 if (rpc.result.status.every(status => status === 1)) {
1968 // substitute values
1969 rpc.result.data.forEach((newData, index) => {
1970 let pathIndex = seqParTable.values.indexOf(valuesLinkODB[index]);
1971 seqParTable.values[pathIndex] = `${newData}`; // Update corresponding value
1973 mjsonrpc_db_paste(seqParTable.paths,seqParTable.values).then(function (rpc) {
1974 if ((rpc.result.status.every(status => status === 1)) || seqParTable.values.length === 0) {
1975 // if parContainer not given produce a dialog
1976 let htmlDlg = `${html}<br><button class="dlgButtonDefault" id="dlgParamStart" type="button">Start</button><button class="dlgButton" id="dlgParamCancel" type="button">Cancel</button><br>`;
1977 let d = dlgGeneral({html: htmlDlg,iddiv: "Parameters",minWidth:500});
1978 let e = document.getElementById("parContainer");
1979 // Append the table to a container
1980 let startBtn = document.getElementById("dlgParamStart");
1981 let cancelBtn = document.getElementById("dlgParamCancel");
1982 cancelBtn.addEventListener("click", function () {d.remove();});
1983 startBtn.addEventListener("click", function () {
1984 // this is what we need to do after setting the values
1985 let doStart = function() {
1988 modbset(`${SeqODB}/Command/Debug script`,true);
1990 modbset(`${SeqODB}/Command/Start script`,true);
1993 // but first collect values from inputs and set them in ODB
1994 let paramTable = document.getElementById("paramTable");
1995 let paths = [], values = [];
1997 paramTable.querySelectorAll(".modbvalue[data-input='1']").forEach(function(span) {
1998 let input = span.querySelector("input[type='text']");
1999 if (input && span.dataset.odbPath) {
2000 paths.push(evalPath(span.dataset.odbPath));
2001 values.push(input.value);
2005 if (paths.length > 0) {
2006 mjsonrpc_db_paste(paths, values).then(function() { doStart(); })
2007 .catch(function(error) { console.error(error); doStart(); });
2013 // refresh immediately modbvalue elements
2016 dlgAlert("Something went wrong. Please try again!");
2018 }).catch(function (error) {
2019 console.error(error);
2022 let message = `ODB "${valuesLinkODB}" was not found.<br>Cannot start sequence!`;
2025 }).catch(function (error) {
2026 console.error(error);
2031// helper debug function
2032function debugSeq(parContainer) {
2033 startSeq(parContainer,true);
2036// helper start function
2037function startSeq(parContainer,debugFlag) {
2038 const [lineNumbers,editor,label] = editorElements();
2039 if (!debugFlag) debugFlag = false;
2040 if (editor.id !== "editorTab1" && parContainer === undefined) {
2041 let filename = label.title;
2042 if (!filename || filename.endsWith("Untitled")) {
2043 dlgAlert("Please give the file a name first (Save as).");
2046 let seqChanged = seqIsChanged();
2047 let message = debugFlag ? `Start debugging ${filename}?` : `Start ${filename}?`;
2049 message = debugFlag ? `Save and debug ${filename}?` : `Save and start ${filename}?`;
2051 dlgConfirm(message,function(resp) {
2053 if (seqChanged) seqSave(filename);
2054 openETab(document.getElementById("etab1-btn"));
2056 // Make sure to load file and reset parameters
2057 mjsonrpc_db_paste([`${SeqODB}/Command/Load new file`],[true]).then(function (rpc) {
2058 if (rpc.result.status[0] === 1) {
2059 // Wait for Load new file to turn false
2060 checkODBValue(`${SeqODB}/Command/Load new file`,false,dlgParam,debugFlag);
2066 // make sure to load file first
2067 mjsonrpc_db_paste([`${SeqODB}/Command/Load new file`],[true]).then(function (rpc) {
2068 if (rpc.result.status[0] === 1) {
2069 // Wait for Load new file to turn false
2070 checkODBValue(`${SeqODB}/Command/Load new file`,false,dlgParam,debugFlag);
2076// Helper function to add the current file to next files queue
2077function setAsNext() {
2078 const [lineNumbers,editor,label] = editorElements();
2079 // If clicked in the console give a file browser
2080 if (editor.id === "editorTab1") {
2084 // This is the addAsNext button cell
2085 const e = document.getElementById("addAsNext");
2086 let filename = label.title.split("\n")[0].replace(/^sequencer\//,'');
2087 let message = `Save and put ${filename} in the next file queue?`;
2088 dlgConfirm(message,function(resp) {
2090 let order = chngNextFilename(e,filename);
2091 if (order !== -1 && order !== undefined) {
2093 dlgAlert(`File saved and placed in position ${order} in the queue.`);
2099// helper stop function
2101 const message = `Are you sure you want to stop the sequence?`;
2102 dlgConfirm(message,function(resp) {
2104 modbset(`${SeqODB}/Command/Stop immediately`,true);
2109// Show or hide parameters table
2110function showParTable(varContainer) {
2111 let e = document.getElementById(varContainer);
2112 let varTable = document.getElementById("varTable");
2113 let vis = document.getElementById("showParTable").checked;
2114 let visNF = (document.getElementById("nextFNContainer").style.display === "none");
2116 e.style.display = "flex";
2117 e.parentElement.style.width = "285px";
2119 e.style.display = "none";
2121 e.parentElement.style.width = "0px";
2127// Show or hide next file list
2128function showFNTable(nextFNContainer) {
2129 let e = document.getElementById(nextFNContainer);
2130 let vis = document.getElementById("showNextFile").checked;
2131 let visVar = document.getElementById("varContainer") ? (document.getElementById("varContainer").style.display === "none") : false;
2133 let addFileRow = "";
2135 let odbpath = mslDefs.filename.next;
2136 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2137 if (rpc.result.status[0] !== 1) return;
2138 let fList_org = rpc.result.data[0];
2139 // Move full slots to the begenning of the array
2140 fList = fList_org.filter(Boolean);
2141 fList = [...fList, ...new Array(10 - fList.length).fill("")];
2142 // If the cleaned up not equal to original update ODB
2143 if (fList !== fList_org) {
2144 mjsonrpc_db_paste([odbpath],[fList]).then(function (rpc2) {
2145 if (rpc2.result.status[0] !== 1) {
2146 dlgAlert(`${odbpath} needs manual cleanup!<br>Please check.`);
2148 }).catch(function (error) {
2149 console.error(error);
2152 for (let i = 0; i < fList.length; i++) {
2153 if (fList[i] && fList[i].trim() !== "") {
2154 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>`;
2158 if (vis && html !== "") {
2159 e.style.display = "flex";
2160 e.parentElement.style.width = "285px";
2162 disableMenuItems("SetAsNext",false);
2163 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>`;
2165 disableMenuItems("SetAsNext",true);
2167 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>`;
2170 e.style.display = "none";
2172 e.parentElement.style.width = "0px";
2176 activateDragDrop(e);
2177 }).catch (function (error) {
2178 console.error(error);
2182// Show extra rows for wait and loop
2183function extraRows(e) {
2185 e - triggering element to identify wait or loop
2188 if (e === undefined) e=document.getElementById('waitTrig');
2191 // get current row, table and dialog
2192 let row = e.parentElement.parentElement;
2193 let rIndex = row.rowIndex;
2194 let table = row.parentElement;
2196 // check if there is a wait or loop command (if non-zero)
2199 if (e.id === "waitTrig") {
2200 // Wait type. Make sure there is only one wait row.
2201 const waitTRs = document.querySelectorAll('.waitTR');
2202 if (waitTRs.length) waitTRs.forEach(element => element.remove());
2203 let waitType = String(e.value);
2204 let isOdbWait = /^ODB(==|!=|>=|<=|>|<)?$/i.test(waitType);
2205 let tr = table.insertRow(rIndex+1);
2206 tr.className = "waitTR";
2210 let op = waitType.replace(/^ODB/i, "");
2212 // For timed WAIT ODBValue, Wait value/limit keep their ODB
2213 // condition meaning. Timeout value/limit provide the progress bar.
2214 tr.innerHTML = `<td style="position: relative;width:80%;height:36px;" colspan="2">
2215 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: lightgreen; height: 15px;" data-odb-path="${SeqODB}/State/Timeout value" id="mwaitProgress"></span>
2216 <span class="waitlooptxt">
2217 Timeout: [<span class="modbvalue" data-format="f0" data-odb-path="${SeqODB}/State/Timeout value"></span>/<span class="modbvalue" data-odb-path="${SeqODB}/State/Timeout limit" onchange="if (document.getElementById('mwaitProgress')) document.getElementById('mwaitProgress').dataset.maxValue = this.value;"></span>] Seconds
2219 <span class="modbvalue" data-odb-path="${SeqODB}/State/Wait ODB"></span>?
2220 <span class="modbvalue" data-odb-path="${SeqODB}/State/Wait value"></span>
2222 <span class="modbvalue" data-odb-path="${SeqODB}/State/Wait limit"></span>
2225 } else if (e.value === "ClientsRunning" || e.value === "ODBValueStability") {
2226 // Timed WAIT, but without an ODB operator
2227 tr.innerHTML = `<td style="position: relative;width:80%;height:36px;" colspan="2">
2228 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: lightgreen; height: 15px;" data-format="f0" data-odb-path="${SeqODB}/State/Timeout value" id="mwaitProgress"></span>
2229 <span class="waitlooptxt">
2230 Timeout: [<span class="modbvalue" data-format="f0" data-odb-path="${SeqODB}/State/Timeout value"></span>/<span class="modbvalue" data-odb-path="${SeqODB}/State/Timeout limit" onchange="if (document.getElementById('mwaitProgress')) document.getElementById('mwaitProgress').dataset.maxValue = this.value;"></span>] Seconds
2233 Wait: [<span class="modbvalue" data-format="f0" data-odb-path="${SeqODB}/State/Wait value"></span>/<span class="modbvalue" data-odb-path="${SeqODB}/State/Wait limit"></span>] <span class="modbvalue" data-odb-path="${SeqODB}/State/Wait type"></span>
2238 const waitFormula = (e.value === "Seconds") ? 'data-formula="x/1000"' : '';
2239 // Insert the normal wait row
2240 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">
2241 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: lightgreen; height: 15px;" data-format="f0" data-odb-path="${SeqODB}/State/Wait value" ${waitFormula} id="mwaitProgress"></span>
2242 <span class="waitlooptxt">
2243 Wait: [<span class="modbvalue" data-format="f0" 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>
2248 } else if (e.id === "loopTrig") {
2250 mjsonrpc_db_get_values([`${SeqODB}/State/Loop n`]).then(function(rpc) {
2251 let loopArray = rpc.result.data[0];
2252 for (let i = 0; i < loopArray.length; i++) {
2253 if (loopArray[i] === 0) break;
2254 let tr = table.insertRow(rIndex+1);
2255 tr.className = "loopTR";
2256 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">
2257 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: #CBC3E3; height: 15px;" data-odb-path="${SeqODB}/State/Loop counter[${i}]" id="mloopProgress${i}"></span>
2258 <span class="waitlooptxt">
2259 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>]
2264 }).catch (function (error) {
2265 console.error(error);
2270 if (e.id === "waitTrig") {
2271 extraRows.waitSerial++;
2272 document.querySelectorAll('.waitTR').forEach(element => element.remove());
2275 if (e.id === "loopTrig") document.querySelectorAll('.loopTR').forEach(element => element.remove());
2280// Helper function to identify browser, 1 FF, 2 Chrome, 3, other
2281function browserType() {
2282 if (navigator.userAgent.indexOf("Chrome") !== -1) {
2284 } else if (navigator.userAgent.indexOf("Firefox") !== -1) {
2291// make visual hint that file is changes
2292function seqIsChanged(flag) {
2293 // flag - true is changed, false is saved
2294 let fileChangedId = "filechanged" + editorElements()[0].id.replace("lineNumbers","");
2295 let filechanged = document.getElementById(fileChangedId);
2298 filechanged.innerHTML = " ✎";
2299 } else if (flag === undefined) {
2300 // true if text has changed false if not
2301 return (filechanged.innerHTML !== "");
2303 filechanged.innerHTML = "";
2306 // also change validation icon to defaule
2307 let vldBtn = document.getElementById("vldBtn");
2308 vldBtn.src = "icons/validate-syntax.svg";
2309 document.querySelectorAll('.seqbtn').forEach(btn => {
2310 btn.style.backgroundColor = "";
2314// save history of edits in element editor
2315function saveState(mslText,editor) {
2316 editor = (editor) ? editor : editorElements()[1];
2317 const editorId = editor.id;
2318 if (saveRevision[editorId] === false) {
2320 } else if (saveRevision[editorId] === undefined){
2321 saveRevision[editorId] = true;
2324 if (!previousRevisions[editorId]) {
2325 previousRevisions[editorId] = [];
2326 revisionIndex[editorId] = -1;
2329 // Add one more revision, and trim array if we had some undos
2330 revisionIndex[editorId]++;
2331 if (revisionIndex[editorId] < previousRevisions[editorId].length - 1) {
2332 previousRevisions[editorId].splice(revisionIndex[editorId] + 1);
2334 // Push new revision and keep only nRevisions revisions
2335 previousRevisions[editorId].push(mslText)
2336 if (previousRevisions[editorId].length > nRevisions) {
2337 previousRevisions[editorId].shift();
2339 revisionIndex[editorId] = previousRevisions[editorId].length - 1;
2343function undoEdit(editor) {
2344 editor = (editor) ? editor : editorElements()[1];
2345 const editorId = editor.id;
2346 if (revisionIndex[editorId] === 0) {
2347 // disable menu item
2348 disableMenuItems("undoMenu",true);
2349 seqIsChanged(false);
2353 disableMenuItems("undoMenu",false);
2354 revisionIndex[editorId]--;
2357 disableMenuItems("redoMenu",false);
2359 let caretPos = getCurrentCursorPosition(editor);
2360 let currText = editor.innerText;
2361 saveRevision[editorId] = false;
2362 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
2363 updateLineNumbers(editor.previousElementSibling,editor);
2364 saveRevision[editorId] = true;
2365 // calculate change in caret position based on length
2366 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
2367 setCurrentCursorPosition(editor, caretPos);
2371function redoEdit(editor) {
2372 editor = (editor) ? editor : editorElements()[1];
2373 const editorId = editor.id;
2374 if (revisionIndex[editorId] >= previousRevisions[editorId].length - 1) {
2375 // disable menu item
2376 disableMenuItems("redoMenu",true);
2380 disableMenuItems("redoMenu",false);
2381 revisionIndex[editorId]++;
2384 disableMenuItems("undoMenu",false);
2387 let caretPos = getCurrentCursorPosition(editor);
2388 let currText = editor.innerText;
2389 saveRevision[editorId] = false;
2390 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
2391 updateLineNumbers(editor.previousElementSibling,editor);
2392 saveRevision[editorId] = true;
2393 // calculate change in caret position based on length
2394 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
2395 setCurrentCursorPosition(editor, caretPos);
2398// Select slines from startLine to endLine
2399function selectLines([startLine, endLine],e) {
2400 const selection = window.getSelection();
2401 // Remove existing selections
2402 selection.removeAllRanges();
2403 let startElementId = '#sline' + startLine;
2404 let endElementId = '#sline' + endLine;
2405 let startElement = null, endElement = null;
2406 if (e.querySelector(startElementId)) startElement = e.querySelector(startElementId).firstChild;
2407 if (e.querySelector(endElementId)) endElement = e.querySelector(endElementId).lastChild;
2408 // we need startElement and endElement with first/lastChild
2409 // the following prevents loosing selection but not ideal
2410 while (startElement === null && startLine <= endLine) {
2412 startElementId = '#sline' + startLine;
2413 startElement = e.querySelector(startElementId).firstChild;
2415 while (endElement === null && endLine > 0) {
2417 endElementId = '#sline' + endLine;
2418 endElement = e.querySelector(endElementId).lastChild;
2420 if (startElement && endElement) {
2421 const range = document.createRange();
2422 // Set the start of the range to the startElement at offset 0
2423 range.setStart(startElement, 0);
2424 // Set the end of the range to the endElement at its length
2425 range.setEnd(endElement, endElement.childNodes.length);
2426 // Add the range to the selection
2427 selection.addRange(range);
2431// switch between dark and light modes on request
2432function lightToDark(lToDcheck) {
2433 if (!lToDcheck) return;
2434 const edt_areas = document.querySelectorAll('.edt_area');
2435 if (lToDcheck.checked) {
2436 localStorage.setItem("darkMode", true);
2437 edt_areas.forEach(area => {
2438 area.style.backgroundColor = "black";
2439 area.style.color = "white";
2441 updateCSSRule(".etab button:hover","background-color","black");
2442 updateCSSRule(".etab button:hover","color","white");
2443 updateCSSRule(".etab button:hover","border-bottom","5px solid black");
2444 updateCSSRule(".etab button.edt_active","background-color","black");
2445 updateCSSRule(".etab button.edt_active","color","white");
2446 updateCSSRule(".etab button.edt_active","border-bottom","5px solid black");
2447 updateCSSRule(".etab button.edt_active:hover","background-color","black");
2448 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid black");
2450 localStorage.removeItem("darkMode");
2451 edt_areas.forEach(area => {
2452 area.style.backgroundColor = "white";
2453 area.style.color = "black";
2455 updateCSSRule(".etab button:hover","background-color","white");
2456 updateCSSRule(".etab button:hover","color","black");
2457 updateCSSRule(".etab button:hover","border-bottom","5px solid white");
2458 updateCSSRule(".etab button.edt_active","background-color","white");
2459 updateCSSRule(".etab button.edt_active","color","black");
2460 updateCSSRule(".etab button.edt_active","border-bottom","5px solid white");
2461 updateCSSRule(".etab button.edt_active:hover","background-color","white");
2462 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid white");
2466function updateCSSRule(selector, property, value) {
2467 for (let i = 0; i < document.styleSheets.length; i++) {
2468 let styleSheet = document.styleSheets[i];
2469 let rules = styleSheet.cssRules || styleSheet.rules;
2470 if (!rules) continue;
2471 for (let j = 0; j < rules.length; j++) {
2472 let rule = rules[j];
2473 if (rule.selectorText === selector) {
2474 rule.style[property] = value;
2481// show/hide wait and loop progress
2482function showProgress(e) {
2483 //const progressDiv = document.getElementById("progressDiv");
2484 const progressDiv = document.getElementById("Progress");
2485 if (e === undefined) e = document.getElementById("showProgressBars");
2487 localStorage.setItem("showProgress",true);
2488 progressDiv.style.display = "block";
2489 //dlgShow(progressDiv);
2491 localStorage.removeItem("showProgress");
2492 progressDiv.style.display = "none";
2493 //dlgHide(progressDiv);
2497// Mark the current line number
2498function markCurrLineNum() {
2499 const [lineNumbers,editor] = editorElements();
2500 const currLines = lineNumbers.querySelectorAll(".edt_linenum_curr");
2501 const [startLine,endLine] = getLinesInSelection(editor);
2502 if (startLine === 0 && endLine === 0) return;
2503 currLines.forEach((line) => line.classList.remove("edt_linenum_curr"));
2504 for (let i = startLine; i <= endLine; i++) {
2505 let lineNumId = "#lNum" + i.toString();
2506 let lineNum = lineNumbers.querySelector(lineNumId);
2508 lineNum.className = "edt_linenum_curr";
2513// Check if program is progName is running.
2514// If not, try to get it going
2515function checkProgram(progName = "Sequencer") {
2516 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc1) {
2517 mjsonrpc_db_get_values([`/Programs/${progName}/Start command`]).then(function(rpc2) {
2518 let isRunning = (rpc1.result.status === 1);
2519 let isDefined = ((rpc2.result.data[0] !== null) && (rpc2.result.data[0] !== ""));
2520 if (isRunning && isDefined) return;
2521 // progName is not running or not defined, stop it just in case and check the reason
2522 mjsonrpc_stop_program(progName);
2525 message = `${progName} program is not running.<br>Should I start it?`
2527 message = `${progName} program is not configured and not running.<br>Should I try to start it anyway?`
2529 dlgConfirm(message,function(resp) {
2530 // Guess the name of the actual binary
2531 let binName = (progName === "Sequencer") ? "msequencer" : progName.toLowerCase();
2534 // assume that progName is in path and create a start command, sleep 2s,
2535 // set value to "progName -D", sleep 2s, start program
2536 mjsonrpc_db_create([{"path" : `/Programs/${progName}/Start command`, "type" : TID_STRING}]).then(function (rpc3) {
2537 setTimeout(function(){
2538 mjsonrpc_db_paste([`/Programs/${progName}/Start command`],[`${binName} -D`]).then(function (rpc4) {
2539 if (rpc4.result.status[0] === 1) {
2540 mjsonrpc_start_program(progName);
2542 }).catch(function (error) {
2543 console.error(error);
2546 }).catch(function (error) {
2547 console.error(error);
2550 mjsonrpc_start_program(progName);
2552 // take 3 seconds and check that it actually started
2553 setTimeout(function(){
2554 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc5) {
2555 if (rpc5.result.status === 1) {
2556 dlgAlert(`${progName} started successfully.`);
2558 dlgAlert(`Failed to start ${progName}!<br>Try to start it manually (${binName} -D)`);
2564 }).catch (function (error) {
2565 console.error(error);
2567 }).catch(function (error) {
2568 console.error(error);
2572// Python programs seem to be different from standard midas - to be checked
2573function checkPyProgram(progName = "PySequencer") {
2574 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc1) {
2575 mjsonrpc_db_get_values([`/Programs/${progName}/Start command`]).then(function(rpc2) {
2576 let isRunning = (rpc1.result.status === 1);
2577 let isDefined = ((rpc2.result.data[0] !== null) && (rpc2.result.data[0] !== ""));
2578 if (isRunning && isDefined) return;
2579 // progName is not running or not defined, stop it just in case and check the reason
2580 mjsonrpc_stop_program(progName);
2583 message = `${progName} program is not running.<br>Should I start it?`
2585 message = `${progName} program is not configured and not running.<br>Should I try to start it anyway?`
2587 dlgConfirm(message,function(resp) {
2588 // Guess the name of the actual binary
2589 let binName = (progName === "PySequencer") ? "python3 $MIDASSYS/python/midas/sequencer.py" : "python3 $MIDASSYS/python/midas/" + progName.toLowerCase() + ".py";
2592 // assume that progName is in path and create a start command, sleep 2s,
2593 // set value to "progName -D", sleep 2s, start program
2594 mjsonrpc_db_create([{"path" : `/Programs/${progName}/Start command`, "type" : TID_STRING}]).then(function (rpc3) {
2595 setTimeout(function(){
2596 mjsonrpc_db_paste([`/Programs/${progName}/Start command`],[`${binName} -D`]).then(function (rpc4) {
2597 if (rpc4.result.status[0] === 1) {
2598 mjsonrpc_start_program(progName);
2600 }).catch(function (error) {
2601 console.error(error);
2604 }).catch(function (error) {
2605 console.error(error);
2608 mjsonrpc_start_program(progName);
2610 // take 3 seconds and check that it actually started
2611 setTimeout(function(){
2612 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc5) {
2613 if (rpc5.result.status === 1) {
2614 dlgAlert(`${progName} started successfully.`);
2616 dlgAlert(`Failed to start ${progName}!<br>Try to start it manually (${binName} -D)`);
2622 }).catch (function (error) {
2623 console.error(error);
2625 }).catch(function (error) {
2626 console.error(error);
2630function captureSelected() {
2631 if (window.getSelection) {
2632 let selection = window.getSelection();
2633 let text = selection.toString();
2634 let range = selection.getRangeAt(0);
2635 if (text && range) {
2637 startPath: range.startContainer,
2638 startOffset: range.startOffset,
2639 endPath: range.endContainer,
2640 endOffset: range.endOffset
2642 sessionStorage.setItem("tempSelText", text);
2643 sessionStorage.setItem("tempSelRange", JSON.stringify(rangeData));
2648function editMenu(action) {
2649 let text = sessionStorage.getItem("tempSelText") ?? "";
2650 let storedRange = sessionStorage.getItem("tempSelRange") ?? "";
2653 let rangeData = JSON.parse(storedRange);
2654 let startContainer = nodeFromPath(rangeData.startPath);
2655 let endContainer = nodeFromPath(rangeData.endPath);
2657 // Create a new range
2658 let newRange = new Range();
2659 newRange.setStart(startContainer, rangeData.startOffset);
2660 newRange.setEnd(endContainer, rangeData.endOffset);
2662 // Select the new range
2663 let selection = window.getSelection();
2664 selection.removeAllRanges();
2665 selection.addRange(newRange);
2668 if (action === "Copy") {
2670 sessionStorage.setItem("copiedText",text);
2672 } else if (action === "Paste") {
2673 const copiedText = sessionStorage.getItem("copiedText");
2675 newRange.deleteContents();
2676 newRange.insertNode(document.createTextNode(copiedText));
2678 } else if (action === "Cut") {
2680 sessionStorage.setItem("copyText",text);
2681 //document.execCommand("cut");
2682 newRange.deleteContents();
2684 } else if (action === "Undo") {
2686 } else if (action === "Redo") {
2691// Switch to the clicked tab
2692function openETab(btn) {
2693 const tabcontent = document.querySelectorAll(".etabcontent");
2694 const tablinks = document.querySelectorAll(".etablinks");
2695 const tab = btn ? btn : document.querySelectorAll(".edt_active")[0];
2696 const tabID = tab.id.replace("-btn","")
2697 tabcontent.forEach(content => {
2698 content.style.display = "none";
2700 tablinks.forEach(link => {
2701 link.classList.remove("edt_active");
2703 tab.className += " edt_active";
2704 //document.getElementById(tabID).style.display = "inline-flex";
2705 document.getElementById(tabID).style.display = "flex";
2706 // For the main sequence tab disable Save and Save as...
2707 if (tabID === "etab1") {
2708 disableMenuItems("noteditor",true);
2710 disableMenuItems("noteditor",false);
2712 // Change validation icon to defaule
2713 let vldBtn = document.getElementById("vldBtn");
2714 vldBtn.src = "icons/validate-syntax.svg";
2715 document.querySelectorAll('.seqbtn').forEach(btn => {
2716 btn.style.backgroundColor = "";
2719 // Adjust height of active editor
2723// Close the clicked tab
2724function closeTab(tab,event) {
2725 const tablinks = document.querySelectorAll(".etablinks");
2726 if (tablinks.length < 3) return;
2727 const tabCount = parseInt(tab.parentElement.id.replace("-btn","").replace("etab",""));
2728 const tabBtn = document.getElementById(`etab${tabCount}-btn`);
2729 const tabContent = document.getElementById(`etab${tabCount}`);
2730 if (seqIsChanged()) {
2731 dlgConfirm("File was changed, close anyway?", function(resp) {
2734 tabContent.remove();
2735 // switch to previous tab
2736 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2741 tabContent.remove();
2742 // switch to previous tab
2743 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2745 // need this since the close button is inside the tab button
2746 event.stopPropagation();
2749// Create and add a new editor tab. If tabNum is provided insert tab at corresponding order
2750function addETab(btn,tabNum) {
2751 // Create tab button
2752 const tabBtn = document.createElement("button");
2753 tabBtn.className = "etablinks";
2754 tabNum = tabNum || (btn.previousElementSibling ? parseInt(btn.previousElementSibling.id.replace("-btn","").replace("etab","")) + 1 : 1);
2755 // Make room for inserted tab if needed
2756 shiftETabsUpFrom(tabNum);
2757 tabBtn.id = `etab${tabNum}-btn`;
2759 tabBtn.innerHTML = `<span id="etab${tabNum}-lbl">Untitled</span><span id="filechanged${tabNum}"></span><span onclick="closeTab(this,event);" class="closebtn">×</span>`;
2760 const refBtn = document.getElementById(`etab${tabNum + 1}-btn`) || btn;
2761 btn.parentNode.insertBefore(tabBtn, refBtn);
2762 tabBtn.onclick = function () { openETab(this);};
2764 // Create editor area
2765 const tabContent = document.createElement("div");
2766 tabContent.id = `etab${tabNum}`;
2767 tabContent.className = "etabcontent";
2770 if (localStorage.getItem("darkMode")) makeDark = "style='background-color: black; color: white;'";
2772 tabContent.innerHTML =
2773 `<pre id="lineNumbers${tabNum}" class="edt_linenum"></pre><pre id="editorTab${tabNum}" ${makeDark} class="edt_area" spellcheck="false" contenteditable="false"></pre>`;
2775 const refContent = document.getElementById(`etab${tabNum + 1}`) || document.getElementById("lastETab");
2776 refContent.parentNode.insertBefore(tabContent, refContent);
2779 // Add event listeners
2780 editorEventListeners();
2784// Helper function to shift and renumber tabs
2785function shiftETabsUpFrom(startNum) {
2786 // Count existing tabs by content divs
2787 const contents = document.querySelectorAll('.etabcontent');
2788 const maxTab = contents.length;
2790 // Rename backwards to avoid collisions
2791 for (let i = maxTab; i >= startNum; i--) {
2792 renameETab(i, i + 1);
2795// Helper function to reorder tabs (change associated ids).
2796function renameETab(oldNum, newNum) {
2798 [`etab${oldNum}`, `etab${newNum}`],
2799 [`etab${oldNum}-btn`, `etab${newNum}-btn`],
2800 [`editorTab${oldNum}`, `editorTab${newNum}`],
2801 [`lineNumbers${oldNum}`, `lineNumbers${newNum}`],
2802 [`etab${oldNum}-lbl`, `etab${newNum}-lbl`],
2803 [`filechanged${oldNum}`, `filechanged${newNum}`]
2806 mappings.forEach(([oldId, newId]) => {
2807 const el = document.getElementById(oldId);
2808 if (el) el.id = newId;
2813// Return the pre of lineNumbers, editor, tab label and tab button element of the active tab
2814function editorElements() {
2815 const btn = (document.querySelectorAll(".edt_active")[0]) ? document.querySelectorAll(".edt_active")[0] : document.getElementById("etab1-btn");
2816 const tab = document.getElementById(btn.id.replace("-btn",""));
2817 const [lineNumbers,editor] = tab.children;
2818 //const btnLabel = (btn.id !== "etab1-btn") ? btn.children[0] : btn.children[1];
2819 const btnLabel = btn.children[0];
2820 return [lineNumbers,editor,btnLabel,btn];
2823// disable and enable clicking on menu item
2824function disableMenuItems(className,flag) {
2826 className - the class name of the item
2827 flag - true/false to enable/disable item
2829 const els = document.querySelectorAll(`.${className}`);
2832 e.style.opacity = 0.5;
2833 e.style.pointerEvents = "none";
2835 e.style.opacity = "";
2836 e.style.pointerEvents = "";
2841// Function to replace some special characters in the text
2842function escapeSpecialCharacters(text) {
2843 return text.replace(/&/g, "&")
2844 .replace(/</g, "<")
2845 .replace(/>/g, ">")
2846 .replace(/\r\n|\n\r/g, '\n')
2847 //.replace(/"/g, """)
2848 //.replace(/'/g, "'")
2849 .replace(/\t/g, " ")
2850 // and illegal ascii characters
2851 .replace(/[^\x00-\x7F]/g, " ");
2854// Produce a help window
2857<span style='text-align: left;'>
2858<b>Hidden features of the sequencer editor</b>
2859<ul style='white-space: pre;font-family: monospace;'>
2860<li>Double click on the edit area of the first (main) tab to edit the currently loaded sequence.</li>
2861<li>Tab - Indent selected lines.</li>
2862<li>Shift+Tab - Unindent selected lines.</li>
2863<li>Escape - Autoindent selected lines according to syntax rules.</li>
2864<li>Ctrl+C - Copy selected text.</li>
2865<li>Ctrl+V - Paste selected text.</li>
2866<li>Ctrl+A - Select all text.</li>
2867<li>Ctrl+Z - Undo last change.</li>
2868<li>Ctrl+R - Redo last undo.</li>
2872 const d = dlgMessage("Editor help",help, false, false);
2873 const btn = d.querySelectorAll('.dlgButton')[0];
2874 btn.className = "dlgButtonDefault";
2879// Activate drag and drop events on next files table
2880function activateDragDrop(table) {
2882 table - The table element containing the list of next files.
2884 // collect all rows with class dragrow
2885 const rows = table.querySelectorAll('.dragrow');
2886 let dragStartIndex,dragEndIndex;
2888 // add event listeners
2889 rows.forEach(row => {
2890 row.addEventListener('dragstart', dragStart);
2891 row.addEventListener('dragover', dragOver);
2892 row.addEventListener('dragend', dragEnd);
2895 function dragStart(e) {
2896 dragStartIndex = Array.from(rows).indexOf(this);
2897 rows.forEach(row => row.classList.remove('dragstart'));
2898 this.classList.add('dragstart');
2900 function dragOver(e) {
2902 dragEndIndex = Array.from(rows).indexOf(this);
2903 // Create or update the empty row element
2905 emptyRow = document.createElement('tr');
2906 emptyRow.innerHTML = "<td colspan=4> </td>";
2907 emptyRow.classList.add('empty-dragrow');
2909 // Insert the empty row element at the appropriate position
2910 if (dragEndIndex > dragStartIndex) {
2911 this.parentNode.insertBefore(emptyRow, this.nextSibling);
2912 } else if (dragEndIndex < dragStartIndex) {
2913 this.parentNode.insertBefore(emptyRow, this);
2916 function dragEnd(e) {
2921 reorderNextFilenames(dragStartIndex,dragEndIndex);
2922 rows.forEach(row => {
2923 row.classList.remove('dragstart');
2928// Move next filename from dragStarIndex to dragEndIndex position
2929function reorderNextFilenames(dragStarIndex,dragEndIndex) {
2931 dragStarIndex - move file from this indext.
2932 dragEndIndex - destination index of the file.
2934 if (dragStarIndex === dragEndIndex) return;
2935 let odbpath = mslDefs.filename.next;
2936 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2937 if (rpc.result.status[0] !== 1) return;
2938 let fList = rpc.result.data[0];
2940 // Remove the draggedFile from the starting index
2941 const draggedFile = fList.splice(dragStarIndex, 1)[0];
2942 // Insert the draggedFile at the new index
2943 fList.splice(dragEndIndex,0,draggedFile);
2944 // Check that the list does not contain more than 10 files
2945 if (fList.length > 10) {
2946 dlgAlert("There are more than 10 files. Aborting!.");
2949 // Move full slots to the begenning of the array
2950 fList = fList.filter(Boolean);
2951 fList = [...fList, ...new Array(10 - fList.length).fill("")];
2952 // Update values in ODB
2953 mjsonrpc_db_paste([odbpath],[fList]).then(function (rpc2) {
2954 if (rpc2.result.status[0] !== 1) {
2955 dlgAlert("Failed to move the file!<br>Please check.");
2957 }).catch(function (error) {
2958 console.error(error);
2960 }).catch (function (error) {
2961 console.error(error);
2965// Change the next file name in the clicked row
2966function chngNextFilename(e,filename) {
2968 e - (optional) cell element on the same row of "next file" to be changed.
2969 If last row (or undefined) add a file to the end of the queue.
2970 filename - (optional) The file name to add to the end of the queue.
2972 e = (e) ? e : document.getElementById("addAsNext");
2973 //if (e === undefined) e = document.getElementById("addAsNext");
2974 // file index from table row index
2975 let index = e ? e.parentElement.rowIndex - 1 : 0;
2976 // Only 10 files are allowed
2978 dlgAlert("Maximum number (10) of next files reached!");
2982 let odbpath = mslDefs.filename.next;
2983 mjsonrpc_db_get_values([`${SeqODB}/State/Path`]).then(function(rpc) {
2984 let path = mslDefs.filename.path + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
2985 sessionStorage.setItem("pathName",path);
2987 odbpath = odbpath + "[" + index.toString() + "]";
2988 modbset(odbpath,filename);
2990 file_picker(path,mslDefs.filename.ext,function(filename) {
2991 filename = filename.replace(/^sequencer\//,'').replace(/^\//,'');
2992 odbpath = odbpath + "[" + index.toString() + "]";
2994 modbset(odbpath,filename);
2999 }).catch(function (error) {
3000 mjsonrpc_error_alert(error);
3004// Reomve the next file name from the queue
3005function remNextFilename(e) {
3007 e - cell element on the same row of "next file" to be removed
3009 // file index from table row index
3010 let index = e.parentElement.rowIndex - 1;
3011 let odbpath = mslDefs.filename.next;
3012 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
3013 if (rpc.result.status[0] !== 1) return;
3014 let fList = rpc.result.data[0];
3017 // Move full slots to the begenning of the array
3018 fList = fList.filter(Boolean);
3019 fList = [...fList, ...new Array(10 - fList.length).fill("")];
3020 mjsonrpc_db_paste([odbpath],[fList]).then(function (rpc2) {
3021 if (rpc2.result.status[0] !== 1) {
3022 dlgAlert("Failed to remove file from list!<br>Please check.");
3024 }).catch(function (error) {
3025 console.error(error);
3027 }).catch (function (error) {
3028 console.error(error);
3033// Wait for ODB in path to have value
3034// If value is not reached, give up after 10s
3035function checkODBValue(path,value,funcCall,args) {
3037 path - ODB path to monitor for value
3038 value - the value to be reached and return success
3039 funcCall - function name to call when value is reached
3040 args - argument to pass to funcCall
3042 // Call the mjsonrpc_db_get_values function
3043 mjsonrpc_db_get_values([path]).then(function(rpc) {
3044 if (rpc.result.status[0] === 1 && rpc.result.data[0] !== value) {
3046 if (NcheckValue < 100) {
3047 // Wait 0.1 second and then call checkODBValue again
3048 // Time out after 10 s
3050 checkODBValue(path,value,funcCall,args);
3054 if (funcCall) funcCall(args);
3058 }).catch(function(error) {
3059 console.error(error);
3063function isTextSelected(element) {
3064 const selection = window.getSelection();
3065 if (!selection.rangeCount) return false;
3067 const range = selection.getRangeAt(0);
3068 const selectedText = selection.toString();
3070 // Check if the selection is within the specified element
3071 if (!element.contains(range.commonAncestorContainer)) return false;
3073 // Check if there is any selected text
3074 return selectedText.length > 0;
3077function deleteSelectedText(element) {
3078 const selection = window.getSelection();
3079 if (!selection.rangeCount) return; // No selection exists
3080 const range = selection.getRangeAt(0);
3081 // Ensure the selection is within the target element
3082 if (!element.contains(range.commonAncestorContainer)) return;
3083 // Delete the selected text
3084 range.deleteContents();
3087// Make sure that the caret stays visible
3088function keepCaretVisible(editor) {
3089 const selection = window.getSelection();
3090 const caretPosition = selection.getRangeAt(0).getBoundingClientRect();
3091 const containerRect = editor.getBoundingClientRect();
3092 const caretTop = caretPosition.top - containerRect.top;
3093 const caretBottom = caretPosition.bottom - containerRect.top;
3094 const containerHeight = containerRect.height;
3095 // Caclulate font size
3096 const emInPixels = parseFloat(window.getComputedStyle(editor).fontSize) * 1.5;
3098 // If the caret is near the bottom, scroll the container down with 1.5 fontSize
3099 if (caretBottom > containerHeight) {
3100 editor.scrollTop += caretBottom - containerHeight + emInPixels;
3103 // If the caret is near the top, scroll the container up
3105 editor.scrollTop -= Math.abs(caretTop);
3110// commands object for syntax validation
3111const mslCommands = {
3113 patterns: [/^INCLUDE\s+.+(\.msl)?$/i],
3114 description: "Include another MSL file.",
3118 patterns: [/^BREAK$/i],
3119 description: "Break (finish prematurely) a loop.",
3123 patterns: [/^CALL\s+\w+(\s*,\s*.+)*$/i],
3124 description: "Call a subroutine with optional parameters.",
3128 patterns: [/^CAT\s+\w+(\s*,\s*.+)*$/i],
3129 description: "Concatenate strings into a single variable.",
3133 patterns: [/^(?:COMMENT\s+.*|#.*)$/i],
3134 description: "A comment line starting with COMMENT or #.",
3138 patterns: [/^EXIT$/i],
3139 description: "Exit the script immediately.",
3143 patterns: [/^GOTO\s+\d+$/i],
3144 description: "Jump to a specific line in the script.",
3148 patterns: [/^IF\s*\(.+\)$/i, /^IF\s+.+$/i],
3149 description: "Conditional execution of code blocks.",
3150 blockStart: true, // Marks this as a block start command
3154 patterns: [/^ELSE$/i],
3155 description: "Alternative block for an IF statement.",
3159 patterns: [/^ENDIF$/i],
3160 description: "Ends an IF block.",
3161 blockEnd: "IF", // Marks this as a block end command for IF
3165 patterns: [/^LIBRARY\s+.+$/i],
3166 description: "Declare the file as a library.",
3171 /^LOOP\s+(\d+|INFINITE|\$\w+)$/i, // LOOP 5, LOOP INFINITE, or LOOP $varName
3172 /^LOOP\s+\w+\s*,\s*.+$/i, // LOOP i, 1:10
3173 // /^LOOP\s+\w+\s*,\s*(?:\d+:\d+|\S+)$/i, // LOOP i, 1:10
3175 description: "Execute a loop a fixed number of times or with values.",
3176 blockStart: true, // Marks this as a block start command
3178 //argcond1: /^(\d+|INFINITE|\$\w+)$/, // First argument can be a number, INFINITE, or a variable
3181 patterns: [/^ENDLOOP$/i],
3182 description: "Ends a LOOP block.",
3183 blockEnd: "LOOP", // Marks this as a block end command for LOOP
3187 patterns: [/^MESSAGE\s+.+(\s*,\s*1)?$/i],
3188 description: "Display a message in the browser.",
3192 patterns: [/^MSG\s+.+(\s*,\s*(ERROR|INFO|DEBUG|LOG|TALK))?$/i],
3193 description: "Send a message to the MIDAS buffer.",
3197 patterns: [/^ODBCREATE\s+.+,\s+\w+(\s*,\s*\d+)?$/i],
3198 description: "Create an ODB key.",
3202 patterns: [/^ODBDELETE\s+.+$/i],
3203 description: "Delete an ODB key or subdirectory.",
3207 patterns: [/^ODBGET\s+(?:"[^"]+"|\/[^,]+|\$\w+(?:\[[^\]]+\])?),\s*[A-Za-z_]\w*(?:\[[^\]]+\])?$/i],
3208 description: "Retrieve a value from the ODB.",
3210 argcond: [/^(?:\/.+|\$\w+(?:\[[^\]]+\])?)$/, // First argument must be an ODB path or a variable containing one
3211 /^[A-Za-z_]\w*(?:\[[^\]]+\])?$/], // Second argument is the destination variable name
3214 patterns: [/^ODBINC\s+.+(\s*,\s*[+-]?\d+)?$/i],
3215 description: "Increment an ODB value.",
3219 patterns: [/^ODBLOOKUP\s+".+",\s+".+",\s+\w+$/i],
3220 description: "Lookup a string in the ODB array.",
3224 //patterns: [/^ODBSET\s+("[^"]*"|\/[^,]+),\s*("[^"]*"|[^,]+)(\s*,\s*[01])?$/i,],
3225 patterns: [/^ODBSET\s+.+,\s+.+(\s*,\s*[01])?$/i],
3226 description: "Set a value in the ODB.",
3229 /^.+$/, // First argument must start with / or be a quoted string
3230 /^.+$/, // Second argument can be a quoted string or unquoted value
3231 /^[01]$/, // Third argument (optional) must be 0 or 1
3235 patterns: [/^ODBLOAD\s+.+(\s*,\s*.+)?$/i],
3236 description: "Load an external file into the ODB.",
3240 patterns: [/^ODBSAVE\s+.+,\s+.+$/i],
3241 description: "Save part of the ODB to a file.",
3245 patterns: [/^ODBSUBDIR\s+("\/[^"]*"|\/[^,]+)$/i],
3246 description: "Set a base path for ODB commands.",
3247 blockStart: true, // Marks this as a block start command
3251 patterns: [/^ENDODBSUBDIR$/i],
3252 description: "Ends an ODBSUBDIR block.",
3253 blockEnd: "ODBSUBDIR", // Marks this as a block end command for ODBSUBDIR
3257 patterns: [/^PARAM\s+\w+(\s*,\s*.+)*$/i],
3258 description: "Define script parameters.",
3262 patterns: [/^RUNDESCRIPTION\s+.+$/i],
3263 description: "Set a run description.",
3267 patterns: [/^SCRIPT\s+.+(\s*,\s*.+)*$/i],
3268 description: "Call a server-side script with optional parameters.",
3272 patterns: [/^SET\s+\w+\s*,\s*.+$/i],
3273 description: "Set a variable to a value.",
3277 patterns: [/^SUBROUTINE\s+\w+$/i],
3278 description: "Start a subroutine block.",
3279 blockStart: true, // Marks this as a block start command
3283 patterns: [/^ENDSUBROUTINE$/i],
3284 description: "Ends a subroutine block.",
3285 blockEnd: "SUBROUTINE", // Marks this as a block end command for SUBROUTINE
3289 patterns: [/^TRANSITION\s+(start|stop|pause|resume)$/i],
3290 description: "Trigger a run state transition.",
3295 /^WAIT\s+seconds,\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)|\$\w+)$/i, // WAIT seconds, 10
3296 /^WAIT\s+events,\s*(\d+|\$\w+)$/i, // WAIT events,10
3298 // WAIT ODBValue, /path, op, value
3299 // WAIT ODBValue, /path, op, value, timeout, seconds
3300 // WAIT ODBValue, /path, op, value, timeout, seconds, status
3302 // The timeout is optional and may be a number or a sequencer variable.
3303 // If present, the optional status argument is the variable name that receives
3304 // the timeout status from the sequencer runtime.
3305 // /^WAIT\s+ODBvalue,\s*(?:"?\/?[^,"]+"?|\$\w+)\s*,\s*(?:>=|<=|==|!=|>|<)\s*,\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)|\$\w+)(?:\s*,\s*timeout\s*,\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)|\$\w+))?$/i,
3306 /^WAIT\s+ODBvalue,\s*(?:"?\/?[^,"]+"?|\$\w+(?:\[[^\]]+\])?)\s*,\s*(?:>=|<=|==|!=|>|<)\s*,\s*([+-]?(?:(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)|\$\w+(?:\[[^\]]+\])?)(?:\s*,\s*timeout\s*,\s*([+-]?(?:(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)|\$\w+(?:\[[^\]]+\])?)(?:\s*,\s*([A-Za-z_]\w*(?:\[[^\]]+\])?|\$\w+(?:\[[^\]]+\])?))?)?$/i,
3308 description: "Wait for a condition, time, or event.",
3312 patterns: [/^[a-zA-Z_]\w*\s*=\s*([0-9]+(\.\d+)?|\$[a-zA-Z_]\w*|[\d\w\$\s\+\-\*\/\^\(\)]+|[\w\$]+\(.*\)|".*"|'.*')$/i],
3313 description: "Variable assignment or equation.",
3318const pythonCommands = {
3319 // Comment lines starting with "#" are ignored.
3322 description: "Comment line. Lines starting with '#' are ignored.",
3325 // Multi line comments between triple brackets are ignored
3327 patterns: [/("""[\s\S]*?""")|('''[\s\S]*?''')/g],
3328 description: "Multi line comments between tripple quotes.",
3331 // Function definition
3333 patterns: [/^\s*def\s+[A-Za-z_]\w*\s*\([^)]*\)\s*:$/],
3334 description: "Function definition. Syntax: def name(params):",
3339 patterns: [/^\s*class\s+[A-Za-z_]\w*(?:\s*\([^)]*\))?\s*:$/],
3340 description: "Class definition. Syntax: class Name(Base1, Base2):",
3343 // Import statements
3346 /^\s*import\s+[A-Za-z_]\w*(?:\.\w+)*(?:\s*,\s*[A-Za-z_]\w*(?:\.\w+)*)*\s*$/,
3347 /^\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*$/
3349 description: "Import statements. Syntax: import module[, module2]\n or: from pkg.mod import name[, name2]",
3352 // Conditional blocks
3354 patterns: [/^\s*if\s+.+\s*:$/],
3355 description: "If statement. Syntax: if condition:",
3360 patterns: [/^\s*elif\s+.+\s*:$/],
3361 description: "Elif statement. Syntax: elif condition:",
3365 patterns: [/^\s*else\s*:$/],
3366 description: "Else statement. Syntax: else:",
3371 patterns: [/^\s*for\s+[A-Za-z_]\w*\s+in\s+.+\s*:$/],
3372 description: "For loop. Syntax: for var in iterable:",
3377 patterns: [/^\s*while\s+.+\s*:$/],
3378 description: "While loop. Syntax: while condition:",
3382 // Try/Except/Finally
3384 patterns: [/^\s*try\s*:$/],
3385 description: "Try block. Syntax: try:",
3387 pythonBlockStart: "TRY",
3390 patterns: [/^\s*except(?:\s+.+?)?(?:\s+as\s+[A-Za-z_]\w*)?\s*:$/],
3391 description: "Except block. Syntax: except ExceptionType as name:",
3393 pythonBlockContinuation: "TRY",
3396 patterns: [/^\s*finally\s*:$/],
3397 description: "Finally block. Syntax: finally:",
3399 pythonBlockContinuation: "TRY",
3401 // With context manager
3403 patterns: [/^\s*with\s+.+\s+as\s+[A-Za-z_]\w*\s*:$/],
3404 description: "With statement. Syntax: with expr as var:",
3408 // Simple statements
3411 /^\s*return(?:\s+.+)?$/
3413 description: "Return statement. Syntax: return [expr]",
3417 patterns: [/^\s*pass\s*$/],
3418 description: "Pass statement. Does nothing.",
3422 patterns: [/^\s*break\s*$/],
3423 description: "Break out of the nearest loop.",
3427 patterns: [/^\s*continue\s*$/],
3428 description: "Continue to next iteration of the nearest loop.",
3432 patterns: [/^\s*raise(?:\s+[A-Za-z_]\w*(?:\s*\(.*\))?)?(?:\s+from\s+[A-Za-z_]\w*(?:\s*\(.*\))?)?\s*$/],
3433 description: "Raise an exception. Syntax: raise ExceptionType(args) [from other]",
3437 patterns: [/^\s*assert\s+.+$/],
3438 description: "Assert statement. Syntax: assert condition[, message]",
3443 patterns: [/^\s*@\w+(?:\.\w+)*(?:\s*\(.*\))?$/],
3444 description: "Decorator. Syntax: @decorator or @decorator(args)",
3447 // Assignment (simple)
3449 patterns: [/^\s*[A-Za-z_]\w*\s*=.*$/],
3450 description: "Variable assignment. Syntax: name = expression",
3453 // Bare function or method call, possibly with args
3455 patterns: [/^\s*[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\s*\(.*\)\s*$/],
3456 description: "Expression statement (e.g. function or method call).",
3461// Validation function to validate script content
3462function checkEditorContent(e, commands) {
3463 // If not provided take appropriate default values
3464 if (commands === undefined) {
3465 commands = (SeqODB === "/PySequencer") ? pythonCommands : mslCommands;
3467 e = e ? e : event.target;
3468 // Return immediately with true if auto validation is disabled
3469 if ((e.id !== "vldBtn") && (document.getElementById("autoValidate").checked === false)) return true;
3471 // Get the content from the current editor
3472 let [lineNumbers,editor] = editorElements();
3473 const editorContent = editor.innerText;
3475 // Validate the script content
3476 const errors = validateScript(editorContent,commands);
3477 if (errors.length > 0) {
3478 // Format and display errors
3479 let vldErrors = errors.map(err => {
3480 if (typeof err === "string") {
3481 return err; // Handle string errors directly
3482 } else if (err.line && err.message) {
3483 return `Line ${err.line}: ${err.message}`; // Format object errors
3485 return "Unknown error"; // Fallback for unexpected error formats
3488 // highlight error lines and scroll to first one
3489 hlLine(errors.map(error => error.line),"var(--mred)",editor,errors.map(error => error.message));
3490 setCurrentCursorPosition(editor, errors[0].line - 1 + editorContent.split("\n").slice(0,errors[0].line).reduce((sum, line) => sum + line.length, 0));
3493 let errorHTML = "<div style='text-align: left;'>" + vldErrors.replaceAll("\n","<br>") + "</div>";
3494 // Show the error message for 5 seconds
3495 //let d = dlgWait(5,errorHTML);
3496 // Show the error message
3497 let d = dlgAlert(errorHTML);
3498 d.querySelector('.dlgTitlebar').innerText = "Validation Errors";
3499 console.error("Validation errors:\n",vldErrors);
3500 if (e.id === "vldBtn") e.src = "icons/validate-syntax-red.svg";
3501 if (e.classList.contains("seqbtn")) e.style.backgroundColor = "var(--mred)";
3504 //dlgAlert("The syntax of the sequence is valid.");
3505 if (e.id === "vldBtn") {
3506 e.src = "icons/validate-syntax-green.svg";
3507 e.style.backgroundColor = "var(--mgreen)";
3509 document.querySelectorAll('.seqbtn').forEach(btn => {
3510 btn.style.backgroundColor = "";
3517// Function to validate the script against commands object
3518function validateScript(script, commands) {
3520 const originalScript = script;
3522 // Stack to track nested blocks
3523 const blockStack = [];
3524 const lineNums = [];
3525 // Stack to track delimiters
3526 const delimiterStack = [];
3528 // Delimiters and their matching pairs
3529 const delimiters = {
3537 // Remove multiline comments when the active language defines them.
3538 const multiCommRegex = commands.MULTICOMM?.patterns?.[0];
3539 if (multiCommRegex) {
3541 // replace each triple-quote block with the same count of blank lines
3542 script = script.replace(
3543 new RegExp(multiCommRegex.source, "g"),
3545 // count the lines in the matched block
3546 const lines = match.split("\n");
3547 // return that many lines, each a single space
3548 return lines.map(() => " ").join("\n");
3553 // Regex to match and replace all comments by space
3554 const commentPatterns = commands.COMMENT?.patterns || [];
3555 if (commentPatterns.length > 0) {
3556 const commentRegex = new RegExp(commentPatterns.map(pattern => pattern.source).join("|"), "gim");
3557 script = script.replace(commentRegex, "");
3559 const lines = script.split("\n");
3561 lines.forEach((line, index) => {
3562 const trimmedLine = line.trim();
3563 if (!trimmedLine) return; // Skip empty lines
3565 // Check if the line matches any command pattern.
3566 const cmdKey = Object.keys(commands).find(cmd => {
3567 const cmdPattern = commands[cmd].patterns || null;
3568 return Array.isArray(cmdPattern) && cmdPattern.some(pattern => regexTest(pattern, trimmedLine));
3572 const cmdData = commands[cmdKey];
3573 // Handle block start commands
3574 if (commands !== pythonCommands && cmdData.blockStart) {
3575 blockStack.push(cmdKey); // Push the block type to the stack
3576 lineNums.push(index + 1);
3579 // Handle block end commands
3580 if (commands !== pythonCommands && cmdData.blockEnd) {
3581 const expectedBlock = cmdData.blockEnd;
3582 const lastBlock = blockStack.pop();
3585 if (lastBlock !== expectedBlock) {
3586 errors.push({ line: index + 1, message: `"${cmdKey}" without matching "${expectedBlock}".` });
3590 // Validate arguments if argcond is defined
3591 if (cmdData.argcond) {
3592 // Remove the command keyword from the line and split remaining arguments by comma
3593 const args = splitArguments(trimmedLine.replace(new RegExp(`^${cmdKey}`, "i"), "").trim());
3594 for (let i = 0; i < args.length; i++) {
3595 const arg = args[i].trim().replace(/^"(.*)"$/, "$1"); // Remove surrounding quotes
3596 const argCond = cmdData.argcond[i]; // Get the condition for this argument
3597 if (argCond && !regexTest(argCond, arg)) {
3598 errors.push({ line: index + 1, message: `Argument ${i + 1} of "${cmdKey}" is invalid.` });
3606 const likelyCmd = findLikelyCommand(trimmedLine, commands);
3608 const description = commands[likelyCmd].description || "Check the command syntax.";
3609 errors.push({ line: index + 1, message: `Invalid syntax for "${likelyCmd}". ${description}` });
3611 const unknownCmd = extractCommandName(trimmedLine);
3612 const suggestedCmd = suggestCommandName(unknownCmd, commands);
3613 const suggestion = suggestedCmd ? ` Did you mean "${suggestedCmd}"?` : "";
3614 errors.push({ line: index + 1, message: `Unknown command "${unknownCmd}".${suggestion}` });
3617 // Delimiter validation
3618 for (let i = 0; i < trimmedLine.length; i++) {
3619 const char = trimmedLine[i];
3620 if (delimiters[char]) {
3621 // Opening delimiter
3622 delimiterStack.push({ char, line: index + 1, pos: i + 1 });
3623 } else if (Object.values(delimiters).includes(char)) {
3624 // Closing delimiter
3625 if (delimiterStack.length === 0) {
3626 errors.push({ line: index + 1, message: `Mismatched "${char}" at line ${index + 1}, column ${i + 1}.` });
3628 const lastDelimiter = delimiterStack.pop();
3629 if (delimiters[lastDelimiter.char] !== char) {
3630 errors.push({ line: index + 1, message: `Mismatched "${char}" at line ${index + 1}, column ${i + 1}. Expected "${delimiters[lastDelimiter.char]}".` });
3637 // Check for unclosed blocks at the end of the script
3638 if (blockStack.length > 0) {
3639 errors.push({ line: lineNums[0], message: `Unclosed "${blockStack[0]}" block starting at line ${lineNums[0]}.` });
3642 // Check for unclosed delimiters at the end of the script
3643 if (delimiterStack.length > 0) {
3644 errors.push({ line: delimiterStack[0].line, message: `Unclosed "${delimiterStack[0].char}" delimiter starting at line ${delimiterStack[0].line}, column ${delimiterStack[0].pos}.` });
3647 errors.push(...validateIndentation(originalScript, commands));
3652function regexTest(regex, text) {
3653 regex.lastIndex = 0;
3654 return regex.test(text);
3657// Return only real syntax entries, excluding comment metadata entries.
3658function commandKeys(commands) {
3659 return Object.keys(commands).filter(cmd => cmd !== "COMMENT" && cmd !== "MULTICOMM");
3662// Extract the first command-like token from a line for diagnostics.
3663function extractCommandName(line) {
3664 const commandMatch = line.match(/^\s*([A-Za-z_]\w*)/);
3666 return commandMatch[1];
3668 return line.trim().split(/\s+/)[0] || line.trim().slice(0, 20) || "<empty>";
3671// Find the command-table entry that probably owns this invalid line.
3672function findLikelyCommand(line, commands) {
3673 const firstToken = extractCommandName(line);
3674 const exactCmd = commandKeys(commands).find(cmd => cmd.toLowerCase() === firstToken.toLowerCase());
3679 // Some language constructs do not use the command object key as their first
3680 // token, but they still have a clear command-table entry.
3681 if (/^\s*from\b/i.test(line) && commands.IMPORT) {
3684 if (/^\s*@/.test(line) && commands.DECORATOR) {
3687 if (/^\s*[A-Za-z_]\w*\s*=/.test(line)) {
3688 if (commands.ASSIGNMENT) {
3689 return "ASSIGNMENT";
3691 if (commands.EQUATION) {
3695 if (/^\s*[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\s*\(/.test(line) && commands.CALL_EXPR) {
3702// Suggest a valid command name for likely typos in unknown commands.
3703function suggestCommandName(commandName, commands) {
3704 if (!commandName || commandName === "<empty>") {
3709 let bestDistance = Infinity;
3710 commandKeys(commands).forEach(cmd => {
3711 const distance = levenshteinDistance(commandName.toLowerCase(), cmd.toLowerCase());
3712 if (distance < bestDistance) {
3713 bestDistance = distance;
3718 const maxDistance = Math.max(2, Math.floor(Math.max(commandName.length, bestCmd.length) * 0.35));
3719 return bestDistance <= maxDistance ? bestCmd : "";
3722// Compute edit distance so diagnostics can suggest nearby command names.
3723function levenshteinDistance(a, b) {
3724 const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
3725 for (let i = 0; i <= a.length; i++) {
3728 for (let j = 0; j <= b.length; j++) {
3732 for (let i = 1; i <= a.length; i++) {
3733 for (let j = 1; j <= b.length; j++) {
3734 const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3735 dp[i][j] = Math.min(
3738 dp[i - 1][j - 1] + cost
3743 return dp[a.length][b.length];
3746// Run language-specific indentation checks after regex syntax validation.
3747function validateIndentation(script, commands) {
3748 if (commands !== pythonCommands) {
3753 const indentStack = [0];
3754 const pythonBlockStack = [];
3755 const lines = stripMultilineCommentsPreserveLines(script, commands).split("\n");
3756 let previousCodeLine = null;
3757 let continuationDepth = 0;
3758 let previousLineContinues = false;
3760 lines.forEach((line, index) => {
3761 const lineNo = index + 1;
3762 const trimmedLine = line.trim();
3763 const inContinuation = continuationDepth > 0 || previousLineContinues;
3765 if (!trimmedLine || trimmedLine.startsWith("#")) {
3769 const leadingWhitespace = line.match(/^\s*/)[0];
3770 if (leadingWhitespace.includes("\t")) {
3771 errors.push({ line: lineNo, message: "Python indentation uses tabs. Use spaces instead." });
3774 const indent = leadingWhitespace.replace(/\t/g, " ").length;
3775 const cmdKey = Object.keys(commands).find(cmd => {
3776 const cmdPattern = commands[cmd].patterns || null;
3777 return Array.isArray(cmdPattern) && cmdPattern.some(pattern => regexTest(pattern, trimmedLine));
3779 const cmdData = cmdKey ? commands[cmdKey] : null;
3781 if (!inContinuation) {
3782 if (indent % 4 !== 0) {
3783 errors.push({ line: lineNo, message: "Python indentation should use multiples of 4 spaces." });
3786 if (previousCodeLine?.opensBlock && indent <= previousCodeLine.indent) {
3787 errors.push({ line: lineNo, message: `Expected an indented block after line ${previousCodeLine.line}.` });
3790 while (indent < indentStack[indentStack.length - 1]) {
3794 const tryBlockAtThisIndent = pythonBlockStack
3797 .find(block => block.type === "TRY" && block.indent === indent);
3798 const isTryContinuation = cmdData?.pythonBlockContinuation === "TRY" ||
3799 (cmdKey === "ELSE" && tryBlockAtThisIndent);
3801 while (pythonBlockStack.length > 0) {
3802 const block = pythonBlockStack[pythonBlockStack.length - 1];
3804 if (indent < block.indent) {
3805 pythonBlockStack.pop();
3806 if (block.type === "TRY" && !block.hasContinuation) {
3807 errors.push({ line: block.line, message: '"try" without matching "except" or "finally".' });
3812 if (indent === block.indent && !isTryContinuation) {
3813 pythonBlockStack.pop();
3814 if (block.type === "TRY" && !block.hasContinuation) {
3815 errors.push({ line: block.line, message: '"try" without matching "except" or "finally".' });
3823 if (indent > indentStack[indentStack.length - 1]) {
3824 if (!previousCodeLine?.opensBlock) {
3825 errors.push({ line: lineNo, message: "Unexpected indent." });
3827 indentStack.push(indent);
3830 if (!indentStack.includes(indent)) {
3831 errors.push({ line: lineNo, message: "Indentation does not match any previous block level." });
3834 if (/^(elif|else|except|finally)\b/.test(trimmedLine) && previousCodeLine?.opensBlock) {
3835 errors.push({ line: lineNo, message: `"${trimmedLine.split(/\s+/)[0]}" cannot directly follow an unclosed block header.` });
3838 if (isTryContinuation) {
3839 const matchingBlock = pythonBlockStack
3842 .find(block => block.type === "TRY" && block.indent === indent);
3844 if (!matchingBlock) {
3845 errors.push({ line: lineNo, message: `"${cmdKey.toLowerCase()}" without matching "try" at the same indentation level.` });
3846 } else if (cmdKey === "EXCEPT") {
3847 if (matchingBlock.seenFinally) {
3848 errors.push({ line: lineNo, message: '"except" cannot follow "finally" for the same "try" block.' });
3849 } else if (matchingBlock.seenElse) {
3850 errors.push({ line: lineNo, message: '"except" cannot follow "else" for the same "try" block.' });
3852 matchingBlock.hasContinuation = true;
3853 matchingBlock.seenExcept = true;
3854 } else if (cmdKey === "FINALLY") {
3855 if (matchingBlock.seenFinally) {
3856 errors.push({ line: lineNo, message: 'Duplicate "finally" for the same "try" block.' });
3858 matchingBlock.hasContinuation = true;
3859 matchingBlock.seenFinally = true;
3860 } else if (cmdKey === "ELSE") {
3861 if (!matchingBlock.seenExcept) {
3862 errors.push({ line: lineNo, message: '"else" in a "try" block requires at least one "except" before it.' });
3863 } else if (matchingBlock.seenElse) {
3864 errors.push({ line: lineNo, message: 'Duplicate "else" for the same "try" block.' });
3865 } else if (matchingBlock.seenFinally) {
3866 errors.push({ line: lineNo, message: '"else" cannot follow "finally" for the same "try" block.' });
3868 matchingBlock.seenElse = true;
3873 if (cmdData?.pythonBlockStart) {
3874 pythonBlockStack.push({
3875 type: cmdData.pythonBlockStart,
3878 hasContinuation: false,
3885 previousCodeLine = {
3888 opensBlock: /:\s*(#.*)?$/.test(trimmedLine),
3892 continuationDepth = Math.max(0, continuationDepth + pythonBracketDelta(line));
3893 previousLineContinues = /\\\s*(#.*)?$/.test(line);
3896 pythonBlockStack.forEach(block => {
3897 if (block.type === "TRY" && !block.hasContinuation) {
3898 errors.push({ line: block.line, message: '"try" without matching "except" or "finally".' });
3905// Preserve line numbers while removing Python triple-quoted comment blocks.
3906function stripMultilineCommentsPreserveLines(script, commands) {
3907 const multiCommRegex = commands.MULTICOMM?.patterns?.[0];
3908 if (!multiCommRegex) {
3912 return script.replace(new RegExp(multiCommRegex.source, "g"), (match) => {
3913 return match.split("\n").map(() => " ").join("\n");
3917// Count open/closed grouping delimiters outside strings and inline comments.
3918function pythonBracketDelta(line) {
3921 let escaped = false;
3923 for (let i = 0; i < line.length; i++) {
3924 const char = line[i];
3930 if (char === "\\") {
3935 if (char === quote) {
3940 if (char === "#" ) {
3943 if (char === '"' || char === "'") {
3947 if (char === "(" || char === "[" || char === "{") {
3949 } else if (char === ")" || char === "]" || char === "}") {
3957// Helper function to split arguments with quoted strings by commas
3958function splitArguments(input) {
3959 // Regex to match quoted strings
3960 const regex = /(["'])(.*?)\1/g;
3964 // Step 1: Find all quoted strings and split the input around them
3966 while ((match = regex.exec(input)) !== null) {
3967 // Add unquoted arguments before the current quoted string
3968 const beforeQuoted = input.slice(lastIndex, match.index).trim();
3970 args.push(...beforeQuoted.split(",").map(arg => arg.trim()).filter(arg => arg.length > 0));
3973 // Add the quoted argument
3974 args.push(match[2]); // Capture the content inside the quotes
3976 // Update lastIndex to the end of the current match
3977 lastIndex = regex.lastIndex;
3980 // Step 2: Add any remaining unquoted arguments after the last quoted string
3981 const remaining = input.slice(lastIndex).trim();
3983 args.push(...remaining.split(",").map(arg => arg.trim()).filter(arg => arg.length > 0));