Enabled multiple relative link patterns in generate_links option (#297)

This commit is contained in:
Bartek Kryza
2024-07-28 18:33:14 +02:00
parent 37314baa3a
commit f97d42083b
7 changed files with 216 additions and 88 deletions

View File

@@ -85,25 +85,14 @@ public:
template <typename E> inja::json element_context(const E &e) const;
std::optional<std::pair<std::string, std::string>> get_link_pattern(
const common::model::source_location &sl) const
{
if (sl.file_relative().empty()) {
return config().generate_links().get_link_pattern(sl.file());
}
return config().generate_links().get_link_pattern(".");
}
const common::model::source_location &sl) const;
std::optional<std::pair<std::string, std::string>> get_tooltip_pattern(
const common::model::source_location &sl) const
{
if (sl.file_relative().empty()) {
return config().generate_links().get_tooltip_pattern(sl.file());
}
return config().generate_links().get_tooltip_pattern(".");
}
const common::model::source_location &sl) const;
/**
* @brief Initialize diagram Jinja context
*/
void init_context();
/**
@@ -318,4 +307,28 @@ inja::json generator<C, D>::element_context(const E &e) const
return ctx;
}
template <typename C, typename D>
std::optional<std::pair<std::string, std::string>>
generator<C, D>::get_link_pattern(
const common::model::source_location &sl) const
{
if (sl.file_relative().empty()) {
return config().generate_links().get_link_pattern(sl.file());
}
return config().generate_links().get_link_pattern(sl.file_relative());
}
template <typename C, typename D>
std::optional<std::pair<std::string, std::string>>
generator<C, D>::get_tooltip_pattern(
const common::model::source_location &sl) const
{
if (sl.file_relative().empty()) {
return config().generate_links().get_tooltip_pattern(sl.file());
}
return config().generate_links().get_tooltip_pattern(sl.file_relative());
}
} // namespace clanguml::common::generators

View File

@@ -240,33 +240,34 @@ void generator<C, D>::generate_link(std::ostream &ostr, const E &e) const
auto maybe_tooltip_pattern =
generators::generator<C, D>::get_tooltip_pattern(e);
if (!maybe_tooltip_pattern)
return;
if (maybe_tooltip_pattern) {
const auto &[tooltip_prefix, tooltip_pattern] = *maybe_tooltip_pattern;
const auto &[tooltip_prefix, tooltip_pattern] = *maybe_tooltip_pattern;
if (!tooltip_pattern.empty()) {
ostr << " \"";
try {
auto ec = generators::generator<C, D>::element_context(e);
common::generators::make_context_source_relative(
ec, tooltip_prefix);
auto tooltip_text = generators::generator<C, D>::env().render(
std::string_view{tooltip_pattern}, ec);
util::replace_all(tooltip_text, "\"", "&bdquo;");
ostr << tooltip_text;
}
catch (const inja::json::parse_error &e) {
LOG_ERROR(
"Failed to parse Jinja template: {}", tooltip_pattern);
ostr << " ";
}
catch (const inja::json::exception &e) {
LOG_ERROR(
"Failed to render PlantUML directive: \n{}\n due to: {}",
tooltip_pattern, e.what());
ostr << " ";
}
if (!tooltip_pattern.empty()) {
ostr << " \"";
try {
auto ec = generators::generator<C, D>::element_context(e);
common::generators::make_context_source_relative(
ec, tooltip_prefix);
auto tooltip_text = generators::generator<C, D>::env().render(
std::string_view{tooltip_pattern}, ec);
util::replace_all(tooltip_text, "\"", "&bdquo;");
ostr << tooltip_text;
ostr << "\"";
}
catch (const inja::json::parse_error &e) {
LOG_ERROR("Failed to parse Jinja template: {}", tooltip_pattern);
ostr << " ";
}
catch (const inja::json::exception &e) {
LOG_ERROR("Failed to render PlantUML directive: \n{}\n due to: {}",
tooltip_pattern, e.what());
ostr << " ";
}
ostr << "\"";
}
ostr << "\n";
}

View File

@@ -450,8 +450,9 @@ void generator<C, D>::generate_link(std::ostream &ostr, const E &e) const
auto maybe_link_pattern = generators::generator<C, D>::get_link_pattern(e);
if (!maybe_link_pattern)
if (!maybe_link_pattern) {
return;
}
const auto &[link_prefix, link_pattern] = *maybe_link_pattern;
@@ -467,7 +468,7 @@ void generator<C, D>::generate_link(std::ostream &ostr, const E &e) const
catch (const inja::json::parse_error &e) {
LOG_ERROR("Failed to parse Jinja template: {}", link_pattern);
}
catch (const inja::json::exception &e) {
catch (const std::exception &e) {
LOG_ERROR("Failed to render PlantUML directive: \n{}\n due to: {}",
link_pattern, e.what());
}
@@ -475,29 +476,28 @@ void generator<C, D>::generate_link(std::ostream &ostr, const E &e) const
auto maybe_tooltip_pattern =
generators::generator<C, D>::get_tooltip_pattern(e);
if (!maybe_tooltip_pattern)
return;
if (maybe_tooltip_pattern) {
const auto &[tooltip_prefix, tooltip_pattern] = *maybe_tooltip_pattern;
const auto &[tooltip_prefix, tooltip_pattern] = *maybe_tooltip_pattern;
ostr << "{";
try {
auto ec = generators::generator<C, D>::element_context(e);
common::generators::make_context_source_relative(ec, tooltip_prefix);
if (!tooltip_pattern.empty()) {
ostr << generators::generator<C, D>::env().render(
std::string_view{tooltip_pattern}, ec);
ostr << "{";
try {
auto ec = generators::generator<C, D>::element_context(e);
common::generators::make_context_source_relative(
ec, tooltip_prefix);
if (!tooltip_pattern.empty()) {
ostr << generators::generator<C, D>::env().render(
std::string_view{tooltip_pattern}, ec);
}
}
catch (const inja::json::parse_error &e) {
LOG_ERROR("Failed to parse Jinja template: {}", tooltip_pattern);
}
catch (const std::exception &e) {
LOG_ERROR("Failed to render PlantUML directive: \n{}\n due to: {}",
tooltip_pattern, e.what());
}
ostr << "}";
}
catch (const inja::json::parse_error &e) {
LOG_ERROR("Failed to parse Jinja template: {}", tooltip_pattern);
}
catch (const inja::json::exception &e) {
LOG_ERROR("Failed to render PlantUML directive: \n{}\n due to: {}",
tooltip_pattern, e.what());
}
ostr << "}";
ostr << "]]";
}

View File

@@ -461,37 +461,13 @@ struct generate_links_config {
std::optional<std::pair<std::string, std::string>> get_link_pattern(
const std::string &path) const
{
if (link.empty())
return {};
if ((path.empty() || path == ".") && link.count(".") > 0) {
return {{".", link.at(".")}};
}
for (const auto &[key, pattern] : link) {
if (util::starts_with(path, key))
return {{key, pattern}};
}
return {};
return util::find_entry_by_path_prefix(link, path);
}
std::optional<std::pair<std::string, std::string>> get_tooltip_pattern(
const std::string &path) const
{
if (tooltip.empty())
return {};
if ((path.empty() || path == ".") && tooltip.count(".") > 0) {
return {{".", tooltip.at(".")}};
}
for (const auto &[key, pattern] : tooltip) {
if (util::starts_with(path, key))
return {{key, pattern}};
}
return {};
return util::find_entry_by_path_prefix(tooltip, path);
}
};

View File

@@ -496,4 +496,75 @@ std::string format_message_comment(const std::string &comment, unsigned width)
return result;
}
std::filesystem::path normalize_relative_path(const std::filesystem::path &path)
{
if (path.is_absolute())
return path;
std::filesystem::path result;
for (const auto &part : path) {
if (part == ".") {
continue;
}
result /= part;
}
return result;
}
bool is_subpath(
const std::filesystem::path &path, const std::filesystem::path &base)
{
if (path.empty())
return false;
auto normalized_path = normalize_relative_path(path);
auto normalized_base = normalize_relative_path(base);
if (normalized_path == normalized_base)
return true;
auto rel = std::filesystem::relative(normalized_path, normalized_base);
std::string rel_str = rel.string();
return !rel_str.empty() && rel.native()[0] != '.';
}
std::optional<std::pair<std::string, std::string>> find_entry_by_path_prefix(
const std::map<std::string, std::string> &m, const std::string &path)
{
if (m.empty())
return {};
// Extract keys and sort them by length in descending order
std::vector<std::string> keys;
keys.reserve(m.size());
for (const auto &[key, pattern] : m) {
keys.push_back(key);
}
std::sort(keys.begin(), keys.end(),
[](const std::string &a, const std::string &b) {
return a.size() > b.size();
});
std::filesystem::path file_path{path};
for (const auto &key : keys) {
const auto &pattern = m.at(key);
std::filesystem::path key_path{key};
if (is_subpath(file_path, key_path)) {
return {{key, pattern}};
}
}
if ((path.empty() || file_path.is_relative() || path == ".") &&
m.count(".") > 0) {
return {{".", m.at(".")}};
}
return {};
}
} // namespace clanguml::util

View File

@@ -23,6 +23,8 @@
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <map>
#include <optional>
#include <string>
#include <type_traits>
#include <vector>
@@ -460,4 +462,9 @@ bool is_relative_to(
std::string format_message_comment(
const std::string &c, unsigned width = kDefaultMessageCommentWidth);
bool is_subpath(
const std::filesystem::path &path, const std::filesystem::path &prefix);
std::optional<std::pair<std::string, std::string>> find_entry_by_path_prefix(
const std::map<std::string, std::string> &m, const std::string &prefix);
} // namespace clanguml::util

View File

@@ -474,4 +474,64 @@ TEST_CASE("Test parse_source_location")
CHECK(column == 456);
result = false, file = "", line = 0, column = 0;
}
TEST_CASE("Test is_subpath")
{
using clanguml::util::is_subpath;
CHECK(is_subpath("./include/f.h", "."));
CHECK(is_subpath("include/f.h", "."));
CHECK(is_subpath("./include/f.h", "./"));
CHECK(is_subpath("./include/f.h", "./include"));
CHECK(is_subpath("./include/f.h", "include"));
CHECK(is_subpath("include/f.h", "./include"));
CHECK(is_subpath("include/f.h", "include"));
CHECK(is_subpath("include/f.h", "include/f.h"));
CHECK(is_subpath("./include/f.h", "include/f.h"));
CHECK(is_subpath("include/f.h", "./include/f.h"));
CHECK(is_subpath("./include/f.h", "include/f.h"));
#if !defined(_MSC_VER)
CHECK(is_subpath("/usr/local/include/f.h", "/usr/local"));
CHECK_FALSE(is_subpath("/usr/local/include/f.h", "/usr/local/lib"));
CHECK_FALSE(is_subpath("/usr/include/f.h", "."));
#else
CHECK(is_subpath("E:\\test\\src\\main.cpp", "E:\\test"));
CHECK_FALSE(is_subpath("E:\\test\\src\\main.cpp", "C:\\test"));
#endif
}
TEST_CASE("Test find_entry_by_path_prefix")
{
using clanguml::util::find_entry_by_path_prefix;
std::map<std::string, std::string> m;
m.emplace(".", "default");
m.emplace("./src", "internal_sources");
m.emplace("./src/thirdparty/", "thirdparty_sources");
m.emplace("/usr/include/boost", "boost_sources");
m.emplace("../my_other_project/src", "my_other_project_sources");
auto kv = find_entry_by_path_prefix(m, "/tmp");
CHECK_FALSE(kv.has_value());
kv = find_entry_by_path_prefix(m, "./src/main.cc");
CHECK(kv.value().second == "internal_sources");
kv = find_entry_by_path_prefix(m, "src/main.cc");
CHECK(kv.value().second == "internal_sources");
kv = find_entry_by_path_prefix(m, "src/thirdparty/main.cc");
CHECK(kv.value().second == "thirdparty_sources");
kv = find_entry_by_path_prefix(m, "include/f.h");
CHECK(kv.value().second == "default");
kv = find_entry_by_path_prefix(m, "./include/f.h");
CHECK(kv.value().second == "default");
kv = find_entry_by_path_prefix(m, "../my_other_project/src/lib.cc");
CHECK(kv.value().second == "my_other_project_sources");
}