|
| 1 | +from rich.console import Console |
| 2 | +from rich.text import Text |
| 3 | +import re |
| 4 | +import os |
| 5 | +import json |
| 6 | +from colorama import Fore, Style |
| 7 | +from cxconst import DOCS_URL, AUTHOR, YEAR, LICENSE |
| 8 | +from argparse import ArgumentParser |
| 9 | + |
| 10 | + |
| 11 | +VERSION = 'v2.0.0' |
| 12 | +DEFAULT_COLORMAP = { |
| 13 | + "FUNCTION": "bold red", |
| 14 | + "NEGATION": "bold magenta", |
| 15 | + "SEPARATOR": "dim yellow", |
| 16 | + "SEMICOLON": "dim bold italic red", |
| 17 | + "COMMENT": "dim italic white", |
| 18 | + "NUMBER": "bold blue", |
| 19 | + "STRING": "green", |
| 20 | + "CACHE_GRAB": "bold cyan", |
| 21 | + "TEXT": "white", |
| 22 | + "UNREACHABLE_CODE": "dim italic white" |
| 23 | +} |
| 24 | + |
| 25 | + |
| 26 | +def syntax_highlight(code: str, colormap: dict[str, str] | None = None, ignore_unreachable_code: bool = True): |
| 27 | + colormap = DEFAULT_COLORMAP if colormap is None else colormap |
| 28 | + lines = code.splitlines() |
| 29 | + text = Text() |
| 30 | + terminated = [False, False] |
| 31 | + comment = False |
| 32 | + |
| 33 | + for line in lines: |
| 34 | + if ignore_unreachable_code: |
| 35 | + terminated = [False, False] |
| 36 | + |
| 37 | + if line.strip().startswith(('#', '//')): |
| 38 | + text.append(f'{line}\n', colormap['COMMENT']) |
| 39 | + continue |
| 40 | + |
| 41 | + tokens = re.split(r'(\s+|".*?"|[?:;]|[\?]{2}|c[0-9]+:[0-9]+[el]:[0-9]+:[0-9]+:[rlbnRLBNekvm])', line) |
| 42 | + semicolon = False |
| 43 | + comment = False |
| 44 | + |
| 45 | + for token in tokens: |
| 46 | + if ignore_unreachable_code: |
| 47 | + terminated = [False, False] |
| 48 | + |
| 49 | + if sum(terminated) == 2: |
| 50 | + text.append(token, colormap['UNREACHABLE_CODE']) |
| 51 | + continue |
| 52 | + |
| 53 | + if not token.strip(): |
| 54 | + text.append(token, colormap['TEXT']) |
| 55 | + continue |
| 56 | + |
| 57 | + token = token.strip() |
| 58 | + |
| 59 | + if comment: |
| 60 | + text.append(token, colormap['COMMENT']) |
| 61 | + continue |
| 62 | + |
| 63 | + # [*] Always Run |
| 64 | + if token in {'&RUN', '&SAFECIN', '&SUM', '&SET', '&SUB', '&FILE.EXISTS', '&CLEAR', '&DIR.EXISTS', '&BACK', '&PATH.EXISTS', '&ECHO', '&STYLE', '&RESET', '&REQUIRES', '&CIN', '&YAYORNAY', '&ENDL2', '&COUT', '&REQINSTALL', '&ENDL', '&REM', '&GETPASS', '&ABS', '&INVERT', '&PKGRUN', '&ROUND', '&DIV', '&FORE', '&PIPRUN', '&NPMRUN', '&TERMINATE', '&ECHORDIE'}: |
| 65 | + text.append(token, colormap.get('ALWAYS_RUN', colormap.get('CACHE_GRAB'))) |
| 66 | + semicolon = False |
| 67 | + |
| 68 | + if token == '&TERMINATE': |
| 69 | + terminated = [True, True] |
| 70 | + |
| 71 | + continue |
| 72 | + |
| 73 | + # [*] Negations |
| 74 | + if token in {'!INVERT', '!PATH.EXISTS', '!FILE.EXISTS', '!DIR.EXISTS', '!RESET', '!SUM', '!REM', '!DIV', '!SUB', '!ABS', '!ROUND', '!PROD', '!ENDL', '!ENDL2', '!GETPASS', '!COUT', '!ECHO', '!CIN', '!TERMINATE', '!REQUIRES', '!REQINSTALL', '!STYLE', '!FORE', '!BACK', '!CLEAR', '!SET', '!ECHORDIE', '!SAFECIN', '!YAYORNAY', '!PKGRUN', '!RUN', '!PIPRUN', '!NPMRUN'}: |
| 75 | + text.append(token, colormap['NEGATION']) |
| 76 | + semicolon = False |
| 77 | + |
| 78 | + if token == "!TERMINATE": |
| 79 | + terminated[1] = True |
| 80 | + |
| 81 | + continue |
| 82 | + |
| 83 | + # [*] Keywords/Functions |
| 84 | + if token in {'INVERT', 'PATH.EXISTS', 'FILE.EXISTS', 'DIR.EXISTS', 'RESET', 'SUM', 'REM', 'DIV', 'SUB', 'ABS', 'ROUND', 'ENDL', 'ENDL2', 'GETPASS', 'COUT', 'ECHO', 'CIN', 'TERMINATE', 'REQUIRES', 'REQINSTALL', 'STYLE', 'FORE', 'BACK', 'CLEAR', 'SET', 'ECHORDIE', 'SAFECIN', 'YAYORNAY', 'PKGRUN', 'RUN', 'PIPRUN', 'NPMRUN'}: |
| 85 | + text.append(token, colormap.get('POSITIVE', colormap.get('FUNCTION'))) |
| 86 | + semicolon = False |
| 87 | + |
| 88 | + if token == "TERMINATE": |
| 89 | + terminated[0] = True |
| 90 | + |
| 91 | + continue |
| 92 | + |
| 93 | + # [*] Strings |
| 94 | + if re.fullmatch(r'"[^"]*"', token): |
| 95 | + text.append(token, colormap['STRING']) |
| 96 | + semicolon = False |
| 97 | + continue |
| 98 | + |
| 99 | + # [*] Cache Grabs |
| 100 | + if re.match(r'c[0-9]+:[0-9]+[el]:[0-9]+:[0-9]+:[rlbn]', token): |
| 101 | + text.append(token, colormap['CACHE_GRAB']) |
| 102 | + semicolon = False |
| 103 | + continue |
| 104 | + |
| 105 | + # [*] Floats |
| 106 | + if re.fullmatch(r'[0-9]+\.[0-9]+', token) or re.fullmatch('[0-9]+[e][0-9]+', token) or re.fullmatch(r'[-][0-9]+\.[0-9]+', token) or re.fullmatch('[-][0-9]+[e][0-9]+', token): |
| 107 | + text.append(token, colormap['NUMBER']) |
| 108 | + semicolon = False |
| 109 | + continue |
| 110 | + |
| 111 | + # [*] Integer |
| 112 | + if re.fullmatch(r'[0-9]+', token) or re.fullmatch(r'[-][0-9]+', token): |
| 113 | + text.append(token, colormap['NUMBER']) |
| 114 | + semicolon = False |
| 115 | + continue |
| 116 | + |
| 117 | + # [*] Semicolon |
| 118 | + if token == ';': |
| 119 | + text.append(token, colormap['SEMICOLON']) |
| 120 | + semicolon = True |
| 121 | + continue |
| 122 | + |
| 123 | + # [*] Comment |
| 124 | + if token.startswith('//') and semicolon: |
| 125 | + comment = True |
| 126 | + semicolon = False |
| 127 | + text.append(token, colormap['COMMENT']) |
| 128 | + continue |
| 129 | + |
| 130 | + # [*] Argument Separator |
| 131 | + if token.strip() == '?': |
| 132 | + text.append(token, colormap['SEPARATOR']) |
| 133 | + semicolon = False |
| 134 | + continue |
| 135 | + |
| 136 | + # [*] Normal text |
| 137 | + text.append(token, colormap['TEXT']) |
| 138 | + |
| 139 | + text.append('\n', colormap['TEXT']) |
| 140 | + |
| 141 | + return text |
| 142 | + |
| 143 | + |
| 144 | +def get_code_from_file(file: str) -> str: |
| 145 | + if not file.lower().endswith('.cxsetup'): |
| 146 | + raise NameError('file must be a CX Setup script') |
| 147 | + |
| 148 | + with open(file, 'r', encoding='utf-8') as f: |
| 149 | + data = f.read() |
| 150 | + |
| 151 | + return data |
| 152 | + |
| 153 | + |
| 154 | +def get_colormap_from_file(file: str) -> dict[str, str]: |
| 155 | + if not file.lower().endswith('.json'): |
| 156 | + raise NameError('colormaps must be in JSON format') |
| 157 | + |
| 158 | + with open(file, 'r', encoding='utf-8') as f: |
| 159 | + data = json.load(f) |
| 160 | + |
| 161 | + return data |
| 162 | + |
| 163 | + |
| 164 | +def _handle_arguments(args): |
| 165 | + if args.where: |
| 166 | + print(f'\n{Fore.MAGENTA}The CX Setup Syntax Highlighter can be found at local path: {Fore.LIGHTCYAN_EX}\033[4m{__file__}{Style.RESET_ALL}') |
| 167 | + os._exit(0) |
| 168 | + |
| 169 | + if args.docs: |
| 170 | + print(f'\n{Fore.MAGENTA}The CX Setup documentation can be found online at: {Fore.LIGHTCYAN_EX}\033[4m{DOCS_URL}{Style.RESET_ALL}') |
| 171 | + os._exit(0) |
| 172 | + |
| 173 | + colormap = None |
| 174 | + |
| 175 | + if args.colormap: |
| 176 | + try: |
| 177 | + colormap = get_colormap_from_file(args.colormap) |
| 178 | + |
| 179 | + except Exception as e: |
| 180 | + print(f'{Fore.RED}⌦ Could not load colormap succesfully (reverting to the default one).{Fore.RESET} Error details: {e}') |
| 181 | + colormap = None |
| 182 | + |
| 183 | + try: |
| 184 | + code = get_code_from_file(args.file) |
| 185 | + |
| 186 | + except Exception as e: |
| 187 | + print(f'{Fore.RED}⌦ Could not load script succesfully.{Fore.RESET} Error details: {e}\n{Fore.BLUE}Goodbye :({Fore.RESET}') |
| 188 | + os._exit(0) |
| 189 | + |
| 190 | + console = Console() |
| 191 | + text = syntax_highlight(code, colormap, not args.highlight_unreachable) |
| 192 | + console.print(text) |
| 193 | + return 0 |
| 194 | + |
| 195 | + |
| 196 | +if __name__ == '__main__': |
| 197 | + parser = ArgumentParser('CX Setup Syntax Highlighter', description='Syntax Highlighter for CX Setup Files and Instructions') |
| 198 | + |
| 199 | + print(f"""\n\n{Fore.YELLOW} ███ █████████ █████ █████ █████████ █████ █████ ███ █████ ███ |
| 200 | + ███ ░███ ███ ███░░░░░███░░███ ░░███ ███░░░░░███ ░░███ ░░███ ░░░ ░░███ ███ ░███ ███ |
| 201 | +░░░█████████░ ███ ░░░ ░░███ ███ ░███ ░░░ █████ ████ ████████ ░███ ░███ ████ ███████ ░███████ ░░░█████████░ |
| 202 | + ░░░█████░ ░███ ░░█████ ░░█████████ ░░███ ░███ ░░███░░███ ░███████████ ░░███ ███░░███ ░███░░███ ░░░█████░ |
| 203 | + █████████ ░███ ███░███ ░░░░░░░░███ ░███ ░███ ░███ ░███ ░███░░░░░███ ░███ ░███ ░███ ░███ ░███ █████████ |
| 204 | + ███░░███░░███ ░░███ ███ ███ ░░███ ███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ███░░███░░███ |
| 205 | +░░░ ░███ ░░░ ░░█████████ █████ █████ ░░█████████ ░░███████ ████ █████ █████ █████ █████░░███████ ████ █████ ░░░ ░███ ░░░ |
| 206 | + ░░░ ░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░░░ ░░░░░███ ░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░███░░░░ ░░░░░ ░░░ |
| 207 | + ███ ░███ ███ ░███ |
| 208 | + ░░██████ ░░██████ |
| 209 | + ░░░░░░ ░░░░░░ |
| 210 | +
|
| 211 | +{Fore.RED}{AUTHOR}{Fore.RESET} * {Fore.GREEN}{LICENSE} License{Fore.RESET} * {Fore.MAGENTA}{YEAR}{Style.RESET_ALL}\n{Fore.YELLOW}{VERSION}{Fore.RESET} * {Fore.BLUE}\033[4mhttps://github.com/MF366-Coding/ContenterX/blob/main/core/parsers/cxhighlight.py{Style.RESET_ALL}\n""") |
| 212 | + |
| 213 | + parser.add_argument("file", type=str, help=f"{Fore.GREEN}specify a file to perform syntax highlighting on{Fore.RESET} ({Fore.BLUE}path as str{Fore.RESET})") |
| 214 | + parser.add_argument("--colormap", "-c", type=str, default="", help=f"{Fore.GREEN}specify a colormap to use instead of the default one{Fore.RESET} ({Fore.BLUE}path as str{Fore.RESET}, defaults to {Fore.BLUE}none{Fore.RESET})") |
| 215 | + parser.add_argument("--highlight-unreachable", "-u", action='store_true', default=False, help=f"{Fore.GREEN}apply syntax highlighting to unreachable code{Fore.RESET} (defaults to {Fore.BLUE}NO{Fore.RESET})") |
| 216 | + parser.add_argument("--where", "-W", action="store_true", help=f"{Fore.GREEN}locate the highlighter{Fore.RESET} on your device") |
| 217 | + parser.add_argument("--docs", "-D", "--documentation", action="store_true", help=f"visit the {Fore.GREEN}online documentation for CX Setup{Fore.RESET}") |
| 218 | + |
| 219 | + _handle_arguments(parser.parse_args()) |
0 commit comments