@@ -0,0 +1,966 @@ |
| 1 | +module syntax_highlighter_module |
| 2 | + use iso_fortran_env, only: int32 |
| 3 | + implicit none |
| 4 | + private |
| 5 | + |
| 6 | + public :: syntax_highlighter_t |
| 7 | + public :: token_t |
| 8 | + public :: init_highlighter, cleanup_highlighter |
| 9 | + public :: tokenize_line, get_token_color |
| 10 | + public :: detect_language |
| 11 | + public :: TOKEN_PLAIN, TOKEN_KEYWORD, TOKEN_STRING, TOKEN_NUMBER |
| 12 | + public :: TOKEN_COMMENT, TOKEN_OPERATOR, TOKEN_TYPE, TOKEN_FUNCTION |
| 13 | + public :: TOKEN_PREPROCESSOR |
| 14 | + |
| 15 | + ! Token types as integer parameters |
| 16 | + integer, parameter :: TOKEN_PLAIN = 0 |
| 17 | + integer, parameter :: TOKEN_KEYWORD = 1 |
| 18 | + integer, parameter :: TOKEN_STRING = 2 |
| 19 | + integer, parameter :: TOKEN_NUMBER = 3 |
| 20 | + integer, parameter :: TOKEN_COMMENT = 4 |
| 21 | + integer, parameter :: TOKEN_OPERATOR = 5 |
| 22 | + integer, parameter :: TOKEN_TYPE = 6 |
| 23 | + integer, parameter :: TOKEN_FUNCTION = 7 |
| 24 | + integer, parameter :: TOKEN_PREPROCESSOR = 8 |
| 25 | + |
| 26 | + ! Token structure |
| 27 | + type :: token_t |
| 28 | + integer :: type = TOKEN_PLAIN |
| 29 | + integer :: start_col |
| 30 | + integer :: end_col |
| 31 | + end type token_t |
| 32 | + |
| 33 | + ! Language definition |
| 34 | + type :: language_def_t |
| 35 | + character(len=32) :: name = "" |
| 36 | + character(len=16), allocatable :: extensions(:) |
| 37 | + character(len=64), allocatable :: keywords(:) |
| 38 | + character(len=64), allocatable :: types(:) |
| 39 | + character(len=8) :: comment_single = "" |
| 40 | + character(len=8) :: comment_start = "" |
| 41 | + character(len=8) :: comment_end = "" |
| 42 | + character(len=4), allocatable :: string_delimiters(:) |
| 43 | + character(len=4), allocatable :: operators(:) |
| 44 | + logical :: case_sensitive = .true. |
| 45 | + end type language_def_t |
| 46 | + |
| 47 | + ! Main highlighter type |
| 48 | + type :: syntax_highlighter_t |
| 49 | + type(language_def_t) :: current_lang |
| 50 | + logical :: enabled = .false. |
| 51 | + logical :: in_multiline_comment = .false. |
| 52 | + logical :: in_multiline_string = .false. |
| 53 | + character(len=4) :: string_delimiter = "" |
| 54 | + end type syntax_highlighter_t |
| 55 | + |
| 56 | + ! Color mapping (ANSI escape codes) |
| 57 | + character(len=16), parameter :: COLOR_KEYWORD = char(27) // '[1;34m' ! Bold Blue |
| 58 | + character(len=16), parameter :: COLOR_STRING = char(27) // '[32m' ! Green |
| 59 | + character(len=16), parameter :: COLOR_NUMBER = char(27) // '[35m' ! Magenta |
| 60 | + character(len=16), parameter :: COLOR_COMMENT = char(27) // '[90m' ! Gray |
| 61 | + character(len=16), parameter :: COLOR_OPERATOR = char(27) // '[33m' ! Yellow |
| 62 | + character(len=16), parameter :: COLOR_TYPE = char(27) // '[36m' ! Cyan |
| 63 | + character(len=16), parameter :: COLOR_FUNCTION = char(27) // '[1;36m' ! Bold Cyan |
| 64 | + character(len=16), parameter :: COLOR_PREPROC = char(27) // '[95m' ! Light Magenta |
| 65 | + character(len=16), parameter :: COLOR_RESET = char(27) // '[0m' |
| 66 | + |
| 67 | +contains |
| 68 | + |
| 69 | + subroutine init_highlighter(highlighter, filename) |
| 70 | + type(syntax_highlighter_t), intent(out) :: highlighter |
| 71 | + character(len=*), intent(in), optional :: filename |
| 72 | + |
| 73 | + highlighter%enabled = .false. |
| 74 | + highlighter%in_multiline_comment = .false. |
| 75 | + highlighter%in_multiline_string = .false. |
| 76 | + |
| 77 | + if (present(filename)) then |
| 78 | + call detect_language(highlighter, filename) |
| 79 | + end if |
| 80 | + end subroutine init_highlighter |
| 81 | + |
| 82 | + subroutine cleanup_highlighter(highlighter) |
| 83 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 84 | + |
| 85 | + if (allocated(highlighter%current_lang%extensions)) & |
| 86 | + deallocate(highlighter%current_lang%extensions) |
| 87 | + if (allocated(highlighter%current_lang%keywords)) & |
| 88 | + deallocate(highlighter%current_lang%keywords) |
| 89 | + if (allocated(highlighter%current_lang%types)) & |
| 90 | + deallocate(highlighter%current_lang%types) |
| 91 | + if (allocated(highlighter%current_lang%string_delimiters)) & |
| 92 | + deallocate(highlighter%current_lang%string_delimiters) |
| 93 | + if (allocated(highlighter%current_lang%operators)) & |
| 94 | + deallocate(highlighter%current_lang%operators) |
| 95 | + end subroutine cleanup_highlighter |
| 96 | + |
| 97 | + subroutine detect_language(highlighter, filename) |
| 98 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 99 | + character(len=*), intent(in) :: filename |
| 100 | + character(len=:), allocatable :: extension |
| 101 | + integer :: dot_pos |
| 102 | + |
| 103 | + ! Find file extension |
| 104 | + dot_pos = index(filename, '.', back=.true.) |
| 105 | + if (dot_pos > 0) then |
| 106 | + extension = filename(dot_pos:) |
| 107 | + |
| 108 | + select case(extension) |
| 109 | + case('.f90', '.f95', '.f03', '.f08', '.f18') |
| 110 | + call load_fortran_syntax(highlighter) |
| 111 | + case('.py', '.pyw') |
| 112 | + call load_python_syntax(highlighter) |
| 113 | + case('.c', '.h') |
| 114 | + call load_c_syntax(highlighter) |
| 115 | + case('.cpp', '.cc', '.cxx', '.hpp', '.hxx') |
| 116 | + call load_cpp_syntax(highlighter) |
| 117 | + case('.rs') |
| 118 | + call load_rust_syntax(highlighter) |
| 119 | + case('.go') |
| 120 | + call load_go_syntax(highlighter) |
| 121 | + case('.js', '.jsx', '.mjs') |
| 122 | + call load_javascript_syntax(highlighter) |
| 123 | + case('.ts', '.tsx') |
| 124 | + call load_typescript_syntax(highlighter) |
| 125 | + case('.sh', '.bash') |
| 126 | + call load_bash_syntax(highlighter) |
| 127 | + case('.md', '.markdown') |
| 128 | + call load_markdown_syntax(highlighter) |
| 129 | + case default |
| 130 | + highlighter%enabled = .false. |
| 131 | + end select |
| 132 | + end if |
| 133 | + end subroutine detect_language |
| 134 | + |
| 135 | + subroutine tokenize_line(highlighter, line, tokens) |
| 136 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 137 | + character(len=*), intent(in) :: line |
| 138 | + type(token_t), allocatable, intent(out) :: tokens(:) |
| 139 | + integer :: i, j, line_len, token_count |
| 140 | + logical :: in_string, in_comment |
| 141 | + character(len=1) :: ch |
| 142 | + character(len=:), allocatable :: word |
| 143 | + |
| 144 | + if (.not. highlighter%enabled) then |
| 145 | + allocate(tokens(1)) |
| 146 | + tokens(1)%type = TOKEN_PLAIN |
| 147 | + tokens(1)%start_col = 1 |
| 148 | + tokens(1)%end_col = len(line) |
| 149 | + return |
| 150 | + end if |
| 151 | + |
| 152 | + line_len = len(line) |
| 153 | + allocate(tokens(line_len)) ! Worst case: each char is a token |
| 154 | + token_count = 0 |
| 155 | + i = 1 |
| 156 | + |
| 157 | + ! Handle multiline comment continuation |
| 158 | + if (highlighter%in_multiline_comment) then |
| 159 | + call process_multiline_comment(highlighter, line, tokens, token_count, i) |
| 160 | + end if |
| 161 | + |
| 162 | + ! Handle multiline string continuation |
| 163 | + if (highlighter%in_multiline_string) then |
| 164 | + call process_multiline_string(highlighter, line, tokens, token_count, i) |
| 165 | + end if |
| 166 | + |
| 167 | + ! Process rest of line |
| 168 | + do while (i <= line_len) |
| 169 | + ch = line(i:i) |
| 170 | + |
| 171 | + ! Check for single-line comment |
| 172 | + if (check_comment_start(highlighter, line, i)) then |
| 173 | + token_count = token_count + 1 |
| 174 | + tokens(token_count)%type = TOKEN_COMMENT |
| 175 | + tokens(token_count)%start_col = i |
| 176 | + tokens(token_count)%end_col = line_len |
| 177 | + exit |
| 178 | + end if |
| 179 | + |
| 180 | + ! Check for string |
| 181 | + if (check_string_start(highlighter, line, i)) then |
| 182 | + call process_string(highlighter, line, tokens, token_count, i) |
| 183 | + |
| 184 | + ! Check for number |
| 185 | + else if (is_digit(ch) .or. (ch == '.' .and. i < line_len .and. is_digit(line(i+1:i+1)))) then |
| 186 | + call process_number(line, tokens, token_count, i) |
| 187 | + |
| 188 | + ! Check for word (keyword, type, identifier) |
| 189 | + else if (is_alpha(ch) .or. ch == '_') then |
| 190 | + call process_word(highlighter, line, tokens, token_count, i) |
| 191 | + |
| 192 | + ! Check for operator |
| 193 | + else if (is_operator_char(highlighter, ch)) then |
| 194 | + token_count = token_count + 1 |
| 195 | + tokens(token_count)%type = TOKEN_OPERATOR |
| 196 | + tokens(token_count)%start_col = i |
| 197 | + tokens(token_count)%end_col = i |
| 198 | + i = i + 1 |
| 199 | + |
| 200 | + ! Plain character (space, etc.) |
| 201 | + else |
| 202 | + token_count = token_count + 1 |
| 203 | + tokens(token_count)%type = TOKEN_PLAIN |
| 204 | + tokens(token_count)%start_col = i |
| 205 | + tokens(token_count)%end_col = i |
| 206 | + i = i + 1 |
| 207 | + end if |
| 208 | + end do |
| 209 | + |
| 210 | + ! Resize tokens array |
| 211 | + if (token_count > 0) then |
| 212 | + tokens = tokens(1:token_count) |
| 213 | + else |
| 214 | + allocate(tokens(1)) |
| 215 | + tokens(1)%type = TOKEN_PLAIN |
| 216 | + tokens(1)%start_col = 1 |
| 217 | + tokens(1)%end_col = max(1, line_len) |
| 218 | + end if |
| 219 | + end subroutine tokenize_line |
| 220 | + |
| 221 | + function get_token_color(tok_type) result(color) |
| 222 | + integer, intent(in) :: tok_type |
| 223 | + character(len=:), allocatable :: color |
| 224 | + |
| 225 | + select case(tok_type) |
| 226 | + case(TOKEN_KEYWORD) |
| 227 | + color = COLOR_KEYWORD |
| 228 | + case(TOKEN_STRING) |
| 229 | + color = COLOR_STRING |
| 230 | + case(TOKEN_NUMBER) |
| 231 | + color = COLOR_NUMBER |
| 232 | + case(TOKEN_COMMENT) |
| 233 | + color = COLOR_COMMENT |
| 234 | + case(TOKEN_OPERATOR) |
| 235 | + color = COLOR_OPERATOR |
| 236 | + case(TOKEN_TYPE) |
| 237 | + color = COLOR_TYPE |
| 238 | + case(TOKEN_FUNCTION) |
| 239 | + color = COLOR_FUNCTION |
| 240 | + case(TOKEN_PREPROCESSOR) |
| 241 | + color = COLOR_PREPROC |
| 242 | + case(TOKEN_PLAIN) |
| 243 | + color = "" |
| 244 | + case default |
| 245 | + color = "" |
| 246 | + end select |
| 247 | + end function get_token_color |
| 248 | + |
| 249 | + ! Language-specific loading routines |
| 250 | + subroutine load_fortran_syntax(highlighter) |
| 251 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 252 | + |
| 253 | + highlighter%current_lang%name = "fortran" |
| 254 | + highlighter%current_lang%case_sensitive = .false. |
| 255 | + |
| 256 | + ! Keywords |
| 257 | + allocate(highlighter%current_lang%keywords(58)) |
| 258 | + highlighter%current_lang%keywords = [ & |
| 259 | + "program ", "end ", "subroutine ", "function ", & |
| 260 | + "module ", "use ", "implicit ", "none ", & |
| 261 | + "if ", "then ", "else ", "elseif ", & |
| 262 | + "endif ", "do ", "while ", "enddo ", & |
| 263 | + "select ", "case ", "default ", "endselect ", & |
| 264 | + "where ", "elsewhere ", "endwhere ", "forall ", & |
| 265 | + "call ", "return ", "contains ", "interface ", & |
| 266 | + "abstract ", "allocate ", "deallocate ", "allocatable ", & |
| 267 | + "intent ", "in ", "out ", "inout ", & |
| 268 | + "optional ", "parameter ", "save ", "pointer ", & |
| 269 | + "target ", "public ", "private ", "protected ", & |
| 270 | + "bind ", "import ", "only ", "operator ", & |
| 271 | + "assignment ", "generic ", "final ", "extends ", & |
| 272 | + "class ", "type ", "endtype ", "enum ", & |
| 273 | + "enumerator ", "namelist " & |
| 274 | + ] |
| 275 | + |
| 276 | + ! Types |
| 277 | + allocate(highlighter%current_lang%types(10)) |
| 278 | + highlighter%current_lang%types = [ & |
| 279 | + "integer ", "real ", "complex ", "logical ", & |
| 280 | + "character ", "double ", "precision ", "int32 ", & |
| 281 | + "int64 ", "real64 " & |
| 282 | + ] |
| 283 | + |
| 284 | + ! Comments |
| 285 | + highlighter%current_lang%comment_single = "!" |
| 286 | + |
| 287 | + ! String delimiters |
| 288 | + allocate(highlighter%current_lang%string_delimiters(2)) |
| 289 | + highlighter%current_lang%string_delimiters = ['"', "'"] |
| 290 | + |
| 291 | + ! Operators |
| 292 | + allocate(highlighter%current_lang%operators(20)) |
| 293 | + highlighter%current_lang%operators = [ & |
| 294 | + "+ ", "- ", "* ", "/ ", "** ", "= ", & |
| 295 | + "== ", "/= ", "< ", "> ", "<= ", ">= ", & |
| 296 | + ".and", ".or.", ".not", ".eq.", ".ne.", ".lt.", & |
| 297 | + ".gt.", ".le." & |
| 298 | + ] |
| 299 | + |
| 300 | + highlighter%enabled = .true. |
| 301 | + end subroutine load_fortran_syntax |
| 302 | + |
| 303 | + subroutine load_python_syntax(highlighter) |
| 304 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 305 | + |
| 306 | + highlighter%current_lang%name = "python" |
| 307 | + highlighter%current_lang%case_sensitive = .true. |
| 308 | + |
| 309 | + ! Keywords |
| 310 | + allocate(highlighter%current_lang%keywords(35)) |
| 311 | + highlighter%current_lang%keywords = [ & |
| 312 | + "def ", "class ", "if ", "elif ", & |
| 313 | + "else ", "for ", "while ", "break ", & |
| 314 | + "continue ", "return ", "yield ", "import ", & |
| 315 | + "from ", "as ", "try ", "except ", & |
| 316 | + "finally ", "raise ", "with ", "assert ", & |
| 317 | + "lambda ", "pass ", "del ", "global ", & |
| 318 | + "nonlocal ", "in ", "is ", "and ", & |
| 319 | + "or ", "not ", "True ", "False ", & |
| 320 | + "None ", "async ", "await " & |
| 321 | + ] |
| 322 | + |
| 323 | + ! Types |
| 324 | + allocate(highlighter%current_lang%types(8)) |
| 325 | + highlighter%current_lang%types = [ & |
| 326 | + "int ", "float ", "str ", "bool ", & |
| 327 | + "list ", "dict ", "tuple ", "set " & |
| 328 | + ] |
| 329 | + |
| 330 | + highlighter%current_lang%comment_single = "#" |
| 331 | + |
| 332 | + ! String delimiters |
| 333 | + allocate(highlighter%current_lang%string_delimiters(4)) |
| 334 | + highlighter%current_lang%string_delimiters = ['" ', "' ", '""" ', "''' "] |
| 335 | + |
| 336 | + ! Operators |
| 337 | + allocate(highlighter%current_lang%operators(15)) |
| 338 | + highlighter%current_lang%operators = [ & |
| 339 | + "+ ", "- ", "* ", "/ ", "// ", "% ", "** ", & |
| 340 | + "= ", "== ", "!= ", "< ", "> ", "<= ", ">= ", & |
| 341 | + "& " & |
| 342 | + ] |
| 343 | + |
| 344 | + highlighter%enabled = .true. |
| 345 | + end subroutine load_python_syntax |
| 346 | + |
| 347 | + ! Helper functions |
| 348 | + function is_alpha(ch) result(res) |
| 349 | + character(len=1), intent(in) :: ch |
| 350 | + logical :: res |
| 351 | + res = (ch >= 'a' .and. ch <= 'z') .or. (ch >= 'A' .and. ch <= 'Z') |
| 352 | + end function is_alpha |
| 353 | + |
| 354 | + function is_digit(ch) result(res) |
| 355 | + character(len=1), intent(in) :: ch |
| 356 | + logical :: res |
| 357 | + res = (ch >= '0' .and. ch <= '9') |
| 358 | + end function is_digit |
| 359 | + |
| 360 | + function is_alnum(ch) result(res) |
| 361 | + character(len=1), intent(in) :: ch |
| 362 | + logical :: res |
| 363 | + res = is_alpha(ch) .or. is_digit(ch) .or. ch == '_' |
| 364 | + end function is_alnum |
| 365 | + |
| 366 | + function is_operator_char(highlighter, ch) result(res) |
| 367 | + type(syntax_highlighter_t), intent(in) :: highlighter |
| 368 | + character(len=1), intent(in) :: ch |
| 369 | + logical :: res |
| 370 | + integer :: i |
| 371 | + |
| 372 | + res = .false. |
| 373 | + if (.not. allocated(highlighter%current_lang%operators)) return |
| 374 | + |
| 375 | + do i = 1, size(highlighter%current_lang%operators) |
| 376 | + if (index(trim(highlighter%current_lang%operators(i)), ch) > 0) then |
| 377 | + res = .true. |
| 378 | + exit |
| 379 | + end if |
| 380 | + end do |
| 381 | + end function is_operator_char |
| 382 | + |
| 383 | + function check_comment_start(highlighter, line, pos) result(res) |
| 384 | + type(syntax_highlighter_t), intent(in) :: highlighter |
| 385 | + character(len=*), intent(in) :: line |
| 386 | + integer, intent(in) :: pos |
| 387 | + logical :: res |
| 388 | + integer :: comment_len |
| 389 | + |
| 390 | + res = .false. |
| 391 | + if (highlighter%current_lang%comment_single /= "") then |
| 392 | + comment_len = len_trim(highlighter%current_lang%comment_single) |
| 393 | + if (pos + comment_len - 1 <= len(line)) then |
| 394 | + res = line(pos:pos+comment_len-1) == trim(highlighter%current_lang%comment_single) |
| 395 | + end if |
| 396 | + end if |
| 397 | + end function check_comment_start |
| 398 | + |
| 399 | + function check_string_start(highlighter, line, pos) result(res) |
| 400 | + type(syntax_highlighter_t), intent(in) :: highlighter |
| 401 | + character(len=*), intent(in) :: line |
| 402 | + integer, intent(in) :: pos |
| 403 | + logical :: res |
| 404 | + integer :: i, delim_len |
| 405 | + |
| 406 | + res = .false. |
| 407 | + if (.not. allocated(highlighter%current_lang%string_delimiters)) return |
| 408 | + |
| 409 | + do i = 1, size(highlighter%current_lang%string_delimiters) |
| 410 | + delim_len = len_trim(highlighter%current_lang%string_delimiters(i)) |
| 411 | + if (pos + delim_len - 1 <= len(line)) then |
| 412 | + if (line(pos:pos+delim_len-1) == trim(highlighter%current_lang%string_delimiters(i))) then |
| 413 | + res = .true. |
| 414 | + exit |
| 415 | + end if |
| 416 | + end if |
| 417 | + end do |
| 418 | + end function check_string_start |
| 419 | + |
| 420 | + subroutine process_string(highlighter, line, tokens, token_count, pos) |
| 421 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 422 | + character(len=*), intent(in) :: line |
| 423 | + type(token_t), intent(inout) :: tokens(:) |
| 424 | + integer, intent(inout) :: token_count, pos |
| 425 | + integer :: i, start_pos, delim_len, line_len |
| 426 | + character(len=:), allocatable :: delimiter |
| 427 | + logical :: found_end |
| 428 | + |
| 429 | + line_len = len(line) |
| 430 | + start_pos = pos |
| 431 | + |
| 432 | + ! Find which delimiter matches |
| 433 | + do i = 1, size(highlighter%current_lang%string_delimiters) |
| 434 | + delim_len = len_trim(highlighter%current_lang%string_delimiters(i)) |
| 435 | + if (pos + delim_len - 1 <= line_len) then |
| 436 | + if (line(pos:pos+delim_len-1) == trim(highlighter%current_lang%string_delimiters(i))) then |
| 437 | + delimiter = trim(highlighter%current_lang%string_delimiters(i)) |
| 438 | + exit |
| 439 | + end if |
| 440 | + end if |
| 441 | + end do |
| 442 | + |
| 443 | + ! Move past opening delimiter |
| 444 | + pos = pos + len(delimiter) |
| 445 | + |
| 446 | + ! Find closing delimiter |
| 447 | + found_end = .false. |
| 448 | + do while (pos <= line_len) |
| 449 | + if (line(pos:pos) == '\' .and. pos < line_len) then |
| 450 | + ! Skip escaped character |
| 451 | + pos = pos + 2 |
| 452 | + else if (pos + len(delimiter) - 1 <= line_len) then |
| 453 | + if (line(pos:pos+len(delimiter)-1) == delimiter) then |
| 454 | + pos = pos + len(delimiter) |
| 455 | + found_end = .true. |
| 456 | + exit |
| 457 | + else |
| 458 | + pos = pos + 1 |
| 459 | + end if |
| 460 | + else |
| 461 | + pos = pos + 1 |
| 462 | + end if |
| 463 | + end do |
| 464 | + |
| 465 | + ! Add string token |
| 466 | + token_count = token_count + 1 |
| 467 | + tokens(token_count)%type = TOKEN_STRING |
| 468 | + tokens(token_count)%start_col = start_pos |
| 469 | + tokens(token_count)%end_col = min(pos - 1, line_len) |
| 470 | + |
| 471 | + ! Handle unclosed string |
| 472 | + if (.not. found_end) then |
| 473 | + pos = line_len + 1 |
| 474 | + end if |
| 475 | + end subroutine process_string |
| 476 | + |
| 477 | + subroutine process_number(line, tokens, token_count, pos) |
| 478 | + character(len=*), intent(in) :: line |
| 479 | + type(token_t), intent(inout) :: tokens(:) |
| 480 | + integer, intent(inout) :: token_count, pos |
| 481 | + integer :: start_pos |
| 482 | + logical :: has_dot, has_e |
| 483 | + |
| 484 | + start_pos = pos |
| 485 | + has_dot = .false. |
| 486 | + has_e = .false. |
| 487 | + |
| 488 | + do while (pos <= len(line)) |
| 489 | + if (is_digit(line(pos:pos))) then |
| 490 | + pos = pos + 1 |
| 491 | + else if (line(pos:pos) == '.' .and. .not. has_dot .and. .not. has_e) then |
| 492 | + has_dot = .true. |
| 493 | + pos = pos + 1 |
| 494 | + else if ((line(pos:pos) == 'e' .or. line(pos:pos) == 'E') .and. .not. has_e) then |
| 495 | + has_e = .true. |
| 496 | + pos = pos + 1 |
| 497 | + if (pos <= len(line) .and. (line(pos:pos) == '+' .or. line(pos:pos) == '-')) then |
| 498 | + pos = pos + 1 |
| 499 | + end if |
| 500 | + else |
| 501 | + exit |
| 502 | + end if |
| 503 | + end do |
| 504 | + |
| 505 | + token_count = token_count + 1 |
| 506 | + tokens(token_count)%type = TOKEN_NUMBER |
| 507 | + tokens(token_count)%start_col = start_pos |
| 508 | + tokens(token_count)%end_col = pos - 1 |
| 509 | + end subroutine process_number |
| 510 | + |
| 511 | + subroutine process_word(highlighter, line, tokens, token_count, pos) |
| 512 | + type(syntax_highlighter_t), intent(in) :: highlighter |
| 513 | + character(len=*), intent(in) :: line |
| 514 | + type(token_t), intent(inout) :: tokens(:) |
| 515 | + integer, intent(inout) :: token_count, pos |
| 516 | + integer :: start_pos, end_pos, i |
| 517 | + character(len=:), allocatable :: word |
| 518 | + logical :: is_keyword, is_type |
| 519 | + |
| 520 | + start_pos = pos |
| 521 | + |
| 522 | + ! Find end of word |
| 523 | + do while (pos <= len(line) .and. is_alnum(line(pos:pos))) |
| 524 | + pos = pos + 1 |
| 525 | + end do |
| 526 | + end_pos = pos - 1 |
| 527 | + |
| 528 | + word = line(start_pos:end_pos) |
| 529 | + |
| 530 | + ! Check if it's a keyword |
| 531 | + is_keyword = .false. |
| 532 | + if (allocated(highlighter%current_lang%keywords)) then |
| 533 | + do i = 1, size(highlighter%current_lang%keywords) |
| 534 | + if (compare_word(word, highlighter%current_lang%keywords(i), & |
| 535 | + highlighter%current_lang%case_sensitive)) then |
| 536 | + is_keyword = .true. |
| 537 | + exit |
| 538 | + end if |
| 539 | + end do |
| 540 | + end if |
| 541 | + |
| 542 | + ! Check if it's a type |
| 543 | + is_type = .false. |
| 544 | + if (.not. is_keyword .and. allocated(highlighter%current_lang%types)) then |
| 545 | + do i = 1, size(highlighter%current_lang%types) |
| 546 | + if (compare_word(word, highlighter%current_lang%types(i), & |
| 547 | + highlighter%current_lang%case_sensitive)) then |
| 548 | + is_type = .true. |
| 549 | + exit |
| 550 | + end if |
| 551 | + end do |
| 552 | + end if |
| 553 | + |
| 554 | + token_count = token_count + 1 |
| 555 | + tokens(token_count)%start_col = start_pos |
| 556 | + tokens(token_count)%end_col = end_pos |
| 557 | + |
| 558 | + if (is_keyword) then |
| 559 | + tokens(token_count)%type = TOKEN_KEYWORD |
| 560 | + else if (is_type) then |
| 561 | + tokens(token_count)%type = TOKEN_TYPE |
| 562 | + else |
| 563 | + tokens(token_count)%type = TOKEN_PLAIN |
| 564 | + end if |
| 565 | + end subroutine process_word |
| 566 | + |
| 567 | + function compare_word(word1, word2, case_sensitive) result(match) |
| 568 | + character(len=*), intent(in) :: word1, word2 |
| 569 | + logical, intent(in) :: case_sensitive |
| 570 | + logical :: match |
| 571 | + |
| 572 | + if (case_sensitive) then |
| 573 | + match = trim(word1) == trim(word2) |
| 574 | + else |
| 575 | + match = to_lower(trim(word1)) == to_lower(trim(word2)) |
| 576 | + end if |
| 577 | + end function compare_word |
| 578 | + |
| 579 | + function to_lower(str) result(lower_str) |
| 580 | + character(len=*), intent(in) :: str |
| 581 | + character(len=len(str)) :: lower_str |
| 582 | + integer :: i |
| 583 | + |
| 584 | + lower_str = str |
| 585 | + do i = 1, len(str) |
| 586 | + if (str(i:i) >= 'A' .and. str(i:i) <= 'Z') then |
| 587 | + lower_str(i:i) = char(ichar(str(i:i)) + 32) |
| 588 | + end if |
| 589 | + end do |
| 590 | + end function to_lower |
| 591 | + |
| 592 | + ! C language support |
| 593 | + subroutine load_c_syntax(highlighter) |
| 594 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 595 | + |
| 596 | + highlighter%current_lang%name = "c" |
| 597 | + highlighter%current_lang%case_sensitive = .true. |
| 598 | + |
| 599 | + ! Keywords |
| 600 | + allocate(highlighter%current_lang%keywords(32)) |
| 601 | + highlighter%current_lang%keywords = [ & |
| 602 | + "auto ", "break ", "case ", "char ", & |
| 603 | + "const ", "continue ", "default ", "do ", & |
| 604 | + "double ", "else ", "enum ", "extern ", & |
| 605 | + "float ", "for ", "goto ", "if ", & |
| 606 | + "inline ", "int ", "long ", "register ", & |
| 607 | + "return ", "short ", "signed ", "sizeof ", & |
| 608 | + "static ", "struct ", "switch ", "typedef ", & |
| 609 | + "union ", "unsigned ", "void ", "while " & |
| 610 | + ] |
| 611 | + |
| 612 | + ! Types |
| 613 | + allocate(highlighter%current_lang%types(8)) |
| 614 | + highlighter%current_lang%types = [ & |
| 615 | + "size_t ", "uint32_t ", "int32_t ", "uint64_t ", & |
| 616 | + "int64_t ", "bool ", "FILE ", "NULL " & |
| 617 | + ] |
| 618 | + |
| 619 | + highlighter%current_lang%comment_single = "//" |
| 620 | + highlighter%current_lang%comment_start = "/*" |
| 621 | + highlighter%current_lang%comment_end = "*/" |
| 622 | + |
| 623 | + allocate(highlighter%current_lang%string_delimiters(2)) |
| 624 | + highlighter%current_lang%string_delimiters = ['"', "'"] |
| 625 | + |
| 626 | + allocate(highlighter%current_lang%operators(20)) |
| 627 | + highlighter%current_lang%operators = [ & |
| 628 | + "+ ", "- ", "* ", "/ ", "% ", "= ", & |
| 629 | + "== ", "!= ", "< ", "> ", "<= ", ">= ", & |
| 630 | + "&& ", "|| ", "! ", "& ", "| ", "^ ", & |
| 631 | + "<< ", ">> " & |
| 632 | + ] |
| 633 | + |
| 634 | + highlighter%enabled = .true. |
| 635 | + end subroutine load_c_syntax |
| 636 | + |
| 637 | + ! C++ language support |
| 638 | + subroutine load_cpp_syntax(highlighter) |
| 639 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 640 | + |
| 641 | + highlighter%current_lang%name = "cpp" |
| 642 | + highlighter%current_lang%case_sensitive = .true. |
| 643 | + |
| 644 | + ! Keywords (C++ specific + C keywords) |
| 645 | + allocate(highlighter%current_lang%keywords(48)) |
| 646 | + highlighter%current_lang%keywords = [ & |
| 647 | + "auto ", "break ", "case ", "char ", & |
| 648 | + "const ", "continue ", "default ", "do ", & |
| 649 | + "double ", "else ", "enum ", "extern ", & |
| 650 | + "float ", "for ", "goto ", "if ", & |
| 651 | + "inline ", "int ", "long ", "register ", & |
| 652 | + "return ", "short ", "signed ", "sizeof ", & |
| 653 | + "static ", "struct ", "switch ", "typedef ", & |
| 654 | + "union ", "unsigned ", "void ", "while ", & |
| 655 | + "class ", "namespace ", "template ", "typename ", & |
| 656 | + "new ", "delete ", "this ", "friend ", & |
| 657 | + "virtual ", "override ", "final ", "public ", & |
| 658 | + "private ", "protected ", "try ", "catch " & |
| 659 | + ] |
| 660 | + |
| 661 | + ! Types |
| 662 | + allocate(highlighter%current_lang%types(12)) |
| 663 | + highlighter%current_lang%types = [ & |
| 664 | + "std ", "string ", "vector ", "map ", & |
| 665 | + "set ", "pair ", "unique_ptr ", "shared_ptr ", & |
| 666 | + "nullptr ", "true ", "false ", "bool " & |
| 667 | + ] |
| 668 | + |
| 669 | + highlighter%current_lang%comment_single = "//" |
| 670 | + highlighter%current_lang%comment_start = "/*" |
| 671 | + highlighter%current_lang%comment_end = "*/" |
| 672 | + |
| 673 | + allocate(highlighter%current_lang%string_delimiters(2)) |
| 674 | + highlighter%current_lang%string_delimiters = ['"', "'"] |
| 675 | + |
| 676 | + allocate(highlighter%current_lang%operators(22)) |
| 677 | + highlighter%current_lang%operators = [ & |
| 678 | + "+ ", "- ", "* ", "/ ", "% ", "= ", & |
| 679 | + "== ", "!= ", "< ", "> ", "<= ", ">= ", & |
| 680 | + "&& ", "|| ", "! ", "& ", "| ", "^ ", & |
| 681 | + "<< ", ">> ", ":: ", "-> " & |
| 682 | + ] |
| 683 | + |
| 684 | + highlighter%enabled = .true. |
| 685 | + end subroutine load_cpp_syntax |
| 686 | + |
| 687 | + ! Rust language support |
| 688 | + subroutine load_rust_syntax(highlighter) |
| 689 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 690 | + |
| 691 | + highlighter%current_lang%name = "rust" |
| 692 | + highlighter%current_lang%case_sensitive = .true. |
| 693 | + |
| 694 | + ! Keywords |
| 695 | + allocate(highlighter%current_lang%keywords(40)) |
| 696 | + highlighter%current_lang%keywords = [ & |
| 697 | + "as ", "async ", "await ", "break ", & |
| 698 | + "const ", "continue ", "crate ", "dyn ", & |
| 699 | + "else ", "enum ", "extern ", "false ", & |
| 700 | + "fn ", "for ", "if ", "impl ", & |
| 701 | + "in ", "let ", "loop ", "match ", & |
| 702 | + "mod ", "move ", "mut ", "pub ", & |
| 703 | + "ref ", "return ", "self ", "Self ", & |
| 704 | + "static ", "struct ", "super ", "trait ", & |
| 705 | + "true ", "type ", "unsafe ", "use ", & |
| 706 | + "where ", "while ", "async ", "await " & |
| 707 | + ] |
| 708 | + |
| 709 | + ! Types |
| 710 | + allocate(highlighter%current_lang%types(16)) |
| 711 | + highlighter%current_lang%types = [ & |
| 712 | + "i8 ", "i16 ", "i32 ", "i64 ", & |
| 713 | + "i128 ", "u8 ", "u16 ", "u32 ", & |
| 714 | + "u64 ", "u128 ", "f32 ", "f64 ", & |
| 715 | + "bool ", "char ", "str ", "String " & |
| 716 | + ] |
| 717 | + |
| 718 | + highlighter%current_lang%comment_single = "//" |
| 719 | + highlighter%current_lang%comment_start = "/*" |
| 720 | + highlighter%current_lang%comment_end = "*/" |
| 721 | + |
| 722 | + allocate(highlighter%current_lang%string_delimiters(2)) |
| 723 | + highlighter%current_lang%string_delimiters = ['"', "'"] |
| 724 | + |
| 725 | + allocate(highlighter%current_lang%operators(20)) |
| 726 | + highlighter%current_lang%operators = [ & |
| 727 | + "+ ", "- ", "* ", "/ ", "% ", "= ", & |
| 728 | + "== ", "!= ", "< ", "> ", "<= ", ">= ", & |
| 729 | + "&& ", "|| ", "! ", "& ", "| ", "^ ", & |
| 730 | + ":: ", "-> " & |
| 731 | + ] |
| 732 | + |
| 733 | + highlighter%enabled = .true. |
| 734 | + end subroutine load_rust_syntax |
| 735 | + |
| 736 | + ! Go language support |
| 737 | + subroutine load_go_syntax(highlighter) |
| 738 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 739 | + |
| 740 | + highlighter%current_lang%name = "go" |
| 741 | + highlighter%current_lang%case_sensitive = .true. |
| 742 | + |
| 743 | + ! Keywords |
| 744 | + allocate(highlighter%current_lang%keywords(25)) |
| 745 | + highlighter%current_lang%keywords = [ & |
| 746 | + "break ", "case ", "chan ", "const ", & |
| 747 | + "continue ", "default ", "defer ", "else ", & |
| 748 | + "fallthrough ", "for ", "func ", "go ", & |
| 749 | + "goto ", "if ", "import ", "interface ", & |
| 750 | + "map ", "package ", "range ", "return ", & |
| 751 | + "select ", "struct ", "switch ", "type ", & |
| 752 | + "var " & |
| 753 | + ] |
| 754 | + |
| 755 | + ! Types |
| 756 | + allocate(highlighter%current_lang%types(15)) |
| 757 | + highlighter%current_lang%types = [ & |
| 758 | + "bool ", "byte ", "complex64 ", "complex128 ", & |
| 759 | + "error ", "float32 ", "float64 ", "int ", & |
| 760 | + "int8 ", "int16 ", "int32 ", "int64 ", & |
| 761 | + "string ", "uint ", "nil " & |
| 762 | + ] |
| 763 | + |
| 764 | + highlighter%current_lang%comment_single = "//" |
| 765 | + highlighter%current_lang%comment_start = "/*" |
| 766 | + highlighter%current_lang%comment_end = "*/" |
| 767 | + |
| 768 | + allocate(highlighter%current_lang%string_delimiters(3)) |
| 769 | + highlighter%current_lang%string_delimiters = ['"', "'", '`'] |
| 770 | + |
| 771 | + allocate(highlighter%current_lang%operators(18)) |
| 772 | + highlighter%current_lang%operators = [ & |
| 773 | + "+ ", "- ", "* ", "/ ", "% ", "= ", & |
| 774 | + "== ", "!= ", "< ", "> ", "<= ", ">= ", & |
| 775 | + "&& ", "|| ", "! ", "& ", "| ", ":= " & |
| 776 | + ] |
| 777 | + |
| 778 | + highlighter%enabled = .true. |
| 779 | + end subroutine load_go_syntax |
| 780 | + |
| 781 | + ! JavaScript language support |
| 782 | + subroutine load_javascript_syntax(highlighter) |
| 783 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 784 | + |
| 785 | + highlighter%current_lang%name = "javascript" |
| 786 | + highlighter%current_lang%case_sensitive = .true. |
| 787 | + |
| 788 | + ! Keywords |
| 789 | + allocate(highlighter%current_lang%keywords(38)) |
| 790 | + highlighter%current_lang%keywords = [ & |
| 791 | + "async ", "await ", "break ", "case ", & |
| 792 | + "catch ", "class ", "const ", "continue ", & |
| 793 | + "debugger ", "default ", "delete ", "do ", & |
| 794 | + "else ", "export ", "extends ", "finally ", & |
| 795 | + "for ", "function ", "if ", "import ", & |
| 796 | + "in ", "instanceof ", "let ", "new ", & |
| 797 | + "return ", "super ", "switch ", "this ", & |
| 798 | + "throw ", "try ", "typeof ", "var ", & |
| 799 | + "void ", "while ", "with ", "yield ", & |
| 800 | + "true ", "false " & |
| 801 | + ] |
| 802 | + |
| 803 | + ! Types |
| 804 | + allocate(highlighter%current_lang%types(10)) |
| 805 | + highlighter%current_lang%types = [ & |
| 806 | + "null ", "undefined ", "Boolean ", "Number ", & |
| 807 | + "String ", "Symbol ", "Object ", "Array ", & |
| 808 | + "Function ", "Promise " & |
| 809 | + ] |
| 810 | + |
| 811 | + highlighter%current_lang%comment_single = "//" |
| 812 | + highlighter%current_lang%comment_start = "/*" |
| 813 | + highlighter%current_lang%comment_end = "*/" |
| 814 | + |
| 815 | + allocate(highlighter%current_lang%string_delimiters(3)) |
| 816 | + highlighter%current_lang%string_delimiters = ['"', "'", '`'] |
| 817 | + |
| 818 | + allocate(highlighter%current_lang%operators(20)) |
| 819 | + highlighter%current_lang%operators = [ & |
| 820 | + "+ ", "- ", "* ", "/ ", "% ", "= ", & |
| 821 | + "== ", "=== ", "!= ", "!== ", "< ", "> ", & |
| 822 | + "<= ", ">= ", "&& ", "|| ", "! ", "? ", & |
| 823 | + ": ", "=> " & |
| 824 | + ] |
| 825 | + |
| 826 | + highlighter%enabled = .true. |
| 827 | + end subroutine load_javascript_syntax |
| 828 | + |
| 829 | + ! TypeScript language support (extends JavaScript) |
| 830 | + subroutine load_typescript_syntax(highlighter) |
| 831 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 832 | + |
| 833 | + ! Start with JavaScript syntax |
| 834 | + call load_javascript_syntax(highlighter) |
| 835 | + highlighter%current_lang%name = "typescript" |
| 836 | + |
| 837 | + ! Add TypeScript-specific keywords |
| 838 | + deallocate(highlighter%current_lang%keywords) |
| 839 | + allocate(highlighter%current_lang%keywords(45)) |
| 840 | + highlighter%current_lang%keywords = [ & |
| 841 | + "async ", "await ", "break ", "case ", & |
| 842 | + "catch ", "class ", "const ", "continue ", & |
| 843 | + "debugger ", "default ", "delete ", "do ", & |
| 844 | + "else ", "export ", "extends ", "finally ", & |
| 845 | + "for ", "function ", "if ", "import ", & |
| 846 | + "in ", "instanceof ", "let ", "new ", & |
| 847 | + "return ", "super ", "switch ", "this ", & |
| 848 | + "throw ", "try ", "typeof ", "var ", & |
| 849 | + "void ", "while ", "with ", "yield ", & |
| 850 | + "true ", "false ", "enum ", "interface ", & |
| 851 | + "type ", "namespace ", "module ", "declare ", & |
| 852 | + "abstract " & |
| 853 | + ] |
| 854 | + |
| 855 | + ! Add TypeScript types |
| 856 | + deallocate(highlighter%current_lang%types) |
| 857 | + allocate(highlighter%current_lang%types(15)) |
| 858 | + highlighter%current_lang%types = [ & |
| 859 | + "null ", "undefined ", "boolean ", "number ", & |
| 860 | + "string ", "symbol ", "object ", "any ", & |
| 861 | + "unknown ", "never ", "void ", "Array ", & |
| 862 | + "Function ", "Promise ", "ReadonlyArra" & |
| 863 | + ] |
| 864 | + |
| 865 | + highlighter%enabled = .true. |
| 866 | + end subroutine load_typescript_syntax |
| 867 | + |
| 868 | + ! Bash/Shell script support |
| 869 | + subroutine load_bash_syntax(highlighter) |
| 870 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 871 | + |
| 872 | + highlighter%current_lang%name = "bash" |
| 873 | + highlighter%current_lang%case_sensitive = .true. |
| 874 | + |
| 875 | + ! Keywords |
| 876 | + allocate(highlighter%current_lang%keywords(30)) |
| 877 | + highlighter%current_lang%keywords = [ & |
| 878 | + "if ", "then ", "else ", "elif ", & |
| 879 | + "fi ", "for ", "while ", "do ", & |
| 880 | + "done ", "case ", "esac ", "function ", & |
| 881 | + "return ", "break ", "continue ", "exit ", & |
| 882 | + "export ", "source ", "alias ", "unset ", & |
| 883 | + "shift ", "local ", "declare ", "readonly ", & |
| 884 | + "echo ", "printf ", "read ", "cd ", & |
| 885 | + "pwd ", "ls " & |
| 886 | + ] |
| 887 | + |
| 888 | + ! Built-in variables/types |
| 889 | + allocate(highlighter%current_lang%types(10)) |
| 890 | + highlighter%current_lang%types = [ & |
| 891 | + "$0 ", "$1 ", "$@ ", "$* ", & |
| 892 | + "$# ", "$? ", "$$ ", "$! ", & |
| 893 | + "true ", "false " & |
| 894 | + ] |
| 895 | + |
| 896 | + highlighter%current_lang%comment_single = "#" |
| 897 | + |
| 898 | + allocate(highlighter%current_lang%string_delimiters(3)) |
| 899 | + highlighter%current_lang%string_delimiters = ['"', "'", '`'] |
| 900 | + |
| 901 | + allocate(highlighter%current_lang%operators(15)) |
| 902 | + highlighter%current_lang%operators = [ & |
| 903 | + "= ", "== ", "!= ", "< ", "> ", & |
| 904 | + "-eq ", "-ne ", "-lt ", "-gt ", "-le ", & |
| 905 | + "-ge ", "&& ", "|| ", "| ", "& " & |
| 906 | + ] |
| 907 | + |
| 908 | + highlighter%enabled = .true. |
| 909 | + end subroutine load_bash_syntax |
| 910 | + |
| 911 | + ! Markdown support (special handling needed) |
| 912 | + subroutine load_markdown_syntax(highlighter) |
| 913 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 914 | + |
| 915 | + highlighter%current_lang%name = "markdown" |
| 916 | + highlighter%current_lang%case_sensitive = .true. |
| 917 | + |
| 918 | + ! Headers and emphasis markers as "keywords" |
| 919 | + allocate(highlighter%current_lang%keywords(8)) |
| 920 | + highlighter%current_lang%keywords = [ & |
| 921 | + "# ", "## ", "### ", "#### ", & |
| 922 | + "##### ", "###### ", "* ", "_ " & |
| 923 | + ] |
| 924 | + |
| 925 | + ! Code block languages as "types" |
| 926 | + allocate(highlighter%current_lang%types(10)) |
| 927 | + highlighter%current_lang%types = [ & |
| 928 | + "``` ", "```python ", "```bash ", "```fortran ", & |
| 929 | + "```c ", "```cpp ", "```rust ", "```go ", & |
| 930 | + "```javascri ", "```typescri " & |
| 931 | + ] |
| 932 | + |
| 933 | + ! No traditional comments in markdown |
| 934 | + highlighter%current_lang%comment_single = "" |
| 935 | + |
| 936 | + ! Links and code spans |
| 937 | + allocate(highlighter%current_lang%string_delimiters(2)) |
| 938 | + highlighter%current_lang%string_delimiters = ['`', '['] |
| 939 | + |
| 940 | + ! List markers and special chars |
| 941 | + allocate(highlighter%current_lang%operators(6)) |
| 942 | + highlighter%current_lang%operators = [ & |
| 943 | + "- ", "+ ", "* ", "> ", "| ", "! " & |
| 944 | + ] |
| 945 | + |
| 946 | + highlighter%enabled = .true. |
| 947 | + end subroutine load_markdown_syntax |
| 948 | + |
| 949 | + ! Stub implementations for multiline handling |
| 950 | + subroutine process_multiline_comment(highlighter, line, tokens, token_count, pos) |
| 951 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 952 | + character(len=*), intent(in) :: line |
| 953 | + type(token_t), intent(inout) :: tokens(:) |
| 954 | + integer, intent(inout) :: token_count, pos |
| 955 | + ! TODO: Implement multiline comment handling |
| 956 | + end subroutine process_multiline_comment |
| 957 | + |
| 958 | + subroutine process_multiline_string(highlighter, line, tokens, token_count, pos) |
| 959 | + type(syntax_highlighter_t), intent(inout) :: highlighter |
| 960 | + character(len=*), intent(in) :: line |
| 961 | + type(token_t), intent(inout) :: tokens(:) |
| 962 | + integer, intent(inout) :: token_count, pos |
| 963 | + ! TODO: Implement multiline string handling |
| 964 | + end subroutine process_multiline_string |
| 965 | + |
| 966 | +end module syntax_highlighter_module |