You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lnav/src/md2attr_line.cc

1089 lines
40 KiB
C++

/**
* 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<string_fragment, text_format_t> 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<void, std::string>
md2attr_line::enter_block(const md4cpp::event_handler::block& bl)
{
if (this->ml_list_stack.empty()
&& (bl.is<MD_BLOCK_H_DETAIL*>() || bl.is<block_hr>()
|| bl.is<block_p>()))
{
this->flush_footnotes();
}
this->ml_blocks.resize(this->ml_blocks.size() + 1);
if (bl.is<MD_BLOCK_OL_DETAIL*>()) {
auto* ol_detail = bl.get<MD_BLOCK_OL_DETAIL*>();
this->ml_list_stack.emplace_back(*ol_detail);
} else if (bl.is<MD_BLOCK_UL_DETAIL*>()) {
this->ml_list_stack.emplace_back(bl.get<MD_BLOCK_UL_DETAIL*>());
} else if (bl.is<MD_BLOCK_TABLE_DETAIL*>()) {
this->ml_tables.resize(this->ml_tables.size() + 1);
} else if (bl.is<block_tr>()) {
this->ml_tables.back().t_rows.resize(
this->ml_tables.back().t_rows.size() + 1);
} else if (bl.is<MD_BLOCK_CODE_DETAIL*>()) {
this->ml_code_depth += 1;
}
return Ok();
}
Result<void, std::string>
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<MD_BLOCK_H_DETAIL*>()) {
auto* hbl = bl.get<MD_BLOCK_H_DETAIL*>();
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_hr>()) {
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<MD_BLOCK_UL_DETAIL*>() || bl.is<MD_BLOCK_OL_DETAIL*>()) {
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<MD_BLOCK_LI_DETAIL*>()) {
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<MD_BLOCK_CODE_DETAIL*>()) {
auto* code_detail = bl.get<MD_BLOCK_CODE_DETAIL*>();
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<block_quote>()) {
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<role_t> 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<MD_BLOCK_TABLE_DETAIL*>()) {
auto* table_detail = bl.get<MD_BLOCK_TABLE_DETAIL*>();
auto tab = std::move(this->ml_tables.back());
this->ml_tables.pop_back();
std::vector<ssize_t> 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<cell_lines> 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<block_th>()) {
this->ml_tables.back().t_headers.push_back(block_text);
} else if (bl.is<MD_BLOCK_TD_DETAIL*>()) {
this->ml_tables.back().t_rows.back().r_columns.push_back(block_text);
} else {
if (bl.is<block_html>()) {
if (startswith(block_text.get_string(), "<!--")) {
return Ok();
}
}
text_wrap_settings tws = {0, this->ml_blocks.size() == 1 ? 70 : 10000};
if (!last_block.empty()) {
last_block.append("\n");
}
last_block.append(block_text, &tws);
}
if (bl.is<block_doc>()) {
this->flush_footnotes();
}
return Ok();
}
Result<void, std::string>
md2attr_line::enter_span(const md4cpp::event_handler::span& sp)
{
auto& last_block = this->ml_blocks.back();
this->ml_span_starts.push_back(last_block.length());
if (sp.is<span_code>()) {
last_block.append(" ");
this->ml_code_depth += 1;
} else if (sp.is<MD_SPAN_IMG_DETAIL*>()) {
last_block.append(":framed_picture:"_emoji).append(" ");
}
return Ok();
}
Result<void, std::string>
md2attr_line::leave_span(const md4cpp::event_handler::span& sp)
{
auto& last_block = this->ml_blocks.back();
if (sp.is<span_code>()) {
this->ml_code_depth -= 1;
last_block.append(" ");
line_range lr{
static_cast<int>(this->ml_span_starts.back()),
static_cast<int>(last_block.length()),
};
last_block.with_attr({
lr,
VC_ROLE.value(role_t::VCR_INLINE_CODE),
});
last_block.with_attr({
lr,
SA_PREFORMATTED.value(),
});
} else if (sp.is<span_em>()) {
line_range lr{
static_cast<int>(this->ml_span_starts.back()),
static_cast<int>(last_block.length()),
};
#if defined(A_ITALIC)
last_block.with_attr({
lr,
VC_STYLE.value(text_attrs{(int32_t) A_ITALIC}),
});
#endif
} else if (sp.is<span_strong>()) {
line_range lr{
static_cast<int>(this->ml_span_starts.back()),
static_cast<int>(last_block.length()),
};
last_block.with_attr({
lr,
VC_STYLE.value(text_attrs{A_BOLD}),
});
} else if (sp.is<span_u>()) {
line_range lr{
static_cast<int>(this->ml_span_starts.back()),
static_cast<int>(last_block.length()),
};
last_block.with_attr({
lr,
VC_STYLE.value(text_attrs{A_UNDERLINE}),
});
} else if (sp.is<MD_SPAN_A_DETAIL*>()) {
const auto* a_detail = sp.get<MD_SPAN_A_DETAIL*>();
auto href_str = std::string(a_detail->href.text, a_detail->href.size);
line_range lr{
static_cast<int>(this->ml_span_starts.back()),
static_cast<int>(last_block.length()),
};
last_block.with_attr({
lr,
VC_HYPERLINK.value(href_str),
});
this->append_url_footnote(href_str);
} else if (sp.is<MD_SPAN_IMG_DETAIL*>()) {
const auto* img_detail = sp.get<MD_SPAN_IMG_DETAIL*>();
const auto src_str
= std::string(img_detail->src.text, img_detail->src.size);
this->append_url_footnote(src_str);
}
this->ml_span_starts.pop_back();
return Ok();
}
enum class border_side {
left,
right,
};
enum class border_line_width {
thin,
medium,
thick,
};
static const char*
left_border_string(border_line_width width)
{
switch (width) {
case border_line_width::thin:
return "\u258F";
case border_line_width::medium:
return "\u258E";
case border_line_width::thick:
return "\u258C";
}
}
static const char*
right_border_string(border_line_width width)
{
switch (width) {
case border_line_width::thin:
return "\u2595";
case border_line_width::medium:
return "\u2595";
case border_line_width::thick:
return "\u2590";
}
}
static attr_line_t
span_style_border(border_side side, const string_fragment& value)
{
static const auto NAME_THIN = string_fragment::from_const("thin");
static const auto NAME_MEDIUM = string_fragment::from_const("medium");
static const auto NAME_THICK = string_fragment::from_const("thick");
static const auto NAME_SOLID = string_fragment::from_const("solid");
static const auto NAME_DASHED = string_fragment::from_const("dashed");
static const auto NAME_DOTTED = string_fragment::from_const("dotted");
static const auto& vc = view_colors::singleton();
text_attrs border_attrs;
auto border_sf = value;
auto width = border_line_width::thick;
auto ch = side == border_side::left ? left_border_string(width)
: right_border_string(width);
while (!border_sf.empty()) {
auto border_split_res
= border_sf.split_when(string_fragment::tag1{' '});
auto bval = border_split_res.first;
if (bval == NAME_THIN) {
width = border_line_width::thin;
} else if (bval == NAME_MEDIUM) {
width = border_line_width::medium;
} else if (bval == NAME_THICK) {
width = border_line_width::thick;
} else if (bval == NAME_DOTTED) {
ch = "\u250A";
} else if (bval == NAME_DASHED) {
ch = "\u254F";
} else if (bval == NAME_SOLID) {
ch = side == border_side::left ? left_border_string(width)
: right_border_string(width);
} else {
auto color_res = styling::color_unit::from_str(bval);
if (color_res.isErr()) {
log_error("invalid border color: %.*s -- %s",
bval.length(),
bval.data(),
color_res.unwrapErr().c_str());
} else {
border_attrs.ta_fg_color = vc.match_color(color_res.unwrap());
}
}
border_sf = border_split_res.second;
}
return attr_line_t(ch).with_attr_for_all(VC_STYLE.value(border_attrs));
}
attr_line_t
md2attr_line::to_attr_line(const pugi::xml_node& doc)
{
static const auto NAME_IMG = string_fragment::from_const("img");
static const auto NAME_SPAN = string_fragment::from_const("span");
static const auto NAME_PRE = string_fragment::from_const("pre");
static const auto NAME_FG = string_fragment::from_const("color");
static const auto NAME_BG = string_fragment::from_const("background-color");
static const auto NAME_FONT_WEIGHT
= string_fragment::from_const("font-weight");
static const auto NAME_TEXT_DECO
= string_fragment::from_const("text-decoration");
static const auto NAME_BORDER_LEFT
= string_fragment::from_const("border-left");
static const auto NAME_BORDER_RIGHT
= string_fragment::from_const("border-right");
static const auto& vc = view_colors::singleton();
attr_line_t retval;
if (doc.children().empty()) {
retval.append(doc.text().get());
}
for (const auto& child : doc.children()) {
if (child.name() == NAME_IMG) {
std::optional<std::string> src_href;
std::string link_label;
auto img_src = child.attribute("src");
auto img_alt = child.attribute("alt");
if (img_alt) {
link_label = img_alt.value();
} else if (img_src) {
link_label = ghc::filesystem::path(img_src.value())
.filename()
.string();
} else {
link_label = "img";
}
if (img_src) {
auto src_value = std::string(img_src.value());
if (is_url(src_value)) {
src_href = src_value;
} else {
auto src_path = ghc::filesystem::path(src_value);
std::error_code ec;
if (src_path.is_relative() && this->ml_source_path) {
src_path = this->ml_source_path.value().parent_path()
/ src_path;
}
auto canon_path = ghc::filesystem::canonical(src_path, ec);
if (!ec) {
src_path = canon_path;
}
src_href = fmt::format(FMT_STRING("file://{}"),
src_path.string());
}
}
if (src_href) {
retval.append(":framed_picture:"_emoji)
.append(" ")
.append(
lnav::string::attrs::href(link_label, src_href.value()))
.append(to_superscript(this->ml_footnotes.size() + 1));
auto href
= attr_line_t()
.append(lnav::roles::hyperlink(src_href.value()))
.append(" ");
href.with_attr_for_all(
VC_ROLE.value(role_t::VCR_FOOTNOTE_TEXT));
href.with_attr_for_all(SA_PREFORMATTED.value());
this->ml_footnotes.emplace_back(href);
} else {
retval.append(link_label);
}
} else if (child.name() == NAME_SPAN) {
std::optional<attr_line_t> left_border;
std::optional<attr_line_t> right_border;
auto styled_span = attr_line_t(child.text().get());
auto span_class = child.attribute("class");
if (span_class) {
auto cl_iter = vc.vc_class_to_role.find(span_class.value());
if (cl_iter == vc.vc_class_to_role.end()) {
log_error("unknown span class: %s", span_class.value());
} else {
styled_span.with_attr_for_all(cl_iter->second);
}
}
text_attrs ta;
auto span_style = child.attribute("style");
if (span_style) {
auto style_sf = string_fragment::from_c_str(span_style.value());
while (!style_sf.empty()) {
auto split_res
= style_sf.split_when(string_fragment::tag1{';'});
auto colon_split_res = split_res.first.split_pair(
string_fragment::tag1{':'});
if (colon_split_res) {
auto key = colon_split_res->first.trim();
auto value = colon_split_res->second.trim();
if (key == NAME_FG) {
auto color_res
= styling::color_unit::from_str(value);
if (color_res.isErr()) {
log_error("invalid color: %.*s -- %s",
value.length(),
value.data(),
color_res.unwrapErr().c_str());
} else {
ta.ta_fg_color
= vc.match_color(color_res.unwrap());
}
} else if (key == NAME_BG) {
auto color_res
= styling::color_unit::from_str(value);
if (color_res.isErr()) {
log_error(
"invalid background-color: %.*s -- %s",
value.length(),
value.data(),
color_res.unwrapErr().c_str());
} else {
ta.ta_bg_color
= vc.match_color(color_res.unwrap());
}
} else if (key == NAME_FONT_WEIGHT) {
if (value == "bold" || value == "bolder") {
ta.ta_attrs |= A_BOLD;
}
} else if (key == NAME_TEXT_DECO) {
auto deco_sf = value;
while (!deco_sf.empty()) {
auto deco_split_res = deco_sf.split_when(
string_fragment::tag1{' '});
if (deco_split_res.first.trim() == "underline")
{
ta.ta_attrs |= A_UNDERLINE;
}
deco_sf = deco_split_res.second;
}
} else if (key == NAME_BORDER_LEFT) {
left_border
= span_style_border(border_side::left, value);
} else if (key == NAME_BORDER_RIGHT) {
right_border
= span_style_border(border_side::right, value);
}
}
style_sf = split_res.second;
}
if (!ta.empty()) {
styled_span.with_attr_for_all(VC_STYLE.value(ta));
}
}
if (left_border) {
retval.append(left_border.value());
}
retval.append(styled_span);
if (right_border) {
retval.append(right_border.value());
}
} else if (child.name() == NAME_PRE) {
auto pre_al = attr_line_t();
for (const auto& sub : child.children()) {
auto child_al = this->to_attr_line(sub);
if (pre_al.empty() && startswith(child_al.get_string(), "\n")) {
child_al.erase(0, 1);
}
pre_al.append(child_al);
}
pre_al.with_attr_for_all(SA_PREFORMATTED.value());
retval.append(pre_al);
} else {
retval.append(child.text().get());
}
}
return retval;
}
Result<void, std::string>
md2attr_line::text(MD_TEXTTYPE tt, const string_fragment& sf)
{
static const auto& entity_map = md4cpp::get_xml_entity_map();
auto& last_block = this->ml_blocks.back();
switch (tt) {
case MD_TEXT_BR:
last_block.append("\n");
break;
case MD_TEXT_SOFTBR: {
if (!last_block.empty() && !isspace(last_block.get_string().back()))
{
last_block.append(" ");
}
break;
}
case MD_TEXT_ENTITY: {
auto xe_iter = entity_map.xem_entities.find(sf.to_string());
if (xe_iter != entity_map.xem_entities.end()) {
last_block.append(xe_iter->second.xe_chars);
}
break;
}
case MD_TEXT_HTML: {
auto last_block_start_length = last_block.length();
last_block.append(sf);
struct open_tag {
std::string ot_name;
};
struct close_tag {
std::string ct_name;
};
struct empty_tag {};
mapbox::util::variant<open_tag, close_tag, empty_tag> tag{
mapbox::util::no_init{}};
if (sf.startswith("</")) {
tag = close_tag{
sf.substr(2)
.split_when(string_fragment::tag1{'>'})
.first.to_string(),
};
} else if (sf.startswith("<")) {
if (sf.endswith("/>")) {
tag = empty_tag{};
} else {
tag = open_tag{
sf.substr(1)
.split_when(
[](char ch) { return ch == ' ' || ch == '>'; })
.first.to_string(),
};
}
}
if (tag.valid()) {
tag.match(
[this, last_block_start_length](const open_tag& ot) {
if (!this->ml_html_starts.empty()) {
return;
}
this->ml_html_starts.emplace_back(
ot.ot_name, last_block_start_length);
},
[this, &last_block](const close_tag& ct) {
if (this->ml_html_starts.empty()) {
return;
}
if (this->ml_html_starts.back().first != ct.ct_name) {
return;
}
const auto html_span = last_block.get_string().substr(
this->ml_html_starts.back().second);
pugi::xml_document doc;
auto load_res = doc.load_string(html_span.c_str());
if (!load_res) {
log_error("XML parsing failure at %d: %s",
load_res.offset,
load_res.description());
auto sf = string_fragment::from_str(html_span);
auto error_line = sf.find_boundaries_around(
load_res.offset, string_fragment::tag1{'\n'});
log_error(" %.*s",
error_line.length(),
error_line.data());
} else {
last_block.erase(
this->ml_html_starts.back().second);
last_block.append(this->to_attr_line(doc));
}
this->ml_html_starts.pop_back();
},
[this, &sf, &last_block, last_block_start_length](
const empty_tag&) {
const auto html_span = sf.to_string();
pugi::xml_document doc;
auto load_res = doc.load_string(html_span.c_str());
if (!load_res) {
log_error("XML parsing failure at %d: %s",
load_res.offset,
load_res.description());
auto error_line = sf.find_boundaries_around(
load_res.offset, string_fragment::tag1{'\n'});
log_error(" %.*s",
error_line.length(),
error_line.data());
} else {
last_block.erase(last_block_start_length);
last_block.append(this->to_attr_line(doc));
}
});
}
break;
}
default: {
static const auto REPL_RE = lnav::pcre2pp::code::from_const(
R"(-{2,3}|:[^:\s]*(?:::[^:\s]*)*:)");
static const auto& emojis = md4cpp::get_emoji_map();
if (this->ml_code_depth > 0) {
last_block.append(sf);
return Ok();
}
std::string span_text;
auto loop_res = REPL_RE.capture_from(sf).for_each(
[&span_text](const lnav::pcre2pp::match_data& md) {
span_text += md.leading();
auto matched = *md[0];
if (matched == "--") {
span_text.append("\u2013");
} else if (matched == "---") {
span_text.append("\u2014");
} else if (matched.startswith(":")) {
auto em_iter = emojis.em_shortname2emoji.find(
matched.to_string());
if (em_iter == emojis.em_shortname2emoji.end()) {
span_text += matched;
} else {
span_text.append(em_iter->second.get().e_value);
}
}
});
if (loop_res.isOk()) {
span_text += loop_res.unwrap();
} else {
log_error("span replacement regex failed: %d",
loop_res.unwrapErr().e_error_code);
}
text_wrap_settings tws
= {0, this->ml_blocks.size() == 1 ? 70 : 10000};
last_block.append(span_text, &tws);
break;
}
}
return Ok();
}
void
md2attr_line::append_url_footnote(std::string href_str)
{
auto is_internal = startswith(href_str, "#");
auto& last_block = this->ml_blocks.back();
last_block.with_attr(string_attr{
line_range{
(int) this->ml_span_starts.back(),
(int) last_block.length(),
},
VC_STYLE.value(text_attrs{A_UNDERLINE}),
});
if (is_internal) {
return;
}
if (this->ml_last_superscript_index == last_block.length()) {
last_block.append("\u02d2");
}
last_block.append(to_superscript(this->ml_footnotes.size() + 1));
this->ml_last_superscript_index = last_block.length();
if (this->ml_source_path && href_str.find(':') == std::string::npos) {
auto link_path = ghc::filesystem::absolute(
this->ml_source_path.value().parent_path() / href_str);
href_str = fmt::format(FMT_STRING("file://{}"), link_path.string());
}
auto href
= attr_line_t().append(lnav::roles::hyperlink(href_str)).append(" ");
href.with_attr_for_all(VC_ROLE.value(role_t::VCR_FOOTNOTE_TEXT));
href.with_attr_for_all(SA_PREFORMATTED.value());
this->ml_footnotes.emplace_back(href);
}