MIDAS
Loading...
Searching...
No Matches
sequencer.js
Go to the documentation of this file.
1/*
2
3 Sequencer syntax highlighting and operation functions
4
5 Created by Zaher Salman on September 16th, 2023
6
7 Usage
8 =====
9
10 Some of these functions, although specific to the midas sequencer,
11 can be used for general syntax highlighting and editing.
12
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.
16
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
22 be used.
23
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
30
31 todo:
32 - syntax validate ODBSET, ODBGET etc. have a valid ODB parameter
33*/
34
35// Using Solarized color scheme - https://ethanschoonover.com/solarized/
36
37// Default msl definitions, keywords, indentation and file name
38var SeqODB = "/Sequencer";
39
40const mslDefs = {
41 get groups() {
42 if (SeqODB === "/PySequencer") {
43 return {
44 strings: {
45 regex: [
46 {pattern: String.raw`(?<!(?:''|\s))'(?:(?!').)*?(?<!')'|(?<!(?:""|\s))(\s*)"(?:(?!").)*?(?<!")"`, flags: "g" }
47 ],
48 color: "#008000",
49 mslclass: "msl_string"
50 },
51 dataManagement: {
52 keywords: [
53 "def", "return", "if", "elif", "else", "pass", "yield", "with", "as", "lambda",
54 "assert", "global", "nonlocal", "del", "await", "async"
55 ],
56 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
57 color: "#268bd2",
58 mslclass: "msl_data_management"
59 },
60 variables: {
61 regex: [
62 { "pattern": "\\$\\w+", "flags": "g" },
63 { "pattern": "^(\\s*)(\\w+)(?=\\s*=)", "flags": "gm" }
64 ],
65 color: "#8B0000",
66 fontWeight: "",
67 mslclass: "msl_variable",
68 },
69 controlFlow: {
70 keywords: ["for", "while", "break", "continue"],
71 kwregex: ["\\b(", ")\\b", "g"],
72 color: "#d33682", // magenta
73 mslclass: "msl_control_flow"
74 },
75 exceptions: {
76 keywords: ["try", "except", "raise", "finally"],
77 kwregex: ["\\b(", ")\\b", "g"],
78 color: "#cb4b16", // orange
79 mslclass: "msl_exceptions"
80 },
81 imports: {
82 keywords: ["import", "from", "as"],
83 kwregex: ["\\b(", ")\\b", "g"],
84 color: "#6c71c4", // purple
85 mslclass: "msl_imports"
86 },
87 operators: {
88 keywords: ["in", "is", "not", "and", "or"],
89 kwregex: ["\\b(", ")\\b", "g"],
90 color: "#2aa198", // teal
91 mslclass: "msl_operators"
92 },
93 datatypes: {
94 keywords: ["int", "float", "str", "list", "dict", "set", "tuple", "bool", "complex"],
95 kwregex: ["\\b(", ")\\b", "g"],
96 color: "#859900", // green
97 mslclass: "msl_datatypes"
98 },
99 builtins: {
100 keywords: [
101 "print", "len", "range", "input", "open", "enumerate", "zip",
102 "map", "filter", "any", "all", "sum", "min", "max", "type", "dir"
103 ],
104 kwregex: ["\\b(", ")\\b", "g"],
105 color: "#b58900", // yellow
106 mslclass: "msl_builtins"
107 },
108 bool: {
109 keywords: ["True", "False", "None"],
110 kwregex: ["\\b(", ")\\b", "g"],
111 color: "#61b7b7",
112 mslclass: "msl_bool"
113 },
114 numbers: {
115 regex: [
116 { pattern: "\\b\\d+(\\.\\d+)?([eE][-+]?\\d+)?\\b", flags: "g" }
117 ],
118 color: "#b58900",
119 mslclass: "msl_number"
120 },
121 multilineComments: {
122 regex: [{
123 pattern: String.raw`^(\s*)("""|''')([^]*?)\2`,
124 flags: "gm"
125 }],
126 color: "#839496",
127 fontWeight: "italic",
128 mslclass: "msl_comment",
129 },
130 comments: {
131 regex: [
132 { pattern: "#.*?$", flags: "gm" }
133 ],
134 color: "#839496",
135 fontWeight: "italic",
136 mslclass: "msl_comment"
137 },
138 };
139 } else {
140 return {
141 strings: {
142 regex: [
143 { "pattern": "([\"'])(.*?)(?<!\\\\‍)\\1", "flags": "g" } // supports escaped quotes
144 ],
145 color: "#008000",
146 fontWeight: "",
147 mslclass: "msl_string",
148 },
149 variables: {
150 regex: [
151 { "pattern": "\\$\\w+", "flags": "g" },
152 { "pattern": "^(\\s*)(\\w+)(?=\\s*=)", "flags": "gm" }
153 ],
154 keywords: ["PARAM","CAT","SET"],
155 kwregex: ["^(\\s*)(?:", ")\\s+(\\w+)\\b", "gim"],
156 color: "#8B0000",
157 fontWeight: "",
158 mslclass: "msl_variable",
159 },
160 dataManagement: {
161 keywords: ["ODBSET", "ODBGET", "ODBCREATE", "ODBDELETE", "ODBINC", "ODBLOAD", "ODBSAVE", "PARAM", "SET", "CAT"],
162 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
163 color: "#2aa198", // cyan
164 fontWeight: "",
165 mslclass: "msl_data_management",
166 },
167 dataTypes: {
168 keywords: ["UNIT8", "INT8", "UNIT16", "INT16", "UNIT32", "INT32", "BOOL", "FLOAT", "DOUBLE", "STRING"],
169 kwregex: ["(?<=,\\s*)\\b(",")\\b","gim"],
170 color: "#859900", // green
171 fontWeight: "",
172 mslclass: "msl_data_types",
173 },
174 loops: {
175 keywords: ["BREAK", "LOOP", "ENDLOOP", "ODBSUBDIR", "ENDODBSUBDIR"],
176 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
177 color: "#d33682", // magenta
178 fontWeight: "",
179 mslclass: "msl_loops",
180 },
181 controlFlow: {
182 keywords: ["GOTO", "CALL", "SCRIPT", "SUBROUTINE", "ENDSUBROUTINE", "TRANSITION", "INCLUDE", "EXIT"],
183 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
184 color: "#268bd2", // blue
185 fontWeight: "",
186 mslclass: "msl_control_flow",
187 },
188 info: {
189 keywords: ["RUNDESCRIPTION", "LIBRARY", "MESSAGE", "MSG"],
190 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
191 color: "#6c71c4", // violet
192 fontWeight: "",
193 mslclass: "msl_info",
194 },
195 cond: {
196 keywords: ["IF", "ELSE", "ENDIF", "WAIT"],
197 kwregex: ["^(\\s*)\\b(",")\\b","gim"],
198 color: "#c577f6", // pink (not solarized)
199 fontWeight: "",
200 mslclass: "msl_cond",
201 },
202 units: {
203 keywords: ["SECONDS", "EVENTS", "ODBVALUE"],
204 kwregex: ["\\b(",")\\b","gi"],
205 color: "#cb4b16", // orange
206 fontWeight: "",
207 mslclass: "msl_units",
208 },
209 actions: {
210 keywords: ["start", "stop", "pause", "resume"],
211 kwregex: ["\\b(", ")\\b", "gi"], //["\\b(",")\\b(\\s*)$","gim"],
212 color: "#dc322f", // red
213 fontWeight: "",
214 mslclass: "msl_actions",
215 },
216 numbers: {
217 //regex: [/\b(\d+(\.\d+)?([eE][-+]?\d+)?)\b/g],
218 regex: [
219 { "pattern": "\\b\\d+(\\.\\d+)?([eE][-+]?\\d+)?\\b", "flags": "g" }
220 ],
221 color: "#b58900", // yellow
222 fontWeight: "",
223 mslclass: "msl_number",
224 },
225 bool: {
226 keywords: ["true","false"],
227 kwregex: ["\\b(",")\\b","gim"],
228 color: "#61b7b7",
229 fontWeight: "",
230 mslclass: "msl_bool",
231 },
232 comments: {
233 regex: [
234 { pattern: "(^\\s*COMMENT\\b.*?$|#.*?$)", flags: "gim" }
235 ],
236 color: "#839496",
237 fontWeight: "italic",
238 mslclass: "msl_comment"
239 }
240 }
241 }
242 },
243
244 get defIndent() {
245 if (SeqODB === "/PySequencer") {
246 return {
247 indentplus: ["def", "for", "if", "elif", "else", "try", "except", "finally", "with", "while"],
248 indentminos: ["elif", "else", "except", "finally"],
249 nIndent: 4,
250 };
251 } else {
252 return {
253 indentplus: ["IF","LOOP","ELSE","SUBROUTINE","ODBSUBDIR"],
254 indentminos: ["ENDIF","ENDLOOP","ELSE","ENDSUBROUTINE","ENDODBSUBDIR"],
255 nIndent: 3,
256 };
257 }
258 },
259
260 get filename() {
261 return {
262 path : "sequencer",
263 next : `${SeqODB}/State/Next Filename`,
264 ext : SeqODB === "/PySequencer" ? "*.py" : "*.msl"
265 };
266 },
267};
268
269var seq_css = `
270/* For buttons */
271.seqbtn {
272 display: none;
273}
274
275#iconsRow img {
276 cursor: pointer;
277 padding: 0.4em;
278 margin: 0;
279}
280#iconsRow img:hover {
281 background: #C0D0D0;
282}
283
284.edt_linenum {
285 background-color: #f0f0f0;
286 user-select: none;
287 width: 3em;
288 overflow: hidden;
289 color: gray;
290 text-align: right;
291 padding: 5px 5px 20px 5px;
292 box-sizing: border-box;
293 vertical-align: top;
294 display: inline-block;
295 margin: 0 0 0 0;
296 font-family: monospace;
297 white-space: pre;
298 -moz-user-select: text;
299 pointer-events: none;
300}
301.edt_linenum span {
302 display: block;
303}
304.edt_linenum_curr {
305 color: #000000;
306 font-weight: bold;
307}
308.edt_area {
309 width: calc(100% - 3em);
310 max-width: calc(100% - 3em);
311 overflow:auto;
312 /* resize: horizontal; */
313 background-color:white;
314 color: black;
315 padding: 5px 5px 20px 5px;
316 box-sizing: border-box;
317 vertical-align: top;
318 display: inline-block;
319 margin: -1px 0 0 0;
320 border-top: 1px solid gray;
321 font-family: monospace;
322 white-space: pre;
323 -moz-user-select: text;
324}
325
326.sline {
327 display: inline-block;
328}
329.esline {
330 display: inline-block;
331}
332
333.msl_comment, .msl_comment * {
334 all: revert !important;
335 color: #839496 !important;
336 font-weight: italic !important;
337 background: none !important;
338}
339
340.msl_current_line {
341 background-color: #FFFF00;
342 font-weight: bold;
343}
344.msl_current_line:hover {
345 opacity : 0.8;
346}
347.msl_fatal {
348 background-color: #FF0000;
349 font-weight: bold;
350}
351.msl_err {
352 background-color: #FF8800;
353 font-weight: bold;
354}
355.msl_msg {
356 background-color: #11FF11;
357 font-weight: bold;
358}
359.msl_current_cmd {
360 background-color: #FFFF00;
361 font-weight: bold;
362}
363.info_column{
364 overflow:auto;
365 /*resize: horizontal;*/
366 align-items: start;
367 padding-left: 5px;
368}
369.infotable {
370 width: 100%;
371 border-spacing: 0px;
372 border: 1px solid black;
373 border-radius: 5px;
374 text-align: left;
375 padding: 0px;
376 float: right;
377 line-height: 30px; /* since modbvalue forces resize */
378}
379.infotable th{
380 white-space: nowrap;
381}
382.infotable tr:first-child th:first-child,
383.infotable tr:first-child td:first-child {
384 border-top-left-radius: 5px;
385}
386.infotable tr:first-child th:last-child,
387.infotable tr:first-child td:last-child {
388 border-top-right-radius: 5px;
389}
390.infotable tr:last-child td:first-child {
391 border-bottom-left-radius: 5px;
392}
393.infotable tr:last-child td:last-child {
394 border-bottom-right-radius: 5px;
395}
396.waitlooptxt {
397 z-index: 33;
398 position: absolute;
399 top: 1px;
400 left: 5px;
401 display: inline-block;
402}
403.waitloopbar {
404 z-index: 32;
405 width: calc(100% - 2px);
406 height: 100%;
407 text-align: left;
408}
409
410/* Dropdown button styles*/
411.dropmenu {
412 background-color: Transparent;
413 font-size: 100%;
414 font-family: verdana,tahoma,sans-serif;
415 border: none;
416 cursor: pointer;
417 text-align: left;
418 padding: 5px;
419 line-height: 1;
420 color:#404040;
421}
422.dropmenu:hover {
423 background-color: #C0D0D0;
424}
425
426/* Style the dropdown content (hidden by default) */
427.dropdown {
428 position: relative;
429 display: inline-block;
430}
431.dropdown-content {
432 display: none;
433 position: absolute;
434 background-color: #f9f9f9;
435 min-width: 150px;
436 box-shadow: 0 8px 16px rgba(0,0,0,0.2);
437 z-index: 10;
438 font-size: 90%;
439 color: black;
440 white-space: nowrap;
441 cursor: pointer;
442}
443.dropdown-content a{
444 display: block;
445 padding: 4px 8px;
446}
447.dropdown-content div{
448 display: flex;
449 justify-content: space-between;
450 align-items: center;
451 padding: 4px 8px;
452}
453.dropdown-content a:hover {
454 background-color: #C0D0D0;
455}
456.dropdown-content div:hover {
457 background-color: #C0D0D0;
458}
459.dropdown:hover .dropdown-content {
460 display: block;
461}
462
463 /* Style the tab */
464.etab {
465 position: relative;
466 top: 2px;
467 z-index: 1;
468 overflow-x: auto;
469 white-space: nowrap;
470 width: 100%;
471 display: block;
472 -webkit-overflow-scrolling: touch;
473 border: none;
474}
475.etab button {
476 background-color: #D0D0D0;
477 float: left;
478 margin: 4px 2px 0px 2px;
479 border-top-left-radius: 10px;
480 border-top-right-radius: 10px;
481 border: none;
482 border-top: 1px solid Transparent;
483 border-right: 1px solid Transparent;
484 border-left: 1px solid Transparent;
485 cursor: pointer;
486 padding: 3px 5px 3px 5px;
487}
488.etab button:hover {
489 background-color: #FFFFFF;
490 border-bottom: 5px solid #FFFFFF;
491}
492.etab button.edt_active:hover {
493 background-color: #FFFFFF;
494 border-bottom: 5px solid #FFFFFF;
495}
496.etab button.edt_active {
497 background-color: white;/*Transparent;*/
498 border-top: 1px solid gray;
499 border-right: 1px solid gray;
500 border-left: 1px solid gray;
501 border-bottom: 5px solid white;
502 color: black;
503}
504.etabcontent {
505 display: none;
506 padding: 0;
507 border-top: none;
508 height: 100%;
509}
510.editbtn{
511 background-color: #f0f0f0;
512 border: 1px solid black;
513 border-radius: 3px;
514 cursor: pointer;
515 padding: 0px 2px 0px 2px;
516 margin-left: 3px;
517}
518.editbtn:hover{
519 background-color: #C0D0D0;
520}
521.closebtn {
522 font-family: verdana,tahoma,sans-serif;
523 border: none;
524 border-radius: 3px;
525 cursor: pointer;
526 padding: 2px;
527 margin-left: 5px;
528}
529.closebtn:hover {
530 background-color: #FD5E59;
531}
532
533.progress {
534 width: 200px;
535 font-size: 0.7em;
536}
537.dlgProgress {
538 border: 1px solid black;
539 box-shadow: 6px 6px 10px 4px rgba(0,0,0,0.2);
540 border-radius: 6px;
541 position: fixed;
542 display: none;
543}
544
545.empty-dragrow td{
546 border: 2px dashed #6bb28c;
547 background-color: white;
548}
549tr.dragstart td{
550 background-color: gray;
551 color: white;
552}
553tr.dragrow td{
554 overflow: hidden;
555 text-overflow: ellipsis;
556 max-width: calc(10em - 30px);
557}
558#nextFNContainer img {
559 cursor: pointer;
560 height: 1em;
561 vertical-align: center;
562}
563`;
564
565// line connector string
566const lc = "\n";
567// revisions array, maximum nRevisions
568const previousRevisions = {};
569const nRevisions = 20;
570var revisionIndex = {};
571var saveRevision = {};
572// Meta combo keydown flag
573var MetaCombo = false;
574// Deal with Chrome issues
575var isChrome = browserType();
576// Make current state global
577var stateText = "Unknown";
578
579// -- Sequencer specific functions --
580// Implement colors and styles from KeywordsGroups in CSS
581function seqCSS() {
582 for (const group in mslDefs.groups) {
583 const { mslclass, color, fontWeight } = mslDefs.groups[group];
584 if (mslclass) {
585 if (fontWeight) {
586 seq_css += `.${mslclass} { color: ${color}; font-weight: ${fontWeight}; }\n`;
587 } else {
588 seq_css += `.${mslclass} { color: ${color};}\n`;
589 }
590 }
591 }
592 const seqStyle = document.createElement('style');
593 seqStyle.textContent = seq_css;
594 document.head.appendChild(seqStyle);
595}
596
597// Setup the correct sequencer state visually
598function seqState(funcCall) {
599 /* Arguments:
600 funcCall - (optional) a function to be called when the state is set (with the state text)
601 */
602 stateText = "Stopped";
603 // Check sequence state
604 mjsonrpc_db_get_values([`${SeqODB}/State/Running`,`${SeqODB}/State/Paused`,`${SeqODB}/State/Finished`,`${SeqODB}/State/Debug`,`${SeqODB}/State/Stop after run`]).then(function(rpc) {
605 if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2] && !rpc.result.data[4]) {
606 stateText = "Running";
607 } else if (rpc.result.data[1] && rpc.result.data[0] && !rpc.result.data[2] && !rpc.result.data[4]) {
608 stateText = "Paused";
609 } else if (rpc.result.data[0] && !rpc.result.data[1] && !rpc.result.data[2] && rpc.result.data[4]) {
610 // Check if stop after run is set and running
611 stateText = "Running.StopAfter"
612 } else if (rpc.result.data[0] && rpc.result.data[1] && !rpc.result.data[2] && rpc.result.data[4]) {
613 // Check if stop after run is set and paused
614 stateText = "Paused.StopAfter"
615 } else {
616 stateText = "Stopped";
617 }
618
619 if (funcCall) {
620 funcCall(stateText);
621 }
622 return stateText;
623 }).catch (function (error) {
624 console.error(error);
625 return "Unknown";
626 });
627}
628
629// Ask user to edit current sequence
630function askToEdit(flag,event) {
631 if (flag) {
632 openETab(document.getElementById("etab1-btn"));
633 const [lineNumbers,editor,btnLabel,label] = editorElements();
634 // make editable and add event listeners
635 //editor.contentEditable = true;
636 addETab(document.getElementById("addETab"));
637 seqOpen(label.title.split("\n")[0]);
638 event.stopPropagation();
639 return;
640 }
641 const message = "To edit the sequence it must be opened in an editor tab.<br>Would you like to proceed?";
642 dlgConfirm(message,function(resp) {
643 if (resp) {
644 const label = editorElements()[3];
645 addETab(document.getElementById("addETab"));
646 seqOpen(label.title.split("\n")[0]);
647 }
648 });
649}
650
651// Enable editing of sequence
652function editorEventListeners() {
653 let [lineNumbers,editor] = editorElements();
654 editor.contentEditable = true;
655 // Attached syntax highlight event editor
656 editor.addEventListener("keydown",checkSyntaxEventDown);
657 editor.addEventListener("keyup",checkSyntaxEventUp);
658 editor.addEventListener("paste", checkSyntaxEventPaste,true);
659 // Change in text
660 editor.addEventListener("input", function() {
661 seqIsChanged(true);
662 });
663 document.addEventListener("selectionchange", function(event) {
664 if (event.target.activeElement === editor) markCurrLineNum();
665 });
666 // Synchronize the scroll position of lineNumbers with editor
667 editor.addEventListener("scroll", function() {
668 lineNumbers.scrollTop = editor.scrollTop;
669 });
670 if (isChrome === 1) {
671 editor.addEventListener("keydown", arrowKeysChrome);
672 }
673}
674
675// apply changes of filename in the ODB (triggers reload)
676function seqChange(filename) {
677 /* Arguments:
678 filename - full file name with path to change
679 */
680 if (!filename) return;
681 const lastIndex = filename.lastIndexOf('/');
682 const path = filename.substring(0, lastIndex).replace(new RegExp('^' + mslDefs.filename.path),"").replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
683 const file = filename.substring(lastIndex + 1);
684 // set path and filename, wait for completion and return
685 mjsonrpc_db_paste([`${SeqODB}/State/Path`,`${SeqODB}/State/Filename`],[path,file]).then(function (rpc1) {
686 sessionStorage.removeItem("depthDir");
687 if (rpc1.result.status[0] === 1 && rpc1.result.status[1] === 1) {
688 mjsonrpc_db_paste([`${SeqODB}/Command/Load new file`],[true]).then(function (rpc2) {
689 if (rpc2.result.status[0] === 1) {
690 return;
691 }
692 }).catch(function (error) {console.error(error);});
693 } else {
694 dlgAlert("Something went wrong, I could not set the filename!");
695 }
696 }).catch(function (error) {console.error(error);});
697}
698
699// Save sequence text in filename.
700function seqSave(filename) {
701 /* Arguments:
702 filename (opt) - save to provided filename with path. If undefined save to original
703 filename and if empty trigger file_picker.
704 */
705 let [lineNumbers,editor,label] = editorElements();
706 let text = editor.innerText;
707 let ext = mslDefs.filename.ext.replace("*","");
708 if (editor.id !== "editorTab1") {
709 if (filename === undefined) {
710 // take name from button title
711 filename = label.title;
712 if (filename.endsWith(ext)) {
713 file_save_ascii_overwrite(filename,text);
714 seqIsChanged(false);
715 } else {
716 seqSave("");
717 }
718 } else if (filename === "") {
719 filename = label.title;
720 let file = filename.split("/").pop();
721 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
722 // If file/path are empty start with default value
723 if (path === "")
724 path = sessionStorage.getItem("pathName") ? sessionStorage.getItem("pathName") : mslDefs.filename.path + "/";
725 if (file === "")
726 file = sessionStorage.getItem("fileName") ? sessionStorage.getItem("fileName") : "filename" + ext;
727 file_picker(path,mslDefs.filename.ext,seqSave,true,{},true);
728 } else {
729 file_save_ascii_overwrite(filename,text);
730 let file = filename.split("/").pop();
731 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
732 label.title = filename;
733 label.innerText = file;
734 sessionStorage.setItem("fileName",file);
735 sessionStorage.setItem("pathName",path);
736 seqIsChanged(false);
737 }
738 // Check if filename is currently in editorTab1 and reload
739 let currFilename = document.getElementById("etab1-btn").title;
740 if (filename == currFilename) {
741 modbset(`${SeqODB}/Command/Load new file`,true);
742 }
743 return;
744 }
745}
746
747// Open filename
748function seqOpen(filename) {
749 /* Arguments:
750 filename - file name to open (empty trigger file_picker)
751 */
752 // if a full filename is provided, open and return
753 if (filename && filename !== "") {
754 // Identify active tab
755 let [lineNumbers,editor,label] = editorElements();
756 // Check the option to open in new tab, also open in new tab if sequence is running
757 if ((document.getElementById("inNewTab").checked && (label.title !== "" || editor.id !== "editorTab1")) ||
758 (editor.id === "editorTab1" && stateText === "Running")) {
759 addETab(document.getElementById("addETab"));
760 [lineNumbers,editor,label] = editorElements();
761 }
762 let file = filename.split("/").pop();
763 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
764 label.title = filename.replaceAll(/\/+/g, '/');
765 label.innerText = file;
766 sessionStorage.setItem("fileName",file);
767 sessionStorage.setItem("pathName",path);
768 if (editor.id === "editorTab1") {
769 seqChange(filename);
770 } else {
771 file_load_ascii(filename, function(text) {
772 editor.innerHTML = syntax_msl(text).join(lc).slice(0,-1);
773 updateLineNumbers(lineNumbers,editor);
774 // Change state to not edited
775 seqIsChanged(false);
776 // save first state
777 saveState(text,editor);
778 });
779 }
780 return;
781 } else {
782 // empty or undefined file name
783 mjsonrpc_db_get_values([`${SeqODB}/State/Path`]).then(function(rpc) {
784 let path = mslDefs.filename.path + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
785 sessionStorage.setItem("pathName",path);
786 file_picker(path,mslDefs.filename.ext,seqOpen,false);
787 }).catch(function (error) {
788 mjsonrpc_error_alert(error);
789 });
790}
791}
792
793// Show/hide buttons according to sequencer state
794function updateBtns(state) {
795 /* Arguments:
796 state - the state of the sequencer
797 */
798 const seqState = {
799 Running: {
800 color: "var(--mgreen)",
801 },
802 Stopped: {
803 color: "var(--mred)",
804 },
805 Editing: {
806 color: "var(--mred)",
807 },
808 Paused: {
809 color: "var(--myellow)",
810 },
811 }
812 const color = seqState[state.split(".")[0]].color;
813 const nclass = state.split(".").length;
814 const seqStateSpans = document.querySelectorAll('.seqstate');
815 seqStateSpans.forEach(e => {e.style.backgroundColor = color;});
816 // hide all buttons
817 const hideBtns = document.querySelectorAll('.seqbtn');
818 hideBtns.forEach(button => {
819 button.style.display = "none";
820 });
821 // then show only those belonging to the current state
822 const showBtns = document.querySelectorAll('.seqbtn.' + state);
823 showBtns.forEach(button => {
824 if (button.tagName === "IMG") {
825 button.style.display = "inline-block";
826 } else {
827 button.style.display = "flex";
828 }
829 });
830 // Hide exclusive buttons
831 if (nclass === 1) {
832 const exclBtns = document.querySelectorAll('.seqbtn.Exclusive');
833 exclBtns.forEach(button => {
834 button.style.display = "none";
835 });
836 }
837 // Hide progress modal when stopped
838 const hideProgress = document.getElementById("Progress");
839 if (state === "Stopped" && hideProgress) hideProgress.style.display = "none";
840}
841
842// Show sequencer messages if present
843function mslMessage(message) {
844 // Empty message, return
845 if (!message) return;
846 // Check message and message wait
847 mjsonrpc_db_get_values([`${SeqODB}/State/Message`,`${SeqODB}/State/Message Wait`]).then(function(rpc) {
848 const message = rpc.result.data[0];
849 const hold = rpc.result.data[1];
850 if (hold) {
851 dlgMessage("Message", message, true, false,clrMessage);
852 } else {
853 dlgAlert(message);
854 }
855 }).catch (function (error) {
856 console.error(error);
857 });
858}
859
860// Clear sequencer messages
861function clrMessage() {
862 mjsonrpc_db_paste([`${SeqODB}/State/Message`], [""]).then(function (rpc) {
863 return;
864 }).catch(function (error) {
865 console.error(error);
866 });
867}
868
869// Adjust size of sequencer editor according to browser window size
870function windowResize() {
871 const [lineNumbers, editor] = editorElements();
872 const seqTable = document.getElementById("seqTable");
873 const infoColumn = document.getElementById("infoColumn");
874
875 // Calculate scrollbar width
876 const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
877
878 // Visible width of the window accounting for scrollbar
879 let winWidth = Math.max(document.documentElement.clientWidth, window.innerWidth) - scrollbarWidth;
880
881 // Visible height of the window
882 let winHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
883
884 // Set seqTable height and width to fit the remaining space in the viewport
885 seqTable.style.width = winWidth - seqTable.getBoundingClientRect().left + "px";
886 seqTable.style.height = winHeight - seqTable.getBoundingClientRect().top - 20 + "px";
887
888 // Adjust editor dimensions
889 const editorTop = editor.getBoundingClientRect().top;
890 editor.style.height = winHeight - editorTop - 20 + "px";
891 editor.style.width = winWidth - editor.getBoundingClientRect().left - infoColumn.getBoundingClientRect().width - 10 + "px";
892 editor.style.maxWidth = editor.style.width;
893
894 // Adjust infoColumn and lineNumbers heights to match the editor height
895 infoColumn.style.height = editor.style.height;
896 lineNumbers.style.height = editor.style.height;
897}
898
899// Load the current sequence from ODB (only on main tab)
900function load_msl() {
901 const editor = document.getElementById("editorTab1");
902 const btn = document.getElementById("etab1-btn");
903 mjsonrpc_db_get_values([`${SeqODB}/Script/Lines`,`${SeqODB}/State/Running`,`${SeqODB}/State/SCurrent line number`,`${SeqODB}/State/Filename`,`${SeqODB}/State/SFilename`,`${SeqODB}/State/Path`]).then(function(rpc) {
904 let seqLines = rpc.result.data[0];
905 let seqState = rpc.result.data[1];
906 let currLine = rpc.result.data[2];
907 let filename = rpc.result.data[3];
908 let sfilename = rpc.result.data[4] ? rpc.result.data[4].split('userfiles/sequencer/')[1] : "";
909 filename = (mslDefs.filename.path + "/" + rpc.result.data[5] + "/" + filename).replace("//","/");
910 // syntax highlight
911 if (seqLines) {
912 editor.innerHTML = syntax_msl(seqLines).join(lc);
913 updateLineNumbers(editor.previousElementSibling,editor);
914 if (seqState) hlLine(currLine);
915 }
916 let file = filename.split("/").pop();
917 let path = filename.substring(0, filename.lastIndexOf("/") + 1).replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
918 sessionStorage.setItem("fileName",file);
919 sessionStorage.setItem("pathName",path);
920 // change button title to add sfilename if present
921 btn.title = (sfilename && sfilename !== filename) ? filename + "\n" + sfilename : filename;
922 // also change validation icon to defaule
923 let vldBtn = document.getElementById("vldBtn");
924 vldBtn.src = "icons/validate-syntax.svg";
925 vldBtn.style.backgroundColor = "";
926 }).catch (function (error) {
927 console.error(error);
928 });
929}
930
931// Highlight (background color) and scroll to current line
932function hlLine(lineNums,color,editor,msgs = []) {
933 /* Arguments:
934 lineNums- the line number to be highlighted (or an array of numbers)
935 color - (optional) background color
936 editor - (optional) if provided, scroll to highlighted line in editor
937 msgs - (optional) if provided, use as title/s for the highlighted line/s
938 */
939
940 // Remove highlight from all lines with the class "msl_current_line"
941 const highlightedLines = document.querySelectorAll(".msl_current_line");
942 highlightedLines.forEach((line) => line.classList.remove("msl_current_line"));
943
944 // If single value make an array
945 lineNums = Array.isArray(lineNums) ? lineNums : [lineNums];
946 msgs = Array.isArray(msgs) ? msgs : [msgs];
947 let counter = 0;
948 lineNums.forEach(lineNum => {
949 const lineId = "sline" + lineNum;
950 const lineHTML = (editor) ? editor.querySelector(`#${lineId}`) : document.getElementById(lineId);
951
952 if (lineHTML) {
953 lineHTML.classList.add("msl_current_line");
954 if (color) lineHTML.style.backgroundColor = color;
955 if (msgs[counter]) lineHTML.title = msgs[counter];
956 // Scroll to the highlighted line if the checkbox is checked
957 const scrollToCurrCheckbox = document.getElementById("scrollToCurr");
958 if (((scrollToCurrCheckbox && scrollToCurrCheckbox.checked) || editor) && (counter == 0)) {
959 lineHTML.scrollIntoView({ block: "center" });
960 }
961 }
962 counter++;
963 });
964}
965
966// Scroll to make line appear in the center of editor
967function scrollToCurr(scrToCur) {
968 if (scrToCur.checked) {
969 localStorage.setItem("scrollToCurr",true);
970 const currLine = document.querySelector(".msl_current_line");
971 if (currLine) {
972 currLine.scrollIntoView({ block: "center" });
973 }
974 } else {
975 localStorage.removeItem("scrollToCurr",true);
976 }
977}
978
979// Open files in new tabs
980function toggleCheck(e) {
981 if (e.checked) {
982 localStorage.setItem(e.id,true);
983 } else {
984 localStorage.removeItem(e.id);
985 }
986}
987
988// shortcut event handling to overtake default behaviour
989function shortCutEvent(event) {
990 const parEditor = editorElements();
991 const notFirstTab = (parEditor[1].id !== "editorTab1");
992 if (notFirstTab) {
993 // Check these only for editors
994 if (event.altKey && event.key === 's') {
995 event.preventDefault();
996 //save as with file_picker
997 seqSave('');
998 event.preventDefault();
999 } else if ((event.ctrlKey || event.metaKey) && event.key === 's') {
1000 event.preventDefault();
1001 //save to the same filename
1002 seqSave();
1003 event.preventDefault();
1004 } else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
1005 event.preventDefault();
1006 undoEdit(event.target);
1007 event.preventDefault();
1008 } else if ((event.ctrlKey || event.metaKey) && event.key === 'r') {
1009 event.preventDefault();
1010 redoEdit(event.target);
1011 event.preventDefault();
1012 }
1013 }
1014 if (!notFirstTab) {
1015 // Check these only for first tab
1016 if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
1017 // open new tab and load current sequence
1018 event.preventDefault();
1019 addETab(document.getElementById("addETab"));
1020 seqOpen(parEditor[3].title.split("\n")[0]);
1021 event.preventDefault();
1022 }
1023 }
1024 // Check these for all tabs
1025 if (event.altKey && event.key === 'n') {
1026 // open new tab
1027 event.preventDefault();
1028 addETab(document.getElementById("addETab"));
1029 event.preventDefault();
1030 } else if (event.altKey && event.key === 'o') {
1031 event.preventDefault();
1032 seqOpen();
1033 event.preventDefault();
1034 }
1035
1036}
1037
1038// Trigger syntax highlighting on keyup events
1039function checkSyntaxEventUp(event) {
1040 if (event.ctrlKey || event.altKey || event.metaKey || MetaCombo) return;
1041 if (event.keyCode >= 0x30 || event.key === ' '
1042 || event.key === 'Backspace' || event.key === 'Delete'
1043 || event.key === 'Enter'
1044 ) {
1045
1046 const e = event.target;
1047 let caretPos = getCurrentCursorPosition(e);
1048 let currText = e.innerText;
1049 // Indentation keywords
1050 const defIndent = mslDefs.defIndent;
1051 const nIndent = defIndent.nIndent || 3;
1052 // save current revision for undo
1053 if (event.key === ' ' || event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
1054 saveState(currText,e);
1055 seqIsChanged(true);
1056 }
1057
1058 // Indent according to previous line
1059 if (event.key === 'Enter') {
1060 event.preventDefault();
1061 // get previous and current line elements (before and after enter)
1062 let pline = whichLine(e,-1);
1063 let cline = whichLine(e);
1064 let plineText = (pline) ? pline.innerText : "";
1065 let clineText = (cline) ? cline.innerText : "";
1066 let indentLevel = 0;
1067 let preSpace = 0;
1068 let indentString = "";
1069 if (plineText) {
1070 // indent line according to the previous line text
1071 // if, loop, else, subroutine - increase indentation
1072 const indentPlus = defIndent.indentplus.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1073 // else, endif, endloop, endsubroutine - decrease indentation
1074 const indentMinos = defIndent.indentminos.some(keyword => plineText.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1075 /* (indentMinos/indentPlus)
1076 true/false - pline indent -1, cline indent 0
1077 fale/true - pline indent 0, cline indent +1
1078 true/true - pline indent -1, cline indent +1
1079 false/false- pline indent 0, cline indent 0
1080 */
1081 // Count number of white spaces at begenning of pline
1082 preSpace = plineText.replace("\n","").search(/\S|$/);
1083 pPreSpace = preSpace - indentMinos * nIndent;
1084 if (pPreSpace < 0) pPreSpace = 0;
1085 cPreSpace = pPreSpace + indentPlus * nIndent;
1086 // Calculate and insert indentation
1087 pIndentString = " ".repeat(pPreSpace);
1088 cIndentString = " ".repeat(cPreSpace);
1089 cline.innerText = cIndentString + clineText.trimStart();
1090 caretPos += cline.innerText.length - clineText.length;
1091 pline.innerText = pIndentString + plineText.trimStart();
1092 caretPos += pline.innerText.length - plineText.length;
1093 }
1094 event.preventDefault();
1095 }
1096 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
1097 setCurrentCursorPosition(e, caretPos);
1098 updateLineNumbers(e.previousElementSibling,e);
1099 e.focus();
1100 }
1101 return;
1102}
1103
1104// Handle arrow up/down keys in Chrome
1105function arrowKeysChrome(event) {
1106 // Skip combos with special keys
1107 if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
1108 if ((event.key === "ArrowUp" || event.key === "ArrowDown") && event.shiftKey ) {
1109 event.preventDefault();
1110 startChromeSelecting(event.target);
1111 } else if ((event.key === "ArrowUp" || event.key === "ArrowDown") && !event.shiftKey ) {
1112 event.preventDefault();
1113 const e = event.target;
1114 let caretPos = getCurrentCursorPosition(e);
1115 let ncaretPos = caretPos;
1116 let currText = e.innerText;
1117 let lines = currText.split("\n");
1118 // Determine the current line number (0-based index)
1119 let clineNum = getLinesInSelection(e)[0] - 1;
1120 // Determine the direction: -1 for up, +1 for down
1121 let addLine = (event.key === "ArrowDown") ? 1 : -1;
1122 // Determine the target line number
1123 let nlineNum = clineNum + addLine;
1124 if (lines[nlineNum] !== undefined) {
1125 // line exists can move cursor
1126 let clineStart = currText.split("\n").slice(0, clineNum).join("\n").length + (clineNum > 0 ? 1 : 0);
1127 let caretInLine = caretPos - clineStart;
1128
1129 let nlength = lines[nlineNum].length;
1130 let nlineStart = currText.split("\n").slice(0, nlineNum).join("\n").length + (nlineNum > 0 ? 1 : 0);
1131 if (nlength >= caretInLine && caretInLine >= 0) {
1132 // If the target line is longer or equal to the caret's position in the current line
1133 ncaretPos = nlineStart + caretInLine;
1134 } else if (caretInLine < 0) {
1135 ncaretPos = nlineStart + nlength + 1;
1136 } else {
1137 // If the target line is shorter than the caret's position in the current line
1138 ncaretPos = nlineStart + nlength;
1139 }
1140 setCurrentCursorPosition(e, ncaretPos);
1141 }
1142 }
1143}
1144
1145
1146// Trigger syntax highlighting on keydown events
1147function checkSyntaxEventDown(event) {
1148 sessionStorage.setItem("keydown",event.target.innerText);
1149 // take care of Mac odd keyup behaviour
1150 if (event.metaKey && (/^[a-z]$/.test(event.key) || event.shiftKey || event.altKey)) {
1151 MetaCombo = true;
1152 } else {
1153 MetaCombo = false;
1154 }
1155
1156 if (event.ctrlKey || event.altKey || event.metaKey) return;
1157 // Quickly return for anything but these keys
1158 if (event.key !== 'Tab' && event.key !== 'Escape' && event.key !== 'Backspace' && event.key !== 'Delete' && event.key !== 'Enter') return;
1159 //if (event.key !== 'Tab' && event.key !== 'Escape' && event.key !== 'Enter') return;
1160 const e = event.target;
1161 event.preventDefault();
1162 const textSelected = isTextSelected(e);
1163 if (textSelected && event.key !== 'Tab' && event.key !== 'Escape') {
1164 deleteSelectedText(e);
1165 }
1166 let caretPos = getCurrentCursorPosition(e);
1167 let currText = e.innerText;
1168
1169 if (event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Enter') {
1170 //if (event.key === 'Enter') {
1171 // Handle Backspace, Delete and Enter manually for better compatibility and control
1172 if (event.key === 'Enter') {
1173 currText = currText.substring(0,caretPos) + "\n" + currText.substring(caretPos);
1174 caretPos = caretPos + 1 <= currText.length ? caretPos + 1 : caretPos;
1175 if (currText.substring(caretPos) == "" && isChrome === 1) currText = currText + "\n";
1176 } else if (event.key === 'Backspace') {
1177 currText = currText.substring(0,caretPos).slice(0,-1) + currText.substring(caretPos);
1178 caretPos = caretPos - 1 >= 0 ? caretPos - 1 : caretPos;
1179 } else if (event.key === 'Delete') {
1180 if (textSelected) {
1181 currText = currText.substring(0,caretPos) + currText.substring(caretPos).slice(0);
1182 } else {
1183 currText = currText.substring(0,caretPos) + currText.substring(caretPos).slice(1);
1184 }
1185 }
1186 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
1187 setCurrentCursorPosition(e, caretPos);
1188 keepCaretVisible(e);
1189 return;
1190 } else if (event.key === 'Tab' || event.key === 'Escape') {
1191 let lines = getLinesInSelection(e);
1192 if (event.shiftKey && event.key === 'Tab') {
1193 indent_msl(lines,-1);
1194 } else if (event.key === 'Tab') {
1195 indent_msl(lines,+1);
1196 } else if (event.key === 'Escape') {
1197 indent_msl(lines);
1198 }
1199 e.innerHTML = syntax_msl(e.innerText).join(lc).slice(0,-1);
1200 let newText = e.innerText;
1201 setCurrentCursorPosition(e, caretPos + newText.length - currText.length);
1202 if (lines[0] !== lines[1]) selectLines(lines,e);
1203 return;
1204 }
1205}
1206
1207// Trigger syntax highlighting when you paste text
1208function checkSyntaxEventPaste(event) {
1209 // set time out to allow default to go first
1210 setTimeout(() => {
1211 let e = event.target;
1212 // make sure you paste in the editor area
1213 if (e.tagName !== 'PRE') e = e.parentElement;
1214 let caretPos = getCurrentCursorPosition(e);
1215 let currText = e.innerText;
1216 // save current revision for undo
1217 saveState(currText,e);
1218 e.innerHTML = syntax_msl(currText).join(lc).slice(0,-1);
1219 setCurrentCursorPosition(e, caretPos);
1220 updateLineNumbers(e.previousElementSibling,e);
1221 e.focus();
1222 }, 0);
1223 return;
1224}
1225
1226// Find on which line is the current carret position in e
1227// This assumes each line has an id="sline#" where # is the line number.
1228function whichLine(e,offset = 0) {
1229 // offset allows to pick previous line (after enter)
1230 let pos = getCurrentCursorPosition(e);
1231 if (!pos) return;
1232 let lineNum = e.innerText.substring(0,pos).split("\n").length + offset;
1233 let sline = e.querySelector("#sline" + lineNum.toString());
1234 return sline;
1235}
1236
1237// Return an array with the first and last line numbers of the selected region
1238/* This assumes that the lines are in a <pre> element and that
1239 each line has an id="sline#" where # is the line number.
1240 When the caret in in an empty line, the anchorNode is the <pre> element.
1241*/
1242function getLinesInSelection(e) {
1243 const selection = window.getSelection();
1244 if (selection.rangeCount === 0) return [0,0];
1245 // is it a single line?
1246 const singleLine = selection.isCollapsed;
1247 if (singleLine) {
1248 const line = whichLine(e);
1249 if (line) {
1250 const startLine = parseInt(line.id.replace("sline",""));
1251 return [startLine,startLine];
1252 } else {
1253 return [0,0];
1254 }
1255 }
1256 const anchorNode = selection.anchorNode;
1257 const range = selection.getRangeAt(0);
1258 let startNode,endNode;
1259 if (anchorNode.tagName === 'PRE') {
1260 let startOffset = range.startOffset;
1261 let endOffset = range.endOffset;
1262 startNode = range.startContainer.childNodes[startOffset];
1263 endNode = range.startContainer.childNodes[endOffset-1];
1264 } else {
1265 startNode = (range.startContainer && range.startContainer.parentElement.tagName !== 'PRE') ? range.startContainer : range.startContainer.nextSibling;
1266 if (startNode && startNode.tagName === 'PRE') startNode = startNode.firstChild;
1267 endNode = (range.endContainer && range.endContainer.parentElement.tagName !== 'PRE') ? range.endContainer : range.endContainer.previousSibling;
1268 if (endNode && endNode.tagName === 'PRE') endNode = endNode.lastChild;
1269 }
1270 let startID = (startNode && startNode.id) ? startNode.id : "";
1271 let endID = (endNode && endNode.id) ? endNode.id : "";
1272 // get first line
1273 while (startNode && !startID.startsWith("sline") && startNode.tagName !== 'PRE') {
1274 startNode = (startNode.parentNode.tagName !== 'PRE') ? startNode.parentNode : startNode.nextSibling;
1275 startID = (startNode && startNode.id) ? startNode.id : "";
1276 }
1277 // get last line
1278 while (endNode && !endID.startsWith("sline") && endNode.tagName !== 'PRE') {
1279 endNode = (endNode.parentNode.tagName !== 'PRE') ? endNode.parentNode : endNode.previousSibling;
1280 endID = (endNode && endNode.id) ? endNode.id : "";
1281 }
1282 let startLine = (startNode && startNode.id) ? parseInt(startNode.id.replace("sline","")) : 0;
1283 let endLine = (endNode && endNode.id) ? parseInt(endNode.id.replace("sline","")) : 0;
1284 if (singleLine) {
1285 startLine = endLine = Math.min(startLine, endLine);
1286 }
1287 return [startLine,endLine];
1288}
1289
1290// get current caret position in chars within element parent
1291function getCurrentCursorPosition(parent) {
1292 let sel = window.getSelection();
1293 if (!sel.focusNode || !parent) return;
1294 const range = sel.getRangeAt(0);
1295 const prefix = range.cloneRange();
1296 prefix.selectNodeContents(parent);
1297 prefix.setEnd(range.endContainer, range.endOffset);
1298 return prefix.toString().length;
1299}
1300
1301// set current caret position at pos within element parent
1302function setCurrentCursorPosition(parent,pos) {
1303 for (const node of parent.childNodes) {
1304 if (node.nodeType === Node.TEXT_NODE) {
1305 if (node.length >= pos) {
1306 const range = document.createRange();
1307 const sel = window.getSelection();
1308 range.setStart(node, pos);
1309 range.collapse(true);
1310 sel.removeAllRanges();
1311 sel.addRange(range);
1312 return -1;
1313 } else {
1314 pos = pos - node.length;
1315 }
1316 } else {
1317 pos = setCurrentCursorPosition(node, pos);
1318 if (pos < 0) {
1319 return pos;
1320 }
1321 }
1322 }
1323 return pos;
1324}
1325
1326// Update line numbers in lineNumbers div
1327function updateLineNumbers(lineNumbers,editor) {
1328 if (lineNumbers === undefined || editor === undefined)
1329 [lineNumbers,editor] = editorElements();
1330 // Clear existing line numbers
1331 lineNumbers.innerHTML = "";
1332 // Get the number of lines accurately
1333 let lineCount = editor.querySelectorAll('[id^="sline"]').length;
1334 let lineTextCount = editor.innerText.split("\n").length;
1335 lineCount = (lineTextCount - lineCount) < 2 ? lineTextCount : lineTextCount - 1;
1336 // Add line numbers to lineNumbers
1337 for (let i = 1; i <= lineCount; i++) {
1338 const lineNumber = document.createElement('span');
1339 lineNumber.id = "lNum" + i.toString();
1340 lineNumber.textContent = i;
1341 lineNumbers.appendChild(lineNumber);
1342 }
1343 lineNumbers.scrollTop = editor.scrollTop;
1344 windowResize();
1345}
1346
1347// Utility function to escape special characters in a string for use in a regular expression
1348function escapeRegExp(s) {
1349 if (!s) return "";
1350 return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
1351}
1352
1353function syntax_msl(seqLines, keywordGroups) {
1354 // Use default if none provided
1355 if (!keywordGroups) {
1356 keywordGroups = mslDefs.groups;
1357 }
1358
1359 // Convert to text
1360 let mslText = Array.isArray(seqLines) ? seqLines.join("\n") : seqLines;
1361 mslText = escapeSpecialCharacters(mslText);
1362 const seqLines_org = mslText.split(/\n/);
1363
1364 // Loop over all keyword groups
1365 for (const [groupName, group] of Object.entries(keywordGroups)) {
1366 const className = group.mslclass;
1367 const spanStart = `<span class='${className}'>`;
1368 const spanEnd = `</span>`;
1369
1370 // If kwregex and keywords are defined
1371 if (group.keywords && group.kwregex && group.kwregex.length === 3) {
1372 const [prefix, suffix, flags] = group.kwregex;
1373 const regexStr = prefix + group.keywords.join("|") + suffix;
1374 const regex = new RegExp(regexStr, flags);
1375
1376 mslText = mslText.replace(regex, (...args) => {
1377 const match = args[0];
1378 const captureGroups = args.slice(1, -2); // remove offset & input
1379 if (captureGroups.length === 0) {
1380 return `${spanStart}${match}${spanEnd}`;
1381 }
1382 // Wrap last capturing group
1383 let result = "";
1384 let index = 0;
1385 let lastIndex = 0;
1386 const raw = match;
1387 for (let i = 0; i < captureGroups.length; i++) {
1388 const part = captureGroups[i];
1389 if (part === undefined) continue;
1390 const idx = raw.indexOf(part, lastIndex);
1391 result += raw.slice(lastIndex, idx);
1392 if (i === captureGroups.length - 1) {
1393 result += `${spanStart}${part}${spanEnd}`;
1394 } else {
1395 result += part;
1396 }
1397 lastIndex = idx + part.length;
1398 }
1399 result += raw.slice(lastIndex);
1400 return result;
1401 });
1402 }
1403
1404 // If explicit regex patterns are provided
1405 if (group.regex) {
1406 group.regex.forEach(r => {
1407 if (r.pattern) {
1408 const flags = r.flags || "g";
1409 const regex = new RegExp(r.pattern, flags);
1410 if (groupName === 'multilineComments') {
1411 mslText = mslText.replace(regex, (fullMatch) => {
1412 // Split the full match by lines
1413 const lines = fullMatch.split('\n');
1414 // Process each line individually
1415 const processedLines = lines.map(line => {
1416 return `${spanStart}${line}${spanEnd}`;
1417 });
1418 // Join back with newlines
1419 return processedLines.join('\n');
1420 });
1421 } else {
1422 mslText = mslText.replace(regex, `${spanStart}$&${spanEnd}`);
1423 }
1424 }
1425 });
1426 }
1427 }
1428
1429 // Split processed lines
1430 seqLines = mslText.split("\n");
1431
1432 // Firefox / Chrome handling
1433 const emptyClass = (typeof isChrome !== 'undefined' && isChrome === 1) ? "esline" : "";
1434
1435 for (let j = 0; j < seqLines_org.length; j++) {
1436 const line = seqLines_org[j];
1437 // Add span wrapper for editor tracking
1438 if (line === "") {
1439 const id = `sline${j + 1}`;
1440 seqLines[j] = `<span class='${emptyClass}' id='${id}'></span>`;
1441 } else {
1442 const id = `sline${j + 1}`;
1443 seqLines[j] = `<span class='sline' id='${id}'>${seqLines[j]}</span>`;
1444 }
1445 }
1446
1447 return seqLines;
1448}
1449
1450// Syntax highlight any text according to provided rules
1451function syntax_msl2(seqLines,keywordGroups) {
1452 // If not provided use the default msl keywords and groups
1453 if (!keywordGroups) {
1454 // take msl default
1455 keywordGroups = mslDefs.groups;
1456 }
1457
1458 // Make full text if you get an array of lines
1459 let mslText = Array.isArray(seqLines) ? seqLines.join("\n") : seqLines;
1460 // Make some cleanup of illegal characters
1461 mslText = escapeSpecialCharacters(mslText);
1462 // Keep original sequence lines (as array)
1463 let seqLines_org = mslText.split(/\n/);
1464 let nLines = (mslText.match(/\n/g) || []).length;
1465
1466 // These can be done on the text in one go
1467 // Strings
1468 let reg = /(["'])(.*?)\1/g;
1469 mslText = mslText.replace(reg,'<span class="msl_string">$1$2$1</span>');
1470
1471 // Comments
1472 //reg = /^(COMMENT|#.*?)(.*)$/gim;
1473 //mslText = mslText.replace(reg,'<span class="msl_comment">$&</span>');
1474
1475 // Variables
1476 reg = /(?:\$[\w]+|^\b\w+(?=\s*=))/gm; // starting with $ or something =
1477 mslText = mslText.replace(reg,'<span class="msl_variable">$&</span>');
1478 reg = new RegExp("(^(?:\\s*)\\b(" + keywordGroups.variables.keywords.join("|") + ")\\s+)(\\w+)\\b", "gim"); // after PARAM, CAT and SET
1479 mslText = mslText.replace(reg,'$1<span class="msl_variable">$3</span>');
1480
1481 // Data Management group excluding variables (must be after variables)
1482 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1483 mslText = mslText.replace(reg, "$1<span class='msl_data_management'>$2</span>");
1484
1485 // Data Type group (must have comma before the keyword)
1486 reg = new RegExp("(?<=,\\s*)\\b(" + keywordGroups.dataTypes.keywords.join("|") + ")\\b", "gim");
1487 mslText = mslText.replace(reg, "<span class='msl_data_types'>$1</span>");
1488
1489 // Loops group (must be at the begenning of the line)
1490 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.loops.keywords.join("|") + ")\\b", "gim");
1491 mslText = mslText.replace(reg, "$1<span class='msl_loops'>$2</span>");
1492
1493 // Control Flow group
1494 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.controlFlow.keywords.join("|") + ")\\b", "gim");
1495 mslText = mslText.replace(reg, "$1<span class='msl_control_flow'>$2</span>");
1496
1497 // Data Management group
1498 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.dataManagement.keywords.join("|") + ")\\b", "gim");
1499 mslText = mslText.replace(reg, "$1<span class='msl_data_managemen'>$2</span>");
1500
1501 // Information group
1502 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.info.keywords.join("|") + ")\\b", "gim");
1503 mslText = mslText.replace(reg, "$1<span class='msl_info'>$2</span>");
1504
1505 // Conditional group
1506 reg = new RegExp("^(\\s*)\\b(" + keywordGroups.cond.keywords.join("|") + ")\\b", "gim");
1507 mslText = mslText.replace(reg,"$1<span class='msl_cond'>$2</span>");
1508
1509 // Units group
1510 reg = new RegExp("\\b(" + keywordGroups.units.keywords.join("|") + ")\\b", "gi");
1511 mslText = mslText.replace(reg, "<span class='msl_units'>$1</span>");
1512
1513 // Action group
1514 reg = new RegExp("\\b(" + keywordGroups.actions.keywords.join("|") + ")\\b(\\s*)$", "gim");
1515 mslText = mslText.replace(reg, "<span class='msl_actions'>$1</span>$2");
1516
1517 // Numbers/boolean group
1518 reg = /\b(\d+(\.\d+)?([eE][-+]?\d+)?)\b/g;
1519 mslText = mslText.replace(reg, '<span class="msl_number">$1</span>');
1520 reg = new RegExp("\\b(" + keywordGroups.bool.keywords.join("|") + ")\\b", "gi");
1521 //reg = /\b(true|false)\b/gi;
1522 mslText = mslText.replace(reg, '<span class="msl_bool">$1</span>');
1523
1524 // Break lines and handle one by one
1525 seqLines = mslText.split("\n");
1526
1527 // This is important for Firefox
1528 let emptyClass = "";
1529 if (isChrome === 1) emptyClass = "esline";
1530 // Loop and restore comment lines and empty lines
1531 for (let j = 0; j < seqLines_org.length ; j++) {
1532 let line = seqLines_org[j];
1533 let inlineComment = mslDefs.groups.comments.keywords[0];
1534 commentIndex = line.indexOf(inlineComment);
1535 if (line.trim().startsWith(inlineComment) || line.trim().toLowerCase().startsWith("comment")) {
1536 // Restore comment lines without highlighting
1537 seqLines[j] = `<span class='msl_comment'>${line}</span>`;
1538 } else if (commentIndex > 0) {
1539 // Restore comment section at end of line
1540 const comment = line.slice(commentIndex);
1541 seqLines[j] = seqLines[j].slice(0, seqLines[j].indexOf("#")) + `</span><span class='msl_comment'>${comment}</span>`;
1542 }
1543
1544 // empty class is needed for cursor movement in Firefox
1545 // for Chrome empty lines are skipped with arrow up??
1546 if (line === "") {
1547 if ((j === seqLines_org.length - 1) && (isChrome === 1)) {
1548 seqLines[j] = "<span class=' ' id='sline" + (j+1).toString() + "'></span>";
1549 } else if (isChrome === 1) {
1550 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1551 } else {
1552 seqLines[j] = "<span class='" + emptyClass + "' id='sline" + (j+1).toString() + "'></span>";
1553 }
1554 } else {
1555 seqLines[j] = "<span class='sline' id='sline" + (j+1).toString() + "'>" + seqLines[j] + "</span>";
1556 }
1557 }
1558 return seqLines;
1559}
1560
1561// Adjust indentation of a selection of lines
1562function indent_msl(lines,addTab) {
1563 /* Arguments:
1564 lines - an array of two elements, first and last line numbers
1565 addTab - (opt) +/-1 to add/subtract three spaces to selected lines
1566 */
1567 let indentLevel = 0;
1568 let singleLine = false;
1569 let editor = editorElements()[1];
1570 // Indentation keywords
1571 const defIndent = mslDefs.defIndent;
1572 const nIndent = defIndent.nIndent || 3;
1573
1574 // Avoid issues of begenning of single line
1575 if (lines[0] > lines[1] || lines[0] == lines[1]) {
1576 lines[0] = lines[0] > 0 ? lines[0] : 1;
1577 lines[1] = lines[0];
1578 singleLine = true;
1579 }
1580 for (let j = lines[0]; j <= lines[1] ; j++) {
1581 let lineId = "#sline" + j.toString();
1582 let prevLineId = "#sline" + (j-1).toString();
1583 let lineEl = editor.querySelector(lineId);
1584 let line = "";
1585 if (lineEl) line = lineEl.innerText;
1586 if (addTab === 1) {
1587 let indentString = " ".repeat(nIndent);
1588 lineEl.innerText = indentString + line;
1589 } else if (addTab === -1) {
1590 lineEl.innerText = line.replace(/^\s{1,nIndent}/, '');
1591 } else if (singleLine && editor.querySelector(prevLineId)) {
1592 let prevLineEl = editor.querySelector(prevLineId);
1593 let prevLine = prevLineEl.innerText;
1594 const indentMinos = defIndent.indentminos.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1595 const indentPlus = defIndent.indentplus.some(keyword => prevLine.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1596 if (indentMinos) {
1597 indentLevel = -1;
1598 } else if (indentPlus) {
1599 indentLevel = 1;
1600 }
1601 let preSpace = prevLine.search(/\S|$/) + (indentLevel * nIndent);
1602 if (preSpace < 0) preSpace = 0;
1603 let indentString = " ".repeat(preSpace);
1604 lineEl.innerText = indentString + line.trimStart();
1605 } else {
1606 const indentMinos = defIndent.indentminos.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1607 if (indentMinos && indentLevel > 0) indentLevel--;
1608 let indentString = " ".repeat(indentLevel * nIndent);
1609 if (line !== "" || indentString !== "") {
1610 lineEl.innerText = indentString + line.trimStart();
1611 }
1612 const indentPlus = defIndent.indentplus.some(keyword => line.trim().toLowerCase().startsWith(keyword.toLowerCase()));
1613 if (indentPlus) indentLevel++;
1614 }
1615 }
1616}
1617
1618// Prepare the parameters/variables (if present) from the ODB as an html table
1619// Also return default ODB Paths and their respective values
1620function varTable(id,odbTreeVar) {
1621 /* Arguments:
1622 id - ID of div to fill with the table of variables
1623 odbTreeVar - values of ${SeqODB}/Variables
1624 */
1625
1626 let e = document.getElementById(id);
1627 if (e === null) {
1628 dlgAlert("Container ID was not give.");
1629 return;
1630 }
1631
1632 let old_var_names = [];
1633 let new_var_names = [];
1634 document.querySelectorAll(".varname").forEach(el => old_var_names.push(el.innerText));
1635
1636 // If ${SeqODB}/Variables are empty return empty
1637 if (!odbTreeVar || (odbTreeVar && Object.keys(odbTreeVar).length === 0)) {
1638 // Clear container row
1639 e.innerHTML = "";
1640 return;
1641 }
1642
1643 let html = "<table id='varTable' class='mtable infotable'>\n";
1644 html += "<tr><th style='min-width: 120px'>Variable&nbsp;&nbsp;</th><th>Current value&nbsp;&nbsp;</th></tr>\n";
1645
1646 // Go over all variables in ODB and count them
1647 for (let key in odbTreeVar) {
1648 const match = key.match(/([^/]+)\/name$/);
1649 if (match) {
1650 const name = match[1];
1651 const value = odbTreeVar[name];
1652 new_var_names.push(name);
1653 let isBool = (typeof(value) === "boolean");
1654 if (isBool) {
1655 html += `<tr><td class="varname">${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="${SeqODB}/Variables/${name}"></span></td></tr>\n`;
1656 } else {
1657 html += `<tr><td class="varname">${name}</td><td><span class="modbvalue" data-odb-path="${SeqODB}/Variables/${name}"></span></td></tr>\n`;
1658 }
1659 }
1660 }
1661 html += "</table>";
1662 let have_new_var = false;
1663 if (old_var_names.length !== new_var_names.length) {
1664 have_new_var = true;
1665 } else {
1666 for (let i = 0; i < old_var_names.length; i++) {
1667 if (old_var_names[i] != new_var_names[i]) {
1668 have_new_var = true;
1669 }
1670 }
1671 }
1672 if (have_new_var) {
1673 e.innerHTML = html;
1674 }
1675 return;
1676}
1677
1678// Prepare the parameters/variables (if present) from the ODB as an object with
1679// html table, default ODB Paths, their respective values and their number
1680function parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq) {
1681 /* Arguments:
1682 odbTree... - Objects of ODB values
1683 */
1684 let html = "";
1685 let odbDefPaths = [];
1686 let odbDefValues = [];
1687 let NValues = 0;
1688
1689 html += "<table id='paramTable' class='mtable infotable'>";
1690 html += "<tr><th>Parameter&nbsp;&nbsp;</th><th>Initial value&nbsp;&nbsp;</th><th>Comment</th></tr>";
1691
1692 const processParam = (name, value, isBool, defValue, optValue, comment) => {
1693 let parLine = `<tr><td>${name}</td>`;
1694 let inParLine = "";
1695 if (optValue) {
1696 // if not given the default is the first option
1697 if (defValue === undefined || defValue === "") defValue = optValue[0];
1698 const optionsHtml = optValue.map(option => `<option value="${option}" ${option === defValue ? 'selected' : ''}>${option}</option>`).join('');
1699 inParLine += `<select class="modbselect" data-odb-path="${SeqODB}/Param/Value/${name}" data-odb-editable="1">${optionsHtml}</select>`;
1700 } else if (isBool) {
1701 inParLine += `<input type="checkbox" class="modbcheckbox" data-odb-path="${SeqODB}/Param/Value/${name}" data-odb-editable="1"></input>`;
1702 } else {
1703 inParLine += `<span class="modbvalue" data-odb-path="${SeqODB}/Param/Value/${name}" data-odb-editable="1" data-input="1"></span>`;
1704 }
1705 if (defValue !== undefined) {
1706 NValues++;
1707 odbDefPaths.push(`${SeqODB}/Param/Value/${name}`);
1708 odbDefValues.push(defValue);
1709 }
1710
1711 parLine += `<td>${inParLine}</td>`;
1712 parLine += `<td>${comment}</td></tr>`;
1713 html += parLine;
1714 };
1715
1716 // Go over all parameters in ODB
1717 for (let key in odbTreeV) {
1718 const match = key.match(/([^/]+)\/name$/);
1719 if (match) {
1720 const name = match[1];
1721 // if variable is found use its value
1722 let value = (odbTreeVar && odbTreeVar[name]) ? odbTreeVar[name] : odbTreeV[name];
1723 let isBool = (typeof(value) === "boolean");
1724 let defValue = (value !== null && value !== undefined && value !== '') ? value : (odbTreeD && odbTreeD[name]) || value;
1725 let optValue = odbTreeO ? odbTreeO[name] : undefined;
1726 let comment = odbTreeC[name] || '';
1727 if (typeof value !== "object") {
1728 processParam(name, value, isBool, defValue, optValue, comment);
1729 }
1730 }
1731 }
1732
1733 // Go over Edit on sequence links
1734 for (let key in editOnSeq) {
1735 const match = key.match(/([^/]+)\/name$/);
1736 if (match) {
1737 const name = match[1];
1738 const value = editOnSeq[name];
1739 let isBool = (typeof(value) === "boolean");
1740 if (isBool) {
1741 html += `<tr><td>${name}</td><td><input type="checkbox" class="modbcheckbox" data-odb-path="/Experiment/Edit on sequence/${name}" data-odb-editable="1"></input></td><td></td></tr>\n`;
1742 } else {
1743 html += `<tr><td>${name}</td><td><span class="modbvalue" data-odb-path="/Experiment/Edit on sequence/${name}" data-odb-editable="1" data-input="1"></span></td><td></td></tr>\n`;
1744 }
1745 }
1746 }
1747
1748 html += "</table>";
1749 return {
1750 html: html,
1751 paths: odbDefPaths,
1752 values: odbDefValues,
1753 NValues: NValues
1754 };
1755}
1756
1757function dlgParam(debugFlag) {
1758 /* Arguments:
1759 debugFlag - (opt) true/false run in debug/normal mode
1760 */
1761
1762 // Ensure we read the latest Params from the ODB, in case we just loaded a new file
1763 let paths = [`${SeqODB}/Param`, `${SeqODB}/Variables`];
1764
1765 mjsonrpc_db_get_values(paths).then(function (rpc) {
1766 // Try to use the very latest values from ODB when building
1767 // the parameters table.
1768 // `dlgParamMain()` reads from sessionStorage, so we need to write
1769 // the latest values there.
1770 // Normally sessionStorage is updated by `mhttpd_refresh()` (when it
1771 // updates "modbvalue" spans in the HTML), but we don't want to hang
1772 // around waiting for the next update to happen, so we just update it
1773 // ourselves.
1774 //console.log(rpc);
1775 const status = rpc?.result?.status ?? [];
1776 const data = rpc?.result?.data ?? [];
1777 // parameters
1778 if (status[0] === 1) {
1779 sessionStorage.setItem("parameters", JSON.stringify(data[0]));
1780 }
1781 // variables
1782 if (status[1] === 1) {
1783 sessionStorage.setItem("variables", JSON.stringify(data[1]));
1784 }
1785 dlgParamMain(debugFlag);
1786 }).catch(function (error) {
1787 console.error(error);
1788 });
1789}
1790
1791// Prepare the parameters/variables (if present) from the ODB as a table
1792function dlgParamMain(debugFlag) {
1793 /* Arguments:
1794 debugFlag - (opt) true/false run in debug/normal mode
1795 */
1796
1797 let odbTree = JSON.parse(sessionStorage.getItem('parameters') !== 'undefined' ? sessionStorage.getItem('parameters') : "null");
1798 const editOnSeq = JSON.parse(sessionStorage.getItem('editonseq') !== 'undefined' ? sessionStorage.getItem('editonseq') : "null");
1799 // If ${SeqODB}/Param are empty, start and return
1800 if ((odbTree === null || Object.keys(odbTree.value).length === 0) && editOnSeq === null ) {
1801 //if ((odbTree === null || Object.keys(odbTree).length) && (editOnSeq === null || Object.keys(editOnSeq).length)) {
1802 if (debugFlag) {
1803 modbset(`${SeqODB}/Command/Debug script`,true);
1804 } else {
1805 modbset(`${SeqODB}/Command/Start script`,true);
1806 }
1807 return;
1808 }
1809
1810 let odbTreeV = null;
1811 let odbTreeC = null;
1812 let odbTreeD = null;
1813 let odbTreeO = null;
1814 let odbTreeVar = null;
1815
1816 if (odbTree) {
1817 odbTreeV = odbTree.value;
1818 odbTreeC = odbTree.comment;
1819 odbTreeD = odbTree.defaults;
1820 odbTreeO = odbTree.options;
1821 odbTreeVar = JSON.parse(sessionStorage.getItem('variables'));
1822 }
1823
1824 // Go over all parameters in ODB
1825 let seqParTable = parTable(odbTreeV,odbTreeC,odbTreeD,odbTreeO,odbTreeVar,editOnSeq);
1826 let html = seqParTable.html;
1827 // set all default values and once finished produce dialog
1828 // Collect paths where values start with "/"
1829 let valuesLinkODB = seqParTable.values.map((valueLinkODB, indexLinkODB) => {
1830 if (valueLinkODB !== undefined && typeof(valueLinkODB) === "string") {
1831 if (valueLinkODB.startsWith("/")) {
1832 return seqParTable.values[indexLinkODB];
1833 }
1834 }
1835 return null;
1836 }).filter(path => path !== null);
1837 mjsonrpc_db_get_values(valuesLinkODB).then(function (rpc) {
1838 if (rpc.result.status.every(status => status === 1)) {
1839 // substitute values
1840 rpc.result.data.forEach((newData, index) => {
1841 let pathIndex = seqParTable.values.indexOf(valuesLinkODB[index]);
1842 seqParTable.values[pathIndex] = `${newData}`; // Update corresponding value
1843 });
1844 mjsonrpc_db_paste(seqParTable.paths,seqParTable.values).then(function (rpc) {
1845 if ((rpc.result.status.every(status => status === 1)) || seqParTable.values.length === 0) {
1846 // if parContainer not given produce a dialog
1847 let htmlDlg = `${html}<br><button class="dlgButtonDefault" id="dlgParamStart" type="button">Start</button><button class="dlgButton" id="dlgParamCancel" type="button">Cancel</button><br>`;
1848 let d = dlgGeneral({html: htmlDlg,iddiv: "Parameters",minWidth:500});
1849 let e = document.getElementById("parContainer");
1850 // Append the table to a container
1851 let startBtn = document.getElementById("dlgParamStart");
1852 let cancelBtn = document.getElementById("dlgParamCancel");
1853 cancelBtn.addEventListener("click", function () {d.remove();});
1854 startBtn.addEventListener("click", function () {
1855 d.remove();
1856 if (debugFlag) {
1857 modbset(`${SeqODB}/Command/Debug script`,true);
1858 } else {
1859 modbset(`${SeqODB}/Command/Start script`,true);
1860 }
1861 });
1862
1863 // refresh immediately modbvalue elements
1864 mhttpd_refresh();
1865 } else {
1866 dlgAlert("Something went wrong. Please try again!");
1867 }
1868 }).catch(function (error) {
1869 console.error(error);
1870 });
1871 } else {
1872 let message = `ODB "${valuesLinkODB}" was not found.<br>Cannot start sequence!`;
1873 dlgAlert(message);
1874 }
1875 }).catch(function (error) {
1876 console.error(error);
1877 });
1878}
1879
1880
1881// helper debug function
1882function debugSeq(parContainer) {
1883 startSeq(parContainer,true);
1884}
1885
1886// helper start function
1887function startSeq(parContainer,debugFlag) {
1888 const [lineNumbers,editor,label] = editorElements();
1889 if (!debugFlag) debugFlag = false;
1890 if (editor.id !== "editorTab1" && parContainer === undefined) {
1891 let filename = label.title;
1892 if (!filename) {
1893 dlgAlert("Please give the file a name first (Save as).");
1894 return;
1895 }
1896 const message = debugFlag ? `Save and debug ${filename}?` : `Save and start ${filename}?`;
1897 dlgConfirm(message,function(resp) {
1898 if (resp) {
1899 seqSave(filename);
1900 openETab(document.getElementById("etab1-btn"));
1901 seqOpen(filename);
1902 // Make sure to load file and reset parameters
1903 mjsonrpc_db_paste([`${SeqODB}/Command/Load new file`],[true]).then(function (rpc) {
1904 if (rpc.result.status[0] === 1) {
1905 // Wait for Load new file to turn false
1906 checkODBValue(`${SeqODB}/Command/Load new file`,false,dlgParam,debugFlag);
1907 }
1908 });
1909 }
1910 });
1911 } else {
1912 // make sure to load file first
1913 mjsonrpc_db_paste([`${SeqODB}/Command/Load new file`],[true]).then(function (rpc) {
1914 if (rpc.result.status[0] === 1) {
1915 // Wait for Load new file to turn false
1916 checkODBValue(`${SeqODB}/Command/Load new file`,false,dlgParam,debugFlag);
1917 }
1918 });
1919 }
1920}
1921
1922// Helper function to add the current file to next files queue
1923function setAsNext() {
1924 const [lineNumbers,editor,label] = editorElements();
1925 // This is the addAsNext button cell
1926 const e = document.getElementById("addAsNext");
1927 let filename = label.title.split("\n")[0].replace(/^sequencer\//,'');
1928 let message = `Save and put ${filename} in the next file queue?`;
1929 dlgConfirm(message,function(resp) {
1930 if (resp) {
1931 let order = chngNextFilename(e,filename);
1932 if (order !== -1 && order !== undefined) {
1933 seqSave(filename);
1934 dlgAlert(`File saved and placed in position ${order} in the queue.`);
1935 }
1936 }
1937 });
1938}
1939
1940// helper stop function
1941function stopSeq() {
1942 const message = `Are you sure you want to stop the sequence?`;
1943 dlgConfirm(message,function(resp) {
1944 if (resp) {
1945 modbset(`${SeqODB}/Command/Stop immediately`,true);
1946 }
1947 });
1948}
1949
1950// Show or hide parameters table
1951function showParTable(varContainer) {
1952 let e = document.getElementById(varContainer);
1953 let varTable = document.getElementById("varTable");
1954 let vis = document.getElementById("showParTable").checked;
1955 let visNF = (document.getElementById("nextFNContainer").style.display === "none");
1956 if (vis) {
1957 e.style.display = "flex";
1958 e.parentElement.style.width = "285px";
1959 } else {
1960 e.style.display = "none";
1961 if (visNF) {
1962 e.parentElement.style.width = "0px";
1963 }
1964 }
1965 windowResize();
1966}
1967
1968// Show or hide next file list
1969function showFNTable(nextFNContainer) {
1970 let e = document.getElementById(nextFNContainer);
1971 let vis = document.getElementById("showNextFile").checked;
1972 let visVar = document.getElementById("varContainer") ? (document.getElementById("varContainer").style.display === "none") : false;
1973 let html = "";
1974 let addFileRow = "";
1975 let nFiles = 0;
1976 let odbpath = mslDefs.filename.next;
1977 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
1978 if (rpc.result.status[0] !== 1) return;
1979 let fList_org = rpc.result.data[0];
1980 // Move full slots to the begenning of the array
1981 fList = fList_org.filter(Boolean);
1982 fList = [...fList, ...new Array(10 - fList.length).fill("")];
1983 // If the cleaned up not equal to original update ODB
1984 if (fList !== fList_org) {
1985 mjsonrpc_db_paste([odbpath],[fList]).then(function (rpc2) {
1986 if (rpc2.result.status[0] !== 1) {
1987 dlgAlert(`${odbpath} needs manual cleanup!<br>Please check.`);
1988 }
1989 }).catch(function (error) {
1990 console.error(error);
1991 });
1992 }
1993 for (let i = 0; i < fList.length; i++) {
1994 if (fList[i] && fList[i].trim() !== "") {
1995 html += `<tr class="dragrow" draggable="true"><td style="cursor: all-scroll;"><img draggable="false" style="cursor: all-scroll;" src="icons/menu.svg" title="Drag and drop to reorder"></td><td><img draggable="false" src="icons/folder-open.svg" title="Change file" onclick="chngNextFilename(this.parentElement);"></td><td style="cursor: all-scroll;" title="Drag and drop to reorder" ondblclick="chngNextFilename(this);">${fList[i]}</td><td><img draggable="false" title="Remove file from list" onclick="remNextFilename(this.parentElement);" src="icons/trash-2.svg"></td></tr>`;
1996 nFiles++;
1997 }
1998 }
1999 if (vis && html !== "") {
2000 e.style.display = "flex";
2001 e.parentElement.style.width = "285px";
2002 if (nFiles < 10) {
2003 disableMenuItems("SetAsNext",false);
2004 addFileRow = `<tr><td id="addAsNext"><img src="icons/file-plus.svg" title="Add file" onclick="chngNextFilename(this.parentElement);"></td><td></td><td></td><td></td></tr>`;
2005 } else {
2006 disableMenuItems("SetAsNext",true);
2007 }
2008 e.innerHTML = `<table class="mtable infotable"><tr><th style="width:10px;"></th><th style="width:10px;"></th><th>Next files</th><th style="width:10px;"></th></tr>${html}${addFileRow}</table>`;
2009 windowResize();
2010 } else {
2011 e.style.display = "none";
2012 if (visVar) {
2013 e.parentElement.style.width = "0px";
2014 windowResize();
2015 }
2016 }
2017 activateDragDrop(e);
2018 }).catch (function (error) {
2019 console.error(error);
2020 });
2021}
2022
2023// Show extra rows for wait and loop
2024function extraRows(e) {
2025 /* Arguments:
2026 e - triggering element to identify wait or loop
2027 */
2028 // get current row, table and dialog
2029 let rIndex = e.parentElement.parentElement.rowIndex;
2030 let table = e.parentElement.parentElement.parentElement;
2031 let progressDlg = table.parentElement.parentElement.parentElement.parentElement.parentElement;
2032 // check if there is a wait or loop commands (if non-zero)
2033 if (e.value) {
2034 showProgress();
2035 if (e.id === "waitTrig") {
2036 // Make sure there is only one wait row
2037 const waitTRs = document.querySelectorAll('.waitTR');
2038 const waitFormula = (e.value === "Seconds" || e.value === "ClientsRunning" || e.value === "ODBValueStability") ? 'data-formula="x/1000"' : '';
2039 if (waitTRs.length) waitTRs.forEach(element => element.remove());
2040 // Insert a new row
2041 let tr = table.insertRow(rIndex+1);
2042 tr.className = "waitTR";
2043 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">&nbsp;
2044 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: lightgreen;" data-odb-path="${SeqODB}/State/Wait value" ${waitFormula} id="mwaitProgress"></span>
2045 <span class="waitlooptxt">
2046 Wait: [<span class="modbvalue" data-odb-path="${SeqODB}/State/Wait value" ${waitFormula}></span>/<span class="modbvalue" data-odb-path="${SeqODB}/State/Wait limit" ${waitFormula} onchange="if (document.getElementById('mwaitProgress')) document.getElementById('mwaitProgress').dataset.maxValue = this.value;"></span>] <span class="modbvalue" data-odb-path="${SeqODB}/State/Wait type"></span>
2047 </span>
2048 </td>`;
2049 windowResize();
2050 } else if (e.id === "loopTrig") {
2051 mjsonrpc_db_get_values([`${SeqODB}/State/Loop n`]).then(function(rpc) {
2052 let loopArray = rpc.result.data[0];
2053 for (let i = 0; i < loopArray.length; i++) {
2054 if (loopArray[i] === 0) break;
2055 let tr = table.insertRow(rIndex+1);
2056 tr.className = "loopTR";
2057 tr.innerHTML = `<td style="position: relative;width:80%;" colspan="2">&nbsp;
2058 <span class="modbhbar waitloopbar" style="position: absolute; top: 0; left: 0; color: #CBC3E3;" data-odb-path="${SeqODB}/State/Loop counter[${i}]" id="mloopProgress${i}"></span>
2059 <span class="waitlooptxt">
2060 Loop ${i}: [<span class="modbvalue" data-odb-path="${SeqODB}/State/Loop counter[${i}]"></span>/<span class="modbvalue" data-odb-path="${SeqODB}/State/Loop n[${i}]" onchange="if (document.getElementById('mloopProgress${i}')) document.getElementById('mloopProgress${i}').dataset.maxValue = this.value;"></span>]
2061 </span>
2062 </td>`;
2063 windowResize();
2064 }
2065 }).catch (function (error) {
2066 console.error(error);
2067 });
2068 }
2069 } else {
2070 // remove rows
2071 if (e.id === "waitTrig") document.querySelectorAll('.waitTR').forEach(element => element.remove());
2072 if (e.id === "loopTrig") document.querySelectorAll('.loopTR').forEach(element => element.remove());
2073 // hide progress div
2074 //dlgHide(progressDlg);
2075 windowResize();
2076 }
2077}
2078
2079// Helper function to identify browser, 1 FF, 2 Chrome, 3, other
2080function browserType() {
2081 if (navigator.userAgent.indexOf("Chrome") !== -1) {
2082 return 1;
2083 } else if (navigator.userAgent.indexOf("Firefox") !== -1) {
2084 return 2;
2085 }
2086
2087 return 1;
2088}
2089
2090// make visual hint that file is changes
2091function seqIsChanged(flag) {
2092 // flag - true is change, false is saved
2093 let fileChangedId = "filechanged" + editorElements()[0].id.replace("lineNumbers","");
2094 let filechanged = document.getElementById(fileChangedId);
2095 if (filechanged) {
2096 if (flag) {
2097 filechanged.innerHTML = "&nbsp;&#9998;";
2098 } else if (flag === undefined) {
2099 // true if text has changed false if not
2100 return (filechanged.innerHTML !== "");
2101 } else {
2102 filechanged.innerHTML = "";
2103 }
2104 }
2105 // also change validation icon to defaule
2106 let vldBtn = document.getElementById("vldBtn");
2107 vldBtn.src = "icons/validate-syntax.svg";
2108 vldBtn.style.backgroundColor = "";
2109}
2110
2111// save history of edits in element editor
2112function saveState(mslText,editor) {
2113 editor = (editor) ? editor : editorElements()[1];
2114 const editorId = editor.id;
2115 if (saveRevision[editorId] === false) {
2116 return;
2117 } else if (saveRevision[editorId] === undefined){
2118 saveRevision[editorId] = true;
2119 }
2120
2121 if (!previousRevisions[editorId]) {
2122 previousRevisions[editorId] = [];
2123 revisionIndex[editorId] = -1;
2124 }
2125
2126 // Add one more revision, and trim array if we had some undos
2127 revisionIndex[editorId]++;
2128 if (revisionIndex[editorId] < previousRevisions[editorId].length - 1) {
2129 previousRevisions[editorId].splice(revisionIndex[editorId] + 1);
2130 }
2131 // Push new revision and keep only nRevisions revisions
2132 previousRevisions[editorId].push(mslText)
2133 if (previousRevisions[editorId].length > nRevisions) {
2134 previousRevisions[editorId].shift();
2135 }
2136 revisionIndex[editorId] = previousRevisions[editorId].length - 1;
2137}
2138
2139// undo last edit
2140function undoEdit(editor) {
2141 editor = (editor) ? editor : editorElements()[1];
2142 const editorId = editor.id;
2143 if (revisionIndex[editorId] === 0) {
2144 // disable menu item
2145 disableMenuItems("undoMenu",true);
2146 seqIsChanged(false);
2147 return;
2148 } else {
2149 // enable menu item
2150 disableMenuItems("undoMenu",false);
2151 revisionIndex[editorId]--;
2152 }
2153 // reset redo
2154 disableMenuItems("redoMenu",false);
2155
2156 let caretPos = getCurrentCursorPosition(editor);
2157 let currText = editor.innerText;
2158 saveRevision[editorId] = false;
2159 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
2160 updateLineNumbers(editor.previousElementSibling,editor);
2161 saveRevision[editorId] = true;
2162 // calculate change in caret position based on length
2163 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
2164 setCurrentCursorPosition(editor, caretPos);
2165}
2166
2167// redo the undo
2168function redoEdit(editor) {
2169 editor = (editor) ? editor : editorElements()[1];
2170 const editorId = editor.id;
2171 if (revisionIndex[editorId] >= previousRevisions[editorId].length - 1) {
2172 // disable menu item
2173 disableMenuItems("redoMenu",true);
2174 return;
2175 } else {
2176 // enable menu item
2177 disableMenuItems("redoMenu",false);
2178 revisionIndex[editorId]++;
2179 }
2180 // reset undo
2181 disableMenuItems("undoMenu",false);
2182 seqIsChanged(true);
2183
2184 let caretPos = getCurrentCursorPosition(editor);
2185 let currText = editor.innerText;
2186 saveRevision[editorId] = false;
2187 editor.innerHTML = syntax_msl(previousRevisions[editorId][revisionIndex[editorId]]).join(lc).slice(0,-1);
2188 updateLineNumbers(editor.previousElementSibling,editor);
2189 saveRevision[editorId] = true;
2190 // calculate change in caret position based on length
2191 caretPos = caretPos + previousRevisions[editorId][revisionIndex[editorId]].length - currText.length;
2192 setCurrentCursorPosition(editor, caretPos);
2193}
2194
2195// Select slines from startLine to endLine
2196function selectLines([startLine, endLine],e) {
2197 const selection = window.getSelection();
2198 // Remove existing selections
2199 selection.removeAllRanges();
2200 let startElementId = '#sline' + startLine;
2201 let endElementId = '#sline' + endLine;
2202 let startElement = null, endElement = null;
2203 if (e.querySelector(startElementId)) startElement = e.querySelector(startElementId).firstChild;
2204 if (e.querySelector(endElementId)) endElement = e.querySelector(endElementId).lastChild;
2205 // we need startElement and endElement with first/lastChild
2206 // the following prevents loosing selection but not ideal
2207 while (startElement === null && startLine <= endLine) {
2208 startLine++;
2209 startElementId = '#sline' + startLine;
2210 startElement = e.querySelector(startElementId).firstChild;
2211 }
2212 while (endElement === null && endLine > 0) {
2213 endLine--;
2214 endElementId = '#sline' + endLine;
2215 endElement = e.querySelector(endElementId).lastChild;
2216 }
2217 if (startElement && endElement) {
2218 const range = document.createRange();
2219 // Set the start of the range to the startElement at offset 0
2220 range.setStart(startElement, 0);
2221 // Set the end of the range to the endElement at its length
2222 range.setEnd(endElement, endElement.childNodes.length);
2223 // Add the range to the selection
2224 selection.addRange(range);
2225 }
2226}
2227
2228// switch between dark and light modes on request
2229function lightToDark(lToDcheck) {
2230 if (!lToDcheck) return;
2231 const edt_areas = document.querySelectorAll('.edt_area');
2232 if (lToDcheck.checked) {
2233 localStorage.setItem("darkMode", true);
2234 edt_areas.forEach(area => {
2235 area.style.backgroundColor = "black";
2236 area.style.color = "white";
2237 });
2238 updateCSSRule(".etab button:hover","background-color","black");
2239 updateCSSRule(".etab button:hover","color","white");
2240 updateCSSRule(".etab button:hover","border-bottom","5px solid black");
2241 updateCSSRule(".etab button.edt_active","background-color","black");
2242 updateCSSRule(".etab button.edt_active","color","white");
2243 updateCSSRule(".etab button.edt_active","border-bottom","5px solid black");
2244 updateCSSRule(".etab button.edt_active:hover","background-color","black");
2245 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid black");
2246 } else {
2247 localStorage.removeItem("darkMode");
2248 edt_areas.forEach(area => {
2249 area.style.backgroundColor = "white";
2250 area.style.color = "black";
2251 });
2252 updateCSSRule(".etab button:hover","background-color","white");
2253 updateCSSRule(".etab button:hover","color","black");
2254 updateCSSRule(".etab button:hover","border-bottom","5px solid white");
2255 updateCSSRule(".etab button.edt_active","background-color","white");
2256 updateCSSRule(".etab button.edt_active","color","black");
2257 updateCSSRule(".etab button.edt_active","border-bottom","5px solid white");
2258 updateCSSRule(".etab button.edt_active:hover","background-color","white");
2259 updateCSSRule(".etab button.edt_active:hover","border-bottom","5px solid white");
2260 }
2261}
2262
2263function updateCSSRule(selector, property, value) {
2264 for (let i = 0; i < document.styleSheets.length; i++) {
2265 let styleSheet = document.styleSheets[i];
2266 let rules = styleSheet.cssRules || styleSheet.rules;
2267 if (!rules) continue;
2268 for (let j = 0; j < rules.length; j++) {
2269 let rule = rules[j];
2270 if (rule.selectorText === selector) {
2271 rule.style[property] = value;
2272 return;
2273 }
2274 }
2275 }
2276}
2277
2278// show/hide wait and loop progress
2279function showProgress(e) {
2280 //const progressDiv = document.getElementById("progressDiv");
2281 const progressDiv = document.getElementById("Progress");
2282 if (e === undefined) e = document.getElementById("showProgressBars");
2283 if (e.checked) {
2284 localStorage.setItem("showProgress",true);
2285 progressDiv.style.display = "block";
2286 //dlgShow(progressDiv);
2287 } else {
2288 localStorage.removeItem("showProgress");
2289 progressDiv.style.display = "none";
2290 //dlgHide(progressDiv);
2291 }
2292}
2293
2294// Mark the current line number
2295function markCurrLineNum() {
2296 const [lineNumbers,editor] = editorElements();
2297 const currLines = lineNumbers.querySelectorAll(".edt_linenum_curr");
2298 const [startLine,endLine] = getLinesInSelection(editor);
2299 if (startLine === 0 && endLine === 0) return;
2300 currLines.forEach((line) => line.classList.remove("edt_linenum_curr"));
2301 for (let i = startLine; i <= endLine; i++) {
2302 let lineNumId = "#lNum" + i.toString();
2303 let lineNum = lineNumbers.querySelector(lineNumId);
2304 if (lineNum)
2305 lineNum.className = "edt_linenum_curr";
2306 }
2307}
2308
2309
2310// Check if program is progName is running.
2311// If not, try to get it going
2312function checkProgram(progName = "Sequencer") {
2313 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc1) {
2314 mjsonrpc_db_get_values([`/Programs/${progName}/Start command`]).then(function(rpc2) {
2315 let isRunning = (rpc1.result.status === 1);
2316 let isDefined = ((rpc2.result.data[0] !== null) && (rpc2.result.data[0] !== ""));
2317 if (isRunning && isDefined) return;
2318 // progName is not running or not defined, stop it just in case and check the reason
2319 mjsonrpc_stop_program(progName);
2320 let message = "";
2321 if (isDefined) {
2322 message = `${progName} program is not running.<br>Should I start it?`
2323 } else {
2324 message = `${progName} program is not configured and not running.<br>Should I try to start it anyway?`
2325 }
2326 dlgConfirm(message,function(resp) {
2327 // Guess the name of the actual binary
2328 let binName = (progName === "Sequencer") ? "msequencer" : progName.toLowerCase();
2329 if (resp) {
2330 if (!isDefined) {
2331 // assume that progName is in path and create a start command, sleep 2s,
2332 // set value to "progName -D", sleep 2s, start program
2333 mjsonrpc_db_create([{"path" : `/Programs/${progName}/Start command`, "type" : TID_STRING}]).then(function (rpc3) {
2334 setTimeout(function(){
2335 mjsonrpc_db_paste([`/Programs/${progName}/Start command`],[`${binName} -D`]).then(function (rpc4) {
2336 if (rpc4.result.status[0] === 1) {
2337 mjsonrpc_start_program(progName);
2338 }
2339 }).catch(function (error) {
2340 console.error(error);
2341 });
2342 }, 2000);
2343 }).catch(function (error) {
2344 console.error(error);
2345 });
2346 } else {
2347 mjsonrpc_start_program(progName);
2348 }
2349 // take 3 seconds and check that it actually started
2350 setTimeout(function(){
2351 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc5) {
2352 if (rpc5.result.status === 1) {
2353 dlgAlert(`${progName} started successfully.`);
2354 } else {
2355 dlgAlert(`Failed to start ${progName}!<br>Try to start it manually (${binName} -D)`);
2356 }
2357 });
2358 }, 3000);
2359 }
2360 });
2361 }).catch (function (error) {
2362 console.error(error);
2363 });
2364 }).catch(function (error) {
2365 console.error(error);
2366 });
2367}
2368
2369// Python programs seem to be different from standard midas - to be checked
2370function checkPyProgram(progName = "PySequencer") {
2371 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc1) {
2372 mjsonrpc_db_get_values([`/Programs/${progName}/Start command`]).then(function(rpc2) {
2373 let isRunning = (rpc1.result.status === 1);
2374 let isDefined = ((rpc2.result.data[0] !== null) && (rpc2.result.data[0] !== ""));
2375 if (isRunning && isDefined) return;
2376 // progName is not running or not defined, stop it just in case and check the reason
2377 mjsonrpc_stop_program(progName);
2378 let message = "";
2379 if (isDefined) {
2380 message = `${progName} program is not running.<br>Should I start it?`
2381 } else {
2382 message = `${progName} program is not configured and not running.<br>Should I try to start it anyway?`
2383 }
2384 dlgConfirm(message,function(resp) {
2385 // Guess the name of the actual binary
2386 let binName = (progName === "PySequencer") ? "python3 $MIDASSYS/python/midas/sequencer.py" : "python3 $MIDASSYS/python/midas/" + progName.toLowerCase() + ".py";
2387 if (resp) {
2388 if (!isDefined) {
2389 // assume that progName is in path and create a start command, sleep 2s,
2390 // set value to "progName -D", sleep 2s, start program
2391 mjsonrpc_db_create([{"path" : `/Programs/${progName}/Start command`, "type" : TID_STRING}]).then(function (rpc3) {
2392 setTimeout(function(){
2393 mjsonrpc_db_paste([`/Programs/${progName}/Start command`],[`${binName} -D`]).then(function (rpc4) {
2394 if (rpc4.result.status[0] === 1) {
2395 mjsonrpc_start_program(progName);
2396 }
2397 }).catch(function (error) {
2398 console.error(error);
2399 });
2400 }, 2000);
2401 }).catch(function (error) {
2402 console.error(error);
2403 });
2404 } else {
2405 mjsonrpc_start_program(progName);
2406 }
2407 // take 3 seconds and check that it actually started
2408 setTimeout(function(){
2409 mjsonrpc_call('cm_exist', {"name":progName,"unique":true}).then(function (rpc5) {
2410 if (rpc5.result.status === 1) {
2411 dlgAlert(`${progName} started successfully.`);
2412 } else {
2413 dlgAlert(`Failed to start ${progName}!<br>Try to start it manually (${binName} -D)`);
2414 }
2415 });
2416 }, 3000);
2417 }
2418 });
2419 }).catch (function (error) {
2420 console.error(error);
2421 });
2422 }).catch(function (error) {
2423 console.error(error);
2424 });
2425}
2426
2427function captureSelected() {
2428 if (window.getSelection) {
2429 let selection = window.getSelection();
2430 let text = selection.toString();
2431 let range = selection.getRangeAt(0);
2432 if (text && range) {
2433 const rangeData = {
2434 startPath: range.startContainer,
2435 startOffset: range.startOffset,
2436 endPath: range.endContainer,
2437 endOffset: range.endOffset
2438 };
2439 sessionStorage.setItem("tempSelText", text);
2440 sessionStorage.setItem("tempSelRange", JSON.stringify(rangeData));
2441 }
2442 }
2443}
2444
2445function editMenu(action) {
2446 let text = sessionStorage.getItem("tempSelText") ?? "";
2447 let storedRange = sessionStorage.getItem("tempSelRange") ?? "";
2448 /*
2449 if (storedRange) {
2450 let rangeData = JSON.parse(storedRange);
2451 let startContainer = nodeFromPath(rangeData.startPath);
2452 let endContainer = nodeFromPath(rangeData.endPath);
2453
2454 // Create a new range
2455 let newRange = new Range();
2456 newRange.setStart(startContainer, rangeData.startOffset);
2457 newRange.setEnd(endContainer, rangeData.endOffset);
2458
2459 // Select the new range
2460 let selection = window.getSelection();
2461 selection.removeAllRanges();
2462 selection.addRange(newRange);
2463 }
2464 */
2465 if (action === "Copy") {
2466 if (text) {
2467 sessionStorage.setItem("copiedText",text);
2468 }
2469 } else if (action === "Paste") {
2470 const copiedText = sessionStorage.getItem("copiedText");
2471 if (copiedText) {
2472 newRange.deleteContents();
2473 newRange.insertNode(document.createTextNode(copiedText));
2474 }
2475 } else if (action === "Cut") {
2476 if (text) {
2477 sessionStorage.setItem("copyText",text);
2478 //document.execCommand("cut");
2479 newRange.deleteContents();
2480 }
2481 } else if (action === "Undo") {
2482 undoEdit();
2483 } else if (action === "Redo") {
2484 redoEdit();
2485 }
2486}
2487
2488// Switch to the clicked tab
2489function openETab(btn) {
2490 const tabcontent = document.querySelectorAll(".etabcontent");
2491 const tablinks = document.querySelectorAll(".etablinks");
2492 const tab = btn ? btn : document.querySelectorAll(".edt_active")[0];
2493 const tabID = tab.id.replace("-btn","")
2494 tabcontent.forEach(content => {
2495 content.style.display = "none";
2496 });
2497 tablinks.forEach(link => {
2498 link.classList.remove("edt_active");
2499 });
2500 tab.className += " edt_active";
2501 //document.getElementById(tabID).style.display = "inline-flex";
2502 document.getElementById(tabID).style.display = "flex";
2503 // For the main sequence tab disable Save and Save as...
2504 if (tabID === "etab1") {
2505 disableMenuItems("noteditor",true);
2506 } else {
2507 disableMenuItems("noteditor",false);
2508 }
2509 // Change validation icon to defaule
2510 let vldBtn = document.getElementById("vldBtn");
2511 vldBtn.src = "icons/validate-syntax.svg";
2512 vldBtn.style.backgroundColor = "";
2513
2514 // Adjust height of active editor
2515 windowResize();
2516}
2517
2518// Close the clicked tab
2519function closeTab(tab,event) {
2520 const tablinks = document.querySelectorAll(".etablinks");
2521 if (tablinks.length < 3) return;
2522 const tabCount = parseInt(tab.parentElement.id.replace("-btn","").replace("etab",""));
2523 const tabBtn = document.getElementById(`etab${tabCount}-btn`);
2524 const tabContent = document.getElementById(`etab${tabCount}`);
2525 if (seqIsChanged()) {
2526 dlgConfirm("File was changed, close anyway?", function(resp) {
2527 if (resp) {
2528 tabBtn.remove();
2529 tabContent.remove();
2530 // switch to previous tab
2531 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2532 }
2533 });
2534 } else {
2535 tabBtn.remove();
2536 tabContent.remove();
2537 // switch to previous tab
2538 openETab(document.getElementById(`etab${(tabCount-1)}-btn`));
2539 }
2540 // need this since the close button is inside the tab button
2541 event.stopPropagation();
2542}
2543
2544// Create and add a new editor tab
2545function addETab(btn) {
2546 // Create tab button
2547 const tabBtn = document.createElement("button");
2548 tabBtn.className = "etablinks";
2549 const tabCount = (btn.previousElementSibling) ? parseInt(btn.previousElementSibling.id.replace("-btn","").replace("etab","")) + 1 : 1;
2550 tabBtn.id = "etab" + tabCount + "-btn";
2551 tabBtn.innerHTML = `<span id="etab${tabCount}-lbl">new file${tabCount}</span><span style="color: var(--mred);" id="filechanged${tabCount}"></span><span onclick="closeTab(this,event);" class="closebtn">&times;</span>`;
2552 btn.parentNode.insertBefore(tabBtn,btn);
2553 tabBtn.onclick = function () { openETab(this);};
2554
2555 // Create editor area
2556 const tabContent = document.createElement("div");
2557 tabContent.id = "etab" + tabCount;
2558 tabContent.className = "etabcontent";
2559 let makeDark = "";
2560 if (localStorage.getItem("darkMode")) makeDark = "style='background-color: black; color: white;'";
2561 const html =
2562 `<pre id="lineNumbers${tabCount}" class="edt_linenum"></pre><pre id="editorTab${tabCount}" ${makeDark} class="edt_area" spellcheck="false" contenteditable="false"></pre>`;
2563 tabContent.innerHTML = html;
2564 const lastETab = document.getElementById("lastETab");
2565 lastETab.parentNode.insertBefore(tabContent,lastETab);
2566 tabBtn.click();
2567 windowResize();
2568 // Add event listeners
2569 editorEventListeners();
2570}
2571
2572// Return the pre of lineNumbers, editor, tab label and tab button element of the active tab
2573function editorElements() {
2574 const btn = (document.querySelectorAll(".edt_active")[0]) ? document.querySelectorAll(".edt_active")[0] : document.getElementById("etab1-btn");
2575 const tab = document.getElementById(btn.id.replace("-btn",""));
2576 const [lineNumbers,editor] = tab.children;
2577 const btnLabel = (btn.id !== "etab1-btn") ? btn.children[0] : btn.children[1];
2578 return [lineNumbers,editor,btnLabel,btn];
2579}
2580
2581// disable and enable clicking on menu item
2582function disableMenuItems(className,flag) {
2583 /* Arguments:
2584 className - the class name of the item
2585 flag - true/false to enable/disable item
2586 */
2587 const els = document.querySelectorAll(`.${className}`);
2588 els.forEach(e => {
2589 if (flag) {
2590 e.style.opacity = 0.5;
2591 e.style.pointerEvents = "none";
2592 } else {
2593 e.style.opacity = "";
2594 e.style.pointerEvents = "";
2595 }
2596 });
2597}
2598
2599// Function to replace some special characters in the text
2600function escapeSpecialCharacters(text) {
2601 return text.replace(/&/g, "&amp;")
2602 .replace(/</g, "&lt;")
2603 .replace(/>/g, "&gt;")
2604 .replace(/\r\n|\n\r/g, '\n')
2605 //.replace(/"/g, "&quot;")
2606 //.replace(/'/g, "&#39;")
2607 .replace(/\t/g, " ")
2608 // and illegal ascii characters
2609 .replace(/[^\x00-\x7F]/g, " ");
2610}
2611
2612// Produce a help window
2613function dlgHelp() {
2614 let help = `
2615<span style='text-align: left;'>
2616<b>Hidden features of the sequencer editor</b>
2617<ul style='white-space: pre;font-family: monospace;'>
2618<li>Double click on the edit area of the first (main) tab to edit the currently loaded sequence.</li>
2619<li>Tab - Indent selected lines.</li>
2620<li>Shift+Tab - Unindent selected lines.</li>
2621<li>Escape - Autoindent selected lines according to syntax rules.</li>
2622<li>Ctrl+C - Copy selected text.</li>
2623<li>Ctrl+V - Paste selected text.</li>
2624<li>Ctrl+A - Select all text.</li>
2625<li>Ctrl+Z - Undo last change.</li>
2626<li>Ctrl+R - Redo last undo.</li>
2627</ul>
2628</span>
2629`;
2630 const d = dlgMessage("Editor help",help, false, false);
2631 const btn = d.querySelectorAll('.dlgButton')[0];
2632 btn.className = "dlgButtonDefault";
2633 btn.focus();
2634
2635}
2636
2637// Activate drag and drop events on next files table
2638function activateDragDrop(table) {
2639 /* Arguments:
2640 table - The table element containing the list of next files.
2641 */
2642 // collect all rows with class dragrow
2643 const rows = table.querySelectorAll('.dragrow');
2644 let dragStartIndex,dragEndIndex;
2645 let emptyRow;
2646 // add event listeners
2647 rows.forEach(row => {
2648 row.addEventListener('dragstart', dragStart);
2649 row.addEventListener('dragover', dragOver);
2650 row.addEventListener('dragend', dragEnd);
2651 });
2652
2653 function dragStart(e) {
2654 dragStartIndex = Array.from(rows).indexOf(this);
2655 rows.forEach(row => row.classList.remove('dragstart'));
2656 this.classList.add('dragstart');
2657 }
2658 function dragOver(e) {
2659 e.preventDefault();
2660 dragEndIndex = Array.from(rows).indexOf(this);
2661 // Create or update the empty row element
2662 if (!emptyRow) {
2663 emptyRow = document.createElement('tr');
2664 emptyRow.innerHTML = "<td colspan=4>&nbsp;</td>";
2665 emptyRow.classList.add('empty-dragrow');
2666 }
2667 // Insert the empty row element at the appropriate position
2668 if (dragEndIndex > dragStartIndex) {
2669 this.parentNode.insertBefore(emptyRow, this.nextSibling);
2670 } else if (dragEndIndex < dragStartIndex) {
2671 this.parentNode.insertBefore(emptyRow, this);
2672 }
2673 }
2674 function dragEnd(e) {
2675 if (emptyRow) {
2676 emptyRow.remove();
2677 emptyRow = null;
2678 }
2679 reorderNextFilenames(dragStartIndex,dragEndIndex);
2680 rows.forEach(row => {
2681 row.classList.remove('dragstart');
2682 });
2683 }
2684}
2685
2686// Move next filename from dragStarIndex to dragEndIndex position
2687function reorderNextFilenames(dragStarIndex,dragEndIndex) {
2688 /* Arguments:
2689 dragStarIndex - move file from this indext.
2690 dragEndIndex - destination index of the file.
2691 */
2692 if (dragStarIndex === dragEndIndex) return;
2693 let odbpath = mslDefs.filename.next;
2694 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2695 if (rpc.result.status[0] !== 1) return;
2696 let fList = rpc.result.data[0];
2697
2698 // Remove the draggedFile from the starting index
2699 const draggedFile = fList.splice(dragStarIndex, 1)[0];
2700 // Insert the draggedFile at the new index
2701 fList.splice(dragEndIndex, 0, draggedFile);
2702 // Check that the list does not contain more than 10 files
2703 if (fList.length > 10) {
2704 dlgAlert("There are more than 10 files. Aborting!.");
2705 return;
2706 }
2707 // Move full slots to the begenning of the array
2708 fList = fList.filter(Boolean);
2709 fList = [...fList, ...new Array(10 - fList.length).fill("")];
2710 // Update values in ODB
2711 mjsonrpc_db_paste([odbpath],[fList]).then(function (rpc2) {
2712 if (rpc2.result.status[0] !== 1) {
2713 dlgAlert("Failed to move the file!<br>Please check.");
2714 }
2715 }).catch(function (error) {
2716 console.error(error);
2717 });
2718 }).catch (function (error) {
2719 console.error(error);
2720 });
2721}
2722
2723// Change the next file name in the clicked row
2724function chngNextFilename(e,filename) {
2725 /* Arguments:
2726 e - (optional) cell element on the same row of "next file" to be changed.
2727 If last row (or undefined) add a file to the end of the queue.
2728 filename - (optional) The file name to add to the end of the queue.
2729 */
2730 e = (e) ? e : document.getElementById("addAsNext");
2731 //if (e === undefined) e = document.getElementById("addAsNext");
2732 // file index from table row index
2733 let index = e ? e.parentElement.rowIndex - 1 : 0;
2734 // Only 10 files are allowed
2735 if (index > 9) {
2736 dlgAlert("Maximum number (10) of next files reached!");
2737 return -1;
2738 }
2739 index++;
2740 let odbpath = mslDefs.filename.next;
2741 mjsonrpc_db_get_values([`${SeqODB}/State/Path`]).then(function(rpc) {
2742 let path = mslDefs.filename.path + "/" + rpc.result.data[0].replace(/\/+/g, '/').replace(/^\/+|\/+$/g, '');
2743 sessionStorage.setItem("pathName",path);
2744 if (filename) {
2745 odbpath = odbpath + "[" + index.toString() + "]";
2746 modbset(odbpath,filename);
2747 } else {
2748 file_picker(path,mslDefs.filename.ext,function(filename) {
2749 filename = filename.replace(/^sequencer\//,'').replace(/^\//,'');
2750 odbpath = odbpath + "[" + index.toString() + "]";
2751 if (filename) {
2752 modbset(odbpath,filename);
2753 }
2754 });
2755 }
2756 return index+1;
2757 }).catch(function (error) {
2758 mjsonrpc_error_alert(error);
2759 });
2760}
2761
2762// Reomve the next file name from the queue
2763function remNextFilename(e) {
2764 /* Arguments:
2765 e - cell element on the same row of "next file" to be removed
2766 */
2767 // file index from table row index
2768 let index = e.parentElement.rowIndex - 1;
2769 let odbpath = mslDefs.filename.next;
2770 mjsonrpc_db_get_values([odbpath]).then(function(rpc) {
2771 if (rpc.result.status[0] !== 1) return;
2772 let fList = rpc.result.data[0];
2773 // Empty element
2774 fList[index] = "";
2775 // Move full slots to the begenning of the array
2776 fList = fList.filter(Boolean);
2777 fList = [...fList, ...new Array(10 - fList.length).fill("")];
2778 mjsonrpc_db_paste([odbpath],[fList]).then(function (rpc2) {
2779 if (rpc2.result.status[0] !== 1) {
2780 dlgAlert("Failed to remove file from list!<br>Please check.");
2781 }
2782 }).catch(function (error) {
2783 console.error(error);
2784 });
2785 }).catch (function (error) {
2786 console.error(error);
2787 });
2788}
2789
2790var NcheckValue = 0;
2791// Wait for ODB in path to have value
2792// If value is not reached, give up after 10s
2793function checkODBValue(path,value,funcCall,args) {
2794 /* Arguments:
2795 path - ODB path to monitor for value
2796 value - the value to be reached and return success
2797 funcCall - function name to call when value is reached
2798 args - argument to pass to funcCall
2799 */
2800 // Call the mjsonrpc_db_get_values function
2801 mjsonrpc_db_get_values([path]).then(function(rpc) {
2802 if (rpc.result.status[0] === 1 && rpc.result.data[0] !== value) {
2803 console.log("Value not reached yet", NcheckValue);
2804 NcheckValue++;
2805 if (NcheckValue < 100) {
2806 // Wait 0.1 second and then call checkODBValue again
2807 // Time out after 10 s
2808 setTimeout(() => {
2809 checkODBValue(path,value,funcCall,args);
2810 }, 100);
2811 }
2812 } else {
2813 if (funcCall) funcCall(args);
2814 console.log("Value reached, proceeding...");
2815 // reset counter
2816 NcheckValue = 0;
2817 }
2818 }).catch(function(error) {
2819 console.error(error);
2820 });
2821}
2822
2823function isTextSelected(element) {
2824 const selection = window.getSelection();
2825 if (!selection.rangeCount) return false;
2826
2827 const range = selection.getRangeAt(0);
2828 const selectedText = selection.toString();
2829
2830 // Check if the selection is within the specified element
2831 if (!element.contains(range.commonAncestorContainer)) return false;
2832
2833 // Check if there is any selected text
2834 return selectedText.length > 0;
2835}
2836
2837function deleteSelectedText(element) {
2838 const selection = window.getSelection();
2839 if (!selection.rangeCount) return; // No selection exists
2840 const range = selection.getRangeAt(0);
2841 // Ensure the selection is within the target element
2842 if (!element.contains(range.commonAncestorContainer)) return;
2843 // Delete the selected text
2844 range.deleteContents();
2845}
2846
2847// Make sure that the caret stays visible
2848function keepCaretVisible(editor) {
2849 const selection = window.getSelection();
2850 const caretPosition = selection.getRangeAt(0).getBoundingClientRect();
2851 const containerRect = editor.getBoundingClientRect();
2852 const caretTop = caretPosition.top - containerRect.top;
2853 const caretBottom = caretPosition.bottom - containerRect.top;
2854 const containerHeight = containerRect.height;
2855 // Caclulate font size
2856 const emInPixels = parseFloat(window.getComputedStyle(editor).fontSize) * 1.5;
2857
2858 // If the caret is near the bottom, scroll the container down with 1.5 fontSize
2859 if (caretBottom > containerHeight) {
2860 editor.scrollTop += caretBottom - containerHeight + emInPixels;
2861 }
2862
2863 // If the caret is near the top, scroll the container up
2864 if (caretTop < 0) {
2865 editor.scrollTop -= Math.abs(caretTop);
2866 }
2867}
2868
2869
2870// commands object for syntax validation
2871const mslCommands = {
2872 INCLUDE: {
2873 patterns: [/^INCLUDE\s+.+(\.msl)?$/i],
2874 description: "Include another MSL file.",
2875 args: 1,
2876 },
2877 BREAK: {
2878 patterns: [/^BREAK$/i],
2879 description: "Break (finish prematurely) a loop.",
2880 args: 0,
2881 },
2882 CALL: {
2883 patterns: [/^CALL\s+\w+(\s*,\s*.+)*$/i],
2884 description: "Call a subroutine with optional parameters.",
2885 args: ">=1",
2886 },
2887 CAT: {
2888 patterns: [/^CAT\s+\w+(\s*,\s*.+)*$/i],
2889 description: "Concatenate strings into a single variable.",
2890 args: ">=1",
2891 },
2892 COMMENT: {
2893 patterns: [/^(?:COMMENT\s+.*|#.*)$/i],
2894 description: "A comment line starting with COMMENT or #.",
2895 args: ">=1",
2896 },
2897 EXIT: {
2898 patterns: [/^EXIT$/i],
2899 description: "Exit the script immediately.",
2900 args: 0,
2901 },
2902 GOTO: {
2903 patterns: [/^GOTO\s+\d+$/i],
2904 description: "Jump to a specific line in the script.",
2905 args: 1,
2906 },
2907 IF: {
2908 patterns: [/^IF\s*\‍(.+\‍)$/i],
2909 description: "Conditional execution of code blocks.",
2910 blockStart: true, // Marks this as a block start command
2911 args: ">=1",
2912 },
2913 ELSE: {
2914 patterns: [/^ELSE$/i],
2915 description: "Alternative block for an IF statement.",
2916 args: 0,
2917 },
2918 ENDIF: {
2919 patterns: [/^ENDIF$/i],
2920 description: "Ends an IF block.",
2921 blockEnd: "IF", // Marks this as a block end command for IF
2922 args: 0,
2923 },
2924 LIBRARY: {
2925 patterns: [/^LIBRARY\s+.+$/i],
2926 description: "Declare the file as a library.",
2927 args: 1,
2928 },
2929 LOOP: {
2930 patterns: [
2931 /^LOOP\s+(\d+|INFINITE|\$\w+)$/i, // LOOP 5, LOOP INFINITE, or LOOP $varName
2932 /^LOOP\s+\w+\s*,\s*.+$/i, // LOOP i, 1:10
2933 // /^LOOP\s+\w+\s*,\s*(?:\d+:\d+|\S+)$/i, // LOOP i, 1:10
2934 ],
2935 description: "Execute a loop a fixed number of times or with values.",
2936 blockStart: true, // Marks this as a block start command
2937 args: ">=1",
2938 //argcond1: /^(\d+|INFINITE|\$\w+)$/, // First argument can be a number, INFINITE, or a variable
2939 },
2940 ENDLOOP: {
2941 patterns: [/^ENDLOOP$/i],
2942 description: "Ends a LOOP block.",
2943 blockEnd: "LOOP", // Marks this as a block end command for LOOP
2944 args: 0,
2945 },
2946 MESSAGE: {
2947 patterns: [/^MESSAGE\s+.+(\s*,\s*1)?$/i],
2948 description: "Display a message in the browser.",
2949 args: ">=1",
2950 },
2951 MSG: {
2952 patterns: [/^MSG\s+.+(\s*,\s*(ERROR|INFO|DEBUG|LOG|TALK))?$/i],
2953 description: "Send a message to the MIDAS buffer.",
2954 args: ">=1",
2955 },
2956 ODBCREATE: {
2957 patterns: [/^ODBCREATE\s+.+,\s+\w+(\s*,\s*\d+)?$/i],
2958 description: "Create an ODB key.",
2959 args: ">=2",
2960 },
2961 ODBDELETE: {
2962 patterns: [/^ODBDELETE\s+.+$/i],
2963 description: "Delete an ODB key or subdirectory.",
2964 args: 1,
2965 },
2966 ODBGET: {
2967 patterns: [/^ODBGET\s+".+",\s+\w+$/i],
2968 description: "Retrieve a value from the ODB.",
2969 args: 2,
2970 argcond: [/^\/.+$/, // First argument must start with /
2971 /^.+$/], // Second argument can be anything
2972 },
2973 ODBINC: {
2974 patterns: [/^ODBINC\s+.+(\s*,\s*[+-]?\d+)?$/i],
2975 description: "Increment an ODB value.",
2976 args: ">=1",
2977 },
2978 ODBLOOKUP: {
2979 patterns: [/^ODBLOOKUP\s+".+",\s+".+",\s+\w+$/i],
2980 description: "Lookup a string in the ODB array.",
2981 args: 3,
2982 },
2983 ODBSET: {
2984 //patterns: [/^ODBSET\s+("[^"]*"|\/[^,]+),\s*("[^"]*"|[^,]+)(\s*,\s*[01])?$/i,],
2985 patterns: [/^ODBSET\s+.+,\s+.+(\s*,\s*[01])?$/i],
2986 description: "Set a value in the ODB.",
2987 args: ">=2",
2988 argcond: [
2989 /^.+$/, // First argument must start with / or be a quoted string
2990 /^.+$/, // Second argument can be a quoted string or unquoted value
2991 /^[01]$/, // Third argument (optional) must be 0 or 1
2992 ],
2993 },
2994 ODBLOAD: {
2995 patterns: [/^ODBLOAD\s+.+(\s*,\s*.+)?$/i],
2996 description: "Load an external file into the ODB.",
2997 args: ">=1",
2998 },
2999 ODBSAVE: {
3000 patterns: [/^ODBSAVE\s+.+,\s+.+$/i],
3001 description: "Save part of the ODB to a file.",
3002 args: 2,
3003 },
3004 ODBSUBDIR: {
3005 patterns: [/^ODBSUBDIR\s+("\/[^"]*"|\/[^,]+)$/i],
3006 description: "Set a base path for ODB commands.",
3007 blockStart: true, // Marks this as a block start command
3008 args: 1,
3009 },
3010 ENDODBSUBDIR: {
3011 patterns: [/^ENDODBSUBDIR$/i],
3012 description: "Ends an ODBSUBDIR block.",
3013 blockEnd: "ODBSUBDIR", // Marks this as a block end command for ODBSUBDIR
3014 args: 1,
3015 },
3016 PARAM: {
3017 patterns: [/^PARAM\s+\w+(\s*,\s*.+)*$/i],
3018 description: "Define script parameters.",
3019 args: ">=1",
3020 },
3021 RUNDESCRIPTION: {
3022 patterns: [/^RUNDESCRIPTION\s+.+$/i],
3023 description: "Set a run description.",
3024 args: 1,
3025 },
3026 SCRIPT: {
3027 patterns: [/^SCRIPT\s+.+(\s*,\s*.+)*$/i],
3028 description: "Call a server-side script with optional parameters.",
3029 args: ">=1",
3030 },
3031 SET: {
3032 patterns: [/^SET\s+\w+\s*,\s*.+$/i],
3033 description: "Set a variable to a value.",
3034 args: 2,
3035 },
3036 SUBROUTINE: {
3037 patterns: [/^SUBROUTINE\s+\w+$/i],
3038 description: "Start a subroutine block.",
3039 blockStart: true, // Marks this as a block start command
3040 args: 1,
3041 },
3042 ENDSUBROUTINE: {
3043 patterns: [/^ENDSUBROUTINE$/i],
3044 description: "Ends a subroutine block.",
3045 blockEnd: "SUBROUTINE", // Marks this as a block end command for SUBROUTINE
3046 args: 0,
3047 },
3048 TRANSITION: {
3049 patterns: [/^TRANSITION\s+(start|stop|pause|resume)$/i],
3050 description: "Trigger a run state transition.",
3051 args: 1,
3052 },
3053 WAIT: {
3054 patterns: [
3055 /^WAIT\s+seconds,\s*(\d+|\$\w+)$/i, // WAIT seconds,10
3056 /^WAIT\s+events,\s*(\d+|\$\w+)$/i, // WAIT events,10
3057 /^WAIT\s+ODBvalue,\s*\/.+,\s*[<>=!]+,\s*(\d+|\$\w+)$/i, // WAIT ODBvalue, /path, <, 100
3058 ],
3059 description: "Wait for a condition, time, or event.",
3060 args: ">=1",
3061 },
3062 EQUATION: {
3063 patterns: [/^[a-zA-Z_]\w*\s*=\s*([0-9]+(\.\d+)?|\$[a-zA-Z_]\w*|[\d\w\$\s\+\-\*\/\^\‍(\‍)]+|[\w\$]+\‍(.*\‍)|".*"|'.*')$/i],
3064 description: "Variable assignment or equation.",
3065 args: ">=1",
3066 },
3067};
3068
3069const pythonCommands = {
3070 // Comment lines starting with "#" are ignored.
3071 COMMENT: {
3072 patterns: [/#.*/g],
3073 description: "Comment line. Lines starting with '#' are ignored.",
3074 args: 0,
3075 },
3076 // Multi line comments between triple brackets are ignored
3077 MULTICOMM: {
3078 patterns: [/("""[\s\S]*?""")|('''[\s\S]*?''')/g],
3079 description: "Multi line comments between tripple quotes.",
3080 args: 0,
3081 },
3082 // Function definition
3083 DEF: {
3084 patterns: [/^\s*def\s+[A-Za-z_]\w*\s*\‍([^)]*\‍)\s*:$/],
3085 description: "Function definition. Syntax: def name(params):",
3086 args: ">=0",
3087 },
3088 // Class definition
3089 CLASS: {
3090 patterns: [/^\s*class\s+[A-Za-z_]\w*(?:\s*\‍([^)]*\‍))?\s*:$/],
3091 description: "Class definition. Syntax: class Name(Base1, Base2):",
3092 args: ">=0",
3093 },
3094 // Import statements
3095 IMPORT: {
3096 patterns: [
3097 /^\s*import\s+[A-Za-z_]\w*(?:\.\w+)*(?:\s*,\s*[A-Za-z_]\w*(?:\.\w+)*)*\s*$/,
3098 /^\s*from\s+[A-Za-z_]\w*(?:\.\w+)*\s+import\s+(?:\*\s*|[A-Za-z_]\w*(?:\s+as\s+[A-Za-z_]\w*)?(?:\s*,\s*[A-Za-z_]\w*(?:\s+as\s+[A-Za-z_]\w*)?)*)\s*$/
3099 ],
3100 description: "Import statements. Syntax: import module[, module2]\n or: from pkg.mod import name[, name2]",
3101 args: ">=1",
3102 },
3103 // Conditional blocks
3104 IF: {
3105 patterns: [/^\s*if\s+.+\s*:$/],
3106 description: "If statement. Syntax: if condition:",
3107 args: ">=1",
3108 blockStart: true,
3109 },
3110 ELIF: {
3111 patterns: [/^\s*elif\s+.+\s*:$/],
3112 description: "Elif statement. Syntax: elif condition:",
3113 args: ">=1",
3114 },
3115 ELSE: {
3116 patterns: [/^\s*else\s*:$/],
3117 description: "Else statement. Syntax: else:",
3118 args: 0,
3119 },
3120 // Loop blocks
3121 FOR: {
3122 patterns: [/^\s*for\s+[A-Za-z_]\w*\s+in\s+.+\s*:$/],
3123 description: "For loop. Syntax: for var in iterable:",
3124 args: ">=1",
3125 //blockStart: true,
3126 },
3127 WHILE: {
3128 patterns: [/^\s*while\s+.+\s*:$/],
3129 description: "While loop. Syntax: while condition:",
3130 args: ">=1",
3131 //blockStart: true,
3132 },
3133 // Try/Except/Finally
3134 TRY: {
3135 patterns: [/^\s*try\s*:$/],
3136 description: "Try block. Syntax: try:",
3137 args: 0,
3138 //blockStart: true,
3139 },
3140 EXCEPT: {
3141 patterns: [/^\s*except(?:\s+[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)?(?:\s+as\s+[A-Za-z_]\w*)?\s*:$/],
3142 description: "Except block. Syntax: except ExceptionType as name:",
3143 args: ">=0",
3144 },
3145 FINALLY: {
3146 patterns: [/^\s*finally\s*:$/],
3147 description: "Finally block. Syntax: finally:",
3148 args: 0,
3149 },
3150 // With context manager
3151 WITH: {
3152 patterns: [/^\s*with\s+.+\s+as\s+[A-Za-z_]\w*\s*:$/],
3153 description: "With statement. Syntax: with expr as var:",
3154 args: ">=1",
3155 //blockStart: true,
3156 },
3157 // Simple statements
3158 RETURN: {
3159 patterns: [
3160 /^\s*return(?:\s+.+)?$/
3161 ],
3162 description: "Return statement. Syntax: return [expr]",
3163 args: ">=0",
3164 },
3165 PASS: {
3166 patterns: [/^\s*pass\s*$/],
3167 description: "Pass statement. Does nothing.",
3168 args: 0,
3169 },
3170 BREAK: {
3171 patterns: [/^\s*break\s*$/],
3172 description: "Break out of the nearest loop.",
3173 args: 0,
3174 },
3175 CONTINUE: {
3176 patterns: [/^\s*continue\s*$/],
3177 description: "Continue to next iteration of the nearest loop.",
3178 args: 0,
3179 },
3180 RAISE: {
3181 patterns: [/^\s*raise(?:\s+[A-Za-z_]\w*(?:\s*\‍(.*\‍))?)?(?:\s+from\s+[A-Za-z_]\w*(?:\s*\‍(.*\‍))?)?\s*$/],
3182 description: "Raise an exception. Syntax: raise ExceptionType(args) [from other]",
3183 args: ">=0",
3184 },
3185 ASSERT: {
3186 patterns: [/^\s*assert\s+.+$/],
3187 description: "Assert statement. Syntax: assert condition[, message]",
3188 args: ">=1",
3189 },
3190 // Decorators
3191 DECORATOR: {
3192 patterns: [/^\s*@\w+(?:\.\w+)*(?:\s*\‍(.*\‍))?$/],
3193 description: "Decorator. Syntax: @decorator or @decorator(args)",
3194 args: ">=0",
3195 },
3196 // Assignment (simple)
3197 ASSIGNMENT: {
3198 patterns: [/^\s*[A-Za-z_]\w*\s*=.*$/],
3199 description: "Variable assignment. Syntax: name = expression",
3200 args: ">=1",
3201 },
3202 // Bare function or method call, possibly with args
3203 CALL_EXPR: {
3204 patterns: [/^\s*[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\s*\‍(.*\‍)\s*$/],
3205 description: "Expression statement (e.g. function or method call).",
3206 args: ">=1"
3207 },
3208};
3209
3210// Validation function to validate script content
3211function checkEditorContent(e, commands) {
3212 // If not provided take appropriate default values
3213 if (commands === undefined) {
3214 commands = (SeqODB === "/PySequencer") ? pythonCommands : mslCommands;
3215 }
3216
3217 // Get the content from the current editor
3218 let [lineNumbers,editor] = editorElements();
3219 const editorContent = editor.innerText;
3220
3221 // Validate the script content
3222 const errors = validateScript(editorContent,commands);
3223 if (errors.length > 0) {
3224 // Format and display errors
3225 let vldErrors = errors.map(err => {
3226 if (typeof err === "string") {
3227 return err; // Handle string errors directly
3228 } else if (err.line && err.message) {
3229 return `Line ${err.line}: ${err.message}`; // Format object errors
3230 } else {
3231 return "Unknown error"; // Fallback for unexpected error formats
3232 }
3233 }).join("\n");
3234 // highlight error lines and scroll to first one
3235 hlLine(errors.map(error => error.line),"var(--mred)",editor,errors.map(error => error.message));
3236 setCurrentCursorPosition(editor, errors[0].line - 1 + editorContent.split("\n").slice(0,errors[0].line).reduce((sum, line) => sum + line.length, 0));
3237 editor.focus();
3238
3239 // Show the error message
3240 //dlgMessage("Validation errors",vldErrors,false,true);
3241 console.error("Validation errors:\n",vldErrors);
3242 e.src = "icons/validate-syntax-red.svg";
3243 e.style.backgroundColor = "var(--mred)";
3244 } else {
3245 //dlgAlert("The syntax of the sequence is valid.");
3246 e.src = "icons/validate-syntax-green.svg";
3247 e.style.backgroundColor = "var(--mgreen)";
3248 }
3249}
3250
3251// Function to validate the script against commands object
3252function validateScript(script, commands) {
3253 const errors = [];
3254
3255 // Stack to track nested blocks
3256 const blockStack = [];
3257 const lineNums = [];
3258 // Stack to track delimiters
3259 const delimiterStack = [];
3260 const delimiterLineNums = [];
3261
3262 // Delimiters and their matching pairs
3263 const delimiters = {
3264 '{': '}',
3265 '[': ']',
3266 '(': ')',
3267 '"': '"',
3268 "'": "'",
3269 };
3270
3271 // Remove multiline comments
3272 if (commands.MULTICOMM.patterns[0]) {
3273 const multiCommRegex = commands.MULTICOMM.patterns[0];
3274
3275 // replace each triple-quote block with the same count of blank lines
3276 script = script.replace(
3277 new RegExp(multiCommRegex.source, "g"),
3278 (match) => {
3279 // count the lines in the matched block
3280 const lines = match.split("\n");
3281 // return that many lines, each a single space
3282 return lines.map(() => " ").join("\n");
3283 }
3284 );
3285 }
3286
3287 // Regex to match and replace all comments by space
3288 const commentRegex = new RegExp(commands.COMMENT.patterns.map(pattern => pattern.source).join("|"), "gim");
3289 script = script.replace(commentRegex, "");
3290 const lines = script.split("\n");
3291
3292 lines.forEach((line, index) => {
3293 const trimmedLine = line.trim();
3294 if (!trimmedLine) return; // Skip empty lines
3295 let isValid = false;
3296
3297 // Check if the line matches any command pattern
3298 const cmdKey = Object.keys(commands).find(cmd => {
3299 const cmdData = commands[cmd];
3300 const cmdPattern = cmdData.patterns || null;
3301 if (Array.isArray(cmdPattern)) {
3302 isValid = cmdPattern.some(pattern => pattern.test(trimmedLine));
3303
3304 // Handle block start commands
3305 if (isValid && cmdData.blockStart) {
3306 blockStack.push(cmd); // Push the block type to the stack
3307 lineNums.push(index + 1);
3308 }
3309
3310 // Handle block end commands
3311 if (isValid && cmdData.blockEnd) {
3312 const expectedBlock = cmdData.blockEnd;
3313 const lastBlock = blockStack.pop();
3314 lineNums.pop();
3315
3316 if (lastBlock !== expectedBlock) {
3317 errors.push({ line: index + 1, message: `"${cmd}" without matching "${expectedBlock}".` });
3318 }
3319 }
3320
3321 // Validate arguments if argcond is defined
3322 if (isValid && cmdData.argcond) {
3323 // Remove the command keyword from the line and split remaining arguments by comma
3324 const args = splitArguments(trimmedLine.replace(new RegExp(`^${cmd}`, "i"), "").trim());
3325 for (let i = 0; i < args.length; i++) {
3326 const arg = args[i].trim().replace(/^"(.*)"$/, "$1"); // Remove surrounding quotes
3327 const argCond = cmdData.argcond[i]; // Get the condition for this argument
3328 if (argCond && !argCond.test(arg)) {
3329 errors.push({ line: index + 1, message: `Argument ${i + 1} of "${cmd}" is invalid.` });
3330 }
3331 }
3332 }
3333
3334 return isValid;
3335 }
3336 return cmdPattern && cmdPattern.test(trimmedLine);
3337 });
3338
3339 if (cmdKey) {
3340 // Valid command or comment, no error
3341 return;
3342 } else {
3343 errors.push({ line: index + 1, message: `Syntax error or unknown command.` });
3344 }
3345
3346 // Delimiter validation
3347 for (let i = 0; i < trimmedLine.length; i++) {
3348 const char = trimmedLine[i];
3349 if (delimiters[char]) {
3350 // Opening delimiter
3351 delimiterStack.push({ char, line: index + 1, pos: i + 1 });
3352 } else if (Object.values(delimiters).includes(char)) {
3353 // Closing delimiter
3354 if (delimiterStack.length === 0) {
3355 errors.push({ line: index + 1, message: `Mismatched "${char}" at line ${index + 1}, column ${i + 1}.` });
3356 } else {
3357 const lastDelimiter = delimiterStack.pop();
3358 if (delimiters[lastDelimiter.char] !== char) {
3359 errors.push({ line: index + 1, message: `Mismatched "${char}" at line ${index + 1}, column ${i + 1}. Expected "${delimiters[lastDelimiter.char]}".` });
3360 }
3361 }
3362 }
3363 }
3364 });
3365
3366 // Check for unclosed blocks at the end of the script
3367 if (blockStack.length > 0) {
3368 errors.push({ line: lineNums[0], message: `Unclosed "${blockStack[0]}" block starting at line ${lineNums[0]}.` });
3369 }
3370
3371 // Check for unclosed delimiters at the end of the script
3372 if (delimiterStack.length > 0) {
3373 errors.push({ line: delimiterLineNums[0], message: `Unclosed "${delimiterStack[0].char}" delimiter starting at line ${delimiterLineNums[0]}, column ${delimiterStack[0].pos}.` });
3374 }
3375
3376 return errors;
3377}
3378
3379// Helper function to split arguments with quoted strings by commas
3380function splitArguments(input) {
3381 // Regex to match quoted strings
3382 const regex = /(["'])(.*?)\1/g;
3383 const args = [];
3384 let lastIndex = 0;
3385
3386 // Step 1: Find all quoted strings and split the input around them
3387 let match;
3388 while ((match = regex.exec(input)) !== null) {
3389 // Add unquoted arguments before the current quoted string
3390 const beforeQuoted = input.slice(lastIndex, match.index).trim();
3391 if (beforeQuoted) {
3392 args.push(...beforeQuoted.split(",").map(arg => arg.trim()).filter(arg => arg.length > 0));
3393 }
3394
3395 // Add the quoted argument
3396 args.push(match[2]); // Capture the content inside the quotes
3397
3398 // Update lastIndex to the end of the current match
3399 lastIndex = regex.lastIndex;
3400 }
3401
3402 // Step 2: Add any remaining unquoted arguments after the last quoted string
3403 const remaining = input.slice(lastIndex).trim();
3404 if (remaining) {
3405 args.push(...remaining.split(",").map(arg => arg.trim()).filter(arg => arg.length > 0));
3406 }
3407
3408 return args;
3409}