summaryrefslogtreecommitdiffstats
path: root/src/md2attr_line.cc
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/md2attr_line.cc486
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;
}