Verbatim-safe typography, ^-punctuation quoting, full-range ^UUUU^, polyglot html

Typographic transforms (---, quote pairs, ~) no longer touch verbatim
text: @c/@code/@source_listing content and ^'...'^ spans show exactly the
characters written. "^" before any punctuation character quotes it in
every target (the apostrophe excepted: ^' opens a literal span), with the
new :resolve option on @@@target declaring per-target renderings. The
^UUUU^ code-point form accepts 4-6 hex digits, the full Unicode range.
The html output and transform spellings are polyglot (XML-valid), in
preparation for an EPUB target. New suites: transform_test, character_test
(engine), typography_test (SKS).

(from dev 07ce5ea86a0a)
This commit is contained in:
2026-08-23 20:48:26 +02:00
parent d982c0d6cc
commit 37b6ba1c4f
77 changed files with 1665 additions and 1608 deletions

View File

@@ -226,14 +226,66 @@ std::string justify_string(const std::string& s, unsigned int width=80, bool fre
return result;
}
// The error-message formatting contract (2026-08-21): a message carries no
// decisions about line breaks EXCEPT by indentation.
// * a line beginning with whitespace is VERBATIM -- an example, a pattern,
// a signature, a list entry -- emitted untouched: no folding, no
// wrapping, no "~" substitution (a pattern may contain a literal ~);
// * blank lines separate blocks (runs collapse to one);
// * everything else is prose: consecutive lines fold into one paragraph
// and are wrapped to the width.
// So prose stays machine-wrapped however the source hand-wraps it, and the
// one legitimate exception is marked in the one place it cannot be missed.
// This replaced a per-call do_justify flag on the Error constructors, whose
// decision lived apart from the text it governed. Line-based by hand: no
// std::regex over the whole message (the ambiguous-alternation hazard).
std::string justify(
const std::string& input_text, unsigned int text_width, unsigned int margin_width)
{
std::string result {};
text_width -= margin_width;
for (const std::string& par : split_into_paragraphs(trim(input_text))) {
result += justify_string(par, text_width) + "\n\n";
strings_t lines {};
{
const std::string text = trim(input_text);
std::string::size_type from = 0;
while (from <= text.size()) {
auto nl = text.find('\n', from);
if (nl == std::string::npos) {
lines.push_back(text.substr(from));
break;
}
lines.push_back(text.substr(from, nl - from));
from = nl + 1;
}
}
std::string result {};
strings_t prose {};
bool pending_blank = false;
auto emit = [&](const std::string& block) {
if (!result.empty()) {
result += pending_blank ? "\n\n" : "\n";
}
result += block;
pending_blank = false;
};
auto flush_prose = [&]() {
if (!prose.empty()) {
emit(justify_string(join(prose, " "), text_width));
prose.clear();
}
};
for (const std::string& line : lines) {
if (trim(line).empty()) { // block separator
flush_prose();
pending_blank = !result.empty();
} else if (line[0] == ' ' || line[0] == '\t') { // verbatim line
flush_prose();
emit(trim_right(line));
} else { // prose
prose.push_back(trim(line));
}
}
flush_prose();
result += "\n";
if (margin_width > 0) {
result = add_margin(result, margin_width);
}
@@ -367,8 +419,8 @@ std::vector<std::pair<std::string, std::string>> environment_variables(bool allo
auto parts = regex_split(environ[i], std::regex("="), true);
if (!allow_empty_definitions && parts.size() < 2) {
throw Internal_error(
"Incorrect environment variable format:\n" + std::string(environ[i]),
Locator(), false);
"Incorrect environment variable format:\n " + std::string(environ[i]),
Locator());
}
std::string name = parts[0];
parts.erase(parts.begin());