A section link names the TEXT of a heading: :section alone means the current file, a value names the file of the same @document that has it. Resolved by @document at assembly in html and tex; errors for a missing file, a missing title, a doubled title and a link outside a @document. The Markdown converter reads a Markdown anchor as a section link to the heading its slug names. Also from dev: the converter's four silent defects fixed and --chapter (9ae2c4cd), and the engine's alone-value error text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
56 lines
2.2 KiB
C++
56 lines
2.2 KiB
C++
#include "section_link.h"
|
|
#include "error.h"
|
|
#include "locator.h"
|
|
#include "util.h"
|
|
#include <regex>
|
|
#include <sstream>
|
|
|
|
std::string normalize_section_title(const std::string& s)
|
|
{
|
|
static const std::regex number_rgx(R"(<span class="sectionnumber">.*?</span>)");
|
|
static const std::regex tag_rgx("<[^>]*>");
|
|
static const std::regex tex_command_rgx(R"(\\[A-Za-z@]+\*?)");
|
|
static const std::regex space_rgx(R"(\s+)");
|
|
std::string t = std::regex_replace(s, number_rgx, "");
|
|
t = std::regex_replace(t, tag_rgx, "");
|
|
t = std::regex_replace(t, tex_command_rgx, "");
|
|
t = string_replace(string_replace(t, "{", ""), "}", "");
|
|
t = std::regex_replace(t, space_rgx, " ");
|
|
return trim(t);
|
|
}
|
|
|
|
Section_target resolve_section_target(
|
|
const std::vector<Section_target>& table, const std::string& title,
|
|
const std::string& file, const std::string& where,
|
|
const std::vector<std::string>& files)
|
|
{
|
|
const std::string wanted = normalize_section_title(title);
|
|
const std::string in = file.empty() ? where : file; // the one file searched
|
|
const std::string in_name = in.empty() ? std::string("the document's :text") : in;
|
|
std::stringstream ss {};
|
|
ss << "In " << (where.empty() ? std::string("the document's :text") : where)
|
|
<< ", a :section link names the heading " << q_(title);
|
|
if (!file.empty()) {
|
|
bool known = false;
|
|
for (const auto& f : files) known = known || f == file;
|
|
if (!known) {
|
|
ss << " in " << file << ", but the document has no file " << file
|
|
<< " (its files are " << join(files, ", ") << ").";
|
|
throw Argument_error(ss.str(), Locator::none());
|
|
}
|
|
}
|
|
std::vector<const Section_target*> hits {};
|
|
for (const auto& t : table) {
|
|
if (t.basename == in && t.title == wanted) hits.push_back(&t);
|
|
}
|
|
if (hits.size() == 1) return *hits[0];
|
|
if (hits.empty()) {
|
|
ss << ", which " << in_name << " does not have"
|
|
<< (file.empty() ? " (a section in another file needs its basename as the value of :section)." : ".");
|
|
} else {
|
|
ss << ", which " << in_name << " has " << hits.size()
|
|
<< " times: give one of them an :id and link to that.";
|
|
}
|
|
throw Argument_error(ss.str(), Locator::none());
|
|
}
|