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 "gn/standard_out.h"
6
7 #include <stddef.h>
8
9 #include <string_view>
10 #include <vector>
11
12 #include "base/command_line.h"
13 #include "base/logging.h"
14 #include "base/strings/string_split.h"
15 #include "base/strings/string_util.h"
16 #include "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 case DECORATION_MAGENTA:
133 ::SetConsoleTextAttribute(
134 hstdout, FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY);
135 break;
136 }
137 }
138
139 std::string tmpstr = output;
140 if (is_markdown && dec == DECORATION_YELLOW) {
141 // https://code.google.com/p/gitiles/issues/detail?id=77
142 // Gitiles will replace "--" with an em dash in non-code text.
143 // Figuring out all instances of this might be difficult, but we can
144 // at least escape the instances where this shows up in a heading.
145 base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "--", "\\--");
146 }
147 if (is_markdown && !in_body && escaping == DEFAULT_ESCAPING) {
148 // Markdown auto-escapes < and > in code sections (and converts < to
149 // &tl; there), but not elsewhere.
150 base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "<", "<");
151 base::ReplaceSubstringsAfterOffset(&tmpstr, 0, ">", ">");
152 }
153 ::WriteFile(hstdout, tmpstr.c_str(), static_cast<DWORD>(tmpstr.size()),
154 &written, nullptr);
155
156 if (is_markdown) {
157 OutputMarkdownDec(dec);
158 } else if (is_console) {
159 ::SetConsoleTextAttribute(hstdout, default_attributes);
160 }
161 }
162
163 #else
164
OutputString(const std::string & output,TextDecoration dec,HtmlEscaping escaping)165 void OutputString(const std::string& output,
166 TextDecoration dec,
167 HtmlEscaping escaping) {
168 EnsureInitialized();
169 if (is_markdown) {
170 OutputMarkdownDec(dec);
171 } else if (is_console) {
172 switch (dec) {
173 case DECORATION_NONE:
174 break;
175 case DECORATION_DIM:
176 WriteToStdOut("\e[2m");
177 break;
178 case DECORATION_RED:
179 WriteToStdOut("\e[31m\e[1m");
180 break;
181 case DECORATION_GREEN:
182 WriteToStdOut("\e[32m");
183 break;
184 case DECORATION_BLUE:
185 WriteToStdOut("\e[34m\e[1m");
186 break;
187 case DECORATION_YELLOW:
188 WriteToStdOut("\e[33m");
189 break;
190 case DECORATION_MAGENTA:
191 WriteToStdOut("\e[35m\e[1m");
192 break;
193 }
194 }
195
196 std::string tmpstr = output;
197 if (is_markdown && dec == DECORATION_YELLOW) {
198 // https://code.google.com/p/gitiles/issues/detail?id=77
199 // Gitiles will replace "--" with an em dash in non-code text.
200 // Figuring out all instances of this might be difficult, but we can
201 // at least escape the instances where this shows up in a heading.
202 base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "--", "\\--");
203 }
204 if (is_markdown && !in_body && escaping == DEFAULT_ESCAPING) {
205 // Markdown auto-escapes < and > in code sections (and converts < to
206 // &tl; there), but not elsewhere.
207 base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "<", "<");
208 base::ReplaceSubstringsAfterOffset(&tmpstr, 0, ">", ">");
209 }
210 WriteToStdOut(tmpstr.data());
211
212 if (is_markdown) {
213 OutputMarkdownDec(dec);
214 } else if (is_console && dec != DECORATION_NONE) {
215 WriteToStdOut("\e[0m");
216 }
217 }
218
219 #endif
220
PrintSectionHelp(const std::string & line,const std::string & topic,const std::string & tag)221 void PrintSectionHelp(const std::string& line,
222 const std::string& topic,
223 const std::string& tag) {
224 EnsureInitialized();
225
226 if (is_markdown) {
227 OutputString("* [" + line + "](#" + tag + ")\n");
228 } else if (topic.size()) {
229 OutputString("\n" + line + " (type \"gn help " + topic +
230 "\" for more help):\n");
231 } else {
232 OutputString("\n" + line + ":\n");
233 }
234 }
235
PrintShortHelp(const std::string & line,const std::string & link_tag)236 void PrintShortHelp(const std::string& line, const std::string& link_tag) {
237 EnsureInitialized();
238
239 if (is_markdown) {
240 if (link_tag.empty())
241 OutputString(" * " + line + "\n");
242 else
243 OutputString(" * [" + line + "](#" + link_tag + ")\n");
244 return;
245 }
246
247 size_t colon_offset = line.find(':');
248 size_t first_normal = 0;
249 if (colon_offset != std::string::npos) {
250 OutputString(" " + line.substr(0, colon_offset), DECORATION_YELLOW);
251 first_normal = colon_offset;
252 }
253
254 // See if the colon is followed by a " [" and if so, dim the contents of [ ].
255 if (first_normal > 0 && line.size() > first_normal + 2 &&
256 line[first_normal + 1] == ' ' && line[first_normal + 2] == '[') {
257 size_t begin_bracket = first_normal + 2;
258 OutputString(": ");
259 first_normal = line.find(']', begin_bracket);
260 if (first_normal == std::string::npos)
261 first_normal = line.size();
262 else
263 first_normal++;
264 OutputString(line.substr(begin_bracket, first_normal - begin_bracket),
265 DECORATION_DIM);
266 }
267
268 OutputString(line.substr(first_normal) + "\n");
269 }
270
PrintLongHelp(const std::string & text,const std::string & tag)271 void PrintLongHelp(const std::string& text, const std::string& tag) {
272 EnsureInitialized();
273
274 bool first_header = true;
275 in_body = false;
276 std::size_t empty_lines = 0;
277 for (const std::string& line : base::SplitString(
278 text, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL)) {
279 // Check for a heading line.
280 if (!line.empty() && line[0] != ' ') {
281 // New paragraph, just skip any trailing empty lines.
282 empty_lines = 0;
283
284 if (is_markdown) {
285 // GN's block-level formatting is converted to markdown as follows:
286 // * The first heading is treated as an H3.
287 // * Subsequent heading are treated as H4s.
288 // * Any other text is wrapped in a code block and displayed as-is.
289 //
290 // Span-level formatting (the decorations) is converted inside
291 // OutputString().
292 if (in_body) {
293 OutputString("```\n\n", DECORATION_NONE);
294 in_body = false;
295 }
296
297 if (first_header && !tag.empty()) {
298 OutputString("### <a name=\"" + tag + "\"></a>", DECORATION_NONE,
299 NO_ESCAPING);
300 first_header = false;
301 } else {
302 OutputString("#### ", DECORATION_NONE);
303 }
304 }
305
306 // Highlight up to the colon (if any).
307 size_t chars_to_highlight = line.find(':');
308 if (chars_to_highlight == std::string::npos)
309 chars_to_highlight = line.size();
310
311 OutputString(line.substr(0, chars_to_highlight), DECORATION_YELLOW);
312 OutputString(line.substr(chars_to_highlight));
313 OutputString("\n");
314 continue;
315 } else if (is_markdown && !line.empty() && !in_body) {
316 OutputString("```\n", DECORATION_NONE);
317 in_body = true;
318 }
319
320 // We buffer empty lines, so we can skip them if needed
321 // (i.e. new paragraph body, end of final paragraph body).
322 if (in_body && is_markdown) {
323 if (!line.empty() && empty_lines != 0) {
324 OutputString(std::string(empty_lines, '\n'));
325 empty_lines = 0;
326 } else if (line.empty()) {
327 ++empty_lines;
328 continue;
329 }
330 }
331
332 // Check for a comment.
333 TextDecoration dec = DECORATION_NONE;
334 for (const auto& elem : line) {
335 if (elem == '#' && !is_markdown) {
336 // Got a comment, draw dimmed.
337 dec = DECORATION_DIM;
338 break;
339 } else if (elem != ' ') {
340 break;
341 }
342 }
343
344 OutputString(line + "\n", dec);
345 }
346
347 if (is_markdown && in_body)
348 OutputString("```\n");
349 }
350