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:
@@ -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
|
Report problems (or send patches) to the author; accepted changes are
|
||||||
applied to the development tree and appear in a following snapshot.
|
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
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ int main(int argc, char* argv[])
|
|||||||
args.opt("input", "Input filename", "filename", "", "'text'");
|
args.opt("input", "Input filename", "filename", "", "'text'");
|
||||||
args.flag("targets", "Show targets defined by the input file");
|
args.flag("targets", "Show targets defined by the input file");
|
||||||
args.flag("klammers", "Show klammers 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("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.var("klammerset", "List the klammersets on the search path. Enter \"--klammerset help\" for details.");
|
||||||
args.opt("v", "'verbosity'", "n", "0", "'verbosity'");
|
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);
|
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) {
|
catch (Error& e) {
|
||||||
e.print_message();
|
e.print_message();
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ include $(K)/env/makefile.env
|
|||||||
# Source files
|
# Source files
|
||||||
BASENAMES := util error locator file argv character ktype katom katom_list \
|
BASENAMES := util error locator file argv character ktype katom katom_list \
|
||||||
log show command argument argument_set argtype argtype_registry \
|
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
|
target target_registry machine font_store check
|
||||||
|
|
||||||
SOURCES := $(addsuffix .cpp,$(BASENAMES))
|
SOURCES := $(addsuffix .cpp,$(BASENAMES))
|
||||||
|
|||||||
@@ -29,10 +29,19 @@ public:
|
|||||||
Argtype m_argtype; // {};
|
Argtype m_argtype; // {};
|
||||||
bool m_optional {};
|
bool m_optional {};
|
||||||
std::string m_default {};
|
std::string m_default {};
|
||||||
// True when m_default was filled from the argument type's :default
|
// Where m_default came from, for kdesc provenance display. Default
|
||||||
// rather than declared in the klammer's parameter list (for kdesc
|
// resolution has three levels -- the argument type's :default, an option
|
||||||
// provenance display).
|
// 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 {};
|
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;
|
Locator m_loc;
|
||||||
std::string m_target {};
|
std::string m_target {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
argument_split(std::vector<Katom>::const_iterator kbegin, std::vector<Katom>::const_iterator kend,
|
||||||
long unsigned int positional_limit = std::numeric_limits<int>::max());
|
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
|
class Parameter_set
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
127
mac/klammer.cpp
127
mac/klammer.cpp
@@ -41,7 +41,10 @@ parse_name(const Target_registry& targets, const Katom& name_katom)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::tuple<Katom, Parameter_set, katom_list, Locator>
|
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));
|
(void)K::log(3, *begin, *(end - 1));
|
||||||
katom_iter deftype = std::find_if(
|
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,
|
std::erase_if(parameter_katoms,
|
||||||
[](const Katom& k) { return k.m_type == katom_t::ignored; });
|
[](const Katom& k) { return k.m_type == katom_t::ignored; });
|
||||||
parameter_katoms = trim_whitespace(parameter_katoms);
|
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) &&
|
deftype->m_type == katom_t::klammer_override) &&
|
||||||
!parameter_katoms.empty()) {
|
!parameter_katoms.empty()) {
|
||||||
std::string sym = deftype->m_type == katom_t::klammer_instance ? "::" : ":::";
|
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.",
|
"The \"" + klammer_name + "\" klammer uses the \"" + sym + "\" symbol but defines parameters.",
|
||||||
begin->m_loc);
|
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);
|
Parameter_set parameters(parameter_katoms, argtypes);
|
||||||
|
stamp_option_set_uses(parameters, option_set_uses);
|
||||||
|
|
||||||
katom_list body_katoms(deftype + 1, end - 1);
|
katom_list body_katoms(deftype + 1, end - 1);
|
||||||
body_katoms = trim_whitespace(body_katoms);
|
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(
|
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));
|
(void)K::log(3, *begin, *(end-1));
|
||||||
auto [deftype, parameters, body, loc] =
|
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";
|
// msg() << "Klammer " << m_name << " add: " << target_name << "\n";
|
||||||
// parameters.describe_parameters();
|
// parameters.describe_parameters();
|
||||||
|
|
||||||
@@ -208,7 +227,8 @@ void Klammer::copy_components(
|
|||||||
(void)K::log(4);
|
(void)K::log(4);
|
||||||
for (const auto& target_name : targets.m_names) {
|
for (const auto& target_name : targets.m_names) {
|
||||||
if (target_name == Target_registry::declare_name ||
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
m_parameters = parameters;
|
m_parameters = parameters;
|
||||||
@@ -243,7 +263,6 @@ void Klammer::check_for_declaration_and_definitions()
|
|||||||
if (def.target != Target_registry::declare_name) {
|
if (def.target != Target_registry::declare_name) {
|
||||||
if (def.deftype == katom_t::klammer_definition ||
|
if (def.deftype == katom_t::klammer_definition ||
|
||||||
def.deftype == katom_t::klammer_default) {
|
def.deftype == katom_t::klammer_default) {
|
||||||
msg() << def << "\n";
|
|
||||||
definitions.push_back(def);
|
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) {
|
for (const auto& target_name : targets.m_names) {
|
||||||
// std::cout << "General copy, considering " << target_name << "\n";
|
// 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";
|
// std::cout << " Copying to " << target_name << "\n";
|
||||||
m_body[target_name] = body;
|
m_body[target_name] = body;
|
||||||
m_body_generic[target_name] = true; // general body -> writer content
|
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
|
// 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 Klammer::no_declarations(const Target_registry& targets)
|
||||||
{
|
{
|
||||||
(void)K::log(4);
|
(void)K::log(4);
|
||||||
@@ -318,14 +374,24 @@ void Klammer::no_declarations(const Target_registry& targets)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!all_equal<Parameter_set>(all_parameter_sets)) {
|
if (!all_equal<Parameter_set>(all_parameter_sets)) {
|
||||||
// std::cout << " Not all equal\n";
|
// Show each target's own parameter list, not just its location: with
|
||||||
throw Definition_error(
|
// more than two targets the designer would otherwise have to diff the
|
||||||
error_list("There is no declaration (.k) target for klammer \"" + m_name + "\"\n"
|
// definitions by hand to find which one drifted.
|
||||||
"but the parameters of all targets are not the same",
|
std::stringstream ss {};
|
||||||
m_defs,
|
ss << "The parameters of klammer \"" << m_name
|
||||||
"Use a .k klammer to define the parameters and describe the klammer,\n"
|
<< "\" are not the same for every target,\n"
|
||||||
"with \"::\" and no parameters for all targets."),
|
"and there is no declaration (.k) target to define them once:\n";
|
||||||
m_defs[0].loc, false);
|
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 {
|
} else {
|
||||||
// std::cout << " All equal\n";
|
// std::cout << " All equal\n";
|
||||||
copy_components(m_defs[0].parameters, m_defs, targets);
|
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 Klammer::describe(int margin) const
|
||||||
{
|
{
|
||||||
std::string result {};
|
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";
|
result = add_margin(result, margin) + "\n";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "deftype.h"
|
#include "deftype.h"
|
||||||
#include "argument_set.h"
|
#include "argument_set.h"
|
||||||
|
#include "option_set_registry.h"
|
||||||
#include "target_registry.h"
|
#include "target_registry.h"
|
||||||
#include "locator.h"
|
#include "locator.h"
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ public:
|
|||||||
|
|
||||||
void add_target_definition(
|
void add_target_definition(
|
||||||
const std::string& target_name, const Argtype_registry& argtypes,
|
const std::string& target_name, const Argtype_registry& argtypes,
|
||||||
|
Option_set_registry& option_sets,
|
||||||
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
|
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
|
||||||
void remove_target_definition(const std::string& target_name);
|
void remove_target_definition(const std::string& target_name);
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ public:
|
|||||||
|
|
||||||
std::string signature_text() const;
|
std::string signature_text() const;
|
||||||
std::string description_text() const;
|
std::string description_text() const;
|
||||||
|
std::string option_set_text() const;
|
||||||
std::string describe(int margin=0) const;
|
std::string describe(int margin=0) const;
|
||||||
|
|
||||||
bool has_literal_param() 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>
|
std::tuple<std::string,std::string>
|
||||||
parse_name(const Target_registry& targets, const Katom& name_katom);
|
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>
|
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);
|
std::vector<Katom>::iterator& begin, std::vector<Katom>::iterator& end);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -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));
|
(void)K::log(3, *begin, *(end - 1));
|
||||||
restore_initial_type(begin, end);
|
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)) {
|
if (!targets.has(target_name)) {
|
||||||
throw Argument_error("The target \"" + target_name + "\" is not defined", begin->m_loc);
|
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
|
// Find the incoming definition mode
|
||||||
katom_t incoming_deftype = katom_t::klammer_definition;
|
katom_t incoming_deftype = katom_t::klammer_definition;
|
||||||
for (auto it = begin + 1; it != end - 1; ++it) {
|
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].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:
|
// This add's target:
|
||||||
Target target = targets.get(target_name, begin->m_loc);
|
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) {
|
if (m_klammers[klammer_name].m_defloc.count(provide_name) > 0) {
|
||||||
m_klammers[klammer_name].remove_target_definition(provide_name);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class Klammer_registry
|
|||||||
public:
|
public:
|
||||||
Klammer_registry() = default;
|
Klammer_registry() = default;
|
||||||
void add(const Argtype_registry& argtypes, const Target_registry& targets,
|
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);
|
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
|
||||||
void rationalize(const Target_registry& targets);
|
void rationalize(const Target_registry& targets);
|
||||||
void check_klammer(const std::string& name, const std::string& target, const Locator& loc) const;
|
void check_klammer(const std::string& name, const std::string& target, const Locator& loc) const;
|
||||||
|
|||||||
@@ -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)
|
void Machine::expand_constant_klammers(katom_list& katoms, const Katom& op, const Katom& cl)
|
||||||
{
|
{
|
||||||
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
||||||
restore_initial_type(begin + 1, end - 1);
|
restore_initial_type(begin + 1, end - 1);
|
||||||
if (std::find_if(begin + 1, end - 1, begin_klammer_apply) == end - 1) return;
|
auto body_begin = std::find_if(
|
||||||
for (const auto& [app_op, app_cl] : find_spans(begin + 1, end - 1, begin_apply, end_apply, false, "def-time")) {
|
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);
|
auto [app_begin, app_end] = find_span_katoms(katoms, app_op, app_cl);
|
||||||
if (app_begin->m_type == katom_t::apply_begin) {
|
if (app_begin->m_type == katom_t::apply_begin) {
|
||||||
std::string name = trim_char(app_begin->m_text, '@');
|
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());
|
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)
|
void Machine::extract_klammer_definitions(katom_list katoms)
|
||||||
{
|
{
|
||||||
fmsg() << katoms << "\n";
|
fmsg() << katoms << "\n";
|
||||||
(void)K::log(3);
|
(void)K::log(3);
|
||||||
for (const auto& [op, cl] : find_spans(katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
|
for (const auto& [op, cl] : find_spans(katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
|
||||||
expand_constant_klammers(katoms, op, cl);
|
add_definition(katoms, op, cl);
|
||||||
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
|
||||||
m_klammers.add(m_argtypes, m_targets, begin, end, katoms);
|
|
||||||
}
|
}
|
||||||
m_klammers.rationalize(m_targets);
|
m_klammers.rationalize(m_targets);
|
||||||
}
|
}
|
||||||
@@ -522,9 +550,7 @@ void Machine::extract_klammer_definitions()
|
|||||||
{
|
{
|
||||||
(void)K::log(3);
|
(void)K::log(3);
|
||||||
for (const auto& [op, cl] : find_spans(m_katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
|
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);
|
add_definition(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);
|
|
||||||
}
|
}
|
||||||
m_klammers.rationalize(m_targets);
|
m_klammers.rationalize(m_targets);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "katom_list.h"
|
#include "katom_list.h"
|
||||||
#include "klammer_registry.h"
|
#include "klammer_registry.h"
|
||||||
#include "klammerset_registry.h"
|
#include "klammerset_registry.h"
|
||||||
|
#include "option_set_registry.h"
|
||||||
#include "target_registry.h"
|
#include "target_registry.h"
|
||||||
#include "argtype_registry.h"
|
#include "argtype_registry.h"
|
||||||
#include "state.h"
|
#include "state.h"
|
||||||
@@ -27,6 +28,7 @@ public:
|
|||||||
, m_targets(other.m_targets)
|
, m_targets(other.m_targets)
|
||||||
, m_klammers(other.m_klammers)
|
, m_klammers(other.m_klammers)
|
||||||
, m_klammersets(other.m_klammersets)
|
, m_klammersets(other.m_klammersets)
|
||||||
|
, m_option_sets(other.m_option_sets)
|
||||||
, m_result(other.m_result)
|
, m_result(other.m_result)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ public:
|
|||||||
m_targets = other.m_targets;
|
m_targets = other.m_targets;
|
||||||
m_klammers = other.m_klammers;
|
m_klammers = other.m_klammers;
|
||||||
m_klammersets = other.m_klammersets;
|
m_klammersets = other.m_klammersets;
|
||||||
|
m_option_sets = other.m_option_sets;
|
||||||
m_result = other.m_result;
|
m_result = other.m_result;
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
@@ -68,6 +71,7 @@ public:
|
|||||||
bool klammers=true, bool eval=true, bool cond=true, bool read=true);
|
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 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 load_klammerset_files(const Klammerset& klammerset, katom_iter insert_at);
|
||||||
void extract_machine_definitions();
|
void extract_machine_definitions();
|
||||||
void extract_klammer_definitions();
|
void extract_klammer_definitions();
|
||||||
@@ -92,6 +96,7 @@ public:
|
|||||||
Target_registry m_targets {};
|
Target_registry m_targets {};
|
||||||
Klammer_registry m_klammers {};
|
Klammer_registry m_klammers {};
|
||||||
Klammerset_registry m_klammersets {};
|
Klammerset_registry m_klammersets {};
|
||||||
|
Option_set_registry m_option_sets {};
|
||||||
input_sources_t m_sources {};
|
input_sources_t m_sources {};
|
||||||
std::string m_result {};
|
std::string m_result {};
|
||||||
std::vector<Katom> m_katoms {};
|
std::vector<Katom> m_katoms {};
|
||||||
|
|||||||
64
mac/option_set.cpp
Normal file
64
mac/option_set.cpp
Normal 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
70
mac/option_set.h
Normal 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
359
mac/option_set_registry.cpp
Normal 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
60
mac/option_set_registry.h
Normal 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);
|
||||||
@@ -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 state_desc = m.m_state.describe(false, 3);
|
||||||
std::string targets_desc = m.m_targets.describe(4);
|
std::string targets_desc = m.m_targets.describe(4);
|
||||||
std::string klammersets_desc = m.m_klammersets.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 klammer_desc = m.m_klammers.describe(2);
|
||||||
std::string source_desc = describe_sources(m);
|
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("Argtypes", m.m_argtypes.m_names.size()) << argtypes_desc << "\n"
|
||||||
<< label("Targets", m.m_targets.m_names.size()) << targets_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("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("Klammers", m.m_klammers.m_klammers.size()) << klammer_desc << "\n"
|
||||||
<< label("State", m.m_state.m_frames.size()) << state_desc;
|
<< label("State", m.m_state.m_frames.size()) << state_desc;
|
||||||
return os;
|
return os;
|
||||||
|
|||||||
@@ -11,14 +11,17 @@
|
|||||||
|
|
||||||
std::string Target_registry::declare_name = "k";
|
std::string Target_registry::declare_name = "k";
|
||||||
std::string Target_registry::general_name = "*";
|
std::string Target_registry::general_name = "*";
|
||||||
|
std::string Target_registry::optionset_name = "o";
|
||||||
|
|
||||||
Target_registry::Target_registry()
|
Target_registry::Target_registry()
|
||||||
: m_parameters(Parameter_set("name | desc :after_apply :after_write :includes :escape | transforms.rest"))
|
: 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 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 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(k);
|
||||||
add(general);
|
add(general);
|
||||||
|
add(option_set);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Target_registry::add(Target target)
|
void Target_registry::add(Target target)
|
||||||
@@ -104,14 +107,19 @@ std::vector<std::string> Target_registry::user_defined() const
|
|||||||
{
|
{
|
||||||
return collect_if(
|
return collect_if(
|
||||||
m_names, [](const auto& name) {
|
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
|
std::vector<std::string> Target_registry::applicable() const
|
||||||
{
|
{
|
||||||
return collect_if(
|
return collect_if(
|
||||||
m_names, [](const auto& name) {
|
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
|
std::string Target_registry::describe(int margin, bool long_format) const
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ class Target_registry
|
|||||||
public:
|
public:
|
||||||
static std::string declare_name;
|
static std::string declare_name;
|
||||||
static std::string general_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();
|
Target_registry();
|
||||||
|
|
||||||
|
|||||||
@@ -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.k : Non-breaking space character @@
|
||||||
@@sp.html :: &^#160; @@
|
@@sp.html :: &^#160; @@
|
||||||
@@sp.tex :: ~ @@
|
@@sp.tex :: ~ @@
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class Indent(klammer_base.Klammer_base):
|
|||||||
result = tab + latex_util.minipage(
|
result = tab + latex_util.minipage(
|
||||||
"\\raggedright " + self.s, f"\\textwidth - {self.w}ex", center=False, vmargin="4pt")
|
"\\raggedright " + self.s, f"\\textwidth - {self.w}ex", center=False, vmargin="4pt")
|
||||||
#print(result)
|
#print(result)
|
||||||
return result
|
return latex_util.block(result)
|
||||||
|
|
||||||
def txt(self):
|
def txt(self):
|
||||||
indent = " " * self.w
|
indent = " " * self.w
|
||||||
@@ -54,13 +54,15 @@ class Note(klammer_base.Klammer_base):
|
|||||||
#width = r'\\textwidth - 16pt - {}\\leftmargin'.format(self.level)
|
#width = r'\\textwidth - 16pt - {}\\leftmargin'.format(self.level)
|
||||||
width = r'\\linewidth - \\leftmargin + 2pt'
|
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 = '''
|
result = '''
|
||||||
\\par\\begingroup
|
\\begingroup
|
||||||
COLOR\\setlength{\\fboxsep}{8pt}
|
COLOR\\setlength{\\fboxsep}{8pt}
|
||||||
\\fcolorbox{bordercolor}{localcolor}{
|
\\fcolorbox{bordercolor}{localcolor}{
|
||||||
\\parbox{WIDTH}{\\raggedright\\setlength{\\parskip}{8pt}
|
\\parbox{WIDTH}{\\raggedright\\setlength{\\parskip}{8pt}
|
||||||
\\textbf{LABEL:} TEXT
|
\\textbf{LABEL:} TEXT
|
||||||
}}\\endgroup\\par
|
}}\\endgroup
|
||||||
'''
|
'''
|
||||||
result = re.sub('LABEL', self.label, result)
|
result = re.sub('LABEL', self.label, result)
|
||||||
result = re.sub('TEXT', re.sub(r'\\', r'\\\\', self.s), 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('COLOR', r'\\definecolor{{bordercolor}}{{rgb}}{{{}}}\n'.format(self.bordercolor), result)
|
||||||
result = re.sub('WIDTH', width, result)
|
result = re.sub('WIDTH', width, result)
|
||||||
print(result)
|
print(result)
|
||||||
return result
|
return latex_util.block(result)
|
||||||
|
|
||||||
|
|
||||||
class Block(klammer_base.Klammer_base):
|
class Block(klammer_base.Klammer_base):
|
||||||
@@ -80,6 +82,9 @@ class Block(klammer_base.Klammer_base):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
def tex(self):
|
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()]
|
to_x, to_y = [float(e) for e in self.to.split()]
|
||||||
pt_x, pt_y = [float(e) for e in self.point.split()]
|
pt_x, pt_y = [float(e) for e in self.point.split()]
|
||||||
result = f"""
|
result = f"""
|
||||||
|
|||||||
122
sks/code/code.k
122
sks/code/code.k
@@ -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
|
A source file displayed verbatim
|
||||||
@@
|
@@
|
||||||
|
|
||||||
@@code :: @eval code_format.Code(K) @ @@
|
@@code :: @eval code_block.Code(K) @ @@
|
||||||
|
|
||||||
@@c.k code_text :
|
@@c.k code_text :
|
||||||
A word or phrase displayed verbatim in a line
|
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
|
# :cwd makes the filename resolve against the DOCUMENT's directory, not
|
||||||
# the directory ktext happens to run in.
|
# the directory ktext happens to run in.
|
||||||
@@source_file filename : @eval :cwd *K_input_dir* code_format.Source(K) @ @@
|
@@source_file filename : @eval :cwd *K_input_dir* code_block.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 :
|
|
||||||
@@
|
|
||||||
]#
|
|
||||||
|
|||||||
361
sks/code/code_block.py
Normal file
361
sks/code/code_block.py
Normal 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 = {"&": "&", "<": "<", ">": ">"}
|
||||||
|
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("<", "<", result)
|
||||||
|
result = re.sub(" ", " ", 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
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
print("DEPRECATED")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
sys.path.append("../kutil")
|
sys.path.append("../kutil")
|
||||||
@@ -22,6 +24,49 @@ def escape_newlines(s):
|
|||||||
# backslash_pat = re.compile(r'(".*?)\n(.*?")', re.S)
|
# backslash_pat = re.compile(r'(".*?)\n(.*?")', re.S)
|
||||||
# return backslash_pat.sub(replace, 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):
|
def get_blocks(s):
|
||||||
comment_pat = re.compile(r"(\s*)//(\d+)\s+(.*)", re.S)
|
comment_pat = re.compile(r"(\s*)//(\d+)\s+(.*)", re.S)
|
||||||
blocks = []
|
blocks = []
|
||||||
@@ -134,7 +179,27 @@ class Code(klammer_base.Klammer_base):
|
|||||||
s = s.replace(">", "\\textgreater{}")
|
s = s.replace(">", "\\textgreater{}")
|
||||||
return s
|
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):
|
def tex(self):
|
||||||
|
parse_blocks(split_blocks(self.text))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Escape Klammertext special characters
|
# Escape Klammertext special characters
|
||||||
self.text = self.text.replace("^", "^^")
|
self.text = self.text.replace("^", "^^")
|
||||||
self.text = self.text.replace("#", "^#")
|
self.text = self.text.replace("#", "^#")
|
||||||
@@ -153,6 +218,14 @@ class Code(klammer_base.Klammer_base):
|
|||||||
result = ""
|
result = ""
|
||||||
count = len(self.blocks)
|
count = len(self.blocks)
|
||||||
for text, comment in self.blocks:
|
for text, comment in self.blocks:
|
||||||
|
print("TEXT:")
|
||||||
|
print(text)
|
||||||
|
print("COMMENT:")
|
||||||
|
print(comment)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
text = " " + re.sub("\n", " \n ", text) + " "
|
text = " " + re.sub("\n", " \n ", text) + " "
|
||||||
longest = longest_line(text)
|
longest = longest_line(text)
|
||||||
text = latex_spaces(text)
|
text = latex_spaces(text)
|
||||||
@@ -160,11 +233,23 @@ class Code(klammer_base.Klammer_base):
|
|||||||
text = f"{start_strut}\\ttfamily {text}{end_strut}"
|
text = f"{start_strut}\\ttfamily {text}{end_strut}"
|
||||||
width = f"\\widthof{{\\ttfamily {longest}}}"
|
width = f"\\widthof{{\\ttfamily {longest}}}"
|
||||||
code = L.environment("minipage", text, width) + "\\\\\n"
|
code = L.environment("minipage", text, width) + "\\\\\n"
|
||||||
|
print(code)
|
||||||
|
|
||||||
|
#code = "\\asymbox{" + self.code_box(text) + "}"
|
||||||
|
#print(code)
|
||||||
|
|
||||||
|
code = text
|
||||||
|
|
||||||
if comment:
|
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"\\fcolorbox{{Gray}}{{LightGray}}{{{code}}}"
|
||||||
code += f"\\rule{{{comment_sep}}}{{{strutvis}}}" \
|
code += f"\\rule{{{comment_sep}}}{{{strutvis}}}" \
|
||||||
+ L.environment("minipage", "\\sffamily\\small\\raggedright " + comment, width)
|
+ L.environment("minipage", "\\sffamily\\small\\raggedright " + comment, width)
|
||||||
|
"""
|
||||||
|
code = "\\asymbox{" + self.code_box(text) + "}" + comment
|
||||||
|
|
||||||
result += f"\\rule{{{indent}}}{{{strutvis}}}{code}"
|
result += f"\\rule{{{indent}}}{{{strutvis}}}{code}"
|
||||||
if comment:
|
if comment:
|
||||||
if i < count - 1 and self.blocks[i+1][1]:
|
if i < count - 1 and self.blocks[i+1][1]:
|
||||||
|
|||||||
@@ -2,12 +2,36 @@
|
|||||||
font-family: var(--monospace);
|
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 {
|
.code_block {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: .125rem 0 0 1rem;
|
margin: 0 0 0 1rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
/* flex-direction: column-reverse; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.code_caption {
|
.code_caption {
|
||||||
@@ -15,20 +39,28 @@
|
|||||||
font-style: italic;
|
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 {
|
.code_text {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
/* vertical-align: top; */
|
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
font-family: var(--monospace);
|
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 {
|
.code_comment {
|
||||||
display: inline-block;
|
flex: 1;
|
||||||
/* vertical-align: top; */
|
padding-left: .75rem;
|
||||||
padding: .25rem;
|
|
||||||
border: solid white 1px;
|
|
||||||
padding: .25rem .25rem .25rem .5rem;
|
|
||||||
font-family: var(--sans-serif);
|
font-family: var(--sans-serif);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
@@ -36,22 +68,23 @@
|
|||||||
line-height: 1.25;
|
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 {
|
.code_border {
|
||||||
border: solid gray 1px;
|
background-color: rgb(100%,100%,60%);
|
||||||
/* margin: .125rem; */
|
/* Clear space above and below a boxed block, so two boxed blocks with
|
||||||
margin: .125rem .125rem .125rem .25rem;
|
no unboxed lines between them read as two boxes rather than one.
|
||||||
padding: .125rem .5rem .25rem .5rem;
|
On the block, never on the line: the lines within a block must still
|
||||||
background-color: rgb(95%,95%,95%);
|
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 {
|
.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);
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,3 +7,168 @@
|
|||||||
\usepackage[strings,nohyphen]{underscore}
|
\usepackage[strings,nohyphen]{underscore}
|
||||||
|
|
||||||
\usepackage{mdframed}
|
\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}}
|
||||||
|
|||||||
@@ -17,51 +17,45 @@
|
|||||||
# @@image.k basename : Image read from a file @@
|
# @@image.k basename : Image read from a file @@
|
||||||
# @@image.html basename | width | height : @eval import image ; result = image.image(K) eval@ @@
|
# @@image.html basename | width | height : @eval import image ; result = image.image(K) eval@ @@
|
||||||
|
|
||||||
@@@argtype image_hpos |
|
# An image once had its own position type, for the sake of the "none" value
|
||||||
horizontal position of an image: the element positions (center, left,
|
# (no positioning container). That value is meaningful for any block
|
||||||
right, or a length used as the left margin), or none for an inline image
|
# element, so the shared hpos type carries it and an image is placed by the
|
||||||
with no positioning container
|
# hpos_args option set like every other block element.
|
||||||
:pattern 'element_hpos'^|none
|
|
||||||
:default center
|
|
||||||
@@@
|
|
||||||
|
|
||||||
@@image
|
@@image.k
|
||||||
basename
|
basename
|
||||||
:id
|
:id
|
||||||
:width.length .5w
|
:width.length .5w
|
||||||
@caption_arguments@
|
@caption_args@
|
||||||
:vmargin.bool true
|
:vmargin.bool true
|
||||||
:hpos.image_hpos
|
@hpos_args@
|
||||||
:rel
|
:rel
|
||||||
:abswidth.number 0.0
|
:abswidth.number 0.0
|
||||||
:border.bool false
|
: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)
|
image_specs.rest(2)
|
||||||
:caption
|
|
||||||
:number.bool true
|
|
||||||
:cell_number.bool false
|
:cell_number.bool false
|
||||||
:landscape.bool false
|
:landscape.bool false
|
||||||
:scale.number 0.98
|
:scale.number 0.98
|
||||||
:caption_side.caption_side
|
@caption_args@
|
||||||
:caption_side_center.bool true
|
@hpos_args@
|
||||||
#
|
|
||||||
:thumbnail.bool false
|
:thumbnail.bool false
|
||||||
:allow_break.bool true
|
:allow_break.bool true
|
||||||
# :indent.length
|
|
||||||
# :xmargin.length
|
|
||||||
:id
|
:id
|
||||||
:captionfont
|
|
||||||
:caption_width.float .9
|
|
||||||
:rel
|
:rel
|
||||||
:hsep.number 0.02
|
:hsep.number 0.02
|
||||||
:
|
: A grid of images
|
||||||
@eval image_grid.Image_grid(K) eval@
|
|
||||||
@@
|
@@
|
||||||
|
|
||||||
@@fig spec.figure_id :
|
@@image_grid :: @eval image_grid.Image_grid(K) eval@ @@
|
||||||
@reference *spec* | Figure @
|
|
||||||
@@
|
@@fig spec.figure_id : @reference *spec* | Figure @ @@
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ class Image(klammer_base.Klammer_base):
|
|||||||
|
|
||||||
result = html_util.add_caption(
|
result = html_util.add_caption(
|
||||||
result, "Figure", self.number, self.caption, self.caption_font,
|
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")
|
result = E("div").body(result).cls("image_margin")
|
||||||
|
|
||||||
@@ -131,16 +132,19 @@ class Image(klammer_base.Klammer_base):
|
|||||||
self.caption += self.file_error_message
|
self.caption += self.file_error_message
|
||||||
result = latex_util.add_caption(
|
result = latex_util.add_caption(
|
||||||
result, "Figure", self.number, self.caption, width,
|
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,
|
#result, "Figure", self.number, self.caption, self.hpos, self.caption_side,
|
||||||
#self.width, self.caption_side_center, self.vmargin, self.caption_margin)
|
#self.width, self.caption_side_center, self.vmargin, self.caption_margin)
|
||||||
else:
|
else:
|
||||||
result = f'\\includegraphics[width={width}]{{{source}}}'
|
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}"
|
name = f"Reference-Figure-{Image.id}"
|
||||||
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
|
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
|
||||||
Image.id += 1
|
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):
|
def txt(self):
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ class Kargs:
|
|||||||
self.caption_side_center = False
|
self.caption_side_center = False
|
||||||
self.Image_search_path = K.Image_search_path
|
self.Image_search_path = K.Image_search_path
|
||||||
self.number = K.cell_number
|
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.hpos = "none"
|
||||||
|
self.offset = "0pt"
|
||||||
self.K_target = target
|
self.K_target = target
|
||||||
self.K_input_dir = K.K_input_dir
|
self.K_input_dir = K.K_input_dir
|
||||||
self.K_output_dir = K.K_output_dir
|
self.K_output_dir = K.K_output_dir
|
||||||
|
|||||||
@@ -1,22 +1,6 @@
|
|||||||
@@show s : @eval :cpp show show @ @@
|
# General definitions for the Standard Klammer Set
|
||||||
|
|
||||||
@@@argtype caption_side |
|
# Argtypes
|
||||||
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*__
|
|
||||||
@@
|
|
||||||
|
|
||||||
@@@argtype number | a number
|
@@@argtype number | a number
|
||||||
:pattern 'float'^|'int'
|
:pattern 'float'^|'int'
|
||||||
@@ -35,16 +19,49 @@
|
|||||||
#:python_cast (lambda s : [__import__("kutil").parse_length("tex", e) for e in s.split()])
|
#:python_cast (lambda s : [__import__("kutil").parse_length("tex", e) for e in s.split()])
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
@@@argtype element_hpos |
|
@@@argtype side |
|
||||||
the horizontal position of a block element (a table or an image) within
|
the side of its element on which a caption is placed: top, right, bottom, left
|
||||||
the text column: center, left, right, or a length, which places the
|
:pattern top^|right^|bottom^|left
|
||||||
element's left edge that far from the left margin (e.g. ^:hpos 4em, ^:hpos
|
:default bottom
|
||||||
.25w). When the element is as wide as the text column, the positions are
|
@@@
|
||||||
indistinguishable.
|
|
||||||
:pattern center^|left^|right^|'length'
|
@@@argtype hpos |
|
||||||
|
a horizontal position: left, center, right, none
|
||||||
|
:pattern left^|center^|right^|none
|
||||||
:default center
|
: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 |
|
@@@argtype figure_id |
|
||||||
an identifier for a figure.
|
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]
|
: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:
|
# State variables
|
||||||
# @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.
|
||||||
@@@state Language :desc Language (ISO 639-1) for generated text :value en @@@
|
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 @ @@
|
||||||
|
|||||||
@@ -193,10 +193,10 @@
|
|||||||
|
|
||||||
@@rowcolor.tex s : \colorrow{*s*} @@
|
@@rowcolor.tex s : \colorrow{*s*} @@
|
||||||
|
|
||||||
@@table rows.rest(2)
|
@@table.k rows.rest(2)
|
||||||
:id
|
:id
|
||||||
@caption_arguments@
|
@caption_args@
|
||||||
:hpos.element_hpos
|
@hpos_args@
|
||||||
:header.bool true
|
:header.bool true
|
||||||
:allow_break.bool false
|
:allow_break.bool false
|
||||||
:column_width.column_width
|
:column_width.column_width
|
||||||
@@ -215,9 +215,20 @@
|
|||||||
:leading.float 1.3
|
:leading.float 1.3
|
||||||
:colsep 4pt
|
: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 :
|
@@tbl spec.figure_id :
|
||||||
@reference *spec* | Table @
|
@reference *spec* | Table @
|
||||||
@@
|
@@
|
||||||
|
|||||||
@@ -591,10 +591,11 @@ class Table(klammer_base.Klammer_base):
|
|||||||
result, "Table", self.number, self.caption,
|
result, "Table", self.number, self.caption,
|
||||||
self.caption_font, hpos=self.hpos,
|
self.caption_font, hpos=self.hpos,
|
||||||
side=self.caption_side,
|
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:
|
else:
|
||||||
result.attr("style", "max-width: 100%")
|
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
|
return result
|
||||||
colgroup, layout, table_width = self.html_colgroup()
|
colgroup, layout, table_width = self.html_colgroup()
|
||||||
result = E("table").body(colgroup + result)
|
result = E("table").body(colgroup + result)
|
||||||
@@ -607,13 +608,14 @@ class Table(klammer_base.Klammer_base):
|
|||||||
result = html_util.add_caption(
|
result = html_util.add_caption(
|
||||||
result, "Table", self.number, self.caption, self.caption_font,
|
result, "Table", self.number, self.caption, self.caption_font,
|
||||||
hpos=self.hpos, side=self.caption_side,
|
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:
|
else:
|
||||||
# An uncaptioned table still gets the position container, so
|
# An uncaptioned table still gets the position container, so
|
||||||
# html and tex agree on where the table sits.
|
# html and tex agree on where the table sits.
|
||||||
if table_width:
|
if table_width:
|
||||||
result.attr("style", f"{layout}width: {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
|
return result
|
||||||
|
|
||||||
# LaTeX
|
# LaTeX
|
||||||
@@ -768,14 +770,25 @@ class Table(klammer_base.Klammer_base):
|
|||||||
# by its :hpos wrapper instead; its glue is left neutral (\fill on
|
# by its :hpos wrapper instead; its glue is left neutral (\fill on
|
||||||
# both sides collapses in the exactly-fitting box), because a fixed
|
# both sides collapses in the exactly-fitting box), because a fixed
|
||||||
# length would overflow the box. A length is the left margin.
|
# 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:
|
if not self.allow_break:
|
||||||
left, right = "\\fill", "\\fill"
|
left, right = "\\fill", "\\fill"
|
||||||
elif self.hpos == "center":
|
elif self.hpos == "center":
|
||||||
left, right = "\\fill", "\\fill"
|
left, right = "\\fill", "\\fill"
|
||||||
elif self.hpos == "left":
|
elif self.hpos == "left":
|
||||||
left, right = "0pt", "\\fill"
|
left, right = inset, "\\fill"
|
||||||
elif self.hpos == "right":
|
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:
|
else:
|
||||||
length, _, _ = kutil.parse_length("tex", self.hpos, 1)
|
length, _, _ = kutil.parse_length("tex", self.hpos, 1)
|
||||||
left, right = length, "\\fill"
|
left, right = length, "\\fill"
|
||||||
@@ -824,9 +837,9 @@ class Table(klammer_base.Klammer_base):
|
|||||||
result, "Table", self.number, self.caption, "\\tableboxwidth",
|
result, "Table", self.number, self.caption, "\\tableboxwidth",
|
||||||
hpos=self.hpos, side=self.caption_side,
|
hpos=self.hpos, side=self.caption_side,
|
||||||
font_symbol=self.caption_font,
|
font_symbol=self.caption_font,
|
||||||
font_size=self.caption_font_size)
|
font_size=self.caption_font_size, offset=self.offset)
|
||||||
else:
|
else:
|
||||||
result = latex_util.caption_wrapper(result, self.hpos)
|
result = latex_util.caption_wrapper(result, self.hpos, offset=self.offset)
|
||||||
result = measure + result
|
result = measure + result
|
||||||
|
|
||||||
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
|
result = f"\\hypertarget{{{name}}}{{}}\\label{{Label-{name}}}\n{result}"
|
||||||
@@ -845,7 +858,11 @@ class Table(klammer_base.Klammer_base):
|
|||||||
+ self.tex_fill_widths()
|
+ self.tex_fill_widths()
|
||||||
+ result)
|
+ result)
|
||||||
result = re.sub(r"\newline", r"\\\\", 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):
|
def txt(self):
|
||||||
return "Table in .txt format not implemented"
|
return "Table in .txt format not implemented"
|
||||||
|
|||||||
@@ -168,6 +168,14 @@
|
|||||||
justify-content: left;
|
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 {
|
.hpos_margin {
|
||||||
margin: 0 1em 0 1em;
|
margin: 0 1em 0 1em;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -193,27 +193,38 @@ def element_tag(element):
|
|||||||
def font_class(font):
|
def font_class(font):
|
||||||
return {"r" : "", "i" : "ritalic", "t" : "monospace", "s" : "sanserif"}[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,
|
# Wrap element in its horizontal-position container. hpos is left,
|
||||||
# center, right, none, or a length, which becomes the left margin
|
# center, right, none, or a length, which becomes the left margin
|
||||||
# (the tex counterparts are the \LTleft glue for tables and the
|
# (the tex counterparts are the \LTleft glue for tables and the
|
||||||
# \hspace* in latex_util.caption_wrapper).
|
# \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"):
|
if hpos not in ("left", "center", "right", "none"):
|
||||||
length, _, _ = kutil.parse_length("html", hpos, 1)
|
length, _, _ = kutil.parse_length("html", hpos, 1)
|
||||||
style = f"margin-left: {length}"
|
styles.append(f"margin-left: {length}")
|
||||||
hpos = "indent"
|
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)
|
result = E("div").cls("hpos_" + hpos).body(element)
|
||||||
if style:
|
if styles:
|
||||||
result.attr("style", style)
|
result.attr("style", "; ".join(styles))
|
||||||
if hpos not in ("center", "indent"):
|
|
||||||
result.cls("hpos_margin")
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def add_caption(element, caption_label, number, caption_text,
|
def add_caption(element, caption_label, number, caption_text,
|
||||||
font_symbol="i", hpos="center", side="bottom", as_string=True,
|
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)
|
tag = element_tag(element)
|
||||||
# Caption
|
# Caption
|
||||||
caption = ""
|
caption = ""
|
||||||
@@ -263,7 +274,7 @@ def add_caption(element, caption_label, number, caption_text,
|
|||||||
element.attr("style", f"max-width: {max_width}")
|
element.attr("style", f"max-width: {max_width}")
|
||||||
|
|
||||||
|
|
||||||
result = hpos_container(element, hpos)
|
result = hpos_container(element, hpos, offset)
|
||||||
|
|
||||||
if number:
|
if number:
|
||||||
result.cls("element_container")
|
result.cls("element_container")
|
||||||
|
|||||||
@@ -12,6 +12,31 @@ def tex_style():
|
|||||||
|
|
||||||
# \usepackage{quoting}
|
# \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):
|
def environment(name, body, required=None, optional=None):
|
||||||
req = f"{{{required}}}" if required else ""
|
req = f"{{{required}}}" if required else ""
|
||||||
opt = f"[{optional}]" if optional 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}}}"
|
result = f"\\fbox{{{result}}}"
|
||||||
return 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
|
# 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
|
# 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.
|
# 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"
|
vmargin = f"{bottom_margin}\\baselineskip"
|
||||||
|
inset = offset_space(offset)
|
||||||
if hpos == "center":
|
if hpos == "center":
|
||||||
return minipage(element, vmargin=vmargin)
|
return minipage(element, vmargin=vmargin)
|
||||||
if hpos == "left":
|
if hpos == "left":
|
||||||
return minipage(element + "\\hfill", vmargin=vmargin, center=False)
|
return minipage(inset + element + "\\hfill", vmargin=vmargin, center=False)
|
||||||
if hpos == "right":
|
if hpos == "right":
|
||||||
return minipage("\\hfill" + element, vmargin=vmargin, center=False)
|
return minipage("\\hfill" + element + inset, vmargin=vmargin, center=False)
|
||||||
if hpos == "none":
|
if hpos == "none":
|
||||||
return element
|
return element
|
||||||
length, _, _ = kutil.parse_length("tex", hpos, 1)
|
length, _, _ = kutil.parse_length("tex", hpos, 1)
|
||||||
@@ -85,7 +129,8 @@ def make_caption_text(number, label, text, font_symbol, font_size):
|
|||||||
return caption
|
return caption
|
||||||
|
|
||||||
def add_caption(element, caption_label, number, caption_text, latex_width,
|
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
|
top_margin = .75 if "includegraphics" in element else .5
|
||||||
caption = make_caption_text(number, caption_label, caption_text, font_symbol, font_size)
|
caption = make_caption_text(number, caption_label, caption_text, font_symbol, font_size)
|
||||||
if caption:
|
if caption:
|
||||||
@@ -118,4 +163,4 @@ def add_caption(element, caption_label, number, caption_text, latex_width,
|
|||||||
# space below its line.
|
# space below its line.
|
||||||
element = minipage(caption + "\\rule[-0.75\\baselineskip]{0pt}{0pt}\n" + element,
|
element = minipage(caption + "\\rule[-0.75\\baselineskip]{0pt}{0pt}\n" + element,
|
||||||
latex_width, vertical="t")
|
latex_width, vertical="t")
|
||||||
return caption_wrapper(element, hpos)
|
return caption_wrapper(element, hpos, offset=offset)
|
||||||
|
|||||||
@@ -17,4 +17,6 @@ test:
|
|||||||
./alone_test.sh
|
./alone_test.sh
|
||||||
./modulepath_test.sh
|
./modulepath_test.sh
|
||||||
./klammerset_test.sh
|
./klammerset_test.sh
|
||||||
|
./option_set_test.sh
|
||||||
|
./signature_test.sh
|
||||||
./editor_test.sh
|
./editor_test.sh
|
||||||
|
|||||||
307
tst/option_set_test.sh
Executable file
307
tst/option_set_test.sh
Executable 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
143
tst/signature_test.sh
Executable 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 ]
|
||||||
Reference in New Issue
Block a user