-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathSlyEdit.js
6915 lines (6518 loc) · 269 KB
/
SlyEdit.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// $Id: SlyEdit.js,v 1.74 2020/04/05 21:03:43 nightfox Exp $
/* This is a text editor for Synchronet designed to mimic the look & feel of
* DCTEdit and IceEdit, since neither of those editors have been developed
* for quite a while and still exist only as 16-bit DOS applications.
*
* Author: Eric Oulashin (AKA Nightfox)
* BBS: Digital Distortion
* BBS address: digdist.bbsindex.com
*
* Date Author Description
* 2009-05-11 Eric Oulashin Started development
* 2009-06-11 Eric Oulashin Taking a break from development
* 2009-08-09 Eric Oulashin Started more development & testing
* 2009-08-22 Eric Oulashin Version 1.00
* Initial public release
* ....Removed some comments...
* 2017-12-16 Eric Oulashin Version 1.52 beta
* Added the ability for the sysop to toggle whether or
* not to allow users to edit quote lines, using the
* configuration option allowEditQuoteLines.
* 2017-12-17 Eric Oulashin Version 1.52
* Releasing this version
* 2017-12-19 Eric Oulashin Version 1.53
* Updated the PageUp and PageDown keys to ensure they
* match what's in sbbsdefs.js, since Synchronet added
* key codes for those keys on December 17, 2018. SlyEdit
* should still work with older and newer builds of
* Synchronet, with or without the updated sbbsdefs.js.
* 2017-12-26 Eric Oulashin Version 1.54
* Improved quoting with author initials when a >
* character exists in the quote lines: Not mistaking
* the preceding text as a quote prefix if it has 3
* or more non-space characters before the >. Also
* fixed an issue where wrapped quote lines were
* sometimes missing the quote line prefix.
* 2018-08-03 Eric Oulashin Version 1.61
* Updated to delete instances of User objects that
* are created, due to an optimization in Synchronet
* 3.17 that leaves user.dat open
* 2018-11-11 Eric Oulashin Version 1.62
* Updated to save the message if the user disconnects,
* to support Synchronet's message draft feature
* that was added recently.
* 2019-04-11 Eric Oulashin Version 1.63 Beta
* Started working on supporting word-wrapping for
* the entire width of any terminal size, beyond
* 79.
* 2019-04-18 Eric Oulashin Version 1.63
* Releasing this version.
* 2019-05-04 Eric Oulashin Version 1.64 Beta
* Started working on adding a spell check feature.
* Also, updated to use require() instead of load()
* for .js scripts when possible.
* 2019-05-24 Eric Oulashin Version 1.64
* Releasing this version
* 2019-05-24 Eric Oulashin Version 1.65
* Added support for parsing many standard language
* tags for the dictionary filenames
* 2019-05-28 Eric Oulashin Version 1.66 Beta
* Added more parsing for dictionary filenames
* for 'general' dictionaries and 'supplimental' ones
* 2019-05-29 Eric Oulashin Version 1.66
* Releasing this version
* 2019-07-19 Eric Oulashin Version 1.67 Beta
* Started working on supporting the RESULT.ED drop
* file, with the ability to change the subject
* 2019-07-21 Eric Oulashin Version 1.67
* Releasing this version. Synchronet 3.17c development
* builds from July 21, 2019 onward support result.ed
* even for editors configured for QuickBBS MSGINF/MSGTMP.
* 2019-07-21 Eric Oulashin Version 1.68 Beta
* Updated to honor the SUB_ANON and SUB_AONLY flags
* for the sub-boards when cross-posting so that the
* "from" name is "Anonymous" if either of those flags
* enabled.
* Updated to allow message uploading.
* Started working on updates to save new text lines
* as one long line, to help with word wrapping in
* offline readers etc.
* 2019-08-09 Eric Oulashin Version 1.68
* Releasing this version
* 2019-08-14 Eric Oulashin Version 1.69
* Updated to only use console.inkey() for user input
* and not use console.getkey() anymore.
* The change was made in the getUserKey() function
* in SlyEdit_Misc.js.
* Also, SlyEdit will now write the editor style
* (ICE or DCT) to result.ed at the end when a message
* is saved. Also, when editing a message, if the cursor
* is at the end of the last line and the user presses
* the DEL key, then treat it as a backspace. Some
* terminals send a delete for backspace, particularly
* with keyboards that have a delete key but no backspace
* key.
* 2019-08-15 Eric Oulashin Version 1.70
* Fix for a bug introduced in the flowing-line update in 1.68
* where some quote blocks were sometimes not being included when
* saving a message. Also, quote lines are now wrapped
* to the user's terminal width rather than 80 columns.
* 2020-03-03 Eric Oulashin Version 1.71
* Added a new configuration option, allowSpellCheck,
* which the sysop can use to configure whether or not
* spell check is allowed. You might want to disable
* spell check if the spell check feature causes SlyEdit
* to abort with an error saying it's out of memory.
* 2020-03-04 Eric Oulashin Version 1.72
* For cross-posting, to make sure the user can post in a
* sub-board, SlyEdit now checks the can_post property of
* the sub-board rather than checking the ARS. The can_post
* property covers more cases.
* 2020-03-30 Eric Oulashin Version 1.73 Beta
* Started working on updating tag line selection to use
* DDLightbarMenu instead of SlyEdit's own internal
* choice menu.
* 2020-03-31 Eric Oulashin Version 1.73
* Finished updating the tag line selection to use DDLightbarMenu.
* Releasing this verison.
* I want to start working on updating lightbar menu behavior
* to scroll one at a time when using the up arrow at the
* top of the list or the down arrow at the bottom of the
* list, to be consistent with DDLightbarMenu and how
* scrolling normally behaves in other apps. However, SlyEdit's
* choice menu now is only used for the user settings
* menu, where only 1 page of items is shown.
*/
/* Command-line arguments:
1 (argv[0]): Filename to read/edit
2 (argv[1]): Editor mode ("DCT", "ICE", or "RANDOM")
*/
// Determine the location of this script (its startup directory).
// The code for figuring this out is a trick that was created by Deuce,
// suggested by Rob Swindell. I've shortened the code a little.
var gStartupPath = '.';
try { throw dig.dist(dist); } catch(e) { gStartupPath = e.fileName; }
gStartupPath = backslash(gStartupPath.replace(/[\/\\][^\/\\]*$/,''));
// EDITOR_STYLE: Can be changed to mimic the look of DCT Edit or IceEdit.
// The following are supported:
// "DCT": DCT Edit style
// "ICE": IceEdit style
// "RANDOM": Randomly choose a style
var EDITOR_STYLE = "DCT";
// The second command-line argument (argv[1]) can change this.
if (typeof(argv[1]) != "undefined")
{
var styleUpper = argv[1].toUpperCase();
// Make sure styleUpper is valid before setting EDITOR_STYLE.
if (styleUpper == "DCT")
EDITOR_STYLE = "DCT";
else if (styleUpper == "ICE")
EDITOR_STYLE = "ICE";
else if (styleUpper == "RANDOM")
EDITOR_STYLE = (Math.floor(Math.random()*2) == 0) ? "DCT" : "ICE";
}
// Load required JavaScript libraries
var requireFnExists = (typeof(require) === "function");
if (requireFnExists)
{
require("sbbsdefs.js", "K_NOCRLF");
require("dd_lightbar_menu.js", "DDLightbarMenu");
require(gStartupPath + "SlyEdit_Misc.js", "gUserSettingsFilename");
}
else
{
load("sbbsdefs.js");
load("dd_lightbar_menu.js");
load(gStartupPath + "SlyEdit_Misc.js");
}
// Determine whether the user settings file exists
const userSettingsFileExistedOnStartup = file_exists(gUserSettingsFilename);
// Load program settings from SlyEdit.cfg, and load the user configuratio nsettings
var gConfigSettings = ReadSlyEditConfigFile();
var gUserSettings = ReadUserSettingsFile(gConfigSettings);
// Load any specified 3rd-party startup scripts
for (var i = 0; i < gConfigSettings.thirdPartyLoadOnStart.length; ++i)
load(gConfigSettings.thirdPartyLoadOnStart[i]);
// Execute any provided startup JavaScript commands
for (var i = 0; i < gConfigSettings.runJSOnStart.length; ++i)
eval(gConfigSettings.runJSOnStart[i]);
const EDITOR_PROGRAM_NAME = "SlyEdit";
const ERRORMSG_PAUSE_MS = 1500;
const TEXT_SEARCH_PAUSE_MS = 1500;
const SPELL_CHECK_PAUSE_MS = 1000;
// This script requires Synchronet version 3.14 or higher.
// Exit if the Synchronet version is below the minimum.
if (system.version_num < 31400)
{
console.print("\1n");
console.crlf();
console.print("\1n\1h\1y\1i* Warning:\1n\1h\1w " + EDITOR_PROGRAM_NAME);
console.print(" " + "requires version \1g3.14\1w or");
console.crlf();
console.print("higher of Synchronet. This BBS is using version \1g");
console.print(system.version + "\1w. Please notify the sysop.");
console.crlf();
console.pause();
exit(1); // 1: Aborted
}
// If the user's terminal doesn't support ANSI, or if their terminal is less
// than 80 characters wide, then exit.
if (!console.term_supports(USER_ANSI))
{
console.print("\1n\r\n\1h\1yERROR: \1w" + EDITOR_PROGRAM_NAME +
" requires an ANSI terminal.\1n\r\n\1p");
exit(1); // 1: Aborted
}
if (console.screen_columns < 80)
{
console.print("\1n\r\n\1h\1w" + EDITOR_PROGRAM_NAME + " requires a terminal width of at least 80 characters.\1n\r\n\1p");
exit(1); // 1: Aborted
}
// Constants
const EDITOR_VERSION = "1.73";
const EDITOR_VER_DATE = "2020-03-31";
// Program variables
var gEditTop = 6; // The top line of the edit area
var gEditBottom = console.screen_rows-2; // The last line of the edit area
/*
// gEditLeft and gEditRight are the rightmost and leftmost columns of the edit
// area, respectively. They default to an edit area 80 characters wide
// in the center of the screen, but for IceEdit mode, the edit area will
// be on the left side of the screen to match up with the screen header.
// gEditLeft and gEditRight are 1-based.
*/
//var gEditLeft = (console.screen_columns/2).toFixed(0) - 40 + 1;
//var gEditRight = gEditLeft + 79; // Based on gEditLeft being 1-based
var gEditLeft = 1;
var gEditRight = gEditLeft + console.screen_columns - 1; // Based on gEditLeft being 1-based
/*
// If the screen has less than 80 columns, then use the whole screen.
if (console.screen_columns < 80)
{
gEditLeft = 1;
gEditRight = console.screen_columns;
}
*/
// Colors
var gQuoteWinTextColor = "\1n\1" + "7\1k"; // Normal text color for the quote window (DCT default)
var gQuoteLineHighlightColor = "\1n\1w"; // Highlighted text color for the quote window (DCT default)
var gTextAttrs = "\1n\1w"; // The text color for edit mode
var gQuoteLineColor = "\1n\1c"; // The text color for quote lines
var gUseTextAttribs = false; // Will be set to true if text colors start to be used
// gQuotePrefix contains the text to prepend to quote lines.
// gQuotePrefix will later be updated to include the "To" user's
// initials or first 2 letters of their username.
var gQuotePrefix = " > ";
// gCrossPostMsgSubs will contain sub-boards for which the user wants to
// cross-post their message into. Sub-board codes will be contained in
// objects whose name is the index to the message group in msg_area.grp_list
// to which the sub-board codes belong.
var gCrossPostMsgSubs = {};
// This function returns whether or not a property of the gCrossPostMsgSubs
// object is one of its member functions (i.e., something to skip when looking
// only for the message groups).
//
// Parameters:
// pPropName: The name of a property
//
// Return value: Boolean - Whether or not the property is the name of one of the
// functions in the gCrossPostMsgSubs object
gCrossPostMsgSubs.propIsFuncName = function(pPropName) {
return((pPropName == "propIsFuncName") || (pPropName == "add") || (pPropName == "remove") ||
(pPropName == "subCodeExists") || (pPropName == "numMsgGrps") ||
(pPropName == "numSubBoards"));
};
// This function returns whether or not the a message sub-coard code exists in
// gCrossPostMsgSubs.
//
// Parameters:
// pSubCode: The sub-code to look for
gCrossPostMsgSubs.subCodeExists = function(pSubCode) {
if (typeof(pSubCode) != "string")
return false;
var grpIndex = msg_area.sub[pSubCode].grp_index;
var foundIt = false;
if (this.hasOwnProperty(grpIndex))
foundIt = this[grpIndex].hasOwnProperty(pSubCode);
return foundIt;
};
// This function adds a sub-board code to gCrossPostMsgSubs.
//
// Parameters:
// pSubCode: The sub-code to add
gCrossPostMsgSubs.add = function(pSubCode) {
if (typeof(pSubCode) != "string")
return;
if (this.subCodeExists(pSubCode))
return;
var grpIndex = msg_area.sub[pSubCode].grp_index;
if (!this.hasOwnProperty(grpIndex))
this[grpIndex] = new Object();
this[grpIndex][pSubCode] = true;
};
// This function removes a sub-board code from gCrossPostMsgSubs.
//
// Parameters:
// pSubCode: The sub-code to remove
gCrossPostMsgSubs.remove = function(pSubCode) {
if (typeof(pSubCode) != "string")
return;
var grpIndex = msg_area.sub[pSubCode].grp_index;
if (this.hasOwnProperty(grpIndex))
{
delete this[grpIndex][pSubCode];
if (numObjProperties(this[grpIndex]) == 0)
delete this[grpIndex];
}
};
// This function returns the number of message groups in
// gCrossPostMsgSubs.
gCrossPostMsgSubs.numMsgGrps = function() {
var msgGrpCount = 0;
for (var prop in this)
{
if (!this.propIsFuncName(prop))
++msgGrpCount;
}
return msgGrpCount;
};
// This function returns the number of sub-boards the user has chosen to post
// the message into.
gCrossPostMsgSubs.numSubBoards = function () {
var numMsgSubs = 0;
for (var grpIndex in this)
{
if (!this.propIsFuncName(grpIndex))
{
for (var subCode in gCrossPostMsgSubs[grpIndex])
++numMsgSubs;
}
}
return numMsgSubs;
}
// Set variables properly for the different editor styles. Also, set up
// function pointers for functionality common to the IceEdit & DCTedit styles.
var fpDrawQuoteWindowTopBorder = null;
var fpDisplayTextAreaBottomBorder = null;
var fpDrawQuoteWindowBottomBorder = null;
var fpRedrawScreen = null;
var fpUpdateInsertModeOnScreen = null;
var fpDisplayBottomHelpLine = null;
var fpDisplayTime = null;
var fpDisplayTimeRemaining = null;
var fpCallESCMenu = null;
var fpRefreshSubjectonScreen = null;
var fpGlobalScreenVarsSetup = null;
// Subject screen position & length (for changing the subject). Note: These
// will be set in the IceStuff or DCTStuff scripts, depending on which theme
// is being used.
var gSubjPos = {
x: 0,
y: 0
};
var gSubjScreenLen = 0;
if (EDITOR_STYLE == "DCT")
{
if (requireFnExists)
require(gStartupPath + "SlyEdit_DCTStuff.js", "DrawQuoteWindowTopBorder_DCTStyle");
else
load(gStartupPath + "SlyEdit_DCTStuff.js");
gEditTop = 6;
gQuoteWinTextColor = gConfigSettings.DCTColors.QuoteWinText;
gQuoteLineHighlightColor = gConfigSettings.DCTColors.QuoteLineHighlightColor;
gTextAttrs = gConfigSettings.DCTColors.TextEditColor;
gQuoteLineColor = gConfigSettings.DCTColors.QuoteLineColor;
// Function pointers for the DCTEdit-style screen update functions
fpDrawQuoteWindowTopBorder = DrawQuoteWindowTopBorder_DCTStyle;
fpDisplayTextAreaBottomBorder = DisplayTextAreaBottomBorder_DCTStyle;
fpDrawQuoteWindowBottomBorder = DrawQuoteWindowBottomBorder_DCTStyle;
fpRedrawScreen = redrawScreen_DCTStyle;
fpUpdateInsertModeOnScreen = updateInsertModeOnScreen_DCTStyle;
fpDisplayBottomHelpLine = DisplayBottomHelpLine_DCTStyle;
fpDisplayTime = displayTime_DCTStyle;
fpDisplayTimeRemaining = displayTimeRemaining_DCTStyle;
fpCallESCMenu = callDCTESCMenu;
fpGlobalScreenVarsSetup = globalScreenVarsSetup_DCTStyle;
// Note: gSubjScreenLen is set in redrawScreen_DCTStyle()
fpRefreshSubjectOnScreen = refreshSubjectOnScreen_DCTStyle;
}
else if (EDITOR_STYLE == "ICE")
{
if (requireFnExists)
require(gStartupPath + "SlyEdit_IceStuff.js", "DrawQuoteWindowTopBorder_IceStyle");
else
load(gStartupPath + "SlyEdit_IceStuff.js");
gEditTop = 5;
gQuoteWinTextColor = gConfigSettings.iceColors.QuoteWinText;
gQuoteLineHighlightColor = gConfigSettings.iceColors.QuoteLineHighlightColor;
gTextAttrs = gConfigSettings.iceColors.TextEditColor;
gQuoteLineColor = gConfigSettings.iceColors.QuoteLineColor;
// Function pointers for the IceEdit-style screen update functions
fpDrawQuoteWindowTopBorder = DrawQuoteWindowTopBorder_IceStyle;
fpDisplayTextAreaBottomBorder = DisplayTextAreaBottomBorder_IceStyle;
fpDrawQuoteWindowBottomBorder = DrawQuoteWindowBottomBorder_IceStyle;
fpRedrawScreen = redrawScreen_IceStyle;
fpUpdateInsertModeOnScreen = updateInsertModeOnScreen_IceStyle;
fpDisplayBottomHelpLine = DisplayBottomHelpLine_IceStyle;
fpDisplayTime = displayTime_IceStyle;
fpDisplayTimeRemaining = displayTimeRemaining_IceStyle;
fpCallESCMenu = callIceESCMenu;
fpGlobalScreenVarsSetup = globalScreenVarsSetup_IceStyle;
// Note: gSubjScreenLen is set in redrawScreen_IceStyle()
fpRefreshSubjectOnScreen = refreshSubjectOnScreen_IceStyle;
}
// Temporary (for testing): Make the edit area small
//gEditLeft = 25;
//gEditRight = 45;
//gEditBottom = gEditTop + 1;
// End Temporary
// Calculate the edit area width & height
const gEditWidth = gEditRight - gEditLeft + 1;
const gEditHeight = gEditBottom - gEditTop + 1;
// Set up any required global screen variables
fpGlobalScreenVarsSetup();
// Message display & edit variables
var gInsertMode = "INS"; // Insert (INS) or overwrite (OVR) mode
var gQuoteLines = []; // Array of quote lines loaded from file, if in quote mode
var gUserHasOpenedQuoteWindow = false; // Whether or not the user has opened the quote line selection window
var gQuoteLinesTopIndex = 0; // Index of the first displayed quote line
var gQuoteLinesIndex = 0; // Index of the current quote line
// The gEditLines array will contain TextLine objects storing the line
// information.
var gEditLines = [];
var gEditLinesIndex = 0; // Index into gEditLines for the line being edited
var gTextLineIndex = 0; // Index into the current text line being edited
// Format strings used for printf() to display text in the edit area
const gFormatStr = "%-" + gEditWidth + "s";
const gFormatStrWithAttr = "%s%-" + gEditWidth + "s";
// Whether or not a message file was uploaded (if so, then SlyEdit won't
// do the line re-wrapping at the end before saving the message).
var gUploadedMessageFile = false;
// gEditAreaBuffer will be an array of strings for the edit area, which
// will be checked by displayEditLines() before outputting text lines
// to optimize the update of message text on the screen. displayEditLines()
// will also update this array after writing a line of text to the screen.
// The indexes in this array are the absolute screen lines.
var gEditAreaBuffer = [];
function clearEditAreaBuffer()
{
for (var lineNum = gEditTop; lineNum <= gEditBottom; ++lineNum)
gEditAreaBuffer[lineNum] = "";
}
clearEditAreaBuffer();
// gTxtreplacements will be an associative array that stores words (in upper
// case) to be replaced with other words.
var gNumTxtReplacements = 0;
var gTxtReplacements = new Object();
if (gConfigSettings.enableTextReplacements)
gNumTxtReplacements = populateTxtReplacements(gTxtReplacements, gConfigSettings.textReplacementsUseRegex);
// Set some stuff up for message editing
var gUseQuotes = true;
var gInputFilename = file_getcase(system.node_dir + "QUOTES.TXT");
if (gInputFilename == undefined)
{
gUseQuotes = false;
if ((argc > 0) && (gInputFilename == undefined))
gInputFilename = argv[0];
}
else
{
var all_files = directory(system.node_dir + "*");
var newest_filedate = -Infinity;
var newest_filename;
for (var file in all_files)
{
if (all_files[file].search(/quotes.txt$/i) != -1)
{
var this_date = file_date(all_files[file]);
if (this_date > newest_filedate)
{
newest_filename = all_files[file];
newest_filedate = this_date;
}
}
}
if (newest_filename != undefined)
gInputFilename = newest_filename;
}
var gOldStatus = bbs.sys_status;
bbs.sys_status &=~SS_PAUSEON;
bbs.sys_status |= SS_PAUSEOFF;
var gOldPassthru = console.ctrlkey_passthru;
console.ctrlkey_passthru = "+ACGKLOPQRTUVWXYZ_";
// Set some on-exit code to ensure that the original ctrl key passthru & system
// status settings are restored upon script exit, even if there is a runtime error.
js.on_exit("console.ctrlkey_passthru = gOldPassthru; bbs.sys_status = gOldStatus;");
// Enable delete line in SyncTERM (Disabling ANSI Music in the process)
console.write("\033[=1M");
console.clear();
var gMsgAreaInfo = null; // Will store the value returned by getCurMsgInfo().
var setMsgAreaInfoObj = false;
var gMsgSubj = "";
var gFromName = user.alias;
var gToName = gInputFilename;
var gMsgArea = "";
var gTaglineFile = "";
var dropFileTime = -Infinity;
var dropFileName = file_getcase(system.node_dir + "msginf");
if (dropFileName != undefined)
{
if (file_date(dropFileName) >= dropFileTime)
{
var dropFile = new File(dropFileName);
if (dropFile.exists && dropFile.open("r"))
{
dropFileTime = dropFile.date;
info = dropFile.readAll();
dropFile.close();
gFromName = info[0];
gToName = info[1];
gMsgSubj = info[2];
gMsgArea = info[4];
// Now that we know the name of the message area
// that the message is being posted in, call
// getCurMsgInfo() to set gMsgAreaInfo.
gMsgAreaInfo = getCurMsgInfo(gMsgArea);
setMsgAreaInfoObj = true;
// If the msginf file has 7 lines, then the 7th line is the full
// path & filename of the tagline file, where we can write the
// user's chosen tag line (if the user has that option enabled)
// for the BBS software to read the tagline & insert it at the
// proper place in the message.
if (info.length >= 7)
gTaglineFile = info[6];
}
}
file_remove(dropFileName);
}
else
{
// There is no msginf - Try editor.inf
dropFileName = file_getcase(system.node_dir + "editor.inf");
if (dropFileName != undefined)
{
if (file_date(dropFileName) >= dropFileTime)
{
var dropFile = new File(dropFileName);
if (dropFile.exists && dropFile.open("r"))
{
dropFileTime = dropFile.date;
info = dropFile.readAll();
dropFile.close();
gFromName = info[3];
gToName = info[1];
gMsgSubj = info[0];
gMsgArea = "";
// TODO: If we can know the name of the message
// area name, we can call getCurMsgInfo(). But
// editor.inf doesn't have the message area name.
// Now that we know the name of the message area
// that the message is being posted in, call
// getCurMsgInfo() to set gMsgAreaInfo.
//gMsgAreaInfo = getCurMsgInfo(gMsgArea);
//setMsgAreaInfoObj = true;
}
}
file_remove(dropFileName);
}
}
// If gMsgAreaInfo hasn't been set yet, then set it.
if (!setMsgAreaInfoObj)
{
gMsgAreaInfo = getCurMsgInfo(gMsgArea);
setMsgAreaInfoObj = true;
}
// Set a variable to store whether or not cross-posting can be done.
var gCanCrossPost = (gConfigSettings.allowCrossPosting && postingInMsgSubBoard(gMsgArea));
// If the user is posting in a message sub-board, then add its information
// to gCrossPostMsgSubs.
if (postingInMsgSubBoard(gMsgArea))
gCrossPostMsgSubs.add(gMsgAreaInfo.subBoardCode);
// Open the quote file / message file
readQuoteOrMessageFile();
// If the subject is blank, set it to something.
if (gMsgSubj == "")
gMsgSubj = gToName.replace(/^.*[\\\/]/,'');
// Store a copy of the current subject (possibly allowing the user to
// change the subject in the future)
var gOldSubj = gMsgSubj;
// Now it's edit time.
var exitCode = doEditLoop();
// Clear the screen and display the end-of-program information (if the setting
// is enabled).
console.clear("\1n");
if (gConfigSettings.displayEndInfoScreen)
{
displayProgramExitInfo(false);
console.crlf();
}
// If the user wrote & saved a message, do post work:
// - If the user changed the subject, then write a RESULT.ED file with
// the new subject.
// - Remove any extra blank lines that may be at the end of the message (in
// gEditLines), and output the message lines to a file with the passed-in
// input filename.
// - Do cross-posting.
var savedTheMessage = false;
if ((exitCode == 0) && (gEditLines.length > 0))
{
// Write a RESULT.ED containing the subject and editor name.
// Note: Normally, this is only supported for WWIV editors supporting
// result.inf/result.ed. However, with Synchronet 3.17c development
// builds from July 21, 2019 onward, result.ed is supported even for
// editors configured for QuickBBS MSGINF/MSGTMP.
// RESULT.ED specs:
// Line 1: Anonymous (1 for true, 0 for false) - Unused by Synchronet
// Line 2: Message subject
// Line 3: Editor details (e.g. full name, version/revision, date of build or release)
// Note: When cross-posting, gMsgSubj will be used, which has the correct current
// subject.
// gMsgSubj is the current subject, and gOldSubj is the original subject
var dropFile = new File(system.node_dir + "RESULT.ED");
if (dropFile.open("w"))
{
dropFile.writeln("0");
dropFile.writeln(gMsgSubj);
dropFile.writeln(EDITOR_PROGRAM_NAME + " " + EDITOR_VERSION + " (" + EDITOR_VER_DATE + ") (" + EDITOR_STYLE + " style)");
dropFile.close();
}
// Remove any extra blank lines at the end of the message
var lineIndex = gEditLines.length - 1;
while ((lineIndex > 0) && (lineIndex < gEditLines.length) && (gEditLines[lineIndex].length() == 0))
{
gEditLines.splice(lineIndex, 1);
--lineIndex;
}
// If the user didn't upload a message from a file, then for paragraphs of
// non-quote lines, make the paragraph all one line. Copy to another array
// of edit lines, and then set gEditLines to that array.
if (!gUploadedMessageFile)
{
var newEditLines = [];
var blockIdxes = findQuoteAndNonQuoteBlockIndexes(gEditLines);
for (var blockIdx = 0; blockIdx < blockIdxes.allBlocks.length; ++blockIdx)
{
if (blockIdxes.allBlocks[blockIdx].isQuoteBlock)
{
for (var i = blockIdxes.allBlocks[blockIdx].start; i < blockIdxes.allBlocks[blockIdx].end; ++i)
{
newEditLines.push(new TextLine(gEditLines[i].text, gEditLines[i].hardNewlineEnd, gEditLines[i].isQuoteLine));
delete gEditLines[i]; // Save some memory
}
}
else
{
var textLine = "";
var i = blockIdxes.allBlocks[blockIdx].start;
while ((i < blockIdxes.allBlocks[blockIdx].end) && (i < gEditLines.length))
{
var continueOn = true;
while (continueOn)
{
if (textLine.length > 0)
textLine += " ";
textLine += gEditLines[i].text;
continueOn = (!gEditLines[i].hardNewlineEnd) && (i+1 < gEditLines.length);
delete gEditLines[i]; // Save some memory
++i;
}
newEditLines.push(new TextLine(textLine, true, false));
textLine = "";
}
}
}
gEditLines = newEditLines;
}
// Store whether the user is still posting the message in the original sub-board
// and whether that's the only sub-board they're posting in.
var postingInOriginalSubBoard = gCrossPostMsgSubs.subCodeExists(gMsgAreaInfo.subBoardCode);
var postingOnlyInOriginalSubBoard = (postingInOriginalSubBoard && (gCrossPostMsgSubs.numSubBoards() == 1));
// If some message areas have been selected for cross-posting, then otuput
// which areas will be cross-posted into, and do the cross-posting.
// TODO: If the user changed the subject, make sure the subject is correct when cross-posting.
var crossPosted = false;
if (gCrossPostMsgSubs.numMsgGrps() > 0)
{
// If the will cross-post into other sub-boards, then create a string containing
// the user's entire message.
var msgContents = "";
if (!postingOnlyInOriginalSubBoard)
{
// Append each line to msgContents. Then,
// - If using Synchronet 3.15 or higher:
// Depending on whether the line has a hard newline
// or a soft newline, append a "\r\n" or a " \n", as
// per Synchronet's standard as of 3.15.
// - Otherwise (Synchronet 3.14 and below):
// Just append a "\r\n" to the line
if (system.version_num >= 31500)
{
var useHardNewline = false;
for (var i = 0; i < gEditLines.length; ++i)
{
// Use a hard newline if the current edit line has one or if this is
// the last line of the message.
useHardNewline = (gEditLines[i].hardNewlineEnd || (i == gEditLines.length-1));
msgContents += gEditLines[i].text + (useHardNewline ? "\r\n" : " \n");
}
}
else // Synchronet 3.14 and below
{
for (var i = 0; i < gEditLines.length; ++i)
msgContents += gEditLines[i].text + "\r\n";
}
// Read the user's signature, in case they have one
var msgSigInfo = readUserSigFile();
// If the user has not chosen to auto-sign messages, then also append their
// signature to the message now.
if (!gUserSettings.autoSignMessages)
{
// Append a blank line to separate the message & signature.
// Note: msgContents already has a newline at the end, so we don't have
// to append one here; just append the signature.
if (msgSigInfo.sigContents.length > 0)
msgContents += msgSigInfo.sigContents + "\r\n";
}
}
console.print("\1n");
console.crlf();
console.print("\1n" + gConfigSettings.genColors.msgWillBePostedHdr + "Your message will be posted into the following area(s):");
console.crlf();
// If the user is posting in the originally-chosen sub-board and other sub-boards,
// then make a log in the BBS log that the user is posting a message (for
// cross-post logging).
if (postingInOriginalSubBoard && !postingOnlyInOriginalSubBoard)
{
log(LOG_INFO, EDITOR_PROGRAM_NAME + ": " + user.alias + " is posting a message in " + msg_area.sub[gMsgAreaInfo.subBoardCode].grp_name +
" " + msg_area.sub[gMsgAreaInfo.subBoardCode].description + " (" + gMsgSubj + ")");
bbs.log_str(EDITOR_PROGRAM_NAME + ": " + user.alias + " is posting a message in " + msg_area.sub[gMsgAreaInfo.subBoardCode].grp_name +
" " + msg_area.sub[gMsgAreaInfo.subBoardCode].description + " (" + gMsgSubj + ")");
}
var postMsgErrStr = ""; // For storing errors related to saving the message
for (var grpIndex in gCrossPostMsgSubs)
{
// Skip the function names (we only want the group indexes)
if (gCrossPostMsgSubs.propIsFuncName(grpIndex))
continue;
console.print("\1n" + gConfigSettings.genColors.msgPostedGrpHdr + msg_area.grp_list[grpIndex].description + ":");
console.crlf();
for (var subCode in gCrossPostMsgSubs[grpIndex])
{
if (subCode == gMsgAreaInfo.subBoardCode)
{
printf("\1n " + gConfigSettings.genColors.msgPostedSubBoardName + "%-48s", msg_area.sub[subCode].description.substr(0, 48));
console.print("\1n " + gConfigSettings.genColors.msgPostedOriginalAreaText + "(original message area)");
}
// If subCode is not the user's current sub, then if the user is allowed
// to post in that sub, then post the message there.
else
{
// Write a log in the BBS log about which message area the user is
// cross-posting into.
log(LOG_INFO, EDITOR_PROGRAM_NAME + ": " + user.alias + " is cross-posting a message in " + msg_area.sub[subCode].grp_name +
" " + msg_area.sub[subCode].description + " (" + gMsgSubj + ")");
bbs.log_str(EDITOR_PROGRAM_NAME + ": " + user.alias + " is cross-posting a message in " + msg_area.sub[subCode].grp_name +
" " + msg_area.sub[subCode].description + " (" + gMsgSubj + ")");
// Write the cross-posting message area on the user's screen.
printf("\1n " + gConfigSettings.genColors.msgPostedSubBoardName + "%-73s", msg_area.sub[subCode].description.substr(0, 73));
if (msg_area.sub[subCode].can_post)
{
// If the user's auto-sign setting is enabled, then auto-sign
// the message and append their signature afterward. Otherwise,
// don't auto-sign, and their signature has already been appended.
if (gUserSettings.autoSignMessages)
{
var msgContents2 = msgContents + "\r\n";
msgContents2 += getSignName(subCode, gUserSettings.autoSignRealNameOnlyFirst, gUserSettings.autoSignEmailsRealName);
msgContents2 += "\r\n\r\n";
if (msgSigInfo.sigContents.length > 0)
msgContents2 += msgSigInfo.sigContents + "\r\n";
postMsgErrStr = postMsgToSubBoard(subCode, gToName, gMsgSubj, msgContents2, user.number);
}
else
postMsgErrStr = postMsgToSubBoard(subCode, gToName, gMsgSubj, msgContents, user.number);
if (postMsgErrStr.length == 0)
{
savedTheMessage = true;
crossPosted = true;
console.print("\1n\1h\1b[\1n\1g" + CHECK_CHAR + "\1n\1h\1b]\1n");
}
else
{
console.print("\1n\1h\1b[\1rX\1b]\1n");
console.crlf();
console.print(" \1n\1h\1r*\1n " + postMsgErrStr);
console.crlf();
}
}
else
{
// The user isn't allowed to post in the sub. Output a message
// saying so.
console.print("\1n\1h\1b[\1rX\1b]\1n");
console.crlf();
console.print(" \1n\1h\1r*\1n You're not allowed to post in this area.");
console.crlf();
}
}
console.crlf();
}
}
console.print("\1n");
console.crlf();
}
// Determine whether or not to save the message to INPUT.MSG. We want to
// save it by default, but if the user is posting in a sub-board, whether we
// want to save it will be determined by whether the user's current sub-board
// code is in the list of cross-post areas.
var saveMsgFile = true;
if (postingInMsgSubBoard(gMsgArea))
{
if (!gCrossPostMsgSubs.subCodeExists(gMsgAreaInfo.subBoardCode))
{
saveMsgFile = false;
// If the message was cross-posted to other message areas and not in the
// user's current message area, output a message to say that Synchronet
// will say the message was aborted, and that's normal.
if (crossPosted)
{
console.print("\1n\1c* Note: Your message has been cross-posted in areas other than your currently-");
console.crlf();
console.print("selected message area. Because your message was not saved for your currently-");
console.crlf();
console.print("selected message area, the BBS will say the message was aborted, even");
console.crlf();
console.print("though it was posted in those other areas. This is normal.n");
console.crlf();
console.crlf();
}
}
}
if (saveMsgFile)
saveMessageToFile();
}
function saveMessageToFile()
{
// Open the output filename. If no arguments were passed, then use
// INPUT.MSG in the node's temporary directory; otherwise, use the
// first program argument.
var msgFile = new File(argc == 0 ? system.temp_dir + "INPUT.MSG" : argv[0]);
if (msgFile.open("w"))
{
// Write each line of the message to the file. Note: The
// "Expand Line Feeds to CRLF" option should be turned on
// in SCFG for this to work properly for all platforms.
for (var i = 0; i < gEditLines.length; ++i)
msgFile.writeln(gEditLines[i].text);
// Auto-sign the message if the user's setting to do so is enabled
if (gUserSettings.autoSignMessages)
{
msgFile.writeln("");
var subCode = (postingInMsgSubBoard(gMsgArea) ? gMsgAreaInfo.subBoardCode : "mail");
msgFile.writeln(getSignName(subCode, gUserSettings.autoSignRealNameOnlyFirst, gUserSettings.autoSignEmailsRealName));
}
msgFile.close();
savedTheMessage = true;
}
else
console.print("\1n\1r\1h* Unable to save the message!\1n\r\n");
}
// Set the original ctrlkey_passthru and sys_status settins back.
console.ctrlkey_passthru = gOldPassthru;
bbs.sys_status = gOldStatus;
// Set the end-of-program status message.
var endStatusMessage = "";
if (exitCode == 1)
endStatusMessage = gConfigSettings.genColors.msgAbortedText + "Message aborted.";
else if (exitCode == 0)
{
if (gEditLines.length > 0)
{
if (savedTheMessage)
endStatusMessage = gConfigSettings.genColors.msgHasBeenSavedText + "The message has been saved.";
else
endStatusMessage = gConfigSettings.genColors.msgAbortedText + "Message aborted.";
}
else
endStatusMessage = gConfigSettings.genColors.emptyMsgNotSentText + "Empty message not sent.";
}
// We shouldn't hit this else case, but it's here just to be safe.
else
endStatusMessage = gConfigSettings.genColors.genMsgErrorText + "Possible message error.";
console.print(endStatusMessage);
console.crlf();
// If the user's setting to pause after every screenful is disabled, then
// pause here so that they can see the exit information.
if (user.settings & USER_PAUSE == 0)
mswait(1000);
// Load any specified 3rd-party exit scripts and execute any provided exit
// JavaScript commands.
for (var i = 0; i < gConfigSettings.thirdPartyLoadOnExit.length; ++i)
load(gConfigSettings.thirdPartyLoadOnExit[i]);
for (var i = 0; i < gConfigSettings.runJSOnExit.length; ++i)
eval(gConfigSettings.runJSOnExit[i]);
exit(exitCode);
// End of script execution
///////////////////////////////////////////////////////////////////////////////////
// Functions
// Reads the quote/message file (must be done on startup)
function readQuoteOrMessageFile()
{
var inputFile = new File(gInputFilename);
if (inputFile.open("r", false))
{
// Read into the gQuoteLines or gEditLines array, depending on the value
// of gUseQuotes. Use a buffer size that should be long enough.
if (gUseQuotes)
{
var textLine = null; // Line of text read from the quotes file
while (!inputFile.eof)
{
textLine = inputFile.readln(2048);
// Only use textLine if it's actually a string.
if (typeof(textLine) == "string")
{
textLine = strip_ctrl(textLine);
// If the line has only whitespace and/or > characters,
// then make the line blank before putting it into
// gQuoteLines.
if (/^[\s>]+$/.test(textLine))
textLine = "";
gQuoteLines.push(textLine);
}
}
}
else
{
var textLine = null;
while (!inputFile.eof)
{
textLine = new TextLine();
textLine.text = inputFile.readln(2048);
if (typeof(textLine.text) == "string")
textLine.text = strip_ctrl(textLine.text);
else
textLine.text = "";