| // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "gn/standard_out.h" |
| |
| #include <stddef.h> |
| |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/logging.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "gn/switches.h" |
| #include "util/build_config.h" |
| |
| #if defined(OS_WIN) |
| #include <windows.h> |
| #else |
| #include <stdio.h> |
| #include <unistd.h> |
| #endif |
| |
| namespace { |
| |
| bool initialized = false; |
| |
| #if defined(OS_WIN) |
| HANDLE hstdout; |
| WORD default_attributes; |
| #endif |
| bool is_console = false; |
| |
| bool is_markdown = false; |
| |
| // True while output is going into a markdown ```...``` code block. |
| bool in_body = false; |
| |
| void EnsureInitialized() { |
| if (initialized) |
| return; |
| initialized = true; |
| |
| const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess(); |
| if (cmdline->HasSwitch(switches::kMarkdown)) { |
| // Output help in Markdown's syntax, not color-highlighted. |
| is_markdown = true; |
| } |
| |
| if (cmdline->HasSwitch(switches::kNoColor)) { |
| // Force color off. |
| is_console = false; |
| return; |
| } |
| |
| #if defined(OS_WIN) |
| // On Windows, we can't force the color on. If the output handle isn't a |
| // console, there's nothing we can do about it. |
| hstdout = ::GetStdHandle(STD_OUTPUT_HANDLE); |
| CONSOLE_SCREEN_BUFFER_INFO info; |
| is_console = !!::GetConsoleScreenBufferInfo(hstdout, &info); |
| default_attributes = info.wAttributes; |
| #else |
| if (cmdline->HasSwitch(switches::kColor)) |
| is_console = true; |
| else |
| is_console = isatty(fileno(stdout)); |
| #endif |
| } |
| |
| #if !defined(OS_WIN) |
| void WriteToStdOut(const std::string& output) { |
| size_t written_bytes = fwrite(output.data(), 1, output.size(), stdout); |
| DCHECK_EQ(output.size(), written_bytes); |
| } |
| #endif // !defined(OS_WIN) |
| |
| void OutputMarkdownDec(TextDecoration dec) { |
| // The markdown rendering turns "dim" text to italics and any |
| // other colored text to bold. |
| |
| #if defined(OS_WIN) |
| DWORD written = 0; |
| if (dec == DECORATION_DIM) |
| ::WriteFile(hstdout, "*", 1, &written, nullptr); |
| else if (dec != DECORATION_NONE) |
| ::WriteFile(hstdout, "**", 2, &written, nullptr); |
| #else |
| if (dec == DECORATION_DIM) |
| WriteToStdOut("*"); |
| else if (dec != DECORATION_NONE) |
| WriteToStdOut("**"); |
| #endif |
| } |
| |
| } // namespace |
| |
| #if defined(OS_WIN) |
| |
| void OutputString(const std::string& output, |
| TextDecoration dec, |
| HtmlEscaping escaping) { |
| EnsureInitialized(); |
| DWORD written = 0; |
| |
| if (is_markdown) { |
| OutputMarkdownDec(dec); |
| } else if (is_console) { |
| switch (dec) { |
| case DECORATION_NONE: |
| break; |
| case DECORATION_DIM: |
| ::SetConsoleTextAttribute(hstdout, FOREGROUND_INTENSITY); |
| break; |
| case DECORATION_RED: |
| ::SetConsoleTextAttribute(hstdout, |
| FOREGROUND_RED | FOREGROUND_INTENSITY); |
| break; |
| case DECORATION_GREEN: |
| // Keep green non-bold. |
| ::SetConsoleTextAttribute(hstdout, FOREGROUND_GREEN); |
| break; |
| case DECORATION_BLUE: |
| ::SetConsoleTextAttribute(hstdout, |
| FOREGROUND_BLUE | FOREGROUND_INTENSITY); |
| break; |
| case DECORATION_YELLOW: |
| ::SetConsoleTextAttribute(hstdout, FOREGROUND_RED | FOREGROUND_GREEN); |
| break; |
| } |
| } |
| |
| std::string tmpstr = output; |
| if (is_markdown && dec == DECORATION_YELLOW) { |
| // https://code.google.com/p/gitiles/issues/detail?id=77 |
| // Gitiles will replace "--" with an em dash in non-code text. |
| // Figuring out all instances of this might be difficult, but we can |
| // at least escape the instances where this shows up in a heading. |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "--", "\\--"); |
| } |
| if (is_markdown && !in_body && escaping == DEFAULT_ESCAPING) { |
| // Markdown auto-escapes < and > in code sections (and converts < to |
| // &tl; there), but not elsewhere. |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "<", "<"); |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, ">", ">"); |
| } |
| ::WriteFile(hstdout, tmpstr.c_str(), static_cast<DWORD>(tmpstr.size()), |
| &written, nullptr); |
| |
| if (is_markdown) { |
| OutputMarkdownDec(dec); |
| } else if (is_console) { |
| ::SetConsoleTextAttribute(hstdout, default_attributes); |
| } |
| } |
| |
| #else |
| |
| void OutputString(const std::string& output, |
| TextDecoration dec, |
| HtmlEscaping escaping) { |
| EnsureInitialized(); |
| if (is_markdown) { |
| OutputMarkdownDec(dec); |
| } else if (is_console) { |
| switch (dec) { |
| case DECORATION_NONE: |
| break; |
| case DECORATION_DIM: |
| WriteToStdOut("\e[2m"); |
| break; |
| case DECORATION_RED: |
| WriteToStdOut("\e[31m\e[1m"); |
| break; |
| case DECORATION_GREEN: |
| WriteToStdOut("\e[32m"); |
| break; |
| case DECORATION_BLUE: |
| WriteToStdOut("\e[34m\e[1m"); |
| break; |
| case DECORATION_YELLOW: |
| WriteToStdOut("\e[33m"); |
| break; |
| } |
| } |
| |
| std::string tmpstr = output; |
| if (is_markdown && dec == DECORATION_YELLOW) { |
| // https://code.google.com/p/gitiles/issues/detail?id=77 |
| // Gitiles will replace "--" with an em dash in non-code text. |
| // Figuring out all instances of this might be difficult, but we can |
| // at least escape the instances where this shows up in a heading. |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "--", "\\--"); |
| } |
| if (is_markdown && !in_body && escaping == DEFAULT_ESCAPING) { |
| // Markdown auto-escapes < and > in code sections (and converts < to |
| // &tl; there), but not elsewhere. |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, "<", "<"); |
| base::ReplaceSubstringsAfterOffset(&tmpstr, 0, ">", ">"); |
| } |
| WriteToStdOut(tmpstr.data()); |
| |
| if (is_markdown) { |
| OutputMarkdownDec(dec); |
| } else if (is_console && dec != DECORATION_NONE) { |
| WriteToStdOut("\e[0m"); |
| } |
| } |
| |
| #endif |
| |
| void PrintSectionHelp(const std::string& line, |
| const std::string& topic, |
| const std::string& tag) { |
| EnsureInitialized(); |
| |
| if (is_markdown) { |
| OutputString("* [" + line + "](#" + tag + ")\n"); |
| } else if (topic.size()) { |
| OutputString("\n" + line + " (type \"gn help " + topic + |
| "\" for more help):\n"); |
| } else { |
| OutputString("\n" + line + ":\n"); |
| } |
| } |
| |
| void PrintShortHelp(const std::string& line, const std::string& link_tag) { |
| EnsureInitialized(); |
| |
| if (is_markdown) { |
| if (link_tag.empty()) |
| OutputString(" * " + line + "\n"); |
| else |
| OutputString(" * [" + line + "](#" + link_tag + ")\n"); |
| return; |
| } |
| |
| size_t colon_offset = line.find(':'); |
| size_t first_normal = 0; |
| if (colon_offset != std::string::npos) { |
| OutputString(" " + line.substr(0, colon_offset), DECORATION_YELLOW); |
| first_normal = colon_offset; |
| } |
| |
| // See if the colon is followed by a " [" and if so, dim the contents of [ ]. |
| if (first_normal > 0 && line.size() > first_normal + 2 && |
| line[first_normal + 1] == ' ' && line[first_normal + 2] == '[') { |
| size_t begin_bracket = first_normal + 2; |
| OutputString(": "); |
| first_normal = line.find(']', begin_bracket); |
| if (first_normal == std::string::npos) |
| first_normal = line.size(); |
| else |
| first_normal++; |
| OutputString(line.substr(begin_bracket, first_normal - begin_bracket), |
| DECORATION_DIM); |
| } |
| |
| OutputString(line.substr(first_normal) + "\n"); |
| } |
| |
| void PrintLongHelp(const std::string& text, const std::string& tag) { |
| EnsureInitialized(); |
| |
| bool first_header = true; |
| in_body = false; |
| std::size_t empty_lines = 0; |
| for (const std::string& line : base::SplitString( |
| text, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL)) { |
| // Check for a heading line. |
| if (!line.empty() && line[0] != ' ') { |
| // New paragraph, just skip any trailing empty lines. |
| empty_lines = 0; |
| |
| if (is_markdown) { |
| // GN's block-level formatting is converted to markdown as follows: |
| // * The first heading is treated as an H3. |
| // * Subsequent heading are treated as H4s. |
| // * Any other text is wrapped in a code block and displayed as-is. |
| // |
| // Span-level formatting (the decorations) is converted inside |
| // OutputString(). |
| if (in_body) { |
| OutputString("```\n\n", DECORATION_NONE); |
| in_body = false; |
| } |
| |
| if (first_header && !tag.empty()) { |
| OutputString("### <a name=\"" + tag + "\"></a>", DECORATION_NONE, |
| NO_ESCAPING); |
| first_header = false; |
| } else { |
| OutputString("#### ", DECORATION_NONE); |
| } |
| } |
| |
| // Highlight up to the colon (if any). |
| size_t chars_to_highlight = line.find(':'); |
| if (chars_to_highlight == std::string::npos) |
| chars_to_highlight = line.size(); |
| |
| OutputString(line.substr(0, chars_to_highlight), DECORATION_YELLOW); |
| OutputString(line.substr(chars_to_highlight)); |
| OutputString("\n"); |
| continue; |
| } else if (is_markdown && !line.empty() && !in_body) { |
| OutputString("```\n", DECORATION_NONE); |
| in_body = true; |
| } |
| |
| // We buffer empty lines, so we can skip them if needed |
| // (i.e. new paragraph body, end of final paragraph body). |
| if (in_body && is_markdown) { |
| if (!line.empty() && empty_lines != 0) { |
| OutputString(std::string(empty_lines, '\n')); |
| empty_lines = 0; |
| } else if (line.empty()) { |
| ++empty_lines; |
| continue; |
| } |
| } |
| |
| // Check for a comment. |
| TextDecoration dec = DECORATION_NONE; |
| for (const auto& elem : line) { |
| if (elem == '#' && !is_markdown) { |
| // Got a comment, draw dimmed. |
| dec = DECORATION_DIM; |
| break; |
| } else if (elem != ' ') { |
| break; |
| } |
| } |
| |
| OutputString(line + "\n", dec); |
| } |
| |
| if (is_markdown && in_body) |
| OutputString("```\n"); |
| } |