/** * 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.hh" #include "base/lnav_log.hh" #include "pcrepp/pcrepp.hh" #include "pugixml/pugixml.hpp" #include "readline_highlighters.hh" #include "view_curses.hh" using namespace lnav::roles::literals; 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::utf8_length_or_length) | lnav::itertools::max(0); size_t index = 1; block_text.append("\n"); for (auto& foot : this->ml_footnotes) { block_text.append(lnav::string::attrs::preformatted(" ")) .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)) .append("\n"); index += 1; } 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); if (lang_sf == "lnav") { readline_lnav_highlighter(block_text, block_text.length()); } else if (lang_sf == "sql" || lang_sf == "sqlite") { 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 pcrepp SH_PROMPT(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(); } pcre_context_static<10> pc; pcre_input pi(line.get_string()); if (SH_PROMPT.match(pc, pi)) { prompt_size = pc.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::utf8_length_or_length) | lnav::itertools::max(0); attr_line_t padded_text; for (auto& line : code_lines) { line.pad_to(std::max(max_width + 4, ssize_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()) { text_wrap_settings tws = {0, 60}; attr_line_t wrapped_text; wrapped_text.append(block_text.rtrim(), &tws); auto quoted_lines = wrapped_text.split_lines(); auto max_width = quoted_lines | lnav::itertools::map(&attr_line_t::utf8_length_or_length) | 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(" ") .append("\u258c"_quote_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()) { 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()) { max_col_sizes[lpc] = tab.t_headers[lpc].utf8_length_or_length(); 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].utf8_length_or_length(); 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++) { 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]); block_text.append( col_sizes[col] - cell.cl_lines[line_index].utf8_length_or_length(), ' '); } else { block_text.append(col_sizes[col], ' '); } col += 1; } 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] .utf8_length_or_length(), ' '); } } 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(), "