clang-tools 23.0.0git
Markup.cpp
Go to the documentation of this file.
1//===--- Markup.cpp -----------------------------------------*- 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#include "support/Markup.h"
9#include "clang/Basic/CharInfo.h"
10#include "llvm/ADT/ArrayRef.h"
11#include "llvm/ADT/STLExtras.h"
12#include "llvm/ADT/SmallVector.h"
13#include "llvm/ADT/StringExtras.h"
14#include "llvm/ADT/StringRef.h"
15#include "llvm/Support/raw_ostream.h"
16#include <cstddef>
17#include <iterator>
18#include <memory>
19#include <string>
20#include <vector>
21
22namespace clang {
23namespace clangd {
24namespace markup {
25namespace {
26
27// Is <contents a plausible start to an HTML tag?
28// Contents may not be the rest of the line, but it's the rest of the plain
29// text, so we expect to see at least the tag name.
30bool looksLikeTag(llvm::StringRef Contents) {
31 if (Contents.empty())
32 return false;
33 if (Contents.front() == '!' || Contents.front() == '?' ||
34 Contents.front() == '/')
35 return true;
36 // Check the start of the tag name.
37 if (!llvm::isAlpha(Contents.front()))
38 return false;
39 // Drop rest of the tag name, and following whitespace.
40 Contents = Contents
41 .drop_while([](char C) {
42 return llvm::isAlnum(C) || C == '-' || C == '_' || C == ':';
43 })
44 .drop_while(llvm::isSpace);
45 // The rest of the tag consists of attributes, which have restrictive names.
46 // If we hit '=', all bets are off (attribute values can contain anything).
47 for (; !Contents.empty(); Contents = Contents.drop_front()) {
48 if (llvm::isAlnum(Contents.front()) || llvm::isSpace(Contents.front()))
49 continue;
50 if (Contents.front() == '>' || Contents.starts_with("/>"))
51 return true; // May close the tag.
52 if (Contents.front() == '=')
53 return true; // Don't try to parse attribute values.
54 return false; // Random punctuation means this isn't a tag.
55 }
56 return true; // Potentially incomplete tag.
57}
58
59// Tests whether C should be backslash-escaped in markdown.
60// The string being escaped is Before + C + After. This is part of a paragraph.
61// StartsLine indicates whether `Before` is the start of the line.
62// After may not be everything until the end of the line.
63//
64// It's always safe to escape punctuation, but want minimal escaping.
65// The strategy is to escape the first character of anything that might start
66// a markdown grammar construct.
67bool needsLeadingEscapePlaintext(char C, llvm::StringRef Before,
68 llvm::StringRef After, bool StartsLine) {
69 assert(Before.take_while(llvm::isSpace).empty());
70 auto RulerLength = [&]() -> /*Length*/ unsigned {
71 if (!StartsLine || !Before.empty())
72 return false;
73 llvm::StringRef A = After.rtrim();
74 return llvm::all_of(A, [C](char D) { return C == D; }) ? 1 + A.size() : 0;
75 };
76 auto IsBullet = [&]() {
77 return StartsLine && Before.empty() &&
78 (After.empty() || After.starts_with(" "));
79 };
80 auto SpaceSurrounds = [&]() {
81 return (After.empty() || llvm::isSpace(After.front())) &&
82 (Before.empty() || llvm::isSpace(Before.back()));
83 };
84 auto WordSurrounds = [&]() {
85 return (!After.empty() && llvm::isAlnum(After.front())) &&
86 (!Before.empty() && llvm::isAlnum(Before.back()));
87 };
88
89 switch (C) {
90 case '\\': // Escaped character.
91 return true;
92 case '`': // Code block or inline code
93 // Any number of backticks can delimit an inline code block that can end
94 // anywhere (including on another line). We must escape them all.
95 return true;
96 case '~': // Code block
97 return StartsLine && Before.empty() && After.starts_with("~~");
98 case '#': { // ATX heading.
99 if (!StartsLine || !Before.empty())
100 return false;
101 llvm::StringRef Rest = After.ltrim(C);
102 return Rest.empty() || Rest.starts_with(" ");
103 }
104 case ']': // Link or link reference.
105 // We escape ] rather than [ here, because it's more constrained:
106 // ](...) is an in-line link
107 // ]: is a link reference
108 // The following are only links if the link reference exists:
109 // ] by itself is a shortcut link
110 // ][...] is an out-of-line link
111 // Because we never emit link references, we don't need to handle these.
112 return After.starts_with(":") || After.starts_with("(");
113 case '=': // Setex heading.
114 return RulerLength() > 0;
115 case '_': // Horizontal ruler or matched delimiter.
116 if (RulerLength() >= 3)
117 return true;
118 // Not a delimiter if surrounded by space, or inside a word.
119 // (The rules at word boundaries are subtle).
120 return !(SpaceSurrounds() || WordSurrounds());
121 case '-': // Setex heading, horizontal ruler, or bullet.
122 if (RulerLength() > 0)
123 return true;
124 return IsBullet();
125 case '+': // Bullet list.
126 return IsBullet();
127 case '*': // Bullet list, horizontal ruler, or delimiter.
128 return IsBullet() || RulerLength() >= 3 || !SpaceSurrounds();
129 case '<': // HTML tag (or autolink, which we choose not to escape)
130 return looksLikeTag(After);
131 case '>': // Quote marker. Needs escaping at start of line.
132 return StartsLine && Before.empty();
133 case '&': { // HTML entity reference
134 auto End = After.find(';');
135 if (End == llvm::StringRef::npos)
136 return false;
137 llvm::StringRef Content = After.substr(0, End);
138 if (Content.consume_front("#")) {
139 if (Content.consume_front("x") || Content.consume_front("X"))
140 return llvm::all_of(Content, llvm::isHexDigit);
141 return llvm::all_of(Content, llvm::isDigit);
142 }
143 return llvm::all_of(Content, llvm::isAlpha);
144 }
145 case '.': // Numbered list indicator. Escape 12. -> 12\. at start of line.
146 case ')':
147 return StartsLine && !Before.empty() &&
148 llvm::all_of(Before, llvm::isDigit) && After.starts_with(" ");
149 default:
150 return false;
151 }
152}
153
154/// \brief Tests whether \p C should be backslash-escaped in markdown.
155///
156/// The MarkupContent LSP specification defines that `markdown` content needs to
157/// follow GFM (GitHub Flavored Markdown) rules. And we can assume that markdown
158/// is rendered on the client side. This means we do not need to escape any
159/// markdown constructs.
160/// The only exception is when the client does not support HTML rendering in
161/// markdown. In that case, we need to escape HTML tags and HTML entities.
162///
163/// **FIXME:** handle the case when the client does support HTML rendering in
164/// markdown. For this, the LSP server needs to check the
165/// [supportsHtml
166/// capability](https://github.com/microsoft/language-server-protocol/issues/1344)
167/// of the client.
168///
169/// \param C The character to check.
170/// \param After The string that follows \p C .
171/// This is used to determine if \p C is part of a tag or an entity reference.
172///
173/// \returns true if \p C should be escaped, false otherwise.
174bool needsLeadingEscapeMarkdown(char C, llvm::StringRef After) {
175 switch (C) {
176 case '<': // HTML tag (or autolink, which we choose not to escape)
177 return looksLikeTag(After);
178 case '&': { // HTML entity reference
179 auto End = After.find(';');
180 if (End == llvm::StringRef::npos)
181 return false;
182 llvm::StringRef Content = After.substr(0, End);
183 if (Content.consume_front("#")) {
184 if (Content.consume_front("x") || Content.consume_front("X"))
185 return llvm::all_of(Content, llvm::isHexDigit);
186 return llvm::all_of(Content, llvm::isDigit);
187 }
188 return llvm::all_of(Content, llvm::isAlpha);
189 }
190 default:
191 return false;
192 }
193}
194
195bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After,
196 bool StartsLine, bool EscapeMarkdown) {
197 if (EscapeMarkdown)
198 return needsLeadingEscapePlaintext(C, Before, After, StartsLine);
199 return needsLeadingEscapeMarkdown(C, After);
200}
201
202/// \brief Render text for markdown output.
203///
204/// If \p EscapeMarkdown is true it ensures the punctuation will not introduce
205/// any of the markdown constructs.
206///
207/// Else, markdown syntax is not escaped, only HTML tags and entities.
208/// HTML is escaped because usually clients do not support HTML rendering by
209/// default. Passing unescaped HTML will therefore often result in not showing
210/// the HTML at all.
211/// \note In markdown code spans, we do not escape anything.
212std::string renderText(llvm::StringRef Input, bool StartsLine,
213 bool EscapeMarkdown) {
214 std::string R;
215 R.reserve(Input.size());
216
217 // split the input into lines, and escape each line separately.
218 llvm::StringRef Line, Rest;
219
220 bool IsFirstLine = true;
221
222 // Inside markdown code spans, we do not escape anything when EscapeMarkdown
223 // is false.
224 bool InCodeSpan = false;
225
226 for (std::tie(Line, Rest) = Input.split('\n');
227 !(Line.empty() && Rest.empty());
228 std::tie(Line, Rest) = Rest.split('\n')) {
229
230 bool StartsLineIntern = IsFirstLine ? StartsLine : true;
231
232 // Ignore leading spaces for the escape logic, but preserve them in the
233 // output.
234 StringRef LeadingSpaces = Line.take_while(llvm::isSpace);
235 if (!LeadingSpaces.empty()) {
236 R.append(LeadingSpaces);
237 }
238
239 // Handle the case where the user escaped a character themselves.
240 // This is relevant for markdown code spans if EscapeMarkdown is false,
241 // because if the user escaped a backtick, we must treat the enclosed text
242 // as normal markdown text.
243 bool UserEscape = false;
244 for (unsigned I = LeadingSpaces.size(); I < Line.size(); ++I) {
245
246 if (!EscapeMarkdown && !UserEscape && Line[I] == '`')
247 InCodeSpan = !InCodeSpan;
248
249 if (!InCodeSpan &&
250 needsLeadingEscape(Line[I], Line.substr(LeadingSpaces.size(), I),
251 Line.substr(I + 1), StartsLineIntern,
252 EscapeMarkdown))
253 R.push_back('\\');
254 R.push_back(Line[I]);
255
256 if (Line[I] == '\\')
257 UserEscape = !UserEscape;
258 else
259 UserEscape = false;
260 }
261
262 IsFirstLine = false;
263 if (!Rest.empty())
264 R.push_back('\n');
265 }
266
267 return R;
268}
269
270/// Renders \p Input as an inline block of code in markdown. The returned value
271/// is surrounded by backticks and the inner contents are properly escaped.
272std::string renderInlineBlock(llvm::StringRef Input) {
273 std::string R;
274 R.reserve(Input.size());
275 // Double all backticks to make sure we don't close the inline block early.
276 for (size_t From = 0; From < Input.size();) {
277 size_t Next = Input.find("`", From);
278 R += Input.substr(From, Next - From);
279 if (Next == llvm::StringRef::npos)
280 break;
281 R += "``"; // double the found backtick.
282
283 From = Next + 1;
284 }
285 // If results starts with a backtick, add spaces on both sides. The spaces
286 // are ignored by markdown renderers.
287 if (llvm::StringRef(R).starts_with("`") || llvm::StringRef(R).ends_with("`"))
288 return "` " + std::move(R) + " `";
289 // Markdown render should ignore first and last space if both are there. We
290 // add an extra pair of spaces in that case to make sure we render what the
291 // user intended.
292 if (llvm::StringRef(R).starts_with(" ") && llvm::StringRef(R).ends_with(" "))
293 return "` " + std::move(R) + " `";
294 return "`" + std::move(R) + "`";
295}
296
297/// Get marker required for \p Input to represent a markdown codeblock. It
298/// consists of at least 3 backticks(`). Although markdown also allows to use
299/// tilde(~) for code blocks, they are never used.
300std::string getMarkerForCodeBlock(llvm::StringRef Input) {
301 // Count the maximum number of consecutive backticks in \p Input. We need to
302 // start and end the code block with more.
303 unsigned MaxBackticks = 0;
304 unsigned Backticks = 0;
305 for (char C : Input) {
306 if (C == '`') {
307 ++Backticks;
308 continue;
309 }
312 }
314 // Use the corresponding number of backticks to start and end a code block.
315 return std::string(/*Repeat=*/std::max(3u, MaxBackticks + 1), '`');
316}
317
318// Trims the input and concatenates whitespace blocks into a single ` `.
319std::string canonicalizeSpaces(llvm::StringRef Input) {
320 llvm::SmallVector<llvm::StringRef> Words;
321 llvm::SplitString(Input, Words);
322 return llvm::join(Words, " ");
323}
324
325std::string renderBlocks(llvm::ArrayRef<std::unique_ptr<Block>> Children,
326 void (Block::*RenderFunc)(llvm::raw_ostream &) const) {
327 std::string R;
328 llvm::raw_string_ostream OS(R);
329
330 // Trim rulers.
331 Children = Children.drop_while(
332 [](const std::unique_ptr<Block> &C) { return C->isRuler(); });
333 auto Last = llvm::find_if(
334 llvm::reverse(Children),
335 [](const std::unique_ptr<Block> &C) { return !C->isRuler(); });
336 Children = Children.drop_back(Children.end() - Last.base());
337
338 bool LastBlockWasRuler = true;
339 for (const auto &C : Children) {
340 if (C->isRuler() && LastBlockWasRuler)
341 continue;
342 LastBlockWasRuler = C->isRuler();
343 ((*C).*RenderFunc)(OS);
344 }
345
346 // Get rid of redundant empty lines introduced in plaintext while imitating
347 // padding in markdown.
348 std::string AdjustedResult;
349 llvm::StringRef TrimmedText(OS.str());
350 TrimmedText = TrimmedText.trim();
351
352 llvm::copy_if(TrimmedText, std::back_inserter(AdjustedResult),
353 [&TrimmedText](const char &C) {
354 return !llvm::StringRef(TrimmedText.data(),
355 &C - TrimmedText.data() + 1)
356 // We allow at most two newlines.
357 .ends_with("\n\n\n");
358 });
359
360 return AdjustedResult;
361}
362
363// Separates two blocks with extra spacing. Note that it might render strangely
364// in vscode if the trailing block is a codeblock, see
365// https://github.com/microsoft/vscode/issues/88416 for details.
366class Ruler : public Block {
367public:
368 void renderEscapedMarkdown(llvm::raw_ostream &OS) const override {
369 renderMarkdown(OS);
370 }
371 void renderMarkdown(llvm::raw_ostream &OS) const override {
372 // Note that we need an extra new line before the ruler, otherwise we might
373 // make previous block a title instead of introducing a ruler.
374 OS << "\n---\n";
375 }
376 void renderPlainText(llvm::raw_ostream &OS) const override { OS << '\n'; }
377 std::unique_ptr<Block> clone() const override {
378 return std::make_unique<Ruler>(*this);
379 }
380 bool isRuler() const override { return true; }
381};
382
383class CodeBlock : public Block {
384public:
385 void renderEscapedMarkdown(llvm::raw_ostream &OS) const override {
386 renderMarkdown(OS);
387 }
388 void renderMarkdown(llvm::raw_ostream &OS) const override {
389 std::string Marker = getMarkerForCodeBlock(Contents);
390 // No need to pad from previous blocks, as they should end with a new line.
391 OS << Marker << Language << '\n' << Contents;
392 if (!Contents.empty() && Contents.back() != '\n')
393 OS << '\n';
394 // Always end with an empty line to separate code blocks from following
395 // paragraphs.
396 OS << Marker << "\n\n";
397 }
398
399 void renderPlainText(llvm::raw_ostream &OS) const override {
400 // In plaintext we want one empty line before and after codeblocks.
401 OS << '\n' << Contents << "\n\n";
402 }
403
404 std::unique_ptr<Block> clone() const override {
405 return std::make_unique<CodeBlock>(*this);
406 }
407
408 CodeBlock(std::string Contents, std::string Language)
409 : Contents(std::move(Contents)), Language(std::move(Language)) {}
410
411private:
412 std::string Contents;
413 std::string Language;
414};
415
416// Inserts two spaces after each `\n` to indent each line. First line is not
417// indented.
418std::string indentLines(llvm::StringRef Input) {
419 assert(!Input.ends_with("\n") && "Input should've been trimmed.");
420 std::string IndentedR;
421 // We'll add 2 spaces after each new line which is not followed by another new
422 // line.
423 IndentedR.reserve(Input.size() + Input.count('\n') * 2);
424 for (size_t I = 0; I < Input.size(); ++I) {
425 char C = Input[I];
426 IndentedR += C;
427 if (C == '\n' && (((I + 1) < Input.size()) && (Input[I + 1] != '\n')))
428 IndentedR.append(" ");
429 }
430 return IndentedR;
431}
432
433class Heading : public Paragraph {
434public:
435 Heading(size_t Level) : Level(Level) {}
436
437 void renderEscapedMarkdown(llvm::raw_ostream &OS) const override {
438 insertHeadingMarkers(OS);
439 Paragraph::renderEscapedMarkdown(OS);
440 }
441
442 void renderMarkdown(llvm::raw_ostream &OS) const override {
443 insertHeadingMarkers(OS);
444 Paragraph::renderMarkdown(OS);
445 }
446
447private:
448 size_t Level;
449
450 void insertHeadingMarkers(llvm::raw_ostream &OS) const {
451 OS << std::string(Level, '#') << ' ';
452 }
453};
454
455} // namespace
456
457std::string Block::asEscapedMarkdown() const {
458 std::string R;
459 llvm::raw_string_ostream OS(R);
461 return llvm::StringRef(OS.str()).trim().str();
462}
463
464std::string Block::asMarkdown() const {
465 std::string R;
466 llvm::raw_string_ostream OS(R);
467 renderMarkdown(OS);
468 return llvm::StringRef(OS.str()).trim().str();
469}
470
471std::string Block::asPlainText() const {
472 std::string R;
473 llvm::raw_string_ostream OS(R);
474 renderPlainText(OS);
475 return llvm::StringRef(OS.str()).trim().str();
476}
477
478void Paragraph::renderNewlinesMarkdown(llvm::raw_ostream &OS,
479 llvm::StringRef ParagraphText) const {
480 llvm::StringRef Line, Rest;
481
482 for (std::tie(Line, Rest) = ParagraphText.ltrim("\n").rtrim().split('\n');
483 !(Line.empty() && Rest.empty());
484 std::tie(Line, Rest) = Rest.split('\n')) {
485
486 if (Line.empty()) {
487 // Blank lines are preserved in markdown.
488 OS << '\n';
489 continue;
490 }
491
492 OS << Line;
493
494 if (Rest.empty())
495 // In this case the paragraph ends and we don't need to add another
496 // newline
497 continue;
498
499 if (isHardLineBreakAfter(Line, Rest, /*IsMarkdown=*/true))
500 // In markdown, 2 spaces before a line break forces a line break.
501 OS << " ";
502 OS << '\n';
503 }
504}
505
506void Paragraph::renderEscapedMarkdown(llvm::raw_ostream &OS) const {
507 bool NeedsSpace = false;
508 bool HasChunks = false;
509 std::string ParagraphText;
510 ParagraphText.reserve(EstimatedStringSize);
511 llvm::raw_string_ostream ParagraphTextOS(ParagraphText);
512 for (auto &C : Chunks) {
513 if (C.SpaceBefore || NeedsSpace)
514 ParagraphTextOS << " ";
515 switch (C.Kind) {
516 case ChunkKind::PlainText:
517 ParagraphTextOS << renderText(C.Contents, !HasChunks,
518 /*EscapeMarkdown=*/true);
519 break;
520 case ChunkKind::InlineCode:
521 ParagraphTextOS << renderInlineBlock(C.Contents);
522 break;
523 case ChunkKind::Bold:
524 ParagraphTextOS << renderText("**" + C.Contents + "**", !HasChunks,
525 /*EscapeMarkdown=*/true);
526 break;
527 case ChunkKind::Emphasized:
528 ParagraphTextOS << renderText("*" + C.Contents + "*", !HasChunks,
529 /*EscapeMarkdown=*/true);
530 break;
531 }
532 HasChunks = true;
533 NeedsSpace = C.SpaceAfter;
534 }
535
536 // We use the same newline handling as for plaintext to "escape" markdown
537 // whitespace rendering.
538 // But we need to use Markdown linebreaks since the client requested Markdown
539 // content.
540 renderNewlinesPlaintext(OS, ParagraphText, /*UseMarkdownLinebreaks=*/true);
541
542 // A paragraph in markdown is separated by a blank line.
543 OS << "\n\n";
544}
545
546void Paragraph::renderMarkdown(llvm::raw_ostream &OS) const {
547 bool NeedsSpace = false;
548 bool HasChunks = false;
549 bool TextEndsWithNewline = false;
550 std::string ParagraphText;
551 ParagraphText.reserve(EstimatedStringSize);
552 llvm::raw_string_ostream ParagraphTextOS(ParagraphText);
553 for (auto &C : Chunks) {
554
555 if (TextEndsWithNewline) {
556 ParagraphTextOS << "\n";
557 TextEndsWithNewline = false;
558 }
559
560 if (C.SpaceBefore || NeedsSpace)
561 ParagraphTextOS << " ";
562
563 switch (C.Kind) {
564 case ChunkKind::PlainText:
565 ParagraphTextOS << renderText(C.Contents, !HasChunks,
566 /*EscapeMarkdown=*/false);
567 // renderText removes trailing newlines, but in case there are additional
568 // chunks to process, we need to keep track of the trailing newline and
569 // add it in the next iteration.
570 TextEndsWithNewline = llvm::StringRef(C.Contents).ends_with("\n");
571 break;
572 case ChunkKind::InlineCode:
573 ParagraphTextOS << renderInlineBlock(C.Contents);
574 break;
575 case ChunkKind::Bold:
576 ParagraphTextOS << "**"
577 << renderText(C.Contents, !HasChunks,
578 /*EscapeMarkdown=*/false)
579 << "**";
580 break;
581 case ChunkKind::Emphasized:
582 ParagraphTextOS << "*"
583 << renderText(C.Contents, !HasChunks,
584 /*EscapeMarkdown=*/false)
585 << "*";
586 break;
587 }
588 HasChunks = true;
589 NeedsSpace = C.SpaceAfter;
590 }
591
592 renderNewlinesMarkdown(OS, ParagraphText);
593
594 // A paragraph in markdown is separated by a blank line.
595 OS << "\n\n";
596}
597
598std::unique_ptr<Block> Paragraph::clone() const {
599 return std::make_unique<Paragraph>(*this);
600}
601
602llvm::StringRef Paragraph::chooseMarker(llvm::ArrayRef<llvm::StringRef> Options,
603 llvm::StringRef Text) const {
604 // Prefer a delimiter whose characters don't appear in the text.
605 for (llvm::StringRef S : Options)
606 if (Text.find_first_of(S) == llvm::StringRef::npos)
607 return S;
608 return Options.front();
609}
610
611bool Paragraph::punctuationIndicatesLineBreak(llvm::StringRef Line,
612 bool IsMarkdown) const {
613 constexpr llvm::StringLiteral Punctuation = R"txt(.:,;!?)txt";
614
615 if (!IsMarkdown && Line.ends_with(" "))
616 return true;
617
618 Line = Line.rtrim();
619 return !Line.empty() && Punctuation.contains(Line.back());
620}
621
622bool Paragraph::isHardLineBreakIndicator(llvm::StringRef Rest,
623 bool IsMarkdown) const {
624 // Plaintext indicators:
625 // '-'/'*' md list, '@'/'\' documentation command, '>' md blockquote,
626 // '#' headings, '`' code blocks
627 constexpr llvm::StringLiteral LinebreakIndicatorsPlainText =
628 R"txt(-*@>#`)txt";
629 // Markdown indicators:
630 // Only '@' and '\' documentation commands/escaped markdown syntax.
631 constexpr llvm::StringLiteral LinebreakIndicatorsMarkdown = R"txt(@\)txt";
632
633 Rest = Rest.ltrim(" \t");
634 if (Rest.empty())
635 return false;
636
637 if (IsMarkdown)
638 return LinebreakIndicatorsMarkdown.contains(Rest.front());
639
640 if (LinebreakIndicatorsPlainText.contains(Rest.front()))
641 return true;
642
643 if (llvm::isDigit(Rest.front())) {
644 llvm::StringRef AfterDigit = Rest.drop_while(llvm::isDigit);
645 if (AfterDigit.starts_with(".") || AfterDigit.starts_with(")"))
646 return true;
647 }
648 return false;
649}
650
651bool Paragraph::isHardLineBreakAfter(llvm::StringRef Line, llvm::StringRef Rest,
652 bool IsMarkdown) const {
653 // Should we also consider whether Line is short?
654 return punctuationIndicatesLineBreak(Line, IsMarkdown) ||
655 isHardLineBreakIndicator(Rest, IsMarkdown);
656}
657
658void Paragraph::renderNewlinesPlaintext(llvm::raw_ostream &OS,
659 llvm::StringRef ParagraphText,
660 bool UseMarkdownLinebreaks) const {
661 llvm::StringRef Line, Rest;
662
663 for (std::tie(Line, Rest) = ParagraphText.trim().split('\n');
664 !(Line.empty() && Rest.empty());
665 std::tie(Line, Rest) = Rest.split('\n')) {
666
667 // Remove lines which only contain whitespace.
668 //
669 // Note: this also handles the case when there are multiple newlines
670 // in a row, since all leading newlines are removed.
671 //
672 // The documentation parsing treats multiple newlines as paragraph
673 // separators, hence it will create a new Paragraph instead of adding
674 // multiple newlines to the same Paragraph.
675 // Therfore multiple newlines are never added to a paragraph
676 // except if the user explicitly adds them using
677 // e.g. appendText("user text\n\nnext text").
678 Line = Line.ltrim();
679 if (Line.empty())
680 continue;
681
682 OS << canonicalizeSpaces(Line);
683
684 if (Rest.empty())
685 // In this case the paragraph ends and we don't need to add another
686 // newline
687 continue;
688
689 if (isHardLineBreakAfter(Line, Rest, /*IsMarkdown=*/false)) {
690 if (UseMarkdownLinebreaks)
691 // In markdown, 2 spaces before a line break forces a line break.
692 OS << " ";
693 OS << '\n';
694 } else {
695 // Since we removed any trailing whitespace from the input using trim(),
696 // we know that the next line contains non-whitespace characters.
697 // Therefore, we can add a space without worrying about trailing spaces.
698 OS << ' ';
699 }
700 }
701}
702
703void Paragraph::renderPlainText(llvm::raw_ostream &OS) const {
704 bool NeedsSpace = false;
705 std::string ParagraphText;
706 ParagraphText.reserve(EstimatedStringSize);
707
708 llvm::raw_string_ostream ParagraphTextOS(ParagraphText);
709
710 for (auto &C : Chunks) {
711
712 if (C.Kind == ChunkKind::PlainText) {
713 if (C.SpaceBefore || NeedsSpace)
714 ParagraphTextOS << ' ';
715
716 ParagraphTextOS << C.Contents;
717 NeedsSpace = llvm::isSpace(C.Contents.back()) || C.SpaceAfter;
718 continue;
719 }
720
721 if (C.SpaceBefore || NeedsSpace)
722 ParagraphTextOS << ' ';
723 llvm::StringRef Marker = "";
724 if (C.Preserve && C.Kind == ChunkKind::InlineCode)
725 Marker = chooseMarker({"`", "'", "\""}, C.Contents);
726 else if (C.Kind == ChunkKind::Bold)
727 Marker = "**";
728 else if (C.Kind == ChunkKind::Emphasized)
729 Marker = "*";
730 ParagraphTextOS << Marker << C.Contents << Marker;
731 NeedsSpace = C.SpaceAfter;
732 }
733
734 renderNewlinesPlaintext(OS, ParagraphText, /*UseMarkdownLinebreaks=*/false);
735
736 // Paragraphs are separated by a blank line.
737 OS << "\n\n";
738}
739
740BulletList::BulletList() = default;
741BulletList::~BulletList() = default;
742
743void BulletList::renderEscapedMarkdown(llvm::raw_ostream &OS) const {
744 for (auto &D : Items) {
745 std::string M = D.asEscapedMarkdown();
746 // Instead of doing this we might prefer passing Indent to children to get
747 // rid of the copies, if it turns out to be a bottleneck.
748 OS << "- " << indentLines(M) << '\n';
749 }
750 // We add 2 newlines after list to terminate it in markdown.
751 OS << "\n\n";
752}
753
754void BulletList::renderMarkdown(llvm::raw_ostream &OS) const {
755 for (auto &D : Items) {
756 std::string M = D.asMarkdown();
757 // Instead of doing this we might prefer passing Indent to children to get
758 // rid of the copies, if it turns out to be a bottleneck.
759 OS << "- " << indentLines(M) << '\n';
760 }
761 // We add 2 newlines after list to terminate it in markdown.
762 OS << "\n\n";
763}
764
765void BulletList::renderPlainText(llvm::raw_ostream &OS) const {
766 for (auto &D : Items) {
767 // Instead of doing this we might prefer passing Indent to children to get
768 // rid of the copies, if it turns out to be a bottleneck.
769 OS << "- " << indentLines(D.asPlainText()) << '\n';
770 }
771 OS << '\n';
772}
773
774Paragraph &Paragraph::appendSpace() {
775 if (!Chunks.empty())
776 Chunks.back().SpaceAfter = true;
777 return *this;
778}
779
780Paragraph &Paragraph::appendChunk(llvm::StringRef Contents, ChunkKind K) {
781 if (Contents.empty())
782 return *this;
783 Chunks.emplace_back();
784 Chunk &C = Chunks.back();
785 C.Contents = Contents;
786 C.Kind = K;
787
788 EstimatedStringSize += Contents.size();
789 return *this;
790}
791
793 if (!Chunks.empty() && Chunks.back().Kind == ChunkKind::PlainText) {
794 Chunks.back().Contents += std::move(Text);
795 return *this;
796 }
797
798 return appendChunk(std::move(Text), ChunkKind::PlainText);
799}
800
802 return appendChunk(canonicalizeSpaces(std::move(Text)),
803 ChunkKind::Emphasized);
804}
805
807 return appendChunk(canonicalizeSpaces(std::move(Text)), ChunkKind::Bold);
808}
809
810Paragraph &Paragraph::appendCode(llvm::StringRef Code, bool Preserve) {
811 bool AdjacentCode =
812 !Chunks.empty() && Chunks.back().Kind == ChunkKind::InlineCode;
813 std::string Norm = canonicalizeSpaces(std::move(Code));
814 if (Norm.empty())
815 return *this;
816 EstimatedStringSize += Norm.size();
817 Chunks.emplace_back();
818 Chunk &C = Chunks.back();
819 C.Contents = std::move(Norm);
820 C.Kind = ChunkKind::InlineCode;
821 C.Preserve = Preserve;
822 // Disallow adjacent code spans without spaces, markdown can't render them.
823 C.SpaceBefore = AdjacentCode;
824
825 return *this;
826}
827
828std::unique_ptr<Block> BulletList::clone() const {
829 return std::make_unique<BulletList>(*this);
830}
831
832class Document &BulletList::addItem() {
833 Items.emplace_back();
834 return Items.back();
835}
836
837Document &Document::operator=(const Document &Other) {
838 Children.clear();
839 for (const auto &C : Other.Children)
840 Children.push_back(C->clone());
841 return *this;
842}
843
844void Document::append(Document Other) {
845 std::move(Other.Children.begin(), Other.Children.end(),
846 std::back_inserter(Children));
847}
848
849Paragraph &Document::addParagraph() {
850 Children.push_back(std::make_unique<Paragraph>());
851 return *static_cast<Paragraph *>(Children.back().get());
852}
853
854void Document::addRuler() { Children.push_back(std::make_unique<Ruler>()); }
855
856void Document::addCodeBlock(std::string Code, std::string Language) {
857 Children.emplace_back(
858 std::make_unique<CodeBlock>(std::move(Code), std::move(Language)));
859}
860
861std::string Document::asEscapedMarkdown() const {
863}
864
865std::string Document::asMarkdown() const {
867}
868
869std::string Document::asPlainText() const {
871}
872
873BulletList &Document::addBulletList() {
874 Children.emplace_back(std::make_unique<BulletList>());
875 return *static_cast<BulletList *>(Children.back().get());
876}
877
878Paragraph &Document::addHeading(size_t Level) {
879 assert(Level > 0);
880 Children.emplace_back(std::make_unique<Heading>(Level));
881 return *static_cast<Paragraph *>(Children.back().get());
882}
883} // namespace markup
884} // namespace clangd
885} // namespace clang
void renderMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:388
CodeBlock(std::string Contents, std::string Language)
Definition Markup.cpp:408
void renderPlainText(llvm::raw_ostream &OS) const override
Definition Markup.cpp:399
void renderEscapedMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:385
std::unique_ptr< Block > clone() const override
Definition Markup.cpp:404
void renderEscapedMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:437
Heading(size_t Level)
Definition Markup.cpp:435
void renderMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:442
void renderEscapedMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:368
void renderPlainText(llvm::raw_ostream &OS) const override
Definition Markup.cpp:376
std::unique_ptr< Block > clone() const override
Definition Markup.cpp:377
bool isRuler() const override
Definition Markup.cpp:380
void renderMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:371
std::string asEscapedMarkdown() const
Definition Markup.cpp:457
virtual void renderPlainText(llvm::raw_ostream &OS) const =0
virtual void renderMarkdown(llvm::raw_ostream &OS) const =0
virtual void renderEscapedMarkdown(llvm::raw_ostream &OS) const =0
std::string asMarkdown() const
Definition Markup.cpp:464
std::string asPlainText() const
Definition Markup.cpp:471
Represents parts of the markup that can contain strings, like inline code, code block or plain text.
Definition Markup.h:45
void renderPlainText(llvm::raw_ostream &OS) const override
Definition Markup.cpp:703
Paragraph & appendEmphasizedText(llvm::StringRef Text)
Append emphasized text, this translates to the * block in markdown.
Definition Markup.cpp:801
Paragraph & appendText(llvm::StringRef Text)
Append plain text to the end of the string.
Definition Markup.cpp:792
void renderEscapedMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:506
void renderNewlinesMarkdown(llvm::raw_ostream &OS, llvm::StringRef ParagraphText) const
Go through the contents line by line to handle the newlines and required spacing correctly for markdo...
Definition Markup.cpp:478
std::unique_ptr< Block > clone() const override
Definition Markup.cpp:598
bool isHardLineBreakAfter(llvm::StringRef Line, llvm::StringRef Rest, bool IsMarkdown) const
Checks if a hard line break should be added after the given line.
Definition Markup.cpp:651
void renderMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:546
Paragraph & appendBoldText(llvm::StringRef Text)
Append bold text, this translates to the ** block in markdown.
Definition Markup.cpp:806
bool isHardLineBreakIndicator(llvm::StringRef Rest, bool IsMarkdown) const
Append inline code, this translates to the ` block in markdown.
Definition Markup.cpp:622
FIXME: Skip testing on windows temporarily due to the different escaping code mode.
Definition AST.cpp:44
===– Representation.cpp - ClangDoc Representation --------—*- C++ -*-===//
std::string renderBlocks(llvm::ArrayRef< std::unique_ptr< Block > > Children, void(Block::*RenderFunc)(llvm::raw_ostream &) const)
Definition Markup.cpp:325
std::string indentLines(llvm::StringRef Input)
Definition Markup.cpp:418
std::string canonicalizeSpaces(llvm::StringRef Input)
Definition Markup.cpp:319