mirror of https://github.com/tstack/lnav
[md4c] initial markdown support
parent
0c7f6145c9
commit
31a670ce27
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 "attr_line.builder.hh"
|
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef lnav_attr_line_builder_hh
|
||||
#define lnav_attr_line_builder_hh
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "attr_line.hh"
|
||||
|
||||
class attr_line_builder {
|
||||
public:
|
||||
explicit attr_line_builder(attr_line_t& al) : alb_line(al) {}
|
||||
|
||||
class attr_guard {
|
||||
public:
|
||||
attr_guard(attr_line_t& al, string_attr_pair sap)
|
||||
: ag_line(al), ag_start(al.get_string().length()),
|
||||
ag_attr(std::move(sap))
|
||||
{
|
||||
}
|
||||
|
||||
attr_guard(const attr_guard&) = delete;
|
||||
|
||||
attr_guard& operator=(const attr_guard&) = delete;
|
||||
|
||||
attr_guard(attr_guard&& other) noexcept
|
||||
: ag_line(other.ag_line), ag_start(other.ag_start),
|
||||
ag_attr(std::move(other.ag_attr))
|
||||
{
|
||||
other.ag_start = nonstd::nullopt;
|
||||
}
|
||||
|
||||
~attr_guard()
|
||||
{
|
||||
if (this->ag_start) {
|
||||
this->ag_line.al_attrs.emplace_back(
|
||||
line_range{
|
||||
this->ag_start.value(),
|
||||
(int) this->ag_line.get_string().length(),
|
||||
},
|
||||
this->ag_attr);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
attr_line_t& ag_line;
|
||||
nonstd::optional<int> ag_start;
|
||||
string_attr_pair ag_attr;
|
||||
};
|
||||
|
||||
attr_guard with_attr(string_attr_pair sap)
|
||||
{
|
||||
return {this->alb_line, std::move(sap)};
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
attr_line_builder& overlay_attr(Args... args)
|
||||
{
|
||||
this->alb_line.al_attrs.template emplace_back(args...);
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
attr_line_builder& overlay_attr_for_char(int index, Args... args)
|
||||
{
|
||||
this->alb_line.al_attrs.template emplace_back(
|
||||
line_range{index, index + 1}, args...);
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<typename... Args>
|
||||
attr_line_builder& append(Args... args)
|
||||
{
|
||||
this->alb_line.append(args...);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
attr_line_builder& indent(size_t amount)
|
||||
{
|
||||
auto pre = this->with_attr(SA_PREFORMATTED.value());
|
||||
|
||||
this->append(amount, ' ');
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
attr_line_t& alb_line;
|
||||
};
|
||||
|
||||
#endif
|
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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 <iostream>
|
||||
|
||||
#include "attr_line.hh"
|
||||
|
||||
#include "config.h"
|
||||
#include "doctest/doctest.h"
|
||||
|
||||
using namespace lnav::roles::literals;
|
||||
|
||||
TEST_CASE("attr_line_t::basic-wrapping")
|
||||
{
|
||||
text_wrap_settings tws = {3, 21};
|
||||
attr_line_t to_be_wrapped{"This line, right here, needs to be wrapped."};
|
||||
attr_line_t dst;
|
||||
|
||||
to_be_wrapped.al_attrs.emplace_back(
|
||||
line_range{0, (int) to_be_wrapped.al_string.length()},
|
||||
VC_ROLE.value(role_t::VCR_ERROR));
|
||||
dst.append(to_be_wrapped, &tws);
|
||||
|
||||
CHECK(dst.get_string() ==
|
||||
"This line, right\n"
|
||||
" here, needs to be\n"
|
||||
" wrapped.");
|
||||
|
||||
for (const auto& attr : dst.al_attrs) {
|
||||
printf("attr %d:%d %s\n",
|
||||
attr.sa_range.lr_start,
|
||||
attr.sa_range.lr_end,
|
||||
attr.sa_type->sat_name);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("attr_line_t::unicode-wrap")
|
||||
{
|
||||
text_wrap_settings tws = {3, 21};
|
||||
attr_line_t prefix;
|
||||
|
||||
prefix.append(" ")
|
||||
.append("\u2022"_list_glyph)
|
||||
.append(" ")
|
||||
.with_attr_for_all(SA_PREFORMATTED.value());
|
||||
|
||||
attr_line_t body;
|
||||
body.append("This is a long line that needs to be wrapped and indented");
|
||||
|
||||
attr_line_t li;
|
||||
|
||||
li.append(prefix)
|
||||
.append(body, &tws)
|
||||
.with_attr_for_all(SA_PREFORMATTED.value());
|
||||
|
||||
attr_line_t dst;
|
||||
|
||||
dst.append(li);
|
||||
|
||||
CHECK(dst.get_string()
|
||||
== " \u2022 This is a long\n"
|
||||
" line that needs to\n"
|
||||
" be wrapped and\n"
|
||||
" indented");
|
||||
}
|
@ -0,0 +1,452 @@
|
||||
/**
|
||||
* 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 <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "document.sections.hh"
|
||||
|
||||
#include "base/enum_util.hh"
|
||||
#include "base/itertools.hh"
|
||||
#include "base/lnav_log.hh"
|
||||
#include "base/opt_util.hh"
|
||||
#include "data_scanner.hh"
|
||||
|
||||
namespace lnav {
|
||||
namespace document {
|
||||
|
||||
nonstd::optional<hier_node*>
|
||||
hier_node::lookup_child(section_key_t key) const
|
||||
{
|
||||
return make_optional_from_nullable(key.match(
|
||||
[this](const std::string& str) -> hier_node* {
|
||||
auto iter = this->hn_named_children.find(str);
|
||||
if (iter != this->hn_named_children.end()) {
|
||||
return iter->second;
|
||||
}
|
||||
return nullptr;
|
||||
},
|
||||
[this](size_t index) -> hier_node* {
|
||||
if (index < this->hn_children.size()) {
|
||||
return this->hn_children[index].get();
|
||||
}
|
||||
return nullptr;
|
||||
}));
|
||||
}
|
||||
|
||||
nonstd::optional<const hier_node*>
|
||||
hier_node::lookup_path(const hier_node* root,
|
||||
const std::vector<section_key_t>& path)
|
||||
{
|
||||
auto retval = make_optional_from_nullable(root);
|
||||
|
||||
for (const auto& comp : path) {
|
||||
if (!retval) {
|
||||
break;
|
||||
}
|
||||
|
||||
retval = retval.value()->lookup_child(comp);
|
||||
}
|
||||
|
||||
if (!retval) {
|
||||
return nonstd::nullopt;
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
metadata
|
||||
discover_metadata(const attr_line_t& al)
|
||||
{
|
||||
const auto& orig_attrs = al.get_attrs();
|
||||
auto headers = orig_attrs
|
||||
| lnav::itertools::filter_in([](const string_attr& attr) {
|
||||
if (attr.sa_type != &VC_ROLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto role = attr.sa_value.get<role_t>();
|
||||
switch (role) {
|
||||
case role_t::VCR_H1:
|
||||
case role_t::VCR_H2:
|
||||
case role_t::VCR_H3:
|
||||
case role_t::VCR_H4:
|
||||
case role_t::VCR_H5:
|
||||
case role_t::VCR_H6:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})
|
||||
| lnav::itertools::sort_by(&string_attr::sa_range);
|
||||
|
||||
// Remove headers from quoted text
|
||||
for (const auto& orig_attr : orig_attrs) {
|
||||
if (orig_attr.sa_type == &VC_ROLE
|
||||
&& orig_attr.sa_value.get<role_t>() == role_t::VCR_QUOTED_TEXT)
|
||||
{
|
||||
remove_string_attr(headers, orig_attr.sa_range);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<section_interval_t> intervals;
|
||||
|
||||
struct open_interval_t {
|
||||
open_interval_t(uint32_t level, file_off_t start, section_key_t id)
|
||||
: oi_level(level), oi_start(start), oi_id(std::move(id))
|
||||
{
|
||||
}
|
||||
|
||||
uint32_t oi_level;
|
||||
file_off_t oi_start;
|
||||
section_key_t oi_id;
|
||||
std::unique_ptr<hier_node> oi_node{std::make_unique<hier_node>()};
|
||||
};
|
||||
std::vector<open_interval_t> open_intervals;
|
||||
auto root_node = std::make_unique<hier_node>();
|
||||
|
||||
for (const auto& hdr_attr : headers) {
|
||||
auto role = hdr_attr.sa_value.get<role_t>();
|
||||
auto role_num = lnav::enums::to_underlying(role)
|
||||
- lnav::enums::to_underlying(role_t::VCR_H1);
|
||||
std::vector<open_interval_t> new_open_intervals;
|
||||
|
||||
for (auto& oi : open_intervals) {
|
||||
if (oi.oi_level >= role_num) {
|
||||
// close out this section
|
||||
intervals.emplace_back(
|
||||
oi.oi_start, hdr_attr.sa_range.lr_start - 1, oi.oi_id);
|
||||
auto* node_ptr = oi.oi_node.get();
|
||||
auto* parent_node = oi.oi_node->hn_parent;
|
||||
if (parent_node != nullptr) {
|
||||
parent_node->hn_children.emplace_back(
|
||||
std::move(oi.oi_node));
|
||||
parent_node->hn_named_children.insert({
|
||||
oi.oi_id.get<std::string>(),
|
||||
node_ptr,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
new_open_intervals.emplace_back(std::move(oi));
|
||||
}
|
||||
}
|
||||
auto* parent_node = new_open_intervals.empty()
|
||||
? root_node.get()
|
||||
: new_open_intervals.back().oi_node.get();
|
||||
new_open_intervals.emplace_back(role_num,
|
||||
hdr_attr.sa_range.lr_start,
|
||||
al.get_substring(hdr_attr.sa_range));
|
||||
new_open_intervals.back().oi_node->hn_parent = parent_node;
|
||||
new_open_intervals.back().oi_node->hn_start
|
||||
= hdr_attr.sa_range.lr_start;
|
||||
|
||||
open_intervals = std::move(new_open_intervals);
|
||||
}
|
||||
|
||||
for (auto& oi : open_intervals) {
|
||||
// close out this section
|
||||
intervals.emplace_back(oi.oi_start, al.length(), oi.oi_id);
|
||||
auto* node_ptr = oi.oi_node.get();
|
||||
auto* parent_node = oi.oi_node->hn_parent;
|
||||
if (parent_node == nullptr) {
|
||||
root_node = std::move(oi.oi_node);
|
||||
} else {
|
||||
parent_node->hn_children.emplace_back(std::move(oi.oi_node));
|
||||
parent_node->hn_named_children.insert({
|
||||
oi.oi_id.get<std::string>(),
|
||||
node_ptr,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sections_tree_t{std::move(intervals)},
|
||||
std::move(root_node),
|
||||
};
|
||||
}
|
||||
|
||||
class structure_walker {
|
||||
public:
|
||||
explicit structure_walker(string_fragment content) : sw_scanner(content)
|
||||
{
|
||||
this->sw_interval_state.resize(1);
|
||||
this->sw_hier_nodes.push_back(std::make_unique<hier_node>());
|
||||
}
|
||||
|
||||
metadata walk()
|
||||
{
|
||||
pcre_context_static<30> pc;
|
||||
data_token_t dt = DT_INVALID;
|
||||
lnav::document::metadata retval;
|
||||
auto& pi = this->sw_scanner.get_input();
|
||||
|
||||
while (this->sw_scanner.tokenize2(pc, dt)) {
|
||||
element el(dt, pc);
|
||||
|
||||
switch (dt) {
|
||||
case DT_XML_DECL_TAG:
|
||||
case DT_XML_EMPTY_TAG:
|
||||
this->sw_values.emplace_back(el);
|
||||
break;
|
||||
case DT_XML_OPEN_TAG:
|
||||
this->flush_values();
|
||||
this->sw_interval_state.back().is_start
|
||||
= el.e_capture.c_begin;
|
||||
this->sw_interval_state.back().is_line_number
|
||||
= this->sw_line_number;
|
||||
this->sw_interval_state.back().is_name
|
||||
= pi.get_substr(&el.e_capture);
|
||||
this->sw_depth += 1;
|
||||
this->sw_interval_state.resize(this->sw_depth + 1);
|
||||
this->sw_hier_nodes.push_back(
|
||||
std::make_unique<hier_node>());
|
||||
break;
|
||||
case DT_XML_CLOSE_TAG: {
|
||||
auto term = this->flush_values();
|
||||
if (this->sw_depth > 0) {
|
||||
this->sw_depth -= 1;
|
||||
this->append_child_node(term);
|
||||
this->sw_interval_state.pop_back();
|
||||
this->sw_hier_stage
|
||||
= std::move(this->sw_hier_nodes.back());
|
||||
this->sw_hier_nodes.pop_back();
|
||||
}
|
||||
this->append_child_node(el.e_capture);
|
||||
this->flush_values();
|
||||
break;
|
||||
}
|
||||
case DT_LCURLY:
|
||||
case DT_LSQUARE:
|
||||
case DT_LPAREN:
|
||||
this->flush_values();
|
||||
this->sw_depth += 1;
|
||||
if (!this->sw_interval_state.back().is_start) {
|
||||
this->sw_interval_state.back().is_start
|
||||
= el.e_capture.c_begin;
|
||||
this->sw_interval_state.back().is_line_number
|
||||
= this->sw_line_number;
|
||||
}
|
||||
this->sw_interval_state.resize(this->sw_depth + 1);
|
||||
this->sw_hier_nodes.push_back(
|
||||
std::make_unique<hier_node>());
|
||||
break;
|
||||
case DT_RCURLY:
|
||||
case DT_RSQUARE:
|
||||
case DT_RPAREN: {
|
||||
auto term = this->flush_values();
|
||||
if (this->sw_depth > 0) {
|
||||
this->sw_depth -= 1;
|
||||
this->append_child_node(term);
|
||||
this->sw_interval_state.pop_back();
|
||||
this->sw_hier_stage
|
||||
= std::move(this->sw_hier_nodes.back());
|
||||
this->sw_hier_nodes.pop_back();
|
||||
}
|
||||
this->sw_values.emplace_back(el);
|
||||
break;
|
||||
}
|
||||
case DT_COMMA:
|
||||
if (this->sw_depth > 0) {
|
||||
auto term = this->flush_values();
|
||||
this->append_child_node(term);
|
||||
}
|
||||
break;
|
||||
case DT_LINE:
|
||||
this->sw_line_number += 1;
|
||||
break;
|
||||
case DT_WHITE:
|
||||
break;
|
||||
default:
|
||||
this->sw_values.emplace_back(el);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->flush_values();
|
||||
|
||||
if (this->sw_hier_stage != nullptr) {
|
||||
this->sw_hier_stage->hn_parent = this->sw_hier_nodes.back().get();
|
||||
this->sw_hier_nodes.back()->hn_children.push_back(
|
||||
std::move(this->sw_hier_stage));
|
||||
}
|
||||
this->sw_hier_stage = std::move(this->sw_hier_nodes.back());
|
||||
this->sw_hier_nodes.pop_back();
|
||||
if (this->sw_hier_stage->hn_children.size() == 1
|
||||
&& this->sw_hier_stage->hn_named_children.empty())
|
||||
{
|
||||
this->sw_hier_stage
|
||||
= std::move(this->sw_hier_stage->hn_children.front());
|
||||
this->sw_hier_stage->hn_parent = nullptr;
|
||||
}
|
||||
|
||||
retval.m_sections_root = std::move(this->sw_hier_stage);
|
||||
retval.m_sections_tree = sections_tree_t(std::move(this->sw_intervals));
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
private:
|
||||
struct element {
|
||||
element(data_token_t token, pcre_context& pc)
|
||||
: e_token(token), e_capture(*pc.all())
|
||||
{
|
||||
}
|
||||
|
||||
element(data_token_t token, pcre_context::capture_t& cap)
|
||||
: e_token(token), e_capture(cap)
|
||||
{
|
||||
}
|
||||
|
||||
data_token_t e_token;
|
||||
pcre_context::capture_t e_capture;
|
||||
};
|
||||
|
||||
struct interval_state {
|
||||
nonstd::optional<file_off_t> is_start;
|
||||
size_t is_line_number{0};
|
||||
std::string is_name;
|
||||
};
|
||||
|
||||
nonstd::optional<pcre_context::capture_t> flush_values()
|
||||
{
|
||||
nonstd::optional<pcre_context::capture_t> last_key;
|
||||
nonstd::optional<pcre_context::capture_t> retval;
|
||||
auto& pi = this->sw_scanner.get_input();
|
||||
|
||||
if (!this->sw_values.empty()) {
|
||||
if (!this->sw_interval_state.back().is_start) {
|
||||
this->sw_interval_state.back().is_start
|
||||
= this->sw_values.front().e_capture.c_begin;
|
||||
this->sw_interval_state.back().is_line_number
|
||||
= this->sw_line_number;
|
||||
}
|
||||
retval = this->sw_values.back().e_capture;
|
||||
}
|
||||
for (const auto& el : this->sw_values) {
|
||||
switch (el.e_token) {
|
||||
case DT_SYMBOL:
|
||||
case DT_CONSTANT:
|
||||
case DT_WORD:
|
||||
case DT_QUOTED_STRING:
|
||||
last_key = el.e_capture;
|
||||
break;
|
||||
case DT_COLON:
|
||||
case DT_EQUALS:
|
||||
if (last_key) {
|
||||
this->sw_interval_state.back().is_name
|
||||
= pi.get_substr(&last_key.value());
|
||||
if (!this->sw_interval_state.back().is_name.empty()) {
|
||||
this->sw_interval_state.back().is_start
|
||||
= static_cast<ssize_t>(
|
||||
last_key.value().c_begin);
|
||||
this->sw_interval_state.back().is_line_number
|
||||
= this->sw_line_number;
|
||||
}
|
||||
last_key = nonstd::nullopt;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this->sw_values.clear();
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
void append_child_node(nonstd::optional<pcre_context::capture_t> terminator)
|
||||
{
|
||||
auto& ivstate = this->sw_interval_state.back();
|
||||
if (!ivstate.is_start || !terminator) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* top_node = this->sw_hier_nodes.back().get();
|
||||
auto new_key = ivstate.is_name.empty()
|
||||
? lnav::document::section_key_t{top_node->hn_children.size()}
|
||||
: lnav::document::section_key_t{ivstate.is_name};
|
||||
this->sw_intervals.emplace_back(
|
||||
ivstate.is_start.value(),
|
||||
static_cast<ssize_t>(terminator.value().c_end),
|
||||
new_key);
|
||||
auto new_node = this->sw_hier_stage != nullptr
|
||||
? std::move(this->sw_hier_stage)
|
||||
: std::make_unique<lnav::document::hier_node>();
|
||||
auto* retval = new_node.get();
|
||||
new_node->hn_parent = top_node;
|
||||
new_node->hn_start = this->sw_intervals.back().start;
|
||||
new_node->hn_line_number = ivstate.is_line_number;
|
||||
if (!ivstate.is_name.empty()) {
|
||||
top_node->hn_named_children.insert({
|
||||
ivstate.is_name,
|
||||
retval,
|
||||
});
|
||||
}
|
||||
top_node->hn_children.emplace_back(std::move(new_node));
|
||||
ivstate.is_start = nonstd::nullopt;
|
||||
ivstate.is_line_number = 0;
|
||||
ivstate.is_name.clear();
|
||||
}
|
||||
|
||||
data_scanner sw_scanner;
|
||||
int sw_depth{0};
|
||||
size_t sw_line_number{0};
|
||||
std::vector<element> sw_values{};
|
||||
std::vector<interval_state> sw_interval_state;
|
||||
std::vector<lnav::document::section_interval_t> sw_intervals;
|
||||
std::vector<std::unique_ptr<lnav::document::hier_node>> sw_hier_nodes;
|
||||
std::unique_ptr<lnav::document::hier_node> sw_hier_stage;
|
||||
};
|
||||
|
||||
metadata
|
||||
discover_structure(string_fragment content)
|
||||
{
|
||||
return structure_walker(content).walk();
|
||||
}
|
||||
|
||||
std::vector<breadcrumb::possibility>
|
||||
metadata::possibility_provider(const std::vector<section_key_t>& path)
|
||||
{
|
||||
std::vector<breadcrumb::possibility> retval;
|
||||
auto curr_node = lnav::document::hier_node::lookup_path(
|
||||
this->m_sections_root.get(), path);
|
||||
if (curr_node) {
|
||||
auto* parent_node = curr_node.value()->hn_parent;
|
||||
|
||||
if (parent_node != nullptr) {
|
||||
for (const auto& sibling : parent_node->hn_named_children) {
|
||||
retval.template emplace_back(sibling.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
} // namespace document
|
||||
} // namespace lnav
|
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef lnav_attr_line_breadcrumbs_hh
|
||||
#define lnav_attr_line_breadcrumbs_hh
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/attr_line.hh"
|
||||
#include "base/file_range.hh"
|
||||
#include "breadcrumb.hh"
|
||||
#include "intervaltree/IntervalTree.h"
|
||||
#include "mapbox/variant.hpp"
|
||||
#include "optional.hpp"
|
||||
|
||||
namespace lnav {
|
||||
namespace document {
|
||||
|
||||
using section_key_t = mapbox::util::variant<std::string, size_t>;
|
||||
using section_interval_t = interval_tree::Interval<file_off_t, section_key_t>;
|
||||
using sections_tree_t = interval_tree::IntervalTree<file_off_t, section_key_t>;
|
||||
|
||||
struct hier_node {
|
||||
hier_node* hn_parent{nullptr};
|
||||
file_off_t hn_start{0};
|
||||
size_t hn_line_number{0};
|
||||
std::multimap<std::string, hier_node*> hn_named_children;
|
||||
std::vector<std::unique_ptr<hier_node>> hn_children;
|
||||
|
||||
nonstd::optional<hier_node*> lookup_child(section_key_t key) const;
|
||||
|
||||
nonstd::optional<size_t> find_line_number(const std::string& str) const
|
||||
{
|
||||
auto iter = this->hn_named_children.find(str);
|
||||
if (iter != this->hn_named_children.end()) {
|
||||
return nonstd::make_optional(iter->second->hn_line_number);
|
||||
}
|
||||
|
||||
return nonstd::nullopt;
|
||||
}
|
||||
|
||||
nonstd::optional<size_t> find_line_number(size_t index) const
|
||||
{
|
||||
if (index < this->hn_children.size()) {
|
||||
return nonstd::make_optional(
|
||||
this->hn_children[index]->hn_line_number);
|
||||
}
|
||||
return nonstd::nullopt;
|
||||
}
|
||||
|
||||
bool is_named_only() const
|
||||
{
|
||||
return this->hn_children.size() == this->hn_named_children.size();
|
||||
}
|
||||
|
||||
static nonstd::optional<const hier_node*> lookup_path(
|
||||
const hier_node* root, const std::vector<section_key_t>& path);
|
||||
|
||||
template<typename F>
|
||||
static void depth_first(hier_node* root, F func)
|
||||
{
|
||||
for (auto& child : root->hn_children) {
|
||||
depth_first(child.get(), func);
|
||||
}
|
||||
func(root);
|
||||
}
|
||||
};
|
||||
|
||||
struct metadata {
|
||||
sections_tree_t m_sections_tree;
|
||||
std::unique_ptr<hier_node> m_sections_root;
|
||||
|
||||
std::vector<breadcrumb::possibility> possibility_provider(
|
||||
const std::vector<section_key_t>& path);
|
||||
};
|
||||
|
||||
metadata discover_metadata(const attr_line_t& al);
|
||||
|
||||
metadata discover_structure(string_fragment sf);
|
||||
|
||||
} // namespace document
|
||||
} // namespace lnav
|
||||
|
||||
#endif
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,525 @@
|
||||
# lnav
|
||||
|
||||
A fancy log file viewer for the terminal.
|
||||
|
||||
## Overview
|
||||
|
||||
The Logfile Navigator, **lnav**, is an enhanced log file viewer that
|
||||
takes advantage of any semantic information that can be gleaned from
|
||||
the files being viewed, such as timestamps and log levels. Using this
|
||||
extra semantic information, lnav can do things like interleaving
|
||||
messages from different files, generate histograms of messages over
|
||||
time, and providing hotkeys for navigating through the file. It is
|
||||
hoped that these features will allow the user to quickly and
|
||||
efficiently zero in on problems.
|
||||
|
||||
## Opening Paths/URLs
|
||||
|
||||
The main arguments to lnav are the files, directories, glob patterns,
|
||||
or URLs to be viewed. If no arguments are given, the default syslog
|
||||
file for your system will be opened. These arguments will be polled
|
||||
periodically so that any new data or files will be automatically
|
||||
loaded. If a previously loaded file is removed or replaced, it will
|
||||
be closed and the replacement opened.
|
||||
|
||||
Note: When opening SFTP URLs, if the password is not provided for the
|
||||
host, the SSH agent can be used to do authentication.
|
||||
|
||||
## Options
|
||||
|
||||
Lnav takes a list of files to view and/or you can use the flag
|
||||
arguments to load well-known log files, such as the syslog log
|
||||
files. The flag arguments are:
|
||||
|
||||
* `-a` Load all of the most recent log file types.
|
||||
* `-r` Recursively load files from the given directory hierarchies.
|
||||
* `-R` Load older rotated log files as well.
|
||||
|
||||
When using the flag arguments, lnav will look for the files relative
|
||||
to the current directory and its parent directories. In other words,
|
||||
if you are working within a directory that has the well-known log
|
||||
files, those will be preferred over any others.
|
||||
|
||||
If you do not want the default syslog file to be loaded when
|
||||
no files are specified, you can pass the `-N` flag.
|
||||
|
||||
Any files given on the command-line are scanned to determine their log
|
||||
file format and to create an index for each line in the file. You do
|
||||
not have to manually specify the log file format. The currently
|
||||
supported formats are: syslog, apache, strace, tcsh history, and
|
||||
generic log files with timestamps.
|
||||
|
||||
Lnav will also display data piped in on the standard input. The
|
||||
following options are available when doing so:
|
||||
|
||||
* `-t` Prepend timestamps to the lines of data being read in
|
||||
on the standard input.
|
||||
* `-w file` Write the contents of the standard input to this file.
|
||||
|
||||
To automatically execute queries or lnav commands after the files
|
||||
have been loaded, you can use the following options:
|
||||
|
||||
* `-c cmd` A command, query, or file to execute. The first character
|
||||
determines the type of operation: a colon (`:`) is used for the
|
||||
built-in commands; a semi-colon (`;`) for SQL queries; and a
|
||||
pipe symbol (`|`) for executing a file containing other
|
||||
commands. For example, to open the file "foo.log" and go
|
||||
to the tenth line in the file, you can do:
|
||||
|
||||
```shell
|
||||
lnav -c ':goto 10' foo.log
|
||||
```
|
||||
|
||||
This option can be given multiple times to execute multiple
|
||||
operations in sequence.
|
||||
* `-f file` A file that contains commands, queries, or files to execute.
|
||||
This option is a shortcut for `-c '|file'`. You can use a dash
|
||||
(`-`) to execute commands from the standard input.
|
||||
|
||||
To execute commands/queries without opening the interactive text UI,
|
||||
you can pass the `-n` option. This combination of options allows you to
|
||||
write scripts for processing logs with lnav. For example, to get a list
|
||||
of IP addresses that dhclient has bound to in CSV format:
|
||||
|
||||
```lnav
|
||||
#! /usr/bin/lnav -nf
|
||||
|
||||
# Usage: dhcp_ip.lnav /var/log/messages
|
||||
# Only include lines that look like:
|
||||
# Apr 29 00:31:56 example-centos5 dhclient: bound to 10.1.10.103 -- renewal in 9938 seconds.
|
||||
|
||||
:filter-in dhclient: bound to
|
||||
|
||||
# The log message parser will extract the IP address
|
||||
# as col_0, so we select that and alias it to "dhcp_ip".
|
||||
;SELECT DISTINCT col_0 AS dhcp_ip FROM logline;
|
||||
|
||||
# Finally, write the results of the query to stdout.
|
||||
:write-csv-to -
|
||||
```
|
||||
|
||||
## Display
|
||||
|
||||
The main part of the display shows the log lines from the files interleaved
|
||||
based on time-of-day. New lines are automatically loaded as they are appended
|
||||
to the files and, if you are viewing the bottom of the files, lnav will scroll
|
||||
down to display the new lines, much like `tail -f`.
|
||||
|
||||
On color displays, the lines will be highlighted as follows:
|
||||
|
||||
* Errors will be colored in ${ansi_red}red${ansi_norm};
|
||||
* warnings will be ${ansi_yellow}yellow${ansi_norm};
|
||||
* boundaries between days will be ${ansi_underline}underlined${ansi_norm}; and
|
||||
* various color highlights will be applied to: IP addresses, SQL keywords,
|
||||
XML tags, file and line numbers in Java backtraces, and quoted strings.
|
||||
|
||||
To give you an idea of where you are spatially, the right side of the
|
||||
display has a proportionally sized 'scroll bar' that indicates your
|
||||
current position in the files. The scroll bar will also show areas of
|
||||
the file where warnings or errors are detected by coloring the bar
|
||||
yellow or red, respectively. Tick marks will also be added to the
|
||||
left and right hand side of the bar, for search hits and bookmarks.
|
||||
|
||||
A bar on the left side is color coded and broken up to indicate which
|
||||
messages are from the same file. Pressing the left-arrow or `h` will
|
||||
reveal the source file names for each message and pressing again will
|
||||
show the full paths.
|
||||
|
||||
When at the bottom of the log view, a summary line will be displayed on the
|
||||
right-hand-side to give you some more information about your logs, including:
|
||||
how long ago the last message was generated, the number of log files, the
|
||||
error rate, and how much time the logs cover. The error rate display shows
|
||||
the errors-per-minute over the last five minutes. A bar chart is also
|
||||
overlaid on the "Error rate" label to show the error rate over the past ten
|
||||
seconds. For example, if there have not been many errors in the past five
|
||||
minutes and there is a sudden spike, the bar chart will fill up completely.
|
||||
But, if there has been a steady stream of errors, then the chart will only
|
||||
partially fill based on the recent error frequency.
|
||||
|
||||
Above and below the main body are status lines that display:
|
||||
|
||||
* the current time;
|
||||
* the name of the file the top line was pulled from;
|
||||
* the log format for the top line;
|
||||
* the current view;
|
||||
* the line number for the top line in the display;
|
||||
* the current search hit, the total number of hits, and the search term;
|
||||
|
||||
If the view supports filtering, there will be a status line showing the
|
||||
following:
|
||||
|
||||
* the number of enabled filters and the total number of filters;
|
||||
* the number of lines not displayed because of filtering.
|
||||
|
||||
To edit the filters, you can press TAB to change the focus from the main
|
||||
view to the filter editor. The editor allows you to create, enable/disable,
|
||||
and delete filters easily.
|
||||
|
||||
Finally, the last line on the display is where you can enter search
|
||||
patterns and execute internal commands, such as converting a
|
||||
unix-timestamp into a human-readable date. The command-line is
|
||||
implemented using the readline library, so the usual set of keyboard
|
||||
shortcuts are available. Most commands and searches also support
|
||||
tab-completion.
|
||||
|
||||
The body of the display is also used to display other content, such
|
||||
as: the help file, histograms of the log messages over time, and
|
||||
SQL results. The views are organized into a stack so that any time
|
||||
you activate a new view with a key press or command, the new view
|
||||
is pushed onto the stack. Pressing the same key again will pop the
|
||||
view off of the stack and return you to the previous view. Note
|
||||
that you can always use `q` to pop the top view off of the stack.
|
||||
|
||||
## Default Key Bindings
|
||||
|
||||
### Views
|
||||
|
||||
| Key(s) | Action |
|
||||
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **?** | View/leave this help message. |
|
||||
| **q** | Leave the current view or quit the program when in the log file view. |
|
||||
| Q | Similar to `q`, except it will try to sync the top time between the current and former views. For example, when leaving the spectrogram view with `Q`, the top time in that view will be matched to the top time in the log view. |
|
||||
| TAB | Toggle focusing on the filter editor or the main view. |
|
||||
| a/A | Restore the view that was previously popped with `q`/`Q`. The `A` hotkey will try to match the top times between the two views. |
|
||||
| X | Close the current text file or log file. |
|
||||
|
||||
### Spatial Navigation
|
||||
|
||||
| Key(s) | Action |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| g/Home | Move to the top of the file. |
|
||||
| G/End | Move to the end of the file. If the view is already at the end, it will move to the last line. |
|
||||
| SPACE/PgDn | Move down a page. |
|
||||
| b/PgUp | Move up a page. |
|
||||
| j/↓ | Move down a line. |
|
||||
| k/↑ | Move up a line. |
|
||||
| h/← | Move to the left. In the log view, moving left will reveal the source log file names for each line. Pressing again will reveal the full path. |
|
||||
| l/→ | Move to the right. |
|
||||
| H/Shift ← | Move to the left by a smaller increment. |
|
||||
| L/Shift → | Move to the right by a smaller increment. |
|
||||
| e/E | Move to the next/previous error. |
|
||||
| w/W | Move to the next/previous warning. |
|
||||
| n/N | Move to the next/previous search hit. When pressed repeatedly within a short time, the view will move at least a full page at a time instead of moving to the next hit. |
|
||||
| f/F | Move to the next/previous file. In the log view, this moves to the next line from a different file. In the text view, this rotates the view to the next file. |
|
||||
| >/< | Move horizontally to the next/previous search hit. |
|
||||
| o/O | Move forward/backward to the log message with a matching 'operation ID' (opid) field. |
|
||||
| u/U | Move forward/backward through any user bookmarks you have added using the 'm' key. This hotkey will also jump to the start of any log partitions that have been created with the 'partition-name' command. |
|
||||
| s/S | Move to the next/previous "slow down" in the log message rate. A slow down is detected by measuring how quickly the message rate has changed over the previous several messages. For example, if one message is logged every second for five seconds and then the last message arrives five seconds later, the last message will be highlighted as a slow down. |
|
||||
| {/} | Move to the previous/next location in history. Whenever you jump to a new location in the view, the location will be added to the history. The history is not updated when using only the arrow keys. |
|
||||
|
||||
### Chronological Navigation
|
||||
|
||||
| Key(s) | Action |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| d/D | Move forward/backward 24 hours from the current position in the log file. |
|
||||
| 1-6/Shift 1-6 | Move to the next/previous n'th ten minute of the hour. For example, '4' would move to the first log line in the fortieth minute of the current hour in the log. And, '6' would move to the next hour boundary. |
|
||||
| 7/8 | Move to the previous/next minute. |
|
||||
| 0/Shift 0 | Move to the next/previous day boundary. |
|
||||
| r/R | Move forward/backward based on the relative time that was last used with the 'goto' command. For example, executing ':goto a minute later' will move the log view forward a minute and then pressing 'r' will move it forward a minute again. Pressing 'R' will then move the view in the opposite direction, so backwards a minute. |
|
||||
|
||||
### Bookmarks
|
||||
|
||||
| Key(s) | Action |
|
||||
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| m | Mark/unmark the line at the top of the display. The line will be highlighted with reverse video to indicate that it is a user bookmark. You can use the `u` hotkey to iterate through marks you have added. |
|
||||
| M | Mark/unmark all the lines between the top of the display and the last line marked/unmarked. |
|
||||
| J | Mark/unmark the next line after the previously marked line. |
|
||||
| K | Like `J` except it toggles the mark on the previous line. |
|
||||
| c | Copy the marked text to the X11 selection buffer or OS X clipboard. |
|
||||
| C | Clear all marked lines. |
|
||||
|
||||
### Display options
|
||||
|
||||
| Key(s) | Action |
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| P | Switch to/from the pretty-printed view of the log or text files currently displayed. In this view, structured data, such as XML, will be reformatted to make it easier to read. |
|
||||
| t | Switch to/from the text file view. The text file view is for any files that are not recognized as log files. |
|
||||
| = | Pause/unpause loading of new file data. |
|
||||
| Ctrl-L | (Lo-fi mode) Exit screen-mode and write the displayed log lines in plain text to the terminal until a key is pressed. Useful for copying long lines from the terminal without picking up any of the extra decorations. |
|
||||
| T | Toggle the display of the "elapsed time" column that shows the time elapsed since the beginning of the logs or the offset from the previous bookmark. Sharp changes in the message rate are highlighted by coloring the separator between the time column and the log message. A red highlight means the message rate has slowed down and green means it has sped up. You can use the "s/S" hotkeys to scan through the slow downs. |
|
||||
| i | View/leave a histogram of the log messages over time. The histogram counts the number of displayed log lines for each bucket of time. The bars are layed out horizontally with colored segments representing the different log levels. You can use the `z` hotkey to change the size of the time buckets (e.g. ten minutes, one hour, one day). |
|
||||
| I | Switch between the log and histogram views while keeping the time displayed at the top of each view in sync. For example, if the top line in the log view is "11:40", hitting `I` will switch to the histogram view and scrolled to display "11:00" at the top (if the zoom level is hours). |
|
||||
| z/Shift Z | Zoom in or out one step in the histogram view. |
|
||||
| v | Switch to/from the SQL result view. |
|
||||
| V | Switch between the log and SQL result views while keeping the top line number in the log view in sync with the log_line column in the SQL view. For example, doing a query that selects for "log_idle_msecs" and "log_line", you can move the top of the SQL view to a line and hit 'V' to switch to the log view and move to the line number that was selected in the "log_line" column. If there is no "log_line" column, lnav will find the first column with a timestamp and move to corresponding time in the log view. |
|
||||
| TAB/Shift TAB | In the SQL result view, cycle through the columns that are graphed. Initially, all number values are displayed in a stacked graph. Pressing TAB will change the display to only graph the first column. Repeatedly pressing TAB will cycle through the columns until they are all graphed again. |
|
||||
| p | In the log view: enable or disable the display of the fields that the log message parser knows about or has discovered. This overlay is temporarily enabled when the semicolon key (;) is pressed so that it is easier to write queries. |
|
||||
| | In the DB view: enable or disable the display of values in columns containing JSON-encoded values in the top row. The overlay will display the JSON-Pointer reference and value for all fields in the JSON data. |
|
||||
| CTRL-W | Toggle word-wrapping. |
|
||||
| CTRL-P | Show/hide the data preview panel that may be opened when entering commands or SQL queries. |
|
||||
| CTRL-F | Toggle the enabled/disabled state of all filters in the current view. |
|
||||
| x | Toggle the hiding of log message fields. The hidden fields will be replaced with three bullets and highlighted in yellow. |
|
||||
| F2 | Toggle mouse support. |
|
||||
|
||||
### Query
|
||||
|
||||
| Key(s) | Action |
|
||||
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **/**_regexp_ | Start a search for the given regular expression. The search is live, so when there is a pause in typing, the currently running search will be canceled and a new one started. The first ten lines that match the search will be displayed in the preview window at the bottom of the view. History is maintained for your searches so you can rerun them easily. Words that are currently displayed are also available for tab-completion, so you can easily search for values without needing to copy-and-paste the string. If there is an error encountered while trying to interpret the expression, the error will be displayed in red on the status line. While the search is active, the 'hits' field in the status line will be green, when finished it will turn back to black. |
|
||||
| **:**<command> | Execute an internal command. The commands are listed below. History is also supported in this context as well as tab-completion for commands and some arguments. The result of the command replaces the command you typed. |
|
||||
| **;**<sql> | Execute an SQL query. Most supported log file formats provide a sqlite virtual table backend that can be used in queries. See the SQL section below for more information. |
|
||||
| **|**<script> [arg1...] | Execute an lnav script contained in a format directory (e.g. \~/.lnav/formats/default). The script can contain lines starting with `:`, `;`, or `\|` to execute commands, SQL queries or execute other files in lnav. Any values after the script name are treated as arguments can be referenced in the script using `\$1`, `\$2`, and so on, like in a shell script. |
|
||||
| CTRL+], ESCAPE | Abort command-line entry started with `/`, `:`, `;`, or `\|`. |
|
||||
|
||||
Note: The regular expression format used by is PCRE
|
||||
(Perl-Compatible Regular Expressions). For example,
|
||||
if you wanted to search for ethernet device names,
|
||||
regardless of their ID number, you can type:
|
||||
|
||||
eth\\d+
|
||||
|
||||
You can find more information about Perl regular
|
||||
expressions at:
|
||||
|
||||
http://perldoc.perl.org/perlre.html
|
||||
|
||||
If the search string is not valid PCRE, a search
|
||||
is done for the exact string instead of doing a
|
||||
regex search.
|
||||
|
||||
|
||||
## Session
|
||||
|
||||
| Key(s) | Action |
|
||||
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| CTRL-R | Reset the session state. This will save the current session state (filters, highlights) and then reset the state to the factory default. |
|
||||
|
||||
## Filter Editor
|
||||
|
||||
The following hotkeys are only available when the focus is on the filter
|
||||
editor. You can change the focus by pressing TAB.
|
||||
|
||||
| Key(s) | Action |
|
||||
| ------------- | ------------------------------------------------------------------- |
|
||||
| q | Switch the focus back to the main view. |
|
||||
| j/↓ | Select the next filter. |
|
||||
| k/↑ | Select the previous filter. |
|
||||
| o | Create a new "out" filter. |
|
||||
| i | Create a new "in" filter . |
|
||||
| SPACE | Toggle the enabled/disabled state of the currently selected filter. |
|
||||
| t | Toggle the type of filter between "in" and "out". |
|
||||
| ENTER | Edit the selected filter. |
|
||||
| D | Delete the selected filter. |
|
||||
|
||||
## Mouse Support (experimental)
|
||||
|
||||
If you are using Xterm, or a compatible terminal, you can use the mouse to
|
||||
mark lines of text and move the view by grabbing the scrollbar.
|
||||
|
||||
NOTE: You need to manually enable this feature by setting the LNAV_EXP
|
||||
environment variable to "mouse". F2 toggles mouse support.
|
||||
|
||||
## SQL Queries (experimental)
|
||||
|
||||
Lnav has support for performing SQL queries on log files using the
|
||||
Sqlite3 "virtual" table feature. For all supported log file types,
|
||||
lnav will create tables that can be queried using the subset of SQL
|
||||
that is supported by Sqlite3. For example, to get the top ten URLs
|
||||
being accessed in any loaded Apache log files, you can execute:
|
||||
|
||||
```lnav
|
||||
;SELECT cs_uri_stem, count(*) AS total FROM access_log
|
||||
GROUP BY cs_uri_stem ORDER BY total DESC LIMIT 10;
|
||||
```
|
||||
|
||||
The query result view shows the results and graphs any numeric
|
||||
values found in the result, much like the histogram view.
|
||||
|
||||
The builtin set of log tables are listed below. Note that only the
|
||||
log messages that match a particular format can be queried by a
|
||||
particular table. You can find the file format and table name for
|
||||
the top log message by looking in the upper right hand corner of the
|
||||
log file view.
|
||||
|
||||
Some commonly used format tables are:
|
||||
|
||||
| Name | Description |
|
||||
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| access_log | Apache common access log format |
|
||||
| syslog_log | Syslog format |
|
||||
| strace_log | Strace log format |
|
||||
| generic_log | 'Generic' log format. This table contains messages from files that have a very simple format with a leading timestamp followed by the message. |
|
||||
|
||||
NOTE: You can get a dump of the schema for the internal tables, and
|
||||
any attached databases, by running the `.schema` SQL command.
|
||||
|
||||
The columns available for the top log line in the view will
|
||||
automatically be displayed after pressing the semicolon (`;`) key.
|
||||
All log tables contain at least the following columns:
|
||||
|
||||
| Column | Description |
|
||||
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| log_line | The line number in the file, starting at zero. |
|
||||
| log_part | The name of the partition. You can change this column using an UPDATE SQL statement or with the 'partition-name' command. After a value is set, the following log messages will have the same partition name up until another name is set. |
|
||||
| log_time | The time of the log entry. |
|
||||
| log_idle_msecs | The amount of time, in milliseconds, between the current log message and the previous one. |
|
||||
| log_level | The log level (e.g. info, error, etc...). |
|
||||
| log_mark | The bookmark status for the line. This column can be written to using an UPDATE query. |
|
||||
| log_path | The full path to the file. |
|
||||
| log_text | The raw line of text. Note that this column is not included in the result of a 'select *', but it does exist. |
|
||||
|
||||
The following tables include the basic columns as listed above and
|
||||
include a few more columns since the log file format is more
|
||||
structured.
|
||||
|
||||
* `syslog_log`
|
||||
|
||||
| Column | Description |
|
||||
| ------------ | ---------------------------------------------------- |
|
||||
| log_hostname | The hostname the message was received from. |
|
||||
| log_procname | The name of the process that sent the message. |
|
||||
| log_pid | The process ID of the process that sent the message. |
|
||||
|
||||
* `access_log` (The column names are the same as those in the
|
||||
Microsoft LogParser tool.)
|
||||
|
||||
| Column | Description |
|
||||
| ------------- | ----------------------------------------- |
|
||||
| c_ip | The client IP address. |
|
||||
| cs_username | The client user name. |
|
||||
| cs_method | The HTTP method. |
|
||||
| cs_uri_stem | The stem portion of the URI. |
|
||||
| cs_uri_query | The query portion of the URI. |
|
||||
| cs_version | The HTTP version string. |
|
||||
| sc_status | The status number returned to the client. |
|
||||
| sc_bytes | The number of bytes sent to the client. |
|
||||
| cs_referrer | The URL of the referring page. |
|
||||
| cs_user_agent | The user agent string. |
|
||||
|
||||
* `strace_log` (Currently, you need to run strace with the `-tt -T`
|
||||
options so there are timestamps for each function call.)
|
||||
|
||||
| Column | Description |
|
||||
| ----------- | ---------------------------------------- |
|
||||
| funcname | The name of the syscall. |
|
||||
| result | The result code. |
|
||||
| duration | The amount of time spent in the syscall. |
|
||||
| arg0 - arg9 | The arguments passed to the syscall. |
|
||||
|
||||
These tables are created dynamically and not stored in memory or on
|
||||
disk. If you would like to persist some information from the tables,
|
||||
you can attach another database and create tables in that database.
|
||||
For example, if you wanted to save the results from the earlier
|
||||
example of a top ten query into the "/tmp/topten.db" file, you can do:
|
||||
|
||||
```lnav
|
||||
;ATTACH DATABASE '/tmp/topten.db' AS topten;
|
||||
;CREATE TABLE topten.foo AS SELECT cs_uri_stem, count(*) AS total
|
||||
FROM access_log GROUP BY cs_uri_stem ORDER BY total DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
## Dynamic logline Table (experimental)
|
||||
|
||||
(NOTE: This feature is still very new and not completely reliable yet,
|
||||
use with care.)
|
||||
|
||||
For log formats that lack message structure, lnav can parse the log
|
||||
message and attempt to extract any data fields that it finds. This
|
||||
feature is available through the `logline` log table. This table is
|
||||
dynamically created and defined based on the message at the top of
|
||||
the log view. For example, given the following log message from "sudo",
|
||||
lnav will create the "logline" table with columns for "TTY", "PWD",
|
||||
"USER", and "COMMAND":
|
||||
|
||||
```
|
||||
May 24 06:48:38 Tim-Stacks-iMac.local sudo[76387]: stack : TTY=ttys003 ; PWD=/Users/stack/github/lbuild ; USER=root ; COMMAND=/bin/echo Hello, World!
|
||||
```
|
||||
|
||||
Queries executed against this table will then only return results for
|
||||
other log messages that have the same format. So, if you were to
|
||||
execute the following query while viewing the above line, you might
|
||||
get the following results:
|
||||
|
||||
```lnav
|
||||
;SELECT USER,COMMAND FROM logline;
|
||||
```
|
||||
|
||||
| USER | COMMAND |
|
||||
| ---- | ------------------------- |
|
||||
| root | /bin/echo Hello, World! |
|
||||
| mal | /bin/echo Goodbye, World! |
|
||||
|
||||
The log parser works by examining each message for key/value pairs
|
||||
separated by an equal sign (=) or a colon (:). For example, in the
|
||||
previous example of a "sudo" message, the parser sees the "USER=root"
|
||||
string as a pair where the key is "USER" and the value is "root".
|
||||
If no pairs can be found, then anything that looks like a value is
|
||||
extracted and assigned a numbered column. For example, the following
|
||||
line is from "dhcpd":
|
||||
|
||||
```
|
||||
Sep 16 22:35:57 drill dhcpd: DHCPDISCOVER from 00:16:ce:54:4e:f3 via hme3
|
||||
```
|
||||
|
||||
In this case, the lnav parser recognizes that "DHCPDISCOVER", the MAC
|
||||
address and the "hme3" device name are values and not normal words. So,
|
||||
it builds a table with three columns for each of these values. The
|
||||
regular words in the message, like "from" and "via", are then used to
|
||||
find other messages with a similar format.
|
||||
|
||||
If you would like to execute queries against log messages of different
|
||||
formats at the same time, you can use the 'create-logline-table' command
|
||||
to permanently create a table using the top line of the log view as a
|
||||
template.
|
||||
|
||||
## Other SQL Features
|
||||
|
||||
Environment variables can be used in SQL statements by prefixing the
|
||||
variable name with a dollar-sign (\$). For example, to read the value of
|
||||
the `HOME` variable, you can do:
|
||||
|
||||
```lnav
|
||||
;SELECT \$HOME;
|
||||
```
|
||||
|
||||
To select the syslog messages that have a hostname field that is equal
|
||||
to the `HOSTNAME` variable:
|
||||
|
||||
```lnav
|
||||
;SELECT * FROM syslog_log WHERE log_hostname = \$HOSTNAME;
|
||||
```
|
||||
|
||||
NOTE: Variable substitution is done for fields in the query and is not
|
||||
a plain text substitution. For example, the following statement
|
||||
WILL NOT WORK:
|
||||
|
||||
```lnav
|
||||
;SELECT * FROM \$TABLE_NAME; -- Syntax error
|
||||
```
|
||||
|
||||
Access to lnav's environment variables is also available via the "environ"
|
||||
table. The table has two columns (name, value) and can be read and written
|
||||
to using SQL SELECT, INSERT, UPDATE, and DELETE statements. For example,
|
||||
to set the "FOO" variable to the value "BAR":
|
||||
|
||||
```lnav
|
||||
;INSERT INTO environ SELECT 'FOO', 'BAR';
|
||||
```
|
||||
|
||||
As a more complex example, you can set the variable "LAST" to the last
|
||||
syslog line number by doing:
|
||||
|
||||
```lnav
|
||||
;INSERT INTO environ SELECT 'LAST', (SELECT max(log_line) FROM syslog_log);
|
||||
```
|
||||
|
||||
A delete will unset the environment variable:
|
||||
|
||||
```lnav
|
||||
;DELETE FROM environ WHERE name='LAST';
|
||||
```
|
||||
|
||||
The table allows you to easily use the results of a SQL query in lnav
|
||||
commands, which is especially useful when scripting lnav.
|
||||
|
||||
## Contact
|
||||
|
||||
For more information, visit the lnav website at:
|
||||
|
||||
http://lnav.org
|
||||
|
||||
For support questions, email:
|
||||
|
||||
lnav@googlegroups.com
|
||||
support@lnav.org
|
@ -0,0 +1,523 @@
|
||||
/**
|
||||
* Copyright (c) 2022, Timothy Stack
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
* * Neither the name of Timothy Stack nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "md2attr_line.hh"
|
||||
|
||||
#include "base/attr_line.builder.hh"
|
||||
#include "base/itertools.hh"
|
||||
#include "base/lnav_log.hh"
|
||||
#include "pcrepp/pcrepp.hh"
|
||||
#include "readline_highlighters.hh"
|
||||
#include "view_curses.hh"
|
||||
|
||||
using namespace lnav::roles::literals;
|
||||
|
||||
void
|
||||
md2attr_line::flush_footnotes()
|
||||
{
|
||||
if (this->ml_footnotes.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& block_text = this->ml_blocks.back();
|
||||
auto longest_foot = this->ml_footnotes
|
||||
| lnav::itertools::map(&attr_line_t::utf8_length_or_length)
|
||||
| lnav::itertools::max(0);
|
||||
size_t index = 1;
|
||||
|
||||
block_text.append("\n");
|
||||
for (auto& foot : this->ml_footnotes) {
|
||||
block_text.append(lnav::string::attrs::preformatted(" "))
|
||||
.append("\u258c"_footnote_border)
|
||||
.append(lnav::roles::footnote_text(
|
||||
index < 10 && this->ml_footnotes.size() >= 10 ? " " : ""))
|
||||
.append(lnav::roles::footnote_text(
|
||||
fmt::format(FMT_STRING("[{}] - "), index)))
|
||||
.append(foot.pad_to(longest_foot))
|
||||
.append("\n");
|
||||
index += 1;
|
||||
}
|
||||
this->ml_footnotes.clear();
|
||||
}
|
||||
|
||||
Result<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{
|
||||
code_detail->lang.text,
|
||||
0,
|
||||
(int) code_detail->lang.size,
|
||||
};
|
||||
if (lang_sf == "lnav") {
|
||||
readline_lnav_highlighter(block_text, block_text.length());
|
||||
}
|
||||
|
||||
auto code_lines = block_text.rtrim().split_lines();
|
||||
auto max_width = code_lines
|
||||
| lnav::itertools::map(&attr_line_t::utf8_length_or_length)
|
||||
| lnav::itertools::max(0);
|
||||
attr_line_t padded_text;
|
||||
|
||||
for (auto& line : code_lines) {
|
||||
line.pad_to(std::max(max_width + 4, ssize_t{40}))
|
||||
.with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
|
||||
padded_text.append(lnav::string::attrs::preformatted(" "))
|
||||
.append("\u258c"_code_border)
|
||||
.append(line)
|
||||
.append("\n");
|
||||
}
|
||||
padded_text.with_attr_for_all(SA_PREFORMATTED.value());
|
||||
last_block.append("\n").append(padded_text);
|
||||
} else if (bl.is<block_quote>()) {
|
||||
text_wrap_settings tws = {0, 60};
|
||||
attr_line_t wrapped_text;
|
||||
|
||||
wrapped_text.append(block_text.rtrim(), &tws);
|
||||
auto quoted_lines = wrapped_text.split_lines();
|
||||
auto max_width = quoted_lines
|
||||
| lnav::itertools::map(&attr_line_t::utf8_length_or_length)
|
||||
| lnav::itertools::max(0);
|
||||
attr_line_t padded_text;
|
||||
|
||||
for (auto& line : quoted_lines) {
|
||||
line.pad_to(max_width + 1)
|
||||
.with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_TEXT));
|
||||
padded_text.append(" ")
|
||||
.append("\u258c"_quote_border)
|
||||
.append(line)
|
||||
.append("\n");
|
||||
}
|
||||
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()) {
|
||||
max_col_sizes[lpc] = tab.t_headers[lpc].utf8_length_or_length();
|
||||
tab.t_headers[lpc].with_attr_for_all(
|
||||
VC_ROLE.value(role_t::VCR_TABLE_HEADER));
|
||||
}
|
||||
}
|
||||
for (const auto& row : tab.t_rows) {
|
||||
for (size_t lpc = 0; lpc < table_detail->col_count; lpc++) {
|
||||
if (lpc >= row.r_columns.size()) {
|
||||
continue;
|
||||
}
|
||||
auto col_len = row.r_columns[lpc].utf8_length_or_length();
|
||||
if (col_len > max_col_sizes[lpc]) {
|
||||
max_col_sizes[lpc] = col_len;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto col_sizes
|
||||
= max_col_sizes | lnav::itertools::map([](const auto& elem) {
|
||||
return std::min(elem, ssize_t{50});
|
||||
});
|
||||
auto full_width = col_sizes | lnav::itertools::sum();
|
||||
text_wrap_settings tws = {0, 50};
|
||||
std::vector<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++) {
|
||||
size_t col = 0;
|
||||
for (const auto& cell : cells) {
|
||||
block_text.append(" ");
|
||||
if (line_index < cell.cl_lines.size()) {
|
||||
block_text.append(cell.cl_lines[line_index]);
|
||||
block_text.append(
|
||||
col_sizes[col]
|
||||
- cell.cl_lines[line_index].utf8_length_or_length(),
|
||||
' ');
|
||||
} else {
|
||||
block_text.append(col_sizes[col], ' ');
|
||||
}
|
||||
col += 1;
|
||||
}
|
||||
block_text.append("\n")
|
||||
.append(lnav::roles::table_border(
|
||||
repeat("\u2550", full_width + col_sizes.size())))
|
||||
.append("\n");
|
||||
}
|
||||
for (const auto& row : tab.t_rows) {
|
||||
cells.clear();
|
||||
max_cell_lines = 0;
|
||||
for (size_t lpc = 0; lpc < row.r_columns.size(); lpc++) {
|
||||
tws.with_width(col_sizes[lpc]);
|
||||
|
||||
attr_line_t td_block;
|
||||
td_block.append(row.r_columns[lpc], &tws);
|
||||
cells.emplace_back(td_block.rtrim().split_lines());
|
||||
if (cells.back().cl_lines.size() > max_cell_lines) {
|
||||
max_cell_lines = cells.back().cl_lines.size();
|
||||
}
|
||||
}
|
||||
for (size_t line_index = 0; line_index < max_cell_lines;
|
||||
line_index++) {
|
||||
size_t col = 0;
|
||||
for (const auto& cell : cells) {
|
||||
block_text.append(" ");
|
||||
if (line_index < cell.cl_lines.size()) {
|
||||
block_text.append(cell.cl_lines[line_index]);
|
||||
if (col < col_sizes.size() - 1) {
|
||||
block_text.append(
|
||||
col_sizes[col]
|
||||
- cell.cl_lines[line_index]
|
||||
.utf8_length_or_length(),
|
||||
' ');
|
||||
}
|
||||
} else if (col < col_sizes.size() - 1) {
|
||||
block_text.append(col_sizes[col], ' ');
|
||||
}
|
||||
col += 1;
|
||||
}
|
||||
block_text.append("\n");
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
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_QUOTED_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()),
|
||||
};
|
||||
last_block.with_attr({
|
||||
lr,
|
||||
VC_STYLE.value(A_ITALIC),
|
||||
});
|
||||
} 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(A_BOLD),
|
||||
});
|
||||
} 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);
|
||||
|
||||
this->append_url_footnote(href_str);
|
||||
} else if (sp.is<MD_SPAN_IMG_DETAIL*>()) {
|
||||
auto* img_detail = sp.get<MD_SPAN_IMG_DETAIL*>();
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
default: {
|
||||
static const pcrepp REPL_RE(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();
|
||||
}
|
||||
|
||||
pcre_input pi(sf);
|
||||
pcre_context_static<30> pc;
|
||||
|
||||
while (REPL_RE.match(pc, pi)) {
|
||||
auto prev = string_fragment{
|
||||
sf.sf_string,
|
||||
(int) pi.pi_offset,
|
||||
pc.all()->c_begin,
|
||||
};
|
||||
|
||||
last_block.append(prev);
|
||||
|
||||
auto matched = pi.get_string_fragment(pc.all());
|
||||
|
||||
if (matched == "--") {
|
||||
last_block.append("\u2013");
|
||||
} else if (matched == "---") {
|
||||
last_block.append("\u2014");
|
||||
} else if (matched.startswith(":")) {
|
||||
auto em_iter
|
||||
= emojis.em_shortname2emoji.find(matched.to_string());
|
||||
if (em_iter == emojis.em_shortname2emoji.end()) {
|
||||
last_block.append(matched);
|
||||
} else {
|
||||
last_block.append(em_iter->second.get().e_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->ml_blocks.back().append(string_fragment{
|
||||
sf.sf_string,
|
||||
(int) pi.pi_offset,
|
||||
sf.sf_end,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Ok();
|
||||
}
|
||||
|
||||
void
|
||||
md2attr_line::append_url_footnote(std::string href_str)
|
||||
{
|
||||
if (startswith(href_str, "#")) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& last_block = this->ml_blocks.back();
|
||||
last_block.append(FMT_STRING("[{}]"), this->ml_footnotes.size() + 1);
|
||||
last_block.with_attr(string_attr{
|
||||
line_range{
|
||||
(int) this->ml_span_starts.back(),
|
||||
(int) last_block.length(),
|
||||
},
|
||||
VC_STYLE.value(A_UNDERLINE),
|
||||
});
|
||||
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);
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef lnav_md2attr_line_hh
|
||||
#define lnav_md2attr_line_hh
|
||||
|
||||
#include "base/attr_line.hh"
|
||||
#include "ghc/filesystem.hpp"
|
||||
#include "md4cpp.hh"
|
||||
|
||||
class md2attr_line : public md4cpp::typed_event_handler<attr_line_t> {
|
||||
public:
|
||||
md2attr_line() { this->ml_blocks.resize(1); }
|
||||
|
||||
md2attr_line& with_source_path(nonstd::optional<ghc::filesystem::path> path)
|
||||
{
|
||||
this->ml_source_path = path;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Result<void, std::string> enter_block(const block& bl) override;
|
||||
|
||||
Result<void, std::string> leave_block(const block& bl) override;
|
||||
Result<void, std::string> enter_span(const span& bl) override;
|
||||
Result<void, std::string> leave_span(const span& sp) override;
|
||||
Result<void, std::string> text(MD_TEXTTYPE tt,
|
||||
const string_fragment& sf) override;
|
||||
|
||||
attr_line_t get_result() override { return this->ml_blocks.back(); }
|
||||
|
||||
private:
|
||||
struct table_t {
|
||||
struct row_t {
|
||||
std::vector<attr_line_t> r_columns;
|
||||
};
|
||||
|
||||
std::vector<attr_line_t> t_headers;
|
||||
std::vector<row_t> t_rows;
|
||||
};
|
||||
|
||||
struct cell_lines {
|
||||
cell_lines(std::vector<attr_line_t> lines) : cl_lines(std::move(lines))
|
||||
{
|
||||
}
|
||||
|
||||
std::vector<attr_line_t> cl_lines;
|
||||
};
|
||||
|
||||
using list_block_t
|
||||
= mapbox::util::variant<MD_BLOCK_UL_DETAIL*, MD_BLOCK_OL_DETAIL>;
|
||||
|
||||
void append_url_footnote(std::string href);
|
||||
void flush_footnotes();
|
||||
|
||||
nonstd::optional<ghc::filesystem::path> ml_source_path;
|
||||
std::vector<attr_line_t> ml_blocks;
|
||||
std::vector<list_block_t> ml_list_stack;
|
||||
std::vector<table_t> ml_tables;
|
||||
std::vector<size_t> ml_span_starts;
|
||||
std::vector<attr_line_t> ml_footnotes;
|
||||
int32_t ml_code_depth{0};
|
||||
};
|
||||
|
||||
#endif
|
@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 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 "md4cpp.hh"
|
||||
|
||||
#include "base/lnav_log.hh"
|
||||
#include "emojis-json.h"
|
||||
#include "xml-entities-json.h"
|
||||
#include "yajlpp/yajlpp_def.hh"
|
||||
|
||||
namespace md4cpp {
|
||||
|
||||
static const typed_json_path_container<xml_entity> xml_entity_handlers = {
|
||||
yajlpp::property_handler("characters").for_field(&xml_entity::xe_chars),
|
||||
};
|
||||
|
||||
static const typed_json_path_container<xml_entity_map> xml_entity_map_handlers
|
||||
= {
|
||||
yajlpp::pattern_property_handler("(?<var_name>\\&\\w+;?)")
|
||||
.with_synopsis("<name>")
|
||||
.with_path_provider<xml_entity_map>(
|
||||
[](struct xml_entity_map* xem,
|
||||
std::vector<std::string>& paths_out) {
|
||||
for (const auto& iter : xem->xem_entities) {
|
||||
paths_out.emplace_back(iter.first);
|
||||
}
|
||||
})
|
||||
.with_obj_provider<xml_entity, xml_entity_map>(
|
||||
[](const yajlpp_provider_context& ypc, xml_entity_map* xem) {
|
||||
auto entity_name = ypc.get_substr(0);
|
||||
return &xem->xem_entities[entity_name];
|
||||
})
|
||||
.with_children(xml_entity_handlers),
|
||||
};
|
||||
|
||||
static const typed_json_path_container<emoji> emoji_handlers = {
|
||||
yajlpp::property_handler("emoji").for_field(&emoji::e_value),
|
||||
yajlpp::property_handler("shortname").for_field(&emoji::e_shortname),
|
||||
};
|
||||
|
||||
static const typed_json_path_container<emoji_map> emoji_map_handlers = {
|
||||
yajlpp::property_handler("emojis#")
|
||||
.for_field(&emoji_map::em_emojis)
|
||||
.with_children(emoji_handlers),
|
||||
};
|
||||
|
||||
static xml_entity_map
|
||||
load_xml_entity_map()
|
||||
{
|
||||
static const intern_string_t name
|
||||
= intern_string::lookup(xml_entities_json.get_name());
|
||||
auto parse_res
|
||||
= xml_entity_map_handlers.parser_for(name).with_ignore_unused(true).of(
|
||||
xml_entities_json.to_string_fragment());
|
||||
|
||||
assert(parse_res.isOk());
|
||||
|
||||
return parse_res.unwrap();
|
||||
}
|
||||
|
||||
const xml_entity_map&
|
||||
get_xml_entity_map()
|
||||
{
|
||||
static const auto retval = load_xml_entity_map();
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
static emoji_map
|
||||
load_emoji_map()
|
||||
{
|
||||
static const intern_string_t name
|
||||
= intern_string::lookup(emojis_json.get_name());
|
||||
auto parse_res
|
||||
= emoji_map_handlers.parser_for(name).with_ignore_unused(true).of(
|
||||
emojis_json.to_string_fragment());
|
||||
|
||||
assert(parse_res.isOk());
|
||||
|
||||
auto retval = parse_res.unwrap();
|
||||
for (auto& em : retval.em_emojis) {
|
||||
retval.em_shortname2emoji.emplace(em.e_shortname, em);
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
const emoji_map&
|
||||
get_emoji_map()
|
||||
{
|
||||
static const auto retval = load_emoji_map();
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
struct parse_userdata {
|
||||
event_handler& pu_handler;
|
||||
std::string pu_error_msg;
|
||||
};
|
||||
|
||||
static event_handler::block
|
||||
build_block(MD_BLOCKTYPE type, void* detail)
|
||||
{
|
||||
switch (type) {
|
||||
case MD_BLOCK_DOC:
|
||||
return event_handler::block_doc{};
|
||||
case MD_BLOCK_QUOTE:
|
||||
return event_handler::block_quote{};
|
||||
case MD_BLOCK_UL:
|
||||
return static_cast<MD_BLOCK_UL_DETAIL*>(detail);
|
||||
case MD_BLOCK_OL:
|
||||
return static_cast<MD_BLOCK_OL_DETAIL*>(detail);
|
||||
case MD_BLOCK_LI:
|
||||
return static_cast<MD_BLOCK_LI_DETAIL*>(detail);
|
||||
case MD_BLOCK_HR:
|
||||
return event_handler::block_hr{};
|
||||
case MD_BLOCK_H:
|
||||
return static_cast<MD_BLOCK_H_DETAIL*>(detail);
|
||||
case MD_BLOCK_CODE:
|
||||
return static_cast<MD_BLOCK_CODE_DETAIL*>(detail);
|
||||
case MD_BLOCK_HTML:
|
||||
return event_handler::block_html{};
|
||||
case MD_BLOCK_P:
|
||||
return event_handler::block_p{};
|
||||
case MD_BLOCK_TABLE:
|
||||
return static_cast<MD_BLOCK_TABLE_DETAIL*>(detail);
|
||||
case MD_BLOCK_THEAD:
|
||||
return event_handler::block_thead{};
|
||||
case MD_BLOCK_TBODY:
|
||||
return event_handler::block_tbody{};
|
||||
case MD_BLOCK_TR:
|
||||
return event_handler::block_tr{};
|
||||
case MD_BLOCK_TH:
|
||||
return event_handler::block_th{};
|
||||
case MD_BLOCK_TD:
|
||||
return static_cast<MD_BLOCK_TD_DETAIL*>(detail);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static event_handler::span
|
||||
build_span(MD_SPANTYPE type, void* detail)
|
||||
{
|
||||
switch (type) {
|
||||
case MD_SPAN_EM:
|
||||
return event_handler::span_em{};
|
||||
case MD_SPAN_STRONG:
|
||||
return event_handler::span_strong{};
|
||||
case MD_SPAN_A:
|
||||
return static_cast<MD_SPAN_A_DETAIL*>(detail);
|
||||
case MD_SPAN_IMG:
|
||||
return static_cast<MD_SPAN_IMG_DETAIL*>(detail);
|
||||
case MD_SPAN_CODE:
|
||||
return event_handler::span_code{};
|
||||
case MD_SPAN_DEL:
|
||||
return event_handler::span_del{};
|
||||
case MD_SPAN_U:
|
||||
return event_handler::span_u{};
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static int
|
||||
md4cpp_enter_block(MD_BLOCKTYPE type, void* detail, void* userdata)
|
||||
{
|
||||
auto* pu = static_cast<parse_userdata*>(userdata);
|
||||
|
||||
auto enter_res = pu->pu_handler.enter_block(build_block(type, detail));
|
||||
if (enter_res.isErr()) {
|
||||
pu->pu_error_msg = enter_res.unwrapErr();
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
md4cpp_leave_block(MD_BLOCKTYPE type, void* detail, void* userdata)
|
||||
{
|
||||
auto* pu = static_cast<parse_userdata*>(userdata);
|
||||
|
||||
auto leave_res = pu->pu_handler.leave_block(build_block(type, detail));
|
||||
if (leave_res.isErr()) {
|
||||
pu->pu_error_msg = leave_res.unwrapErr();
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
md4cpp_enter_span(MD_SPANTYPE type, void* detail, void* userdata)
|
||||
{
|
||||
auto* pu = static_cast<parse_userdata*>(userdata);
|
||||
|
||||
auto enter_res = pu->pu_handler.enter_span(build_span(type, detail));
|
||||
if (enter_res.isErr()) {
|
||||
pu->pu_error_msg = enter_res.unwrapErr();
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
md4cpp_leave_span(MD_SPANTYPE type, void* detail, void* userdata)
|
||||
{
|
||||
auto* pu = static_cast<parse_userdata*>(userdata);
|
||||
|
||||
auto leave_res = pu->pu_handler.leave_span(build_span(type, detail));
|
||||
if (leave_res.isErr()) {
|
||||
pu->pu_error_msg = leave_res.unwrapErr();
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
md4cpp_text(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* userdata)
|
||||
{
|
||||
auto* pu = static_cast<parse_userdata*>(userdata);
|
||||
auto leave_res = pu->pu_handler.text(type, string_fragment(text, 0, size));
|
||||
if (leave_res.isErr()) {
|
||||
pu->pu_error_msg = leave_res.unwrapErr();
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace details {
|
||||
Result<void, std::string>
|
||||
parse(const string_fragment& sf, event_handler& eh)
|
||||
{
|
||||
MD_PARSER parser = {0};
|
||||
auto pu = parse_userdata{eh};
|
||||
|
||||
parser.abi_version = 0;
|
||||
parser.flags = MD_DIALECT_GITHUB;
|
||||
parser.enter_block = md4cpp_enter_block;
|
||||
parser.leave_block = md4cpp_leave_block;
|
||||
parser.enter_span = md4cpp_enter_span;
|
||||
parser.leave_span = md4cpp_leave_span;
|
||||
parser.text = md4cpp_text;
|
||||
|
||||
auto rc = md_parse(sf.data(), sf.length(), &parser, &pu);
|
||||
|
||||
if (rc == 0) {
|
||||
return Ok();
|
||||
}
|
||||
|
||||
return Err(pu.pu_error_msg);
|
||||
}
|
||||
} // namespace details
|
||||
|
||||
} // namespace md4cpp
|
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef lnav_md4cpp_hh
|
||||
#define lnav_md4cpp_hh
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "base/intern_string.hh"
|
||||
#include "base/result.h"
|
||||
#include "mapbox/variant.hpp"
|
||||
#include "md4c/md4c.h"
|
||||
|
||||
namespace md4cpp {
|
||||
|
||||
struct xml_entity {
|
||||
std::string xe_chars;
|
||||
};
|
||||
|
||||
struct xml_entity_map {
|
||||
std::map<std::string, xml_entity> xem_entities;
|
||||
};
|
||||
|
||||
struct emoji {
|
||||
std::string e_shortname;
|
||||
std::string e_value;
|
||||
};
|
||||
|
||||
struct emoji_map {
|
||||
std::vector<emoji> em_emojis;
|
||||
std::unordered_map<std::string, std::reference_wrapper<emoji>>
|
||||
em_shortname2emoji;
|
||||
};
|
||||
|
||||
class event_handler {
|
||||
public:
|
||||
virtual ~event_handler() = default;
|
||||
|
||||
struct block_doc {};
|
||||
struct block_quote {};
|
||||
struct block_hr {};
|
||||
struct block_html {};
|
||||
struct block_p {};
|
||||
struct block_thead {};
|
||||
struct block_tbody {};
|
||||
struct block_tr {};
|
||||
struct block_th {};
|
||||
|
||||
using block = mapbox::util::variant<block_doc,
|
||||
block_quote,
|
||||
MD_BLOCK_UL_DETAIL*,
|
||||
MD_BLOCK_OL_DETAIL*,
|
||||
MD_BLOCK_LI_DETAIL*,
|
||||
block_hr,
|
||||
MD_BLOCK_H_DETAIL*,
|
||||
MD_BLOCK_CODE_DETAIL*,
|
||||
block_html,
|
||||
block_p,
|
||||
MD_BLOCK_TABLE_DETAIL*,
|
||||
block_thead,
|
||||
block_tbody,
|
||||
block_tr,
|
||||
block_th,
|
||||
MD_BLOCK_TD_DETAIL*>;
|
||||
|
||||
virtual Result<void, std::string> enter_block(const block& bl) = 0;
|
||||
virtual Result<void, std::string> leave_block(const block& bl) = 0;
|
||||
|
||||
struct span_em {};
|
||||
struct span_strong {};
|
||||
struct span_code {};
|
||||
struct span_del {};
|
||||
struct span_u {};
|
||||
|
||||
using span = mapbox::util::variant<span_em,
|
||||
span_strong,
|
||||
MD_SPAN_A_DETAIL*,
|
||||
MD_SPAN_IMG_DETAIL*,
|
||||
span_code,
|
||||
span_del,
|
||||
span_u>;
|
||||
|
||||
virtual Result<void, std::string> enter_span(const span& bl) = 0;
|
||||
virtual Result<void, std::string> leave_span(const span& bl) = 0;
|
||||
|
||||
virtual Result<void, std::string> text(MD_TEXTTYPE tt,
|
||||
const string_fragment& sf)
|
||||
= 0;
|
||||
};
|
||||
|
||||
namespace details {
|
||||
Result<void, std::string> parse(const string_fragment& sf, event_handler& eh);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
class typed_event_handler : public event_handler {
|
||||
public:
|
||||
virtual T get_result() = 0;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
Result<T, std::string>
|
||||
parse(const string_fragment& sf, typed_event_handler<T>& eh)
|
||||
{
|
||||
TRY(details::parse(sf, eh));
|
||||
|
||||
return Ok(eh.get_result());
|
||||
}
|
||||
|
||||
const xml_entity_map& get_xml_entity_map();
|
||||
|
||||
const emoji_map& get_emoji_map();
|
||||
|
||||
} // namespace md4cpp
|
||||
|
||||
#endif
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,405 @@
|
||||
/*
|
||||
* MD4C: Markdown parser for C
|
||||
* (http://github.com/mity/md4c)
|
||||
*
|
||||
* Copyright (c) 2016-2020 Martin Mitas
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef MD4C_H
|
||||
#define MD4C_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if defined MD4C_USE_UTF16
|
||||
/* Magic to support UTF-16. Note that in order to use it, you have to define
|
||||
* the macro MD4C_USE_UTF16 both when building MD4C as well as when
|
||||
* including this header in your code. */
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
typedef WCHAR MD_CHAR;
|
||||
#else
|
||||
#error MD4C_USE_UTF16 is only supported on Windows.
|
||||
#endif
|
||||
#else
|
||||
typedef char MD_CHAR;
|
||||
#endif
|
||||
|
||||
typedef unsigned MD_SIZE;
|
||||
typedef unsigned MD_OFFSET;
|
||||
|
||||
|
||||
/* Block represents a part of document hierarchy structure like a paragraph
|
||||
* or list item.
|
||||
*/
|
||||
typedef enum MD_BLOCKTYPE {
|
||||
/* <body>...</body> */
|
||||
MD_BLOCK_DOC = 0,
|
||||
|
||||
/* <blockquote>...</blockquote> */
|
||||
MD_BLOCK_QUOTE,
|
||||
|
||||
/* <ul>...</ul>
|
||||
* Detail: Structure MD_BLOCK_UL_DETAIL. */
|
||||
MD_BLOCK_UL,
|
||||
|
||||
/* <ol>...</ol>
|
||||
* Detail: Structure MD_BLOCK_OL_DETAIL. */
|
||||
MD_BLOCK_OL,
|
||||
|
||||
/* <li>...</li>
|
||||
* Detail: Structure MD_BLOCK_LI_DETAIL. */
|
||||
MD_BLOCK_LI,
|
||||
|
||||
/* <hr> */
|
||||
MD_BLOCK_HR,
|
||||
|
||||
/* <h1>...</h1> (for levels up to 6)
|
||||
* Detail: Structure MD_BLOCK_H_DETAIL. */
|
||||
MD_BLOCK_H,
|
||||
|
||||
/* <pre><code>...</code></pre>
|
||||
* Note the text lines within code blocks are terminated with '\n'
|
||||
* instead of explicit MD_TEXT_BR. */
|
||||
MD_BLOCK_CODE,
|
||||
|
||||
/* Raw HTML block. This itself does not correspond to any particular HTML
|
||||
* tag. The contents of it _is_ raw HTML source intended to be put
|
||||
* in verbatim form to the HTML output. */
|
||||
MD_BLOCK_HTML,
|
||||
|
||||
/* <p>...</p> */
|
||||
MD_BLOCK_P,
|
||||
|
||||
/* <table>...</table> and its contents.
|
||||
* Detail: Structure MD_BLOCK_TABLE_DETAIL (for MD_BLOCK_TABLE),
|
||||
* structure MD_BLOCK_TD_DETAIL (for MD_BLOCK_TH and MD_BLOCK_TD)
|
||||
* Note all of these are used only if extension MD_FLAG_TABLES is enabled. */
|
||||
MD_BLOCK_TABLE,
|
||||
MD_BLOCK_THEAD,
|
||||
MD_BLOCK_TBODY,
|
||||
MD_BLOCK_TR,
|
||||
MD_BLOCK_TH,
|
||||
MD_BLOCK_TD
|
||||
} MD_BLOCKTYPE;
|
||||
|
||||
/* Span represents an in-line piece of a document which should be rendered with
|
||||
* the same font, color and other attributes. A sequence of spans forms a block
|
||||
* like paragraph or list item. */
|
||||
typedef enum MD_SPANTYPE {
|
||||
/* <em>...</em> */
|
||||
MD_SPAN_EM,
|
||||
|
||||
/* <strong>...</strong> */
|
||||
MD_SPAN_STRONG,
|
||||
|
||||
/* <a href="xxx">...</a>
|
||||
* Detail: Structure MD_SPAN_A_DETAIL. */
|
||||
MD_SPAN_A,
|
||||
|
||||
/* <img src="xxx">...</a>
|
||||
* Detail: Structure MD_SPAN_IMG_DETAIL.
|
||||
* Note: Image text can contain nested spans and even nested images.
|
||||
* If rendered into ALT attribute of HTML <IMG> tag, it's responsibility
|
||||
* of the parser to deal with it.
|
||||
*/
|
||||
MD_SPAN_IMG,
|
||||
|
||||
/* <code>...</code> */
|
||||
MD_SPAN_CODE,
|
||||
|
||||
/* <del>...</del>
|
||||
* Note: Recognized only when MD_FLAG_STRIKETHROUGH is enabled.
|
||||
*/
|
||||
MD_SPAN_DEL,
|
||||
|
||||
/* For recognizing inline ($) and display ($$) equations
|
||||
* Note: Recognized only when MD_FLAG_LATEXMATHSPANS is enabled.
|
||||
*/
|
||||
MD_SPAN_LATEXMATH,
|
||||
MD_SPAN_LATEXMATH_DISPLAY,
|
||||
|
||||
/* Wiki links
|
||||
* Note: Recognized only when MD_FLAG_WIKILINKS is enabled.
|
||||
*/
|
||||
MD_SPAN_WIKILINK,
|
||||
|
||||
/* <u>...</u>
|
||||
* Note: Recognized only when MD_FLAG_UNDERLINE is enabled. */
|
||||
MD_SPAN_U
|
||||
} MD_SPANTYPE;
|
||||
|
||||
/* Text is the actual textual contents of span. */
|
||||
typedef enum MD_TEXTTYPE {
|
||||
/* Normal text. */
|
||||
MD_TEXT_NORMAL = 0,
|
||||
|
||||
/* NULL character. CommonMark requires replacing NULL character with
|
||||
* the replacement char U+FFFD, so this allows caller to do that easily. */
|
||||
MD_TEXT_NULLCHAR,
|
||||
|
||||
/* Line breaks.
|
||||
* Note these are not sent from blocks with verbatim output (MD_BLOCK_CODE
|
||||
* or MD_BLOCK_HTML). In such cases, '\n' is part of the text itself. */
|
||||
MD_TEXT_BR, /* <br> (hard break) */
|
||||
MD_TEXT_SOFTBR, /* '\n' in source text where it is not semantically meaningful (soft break) */
|
||||
|
||||
/* Entity.
|
||||
* (a) Named entity, e.g.
|
||||
* (Note MD4C does not have a list of known entities.
|
||||
* Anything matching the regexp /&[A-Za-z][A-Za-z0-9]{1,47};/ is
|
||||
* treated as a named entity.)
|
||||
* (b) Numerical entity, e.g. Ӓ
|
||||
* (c) Hexadecimal entity, e.g. ካ
|
||||
*
|
||||
* As MD4C is mostly encoding agnostic, application gets the verbatim
|
||||
* entity text into the MD_PARSER::text_callback(). */
|
||||
MD_TEXT_ENTITY,
|
||||
|
||||
/* Text in a code block (inside MD_BLOCK_CODE) or inlined code (`code`).
|
||||
* If it is inside MD_BLOCK_CODE, it includes spaces for indentation and
|
||||
* '\n' for new lines. MD_TEXT_BR and MD_TEXT_SOFTBR are not sent for this
|
||||
* kind of text. */
|
||||
MD_TEXT_CODE,
|
||||
|
||||
/* Text is a raw HTML. If it is contents of a raw HTML block (i.e. not
|
||||
* an inline raw HTML), then MD_TEXT_BR and MD_TEXT_SOFTBR are not used.
|
||||
* The text contains verbatim '\n' for the new lines. */
|
||||
MD_TEXT_HTML,
|
||||
|
||||
/* Text is inside an equation. This is processed the same way as inlined code
|
||||
* spans (`code`). */
|
||||
MD_TEXT_LATEXMATH
|
||||
} MD_TEXTTYPE;
|
||||
|
||||
|
||||
/* Alignment enumeration. */
|
||||
typedef enum MD_ALIGN {
|
||||
MD_ALIGN_DEFAULT = 0, /* When unspecified. */
|
||||
MD_ALIGN_LEFT,
|
||||
MD_ALIGN_CENTER,
|
||||
MD_ALIGN_RIGHT
|
||||
} MD_ALIGN;
|
||||
|
||||
|
||||
/* String attribute.
|
||||
*
|
||||
* This wraps strings which are outside of a normal text flow and which are
|
||||
* propagated within various detailed structures, but which still may contain
|
||||
* string portions of different types like e.g. entities.
|
||||
*
|
||||
* So, for example, lets consider this image:
|
||||
*
|
||||
* ![image alt text](http://example.org/image.png 'foo " bar')
|
||||
*
|
||||
* The image alt text is propagated as a normal text via the MD_PARSER::text()
|
||||
* callback. However, the image title ('foo " bar') is propagated as
|
||||
* MD_ATTRIBUTE in MD_SPAN_IMG_DETAIL::title.
|
||||
*
|
||||
* Then the attribute MD_SPAN_IMG_DETAIL::title shall provide the following:
|
||||
* -- [0]: "foo " (substr_types[0] == MD_TEXT_NORMAL; substr_offsets[0] == 0)
|
||||
* -- [1]: """ (substr_types[1] == MD_TEXT_ENTITY; substr_offsets[1] == 4)
|
||||
* -- [2]: " bar" (substr_types[2] == MD_TEXT_NORMAL; substr_offsets[2] == 10)
|
||||
* -- [3]: (n/a) (n/a ; substr_offsets[3] == 14)
|
||||
*
|
||||
* Note that these invariants are always guaranteed:
|
||||
* -- substr_offsets[0] == 0
|
||||
* -- substr_offsets[LAST+1] == size
|
||||
* -- Currently, only MD_TEXT_NORMAL, MD_TEXT_ENTITY, MD_TEXT_NULLCHAR
|
||||
* substrings can appear. This could change only of the specification
|
||||
* changes.
|
||||
*/
|
||||
typedef struct MD_ATTRIBUTE {
|
||||
const MD_CHAR* text;
|
||||
MD_SIZE size;
|
||||
const MD_TEXTTYPE* substr_types;
|
||||
const MD_OFFSET* substr_offsets;
|
||||
} MD_ATTRIBUTE;
|
||||
|
||||
|
||||
/* Detailed info for MD_BLOCK_UL. */
|
||||
typedef struct MD_BLOCK_UL_DETAIL {
|
||||
int is_tight; /* Non-zero if tight list, zero if loose. */
|
||||
MD_CHAR mark; /* Item bullet character in MarkDown source of the list, e.g. '-', '+', '*'. */
|
||||
} MD_BLOCK_UL_DETAIL;
|
||||
|
||||
/* Detailed info for MD_BLOCK_OL. */
|
||||
typedef struct MD_BLOCK_OL_DETAIL {
|
||||
unsigned start; /* Start index of the ordered list. */
|
||||
int is_tight; /* Non-zero if tight list, zero if loose. */
|
||||
MD_CHAR mark_delimiter; /* Character delimiting the item marks in MarkDown source, e.g. '.' or ')' */
|
||||
} MD_BLOCK_OL_DETAIL;
|
||||
|
||||
/* Detailed info for MD_BLOCK_LI. */
|
||||
typedef struct MD_BLOCK_LI_DETAIL {
|
||||
int is_task; /* Can be non-zero only with MD_FLAG_TASKLISTS */
|
||||
MD_CHAR task_mark; /* If is_task, then one of 'x', 'X' or ' '. Undefined otherwise. */
|
||||
MD_OFFSET task_mark_offset; /* If is_task, then offset in the input of the char between '[' and ']'. */
|
||||
} MD_BLOCK_LI_DETAIL;
|
||||
|
||||
/* Detailed info for MD_BLOCK_H. */
|
||||
typedef struct MD_BLOCK_H_DETAIL {
|
||||
unsigned level; /* Header level (1 - 6) */
|
||||
} MD_BLOCK_H_DETAIL;
|
||||
|
||||
/* Detailed info for MD_BLOCK_CODE. */
|
||||
typedef struct MD_BLOCK_CODE_DETAIL {
|
||||
MD_ATTRIBUTE info;
|
||||
MD_ATTRIBUTE lang;
|
||||
MD_CHAR fence_char; /* The character used for fenced code block; or zero for indented code block. */
|
||||
} MD_BLOCK_CODE_DETAIL;
|
||||
|
||||
/* Detailed info for MD_BLOCK_TABLE. */
|
||||
typedef struct MD_BLOCK_TABLE_DETAIL {
|
||||
unsigned col_count; /* Count of columns in the table. */
|
||||
unsigned head_row_count; /* Count of rows in the table header (currently always 1) */
|
||||
unsigned body_row_count; /* Count of rows in the table body */
|
||||
} MD_BLOCK_TABLE_DETAIL;
|
||||
|
||||
/* Detailed info for MD_BLOCK_TH and MD_BLOCK_TD. */
|
||||
typedef struct MD_BLOCK_TD_DETAIL {
|
||||
MD_ALIGN align;
|
||||
} MD_BLOCK_TD_DETAIL;
|
||||
|
||||
/* Detailed info for MD_SPAN_A. */
|
||||
typedef struct MD_SPAN_A_DETAIL {
|
||||
MD_ATTRIBUTE href;
|
||||
MD_ATTRIBUTE title;
|
||||
} MD_SPAN_A_DETAIL;
|
||||
|
||||
/* Detailed info for MD_SPAN_IMG. */
|
||||
typedef struct MD_SPAN_IMG_DETAIL {
|
||||
MD_ATTRIBUTE src;
|
||||
MD_ATTRIBUTE title;
|
||||
} MD_SPAN_IMG_DETAIL;
|
||||
|
||||
/* Detailed info for MD_SPAN_WIKILINK. */
|
||||
typedef struct MD_SPAN_WIKILINK {
|
||||
MD_ATTRIBUTE target;
|
||||
} MD_SPAN_WIKILINK_DETAIL;
|
||||
|
||||
/* Flags specifying extensions/deviations from CommonMark specification.
|
||||
*
|
||||
* By default (when MD_PARSER::flags == 0), we follow CommonMark specification.
|
||||
* The following flags may allow some extensions or deviations from it.
|
||||
*/
|
||||
#define MD_FLAG_COLLAPSEWHITESPACE 0x0001 /* In MD_TEXT_NORMAL, collapse non-trivial whitespace into single ' ' */
|
||||
#define MD_FLAG_PERMISSIVEATXHEADERS 0x0002 /* Do not require space in ATX headers ( ###header ) */
|
||||
#define MD_FLAG_PERMISSIVEURLAUTOLINKS 0x0004 /* Recognize URLs as autolinks even without '<', '>' */
|
||||
#define MD_FLAG_PERMISSIVEEMAILAUTOLINKS 0x0008 /* Recognize e-mails as autolinks even without '<', '>' and 'mailto:' */
|
||||
#define MD_FLAG_NOINDENTEDCODEBLOCKS 0x0010 /* Disable indented code blocks. (Only fenced code works.) */
|
||||
#define MD_FLAG_NOHTMLBLOCKS 0x0020 /* Disable raw HTML blocks. */
|
||||
#define MD_FLAG_NOHTMLSPANS 0x0040 /* Disable raw HTML (inline). */
|
||||
#define MD_FLAG_TABLES 0x0100 /* Enable tables extension. */
|
||||
#define MD_FLAG_STRIKETHROUGH 0x0200 /* Enable strikethrough extension. */
|
||||
#define MD_FLAG_PERMISSIVEWWWAUTOLINKS 0x0400 /* Enable WWW autolinks (even without any scheme prefix, if they begin with 'www.') */
|
||||
#define MD_FLAG_TASKLISTS 0x0800 /* Enable task list extension. */
|
||||
#define MD_FLAG_LATEXMATHSPANS 0x1000 /* Enable $ and $$ containing LaTeX equations. */
|
||||
#define MD_FLAG_WIKILINKS 0x2000 /* Enable wiki links extension. */
|
||||
#define MD_FLAG_UNDERLINE 0x4000 /* Enable underline extension (and disables '_' for normal emphasis). */
|
||||
|
||||
#define MD_FLAG_PERMISSIVEAUTOLINKS (MD_FLAG_PERMISSIVEEMAILAUTOLINKS | MD_FLAG_PERMISSIVEURLAUTOLINKS | MD_FLAG_PERMISSIVEWWWAUTOLINKS)
|
||||
#define MD_FLAG_NOHTML (MD_FLAG_NOHTMLBLOCKS | MD_FLAG_NOHTMLSPANS)
|
||||
|
||||
/* Convenient sets of flags corresponding to well-known Markdown dialects.
|
||||
*
|
||||
* Note we may only support subset of features of the referred dialect.
|
||||
* The constant just enables those extensions which bring us as close as
|
||||
* possible given what features we implement.
|
||||
*
|
||||
* ABI compatibility note: Meaning of these can change in time as new
|
||||
* extensions, bringing the dialect closer to the original, are implemented.
|
||||
*/
|
||||
#define MD_DIALECT_COMMONMARK 0
|
||||
#define MD_DIALECT_GITHUB (MD_FLAG_PERMISSIVEAUTOLINKS | MD_FLAG_TABLES | MD_FLAG_STRIKETHROUGH | MD_FLAG_TASKLISTS)
|
||||
|
||||
/* Parser structure.
|
||||
*/
|
||||
typedef struct MD_PARSER {
|
||||
/* Reserved. Set to zero.
|
||||
*/
|
||||
unsigned abi_version;
|
||||
|
||||
/* Dialect options. Bitmask of MD_FLAG_xxxx values.
|
||||
*/
|
||||
unsigned flags;
|
||||
|
||||
/* Caller-provided rendering callbacks.
|
||||
*
|
||||
* For some block/span types, more detailed information is provided in a
|
||||
* type-specific structure pointed by the argument 'detail'.
|
||||
*
|
||||
* The last argument of all callbacks, 'userdata', is just propagated from
|
||||
* md_parse() and is available for any use by the application.
|
||||
*
|
||||
* Note any strings provided to the callbacks as their arguments or as
|
||||
* members of any detail structure are generally not zero-terminated.
|
||||
* Application has to take the respective size information into account.
|
||||
*
|
||||
* Any rendering callback may abort further parsing of the document by
|
||||
* returning non-zero.
|
||||
*/
|
||||
int (*enter_block)(MD_BLOCKTYPE /*type*/, void* /*detail*/, void* /*userdata*/);
|
||||
int (*leave_block)(MD_BLOCKTYPE /*type*/, void* /*detail*/, void* /*userdata*/);
|
||||
|
||||
int (*enter_span)(MD_SPANTYPE /*type*/, void* /*detail*/, void* /*userdata*/);
|
||||
int (*leave_span)(MD_SPANTYPE /*type*/, void* /*detail*/, void* /*userdata*/);
|
||||
|
||||
int (*text)(MD_TEXTTYPE /*type*/, const MD_CHAR* /*text*/, MD_SIZE /*size*/, void* /*userdata*/);
|
||||
|
||||
/* Debug callback. Optional (may be NULL).
|
||||
*
|
||||
* If provided and something goes wrong, this function gets called.
|
||||
* This is intended for debugging and problem diagnosis for developers;
|
||||
* it is not intended to provide any errors suitable for displaying to an
|
||||
* end user.
|
||||
*/
|
||||
void (*debug_log)(const char* /*msg*/, void* /*userdata*/);
|
||||
|
||||
/* Reserved. Set to NULL.
|
||||
*/
|
||||
void (*syntax)(void);
|
||||
} MD_PARSER;
|
||||
|
||||
|
||||
/* For backward compatibility. Do not use in new code.
|
||||
*/
|
||||
typedef MD_PARSER MD_RENDERER;
|
||||
|
||||
|
||||
/* Parse the Markdown document stored in the string 'text' of size 'size'.
|
||||
* The parser provides callbacks to be called during the parsing so the
|
||||
* caller can render the document on the screen or convert the Markdown
|
||||
* to another format.
|
||||
*
|
||||
* Zero is returned on success. If a runtime error occurs (e.g. a memory
|
||||
* fails), -1 is returned. If the processing is aborted due any callback
|
||||
* returning non-zero, the return value of the callback is returned.
|
||||
*/
|
||||
int md_parse(const MD_CHAR* text, MD_SIZE size, const MD_PARSER* parser, void* userdata);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" { */
|
||||
#endif
|
||||
|
||||
#endif /* MD4C_H */
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 "config.h"
|
||||
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include "doctest/doctest.h"
|
||||
#include "document.sections.hh"
|
||||
|
||||
TEST_CASE("lnav::document::sections::basics")
|
||||
{
|
||||
static const std::string INPUT = R"(
|
||||
{
|
||||
"msg": "Hello, World!",
|
||||
"obj": {
|
||||
"a": 1,
|
||||
"b": "Two",
|
||||
"c": 3.0
|
||||
},
|
||||
"arr": [1, 2, 3],
|
||||
"arr2": [
|
||||
456,
|
||||
789,
|
||||
{
|
||||
"def": 123,
|
||||
"ghi": null,
|
||||
"jkl": "other"
|
||||
},
|
||||
{
|
||||
"def": 456,
|
||||
"ghi": null,
|
||||
"jkl": "OTHER"
|
||||
},
|
||||
{
|
||||
"def": 789,
|
||||
"ghi": null,
|
||||
"jkl": "OtHeR"
|
||||
}
|
||||
]
|
||||
}
|
||||
)";
|
||||
|
||||
auto meta = lnav::document::discover_structure(INPUT);
|
||||
|
||||
meta.m_sections_tree.visit_all([](const auto& intv) {
|
||||
auto ser = intv.value.match(
|
||||
[](const std::string& name) { return name; },
|
||||
[](const size_t index) { return fmt::format("{}", index); });
|
||||
printf("interval %d:%d %s\n", intv.start, intv.stop, ser.c_str());
|
||||
});
|
||||
lnav::document::hier_node::depth_first(
|
||||
meta.m_sections_root.get(), [](const auto* node) {
|
||||
printf("node %p %d\n", node, node->hn_start);
|
||||
for (const auto& pair : node->hn_named_children) {
|
||||
printf(" child: %p %s\n", pair.second, pair.first.c_str());
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
192.168.202.254 - - [20/Jul/2009:21:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
|
||||
192.168.202.254 - - [20/Jul/2009:21:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
|
||||
192.168.202.254 - - [20/Jul/2009:21:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
|
@ -0,0 +1,3 @@
|
||||
c1,c2
|
||||
1,"Hello
|
||||
World!"
|
@ -0,0 +1,3 @@
|
||||
192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
|
||||
192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
|
||||
192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
|
@ -0,0 +1,2 @@
|
||||
192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
|
||||
192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
|
@ -0,0 +1 @@
|
||||
How are you?
|
@ -0,0 +1,2 @@
|
||||
192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
|
||||
192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue