/** * Copyright (c) 2022, 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 "md2attr_line.hh" #include "base/attr_line.builder.hh" #include "base/itertools.enumerate.hh" #include "base/itertools.hh" #include "base/lnav_log.hh" #include "base/map_util.hh" #include "document.sections.hh" #include "pcrepp/pcre2pp.hh" #include "pugixml/pugixml.hpp" #include "readline_highlighters.hh" #include "text_format.hh" #include "textfile_highlighters.hh" #include "view_curses.hh" using namespace lnav::roles::literals; using namespace md4cpp::literals; static const std::map CODE_NAME_TO_TEXT_FORMAT = { {"c"_frag, text_format_t::TF_C_LIKE}, {"c++"_frag, text_format_t::TF_C_LIKE}, {"java"_frag, text_format_t::TF_JAVA}, {"python"_frag, text_format_t::TF_PYTHON}, {"rust"_frag, text_format_t::TF_RUST}, {"toml"_frag, text_format_t::TF_TOML}, {"yaml"_frag, text_format_t::TF_YAML}, {"xml"_frag, text_format_t::TF_XML}, }; static highlight_map_t get_highlight_map() { highlight_map_t retval; setup_highlights(retval); return retval; } void md2attr_line::flush_footnotes() { if (this->ml_footnotes.empty()) { return; } auto& block_text = this->ml_blocks.back(); auto longest_foot = this->ml_footnotes | lnav::itertools::map(&attr_line_t::column_width) | lnav::itertools::max(0); block_text.append("\n"); for (const auto& [index, foot] : lnav::itertools::enumerate(this->ml_footnotes, 1)) { auto footline = attr_line_t(" ") .append("\u258c"_footnote_border) .append(lnav::roles::footnote_text( index < 10 && this->ml_footnotes.size() >= 10 ? " " : "")) .append(lnav::roles::footnote_text( fmt::format(FMT_STRING("[{}] - "), index))) .append(foot.pad_to(longest_foot)) .with_attr_for_all(SA_PREFORMATTED.value()); block_text.append(footline).append("\n"); } this->ml_footnotes.clear(); } Result md2attr_line::enter_block(const md4cpp::event_handler::block& bl) { if (this->ml_list_stack.empty() && (bl.is() || bl.is() || bl.is())) { this->flush_footnotes(); } this->ml_blocks.resize(this->ml_blocks.size() + 1); if (bl.is()) { auto* ol_detail = bl.get(); this->ml_list_stack.emplace_back(*ol_detail); } else if (bl.is()) { this->ml_list_stack.emplace_back(bl.get()); } else if (bl.is()) { this->ml_tables.resize(this->ml_tables.size() + 1); } else if (bl.is()) { this->ml_tables.back().t_rows.resize( this->ml_tables.back().t_rows.size() + 1); } else if (bl.is()) { this->ml_code_depth += 1; } return Ok(); } Result md2attr_line::leave_block(const md4cpp::event_handler::block& bl) { auto block_text = std::move(this->ml_blocks.back()); this->ml_blocks.pop_back(); auto& last_block = this->ml_blocks.back(); if (!endswith(block_text.get_string(), "\n")) { block_text.append("\n"); } if (bl.is()) { auto* hbl = bl.get(); auto role = role_t::VCR_TEXT; switch (hbl->level) { case 1: role = role_t::VCR_H1; break; case 2: role = role_t::VCR_H2; break; case 3: role = role_t::VCR_H3; break; case 4: role = role_t::VCR_H4; break; case 5: role = role_t::VCR_H5; break; case 6: role = role_t::VCR_H6; break; } block_text.rtrim().with_attr_for_all(VC_ROLE.value(role)); last_block.append("\n").append(block_text).append("\n"); } else if (bl.is()) { block_text = attr_line_t() .append(lnav::roles::hr(repeat("\u2501", 70))) .with_attr_for_all(SA_PREFORMATTED.value()); last_block.append("\n").append(block_text).append("\n"); } else if (bl.is() || bl.is()) { this->ml_list_stack.pop_back(); if (last_block.empty()) { last_block.append("\n"); } else { if (!endswith(last_block.get_string(), "\n")) { last_block.append("\n"); } if (this->ml_list_stack.empty() && !endswith(last_block.get_string(), "\n\n")) { last_block.append("\n"); } } last_block.append(block_text); } else if (bl.is()) { auto last_list_block = this->ml_list_stack.back(); text_wrap_settings tws = {0, 60}; attr_line_builder alb(last_block); { auto prefix = alb.with_attr(SA_PREFORMATTED.value()); alb.append(" ") .append(last_list_block.match( [this, &tws](const MD_BLOCK_UL_DETAIL*) { tws.tws_indent = 3; return this->ml_list_stack.size() % 2 == 1 ? "\u2022"_list_glyph : "\u2014"_list_glyph; }, [this, &tws](MD_BLOCK_OL_DETAIL ol_detail) { auto retval = lnav::roles::list_glyph( fmt::format(FMT_STRING("{}{}"), ol_detail.start, ol_detail.mark_delimiter)); tws.tws_indent = retval.first.length() + 2; this->ml_list_stack.pop_back(); ol_detail.start += 1; this->ml_list_stack.emplace_back(ol_detail); return retval; })) .append(" "); } alb.append(block_text, &tws); } else if (bl.is()) { auto* code_detail = bl.get(); this->ml_code_depth -= 1; auto lang_sf = string_fragment::from_bytes(code_detail->lang.text, code_detail->lang.size); auto tf_opt = lnav::map::find(CODE_NAME_TO_TEXT_FORMAT, lang_sf); if (tf_opt) { static const auto highlighters = get_highlight_map(); lnav::document::discover_structure( block_text, line_range{0, -1}, tf_opt.value()); for (const auto& hl_pair : highlighters) { const auto& hl = hl_pair.second; if (!hl.h_text_formats.empty() && hl.h_text_formats.count(tf_opt.value()) == 0) { continue; } hl.annotate(block_text, 0); } } else if (lang_sf == "lnav") { readline_lnav_highlighter(block_text, block_text.length()); } else if (lang_sf == "sql" || lang_sf == "sqlite" || lang_sf == "prql") { readline_sqlite_highlighter(block_text, block_text.length()); } else if (lang_sf == "shell" || lang_sf == "bash") { readline_shlex_highlighter(block_text, block_text.length()); } else if (lang_sf == "console" || lang_sf.iequal( string_fragment::from_const("shellsession"))) { static const auto SH_PROMPT = lnav::pcre2pp::code::from_const(R"([^\$>#%]*[\$>#%]\s+)"); attr_line_t new_block_text; attr_line_t cmd_block; int prompt_size = 0; for (auto line : block_text.split_lines()) { if (!cmd_block.empty() && endswith(cmd_block.get_string(), "\\\n")) { cmd_block.append(line).append("\n"); continue; } if (!cmd_block.empty()) { readline_shlex_highlighter_int( cmd_block, cmd_block.length(), line_range{prompt_size, (int) cmd_block.length()}); new_block_text.append(cmd_block); cmd_block.clear(); } auto sh_find_res = SH_PROMPT.find_in(line.get_string()).ignore_error(); if (sh_find_res) { prompt_size = sh_find_res->f_all.length(); line.with_attr(string_attr{ line_range{0, prompt_size}, VC_ROLE.value(role_t::VCR_LIST_GLYPH), }); cmd_block.append(line).append("\n"); } else { line.with_attr_for_all(VC_ROLE.value(role_t::VCR_COMMENT)); new_block_text.append(line).append("\n"); } } block_text = new_block_text; } auto code_lines = block_text.rtrim().split_lines(); auto max_width = code_lines | lnav::itertools::map(&attr_line_t::column_width) | lnav::itertools::max(0); attr_line_t padded_text; for (auto& line : code_lines) { line.pad_to(std::max(max_width + 4, size_t{40})) .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE)); padded_text.append(lnav::string::attrs::preformatted(" ")) .append("\u258c"_code_border) .append(line) .append("\n"); } if (!padded_text.empty()) { padded_text.with_attr_for_all(SA_PREFORMATTED.value()); last_block.append("\n").append(padded_text); } } else if (bl.is()) { const static auto ALERT_TYPE = lnav::pcre2pp::code::from_const( R"(^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\])"); text_wrap_settings tws = {0, 60}; attr_line_t wrapped_text; auto md = ALERT_TYPE.create_match_data(); std::optional border_role; block_text.rtrim(); if (ALERT_TYPE.capture_from(block_text.al_string) .into(md) .matches() .ignore_error()) { attr_line_t replacement; if (md[1] == "NOTE") { replacement.append("\u24d8 Note\n"_footnote_border); border_role = role_t::VCR_FOOTNOTE_BORDER; } else if (md[1] == "TIP") { replacement.append(":bulb:"_emoji) .append(" Tip\n") .with_attr_for_all(VC_ROLE.value(role_t::VCR_OK)); border_role = role_t::VCR_OK; } else if (md[1] == "IMPORTANT") { replacement.append(":star2:"_emoji) .append(" Important\n") .with_attr_for_all(VC_ROLE.value(role_t::VCR_INFO)); border_role = role_t::VCR_INFO; } else if (md[1] == "WARNING") { replacement.append(":warning:"_emoji) .append(" Warning\n") .with_attr_for_all(VC_ROLE.value(role_t::VCR_WARNING)); border_role = role_t::VCR_WARNING; } else if (md[1] == "CAUTION") { replacement.append(":small_red_triangle:"_emoji) .append(" Caution\n") .with_attr_for_all(VC_ROLE.value(role_t::VCR_ERROR)); border_role = role_t::VCR_ERROR; } else { ensure(0); } block_text.erase(md[0]->sf_begin, md[0]->length()); block_text.insert(0, replacement); } wrapped_text.append(block_text, &tws); auto quoted_lines = wrapped_text.split_lines(); auto max_width = quoted_lines | lnav::itertools::map(&attr_line_t::column_width) | lnav::itertools::max(0); attr_line_t padded_text; for (auto& line : quoted_lines) { line.pad_to(max_width + 1) .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_TEXT)); padded_text.append(" "); auto start_index = padded_text.length(); padded_text.append("\u258c"_quote_border); if (border_role) { padded_text.with_attr(string_attr{ line_range{ (int) start_index, (int) padded_text.length(), }, VC_ROLE_FG.value(border_role.value()), }); } padded_text.append(line).append("\n"); } if (!padded_text.empty()) { padded_text.with_attr_for_all(SA_PREFORMATTED.value()); last_block.append("\n").append(padded_text); } } else if (bl.is()) { auto* table_detail = bl.get(); auto tab = std::move(this->ml_tables.back()); this->ml_tables.pop_back(); std::vector max_col_sizes; block_text.clear(); block_text.append("\n"); max_col_sizes.resize(table_detail->col_count); for (size_t lpc = 0; lpc < table_detail->col_count; lpc++) { if (lpc >= tab.t_headers.size()) { continue; } max_col_sizes[lpc] = tab.t_headers[lpc].column_width(); tab.t_headers[lpc].with_attr_for_all( VC_ROLE.value(role_t::VCR_TABLE_HEADER)); } for (const auto& row : tab.t_rows) { for (size_t lpc = 0; lpc < table_detail->col_count; lpc++) { if (lpc >= row.r_columns.size()) { continue; } auto col_len = row.r_columns[lpc].column_width(); if (col_len > max_col_sizes[lpc]) { max_col_sizes[lpc] = col_len; } } } auto col_sizes = max_col_sizes | lnav::itertools::map([](const auto& elem) { return std::min(elem, ssize_t{50}); }); auto full_width = col_sizes | lnav::itertools::sum(); text_wrap_settings tws = {0, 50}; std::vector cells; size_t max_cell_lines = 0; for (size_t lpc = 0; lpc < tab.t_headers.size(); lpc++) { tws.with_width(col_sizes[lpc]); attr_line_t td_block; td_block.append(tab.t_headers[lpc], &tws); cells.emplace_back(td_block.rtrim().split_lines()); if (cells.back().cl_lines.size() > max_cell_lines) { max_cell_lines = cells.back().cl_lines.size(); } } for (size_t line_index = 0; line_index < max_cell_lines; line_index++) { for (const auto& [col, cell] : lnav::itertools::enumerate(cells)) { block_text.append(" "); if (line_index < cell.cl_lines.size()) { block_text.append(cell.cl_lines[line_index]); block_text.append( col_sizes[col] - cell.cl_lines[line_index].column_width(), ' '); } else { block_text.append(col_sizes[col], ' '); } } block_text.append("\n") .append(lnav::roles::table_border( repeat("\u2550", full_width + col_sizes.size()))) .append("\n"); } for (const auto& row : tab.t_rows) { cells.clear(); max_cell_lines = 0; for (size_t lpc = 0; lpc < row.r_columns.size(); lpc++) { tws.with_width(col_sizes[lpc]); attr_line_t td_block; td_block.append(row.r_columns[lpc], &tws); cells.emplace_back(td_block.rtrim().split_lines()); if (cells.back().cl_lines.size() > max_cell_lines) { max_cell_lines = cells.back().cl_lines.size(); } } for (size_t line_index = 0; line_index < max_cell_lines; line_index++) { size_t col = 0; for (const auto& cell : cells) { block_text.append(" "); if (line_index < cell.cl_lines.size()) { block_text.append(cell.cl_lines[line_index]); if (col < col_sizes.size() - 1) { block_text.append( col_sizes[col] - cell.cl_lines[line_index].column_width(), ' '); } } else if (col < col_sizes.size() - 1) { block_text.append(col_sizes[col], ' '); } col += 1; } block_text.append("\n"); } } if (!block_text.empty()) { block_text.with_attr_for_all(SA_PREFORMATTED.value()); last_block.append(block_text); } } else if (bl.is()) { this->ml_tables.back().t_headers.push_back(block_text); } else if (bl.is()) { this->ml_tables.back().t_rows.back().r_columns.push_back(block_text); } else { if (bl.is()) { if (startswith(block_text.get_string(), "