| 1 | //== HTMLRewrite.cpp - Translate source code into prettified HTML --*- C++ -*-// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | // |
| 9 | // This file defines the HTMLRewriter class, which is used to translate the |
| 10 | // text of a source file into prettified HTML. |
| 11 | // |
| 12 | //===----------------------------------------------------------------------===// |
| 13 | |
| 14 | #include "clang/Rewrite/Core/HTMLRewrite.h" |
| 15 | #include "clang/Basic/SourceManager.h" |
| 16 | #include "clang/Lex/Preprocessor.h" |
| 17 | #include "clang/Lex/TokenConcatenation.h" |
| 18 | #include "clang/Rewrite/Core/Rewriter.h" |
| 19 | #include "llvm/ADT/RewriteBuffer.h" |
| 20 | #include "llvm/Support/ErrorHandling.h" |
| 21 | #include "llvm/Support/raw_ostream.h" |
| 22 | #include <memory> |
| 23 | |
| 24 | using namespace clang; |
| 25 | using namespace llvm; |
| 26 | using namespace html; |
| 27 | |
| 28 | /// HighlightRange - Highlight a range in the source code with the specified |
| 29 | /// start/end tags. B/E must be in the same file. This ensures that |
| 30 | /// start/end tags are placed at the start/end of each line if the range is |
| 31 | /// multiline. |
| 32 | void html::HighlightRange(Rewriter &R, SourceLocation B, SourceLocation E, |
| 33 | const char *StartTag, const char *EndTag, |
| 34 | bool IsTokenRange) { |
| 35 | SourceManager &SM = R.getSourceMgr(); |
| 36 | B = SM.getExpansionLoc(Loc: B); |
| 37 | E = SM.getExpansionLoc(Loc: E); |
| 38 | FileID FID = SM.getFileID(SpellingLoc: B); |
| 39 | assert(SM.getFileID(E) == FID && "B/E not in the same file!" ); |
| 40 | |
| 41 | unsigned BOffset = SM.getFileOffset(SpellingLoc: B); |
| 42 | unsigned EOffset = SM.getFileOffset(SpellingLoc: E); |
| 43 | |
| 44 | // Include the whole end token in the range. |
| 45 | if (IsTokenRange) |
| 46 | EOffset += Lexer::MeasureTokenLength(Loc: E, SM: R.getSourceMgr(), LangOpts: R.getLangOpts()); |
| 47 | |
| 48 | bool Invalid = false; |
| 49 | const char *BufferStart = SM.getBufferData(FID, Invalid: &Invalid).data(); |
| 50 | if (Invalid) |
| 51 | return; |
| 52 | |
| 53 | HighlightRange(RB&: R.getEditBuffer(FID), B: BOffset, E: EOffset, |
| 54 | BufferStart, StartTag, EndTag); |
| 55 | } |
| 56 | |
| 57 | /// HighlightRange - This is the same as the above method, but takes |
| 58 | /// decomposed file locations. |
| 59 | void html::HighlightRange(RewriteBuffer &RB, unsigned B, unsigned E, |
| 60 | const char *BufferStart, |
| 61 | const char *StartTag, const char *EndTag) { |
| 62 | // Insert the tag at the absolute start/end of the range. |
| 63 | RB.InsertTextAfter(OrigOffset: B, Str: StartTag); |
| 64 | RB.InsertTextBefore(OrigOffset: E, Str: EndTag); |
| 65 | |
| 66 | // Scan the range to see if there is a \r or \n. If so, and if the line is |
| 67 | // not blank, insert tags on that line as well. |
| 68 | bool HadOpenTag = true; |
| 69 | |
| 70 | unsigned LastNonWhiteSpace = B; |
| 71 | for (unsigned i = B; i != E; ++i) { |
| 72 | switch (BufferStart[i]) { |
| 73 | case '\r': |
| 74 | case '\n': |
| 75 | // Okay, we found a newline in the range. If we have an open tag, we need |
| 76 | // to insert a close tag at the first non-whitespace before the newline. |
| 77 | if (HadOpenTag) |
| 78 | RB.InsertTextBefore(OrigOffset: LastNonWhiteSpace+1, Str: EndTag); |
| 79 | |
| 80 | // Instead of inserting an open tag immediately after the newline, we |
| 81 | // wait until we see a non-whitespace character. This prevents us from |
| 82 | // inserting tags around blank lines, and also allows the open tag to |
| 83 | // be put *after* whitespace on a non-blank line. |
| 84 | HadOpenTag = false; |
| 85 | break; |
| 86 | case '\0': |
| 87 | case ' ': |
| 88 | case '\t': |
| 89 | case '\f': |
| 90 | case '\v': |
| 91 | // Ignore whitespace. |
| 92 | break; |
| 93 | |
| 94 | default: |
| 95 | // If there is no tag open, do it now. |
| 96 | if (!HadOpenTag) { |
| 97 | RB.InsertTextAfter(OrigOffset: i, Str: StartTag); |
| 98 | HadOpenTag = true; |
| 99 | } |
| 100 | |
| 101 | // Remember this character. |
| 102 | LastNonWhiteSpace = i; |
| 103 | break; |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | namespace clang::html { |
| 109 | struct RelexRewriteCache { |
| 110 | // These structs mimic input arguments of HighlightRange(). |
| 111 | struct Highlight { |
| 112 | SourceLocation B, E; |
| 113 | std::string StartTag, EndTag; |
| 114 | bool IsTokenRange; |
| 115 | }; |
| 116 | struct RawHighlight { |
| 117 | unsigned B, E; |
| 118 | std::string StartTag, EndTag; |
| 119 | }; |
| 120 | |
| 121 | // SmallVector isn't appropriate because these vectors are almost never small. |
| 122 | using HighlightList = std::vector<Highlight>; |
| 123 | using RawHighlightList = std::vector<RawHighlight>; |
| 124 | |
| 125 | DenseMap<FileID, RawHighlightList> SyntaxHighlights; |
| 126 | DenseMap<FileID, HighlightList> MacroHighlights; |
| 127 | }; |
| 128 | } // namespace clang::html |
| 129 | |
| 130 | html::RelexRewriteCacheRef html::instantiateRelexRewriteCache() { |
| 131 | return std::make_shared<RelexRewriteCache>(); |
| 132 | } |
| 133 | |
| 134 | void html::EscapeText(Rewriter &R, FileID FID, |
| 135 | bool EscapeSpaces, bool ReplaceTabs) { |
| 136 | |
| 137 | llvm::MemoryBufferRef Buf = R.getSourceMgr().getBufferOrFake(FID); |
| 138 | const char* C = Buf.getBufferStart(); |
| 139 | const char* FileEnd = Buf.getBufferEnd(); |
| 140 | |
| 141 | assert (C <= FileEnd); |
| 142 | |
| 143 | RewriteBuffer &RB = R.getEditBuffer(FID); |
| 144 | |
| 145 | unsigned ColNo = 0; |
| 146 | for (unsigned FilePos = 0; C != FileEnd ; ++C, ++FilePos) { |
| 147 | switch (*C) { |
| 148 | default: ++ColNo; break; |
| 149 | case '\n': |
| 150 | case '\r': |
| 151 | ColNo = 0; |
| 152 | break; |
| 153 | |
| 154 | case ' ': |
| 155 | if (EscapeSpaces) |
| 156 | RB.ReplaceText(OrigOffset: FilePos, OrigLength: 1, NewStr: " " ); |
| 157 | ++ColNo; |
| 158 | break; |
| 159 | case '\f': |
| 160 | RB.ReplaceText(OrigOffset: FilePos, OrigLength: 1, NewStr: "<hr>" ); |
| 161 | ColNo = 0; |
| 162 | break; |
| 163 | |
| 164 | case '\t': { |
| 165 | if (!ReplaceTabs) |
| 166 | break; |
| 167 | unsigned NumSpaces = 8-(ColNo&7); |
| 168 | if (EscapeSpaces) |
| 169 | RB.ReplaceText(OrigOffset: FilePos, OrigLength: 1, |
| 170 | NewStr: StringRef(" " |
| 171 | " " , 6*NumSpaces)); |
| 172 | else |
| 173 | RB.ReplaceText(OrigOffset: FilePos, OrigLength: 1, NewStr: StringRef(" " , NumSpaces)); |
| 174 | ColNo += NumSpaces; |
| 175 | break; |
| 176 | } |
| 177 | case '<': |
| 178 | RB.ReplaceText(OrigOffset: FilePos, OrigLength: 1, NewStr: "<" ); |
| 179 | ++ColNo; |
| 180 | break; |
| 181 | |
| 182 | case '>': |
| 183 | RB.ReplaceText(OrigOffset: FilePos, OrigLength: 1, NewStr: ">" ); |
| 184 | ++ColNo; |
| 185 | break; |
| 186 | |
| 187 | case '&': |
| 188 | RB.ReplaceText(OrigOffset: FilePos, OrigLength: 1, NewStr: "&" ); |
| 189 | ++ColNo; |
| 190 | break; |
| 191 | } |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | std::string html::EscapeText(StringRef s, bool EscapeSpaces, bool ReplaceTabs) { |
| 196 | |
| 197 | unsigned len = s.size(); |
| 198 | std::string Str; |
| 199 | llvm::raw_string_ostream os(Str); |
| 200 | |
| 201 | for (unsigned i = 0 ; i < len; ++i) { |
| 202 | |
| 203 | char c = s[i]; |
| 204 | switch (c) { |
| 205 | default: |
| 206 | os << c; break; |
| 207 | |
| 208 | case ' ': |
| 209 | if (EscapeSpaces) os << " " ; |
| 210 | else os << ' '; |
| 211 | break; |
| 212 | |
| 213 | case '\t': |
| 214 | if (ReplaceTabs) { |
| 215 | if (EscapeSpaces) |
| 216 | for (unsigned i = 0; i < 4; ++i) |
| 217 | os << " " ; |
| 218 | else |
| 219 | for (unsigned i = 0; i < 4; ++i) |
| 220 | os << " " ; |
| 221 | } |
| 222 | else |
| 223 | os << c; |
| 224 | |
| 225 | break; |
| 226 | |
| 227 | case '<': os << "<" ; break; |
| 228 | case '>': os << ">" ; break; |
| 229 | case '&': os << "&" ; break; |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | return Str; |
| 234 | } |
| 235 | |
| 236 | static void AddLineNumber(RewriteBuffer &RB, unsigned LineNo, |
| 237 | unsigned B, unsigned E) { |
| 238 | SmallString<256> Str; |
| 239 | llvm::raw_svector_ostream OS(Str); |
| 240 | |
| 241 | OS << "<tr class=\"codeline\" data-linenumber=\"" << LineNo << "\">" |
| 242 | << "<td class=\"num\" id=\"LN" << LineNo << "\">" << LineNo |
| 243 | << "</td><td class=\"line\">" ; |
| 244 | |
| 245 | if (B == E) { // Handle empty lines. |
| 246 | OS << " </td></tr>" ; |
| 247 | RB.InsertTextBefore(OrigOffset: B, Str: OS.str()); |
| 248 | } else { |
| 249 | RB.InsertTextBefore(OrigOffset: B, Str: OS.str()); |
| 250 | RB.InsertTextBefore(OrigOffset: E, Str: "</td></tr>" ); |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | void html::AddLineNumbers(Rewriter& R, FileID FID) { |
| 255 | |
| 256 | llvm::MemoryBufferRef Buf = R.getSourceMgr().getBufferOrFake(FID); |
| 257 | const char* FileBeg = Buf.getBufferStart(); |
| 258 | const char* FileEnd = Buf.getBufferEnd(); |
| 259 | const char* C = FileBeg; |
| 260 | RewriteBuffer &RB = R.getEditBuffer(FID); |
| 261 | |
| 262 | assert (C <= FileEnd); |
| 263 | |
| 264 | unsigned LineNo = 0; |
| 265 | unsigned FilePos = 0; |
| 266 | |
| 267 | while (C != FileEnd) { |
| 268 | |
| 269 | ++LineNo; |
| 270 | unsigned LineStartPos = FilePos; |
| 271 | unsigned LineEndPos = FileEnd - FileBeg; |
| 272 | |
| 273 | assert (FilePos <= LineEndPos); |
| 274 | assert (C < FileEnd); |
| 275 | |
| 276 | // Scan until the newline (or end-of-file). |
| 277 | |
| 278 | while (C != FileEnd) { |
| 279 | char c = *C; |
| 280 | ++C; |
| 281 | |
| 282 | if (c == '\n') { |
| 283 | LineEndPos = FilePos++; |
| 284 | break; |
| 285 | } |
| 286 | |
| 287 | ++FilePos; |
| 288 | } |
| 289 | |
| 290 | AddLineNumber(RB, LineNo, B: LineStartPos, E: LineEndPos); |
| 291 | } |
| 292 | |
| 293 | // Add one big table tag that surrounds all of the code. |
| 294 | std::string s; |
| 295 | llvm::raw_string_ostream os(s); |
| 296 | os << "<table class=\"code\" data-fileid=\"" << FID.getHashValue() << "\">\n" ; |
| 297 | RB.InsertTextBefore(OrigOffset: 0, Str: os.str()); |
| 298 | RB.InsertTextAfter(OrigOffset: FileEnd - FileBeg, Str: "</table>" ); |
| 299 | } |
| 300 | |
| 301 | void html::(Rewriter &R, FileID FID, |
| 302 | StringRef title) { |
| 303 | |
| 304 | llvm::MemoryBufferRef Buf = R.getSourceMgr().getBufferOrFake(FID); |
| 305 | const char* FileStart = Buf.getBufferStart(); |
| 306 | const char* FileEnd = Buf.getBufferEnd(); |
| 307 | |
| 308 | SourceLocation StartLoc = R.getSourceMgr().getLocForStartOfFile(FID); |
| 309 | SourceLocation EndLoc = StartLoc.getLocWithOffset(Offset: FileEnd-FileStart); |
| 310 | |
| 311 | std::string s; |
| 312 | llvm::raw_string_ostream os(s); |
| 313 | os << "<!doctype html>\n" // Use HTML 5 doctype |
| 314 | "<html>\n<head>\n" ; |
| 315 | |
| 316 | if (!title.empty()) |
| 317 | os << "<title>" << html::EscapeText(s: title) << "</title>\n" ; |
| 318 | |
| 319 | os << R"<<<( |
| 320 | <style type="text/css"> |
| 321 | body { color:#000000; background-color:#ffffff } |
| 322 | body { font-family:Helvetica, sans-serif; font-size:10pt } |
| 323 | h1 { font-size:14pt } |
| 324 | .FileName { margin-top: 5px; margin-bottom: 5px; display: inline; } |
| 325 | .FileNav { margin-left: 5px; margin-right: 5px; display: inline; } |
| 326 | .FileNav a { text-decoration:none; font-size: larger; } |
| 327 | .divider { margin-top: 30px; margin-bottom: 30px; height: 15px; } |
| 328 | .divider { background-color: gray; } |
| 329 | .code { border-collapse:collapse; width:100%; } |
| 330 | .code { font-family: "Monospace", monospace; font-size:10pt } |
| 331 | .code { line-height: 1.2em } |
| 332 | .comment { color: green; font-style: oblique } |
| 333 | .keyword { color: blue } |
| 334 | .string_literal { color: red } |
| 335 | .directive { color: darkmagenta } |
| 336 | |
| 337 | /* Macros and variables could have pop-up notes hidden by default. |
| 338 | - Macro pop-up: expansion of the macro |
| 339 | - Variable pop-up: value (table) of the variable */ |
| 340 | .macro_popup, .variable_popup { display: none; } |
| 341 | |
| 342 | /* Pop-up appears on mouse-hover event. */ |
| 343 | .macro:hover .macro_popup, .variable:hover .variable_popup { |
| 344 | display: block; |
| 345 | padding: 2px; |
| 346 | -webkit-border-radius:5px; |
| 347 | -webkit-box-shadow:1px 1px 7px #000; |
| 348 | border-radius:5px; |
| 349 | box-shadow:1px 1px 7px #000; |
| 350 | position: absolute; |
| 351 | top: -1em; |
| 352 | left:10em; |
| 353 | z-index: 1 |
| 354 | } |
| 355 | |
| 356 | .macro_popup { |
| 357 | border: 2px solid red; |
| 358 | background-color:#FFF0F0; |
| 359 | font-weight: normal; |
| 360 | } |
| 361 | |
| 362 | .variable_popup { |
| 363 | border: 2px solid blue; |
| 364 | background-color:#F0F0FF; |
| 365 | font-weight: bold; |
| 366 | font-family: Helvetica, sans-serif; |
| 367 | font-size: 9pt; |
| 368 | } |
| 369 | |
| 370 | /* Pop-up notes needs a relative position as a base where they pops up. */ |
| 371 | .macro, .variable { |
| 372 | background-color: PaleGoldenRod; |
| 373 | position: relative; |
| 374 | } |
| 375 | .macro { color: DarkMagenta; } |
| 376 | |
| 377 | #tooltiphint { |
| 378 | position: fixed; |
| 379 | width: 50em; |
| 380 | margin-left: -25em; |
| 381 | left: 50%; |
| 382 | padding: 10px; |
| 383 | border: 1px solid #b0b0b0; |
| 384 | border-radius: 2px; |
| 385 | box-shadow: 1px 1px 7px black; |
| 386 | background-color: #c0c0c0; |
| 387 | z-index: 2; |
| 388 | } |
| 389 | |
| 390 | .num { width:2.5em; padding-right:2ex; background-color:#eeeeee } |
| 391 | .num { text-align:right; font-size:8pt } |
| 392 | .num { color:#444444 } |
| 393 | .line { padding-left: 1ex; border-left: 3px solid #ccc } |
| 394 | .line { white-space: pre } |
| 395 | .msg { -webkit-box-shadow:1px 1px 7px #000 } |
| 396 | .msg { box-shadow:1px 1px 7px #000 } |
| 397 | .msg { -webkit-border-radius:5px } |
| 398 | .msg { border-radius:5px } |
| 399 | .msg { font-family:Helvetica, sans-serif; font-size:8pt } |
| 400 | .msg { float:left } |
| 401 | .msg { position:relative } |
| 402 | .msg { padding:0.25em 1ex 0.25em 1ex } |
| 403 | .msg { margin-top:10px; margin-bottom:10px } |
| 404 | .msg { font-weight:bold } |
| 405 | .msg { max-width:60em; word-wrap: break-word; white-space: pre-wrap } |
| 406 | .msgT { padding:0x; spacing:0x } |
| 407 | .msgEvent { background-color:#fff8b4; color:#000000 } |
| 408 | .msgControl { background-color:#bbbbbb; color:#000000 } |
| 409 | .msgNote { background-color:#ddeeff; color:#000000 } |
| 410 | .mrange { background-color:#dfddf3 } |
| 411 | .mrange { border-bottom:1px solid #6F9DBE } |
| 412 | .PathIndex { font-weight: bold; padding:0px 5px; margin-right:5px; } |
| 413 | .PathIndex { -webkit-border-radius:8px } |
| 414 | .PathIndex { border-radius:8px } |
| 415 | .PathIndexEvent { background-color:#bfba87 } |
| 416 | .PathIndexControl { background-color:#8c8c8c } |
| 417 | .PathIndexPopUp { background-color: #879abc; } |
| 418 | .PathNav a { text-decoration:none; font-size: larger } |
| 419 | .CodeInsertionHint { font-weight: bold; background-color: #10dd10 } |
| 420 | .CodeRemovalHint { background-color:#de1010 } |
| 421 | .CodeRemovalHint { border-bottom:1px solid #6F9DBE } |
| 422 | .msg.selected{ background-color:orange !important; } |
| 423 | |
| 424 | table.simpletable { |
| 425 | padding: 5px; |
| 426 | font-size:12pt; |
| 427 | margin:20px; |
| 428 | border-collapse: collapse; border-spacing: 0px; |
| 429 | } |
| 430 | td.rowname { |
| 431 | text-align: right; |
| 432 | vertical-align: top; |
| 433 | font-weight: bold; |
| 434 | color:#444444; |
| 435 | padding-right:2ex; |
| 436 | } |
| 437 | |
| 438 | /* Hidden text. */ |
| 439 | input.spoilerhider + label { |
| 440 | cursor: pointer; |
| 441 | text-decoration: underline; |
| 442 | display: block; |
| 443 | } |
| 444 | input.spoilerhider { |
| 445 | display: none; |
| 446 | } |
| 447 | input.spoilerhider ~ .spoiler { |
| 448 | overflow: hidden; |
| 449 | margin: 10px auto 0; |
| 450 | height: 0; |
| 451 | opacity: 0; |
| 452 | } |
| 453 | input.spoilerhider:checked + label + .spoiler{ |
| 454 | height: auto; |
| 455 | opacity: 1; |
| 456 | } |
| 457 | </style> |
| 458 | </head> |
| 459 | <body>)<<<" ; |
| 460 | |
| 461 | // Generate header |
| 462 | R.InsertTextBefore(Loc: StartLoc, Str: os.str()); |
| 463 | // Generate footer |
| 464 | |
| 465 | R.InsertTextAfter(Loc: EndLoc, Str: "</body></html>\n" ); |
| 466 | } |
| 467 | |
| 468 | /// SyntaxHighlight - Relex the specified FileID and annotate the HTML with |
| 469 | /// information about keywords, macro expansions etc. This uses the macro |
| 470 | /// table state from the end of the file, so it won't be perfectly perfect, |
| 471 | /// but it will be reasonably close. |
| 472 | static void SyntaxHighlightImpl( |
| 473 | Rewriter &R, FileID FID, const Preprocessor &PP, |
| 474 | llvm::function_ref<void(RewriteBuffer &, unsigned, unsigned, const char *, |
| 475 | const char *, const char *)> |
| 476 | HighlightRangeCallback) { |
| 477 | |
| 478 | RewriteBuffer &RB = R.getEditBuffer(FID); |
| 479 | const SourceManager &SM = PP.getSourceManager(); |
| 480 | llvm::MemoryBufferRef FromFile = SM.getBufferOrFake(FID); |
| 481 | const char *BufferStart = FromFile.getBuffer().data(); |
| 482 | |
| 483 | Lexer L(FID, FromFile, SM, PP.getLangOpts()); |
| 484 | |
| 485 | // Inform the preprocessor that we want to retain comments as tokens, so we |
| 486 | // can highlight them. |
| 487 | L.SetCommentRetentionState(true); |
| 488 | |
| 489 | // Lex all the tokens in raw mode, to avoid entering #includes or expanding |
| 490 | // macros. |
| 491 | Token Tok; |
| 492 | L.LexFromRawLexer(Result&: Tok); |
| 493 | |
| 494 | while (Tok.isNot(K: tok::eof)) { |
| 495 | // Since we are lexing unexpanded tokens, all tokens are from the main |
| 496 | // FileID. |
| 497 | unsigned TokOffs = SM.getFileOffset(SpellingLoc: Tok.getLocation()); |
| 498 | unsigned TokLen = Tok.getLength(); |
| 499 | switch (Tok.getKind()) { |
| 500 | default: break; |
| 501 | case tok::identifier: |
| 502 | llvm_unreachable("tok::identifier in raw lexing mode!" ); |
| 503 | case tok::raw_identifier: { |
| 504 | // Fill in Result.IdentifierInfo and update the token kind, |
| 505 | // looking up the identifier in the identifier table. |
| 506 | PP.LookUpIdentifierInfo(Identifier&: Tok); |
| 507 | |
| 508 | // If this is a pp-identifier, for a keyword, highlight it as such. |
| 509 | if (Tok.isNot(K: tok::identifier)) |
| 510 | HighlightRangeCallback(RB, TokOffs, TokOffs + TokLen, BufferStart, |
| 511 | "<span class='keyword'>" , "</span>" ); |
| 512 | break; |
| 513 | } |
| 514 | case tok::comment: |
| 515 | HighlightRangeCallback(RB, TokOffs, TokOffs + TokLen, BufferStart, |
| 516 | "<span class='comment'>" , "</span>" ); |
| 517 | break; |
| 518 | case tok::utf8_string_literal: |
| 519 | // Chop off the u part of u8 prefix |
| 520 | ++TokOffs; |
| 521 | --TokLen; |
| 522 | // FALL THROUGH to chop the 8 |
| 523 | [[fallthrough]]; |
| 524 | case tok::wide_string_literal: |
| 525 | case tok::utf16_string_literal: |
| 526 | case tok::utf32_string_literal: |
| 527 | // Chop off the L, u, U or 8 prefix |
| 528 | ++TokOffs; |
| 529 | --TokLen; |
| 530 | [[fallthrough]]; |
| 531 | case tok::string_literal: |
| 532 | // FIXME: Exclude the optional ud-suffix from the highlighted range. |
| 533 | HighlightRangeCallback(RB, TokOffs, TokOffs + TokLen, BufferStart, |
| 534 | "<span class='string_literal'>" , "</span>" ); |
| 535 | break; |
| 536 | case tok::hash: { |
| 537 | // If this is a preprocessor directive, all tokens to end of line are too. |
| 538 | if (!Tok.isAtStartOfLine()) |
| 539 | break; |
| 540 | |
| 541 | // Eat all of the tokens until we get to the next one at the start of |
| 542 | // line. |
| 543 | unsigned TokEnd = TokOffs+TokLen; |
| 544 | L.LexFromRawLexer(Result&: Tok); |
| 545 | while (!Tok.isAtStartOfLine() && Tok.isNot(K: tok::eof)) { |
| 546 | TokEnd = SM.getFileOffset(SpellingLoc: Tok.getLocation())+Tok.getLength(); |
| 547 | L.LexFromRawLexer(Result&: Tok); |
| 548 | } |
| 549 | |
| 550 | // Find end of line. This is a hack. |
| 551 | HighlightRangeCallback(RB, TokOffs, TokEnd, BufferStart, |
| 552 | "<span class='directive'>" , "</span>" ); |
| 553 | |
| 554 | // Don't skip the next token. |
| 555 | continue; |
| 556 | } |
| 557 | } |
| 558 | |
| 559 | L.LexFromRawLexer(Result&: Tok); |
| 560 | } |
| 561 | } |
| 562 | void html::SyntaxHighlight(Rewriter &R, FileID FID, const Preprocessor &PP, |
| 563 | RelexRewriteCacheRef Cache) { |
| 564 | RewriteBuffer &RB = R.getEditBuffer(FID); |
| 565 | const SourceManager &SM = PP.getSourceManager(); |
| 566 | llvm::MemoryBufferRef FromFile = SM.getBufferOrFake(FID); |
| 567 | const char *BufferStart = FromFile.getBuffer().data(); |
| 568 | |
| 569 | if (Cache) { |
| 570 | auto CacheIt = Cache->SyntaxHighlights.find(Val: FID); |
| 571 | if (CacheIt != Cache->SyntaxHighlights.end()) { |
| 572 | for (const RelexRewriteCache::RawHighlight &H : CacheIt->second) { |
| 573 | HighlightRange(RB, B: H.B, E: H.E, BufferStart, StartTag: H.StartTag.data(), |
| 574 | EndTag: H.EndTag.data()); |
| 575 | } |
| 576 | return; |
| 577 | } |
| 578 | } |
| 579 | |
| 580 | // "Every time you would call HighlightRange, cache the inputs as well." |
| 581 | auto HighlightRangeCallback = [&](RewriteBuffer &RB, unsigned B, unsigned E, |
| 582 | const char *BufferStart, |
| 583 | const char *StartTag, const char *EndTag) { |
| 584 | HighlightRange(RB, B, E, BufferStart, StartTag, EndTag); |
| 585 | |
| 586 | if (Cache) |
| 587 | Cache->SyntaxHighlights[FID].push_back(x: {.B: B, .E: E, .StartTag: StartTag, .EndTag: EndTag}); |
| 588 | }; |
| 589 | |
| 590 | SyntaxHighlightImpl(R, FID, PP, HighlightRangeCallback); |
| 591 | } |
| 592 | |
| 593 | static void HighlightMacrosImpl( |
| 594 | Rewriter &R, FileID FID, const Preprocessor &PP, |
| 595 | llvm::function_ref<void(Rewriter &, SourceLocation, SourceLocation, |
| 596 | const char *, const char *, bool)> |
| 597 | HighlightRangeCallback) { |
| 598 | |
| 599 | // Re-lex the raw token stream into a token buffer. |
| 600 | const SourceManager &SM = PP.getSourceManager(); |
| 601 | std::vector<Token> TokenStream; |
| 602 | |
| 603 | llvm::MemoryBufferRef FromFile = SM.getBufferOrFake(FID); |
| 604 | Lexer L(FID, FromFile, SM, PP.getLangOpts()); |
| 605 | |
| 606 | // Lex all the tokens in raw mode, to avoid entering #includes or expanding |
| 607 | // macros. |
| 608 | while (true) { |
| 609 | Token Tok; |
| 610 | L.LexFromRawLexer(Result&: Tok); |
| 611 | |
| 612 | // If this is a # at the start of a line, discard it from the token stream. |
| 613 | // We don't want the re-preprocess step to see #defines, #includes or other |
| 614 | // preprocessor directives. |
| 615 | if (Tok.is(K: tok::hash) && Tok.isAtStartOfLine()) |
| 616 | continue; |
| 617 | |
| 618 | // If this is a ## token, change its kind to unknown so that repreprocessing |
| 619 | // it will not produce an error. |
| 620 | if (Tok.is(K: tok::hashhash)) |
| 621 | Tok.setKind(tok::unknown); |
| 622 | |
| 623 | // If this raw token is an identifier, the raw lexer won't have looked up |
| 624 | // the corresponding identifier info for it. Do this now so that it will be |
| 625 | // macro expanded when we re-preprocess it. |
| 626 | if (Tok.is(K: tok::raw_identifier)) |
| 627 | PP.LookUpIdentifierInfo(Identifier&: Tok); |
| 628 | |
| 629 | TokenStream.push_back(x: Tok); |
| 630 | |
| 631 | if (Tok.is(K: tok::eof)) break; |
| 632 | } |
| 633 | |
| 634 | // Temporarily change the diagnostics object so that we ignore any generated |
| 635 | // diagnostics from this pass. |
| 636 | DiagnosticsEngine TmpDiags(PP.getDiagnostics().getDiagnosticIDs(), |
| 637 | PP.getDiagnostics().getDiagnosticOptions(), |
| 638 | new IgnoringDiagConsumer); |
| 639 | |
| 640 | // FIXME: This is a huge hack; we reuse the input preprocessor because we want |
| 641 | // its state, but we aren't actually changing it (we hope). This should really |
| 642 | // construct a copy of the preprocessor. |
| 643 | Preprocessor &TmpPP = const_cast<Preprocessor&>(PP); |
| 644 | DiagnosticsEngine *OldDiags = &TmpPP.getDiagnostics(); |
| 645 | TmpPP.setDiagnostics(TmpDiags); |
| 646 | |
| 647 | // Inform the preprocessor that we don't want comments. |
| 648 | TmpPP.SetCommentRetentionState(KeepComments: false, KeepMacroComments: false); |
| 649 | |
| 650 | // We don't want pragmas either. Although we filtered out #pragma, removing |
| 651 | // _Pragma and __pragma is much harder. |
| 652 | bool PragmasPreviouslyEnabled = TmpPP.getPragmasEnabled(); |
| 653 | TmpPP.setPragmasEnabled(false); |
| 654 | |
| 655 | // Enter the tokens we just lexed. This will cause them to be macro expanded |
| 656 | // but won't enter sub-files (because we removed #'s). |
| 657 | TmpPP.EnterTokenStream(Toks: TokenStream, DisableMacroExpansion: false, /*IsReinject=*/false); |
| 658 | |
| 659 | TokenConcatenation ConcatInfo(TmpPP); |
| 660 | |
| 661 | // Lex all the tokens. |
| 662 | Token Tok; |
| 663 | TmpPP.Lex(Result&: Tok); |
| 664 | while (Tok.isNot(K: tok::eof)) { |
| 665 | // Ignore non-macro tokens. |
| 666 | if (!Tok.getLocation().isMacroID()) { |
| 667 | TmpPP.Lex(Result&: Tok); |
| 668 | continue; |
| 669 | } |
| 670 | |
| 671 | // Okay, we have the first token of a macro expansion: highlight the |
| 672 | // expansion by inserting a start tag before the macro expansion and |
| 673 | // end tag after it. |
| 674 | CharSourceRange LLoc = SM.getExpansionRange(Loc: Tok.getLocation()); |
| 675 | |
| 676 | // Ignore tokens whose instantiation location was not the main file. |
| 677 | if (SM.getFileID(SpellingLoc: LLoc.getBegin()) != FID) { |
| 678 | TmpPP.Lex(Result&: Tok); |
| 679 | continue; |
| 680 | } |
| 681 | |
| 682 | assert(SM.getFileID(LLoc.getEnd()) == FID && |
| 683 | "Start and end of expansion must be in the same ultimate file!" ); |
| 684 | |
| 685 | std::string Expansion = EscapeText(s: TmpPP.getSpelling(Tok)); |
| 686 | unsigned LineLen = Expansion.size(); |
| 687 | |
| 688 | Token PrevPrevTok; |
| 689 | Token PrevTok = Tok; |
| 690 | // Okay, eat this token, getting the next one. |
| 691 | TmpPP.Lex(Result&: Tok); |
| 692 | |
| 693 | // Skip all the rest of the tokens that are part of this macro |
| 694 | // instantiation. It would be really nice to pop up a window with all the |
| 695 | // spelling of the tokens or something. |
| 696 | while (!Tok.is(K: tok::eof) && |
| 697 | SM.getExpansionLoc(Loc: Tok.getLocation()) == LLoc.getBegin()) { |
| 698 | // Insert a newline if the macro expansion is getting large. |
| 699 | if (LineLen > 60) { |
| 700 | Expansion += "<br>" ; |
| 701 | LineLen = 0; |
| 702 | } |
| 703 | |
| 704 | LineLen -= Expansion.size(); |
| 705 | |
| 706 | // If the tokens were already space separated, or if they must be to avoid |
| 707 | // them being implicitly pasted, add a space between them. |
| 708 | if (Tok.hasLeadingSpace() || |
| 709 | ConcatInfo.AvoidConcat(PrevPrevTok, PrevTok, Tok)) |
| 710 | Expansion += ' '; |
| 711 | |
| 712 | // Escape any special characters in the token text. |
| 713 | Expansion += EscapeText(s: TmpPP.getSpelling(Tok)); |
| 714 | LineLen += Expansion.size(); |
| 715 | |
| 716 | PrevPrevTok = PrevTok; |
| 717 | PrevTok = Tok; |
| 718 | TmpPP.Lex(Result&: Tok); |
| 719 | } |
| 720 | |
| 721 | // Insert the 'macro_popup' as the end tag, so that multi-line macros all |
| 722 | // get highlighted. |
| 723 | Expansion = "<span class='macro_popup'>" + Expansion + "</span></span>" ; |
| 724 | |
| 725 | HighlightRangeCallback(R, LLoc.getBegin(), LLoc.getEnd(), |
| 726 | "<span class='macro'>" , Expansion.c_str(), |
| 727 | LLoc.isTokenRange()); |
| 728 | } |
| 729 | |
| 730 | // Restore the preprocessor's old state. |
| 731 | TmpPP.setDiagnostics(*OldDiags); |
| 732 | TmpPP.setPragmasEnabled(PragmasPreviouslyEnabled); |
| 733 | } |
| 734 | |
| 735 | /// HighlightMacros - This uses the macro table state from the end of the |
| 736 | /// file, to re-expand macros and insert (into the HTML) information about the |
| 737 | /// macro expansions. This won't be perfectly perfect, but it will be |
| 738 | /// reasonably close. |
| 739 | void html::HighlightMacros(Rewriter &R, FileID FID, const Preprocessor &PP, |
| 740 | RelexRewriteCacheRef Cache) { |
| 741 | if (Cache) { |
| 742 | auto CacheIt = Cache->MacroHighlights.find(Val: FID); |
| 743 | if (CacheIt != Cache->MacroHighlights.end()) { |
| 744 | for (const RelexRewriteCache::Highlight &H : CacheIt->second) { |
| 745 | HighlightRange(R, B: H.B, E: H.E, StartTag: H.StartTag.data(), EndTag: H.EndTag.data(), |
| 746 | IsTokenRange: H.IsTokenRange); |
| 747 | } |
| 748 | return; |
| 749 | } |
| 750 | } |
| 751 | |
| 752 | // "Every time you would call HighlightRange, cache the inputs as well." |
| 753 | auto HighlightRangeCallback = [&](Rewriter &R, SourceLocation B, |
| 754 | SourceLocation E, const char *StartTag, |
| 755 | const char *EndTag, bool isTokenRange) { |
| 756 | HighlightRange(R, B, E, StartTag, EndTag, IsTokenRange: isTokenRange); |
| 757 | |
| 758 | if (Cache) { |
| 759 | Cache->MacroHighlights[FID].push_back( |
| 760 | x: {.B: B, .E: E, .StartTag: StartTag, .EndTag: EndTag, .IsTokenRange: isTokenRange}); |
| 761 | } |
| 762 | }; |
| 763 | |
| 764 | HighlightMacrosImpl(R, FID, PP, HighlightRangeCallback); |
| 765 | } |
| 766 | |