Escaping, table layout, and document fixes

Quoted Klammertext specials (^@ ^| ^# ^^ ^: ^*) and ^'...'^ regions now
survive re-processing (held as escape markers until final output);
:after_apply phase functions receive and return raw target text.

Tables: :hpos element position (center|left|right|<length>) replaces the
unimplemented :center/:indent; the ranged cell override is renamed
:justify; :column_width works in html (colgroup widths) and gains
'fill' -- the remaining width, capped at the column's widest entry, in
both targets; a table wider than the text column warns on the console;
table edges without an outer line set their text flush on the margins.

@document: no empty title bar for untitled documents; @vfill fills to
the bottom of the window in html (pure CSS); @vspace in plain text;
new @dot klammer; monospace email links.
This commit is contained in:
2026-07-25 21:16:21 +02:00
parent 4262fc6136
commit d61336b191
26 changed files with 650 additions and 75 deletions

View File

@@ -1,3 +1,6 @@
#include <algorithm>
#include <cctype>
#include "target.h"
#include "log.h"
#include "show.h"
@@ -85,20 +88,60 @@ std::string Target::escape_text(std::string text) const
std::string Target::unescape_text(std::string text) const
{
// Restore KTESC markers to original characters (for programmatic use)
for (const auto& [ch, repl] : m_escapes) {
text = string_replace(text, escape_marker(ch), ch);
}
return text;
return ktesc_resolve(text);
}
std::string Target::resolve_escapes(std::string text) const
{
// Target-declared escapes first (marker -> declared replacement), then
// the generic decode for the remaining markers (marker -> the character
// itself: quoted Klammertext specials and literal-span content).
for (const auto& [ch, repl] : m_escapes) {
text = string_replace(text, escape_marker(ch), repl);
}
return ktesc_resolve(text);
}
std::string ktesc_resolve(std::string text)
{
// Hand-rolled scan: no std::regex here, this runs over document-sized
// strings.
static const std::string tag = "KTESC";
size_t pos = 0;
while ((pos = text.find(tag, pos)) != std::string::npos) {
size_t start = pos + tag.size();
size_t close = text.find(tag, start);
if (close == std::string::npos) break;
size_t len = close - start;
bool is_hex = len > 0 && len % 4 == 0 &&
std::all_of(text.begin() + start, text.begin() + close,
[](unsigned char c) { return std::isxdigit(c) != 0; });
if (!is_hex) {
// Not a marker body; the closing tag may open a real marker.
pos = start;
continue;
}
std::string chars {};
for (size_t i = start; i < close; i += 4)
chars += (char)std::stoi(text.substr(i, 4), nullptr, 16);
text.replace(pos, close + tag.size() - pos, chars);
pos += chars.size();
}
return text;
}
std::string hide_structural_characters(const std::string& s)
{
std::string result {};
for (char c : s) {
if (c == '@' || c == '|' || c == '#' || c == '^' || c == ':' || c == '*')
result += Target::escape_marker(std::string(1, c));
else
result += c;
}
return result;
}
void Target::add_after_apply(std::string function_specs)
{
for (auto f : regex_split(function_specs, std::regex(R"(\s+;\s+)"), true)) {