| 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=*), parameter :: COLOR_KEYWORD = char(27) // '[1;34m' ! Bold Blue |
| 58 | character(len=*), parameter :: COLOR_STRING = char(27) // '[32m' ! Green |
| 59 | character(len=*), parameter :: COLOR_NUMBER = char(27) // '[35m' ! Magenta |
| 60 | character(len=*), parameter :: COLOR_COMMENT = char(27) // '[90m' ! Gray |
| 61 | character(len=*), parameter :: COLOR_OPERATOR = char(27) // '[33m' ! Yellow |
| 62 | character(len=*), parameter :: COLOR_TYPE = char(27) // '[36m' ! Cyan |
| 63 | character(len=*), parameter :: COLOR_FUNCTION = char(27) // '[1;36m' ! Bold Cyan |
| 64 | character(len=*), parameter :: COLOR_PREPROC = char(27) // '[95m' ! Light Magenta |
| 65 | character(len=*), 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, line_len, token_count |
| 140 | character :: ch |
| 141 | |
| 142 | if (.not. highlighter%enabled) then |
| 143 | allocate(tokens(1)) |
| 144 | tokens(1)%type = TOKEN_PLAIN |
| 145 | tokens(1)%start_col = 1 |
| 146 | tokens(1)%end_col = len(line) |
| 147 | return |
| 148 | end if |
| 149 | |
| 150 | line_len = len(line) |
| 151 | allocate(tokens(line_len)) ! Worst case: each char is a token |
| 152 | token_count = 0 |
| 153 | i = 1 |
| 154 | |
| 155 | ! Handle multiline comment continuation |
| 156 | if (highlighter%in_multiline_comment) then |
| 157 | call process_multiline_comment(highlighter, line, tokens, token_count, i) |
| 158 | end if |
| 159 | |
| 160 | ! Handle multiline string continuation |
| 161 | if (highlighter%in_multiline_string) then |
| 162 | call process_multiline_string(highlighter, line, tokens, token_count, i) |
| 163 | end if |
| 164 | |
| 165 | ! Process rest of line |
| 166 | do while (i <= line_len) |
| 167 | ch = line(i:i) |
| 168 | |
| 169 | ! Check for multiline comment start |
| 170 | if (check_multiline_comment_start(highlighter, line, i)) then |
| 171 | call process_multiline_comment_start(highlighter, line, tokens, token_count, i) |
| 172 | |
| 173 | ! Check for single-line comment |
| 174 | else if (check_comment_start(highlighter, line, i)) then |
| 175 | token_count = token_count + 1 |
| 176 | tokens(token_count)%type = TOKEN_COMMENT |
| 177 | tokens(token_count)%start_col = i |
| 178 | tokens(token_count)%end_col = line_len |
| 179 | exit |
| 180 | end if |
| 181 | |
| 182 | ! Check for string |
| 183 | if (check_string_start(highlighter, line, i)) then |
| 184 | call process_string(highlighter, line, tokens, token_count, i) |
| 185 | |
| 186 | ! Check for number |
| 187 | else if (is_digit(ch) .or. (ch == '.' .and. i + 1 <= line_len .and. is_digit(line(i+1:i+1)))) then |
| 188 | call process_number(line, tokens, token_count, i) |
| 189 | |
| 190 | ! Check for word (keyword, type, identifier) |
| 191 | else if (is_alpha(ch) .or. ch == '_') then |
| 192 | call process_word(highlighter, line, tokens, token_count, i) |
| 193 | |
| 194 | ! Check for operator |
| 195 | else if (is_operator_char(highlighter, ch)) then |
| 196 | token_count = token_count + 1 |
| 197 | tokens(token_count)%type = TOKEN_OPERATOR |
| 198 | tokens(token_count)%start_col = i |
| 199 | tokens(token_count)%end_col = i |
| 200 | i = i + 1 |
| 201 | |
| 202 | ! Plain character (space, etc.) |
| 203 | else |
| 204 | token_count = token_count + 1 |
| 205 | tokens(token_count)%type = TOKEN_PLAIN |
| 206 | tokens(token_count)%start_col = i |
| 207 | tokens(token_count)%end_col = i |
| 208 | i = i + 1 |
| 209 | end if |
| 210 | end do |
| 211 | |
| 212 | ! Resize tokens array |
| 213 | if (token_count > 0) then |
| 214 | tokens = tokens(1:token_count) |
| 215 | else |
| 216 | ! tokens is already allocated, just resize to 1 element |
| 217 | deallocate(tokens) |
| 218 | allocate(tokens(1)) |
| 219 | tokens(1)%type = TOKEN_PLAIN |
| 220 | tokens(1)%start_col = 1 |
| 221 | tokens(1)%end_col = max(1, line_len) |
| 222 | end if |
| 223 | end subroutine tokenize_line |
| 224 | |
| 225 | function get_token_color(tok_type) result(color) |
| 226 | integer, intent(in) :: tok_type |
| 227 | character(len=:), allocatable :: color |
| 228 | |
| 229 | select case(tok_type) |
| 230 | case(TOKEN_KEYWORD) |
| 231 | color = COLOR_KEYWORD |
| 232 | case(TOKEN_STRING) |
| 233 | color = COLOR_STRING |
| 234 | case(TOKEN_NUMBER) |
| 235 | color = COLOR_NUMBER |
| 236 | case(TOKEN_COMMENT) |
| 237 | color = COLOR_COMMENT |
| 238 | case(TOKEN_OPERATOR) |
| 239 | color = COLOR_OPERATOR |
| 240 | case(TOKEN_TYPE) |
| 241 | color = COLOR_TYPE |
| 242 | case(TOKEN_FUNCTION) |
| 243 | color = COLOR_FUNCTION |
| 244 | case(TOKEN_PREPROCESSOR) |
| 245 | color = COLOR_PREPROC |
| 246 | case(TOKEN_PLAIN) |
| 247 | color = "" |
| 248 | case default |
| 249 | color = "" |
| 250 | end select |
| 251 | end function get_token_color |
| 252 | |
| 253 | ! Language-specific loading routines |
| 254 | subroutine load_fortran_syntax(highlighter) |
| 255 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 256 | |
| 257 | highlighter%current_lang%name = "fortran" |
| 258 | highlighter%current_lang%case_sensitive = .false. |
| 259 | |
| 260 | ! Keywords |
| 261 | allocate(highlighter%current_lang%keywords(58)) |
| 262 | highlighter%current_lang%keywords = [ & |
| 263 | "program ", "end ", "subroutine ", "function ", & |
| 264 | "module ", "use ", "implicit ", "none ", & |
| 265 | "if ", "then ", "else ", "elseif ", & |
| 266 | "endif ", "do ", "while ", "enddo ", & |
| 267 | "select ", "case ", "default ", "endselect ", & |
| 268 | "where ", "elsewhere ", "endwhere ", "forall ", & |
| 269 | "call ", "return ", "contains ", "interface ", & |
| 270 | "abstract ", "allocate ", "deallocate ", "allocatable ", & |
| 271 | "intent ", "in ", "out ", "inout ", & |
| 272 | "optional ", "parameter ", "save ", "pointer ", & |
| 273 | "target ", "public ", "private ", "protected ", & |
| 274 | "bind ", "import ", "only ", "operator ", & |
| 275 | "assignment ", "generic ", "final ", "extends ", & |
| 276 | "class ", "type ", "endtype ", "enum ", & |
| 277 | "enumerator ", "namelist " & |
| 278 | ] |
| 279 | |
| 280 | ! Types |
| 281 | allocate(highlighter%current_lang%types(10)) |
| 282 | highlighter%current_lang%types = [ & |
| 283 | "integer ", "real ", "complex ", "logical ", & |
| 284 | "character ", "double ", "precision ", "int32 ", & |
| 285 | "int64 ", "real64 " & |
| 286 | ] |
| 287 | |
| 288 | ! Comments |
| 289 | highlighter%current_lang%comment_single = "!" |
| 290 | |
| 291 | ! String delimiters |
| 292 | allocate(highlighter%current_lang%string_delimiters(2)) |
| 293 | highlighter%current_lang%string_delimiters = ['"', "'"] |
| 294 | |
| 295 | ! Operators |
| 296 | allocate(highlighter%current_lang%operators(20)) |
| 297 | highlighter%current_lang%operators = [ & |
| 298 | "+ ", "- ", "* ", "/ ", "** ", "= ", & |
| 299 | "== ", "/= ", "< ", "> ", "<= ", ">= ", & |
| 300 | ".and", ".or.", ".not", ".eq.", ".ne.", ".lt.", & |
| 301 | ".gt.", ".le." & |
| 302 | ] |
| 303 | |
| 304 | highlighter%enabled = .true. |
| 305 | end subroutine load_fortran_syntax |
| 306 | |
| 307 | subroutine load_python_syntax(highlighter) |
| 308 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 309 | |
| 310 | highlighter%current_lang%name = "python" |
| 311 | highlighter%current_lang%case_sensitive = .true. |
| 312 | |
| 313 | ! Keywords |
| 314 | allocate(highlighter%current_lang%keywords(35)) |
| 315 | highlighter%current_lang%keywords = [ & |
| 316 | "def ", "class ", "if ", "elif ", & |
| 317 | "else ", "for ", "while ", "break ", & |
| 318 | "continue ", "return ", "yield ", "import ", & |
| 319 | "from ", "as ", "try ", "except ", & |
| 320 | "finally ", "raise ", "with ", "assert ", & |
| 321 | "lambda ", "pass ", "del ", "global ", & |
| 322 | "nonlocal ", "in ", "is ", "and ", & |
| 323 | "or ", "not ", "True ", "False ", & |
| 324 | "None ", "async ", "await " & |
| 325 | ] |
| 326 | |
| 327 | ! Types |
| 328 | allocate(highlighter%current_lang%types(8)) |
| 329 | highlighter%current_lang%types = [ & |
| 330 | "int ", "float ", "str ", "bool ", & |
| 331 | "list ", "dict ", "tuple ", "set " & |
| 332 | ] |
| 333 | |
| 334 | highlighter%current_lang%comment_single = "#" |
| 335 | |
| 336 | ! String delimiters |
| 337 | allocate(highlighter%current_lang%string_delimiters(4)) |
| 338 | highlighter%current_lang%string_delimiters = ['" ', "' ", '""" ', "''' "] |
| 339 | |
| 340 | ! Operators |
| 341 | allocate(highlighter%current_lang%operators(15)) |
| 342 | highlighter%current_lang%operators = [ & |
| 343 | "+ ", "- ", "* ", "/ ", "// ", "% ", "** ", & |
| 344 | "= ", "== ", "!= ", "< ", "> ", "<= ", ">= ", & |
| 345 | "& " & |
| 346 | ] |
| 347 | |
| 348 | highlighter%enabled = .true. |
| 349 | end subroutine load_python_syntax |
| 350 | |
| 351 | ! Helper functions |
| 352 | function is_alpha(ch) result(res) |
| 353 | character(len=1), intent(in) :: ch |
| 354 | logical :: res |
| 355 | res = (ch >= 'a' .and. ch <= 'z') .or. (ch >= 'A' .and. ch <= 'Z') |
| 356 | end function is_alpha |
| 357 | |
| 358 | function is_digit(ch) result(res) |
| 359 | character(len=1), intent(in) :: ch |
| 360 | logical :: res |
| 361 | res = (ch >= '0' .and. ch <= '9') |
| 362 | end function is_digit |
| 363 | |
| 364 | function is_alnum(ch) result(res) |
| 365 | character(len=1), intent(in) :: ch |
| 366 | logical :: res |
| 367 | res = is_alpha(ch) .or. is_digit(ch) .or. ch == '_' |
| 368 | end function is_alnum |
| 369 | |
| 370 | function is_operator_char(highlighter, ch) result(res) |
| 371 | type(syntax_highlighter_t), intent(in) :: highlighter |
| 372 | character(len=1), intent(in) :: ch |
| 373 | logical :: res |
| 374 | integer :: i |
| 375 | |
| 376 | res = .false. |
| 377 | if (.not. allocated(highlighter%current_lang%operators)) return |
| 378 | |
| 379 | do i = 1, size(highlighter%current_lang%operators) |
| 380 | if (index(trim(highlighter%current_lang%operators(i)), ch) > 0) then |
| 381 | res = .true. |
| 382 | exit |
| 383 | end if |
| 384 | end do |
| 385 | end function is_operator_char |
| 386 | |
| 387 | function check_comment_start(highlighter, line, pos) result(res) |
| 388 | type(syntax_highlighter_t), intent(in) :: highlighter |
| 389 | character(len=*), intent(in) :: line |
| 390 | integer, intent(in) :: pos |
| 391 | logical :: res |
| 392 | integer :: comment_len |
| 393 | |
| 394 | res = .false. |
| 395 | if (highlighter%current_lang%comment_single /= "") then |
| 396 | comment_len = len_trim(highlighter%current_lang%comment_single) |
| 397 | if (pos + comment_len - 1 <= len(line)) then |
| 398 | res = line(pos:pos+comment_len-1) == trim(highlighter%current_lang%comment_single) |
| 399 | end if |
| 400 | end if |
| 401 | end function check_comment_start |
| 402 | |
| 403 | function check_multiline_comment_start(highlighter, line, pos) result(res) |
| 404 | type(syntax_highlighter_t), intent(in) :: highlighter |
| 405 | character(len=*), intent(in) :: line |
| 406 | integer, intent(in) :: pos |
| 407 | logical :: res |
| 408 | integer :: comment_len |
| 409 | |
| 410 | res = .false. |
| 411 | if (highlighter%current_lang%comment_start /= "") then |
| 412 | comment_len = len_trim(highlighter%current_lang%comment_start) |
| 413 | if (pos + comment_len - 1 <= len(line)) then |
| 414 | res = line(pos:pos+comment_len-1) == trim(highlighter%current_lang%comment_start) |
| 415 | end if |
| 416 | end if |
| 417 | end function check_multiline_comment_start |
| 418 | |
| 419 | function check_string_start(highlighter, line, pos) result(res) |
| 420 | type(syntax_highlighter_t), intent(in) :: highlighter |
| 421 | character(len=*), intent(in) :: line |
| 422 | integer, intent(in) :: pos |
| 423 | logical :: res |
| 424 | integer :: i, delim_len |
| 425 | |
| 426 | res = .false. |
| 427 | if (.not. allocated(highlighter%current_lang%string_delimiters)) return |
| 428 | |
| 429 | do i = 1, size(highlighter%current_lang%string_delimiters) |
| 430 | delim_len = len_trim(highlighter%current_lang%string_delimiters(i)) |
| 431 | if (pos + delim_len - 1 <= len(line)) then |
| 432 | if (line(pos:pos+delim_len-1) == trim(highlighter%current_lang%string_delimiters(i))) then |
| 433 | res = .true. |
| 434 | exit |
| 435 | end if |
| 436 | end if |
| 437 | end do |
| 438 | end function check_string_start |
| 439 | |
| 440 | subroutine process_string(highlighter, line, tokens, token_count, pos) |
| 441 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 442 | character(len=*), intent(in) :: line |
| 443 | type(token_t), intent(inout) :: tokens(:) |
| 444 | integer, intent(inout) :: token_count, pos |
| 445 | integer :: i, start_pos, delim_len, line_len |
| 446 | character(len=:), allocatable :: delimiter |
| 447 | logical :: found_end, is_multiline |
| 448 | |
| 449 | line_len = len(line) |
| 450 | start_pos = pos |
| 451 | |
| 452 | ! Initialize delimiter (defensive programming - should always be set in loop below) |
| 453 | delimiter = '"' ! Default fallback |
| 454 | |
| 455 | ! Find which delimiter matches |
| 456 | do i = 1, size(highlighter%current_lang%string_delimiters) |
| 457 | delim_len = len_trim(highlighter%current_lang%string_delimiters(i)) |
| 458 | if (pos + delim_len - 1 <= line_len) then |
| 459 | if (line(pos:pos+delim_len-1) == trim(highlighter%current_lang%string_delimiters(i))) then |
| 460 | delimiter = trim(highlighter%current_lang%string_delimiters(i)) |
| 461 | exit |
| 462 | end if |
| 463 | end if |
| 464 | end do |
| 465 | |
| 466 | ! Check if this is a multiline-capable delimiter |
| 467 | ! Python: """ or ''' (length 3) |
| 468 | ! JavaScript/TypeScript template literals (```) would need length 3 |
| 469 | ! Single backticks should NOT be multiline (breaks markdown with Ctrl+\`) |
| 470 | is_multiline = (len(delimiter) >= 3) |
| 471 | |
| 472 | ! Move past opening delimiter |
| 473 | pos = pos + len(delimiter) |
| 474 | |
| 475 | ! Find closing delimiter |
| 476 | found_end = .false. |
| 477 | do while (pos <= line_len) |
| 478 | ! Skip escaped character (but NOT for backticks - markdown inline code is literal) |
| 479 | if (line(pos:pos) == '\' .and. pos < line_len .and. delimiter /= '`') then |
| 480 | pos = pos + 2 |
| 481 | else if (pos + len(delimiter) - 1 <= line_len) then |
| 482 | if (line(pos:pos+len(delimiter)-1) == delimiter) then |
| 483 | pos = pos + len(delimiter) |
| 484 | found_end = .true. |
| 485 | exit |
| 486 | else |
| 487 | pos = pos + 1 |
| 488 | end if |
| 489 | else |
| 490 | pos = pos + 1 |
| 491 | end if |
| 492 | end do |
| 493 | |
| 494 | ! Add string token |
| 495 | token_count = token_count + 1 |
| 496 | tokens(token_count)%type = TOKEN_STRING |
| 497 | tokens(token_count)%start_col = start_pos |
| 498 | tokens(token_count)%end_col = min(pos - 1, line_len) |
| 499 | |
| 500 | ! Handle unclosed string |
| 501 | if (.not. found_end) then |
| 502 | if (is_multiline) then |
| 503 | ! Enter multiline string mode |
| 504 | highlighter%in_multiline_string = .true. |
| 505 | highlighter%string_delimiter = delimiter |
| 506 | end if |
| 507 | pos = line_len + 1 |
| 508 | end if |
| 509 | end subroutine process_string |
| 510 | |
| 511 | subroutine process_number(line, tokens, token_count, pos) |
| 512 | character(len=*), intent(in) :: line |
| 513 | type(token_t), intent(inout) :: tokens(:) |
| 514 | integer, intent(inout) :: token_count, pos |
| 515 | integer :: start_pos |
| 516 | logical :: has_dot, has_e |
| 517 | |
| 518 | start_pos = pos |
| 519 | has_dot = .false. |
| 520 | has_e = .false. |
| 521 | |
| 522 | do while (pos <= len(line)) |
| 523 | if (is_digit(line(pos:pos))) then |
| 524 | pos = pos + 1 |
| 525 | else if (line(pos:pos) == '.' .and. .not. has_dot .and. .not. has_e) then |
| 526 | has_dot = .true. |
| 527 | pos = pos + 1 |
| 528 | else if ((line(pos:pos) == 'e' .or. line(pos:pos) == 'E') .and. .not. has_e) then |
| 529 | has_e = .true. |
| 530 | pos = pos + 1 |
| 531 | if (pos <= len(line) .and. (line(pos:pos) == '+' .or. line(pos:pos) == '-')) then |
| 532 | pos = pos + 1 |
| 533 | end if |
| 534 | else |
| 535 | exit |
| 536 | end if |
| 537 | end do |
| 538 | |
| 539 | token_count = token_count + 1 |
| 540 | tokens(token_count)%type = TOKEN_NUMBER |
| 541 | tokens(token_count)%start_col = start_pos |
| 542 | tokens(token_count)%end_col = pos - 1 |
| 543 | end subroutine process_number |
| 544 | |
| 545 | subroutine process_word(highlighter, line, tokens, token_count, pos) |
| 546 | type(syntax_highlighter_t), intent(in) :: highlighter |
| 547 | character(len=*), intent(in) :: line |
| 548 | type(token_t), intent(inout) :: tokens(:) |
| 549 | integer, intent(inout) :: token_count, pos |
| 550 | integer :: start_pos, end_pos, i |
| 551 | character(len=:), allocatable :: word |
| 552 | logical :: is_keyword, is_type |
| 553 | |
| 554 | start_pos = pos |
| 555 | |
| 556 | ! Find end of word |
| 557 | do while (pos <= len(line) .and. is_alnum(line(pos:pos))) |
| 558 | pos = pos + 1 |
| 559 | end do |
| 560 | end_pos = pos - 1 |
| 561 | |
| 562 | word = line(start_pos:end_pos) |
| 563 | |
| 564 | ! Check if it's a keyword |
| 565 | is_keyword = .false. |
| 566 | if (allocated(highlighter%current_lang%keywords)) then |
| 567 | do i = 1, size(highlighter%current_lang%keywords) |
| 568 | if (compare_word(word, highlighter%current_lang%keywords(i), & |
| 569 | highlighter%current_lang%case_sensitive)) then |
| 570 | is_keyword = .true. |
| 571 | exit |
| 572 | end if |
| 573 | end do |
| 574 | end if |
| 575 | |
| 576 | ! Check if it's a type |
| 577 | is_type = .false. |
| 578 | if (.not. is_keyword .and. allocated(highlighter%current_lang%types)) then |
| 579 | do i = 1, size(highlighter%current_lang%types) |
| 580 | if (compare_word(word, highlighter%current_lang%types(i), & |
| 581 | highlighter%current_lang%case_sensitive)) then |
| 582 | is_type = .true. |
| 583 | exit |
| 584 | end if |
| 585 | end do |
| 586 | end if |
| 587 | |
| 588 | token_count = token_count + 1 |
| 589 | tokens(token_count)%start_col = start_pos |
| 590 | tokens(token_count)%end_col = end_pos |
| 591 | |
| 592 | if (is_keyword) then |
| 593 | tokens(token_count)%type = TOKEN_KEYWORD |
| 594 | else if (is_type) then |
| 595 | tokens(token_count)%type = TOKEN_TYPE |
| 596 | else |
| 597 | tokens(token_count)%type = TOKEN_PLAIN |
| 598 | end if |
| 599 | end subroutine process_word |
| 600 | |
| 601 | function compare_word(word1, word2, case_sensitive) result(match) |
| 602 | character(len=*), intent(in) :: word1, word2 |
| 603 | logical, intent(in) :: case_sensitive |
| 604 | logical :: match |
| 605 | |
| 606 | if (case_sensitive) then |
| 607 | match = trim(word1) == trim(word2) |
| 608 | else |
| 609 | match = to_lower(trim(word1)) == to_lower(trim(word2)) |
| 610 | end if |
| 611 | end function compare_word |
| 612 | |
| 613 | function to_lower(str) result(lower_str) |
| 614 | character(len=*), intent(in) :: str |
| 615 | character(len=len(str)) :: lower_str |
| 616 | integer :: i |
| 617 | |
| 618 | lower_str = str |
| 619 | do i = 1, len(str) |
| 620 | if (str(i:i) >= 'A' .and. str(i:i) <= 'Z') then |
| 621 | lower_str(i:i) = char(ichar(str(i:i)) + 32) |
| 622 | end if |
| 623 | end do |
| 624 | end function to_lower |
| 625 | |
| 626 | ! C language support |
| 627 | subroutine load_c_syntax(highlighter) |
| 628 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 629 | |
| 630 | highlighter%current_lang%name = "c" |
| 631 | highlighter%current_lang%case_sensitive = .true. |
| 632 | |
| 633 | ! Keywords |
| 634 | allocate(highlighter%current_lang%keywords(32)) |
| 635 | highlighter%current_lang%keywords = [ & |
| 636 | "auto ", "break ", "case ", "char ", & |
| 637 | "const ", "continue ", "default ", "do ", & |
| 638 | "double ", "else ", "enum ", "extern ", & |
| 639 | "float ", "for ", "goto ", "if ", & |
| 640 | "inline ", "int ", "long ", "register ", & |
| 641 | "return ", "short ", "signed ", "sizeof ", & |
| 642 | "static ", "struct ", "switch ", "typedef ", & |
| 643 | "union ", "unsigned ", "void ", "while " & |
| 644 | ] |
| 645 | |
| 646 | ! Types |
| 647 | allocate(highlighter%current_lang%types(8)) |
| 648 | highlighter%current_lang%types = [ & |
| 649 | "size_t ", "uint32_t ", "int32_t ", "uint64_t ", & |
| 650 | "int64_t ", "bool ", "FILE ", "NULL " & |
| 651 | ] |
| 652 | |
| 653 | highlighter%current_lang%comment_single = "//" |
| 654 | highlighter%current_lang%comment_start = "/*" |
| 655 | highlighter%current_lang%comment_end = "*/" |
| 656 | |
| 657 | allocate(highlighter%current_lang%string_delimiters(2)) |
| 658 | highlighter%current_lang%string_delimiters = ['"', "'"] |
| 659 | |
| 660 | allocate(highlighter%current_lang%operators(20)) |
| 661 | highlighter%current_lang%operators = [ & |
| 662 | "+ ", "- ", "* ", "/ ", "% ", "= ", & |
| 663 | "== ", "!= ", "< ", "> ", "<= ", ">= ", & |
| 664 | "&& ", "|| ", "! ", "& ", "| ", "^ ", & |
| 665 | "<< ", ">> " & |
| 666 | ] |
| 667 | |
| 668 | highlighter%enabled = .true. |
| 669 | end subroutine load_c_syntax |
| 670 | |
| 671 | ! C++ language support |
| 672 | subroutine load_cpp_syntax(highlighter) |
| 673 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 674 | |
| 675 | highlighter%current_lang%name = "cpp" |
| 676 | highlighter%current_lang%case_sensitive = .true. |
| 677 | |
| 678 | ! Keywords (C++ specific + C keywords) |
| 679 | allocate(highlighter%current_lang%keywords(48)) |
| 680 | highlighter%current_lang%keywords = [ & |
| 681 | "auto ", "break ", "case ", "char ", & |
| 682 | "const ", "continue ", "default ", "do ", & |
| 683 | "double ", "else ", "enum ", "extern ", & |
| 684 | "float ", "for ", "goto ", "if ", & |
| 685 | "inline ", "int ", "long ", "register ", & |
| 686 | "return ", "short ", "signed ", "sizeof ", & |
| 687 | "static ", "struct ", "switch ", "typedef ", & |
| 688 | "union ", "unsigned ", "void ", "while ", & |
| 689 | "class ", "namespace ", "template ", "typename ", & |
| 690 | "new ", "delete ", "this ", "friend ", & |
| 691 | "virtual ", "override ", "final ", "public ", & |
| 692 | "private ", "protected ", "try ", "catch " & |
| 693 | ] |
| 694 | |
| 695 | ! Types |
| 696 | allocate(highlighter%current_lang%types(12)) |
| 697 | highlighter%current_lang%types = [ & |
| 698 | "std ", "string ", "vector ", "map ", & |
| 699 | "set ", "pair ", "unique_ptr ", "shared_ptr ", & |
| 700 | "nullptr ", "true ", "false ", "bool " & |
| 701 | ] |
| 702 | |
| 703 | highlighter%current_lang%comment_single = "//" |
| 704 | highlighter%current_lang%comment_start = "/*" |
| 705 | highlighter%current_lang%comment_end = "*/" |
| 706 | |
| 707 | allocate(highlighter%current_lang%string_delimiters(2)) |
| 708 | highlighter%current_lang%string_delimiters = ['"', "'"] |
| 709 | |
| 710 | allocate(highlighter%current_lang%operators(22)) |
| 711 | highlighter%current_lang%operators = [ & |
| 712 | "+ ", "- ", "* ", "/ ", "% ", "= ", & |
| 713 | "== ", "!= ", "< ", "> ", "<= ", ">= ", & |
| 714 | "&& ", "|| ", "! ", "& ", "| ", "^ ", & |
| 715 | "<< ", ">> ", ":: ", "-> " & |
| 716 | ] |
| 717 | |
| 718 | highlighter%enabled = .true. |
| 719 | end subroutine load_cpp_syntax |
| 720 | |
| 721 | ! Rust language support |
| 722 | subroutine load_rust_syntax(highlighter) |
| 723 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 724 | |
| 725 | highlighter%current_lang%name = "rust" |
| 726 | highlighter%current_lang%case_sensitive = .true. |
| 727 | |
| 728 | ! Keywords |
| 729 | allocate(highlighter%current_lang%keywords(40)) |
| 730 | highlighter%current_lang%keywords = [ & |
| 731 | "as ", "async ", "await ", "break ", & |
| 732 | "const ", "continue ", "crate ", "dyn ", & |
| 733 | "else ", "enum ", "extern ", "false ", & |
| 734 | "fn ", "for ", "if ", "impl ", & |
| 735 | "in ", "let ", "loop ", "match ", & |
| 736 | "mod ", "move ", "mut ", "pub ", & |
| 737 | "ref ", "return ", "self ", "Self ", & |
| 738 | "static ", "struct ", "super ", "trait ", & |
| 739 | "true ", "type ", "unsafe ", "use ", & |
| 740 | "where ", "while ", "async ", "await " & |
| 741 | ] |
| 742 | |
| 743 | ! Types |
| 744 | allocate(highlighter%current_lang%types(16)) |
| 745 | highlighter%current_lang%types = [ & |
| 746 | "i8 ", "i16 ", "i32 ", "i64 ", & |
| 747 | "i128 ", "u8 ", "u16 ", "u32 ", & |
| 748 | "u64 ", "u128 ", "f32 ", "f64 ", & |
| 749 | "bool ", "char ", "str ", "String " & |
| 750 | ] |
| 751 | |
| 752 | highlighter%current_lang%comment_single = "//" |
| 753 | highlighter%current_lang%comment_start = "/*" |
| 754 | highlighter%current_lang%comment_end = "*/" |
| 755 | |
| 756 | allocate(highlighter%current_lang%string_delimiters(2)) |
| 757 | highlighter%current_lang%string_delimiters = ['"', "'"] |
| 758 | |
| 759 | allocate(highlighter%current_lang%operators(20)) |
| 760 | highlighter%current_lang%operators = [ & |
| 761 | "+ ", "- ", "* ", "/ ", "% ", "= ", & |
| 762 | "== ", "!= ", "< ", "> ", "<= ", ">= ", & |
| 763 | "&& ", "|| ", "! ", "& ", "| ", "^ ", & |
| 764 | ":: ", "-> " & |
| 765 | ] |
| 766 | |
| 767 | highlighter%enabled = .true. |
| 768 | end subroutine load_rust_syntax |
| 769 | |
| 770 | ! Go language support |
| 771 | subroutine load_go_syntax(highlighter) |
| 772 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 773 | |
| 774 | highlighter%current_lang%name = "go" |
| 775 | highlighter%current_lang%case_sensitive = .true. |
| 776 | |
| 777 | ! Keywords |
| 778 | allocate(highlighter%current_lang%keywords(25)) |
| 779 | highlighter%current_lang%keywords = [ & |
| 780 | "break ", "case ", "chan ", "const ", & |
| 781 | "continue ", "default ", "defer ", "else ", & |
| 782 | "fallthrough ", "for ", "func ", "go ", & |
| 783 | "goto ", "if ", "import ", "interface ", & |
| 784 | "map ", "package ", "range ", "return ", & |
| 785 | "select ", "struct ", "switch ", "type ", & |
| 786 | "var " & |
| 787 | ] |
| 788 | |
| 789 | ! Types |
| 790 | allocate(highlighter%current_lang%types(15)) |
| 791 | highlighter%current_lang%types = [ & |
| 792 | "bool ", "byte ", "complex64 ", "complex128 ", & |
| 793 | "error ", "float32 ", "float64 ", "int ", & |
| 794 | "int8 ", "int16 ", "int32 ", "int64 ", & |
| 795 | "string ", "uint ", "nil " & |
| 796 | ] |
| 797 | |
| 798 | highlighter%current_lang%comment_single = "//" |
| 799 | highlighter%current_lang%comment_start = "/*" |
| 800 | highlighter%current_lang%comment_end = "*/" |
| 801 | |
| 802 | allocate(highlighter%current_lang%string_delimiters(3)) |
| 803 | highlighter%current_lang%string_delimiters = ['"', "'", '`'] |
| 804 | |
| 805 | allocate(highlighter%current_lang%operators(18)) |
| 806 | highlighter%current_lang%operators = [ & |
| 807 | "+ ", "- ", "* ", "/ ", "% ", "= ", & |
| 808 | "== ", "!= ", "< ", "> ", "<= ", ">= ", & |
| 809 | "&& ", "|| ", "! ", "& ", "| ", ":= " & |
| 810 | ] |
| 811 | |
| 812 | highlighter%enabled = .true. |
| 813 | end subroutine load_go_syntax |
| 814 | |
| 815 | ! JavaScript language support |
| 816 | subroutine load_javascript_syntax(highlighter) |
| 817 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 818 | |
| 819 | highlighter%current_lang%name = "javascript" |
| 820 | highlighter%current_lang%case_sensitive = .true. |
| 821 | |
| 822 | ! Keywords |
| 823 | allocate(highlighter%current_lang%keywords(38)) |
| 824 | highlighter%current_lang%keywords = [ & |
| 825 | "async ", "await ", "break ", "case ", & |
| 826 | "catch ", "class ", "const ", "continue ", & |
| 827 | "debugger ", "default ", "delete ", "do ", & |
| 828 | "else ", "export ", "extends ", "finally ", & |
| 829 | "for ", "function ", "if ", "import ", & |
| 830 | "in ", "instanceof ", "let ", "new ", & |
| 831 | "return ", "super ", "switch ", "this ", & |
| 832 | "throw ", "try ", "typeof ", "var ", & |
| 833 | "void ", "while ", "with ", "yield ", & |
| 834 | "true ", "false " & |
| 835 | ] |
| 836 | |
| 837 | ! Types |
| 838 | allocate(highlighter%current_lang%types(10)) |
| 839 | highlighter%current_lang%types = [ & |
| 840 | "null ", "undefined ", "Boolean ", "Number ", & |
| 841 | "String ", "Symbol ", "Object ", "Array ", & |
| 842 | "Function ", "Promise " & |
| 843 | ] |
| 844 | |
| 845 | highlighter%current_lang%comment_single = "//" |
| 846 | highlighter%current_lang%comment_start = "/*" |
| 847 | highlighter%current_lang%comment_end = "*/" |
| 848 | |
| 849 | allocate(highlighter%current_lang%string_delimiters(3)) |
| 850 | highlighter%current_lang%string_delimiters = ['"', "'", '`'] |
| 851 | |
| 852 | allocate(highlighter%current_lang%operators(20)) |
| 853 | highlighter%current_lang%operators = [ & |
| 854 | "+ ", "- ", "* ", "/ ", "% ", "= ", & |
| 855 | "== ", "=== ", "!= ", "!== ", "< ", "> ", & |
| 856 | "<= ", ">= ", "&& ", "|| ", "! ", "? ", & |
| 857 | ": ", "=> " & |
| 858 | ] |
| 859 | |
| 860 | highlighter%enabled = .true. |
| 861 | end subroutine load_javascript_syntax |
| 862 | |
| 863 | ! TypeScript language support (extends JavaScript) |
| 864 | subroutine load_typescript_syntax(highlighter) |
| 865 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 866 | |
| 867 | ! Start with JavaScript syntax |
| 868 | call load_javascript_syntax(highlighter) |
| 869 | highlighter%current_lang%name = "typescript" |
| 870 | |
| 871 | ! Add TypeScript-specific keywords |
| 872 | deallocate(highlighter%current_lang%keywords) |
| 873 | allocate(highlighter%current_lang%keywords(45)) |
| 874 | highlighter%current_lang%keywords = [ & |
| 875 | "async ", "await ", "break ", "case ", & |
| 876 | "catch ", "class ", "const ", "continue ", & |
| 877 | "debugger ", "default ", "delete ", "do ", & |
| 878 | "else ", "export ", "extends ", "finally ", & |
| 879 | "for ", "function ", "if ", "import ", & |
| 880 | "in ", "instanceof ", "let ", "new ", & |
| 881 | "return ", "super ", "switch ", "this ", & |
| 882 | "throw ", "try ", "typeof ", "var ", & |
| 883 | "void ", "while ", "with ", "yield ", & |
| 884 | "true ", "false ", "enum ", "interface ", & |
| 885 | "type ", "namespace ", "module ", "declare ", & |
| 886 | "abstract " & |
| 887 | ] |
| 888 | |
| 889 | ! Add TypeScript types |
| 890 | deallocate(highlighter%current_lang%types) |
| 891 | allocate(highlighter%current_lang%types(15)) |
| 892 | highlighter%current_lang%types = [ & |
| 893 | "null ", "undefined ", "boolean ", "number ", & |
| 894 | "string ", "symbol ", "object ", "any ", & |
| 895 | "unknown ", "never ", "void ", "Array ", & |
| 896 | "Function ", "Promise ", "ReadonlyArra" & |
| 897 | ] |
| 898 | |
| 899 | highlighter%enabled = .true. |
| 900 | end subroutine load_typescript_syntax |
| 901 | |
| 902 | ! Bash/Shell script support |
| 903 | subroutine load_bash_syntax(highlighter) |
| 904 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 905 | |
| 906 | highlighter%current_lang%name = "bash" |
| 907 | highlighter%current_lang%case_sensitive = .true. |
| 908 | |
| 909 | ! Keywords |
| 910 | allocate(highlighter%current_lang%keywords(30)) |
| 911 | highlighter%current_lang%keywords = [ & |
| 912 | "if ", "then ", "else ", "elif ", & |
| 913 | "fi ", "for ", "while ", "do ", & |
| 914 | "done ", "case ", "esac ", "function ", & |
| 915 | "return ", "break ", "continue ", "exit ", & |
| 916 | "export ", "source ", "alias ", "unset ", & |
| 917 | "shift ", "local ", "declare ", "readonly ", & |
| 918 | "echo ", "printf ", "read ", "cd ", & |
| 919 | "pwd ", "ls " & |
| 920 | ] |
| 921 | |
| 922 | ! Built-in variables/types |
| 923 | allocate(highlighter%current_lang%types(10)) |
| 924 | highlighter%current_lang%types = [ & |
| 925 | "$0 ", "$1 ", "$@ ", "$* ", & |
| 926 | "$# ", "$? ", "$$ ", "$! ", & |
| 927 | "true ", "false " & |
| 928 | ] |
| 929 | |
| 930 | highlighter%current_lang%comment_single = "#" |
| 931 | |
| 932 | allocate(highlighter%current_lang%string_delimiters(3)) |
| 933 | highlighter%current_lang%string_delimiters = ['"', "'", '`'] |
| 934 | |
| 935 | allocate(highlighter%current_lang%operators(15)) |
| 936 | highlighter%current_lang%operators = [ & |
| 937 | "= ", "== ", "!= ", "< ", "> ", & |
| 938 | "-eq ", "-ne ", "-lt ", "-gt ", "-le ", & |
| 939 | "-ge ", "&& ", "|| ", "| ", "& " & |
| 940 | ] |
| 941 | |
| 942 | highlighter%enabled = .true. |
| 943 | end subroutine load_bash_syntax |
| 944 | |
| 945 | ! Markdown support (special handling needed) |
| 946 | subroutine load_markdown_syntax(highlighter) |
| 947 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 948 | |
| 949 | highlighter%current_lang%name = "markdown" |
| 950 | highlighter%current_lang%case_sensitive = .true. |
| 951 | |
| 952 | ! Headers and emphasis markers as "keywords" |
| 953 | allocate(highlighter%current_lang%keywords(8)) |
| 954 | highlighter%current_lang%keywords = [ & |
| 955 | "# ", "## ", "### ", "#### ", & |
| 956 | "##### ", "###### ", "* ", "_ " & |
| 957 | ] |
| 958 | |
| 959 | ! Code block languages as "types" |
| 960 | allocate(highlighter%current_lang%types(10)) |
| 961 | highlighter%current_lang%types = [ & |
| 962 | "``` ", "```python ", "```bash ", "```fortran ", & |
| 963 | "```c ", "```cpp ", "```rust ", "```go ", & |
| 964 | "```javascri ", "```typescri " & |
| 965 | ] |
| 966 | |
| 967 | ! No traditional comments in markdown |
| 968 | highlighter%current_lang%comment_single = "" |
| 969 | |
| 970 | ! Links and code spans |
| 971 | allocate(highlighter%current_lang%string_delimiters(2)) |
| 972 | highlighter%current_lang%string_delimiters = ['`', '['] |
| 973 | |
| 974 | ! List markers and special chars |
| 975 | allocate(highlighter%current_lang%operators(6)) |
| 976 | highlighter%current_lang%operators = [ & |
| 977 | "- ", "+ ", "* ", "> ", "| ", "! " & |
| 978 | ] |
| 979 | |
| 980 | highlighter%enabled = .true. |
| 981 | end subroutine load_markdown_syntax |
| 982 | |
| 983 | ! Handle multiline comment start (when we encounter /* on a line) |
| 984 | subroutine process_multiline_comment_start(highlighter, line, tokens, token_count, pos) |
| 985 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 986 | character(len=*), intent(in) :: line |
| 987 | type(token_t), intent(inout) :: tokens(:) |
| 988 | integer, intent(inout) :: token_count, pos |
| 989 | integer :: start_pos, end_pos, comment_start_len, comment_end_len, line_len |
| 990 | |
| 991 | line_len = len(line) |
| 992 | start_pos = pos |
| 993 | comment_start_len = len_trim(highlighter%current_lang%comment_start) |
| 994 | comment_end_len = len_trim(highlighter%current_lang%comment_end) |
| 995 | |
| 996 | ! Skip past the opening delimiter |
| 997 | pos = pos + comment_start_len |
| 998 | |
| 999 | ! Look for closing delimiter on the same line |
| 1000 | end_pos = index(line(pos:), trim(highlighter%current_lang%comment_end)) |
| 1001 | |
| 1002 | if (end_pos > 0) then |
| 1003 | ! Found closing delimiter on same line - this is a complete comment |
| 1004 | end_pos = pos + end_pos - 1 + comment_end_len - 1 |
| 1005 | |
| 1006 | token_count = token_count + 1 |
| 1007 | tokens(token_count)%type = TOKEN_COMMENT |
| 1008 | tokens(token_count)%start_col = start_pos |
| 1009 | tokens(token_count)%end_col = end_pos |
| 1010 | |
| 1011 | pos = end_pos + 1 |
| 1012 | else |
| 1013 | ! Closing delimiter not found - rest of line is comment, enter multiline mode |
| 1014 | token_count = token_count + 1 |
| 1015 | tokens(token_count)%type = TOKEN_COMMENT |
| 1016 | tokens(token_count)%start_col = start_pos |
| 1017 | tokens(token_count)%end_col = line_len |
| 1018 | |
| 1019 | highlighter%in_multiline_comment = .true. |
| 1020 | pos = line_len + 1 |
| 1021 | end if |
| 1022 | end subroutine process_multiline_comment_start |
| 1023 | |
| 1024 | ! Handle multiline comment continuation (when we start a line in comment mode) |
| 1025 | subroutine process_multiline_comment(highlighter, line, tokens, token_count, pos) |
| 1026 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 1027 | character(len=*), intent(in) :: line |
| 1028 | type(token_t), intent(inout) :: tokens(:) |
| 1029 | integer, intent(inout) :: token_count, pos |
| 1030 | integer :: end_pos, comment_end_len, line_len |
| 1031 | |
| 1032 | line_len = len(line) |
| 1033 | comment_end_len = len_trim(highlighter%current_lang%comment_end) |
| 1034 | |
| 1035 | ! Look for closing delimiter |
| 1036 | end_pos = index(line(pos:), trim(highlighter%current_lang%comment_end)) |
| 1037 | |
| 1038 | if (end_pos > 0) then |
| 1039 | ! Found closing delimiter on this line |
| 1040 | end_pos = pos + end_pos - 1 + comment_end_len - 1 |
| 1041 | |
| 1042 | token_count = token_count + 1 |
| 1043 | tokens(token_count)%type = TOKEN_COMMENT |
| 1044 | tokens(token_count)%start_col = pos |
| 1045 | tokens(token_count)%end_col = end_pos |
| 1046 | |
| 1047 | ! Exit multiline comment mode |
| 1048 | highlighter%in_multiline_comment = .false. |
| 1049 | pos = end_pos + 1 |
| 1050 | else |
| 1051 | ! Closing delimiter not found - entire rest of line is comment |
| 1052 | token_count = token_count + 1 |
| 1053 | tokens(token_count)%type = TOKEN_COMMENT |
| 1054 | tokens(token_count)%start_col = pos |
| 1055 | tokens(token_count)%end_col = line_len |
| 1056 | |
| 1057 | ! Stay in multiline comment mode |
| 1058 | pos = line_len + 1 |
| 1059 | end if |
| 1060 | end subroutine process_multiline_comment |
| 1061 | |
| 1062 | subroutine process_multiline_string(highlighter, line, tokens, token_count, pos) |
| 1063 | type(syntax_highlighter_t), intent(inout) :: highlighter |
| 1064 | character(len=*), intent(in) :: line |
| 1065 | type(token_t), intent(inout) :: tokens(:) |
| 1066 | integer, intent(inout) :: token_count, pos |
| 1067 | integer :: end_pos, delim_len, line_len |
| 1068 | |
| 1069 | line_len = len(line) |
| 1070 | delim_len = len(highlighter%string_delimiter) |
| 1071 | |
| 1072 | ! Look for closing delimiter |
| 1073 | end_pos = index(line(pos:), highlighter%string_delimiter) |
| 1074 | |
| 1075 | if (end_pos > 0) then |
| 1076 | ! Found closing delimiter on this line |
| 1077 | end_pos = pos + end_pos - 1 + delim_len - 1 |
| 1078 | |
| 1079 | token_count = token_count + 1 |
| 1080 | tokens(token_count)%type = TOKEN_STRING |
| 1081 | tokens(token_count)%start_col = pos |
| 1082 | tokens(token_count)%end_col = end_pos |
| 1083 | |
| 1084 | ! Exit multiline string mode |
| 1085 | highlighter%in_multiline_string = .false. |
| 1086 | highlighter%string_delimiter = "" |
| 1087 | pos = end_pos + 1 |
| 1088 | else |
| 1089 | ! Closing delimiter not found - entire rest of line is string |
| 1090 | token_count = token_count + 1 |
| 1091 | tokens(token_count)%type = TOKEN_STRING |
| 1092 | tokens(token_count)%start_col = pos |
| 1093 | tokens(token_count)%end_col = line_len |
| 1094 | |
| 1095 | ! Stay in multiline string mode |
| 1096 | pos = line_len + 1 |
| 1097 | end if |
| 1098 | end subroutine process_multiline_string |
| 1099 | |
| 1100 | end module syntax_highlighter_module |