Option sets: a .o target for shared parameters

A named group of optional parameters, declared once and used by several
klammers, so a writer learns one vocabulary instead of a spelling per
klammer.  The "o" target is a pseudo-target beside "k": "k" declares a
klammer's interface and documents it, "o" declares an option interface and
documents it, and neither produces output for any target.

    @@caption_args.o :caption :number.bool true :caption_side.side
    : Arguments that define a caption for a block element @@

    @@code.k :filename @hpos_args :hpos left @ @caption_args :caption_side top @
    | text.literal : A source file displayed verbatim @@

A set is used only in the parameter list of a ".k" declaration -- the one
place a klammer's interface is declared once for all of its targets -- and
is resolved as that list is read.  Names and types come from the set; a
default may be overridden where it is used.  A klammer application in a
parameter list is now a definition-time error.

The SKS gains the sets caption_args and hpos_args (:hpos and :offset), and
@table, @image, @image_grid, @reference and @show gain .k declarations.  A
distance is no longer written as a position: :hpos 4em is rejected, and the
same layout is :hpos left :offset 4em.  Code listings are numbered by
default, like tables and figures.

New engine sources mac/option_set{,_registry}.{h,cpp}; tst/ ships two more
suites, option_set_test.sh and signature_test.sh (twelve in all).

 (from dev 34e536cb0329)
This commit is contained in:
2026-08-06 13:11:37 +02:00
parent 4306dcd490
commit 6e7596ab2e
37 changed files with 2198 additions and 267 deletions

View File

@@ -41,7 +41,7 @@ are regenerated on each release — patches cannot be merged directly.
Report problems (or send patches) to the author; accepted changes are
applied to the development tree and appear in a following snapshot.
This snapshot was assembled from development commit `c27e63802406`.
This snapshot was assembled from development commit `34e536cb0329`.
## License

View File

@@ -101,6 +101,7 @@ int main(int argc, char* argv[])
args.opt("input", "Input filename", "filename", "", "'text'");
args.flag("targets", "Show targets defined by the input file");
args.flag("klammers", "Show klammers defined by the input file");
args.flag("optionsets", "Show option sets declared by the input file");
args.var("font", "List installed fonts. Enter \"--font help\" for font maintenance commands.");
args.var("klammerset", "List the klammersets on the search path. Enter \"--klammerset help\" for details.");
args.opt("v", "'verbosity'", "n", "0", "'verbosity'");
@@ -190,6 +191,10 @@ int main(int argc, char* argv[])
std::cout << boldblack << "Klammers\n" << black << M.m_klammers.describe(2);
}
if (p("optionsets")) {
std::cout << boldblack << "Option sets\n" << black << M.m_option_sets.describe(2);
}
}
catch (Error& e) {
e.print_message();

View File

@@ -9,7 +9,8 @@ include $(K)/env/makefile.env
# Source files
BASENAMES := util error locator file argv character ktype katom katom_list \
log show command argument argument_set argtype argtype_registry \
state eval eval_python eval_cpp klammer klammer_registry klammerset klammerset_registry deftype \
state eval eval_python eval_cpp klammer klammer_registry klammerset klammerset_registry \
option_set option_set_registry deftype \
target target_registry machine font_store check
SOURCES := $(addsuffix .cpp,$(BASENAMES))

View File

@@ -29,10 +29,19 @@ public:
Argtype m_argtype; // {};
bool m_optional {};
std::string m_default {};
// True when m_default was filled from the argument type's :default
// rather than declared in the klammer's parameter list (for kdesc
// provenance display).
// Where m_default came from, for kdesc provenance display. Default
// resolution has three levels -- the argument type's :default, an option
// set's declaration, and the site where the set is used (or the klammer's
// own parameter list) -- and a reader of kdesc is entitled to know which
// one produced the value a klammer will use when the argument is absent.
// True when m_default was filled from the argument type's :default:
bool m_default_from_type {};
// The option set (".o" target) this parameter was declared in; empty when
// it was declared in the klammer's own parameter list.
std::string m_option_set {};
// True when the option set declares a default for this parameter and the
// declaration using the set overrode it.
bool m_default_overridden {};
Locator m_loc;
std::string m_target {};
};

View File

@@ -13,6 +13,13 @@ std::tuple<std::vector<std::vector<Katom>>,std::vector<std::vector<Katom>>,std::
argument_split(std::vector<Katom>::const_iterator kbegin, std::vector<Katom>::const_iterator kend,
long unsigned int positional_limit = std::numeric_limits<int>::max());
// The katoms of a parameter list, split into one list per parameter:
// positional first, then optional (each beginning with its option-name
// katom). An option set keeps the katoms of its members this way, because
// those katoms are what a declaration using the set ends up declaring.
std::tuple<std::vector<std::vector<Katom>>,std::vector<std::vector<Katom>>>
parameter_split(std::vector<Katom>::const_iterator kbegin, std::vector<Katom>::const_iterator kend);
class Parameter_set
{
public:

View File

@@ -41,7 +41,10 @@ parse_name(const Target_registry& targets, const Katom& name_katom)
}
std::tuple<Katom, Parameter_set, katom_list, Locator>
parse_definition_katoms(const std::string& klammer_name, const Argtype_registry& argtypes, katom_iter& begin, katom_iter& end)
parse_definition_katoms(
const std::string& klammer_name, const std::string& target_name,
const Argtype_registry& argtypes, Option_set_registry& option_sets,
katom_iter& begin, katom_iter& end)
{
(void)K::log(3, *begin, *(end - 1));
katom_iter deftype = std::find_if(
@@ -68,7 +71,13 @@ parse_definition_katoms(const std::string& klammer_name, const Argtype_registry&
std::erase_if(parameter_katoms,
[](const Katom& k) { return k.m_type == katom_t::ignored; });
parameter_katoms = trim_whitespace(parameter_katoms);
if ((deftype->m_type == katom_t::klammer_instance ||
// "::" and ":::" take their parameters from the ".k" declaration, so a
// parameter list written with them is a mistake -- EXCEPT for an option
// set, which has no separate declaration to inherit from: a set is its
// own declaration, so an override restates what it declares.
bool inherits_parameters = target_name != Target_registry::optionset_name;
if (inherits_parameters &&
(deftype->m_type == katom_t::klammer_instance ||
deftype->m_type == katom_t::klammer_override) &&
!parameter_katoms.empty()) {
std::string sym = deftype->m_type == katom_t::klammer_instance ? "::" : ":::";
@@ -76,7 +85,16 @@ parse_definition_katoms(const std::string& klammer_name, const Argtype_registry&
"The \"" + klammer_name + "\" klammer uses the \"" + sym + "\" symbol but defines parameters.",
begin->m_loc);
}
// Replace the option sets used in the parameter list with the parameters
// they declare. This is where the difference between an option set and a
// klammer lies: a set is resolved HERE, as the parameter list is read,
// rather than in the fixed-point apply loop, so what it contributes is
// present when the list is parsed. Any other klammer application in a
// parameter list is rejected by the same pass.
option_set_uses_t option_set_uses =
expand_option_sets(parameter_katoms, klammer_name, target_name, option_sets);
Parameter_set parameters(parameter_katoms, argtypes);
stamp_option_set_uses(parameters, option_set_uses);
katom_list body_katoms(deftype + 1, end - 1);
body_katoms = trim_whitespace(body_katoms);
@@ -84,11 +102,12 @@ parse_definition_katoms(const std::string& klammer_name, const Argtype_registry&
}
void Klammer::add_target_definition(
const std::string& target_name, const Argtype_registry& argtypes, katom_iter begin, katom_iter end)
const std::string& target_name, const Argtype_registry& argtypes,
Option_set_registry& option_sets, katom_iter begin, katom_iter end)
{
(void)K::log(3, *begin, *(end-1));
auto [deftype, parameters, body, loc] =
parse_definition_katoms(m_name, argtypes, begin, end); // targets, begin, end);
parse_definition_katoms(m_name, target_name, argtypes, option_sets, begin, end);
// msg() << "Klammer " << m_name << " add: " << target_name << "\n";
// parameters.describe_parameters();
@@ -208,7 +227,8 @@ void Klammer::copy_components(
(void)K::log(4);
for (const auto& target_name : targets.m_names) {
if (target_name == Target_registry::declare_name ||
target_name == Target_registry::general_name) {
target_name == Target_registry::general_name ||
target_name == Target_registry::optionset_name) {
continue;
}
m_parameters = parameters;
@@ -243,7 +263,6 @@ void Klammer::check_for_declaration_and_definitions()
if (def.target != Target_registry::declare_name) {
if (def.deftype == katom_t::klammer_definition ||
def.deftype == katom_t::klammer_default) {
msg() << def << "\n";
definitions.push_back(def);
}
}
@@ -286,7 +305,11 @@ void Klammer::copy_general_klammer_to_undefined(const Target_registry& targets)
}
for (const auto& target_name : targets.m_names) {
// std::cout << "General copy, considering " << target_name << "\n";
if (m_body.count(target_name) == 0 && target_name != Target_registry::declare_name) {
// "k" and "o" declare interfaces rather than produce output, so a
// general body is never copied to them.
if (m_body.count(target_name) == 0 &&
target_name != Target_registry::declare_name &&
target_name != Target_registry::optionset_name) {
// std::cout << " Copying to " << target_name << "\n";
m_body[target_name] = body;
m_body_generic[target_name] = true; // general body -> writer content
@@ -299,6 +322,39 @@ void Klammer::copy_general_klammer_to_undefined(const Target_registry& targets)
// Three declaration cases: none, one, many
namespace {
// A compact, plain-text rendering of ONE definition's parameter list, for
// diagnostics that must show how two definitions differ.
// Klammer::signature_text() cannot serve here: it renders the rationalized
// m_parameters, which is exactly what does not exist yet when the per-target
// lists disagree.
std::string parameter_signature(const Parameter_set& parameters)
{
std::string result {};
auto add = [&result](const std::string& s) {
if (!result.empty()) result += " ";
result += s;
};
auto typed = [](const Parameter& p) {
return p.m_argtype.m_name == default_argtype
? p.m_name : p.m_name + "." + p.m_argtype.m_name;
};
bool first = true;
for (const auto& pos : parameters.m_positional) {
add(first ? typed(pos) : "| " + typed(pos));
first = false;
}
for (const auto& rest : parameters.m_rest)
add(typed(rest));
for (const auto& opt : parameters.m_optional)
add(":" + typed(opt)
+ (opt.m_default.empty() ? "" : " " + opt.m_default));
return result.empty() ? "(no parameters)" : result;
}
} // namespace
void Klammer::no_declarations(const Target_registry& targets)
{
(void)K::log(4);
@@ -318,14 +374,24 @@ void Klammer::no_declarations(const Target_registry& targets)
}
}
if (!all_equal<Parameter_set>(all_parameter_sets)) {
// std::cout << " Not all equal\n";
throw Definition_error(
error_list("There is no declaration (.k) target for klammer \"" + m_name + "\"\n"
"but the parameters of all targets are not the same",
m_defs,
"Use a .k klammer to define the parameters and describe the klammer,\n"
"with \"::\" and no parameters for all targets."),
m_defs[0].loc, false);
// Show each target's own parameter list, not just its location: with
// more than two targets the designer would otherwise have to diff the
// definitions by hand to find which one drifted.
std::stringstream ss {};
ss << "The parameters of klammer \"" << m_name
<< "\" are not the same for every target,\n"
"and there is no declaration (.k) target to define them once:\n";
for (const auto& def : m_defs) {
if (std::ranges::find(target_names, def.target) == target_names.end())
continue;
std::string target = def.target;
target.resize(std::max(target.size(), size_t(6)), ' ');
ss << " " << target << " " << parameter_signature(def.parameters)
<< "\n " << def.loc.desc() << "\n";
}
ss << "Use a .k target to declare the parameters and describe the klammer,\n"
"and \"::\" with no parameters for each target's definition.";
throw Definition_error(ss.str(), m_defs[0].loc, false);
} else {
// std::cout << " All equal\n";
copy_components(m_defs[0].parameters, m_defs, targets);
@@ -485,10 +551,39 @@ std::string Klammer::description_text() const
}
// Which option sets this klammer's parameters came from, and which of their
// defaults it overrode. A reader of the signature sees the effective
// interface; this says how much of it the klammer shares with other klammers,
// which is the reason for declaring a set in the first place.
std::string Klammer::option_set_text() const
{
std::vector<std::string> sets {};
std::map<std::string, std::vector<std::string>> overridden {};
for (const auto& opt : m_parameters.m_optional) {
if (opt.m_option_set.empty()) continue;
if (!is_in(opt.m_option_set, sets)) {
sets.push_back(opt.m_option_set);
}
if (opt.m_default_overridden) {
overridden[opt.m_option_set].push_back(":" + opt.m_name);
}
}
if (sets.empty()) return "";
std::vector<std::string> descriptions {};
for (const auto& set : sets) {
std::string desc = set;
if (overridden.count(set) > 0) {
desc += " (" + join(overridden[set], ", ") + " defaulted here)";
}
descriptions.push_back(desc);
}
return "\n Option sets: " + join(descriptions, ", ");
}
std::string Klammer::describe(int margin) const
{
std::string result {};
result += "@" + m_name + signature_text() + description_text();
result += "@" + m_name + signature_text() + description_text() + option_set_text();
result = add_margin(result, margin) + "\n";
return result;
}

View File

@@ -4,6 +4,7 @@
#include "deftype.h"
#include "argument_set.h"
#include "option_set_registry.h"
#include "target_registry.h"
#include "locator.h"
@@ -32,6 +33,7 @@ public:
void add_target_definition(
const std::string& target_name, const Argtype_registry& argtypes,
Option_set_registry& option_sets,
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
void remove_target_definition(const std::string& target_name);
@@ -62,6 +64,7 @@ public:
std::string signature_text() const;
std::string description_text() const;
std::string option_set_text() const;
std::string describe(int margin=0) const;
bool has_literal_param() const {
@@ -97,8 +100,13 @@ std::string klammer_name_from_katom(const std::string& s, const Locator& loc);
std::tuple<std::string,std::string>
parse_name(const Target_registry& targets, const Katom& name_katom);
// Split a definition's katoms into its definition separator, parameters,
// body and location. The target name is needed because the parameter list
// is where option sets are used, and a set may be used only in a ".k"
// declaration; option_sets is not const because a use is recorded on the set.
std::tuple<Katom, Parameter_set, std::vector<Katom>, Locator>
parse_definition_katoms(const std::string& klammer_name, const Argtype_registry& argtypes,
parse_definition_katoms(const std::string& klammer_name, const std::string& target_name,
const Argtype_registry& argtypes, Option_set_registry& option_sets,
std::vector<Katom>::iterator& begin, std::vector<Katom>::iterator& end);
/*

View File

@@ -12,7 +12,9 @@ bool Klammer_registry::has(std::string name, std::string target)
}
*/
void Klammer_registry::add(const Argtype_registry& argtypes, const Target_registry& targets, katom_iter begin, katom_iter end, katom_list& katoms)
void Klammer_registry::add(
const Argtype_registry& argtypes, const Target_registry& targets,
Option_set_registry& option_sets, katom_iter begin, katom_iter end, katom_list& katoms)
{
(void)K::log(3, *begin, *(end - 1));
restore_initial_type(begin, end);
@@ -20,6 +22,13 @@ void Klammer_registry::add(const Argtype_registry& argtypes, const Target_regist
if (!targets.has(target_name)) {
throw Argument_error("The target \"" + target_name + "\" is not defined", begin->m_loc);
}
if (target_name == Target_registry::optionset_name) {
// The Machine routes an ".o" definition to the option set registry;
// reaching here means it did not.
throw Internal_error(
"The option set declaration \"" + klammer_name + ".o\" reached the klammer registry",
begin->m_loc);
}
// Find the incoming definition mode
katom_t incoming_deftype = katom_t::klammer_definition;
for (auto it = begin + 1; it != end - 1; ++it) {
@@ -57,7 +66,8 @@ void Klammer_registry::add(const Argtype_registry& argtypes, const Target_regist
}
m_klammers[klammer_name].remove_target_definition(target_name);
}
m_klammers[klammer_name].add_target_definition(target_name, argtypes, begin + 1, end - 1);
m_klammers[klammer_name].add_target_definition(
target_name, argtypes, option_sets, begin + 1, end - 1);
// This add's target:
Target target = targets.get(target_name, begin->m_loc);
@@ -66,7 +76,8 @@ void Klammer_registry::add(const Argtype_registry& argtypes, const Target_regist
if (m_klammers[klammer_name].m_defloc.count(provide_name) > 0) {
m_klammers[klammer_name].remove_target_definition(provide_name);
}
m_klammers[klammer_name].add_target_definition(provide_name, argtypes, begin + 1, end - 1);
m_klammers[klammer_name].add_target_definition(
provide_name, argtypes, option_sets, begin + 1, end - 1);
}
}

View File

@@ -8,6 +8,7 @@ class Klammer_registry
public:
Klammer_registry() = default;
void add(const Argtype_registry& argtypes, const Target_registry& targets,
Option_set_registry& option_sets,
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
void rationalize(const Target_registry& targets);
void check_klammer(const std::string& name, const std::string& target, const Locator& loc) const;

View File

@@ -207,12 +207,27 @@ void Machine::process_cond_katoms(katom_list& katoms)
}
// Expand the constant klammers written in a definition's BODY. A constant is
// expanded at definition time, which is what makes it a constant; the body is
// where that is meaningful.
//
// The parameter list is deliberately excluded. A klammer application there
// is an error (see expand_option_sets): parameters shared between klammers
// are declared by an option set, whose ".o" declaration is resolved as the
// parameter list is read. Expanding a constant into a parameter list used to
// be the way to share parameters, and it silently destroyed the parameter
// list of a ".k" declaration -- the spliced options AND the declared
// positionals -- surfacing only as an argument error at the first
// application, in the document rather than the declaration.
void Machine::expand_constant_klammers(katom_list& katoms, const Katom& op, const Katom& cl)
{
auto [begin, end] = find_span_katoms(katoms, op, cl);
restore_initial_type(begin + 1, end - 1);
if (std::find_if(begin + 1, end - 1, begin_klammer_apply) == end - 1) return;
for (const auto& [app_op, app_cl] : find_spans(begin + 1, end - 1, begin_apply, end_apply, false, "def-time")) {
auto body_begin = std::find_if(
begin + 1, end - 1, [](const Katom& k) { return is_deftype(k.m_type); });
if (body_begin == end - 1) return;
if (std::find_if(body_begin, end - 1, begin_klammer_apply) == end - 1) return;
for (const auto& [app_op, app_cl] : find_spans(body_begin, end - 1, begin_apply, end_apply, false, "def-time")) {
auto [app_begin, app_end] = find_span_katoms(katoms, app_op, app_cl);
if (app_begin->m_type == katom_t::apply_begin) {
std::string name = trim_char(app_begin->m_text, '@');
@@ -506,14 +521,27 @@ void Machine::load_klammerset_files(const Klammerset& klammerset, katom_iter ins
m_katoms.insert(insert_at, loaded.begin(), loaded.end());
}
// Register one "@@...@@" definition. An ".o" target declares an option set
// -- parameters shared by klammers -- and goes to its own registry: it
// defines no klammer and produces no output for any target.
void Machine::add_definition(katom_list& katoms, const Katom& op, const Katom& cl)
{
expand_constant_klammers(katoms, op, cl);
auto [begin, end] = find_span_katoms(katoms, op, cl);
auto [name, target] = parse_name(m_targets, *begin);
if (target == Target_registry::optionset_name) {
m_option_sets.add(m_argtypes, name, begin, end, katoms);
} else {
m_klammers.add(m_argtypes, m_targets, m_option_sets, begin, end, katoms);
}
}
void Machine::extract_klammer_definitions(katom_list katoms)
{
fmsg() << katoms << "\n";
(void)K::log(3);
for (const auto& [op, cl] : find_spans(katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
expand_constant_klammers(katoms, op, cl);
auto [begin, end] = find_span_katoms(katoms, op, cl);
m_klammers.add(m_argtypes, m_targets, begin, end, katoms);
add_definition(katoms, op, cl);
}
m_klammers.rationalize(m_targets);
}
@@ -522,9 +550,7 @@ void Machine::extract_klammer_definitions()
{
(void)K::log(3);
for (const auto& [op, cl] : find_spans(m_katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
expand_constant_klammers(m_katoms, op, cl);
auto [begin, end] = find_span_katoms(m_katoms, op, cl);
m_klammers.add(m_argtypes, m_targets, begin, end, m_katoms);
add_definition(m_katoms, op, cl);
}
m_klammers.rationalize(m_targets);
}

View File

@@ -6,6 +6,7 @@
#include "katom_list.h"
#include "klammer_registry.h"
#include "klammerset_registry.h"
#include "option_set_registry.h"
#include "target_registry.h"
#include "argtype_registry.h"
#include "state.h"
@@ -27,6 +28,7 @@ public:
, m_targets(other.m_targets)
, m_klammers(other.m_klammers)
, m_klammersets(other.m_klammersets)
, m_option_sets(other.m_option_sets)
, m_result(other.m_result)
{}
@@ -38,6 +40,7 @@ public:
m_targets = other.m_targets;
m_klammers = other.m_klammers;
m_klammersets = other.m_klammersets;
m_option_sets = other.m_option_sets;
m_result = other.m_result;
}
return *this;
@@ -68,6 +71,7 @@ public:
bool klammers=true, bool eval=true, bool cond=true, bool read=true);
void expand_constant_klammers(katom_list& katoms, const Katom& op, const Katom& cl);
void add_definition(katom_list& katoms, const Katom& op, const Katom& cl);
void load_klammerset_files(const Klammerset& klammerset, katom_iter insert_at);
void extract_machine_definitions();
void extract_klammer_definitions();
@@ -92,6 +96,7 @@ public:
Target_registry m_targets {};
Klammer_registry m_klammers {};
Klammerset_registry m_klammersets {};
Option_set_registry m_option_sets {};
input_sources_t m_sources {};
std::string m_result {};
std::vector<Katom> m_katoms {};

64
mac/option_set.cpp Normal file
View File

@@ -0,0 +1,64 @@
#include <sstream>
#include "option_set.h"
#include "argtype_registry.h"
#include "util.h"
Option_set::Option_set(
const std::string& name, const std::string& desc,
const Parameter_set& parameters, const katom_lists& members, const Locator& loc)
: m_name(name)
, m_desc(desc)
, m_parameters(parameters)
, m_members(members)
, m_loc(loc)
{}
const Parameter* Option_set::find(const std::string& name) const
{
for (const Parameter& p : m_parameters.m_optional) {
if (p.m_name == name) {
return &p;
}
}
return nullptr;
}
std::string Option_set::member_names() const
{
std::vector<std::string> names {};
for (const Parameter& p : m_parameters.m_optional) {
names.push_back(":" + p.m_name);
}
return join(names, " ");
}
// One member per line: the name, its type, and its default -- the same three
// things a klammer's signature shows, since that is what the using klammer
// ends up declaring.
std::string Option_set::describe(int margin) const
{
std::string tab(margin, ' ');
std::stringstream ss {};
ss << tab << "@@" << m_name << ".o\n";
for (const Parameter& p : m_parameters.m_optional) {
std::string line = ":" + p.m_name;
if (p.m_argtype.m_name != default_argtype) {
line += "." + p.m_argtype.m_name;
}
if (!p.m_default.empty()) {
line += " " + p.m_default;
if (p.m_default_from_type) {
line += " (from the " + p.m_argtype.m_name + " argument type)";
}
}
ss << tab << " " << line << "\n";
}
if (!m_desc.empty()) {
ss << justify(m_desc, 80, margin + 2) << "\n";
}
if (!m_users.empty()) {
ss << tab << " Used by: " << join(m_users, ", ") << "\n";
}
return ss.str();
}

70
mac/option_set.h Normal file
View File

@@ -0,0 +1,70 @@
#pragma once
#include <string>
#include <vector>
#include "argument_set.h"
#include "deftype.h"
#include "locator.h"
#include "util.h"
// An Option_set is the construct declared by the "o" target:
//
// @@name.o :opt.argtype default ... : <description> @@
//
// a named group of OPTIONAL parameters declared once and used by several
// klammers, so that a writer learns one vocabulary (:hpos, :offset, the
// caption parameters) instead of a spelling per klammer. The "o" target is
// symmetric with "k": "k" declares a klammer's interface and documents it,
// "o" declares an option interface and documents it. Informally a klammer
// mix-in -- the term is from Flavors, where a mixin contributes slots
// without necessarily contributing methods, which is the shape here: a set
// contributes a DECLARATION, and the obligation to honor it stays with the
// klammer that uses it.
//
// A set is used in the parameter list of a ".k" declaration and nowhere
// else:
//
// @@code.k :filename @caption_args :caption_side top @ | text.literal :
//
// Names and types come from the set and cannot be changed at the use site; a
// DEFAULT may be overridden there, because what varies between klammers is
// only what silence means for that one klammer -- and kdesc shows a klammer's
// effective defaults anyway. Everything a reader relies on is the same
// wherever the set is used, which is the whole point of having one.
//
// The set keeps the katoms of each member as declared, because those are what
// is spliced into the using declaration's parameter list. The parsed
// Parameter_set beside them validates a use-site override at definition time
// and describes the set for kdesc.
class Option_set
{
public:
Option_set() = default;
Option_set(const std::string& name, const std::string& desc,
const Parameter_set& parameters, const katom_lists& members,
const Locator& loc);
// The member parameter named `name`, or nullptr if the set has none.
const Parameter* find(const std::string& name) const;
// ":a :b :c" -- the members, for a diagnostic that has to show them.
std::string member_names() const;
std::string describe(int margin = 2) const;
std::string m_name {};
std::string m_desc {};
Parameter_set m_parameters {};
// One katom list per member, in declared order, each beginning with the
// member's option-name katom.
katom_lists m_members {};
Locator m_loc {};
// How this set was declared, for the redefinition policy: the same
// transition table that governs klammer redefinition governs sets.
defmode_t m_defmode { defmode_t::def_create };
// The klammers whose ".k" declaration uses this set, in declaration
// order. A set's users are as interesting as its members: they are the
// klammers that promise to honor the vocabulary.
std::vector<std::string> m_users {};
};

359
mac/option_set_registry.cpp Normal file
View File

@@ -0,0 +1,359 @@
#include <sstream>
#include "option_set_registry.h"
#include "argument_set.h"
#include "error.h"
#include "katom_list.h"
#include "klammer.h"
#include "log.h"
#include "show.h"
#include "target_registry.h"
#include "util.h"
// --- Registration ---------------------------------------------------------
void Option_set_registry::add(
const Argtype_registry& argtypes, const std::string& name,
katom_iter begin, katom_iter end, katom_list& katoms)
{
(void)K::log(3, *begin, *(end - 1));
restore_initial_type(begin, end);
katom_iter definition_begin = begin + 1;
katom_iter definition_end = end - 1;
auto [deftype, parameters, body, loc] = parse_definition_katoms(
name, Target_registry::optionset_name, argtypes, *this,
definition_begin, definition_end);
if (deftype.m_initial_type == katom_t::klammer_instance) {
throw Definition_error(
"The option set " + q_(name) + " is declared with \"::\", which takes its "
"parameters from a \".k\" declaration. An option set IS a declaration: "
"it declares its parameters itself, after \":\".",
begin->m_loc);
}
defmode_t incoming_mode = defmode_from_katom(deftype.m_initial_type);
if (has(name)) {
const Option_set& current = m_option_sets.at(name);
const auto& result = defmode_transition(current.m_defmode, incoming_mode);
// The transition table is the single statement of the redefinition
// policy, for klammers and option sets alike; only the noun in its
// messages is specific to what is being redefined.
std::string message = string_replace(result.message, "Klammer NAME", "Option set NAME");
message = string_replace(message, "klammer NAME", "option set NAME");
message = string_replace(message, "NAME", q_(name + ".o"));
message = string_replace(message, "AT", current.m_loc.desc());
if (!result.replace) {
if (message.empty()) { // a default silently superseded
modify_type(katom_t::replaced, begin, end);
auto next_iter = end;
ignore_whitespace(next_iter, katoms);
return;
}
throw Definition_error(message, begin->m_loc);
}
if (result.warn) {
warning(message, begin->m_loc);
}
}
// Optional parameters only. A positional parameter is not writer-facing
// -- the author never types its name -- so there is nothing for an option
// set to standardize, and a set of them would be a klammer signature
// rather than a shared vocabulary.
if (!parameters.m_positional.empty() || !parameters.m_rest.empty()) {
std::vector<std::string> names {};
for (const auto& p : parameters.m_positional) names.push_back(p.m_name);
for (const auto& p : parameters.m_rest) names.push_back(p.m_name);
throw Definition_error(
"The option set " + q_(name) + " declares the positional "
+ plural("parameter", static_cast<int>(names.size())) + " "
+ join(names, ", ") + ".\n"
"An option set declares only optional parameters -- names written with "
"a leading \":\".",
begin->m_loc, false);
}
if (parameters.m_optional.empty()) {
throw Definition_error(
"The option set " + q_(name) + " declares no parameters.\n"
"The form is: @@" + name + ".o :name.argtype default ... : <description> @@",
begin->m_loc, false);
}
// The members as written: these katoms are what is spliced into the
// parameter list of a declaration that uses the set.
auto [positional_katoms, member_katoms] =
parameter_split(parameters.m_katoms.cbegin(), parameters.m_katoms.cend());
(void)positional_katoms; // already rejected above
// The two views of the members -- as katoms and as parsed parameters --
// are used together when the set is spliced into a parameter list, and
// are paired by position.
if (member_katoms.size() != parameters.m_optional.size()) {
throw Internal_error(
"The option set " + q_(name) + " parsed " + std::to_string(parameters.m_optional.size())
+ " parameters from " + std::to_string(member_katoms.size()) + " declarations",
begin->m_loc);
}
Option_set option_set(name, to_string(body, true), parameters, member_katoms, begin->m_loc);
option_set.m_defmode = incoming_mode;
if (has(name)) {
// A redefinition keeps the users recorded so far: they used the name,
// and the name is what they are bound to.
option_set.m_users = m_option_sets.at(name).m_users;
} else {
m_names.push_back(name);
}
m_option_sets[name] = option_set;
modify_type(katom_t::replaced, begin, end);
auto next_iter = end;
ignore_whitespace(next_iter, katoms);
}
bool Option_set_registry::has(const std::string& name) const
{
return m_option_sets.count(name) > 0;
}
const Option_set& Option_set_registry::get(const std::string& name, const Locator& loc) const
{
auto it = m_option_sets.find(name);
if (it == m_option_sets.end()) {
throw Definition_error("The option set " + q_(name) + " is not declared", loc);
}
return it->second;
}
void Option_set_registry::add_user(const std::string& set_name, const std::string& klammer_name)
{
auto it = m_option_sets.find(set_name);
if (it != m_option_sets.end() && !is_in(klammer_name, it->second.m_users)) {
it->second.m_users.push_back(klammer_name);
}
}
std::string Option_set_registry::available() const
{
return m_names.empty() ? "(none are declared)" : join(m_names, ", ");
}
std::string Option_set_registry::describe(int margin) const
{
std::string result {};
for (const auto& name : m_names) {
result += m_option_sets.at(name).describe(margin) + "\n";
}
return result;
}
// --- Use in a parameter list ----------------------------------------------
namespace {
// The end of the application opening at `begin`: one past its closing
// delimiter. Depth is counted over klammer applications only, which is all
// that can nest inside an option-set use.
katom_iter application_end(katom_iter begin, katom_iter end)
{
int depth = 0;
for (auto k = begin; k != end; ++k) {
if (begin_klammer_apply(*k)) {
++depth;
} else if (end_klammer_apply(*k)) {
if (--depth == 0) return k + 1;
}
}
throw Definition_error(
"The klammer " + q_(trim_char(begin->m_text, '@')) +
" in a parameter list is not closed", begin->m_loc);
}
// The parameter name in an option-name katom, which carries the type and any
// target as written: ":number.bool" declares the parameter named "number".
std::string option_name(const Katom& katom)
{
std::string name(katom.m_text, 1);
auto type = name.find('.');
return type == std::string::npos ? name : name.substr(0, type);
}
// How a definition is named in a diagnostic: a general definition has no
// target to name, and the pseudo-targets read better with their suffix.
std::string definition_name(const std::string& klammer_name, const std::string& target_name)
{
return target_name == Target_registry::general_name
? q_(klammer_name) : q_(klammer_name + "." + target_name);
}
// A klammer application in a parameter list is never legal. Which of the two
// ways it is wrong decides what the writer has to do about it, so the message
// says which.
[[noreturn]] void reject_application(
const std::string& name, const std::string& klammer_name,
const std::string& target_name, const Option_set_registry& option_sets,
const Locator& loc)
{
std::stringstream ss {};
if (option_sets.has(name) && target_name == Target_registry::optionset_name) {
ss << "The option set " << q_(name) << " is used in the declaration of the option "
<< "set " << q_(klammer_name) << ".\n"
<< "An option set is used only in the parameter list of a \".k\" declaration, "
<< "so a set does not include another set: a klammer that needs two "
<< "vocabularies names two sets, and each set stays a vocabulary that can "
<< "be learned whole.";
} else if (option_sets.has(name)) {
ss << "The option set " << q_(name) << " is used in the parameter list of "
<< definition_name(klammer_name, target_name) << ".\n"
<< "An option set may be used only in the parameter list of a \".k\" "
<< "declaration, which is where a klammer's interface is declared once "
<< "for all of its targets. Declare "
<< q_(klammer_name + ".k") << " and give each target's definition as "
<< "an instance (\"::\"), which inherits the declared parameters.";
} else {
ss << "The klammer " << q_(name) << " is applied in the parameter list of "
<< definition_name(klammer_name, target_name) << ".\n"
<< "A klammer application in a parameter list is not allowed: it is "
<< "resolved after the parameters are parsed, so the parameter list it "
<< "was meant to contribute is not there when the list is read. An "
<< "option set, declared with a \".o\" target, is how parameters are "
<< "shared between klammers. Declared option sets: "
<< option_sets.available() << ".";
}
throw Definition_error(ss.str(), loc, false);
}
// The default written for each member at the use site:
// @caption_args :number false :caption_side top @
// Only defaults may be given -- names and types belong to the set.
std::map<std::string, std::string> use_site_defaults(
const Option_set& option_set, katom_iter begin, katom_iter end)
{
auto [positional, optional, rest] = argument_split(begin + 1, end - 1, 0);
if (!positional.empty() || active(rest)) {
throw Definition_error(
"The use of the option set " + q_(option_set.m_name) +
" gives a value that is not an option.\n"
"A set's names and types are fixed where the set is declared; only a "
"default may be given where it is used, written as \":name value\".",
begin->m_loc, false);
}
std::map<std::string, std::string> defaults {};
for (const auto& option : optional) {
std::string name = option_name(option[0]);
if (name.size() + 1 != option[0].m_text.size()) {
throw Definition_error(
"The use of the option set " + q_(option_set.m_name) + " gives a type for \":"
+ name + "\".\n"
"A set's names and types are declared where the set is; only a default "
"may be given where it is used.",
option[0].m_loc, false);
}
const Parameter* member = option_set.find(name);
if (member == nullptr) {
throw Definition_error(
"The option set " + q_(option_set.m_name) + " has no parameter \":"
+ name + "\".\n It declares: " + option_set.member_names(),
option[0].m_loc, false);
}
if (defaults.count(name) > 0) {
throw Definition_error(
"A default for \":" + name + "\" is given more than once in the use of "
"the option set " + q_(option_set.m_name),
option[0].m_loc);
}
std::string value = trim(to_string(katom_list(option.begin() + 1, option.end())));
// A bare option name means the argument type's :alone value, exactly
// as it does where an argument is written.
if (value.empty() && !member->m_argtype.m_alone.empty()) {
value = member->m_argtype.m_alone;
}
// Validated here, at definition time: an invalid default must not
// wait for an application that happens not to supply the argument.
Parameter_set::validate(*member, value, option[0].m_loc);
defaults[name] = value;
}
return defaults;
}
} // namespace
option_set_uses_t expand_option_sets(
katom_list& parameters, const std::string& klammer_name,
const std::string& target_name, Option_set_registry& option_sets)
{
option_set_uses_t uses {};
// Every parameter name in the list, and where it came from, so that a
// collision between two sets (or between a set and a name written here)
// can name both origins rather than just the name.
std::map<std::string, std::string> origin {};
auto declare = [&](const std::string& name, const std::string& from, const Locator& loc) {
auto previous = origin.find(name);
if (previous != origin.end()) {
throw Definition_error(
"The parameter \":" + name + "\" of " + q_(klammer_name) +
" is declared twice:\n " + previous->second + "\n " + from,
loc, false);
}
origin[name] = from;
};
katom_list result {};
result.reserve(parameters.size());
for (auto k = parameters.begin(); k != parameters.end(); ) {
if (!begin_klammer_apply(*k)) {
if (k->m_type == katom_t::option_name) {
declare(option_name(*k), "declared in the parameter list", k->m_loc);
}
result.push_back(*k);
++k;
continue;
}
std::string name = trim_char(k->m_text, '@');
auto end = application_end(k, parameters.end());
if (target_name != Target_registry::declare_name || !option_sets.has(name)) {
reject_application(name, klammer_name, target_name, option_sets, k->m_loc);
}
const Option_set& option_set = option_sets.get(name, k->m_loc);
std::map<std::string, std::string> defaults = use_site_defaults(option_set, k, end);
// m_members and m_parameters.m_optional are the same members in the
// same order -- one is the katoms as declared, the other what they
// parsed to -- so the parameter's name is taken from the parsed side
// rather than re-derived from the katom text (":number.bool").
for (size_t i = 0; i < option_set.m_members.size(); ++i) {
const katom_list& member = option_set.m_members[i];
const std::string& member_name = option_set.m_parameters.m_optional[i].m_name;
declare(member_name, "from the option set " + q_(name) + ", "
+ option_set.m_loc.desc(), k->m_loc);
if (!result.empty()) {
result.push_back(Katom(" ", katom_t::space, member[0].m_loc));
}
// The name katom comes from the set, so the parameter's location
// is where it is declared; an overriding default comes from here.
result.push_back(member[0]);
auto given = defaults.find(member_name);
if (given == defaults.end()) {
result.insert(result.end(), member.begin() + 1, member.end());
} else if (!given->second.empty()) {
result.push_back(Katom(" ", katom_t::space, k->m_loc));
result.push_back(Katom(given->second, katom_t::text, k->m_loc));
}
uses[member_name] = {name, given != defaults.end()};
}
option_sets.add_user(name, klammer_name);
k = end;
}
parameters = result;
return uses;
}
void stamp_option_set_uses(Parameter_set& parameters, const option_set_uses_t& uses)
{
if (uses.empty()) return;
for (Parameter& parameter : parameters.m_optional) {
auto use = uses.find(parameter.m_name);
if (use == uses.end()) continue;
parameter.m_option_set = use->second.m_set;
parameter.m_default_overridden = use->second.m_overridden;
}
}

60
mac/option_set_registry.h Normal file
View File

@@ -0,0 +1,60 @@
#pragma once
#include <map>
#include <string>
#include <vector>
#include "argtype_registry.h"
#include "katom.h"
#include "option_set.h"
class Option_set_registry
{
public:
// Parse and register an "@@name.o <parameters> : <description> @@"
// declaration. The name is passed in because the caller has already
// split it from its ".o" target suffix in order to route here.
void add(const Argtype_registry& argtypes, const std::string& name,
katom_iter begin, katom_iter end, katom_list& katoms);
bool has(const std::string& name) const;
const Option_set& get(const std::string& name, const Locator& loc) const;
void add_user(const std::string& set_name, const std::string& klammer_name);
std::string describe(int margin = 2) const;
// "a, b, c" -- the sets that exist, for a diagnostic about one that does not.
std::string available() const;
std::map<std::string, Option_set> m_option_sets {};
std::vector<std::string> m_names {};
};
// What one parameter inherited from an option set: which set it came from,
// and whether its default was overridden where the set was used. Default
// resolution has three levels -- argument type, option set, use site -- and
// kdesc says which one a klammer's effective default came from, so the level
// has to be recorded rather than inferred from the value.
struct Option_set_use
{
std::string m_set {};
bool m_overridden {};
};
// parameter name -> where it came from
using option_set_uses_t = std::map<std::string, Option_set_use>;
// Replace every option-set use in a parameter list with the parameters it
// stands for. Any other klammer application in a parameter list is an
// error: an option set is the only construct that may put parameters there,
// and only in a ".k" declaration.
//
// `parameters` is modified in place. Sets are recorded as used by
// `klammer_name`, so `option_sets` is not const.
option_set_uses_t expand_option_sets(
katom_list& parameters, const std::string& klammer_name,
const std::string& target_name, Option_set_registry& option_sets);
// Record on each parameter that came from a set which set it came from and
// whether its default was overridden. Called after the expanded parameter
// list has been parsed.
void stamp_option_set_uses(Parameter_set& parameters, const option_set_uses_t& uses);

View File

@@ -570,6 +570,7 @@ std::ostream& operator<<(std::ostream& os, const Machine& m)
std::string state_desc = m.m_state.describe(false, 3);
std::string targets_desc = m.m_targets.describe(4);
std::string klammersets_desc = m.m_klammersets.describe(4);
std::string option_sets_desc = m.m_option_sets.describe(4);
std::string klammer_desc = m.m_klammers.describe(2);
std::string source_desc = describe_sources(m);
@@ -578,6 +579,7 @@ std::ostream& operator<<(std::ostream& os, const Machine& m)
<< label("Argtypes", m.m_argtypes.m_names.size()) << argtypes_desc << "\n"
<< label("Targets", m.m_targets.m_names.size()) << targets_desc << "\n"
<< label("Klammersets", m.m_klammersets.m_symbols.size()) << klammersets_desc << "\n"
<< label("Option sets", m.m_option_sets.m_names.size()) << option_sets_desc << "\n"
<< label("Klammers", m.m_klammers.m_klammers.size()) << klammer_desc << "\n"
<< label("State", m.m_state.m_frames.size()) << state_desc;
return os;

View File

@@ -11,14 +11,17 @@
std::string Target_registry::declare_name = "k";
std::string Target_registry::general_name = "*";
std::string Target_registry::optionset_name = "o";
Target_registry::Target_registry()
: m_parameters(Parameter_set("name | desc :after_apply :after_write :includes :escape | transforms.rest"))
{
Target k(declare_name, "Description of parameters and klammer result", Locator());
Target general(general_name, "General target, used when a target is not specified", Locator());
Target option_set(optionset_name, "Declaration of an option set: parameters shared by klammers", Locator());
add(k);
add(general);
add(option_set);
}
void Target_registry::add(Target target)
@@ -104,14 +107,19 @@ std::vector<std::string> Target_registry::user_defined() const
{
return collect_if(
m_names, [](const auto& name) {
return name != Target_registry::declare_name && name != Target_registry::general_name; });
return name != Target_registry::declare_name
&& name != Target_registry::general_name
&& name != Target_registry::optionset_name; });
}
// The targets a klammer body can be applied under. Neither "k" nor "o"
// produces output: both declare an interface and describe it.
std::vector<std::string> Target_registry::applicable() const
{
return collect_if(
m_names, [](const auto& name) {
return name != Target_registry::declare_name; });
return name != Target_registry::declare_name
&& name != Target_registry::optionset_name; });
}
std::string Target_registry::describe(int margin, bool long_format) const

View File

@@ -11,6 +11,10 @@ class Target_registry
public:
static std::string declare_name;
static std::string general_name;
// The pseudo-target of an option set declaration (@@name.o), reserved in
// the target namespace exactly as "k" is: both declare an interface and
// document it, and neither produces output for any target.
static std::string optionset_name;
Target_registry();

View File

@@ -1,3 +1,26 @@
#[
A paragraph is a document-structural object, like a section or a table of
contents, and making paragraphs is a service @document provides: inside a
@document, blank-line-separated text becomes paragraphs (the LaTeX
convention) and @par need not be written. @par is the explicit form, for
a text FRAGMENT rendered without a @document, where nothing infers them.
Written inside a @document it is harmless: html sees a <p>, which the
paragraph pass recognizes as a block and leaves alone, and \par in
vertical mode is a no-op.
]#
@@par.k s : A paragraph of text @@
@@par.html :: <p>*s*</p> @@
@@par.tex ::
\par
*s*
\par
@@
# In plain text a paragraph is delimited by blank lines, which
# phases.justify_blocks then fills; #/2 inserts them without depending on
# the definition body's own whitespace surviving extraction.
@@par.txt :: #/2*s*#/2 @@
@@sp.k : Non-breaking space character @@
@@sp.html :: &^#160; @@
@@sp.tex :: ~ @@

View File

@@ -27,7 +27,7 @@ class Indent(klammer_base.Klammer_base):
result = tab + latex_util.minipage(
"\\raggedright " + self.s, f"\\textwidth - {self.w}ex", center=False, vmargin="4pt")
#print(result)
return result
return latex_util.block(result)
def txt(self):
indent = " " * self.w
@@ -54,13 +54,15 @@ class Note(klammer_base.Klammer_base):
#width = r'\\textwidth - 16pt - {}\\leftmargin'.format(self.level)
width = r'\\linewidth - \\leftmargin + 2pt'
# The \par on each side comes from latex_util.block() below: an
# \fcolorbox is box material and must sit in vertical mode.
result = '''
\\par\\begingroup
\\begingroup
COLOR\\setlength{\\fboxsep}{8pt}
\\fcolorbox{bordercolor}{localcolor}{
\\parbox{WIDTH}{\\raggedright\\setlength{\\parskip}{8pt}
\\textbf{LABEL:} TEXT
}}\\endgroup\\par
}}\\endgroup
'''
result = re.sub('LABEL', self.label, result)
result = re.sub('TEXT', re.sub(r'\\', r'\\\\', self.s), result)
@@ -68,7 +70,7 @@ COLOR\\setlength{\\fboxsep}{8pt}
result = re.sub('COLOR', r'\\definecolor{{bordercolor}}{{rgb}}{{{}}}\n'.format(self.bordercolor), result)
result = re.sub('WIDTH', width, result)
print(result)
return result
return latex_util.block(result)
class Block(klammer_base.Klammer_base):
@@ -80,6 +82,9 @@ class Block(klammer_base.Klammer_base):
return ""
def tex(self):
# NOT latex_util.block(): a textblock is absolutely positioned and
# does not participate in the normal flow, so breaking the paragraph
# around it would move the surrounding text.
to_x, to_y = [float(e) for e in self.to.split()]
pt_x, pt_y = [float(e) for e in self.point.split()]
result = f"""

View File

@@ -1,128 +1,20 @@
@@code.k :filename :pattern :caption :number.bool | text.literal :
@@code.k :filename :pattern
@hpos_args :hpos left @
@caption_args :caption_side top @
| text.literal :
A source file displayed verbatim
@@
@@code :: @eval code_format.Code(K) @ @@
@@code :: @eval code_block.Code(K) @ @@
@@c.k code_text :
A word or phrase displayed verbatim in a line
@@
@@c :: @eval code_format.Code_fragment(K) eval@
@@c :: @eval code_block.Code_fragment(K) eval@
@@
# :cwd makes the filename resolve against the DOCUMENT's directory, not
# the directory ktext happens to run in.
@@source_file filename : @eval :cwd *K_input_dir* code_format.Source(K) @ @@
#[
@@code.k text :file :number.bool true :caption : Code listing with formatted comments @@
@@code :
@eval code_format.Code(K) eval@
@@
@@lst spec.figure_id :
@reference *spec* | Listing @
@@
c <text>
code <text> :caption :filename :pattern
# --------------------------------------------------------------------------------
@@codebox.k s :color 1,1,1 :size normalsize :escapechar ^^
:space_break_only.bool false :linenumber.bool false :scale 1.0
:indent :standalone :vcenter :
Verbatim text for source code preserving whitepace, surrounded by a box
that extends to the margins @@
@@codebox.html ::
@code :text *s* @
@@
@@codebox.tex ::
\definecolor{codeboxbgcolor}{rgb}{*color*}
\setlength{\codeboxlinelength}{\linewidth - 6pt}
\vspace*{4pt}
\begin{lstlisting}%
[frame=single,
framerule=1pt,
basicstyle=\*size*\ttfamily,
lineskip=0pt,
linewidth=*scale*\codeboxlinelength,
columns=fullflexible,
keepspaces=true,
framesep=6pt,
xleftmargin=6pt,
escapechar=*escapechar*,
breaklines=true,
prebreak=\hbox{\large$\mapsto$},
%postbreak={\textbf{\hbox{$\rightarrow$}}},
rulecolor=\color{codeboxcolor},
backgroundcolor=\color{codeboxbgcolor},
breakatwhitespace=*space_break_only*,
numbers=none, # #- @if *linenumber* | left | none @ #- ,
numbersep=12pt,
numberstyle=\small\color{Darkred}]
*s*
\end{lstlisting}
@@
@@codebox.txt ::
@code :text *s* @
@@
@@pathname.k s :small.bool false : Pathname @@
@@pathname :: @eval code_format.Pathname(K) eval@ @@
@@annotate.k text :caption :
Comments put in boxes to the right of the code
@@
# @@annotate : @eval code_format.Annotate(K) eval@ @@
# @@annotate : @codebox *text* @ @@
@@annotate :: @code :text *text @ @@
@@listing s : @code :text *s* @ @@
# --------------------------------------------------------------------------------
@@sv.k s : Italic font for variable in @t syntax @ argument @@
@@sv.tex :: ^^textrm"^^textit"*s*$$ @@
@@sv.html :: @ri *s* @ @@
@@svs.k s : Sans-serif font for variable in @t syntax @ argument @@
@@svs.tex :: "^^small^^textsf"^^textit"*s*$$$ @@
@@svs.html :: @s @i *s* @ @ @@
@@svsub.k base | sub : Italic font for subscripted variable in @t syntax @ argument @@
@@svsub.tex :: ^^textrm"^^textit"*base*$$^^textsubscript"*sub*$ @@
@@svsub.html :: <span class="ritalic">*base*</span><sub>*sub*</sub> @@
@@syntax.k s :fontsize normalsize :indent.bool true :
Verbatim text that includes italic font for syntax descriptions @@
@@syntax.html ::
@code :text *s* @ # :indent *indent* @
@@
@@syntax.tex ::
\vspace*{8pt}\begin{LVerbatim}[xleftmargin=0pt, # @if true | 0pt | -24pt @ ,
baselinestretch=1.05, fontsize=\*fontsize*, frame=single, framesep=8pt,
commandchars=\\̈\$, fontfamily = @verbatimfont@, framesep=12pt]
*s*
\end{LVerbatim}
@@
@@svspace.tex length :
^^vspace*"*length*$
@@
@@svspace.html length :
@@
]#
@@source_file filename : @eval :cwd *K_input_dir* code_block.Source(K) @ @@

361
sks/code/code_block.py Normal file
View File

@@ -0,0 +1,361 @@
if __name__ == "__main__":
import sys
sys.path.append("../kutil")
sys.path.append("../target")
import sys
import re
import klammer_base
import kutil
import html_util
from html_util import E
import latex_util as L
import pprint
import phases
def escape_newlines(s):
# A newline becomes the marker the html paragraph pass turns back into a
# line break, so a verbatim source file keeps its lines. Used by Source.
return re.sub("\n", " ___NL___ ", s)
def undash(s):
# Verbatim text must show the hyphens the writer typed: the target's
# "--"/"---" transforms have already run, so put them back. Used by
# Code_fragment.
result = re.sub("__MDASH__", "---", s)
return re.sub("__NDASH__", "--", result)
def is_comment(s):
return s.strip().startswith("//")
def split_blocks(s):
kutil.msg()
blocks = []
in_code = True
block = ""
for line in s.rstrip().split("\n"):
if is_comment(line):
if in_code:
blocks.append(block)
block = line + "\n"
in_code = False
else:
block += line + "\n"
else:
if not in_code:
blocks.append(block)
block = line + "\n"
in_code = True
else:
block += line + "\n"
if block:
blocks.append(block)
return blocks
def get_lines(comment, code, n):
kutil.msg()
lines = code.split("\n")
if len(lines) < n:
raise Exception(
f"Error in @code block comment:\n{comment}\n{code}\nNeed {n} lines, but there are only {len(lines)}")
if len(lines) == n:
return (code, "")
else:
return ("\n".join(lines[:n]), "\n".join(lines[n:]))
def parse_blocks(blocks):
kutil.msg()
box_comment_rgx = re.compile("\\s*//(\\d+)\\s+.*", re.S)
i = 0
block_pairs = []
while i < len(blocks):
match = box_comment_rgx.match(blocks[i])
if match:
boxed_code, rest_code = get_lines(blocks[i], blocks[i+1], int(match.group(1)))
block_pairs.append([boxed_code, blocks[i]])
block_pairs.append([rest_code, ""])
i += 2
else:
block_pairs.append([blocks[i], ""])
i += 1
return block_pairs
# Characters that are special to LATEX but not to Klammertext. # and ^ are
# deliberately ABSENT: they are Klammertext specials and are handled by
# quote_specials() below, not here -- see tex_line().
latex_escapes = {
"\\": "\\textbackslash{}", "{": "\\{", "}": "\\}", "%": "\\%",
"$": "\\$", "&": "\\&", "_": "\\_", "~": "\\textasciitilde{}",
"<": "\\textless{}", ">": "\\textgreater{}",
}
latex_escape_rgx = re.compile("[" + re.escape("".join(latex_escapes)) + "]")
def escape_latex(s):
r"""Escape LaTeX special characters in code text.
ONE pass, not a sequence of str.replace() calls: a sequence corrupts
the replacements it has already made. Replacing \ first yields
\textbackslash{}, whose braces the later { and } replacements then
escape in turn -- so a C++ "\n" typesets as \{}n. A single regex pass
over the original string cannot revisit its own output.
"""
return latex_escape_rgx.sub(lambda m: latex_escapes[m.group()], s)
def quote_specials(s):
r"""Quote the Klammertext special characters in code text.
An @eval result is RE-READ as Klammertext, so a special character that
reaches the result raw is interpreted again: a # in "#include" starts a
text removal and silently eats the rest of the line. Quoting is the
fix, and it must be quoting rather than LaTeX escaping -- \# would put
the # back and be eaten in turn.
The quoted forms resolve at final processing: the tex target declares
^# -> \# and ^^ -> \textasciicircum{} in its :escape list, and any
other quoted special decodes back to its own character. So the target,
not this code, decides what a # becomes in LaTeX.
^ is quoted FIRST, since the other quotings introduce ^ characters.
"""
for ch in "^@#|":
s = s.replace(ch, "^" + ch)
return s
def tex_line(line):
r"""One code line, ready to be a \klline argument.
Three steps in this order: LaTeX-escape the characters only LaTeX cares
about; quote the ones Klammertext would re-interpret (the escapes above
introduce none of them, so the two passes cannot interfere); then make
every space a ~ -- a non-breaking space, exactly one character wide in
a monospace font, which LaTeX will not collapse -- so the code's
indentation and internal alignment survive.
"""
return quote_specials(escape_latex(line)).replace(" ", "~")
def widest_line(lines):
r"""The line to measure the block's width with.
Measured on the RAW lines: in a monospace font character count is
exact, whereas the escaped text is longer than it typesets
(\textbackslash{} is eleven characters and one glyph). The chosen
line is escaped afterwards, for \settowidth.
"""
return max(lines, key=len) if lines else ""
def tex_block(code, comment):
r"""One block: its lines, shaded if the block is annotated, beside its
comment. See the "Annotated code listings" section of sty/code.sty,
which owns the layout; this only supplies the three arguments."""
lines = code.strip("\n").split("\n")
if not any(line.strip() for line in lines):
return ""
# Every line is a plain \klline; whether the BLOCK is shaded is decided
# by \klcodebox from the comment, because the shading is one box around
# the block (that is what gives it vertical padding).
body = "\\\\\n".join(f"\\klline{{{tex_line(line)}}}" for line in lines)
return ("\\klblock{" + tex_line(widest_line(lines)) + "}{%\n"
+ body + "}{" + comment + "}\n")
def comment_text(comment):
"""The prose of a block comment: the // and the line count removed."""
return re.sub(r"^\s*//\d*\s*", "", comment.strip(), flags=re.S).strip()
html_escapes = {"&": "&amp;", "<": "&lt;", ">": "&gt;"}
html_escape_rgx = re.compile("[" + re.escape("".join(html_escapes)) + "]")
def html_line(text):
"""Code text as html: the markup characters escaped, then the
Klammertext specials quoted for the @eval read-back (same reason as
tex_line -- a raw # in "#include" would start a text removal). The
html entities introduce no Klammertext special, so the two passes
cannot interfere. No target :escape entries apply here, so each
quoted special decodes back to its own character."""
return quote_specials(html_escape_rgx.sub(
lambda m: html_escapes[m.group()], text))
def html_block(code, comment):
r"""One block: its lines beside its comment.
The layout is CSS (sks/code/css/code.css): .code_block is a flex row
with align-items center -- the same model as the LaTeX \parbox[c]
pair -- and .code_text is an inline-block with white-space: pre, so
its shrink-to-fit width IS the block's longest line and the shading
is one solid rectangle with no per-line work. Nothing is measured
here: unlike LaTeX, the browser does the layout.
"""
lines = code.strip("\n").split("\n")
if not any(line.strip() for line in lines):
return ""
shade = "code_border" if comment else "code_no_border"
result = f'<div class="code_text {shade}">{html_line(code.strip(chr(10)))}</div>'
if comment:
result += f'<div class="code_comment">{comment}</div>'
return f'<div class="code_block">{result}</div>\n'
class Code(klammer_base.Klammer_base):
id = 0
def __init__(self, K):
super().__init__(K)
self.text = phases.expand_whitespace_markers(self.text)
def annotated(self, pairs):
"""Does any block of this listing carry a comment?"""
return any(comment.strip() for _, comment in pairs)
def tex(self):
# No table: comments sit a fixed distance from their own block and
# deliberately do not align with each other, and each block's box is
# as wide as that block's longest line — so there is no column to
# align and nothing for a table to do. The layout is in
# sty/code.sty; see its "Annotated code listings" section.
#
# This is a RENDERER (final LaTeX, no klammers in the result), so
# the Klammermachine leaves it alone. It must never emit @code:
# a klammer that generates itself re-enters its own body with no
# base case, which the depth guard catches at 200 levels.
pairs = parse_blocks(split_blocks(self.text))
blocks = "".join(tex_block(code, comment_text(comment))
for code, comment in pairs)
listing = "\\begin{klcode}\n" + blocks + "\\end{klcode}\n"
captioned = bool(self.number or self.caption)
annotated = self.annotated(pairs)
inset = L.offset_length(self.offset)
# A caption does NOT by itself require a box, and boxing a listing
# costs page breaking -- a minipage cannot break, and listings are
# often long. Only two things genuinely need the shared box that
# add_caption builds:
#
# * a caption BESIDE the listing (:caption_side left or right),
# which has to know the listing's width;
# * an unannotated listing that must be centered or right-aligned,
# which has to be measured before it can be moved.
#
# Everything else is placed unboxed, and keeps breaking: the offset
# becomes \klshift/\klindent inside the environment, and the caption
# becomes a paragraph above or below, indented and width-matched to
# the listing. An annotated listing never needs measuring -- it
# spans the text column by construction.
beside = captioned and self.caption_side in ("left", "right")
must_measure = not annotated and (self.hpos != "left" or inset != "0pt")
if beside or must_measure:
return L.block(self.tex_boxed(listing, pairs, annotated,
captioned, inset))
return L.block(self.tex_unboxed(listing, annotated, captioned, inset))
def tex_boxed(self, listing, pairs, annotated, captioned, inset):
r"""The listing as a box: placed and captioned like a table or an
image, at the cost of not breaking across pages."""
measure = ""
if annotated:
# It spans the text column, so an offset narrows it rather than
# moving it; a centered element has no margin to be inset from.
width = ("\\linewidth" if self.hpos == "center"
else f"\\dimexpr\\linewidth-{inset}\\relax")
else:
# The \settowidth must stay OUTSIDE the minipage it sizes.
lines = [line for code, _ in pairs
for line in code.strip("\n").split("\n")]
measure = ("\\settowidth{\\kllistingwidth}{\\ttfamily "
+ tex_line(widest_line(lines)) + "}\n")
width = "\\kllistingwidth"
boxed = L.minipage(listing, width, vertical="t", center=False)
if captioned:
# add_caption attaches the caption to the box and hands the pair
# to caption_wrapper, so :pos and :offset move both together.
return measure + L.add_caption(
boxed, "Listing", self.number, self.caption, width,
hpos=self.hpos, side=self.caption_side,
font_symbol=self.caption_font,
font_size=self.caption_font_size, offset=self.offset)
return measure + L.caption_wrapper(boxed, self.hpos,
offset=self.offset)
def tex_unboxed(self, listing, annotated, captioned, inset):
r"""The listing in the running vertical list, so it can break across
pages. The offset is \leftskip plus a matching reduction of the
width \klblock computes its comment column from; the caption is a
paragraph on the same indent and width."""
shift = inset if self.hpos == "left" else "0pt"
# \hpos center cannot move a full-width listing, so it takes no
# offset -- the same rule the boxed path and caption_wrapper use.
indent = "0pt" if (annotated and self.hpos == "center") else inset
setup = (f"\\setlength{{\\klshift}}{{{shift}}}"
f"\\setlength{{\\klindent}}{{{indent}}}\n")
result = setup + listing
if captioned:
caption = L.make_caption_text(
self.number, "Listing", self.caption,
self.caption_font, self.caption_font_size)
# \nobreak: a caption must not be separated from its listing by
# a page break, even though the listing itself may break.
block = (f"\\noindent\\hspace*{{{shift}}}"
f"\\parbox[t]{{\\dimexpr\\linewidth-{indent}\\relax}}"
f"{{{caption}}}\\par")
if self.caption_side == "top":
result = block + "\\nobreak\n" + result
else:
result = result + "\\nobreak\n" + block + "\n"
return result
def html(self):
pairs = parse_blocks(split_blocks(self.text))
result = "".join(html_block(code, comment_text(comment))
for code, comment in pairs)
# The same two cases as tex_place, with the browser doing the work.
# An annotated listing's rows must stay full width so the comment
# column's flex: 1 has a remainder to take; an unannotated one gets
# width: fit-content, which is what lets the position container
# center or right-align it.
listing = E("div").cls("code_listing").body(result)
if not self.annotated(pairs):
listing.cls("code_listing_box")
# As in tex: the caption goes through the shared helper, so it sits
# in the same position container as the listing and moves with it.
if self.number or self.caption:
return html_util.add_caption(
listing, "Listing", self.number, self.caption,
self.caption_font, self.hpos, self.caption_side, True,
self.caption_font_size, offset=self.offset)
return html_util.hpos_container(listing, self.hpos, self.offset).str()
class Code_fragment(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
def html(self):
#print(f"code: |{self.code_text}|")
result = self.code_text.strip()
result = undash(result)
#result = re.escape(result)
result = re.sub("<", "&lt;", result)
result = re.sub(" ", "&nbsp;", result)
#print(f"code: |{self.code_text}| -> |{result}|")
return f'<span class="code">{result}</span>'
def tex(self):
return f"{{\\tt {self.code_text.strip()}}}"
class Source(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
with open(self.filename) as fp:
self.src = fp.read()
def tex(self):
result = self.src
# result = re.sub("#", "^#", result)
# result = re.sub("\\^", "\\^", result)
result = f"\\begin{{verbatim}}\n{result}\n\\end{{verbatim}}\n"
return result
def html(self):
result = escape_newlines(self.src.strip()) + "\n"
result = re.sub("@", "^@", result)
result = E("div").body(result).cls("code_text").str()
return result

View File

@@ -1,3 +1,5 @@
print("DEPRECATED")
if __name__ == "__main__":
import sys
sys.path.append("../kutil")
@@ -22,6 +24,49 @@ def escape_newlines(s):
# backslash_pat = re.compile(r'(".*?)\n(.*?")', re.S)
# return backslash_pat.sub(replace, s)
def is_comment(s):
return s.strip().startswith("//")
def split_blocks(s):
print('-'*40)
print(s)
print('-'*40)
blocks = []
in_code = True
block = ""
for line in s.rstrip().split("\n"):
if is_comment(line):
if in_code:
blocks.append(block)
block = line + "\n"
in_code = False
else:
block += line + "\n"
else:
if not in_code:
blocks.append(block)
block = line + "\n"
in_code = True
else:
block += line + "\n"
if block:
blocks.append(block)
for block in blocks:
print("B:")
print(block)
return blocks
def parse_blocks(blocks):
box_comment_rgx = re.compile("\s*//(\d+)\s+.*", re.S)
i = 0
while i < len(blocks):
match = box_comment_rgx.match(blocks[i])
if match:
print(match.group(1))
i += 1
def get_blocks(s):
comment_pat = re.compile(r"(\s*)//(\d+)\s+(.*)", re.S)
blocks = []
@@ -134,7 +179,27 @@ class Code(klammer_base.Klammer_base):
s = s.replace(">", "\\textgreater{}")
return s
def code_box(self, text):
kutil.msg(text)
result = ""
for line in text.rstrip().split("\n"):
indent = len(line) - len(line.lstrip())
eline = ("~" * indent) + line[indent:]
print(indent, line)
print(eline)
result += eline + "\\\\\n"
result = result[:-3]
print("RESULT:")
print(result)
return result
def tex(self):
parse_blocks(split_blocks(self.text))
return ""
# Escape Klammertext special characters
self.text = self.text.replace("^", "^^")
self.text = self.text.replace("#", "^#")
@@ -153,6 +218,14 @@ class Code(klammer_base.Klammer_base):
result = ""
count = len(self.blocks)
for text, comment in self.blocks:
print("TEXT:")
print(text)
print("COMMENT:")
print(comment)
text = " " + re.sub("\n", " \n ", text) + " "
longest = longest_line(text)
text = latex_spaces(text)
@@ -160,11 +233,23 @@ class Code(klammer_base.Klammer_base):
text = f"{start_strut}\\ttfamily {text}{end_strut}"
width = f"\\widthof{{\\ttfamily {longest}}}"
code = L.environment("minipage", text, width) + "\\\\\n"
print(code)
#code = "\\asymbox{" + self.code_box(text) + "}"
#print(code)
code = text
if comment:
width = f"\\linewidth - {width} - {indent} - {comment_sep}"
#width = f"\\linewidth - {width} - {indent} - {comment_sep}"
"""
width = f"\\linewidth - \widestline - {indent} - {comment_sep}"
code = f"\\fcolorbox{{Gray}}{{LightGray}}{{{code}}}"
code += f"\\rule{{{comment_sep}}}{{{strutvis}}}" \
+ L.environment("minipage", "\\sffamily\\small\\raggedright " + comment, width)
"""
code = "\\asymbox{" + self.code_box(text) + "}" + comment
result += f"\\rule{{{indent}}}{{{strutvis}}}{code}"
if comment:
if i < count - 1 and self.blocks[i+1][1]:

View File

@@ -2,12 +2,36 @@
font-family: var(--monospace);
}
/* A whole listing, inside its :hpos position container. Full width by
default so an annotated listing's comment column has a remainder to take
(.code_comment is flex: 1); an UNANNOTATED listing is shrink-to-fit
instead, which is what lets :hpos center or right-align it -- a
full-width child cannot be moved within its container. The LaTeX
counterpart is Code.tex_place. */
.code_listing {
}
.code_listing_box {
width: fit-content;
}
/* An annotated code listing (sks/code/code_block.py). The same model as
the LaTeX side in sty/code.sty: each BLOCK is a run of code lines with
an optional comment beside it, the shaded box is as wide as that block's
longest line, and comments sit a fixed distance from their own block
without aligning with each other.
align-items: center is the counterpart of LaTeX's \parbox[c] pair. No
vertical margin or padding anywhere in the stack: consecutive blocks
must abut exactly, so that adjacent shaded blocks read as one region and
the line rhythm is the same whether a line is shaded or not. That is
what \strut and \offinterlineskip buy on the LaTeX side; here it is just
the absence of vertical space. */
.code_block {
display: flex;
align-items: center;
margin: .125rem 0 0 1rem;
margin: 0 0 0 1rem;
padding: 0;
/* flex-direction: column-reverse; */
}
.code_caption {
@@ -15,20 +39,28 @@
font-style: italic;
}
/* white-space: pre keeps the code's own spacing, and the shrink-to-fit
width of an inline-block IS the block's longest line — so the shading is
one solid rectangle with no per-line work and nothing measured. Padding
is horizontal only; see the note on .code_block. */
.code_text {
display: inline-block;
/* vertical-align: top; */
white-space: pre;
font-family: var(--monospace);
line-height: 1.2;
line-height: 1.4;
/* No horizontal padding: the box's left edge sits ON the code column,
where the line's own indentation begins, rather than out to the left
of it. LaTeX counterpart: \klcodepad 0pt. */
padding: 0;
}
/* The comment takes what the block leaves of the containing width — the
browser's counterpart of \dimexpr\linewidth-\klcodewidth-\klcodegap.
min-width is the counterpart of \klcodemin: below it the comment would
be too narrow to set prose in. */
.code_comment {
display: inline-block;
/* vertical-align: top; */
padding: .25rem;
border: solid white 1px;
padding: .25rem .25rem .25rem .5rem;
flex: 1;
padding-left: .75rem;
font-family: var(--sans-serif);
font-style: italic;
font-size: .8rem;
@@ -36,22 +68,23 @@
line-height: 1.25;
}
/* An annotated block: background only, no border. A border would need a
matching one on the unshaded blocks to keep the code aligned, and the
design settled on shading alone. */
.code_border {
border: solid gray 1px;
/* margin: .125rem; */
margin: .125rem .125rem .125rem .25rem;
padding: .125rem .5rem .25rem .5rem;
background-color: rgb(95%,95%,95%);
background-color: rgb(100%,100%,60%);
/* Clear space above and below a boxed block, so two boxed blocks with
no unboxed lines between them read as two boxes rather than one.
On the block, never on the line: the lines within a block must still
abut, or the shading stops being a solid rectangle. The LaTeX
counterpart is \klboxgap in sty/code.sty. */
margin: 2px 0;
/* The box's own vertical margin: space inside the shading, above the
first line and below the last. LaTeX counterpart: \klboxpad. */
padding-top: 2px;
padding-bottom: 2px;
}
.code_no_border {
padding: .125rem 0 .125rem .5rem;
margin: 0 0 0 .125rem;
border: solid white 1px;
/* Debugging:
border: solid lightgray 1px;
background-color: rgb(250,250,127);
*/
}

View File

@@ -7,3 +7,168 @@
\usepackage[strings,nohyphen]{underscore}
\usepackage{mdframed}
\newsavebox{\measurebox}
\newlength{\widestline}
% \measurewidest{line one\\line two\\...} -> \widestline = width of widest
\newcommand{\measurewidest}[1]{%
\begin{lrbox}{\measurebox}%
\begin{tabular}{@{}l@{}}#1\end{tabular}%
\end{lrbox}%
\setlength{\widestline}{\wd\measurebox}%
}
% \asymbox[<left pad>]{line one\\line two\\...} other three sides: 8pt
\newcommand{\asymbox}[2][0pt]{%
\begin{lrbox}{\measurebox}%
\begin{tabular}{@{}l@{}}\tt #2\end{tabular}%
\end{lrbox}%
{\setlength{\fboxsep}{0pt}%
\fcolorbox{black}{yellow!20}{%
\kern#1%
\vbox{\kern0pt\hbox{\usebox{\measurebox}}\kern0pt}%
\kern0pt}}%
}
% ===========================================================================
% Annotated code listings. Emitted by sks/code/code_block.py; the layout
% policy lives here so it can be tuned without touching the Python.
%
% The model: a listing is a vertical stack of BLOCKS, each block a run of
% code lines with an optional comment beside it. There is deliberately no
% table -- comments are a fixed distance from their own block and do not
% align with each other, and each block's shaded box is as wide as that
% block's longest line.
%
% Two mechanisms carry the whole appearance:
%
% 1. Every line is a \strut'ed \makebox of the block's width. A LaTeX
% \strut is exactly 0.7\baselineskip high and 0.3 deep -- one full
% \baselineskip -- so consecutive lines inside a block abut exactly,
% with no gap and no overlap, at any point size. Without the strut a
% line with no descender would sit closer to its neighbour and the
% rhythm would follow the text rather than the grid.
%
% 2. \offinterlineskip in the listing removes the interline glue BETWEEN
% BLOCKS, so consecutive blocks abut too and a boxed block continues
% the rhythm of the plain lines around it. (It does not reach inside
% a \parbox, which restores normal interline spacing -- that is why
% the struts in point 1 are doing the work there, and why the box's
% vertical padding comes from \fboxsep below rather than from a
% padding line, which would be spaced off the grid.)
% ===========================================================================
\definecolor{klcodeshade}{rgb}{1,1,.6}
\newlength{\kllistingwidth} % width of a whole unannotated listing
% How the listing is placed WITHOUT being boxed, so that it can still break
% across pages: \klshift moves it right (an :offset with :hpos left) and
% \klindent is the width taken out of \linewidth (either side's offset).
% A minipage would do both at once but cannot break -- see Code.tex in
% sks/code/code_block.py, which sets these before \begin{klcode}.
\newlength{\klshift} \setlength{\klshift}{0pt}
\newlength{\klindent} \setlength{\klindent}{0pt}
\newlength{\klcodewidth} % width of the current block's box
\newlength{\klcommentwidth} % what is left for its comment
% Horizontal inset of the code from the box's left edge. 0pt puts the
% box's left edge ON the code column, where the line's own indentation
% begins, rather than out to the left of it; a positive value moves the
% CODE right, never the box left.
\newlength{\klcodepad} \setlength{\klcodepad}{0pt}
\newlength{\klcodegap} \setlength{\klcodegap}{14pt} % box to comment
\newlength{\klcodemin} \setlength{\klcodemin}{6em} % narrower: see below
% Clear space above and below a block that HAS a box, so that two boxed
% blocks with no unboxed lines between them read as two boxes rather than
% one. Applied per block, never per line: the lines within a block must
% still abut, or the shading stops being a solid rectangle.
% 1.5pt is 2px at the CSS reference 96dpi, matching the html margin in
% css/code.css -- these two are meant to look the same, so change them
% together.
\newlength{\klboxgap} \setlength{\klboxgap}{1.5pt}
% The box's own vertical margin: space INSIDE the shading, above the first
% line and below the last. Same 2px equivalent, matching the html padding.
\newlength{\klboxpad} \setlength{\klboxpad}{1.5pt}
% \klline{<line>} — one code line, padded and strut'ed to the block width.
\newcommand{\klline}[1]{%
\makebox[\klcodewidth][l]{\hspace{\klcodepad}\strut\ttfamily #1}}
% \klcodebox{<lines>} — the block's lines, shaded when the block has a
% comment (\klcomment is set by \klblock).
%
% The shading is ONE \colorbox around the whole block, not one per line,
% because only a box around the whole block can have a vertical margin.
%
% That margin is \vspace* INSIDE the \parbox rather than \fboxsep: \fboxsep
% pads all four sides, and any horizontal padding would put the box's left
% edge out to the LEFT of the code column. With \fboxsep 0 the box spans
% exactly the code's own extent, so it begins where the line's indentation
% begins and the code stays aligned with the unshaded lines around it.
\newcommand{\klcodebox}[1]{%
\ifx\klcomment\empty
\parbox[c]{\klcodewidth}{#1}%
\else
\setlength{\fboxsep}{0pt}%
\colorbox{klcodeshade}{%
\parbox[c]{\klcodewidth}{\vspace*{\klboxpad}#1\vspace*{\klboxpad}}}%
\fi}
% \klblock{<widest line>}{<lines>}{<comment>}
% <lines> is \klline/\klshaded calls separated by \\; <comment> may be
% empty. The comment gets what the box leaves of \linewidth (NOT
% \textwidth: inside a list or minipage they differ, and \textwidth would
% push the comment into the margin). If that remainder is too narrow to
% set prose in, the comment goes BELOW the block rather than being
% squeezed into an overfull box.
\newcommand{\klblock}[3]{%
\begingroup
% A block is boxed exactly when it has a comment, so this one test also
% decides whether the block gets the \klboxgap separation.
\def\klcomment{#3}%
\ifx\klcomment\empty\else\vskip\klboxgap\fi
\settowidth{\klcodewidth}{\ttfamily #1}%
\addtolength{\klcodewidth}{2\klcodepad}%
% The box advances the line by exactly \klcodewidth (\fboxsep is 0 in
% \klcodebox). If a horizontal padding is ever reintroduced there, its
% width must be subtracted here too: leaving it out overfills the line,
% and \raggedright then breaks it -- dropping the comment onto the next
% line at the margin, which looks like the narrow-comment fallback.
\setlength{\klcommentwidth}%
{\dimexpr\linewidth-\klindent-\klcodewidth-\klcodegap\relax}%
\noindent
\ifdim\klcommentwidth<\klcodemin
\klcodebox{#2}%
\ifx\klcomment\empty\else
\\\parbox[t]{\linewidth}{\klcommentfont #3}%
\fi
\else
\klcodebox{#2}%
\ifx\klcomment\empty\else
\hspace{\klcodegap}%
\parbox[c]{\klcommentwidth}{\klcommentfont #3}%
\fi
\fi
% End the block's line. Without this every block joins ONE horizontal
% list and the blocks are broken into lines and justified like words.
\par
\ifx\klcomment\empty\else\vskip\klboxgap\fi
\endgroup}
% The annotation font. Ragged right: a justified annotation beside a
% narrow box hyphenates badly.
\newcommand{\klcommentfont}{\sffamily\itshape\small\raggedright}
% \begin{klcode} ... \end{klcode} — the listing itself. \offinterlineskip
% is what makes the struts the only thing setting vertical rhythm.
\newenvironment{klcode}
{\par\addvspace{0.5\baselineskip}%
\begingroup
\setlength{\parindent}{0pt}%
\setlength{\parskip}{0pt}%
\raggedright
% An unboxed listing is shifted with \leftskip rather than wrapped in a
% minipage, so that it can still break across pages.
\leftskip=\klshift
\offinterlineskip}
{\endgroup\par\addvspace{0.5\baselineskip}}

View File

@@ -17,51 +17,45 @@
# @@image.k basename : Image read from a file @@
# @@image.html basename | width | height : @eval import image ; result = image.image(K) eval@ @@
@@@argtype image_hpos |
horizontal position of an image: the element positions (center, left,
right, or a length used as the left margin), or none for an inline image
with no positioning container
:pattern 'element_hpos'^|none
:default center
@@@
# An image once had its own position type, for the sake of the "none" value
# (no positioning container). That value is meaningful for any block
# element, so the shared hpos type carries it and an image is placed by the
# hpos_args option set like every other block element.
@@image
@@image.k
basename
:id
:width.length .5w
@caption_arguments@
@caption_args@
:vmargin.bool true
:hpos.image_hpos
@hpos_args@
:rel
:abswidth.number 0.0
:border.bool false
:
@eval image.Image(K) eval@
An image read from a file. The basename names the file, which is looked
for in the image search path and converted to a format the target can use.
^:width is the image's width in the text column, ^:hpos and ^:offset place
it there, and the caption arguments caption it.
@@
@@image_grid
@@image :: @eval image.Image(K) eval@ @@
@@image_grid.k
image_specs.rest(2)
:caption
:number.bool true
:cell_number.bool false
:landscape.bool false
:scale.number 0.98
:caption_side.caption_side
:caption_side_center.bool true
#
@caption_args@
@hpos_args@
:thumbnail.bool false
:allow_break.bool true
# :indent.length
# :xmargin.length
:id
:captionfont
:caption_width.float .9
:rel
:hsep.number 0.02
:
@eval image_grid.Image_grid(K) eval@
: A grid of images
@@
@@fig spec.figure_id :
@reference *spec* | Figure @
@@
@@image_grid :: @eval image_grid.Image_grid(K) eval@ @@
@@fig spec.figure_id : @reference *spec* | Figure @ @@

View File

@@ -103,7 +103,8 @@ class Image(klammer_base.Klammer_base):
result = html_util.add_caption(
result, "Figure", self.number, self.caption, self.caption_font,
self.hpos, self.caption_side, False, self.caption_font_size)
self.hpos, self.caption_side, False, self.caption_font_size,
offset=self.offset)
result = E("div").body(result).cls("image_margin")
@@ -131,16 +132,19 @@ class Image(klammer_base.Klammer_base):
self.caption += self.file_error_message
result = latex_util.add_caption(
result, "Figure", self.number, self.caption, width,
self.hpos, self.caption_side)
self.hpos, self.caption_side, offset=self.offset)
#result, "Figure", self.number, self.caption, self.hpos, self.caption_side,
#self.width, self.caption_side_center, self.vmargin, self.caption_margin)
else:
result = f'\\includegraphics[width={width}]{{{source}}}'
result = latex_util.caption_wrapper(result, self.hpos)
result = latex_util.caption_wrapper(result, self.hpos, offset=self.offset)
name = f"Reference-Figure-{Image.id}"
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
Image.id += 1
return result
# \includegraphics (bare or inside a caption wrapper's minipage) is
# box material: it must sit in vertical mode or it is typeset beside
# any text it follows.
return latex_util.block(result)
def txt(self):

View File

@@ -20,7 +20,13 @@ class Kargs:
self.caption_side_center = False
self.Image_search_path = K.Image_search_path
self.number = K.cell_number
# A cell is placed by the grid, not by itself: no positioning
# container, and nothing to inset it from. Both members of the
# hpos_args option set have to be set here -- image.py reads them
# unconditionally, so a missing one is an AttributeError at render
# time rather than a message.
self.hpos = "none"
self.offset = "0pt"
self.K_target = target
self.K_input_dir = K.K_input_dir
self.K_output_dir = K.K_output_dir

View File

@@ -1,22 +1,6 @@
@@show s : @eval :cpp show show @ @@
# General definitions for the Standard Klammer Set
@@@argtype caption_side |
the side of its element on which a caption is placed
:pattern top^|right^|bottom^|left
:default bottom
@@@
@@caption_arguments :
:caption
:number.bool true
:caption_side.caption_side
:caption_font.font i
:caption_font_size.float .9
@@
@@reference spec | name :
__REF__*spec*__*name*__
@@
# Argtypes
@@@argtype number | a number
:pattern 'float'^|'int'
@@ -35,16 +19,49 @@
#:python_cast (lambda s : [__import__("kutil").parse_length("tex", e) for e in s.split()])
@@@
@@@argtype element_hpos |
the horizontal position of a block element (a table or an image) within
the text column: center, left, right, or a length, which places the
element's left edge that far from the left margin (e.g. ^:hpos 4em, ^:hpos
.25w). When the element is as wide as the text column, the positions are
indistinguishable.
:pattern center^|left^|right^|'length'
@@@argtype side |
the side of its element on which a caption is placed: top, right, bottom, left
:pattern top^|right^|bottom^|left
:default bottom
@@@
@@@argtype hpos |
a horizontal position: left, center, right, none
:pattern left^|center^|right^|none
:default center
@@@
@@@argtype offset_length |
how far a block element is inset from the margin that ^:hpos names: from
the left margin for ^:hpos left, from the right margin for ^:hpos right.
An offset has no meaning for a centered element and is ignored there.
Written alone, ^:offset is the standard indentation (e.g. ^:hpos left
^:offset), and a value overrides it (^:hpos right ^:offset 4em).
# A single-purpose type, not a use of "length", because the two values
# below are what the writer relies on and only a type can carry them: an
# element is flush unless an offset is asked for (:default), and asking
# without saying how much is the standard indentation (:alone). A general
# length type must not declare :alone -- a bare option name has to read the
# same way wherever it appears, and "2em" is meaningless for :width.
:pattern 'length'
:default 0pt
:alone 2em
@@@
#[
@@@argtype element_hpos |
the horizontal position of a block element (a table, an image, a code
listing) within the text column: center, left, right, or a length, which
places the element's left edge that far from the left margin (e.g. ^:hpos
4em, ^:hpos .25w). When the element is as wide as the text column, the
positions are indistinguishable. The value none puts the element in no
positioning container at all, so that it flows with the text around it.
:pattern center^|left^|right^|none^|'length'
:default center
@@@
]#
@@@argtype figure_id |
an identifier for a figure.
@@ -89,8 +106,75 @@ An <id> is the value of the ^:id argument for an image.
:pattern [a-z][a-z]
@@@
# The document-wide language for generated text. A klammer's own :lang
# argument overrides it; see the language argtype above. Consumers today:
# @date and @datetime (month names and date form). Set it for a whole
# document with @@@state Language :value de @@@
@@@state Language :desc Language (ISO 639-1) for generated text :value en @@@
# State variables
@@@state Language :desc Language (ISO 639-1) for generated text.
The document-wide language for generated text. A klammer's own ^:lang
argument overrides it; see the language argtype above. Currently used by
^@date and ^@datetime (month names and date form). Set it for a whole
document with ^@^@^@state Language ^:value de ^@^@^@
:value en
@@@
# Option sets
#[
The horizontal placement of a block element is two parameters: where it sits
(hpos_arg) and how far it is inset from that side (offset_arg). They are two
sets rather than one because a klammer may need the second without the first
-- and because a set is a vocabulary a reader learns whole, so a small one is
easier to learn than a large one.
An option set is a claim about behavior, not just a saving of keystrokes: a
klammer whose declaration uses these must also POSITION itself with them,
through latex_util.caption_wrapper (tex) and html_util.hpos_container
(html), or the parameter is accepted and silently ignored. Asserted by
outcome in sks/tst/placement_test.sh, so a new block klammer that takes the
parameters and ignores them is caught.
A klammer that wants a different DEFAULT position says so where it uses the
set (@hpos_arg :hpos left @ in the code klammer): a set owns the names and
the types, and the klammer owns what silence means for it.
]#
#[
@@offset_arg.o
:offset.offset_length
: How far a block element is inset from the margin its position names @@
@@hpos_arg.o
:hpos.element_hpos
: Where a block element sits in the text column @@
]#
@@hpos_args.o
:hpos.hpos
:offset.offset_length
:
The horizontal position of a block element. If the ^:hpos value is "none", no
outer structure is added to the element so that it can be used inline or in
other structures. The ^:offset is used only if the ^:hpos value is "left" or
"right".
@@
@@caption_args.o
:caption
:number.bool true
:caption_side.side
:caption_font.font i
:caption_font_size.float .9
: Arguments that define a caption for a block element.
@@
# Klammers
@@reference.k spec | name : Reference marker for captioned elements @@
@@reference ::
__REF__*spec*__*name*__
@@
@@show.k s : Show the raw Klammertext and the result @@
@@show :: @eval :cpp show show @ @@

View File

@@ -193,10 +193,10 @@
@@rowcolor.tex s : \colorrow{*s*} @@
@@table rows.rest(2)
@@table.k rows.rest(2)
:id
@caption_arguments@
:hpos.element_hpos
@caption_args@
@hpos_args@
:header.bool true
:allow_break.bool false
:column_width.column_width
@@ -215,9 +215,20 @@
:leading.float 1.3
:colsep 4pt
:
@eval table.Table(K) eval@
A table of rows of cells. The cells of a row are separated by "|" and the
rows by "||", so the argument is two-dimensional; the first row is the
header unless ^:header is false.
Everything else is optional, and a table written with no options is a plain
grid of its cells. ^:column_width lays the columns out, ^:hline and ^:vline
draw lines, ^:colspan and ^:rowspan merge cells, ^:justify and ^:cell_hpos
place text within them, ^:calc computes cells from other cells and ^:format
formats them, and ^:hpos and ^:offset place the whole table in the text
column.
@@
@@table :: @eval table.Table(K) eval@ @@
@@tbl spec.figure_id :
@reference *spec* | Table @
@@

View File

@@ -591,10 +591,11 @@ class Table(klammer_base.Klammer_base):
result, "Table", self.number, self.caption,
self.caption_font, hpos=self.hpos,
side=self.caption_side,
font_size=self.caption_font_size, max_width="100%")
font_size=self.caption_font_size, max_width="100%",
offset=self.offset)
else:
result.attr("style", "max-width: 100%")
result = html_util.hpos_container(result, self.hpos).str()
result = html_util.hpos_container(result, self.hpos, self.offset).str()
return result
colgroup, layout, table_width = self.html_colgroup()
result = E("table").body(colgroup + result)
@@ -607,13 +608,14 @@ class Table(klammer_base.Klammer_base):
result = html_util.add_caption(
result, "Table", self.number, self.caption, self.caption_font,
hpos=self.hpos, side=self.caption_side,
font_size=self.caption_font_size, width=table_width)
font_size=self.caption_font_size, width=table_width,
offset=self.offset)
else:
# An uncaptioned table still gets the position container, so
# html and tex agree on where the table sits.
if table_width:
result.attr("style", f"{layout}width: {table_width}")
result = html_util.hpos_container(result, self.hpos).str()
result = html_util.hpos_container(result, self.hpos, self.offset).str()
return result
# LaTeX
@@ -768,14 +770,25 @@ class Table(klammer_base.Klammer_base):
# by its :hpos wrapper instead; its glue is left neutral (\fill on
# both sides collapses in the exactly-fitting box), because a fixed
# length would overflow the box. A length is the left margin.
# :offset insets the table from the margin :hpos names -- the same
# rule the boxed path gets from latex_util.caption_wrapper, here
# expressed as the fixed side of the glue pair.
inset = latex_util.offset_length(self.offset)
if not self.allow_break:
left, right = "\\fill", "\\fill"
elif self.hpos == "center":
left, right = "\\fill", "\\fill"
elif self.hpos == "left":
left, right = "0pt", "\\fill"
left, right = inset, "\\fill"
elif self.hpos == "right":
left, right = "\\fill", "0pt"
left, right = "\\fill", inset
elif self.hpos == "none":
# No positioning container: the table starts at the text margin
# like ordinary text, which is what "none" does in html (the
# hpos_none class is inline-flex). A longtable cannot flow
# inline, so flush left is as close as the target gets. The
# offset names no margin here and is ignored, as when centered.
left, right = "0pt", "\\fill"
else:
length, _, _ = kutil.parse_length("tex", self.hpos, 1)
left, right = length, "\\fill"
@@ -824,9 +837,9 @@ class Table(klammer_base.Klammer_base):
result, "Table", self.number, self.caption, "\\tableboxwidth",
hpos=self.hpos, side=self.caption_side,
font_symbol=self.caption_font,
font_size=self.caption_font_size)
font_size=self.caption_font_size, offset=self.offset)
else:
result = latex_util.caption_wrapper(result, self.hpos)
result = latex_util.caption_wrapper(result, self.hpos, offset=self.offset)
result = measure + result
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
@@ -845,7 +858,11 @@ class Table(klammer_base.Klammer_base):
+ self.tex_fill_widths()
+ result)
result = re.sub(r"\newline", r"\\\\", result)
return result
# A boxed table is box material: it must sit in vertical mode or it
# is typeset beside any text it follows. (A page-breaking longtable
# breaks the paragraph itself, but \par on both sides is a no-op
# there, so the rule stays unconditional.)
return latex_util.block(result)
def txt(self):
return "Table in .txt format not implemented"

View File

@@ -168,6 +168,14 @@
justify-content: left;
}
/* Retired 2026-08-03: this fixed 1em on every left- and right-placed
element was the :offset argument, hard coded and only in html -- the tex
side had no counterpart, so the two targets disagreed. It is now the
offset_length argtype (sks/kutil/kutil.k), applied by
html_util.hpos_container and latex_util.caption_wrapper. A document
that wants the old inset writes ":offset", whose alone value is the
standard indentation.
.hpos_margin {
margin: 0 1em 0 1em;
}
*/

View File

@@ -193,27 +193,38 @@ def element_tag(element):
def font_class(font):
return {"r" : "", "i" : "ritalic", "t" : "monospace", "s" : "sanserif"}[font]
def hpos_container(element, hpos):
def hpos_container(element, hpos, offset=None):
# Wrap element in its horizontal-position container. hpos is left,
# center, right, none, or a length, which becomes the left margin
# (the tex counterparts are the \LTleft glue for tables and the
# \hspace* in latex_util.caption_wrapper).
style = None
#
# offset insets the element from the margin hpos names, as padding on
# that side of the flex container: the left for "left", the right for
# "right". A centered element has no such margin, so the offset is
# ignored there rather than being an error -- same rule as
# latex_util.caption_wrapper, which is the tex counterpart. (It
# replaces the fixed 1em that the hpos_margin class used to add to
# every left- and right-placed element: that was this offset, hard
# coded, and only in html.)
styles = []
if hpos not in ("left", "center", "right", "none"):
length, _, _ = kutil.parse_length("html", hpos, 1)
style = f"margin-left: {length}"
styles.append(f"margin-left: {length}")
hpos = "indent"
if offset and offset not in ("0", "0pt") and hpos in ("left", "right", "indent"):
length, _, _ = kutil.parse_length("html", offset, 1)
side = "right" if hpos == "right" else "left"
styles.append(f"padding-{side}: {length}")
result = E("div").cls("hpos_" + hpos).body(element)
if style:
result.attr("style", style)
if hpos not in ("center", "indent"):
result.cls("hpos_margin")
if styles:
result.attr("style", "; ".join(styles))
return result
def add_caption(element, caption_label, number, caption_text,
font_symbol="i", hpos="center", side="bottom", as_string=True,
font_size=.9, width=None, max_width=None):
font_size=.9, width=None, max_width=None, offset=None):
tag = element_tag(element)
# Caption
caption = ""
@@ -263,7 +274,7 @@ def add_caption(element, caption_label, number, caption_text,
element.attr("style", f"max-width: {max_width}")
result = hpos_container(element, hpos)
result = hpos_container(element, hpos, offset)
if number:
result.cls("element_container")

View File

@@ -12,6 +12,31 @@ def tex_style():
# \usepackage{quoting}
def block(material):
r"""Return tex material as a block: vertical mode on both sides.
A block element -- a table, an image, a code listing -- is box
material. Appended to a non-empty horizontal list (an @image written
after text on the same line, with no blank line between) a box is
typeset BESIDE the text rather than below it, and text following it
flows to its right. A \par on each side ends the paragraph in
progress and starts a new one after. \par in vertical mode is a
no-op, so this is safe wherever it is applied: a block klammer needs
no knowledge of what preceded it.
Every SKS klammer whose tex output is box material returns it through
this function. LaTeX ENVIRONMENTS -- center, flushright, quote,
itemize, verbatim -- already break the paragraph themselves and do
not need it.
The paragraph policy this serves: in an @document, paragraphs are
made from blank-line-separated text (@par is the explicit form for a
fragment rendered without @document), and the author is never
required to know that a target distinguishes horizontal from
vertical mode. Asserted by sks/tst/paragraph_test.sh.
"""
return "\\par\n" + material.strip("\n") + "\n\\par\n"
def environment(name, body, required=None, optional=None):
req = f"{{{required}}}" if required else ""
opt = f"[{optional}]" if optional else ""
@@ -54,17 +79,36 @@ def minipage(content, width="\\textwidth", vertical="c", center=True, vmargin=""
result = f"\\fbox{{{result}}}"
return result
def caption_wrapper(element, hpos, bottom_margin=.67):
def offset_length(offset):
r"""An :offset as a LaTeX length, or "0pt" when there is none."""
if not offset or offset in ("0", "0pt"):
return "0pt"
return kutil.parse_length("tex", offset, 1)[0]
def offset_space(offset):
r"""An :offset as an \hspace*, or "" when there is none."""
length = offset_length(offset)
return "" if length == "0pt" else f"\\hspace*{{{length}}}"
def caption_wrapper(element, hpos, bottom_margin=.67, offset=None):
# hpos is left, center, right, none (no wrapper), or a length, which
# becomes the left margin. The element is a box on a line inside a
# full-width minipage; \hfill on the empty side pushes it into place.
#
# offset insets the element from the margin hpos names -- from the left
# for "left", from the right for "right". A centered element has no
# such margin, so the offset is ignored there rather than being an
# error: the two arguments are independent, and :hpos is what decides
# whether the offset has anything to measure from. (The html
# counterpart is html_util.hpos_container.)
vmargin = f"{bottom_margin}\\baselineskip"
inset = offset_space(offset)
if hpos == "center":
return minipage(element, vmargin=vmargin)
if hpos == "left":
return minipage(element + "\\hfill", vmargin=vmargin, center=False)
return minipage(inset + element + "\\hfill", vmargin=vmargin, center=False)
if hpos == "right":
return minipage("\\hfill" + element, vmargin=vmargin, center=False)
return minipage("\\hfill" + element + inset, vmargin=vmargin, center=False)
if hpos == "none":
return element
length, _, _ = kutil.parse_length("tex", hpos, 1)
@@ -85,7 +129,8 @@ def make_caption_text(number, label, text, font_symbol, font_size):
return caption
def add_caption(element, caption_label, number, caption_text, latex_width,
hpos="center", side="bottom", font_symbol="i", font_size=.9):
hpos="center", side="bottom", font_symbol="i", font_size=.9,
offset=None):
top_margin = .75 if "includegraphics" in element else .5
caption = make_caption_text(number, caption_label, caption_text, font_symbol, font_size)
if caption:
@@ -118,4 +163,4 @@ def add_caption(element, caption_label, number, caption_text, latex_width,
# space below its line.
element = minipage(caption + "\\rule[-0.75\\baselineskip]{0pt}{0pt}\n" + element,
latex_width, vertical="t")
return caption_wrapper(element, hpos)
return caption_wrapper(element, hpos, offset=offset)

View File

@@ -17,4 +17,6 @@ test:
./alone_test.sh
./modulepath_test.sh
./klammerset_test.sh
./option_set_test.sh
./signature_test.sh
./editor_test.sh

307
tst/option_set_test.sh Executable file
View File

@@ -0,0 +1,307 @@
#!/bin/bash
#
# option_set_test.sh — Regression tests for option sets (the ".o" target).
#
# An option set is a named group of OPTIONAL parameters, declared once and
# used by several klammers, so that a writer learns one vocabulary instead of
# a spelling per klammer:
#
# @@caption_args.o :caption :number.bool true : A caption @@
# @@table.k rows.rest(2) @caption_args@ : A table of rows of cells @@
#
# The rules this suite holds to:
#
# * "o" is a pseudo-target beside "k". A ".o" declaration defines no
# klammer and produces no output for any target.
# * A set declares optional parameters only. A positional is not
# writer-facing, so there is nothing for a set to standardize.
# * Names and types come from the set; a DEFAULT may be overridden where
# the set is used, because what varies between klammers is only what
# silence means for that one klammer.
# * A set may be used only in the parameter list of a ".k" declaration --
# the one place a klammer's interface is declared once for all of its
# targets. Not in a per-target definition, and not in another set.
# * A klammer application in a parameter list is an error. Splicing a
# constant klammer there used to be the way to share parameters, and in
# a ".k" declaration it silently destroyed the whole parameter list.
#
# Engine tier: no SKS. Every fixture defines its own target inline.
#
# Usage: ./option_set_test.sh
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
KTEXT=ktext
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
# The fixture prelude: a target, and two sets to use in declarations.
PRELUDE='@@@target fix | a fixture target @@@
@@cap.o :caption :number.bool true :side bottom : A caption for an element @@
@@pos.o :hpos left : Where an element sits @@'
# check_eq NAME EXPECTED SOURCE [TARGET] — render SOURCE (for the fix target
# unless TARGET says otherwise) and compare the trimmed output with EXPECTED.
check_eq() {
local name="$1" expected="$2" source="$3" target="${4:-fix}"
local output status
output=$("$KTEXT" -k none -s "$PRELUDE
$source" -t "$target" -d 2>&1)
status=$?
output=$(printf '%s' "$output" | tr -d '\n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if [ $status -ne 0 ]; then
echo "${red}FAIL${reset} $name — ktext exited $status"
echo " output: $(printf '%s' "$output" | head -3)"
FAIL=$((FAIL + 1))
return
fi
if [ "$output" = "$expected" ]; then
echo "${green}PASS${reset} $name"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} $name"
echo " expected: [$expected]"
echo " got: [$output]"
FAIL=$((FAIL + 1))
fi
}
# check_fails NAME SUBSTRING SOURCE — SOURCE must be rejected, with a
# message containing SUBSTRING.
check_fails() {
local name="$1" needle="$2" source="$3"
local output status
output=$("$KTEXT" -k none -s "$PRELUDE
$source" -t fix -d 2>&1)
status=$?
if [ $status -eq 0 ]; then
echo "${red}FAIL${reset} $name — expected an error, ktext exited 0"
FAIL=$((FAIL + 1))
return
fi
if printf '%s' "$output" | tr '\n' ' ' | grep -qF "$needle"; then
echo "${green}PASS${reset} $name"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} $name"
echo " expected error containing: [$needle]"
echo " got: $(printf '%s' "$output" | tr '\n' ' ' | head -c 300)"
FAIL=$((FAIL + 1))
fi
}
# check_warns NAME SUBSTRING SOURCE — SOURCE must be accepted, and its
# combined output must contain SUBSTRING (used where a warning is expected
# beside the result, so an exact comparison would test the warning's wording).
check_warns() {
local name="$1" needle="$2" source="$3"
local output status
output=$("$KTEXT" -k none -s "$PRELUDE
$source" -t fix -d 2>&1)
status=$?
if [ $status -ne 0 ]; then
echo "${red}FAIL${reset} $name — ktext exited $status"
FAIL=$((FAIL + 1))
return
fi
if printf '%s' "$output" | tr '\n' ' ' | grep -qF "$needle"; then
echo "${green}PASS${reset} $name"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} $name"
echo " expected a warning containing: [$needle]"
echo " got: $(printf '%s' "$output" | tr '\n' ' ' | head -c 300)"
FAIL=$((FAIL + 1))
fi
}
echo "${bold}Option set (.o) tests${reset}"
echo "====================="
echo
# --- The members become the klammer's parameters --------------------------
# A declaration using a set accepts the set's options, and the writer's
# arguments reach the body under the names the set declares.
check_eq " 1. a set's parameters are the klammer's" \
"[A|top]" \
'@@e.k x @cap@ : an element @@
@@e.fix :: [*caption*|*side*] @@
@e X :caption A :side top @'
# Absent argument, declared default: the set supplies it.
check_eq " 2. a member's default applies when the argument is absent" \
"[|true|bottom]" \
'@@e.k x @cap@ : an element @@
@@e.fix :: [*caption*|*number*|*side*] @@
@e X @'
# The use site may override a default -- and only the default.
check_eq " 3. a use-site override changes the default" \
"[|true|top]" \
'@@e.k x @cap :side top @ : an element @@
@@e.fix :: [*caption*|*number*|*side*] @@
@e X @'
check_eq " 4. a boolean default can be overridden to false" \
"[false]" \
'@@e.k x @cap :number false @ : an element @@
@@e.fix :: [*number*] @@
@e X @'
# An option written alone at the use site takes the argument type's :alone
# value, exactly as it does where an argument is written.
check_eq " 5. an override written alone takes the argtype's alone value" \
"[true]" \
'@@n.o :number.bool false : A number @@
@@e.k x @n :number @ : an element @@
@@e.fix :: [*number*] @@
@e X @'
# Three levels: argument type, option set, use site -- and the writer's
# argument still wins over all of them.
check_eq " 6. a written argument wins over the overridden default" \
"[maybe]" \
'@@e.k x @cap :side top @ : an element @@
@@e.fix :: [*side*] @@
@e X :side maybe @'
check_eq " 7. two sets in one declaration" \
"[bottom|left]" \
'@@e.k x @cap@ @pos@ : an element @@
@@e.fix :: [*side*|*hpos*] @@
@e X @'
check_eq " 8. a set's parameters mix with the klammer's own" \
"[own|bottom]" \
'@@e.k x :mine own @cap@ : an element @@
@@e.fix :: [*mine*|*side*] @@
@e X @'
# Every target definition is an instance, so all of them get the expanded
# list: the set is resolved once, in the declaration.
check_eq " 9. a second target's instance inherits the same parameters" \
"<bottom>" \
'@@@target fix2 | another fixture target @@@
@@e.k x @cap@ : an element @@
@@e.fix :: [*side*] @@
@@e.fix2 :: <*side*> @@
@e X @' \
fix2
# --- The declaration is not a klammer -------------------------------------
check_fails "10. a set defines no klammer" \
'The klammer "cap" is not defined' \
'@cap@'
# --- Where a set may be used ----------------------------------------------
check_fails "11. not in a per-target definition" \
'may be used only in the parameter list of a ".k" declaration' \
'@@e.fix x @cap@ : [*caption*] @@'
check_fails "12. not in a general definition" \
'may be used only in the parameter list of a ".k" declaration' \
'@@e x @cap@ : [*caption*] @@'
check_fails "13. not in another set" \
'a set does not include another set' \
'@@both.o :extra @cap@ : two vocabularies @@'
# --- A klammer application in a parameter list ----------------------------
check_fails "14. a klammer application in a parameter list is rejected" \
'A klammer application in a parameter list is not allowed' \
'@@c : :spliced @@
@@e.k x @c@ : an element @@'
check_fails "15. ... and the message names the declared sets" \
'Declared option sets: cap, pos' \
'@@e.k x @nosuch@ : an element @@'
# --- Use-site overrides ---------------------------------------------------
check_fails "16. an override must name a member of the set" \
'The option set "cap" has no parameter ":nope"' \
'@@e.k x @cap :nope 1 @ : an element @@'
check_fails "17. an override value is validated at definition time" \
'does not match the "bool" argument type' \
'@@e.k x @cap :number perhaps @ : an element @@'
check_fails "18. an override may not restate the type" \
'only a default may be given where it is used' \
'@@e.k x @cap :number.bool false @ : an element @@'
check_fails "19. an override may not give a positional value" \
'gives a value that is not an option' \
'@@e.k x @cap here @ : an element @@'
# --- What a set may declare -----------------------------------------------
check_fails "20. a positional parameter is rejected" \
'An option set declares only optional parameters' \
'@@bad.o p :q : oops @@'
check_fails "21. a rest parameter is rejected" \
'An option set declares only optional parameters' \
'@@bad.o r.rest :q : oops @@'
check_fails "22. a set with no parameters is rejected" \
'declares no parameters' \
'@@bad.o : nothing at all @@'
check_fails '23. "::" has no meaning for a set' \
'An option set IS a declaration' \
'@@cap.o :: nope @@'
# --- Collisions -----------------------------------------------------------
check_fails "24. two sets declaring the same name name both sets" \
'from the option set "cap"' \
'@@other.o :side right : another side @@
@@e.k x @cap@ @other@ : an element @@'
check_fails "25. a set colliding with a declared parameter" \
'declared in the parameter list' \
'@@e.k x :side own @cap@ : an element @@'
# --- Redefinition ---------------------------------------------------------
check_fails "26. declaring a set twice is an error" \
'Option set "cap.o" already defined' \
'@@cap.o :caption : a second caption @@'
# A set is its own declaration, so an override restates what it declares --
# there is no ".k" for it to inherit a parameter list from.
check_warns '27. ":::" overrides a set, with a warning' \
'overridden' \
'@@cap.o :caption :number.bool true :side top ::: a replaced caption @@'
check_warns "28. ... and the overriding declaration is what a klammer gets" \
"[top]" \
'@@cap.o :caption :number.bool true :side top ::: a replaced caption @@
@@e.k x @cap@ : an element @@
@@e.fix :: [*side*] @@
@e X @'
# A "::::" default is silently superseded by a later create, and the create
# is what the using declaration gets.
check_eq "29. a default set is superseded by a later declaration" \
"[right]" \
'@@d.o :where left :::: a default @@
@@d.o :where right : the real one @@
@@e.k x @d@ : an element @@
@@e.fix :: [*where*] @@
@e X @'
echo
echo "====================="
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
[ $FAIL -eq 0 ]

143
tst/signature_test.sh Executable file
View File

@@ -0,0 +1,143 @@
#!/bin/bash
#
# signature_test.sh — one klammer, one interface (engine suite, tst/).
#
# A klammer may be defined separately for each target. When it is, every
# target's definition carries its own parameter list, and those lists must
# agree: a klammer's interface is a property of the KLAMMER, not of the
# target it is being rendered to. If they disagree, the same document would
# bind arguments differently — or fail — depending only on the target, which
# is exactly the thing an author must be able to rely on not happening.
#
# The engine enforces this rather than warning about it, and the remedy it
# names is the .k declaration: declare the parameters once, and give each
# target a "::" instance with no parameter list of its own. That is the
# migration from repeating an argument list per target to declaring it once.
#
# These are engine tests, so they use -k none and define their own targets
# inline: a target is a Machine construct (@@@target), not owned by any
# klammer set.
#
# Usage: ./signature_test.sh (needs KLAMMERTEXT_HOME set; ktext on PATH)
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
KTEXT=ktext
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
pass() { echo "${green}PASS${reset} $1"; PASS=$((PASS+1)); }
fail() {
echo "${red}FAIL${reset} $1"
echo " expected: $2"
echo " got: $3"
FAIL=$((FAIL+1))
}
TARGETS='@@@target ta | Target A @@@
@@@target tb | Target B @@@
'
# accepted NAME SRC — the definitions are consistent and the klammer applies.
accepted() {
local name="$1" src="$2" out
out=$("$KTEXT" -k none -s "$TARGETS$src" -t ta -d 2>&1)
if echo "$out" | grep -qi "error"; then
fail "$name" "no error" "$(echo "$out" | grep -i -A1 error | tail -1)"
else
pass "$name"
fi
}
# rejected NAME SRC PATTERN — the drift is caught, and the message says how.
rejected() {
local name="$1" src="$2" want="$3" out
out=$("$KTEXT" -k none -s "$TARGETS$src" -t ta -d 2>&1)
if ! echo "$out" | grep -qi "error"; then
fail "$name" "a definition error" "accepted"
elif ! echo "$out" | grep -qF "$want"; then
fail "$name" "message containing: $want" "$(echo "$out" | head -6 | tail -3)"
else
pass "$name"
fi
}
echo "${bold}Klammer signature consistency${reset}"
echo "============================="
echo
accepted " 1. identical parameter lists" \
'@@k1.ta s :n : [*s*] @@
@@k1.tb s :n : [*s*] @@
@k1 x @'
rejected " 2. positional names differ" \
'@@k2.ta s : [*s*] @@
@@k2.tb t : [*t*] @@
@k2 x @' 'are not the same for every target'
rejected " 3. option names differ" \
'@@k3.ta s :one : [*s*] @@
@@k3.tb s :two : [*s*] @@
@k3 x @' 'are not the same for every target'
rejected " 4. defaults differ" \
'@@k4.ta s :n abc : [*s*] @@
@@k4.tb s :n xyz : [*s*] @@
@k4 x @' 'are not the same for every target'
rejected " 5. argument types differ" \
'@@k5.ta s :n.int : [*s*] @@
@@k5.tb s :n.word : [*s*] @@
@k5 x @' 'are not the same for every target'
echo
echo "-- the message must show HOW they differ, not just where"
# With more than two targets the designer would otherwise have to diff the
# definitions by hand; each target's own signature is listed beside its name.
name=" 6. each target's signature is shown"
out=$("$KTEXT" -k none -s "$TARGETS"'@@k6.ta s :one : [*s*] @@
@@k6.tb s :two : [*s*] @@
@k6 x @' -t ta -d 2>&1)
if echo "$out" | grep -q "ta .*s :one" && echo "$out" | grep -q "tb .*s :two"; then
pass "$name"
else
fail "$name" "both signatures listed by target" "$(echo "$out" | head -8 | tail -4)"
fi
# The error path once printed internal katom detail through msg(), which is
# debugging scaffolding and must never reach a user-facing message.
name=" 7. no internal debug output on the error path"
out=$("$KTEXT" -k none -s "$TARGETS"'@@k7.k s : a declaration @@
@@k7.ta s2 :other : [*s2*] @@
@k7 x @' -t ta -d 2>&1)
if echo "$out" | grep -q "klammer-definition"; then
fail "$name" "no internal dump" "$(echo "$out" | head -2)"
else
pass "$name"
fi
echo
echo "-- the remedy the message names"
accepted " 8. a .k declaration with :: instances" \
'@@k8.k s :n : a declaration @@
@@k8.ta :: [*s*] @@
@@k8.tb :: [*s*] @@
@k8 x @'
rejected " 9. a .k declaration plus a parameterized definition" \
'@@k9.k s : a declaration @@
@@k9.ta s2 :other : [*s2*] @@
@k9 x @' 'both a declaration'
echo
echo "============================="
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
[ $FAIL -eq 0 ]