1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "tools/gn/standard_out.h"
6 
7 #include <stddef.h>
8 
9 #include <vector>
10 
11 #include "base/command_line.h"
12 #include "base/logging.h"
13 #include "base/strings/string_piece.h"
14 #include "base/strings/string_split.h"
15 #include "base/strings/string_util.h"
16 #include "tools/gn/switches.h"
17 #include "util/build_config.h"
18 
19 #if defined(OS_WIN)
20 #include <windows.h>
21 #else
22 #include <stdio.h>
23 #include <unistd.h>
24 #endif
25 
26 namespace {
27 
28 bool initialized = false;
29 
30 #if defined(OS_WIN)
31 HANDLE hstdout;
32 WORD default_attributes;
33 #endif
34 bool is_console = false;
35 
36 bool is_markdown = false;
37 
38 // True while output is going into a markdown ```...``` code block.
39 bool in_body = false;
40 
EnsureInitialized()41 void EnsureInitialized() {
42   if (initialized)
43     return;
44   initialized = true;
45 
46   const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess();
47   if (cmdline->HasSwitch(switches::kMarkdown)) {
48     // Output help in Markdown's syntax, not color-highlighted.
49     is_markdown = true;
50   }
51 
52   if (cmdline->HasSwitch(switches::kNoColor)) {
53     // Force color off.
54     is_console = false;
55     return;
56   }
57 
58 #if defined(OS_WIN)
59   // On Windows, we can't force the color on. If the output handle isn't a
60   // console, there's nothing we can do about it.
61   hstdout = ::GetStdHandle(STD_OUTPUT_HANDLE);
62   CONSOLE_SCREEN_BUFFER_INFO info;
63   is_console = !!::GetConsoleScreenBufferInfo(hstdout, &info);
64   default_attributes = info.wAttributes;
65 #else
66   if (cmdline->HasSwitch(switches::kColor))
67     is_console = true;
68   else
69     is_console = isatty(fileno(stdout));
70 #endif
71 }
72 
73 #if !defined(OS_WIN)
WriteToStdOut(const std::string & output)74 void WriteToStdOut(const std::string& output) {
75   size_t written_bytes = fwrite(output.data(), 1, output.size(), stdout);
76   DCHECK_EQ(output.size(), written_bytes);
77 }
78 #endif  // !defined(OS_WIN)
79 
OutputMarkdownDec(TextDecoration dec)80 void OutputMarkdownDec(TextDecoration dec) {
81 // The markdown rendering turns "dim" text to italics and any
82 // other colored text to bold.
83 
84 #if defined(OS_WIN)
85   DWORD written = 0;
86   if (dec == DECORATION_DIM)
87     ::WriteFile(hstdout, "*", 1, &written, nullptr);
88   else if (dec != DECORATION_NONE)
89     ::WriteFile(hstdout, "**", 2, &written, nullptr);
90 #else
91   if (dec == DECORATION_DIM)
92     WriteToStdOut("*");
93   else if (dec != DECORATION_NONE)
94     WriteToStdOut("**");
95 #endif
96 }
97 
98 }  // namespace
99 
100 #if defined(OS_WIN)
101 
OutputString(const std::string & output,TextDecoration dec,HtmlEscaping escaping)102 void OutputString(const std::string& output,
103                   TextDecoration dec,
104                   HtmlEscaping escaping) {
105   EnsureInitialized();
106   DWORD written = 0;
107 
108   if (is_markdown) {
109     OutputMarkdownDec(dec);
110   } else if (is_console) {
111     switch (dec) {
112       case DECORATION_NONE:
113         break;
114       case DECORATION_DIM:
115         ::SetConsoleTextAttribute(hstdout, FOREGROUND_INTENSITY);
116         break;
117       case DECORATION_RED:
118         ::SetConsoleTextAttribute(hstdout,
119                                   FOREGROUND_RED | FOREGROUND_INTENSITY);
120         break;
121       case DECORATION_GREEN:
122         // Keep green non-bold.
123         ::SetConsoleTextAttribute(hstdout, FOREGROUND_GREEN);
124         break;
125       case DECORATION_BLUE:
126         ::SetConsoleTextAttribute(hstdout,
127                                   FOREGROUND_BLUE | FOREGROUND_INTENSITY);
128         break;
129       case DECORATION_YELLOW:
130         ::SetConsoleTextAttribute(hstdout, FOREGROUND_RED | FOREGROUND_GREEN);
131         break;
132     }
133   }
134 
135   std::string tmpstr = output;
136   if (is_markdown && dec == DECORATION_YELLOW) {
137     // https://code.google.com/p/gitiles/issues/detail?id=77
138     // Gitiles will replace "--" with an em dash in non-code text.
139     // Figuring out all instances of this might be difficult, but we can
140     // at least escape the instances where this shows up in a heading.
141     base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "--", "\\--");
142   }
143   if (is_markdown && !in_body && escaping == DEFAULT_ESCAPING) {
144     // Markdown auto-escapes < and > in code sections (and converts &lt; to
145     // &amp;tl; there), but not elsewhere.
146     base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "<", "&lt;");
147     base::ReplaceSubstringsAfterOffset(&tmpstr, 0, ">", "&gt;");
148   }
149   ::WriteFile(hstdout, tmpstr.c_str(), static_cast<DWORD>(tmpstr.size()),
150               &written, nullptr);
151 
152   if (is_markdown) {
153     OutputMarkdownDec(dec);
154   } else if (is_console) {
155     ::SetConsoleTextAttribute(hstdout, default_attributes);
156   }
157 }
158 
159 #else
160 
OutputString(const std::string & output,TextDecoration dec,HtmlEscaping escaping)161 void OutputString(const std::string& output,
162                   TextDecoration dec,
163                   HtmlEscaping escaping) {
164   EnsureInitialized();
165   if (is_markdown) {
166     OutputMarkdownDec(dec);
167   } else if (is_console) {
168     switch (dec) {
169       case DECORATION_NONE:
170         break;
171       case DECORATION_DIM:
172         WriteToStdOut("\e[2m");
173         break;
174       case DECORATION_RED:
175         WriteToStdOut("\e[31m\e[1m");
176         break;
177       case DECORATION_GREEN:
178         WriteToStdOut("\e[32m");
179         break;
180       case DECORATION_BLUE:
181         WriteToStdOut("\e[34m\e[1m");
182         break;
183       case DECORATION_YELLOW:
184         WriteToStdOut("\e[33m");
185         break;
186     }
187   }
188 
189   std::string tmpstr = output;
190   if (is_markdown && dec == DECORATION_YELLOW) {
191     // https://code.google.com/p/gitiles/issues/detail?id=77
192     // Gitiles will replace "--" with an em dash in non-code text.
193     // Figuring out all instances of this might be difficult, but we can
194     // at least escape the instances where this shows up in a heading.
195     base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "--", "\\--");
196   }
197   if (is_markdown && !in_body && escaping == DEFAULT_ESCAPING) {
198     // Markdown auto-escapes < and > in code sections (and converts &lt; to
199     // &amp;tl; there), but not elsewhere.
200     base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "<", "&lt;");
201     base::ReplaceSubstringsAfterOffset(&tmpstr, 0, ">", "&gt;");
202   }
203   WriteToStdOut(tmpstr.data());
204 
205   if (is_markdown) {
206     OutputMarkdownDec(dec);
207   } else if (is_console && dec != DECORATION_NONE) {
208     WriteToStdOut("\e[0m");
209   }
210 }
211 
212 #endif
213 
PrintSectionHelp(const std::string & line,const std::string & topic,const std::string & tag)214 void PrintSectionHelp(const std::string& line,
215                       const std::string& topic,
216                       const std::string& tag) {
217   EnsureInitialized();
218 
219   if (is_markdown) {
220     OutputString("*   [" + line + "](#" + tag + ")\n");
221   } else if (topic.size()) {
222     OutputString("\n" + line + " (type \"gn help " + topic +
223                  "\" for more help):\n");
224   } else {
225     OutputString("\n" + line + ":\n");
226   }
227 }
228 
PrintShortHelp(const std::string & line,const std::string & link_tag)229 void PrintShortHelp(const std::string& line, const std::string& link_tag) {
230   EnsureInitialized();
231 
232   if (is_markdown) {
233     if (link_tag.empty())
234       OutputString("    *   " + line + "\n");
235     else
236       OutputString("    *   [" + line + "](#" + link_tag + ")\n");
237     return;
238   }
239 
240   size_t colon_offset = line.find(':');
241   size_t first_normal = 0;
242   if (colon_offset != std::string::npos) {
243     OutputString("  " + line.substr(0, colon_offset), DECORATION_YELLOW);
244     first_normal = colon_offset;
245   }
246 
247   // See if the colon is followed by a " [" and if so, dim the contents of [ ].
248   if (first_normal > 0 && line.size() > first_normal + 2 &&
249       line[first_normal + 1] == ' ' && line[first_normal + 2] == '[') {
250     size_t begin_bracket = first_normal + 2;
251     OutputString(": ");
252     first_normal = line.find(']', begin_bracket);
253     if (first_normal == std::string::npos)
254       first_normal = line.size();
255     else
256       first_normal++;
257     OutputString(line.substr(begin_bracket, first_normal - begin_bracket),
258                  DECORATION_DIM);
259   }
260 
261   OutputString(line.substr(first_normal) + "\n");
262 }
263 
PrintLongHelp(const std::string & text,const std::string & tag)264 void PrintLongHelp(const std::string& text, const std::string& tag) {
265   EnsureInitialized();
266 
267   bool first_header = true;
268   in_body = false;
269   std::size_t empty_lines = 0;
270   for (const std::string& line : base::SplitString(
271            text, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL)) {
272     // Check for a heading line.
273     if (!line.empty() && line[0] != ' ') {
274       // New paragraph, just skip any trailing empty lines.
275       empty_lines = 0;
276 
277       if (is_markdown) {
278         // GN's block-level formatting is converted to markdown as follows:
279         // * The first heading is treated as an H3.
280         // * Subsequent heading are treated as H4s.
281         // * Any other text is wrapped in a code block and displayed as-is.
282         //
283         // Span-level formatting (the decorations) is converted inside
284         // OutputString().
285         if (in_body) {
286           OutputString("```\n\n", DECORATION_NONE);
287           in_body = false;
288         }
289 
290         if (first_header && !tag.empty()) {
291           OutputString("### <a name=\"" + tag + "\"></a>", DECORATION_NONE,
292                        NO_ESCAPING);
293           first_header = false;
294         } else {
295           OutputString("#### ", DECORATION_NONE);
296         }
297       }
298 
299       // Highlight up to the colon (if any).
300       size_t chars_to_highlight = line.find(':');
301       if (chars_to_highlight == std::string::npos)
302         chars_to_highlight = line.size();
303 
304       OutputString(line.substr(0, chars_to_highlight), DECORATION_YELLOW);
305       OutputString(line.substr(chars_to_highlight));
306       OutputString("\n");
307       continue;
308     } else if (is_markdown && !line.empty() && !in_body) {
309       OutputString("```\n", DECORATION_NONE);
310       in_body = true;
311     }
312 
313     // We buffer empty lines, so we can skip them if needed
314     // (i.e. new paragraph body, end of final paragraph body).
315     if (in_body && is_markdown) {
316       if (!line.empty() && empty_lines != 0) {
317         OutputString(std::string(empty_lines, '\n'));
318         empty_lines = 0;
319       } else if (line.empty()) {
320         ++empty_lines;
321         continue;
322       }
323     }
324 
325     // Check for a comment.
326     TextDecoration dec = DECORATION_NONE;
327     for (const auto& elem : line) {
328       if (elem == '#' && !is_markdown) {
329         // Got a comment, draw dimmed.
330         dec = DECORATION_DIM;
331         break;
332       } else if (elem != ' ') {
333         break;
334       }
335     }
336 
337     OutputString(line + "\n", dec);
338   }
339 
340   if (is_markdown && in_body)
341     OutputString("```\n");
342 }
343