Offer-motivated features: :hline defaults, ranged :hpos, @date :days, :bottom none

- @table: a writer's :hline/:vline replaces the default lines; new
  boundary name 'none' removes all lines
- @table: ranged :hpos argument overrides :cell_hpos per cell
  (\multicolumn{1} in tex; positions a colspan anchor's merged cell)
- @date/@datetime: :days offset argument (sks/date/date.py)
- @document: ":bottom none" suppresses the footer (\pagestyle{empty})

Also carries the escape-system generator/renderer fixes, per-cell-range
:format, and uppercase .TTF/.OTF font recognition from klammertext-dev.
This commit is contained in:
2026-07-24 21:37:58 +02:00
parent 8a2699a253
commit 4262fc6136
14 changed files with 657 additions and 146 deletions

83
mac/.clang-tidy Normal file
View File

@@ -0,0 +1,83 @@
Checks: >
*,
-boost-*,
-fuchsia-*,
-google-*,
-zircon-*,
-abseil-*,
-modernize-use-trailing-return-type,
-llvmlibc-*,
-altera-*,
-android-*,
-objc-*,
-fuchsia-*,
-hicpp-*,
-misc-*,
-performance-*,
-portability-*,
-readability-*,
-bugprone-*,
-cert-*,
-clang-analyzer-*,
-cppcoreguidelines-*,
-google-*,
-hicpp-*,
-llvm-*,
-llvmlibc-*,
-misc-*,
-modernize-*,
-performance-*,
-portability-*,
-readability-*,
-zircon-*
WarningsAsErrors: ''
HeaderFilterRegex: '.*'
FormatStyle: none
CheckOptions:
- key: readability-identifier-naming.ClassCase
value: CamelCase
- key: readability-identifier-naming.ClassMemberCase
value: camelBack
- key: readability-identifier-naming.ConstexprVariableCase
value: CamelCase
- key: readability-identifier-naming.ConstexprVariablePrefix
value: k
- key: readability-identifier-naming.EnumCase
value: CamelCase
- key: readability-identifier-naming.EnumConstantCase
value: CamelCase
- key: readability-identifier-naming.FunctionCase
value: camelBack
- key: readability-identifier-naming.GlobalConstantCase
value: CamelCase
- key: readability-identifier-naming.GlobalConstantPrefix
value: k
- key: readability-identifier-naming.GlobalVariableCase
value: camelBack
- key: readability-identifier-naming.GlobalVariablePrefix
value: g_
- key: readability-identifier-naming.LocalConstantCase
value: camelBack
- key: readability-identifier-naming.LocalVariableCase
value: camelBack
- key: readability-identifier-naming.MemberCase
value: camelBack
- key: readability-identifier-naming.MemberPrefix
value: m_
- key: readability-identifier-naming.NamespaceCase
value: lower_case
- key: readability-identifier-naming.PrivateMemberPrefix
value: m_
- key: readability-identifier-naming.StaticConstantCase
value: CamelCase
- key: readability-identifier-naming.StaticConstantPrefix
value: k
- key: readability-identifier-naming.StaticVariableCase
value: camelBack
- key: readability-identifier-naming.StaticVariablePrefix
value: s_
- key: readability-identifier-naming.TemplateParameterCase
value: CamelCase
- key: readability-identifier-naming.VariableCase
value: camelBack

View File

@@ -6,6 +6,7 @@
#include "show.h"
#include "katom.h"
#include "file.h"
#include <algorithm>
#include <unistd.h>
std::string shell(State state, std::string command, Locator loc)
@@ -151,8 +152,19 @@ katom_list Eval::eval(katom_iter begin, katom_iter end)
}
katom_list result {};
Machine M = m_machine;
size_t before = M.m_katoms.size();
M.read(eval_result);
M.apply(M.m_state.value("K_target"), false, false);
// If the @eval produced more Klammertext -- the read-back result still
// holds klammers to reduce (a "generator", e.g. a Python klammer returning
// a @table call with data) -- then its text is writer content: escape it
// for the target before applying, exactly as typed input is escaped, so a
// "%" in the data becomes "\%". If it produced final target markup (no
// klammers left, a "renderer" such as @table itself emitting \begin{tabular})
// leave it untouched. ^'...'^ literal spans are skipped by the escape pass,
// so a generator can still carry raw target markup.
bool has_klammer = std::any_of(
M.m_katoms.begin() + before, M.m_katoms.end(), begin_apply);
M.apply(M.m_state.value("K_target"), false, has_klammer);
result = trim(M.m_katoms);
return result;
}

View File

@@ -24,6 +24,18 @@
namespace fs = std::filesystem;
// A filesystem extension lowercased for case-insensitive matching, so a
// font named Foo.TTF or Foo.OTF is recognized like Foo.ttf / Foo.otf.
static std::string lower_extension(const fs::path& path)
{
std::string ext = path.extension();
for (char& c : ext) {
c = std::tolower(static_cast<unsigned char>(c));
}
return ext;
}
std::string name_to_dirname(std::string name)
{
std::string result {};
@@ -275,7 +287,10 @@ Font_file classify_font_file(const std::string& path)
{
Font_file file {};
file.path = path;
file.extension = fs::path(path).extension();
// Normalize to lowercase so the installed filename, its @font-face url,
// and the truetype/opentype format detection are uniform regardless of
// how the source file's extension was capitalized.
file.extension = lower_extension(path);
std::string d = read_binary_file(path);
if (d.size() < 12) {
@@ -372,7 +387,7 @@ std::vector<Font_file> classify_font_files(const std::string& directory)
"The font directory \"" + directory + "\" does not exist");
}
for (auto& entry : fs::recursive_directory_iterator(directory)) {
std::string ext = entry.path().extension();
std::string ext = lower_extension(entry.path());
if (entry.is_regular_file() && (ext == ".ttf" || ext == ".otf")) {
result.push_back(classify_font_file(entry.path()));
}

View File

@@ -214,6 +214,7 @@ void Klammer::copy_components(
}
for (auto c : cs) {
m_body[c.target] = c.body;
m_body_generic[c.target] = (c.target == Target_set::general_name);
m_varmap[c.target] = c.varmap;
}
}
@@ -287,6 +288,7 @@ void Klammer::copy_general_klammer_to_undefined(Target_set targets)
if (m_body.count(target_name) == 0 && target_name != Target_set::declare_name) {
// std::cout << " Copying to " << target_name << "\n";
m_body[target_name] = body;
m_body_generic[target_name] = true; // general body -> writer content
m_defloc[target_name] = loc;
m_varmap[target_name] = varmap;
}

View File

@@ -86,6 +86,9 @@ public:
std::map<std::string, Locator> m_defloc {}; // target -> Locator
std::map<std::string, defmode_t> m_defmode {}; // target -> defmode
target_variable_map_t m_varmap {}; // target -> map: variable -> index
// target -> true if this target's body came from a general ("*") definition
// (writer content, subject to target escaping) vs a target-specific one.
std::map<std::string, bool> m_body_generic {};
};
std::string klammer_name_from_katom(std::string s, Locator loc);

View File

@@ -413,6 +413,66 @@ katom_list Machine::apply_klammer(
result[i].m_type = katom_t::text;
}
}
// Escape target-specific characters (e.g. tex "&" -> "\&") in the writer
// text of a GENERAL klammer's body. Runs BEFORE process_katoms/apply()
// below expand the body, so that target-native markup pulled in by nested
// klammers (e.g. nl.tex -> "\newline") is left untouched -- only this
// klammer's own literal writer text is escaped here; nested klammers escape
// theirs when they are applied in turn. Bodies from target-specific
// definitions (m_body_generic[target] == false) are already in target form
// and skipped. KTESC markers are idempotent, so text already escaped at the
// top level passes through unchanged. Two kinds of body content are NOT
// writer text and must be skipped:
// * ^'...'^ literal spans -- raw target markup the writer typed directly.
// At this point they are typed literal_begin/literal_end with plain-text
// content (the literal phase runs in process_katoms, below), so track
// span depth rather than testing katom type.
// * @eval / @read / @cond argument spans -- code, filenames, and
// predicates consumed by the primitive, NOT emitted as target text.
// (Escaping an underscore in "offer.Price_list(K)" broke @eval.) The
// primitive's KLAMMERTEXT result, produced by process_katoms below, is
// klammer output and is likewise never escaped -- it is inserted after
// this pass and so is untouched, matching the top-level behavior where
// @eval is resolved before the escape pass runs.
auto gen = klammer.m_body_generic.find(target);
if (gen != klammer.m_body_generic.end() && gen->second) {
Target tgt = m_targets.get(target, Locator());
if (!tgt.m_escapes.empty()) {
int literal_depth = 0;
int code_depth = 0; // inside an @eval/@read/@cond span
std::vector<bool> apply_is_code; // one entry per open application
for (auto& k : result) {
if (k.m_type == katom_t::literal_begin) { ++literal_depth; continue; }
if (k.m_type == katom_t::literal_end) {
if (literal_depth > 0) --literal_depth;
continue;
}
if (k.m_type == katom_t::eval_begin ||
k.m_type == katom_t::read_begin ||
k.m_type == katom_t::cond_begin) {
apply_is_code.push_back(true);
++code_depth;
continue;
}
if (k.m_type == katom_t::apply_begin) {
apply_is_code.push_back(false);
continue;
}
if (k.m_type == katom_t::apply_end) {
if (!apply_is_code.empty()) {
if (apply_is_code.back()) --code_depth;
apply_is_code.pop_back();
}
continue;
}
if (literal_depth == 0 && code_depth == 0 &&
(k.m_type == katom_t::text ||
k.m_type == katom_t::word ||
k.m_type == katom_t::newline))
k.m_text = tgt.escape_text(k.m_text);
}
}
}
process_katoms(result, klammer.m_name);
apply(m_klammers, result, target);
m_state.close_frame();