diff options
Diffstat (limited to 'src/help_text_formatter.cc')
-rw-r--r-- | src/help_text_formatter.cc | 691 |
1 files changed, 691 insertions, 0 deletions
diff --git a/src/help_text_formatter.cc b/src/help_text_formatter.cc new file mode 100644 index 0000000..e0b92a0 --- /dev/null +++ b/src/help_text_formatter.cc @@ -0,0 +1,691 @@ +/** + * Copyright (c) 2017, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <algorithm> +#include <regex> + +#include "help_text_formatter.hh" + +#include "base/ansi_scrubber.hh" +#include "base/attr_line.builder.hh" +#include "base/string_util.hh" +#include "config.h" +#include "fmt/format.h" +#include "fmt/printf.h" +#include "readline_highlighters.hh" + +using namespace lnav::roles::literals; + +std::multimap<std::string, help_text*> help_text::TAGGED; + +static std::vector<help_text*> +get_related(const help_text& ht) +{ + std::vector<help_text*> retval; + + for (const auto& tag : ht.ht_tags) { + auto tagged = help_text::TAGGED.equal_range(tag); + + for (auto tag_iter = tagged.first; tag_iter != tagged.second; + ++tag_iter) { + if (tag_iter->second == &ht) { + continue; + } + + help_text& related = *tag_iter->second; + + if (!related.ht_opposites.empty() + && find_if(related.ht_opposites.begin(), + related.ht_opposites.end(), + [&ht](const char* x) { + return strcmp(x, ht.ht_name) == 0; + }) + == related.ht_opposites.end()) + { + continue; + } + + retval.push_back(&related); + } + } + + return retval; +} + +void +format_help_text_for_term(const help_text& ht, + size_t width, + attr_line_t& out, + help_text_content htc) +{ + static const size_t body_indent = 2; + + attr_line_builder alb(out); + text_wrap_settings tws; + size_t start_index = out.get_string().length(); + + tws.with_width(width); + + switch (ht.ht_context) { + case help_context_t::HC_COMMAND: { + auto line_start = out.al_string.length(); + + out.append(":").append(lnav::roles::symbol(ht.ht_name)); + for (const auto& param : ht.ht_parameters) { + out.append(" "); + if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) { + out.append("["); + } + out.append(lnav::roles::variable(param.ht_name)); + if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) { + out.append("]"); + } + if (param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE) { + out.append("1"_variable); + out.append(" ["); + out.append("..."_variable); + out.append(" "); + out.append(lnav::roles::variable(param.ht_name)); + out.append("N"_variable); + out.append("]"); + } + } + out.with_attr(string_attr{ + line_range{(int) line_start, (int) out.get_string().length()}, + VC_ROLE.value(role_t::VCR_H3), + }); + if (htc != help_text_content::synopsis) { + alb.append("\n") + .append(lnav::roles::table_border( + repeat("\u2550", tws.tws_width))) + .append("\n") + .indent(body_indent) + .append(attr_line_t::from_ansi_str(ht.ht_summary), + &tws.with_indent(body_indent)) + .append("\n"); + } + break; + } + case help_context_t::HC_SQL_FUNCTION: + case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION: { + auto line_start = out.al_string.length(); + bool break_all = false; + bool needs_comma = false; + + out.append(lnav::roles::symbol(ht.ht_name)).append("("); + for (const auto& param : ht.ht_parameters) { + if (!param.ht_flag_name && needs_comma) { + out.append(", "); + } + if (break_all + || (int) (out.get_string().length() - line_start + 10) + >= tws.tws_width) + { + out.append("\n"); + line_start = out.get_string().length(); + alb.indent(body_indent + strlen(ht.ht_name) + 1); + break_all = true; + } + if (param.ht_flag_name) { + out.append(" ") + .append(lnav::roles::symbol(param.ht_flag_name)) + .append(" "); + } + if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) { + out.append("["); + } + out.append(lnav::roles::variable(param.ht_name)); + if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) { + out.append("]"); + } + if (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE + || param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE) + { + out.append(", ..."); + } + needs_comma = true; + } + out.append(")"); + out.with_attr(string_attr{ + line_range{(int) line_start, (int) out.get_string().length()}, + VC_ROLE.value(role_t::VCR_H3), + }); + if (htc != help_text_content::synopsis) { + if (break_all) { + alb.append("\n") + .append(lnav::roles::table_border( + repeat("\u2550", tws.tws_width))) + .append("\n") + .indent(body_indent + strlen(ht.ht_name) + 1); + } else { + alb.append("\n") + .append(lnav::roles::table_border( + repeat("\u2550", tws.tws_width))) + .append("\n") + .indent(body_indent); + } + out.append(attr_line_t::from_ansi_str(ht.ht_summary), + &tws.with_indent(body_indent)) + .append("\n"); + } + break; + } + case help_context_t::HC_SQL_COMMAND: { + auto line_start = out.al_string.length(); + + out.append(";").append(lnav::roles::symbol(ht.ht_name)); + for (const auto& param : ht.ht_parameters) { + out.append(" "); + if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) { + out.append("["); + } + out.append(lnav::roles::variable(param.ht_name)); + if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) { + out.append("]"); + } + if (param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE) { + out.append("1"_variable); + out.append(" ["); + out.append("..."_variable); + out.append(" "); + out.append(lnav::roles::variable(param.ht_name)); + out.append("N"_variable); + out.append("]"); + } + } + out.with_attr(string_attr{ + line_range{(int) line_start, (int) out.get_string().length()}, + VC_ROLE.value(role_t::VCR_H3), + }); + if (htc != help_text_content::synopsis) { + alb.append("\n") + .append(lnav::roles::table_border( + repeat("\u2550", tws.tws_width))) + .append("\n") + .indent(body_indent) + .append(attr_line_t::from_ansi_str(ht.ht_summary), + &tws.with_indent(body_indent + 2)) + .append("\n"); + } + break; + } + case help_context_t::HC_SQL_INFIX: + case help_context_t::HC_SQL_KEYWORD: { + size_t line_start = out.get_string().length(); + bool break_all = false; + bool is_infix = ht.ht_context == help_context_t::HC_SQL_INFIX; + + if (is_infix) { + out.append(ht.ht_name); + } else { + out.append(lnav::roles::keyword(ht.ht_name)); + } + for (const auto& param : ht.ht_parameters) { + if (break_all + || (int) (out.get_string().length() - start_index + - line_start + 10) + >= tws.tws_width) + { + out.append("\n"); + line_start = out.get_string().length(); + alb.indent(body_indent + strlen(ht.ht_name) + 1); + break_all = true; + } + if (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE + || param.ht_nargs == help_nargs_t::HN_OPTIONAL) + { + if (!break_all) { + out.append(" "); + } + out.append("["); + } + if (param.ht_flag_name) { + out.ensure_space().append( + lnav::roles::keyword(param.ht_flag_name)); + } + if (param.ht_group_start) { + out.ensure_space().append( + lnav::roles::keyword(param.ht_group_start)); + } + if (param.ht_name[0]) { + out.ensure_space().append( + lnav::roles::variable(param.ht_name)); + if (!param.ht_parameters.empty()) { + if (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE + || param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE) + { + out.append("1"_variable); + } + if (param.ht_parameters[0].ht_flag_name) { + out.append(" ") + .append(lnav::roles::keyword( + param.ht_parameters[0].ht_flag_name)) + .append(" "); + } + out.append(lnav::roles::variable( + param.ht_parameters[0].ht_name)); + } + } + if (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE + || param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE) + { + bool needs_comma = param.ht_parameters.empty() + || !param.ht_flag_name; + + out.append("1"_variable) + .append(" [") + .append(needs_comma ? ", " : "") + .append("...") + .append(needs_comma ? "" : " ") + .append(lnav::roles::keyword( + (needs_comma || !param.ht_flag_name) + ? "" + : param.ht_flag_name)) + .append(" ") + .append(lnav::roles::variable(param.ht_name)) + .append("N"_variable); + if (!param.ht_parameters.empty()) { + if (param.ht_parameters[0].ht_flag_name) { + out.append(" ") + .append(lnav::roles::keyword( + param.ht_parameters[0].ht_flag_name)) + .append(" "); + } + + out.append(lnav::roles::variable( + param.ht_parameters[0].ht_name)) + .append("N"_variable); + } + out.append("]"); + } + if (param.ht_group_end) { + out.ensure_space().append( + lnav::roles::keyword(param.ht_group_end)); + } + if (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE + || param.ht_nargs == help_nargs_t::HN_OPTIONAL) + { + out.append("]"); + } + } + out.with_attr(string_attr{ + line_range{(int) line_start, (int) out.get_string().length()}, + VC_ROLE.value(role_t::VCR_H3), + }); + if (htc != help_text_content::synopsis) { + alb.append("\n") + .append(lnav::roles::table_border( + repeat("\u2550", tws.tws_width))) + .append("\n") + .indent(body_indent) + .append(ht.ht_summary, &tws) + .append("\n"); + } + break; + } + default: + break; + } + + if (htc == help_text_content::full && !ht.ht_parameters.empty()) { + size_t max_param_name_width = 0; + + for (const auto& param : ht.ht_parameters) { + max_param_name_width + = std::max(strlen(param.ht_name), max_param_name_width); + } + + out.append(ht.ht_parameters.size() == 1 ? "Parameter"_h4 + : "Parameters"_h4) + .append("\n"); + + for (const auto& param : ht.ht_parameters) { + if (!param.ht_summary) { + continue; + } + + alb.indent(body_indent) + .append(lnav::roles::variable(param.ht_name)) + .append(max_param_name_width - strlen(param.ht_name), ' ') + .append(" ") + .append(attr_line_t::from_ansi_str(param.ht_summary), + &(tws.with_indent(2 + max_param_name_width + 3))) + .append("\n"); + } + } + if (htc == help_text_content::full && !ht.ht_results.empty()) { + size_t max_result_name_width = 0; + + for (const auto& result : ht.ht_results) { + max_result_name_width + = std::max(strlen(result.ht_name), max_result_name_width); + } + + out.append(ht.ht_results.size() == 1 ? "Result"_h4 : "Results"_h4) + .append("\n"); + + for (const auto& result : ht.ht_results) { + if (!result.ht_summary) { + continue; + } + + alb.indent(body_indent) + .append(lnav::roles::variable(result.ht_name)) + .append(max_result_name_width - strlen(result.ht_name), ' ') + .append(" ") + .append(attr_line_t::from_ansi_str(result.ht_summary), + &(tws.with_indent(2 + max_result_name_width + 3))) + .append("\n"); + } + } + if (htc == help_text_content::full && !ht.ht_tags.empty()) { + auto related_help = get_related(ht); + auto related_refs = std::vector<std::string>(); + + for (const auto* related : related_help) { + std::string name = related->ht_name; + switch (related->ht_context) { + case help_context_t::HC_COMMAND: + name = ":" + name; + break; + case help_context_t::HC_SQL_FUNCTION: + case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION: + name = name + "()"; + break; + default: + break; + } + related_refs.push_back(name); + } + stable_sort(related_refs.begin(), related_refs.end()); + + alb.append("See Also"_h4).append("\n").indent(body_indent); + + bool first = true; + size_t line_start = out.get_string().length(); + for (const auto& ref : related_refs) { + if (!first) { + out.append(", "); + } + if ((out.get_string().length() - line_start + ref.length()) > width) + { + alb.append("\n").indent(body_indent); + line_start = out.get_string().length(); + } + out.append(lnav::roles::symbol(ref)); + first = false; + } + } +} + +void +format_example_text_for_term(const help_text& ht, + const help_example_to_attr_line_fun_t eval, + size_t width, + attr_line_t& out) +{ + if (ht.ht_example.empty()) { + return; + } + + attr_line_builder alb(out); + int count = 1; + + out.append(ht.ht_example.size() == 1 ? "Example"_h4 : "Examples"_h4) + .append("\n"); + for (const auto& ex : ht.ht_example) { + attr_line_t ex_line(ex.he_cmd); + const char* prompt = ""; + text_wrap_settings tws; + + tws.with_width(width); + if (count > 1) { + out.append("\n"); + } + switch (ht.ht_context) { + case help_context_t::HC_COMMAND: + ex_line.insert(0, 1, ' '); + ex_line.insert(0, 1, ':'); + ex_line.insert(1, ht.ht_name); + readline_command_highlighter(ex_line, 0); + break; + case help_context_t::HC_SQL_INFIX: + case help_context_t::HC_SQL_KEYWORD: + case help_context_t::HC_SQL_FUNCTION: + case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION: + readline_sqlite_highlighter(ex_line, 0); + prompt = ";"; + break; + default: + break; + } + + ex_line.pad_to(50).with_attr_for_all( + VC_ROLE.value(role_t::VCR_QUOTED_CODE)); + alb.append("#") + .append(fmt::to_string(count)) + .append(" ") + .append(ex.he_description, &tws.with_indent(3)) + .append(":\n") + .indent(3) + .append(prompt, VC_ROLE.value(role_t::VCR_QUOTED_CODE)) + .append(ex_line, &tws.with_indent(3).with_padding_indent(3)) + .append("\n") + .indent(3) + .append(eval(ht, ex), &tws.with_indent(3)) + .append("\n"); + + count += 1; + } +} + +static std::string +link_name(const help_text& ht) +{ + const static std::regex SCRUBBER("[^\\w_]"); + + bool is_sql_infix = ht.ht_context == help_context_t::HC_SQL_INFIX; + std::string scrubbed_name; + + if (is_sql_infix) { + scrubbed_name = "infix"; + } else { + scrubbed_name = ht.ht_name; + } + if (ht.ht_function_type == help_function_type_t::HFT_AGGREGATE) { + scrubbed_name += "_agg"; + } + for (const auto& param : ht.ht_parameters) { + if (!is_sql_infix && param.ht_name[0]) { + continue; + } + if (!param.ht_flag_name) { + continue; + } + + scrubbed_name += "_"; + scrubbed_name += param.ht_flag_name; + } + scrubbed_name = std::regex_replace(scrubbed_name, SCRUBBER, "_"); + + return tolower(scrubbed_name); +} + +void +format_help_text_for_rst(const help_text& ht, + const help_example_to_attr_line_fun_t eval, + FILE* rst_file) +{ + const char* prefix; + int out_count = 0; + + if (!ht.ht_name || !ht.ht_name[0]) { + return; + } + + bool is_sql_func = false, is_sql = false; + switch (ht.ht_context) { + case help_context_t::HC_COMMAND: + prefix = ":"; + break; + case help_context_t::HC_SQL_COMMAND: + prefix = ";"; + break; + case help_context_t::HC_SQL_FUNCTION: + case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION: + is_sql = is_sql_func = true; + prefix = ""; + break; + case help_context_t::HC_SQL_INFIX: + case help_context_t::HC_SQL_KEYWORD: + is_sql = true; + prefix = ""; + break; + default: + prefix = ""; + break; + } + + fmt::print(rst_file, FMT_STRING("\n.. _{}:\n\n"), link_name(ht)); + out_count += fmt::fprintf(rst_file, "%s%s", prefix, ht.ht_name); + if (is_sql_func) { + out_count += fmt::fprintf(rst_file, "("); + } + bool needs_comma = false; + for (const auto& param : ht.ht_parameters) { + if (needs_comma) { + if (param.ht_flag_name) { + out_count += fmt::fprintf(rst_file, " "); + } else { + out_count += fmt::fprintf(rst_file, ", "); + } + } + if (!is_sql_func) { + out_count += fmt::fprintf(rst_file, " "); + } + + if (param.ht_flag_name) { + out_count += fmt::fprintf(rst_file, "%s ", param.ht_flag_name); + } + if (param.ht_name[0]) { + out_count += fmt::fprintf(rst_file, "*"); + if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) { + out_count += fmt::fprintf(rst_file, "\\["); + } + out_count += fmt::fprintf(rst_file, "%s", param.ht_name); + if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) { + out_count += fmt::fprintf(rst_file, "\\]"); + } + out_count += fmt::fprintf(rst_file, "*"); + } + if (is_sql_func) { + needs_comma = true; + } + } + if (is_sql_func) { + out_count += fmt::fprintf(rst_file, ")"); + } + fmt::fprintf(rst_file, "\n"); + fmt::print(rst_file, FMT_STRING("{0:^^{1}}\n\n"), "", out_count); + + fmt::fprintf(rst_file, " %s\n", ht.ht_summary); + fmt::fprintf(rst_file, "\n"); + if (ht.ht_description != nullptr) { + fmt::fprintf(rst_file, " %s\n", ht.ht_description); + } + + int param_count = 0; + for (const auto& param : ht.ht_parameters) { + if (param.ht_summary && param.ht_summary[0]) { + param_count += 1; + } + } + + if (param_count > 0) { + fmt::fprintf(rst_file, " **Parameters**\n"); + for (const auto& param : ht.ht_parameters) { + if (param.ht_summary && param.ht_summary[0]) { + fmt::fprintf( + rst_file, + " * **%s%s** --- %s\n", + param.ht_name, + param.ht_nargs == help_nargs_t::HN_REQUIRED ? "\\*" : "", + param.ht_summary); + } + } + fmt::fprintf(rst_file, "\n"); + } + if (is_sql) { + prefix = ";"; + } + if (!ht.ht_example.empty()) { + fmt::fprintf(rst_file, " **Examples**\n"); + for (const auto& example : ht.ht_example) { + fmt::fprintf(rst_file, " %s:\n\n", example.he_description); + fmt::fprintf(rst_file, + " .. code-block:: %s\n\n", + is_sql ? "custsqlite" : "lnav"); + if (ht.ht_context == help_context_t::HC_COMMAND) { + fmt::fprintf(rst_file, + " %s%s %s\n", + prefix, + ht.ht_name, + example.he_cmd); + } else { + fmt::fprintf(rst_file, " %s%s\n", prefix, example.he_cmd); + } + auto result = eval(ht, example); + if (!result.empty()) { + std::vector<attr_line_t> lines; + + result.split_lines(lines); + for (const auto& line : lines) { + fmt::fprintf(rst_file, " %s\n", line.get_string()); + } + } + fmt::fprintf(rst_file, "\n"); + } + } + + if (!ht.ht_tags.empty()) { + auto related_refs = std::vector<std::string>(); + + for (const auto* related : get_related(ht)) { + related_refs.emplace_back( + fmt::format(FMT_STRING(":ref:`{}`"), link_name(*related))); + } + stable_sort(related_refs.begin(), related_refs.end()); + + fmt::print(rst_file, + FMT_STRING(" **See Also**\n {}\n"), + fmt::join(related_refs, ", ")); + } + + fmt::fprintf(rst_file, "\n----\n\n"); +} |