[ansi_scrubber] handle unknown sequences

... and many other tweaks.
pull/1205/head
Tim Stack 9 months ago
parent ce391166ee
commit dd8a23ad51

@ -63,7 +63,8 @@ Features:
variables are now defined inside **lnav** and refer to
the location of the user's configuration directory and
the directory where cached data is stored, respectively.
* The `<pre>` tag is now recognized in Markdown files.
* The `<pre>` and `<img>` tags are now recognized in
Markdown files.
* The `style` attribute in `<span>` tags is now supported.
The following CSS properties and values are supported:
* `color` and `background-color` with CSS color names

@ -5,7 +5,7 @@
[![Coverage Status](https://coveralls.io/repos/github/tstack/lnav/badge.svg?branch=master)](https://coveralls.io/github/tstack/lnav?branch=master)
[![lnav](https://snapcraft.io/lnav/badge.svg)](https://snapcraft.io/lnav)
[<img src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/62594fddd654fc29fcc07359_cb48d2a8d4991281d7a6a95d2f58195e.svg" height="20"/>](https://discord.gg/erBPnKwz7R)
[<img src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/62594fddd654fc29fcc07359_cb48d2a8d4991281d7a6a95d2f58195e.svg" height="20" alt="Discord Logo"/>](https://discord.gg/erBPnKwz7R)
_This is the source repository for **lnav**, visit [https://lnav.org](https://lnav.org) for a high level overview._

@ -34,6 +34,7 @@
#include "ansi_scrubber.hh"
#include "ansi_vars.hh"
#include "base/lnav_log.hh"
#include "base/opt_util.hh"
#include "config.h"
#include "pcrepp/pcre2pp.hh"
@ -44,7 +45,7 @@ static const lnav::pcre2pp::code&
ansi_regex()
{
static const auto retval = lnav::pcre2pp::code::from_const(
"\x1b\\[([\\d=;\\?]*)([a-zA-Z])|(?:\\X\x08\\X)+");
R"(\x1b\[([\d=;\?]*)([a-zA-Z])|\x1b\](\d+);(.*?)(?:\x07|\x1b\\)|(?:\X\x08\X)+)");
return retval;
}
@ -124,6 +125,8 @@ scrub_ansi_string(std::string& str, string_attrs_t* sa)
const auto& regex = ansi_regex();
int64_t origin_offset = 0;
int last_origin_offset_end = 0;
nonstd::optional<std::string> href;
size_t href_start = 0;
replace(str.begin(), str.end(), '\0', ' ');
auto matcher = regex.capture_from(str).into(md);
@ -239,134 +242,164 @@ scrub_ansi_string(std::string& str, string_attrs_t* sa)
continue;
}
if (!md[1]) {
continue;
}
auto seq = md[1].value();
auto terminator = md[2].value();
struct line_range lr;
text_attrs attrs;
bool has_attrs = false;
nonstd::optional<role_t> role;
size_t lpc;
switch (terminator[0]) {
case 'm':
for (lpc = seq.sf_begin;
lpc != std::string::npos && lpc < (size_t) seq.sf_end;)
{
auto ansi_code_res = scn::scan_value<int>(
scn::string_view{&str[lpc], &str[seq.sf_end]});
if (ansi_code_res) {
auto ansi_code = ansi_code_res.value();
if (90 <= ansi_code && ansi_code <= 97) {
ansi_code -= 60;
attrs.ta_attrs |= A_STANDOUT;
}
if (30 <= ansi_code && ansi_code <= 37) {
attrs.ta_fg_color = ansi_code - 30;
if (md[3]) {
auto osc_id = scn::scan_value<int32_t>(md[3]->to_string_view());
if (osc_id) {
switch (osc_id.value()) {
case 8:
auto split_res
= md[4]->split_pair(string_fragment::tag1{';'});
if (split_res) {
// auto params = split_res->first;
auto uri = split_res->second;
if (href) {
if (sa != nullptr) {
sa->emplace_back(
line_range{(int) href_start,
(int) str.size()},
VC_HYPERLINK.value(href.value()));
}
href = nonstd::nullopt;
}
if (!uri.empty()) {
href = uri.to_string();
href_start = sf.sf_begin;
}
}
if (40 <= ansi_code && ansi_code <= 47) {
attrs.ta_bg_color = ansi_code - 40;
break;
}
}
} else if (md[1]) {
auto seq = md[1].value();
auto terminator = md[2].value();
switch (terminator[0]) {
case 'm':
for (auto lpc = seq.sf_begin;
lpc != std::string::npos && lpc < (size_t) seq.sf_end;)
{
auto ansi_code_res = scn::scan_value<int>(
scn::string_view{&str[lpc], &str[seq.sf_end]});
if (ansi_code_res) {
auto ansi_code = ansi_code_res.value();
if (90 <= ansi_code && ansi_code <= 97) {
ansi_code -= 60;
attrs.ta_attrs |= A_STANDOUT;
}
if (30 <= ansi_code && ansi_code <= 37) {
attrs.ta_fg_color = ansi_code - 30;
}
if (40 <= ansi_code && ansi_code <= 47) {
attrs.ta_bg_color = ansi_code - 40;
}
switch (ansi_code) {
case 1:
attrs.ta_attrs |= A_BOLD;
break;
case 2:
attrs.ta_attrs |= A_DIM;
break;
case 4:
attrs.ta_attrs |= A_UNDERLINE;
break;
case 7:
attrs.ta_attrs |= A_REVERSE;
break;
}
}
switch (ansi_code) {
case 1:
attrs.ta_attrs |= A_BOLD;
break;
case 2:
attrs.ta_attrs |= A_DIM;
break;
case 4:
attrs.ta_attrs |= A_UNDERLINE;
break;
case 7:
attrs.ta_attrs |= A_REVERSE;
break;
lpc = str.find(';', lpc);
if (lpc != std::string::npos) {
lpc += 1;
}
}
lpc = str.find(';', lpc);
if (lpc != std::string::npos) {
lpc += 1;
}
}
has_attrs = true;
break;
has_attrs = true;
break;
case 'C': {
auto spaces_res
= scn::scan_value<unsigned int>(seq.to_string_view());
case 'C': {
auto spaces_res
= scn::scan_value<unsigned int>(seq.to_string_view());
if (spaces_res && spaces_res.value() > 0) {
str.insert((std::string::size_type) sf.sf_end,
spaces_res.value(),
' ');
if (spaces_res && spaces_res.value() > 0) {
str.insert((std::string::size_type) sf.sf_end,
spaces_res.value(),
' ');
}
break;
}
break;
}
case 'H': {
unsigned int row = 0, spaces = 0;
case 'H': {
unsigned int row = 0, spaces = 0;
if (scn::scan(seq.to_string_view(), "{};{}", row, spaces)
&& spaces > 1)
{
int ispaces = spaces - 1;
if (ispaces > sf.sf_begin) {
str.insert((unsigned long) sf.sf_end,
ispaces - sf.sf_begin,
' ');
if (scn::scan(seq.to_string_view(), "{};{}", row, spaces)
&& spaces > 1)
{
int ispaces = spaces - 1;
if (ispaces > sf.sf_begin) {
str.insert((unsigned long) sf.sf_end,
ispaces - sf.sf_begin,
' ');
}
}
break;
}
break;
}
case 'O': {
auto role_res = scn::scan_value<int>(seq.to_string_view());
case 'O': {
auto role_res = scn::scan_value<int>(seq.to_string_view());
if (role_res) {
role_t role_tmp = (role_t) role_res.value();
if (role_tmp > role_t::VCR_NONE
&& role_tmp < role_t::VCR__MAX)
{
role = role_tmp;
has_attrs = true;
if (role_res) {
role_t role_tmp = (role_t) role_res.value();
if (role_tmp > role_t::VCR_NONE
&& role_tmp < role_t::VCR__MAX)
{
role = role_tmp;
has_attrs = true;
}
}
break;
}
break;
}
}
str.erase(str.begin() + sf.sf_begin, str.begin() + sf.sf_end);
if (sa != nullptr) {
shift_string_attrs(*sa, sf.sf_begin, -sf.length());
if (has_attrs) {
for (auto rit = sa->rbegin(); rit != sa->rend(); rit++) {
if (rit->sa_range.lr_end != -1) {
continue;
if (md[1] || md[3]) {
str.erase(str.begin() + sf.sf_begin, str.begin() + sf.sf_end);
if (sa != nullptr) {
shift_string_attrs(*sa, sf.sf_begin, -sf.length());
if (has_attrs) {
for (auto rit = sa->rbegin(); rit != sa->rend(); rit++) {
if (rit->sa_range.lr_end != -1) {
continue;
}
rit->sa_range.lr_end = sf.sf_begin;
}
rit->sa_range.lr_end = sf.sf_begin;
}
lr.lr_start = sf.sf_begin;
lr.lr_end = -1;
if (!attrs.empty()) {
sa->emplace_back(lr, VC_STYLE.value(attrs));
lr.lr_start = sf.sf_begin;
lr.lr_end = -1;
if (!attrs.empty()) {
sa->emplace_back(lr, VC_STYLE.value(attrs));
}
role | [&lr, &sa](role_t r) {
sa->emplace_back(lr, VC_ROLE.value(r));
};
}
role | [&lr, &sa](role_t r) {
sa->emplace_back(lr, VC_ROLE.value(r));
};
sa->emplace_back(
line_range{last_origin_offset_end, sf.sf_begin},
SA_ORIGIN_OFFSET.value(origin_offset));
last_origin_offset_end = sf.sf_begin;
origin_offset += sf.length();
}
sa->emplace_back(line_range{last_origin_offset_end, sf.sf_begin},
SA_ORIGIN_OFFSET.value(origin_offset));
last_origin_offset_end = sf.sf_begin;
origin_offset += sf.length();
}
matcher.reload_input(str, sf.sf_begin);
matcher.reload_input(str, sf.sf_begin);
}
}
if (sa != nullptr && last_origin_offset_end > 0) {

@ -312,6 +312,7 @@ println(FILE* file, const attr_line_t& al)
auto line_style = fmt::text_style{};
auto fg_style = fmt::text_style{};
auto start = last_point.value();
nonstd::optional<std::string> href;
for (const auto& attr : al.get_attrs()) {
if (!attr.sa_range.contains(start)
@ -321,7 +322,10 @@ println(FILE* file, const attr_line_t& al)
}
try {
if (attr.sa_type == &VC_BACKGROUND) {
if (attr.sa_type == &VC_HYPERLINK) {
auto saw = string_attr_wrapper<std::string>(&attr);
href = saw.get();
} else if (attr.sa_type == &VC_BACKGROUND) {
auto saw = string_attr_wrapper<int64_t>(&attr);
auto color_opt = curses_color_to_terminal_color(saw.get());
@ -484,6 +488,9 @@ println(FILE* file, const attr_line_t& al)
line_style |= default_bg_style;
}
if (href) {
fmt::print(file, FMT_STRING("\x1b]8;;{}\x1b\\"), href.value());
}
if (start < str.size()) {
auto actual_end = std::min(str.size(), static_cast<size_t>(point));
fmt::print(file,
@ -491,6 +498,9 @@ println(FILE* file, const attr_line_t& al)
FMT_STRING("{}"),
str.substr(start, actual_end - start));
}
if (href) {
fmt::print(file, FMT_STRING("\x1b]8;;\x1b\\"));
}
last_point = point;
}
fmt::print(file, "\n");

@ -50,3 +50,4 @@ string_attr_type<int64_t> VC_GRAPHIC("graphic");
string_attr_type<block_elem_t> VC_BLOCK_ELEM("block-elem");
string_attr_type<int64_t> VC_FOREGROUND("foreground");
string_attr_type<int64_t> VC_BACKGROUND("background");
string_attr_type<std::string> VC_HYPERLINK("hyperlink");

@ -233,6 +233,7 @@ extern string_attr_type<int64_t> VC_GRAPHIC;
extern string_attr_type<block_elem_t> VC_BLOCK_ELEM;
extern string_attr_type<int64_t> VC_FOREGROUND;
extern string_attr_type<int64_t> VC_BACKGROUND;
extern string_attr_type<std::string> VC_HYPERLINK;
namespace lnav {
@ -246,6 +247,14 @@ preformatted(S str)
return std::make_pair(std::move(str), SA_PREFORMATTED.template value());
}
template<typename S>
inline std::pair<S, string_attr_pair>
href(S str, std::string href)
{
return std::make_pair(std::move(str),
VC_HYPERLINK.template value(std::move(href)));
}
} // namespace attrs
} // namespace string
@ -699,6 +708,13 @@ inline std::pair<std::string, string_attr_pair> operator"" _snippet_border(
VC_ROLE.template value(role_t::VCR_SNIPPET_BORDER));
}
inline std::pair<std::string, string_attr_pair> operator"" _link(
const char* str, std::size_t len)
{
return std::make_pair(std::string(str, len),
VC_HYPERLINK.template value(std::string(str, len)));
}
} // namespace literals
} // namespace roles

@ -134,7 +134,7 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
time_lr.lr_end = time_str.length();
time_line.with_attr(
string_attr(time_lr, VC_STYLE.value(text_attrs{A_BOLD})));
time_str.append(" -- ");
time_str.append(" \u2014 ");
time_lr.lr_start = time_str.length();
time_str.append(humanize::time::point::from_tv(ll->get_timeval())
.with_convert_to_local(true)
@ -157,14 +157,13 @@ field_overlay_source::build_field_lines(const listview_curses& lv,
dts.set_base_time(format->lf_date_time.dts_base_time,
format->lf_date_time.dts_base_tm.et_tm);
dts.dts_zoned_to_local = format->lf_date_time.dts_zoned_to_local;
if (format->lf_date_time.scan(time_src,
time_range.length(),
format->get_timestamp_formats(),
&tm,
actual_tv,
false)
|| dts.scan(
time_src, time_range.length(), nullptr, &tm, actual_tv, false))
actual_tv)
|| dts.scan(time_src, time_range.length(), nullptr, &tm, actual_tv))
{
sql_strftime(
orig_timestamp, sizeof(orig_timestamp), actual_tv, 'T');

@ -12,6 +12,7 @@
"pattern": "^(?<timestamp>(?:\\S{3,8}\\s+\\d{1,2} \\d{2}:\\d{2}:\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z))\\s+(?<level>\\w+)\\((?<syslog_pri>\\d+)\\)(?:\\[\\+\\]|\\+)? (?:vmkernel|vmkwarning):\\s* (?:cpu(?<cpu>\\d+):(?<world_id>\\d+)(?: opID=(?<opid>[^\\)]+))?\\))?((?:(?:WARNING|ALERT)|(?<subsystem>[^:]+)): )?(?<body>.*)"
}
},
"ordered-by-time": false,
"level-field": "level",
"level": {
"debug": "^Db$",

@ -376,36 +376,43 @@ handle_paging_key(int ch)
break;
case 'J':
if (lnav_data.ld_last_user_mark.find(tc)
== lnav_data.ld_last_user_mark.end()
|| !tc->is_line_visible(
vis_line_t(lnav_data.ld_last_user_mark[tc])))
{
if (tc->is_selectable()) {
tc->toggle_user_mark(&textview_curses::BM_USER,
tc->get_selection());
lnav_data.ld_select_start[tc] = tc->get_selection();
lnav_data.ld_last_user_mark[tc] = tc->get_selection();
} else {
vis_line_t height;
unsigned long width;
tc->get_dimensions(height, width);
if (lnav_data.ld_last_user_mark[tc] > (tc->get_bottom() - 2)
&& tc->get_selection() + height < tc->get_inner_height())
{
tc->shift_top(1_vl);
if (tc->get_selection() + 1_vl < tc->get_inner_height()) {
tc->set_selection(tc->get_selection() + 1_vl);
}
if (lnav_data.ld_last_user_mark[tc] + 1
>= tc->get_inner_height())
} else {
if (lnav_data.ld_last_user_mark.find(tc)
== lnav_data.ld_last_user_mark.end()
|| !tc->is_line_visible(
vis_line_t(lnav_data.ld_last_user_mark[tc])))
{
break;
lnav_data.ld_select_start[tc] = tc->get_selection();
lnav_data.ld_last_user_mark[tc] = tc->get_selection();
} else {
vis_line_t height;
unsigned long width;
tc->get_dimensions(height, width);
if (lnav_data.ld_last_user_mark[tc] > (tc->get_bottom() - 2)
&& tc->get_selection() + height
< tc->get_inner_height())
{
tc->shift_top(1_vl);
}
if (lnav_data.ld_last_user_mark[tc] + 1
>= tc->get_inner_height())
{
break;
}
lnav_data.ld_last_user_mark[tc] += 1;
}
lnav_data.ld_last_user_mark[tc] += 1;
}
tc->toggle_user_mark(&textview_curses::BM_USER,
vis_line_t(lnav_data.ld_last_user_mark[tc]));
if (tc->is_selectable()
&& tc->get_selection() + 1_vl < tc->get_inner_height())
{
tc->set_selection(tc->get_selection() + 1_vl);
tc->toggle_user_mark(
&textview_curses::BM_USER,
vis_line_t(lnav_data.ld_last_user_mark[tc]));
}
tc->reload_data();

@ -721,7 +721,9 @@ make it easier to navigate through files quickly.
.append(lnav::roles::file(lnav::paths::workdir().string()))
.append("\n\n")
.append("Documentation"_h1)
.append(": https://docs.lnav.org\n")
.append(": ")
.append("https://docs.lnav.org"_hyperlink)
.append("\n")
.append("Contact"_h1)
.append("\n")
.append(" ")
@ -739,7 +741,7 @@ make it easier to navigate through files quickly.
static void
clear_last_user_mark(listview_curses* lv)
{
textview_curses* tc = (textview_curses*) lv;
auto* tc = (textview_curses*) lv;
if (lnav_data.ld_select_start.find(tc) != lnav_data.ld_select_start.end()
&& !tc->is_line_visible(vis_line_t(lnav_data.ld_last_user_mark[tc])))
{

@ -353,7 +353,7 @@ rebuild_indexes(nonstd::optional<ui_clock::time_point> deadline)
}
}
lnav_data.ld_view_stack.top() | [&closed_files](auto tc) {
lnav_data.ld_view_stack.top() | [&closed_files, &retval](auto tc) {
if (!closed_files.empty() && tc == &lnav_data.ld_views[LNV_GANTT]) {
auto* gantt_source = lnav_data.ld_views[LNV_GANTT].get_sub_source();
if (gantt_source != nullptr) {
@ -361,9 +361,11 @@ rebuild_indexes(nonstd::optional<ui_clock::time_point> deadline)
}
}
auto* tss = tc->get_sub_source();
lnav_data.ld_filter_status_source.update_filtered(tss);
lnav_data.ld_scroll_broadcaster(tc);
if (retval > 0) {
auto* tss = tc->get_sub_source();
lnav_data.ld_filter_status_source.update_filtered(tss);
lnav_data.ld_scroll_broadcaster(tc);
}
};
return retval;

@ -1287,16 +1287,41 @@ external_log_format::scan(logfile& lf,
{
this->lf_date_time.relock(ls);
continue;
} else {
log_debug("%s:%d:date-time re-locked to %d",
lf.get_unique_path().c_str(),
dst.size(),
this->lf_date_time.dts_fmt_lock);
}
if (last != nullptr) {
auto old_flags = this->lf_timestamp_flags & DATE_TIME_SET_FLAGS;
auto new_flags = log_time_tm.et_flags & DATE_TIME_SET_FLAGS;
// It is unlikely a valid timestamp would lose much
// precision.
if (new_flags != old_flags) {
continue;
}
}
log_debug("%s:%d:date-time re-locked to %d",
lf.get_unique_path().c_str(),
dst.size(),
this->lf_date_time.dts_fmt_lock);
}
this->lf_timestamp_flags = log_time_tm.et_flags;
if (!(this->lf_timestamp_flags
& (ETF_MILLIS_SET | ETF_MICROS_SET | ETF_NANOS_SET))
&& !dst.empty() && dst.back().get_time() == log_tv.tv_sec
&& dst.back().get_millis() != 0)
{
auto log_ms = std::chrono::milliseconds(dst.back().get_millis());
log_time_tm.et_nsec
= std::chrono::duration_cast<std::chrono::nanoseconds>(log_ms)
.count();
log_tv.tv_usec
= std::chrono::duration_cast<std::chrono::microseconds>(log_ms)
.count();
}
if (!((log_time_tm.et_flags & ETF_DAY_SET)
&& (log_time_tm.et_flags & ETF_MONTH_SET)
&& (log_time_tm.et_flags & ETF_YEAR_SET)))

@ -130,6 +130,24 @@ class generic_log_format : public log_format {
this->check_for_new_year(dst, log_time, log_tv);
}
if (!(this->lf_timestamp_flags
& (ETF_MILLIS_SET | ETF_MICROS_SET | ETF_NANOS_SET))
&& !dst.empty() && dst.back().get_time() == log_tv.tv_sec
&& dst.back().get_millis() != 0)
{
auto log_ms
= std::chrono::milliseconds(dst.back().get_millis());
log_time.et_nsec
= std::chrono::duration_cast<std::chrono::nanoseconds>(
log_ms)
.count();
log_tv.tv_usec
= std::chrono::duration_cast<std::chrono::microseconds>(
log_ms)
.count();
}
dst.emplace_back(li.li_file_range.fr_offset, log_tv, level_val);
return scan_match{0};
}

@ -42,6 +42,7 @@
#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
= {
@ -462,6 +463,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();
}
@ -517,7 +520,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*>();
@ -617,9 +627,10 @@ span_style_border(border_side side, const string_fragment& value)
return attr_line_t(ch).with_attr_for_all(VC_STYLE.value(border_attrs));
}
static attr_line_t
to_attr_line(const pugi::xml_node& doc)
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");
@ -639,7 +650,62 @@ to_attr_line(const pugi::xml_node& doc)
retval.append(doc.text().get());
}
for (const auto& child : doc.children()) {
if (child.name() == NAME_SPAN) {
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());
@ -738,7 +804,7 @@ to_attr_line(const pugi::xml_node& doc)
auto pre_al = attr_line_t();
for (const auto& sub : child.children()) {
auto child_al = to_attr_line(sub);
auto child_al = this->to_attr_line(sub);
if (pre_al.empty() && startswith(child_al.get_string(), "\n")) {
child_al.erase(0, 1);
}
@ -781,6 +847,7 @@ 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);
struct open_tag {
@ -789,8 +856,9 @@ md2attr_line::text(MD_TEXTTYPE tt, const string_fragment& sf)
struct close_tag {
std::string ct_name;
};
struct empty_tag {};
mapbox::util::variant<open_tag, close_tag> tag{
mapbox::util::variant<open_tag, close_tag, empty_tag> tag{
mapbox::util::no_init{}};
if (sf.startswith("</")) {
@ -800,22 +868,26 @@ md2attr_line::text(MD_TEXTTYPE tt, const string_fragment& sf)
.first.to_string(),
};
} else if (sf.startswith("<")) {
tag = open_tag{
sf.substr(1)
.split_when(
[](char ch) { return ch == ' ' || ch == '>'; })
.first.to_string(),
};
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, &sf, &last_block](const open_tag& ot) {
[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.length() - sf.length());
ot.ot_name, last_block_start_length);
},
[this, &last_block](const close_tag& ct) {
if (this->ml_html_starts.empty()) {
@ -845,9 +917,31 @@ md2attr_line::text(MD_TEXTTYPE tt, const string_fragment& sf)
} else {
last_block.erase(
this->ml_html_starts.back().second);
last_block.append(to_attr_line(doc));
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;

@ -34,6 +34,10 @@
#include "ghc/filesystem.hpp"
#include "md4cpp.hh"
namespace pugi {
class xml_node;
}
class md2attr_line : public md4cpp::typed_event_handler<attr_line_t> {
public:
md2attr_line() { this->ml_blocks.resize(1); }
@ -77,6 +81,7 @@ private:
void append_url_footnote(std::string href);
void flush_footnotes();
attr_line_t to_attr_line(const pugi::xml_node& doc);
nonstd::optional<ghc::filesystem::path> ml_source_path;
std::vector<attr_line_t> ml_blocks;

@ -123,9 +123,9 @@ struct utf_to_display_adjustment {
int uda_offset;
utf_to_display_adjustment(int utf_origin, int offset)
: uda_origin(utf_origin), uda_offset(offset){
};
: uda_origin(utf_origin), uda_offset(offset)
{
}
};
void
@ -185,6 +185,12 @@ view_curses::mvwattrline(WINDOW* window,
break;
}
case '\x1b':
expanded_line.append("\u238b");
utf_adjustments.emplace_back(lpc, -1);
char_index += 1;
break;
case '\r':
case '\n':
expanded_line.push_back(' ');

@ -424,7 +424,7 @@ can always use q to pop the top view off of the stack.
CTRL+], ESCAPE Abort command-line entry started with / , : , ;
, or | .
Note: The regular expression format used by lnav is PCRE[1]
Note: The regular expression format used by lnav is ]8;;http://perldoc.perl.org/perlre.html\PCRE]8;;\[1]
▌(Perl-Compatible Regular Expressions).
▌ ▌[1] - http://perldoc.perl.org/perlre.html

@ -1,3 +1,3 @@
2600-12-03 09:23:00 0:
2600-12-03 09:23:00 0:
2600-12-03 09:23:00 0:
2600-01-03 09:23:00 0:
00:2 0 00:00:00 0:
2600-01-03 09:23:00 0:

@ -11,7 +11,7 @@ Run ./autogen.sh if compiling from a cloned repository.
See Also
Angle-grinder[1] is a tool to slice and dice log files on the
]8;;https://github.com/rcoh/angle-grinder\Angle-grinder]8;;\[1] is a tool to slice and dice log files on the
command-line. If you're familiar with the SumoLogic query language,
you might find this tool more comfortable to work with.

@ -1,4 +1,4 @@
Build[1][2] Docs[3][4] Coverage Status[5][6] lnav[7][8]
]8;;https://github.com/tstack/lnav/actions?query=workflow%3Aci-build\🖼 Build[1]]8;;\[2] ]8;;https://docs.lnav.org\🖼 Docs[3]]8;;\[4] ]8;;https://coveralls.io/github/tstack/lnav?branch=master\🖼 Coverage Status[5]]8;;\[6] ]8;;https://snapcraft.io/lnav\🖼 lnav[7]]8;;\[8]
▌[1] - https://github.com/tstack/lnav/workflows/ci-build/badge.svg
▌[2] - https://github.com/tstack/lnav/actions?query=workflow%3Aci-build
@ -9,13 +9,12 @@
▌[7] - https://snapcraft.io/lnav/badge.svg
▌[8] - https://snapcraft.io/lnav
<img
src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/62594fddd654fc29fcc07359_cb48d2a8d4991281d7a6a95d2f58195e.svg"
height="20"/>[1]
]8;;https://discord.gg/erBPnKwz7R\🖼 ]8;;\]8;;https://discord.gg/erBPnKwz7R\Discord Logo]8;;\]8;;https://discord.gg/erBPnKwz7R\[1]]8;;\[2]
▌[1] - https://discord.gg/erBPnKwz7R
▌[1] - https://assets-global.website-files.com/6257adef93867e50d84d30e2/62594fddd654fc29fcc07359_cb48d2a8d4991281d7a6a95d2f58195e.svg
▌[2] - https://discord.gg/erBPnKwz7R
This is the source repository for lnav, visit https://lnav.org[1] for
This is the source repository for lnav, visit ]8;;https://lnav.org\https://lnav.org]8;;\[1] for
a high level overview.
▌[1] - https://lnav.org
@ -32,7 +31,7 @@ to no setup.
The following screenshot shows a syslog file. Log lines are displayed
with highlights. Errors are red and warnings are yellow.
Screenshot[1][2]
]8;;docs/assets/images/lnav-syslog.png\🖼 Screenshot[1]]8;;\[2]
▌[1] - file://{top_srcdir}/docs/assets/images/lnav-syslog-thumb.png
▌[2] - file://{top_srcdir}/docs/assets/images/lnav-syslog.png
@ -49,8 +48,8 @@ with highlights. Errors are red and warnings are yellow.
Installation
Download a statically-linked binary for Linux/MacOS from the release
page[1]
]8;;https://github.com/tstack/lnav/releases/latest#release-artifacts\Download a statically-linked binary for Linux/MacOS from the release]8;;\
]8;;https://github.com/tstack/lnav/releases/latest#release-artifacts\page]8;;\[1]
▌[1] - https://github.com/tstack/lnav/releases/latest#release-artifacts
@ -111,9 +110,9 @@ log lines fed into lnav via journalctl 's -b option.
Please file issues on this repository or use the discussions section.
The following alternatives are also available:
• support@lnav.org[1]
• Discord[2]
• Google Groups[3]
• ]8;;mailto:support@lnav.org\support@lnav.org]8;;\[1]
• ]8;;https://discord.gg/erBPnKwz7R\Discord]8;;\[2]
• ]8;;https://groups.google.com/g/lnav\Google Groups]8;;\[3]
▌[1] - mailto:support@lnav.org
▌[2] - https://discord.gg/erBPnKwz7R
@ -121,9 +120,9 @@ The following alternatives are also available:
Links
• Main Site[1]
• Documentation[2] on Read the Docs
• Internal Architecture[3]
• ]8;;https://lnav.org\Main Site]8;;\[1]
• ]8;;https://docs.lnav.org\Documentation]8;;\[2] on Read the Docs
• ]8;;ARCHITECTURE.md\Internal Architecture]8;;\[3]
▌[1] - https://lnav.org
▌[2] - https://docs.lnav.org
@ -131,7 +130,7 @@ The following alternatives are also available:
Contributing
• Become a Sponsor on GitHub[1]
• ]8;;https://github.com/sponsors/tstack\Become a Sponsor on GitHub]8;;\[1]
▌[1] - https://github.com/sponsors/tstack
@ -170,7 +169,7 @@ Run ./autogen.sh if compiling from a cloned repository.
See Also
Angle-grinder[1] is a tool to slice and dice log files on the
]8;;https://github.com/rcoh/angle-grinder\Angle-grinder]8;;\[1] is a tool to slice and dice log files on the
command-line. If you're familiar with the SumoLogic query language,
you might find this tool more comfortable to work with.

@ -3,7 +3,7 @@
The following screenshot shows a syslog file. Log lines are displayed
with highlights. Errors are red and warnings are yellow.
Screenshot[1][2]
]8;;docs/assets/images/lnav-syslog.png\🖼 Screenshot[1]]8;;\[2]
▌[1] - file://{top_srcdir}/docs/assets/images/lnav-syslog-thumb.png
▌[2] - file://{top_srcdir}/docs/assets/images/lnav-syslog.png
@ -20,8 +20,8 @@ with highlights. Errors are red and warnings are yellow.
Installation
Download a statically-linked binary for Linux/MacOS from the release
page[1]
]8;;https://github.com/tstack/lnav/releases/latest#release-artifacts\Download a statically-linked binary for Linux/MacOS from the release]8;;\
]8;;https://github.com/tstack/lnav/releases/latest#release-artifacts\page]8;;\[1]
▌[1] - https://github.com/tstack/lnav/releases/latest#release-artifacts
@ -82,9 +82,9 @@ log lines fed into lnav via journalctl 's -b option.
Please file issues on this repository or use the discussions section.
The following alternatives are also available:
• support@lnav.org[1]
• Discord[2]
• Google Groups[3]
• ]8;;mailto:support@lnav.org\support@lnav.org]8;;\[1]
• ]8;;https://discord.gg/erBPnKwz7R\Discord]8;;\[2]
• ]8;;https://groups.google.com/g/lnav\Google Groups]8;;\[3]
▌[1] - mailto:support@lnav.org
▌[2] - https://discord.gg/erBPnKwz7R
@ -92,9 +92,9 @@ The following alternatives are also available:
Links
• Main Site[1]
• Documentation[2] on Read the Docs
• Internal Architecture[3]
• ]8;;https://lnav.org\Main Site]8;;\[1]
• ]8;;https://docs.lnav.org\Documentation]8;;\[2] on Read the Docs
• ]8;;ARCHITECTURE.md\Internal Architecture]8;;\[3]
▌[1] - https://lnav.org
▌[2] - https://docs.lnav.org
@ -102,7 +102,7 @@ The following alternatives are also available:
Contributing
• Become a Sponsor on GitHub[1]
• ]8;;https://github.com/sponsors/tstack\Become a Sponsor on GitHub]8;;\[1]
▌[1] - https://github.com/sponsors/tstack
@ -141,7 +141,7 @@ Run ./autogen.sh if compiling from a cloned repository.
See Also
Angle-grinder[1] is a tool to slice and dice log files on the
]8;;https://github.com/rcoh/angle-grinder\Angle-grinder]8;;\[1] is a tool to slice and dice log files on the
command-line. If you're familiar with the SumoLogic query language,
you might find this tool more comfortable to work with.

@ -5,6 +5,13 @@
• Two
• Three
🖼 ]8;;file://{top_srcdir}/docs/lnav-tui.png\lnav-tui.png]8;;\[1]
🖼 ]8;;file://{top_srcdir}/docs/lnav-architecture.png\The internal architecture of lnav]8;;\[2]
▌[1] - file://{top_srcdir}/docs/lnav-tui.png
▌[2] - file://{top_srcdir}/docs/lnav-architecture.png
Bold red
Underline

@ -3,7 +3,7 @@
The following screenshot shows a syslog file. Log lines are displayed
with highlights. Errors are red and warnings are yellow.
Screenshot[1][2]
]8;;docs/assets/images/lnav-syslog.png\🖼 Screenshot[1]]8;;\[2]
▌[1] - file://{top_srcdir}/docs/assets/images/lnav-syslog-thumb.png
▌[2] - file://{top_srcdir}/docs/assets/images/lnav-syslog.png
@ -20,8 +20,8 @@ with highlights. Errors are red and warnings are yellow.
Installation
Download a statically-linked binary for Linux/MacOS from the release
page[1]
]8;;https://github.com/tstack/lnav/releases/latest#release-artifacts\Download a statically-linked binary for Linux/MacOS from the release]8;;\
]8;;https://github.com/tstack/lnav/releases/latest#release-artifacts\page]8;;\[1]
▌[1] - https://github.com/tstack/lnav/releases/latest#release-artifacts
@ -82,9 +82,9 @@ log lines fed into lnav via journalctl 's -b option.
Please file issues on this repository or use the discussions section.
The following alternatives are also available:
• support@lnav.org[1]
• Discord[2]
• Google Groups[3]
• ]8;;mailto:support@lnav.org\support@lnav.org]8;;\[1]
• ]8;;https://discord.gg/erBPnKwz7R\Discord]8;;\[2]
• ]8;;https://groups.google.com/g/lnav\Google Groups]8;;\[3]
▌[1] - mailto:support@lnav.org
▌[2] - https://discord.gg/erBPnKwz7R
@ -92,9 +92,9 @@ The following alternatives are also available:
Links
• Main Site[1]
• Documentation[2] on Read the Docs
• Internal Architecture[3]
• ]8;;https://lnav.org\Main Site]8;;\[1]
• ]8;;https://docs.lnav.org\Documentation]8;;\[2] on Read the Docs
• ]8;;ARCHITECTURE.md\Internal Architecture]8;;\[3]
▌[1] - https://lnav.org
▌[2] - https://docs.lnav.org
@ -102,7 +102,7 @@ The following alternatives are also available:
Contributing
• Become a Sponsor on GitHub[1]
• ]8;;https://github.com/sponsors/tstack\Become a Sponsor on GitHub]8;;\[1]
▌[1] - https://github.com/sponsors/tstack
@ -141,7 +141,7 @@ Run ./autogen.sh if compiling from a cloned repository.
See Also
Angle-grinder[1] is a tool to slice and dice log files on the
]8;;https://github.com/rcoh/angle-grinder\Angle-grinder]8;;\[1] is a tool to slice and dice log files on the
command-line. If you're familiar with the SumoLogic query language,
you might find this tool more comfortable to work with.

@ -90,6 +90,27 @@ main(int argc, char* argv[])
}
}
{
auto hlink = std::string(
"\033]8;;http://example.com\033\\This is a "
"link\033]8;;\033\\\n");
string_attrs_t sa;
scrub_ansi_string(hlink, &sa);
printf("hlink %d %d %s", hlink.size(), sa.size(), hlink.c_str());
assert(sa.size() == 4);
for (const auto& attr : sa) {
printf("attr %d:%d %s\n",
attr.sa_range.lr_start,
attr.sa_range.lr_end,
attr.sa_type->sat_name);
if (attr.sa_type == &VC_HYPERLINK) {
printf(" value: %s\n",
attr.sa_value.get<std::string>().c_str());
}
}
}
string_attrs_t sa;
string str_cp;

@ -8,6 +8,10 @@
* Two
* Three
<img src="../docs/lnav-tui.png" />
<img src="../docs/lnav-architecture.png" alt="The internal architecture of lnav" />
<span style="color: #f00; font-weight: bold">Bold red</span>
<span style="text-decoration: underline; background-color: darkblue">Underline</span>

Loading…
Cancel
Save