diff options
Diffstat (limited to '')
-rw-r--r-- | src/md2attr_line.cc | 486 |
1 files changed, 441 insertions, 45 deletions
diff --git a/src/md2attr_line.cc b/src/md2attr_line.cc index a208616..7c58ae1 100644 --- a/src/md2attr_line.cc +++ b/src/md2attr_line.cc @@ -32,12 +32,38 @@ #include "base/attr_line.builder.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() @@ -54,14 +80,17 @@ md2attr_line::flush_footnotes() 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"); + 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"); index += 1; } this->ml_footnotes.clear(); @@ -191,9 +220,26 @@ md2attr_line::leave_block(const md4cpp::event_handler::block& bl) auto lang_sf = string_fragment::from_bytes(code_detail->lang.text, code_detail->lang.size); - if (lang_sf == "lnav") { + 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") { + } 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()); @@ -421,6 +467,8 @@ md2attr_line::enter_span(const md4cpp::event_handler::span& sp) 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(); } @@ -438,7 +486,7 @@ md2attr_line::leave_span(const md4cpp::event_handler::span& sp) }; last_block.with_attr({ lr, - VC_ROLE.value(role_t::VCR_QUOTED_CODE), + VC_ROLE.value(role_t::VCR_INLINE_CODE), }); last_block.with_attr({ lr, @@ -476,7 +524,14 @@ md2attr_line::leave_span(const md4cpp::event_handler::span& sp) } else if (sp.is<MD_SPAN_A_DETAIL*>()) { 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*>()) { auto* img_detail = sp.get<MD_SPAN_IMG_DETAIL*>(); @@ -488,11 +543,291 @@ md2attr_line::leave_span(const md4cpp::event_handler::span& sp) 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) { + nonstd::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())) + .appendf(FMT_STRING("[{}]"), 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) { + nonstd::optional<attr_line_t> left_border; + nonstd::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(); - static const auto& vc = view_colors::singleton(); auto& last_block = this->ml_blocks.back(); @@ -516,41 +851,102 @@ md2attr_line::text(MD_TEXTTYPE tt, const string_fragment& sf) break; } case MD_TEXT_HTML: { + auto last_block_start_length = last_block.length(); last_block.append(sf); - if (sf.startswith("<span ")) { - this->ml_html_span_starts.push_back(last_block.length() - - sf.length()); - } else if (sf == "</span>" && !this->ml_html_span_starts.empty()) { - std::string html_span = last_block.get_string().substr( - this->ml_html_span_starts.back()); - - pugi::xml_document doc; - - auto load_res = doc.load_string(html_span.c_str()); - if (load_res) { - auto span = doc.child("span"); - if (span) { - auto styled_span = attr_line_t(span.text().get()); - - auto span_class = span.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); - } - } - last_block.erase(this->ml_html_span_starts.back()); - last_block.append(styled_span); - } + + 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 { - log_error("failed to parse: %s", load_res.description()); + tag = open_tag{ + sf.substr(1) + .split_when( + [](char ch) { return ch == ' ' || ch == '>'; }) + .first.to_string(), + }; } - this->ml_html_span_starts.pop_back(); + } + + 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; } |