clang-tools 22.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/// Escape a markdown text block.
203/// If \p EscapeMarkdown is true it ensures the punctuation will not introduce
204/// any of the markdown constructs.
205/// Else, markdown syntax is not escaped, only HTML tags and entities.
206std::string renderText(llvm::StringRef Input, bool StartsLine,
207 bool EscapeMarkdown) {
208 std::string R;
209 R.reserve(Input.size());
210
211 // split the input into lines, and escape each line separately.
212 llvm::StringRef Line, Rest;
213
214 bool IsFirstLine = true;
215
216 for (std::tie(Line, Rest) = Input.split('\n');
217 !(Line.empty() && Rest.empty());
218 std::tie(Line, Rest) = Rest.split('\n')) {
219
220 bool StartsLineIntern = IsFirstLine ? StartsLine : true;
221
222 // Ignore leading spaces for the escape logic, but preserve them in the
223 // output.
224 StringRef LeadingSpaces = Line.take_while(llvm::isSpace);
225 if (!LeadingSpaces.empty()) {
226 R.append(LeadingSpaces);
227 }
228
229 for (unsigned I = LeadingSpaces.size(); I < Line.size(); ++I) {
230 if (needsLeadingEscape(Line[I], Line.substr(LeadingSpaces.size(), I),
231 Line.substr(I + 1), StartsLineIntern,
232 EscapeMarkdown))
233 R.push_back('\\');
234 R.push_back(Line[I]);
235 }
236
237 IsFirstLine = false;
238 if (!Rest.empty())
239 R.push_back('\n');
240 }
241
242 return R;
243}
244
245/// Renders \p Input as an inline block of code in markdown. The returned value
246/// is surrounded by backticks and the inner contents are properly escaped.
247std::string renderInlineBlock(llvm::StringRef Input) {
248 std::string R;
249 R.reserve(Input.size());
250 // Double all backticks to make sure we don't close the inline block early.
251 for (size_t From = 0; From < Input.size();) {
252 size_t Next = Input.find("`", From);
253 R += Input.substr(From, Next - From);
254 if (Next == llvm::StringRef::npos)
255 break;
256 R += "``"; // double the found backtick.
257
258 From = Next + 1;
259 }
260 // If results starts with a backtick, add spaces on both sides. The spaces
261 // are ignored by markdown renderers.
262 if (llvm::StringRef(R).starts_with("`") || llvm::StringRef(R).ends_with("`"))
263 return "` " + std::move(R) + " `";
264 // Markdown render should ignore first and last space if both are there. We
265 // add an extra pair of spaces in that case to make sure we render what the
266 // user intended.
267 if (llvm::StringRef(R).starts_with(" ") && llvm::StringRef(R).ends_with(" "))
268 return "` " + std::move(R) + " `";
269 return "`" + std::move(R) + "`";
270}
271
272/// Get marker required for \p Input to represent a markdown codeblock. It
273/// consists of at least 3 backticks(`). Although markdown also allows to use
274/// tilde(~) for code blocks, they are never used.
275std::string getMarkerForCodeBlock(llvm::StringRef Input) {
276 // Count the maximum number of consecutive backticks in \p Input. We need to
277 // start and end the code block with more.
278 unsigned MaxBackticks = 0;
279 unsigned Backticks = 0;
280 for (char C : Input) {
281 if (C == '`') {
282 ++Backticks;
283 continue;
284 }
287 }
289 // Use the corresponding number of backticks to start and end a code block.
290 return std::string(/*Repeat=*/std::max(3u, MaxBackticks + 1), '`');
291}
292
293// Trims the input and concatenates whitespace blocks into a single ` `.
294std::string canonicalizeSpaces(llvm::StringRef Input) {
295 llvm::SmallVector<llvm::StringRef> Words;
296 llvm::SplitString(Input, Words);
297 return llvm::join(Words, " ");
298}
299
300std::string renderBlocks(llvm::ArrayRef<std::unique_ptr<Block>> Children,
301 void (Block::*RenderFunc)(llvm::raw_ostream &) const) {
302 std::string R;
303 llvm::raw_string_ostream OS(R);
304
305 // Trim rulers.
306 Children = Children.drop_while(
307 [](const std::unique_ptr<Block> &C) { return C->isRuler(); });
308 auto Last = llvm::find_if(
309 llvm::reverse(Children),
310 [](const std::unique_ptr<Block> &C) { return !C->isRuler(); });
311 Children = Children.drop_back(Children.end() - Last.base());
312
313 bool LastBlockWasRuler = true;
314 for (const auto &C : Children) {
315 if (C->isRuler() && LastBlockWasRuler)
316 continue;
317 LastBlockWasRuler = C->isRuler();
318 ((*C).*RenderFunc)(OS);
319 }
320
321 // Get rid of redundant empty lines introduced in plaintext while imitating
322 // padding in markdown.
323 std::string AdjustedResult;
324 llvm::StringRef TrimmedText(OS.str());
325 TrimmedText = TrimmedText.trim();
326
327 llvm::copy_if(TrimmedText, std::back_inserter(AdjustedResult),
328 [&TrimmedText](const char &C) {
329 return !llvm::StringRef(TrimmedText.data(),
330 &C - TrimmedText.data() + 1)
331 // We allow at most two newlines.
332 .ends_with("\n\n\n");
333 });
334
335 return AdjustedResult;
336}
337
338// Separates two blocks with extra spacing. Note that it might render strangely
339// in vscode if the trailing block is a codeblock, see
340// https://github.com/microsoft/vscode/issues/88416 for details.
341class Ruler : public Block {
342public:
343 void renderEscapedMarkdown(llvm::raw_ostream &OS) const override {
344 renderMarkdown(OS);
345 }
346 void renderMarkdown(llvm::raw_ostream &OS) const override {
347 // Note that we need an extra new line before the ruler, otherwise we might
348 // make previous block a title instead of introducing a ruler.
349 OS << "\n---\n";
350 }
351 void renderPlainText(llvm::raw_ostream &OS) const override { OS << '\n'; }
352 std::unique_ptr<Block> clone() const override {
353 return std::make_unique<Ruler>(*this);
354 }
355 bool isRuler() const override { return true; }
356};
357
358class CodeBlock : public Block {
359public:
360 void renderEscapedMarkdown(llvm::raw_ostream &OS) const override {
361 renderMarkdown(OS);
362 }
363 void renderMarkdown(llvm::raw_ostream &OS) const override {
364 std::string Marker = getMarkerForCodeBlock(Contents);
365 // No need to pad from previous blocks, as they should end with a new line.
366 OS << Marker << Language << '\n' << Contents;
367 if (!Contents.empty() && Contents.back() != '\n')
368 OS << '\n';
369 // Always end with an empty line to separate code blocks from following
370 // paragraphs.
371 OS << Marker << "\n\n";
372 }
373
374 void renderPlainText(llvm::raw_ostream &OS) const override {
375 // In plaintext we want one empty line before and after codeblocks.
376 OS << '\n' << Contents << "\n\n";
377 }
378
379 std::unique_ptr<Block> clone() const override {
380 return std::make_unique<CodeBlock>(*this);
381 }
382
383 CodeBlock(std::string Contents, std::string Language)
384 : Contents(std::move(Contents)), Language(std::move(Language)) {}
385
386private:
387 std::string Contents;
388 std::string Language;
389};
390
391// Inserts two spaces after each `\n` to indent each line. First line is not
392// indented.
393std::string indentLines(llvm::StringRef Input) {
394 assert(!Input.ends_with("\n") && "Input should've been trimmed.");
395 std::string IndentedR;
396 // We'll add 2 spaces after each new line which is not followed by another new
397 // line.
398 IndentedR.reserve(Input.size() + Input.count('\n') * 2);
399 for (size_t I = 0; I < Input.size(); ++I) {
400 char C = Input[I];
401 IndentedR += C;
402 if (C == '\n' && (((I + 1) < Input.size()) && (Input[I + 1] != '\n')))
403 IndentedR.append(" ");
404 }
405 return IndentedR;
406}
407
408class Heading : public Paragraph {
409public:
410 Heading(size_t Level) : Level(Level) {}
411
412 void renderEscapedMarkdown(llvm::raw_ostream &OS) const override {
413 insertHeadingMarkers(OS);
414 Paragraph::renderEscapedMarkdown(OS);
415 }
416
417 void renderMarkdown(llvm::raw_ostream &OS) const override {
418 insertHeadingMarkers(OS);
419 Paragraph::renderMarkdown(OS);
420 }
421
422private:
423 size_t Level;
424
425 void insertHeadingMarkers(llvm::raw_ostream &OS) const {
426 OS << std::string(Level, '#') << ' ';
427 }
428};
429
430} // namespace
431
432std::string Block::asEscapedMarkdown() const {
433 std::string R;
434 llvm::raw_string_ostream OS(R);
436 return llvm::StringRef(OS.str()).trim().str();
437}
438
439std::string Block::asMarkdown() const {
440 std::string R;
441 llvm::raw_string_ostream OS(R);
442 renderMarkdown(OS);
443 return llvm::StringRef(OS.str()).trim().str();
444}
445
446std::string Block::asPlainText() const {
447 std::string R;
448 llvm::raw_string_ostream OS(R);
449 renderPlainText(OS);
450 return llvm::StringRef(OS.str()).trim().str();
451}
452
453void Paragraph::renderEscapedMarkdown(llvm::raw_ostream &OS) const {
454 bool NeedsSpace = false;
455 bool HasChunks = false;
456 for (auto &C : Chunks) {
457 if (C.SpaceBefore || NeedsSpace)
458 OS << " ";
459 switch (C.Kind) {
460 case ChunkKind::PlainText:
461 OS << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/true);
462 break;
463 case ChunkKind::InlineCode:
464 OS << renderInlineBlock(C.Contents);
465 break;
466 case ChunkKind::Bold:
467 OS << renderText("**" + C.Contents + "**", !HasChunks,
468 /*EscapeMarkdown=*/true);
469 break;
470 case ChunkKind::Emphasized:
471 OS << renderText("*" + C.Contents + "*", !HasChunks,
472 /*EscapeMarkdown=*/true);
473 break;
474 }
475 HasChunks = true;
476 NeedsSpace = C.SpaceAfter;
477 }
478 // A paragraph in markdown is separated by a blank line.
479 OS << "\n\n";
480}
481
482void Paragraph::renderMarkdown(llvm::raw_ostream &OS) const {
483 bool NeedsSpace = false;
484 bool HasChunks = false;
485 for (auto &C : Chunks) {
486 if (C.SpaceBefore || NeedsSpace)
487 OS << " ";
488 switch (C.Kind) {
489 case ChunkKind::PlainText:
490 OS << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false);
491 break;
492 case ChunkKind::InlineCode:
493 OS << renderInlineBlock(C.Contents);
494 break;
495 case ChunkKind::Bold:
496 OS << "**" << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false)
497 << "**";
498 break;
499 case ChunkKind::Emphasized:
500 OS << "*" << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false)
501 << "*";
502 break;
503 }
504 HasChunks = true;
505 NeedsSpace = C.SpaceAfter;
506 }
507 // A paragraph in markdown is separated by a blank line.
508 OS << "\n\n";
509}
510
511std::unique_ptr<Block> Paragraph::clone() const {
512 return std::make_unique<Paragraph>(*this);
513}
514
515/// Choose a marker to delimit `Text` from a prioritized list of options.
516/// This is more readable than escaping for plain-text.
517llvm::StringRef Paragraph::chooseMarker(llvm::ArrayRef<llvm::StringRef> Options,
518 llvm::StringRef Text) const {
519 // Prefer a delimiter whose characters don't appear in the text.
520 for (llvm::StringRef S : Options)
521 if (Text.find_first_of(S) == llvm::StringRef::npos)
522 return S;
523 return Options.front();
524}
525
526bool Paragraph::punctuationIndicatesLineBreak(llvm::StringRef Line) const {
527 constexpr llvm::StringLiteral Punctuation = R"txt(.:,;!?)txt";
528
529 Line = Line.rtrim();
530 return !Line.empty() && Punctuation.contains(Line.back());
531}
532
533bool Paragraph::isHardLineBreakIndicator(llvm::StringRef Rest) const {
534 // '-'/'*' md list, '@'/'\' documentation command, '>' md blockquote,
535 // '#' headings, '`' code blocks, two spaces (markdown force newline)
536 constexpr llvm::StringLiteral LinebreakIndicators = R"txt(-*@>#`)txt";
537
538 Rest = Rest.ltrim(" \t");
539 if (Rest.empty())
540 return false;
541
542 if (LinebreakIndicators.contains(Rest.front()))
543 return true;
544
545 if (llvm::isDigit(Rest.front())) {
546 llvm::StringRef AfterDigit = Rest.drop_while(llvm::isDigit);
547 if (AfterDigit.starts_with(".") || AfterDigit.starts_with(")"))
548 return true;
549 }
550 return false;
551}
552
553bool Paragraph::isHardLineBreakAfter(llvm::StringRef Line,
554 llvm::StringRef Rest) const {
555 // In Markdown, 2 spaces before a line break forces a line break.
556 // Add a line break for plaintext in this case too.
557 // Should we also consider whether Line is short?
558 return Line.ends_with(" ") || punctuationIndicatesLineBreak(Line) ||
559 isHardLineBreakIndicator(Rest);
560}
561
562void Paragraph::renderPlainText(llvm::raw_ostream &OS) const {
563 bool NeedsSpace = false;
564 std::string ConcatenatedText;
565 ConcatenatedText.reserve(EstimatedStringSize);
566
567 llvm::raw_string_ostream ConcatenatedOS(ConcatenatedText);
568
569 for (auto &C : Chunks) {
570
571 if (C.Kind == ChunkKind::PlainText) {
572 if (C.SpaceBefore || NeedsSpace)
573 ConcatenatedOS << ' ';
574
575 ConcatenatedOS << C.Contents;
576 NeedsSpace = llvm::isSpace(C.Contents.back()) || C.SpaceAfter;
577 continue;
578 }
579
580 if (C.SpaceBefore || NeedsSpace)
581 ConcatenatedOS << ' ';
582 llvm::StringRef Marker = "";
583 if (C.Preserve && C.Kind == ChunkKind::InlineCode)
584 Marker = chooseMarker({"`", "'", "\""}, C.Contents);
585 else if (C.Kind == ChunkKind::Bold)
586 Marker = "**";
587 else if (C.Kind == ChunkKind::Emphasized)
588 Marker = "*";
589 ConcatenatedOS << Marker << C.Contents << Marker;
590 NeedsSpace = C.SpaceAfter;
591 }
592
593 // We go through the contents line by line to handle the newlines
594 // and required spacing correctly.
595 //
596 // Newlines are added if:
597 // - the line ends with 2 spaces and a newline follows
598 // - the line ends with punctuation that indicates a line break (.:,;!?)
599 // - the next line starts with a hard line break indicator (-@>#`, or a digit
600 // followed by '.' or ')'), ignoring leading whitespace.
601 //
602 // Otherwise, newlines in the input are replaced with a single space.
603 //
604 // Multiple spaces are collapsed into a single space.
605 //
606 // Lines containing only whitespace are ignored.
607 llvm::StringRef Line, Rest;
608
609 for (std::tie(Line, Rest) =
610 llvm::StringRef(ConcatenatedText).trim().split('\n');
611 !(Line.empty() && Rest.empty());
612 std::tie(Line, Rest) = Rest.split('\n')) {
613
614 // Remove lines which only contain whitespace.
615 //
616 // Note: this also handles the case when there are multiple newlines
617 // in a row, since all leading newlines are removed.
618 //
619 // The documentation parsing treats multiple newlines as paragraph
620 // separators, hence it will create a new Paragraph instead of adding
621 // multiple newlines to the same Paragraph.
622 // Therfore multiple newlines are never added to a paragraph
623 // except if the user explicitly adds them using
624 // e.g. appendText("user text\n\nnext text").
625 Line = Line.ltrim();
626 if (Line.empty())
627 continue;
628
629 OS << canonicalizeSpaces(Line);
630
631 if (isHardLineBreakAfter(Line, Rest))
632 OS << '\n';
633 else if (!Rest.empty())
634 // Since we removed any trailing whitespace from the input using trim(),
635 // we know that the next line contains non-whitespace characters.
636 // Therefore, we can add a space without worrying about trailing spaces.
637 OS << ' ';
638 }
639
640 // Paragraphs are separated by a blank line.
641 OS << "\n\n";
642}
643
644BulletList::BulletList() = default;
645BulletList::~BulletList() = default;
646
647void BulletList::renderEscapedMarkdown(llvm::raw_ostream &OS) const {
648 for (auto &D : Items) {
649 std::string M = D.asEscapedMarkdown();
650 // Instead of doing this we might prefer passing Indent to children to get
651 // rid of the copies, if it turns out to be a bottleneck.
652 OS << "- " << indentLines(M) << '\n';
653 }
654 // We add 2 newlines after list to terminate it in markdown.
655 OS << "\n\n";
656}
657
658void BulletList::renderMarkdown(llvm::raw_ostream &OS) const {
659 for (auto &D : Items) {
660 std::string M = D.asMarkdown();
661 // Instead of doing this we might prefer passing Indent to children to get
662 // rid of the copies, if it turns out to be a bottleneck.
663 OS << "- " << indentLines(M) << '\n';
664 }
665 // We add 2 newlines after list to terminate it in markdown.
666 OS << "\n\n";
667}
668
669void BulletList::renderPlainText(llvm::raw_ostream &OS) const {
670 for (auto &D : Items) {
671 // Instead of doing this we might prefer passing Indent to children to get
672 // rid of the copies, if it turns out to be a bottleneck.
673 OS << "- " << indentLines(D.asPlainText()) << '\n';
674 }
675 OS << '\n';
676}
677
678Paragraph &Paragraph::appendSpace() {
679 if (!Chunks.empty())
680 Chunks.back().SpaceAfter = true;
681 return *this;
682}
683
684Paragraph &Paragraph::appendChunk(llvm::StringRef Contents, ChunkKind K) {
685 if (Contents.empty())
686 return *this;
687 Chunks.emplace_back();
688 Chunk &C = Chunks.back();
689 C.Contents = Contents;
690 C.Kind = K;
691
692 EstimatedStringSize += Contents.size();
693 return *this;
694}
695
697 if (!Chunks.empty() && Chunks.back().Kind == ChunkKind::PlainText) {
698 Chunks.back().Contents += std::move(Text);
699 return *this;
700 }
701
702 return appendChunk(std::move(Text), ChunkKind::PlainText);
703}
704
706 return appendChunk(canonicalizeSpaces(std::move(Text)),
707 ChunkKind::Emphasized);
708}
709
711 return appendChunk(canonicalizeSpaces(std::move(Text)), ChunkKind::Bold);
712}
713
714Paragraph &Paragraph::appendCode(llvm::StringRef Code, bool Preserve) {
715 bool AdjacentCode =
716 !Chunks.empty() && Chunks.back().Kind == ChunkKind::InlineCode;
717 std::string Norm = canonicalizeSpaces(std::move(Code));
718 if (Norm.empty())
719 return *this;
720 EstimatedStringSize += Norm.size();
721 Chunks.emplace_back();
722 Chunk &C = Chunks.back();
723 C.Contents = std::move(Norm);
724 C.Kind = ChunkKind::InlineCode;
725 C.Preserve = Preserve;
726 // Disallow adjacent code spans without spaces, markdown can't render them.
727 C.SpaceBefore = AdjacentCode;
728
729 return *this;
730}
731
732std::unique_ptr<Block> BulletList::clone() const {
733 return std::make_unique<BulletList>(*this);
734}
735
736class Document &BulletList::addItem() {
737 Items.emplace_back();
738 return Items.back();
739}
740
741Document &Document::operator=(const Document &Other) {
742 Children.clear();
743 for (const auto &C : Other.Children)
744 Children.push_back(C->clone());
745 return *this;
746}
747
748void Document::append(Document Other) {
749 std::move(Other.Children.begin(), Other.Children.end(),
750 std::back_inserter(Children));
751}
752
753Paragraph &Document::addParagraph() {
754 Children.push_back(std::make_unique<Paragraph>());
755 return *static_cast<Paragraph *>(Children.back().get());
756}
757
758void Document::addRuler() { Children.push_back(std::make_unique<Ruler>()); }
759
760void Document::addCodeBlock(std::string Code, std::string Language) {
761 Children.emplace_back(
762 std::make_unique<CodeBlock>(std::move(Code), std::move(Language)));
763}
764
765std::string Document::asEscapedMarkdown() const {
767}
768
769std::string Document::asMarkdown() const {
771}
772
773std::string Document::asPlainText() const {
775}
776
777BulletList &Document::addBulletList() {
778 Children.emplace_back(std::make_unique<BulletList>());
779 return *static_cast<BulletList *>(Children.back().get());
780}
781
782Paragraph &Document::addHeading(size_t Level) {
783 assert(Level > 0);
784 Children.emplace_back(std::make_unique<Heading>(Level));
785 return *static_cast<Paragraph *>(Children.back().get());
786}
787} // namespace markup
788} // namespace clangd
789} // namespace clang
void renderMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:363
CodeBlock(std::string Contents, std::string Language)
Definition Markup.cpp:383
void renderPlainText(llvm::raw_ostream &OS) const override
Definition Markup.cpp:374
void renderEscapedMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:360
std::unique_ptr< Block > clone() const override
Definition Markup.cpp:379
void renderEscapedMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:412
Heading(size_t Level)
Definition Markup.cpp:410
void renderMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:417
void renderEscapedMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:343
void renderPlainText(llvm::raw_ostream &OS) const override
Definition Markup.cpp:351
std::unique_ptr< Block > clone() const override
Definition Markup.cpp:352
bool isRuler() const override
Definition Markup.cpp:355
void renderMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:346
std::string asEscapedMarkdown() const
Definition Markup.cpp:432
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:439
std::string asPlainText() const
Definition Markup.cpp:446
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:562
Paragraph & appendEmphasizedText(llvm::StringRef Text)
Append emphasized text, this translates to the * block in markdown.
Definition Markup.cpp:705
Paragraph & appendText(llvm::StringRef Text)
Append plain text to the end of the string.
Definition Markup.cpp:696
void renderEscapedMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:453
std::unique_ptr< Block > clone() const override
Definition Markup.cpp:511
void renderMarkdown(llvm::raw_ostream &OS) const override
Definition Markup.cpp:482
Paragraph & appendBoldText(llvm::StringRef Text)
Append bold text, this translates to the ** block in markdown.
Definition Markup.cpp:710
FIXME: Skip testing on windows temporarily due to the different escaping code mode.
Definition AST.cpp:45
===– 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:300
std::string indentLines(llvm::StringRef Input)
Definition Markup.cpp:393
std::string canonicalizeSpaces(llvm::StringRef Input)
Definition Markup.cpp:294