diff --git a/htdocs/js/PGCodeMirror/PG.js b/htdocs/js/PGCodeMirror/PG.js deleted file mode 100644 index e003158379..0000000000 --- a/htdocs/js/PGCodeMirror/PG.js +++ /dev/null @@ -1,1567 +0,0 @@ -// Bassed off of CodeMirror mode perl file: -// https://github.com/codemirror/CodeMirror/blob/master/mode/perl/perl.js - -'use strict'; - -(() => { - CodeMirror.defineMode('PG', function () { - // http://perldoc.perl.org - const PERL = { - // null - magic touch - // 1 - keyword - // 2 - def - // 3 - atom - // 4 - operator - // 5 - variable-2 (predefined) - // [x,y] - x=1,2,3; y=must be defined if x{...} - // PERL operators - '->': 4, - '++': 4, - '--': 4, - '**': 4, - // ! ~ \ and unary + and - - '=~': 4, - '!~': 4, - '*': 4, - '/': 4, - '%': 4, - x: 4, - '+': 4, - '-': 4, - '.': 4, - '<<': 4, - '>>': 4, - // named unary operators - '<': 4, - '>': 4, - '<=': 4, - '>=': 4, - lt: 4, - gt: 4, - le: 4, - ge: 4, - '==': 4, - '!=': 4, - '<=>': 4, - eq: 4, - ne: 4, - cmp: 4, - '~~': 4, - '&': 4, - '|': 4, - '^': 4, - '&&': 4, - '||': 4, - '//': 4, - '..': 4, - '...': 4, - '?': 4, - ':': 4, - '=': 4, - '+=': 4, - '-=': 4, - '*=': 4, // etc. ??? - ',': 4, - '=>': 4, - '::': 4, - // list operators (rightward) - not: 4, - and: 4, - or: 4, - xor: 4, - // PERL predefined variables (I know, what this is a paranoid idea, but may be needed for people, who learn PERL, and for me as well, ...and may be for you?;) - BEGIN: [5, 1], - END: [5, 1], - PRINT: [5, 1], - PRINTF: [5, 1], - GETC: [5, 1], - READ: [5, 1], - READLINE: [5, 1], - DESTROY: [5, 1], - TIE: [5, 1], - TIEHANDLE: [5, 1], - UNTIE: [5, 1], - STDIN: 5, - STDIN_TOP: 5, - STDOUT: 5, - STDOUT_TOP: 5, - STDERR: 5, - STDERR_TOP: 5, - $ARG: 5, - $_: 5, - '@ARG': 5, - '@_': 5, - $LIST_SEPARATOR: 5, - '$"': 5, - $PROCESS_ID: 5, - $PID: 5, - $$: 5, - $REAL_GROUP_ID: 5, - $GID: 5, - '$(': 5, - $EFFECTIVE_GROUP_ID: 5, - $EGID: 5, - '$)': 5, - $PROGRAM_NAME: 5, - $0: 5, - $SUBSCRIPT_SEPARATOR: 5, - $SUBSEP: 5, - '$;': 5, - $REAL_USER_ID: 5, - $UID: 5, - '$<': 5, - $EFFECTIVE_USER_ID: 5, - $EUID: 5, - '$>': 5, - // '$a' : 5, - // '$b' : 5, - $COMPILING: 5, - '$^C': 5, - $DEBUGGING: 5, - '$^D': 5, - '${^ENCODING}': 5, - $ENV: 5, - '%ENV': 5, - $SYSTEM_FD_MAX: 5, - '$^F': 5, - '@F': 5, - '${^GLOBAL_PHASE}': 5, - '$^H': 5, - '%^H': 5, - '@INC': 5, - '%INC': 5, - $INPLACE_EDIT: 5, - '$^I': 5, - '$^M': 5, - $OSNAME: 5, - '$^O': 5, - '${^OPEN}': 5, - $PERLDB: 5, - '$^P': 5, - $SIG: 5, - '%SIG': 5, - $BASETIME: 5, - '$^T': 5, - '${^TAINT}': 5, - '${^UNICODE}': 5, - '${^UTF8CACHE}': 5, - '${^UTF8LOCALE}': 5, - $PERL_VERSION: 5, - '$^V': 5, - '${^WIN32_SLOPPY_STAT}': 5, - $EXECUTABLE_NAME: 5, - '$^X': 5, - $1: 5, // - regexp $1, $2... - $MATCH: 5, - '$&': 5, - '${^MATCH}': 5, - $PREMATCH: 5, - '$`': 5, - '${^PREMATCH}': 5, - $POSTMATCH: 5, - "$'": 5, - '${^POSTMATCH}': 5, - $LAST_PAREN_MATCH: 5, - '$+': 5, - $LAST_SUBMATCH_RESULT: 5, - '$^N': 5, - '@LAST_MATCH_END': 5, - '@+': 5, - '%LAST_PAREN_MATCH': 5, - '%+': 5, - '@LAST_MATCH_START': 5, - '@-': 5, - '%LAST_MATCH_START': 5, - '%-': 5, - $LAST_REGEXP_CODE_RESULT: 5, - '$^R': 5, - '${^RE_DEBUG_FLAGS}': 5, - '${^RE_TRIE_MAXBUF}': 5, - $ARGV: 5, - '@ARGV': 5, - ARGV: 5, - ARGVOUT: 5, - $OUTPUT_FIELD_SEPARATOR: 5, - $OFS: 5, - '$,': 5, - $INPUT_LINE_NUMBER: 5, - $NR: 5, - '$.': 5, - $INPUT_RECORD_SEPARATOR: 5, - $RS: 5, - '$/': 5, - $OUTPUT_RECORD_SEPARATOR: 5, - $ORS: 5, - '$\\': 5, - $OUTPUT_AUTOFLUSH: 5, - '$|': 5, - $ACCUMULATOR: 5, - '$^A': 5, - $FORMAT_FORMFEED: 5, - '$^L': 5, - $FORMAT_PAGE_NUMBER: 5, - '$%': 5, - $FORMAT_LINES_LEFT: 5, - '$-': 5, - $FORMAT_LINE_BREAK_CHARACTERS: 5, - '$:': 5, - $FORMAT_LINES_PER_PAGE: 5, - '$=': 5, - $FORMAT_TOP_NAME: 5, - '$^': 5, - $FORMAT_NAME: 5, - '$~': 5, - '${^CHILD_ERROR_NATIVE}': 5, - $EXTENDED_OS_ERROR: 5, - '$^E': 5, - $EXCEPTIONS_BEING_CAUGHT: 5, - '$^S': 5, - $WARNING: 5, - '$^W': 5, - '${^WARNING_BITS}': 5, - $OS_ERROR: 5, - $ERRNO: 5, - '$!': 5, - '%OS_ERROR': 5, - '%ERRNO': 5, - '%!': 5, - $CHILD_ERROR: 5, - '$?': 5, - $EVAL_ERROR: 5, - '$@': 5, - $OFMT: 5, - '$#': 5, - '$*': 5, - $ARRAY_BASE: 5, - '$[': 5, - $OLD_PERL_VERSION: 5, - '$]': 5, - // PERL blocks - if: [1, 1], - elsif: [1, 1], - else: [1, 1], - while: [1, 1], - unless: [1, 1], - until: [1, 1], - for: [1, 1], - foreach: [1, 1], - // PERL functions - abs: 1, // - absolute value function - accept: 1, // - accept an incoming socket connect - alarm: 1, // - schedule a SIGALRM - atan2: 1, // - arctangent of Y/X in the range -PI to PI - bind: 1, // - binds an address to a socket - binmode: 1, // - prepare binary files for I/O - bless: 1, // - create an object - bootstrap: 1, // - break: 1, // - break out of a "given" block - caller: 1, // - get context of the current subroutine call - chdir: 1, // - change your current working directory - chmod: 1, // - changes the permissions on a list of files - chomp: 1, // - remove a trailing record separator from a string - chop: 1, // - remove the last character from a string - chown: 1, // - change the ownership on a list of files - chr: 1, // - get character this number represents - chroot: 1, // - make directory new root for path lookups - close: 1, // - close file (or pipe or socket) handle - closedir: 1, // - close directory handle - connect: 1, // - connect to a remote socket - continue: [1, 1], // - optional trailing block in a while or foreach - cos: 1, // - cosine function - crypt: 1, // - one-way passwd-style encryption - dbmclose: 1, // - breaks binding on a tied dbm file - dbmopen: 1, // - create binding on a tied dbm file - default: 1, // - defined: 1, // - test whether a value, variable, or function is defined - delete: 1, // - deletes a value from a hash - die: 1, // - raise an exception or bail out - do: 1, // - turn a BLOCK into a TERM - dump: 1, // - create an immediate core dump - each: 1, // - retrieve the next key/value pair from a hash - endgrent: 1, // - be done using group file - endhostent: 1, // - be done using hosts file - endnetent: 1, // - be done using networks file - endprotoent: 1, // - be done using protocols file - endpwent: 1, // - be done using passwd file - endservent: 1, // - be done using services file - eof: 1, // - test a filehandle for its end - eval: 1, // - catch exceptions or compile and run code - exec: 1, // - abandon this program to run another - exists: 1, // - test whether a hash key is present - exit: 1, // - terminate this program - exp: 1, // - raise I to a power - fcntl: 1, // - file control system call - fileno: 1, // - return file descriptor from filehandle - flock: 1, // - lock an entire file with an advisory lock - fork: 1, // - create a new process just like this one - format: 1, // - declare a picture format with use by the write() function - formline: 1, // - internal function used for formats - getc: 1, // - get the next character from the filehandle - getgrent: 1, // - get next group record - getgrgid: 1, // - get group record given group user ID - getgrnam: 1, // - get group record given group name - gethostbyaddr: 1, // - get host record given its address - gethostbyname: 1, // - get host record given name - gethostent: 1, // - get next hosts record - getlogin: 1, // - return who logged in at this tty - getnetbyaddr: 1, // - get network record given its address - getnetbyname: 1, // - get networks record given name - getnetent: 1, // - get next networks record - getpeername: 1, // - find the other end of a socket connection - getpgrp: 1, // - get process group - getppid: 1, // - get parent process ID - getpriority: 1, // - get current nice value - getprotobyname: 1, // - get protocol record given name - getprotobynumber: 1, // - get protocol record numeric protocol - getprotoent: 1, // - get next protocols record - getpwent: 1, // - get next passwd record - getpwnam: 1, // - get passwd record given user login name - getpwuid: 1, // - get passwd record given user ID - getservbyname: 1, // - get services record given its name - getservbyport: 1, // - get services record given numeric port - getservent: 1, // - get next services record - getsockname: 1, // - retrieve the sockaddr for a given socket - getsockopt: 1, // - get socket options on a given socket - given: 1, // - glob: 1, // - expand filenames using wildcards - gmtime: 1, // - convert UNIX time into record or string using Greenwich time - goto: 1, // - create spaghetti code - grep: 1, // - locate elements in a list test true against a given criterion - hex: 1, // - convert a string to a hexadecimal number - import: 1, // - patch a module's namespace into your own - index: 1, // - find a substring within a string - int: 1, // - get the integer portion of a number - ioctl: 1, // - system-dependent device control system call - join: 1, // - join a list into a string using a separator - keys: 1, // - retrieve list of indices from a hash - kill: 1, // - send a signal to a process or process group - last: 1, // - exit a block prematurely - lc: 1, // - return lower-case version of a string - lcfirst: 1, // - return a string with just the next letter in lower case - length: 1, // - return the number of bytes in a string - link: 1, // - create a hard link in the filesystem - listen: 1, // - register your socket as a server - local: 2, // - create a temporary value for a global variable (dynamic scoping) - localtime: 1, // - convert UNIX time into record or string using local time - lock: 1, // - get a thread lock on a variable, subroutine, or method - log: 1, // - retrieve the natural logarithm for a number - lstat: 1, // - stat a symbolic link - m: null, // - match a string with a regular expression pattern - map: 1, // - apply a change to a list to get back a new list with the changes - mkdir: 1, // - create a directory - msgctl: 1, // - SysV IPC message control operations - msgget: 1, // - get SysV IPC message queue - msgrcv: 1, // - receive a SysV IPC message from a message queue - msgsnd: 1, // - send a SysV IPC message to a message queue - my: 2, // - declare and assign a local variable (lexical scoping) - new: 1, // - next: 1, // - iterate a block prematurely - no: 1, // - unimport some module symbols or semantics at compile time - oct: 1, // - convert a string to an octal number - open: 1, // - open a file, pipe, or descriptor - opendir: 1, // - open a directory - ord: 1, // - find a character's numeric representation - our: 2, // - declare and assign a package variable (lexical scoping) - pack: 1, // - convert a list into a binary representation - package: 1, // - declare a separate global namespace - pipe: 1, // - open a pair of connected filehandles - pop: 1, // - remove the last element from an array and return it - pos: 1, // - find or set the offset for the last/next m//g search - print: 1, // - output a list to a filehandle - printf: 1, // - output a formatted list to a filehandle - prototype: 1, // - get the prototype (if any) of a subroutine - push: 1, // - append one or more elements to an array - q: null, // - singly quote a string - qq: null, // - doubly quote a string - qr: null, // - Compile pattern - quotemeta: null, // - quote regular expression magic characters - qw: null, // - quote a list of words - qx: null, // - backquote quote a string - rand: 1, // - retrieve the next pseudorandom number - read: 1, // - fixed-length buffered input from a filehandle - readdir: 1, // - get a directory from a directory handle - readline: 1, // - fetch a record from a file - readlink: 1, // - determine where a symbolic link is pointing - readpipe: 1, // - execute a system command and collect standard output - recv: 1, // - receive a message over a Socket - redo: 1, // - start this loop iteration over again - ref: 1, // - find out the type of thing being referenced - rename: 1, // - change a filename - require: 1, // - load in external functions from a library at runtime - reset: 1, // - clear all variables of a given name - return: 1, // - get out of a function early - reverse: 1, // - flip a string or a list - rewinddir: 1, // - reset directory handle - rindex: 1, // - right-to-left substring search - rmdir: 1, // - remove a directory - s: null, // - replace a pattern with a string - say: 1, // - print with newline - scalar: 1, // - force a scalar context - seek: 1, // - reposition file pointer for random-access I/O - seekdir: 1, // - reposition directory pointer - select: 1, // - reset default output or do I/O multiplexing - semctl: 1, // - SysV semaphore control operations - semget: 1, // - get set of SysV semaphores - semop: 1, // - SysV semaphore operations - send: 1, // - send a message over a socket - setgrent: 1, // - prepare group file for use - sethostent: 1, // - prepare hosts file for use - setnetent: 1, // - prepare networks file for use - setpgrp: 1, // - set the process group of a process - setpriority: 1, // - set a process's nice value - setprotoent: 1, // - prepare protocols file for use - setpwent: 1, // - prepare passwd file for use - setservent: 1, // - prepare services file for use - setsockopt: 1, // - set some socket options - shift: 1, // - remove the first element of an array, and return it - shmctl: 1, // - SysV shared memory operations - shmget: 1, // - get SysV shared memory segment identifier - shmread: 1, // - read SysV shared memory - shmwrite: 1, // - write SysV shared memory - shutdown: 1, // - close down just half of a socket connection - sin: 1, // - return the sine of a number - sleep: 1, // - block for some number of seconds - socket: 1, // - create a socket - socketpair: 1, // - create a pair of sockets - sort: 1, // - sort a list of values - splice: 1, // - add or remove elements anywhere in an array - split: 1, // - split up a string using a regexp delimiter - sprintf: 1, // - formatted print into a string - sqrt: 1, // - square root function - srand: 1, // - seed the random number generator - stat: 1, // - get a file's status information - state: 1, // - declare and assign a state variable (persistent lexical scoping) - study: 1, // - optimize input data for repeated searches - sub: 1, // - declare a subroutine, possibly anonymously - substr: 1, // - get or alter a portion of a string - symlink: 1, // - create a symbolic link to a file - syscall: 1, // - execute an arbitrary system call - sysopen: 1, // - open a file, pipe, or descriptor - sysread: 1, // - fixed-length unbuffered input from a filehandle - sysseek: 1, // - position I/O pointer on handle used with sysread and syswrite - system: 1, // - run a separate program - syswrite: 1, // - fixed-length unbuffered output to a filehandle - tell: 1, // - get current seekpointer on a filehandle - telldir: 1, // - get current seekpointer on a directory handle - tie: 1, // - bind a variable to an object class - tied: 1, // - get a reference to the object underlying a tied variable - time: 1, // - return number of seconds since 1970 - times: 1, // - return elapsed time for self and child processes - tr: null, // - transliterate a string - truncate: 1, // - shorten a file - uc: 1, // - return upper-case version of a string - ucfirst: 1, // - return a string with just the next letter in upper case - umask: 1, // - set file creation mode mask - undef: 1, // - remove a variable or function definition - unlink: 1, // - remove one link to a file - unpack: 1, // - convert binary structure into normal perl variables - unshift: 1, // - prepend more elements to the beginning of a list - untie: 1, // - break a tie binding to a variable - use: 1, // - load in a module at compile time - utime: 1, // - set a file's last access and modify times - values: 1, // - return a list of the values in a hash - vec: 1, // - test or set particular bits in a string - wait: 1, // - wait for any child process to die - waitpid: 1, // - wait for a particular child process to die - wantarray: 1, // - get void vs scalar vs list context of current subroutine call - warn: 1, // - print debugging info - when: 1, // - write: 1, // - print a picture record - y: null // - transliterate a string - }; - // PG Keywords and Variables - const PGstyle = 'atom'; - const PGkeyword = 'keyword'; - const PGcmds = new Set([ - 'DOCUMENT', - 'ENDDOCUMENT', - 'loadMacros', - 'TEXT', - 'SOLUTION', - 'HINT', - 'STATEMENT', - 'COMMENT', - 'MODES', - 'htmlLink', - 'helpLink', - 'knowlLink', - 'image', - 'Context', - 'Compute', - 'Real', - 'Formula', - 'String', - 'List', - 'Complex', - 'Point', - 'Vector', - 'Matrix', - 'Interval', - 'Set', - 'Fraction', - 'ANS', - 'NAMED_ANS', - 'WEIGHTED_ANS', - 'MultiAnswer', - 'Value', - 'random', - 'list_random', - 'non_zero_random', - 'NchooseK' - ]); - const PGvars = new Set([ - 'BR', - 'RBR', - 'PAR', - 'LQ', - 'RQ', - 'BM', - 'EM', - 'BDM', - 'EDM', - 'LTS', - 'GTS', - 'LTE', - 'GTE', - 'BEGIN_ONE_COLUMN', - 'END_ONE_COLUMN', - 'SOL', - 'SOLUTION', - 'HINT', - 'COMMENT', - 'US', - 'SPACE', - 'NBSP', - 'NDASH', - 'MDASH', - 'BLABEL', - 'ELABEL', - 'BBOLD', - 'EBOLD', - 'BITALIC', - 'EITALIC', - 'BUL', - 'EUL', - 'BCENTER', - 'ECENTER', - 'BLTR', - 'ELTR', - 'BKBD', - 'EKBD', - 'HR', - 'LBRACE', - 'RBRACE', - 'LB', - 'RB', - 'DOLLAR', - 'PERCENT', - 'CARET', - 'PI', - 'E', - 'LATEX', - 'TEX', - 'APOS', - 'showPartialCorrectAnswers', - 'refreshCachedImages', - 'ITEM', - 'ITEMSEP' - ]); - - const RXstyle = 'string-2'; - const RXmodifiers = /[goseximacplud]/; // NOTE: "m", "s", "y" and "tr" need to correct real modifiers for each regexp type - - function tokenChain(stream, state, chain, style, tail, tokener) { - // NOTE: chain.length > 2 is not working now (it's for s[...][...]geos;) - state.chain = null; // 12 3tail - state.style = null; - state.tail = null; - state.tokenize = function (stream, state) { - var e = false, - c, - i = 0; - while ((c = stream.next())) { - if (c === chain[i] && !e) { - if (chain[++i] !== undefined) { - state.chain = chain[i]; - state.style = style; - state.tail = tail; - } else if (tail) stream.eatWhile(tail); - state.tokenize = tokener || tokenPerl; - return style; - } - e = !e && c == '\\'; - } - return style; - }; - return state.tokenize(stream, state); - } - - function tokenSOMETHING(stream, state, string) { - state.tokenize = function (stream, state) { - if (stream.string == string) state.tokenize = tokenPerl; - stream.skipToEnd(); - return 'string'; - }; - return state.tokenize(stream, state); - } - - // EV3 block formatting - function tokenEV3(stream, state, string, style, prevState) { - state.tokenize = function (stream, state) { - if (prevState && prevState.mode == 'math') { - var reg = new RegExp('^\\\\' + string); - } else { - var reg = new RegExp('^' + string); - } - if (stream.match(reg)) { - if (!prevState) { - state.tokenize = tokenPerl; - return PGstyle; - } else { - state.tokenize = function (stream, state) { - return tokenEV3(stream, state, prevState.string, prevState.style, prevState.prevState); - }; - } - if (string.includes('BOLD')) return PGstyle + ' strong'; - if (string.includes('ITALIC')) return PGstyle + ' em'; - if (prevState.endstyle) return prevState.endstyle; - return style; - } else { - state.tokenize = function (stream, state) { - return tokenEV3(stream, state, string, style, prevState); - }; - } - - const newPrevState = {}; - if (prevState) { - newPrevState.prevState = JSON.parse(JSON.stringify(prevState)); - } else { - newPrevState.prevState = null; - } - newPrevState.style = style; - newPrevState.string = string; - - if (prevState && prevState.mode == 'cmd') { - // Some additional formatting for perl code blocks - newPrevState.mode = 'cmd'; - if (stream.match(/^[$@%]{/)) { - // ${, @{, %{ nested blocks - style = 'variable'; - state.tokenize = function (stream, state) { - return tokenEV3(stream, state, '\\}', style, newPrevState); - }; - return style; - } - if (stream.match(/^\(/)) { - // Nested ( ) blocks - newPrevState.endstyle = 'variable'; - state.tokenize = function (stream, state) { - return tokenEV3(stream, state, '\\)', style, newPrevState); - }; - return 'variable'; - } - if (stream.match(/^\[/)) { - // Nested [ ] blocks - newPrevState.endstyle = 'variable'; - state.tokenize = function (stream, state) { - return tokenEV3(stream, state, '\\]', style, newPrevState); - }; - return 'variable'; - } - if (stream.match(/^\{/)) { - // Nested { } blocks - newPrevState.endstyle = 'variable'; - state.tokenize = function (stream, state) { - return tokenEV3(stream, state, '\\}', style, newPrevState); - }; - return 'variable'; - } - if (stream.match(/^\w+/)) { - // Check for PG keywords - if (PGcmds.has(stream.current())) return PGkeyword; - else return style; - } - if (stream.match(/^['"]/)) { - // Quotes - return tokenChain(stream, state, [stream.current()], 'string', null, function (stream, state) { - tokenEV3(stream, state, string, style, prevState); - }); - } - if (stream.match(/^[=,;/\*><%&|.~?:+/-]/)) { - // Catch some perl operators - return 'variable'; - } - } - - if (stream.match(/^\\\(/)) { - // \(...\) TeX block - if (prevState) { - style = 'error'; - } else { - style = 'comment'; - } - state.tokenize = function (stream, state) { - newPrevState.mode = 'math'; - return tokenEV3(stream, state, '\\)', style, newPrevState); - }; - } else if (stream.match(/^\\\[/)) { - // \[...\] TeX block - if (prevState) { - style = 'error'; - } else { - style = 'comment'; - } - state.tokenize = function (stream, state) { - newPrevState.mode = 'math'; - return tokenEV3(stream, state, '\\]', style, newPrevState); - }; - } else if (stream.match(/^\\{/)) { - // \{...\} Perl code block - if (prevState) { - style = 'error'; - } else { - style = 'variable-2'; - } - state.tokenize = function (stream, state) { - newPrevState.mode = 'cmd'; - return tokenEV3(stream, state, '\\\\}', style, newPrevState); - }; - } else if (stream.match(/^``/)) { - // ``...`` math object math - if (prevState && prevState.mode != 'math') { - style = 'error'; - } else { - style = 'variable-3'; - } - state.tokenize = function (stream, state) { - return tokenEV3(stream, state, '``', style, newPrevState); - }; - } else if (stream.match(/^`/)) { - // `...` math object math - if (prevState && prevState.mode != 'math') { - style = 'error'; - } else { - style = 'variable-3'; - } - state.tokenize = function (stream, state) { - return tokenEV3(stream, state, '`([^`]|$)', style, newPrevState, 'tick'); - }; - } else if (stream.match(/^(\$BBOLD|\${BBOLD})/)) { - // Bold - style = style = ' strong'; - state.tokenize = function (stream, state) { - return tokenEV3(stream, state, '(\\$EBOLD|\\${EBOLD})', style, newPrevState); - }; - return PGstyle + ' strong'; - } else if (stream.match(/^(\$BITALIC|\${BITALIC})/)) { - // Italic - style = style + ' em'; - state.tokenize = function (stream, state) { - return tokenEV3(stream, state, '(\\$EITALIC|\\${EITALIC})', style, newPrevState); - }; - return PGstyle + ' em'; - } else if (stream.match(/^[$@%]\w+/)) { - // PG Variables - if (PGvars.has(stream.current().substring(1))) return PGstyle; - return 'variable'; - } else if (stream.match(/^[$@%]{\w+}/)) { - // ${foo} PG variables - if (PGvars.has(stream.current().slice(2, -1))) return PGstyle; - return 'variable'; - } else if (stream.match(/^ +$/)) { - // Trailing white space - return 'trailingspace'; - } else if (stream.match(/^[\[\]\\ (){}$@%`]/)) { - // Advance a single character if special - return style; - } else { - // Otherwise advance through all non special characters - if (prevState && prevState.mode == 'cmd') { - // Only eat through words in perl code mode - if (stream.match(/\w+/)) return style; - else stream.next(); - } else { - stream.eatWhile(/[^\[\]\\ (){}$@%`]/); - } - } - return style; - }; - return state.tokenize(stream, state); - } - - // No additional formatting inside comment block, only looks for end string. - // Currently only used for comments and ``` code blocks. - // The final stream.match and stream.eatWhile may need updated if used for other blocks. - function tokenPGMLComment(stream, state, string, style, prevState) { - state.tokenize = function (stream, state) { - var reg = new RegExp('^' + string); - if (stream.match(reg)) { - state.tokenize = function (stream, state) { - return tokenPGML(stream, state, prevState.string, prevState.style, prevState.prevState); - }; - return style; - } else { - state.tokenize = function (stream, state) { - return tokenPGMLComment(stream, state, string, style, prevState); - }; - } - if (stream.match(/^[\]%`]/)) return style; - stream.eatWhile(/[^\]%`]/); - return style; - }; - return state.tokenize(stream, state); - } - - // PGML subblock which has limited formatting options compared to main block. - // This block nests {} and [] blocks, for correct pairing in variables and commands. - function tokenPGMLSubBlock(stream, state, string, style, prevState) { - state.tokenize = function (stream, state) { - var reg = new RegExp('^' + string); - if (stream.match(reg)) { - // Needed to ensure ': ' verbatim lines exit out if ended with a secondary subblock. - if (stream.eol() && prevState.subblock && prevState.prevState && prevState.prevState.stopeol) { - state.tokenize = function (stream, state) { - return tokenPGMLSubBlock( - stream, - state, - prevState.prevState.string, - prevState.prevState.style, - prevState.prevState.prevState - ); - }; - } else if (stream.eol() && prevState.prevState && prevState.prevState.stopeol) { - state.tokenize = function (stream, state) { - return tokenPGML( - stream, - state, - prevState.prevState.string, - prevState.prevState.style, - prevState.prevState.prevState - ); - }; - } else if (prevState.subblock) { - state.tokenize = function (stream, state) { - return tokenPGMLSubBlock( - stream, - state, - prevState.string, - prevState.style, - prevState.prevState - ); - }; - } else { - state.tokenize = function (stream, state) { - return tokenPGML(stream, state, prevState.string, prevState.style, prevState.prevState); - }; - } - if (prevState.mode == 'var' || prevState.mode == 'cmd') stream.match(/^\*{1,3}([^\*]|$)/); - if (prevState.mode == 'calc') { - if (!stream.match(/^\*(\s|$)/)) stream.match(/^\{.+\}/); - } - if (prevState.endstyle) return prevState.endstyle; - return style; - } else { - state.tokenize = function (stream, state) { - return tokenPGMLSubBlock(stream, state, string, style, prevState); - }; - } - - var newPrevState = {}; - if (prevState) { - newPrevState.prevState = JSON.parse(JSON.stringify(prevState)); - } else { - newPrevState.prevState = null; - } - newPrevState.style = style; - newPrevState.string = string; - newPrevState.subblock = true; - if (prevState.mode) newPrevState.mode = prevState.mode; - - if (prevState.mode == 'cmd') { - // Some formatting for [@ ... @] blocks - if (stream.match(/^[$@%]\w+/)) { - // $, @, % variables - if (PGvars.has(stream.current().substring(1))) return PGstyle; - else return 'variable'; - } - if (stream.match(/^[$@%]{\w+}/)) { - // ${foo}, @{foo}, %{foo} variables - if (PGvars.has(stream.current().slice(2, -1))) return PGstyle; - else return 'variable'; - } - if (stream.match(/^[$@%]{/)) { - // ${, @{, %{ nested blocks - style = 'variable'; - state.tokenize = function (stream, state) { - return tokenPGMLSubBlock(stream, state, '\\}', style, newPrevState); - }; - return style; - } - if (stream.match(/^\(/)) { - // Nested ( ) blocks - newPrevState.endstyle = 'variable'; - state.tokenize = function (stream, state) { - return tokenPGMLSubBlock(stream, state, '\\)', style, newPrevState); - }; - return 'variable'; - } - if (stream.match(/^\w+/)) { - // Check for PG keywords - if (PGcmds.has(stream.current())) return PGkeyword; - else return style; - } - if (stream.match(/^['"]/)) { - // Quotes - return tokenChain(stream, state, [stream.current()], 'string', null, function (stream, state) { - tokenPGMLSubBlock(stream, state, string, style, prevState); - }); - } - if (stream.match(/^[=,;/\*><%$&|.~?:]/)) - // Catch some perl operators - return 'variable'; - } - - if (stream.match(/^\[\$/)) { - // Variable - const p = stream.pos; - if (stream.match(/^\w+/) && PGvars.has(stream.current().substring(2)) && stream.eat(']')) { - stream.match(/^\*{1,3}/); - return PGstyle; - } else { - stream.pos = p; - } - style = 'variable'; - state.tokenize = function (stream, state) { - newPrevState.mode = 'var'; - return tokenPGMLSubBlock(stream, state, '\\]', style, newPrevState); - }; - } else if (stream.match(/^\[/)) { - // Nested [ ] blocks - if (prevState.mode == 'cmd') newPrevState.endstyle = 'variable'; - state.tokenize = function (stream, state) { - return tokenPGMLSubBlock(stream, state, '\\]', style, newPrevState); - }; - if (prevState.mode == 'cmd') return 'variable'; - } else if (stream.match(/^\{/)) { - // Nested { } blocks - if (prevState.mode == 'cmd') newPrevState.endstyle = 'variable'; - state.tokenize = function (stream, state) { - return tokenPGMLSubBlock(stream, state, '\\}', style, newPrevState); - }; - if (prevState.mode == 'cmd') return 'variable'; - } else if (stream.match(/^\w+\s*/)) { - // Grab next word before going forward - return style; - } else { - // Catchall to advanced one character if no match was found. - stream.eat(/./); - } - return style; - }; - return state.tokenize(stream, state); - } - - // Main PGML block. Can nest to allow subblocks with PGML formatting in them. - function tokenPGML(stream, state, string, style, prevState) { - state.tokenize = function (stream, state) { - var reg = new RegExp('^' + string); - if (stream.match(reg)) { - if (!prevState) { - state.tokenize = tokenPerl; - return PGkeyword; - // Needed to ensure ': ' verbatim lines exit out if ended with a secondary block. - } else if (stream.eol() && prevState.prevState && prevState.prevState.stopeol) { - state.tokenize = function (stream, state) { - return tokenPGML( - stream, - state, - prevState.prevState.string, - prevState.prevState.style, - prevState.prevState.prevState - ); - }; - } else { - state.tokenize = function (stream, state) { - return tokenPGML(stream, state, prevState.string, prevState.style, prevState.prevState); - }; - } - return style; - } else { - state.tokenize = function (stream, state) { - return tokenPGML(stream, state, string, style, prevState); - }; - } - - var newPrevState = {}; - if (prevState) { - newPrevState.prevState = JSON.parse(JSON.stringify(prevState)); - } else { - newPrevState.prevState = null; - } - newPrevState.style = style; - newPrevState.string = string; - - if (stream.sol()) { - if ( - stream.match(/^ *(>> +)?[ivxlIVXL]+[.)] /) || - stream.match(/^ *(>> +)?\d+[.)] /) || - stream.match(/^ *(>> +)?\w[.)] /) || - stream.match(/^ *(>> +)?[*\-+o] /) - ) { - // Lists - return 'atom strong'; - } - if (stream.match(/^ *[\-=]{3,}/)) { - // Rules - stream.match(/^\{[^}]*\}/); - stream.match(/^\{[^}]*\}/); - return 'hr'; - } - if (stream.match(/^ *(>> +)?#{1,}.*$/)) - // Headers - return 'header'; - if (stream.match(/^ *>> /)) - // Justification - return 'atom strong'; - if (stream.match(/^ *: /)) { - // Single line verbatim - style = 'tag'; - state.tokenize = function (stream, state) { - newPrevState.stopeol = true; - return tokenPGML(stream, state, '.$', style, newPrevState); - }; - return style; - } - } - - if (stream.match(/^\[:{1,3}/)) { - // Algebra notation math - style = 'variable-3'; - const endstring = stream.current().substring(1) + '\\]'; - state.tokenize = function (stream, state) { - newPrevState.mode = 'calc'; - return tokenPGMLSubBlock(stream, state, endstring, style, newPrevState); - }; - } else if (stream.match(/^\[`{1,3}/)) { - // TeX notation math - style = 'comment'; - const endstring = stream.current().substring(1) + '\\]'; - state.tokenize = function (stream, state) { - newPrevState.mode = 'tex'; - return tokenPGMLSubBlock(stream, state, endstring, style, newPrevState); - }; - } else if (stream.match(/^\[\|+/)) { - // Verbatim - style = 'tag'; - const endstring = stream.current().substring(1) + '\\]'; - state.tokenize = function (stream, state) { - return tokenPGML(stream, state, endstring, style, newPrevState); - }; - } else if (!prevState && stream.match(/^```/)) { - // Multiline verbatim / code - style = 'tag'; - state.tokenize = function (stream, state) { - return tokenPGMLComment(stream, state, '```', style, newPrevState); - }; - } else if (stream.match(/^\[%/)) { - // Comment - style = 'comment'; - state.tokenize = function (stream, state) { - return tokenPGMLComment(stream, state, '%\\]', style, newPrevState); - }; - } else if (stream.match(/^\[@/)) { - // Perl code - style = 'variable-2'; - state.tokenize = function (stream, state) { - newPrevState.mode = 'cmd'; - return tokenPGMLSubBlock(stream, state, '@\\]', style, newPrevState); - }; - } else if (stream.match(/^\[\$/)) { - // Variable - const p = stream.pos; - if (stream.match(/^[\w\d_]+/) && PGvars.has(stream.current().substring(2)) && stream.eat(']')) { - stream.match(/^\*{1,3}/); - return PGstyle; - } else { - stream.pos = p; - } - style = 'variable'; - state.tokenize = function (stream, state) { - newPrevState.mode = 'var'; - return tokenPGMLSubBlock(stream, state, '\\]', style, newPrevState); - }; - } else if (stream.match(/^\[_+\]/)) { - // Answer blank - if (stream.match(/^\*?\{/)) { - state.tokenize = function (stream, state) { - return tokenPGMLSubBlock(stream, state, '\\}', 'builtin', newPrevState); - }; - } - return 'builtin'; - } else if (stream.match(/<< *$/)) { - // Justification - return 'atom strong'; - } else if (stream.match(/^(\*_|_\*)\w/)) { - // Bold and italic - style = style + ' strong em'; - const endstring = (stream.current().charAt(1) + stream.current().charAt(0)).replace(/\*/, '\\*'); - state.tokenize = function (stream, state) { - return tokenPGML(stream, state, endstring, style, newPrevState); - }; - } else if (stream.match(/^\*{1,3}\w/)) { - // Bold - style = style + ' strong'; - const endstring = stream.current().slice(0, -1).replace(/\*/g, '\\*'); - state.tokenize = function (stream, state) { - return tokenPGML(stream, state, endstring, style, newPrevState); - }; - } else if (stream.match(/^_{1,3}\w/)) { - // Italic - style = style + ' em'; - const endstring = stream.current().slice(0, -1); - state.tokenize = function (stream, state) { - return tokenPGML(stream, state, endstring, style, newPrevState); - }; - } else if (stream.match(/^ +$/)) { - // Trailing whitespace - return 'trailingspace'; - } else if (stream.match(/[A-Za-z0-9]+\s*/)) { - // Grab next word before going forward - return style; - } else { - // Catchall to advanced one character if no match was found. - stream.eat(/./); - } - return style; - }; - - return state.tokenize(stream, state); - } - - function tokenPerl(stream, state) { - if (stream.eatSpace()) return null; - if (state.chain) return tokenChain(stream, state, state.chain, state.style, state.tail); - if ( - stream.match( - /^(\-?((\d[\d_]*)?\.\d+(e[+-]?\d+)?|\d+\.\d*)|0x[\da-fA-F_]+|0b[01_]+|\d[\d_]*(e[+-]?\d+)?)/ - ) - ) - return 'number'; - if (stream.match(/^<<(?=[_a-zA-Z])/)) { - // NOTE: <'], RXstyle, RXmodifiers); - } - if (/[\^'"!~\/]/.test(c)) { - eatSuffix(stream, 1); - return tokenChain(stream, state, [stream.eat(c)], RXstyle, RXmodifiers); - } - } else if (c == 'q') { - c = look(stream, 1); - if (c == '(') { - eatSuffix(stream, 2); - return tokenChain(stream, state, [')'], 'string'); - } - if (c == '[') { - eatSuffix(stream, 2); - return tokenChain(stream, state, [']'], 'string'); - } - if (c == '{') { - eatSuffix(stream, 2); - return tokenChain(stream, state, ['}'], 'string'); - } - if (c == '<') { - eatSuffix(stream, 2); - return tokenChain(stream, state, ['>'], 'string'); - } - if (/[\^'"!~\/]/.test(c)) { - eatSuffix(stream, 1); - return tokenChain(stream, state, [stream.eat(c)], 'string'); - } - } else if (c == 'w') { - c = look(stream, 1); - if (c == '(') { - eatSuffix(stream, 2); - return tokenChain(stream, state, [')'], 'bracket'); - } - if (c == '[') { - eatSuffix(stream, 2); - return tokenChain(stream, state, [']'], 'bracket'); - } - if (c == '{') { - eatSuffix(stream, 2); - return tokenChain(stream, state, ['}'], 'bracket'); - } - if (c == '<') { - eatSuffix(stream, 2); - return tokenChain(stream, state, ['>'], 'bracket'); - } - if (/[\^'"!~\/]/.test(c)) { - eatSuffix(stream, 1); - return tokenChain(stream, state, [stream.eat(c)], 'bracket'); - } - } else if (c == 'r') { - c = look(stream, 1); - if (c == '(') { - eatSuffix(stream, 2); - return tokenChain(stream, state, [')'], RXstyle, RXmodifiers); - } - if (c == '[') { - eatSuffix(stream, 2); - return tokenChain(stream, state, [']'], RXstyle, RXmodifiers); - } - if (c == '{') { - eatSuffix(stream, 2); - return tokenChain(stream, state, ['}'], RXstyle, RXmodifiers); - } - if (c == '<') { - eatSuffix(stream, 2); - return tokenChain(stream, state, ['>'], RXstyle, RXmodifiers); - } - if (/[\^'"!~\/]/.test(c)) { - eatSuffix(stream, 1); - return tokenChain(stream, state, [stream.eat(c)], RXstyle, RXmodifiers); - } - } else if (/[\^'"!~\/(\[{<]/.test(c)) { - if (c == '(') { - eatSuffix(stream, 1); - return tokenChain(stream, state, [')'], 'string'); - } - if (c == '[') { - eatSuffix(stream, 1); - return tokenChain(stream, state, [']'], 'string'); - } - if (c == '{') { - eatSuffix(stream, 1); - return tokenChain(stream, state, ['}'], 'string'); - } - if (c == '<') { - eatSuffix(stream, 1); - return tokenChain(stream, state, ['>'], 'string'); - } - if (/[\^'"!~\/]/.test(c)) { - return tokenChain(stream, state, [stream.eat(c)], 'string'); - } - } - } - } - if (ch == 'm') { - var c = look(stream, -2); - if (!(c && /\w/.test(c))) { - c = stream.eat(/[(\[{<\^'"!~\/]/); - if (c) { - if (/[\^'"!~\/]/.test(c)) { - return tokenChain(stream, state, [c], RXstyle, RXmodifiers); - } - if (c == '(') { - return tokenChain(stream, state, [')'], RXstyle, RXmodifiers); - } - if (c == '[') { - return tokenChain(stream, state, [']'], RXstyle, RXmodifiers); - } - if (c == '{') { - return tokenChain(stream, state, ['}'], RXstyle, RXmodifiers); - } - if (c == '<') { - return tokenChain(stream, state, ['>'], RXstyle, RXmodifiers); - } - } - } - } - if (ch == 's') { - var c = /[\/>\]})\w]/.test(look(stream, -2)); - if (!c) { - c = stream.eat(/[(\[{<\^'"!~\/]/); - if (c) { - if (c == '[') return tokenChain(stream, state, [']', ']'], RXstyle, RXmodifiers); - if (c == '{') return tokenChain(stream, state, ['}', '}'], RXstyle, RXmodifiers); - if (c == '<') return tokenChain(stream, state, ['>', '>'], RXstyle, RXmodifiers); - if (c == '(') return tokenChain(stream, state, [')', ')'], RXstyle, RXmodifiers); - return tokenChain(stream, state, [c, c], RXstyle, RXmodifiers); - } - } - } - if (ch == 'y') { - var c = /[\/>\]})\w]/.test(look(stream, -2)); - if (!c) { - c = stream.eat(/[(\[{<\^'"!~\/]/); - if (c) { - if (c == '[') return tokenChain(stream, state, [']', ']'], RXstyle, RXmodifiers); - if (c == '{') return tokenChain(stream, state, ['}', '}'], RXstyle, RXmodifiers); - if (c == '<') return tokenChain(stream, state, ['>', '>'], RXstyle, RXmodifiers); - if (c == '(') return tokenChain(stream, state, [')', ')'], RXstyle, RXmodifiers); - return tokenChain(stream, state, [c, c], RXstyle, RXmodifiers); - } - } - } - if (ch == 't') { - var c = /[\/>\]})\w]/.test(look(stream, -2)); - if (!c) { - c = stream.eat('r'); - if (c) { - c = stream.eat(/[(\[{<\^'"!~\/]/); - if (c) { - if (c == '[') return tokenChain(stream, state, [']', ']'], RXstyle, RXmodifiers); - if (c == '{') return tokenChain(stream, state, ['}', '}'], RXstyle, RXmodifiers); - if (c == '<') return tokenChain(stream, state, ['>', '>'], RXstyle, RXmodifiers); - if (c == '(') return tokenChain(stream, state, [')', ')'], RXstyle, RXmodifiers); - return tokenChain(stream, state, [c, c], RXstyle, RXmodifiers); - } - } - } - } - if (ch == '`') { - return tokenChain(stream, state, [ch], 'variable-2'); - } - if (ch == '/') { - if (!/~\s*$/.test(prefix(stream))) return 'operator'; - else return tokenChain(stream, state, [ch], RXstyle, RXmodifiers); - } - if (ch == '$') { - var p = stream.pos; - if (stream.eatWhile(/\w/) && PGvars.has(stream.current().substring(1))) return PGstyle; - else stream.pos = p; - if (stream.eatWhile(/\d/) || (stream.eat('{') && stream.eatWhile(/\d/) && stream.eat('}'))) - return 'variable-2'; - else stream.pos = p; - } - if (/[$@%]/.test(ch)) { - var p = stream.pos; - if ( - (stream.eat('^') && stream.eat(/[A-Z]/)) || - (!/[@$%&]/.test(look(stream, -2)) && stream.eat(/[=|\\\-#?@;:&`~\^!\[\]*'"$+.,\/<>()]/)) - ) { - var c = stream.current(); - if (PERL[c]) return 'variable-2'; - } - stream.pos = p; - } - if (/[$@%&]/.test(ch)) { - if (stream.eatWhile(/[\w$]/) || (stream.eat('{') && stream.eatWhile(/[\w$]/) && stream.eat('}'))) { - var c = stream.current(); - if (PERL[c]) return 'variable-2'; - else return 'variable'; - } - } - if (ch == '#') { - if (look(stream, -2) != '$') { - stream.skipToEnd(); - return 'comment'; - } - } - if (ch == '-' && look(stream, -2) != ' ' && stream.match(/>\w+/)) return 'variable'; - if (/[:+\-\^*$&%@=<>!?|\/~\.]/.test(ch)) { - var p = stream.pos; - stream.eatWhile(/[:+\-\^*$&%@=<>!?|\/~\.]/); - if (PERL[stream.current()]) return 'operator'; - else stream.pos = p; - } - if (ch == '_') { - if (stream.pos == 1) { - if (suffix(stream, 6) == '_END__') { - return tokenChain(stream, state, ['\0'], 'comment'); - } else if (suffix(stream, 7) == '_DATA__') { - return tokenChain(stream, state, ['\0'], 'variable-2'); - } else if (suffix(stream, 7) == '_C__') { - return tokenChain(stream, state, ['\0'], 'string'); - } - } - } - if (/\w/.test(ch)) { - var p = stream.pos; - if ( - look(stream, -2) == '{' && - (look(stream, 0) == '}' || (stream.eatWhile(/\w/) && look(stream, 0) == '}')) - ) - return 'string'; - else stream.pos = p; - if (stream.match(/\w* *=>/)) return 'string'; - } - if (/[A-Z]/.test(ch)) { - var l = look(stream, -2); - var p = stream.pos; - stream.eatWhile(/[A-Z_]/); - if (/[\da-z]/.test(look(stream, 0))) { - stream.pos = p; - } else { - var c = PERL[stream.current()]; - var isPG = PGcmds.has(stream.current()); - if (!c && !isPG) return 'meta'; - if (isPG) return PGkeyword; - if (c[1]) c = c[0]; - if (l != ':') { - if (c == 1) return 'keyword'; - else if (c == 2) return 'def'; - else if (c == 3) return 'atom'; - else if (c == 4) return 'operator'; - else if (c == 5) return 'variable-2'; - else return 'meta'; - } else return 'meta'; - } - } - if (/[a-zA-Z_]/.test(ch)) { - var l = look(stream, -2); - stream.eatWhile(/\w/); - var c = PERL[stream.current()]; - var isPG = PGcmds.has(stream.current()); - if (!c && !isPG) return 'meta'; - if (isPG) return PGkeyword; - if (c[1]) c = c[0]; - if (l != ':') { - if (c == 1) return 'keyword'; - else if (c == 2) return 'def'; - else if (c == 3) return 'atom'; - else if (c == 4) return 'operator'; - else if (c == 5) return 'variable-2'; - else return 'meta'; - } else return 'meta'; - } - return null; - } - - return { - startState: function () { - return { - tokenize: tokenPerl, - chain: null, - style: null, - tail: null - }; - }, - token: function (stream, state) { - return (state.tokenize || tokenPerl)(stream, state); - }, - lineComment: '#' - }; - }); - - CodeMirror.registerHelper('wordChars', 'perl', /[\w$]/); - - CodeMirror.registerHelper('fold', 'PG', (cm, start) => { - const m1 = - /^\s*BEGIN_(PGML|PGML_SOLUTION|PGML_HINT|TEXT)\s*$/.exec(cm.getLine(start.line)) || - /^\s*[$\w]*\s*->\s*BEGIN_(TIKZ|LATEX_IMAGE)\s*$/.exec(cm.getLine(start.line)); - const m2 = /^\s*(Section|Scaffold)::Begin/.exec(cm.getLine(start.line)); - if (m1 || m2) { - for (let current_line = start.line + 1; current_line <= cm.lineCount(); ++current_line) { - const end_re = m1 ? RegExp(`END_${m1[1]}`) : RegExp(`${m2[1]}::End`); - if (end_re.test(cm.getLine(current_line))) { - return { - from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), - to: CodeMirror.Pos(current_line, cm.getLine(current_line).length) - }; - } - } - } - return; - }); - - CodeMirror.defineMIME('text/x-perl', 'perl'); - - // it's like "peek", but need for look-ahead or look-behind if index < 0 - function look(stream, c) { - return stream.string.charAt(stream.pos + (c || 0)); - } - - // return a part of prefix of current stream from current position - function prefix(stream, c) { - if (c) { - var x = stream.pos - c; - return stream.string.substr(x >= 0 ? x : 0, c); - } else { - return stream.string.substr(0, stream.pos - 1); - } - } - - // return a part of suffix of current stream from current position - function suffix(stream, c) { - var y = stream.string.length; - var x = y - stream.pos + 1; - return stream.string.substr(stream.pos, c && c < y ? c : x); - } - - // eating and vomiting a part of stream from current position - function eatSuffix(stream, c) { - var x = stream.pos + c; - var y; - if (x <= 0) stream.pos = 0; - else if (x >= (y = stream.string.length - 1)) stream.pos = y; - else stream.pos = x; - } -})(); diff --git a/htdocs/js/PGCodeMirror/comment.js b/htdocs/js/PGCodeMirror/comment.js deleted file mode 100644 index 356b9ce146..0000000000 --- a/htdocs/js/PGCodeMirror/comment.js +++ /dev/null @@ -1,287 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/5/LICENSE - -(function (mod) { - if (typeof exports == 'object' && typeof module == 'object') - // CommonJS - mod(require('../../lib/codemirror')); - else if (typeof define == 'function' && define.amd) - // AMD - define(['../../lib/codemirror'], mod); - // Plain browser env - else mod(CodeMirror); -})(function (CodeMirror) { - 'use strict'; - - var noOptions = {}; - var nonWS = /[^\s\u00a0]/; - var Pos = CodeMirror.Pos, - cmp = CodeMirror.cmpPos; - - function firstNonWS(str) { - var found = str.search(nonWS); - return found == -1 ? 0 : found; - } - - CodeMirror.commands.toggleComment = function (cm) { - cm.toggleComment(); - }; - - CodeMirror.defineExtension('toggleComment', function (options) { - if (!options) options = noOptions; - var cm = this; - var minLine = Infinity, - ranges = this.listSelections(), - mode = null; - for (var i = ranges.length - 1; i >= 0; i--) { - var from = ranges[i].from(), - to = ranges[i].to(); - if (from.line >= minLine) continue; - if (to.line >= minLine) to = Pos(minLine, 0); - minLine = from.line; - if (mode == null) { - if (cm.uncomment(from, to, options)) mode = 'un'; - else { - cm.lineComment(from, to, options); - mode = 'line'; - } - } else if (mode == 'un') { - cm.uncomment(from, to, options); - } else { - cm.lineComment(from, to, options); - } - } - }); - - // Rough heuristic to try and detect lines that are part of multi-line string - function probablyInsideString(cm, pos, line) { - return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line); - } - - const md_section = RegExp( - 'DESCRIPTION|KEYWORDS|DBsubject|DBchapter|DBsection|Date|Author|Institution ' + - '|MO|Static|TitleText|EditionText|AuthorText|Section|Problem|Language|Level' - ); - - // Custom version of getMode for PG files. - function getMode(cm, pos) { - const mode = cm.getModeAt(pos); - // Clear any comment fields of mode. - delete mode.lineComment; - delete mode.blockCommentStart; - delete mode.blockCommentEnd; - - if (md_section.test(cm.getLine(pos.line)) || insideDescriptionBlock(cm, pos)) { - mode.lineComment = '##'; - mode.name = 'PG_meta'; - } else if (inPGMLBlock(cm, pos)) { - mode.name = 'PGML'; - mode.blockCommentStart = '[%'; - mode.blockCommentEnd = '%]'; - } else if (inTikzBlock(cm, pos)) { - mode.lineComment = '%'; - mode.name = 'tikz'; - } else { - mode.name = 'perl'; - mode.lineComment = '#'; - } - return mode; - } - - function insideDescriptionBlock(cm, pos) { - for (let line = pos.line; line >= 0; --line) { - if (/ENDDESCRIPTION/.test(cm.getLine(line))) return false; - if (/DESCRIPTION/.test(cm.getLine(line))) return true; - } - return false; - } - - function inTikzBlock(cm, pos) { - for (let line = pos.line; line >= 0; --line) { - if (/BEGIN_TIKZ|BEGIN_LATEX_IMAGE/.test(cm.getLine(line))) return true; - if (/END_PGML|END_TIKZ|END_LATEX_IMAGE/.test(cm.getLine(line))) return false; - } - return false; - } - - function inPGMLBlock(cm, pos) { - for (let line = pos.line; line >= 0; --line) { - if (/BEGIN_PGML/.test(cm.getLine(line))) return true; - if (/END_PGML/.test(cm.getLine(line))) return false; - } - return false; - } - - CodeMirror.defineExtension('lineComment', function (from, to, options) { - if (!options) options = noOptions; - var self = this, - mode = getMode(self, from); - var firstLine = self.getLine(from.line); - if (firstLine == null || probablyInsideString(self, from, firstLine)) return; - - var commentString = options.lineComment || mode.lineComment; - if (!commentString) { - if (options.blockCommentStart || mode.blockCommentStart) { - options.fullLines = true; - self.blockComment(from, to, options); - } - return; - } - - var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1); - var pad = options.padding == null ? ' ' : options.padding; - var blankLines = options.commentBlankLines || from.line == to.line; - - self.operation(function () { - if (options.indent) { - var baseString = null; - for (var i = from.line; i < end; ++i) { - var line = self.getLine(i); - var whitespace = line.search(nonWS) === -1 ? line : line.slice(0, firstNonWS(line)); - if (baseString == null || baseString.length > whitespace.length) { - baseString = whitespace; - } - } - for (var i = from.line; i < end; ++i) { - var line = self.getLine(i), - cut = baseString.length; - if (!blankLines && !nonWS.test(line)) continue; - if (line.slice(0, cut) != baseString) cut = firstNonWS(line); - self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut)); - } - } else { - for (var i = from.line; i < end; ++i) { - if (blankLines || nonWS.test(self.getLine(i))) self.replaceRange(commentString + pad, Pos(i, 0)); - } - } - }); - }); - - CodeMirror.defineExtension('blockComment', function (from, to, options) { - if (!options) options = noOptions; - var self = this, - mode = getMode(self, from); - var startString = options.blockCommentStart || mode.blockCommentStart; - var endString = options.blockCommentEnd || mode.blockCommentEnd; - if (!startString || !endString) { - if ((options.lineComment || mode.lineComment) && options.fullLines != false) - self.lineComment(from, to, options); - return; - } - if (/\bcomment\b/.test(self.getTokenTypeAt(Pos(from.line, 0)))) return; - - var end = Math.min(to.line, self.lastLine()); - if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end; - - var pad = options.padding == null ? ' ' : options.padding; - if (from.line > end) return; - - self.operation(function () { - if (options.fullLines != false) { - var lastLineHasText = nonWS.test(self.getLine(end)); - self.replaceRange(pad + endString, Pos(end)); - self.replaceRange(startString + pad, Pos(from.line, 0)); - var lead = options.blockCommentLead || mode.blockCommentLead; - if (lead != null) - for (var i = from.line + 1; i <= end; ++i) - if (i != end || lastLineHasText) self.replaceRange(lead + pad, Pos(i, 0)); - } else { - var atCursor = cmp(self.getCursor('to'), to) == 0, - empty = !self.somethingSelected(); - self.replaceRange(endString, to); - if (atCursor) self.setSelection(empty ? to : self.getCursor('from'), to); - self.replaceRange(startString, from); - } - }); - }); - - CodeMirror.defineExtension('uncomment', function (from, to, options) { - if (!options) options = noOptions; - var self = this, - mode = getMode(self, from); - var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), - start = Math.min(from.line, end); - - // Try finding line comments - var lineString = options.lineComment || mode.lineComment, - lines = []; - var pad = options.padding == null ? ' ' : options.padding, - didSomething; - lineComment: { - if (!lineString) break lineComment; - for (var i = start; i <= end; ++i) { - var line = self.getLine(i); - var found = line.indexOf(lineString); - if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1; - if (found == -1 && nonWS.test(line)) break lineComment; - if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment; - lines.push(line); - } - self.operation(function () { - for (var i = start; i <= end; ++i) { - var line = lines[i - start]; - var pos = line.indexOf(lineString), - endPos = pos + lineString.length; - if (pos < 0) continue; - if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length; - didSomething = true; - self.replaceRange('', Pos(i, pos), Pos(i, endPos)); - } - }); - if (didSomething) return true; - } - - // Try block comments - var startString = options.blockCommentStart || mode.blockCommentStart; - var endString = options.blockCommentEnd || mode.blockCommentEnd; - if (!startString || !endString) return false; - var lead = options.blockCommentLead || mode.blockCommentLead; - var startLine = self.getLine(start), - open = startLine.indexOf(startString); - if (open == -1) return false; - var endLine = end == start ? startLine : self.getLine(end); - var close = endLine.indexOf(endString, end == start ? open + startString.length : 0); - var insideStart = Pos(start, open + 1), - insideEnd = Pos(end, close + 1); - if ( - close == -1 || - !/comment/.test(self.getTokenTypeAt(insideStart)) || - !/comment/.test(self.getTokenTypeAt(insideEnd)) || - self.getRange(insideStart, insideEnd, '\n').indexOf(endString) > -1 - ) - return false; - - // Avoid killing block comments completely outside the selection. - // Positions of the last startString before the start of the selection, and the first endString after it. - var lastStart = startLine.lastIndexOf(startString, from.ch); - var firstEnd = - lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length); - if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false; - // Positions of the first endString after the end of the selection, and the last startString before it. - firstEnd = endLine.indexOf(endString, to.ch); - var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch); - lastStart = firstEnd == -1 || almostLastStart == -1 ? -1 : to.ch + almostLastStart; - if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false; - - self.operation(function () { - self.replaceRange( - '', - Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)), - Pos(end, close + endString.length) - ); - var openEnd = open + startString.length; - if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length; - self.replaceRange('', Pos(start, open), Pos(start, openEnd)); - if (lead) - for (var i = start + 1; i <= end; ++i) { - var line = self.getLine(i), - found = line.indexOf(lead); - if (found == -1 || nonWS.test(line.slice(0, found))) continue; - var foundEnd = found + lead.length; - if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length; - self.replaceRange('', Pos(i, found), Pos(i, foundEnd)); - } - }); - return true; - }); -}); diff --git a/htdocs/js/PGCodeMirror/pgeditor.js b/htdocs/js/PGCodeMirror/pgeditor.js index 286cf45247..85209e4eba 100644 --- a/htdocs/js/PGCodeMirror/pgeditor.js +++ b/htdocs/js/PGCodeMirror/pgeditor.js @@ -12,139 +12,18 @@ * Artistic License for more details. */ -(async function () { - if (!CodeMirror) return; +(async () => { + const editorContainer = document.querySelector('.code-mirror-editor'); + if (!PGCodeMirrorEditor || !editorContainer) return; - const loadResource = async (src) => { - return new Promise((resolve, reject) => { - let shouldAppend = false; - let el; - if (/\.js(?:\?[0-9a-zA-Z=^.]*)?$/.exec(src)) { - el = document.querySelector(`script[src="${src}"]`); - if (!el) { - el = document.createElement('script'); - el.async = false; - el.src = src; - shouldAppend = true; - } - } else if (/\.css(?:\?[0-9a-zA-Z=^.]*)?$/.exec(src)) { - el = document.querySelector(`link[href="${src}"]`); - if (!el) { - el = document.createElement('link'); - el.rel = 'stylesheet'; - el.href = src; - shouldAppend = true; - } - } else { - reject(); - return; - } + const editorInput = document.getElementsByName(editorContainer.id)[0]; - if (el.dataset.loaded) { - resolve(); - return; - } + const cm = (webworkConfig.pgCodeMirror = new PGCodeMirrorEditor.View(editorContainer, { + source: editorInput?.value ?? '', + language: editorContainer.dataset.language ?? 'pg' + })); - el.addEventListener('error', reject); - el.addEventListener('abort', reject); - el.addEventListener('load', () => { - if (el) el.dataset.loaded = 'true'; - resolve(); - }); + new ResizeObserver(() => cm.refresh('window-resize')).observe(editorContainer); - if (shouldAppend) document.head.appendChild(el); - }); - }; - - const loadConfig = async (file) => { - const configName = - [...file.matchAll(/.*\/([^.]*?)(?:\.min)?\.(?:js|css)(?:\?[0-9a-zA-Z=^.]*)?$/g)][0]?.[1] ?? 'default'; - if (configName !== 'default') { - try { - await loadResource(file); - } catch { - return 'default'; - } - } - return configName; - }; - - const mode = document.querySelector('.codeMirrorEditor')?.dataset.mode ?? 'PG'; - const options = { - mode, - indentUnit: 4, - tabMode: 'spaces', - lineNumbers: true, - lineWrapping: true, - extraKeys: { - Tab: (cm) => cm.execCommand('insertSoftTab'), - 'Shift-Ctrl-F': (cm) => cm.foldCode(cm.getCursor(), { scanUp: true }), - 'Shift-Cmd-F': (cm) => cm.foldCode(cm.getCursor(), { scanUp: true }), - 'Shift-Ctrl-A': (cm) => CodeMirror.commands.foldAll(cm), - 'Shift-Cmd-A': (cm) => CodeMirror.commands.foldAll(cm), - 'Shift-Ctrl-G': (cm) => CodeMirror.commands.unfoldAll(cm), - 'Shift-Cmd-G': (cm) => CodeMirror.commands.unfoldAll(cm) - }, - highlightSelectionMatches: { annotateScrollbar: true }, - matchBrackets: true, - inputStyle: 'contenteditable', - spellcheck: localStorage.getItem('WW_PGEditor_spellcheck') === 'true', - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] - }; - - if (mode === 'PG') { - options.extraKeys['Ctrl-/'] = (cm) => cm.execCommand('toggleComment'); - options.extraKeys['Cmd-/'] = (cm) => cm.execCommand('toggleComment'); - options.foldGutter = { rangeFinder: new CodeMirror.fold.combine(CodeMirror.fold.PG) }; - options.fold = 'PG'; - } else { - options.foldGutter = true; - } - - const cm = (webworkConfig.pgCodeMirror = CodeMirror.fromTextArea( - document.querySelector('.codeMirrorEditor'), - options - )); - cm.setSize('100%', '550px'); - - // Refresh the CodeMirror instance anytime the containing div resizes so that if line wrapping changes, - // the mouse cursor will still go to the correct place when the user clicks on the CodeMirror window. - new ResizeObserver(() => cm.refresh()).observe(document.querySelector('.CodeMirror')); - - const currentThemeFile = localStorage.getItem('WW_PGEditor_selected_theme') ?? 'default'; - const currentThemeName = await loadConfig(currentThemeFile); - cm.setOption('theme', currentThemeName); - - const currentKeymapFile = localStorage.getItem('WW_PGEditor_selected_keymap') ?? 'default'; - const currentKeymapName = await loadConfig(currentKeymapFile); - cm.setOption('keyMap', currentKeymapName); - - const selectTheme = document.getElementById('selectTheme'); - selectTheme.value = currentThemeName === 'default' ? 'default' : currentThemeFile; - selectTheme.addEventListener('change', async () => { - const themeName = await loadConfig(selectTheme.value); - cm.setOption('theme', themeName); - localStorage.setItem('WW_PGEditor_selected_theme', themeName === 'default' ? 'default' : selectTheme.value); - }); - - const selectKeymap = document.getElementById('selectKeymap'); - selectKeymap.value = currentKeymapName === 'default' ? 'default' : currentKeymapFile; - selectKeymap.addEventListener('change', async () => { - const keymapName = await loadConfig(selectKeymap.value); - cm.setOption('keyMap', keymapName); - localStorage.setItem('WW_PGEditor_selected_keymap', keymapName === 'default' ? 'default' : selectKeymap.value); - }); - - const enableSpell = document.getElementById('enableSpell'); - enableSpell.checked = localStorage.getItem('WW_PGEditor_spellcheck') === 'true'; - enableSpell.addEventListener('change', () => { - cm.setOption('spellcheck', enableSpell.checked); - localStorage.setItem('WW_PGEditor_spellcheck', enableSpell.checked); - cm.focus(); - }); - - const forceRTL = document.getElementById('forceRTL'); - forceRTL.addEventListener('change', () => { - cm.setOption('direction', forceRTL.checked ? 'rtl' : 'ltr'); - }); + editorInput?.form.addEventListener('submit', () => (editorInput.value = cm.source)); })(); diff --git a/htdocs/js/PGCodeMirror/pgeditor.scss b/htdocs/js/PGCodeMirror/pgeditor.scss index 1fd132c9fc..f37a1aabef 100644 --- a/htdocs/js/PGCodeMirror/pgeditor.scss +++ b/htdocs/js/PGCodeMirror/pgeditor.scss @@ -12,14 +12,33 @@ * Artistic License for more details. */ -.CodeMirror { +.code-mirror-editor { border: 1px solid #ddd; min-height: 400px; + overflow: auto; resize: vertical; + height: 600px; + + .cm-editor { + height: 100%; + + .cm-scroller { + height: 100%; + + .cm-content { + height: 100%; + min-height: 400px; + } + } + + .cm-panels { + z-index: 18; + } + } } -// This style is only used if the CodeMirror editor is disabled in localOverrides.conf. -.codeMirrorEditor { +// This style is used if the CodeMirror editor is disabled in localOverrides.conf. +.text-area-editor { border: 1px solid #ddd; padding: 2px; height: 550px; @@ -27,38 +46,3 @@ width: 100%; resize: vertical; } - -// Additional CSS for codemirror addons and overrides - -// CodeMirror overrides -.CodeMirror-code { - outline: none; -} - -pre.CodeMirror-line { - unicode-bidi: embed; -} - -// Match Highligher CSS -.CodeMirror-focused { - .cm-matchhighlight { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==); - background-position: bottom; - background-repeat: repeat-x; - } -} - -.cm-matchhighlight { - background-color: lightgreen; -} - -.CodeMirror-selection-highlight-scrollbar { - background-color: green; -} - -// CSS to highlight trailing whitespace in PGML blocks -.cm-trailingspace { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==); - background-position: bottom left; - background-repeat: repeat-x; -} diff --git a/htdocs/js/PGProblemEditor/pgproblemeditor.js b/htdocs/js/PGProblemEditor/pgproblemeditor.js index f7e419d47b..a5ce979a5c 100644 --- a/htdocs/js/PGProblemEditor/pgproblemeditor.js +++ b/htdocs/js/PGProblemEditor/pgproblemeditor.js @@ -48,7 +48,7 @@ request_object.rpc_command = 'saveFile'; request_object.outputFilePath = document.getElementsByName('temp_file_path')[0]?.value ?? ''; request_object.fileContents = - webworkConfig?.pgCodeMirror?.getValue() ?? document.getElementById('problemContents')?.value ?? ''; + webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? ''; if (!request_object.outputFilePath) return; @@ -119,7 +119,7 @@ request_object.rpc_command = 'tidyPGCode'; request_object.pgCode = - webworkConfig?.pgCodeMirror?.getValue() ?? document.getElementById('problemContents')?.value ?? ''; + webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? ''; fetch(webserviceURL, { method: 'post', mode: 'same-origin', body: new URLSearchParams(request_object) }) .then((response) => response.json()) @@ -141,7 +141,7 @@ if (request_object.pgCode === data.result_data.tidiedPGCode) { showMessage('There were no changes to the code.', true); } else { - if (webworkConfig?.pgCodeMirror) webworkConfig.pgCodeMirror.setValue(data.result_data.tidiedPGCode); + if (webworkConfig?.pgCodeMirror) webworkConfig.pgCodeMirror.source = data.result_data.tidiedPGCode; else document.getElementById('problemContents').value = data.result_data.tidiedPGCode; saveTempFile(); showMessage('Successfuly perltidied code.', true); @@ -161,7 +161,7 @@ request_object.rpc_command = 'convertCodeToPGML'; request_object.pgCode = - webworkConfig?.pgCodeMirror?.getValue() ?? document.getElementById('problemContents')?.value ?? ''; + webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? ''; fetch(webserviceURL, { method: 'post', mode: 'same-origin', body: new URLSearchParams(request_object) }) .then((response) => response.json()) @@ -169,7 +169,7 @@ if (request_object.pgCode === data.result_data.pgmlCode) { showMessage('There were no changes to the code.', true); } else { - if (webworkConfig?.pgCodeMirror) webworkConfig.pgCodeMirror.setValue(data.result_data.pgmlCode); + if (webworkConfig?.pgCodeMirror) webworkConfig.pgCodeMirror.source = data.result_data.pgmlCode; else document.getElementById('problemContents').value = data.result_data.pgmlCode; saveTempFile(); showMessage('Successfully converted code to PGML', true); @@ -252,9 +252,9 @@ const renderArea = document.getElementById('pgedit-render-area'); const fileType = document.getElementsByName('file_type')[0]?.value; - // This is either the div created by the CodeMirror editor or the problemContents textarea in the case that + // This is either the div containing the CodeMirror editor or the problemContents textarea in the case that // CodeMirror is disabled in localOverrides.conf. - const editorArea = document.querySelector('.CodeMirror') ?? document.getElementById('problemContents'); + const editorArea = document.querySelector('.code-mirror-editor') ?? document.getElementById('problemContents'); // Add hot key, ctrl-enter, to render the problem editorArea.addEventListener('keydown', async (e) => { @@ -279,8 +279,7 @@ if (window.getComputedStyle(renderArea).getPropertyValue('height') !== `${height}px`) renderArea.style.height = `${height}px`; if (window.getComputedStyle(editorArea).getPropertyValue('height') !== `${height}px`) { - if (webworkConfig?.pgCodeMirror) webworkConfig.pgCodeMirror.setSize('100%', `${height}px`); - else editorArea.style.height = `${height}px`; + editorArea.style.height = `${height}px`; } } } @@ -321,7 +320,7 @@ const requestData = new URLSearchParams(new FormData(problemForm)); requestData.set( 'rawProblemSource', - webworkConfig?.pgCodeMirror?.getValue() ?? document.getElementById('problemContents')?.value ?? '' + webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? '' ); requestData.set('send_pg_flags', 1); requestData.set(button.name, button.value); @@ -352,7 +351,7 @@ } if (fileType === 'course_info') { - const contents = webworkConfig?.pgCodeMirror?.getValue(); + const contents = webworkConfig?.pgCodeMirror?.source; if (contents) renderArea.innerHTML = `
${contents}
`; else renderArea.innerHTML = @@ -370,7 +369,7 @@ } if (fileType === 'hardcopy_theme') { - const contents = webworkConfig?.pgCodeMirror?.getValue(); + const contents = webworkConfig?.pgCodeMirror?.source; if (contents) { renderArea.innerHTML = '
' + contents.replace(/&/g, '&').replace(/';
 				} else
@@ -396,9 +395,7 @@
 					problemSeed: document.getElementById('action_view_seed_id')?.value ?? 1,
 					sourceFilePath: document.getElementsByName('edit_file_path')[0]?.value,
 					rawProblemSource:
-						webworkConfig?.pgCodeMirror?.getValue() ??
-						document.getElementById('problemContents')?.value ??
-						'',
+						webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? '',
 					outputformat: 'simple',
 					showAnswerNumbers: 0,
 					// The set id is really only needed by set headers to get the correct dates for the set.
@@ -508,9 +505,7 @@
 					problemSeed: document.getElementById('action_hardcopy_seed_id')?.value ?? 1,
 					sourceFilePath: document.getElementsByName('edit_file_path')[0]?.value,
 					rawProblemSource:
-						webworkConfig?.pgCodeMirror?.getValue() ??
-						document.getElementById('problemContents')?.value ??
-						'',
+						webworkConfig?.pgCodeMirror?.source ?? document.getElementById('problemContents')?.value ?? '',
 					outputformat: document.getElementById('action_hardcopy_format_id')?.value ?? 'pdf',
 					hardcopy_theme: document.getElementById('action_hardcopy_theme_id')?.value ?? 'oneColumn',
 					// The set id is really only needed by set headers to get the correct dates for the set.
diff --git a/htdocs/js/System/system.scss b/htdocs/js/System/system.scss
index 6f60b1b4ab..18164250ce 100644
--- a/htdocs/js/System/system.scss
+++ b/htdocs/js/System/system.scss
@@ -809,7 +809,7 @@ input.changed[type='text'] {
 #pgedit-render-area {
 	border: 1px solid #ddd;
 	min-height: 400px;
-	height: 550px;
+	height: 600px;
 	resize: vertical;
 	display: flex;
 	flex-direction: column;
diff --git a/htdocs/package-lock.json b/htdocs/package-lock.json
index 3b186ab181..b439141302 100644
--- a/htdocs/package-lock.json
+++ b/htdocs/package-lock.json
@@ -8,8 +8,8 @@
             "license": "GPL-2.0+",
             "dependencies": {
                 "@fortawesome/fontawesome-free": "^6.5.2",
+                "@openwebwork/pg-codemirror-editor": "^0.0.1-beta.6",
                 "bootstrap": "~5.3.3",
-                "codemirror": "^5.65.15",
                 "flatpickr": "^4.6.13",
                 "iframe-resizer": "^4.3.11",
                 "jquery": "^3.7.1",
@@ -31,6 +31,203 @@
                 "yargs": "^17.7.2"
             }
         },
+        "../../../../home/rice/Projects/Javascript/CodeMirror/pg-codemirror-editor": {
+            "name": "@openwebwork/pg-codemirror-editor",
+            "version": "0.0.1-beta.1",
+            "extraneous": true,
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/lang-html": "^6.4.9",
+                "@codemirror/lang-xml": "^6.1.0",
+                "@codemirror/theme-one-dark": "^6.1.2",
+                "@openwebwork/codemirror-lang-pg": "^0.0.1-beta.2",
+                "@replit/codemirror-emacs": "^6.1.0",
+                "@replit/codemirror-vim": "^6.2.1",
+                "cm6-theme-basic-dark": "^0.2.0",
+                "cm6-theme-basic-light": "^0.2.0",
+                "cm6-theme-gruvbox-dark": "^0.2.0",
+                "cm6-theme-gruvbox-light": "^0.2.0",
+                "cm6-theme-material-dark": "^0.2.0",
+                "cm6-theme-nord": "^0.2.0",
+                "cm6-theme-solarized-dark": "^0.2.0",
+                "cm6-theme-solarized-light": "^0.2.0",
+                "codemirror": "^6.0.1",
+                "codemirror-lang-perl": "^0.1.3",
+                "thememirror": "^2.0.1"
+            },
+            "devDependencies": {
+                "@awmottaz/prettier-plugin-void-html": "^1.6.1",
+                "@stylistic/eslint-plugin": "^2.7.2",
+                "css-loader": "^7.1.2",
+                "eslint": "^9.9.1",
+                "eslint-config-prettier": "^9.1.0",
+                "eslint-webpack-plugin": "^4.2.0",
+                "prettier": "^3.3.3",
+                "sass": "^1.78.0",
+                "sass-loader": "^16.0.1",
+                "style-loader": "^4.0.0",
+                "ts-loader": "^9.5.1",
+                "typescript": "^5.5.4",
+                "typescript-eslint": "^8.4.0",
+                "webpack": "^5.94.0",
+                "webpack-cli": "^5.1.4",
+                "webpack-dev-server": "^5.1.0"
+            }
+        },
+        "node_modules/@codemirror/autocomplete": {
+            "version": "6.18.1",
+            "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz",
+            "integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.17.0",
+                "@lezer/common": "^1.0.0"
+            },
+            "peerDependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/common": "^1.0.0"
+            }
+        },
+        "node_modules/@codemirror/commands": {
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.0.tgz",
+            "integrity": "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.4.0",
+                "@codemirror/view": "^6.27.0",
+                "@lezer/common": "^1.1.0"
+            }
+        },
+        "node_modules/@codemirror/lang-css": {
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz",
+            "integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/autocomplete": "^6.0.0",
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@lezer/common": "^1.0.2",
+                "@lezer/css": "^1.1.7"
+            }
+        },
+        "node_modules/@codemirror/lang-html": {
+            "version": "6.4.9",
+            "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
+            "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/autocomplete": "^6.0.0",
+                "@codemirror/lang-css": "^6.0.0",
+                "@codemirror/lang-javascript": "^6.0.0",
+                "@codemirror/language": "^6.4.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.17.0",
+                "@lezer/common": "^1.0.0",
+                "@lezer/css": "^1.1.0",
+                "@lezer/html": "^1.3.0"
+            }
+        },
+        "node_modules/@codemirror/lang-javascript": {
+            "version": "6.2.2",
+            "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
+            "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/autocomplete": "^6.0.0",
+                "@codemirror/language": "^6.6.0",
+                "@codemirror/lint": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.17.0",
+                "@lezer/common": "^1.0.0",
+                "@lezer/javascript": "^1.0.0"
+            }
+        },
+        "node_modules/@codemirror/lang-xml": {
+            "version": "6.1.0",
+            "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
+            "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/autocomplete": "^6.0.0",
+                "@codemirror/language": "^6.4.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/common": "^1.0.0",
+                "@lezer/xml": "^1.0.0"
+            }
+        },
+        "node_modules/@codemirror/language": {
+            "version": "6.10.3",
+            "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz",
+            "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.23.0",
+                "@lezer/common": "^1.1.0",
+                "@lezer/highlight": "^1.0.0",
+                "@lezer/lr": "^1.0.0",
+                "style-mod": "^4.0.0"
+            }
+        },
+        "node_modules/@codemirror/lint": {
+            "version": "6.8.2",
+            "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz",
+            "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "crelt": "^1.0.5"
+            }
+        },
+        "node_modules/@codemirror/search": {
+            "version": "6.5.6",
+            "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
+            "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "crelt": "^1.0.5"
+            }
+        },
+        "node_modules/@codemirror/state": {
+            "version": "6.4.1",
+            "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
+            "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==",
+            "license": "MIT"
+        },
+        "node_modules/@codemirror/theme-one-dark": {
+            "version": "6.1.2",
+            "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
+            "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/highlight": "^1.0.0"
+            }
+        },
+        "node_modules/@codemirror/view": {
+            "version": "6.34.1",
+            "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz",
+            "integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/state": "^6.4.0",
+                "style-mod": "^4.1.0",
+                "w3c-keyname": "^2.2.4"
+            }
+        },
         "node_modules/@fortawesome/fontawesome-free": {
             "version": "6.5.2",
             "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz",
@@ -98,6 +295,110 @@
                 "@jridgewell/sourcemap-codec": "^1.4.14"
             }
         },
+        "node_modules/@lezer/common": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.2.tgz",
+            "integrity": "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw==",
+            "license": "MIT"
+        },
+        "node_modules/@lezer/css": {
+            "version": "1.1.9",
+            "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz",
+            "integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==",
+            "license": "MIT",
+            "dependencies": {
+                "@lezer/common": "^1.2.0",
+                "@lezer/highlight": "^1.0.0",
+                "@lezer/lr": "^1.0.0"
+            }
+        },
+        "node_modules/@lezer/highlight": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
+            "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
+            "license": "MIT",
+            "dependencies": {
+                "@lezer/common": "^1.0.0"
+            }
+        },
+        "node_modules/@lezer/html": {
+            "version": "1.3.10",
+            "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
+            "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
+            "license": "MIT",
+            "dependencies": {
+                "@lezer/common": "^1.2.0",
+                "@lezer/highlight": "^1.0.0",
+                "@lezer/lr": "^1.0.0"
+            }
+        },
+        "node_modules/@lezer/javascript": {
+            "version": "1.4.19",
+            "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz",
+            "integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==",
+            "license": "MIT",
+            "dependencies": {
+                "@lezer/common": "^1.2.0",
+                "@lezer/highlight": "^1.1.3",
+                "@lezer/lr": "^1.3.0"
+            }
+        },
+        "node_modules/@lezer/lr": {
+            "version": "1.4.2",
+            "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
+            "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
+            "license": "MIT",
+            "dependencies": {
+                "@lezer/common": "^1.0.0"
+            }
+        },
+        "node_modules/@lezer/xml": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.5.tgz",
+            "integrity": "sha512-VFouqOzmUWfIg+tfmpcdV33ewtK+NSwd4ngSe1aG7HFb4BN0ExyY1b8msp+ndFrnlG4V4iC8yXacjFtrwERnaw==",
+            "license": "MIT",
+            "dependencies": {
+                "@lezer/common": "^1.2.0",
+                "@lezer/highlight": "^1.0.0",
+                "@lezer/lr": "^1.0.0"
+            }
+        },
+        "node_modules/@openwebwork/codemirror-lang-pg": {
+            "version": "0.0.1-beta.5",
+            "resolved": "https://registry.npmjs.org/@openwebwork/codemirror-lang-pg/-/codemirror-lang-pg-0.0.1-beta.5.tgz",
+            "integrity": "sha512-7TAI1h/V9+2pwhM/AJtj5HZyDHDqN4ly1dEiW1klaXbv4AX0aYQr8aCU9PYrUOEUJnWH7JKWgh26ft/Ti+r71g==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/language": "^6.10.2",
+                "@lezer/highlight": "^1.2.1",
+                "@lezer/lr": "^1.4.2"
+            }
+        },
+        "node_modules/@openwebwork/pg-codemirror-editor": {
+            "version": "0.0.1-beta.7",
+            "resolved": "https://registry.npmjs.org/@openwebwork/pg-codemirror-editor/-/pg-codemirror-editor-0.0.1-beta.7.tgz",
+            "integrity": "sha512-ILrr+/8hX0HYLvfg6LD1sOJt44073Wofus187K39p6RWBArXGvZ2GQvHCwReVxj+z0CzHnbJ4lKFyVF+UEY+SA==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/lang-html": "^6.4.9",
+                "@codemirror/lang-xml": "^6.1.0",
+                "@codemirror/theme-one-dark": "^6.1.2",
+                "@openwebwork/codemirror-lang-pg": "^0.0.1-beta.5",
+                "@replit/codemirror-emacs": "^6.1.0",
+                "@replit/codemirror-vim": "^6.2.1",
+                "cm6-theme-basic-dark": "^0.2.0",
+                "cm6-theme-basic-light": "^0.2.0",
+                "cm6-theme-gruvbox-dark": "^0.2.0",
+                "cm6-theme-gruvbox-light": "^0.2.0",
+                "cm6-theme-material-dark": "^0.2.0",
+                "cm6-theme-nord": "^0.2.0",
+                "cm6-theme-solarized-dark": "^0.2.0",
+                "cm6-theme-solarized-light": "^0.2.0",
+                "codemirror": "^6.0.1",
+                "codemirror-lang-perl": "^0.1.5-beta.1",
+                "thememirror": "^2.0.1"
+            }
+        },
         "node_modules/@popperjs/core": {
             "version": "2.11.8",
             "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -108,6 +409,32 @@
                 "url": "https://opencollective.com/popperjs"
             }
         },
+        "node_modules/@replit/codemirror-emacs": {
+            "version": "6.1.0",
+            "resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz",
+            "integrity": "sha512-74DITnht6Cs6sHg02PQ169IKb1XgtyhI9sLD0JeOFco6Ds18PT+dkD8+DgXBDokne9UIFKsBbKPnpFRAz60/Lw==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/autocomplete": "^6.0.2",
+                "@codemirror/commands": "^6.0.0",
+                "@codemirror/search": "^6.0.0",
+                "@codemirror/state": "^6.0.1",
+                "@codemirror/view": "^6.3.0"
+            }
+        },
+        "node_modules/@replit/codemirror-vim": {
+            "version": "6.2.1",
+            "resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.2.1.tgz",
+            "integrity": "sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/commands": "^6.0.0",
+                "@codemirror/language": "^6.1.0",
+                "@codemirror/search": "^6.2.0",
+                "@codemirror/state": "^6.0.1",
+                "@codemirror/view": "^6.0.3"
+            }
+        },
         "node_modules/@trysound/sax": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -356,10 +683,127 @@
                 "node": ">=12"
             }
         },
+        "node_modules/cm6-theme-basic-dark": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-basic-dark/-/cm6-theme-basic-dark-0.2.0.tgz",
+            "integrity": "sha512-+mNNJecRtxS/KkloMDCQF0oTrT6aFGRZTjnBcdT5UG1pcDO4Brq8l1+0KR/8dZ7hub2gOGOzoi3rGFD8GzlH7Q==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/highlight": "^1.0.0"
+            }
+        },
+        "node_modules/cm6-theme-basic-light": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-basic-light/-/cm6-theme-basic-light-0.2.0.tgz",
+            "integrity": "sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/highlight": "^1.0.0"
+            }
+        },
+        "node_modules/cm6-theme-gruvbox-dark": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-gruvbox-dark/-/cm6-theme-gruvbox-dark-0.2.0.tgz",
+            "integrity": "sha512-xyqsG19qV+nb7ZHTMocSNWwZHMExfQxDm0FlbNMqEGKeQR96WryssXJH/IZZQudwrPpWU2dCoyOgMFhti2UTYA==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/highlight": "^1.0.0"
+            }
+        },
+        "node_modules/cm6-theme-gruvbox-light": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-gruvbox-light/-/cm6-theme-gruvbox-light-0.2.0.tgz",
+            "integrity": "sha512-sc4dEMLU5y4F3QGLjwMQs1H3Q0a0ooXA1EvyWnknxLEGQVXwJrxkkV67gs1TqWASl2i63iomt4zyz5pkbfO1yg==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/highlight": "^1.0.0"
+            }
+        },
+        "node_modules/cm6-theme-material-dark": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-material-dark/-/cm6-theme-material-dark-0.2.0.tgz",
+            "integrity": "sha512-H09JZihzg4w0mTtOqo5bQdxItkQWw+ergKlk7BSfwYjaR2nOi+wIN0R+ByAo7bON8GbFODvjTxH3EIqdhovFeA==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/highlight": "^1.0.0"
+            }
+        },
+        "node_modules/cm6-theme-nord": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-nord/-/cm6-theme-nord-0.2.0.tgz",
+            "integrity": "sha512-jTh+5nvl+N/5CtTK7UVcrxDCj2AOStvbNM8uP6tx6amq4QaaLDlapjMw+MNzEkvxcPnHY+YM91tbklS2KNlR2w==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/highlight": "^1.0.0"
+            }
+        },
+        "node_modules/cm6-theme-solarized-dark": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-solarized-dark/-/cm6-theme-solarized-dark-0.2.0.tgz",
+            "integrity": "sha512-FWtYHcX8NLzNSs21yGbkLF+q/5m2u80ug0JytKoI9nMZWPP5dcnsFYp1iZBEegLehiZnpv1qcmTsLTUG2KD39w==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/highlight": "^1.0.0"
+            }
+        },
+        "node_modules/cm6-theme-solarized-light": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-solarized-light/-/cm6-theme-solarized-light-0.2.0.tgz",
+            "integrity": "sha512-Iw7Xv+9A6NlT7sRGlM2pOwD3ZBETkAqpb7c6O0LPj5kjwcK6C3k+mvjzaQt1gzfBErMmhL1HHuK07zICeXkE+w==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/highlight": "^1.0.0"
+            }
+        },
         "node_modules/codemirror": {
-            "version": "5.65.16",
-            "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.16.tgz",
-            "integrity": "sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg=="
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
+            "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/autocomplete": "^6.0.0",
+                "@codemirror/commands": "^6.0.0",
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/lint": "^6.0.0",
+                "@codemirror/search": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0"
+            }
+        },
+        "node_modules/codemirror-lang-perl": {
+            "version": "0.1.5-beta.1",
+            "resolved": "https://registry.npmjs.org/codemirror-lang-perl/-/codemirror-lang-perl-0.1.5-beta.1.tgz",
+            "integrity": "sha512-iAp+G5UHpoQwrtCs1kD5yKuZ/tYYh812aI66FDoJYnOESysr/BlTaoTsgH9u3fskEcA6SRYmB3UqpkxiFH5fvw==",
+            "license": "MIT",
+            "dependencies": {
+                "@codemirror/language": "^6.10.2",
+                "@lezer/highlight": "^1.2.1",
+                "@lezer/lr": "^1.4.2"
+            }
         },
         "node_modules/color-convert": {
             "version": "2.0.1",
@@ -394,6 +838,12 @@
                 "node": ">= 10"
             }
         },
+        "node_modules/crelt": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
+            "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
+            "license": "MIT"
+        },
         "node_modules/css-declaration-sorter": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz",
@@ -1519,6 +1969,12 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/style-mod": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
+            "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
+            "license": "MIT"
+        },
         "node_modules/stylehacks": {
             "version": "6.1.1",
             "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz",
@@ -1584,6 +2040,17 @@
             "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
             "dev": true
         },
+        "node_modules/thememirror": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/thememirror/-/thememirror-2.0.1.tgz",
+            "integrity": "sha512-d5i6FVvWWPkwrm4cHLI3t9AT1OrkAt7Ig8dtdYSofgF7C/eiyNuq6zQzSTusWTde3jpW9WLvA9J/fzNKMUsd0w==",
+            "license": "MIT",
+            "peerDependencies": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0"
+            }
+        },
         "node_modules/to-regex-range": {
             "version": "5.0.1",
             "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -1632,6 +2099,12 @@
             "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
             "dev": true
         },
+        "node_modules/w3c-keyname": {
+            "version": "2.2.8",
+            "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+            "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+            "license": "MIT"
+        },
         "node_modules/wrap-ansi": {
             "version": "7.0.0",
             "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -1687,6 +2160,142 @@
         }
     },
     "dependencies": {
+        "@codemirror/autocomplete": {
+            "version": "6.18.1",
+            "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz",
+            "integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==",
+            "requires": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.17.0",
+                "@lezer/common": "^1.0.0"
+            }
+        },
+        "@codemirror/commands": {
+            "version": "6.7.0",
+            "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.0.tgz",
+            "integrity": "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw==",
+            "requires": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.4.0",
+                "@codemirror/view": "^6.27.0",
+                "@lezer/common": "^1.1.0"
+            }
+        },
+        "@codemirror/lang-css": {
+            "version": "6.3.0",
+            "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz",
+            "integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==",
+            "requires": {
+                "@codemirror/autocomplete": "^6.0.0",
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@lezer/common": "^1.0.2",
+                "@lezer/css": "^1.1.7"
+            }
+        },
+        "@codemirror/lang-html": {
+            "version": "6.4.9",
+            "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
+            "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
+            "requires": {
+                "@codemirror/autocomplete": "^6.0.0",
+                "@codemirror/lang-css": "^6.0.0",
+                "@codemirror/lang-javascript": "^6.0.0",
+                "@codemirror/language": "^6.4.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.17.0",
+                "@lezer/common": "^1.0.0",
+                "@lezer/css": "^1.1.0",
+                "@lezer/html": "^1.3.0"
+            }
+        },
+        "@codemirror/lang-javascript": {
+            "version": "6.2.2",
+            "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
+            "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
+            "requires": {
+                "@codemirror/autocomplete": "^6.0.0",
+                "@codemirror/language": "^6.6.0",
+                "@codemirror/lint": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.17.0",
+                "@lezer/common": "^1.0.0",
+                "@lezer/javascript": "^1.0.0"
+            }
+        },
+        "@codemirror/lang-xml": {
+            "version": "6.1.0",
+            "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
+            "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
+            "requires": {
+                "@codemirror/autocomplete": "^6.0.0",
+                "@codemirror/language": "^6.4.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/common": "^1.0.0",
+                "@lezer/xml": "^1.0.0"
+            }
+        },
+        "@codemirror/language": {
+            "version": "6.10.3",
+            "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz",
+            "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==",
+            "requires": {
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.23.0",
+                "@lezer/common": "^1.1.0",
+                "@lezer/highlight": "^1.0.0",
+                "@lezer/lr": "^1.0.0",
+                "style-mod": "^4.0.0"
+            }
+        },
+        "@codemirror/lint": {
+            "version": "6.8.2",
+            "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz",
+            "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==",
+            "requires": {
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "crelt": "^1.0.5"
+            }
+        },
+        "@codemirror/search": {
+            "version": "6.5.6",
+            "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
+            "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
+            "requires": {
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "crelt": "^1.0.5"
+            }
+        },
+        "@codemirror/state": {
+            "version": "6.4.1",
+            "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
+            "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A=="
+        },
+        "@codemirror/theme-one-dark": {
+            "version": "6.1.2",
+            "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
+            "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
+            "requires": {
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0",
+                "@lezer/highlight": "^1.0.0"
+            }
+        },
+        "@codemirror/view": {
+            "version": "6.34.1",
+            "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz",
+            "integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==",
+            "requires": {
+                "@codemirror/state": "^6.4.0",
+                "style-mod": "^4.1.0",
+                "w3c-keyname": "^2.2.4"
+            }
+        },
         "@fortawesome/fontawesome-free": {
             "version": "6.5.2",
             "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz",
@@ -1741,12 +2350,119 @@
                 "@jridgewell/sourcemap-codec": "^1.4.14"
             }
         },
+        "@lezer/common": {
+            "version": "1.2.2",
+            "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.2.tgz",
+            "integrity": "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="
+        },
+        "@lezer/css": {
+            "version": "1.1.9",
+            "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz",
+            "integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==",
+            "requires": {
+                "@lezer/common": "^1.2.0",
+                "@lezer/highlight": "^1.0.0",
+                "@lezer/lr": "^1.0.0"
+            }
+        },
+        "@lezer/highlight": {
+            "version": "1.2.1",
+            "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
+            "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
+            "requires": {
+                "@lezer/common": "^1.0.0"
+            }
+        },
+        "@lezer/html": {
+            "version": "1.3.10",
+            "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
+            "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
+            "requires": {
+                "@lezer/common": "^1.2.0",
+                "@lezer/highlight": "^1.0.0",
+                "@lezer/lr": "^1.0.0"
+            }
+        },
+        "@lezer/javascript": {
+            "version": "1.4.19",
+            "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz",
+            "integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==",
+            "requires": {
+                "@lezer/common": "^1.2.0",
+                "@lezer/highlight": "^1.1.3",
+                "@lezer/lr": "^1.3.0"
+            }
+        },
+        "@lezer/lr": {
+            "version": "1.4.2",
+            "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
+            "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
+            "requires": {
+                "@lezer/common": "^1.0.0"
+            }
+        },
+        "@lezer/xml": {
+            "version": "1.0.5",
+            "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.5.tgz",
+            "integrity": "sha512-VFouqOzmUWfIg+tfmpcdV33ewtK+NSwd4ngSe1aG7HFb4BN0ExyY1b8msp+ndFrnlG4V4iC8yXacjFtrwERnaw==",
+            "requires": {
+                "@lezer/common": "^1.2.0",
+                "@lezer/highlight": "^1.0.0",
+                "@lezer/lr": "^1.0.0"
+            }
+        },
+        "@openwebwork/codemirror-lang-pg": {
+            "version": "0.0.1-beta.5",
+            "resolved": "https://registry.npmjs.org/@openwebwork/codemirror-lang-pg/-/codemirror-lang-pg-0.0.1-beta.5.tgz",
+            "integrity": "sha512-7TAI1h/V9+2pwhM/AJtj5HZyDHDqN4ly1dEiW1klaXbv4AX0aYQr8aCU9PYrUOEUJnWH7JKWgh26ft/Ti+r71g==",
+            "requires": {
+                "@codemirror/language": "^6.10.2",
+                "@lezer/highlight": "^1.2.1",
+                "@lezer/lr": "^1.4.2"
+            }
+        },
+        "@openwebwork/pg-codemirror-editor": {
+            "version": "0.0.1-beta.7",
+            "resolved": "https://registry.npmjs.org/@openwebwork/pg-codemirror-editor/-/pg-codemirror-editor-0.0.1-beta.7.tgz",
+            "integrity": "sha512-ILrr+/8hX0HYLvfg6LD1sOJt44073Wofus187K39p6RWBArXGvZ2GQvHCwReVxj+z0CzHnbJ4lKFyVF+UEY+SA==",
+            "requires": {
+                "@codemirror/lang-html": "^6.4.9",
+                "@codemirror/lang-xml": "^6.1.0",
+                "@codemirror/theme-one-dark": "^6.1.2",
+                "@openwebwork/codemirror-lang-pg": "^0.0.1-beta.5",
+                "@replit/codemirror-emacs": "^6.1.0",
+                "@replit/codemirror-vim": "^6.2.1",
+                "cm6-theme-basic-dark": "^0.2.0",
+                "cm6-theme-basic-light": "^0.2.0",
+                "cm6-theme-gruvbox-dark": "^0.2.0",
+                "cm6-theme-gruvbox-light": "^0.2.0",
+                "cm6-theme-material-dark": "^0.2.0",
+                "cm6-theme-nord": "^0.2.0",
+                "cm6-theme-solarized-dark": "^0.2.0",
+                "cm6-theme-solarized-light": "^0.2.0",
+                "codemirror": "^6.0.1",
+                "codemirror-lang-perl": "^0.1.5-beta.1",
+                "thememirror": "^2.0.1"
+            }
+        },
         "@popperjs/core": {
             "version": "2.11.8",
             "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
             "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
             "peer": true
         },
+        "@replit/codemirror-emacs": {
+            "version": "6.1.0",
+            "resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz",
+            "integrity": "sha512-74DITnht6Cs6sHg02PQ169IKb1XgtyhI9sLD0JeOFco6Ds18PT+dkD8+DgXBDokne9UIFKsBbKPnpFRAz60/Lw==",
+            "requires": {}
+        },
+        "@replit/codemirror-vim": {
+            "version": "6.2.1",
+            "resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.2.1.tgz",
+            "integrity": "sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==",
+            "requires": {}
+        },
         "@trysound/sax": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -1888,10 +2604,77 @@
                 "wrap-ansi": "^7.0.0"
             }
         },
+        "cm6-theme-basic-dark": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-basic-dark/-/cm6-theme-basic-dark-0.2.0.tgz",
+            "integrity": "sha512-+mNNJecRtxS/KkloMDCQF0oTrT6aFGRZTjnBcdT5UG1pcDO4Brq8l1+0KR/8dZ7hub2gOGOzoi3rGFD8GzlH7Q==",
+            "requires": {}
+        },
+        "cm6-theme-basic-light": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-basic-light/-/cm6-theme-basic-light-0.2.0.tgz",
+            "integrity": "sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==",
+            "requires": {}
+        },
+        "cm6-theme-gruvbox-dark": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-gruvbox-dark/-/cm6-theme-gruvbox-dark-0.2.0.tgz",
+            "integrity": "sha512-xyqsG19qV+nb7ZHTMocSNWwZHMExfQxDm0FlbNMqEGKeQR96WryssXJH/IZZQudwrPpWU2dCoyOgMFhti2UTYA==",
+            "requires": {}
+        },
+        "cm6-theme-gruvbox-light": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-gruvbox-light/-/cm6-theme-gruvbox-light-0.2.0.tgz",
+            "integrity": "sha512-sc4dEMLU5y4F3QGLjwMQs1H3Q0a0ooXA1EvyWnknxLEGQVXwJrxkkV67gs1TqWASl2i63iomt4zyz5pkbfO1yg==",
+            "requires": {}
+        },
+        "cm6-theme-material-dark": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-material-dark/-/cm6-theme-material-dark-0.2.0.tgz",
+            "integrity": "sha512-H09JZihzg4w0mTtOqo5bQdxItkQWw+ergKlk7BSfwYjaR2nOi+wIN0R+ByAo7bON8GbFODvjTxH3EIqdhovFeA==",
+            "requires": {}
+        },
+        "cm6-theme-nord": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-nord/-/cm6-theme-nord-0.2.0.tgz",
+            "integrity": "sha512-jTh+5nvl+N/5CtTK7UVcrxDCj2AOStvbNM8uP6tx6amq4QaaLDlapjMw+MNzEkvxcPnHY+YM91tbklS2KNlR2w==",
+            "requires": {}
+        },
+        "cm6-theme-solarized-dark": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-solarized-dark/-/cm6-theme-solarized-dark-0.2.0.tgz",
+            "integrity": "sha512-FWtYHcX8NLzNSs21yGbkLF+q/5m2u80ug0JytKoI9nMZWPP5dcnsFYp1iZBEegLehiZnpv1qcmTsLTUG2KD39w==",
+            "requires": {}
+        },
+        "cm6-theme-solarized-light": {
+            "version": "0.2.0",
+            "resolved": "https://registry.npmjs.org/cm6-theme-solarized-light/-/cm6-theme-solarized-light-0.2.0.tgz",
+            "integrity": "sha512-Iw7Xv+9A6NlT7sRGlM2pOwD3ZBETkAqpb7c6O0LPj5kjwcK6C3k+mvjzaQt1gzfBErMmhL1HHuK07zICeXkE+w==",
+            "requires": {}
+        },
         "codemirror": {
-            "version": "5.65.16",
-            "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.16.tgz",
-            "integrity": "sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg=="
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
+            "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
+            "requires": {
+                "@codemirror/autocomplete": "^6.0.0",
+                "@codemirror/commands": "^6.0.0",
+                "@codemirror/language": "^6.0.0",
+                "@codemirror/lint": "^6.0.0",
+                "@codemirror/search": "^6.0.0",
+                "@codemirror/state": "^6.0.0",
+                "@codemirror/view": "^6.0.0"
+            }
+        },
+        "codemirror-lang-perl": {
+            "version": "0.1.5-beta.1",
+            "resolved": "https://registry.npmjs.org/codemirror-lang-perl/-/codemirror-lang-perl-0.1.5-beta.1.tgz",
+            "integrity": "sha512-iAp+G5UHpoQwrtCs1kD5yKuZ/tYYh812aI66FDoJYnOESysr/BlTaoTsgH9u3fskEcA6SRYmB3UqpkxiFH5fvw==",
+            "requires": {
+                "@codemirror/language": "^6.10.2",
+                "@lezer/highlight": "^1.2.1",
+                "@lezer/lr": "^1.4.2"
+            }
         },
         "color-convert": {
             "version": "2.0.1",
@@ -1920,6 +2703,11 @@
             "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
             "dev": true
         },
+        "crelt": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
+            "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
+        },
         "css-declaration-sorter": {
             "version": "7.2.0",
             "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz",
@@ -2665,6 +3453,11 @@
             "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
             "dev": true
         },
+        "style-mod": {
+            "version": "4.1.2",
+            "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
+            "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
+        },
         "stylehacks": {
             "version": "6.1.1",
             "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz",
@@ -2710,6 +3503,12 @@
                 }
             }
         },
+        "thememirror": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/thememirror/-/thememirror-2.0.1.tgz",
+            "integrity": "sha512-d5i6FVvWWPkwrm4cHLI3t9AT1OrkAt7Ig8dtdYSofgF7C/eiyNuq6zQzSTusWTde3jpW9WLvA9J/fzNKMUsd0w==",
+            "requires": {}
+        },
         "to-regex-range": {
             "version": "5.0.1",
             "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2735,6 +3534,11 @@
             "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
             "dev": true
         },
+        "w3c-keyname": {
+            "version": "2.2.8",
+            "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+            "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
+        },
         "wrap-ansi": {
             "version": "7.0.0",
             "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
diff --git a/htdocs/package.json b/htdocs/package.json
index ddbb13e751..bdbc7bfb86 100644
--- a/htdocs/package.json
+++ b/htdocs/package.json
@@ -14,8 +14,8 @@
     },
     "dependencies": {
         "@fortawesome/fontawesome-free": "^6.5.2",
+        "@openwebwork/pg-codemirror-editor": "^0.0.1-beta.7",
         "bootstrap": "~5.3.3",
-        "codemirror": "^5.65.15",
         "flatpickr": "^4.6.13",
         "iframe-resizer": "^4.3.11",
         "jquery": "^3.7.1",
diff --git a/lib/WeBWorK/HTML/CodeMirrorEditor.pm b/lib/WeBWorK/HTML/CodeMirrorEditor.pm
index 21bb0f60b9..d38800970d 100644
--- a/lib/WeBWorK/HTML/CodeMirrorEditor.pm
+++ b/lib/WeBWorK/HTML/CodeMirrorEditor.pm
@@ -24,80 +24,33 @@ PGProblemEditor.pm modules.
 
 =cut
 
-use WeBWorK::Utils qw(getAssetURL);
-
-our @EXPORT_OK = qw(generate_codemirror_html generate_codemirror_controls_html output_codemirror_static_files);
-
-# Available CodeMirror themes
-use constant CODEMIRROR_THEMES => [
-	'3024-day',                '3024-night',         'abbott',        'abcdef',
-	'ambiance',                'ambiance-mobile',    'ayu-dark',      'ayu-mirage',
-	'base16-dark',             'base16-light',       'bespin',        'blackboard',
-	'cobalt',                  'colorforth',         'darcula',       'dracula',
-	'duotone-dark',            'duotone-light',      'eclipse',       'elegant',
-	'erlang-dark',             'gruvbox-dark',       'hopscotch',     'icecoder',
-	'idea',                    'isotope',            'juejin',        'lesser-dark',
-	'liquibyte',               'lucario',            'material',      'material-darker',
-	'material-ocean',          'material-palenight', 'mbo',           'mdn-like',
-	'midnight',                'monokai',            'moxer',         'neat',
-	'neo',                     'night',              'nord',          'oceanic-next',
-	'panda-syntax',            'paraiso-dark',       'paraiso-light', 'pastel-on-dark',
-	'railscasts',              'rubyblue',           'seti',          'shadowfox',
-	'solarized',               'ssms',               'the-matrix',    'tomorrow-night-bright',
-	'tomorrow-night-eighties', 'ttcn',               'twilight',      'vibrant-ink',
-	'xq-dark',                 'xq-light',           'yeti',          'yonce',
-	'zenburn'
-];
-
-# Available CodeMirror keymaps
-use constant CODEMIRROR_KEYMAPS => [ 'emacs', 'sublime', 'vim' ];
-
-# Javascript for addons used by the PG editor (relative to the node_modules/codemirror/addon directory).
-use constant CODEMIRROR_ADDONS_CSS => [ 'dialog/dialog.css', 'search/matchesonscrollbar.css', 'fold/foldgutter.css' ];
-
-# Javascript for addons used by the PG editor (relative to the node_modules/codemirror/addon directory).
-use constant CODEMIRROR_ADDONS_JS => [
-	'dialog/dialog.js',            'search/search.js',
-	'search/searchcursor.js',      'search/matchesonscrollbar.js',
-	'search/match-highlighter.js', 'search/match-highlighter.js',
-	'scroll/annotatescrollbar.js', 'edit/matchbrackets.js',
-	'fold/foldcode.js',            'fold/foldgutter.js',
-	'fold/xml-fold.js'
-];
-
-sub generate_codemirror_html ($c, $name, $contents = '', $mode = 'PG') {
-	# Output the textarea that will be used by CodeMirror.
-	# If CodeMirror is disabled, then this is directly the editing area.
-	return $c->text_area($name => $contents, id => $name, class => 'codeMirrorEditor', data => { mode => $mode });
-}
-
-sub generate_codemirror_controls_html ($c) {
-	my $ce = $c->ce;
-
-	return '' unless $ce->{options}{PGCodeMirror};
-
-	# Construct the labels and values for the theme menu.
-	my $themeValues = [ [ default => 'default', selected => 'selected' ] ];
-	for (@{ CODEMIRROR_THEMES() }) {
-		push @$themeValues, [ $_ => getAssetURL($ce, "node_modules/codemirror/theme/$_.css") ];
+our @EXPORT_OK = qw(generate_codemirror_html output_codemirror_static_files);
+
+sub generate_codemirror_html ($c, $name, $contents = '', $language = 'pg') {
+	if ($c->ce->{options}{PGCodeMirror}) {
+		# Output the div that will be used by CodeMirror and a hidden input containing the contents.
+		return $c->c(
+			$c->hidden_field($name => $contents),
+			$c->tag(
+				'div',
+				id    => $name,
+				class => 'code-mirror-editor tex2jax_ignore',
+				data  => { language => $language }
+			)
+		)->join('');
+	} else {
+		# If CodeMirror is disabled, then a text area is used instead.
+		return $c->text_area(
+			$name => $contents,
+			id    => $name,
+			class => 'text-area-editor',
+			data  => { language => $language }
+		);
 	}
-
-	# Construct the labels and values for the keymap menu.
-	my $keymapValues = [ [ default => 'default', selected => 'selected' ] ];
-	for (@{ CODEMIRROR_KEYMAPS() }) {
-		push @$keymapValues, [ $_ => getAssetURL($ce, "node_modules/codemirror/keymap/$_.js") ];
-	}
-
-	return $c->include('HTML/CodeMirrorEditor/controls', themeValues => $themeValues, keymapValues => $keymapValues);
 }
 
-sub output_codemirror_static_files ($c, $mode = 'PG') {
-	return $c->include(
-		'HTML/CodeMirrorEditor/js',
-		codemirrorAddonsCSS => CODEMIRROR_ADDONS_CSS(),
-		codemirrorAddonsJS  => CODEMIRROR_ADDONS_JS(),
-		codemirrorModesJS   => $mode eq 'htmlmixed' ? [ 'xml', 'css', 'javascript', 'htmlmixed' ] : [$mode]
-	);
+sub output_codemirror_static_files ($c) {
+	return $c->include('HTML/CodeMirrorEditor/js');
 }
 
 1;
diff --git a/templates/ContentGenerator/Instructor/AchievementEditor.html.ep b/templates/ContentGenerator/Instructor/AchievementEditor.html.ep
index 28c4097172..b593704ec0 100644
--- a/templates/ContentGenerator/Instructor/AchievementEditor.html.ep
+++ b/templates/ContentGenerator/Instructor/AchievementEditor.html.ep
@@ -1,9 +1,8 @@
 % use WeBWorK::Utils qw(not_blank getAssetURL);
-% use WeBWorK::HTML::CodeMirrorEditor
-	% qw(generate_codemirror_html generate_codemirror_controls_html output_codemirror_static_files);
+% use WeBWorK::HTML::CodeMirrorEditor qw(generate_codemirror_html output_codemirror_static_files);
 %
 % content_for js => begin
-	<%= output_codemirror_static_files($c, 'perl') =%>
+	<%= output_codemirror_static_files($c) =%>
 	<%= javascript getAssetURL($ce, 'js/ActionTabs/actiontabs.js'), defer => undef =%>
 % end
 %
@@ -24,7 +23,6 @@
 	% }
 	%
 	
<%= generate_codemirror_html($c, 'achievementContents', $achievementContents, 'perl') =%>
- <%= generate_codemirror_controls_html($c) =%> % % # Output action forms % my $default_choice; diff --git a/templates/ContentGenerator/Instructor/AchievementNotificationEditor.html.ep b/templates/ContentGenerator/Instructor/AchievementNotificationEditor.html.ep index 01e5697a11..4a26f76c8f 100644 --- a/templates/ContentGenerator/Instructor/AchievementNotificationEditor.html.ep +++ b/templates/ContentGenerator/Instructor/AchievementNotificationEditor.html.ep @@ -1,9 +1,8 @@ % use WeBWorK::Utils qw(not_blank getAssetURL); -% use WeBWorK::HTML::CodeMirrorEditor - % qw(generate_codemirror_html generate_codemirror_controls_html output_codemirror_static_files); +% use WeBWorK::HTML::CodeMirrorEditor qw(generate_codemirror_html output_codemirror_static_files); % % content_for js => begin - <%= output_codemirror_static_files($c, 'perl') =%> + <%= output_codemirror_static_files($c) =%> <%= javascript getAssetURL($ce, 'js/ActionTabs/actiontabs.js'), defer => undef =%> % end % @@ -24,8 +23,8 @@ % } %
+ % # FIXME: This should not be using perl. Mojolicious templates have embedded perl, but are not perl. <%= generate_codemirror_html($c, 'achievementNotification', $achievementNotification, 'perl') =%>
- <%= generate_codemirror_controls_html($c) =%> % % # Output action forms % my $default_choice; diff --git a/templates/ContentGenerator/Instructor/PGProblemEditor.html.ep b/templates/ContentGenerator/Instructor/PGProblemEditor.html.ep index 6c11e49345..bc85653afa 100644 --- a/templates/ContentGenerator/Instructor/PGProblemEditor.html.ep +++ b/templates/ContentGenerator/Instructor/PGProblemEditor.html.ep @@ -4,16 +4,10 @@ % use WeBWorK::Utils qw(not_blank x getAssetURL); % use WeBWorK::Utils::Files qw(readFile); % use WeBWorK::Utils::Sets qw(format_set_name_display); -% use WeBWorK::HTML::CodeMirrorEditor - % qw(generate_codemirror_html generate_codemirror_controls_html output_codemirror_static_files); +% use WeBWorK::HTML::CodeMirrorEditor qw(generate_codemirror_html output_codemirror_static_files); % -% my $codemirrorMode = 'PG'; -% if ($c->{file_type}) { -% $codemirrorMode = 'htmlmixed' if ($c->{file_type} eq 'course_info'); -% $codemirrorMode = 'xml' if ($c->{file_type} eq 'hardcopy_theme'); -% } % content_for js => begin - <%= output_codemirror_static_files($c, $codemirrorMode) =%> + <%= output_codemirror_static_files($c) =%> <%= javascript getAssetURL($ce, 'js/ActionTabs/actiontabs.js'), defer => undef =%> <%= javascript getAssetURL($ce, 'js/PGProblemEditor/pgproblemeditor.js'), defer => undef =%> % end @@ -156,7 +150,12 @@ % }
- <%= generate_codemirror_html($c, 'problemContents', $problemContents, $codemirrorMode) =%> + <%= generate_codemirror_html( + $c, + 'problemContents', + $problemContents, + { course_info => 'html', hardcopy_theme => 'xml' }->{ $c->{file_type} } // 'pg' + ) =%>
@@ -168,7 +167,6 @@
- <%= generate_codemirror_controls_html($c) =%> <%= $fileInfo->() %> % % # Output action forms diff --git a/templates/HTML/CodeMirrorEditor/controls.html.ep b/templates/HTML/CodeMirrorEditor/controls.html.ep deleted file mode 100644 index c91a9996fc..0000000000 --- a/templates/HTML/CodeMirrorEditor/controls.html.ep +++ /dev/null @@ -1,33 +0,0 @@ -% # Output the html elements for setting the CodeMirror options. -
-
-
- <%= label_for selectTheme => maketext('Theme:'), class => 'col-form-label col-auto' =%> -
- <%= select_field selectTheme => $themeValues, - id => 'selectTheme', class => 'form-select form-select-sm d-inline w-auto' =%> -
-
-
-
-
- <%= label_for selectKeymap => maketext('Key Map:'), class => 'col-form-label col-auto' =%> -
- <%= select_field selectKeymap => $keymapValues, - id => 'selectKeymap', class => 'form-select form-select-sm d-inline w-auto' =%> -
-
-
-
-
- <%= check_box 'enableSpell', id => 'enableSpell', class => 'form-check-input' =%> - <%= label_for enableSpell => maketext('Enable Spell Checking'), class => 'form-check-label' =%> -
-
-
-
- <%= check_box 'forceRTL', id => 'forceRTL', class => 'form-check-input' =%> - <%= label_for forceRTL => maketext('Force editor to RTL'), class => 'form-check-label' =%> -
-
-
diff --git a/templates/HTML/CodeMirrorEditor/js.html.ep b/templates/HTML/CodeMirrorEditor/js.html.ep index 33a4be141f..3d6edad16b 100644 --- a/templates/HTML/CodeMirrorEditor/js.html.ep +++ b/templates/HTML/CodeMirrorEditor/js.html.ep @@ -1,34 +1,17 @@ % use WeBWorK::Utils qw(getAssetURL); % +% # The textarea styles in this file are still needed if CodeMirror is disabled. +% content_for css => begin + <%= stylesheet getAssetURL($ce, 'js/PGCodeMirror/pgeditor.css') =%> +% end + % if ($ce->{options}{PGCodeMirror}) { - % content_for css => begin - <%= stylesheet getAssetURL($ce, 'node_modules/codemirror/lib/codemirror.css') =%> - % - % for my $addon (@$codemirrorAddonsCSS) { - <%= stylesheet getAssetURL($ce, "node_modules/codemirror/addon/$addon") =%> - % } - <%= stylesheet getAssetURL($ce, 'js/PGCodeMirror/pgeditor.css') =%> - % end - % % content_for js => begin - <%= javascript getAssetURL($ce, 'node_modules/codemirror/lib/codemirror.js'), defer => undef =%> - % - % for my $addon (@$codemirrorAddonsJS) { - <%= javascript getAssetURL($ce, "node_modules/codemirror/addon/$addon"), defer => undef =%> - % } - % for my $mode (@$codemirrorModesJS) { - <%= javascript getAssetURL( - $ce, - $mode eq 'PG' ? 'js/PGCodeMirror/PG.js' : "node_modules/codemirror/mode/$mode/$mode.js" - ), defer => undef =%> - % } - % + <%= javascript getAssetURL( + $ce, + 'node_modules/@openwebwork/pg-codemirror-editor/dist/pg-codemirror-editor.js' + ), + defer => undef =%> <%= javascript getAssetURL($ce, 'js/PGCodeMirror/pgeditor.js'), defer => undef =%> - <%= javascript getAssetURL($ce, 'js/PGCodeMirror/comment.js'), defer => undef =%> % end % } -% -% # The textarea styles in this file are still needed if CodeMirror is disabled. -% content_for css => begin - <%= stylesheet getAssetURL($ce, 'js/PGCodeMirror/pgeditor.css') =%> -% end diff --git a/templates/HelpFiles/InstructorPGProblemEditor.html.ep b/templates/HelpFiles/InstructorPGProblemEditor.html.ep index c28751e7ee..a57c23dd2f 100644 --- a/templates/HelpFiles/InstructorPGProblemEditor.html.ep +++ b/templates/HelpFiles/InstructorPGProblemEditor.html.ep @@ -72,13 +72,55 @@
<%= maketext('This links to problem authoring information on the WeBWorK wiki.') %>
-
<%= maketext('The large text window') %>
+
<%= maketext('Text Editor Window') %>
- <%= maketext('This is where you edit the text of the problem template. Type Ctrl-Enter while this ' - . 'window has focus to re-render the problem. Code folding is enabled either by clicking on ' - . 'the triangles in the gutter next to line numbers or using the shortcut Shift-Ctrl-F. Folding ' - . 'all regions can be accomplished with Shift-Ctrl-A and unfold all regions with Shift-Ctrl-G. ' - . 'Comments can be toggled with Ctrl-/.') =%> + <%= maketext('This is where you edit the code of the problem. Type Ctrl-Enter while this window ' + . 'has focus to re-render the problem. In addition, the following keyboard shortcuts are available.') =%> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
<%= maketext('Key Binding') %><%= maketext('Mac alternate') %><%= maketext('Action') %>
Ctrl-mAlt-Shift-m + <%= maketext('Enable or disable tab-focus mode. When tab-focus mode is off, pressing Tab ' + . 'inside the editor window indents the current line. When tab-focus mode is on ' + . 'Tab and Shift-Tab move focus out of the editor window.') %> +
Ctrl-Shift-[Cmd-Shift-[ + <%= maketext('Fold the region that begins on the current line. Triangles in the gutter ' + . 'next to line numbers indicate which regions can be folded.') %> +
Ctrl-Shift-]Cmd-Shift-]<%= maketext('Unfold the region that begins on the current line.') %>
Ctrl-Alt-[Cmd-Alt-[<%= maketext('Fold all regions.') %>
Cmd-Alt-]Cmd-Alt-]<%= maketext('Unfold all regions.') %>
Ctrl-/Cmd-/<%= maketext('Toggle comment.') %>
Shift-Alt-a<%= maketext('Toggle block comment. (Only has effect inside PGML blocks.)') %>
Ctrl-SpaceAlt-`<%= maketext('Show available autocompletions at the current cursor location.') %>
<%= maketext('Text Editor Options') %>
@@ -89,14 +131,19 @@
<%= maketext('The key maps that are available are "default", "emacs", "sublime", and "vim". The ' . '"default" key map has the standard behavior of a browser text area. You can use Ctrl-C to copy, ' - . 'Ctrl-V to paste, Ctrl-F to search, etc. If you are more comfortable with the "emacs", ' - . '"sublime", or "vim" text editors then you may choose to use one of those key maps instead.') =%> + . 'Ctrl-V to paste, Ctrl-F to search, etc. If you are more comfortable with the "emacs" ' + . ' or "vim" text editors then you may choose to use one of those key maps instead.') =%>
<%= maketext('Enable Spell Checking') %>
<%= maketext('Spell check the text of the file that your are editing. Note that there will be many ' . 'spelling errors in the code parts of your file.') =%>
+
<%= maketext('Force RTL') %>
+
+ <%= maketext('Force the editor to display text from right-to-left. ' + . '(Note that this does not persist when reloading the page.)') =%> +