|
16 | 16 | #include <mrdocs/Support/Error.hpp>
|
17 | 17 | #include <mrdocs/Support/Path.hpp>
|
18 | 18 | #include <mrdocs/Support/String.hpp>
|
| 19 | +#include <mrdocs/Support/ScopeExit.hpp> |
19 | 20 | #include <clang/AST/CommentCommandTraits.h>
|
20 | 21 | #include <clang/AST/ASTContext.h>
|
21 | 22 | #include <clang/AST/RawCommentList.h>
|
@@ -397,22 +398,183 @@ ensureUTF8(
|
397 | 398 | return s;
|
398 | 399 | }
|
399 | 400 |
|
| 401 | +/* Parse the inline content of a text |
| 402 | +
|
| 403 | + This function takes a string from a comment |
| 404 | + and parses it into a sequence of styled text |
| 405 | + nodes. |
| 406 | +
|
| 407 | + The string may contain inline commands that |
| 408 | + change the style of the text: |
| 409 | +
|
| 410 | + Regular text is stored as a doc::Text. |
| 411 | + Styled text is stored as a doc::Styled. |
| 412 | +
|
| 413 | + The styles can be one of: mono, bold, or italic. |
| 414 | +
|
| 415 | + The tags "`", "*", and "_" are used to indicate |
| 416 | + the start and end of styled text. They can be |
| 417 | + escaped by prefixing them with a backslash. |
| 418 | +
|
| 419 | + */ |
| 420 | +doc::List<doc::Text> |
| 421 | +parseStyled(StringRef s) |
| 422 | +{ |
| 423 | + doc::List<doc::Text> result; |
| 424 | + std::string currentText; |
| 425 | + doc::Style currentStyle = doc::Style::none; |
| 426 | + bool escapeNext = false; |
| 427 | + |
| 428 | + auto isStyleMarker = [](char c) { |
| 429 | + return c == '`' || c == '*' || c == '_'; |
| 430 | + }; |
| 431 | + |
| 432 | + auto flushCurrentText = [&]() { |
| 433 | + if (!currentText.empty()) { |
| 434 | + if (currentStyle == doc::Style::none) { |
| 435 | + bool const lastIsSame = |
| 436 | + !result.empty() && |
| 437 | + result.back()->kind == doc::Kind::text; |
| 438 | + if (lastIsSame) |
| 439 | + { |
| 440 | + auto& lastText = static_cast<doc::Text&>(*result.back()); |
| 441 | + lastText.string.append(currentText); |
| 442 | + } |
| 443 | + else |
| 444 | + { |
| 445 | + result.emplace_back(std::make_unique<doc::Text>(std::move(currentText))); |
| 446 | + } |
| 447 | + } else { |
| 448 | + bool const lastIsSame = |
| 449 | + !result.empty() && |
| 450 | + result.back()->kind == doc::Kind::styled && |
| 451 | + static_cast<doc::Styled&>(*result.back()).style == currentStyle; |
| 452 | + if (lastIsSame) |
| 453 | + { |
| 454 | + auto& lastStyled = static_cast<doc::Styled&>(*result.back()); |
| 455 | + lastStyled.string.append(currentText); |
| 456 | + } |
| 457 | + else |
| 458 | + { |
| 459 | + result.emplace_back(std::make_unique<doc::Styled>(std::move(currentText), currentStyle)); |
| 460 | + } |
| 461 | + } |
| 462 | + currentText.clear(); |
| 463 | + } |
| 464 | + }; |
| 465 | + |
| 466 | + auto isPunctuationOrSpace = [](char c) { |
| 467 | + return std::isspace(c) || std::ispunct(c); |
| 468 | + }; |
| 469 | + |
| 470 | + for (std::size_t i = 0; i < s.size(); ++i) { |
| 471 | + char c = s[i]; |
| 472 | + if (escapeNext) { |
| 473 | + currentText.push_back(c); |
| 474 | + escapeNext = false; |
| 475 | + } else if (c == '\\') { |
| 476 | + escapeNext = true; |
| 477 | + } else if (isStyleMarker(c)) { |
| 478 | + bool const atWordBoundary = |
| 479 | + (currentStyle == doc::Style::none && ((i == 0) || isPunctuationOrSpace(s[i - 1]))) || |
| 480 | + (currentStyle != doc::Style::none && ((i == s.size() - 1) || isPunctuationOrSpace(s[i + 1]))); |
| 481 | + if (atWordBoundary) { |
| 482 | + flushCurrentText(); |
| 483 | + if (c == '`') { |
| 484 | + currentStyle = (currentStyle == doc::Style::mono) ? doc::Style::none : doc::Style::mono; |
| 485 | + } else if (c == '*') { |
| 486 | + currentStyle = (currentStyle == doc::Style::bold) ? doc::Style::none : doc::Style::bold; |
| 487 | + } else if (c == '_') { |
| 488 | + currentStyle = (currentStyle == doc::Style::italic) ? doc::Style::none : doc::Style::italic; |
| 489 | + } |
| 490 | + } else { |
| 491 | + currentText.push_back(c); |
| 492 | + } |
| 493 | + } else { |
| 494 | + currentText.push_back(c); |
| 495 | + } |
| 496 | + } |
| 497 | + |
| 498 | + // Whatever style we started, we should end it because |
| 499 | + // we reached the end of the string without a closing |
| 500 | + // marker. |
| 501 | + currentStyle = doc::Style::none; |
| 502 | + flushCurrentText(); |
| 503 | + |
| 504 | + return result; |
| 505 | +} |
| 506 | + |
400 | 507 | void
|
401 | 508 | JavadocVisitor::
|
402 | 509 | visitChildren(
|
403 | 510 | Comment const* C)
|
404 | 511 | {
|
405 |
| - auto const it0 = it_; |
406 |
| - auto const end0 = end_; |
407 |
| - it_ = C->child_begin(); |
408 |
| - end_ = C->child_end(); |
| 512 | + ScopeExitRestore s1(it_, C->child_begin()); |
| 513 | + ScopeExitRestore s2(end_, C->child_end()); |
409 | 514 | while(it_ != end_)
|
410 | 515 | {
|
411 | 516 | visit(*it_);
|
412 | 517 | ++it_; // must happen after
|
413 | 518 | }
|
414 |
| - it_ = it0; |
415 |
| - end_ = end0; |
| 519 | + |
| 520 | + if (!block_) |
| 521 | + { |
| 522 | + return; |
| 523 | + } |
| 524 | + |
| 525 | + bool const isVerbatim = block_->kind == doc::Kind::code; |
| 526 | + if (isVerbatim) |
| 527 | + { |
| 528 | + return; |
| 529 | + } |
| 530 | + |
| 531 | + // Merge consecutive plain text nodes in the current block |
| 532 | + auto it = block_->children.begin(); |
| 533 | + while(it != block_->children.end()) |
| 534 | + { |
| 535 | + auto& child = *it; |
| 536 | + if (child.get()->kind == doc::Kind::text) |
| 537 | + { |
| 538 | + auto* text = dynamic_cast<doc::Text*>(child.get()); |
| 539 | + MRDOCS_ASSERT(text); |
| 540 | + auto next = std::next(it); |
| 541 | + if(next != block_->children.end()) |
| 542 | + { |
| 543 | + if(next->get()->kind == doc::Kind::text) |
| 544 | + { |
| 545 | + auto* next_text = dynamic_cast<doc::Text*>(next->get()); |
| 546 | + MRDOCS_ASSERT(next_text); |
| 547 | + text->string.append(next_text->string); |
| 548 | + it = block_->children.erase(next); |
| 549 | + continue; |
| 550 | + } |
| 551 | + } |
| 552 | + } |
| 553 | + ++it; |
| 554 | + } |
| 555 | + |
| 556 | + // Parse any Text nodes for styled text |
| 557 | + for (auto it = block_->children.begin(); it != block_->children.end();) |
| 558 | + { |
| 559 | + MRDOCS_ASSERT(it->get()); |
| 560 | + if (it->get()->kind == doc::Kind::text) |
| 561 | + { |
| 562 | + auto* text = dynamic_cast<doc::Text*>(it->get()); |
| 563 | + auto styledText = parseStyled(text->string); |
| 564 | + std::size_t const offset = std::distance(block_->children.begin(), it); |
| 565 | + std::size_t const n = styledText.size(); |
| 566 | + block_->children.erase(it); |
| 567 | + block_->children.insert( |
| 568 | + block_->children.begin() + offset, |
| 569 | + std::make_move_iterator(styledText.begin()), |
| 570 | + std::make_move_iterator(styledText.end())); |
| 571 | + it = block_->children.begin() + offset + n; |
| 572 | + } |
| 573 | + else |
| 574 | + { |
| 575 | + ++it; |
| 576 | + } |
| 577 | + } |
416 | 578 | }
|
417 | 579 |
|
418 | 580 | //------------------------------------------------
|
@@ -462,16 +624,18 @@ visitTextComment(
|
462 | 624 | // If this is the first text comment in the
|
463 | 625 | // paragraph then remove all the leading space.
|
464 | 626 | // Otherwise, just remove the trailing space.
|
465 |
| - if(block_->children.empty()) |
| 627 | + if (block_->children.empty()) |
| 628 | + { |
466 | 629 | s = s.ltrim();
|
467 |
| - else |
468 |
| - s = s.rtrim(); |
| 630 | + } |
469 | 631 |
|
470 | 632 | // Only insert non-empty text nodes
|
471 | 633 | if(! s.empty())
|
| 634 | + { |
472 | 635 | emplaceText<doc::Text>(
|
473 | 636 | C->hasTrailingNewline(),
|
474 | 637 | ensureUTF8(s.str()));
|
| 638 | + } |
475 | 639 | }
|
476 | 640 |
|
477 | 641 | Expected<JavadocVisitor::TagComponents>
|
|
0 commit comments