Files
klammertext/mac/katom_rewrite_rules.h
T
ack e4f1307aa9 Characters that killed a render, what may sit next to a klammer, and ref/
A character written in the ^...^ mnemonic form -- ^oe^, ^zz^, ^hello^ --
used to kill ktext outright: no message, no output, and an exit status
that looked like a crash because it was one.  Only the forms that happen
to spell hexadecimal digits escaped, and they were right by luck rather
than by reading.  Any caret form that names no character is now a located
error that quotes what was written.

A klammer may now sit inside square brackets and braces as it already
could inside parentheses, and may be followed by the punctuation that
ends a sentence: [@i word@], {@b word@}, @i word@'s.  The rule is a fixed
list of characters, because none of them can occur inside a klammer name
and so none of them is ambiguous next to a delimiter.  A letter still
cannot: "x@" is the named closing delimiter, so a letter before "@" has
two readings and no rule can separate them.

The abbreviations an author writes -- the hyphen form @i-word, a klammer
glued to its neighbouring punctuation -- are now recognised as author
conveniences only.  They are not applied to @@ definitions or @@@ system
commands, which are written by someone who needs no such shorthand and
whose forms are already whole.

Also fixed, each of them silent: an exported shell function in the
environment broke every @eval in the document; a line break written with
@nl was destroyed when it passed through a klammer that generates a
table, printing as \textbackslash{}newline in the finished PDF; and
"kdesc --coverage" shortened its file column by comparing the spelling of
a path rather than the path, so a klammer set reached by a symbolic link
showed a full path where its neighbours showed a short one.

New in the distribution: ref/, a small klammerset that describes the
Klammermachine's own tables in Klammertext.  It currently provides the
character tables -- the diacritics and the extended Latin letters -- as
klammers that render to any target, so the documentation of what can be
written cannot drift from what the machine accepts.  It is early, and
will grow.

The Klammertext taxonomy now has a section on targets, describes the
@@@klammerset system command alongside the other three, and states the
precedence of "^" -- what a caret means before a letter, a digit, a
punctuation character, and a quote, in one ordered list.

(from dev 6594c7ff8c1f)
2026-09-07 22:37:05 +02:00

151 lines
7.0 KiB
C++

#pragma once
#include <regex>
#include <string>
#include <vector>
#include "ktype.h"
// A regex kept beside the pattern it was built from, so a rule can be
// DISPLAYED as written (kdesc --rewrite) as well as applied. It lived in
// ktype.h until the rule table moved here, and nothing else ever used it.
class Rgx {
public:
explicit Rgx(std::string pattern)
: m_pattern(pattern)
, m_regex(std::regex(pattern)) {}
std::string m_pattern;
std::regex m_regex;
};
// THE KATOM REWRITE RULES: what divides text that is not yet a katom.
// Live since 2026-09-02. The table moved here from ktype.h, where it had
// been a flat list of tuples, and gained a category per rule; its companion
// is katomize.{h,cpp}, which does the dividing.
//
// WHY CATEGORIES. The rule list is a PRIORITY list: the first rule whose
// rewrite changes the word fires, its parts are re-katomized, and the loop
// RETURNS. Every later rule that would also have matched is preempted --
// and that preemption is the interaction the flat list states nowhere.
// Measured over every tracked .k and .kt (dbg/rule_conflicts.cpp): 1,971
// words reach the rules and there are 806 preemptions.
//
// Two facts from that measurement make the categories worth having:
//
// * TWO THIRDS OF THE INTERACTION IS LOCAL. 532 of the 806 preemptions
// are between rules of the SAME category. Grouping puts those pairs
// next to each other, where the reader can see them.
//
// * THE CATEGORY RELATION IS A STRICT ORDER. Cross-category preemption
// has no cycles, and -- this is the surprise -- the current declaration
// order already violates it nowhere. The order in ktype.h was arrived
// at one precedent at a time and is nevertheless a valid topological
// sort. Categories therefore DOCUMENT the existing order rather than
// changing it, which is why this file is expected to be behaviorally
// identical to the current katomizer. Any divergence the comparison
// reports is a transcription error here, not a design difference.
//
// The order between categories, and how much of it is forced:
//
// caret FORCED first -- it preempts every other category
// bar free (only "after caret" is forced)
// removal_span before removal_line
// karg before klammer
// punctuation before klammer
// desugar free (only "after caret" is forced)
// klammer before removal_line
// spacing before removal_line
// removal_line FORCED last
//
// Six constraints in place of thirty positions. Note that removal_span
// (#[, ]#, #-) and removal_line (a bare #) are one CHARACTER and two
// categories: the specific forms are decided before the general one, which
// is longest match stated as an ordering.
enum class rule_category {
caret, // ^| ^' '^ ^X ^...^ ^Xy -- the quoting and character forms
bar, // || | -- argument separators
removal_span, // #[ ]# #- -- delimited removal and whitespace removal
karg, // *name* -- argument substitution in a body
punctuation, // @ before . , ; : ? ! ( ) '
desugar, // @name-arg -- the abbreviated application
klammer, // @name@ and klammers embedded in a word
spacing, // #+N -- added whitespace
removal_line, // # -- remove to end of line
};
std::string category_name(rule_category category);
// The category sequence the dispatch follows. This is the whole of the
// cross-category ordering decision, in one place.
const std::vector<rule_category>& category_order();
struct Rewrite_rule {
rule_category category;
std::string description;
Rgx pattern;
std::string replacement;
// Non-null for a rule whose rewrite is not a plain regex_replace. One
// rule needs it: the abbreviated application @name-a-b, which today is
// a hand-cut special case at the TOP of expand_compound_katom() rather
// than an entry in the table at all. Making it a rule in the desugar
// category is safe -- no rule of an earlier category can match a word
// made only of "@", word characters and hyphens -- and it removes the
// oddity that the shortcut is currently reported by "kdiag --rewrite"
// as the doing of whichever rule happened to be first in the list.
std::string (*special)(const std::string& word) = nullptr;
};
// In DECLARATION order, matching ktype.h's katom_rewrite_rules index for
// index, so the two can be diffed. Dispatch order is a separate question:
// see rule_order() below.
extern const std::vector<Rewrite_rule> rewrite_rules;
enum class rewrite_order {
// The order written in ktype.h. Categories are annotation only, and
// the behavior is by construction identical to the current katomizer.
as_declared,
// Rules grouped so each category is contiguous, in category_order().
// Differs from as_declared by the position of ONE rule -- "single
// argument for special character", which moves from 15 to just after
// the punctuation rules -- and that rule is one of the eight that never
// fire. So this too is expected to be identical, and the comparison is
// what turns "expected" into "measured".
by_category,
};
// Indices into rewrite_rules, in dispatch order. Computed once.
const std::vector<size_t>& rule_order(rewrite_order order);
struct Rewrite_result {
bool matched = false;
size_t rule = 0; // index into rewrite_rules
std::string text {}; // the rewritten word
};
// The first rule that USEFULLY rewrites word: it changes the string, and no
// part of the result is the whole input. That second condition is the
// termination guard -- a rule that only pads a word with whitespace hands
// back the word it was given, and recursing on it exhausts the stack.
Rewrite_result find_rewrite(const std::string& word, rewrite_order order = rewrite_order::by_category);
// Every rule that would usefully rewrite word, in dispatch order. The
// first is what find_rewrite() returns; the rest are preempted by it.
std::vector<size_t> matching_rules(const std::string& word, rewrite_order order = rewrite_order::by_category);
// The executable form of the claim this file rests on: whatever fires, no
// rule of an EARLIER category also matched. When that holds for every
// word, the category order is a complete account of cross-category
// interaction and a reader need only understand one category at a time.
// Returns true when it holds; otherwise fills why with a description.
// This is what a flat priority list cannot offer -- it has no boundary to
// state an invariant across.
bool category_order_holds(const std::string& word, std::string& why,
rewrite_order order = rewrite_order::by_category);
// The "kdesc --rewrite" listing. It moved here with the table it reads,
// and gained the CATEGORY column -- which is the point of the categories
// reaching the user rather than only the maintainer: a reader sees the nine
// groups and their order instead of thirty positions.
void describe_rewrite_patterns();