Initial commit: Klammertext source distribution

Curated source subset assembled by klammertext-dev's doc/make_dist.sh: the Klammermachine (mac), the Standard Klammer Set (sks), the commands (com), editor plugins and install guides (doc), a test subset (tst), and lib/bin placeholders. Builds with 'make -C com'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-18 18:48:23 +02:00
commit 2ba7ceee7a
272 changed files with 27634 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# Build artifacts
*.o
*.d
*.so
# Editor / OS cruft
*~
.DS_Store

18
LICENSE.md Normal file
View File

@@ -0,0 +1,18 @@
# License
Copyright © 2026 Andy Kopra. All rights reserved.
**This is a preliminary notice. A license will be published here.**
You may use this software and modify it for your own experimentation and use.
The name "Klammertext" and the definition of the Klammertext language are
reserved by the author. Nothing here grants permission to publish a modified
or alternative definition of the Klammertext language, or to distribute
software under the name "Klammertext," without the author's authorization.
Terms for redistribution, and for creating and sharing klammers, klammer sets,
commands, and other works built with Klammertext, will be set out in the
published license.
Contact: Andy Kopra <ack@acm.org>

35
README.md Normal file
View File

@@ -0,0 +1,35 @@
# Klammertext
Klammertext is a markup language that produces multiple output formats —
HTML, LaTeX/PDF, and plain text — from a single source description. Its core
engine, the Klammermachine, is written in C++; the Standard Klammer Set (SKS)
adds a default library of formatting and document-structuring operators on top.
## Installing
Installation guides are in [`doc/install/`](doc/install/):
- Linux, from source — `doc/install/linux_source_install.md`
- macOS, from source — `doc/install/macos_source_install.md`
- Linux, container — `doc/install/linux_container_install.md`
- macOS, container — `doc/install/macos_container_install.md`
## Building from source
With a C++20 compiler and `KLAMMERTEXT_HOME` set to this directory:
make -C com
This builds the Klammermachine library (into `lib/`), the SKS components, and
the three commands — `ktext`, `kdesc`, `kdiag` (into `bin/`). See the
source-install guide for prerequisites (TeX Live for PDF output, Python, and so
on).
## Editor support
Syntax highlighting and editing support for Emacs and Sublime Text are in
[`doc/edit/`](doc/edit/).
## License
See [`LICENSE.md`](LICENSE.md).

3
bin/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!.gitignore
!README.md

5
bin/README.md Normal file
View File

@@ -0,0 +1,5 @@
# bin
This directory holds the built Klammertext command executables (`ktext`,
`kdesc`, `kdiag`) after you build Klammertext with `make -C com`.
It is empty in the repository.

4
com/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
ktext
kdesc
kdiag
*.d

101
com/Makefile Normal file
View File

@@ -0,0 +1,101 @@
# Klammertext com/ Makefile
# Improved version with automatic header dependency tracking
K := $(KLAMMERTEXT_HOME)
KS := $(K)/sks
KM := $(K)/mac
include $(KM)/env/makefile.env
# Commands to build
COMMANDS := kdiag kdesc ktext
# Build output directory: commands are linked directly into ../bin, so there is
# exactly one copy of each (no redundant executables in com/) and make still
# tracks them by path for incremental builds.
BINDIR := ../bin
BINCOMMANDS := $(addprefix $(BINDIR)/,$(COMMANDS))
# System install location (out-of-tree). Override on the command line, e.g.
# make install PREFIX=/opt DESTDIR=/tmp/stage
PREFIX ?= /usr/local
DESTDIR ?=
# Shared library location
LIBDIR := ../lib
LIBRARY := $(LIBDIR)/libklammertext.so
# Linker flags for commands
COM_LDFLAGS := $(EXPORT_DYNAMIC) -Wl,-rpath,'$(ORIGIN)/../lib' -L$(LIBDIR)
# Dependency files for command sources
DEPFILES := $(addsuffix .d,$(COMMANDS))
# Compiler flags for dependency generation ($(@F) keeps the .d files in com/,
# not in the ../bin output directory).
DEPFLAGS = -MMD -MP -MF $(@F).d
.PHONY: all clean redo clang mac sks install
# Default target: build dependencies first, then commands
all : | mac sks
$(MAKE) commands
# Separate target to build commands (called after dependencies are ready)
.PHONY: commands
commands : $(BINCOMMANDS)
# Ensure the output directory exists before linking into it.
$(BINDIR) :
mkdir -p $@
# Build mac/ objects
mac :
$(MAKE) -C $(KM) -j
# Build sks/ components (depends on mac)
sks : | mac
$(MAKE) -C $(KS)/kutil -j
$(MAKE) -C $(KS)/target -j
$(MAKE) -C $(KS)/document
# Explicit rules for each command - link against shared library, output to bin/
$(BINDIR)/kdiag : kdiag.cpp $(LIBRARY) | $(BINDIR)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $(COM_LDFLAGS) $(LDFLAGS) $< -o $@ -lklammertext $(LDLIBS)
$(BINDIR)/kdesc : kdesc.cpp $(LIBRARY) | $(BINDIR)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $(COM_LDFLAGS) $(LDFLAGS) $< -o $@ -lklammertext $(LDLIBS)
$(BINDIR)/ktext : ktext.cpp $(LIBRARY) | $(BINDIR)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $(COM_LDFLAGS) $(LDFLAGS) $< -o $@ -lklammertext $(LDLIBS)
# Out-of-tree system install (copies; the build tree stays intact). The
# installed commands still need KLAMMERTEXT_HOME pointing at a Klammertext tree
# for sks/ and the Python modules; their rpath finds libklammertext.so in
# $(PREFIX)/lib.
install : all
install -d $(DESTDIR)$(PREFIX)/bin $(DESTDIR)$(PREFIX)/lib
install -m755 $(BINCOMMANDS) $(DESTDIR)$(PREFIX)/bin
install -m755 $(LIBRARY) $(DESTDIR)$(PREFIX)/lib
clean :
rm -f $(BINCOMMANDS) $(DEPFILES) *~
redo :
ifneq ($(filter clang,$(MAKECMDGOALS)),)
@:
else
$(MAKE) -C $(KM) clean
$(MAKE) -C $(KS)/kutil clean
$(MAKE) -C $(KS)/target clean
$(MAKE) -C $(KS)/document clean
$(MAKE) clean
$(MAKE) all
endif
# "make clang" = incremental build; "make clang redo" = full clean rebuild
clang :
$(MAKE) $(or $(filter-out clang,$(MAKECMDGOALS)),all) COMPILER=clang
# Include generated dependency files (if they exist)
-include $(DEPFILES)

83
com/kdesc.cpp Normal file
View File

@@ -0,0 +1,83 @@
#include "argv.h"
#include "command.h"
#include "error.h"
#include "file.h"
#include "ktype.h"
#include "log.h"
#include "argtype_set.h"
#include "character.h"
#include "show.h"
#include "util.h"
int main(int argc, char* argv[])
{
try {
set_verbose_level(argc, argv);
Argv args {};
args.flag("c", "Special characters");
args.flag("a", "Argument types");
args.flag("k", "Katom types");
args.flag("r", "Katom rewrite patterns");
args.opt("input", "Input filename", "filename", "", "'text'");
args.flag("targets", "Show targets defined by the input file");
args.flag("klammers", "Show klammers defined by the input file");
args.opt("v", "'verbosity'", "n", "0", "'verbosity'");
if (show_usage(argc, argv)) {
args.usage(file_basename(argv[0]));
exit(1);
}
auto p = [&](std::string name) { return args.get(name) == "true"; };
args.parse(argc, argv);
verbose_level = stoi(args.get("v"));
if (verbose_level > 0) {
args.describe();
}
if (p("c")) {
show_special_characters();
std::cout << "\n";
}
if (p("a")) {
Argtype_set argtypes;
std::cout << boldblack << "\nStandard klammer argument types\n" << black;
std::cout << argtypes.describe() << "\n";
}
if (p("k")) {
describe_katoms(verbose_level > 2);
}
if (p("r")) {
describe_rewrite_patterns();
}
Machine M;
strings_t input_filenames = args.as_vector("input");
std::cout << "input_filenames: " << input_filenames << "\n";
if (input_filenames.empty()) {
M.read(fs::path(M.m_state.value("KLAMMERTEXT_HOME") + "/sks/sks.k"));
} else {
for (auto fname : input_filenames) {
std::cout << "Read " << fname << "\n";
M.read(fs::path(absolute_pathname(fname)));
}
}
if (p("targets")) {
std::cout << boldblack << "Targets\n" << black << M.m_targets.describe(2, true);
}
if (p("klammers")) {
std::cout << boldblack << "Klammers\n" << black << M.m_klammers.describe(2);
}
}
catch (Error& e) {
e.print_message();
}
}

118
com/kdiag.cpp Normal file
View File

@@ -0,0 +1,118 @@
#include <iostream>
#include <limits>
#include <map>
#include "util.h"
#include "argument_set.h"
#include "argv.h"
#include "argument.h"
#include "command.h"
#include "error.h"
#include "file.h"
#include "katom.h"
#include "log.h"
#include "show.h"
int main(int argc, char* argv[])
{
try {
set_verbose_level(argc, argv);
Argv args {};
args.req("input", "Klammertext input text", "'text'");
args.flag("type", "Show katom types in subscript");
args.flag("index", "Show the list index of the katom");
args.flag("text", "Show text katoms with selected attributes");
args.flag("replaced","Show replaced katoms");
args.flag("ignored", "Show ignored katoms");
args.flag("all", "Show all katoms, including katoms replaced or ignored"); // in brackets with selected attributes");
args.flag("spans", "Show the beginning and ending katoms of spans");
args.flag("rewrite", "Show applied rewrite rules");
args.flag("args", "Show how the text would be parsed as klammer arguments");
args.opt("pos", "Number of positional arguments to parse for --args", "count", "-1", R"(([^\s]+))");
args.flag("read", "Process read: @read <filename> @");
args.flag("eval", "Process eval: @eval <expression> @");
args.flag("cond", "Process cond: @cond <condition> | <if-true> | <if_false> @");
args.flag("nonascii","Process encoded characters (not ASCII): ^... or ^...^");
args.flag("literal", "Process literal: ^'...'^");
args.flag("ignore", "Process ignored: #, ##, #[...]#");
args.flag("ws", "Process whitespace: #-, #+, #/");
args.flag("klammer", "Process klammer definitions: @@<name> ... @@");
args.flag("process", "Process all");
args.opt("v", "'verbosity'", "degree", "0", "'verbosity'");
if (show_usage(argc, argv)) {
args.usage(file_basename(argv[0]));
exit(1);
}
auto p = [&](std::string name) { return args.get(name) == "true"; };
args.parse(argc, argv);
verbose_level = args.as_int("v");
if (verbose_level > 0) {
args.describe();
}
if (p("rewrite")) {
show_rewrite_rules = true;
}
Machine machine;
std::string input = args.as_string("input");
std::string command = construct_command_pathname(argv[0]);
katom_list katoms {};
if (p("process")) {
katoms = machine.process(input, command);
} else {
katoms = machine.process(
//args.as_string("input"), construct_command_pathname(argv[0]),
input, command,
p("nonascii"), p("literal"), p("ignore"), p("ws"), p("klammer"),
p("eval"), p("cond"), p("read"));
}
if (p("spans")) {
describe_spans(katoms);
} else {
if (p("ignored")) std::cout << kignored;
if (p("type")) std::cout << ktype;
if (p("index")) std::cout << kindex;
if (p("text")) std::cout << kall;
if (p("all")) std::cout << kall << kreplaced << kignored;
if (p("replaced")) std::cout << kreplaced;
std::cout << katoms << "\n";
}
if (p("args")) {
int req_count = args.as_int("pos");
bool limit_req = req_count != -1;
if (req_count == -1) {
req_count = std::numeric_limits<int>::max();
}
auto [required, optional, rest] =
argument_split(katoms.begin(), katoms.end(), req_count);
std::stringstream ss {};
ss << "required";
if (limit_req) {
ss << " (" << req_count << ")";
}
ss << ":";
std::string req_label = ss.str();
auto label_width = req_label.size() + 2;
std::cout << std::setfill(' ')
<< std::right << std::setw(label_width) << req_label << " " << required << "\n"
<< std::right << std::setw(label_width) << "optional:" << " " << optional << "\n";
if (limit_req) {
std::cout << std::right << std::setw(label_width) << "rest:" << " " << rest << "\n";
}
}
}
catch (Error& e) {
std::string advice = "";
if (e.m_type == "target")
advice = "To include the Standard Klammer Set, add flag \"--sks\".";
e.print_message(advice);
}
std::cout << black;
return 0;
}

109
com/ktext.cpp Normal file
View File

@@ -0,0 +1,109 @@
#include <iostream>
#include "error.h"
#include "command.h"
#include "log.h"
#include "argv.h"
#include "file.h"
#include "util.h"
#include "machine.h"
#include "show.h"
#include "target_set.h"
int main(int argc, char* argv[])
{
try {
set_verbose_level(argc, argv);
Argv args {};
args.req("filenames", "Input files in Klammertext format. ", "'list'");
args.opt("s", "Text processed before input files.", "input-string", "", "'text'");
args.opt("t","Output target; default is general (unspecified)", "target",
Target_set::general_name, "'word'");
args.opt("o", "Output basename; meaning and default defined by target.", "basename",
"", "'word'");
args.opt("k", "File containing the klammerset definition; default is the Standard Klammer Set. With a value of \"none\", no klammerset is loaded.",
"pathname", "", "'word'");
args.flag("d", "Display the output to the screen, rather than writing files.");
args.flag("m", "Show the Klammermachine state at the beginning of processing.");
args.opt("v", "'verbosity'", "degree", "0", "'verbosity'");
if (show_usage(argc, argv)) {
args.usage(file_basename(argv[0]));
exit(1);
}
args.parse(argc, argv);
if (verbose_level > 0) {
args.describe();
}
std::string input_text = args.as_string("s");
std::vector<std::string> input_filenames = args.as_vector("filenames");
if (input_text.empty() && input_filenames.empty()) {
throw Argument_error(
"You must specify input filenames and/or text", Locator());
}
auto [target, output_dir, output_basename, output_filename,
write_files, display_only] =
parse_args(input_filenames, args.as_string("t"), args.as_string("o"),
args.as_bool("d"));
if (*(output_filename.end() - 1) == '*'
&& !display_only) {
throw Argument_error(
"You must specify an output target or "
"display the results with the \"-d\" flag.",
Locator());
}
Machine M;
M.m_state.open_frame("ktext");
M.m_state.set("K_target", target);
M.m_state.set("K_output_dir", output_dir);
M.m_state.set("K_output_basename", output_basename);
M.m_state.set("K_stdout_only", display_only ? "true" : "false");
M.m_state.set("K_input_filenames", join(input_filenames, " "));
M.m_state.set("K_verbose_level", std::to_string(verbose_level));
if (!input_filenames.empty()) {
M.m_state.set(
"K_input_dir",absolute_pathname(file_directory(input_filenames[0])));
} else {
M.m_state.set("K_input_dir", fs::current_path().string());
}
std::string klammerset_filename = args.as_string("k");
if (klammerset_filename != "none") {
if (klammerset_filename.empty()) {
klammerset_filename = M.m_state.value("KLAMMERTEXT_HOME") + "/sks/sks.k";
}
K::log(1, "Reading klammerset filename: " + klammerset_filename);
M.read(fs::path(klammerset_filename));
}
if (!input_text.empty()) {
M.read(input_text + "\n");
}
for (auto p : input_filenames) {
M.read(fs::path(p));
}
if (args.as_bool("m")) {
std::cout << M << "\n";
}
std::string result = trim(M.apply(target));
if (display_only && !result.empty()) {
std::cout << result << "\n";
} else if (!result.empty()) {
msg() << "Output filename: " << output_filename << "\n";
string_to_file(output_filename, result + "\n");
(void)K::log(1, "Wrote file: " + output_filename);
}
}
catch (Error& err) {
err.print_message();
std::cout << "\n";
return 1;
}
return 0;
}

183
doc/edit/emacs/README.md Normal file
View File

@@ -0,0 +1,183 @@
# Emacs mode for Klammertext
`klammertext-mode.el` is an Emacs major mode for editing Klammertext files. It
helps Klammertext authors see the structure of klammer application through
syntax highlighting.
## Install
Put the `emacs` directory somewhere on your system, then tell Emacs where it is
and load the mode. Add to `~/.emacs.d/init.el`:
```elisp
(add-to-list 'load-path "full-pathname-of-the-emacs-directory")
(require 'klammertext-mode)
```
Replace `full-pathname-of-the-emacs-directory` with the full path to the
directory that contains `klammertext-mode.el`.
The mode auto-activates for `.kt` and `.k` files. (The `.k` / `.kt` distinction
is a filing convention, not a lexical one — the same mode serves both.) You can
also switch to it manually with `M-x klammertext-mode`.
## What it highlights
**Text-removal ("ignore") constructs** — in two independently chosen colors,
one for the *removed content*, one for the *marker characters*:
| Construct | Meaning |
|--------------|----------------------------------|
| `#` ... | remove to end of line |
| `##` ... | remove to end of buffer |
| `#[ ... ]#` | remove enclosed text (nestable) |
**Klammer applications** — in two independent colors: one for *opening* a
klammer, one for *closing* it. The `@` and the name of an opening are one
syntactic unit and share the opening color; the close (named or bare) gets the
closing color, so you always have visual confirmation of where a klammer ends:
| Form | Color | Meaning |
|-----------|---------|----------------------------------|
| `@name` | opening | opening `@` + name (one unit) |
| `name@` | closing | named closing delimiter |
| `@` | closing | bare closing delimiter |
In the abbreviated form `@name-arg1-arg2` (equivalent to `@name arg1 | arg2 @`)
only the name is colored — the name ends at the first hyphen, and the
hyphen-separated arguments stay plain, just as `arg1`/`arg2` would be plain in
the long form.
Because a named closing carries the closing color across its name too, long
klammers that name their closing delimiter (`@document ... document@`) stand
out — which is exactly where naming the closing delimiter earns its keep
(accurate unmatched-delimiter error messages). Short bodies (`@i word @`) are
conventionally left with a bare `@` to keep the text uncluttered.
**Klammer definitions** (`@@`) are highlighted the same way, in their own pair
of colors — so definitions read as distinct from applications at a glance:
| Form | Color | Meaning |
|------------|---------|-----------------------------------|
| `@@name` | opening | opening `@@` + name (one unit) |
| `name@@` | closing | named closing delimiter |
| `@@` | closing | bare closing delimiter |
The name ends at the first non-name character, so a target suffix like
`@@name.html` colors only `@@name` and leaves `.html` plain. A definition's
body (between `@@name` and the closing `@@`) is highlighted like ordinary
Klammertext — e.g. an `@i … @` inside it shows as a normal application.
**System/target commands** (`@@@`) — `@@@target`, `@@@argtype`, `@@@state` — get
a third pair of colors, so the three `@`-levels (application, definition, system)
are visually distinct:
| Form | Color | Meaning |
|------------|---------|-----------------------------------|
| `@@@name` | opening | opening `@@@` + name (one unit) |
| `name@@@` | closing | named closing delimiter |
| `@@@` | closing | bare closing delimiter |
`@@@` commands do not nest, so each delimiter is colored independently; their
bodies (`| … |` option lists) are highlighted as ordinary Klammertext.
## The eight faces
All eight faces are defined by the `defconst klammertext--palette` at the
beginning of `klammertext-mode.el`.
| Face | Applies to |
|-----------------------------------|-------------------------------------|
| `klammertext-ignored-face` | removed content |
| `klammertext-marker-face` | `#`, `##`, `#[`, `]#` |
| `klammertext-klammer-open-face` | an application opening `@name` |
| `klammertext-klammer-close-face` | an application close `name@` or `@` |
| `klammertext-def-open-face` | a definition opening `@@name` |
| `klammertext-def-close-face` | a definition close `name@@` or `@@` |
| `klammertext-system-open-face` | a system opening `@@@name` |
| `klammertext-system-close-face` | a system close `name@@@` or `@@@` |
To experiment with a color (evaluate in `*scratch*`, or add to your init):
```elisp
(set-face-foreground 'klammertext-marker-face "cyan")
(set-face-foreground 'klammertext-system-close-face "chocolate4")
```
or `M-x customize-face RET klammertext-marker-face RET`.
## Matching delimiters (show-paren)
With `show-paren-mode` on (the default in Emacs 28+), placing point on a klammer
**application** delimiter highlights its partner, in both directions: on an
opening `@name` it highlights the closing `@`/`name@`, and on a close it
highlights the opening `@name`. Nesting is respected — in `@a @b x @ @`, the
outer `@a` matches the last `@`, not the first.
Matching covers applications only (`@`), not `@@`/`@@@`, since that is where
paired delimiters matter most. The matcher steps over `@@`/`@@@`, removed text,
other literal spans and escaped `^@`; the abbreviated `@name-arg` form has no
closing delimiter, so nothing is highlighted on it.
**Literal klammers** (those in `klammertext-literal-klammers`, e.g. `@code`) are
closed with the full `NAME@` form because their content is verbatim. These are
matched *by name*`@code``code@` — with the content treated as opaque, so a
stray `@` inside (`@code x @ y code@`) doesn't confuse the match, in either
direction. Register any klammer that declares a `literal` argument with
`(klammertext-add-literal-klammer "name")` in your init file so both its
highlighting and its delimiter matching work.
If a **named** close disagrees with its opening — e.g. `@doc … foo@` (should be
`doc@`) — the mismatched delimiter is shown in **bright red** (bold), and a
message describing the mismatch appears in the minibuffer, e.g.
> `Klammertext: closing foo@ does not match opening @doc`
An unbalanced delimiter (an opening with no close, or vice versa) is flagged the
same way. This turns the naming convention into a live check: name a long
klammer's closing delimiter and a typo'd or unbalanced name lights up
immediately. The red comes from `klammertext-mismatch-face`, which is remapped
over `show-paren-mismatch` **only in Klammertext buffers** (your global
`show-paren-mismatch` face is left untouched); customize it to taste.
This is wired in automatically (`show-paren-data-function`); you only need
`show-paren-mode` enabled. It relies on nothing in the syntax table — Klammertext
delimiters can't be expressed there — so it does not interfere with other
`@`/`#` characters.
### Jumping between matches
`klammertext-jump-to-match`, bound to **`C-c C-j`**, moves point to the matching
delimiter: from an opening `@name` to its close, and from a close back to its
opening `@name`. It uses the same matcher as the highlighting. The starting
position is pushed to the mark ring, so `C-u C-SPC` jumps back. (Also available
as `M-x klammertext-jump-to-match`.)
## Literal klammers
Inside a `literal` argument — for example the body of `@code ... code@``#`
and `@` are literal text, not Klammertext syntax. The mode highlights the
opening `@code` and closing `code@` but leaves the interior as normal text,
for any klammer registered in `klammertext-literal-klammers`. `@code` is
registered by default.
If you define your own klammer with a `literal` parameter (a relatively
advanced action — see the `literal` argument type in the project
documentation), register it in your init file:
```elisp
(klammertext-add-literal-klammer "myverbatim")
```
Because the list is consulted at fontification time, registering a klammer
while a buffer is already open takes effect after `M-x font-lock-update` (or
re-visiting the file).
## Known limitations (deliberate, for now)
- Unescaped `@` is always treated as a delimiter (as the Klammermachine does),
so an `@` in prose that is *not* meant as a klammer — e.g. an email address
written `foo@bar` instead of `foo^@bar` — will be highlighted. This reflects
what the machine actually sees.
- Very large multiline blocks or literal spans edited far from their opening
may occasionally need `M-x font-lock-update` to re-highlight correctly.

View File

@@ -0,0 +1,680 @@
;;; klammertext-mode.el --- Major mode for Klammertext files -*- lexical-binding: t; -*-
;; An Emacs major mode for editing Klammertext files. It highlights:
;;
;; Text-removal ("ignore") constructs:
;; # ... remove to end of line
;; ## ... remove to end of buffer
;; #[ ... ]# remove enclosed text (nestable)
;;
;; Klammer applications:
;; @name opening delimiter + name
;; name@ named closing delimiter
;; @ bare closing delimiter
;;
;; Klammer definitions (@@), analogous to applications:
;; @@name opening delimiter + name
;; name@@ named closing delimiter
;; @@ bare closing delimiter
;;
;; System/target commands (@@@), analogous again:
;; @@@name opening delimiter + name
;; name@@@ named closing delimiter
;; @@@ bare closing delimiter
;;
;; Independent faces carry each pair of colors: removed content vs. the removal
;; marker characters; and, for applications (@name), definitions (@@name) and
;; system commands (@@@name), each construct's opening vs. its close.
;;
;; The same mode serves both .kt (content) and .k (klammer definition) files:
;; the .k/.kt split is a filing convention, not a lexical difference.
;;
;; Everything is driven by ONE left-to-right scanner (`klammertext--fontify').
;; That is what makes the interactions correct: inside removed text and inside
;; literal-klammer spans the scanner jumps over the content, so it is never
;; re-interpreted as klammers or comments.
;;
;; It also matches klammer APPLICATION delimiters for `show-paren-mode' (both
;; directions, with mismatched-name flagging); see the show-paren section below.
;;
;; Not handled yet (deliberately):
;; * The bodies of definitions (@@) and system commands (@@@) are highlighted
;; like ordinary Klammertext (an @i ... @ inside shows as a normal
;; application), rather than being treated specially.
;; * Inside a `literal' argument (e.g. @code ... code@) neither # nor @ is a
;; marker. The scanner highlights the opening @code and closing code@ but
;; leaves the interior as normal text, for any klammer registered in
;; `klammertext-literal-klammers'.
;;; Code:
(defgroup klammertext nil
"Editing Klammertext files."
:group 'text)
;; --- Faces: colors from a central palette table -------------------------
;;
;; All highlighting colors live in one table, `klammertext--palette', so they
;; can be tuned in a single place. Each face carries a DARK-background value
;; and a LIGHT-background value; Emacs picks automatically from the frame or
;; terminal background (there is no "dark mode" toggle to set).
;;
;; The DARK column is a systematic scheme (developed for the Sublime Text port):
;; three tier hues — application blue, definition green, system orange — each
;; opening bright and its close the same hue at 0.80 intensity.
;;
;; The LIGHT column is a hybrid tuned by eye: the three delimiter opens are the
;; original Emacs colors (RoyalBlue2 / green4 / orange3), each close = 0.60 x its
;; open (from the open name's X11 RGB #436eee/#008b00/#cd8500); the marker and
;; mismatch light values are the systematic ones; the ignored (gray) light value
;; is the systematic gray reduced by 0.90 (#9a9a9a -> #8b8b8b).
(defconst klammertext--palette
;; (face dark light description [extra-attrs])
'((klammertext-ignored-face "#8a8272" "#8b8b8b" "removed (ignored) content")
(klammertext-marker-face "#ff6b6b" "#994040" "removal markers # ## #[ ]#")
(klammertext-klammer-open-face "#89ddff" "RoyalBlue2" "application opening @name")
(klammertext-klammer-close-face "#6eb1cc" "#28428f" "application close name@ or bare @")
(klammertext-def-open-face "#c3e88d" "green4" "definition opening @@name")
(klammertext-def-close-face "#9cba71" "#005300" "definition close name@@ or bare @@")
(klammertext-system-open-face "#ffab70" "orange3" "system opening @@@name")
(klammertext-system-close-face "#cc895a" "#7b5000" "system close name@@@ or bare @@@")
(klammertext-mismatch-face "#ff5555" "#c02020" "mismatched/unbalanced delimiter"
(:weight bold)))
"Klammertext face colors: (FACE DARK LIGHT DESCRIPTION [EXTRA-ATTRS]).
DARK is the foreground on dark backgrounds, LIGHT on light backgrounds; each
face is generated with both. EXTRA-ATTRS, if present, is a plist merged into
both grounds. See the notes above the table.")
;; Generate the faces from the table. Using `custom-declare-face' (what
;; `defface' expands to) keeps each face customizable via M-x customize-face.
(dolist (entry klammertext--palette)
(let ((face (nth 0 entry))
(dark (nth 1 entry))
(light (nth 2 entry))
(desc (nth 3 entry))
(extra (nth 4 entry)))
(custom-declare-face
face
`((((background dark)) :foreground ,dark ,@extra)
(((background light)) :foreground ,light ,@extra))
(format "Klammertext highlighting for %s.\nColor is set from the `klammertext--palette' table." desc)
:group 'klammertext)))
;; --- Klammers whose literal content must not be interpreted -------------
(defcustom klammertext-literal-klammers nil
"Names of klammers whose content is a `literal' argument.
Such a klammer must be closed with the full NAME@ form (e.g. @code ... code@),
because its content is verbatim. This list is consulted in two places:
* Font-lock leaves the verbatim interior as normal text (#, @, etc. inside
are not interpreted).
* Delimiter matching (`show-paren-mode' and `klammertext-jump-to-match')
pairs the opening @NAME with its closing NAME@ *by name* rather than by
depth counting, so a literal close is matched even though its content may
contain unbalanced @ characters.
Any klammer that declares a `literal' argument should be registered here.
Register one with `klammertext-add-literal-klammer', e.g. in your init file:
(klammertext-add-literal-klammer \"mycode\")"
:type '(repeat string)
:group 'klammertext)
;; SYNC: the Sublime Text port in doc/sublime/ duplicates this list statically
;; (a Sublime syntax/plugin cannot read this Emacs defcustom). When you add or
;; remove a literal klammer, mirror it in BOTH:
;; * LITERAL_KLAMMERS in doc/sublime/Klammertext.py
;; * the @NAME literal rule + literal_NAME context in
;; doc/sublime/Klammertext.sublime-syntax
;; All three are currently seeded with just "code".
(defun klammertext-add-literal-klammer (name)
"Register NAME as a klammer whose literal content must not be interpreted.
NAME is the klammer name without the leading @ (e.g. \"code\")."
(add-to-list 'klammertext-literal-klammers name))
;; Seed the list through the same entry point future users will use.
(klammertext-add-literal-klammer "code")
;; --- Helpers -----------------------------------------------------------
(defun klammertext--escaped-p (pos)
"Non-nil if the character at POS is escaped by an odd run of ^ before it.
In Klammertext `^#' and `^@' are literal, so such a character is not a
marker or a delimiter."
(let ((n 0) (i (1- pos)))
(while (and (>= i (point-min)) (eq (char-after i) ?^))
(setq n (1+ n) i (1- i)))
(= (mod n 2) 1)))
(defun klammertext--name-char-p (ch)
"Non-nil if CH can be part of a klammer name (letter, digit or _).
A hyphen is NOT a name character: in the abbreviated form
@name-arg1-arg2 the hyphen separates the name from its arguments, so a
klammer name ends at the first hyphen."
(and ch (or (and (>= ch ?a) (<= ch ?z))
(and (>= ch ?A) (<= ch ?Z))
(and (>= ch ?0) (<= ch ?9))
(eq ch ?_))))
(defun klammertext--block-end (from)
"Return the position just after the ]# that closes a #[ block.
FROM is the position just after the opening #[. Counts nested #[ ... ]#
pairs; returns `point-max' if the block is never closed."
(goto-char from)
(let ((depth 1))
(while (and (> depth 0)
(re-search-forward "#\\[\\|]#" nil t))
(if (string= (match-string 0) "#[")
(setq depth (1+ depth))
(setq depth (1- depth))))
(if (> depth 0) (point-max) (point))))
(defun klammertext--set-match (wb we &rest groups)
"Set match data covering WB..WE with up to nine GROUPS.
Each group is a cons (BEG . END), or nil for an absent group (whose
highlight spec must use LAXMATCH)."
(let ((md (list wb we)))
(dotimes (_ 9)
(let ((g (pop groups)))
(setq md (append md (if g (list (car g) (cdr g)) (list nil nil))))))
(set-match-data md)))
;; --- Token emitters (called by the scanner) ----------------------------
;; Each returns non-nil when it has emitted a highlight token (and set the
;; match data + moved point past it), or nil to let the scanner keep going.
;; Groups: 1 removal-marker 2 removed-content 3 removal-close-marker
;; 4 app-open (@name) 5 app-close (name@ or bare @)
;; 6 def-open (@@name) 7 def-close (name@@ or bare @@)
;; 8 sys-open (@@@name) 9 sys-close (name@@@ or bare @@@)
(defun klammertext--emit-removal (pos _limit)
"POS is at a #. Point is at POS+1 on entry."
(let ((next (char-after (1+ pos))))
(cond
;; ## ... end of buffer
((eq next ?#)
(klammertext--set-match pos (point-max)
(cons pos (+ pos 2))
(cons (+ pos 2) (point-max))
nil nil nil)
(put-text-property pos (point-max) 'font-lock-multiline t)
(goto-char (point-max))
t)
;; #[ ... ]# (nestable)
((eq next ?\[)
(let* ((end (klammertext--block-end (+ pos 2)))
(close (if (and (>= end (+ pos 4))
(eq (char-before end) ?#)
(eq (char-before (1- end)) ?\]))
(- end 2) end)))
(klammertext--set-match pos end
(cons pos (+ pos 2))
(cons (+ pos 2) close)
(cons close end)
nil nil)
(put-text-property pos end 'font-lock-multiline t)
(goto-char end)
t))
;; #+ #/ #- are whitespace operators, NOT removals: keep scanning.
((memq next '(?+ ?/ ?-))
nil)
;; # ... end of line
(t
(let ((eol (line-end-position)))
(klammertext--set-match pos eol
(cons pos (1+ pos))
(cons (1+ pos) eol)
nil nil nil)
(goto-char eol)
t)))))
(defun klammertext--emit-klammer (pos _limit)
"POS is at an @. Point is at POS+1 on entry.
Dispatch by the length of the @-run at POS: a single @ is a klammer
APPLICATION (@name / name@ / @); @@ is a klammer DEFINITION (@@name /
name@@ / @@); @@@ is a system/target command (@@@name / name@@@ / @@@)."
(let ((before (and (> pos (point-min)) (char-before pos))))
(cond
;; Mid-run @ (previous char is @): the run's first @ drives everything,
;; so skip this one (e.g. the @ after an escaped ^@).
((eq before ?@)
(goto-char (1+ pos))
nil)
;; @@@ (or longer): system/target command (@@@target, @@@argtype, ...).
((and (eq (char-after (1+ pos)) ?@)
(eq (char-after (+ pos 2)) ?@))
(klammertext--emit-system pos before))
;; @@ : klammer definition delimiter
((eq (char-after (1+ pos)) ?@)
(klammertext--emit-def pos before))
;; single @ : klammer application
(t
(klammertext--emit-application pos before)))))
(defun klammertext--emit-application (pos before)
"Emit a single-@ klammer-application token at POS (groups 4 open / 5 close).
BEFORE is the character before POS."
(let ((after (char-after (1+ pos))))
(cond
;; @name : opening application (the @ and name are one unit). The name
;; ends at the first hyphen; any -arg1-arg2 abbreviation stays uncolored.
((klammertext--name-char-p after)
(goto-char (1+ pos))
(skip-chars-forward "A-Za-z0-9_")
(let* ((name-end (point))
(name (buffer-substring-no-properties (1+ pos) name-end)))
;; For a literal klammer, first locate the closing NAME@ and move the
;; scanner to its start (skipping the verbatim interior); the closing
;; is highlighted on the next scanner call. The search must happen
;; BEFORE `klammertext--set-match', because `re-search-forward'
;; clobbers the match data.
(if (member name klammertext-literal-klammers)
(let ((close (concat (regexp-quote name) "@")))
(if (re-search-forward close nil t)
(let ((close-end (point)))
(put-text-property pos close-end 'font-lock-multiline t)
(goto-char (- close-end (length name) 1)))
(put-text-property pos (point-max) 'font-lock-multiline t)
(goto-char (point-max))))
(goto-char name-end))
;; Set the match data for the opening LAST, so it survives to the
;; highlight step.
(klammertext--set-match pos name-end
nil nil nil
(cons pos name-end) ; 4: @name opening
nil)
t))
;; A @ preceded by name characters is either a named close NAME@, or the
;; bare close of a compact no-argument application @name@. They differ by
;; what precedes the name run: an @ there means the name belongs to the
;; opening (@name@), so this @ is a bare close and only it is coloured;
;; otherwise the whole NAME@ is the closing token.
((klammertext--name-char-p before)
(let ((name-start (save-excursion
(goto-char pos)
(skip-chars-backward "A-Za-z0-9_")
(point))))
(if (and (> name-start (point-min))
(eq (char-before name-start) ?@))
(klammertext--set-match pos (1+ pos) ; @name@ -> bare @
nil nil nil nil
(cons pos (1+ pos)))
(klammertext--set-match name-start (1+ pos) ; NAME@ named close
nil nil nil nil
(cons name-start (1+ pos))))
(goto-char (1+ pos))
t))
;; bare @ : unnamed closing delimiter
(t
(klammertext--set-match pos (1+ pos)
nil nil nil
nil
(cons pos (1+ pos))) ; 5: bare @ close
(goto-char (1+ pos))
t))))
(defun klammertext--emit-def (pos before)
"Emit a @@ klammer-definition delimiter token at POS (groups 6 open / 7 close).
POS and POS+1 are both @. BEFORE is the character before POS. Mirrors
`klammertext--emit-application', with @@ in place of @."
(let ((after (char-after (+ pos 2)))) ; char right after the @@
(cond
;; @@name : opening definition (the @@ and name are one unit). The name
;; ends at the first non-name char (a space, or the .target suffix).
((klammertext--name-char-p after)
(goto-char (+ pos 2))
(skip-chars-forward "A-Za-z0-9_")
(let ((name-end (point)))
(klammertext--set-match pos name-end
nil nil nil nil nil
(cons pos name-end) ; 6: @@name opening
nil)
(goto-char name-end)
t))
;; name@@ (named close) or the bare close of a compact no-body @@name@@.
;; As with applications, an @ before the name run means the name belongs
;; to the opening, so only the @@ is the closing token.
((klammertext--name-char-p before)
(let ((name-start (save-excursion
(goto-char pos)
(skip-chars-backward "A-Za-z0-9_")
(point))))
(if (and (> name-start (point-min))
(eq (char-before name-start) ?@))
(klammertext--set-match pos (+ pos 2) ; @@name@@ -> bare @@
nil nil nil nil nil nil
(cons pos (+ pos 2)))
(klammertext--set-match name-start (+ pos 2) ; NAME@@ named close
nil nil nil nil nil nil
(cons name-start (+ pos 2))))
(goto-char (+ pos 2))
t))
;; bare @@ : unnamed closing delimiter
(t
(klammertext--set-match pos (+ pos 2)
nil nil nil nil nil nil
(cons pos (+ pos 2))) ; 7: bare @@ close
(goto-char (+ pos 2))
t))))
(defun klammertext--emit-system (pos before)
"Emit a @@@ system/target delimiter token at POS (groups 8 open / 9 close).
POS, POS+1 and POS+2 are all @. BEFORE is the character before POS. These
commands (@@@target, @@@argtype, @@@state) do not nest, so each delimiter is
coloured independently, mirroring `klammertext--emit-def' with @@@ for @@."
(let ((after (char-after (+ pos 3)))) ; char right after the @@@
(cond
;; @@@name : opening command (the @@@ and name are one unit).
((klammertext--name-char-p after)
(goto-char (+ pos 3))
(skip-chars-forward "A-Za-z0-9_")
(let ((name-end (point)))
(klammertext--set-match pos name-end
nil nil nil nil nil nil nil
(cons pos name-end) ; 8: @@@name opening
nil)
(goto-char name-end)
t))
;; name@@@ (named close) or the bare close of a compact @@@name@@@.
((klammertext--name-char-p before)
(let ((name-start (save-excursion
(goto-char pos)
(skip-chars-backward "A-Za-z0-9_")
(point))))
(if (and (> name-start (point-min))
(eq (char-before name-start) ?@))
(klammertext--set-match pos (+ pos 3) ; @@@name@@@ -> bare @@@
nil nil nil nil nil nil nil nil
(cons pos (+ pos 3)))
(klammertext--set-match name-start (+ pos 3) ; NAME@@@ named close
nil nil nil nil nil nil nil nil
(cons name-start (+ pos 3))))
(goto-char (+ pos 3))
t))
;; bare @@@ : unnamed closing delimiter
(t
(klammertext--set-match pos (+ pos 3)
nil nil nil nil nil nil nil nil
(cons pos (+ pos 3))) ; 9: bare @@@ close
(goto-char (+ pos 3))
t))))
;; --- The single scanning matcher ---------------------------------------
(defun klammertext--fontify (limit)
"Font-lock matcher: emit the next Klammertext token up to LIMIT.
Removed and literal-klammer regions are jumped over, so their interiors
are never re-interpreted."
(let ((result nil))
(while (and (not result)
(re-search-forward "[#@]" limit t))
(let* ((pos (1- (point)))
(ch (char-after pos)))
(setq result
(cond
((klammertext--escaped-p pos) nil) ; ^# or ^@
((eq ch ?#) (klammertext--emit-removal pos limit))
(t (klammertext--emit-klammer pos limit))))))
result))
(defvar klammertext-font-lock-keywords
'((klammertext--fontify
(1 'klammertext-marker-face t t)
(2 'klammertext-ignored-face t t)
(3 'klammertext-marker-face t t)
(4 'klammertext-klammer-open-face t t)
(5 'klammertext-klammer-close-face t t)
(6 'klammertext-def-open-face t t)
(7 'klammertext-def-close-face t t)
(8 'klammertext-system-open-face t t)
(9 'klammertext-system-close-face t t)))
"Font-lock keywords for `klammertext-mode'.")
;; --- show-paren support (klammer applications only) --------------------
;;
;; show-paren cannot use the syntax table for Klammertext (the same @ is both
;; open and close, delimiters are multi-character, and open/close is decided by
;; context), so matching is driven by `show-paren-data-function'. Only single-@
;; APPLICATION delimiters are matched: @name <-> its closing @ or name@. The
;; matcher steps over @@/@@@ runs, removed text, other literal spans and escaped
;; ^@; the abbreviated @name-arg form opens no span. A LITERAL klammer (one in
;; `klammertext-literal-klammers', e.g. @code) is matched by name — @code <->
;; code@ — with its verbatim content opaque, since a depth scan would miscount
;; unbalanced @ inside it. A named close name@ whose name disagrees with its
;; opening @name is reported as a mismatch.
(defun klammertext--at-run-end (pos)
"Return the position just after the run of @ that begins at POS."
(let ((p pos)) (while (eq (char-after p) ?@) (setq p (1+ p))) p))
(defun klammertext--next-app-delim (limit)
"From point, find the next single-@ application delimiter before LIMIT.
Step over @@/@@@ runs, removed text, literal spans, escaped ^@, and the
abbreviated @name-arg form (which opens no span). Move point past the
delimiter (or skipped region) and return (POS . KIND) with KIND `open or
`close, or nil when none is found."
(catch 'found
(while (re-search-forward "[@#]" limit t)
(let ((hit (1- (point))))
(cond
((klammertext--escaped-p hit)) ; ^@ / ^# : keep going
((eq (char-after hit) ?#) ; removal: step over it
(let ((next (char-after (1+ hit))))
(goto-char (cond ((eq next ?#) (point-max))
((eq next ?\[) (klammertext--block-end (+ hit 2)))
((memq next '(?+ ?/ ?-)) (1+ hit))
(t (line-end-position))))))
((eq (char-after (1+ hit)) ?@) ; @@ / @@@ : step over run
(goto-char (klammertext--at-run-end hit)))
((klammertext--name-char-p (char-after (1+ hit))) ; @name : opening?
(goto-char (1+ hit))
(skip-chars-forward "A-Za-z0-9_")
(let ((name (buffer-substring-no-properties (1+ hit) (point))))
(cond
((member name klammertext-literal-klammers) ; literal span: skip
(let ((close (concat (regexp-quote name) "@")))
(unless (re-search-forward close nil t) (goto-char (point-max)))))
((eq (char-after) ?-)) ; @name-arg : no span
(t (throw 'found (cons hit 'open))))))
(t ; name@ / bare @ : closing
(goto-char (1+ hit))
(throw 'found (cons hit 'close))))))
nil))
(defun klammertext--match-forward (open-pos)
"OPEN-POS is the @ of an opening application. Return the matching close @
position, or nil if unbalanced."
(save-excursion
(goto-char (1+ open-pos))
(skip-chars-forward "A-Za-z0-9_") ; past the opening name
(let ((depth 1) (result nil) (go t))
(while (and go (> depth 0))
(let ((d (klammertext--next-app-delim nil)))
(if (null d)
(setq go nil)
(if (eq (cdr d) 'open)
(setq depth (1+ depth))
(setq depth (1- depth))
(when (= depth 0) (setq result (car d)))))))
result)))
(defun klammertext--match-backward (close-pos)
"CLOSE-POS is the @ of a closing application. Return the matching open @
position, or nil if unbalanced. Scans forward from `point-min' with a stack."
(save-excursion
(goto-char (point-min))
(let ((stack nil) (result nil) (go t))
(while go
(let ((d (klammertext--next-app-delim (1+ close-pos))))
(cond
((null d) (setq go nil))
((eq (cdr d) 'open) (push (car d) stack))
(t (let ((open (pop stack)))
(when (= (car d) close-pos)
(setq result open go nil)))))))
result)))
(defun klammertext--app-delim-info (pos)
"If the char at POS is a single-@ application delimiter, return (POS . KIND)
with KIND `open or `close; else nil. The abbreviated @name-arg form (which
opens no span) returns nil."
(when (and (eq (char-after pos) ?@)
(not (eq (char-before pos) ?@))
(not (eq (char-after (1+ pos)) ?@))
(not (klammertext--escaped-p pos)))
(if (klammertext--name-char-p (char-after (1+ pos)))
(let ((name-end (save-excursion (goto-char (1+ pos))
(skip-chars-forward "A-Za-z0-9_")
(point))))
(unless (eq (char-after name-end) ?-)
(cons pos 'open)))
(cons pos 'close))))
(defun klammertext--open-name (open-pos)
"Name of the opening @name at OPEN-POS."
(save-excursion (goto-char (1+ open-pos))
(buffer-substring-no-properties
(point) (progn (skip-chars-forward "A-Za-z0-9_") (point)))))
(defun klammertext--close-name (close-pos)
"Name of a named close NAME@ at CLOSE-POS, or nil for a bare @ (incl. @name@)."
(save-excursion
(goto-char close-pos)
(let ((ns (progn (skip-chars-backward "A-Za-z0-9_") (point))))
(when (and (< ns close-pos)
(not (eq (char-before ns) ?@)))
(buffer-substring-no-properties ns close-pos)))))
(defun klammertext--paren-mismatch (open-pos close-pos)
"Non-nil if OPEN-POS/CLOSE-POS is unbalanced, or the named close disagrees
with the opening name."
(or (null open-pos) (null close-pos)
(let ((cname (klammertext--close-name close-pos)))
(and cname (not (string= cname (klammertext--open-name open-pos)))))))
(defun klammertext--literal-delim-name (pos kind)
"If the application delimiter at POS (KIND `open or `close) belongs to a
literal klammer (one in `klammertext-literal-klammers'), return its name;
else nil. A literal klammer must be closed with the full NAME@ form because
its content is verbatim, so its @NAME open and NAME@ close are matched by
name, not by depth counting."
(let ((name (if (eq kind 'open)
(klammertext--open-name pos)
(klammertext--close-name pos))))
(and name (member name klammertext-literal-klammers) name)))
(defun klammertext--literal-match-forward (open-pos name)
"Return the @ of the NAME@ that closes the literal @NAME at OPEN-POS, or nil.
The verbatim content is opaque, so we search for the literal close string."
(save-excursion
(goto-char (+ open-pos 1 (length name)))
(when (search-forward (concat name "@") nil t)
(1- (point)))))
(defun klammertext--literal-match-backward (close-pos name)
"Return the @ of the @NAME that opens the literal NAME@ whose @ is at
CLOSE-POS, or nil. Literal spans do not nest, so the nearest preceding real
@NAME is the opener."
(save-excursion
(goto-char close-pos)
(let ((open-str (concat "@" name)) (result nil))
(while (and (not result) (search-backward open-str nil t))
(let ((op (point)))
(unless (or (eq (char-before op) ?@) ; @@NAME = definition
(klammertext--escaped-p op))
(setq result op))))
result)))
(defun klammertext--app-match (pos kind)
"Return the matching application delimiter for the delimiter at POS of KIND
\(`open or `close), or nil.
A literal klammer (in `klammertext-literal-klammers') matches by name
(@NAME <-> NAME@) with its content opaque; other klammers match by depth."
(let ((lit (klammertext--literal-delim-name pos kind)))
(cond
((and lit (eq kind 'open)) (klammertext--literal-match-forward pos lit))
((and lit (eq kind 'close)) (klammertext--literal-match-backward pos lit))
((eq kind 'open) (klammertext--match-forward pos))
(t (klammertext--match-backward pos)))))
(defun klammertext--report-mismatch (open-pos close-pos)
"Show a minibuffer message describing a klammer application mismatch.
Either position may be nil (an unbalanced delimiter)."
(message "%s"
(cond
((null close-pos)
(format "Klammertext: opening @%s has no matching close"
(klammertext--open-name open-pos)))
((null open-pos)
"Klammertext: closing delimiter has no matching open")
(t
(format "Klammertext: closing %s@ does not match opening @%s"
(or (klammertext--close-name close-pos) "?")
(klammertext--open-name open-pos))))))
(defun klammertext--show-paren-data ()
"`show-paren-data-function' for klammer applications, both directions.
Returns (HERE-BEG HERE-END THERE-BEG THERE-END MISMATCH) or nil, and reports
any mismatch in the minibuffer."
(let* ((p (point))
(info (or (klammertext--app-delim-info p)
(and (> p (point-min))
(klammertext--app-delim-info (1- p))))))
(when info
(let* ((dpos (car info)) (kind (cdr info))
(match (klammertext--app-match dpos kind))
(open (if (eq kind 'open) dpos match))
(close (if (eq kind 'open) match dpos))
(mism (klammertext--paren-mismatch open close)))
(when mism (klammertext--report-mismatch open close))
(list dpos (1+ dpos) match (and match (1+ match)) mism)))))
;; --- Interactive: jump to the matching application delimiter -----------
(defun klammertext-jump-to-match ()
"Jump to the matching klammer application delimiter.
On an opening @name, move to its closing @ or name@; on a close, move to the
opening @name. Uses the same matcher as `show-paren-mode'. The starting
position is pushed to the mark ring, so \\`C-u C-SPC' jumps back."
(interactive)
(let* ((p (point))
(info (or (klammertext--app-delim-info p)
(and (> p (point-min))
(klammertext--app-delim-info (1- p))))))
(unless info
(user-error "Point is not on a klammer application delimiter (@)"))
(let* ((dpos (car info)) (kind (cdr info))
(match (klammertext--app-match dpos kind)))
(unless match
(user-error "No matching delimiter for this %s klammer"
(if (eq kind 'open) "opening" "closing")))
(push-mark nil t)
(goto-char match))))
;; --- The mode ----------------------------------------------------------
;;;###autoload
(define-derived-mode klammertext-mode text-mode "Klammertext"
"Major mode for editing Klammertext files."
(setq-local font-lock-multiline t)
(setq-local font-lock-defaults '(klammertext-font-lock-keywords))
;; Match klammer application delimiters with `show-paren-mode' (which must be
;; enabled separately; it is on by default in Emacs 28+).
(setq-local show-paren-data-function #'klammertext--show-paren-data)
;; Show a mismatched delimiter in bright red rather than the default purple
;; `show-paren-mismatch', but only in Klammertext buffers.
(setq-local face-remapping-alist
(cons '(show-paren-mismatch klammertext-mismatch-face)
face-remapping-alist)))
(define-key klammertext-mode-map (kbd "C-c C-j") #'klammertext-jump-to-match)
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.kt\\'" . klammertext-mode))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.k\\'" . klammertext-mode))
(provide 'klammertext-mode)
;;; klammertext-mode.el ends here

View File

@@ -0,0 +1,60 @@
// Klammertext colors for the "Breakers" scheme (light ground).
// One hue system across all schemes: application = blue, definition =
// green, system = orange; each opens bright and its close is 80%% of the
// open (a klammer "begins bright and gets dark"). Shown at full intensity
// on dark grounds, at 60%% on light grounds for contrast. Delimiters are
// forced to normal style. Merged onto Breakers by filename; recolors only
// .klammertext scopes. (The highlighting was first developed as an Emacs
// major mode; see Klammertext_in_Sublime_Text.md.)
//
// #999999 removed text (Breakers's comment grey)
// #994040 removal markers
// #528599 @name open blue
// #426a7a name@ close darker blue
// #758b55 @@name open green
// #5e7044 name@@ close darker green
// #996743 @@@name open orange
// #7a5236 name@@@ close darker orange
{
"name": "Breakers",
"rules": [
{
"scope": "comment.line.klammertext, comment.block.klammertext",
"foreground": "#999999"
},
{
"scope": "punctuation.definition.comment.klammertext",
"foreground": "#994040"
},
{
"scope": "entity.name.function.begin.klammertext",
"foreground": "#528599",
"font_style": ""
},
{
"scope": "entity.name.function.end.klammertext",
"foreground": "#426a7a",
"font_style": ""
},
{
"scope": "storage.type.begin.klammertext",
"foreground": "#758b55",
"font_style": ""
},
{
"scope": "storage.type.end.klammertext",
"foreground": "#5e7044",
"font_style": ""
},
{
"scope": "keyword.control.begin.klammertext",
"foreground": "#996743",
"font_style": ""
},
{
"scope": "keyword.control.end.klammertext",
"foreground": "#7a5236",
"font_style": ""
}
]
}

View File

@@ -0,0 +1,60 @@
// Klammertext colors for the "Celeste" scheme (light ground).
// One hue system across all schemes: application = blue, definition =
// green, system = orange; each opens bright and its close is 80%% of the
// open (a klammer "begins bright and gets dark"). Shown at full intensity
// on dark grounds, at 60%% on light grounds for contrast. Delimiters are
// forced to normal style. Merged onto Celeste by filename; recolors only
// .klammertext scopes. (The highlighting was first developed as an Emacs
// major mode; see Klammertext_in_Sublime_Text.md.)
//
// #9a9a9a removed text (Celeste's comment grey)
// #994040 removal markers
// #528599 @name open blue
// #426a7a name@ close darker blue
// #758b55 @@name open green
// #5e7044 name@@ close darker green
// #996743 @@@name open orange
// #7a5236 name@@@ close darker orange
{
"name": "Celeste",
"rules": [
{
"scope": "comment.line.klammertext, comment.block.klammertext",
"foreground": "#9a9a9a"
},
{
"scope": "punctuation.definition.comment.klammertext",
"foreground": "#994040"
},
{
"scope": "entity.name.function.begin.klammertext",
"foreground": "#528599",
"font_style": ""
},
{
"scope": "entity.name.function.end.klammertext",
"foreground": "#426a7a",
"font_style": ""
},
{
"scope": "storage.type.begin.klammertext",
"foreground": "#758b55",
"font_style": ""
},
{
"scope": "storage.type.end.klammertext",
"foreground": "#5e7044",
"font_style": ""
},
{
"scope": "keyword.control.begin.klammertext",
"foreground": "#996743",
"font_style": ""
},
{
"scope": "keyword.control.end.klammertext",
"foreground": "#7a5236",
"font_style": ""
}
]
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!--
Comment toggling for Klammertext. Companion to Klammertext.sublime-syntax.
Ctrl+/ (toggle_comment) -> line removal: "# "
Ctrl+Shift+/ (toggle_comment block:true) -> block removal: "#[ ... ]#"
These map onto Klammertext's own text-removal syntax:
# removes to end of line (the line comment)
#[ ... ]# removes enclosed text, nestable (the block comment)
-->
<key>name</key>
<string>Comments</string>
<key>scope</key>
<string>text.klammertext</string>
<key>settings</key>
<dict>
<key>shellVariables</key>
<array>
<dict>
<key>name</key>
<string>TM_COMMENT_START</string>
<key>value</key>
<string># </string>
</dict>
<dict>
<key>name</key>
<string>TM_COMMENT_START_2</string>
<key>value</key>
<string>#[</string>
</dict>
<dict>
<key>name</key>
<string>TM_COMMENT_END_2</string>
<key>value</key>
<string>]#</string>
</dict>
</array>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,20 @@
// Klammertext key bindings.
//
// Binds "jump to matching klammer delimiter" (the companion Klammertext.py
// command) to Ctrl+M — Sublime's own "go to matching bracket" key, repurposed
// for klammers, since the built-in cannot match context-dependent @ pairs.
//
// The "selector" context confines the binding to Klammertext files, so Ctrl+M
// keeps its normal meaning everywhere else.
//
// macOS users may prefer "super+m"; change the "keys" value below. This file
// (no platform suffix) is loaded on all platforms.
[
{
"keys": ["ctrl+m"],
"command": "klammertext_jump_to_match",
"context": [
{ "key": "selector", "operator": "equal", "operand": "text.klammertext" }
]
}
]

View File

@@ -0,0 +1,471 @@
# Klammertext.py
#
# Sublime Text plugin for klammer APPLICATION (@) delimiters. Two features,
# both ports of doc/emacs/klammertext-mode.el, both reusing one matcher:
#
# 1. Jump between an opening and its close — the Sublime equivalent of the
# Emacs mode's `klammertext-jump-to-match' (bound C-c C-j). Command name
# klammertext_jump_to_match; keybinding in Default.sublime-keymap.
#
# 2. Live highlighting of the matching delimiter as the caret sits on one —
# the equivalent of the Emacs mode's show-paren support. Implemented as a
# ViewEventListener (see KlammertextMatchHighlighter at the bottom); no
# language server is involved. A mismatched named close or an unbalanced
# delimiter is highlighted in red with a status-bar message, mirroring the
# Emacs mode's klammertext-mismatch-face + minibuffer report.
#
# This is the companion to Klammertext.sublime-syntax. The syntax file only
# colors tokens; a tokenizer cannot match context-dependent delimiters, so the
# jump is implemented here as a TextCommand. The keybinding lives in the
# companion Default.sublime-keymap.
#
# Command name (for keymaps / the command palette): klammertext_jump_to_match
#
# ---------------------------------------------------------------------------
# What it does (a direct port of the elisp matcher):
# * On an opening @name, move to its closing @ or name@.
# * On a close (bare @ or name@), move to the opening @name.
# * Triggers when the caret is ON the @ or immediately AFTER it (the same
# on-or-just-after rule the Emacs command uses).
# * Only single-@ APPLICATION delimiters match. @@/@@@ runs, removed text
# (#, ##, #[...]#), escaped ^@, and literal-klammer spans (@code ... code@)
# are stepped over, exactly as in the Emacs mode. The abbreviated
# @name-arg form opens no span.
# * Works at every caret when there are multiple selections.
#
# Literal klammers (identical to C-c C-j): a @code ... code@ span is opaque.
# The general depth scan still steps over such a span WHOLESALE when matching
# some OTHER klammer, so verbatim @ inside it never miscount. A literal
# klammer's OWN delimiters are matched BY NAME rather than by depth (see
# app_match): @code jumps to the next code@, and code@ to the nearest preceding
# @code — correct even when the content holds unbalanced @, e.g. @code x @ y
# code@. LITERAL_KLAMMERS lists these names; keep it in sync with the '@code'
# handling in Klammertext.sublime-syntax.
#
# LITERAL_KLAMMERS must stay in sync with the literal klammers recognized in
# Klammertext.sublime-syntax (seeded there as @code). The Emacs mode keeps this
# list in the `klammertext-literal-klammers' defcustom; a plugin has no access
# to it, so it is duplicated here.
import sublime
import sublime_plugin
# Klammer names whose content is a literal argument (verbatim interior).
#
# SYNC: this list is one of three copies that must agree. When you add or
# remove a literal klammer, mirror it in all three:
# * klammertext-literal-klammers in doc/emacs/klammertext-mode.el (the source
# of truth; a Sublime syntax/plugin cannot read that Emacs defcustom)
# * LITERAL_KLAMMERS here
# * the @NAME literal rule + literal_NAME context in Klammertext.sublime-syntax
# All three are currently seeded with just "code".
LITERAL_KLAMMERS = set(["code"])
# --- pure helpers (operate on the whole buffer as a string) ----------------
def name_char_p(ch):
"""True if CH can be part of a klammer name (letter, digit or _).
A hyphen is NOT a name char: @name-arg1 ends the name at the first hyphen."""
if ch is None:
return False
return (('a' <= ch <= 'z') or ('A' <= ch <= 'Z')
or ('0' <= ch <= '9') or ch == '_')
def escaped_p(s, pos):
"""True if the char at POS is escaped by an odd run of ^ before it.
In Klammertext ^# and ^@ are literal, so such a char is not a delimiter."""
n = 0
i = pos - 1
while i >= 0 and s[i] == '^':
n += 1
i -= 1
return (n % 2) == 1
def block_end(s, frm):
"""Index just after the ]# that closes a #[ block opened at FROM (the index
just after the opening #[). Counts nested #[ ... ]#; len(s) if unclosed."""
depth = 1
i = frm
n = len(s)
while depth > 0:
a = s.find('#[', i)
b = s.find(']#', i)
if a == -1 and b == -1:
return n
if b == -1 or (a != -1 and a < b):
depth += 1
i = a + 2
else:
depth -= 1
i = b + 2
return i
def at_run_end(s, pos):
"""Index just after the run of @ that begins at POS."""
p = pos
n = len(s)
while p < n and s[p] == '@':
p += 1
return p
def next_app_delim(s, i, limit):
"""From index I, find the next single-@ application delimiter before LIMIT.
Step over @@/@@@ runs, removed text, literal spans, escaped ^@, and the
abbreviated @name-arg form. Return (pos, kind, next_i) with kind 'open' or
'close' and next_i the index to resume from, or None when none is found."""
n = len(s)
if limit is None:
limit = n
while i < limit:
# find next @ or # at or after i (emacs re-search-forward "[@#]")
j = i
while j < limit and s[j] != '@' and s[j] != '#':
j += 1
if j >= limit:
return None
hit = j
i = hit + 1 # default: advance past the hit
if escaped_p(s, hit): # ^@ / ^# : keep going
continue
nxt = s[hit + 1] if hit + 1 < n else None
if s[hit] == '#': # removal: step over it
if nxt == '#':
i = n
elif nxt == '[':
i = block_end(s, hit + 2)
elif nxt in ('+', '/', '-'):
i = hit + 1
else: # to end of line
eol = s.find('\n', hit)
i = n if eol == -1 else eol
continue
# s[hit] == '@'
if nxt == '@': # @@ / @@@ : step over the run
i = at_run_end(s, hit)
continue
if name_char_p(nxt): # @name : opening?
k = hit + 1
while k < n and name_char_p(s[k]):
k += 1
name = s[hit + 1:k]
after = s[k] if k < n else None
if name in LITERAL_KLAMMERS: # literal span: skip to its close
close = name + '@'
idx = s.find(close, k)
i = n if idx == -1 else idx + len(close)
continue
elif after == '-': # @name-arg : opens no span
i = k
continue
else:
return (hit, 'open', k)
else: # name@ / bare @ : closing
return (hit, 'close', hit + 1)
return None
def match_forward(s, open_pos):
"""OPEN_POS is the @ of an opening application. Return the matching close @
index, or None if unbalanced."""
n = len(s)
i = open_pos + 1
while i < n and name_char_p(s[i]): # past the opening name
i += 1
depth = 1
while depth > 0:
d = next_app_delim(s, i, None)
if d is None:
return None
pos, kind, nxt = d
i = nxt
if kind == 'open':
depth += 1
else:
depth -= 1
if depth == 0:
return pos
return None
def match_backward(s, close_pos):
"""CLOSE_POS is the @ of a closing application. Return the matching open @
index, or None if unbalanced. Scans forward from 0 with a stack."""
stack = []
i = 0
limit = close_pos + 1
while True:
d = next_app_delim(s, i, limit)
if d is None:
return None
pos, kind, nxt = d
i = nxt
if kind == 'open':
stack.append(pos)
else:
open_pos = stack.pop() if stack else None
if pos == close_pos:
return open_pos
def app_delim_info(s, pos):
"""If the char at POS is a single-@ application delimiter, return
(pos, kind) with kind 'open' or 'close'; else None. The abbreviated
@name-arg form (which opens no span) returns None."""
n = len(s)
if not (0 <= pos < n):
return None
if s[pos] != '@':
return None
if pos > 0 and s[pos - 1] == '@':
return None
if pos + 1 < n and s[pos + 1] == '@':
return None
if escaped_p(s, pos):
return None
nxt = s[pos + 1] if pos + 1 < n else None
if name_char_p(nxt):
k = pos + 1
while k < n and name_char_p(s[k]):
k += 1
after = s[k] if k < n else None
if after == '-':
return None
return (pos, 'open')
return (pos, 'close')
# --- name / mismatch helpers (for the live highlighter) --------------------
def _name_forward(s, pos):
"""Index just past the run of name chars starting at POS."""
n = len(s)
k = pos
while k < n and name_char_p(s[k]):
k += 1
return k
def open_name(s, open_pos):
"""Name of the opening @name whose @ is at OPEN_POS."""
return s[open_pos + 1:_name_forward(s, open_pos + 1)]
def close_name(s, close_pos):
"""Name of a named close NAME@ whose @ is at CLOSE_POS, or None for a bare @
(including the compact @name@ form, whose name belongs to the opening)."""
ns = close_pos
while ns > 0 and name_char_p(s[ns - 1]):
ns -= 1
if ns < close_pos and (ns == 0 or s[ns - 1] != '@'):
return s[ns:close_pos]
return None
def paren_mismatch(s, open_pos, close_pos):
"""True if the pair is unbalanced (either side None) or the named close
disagrees with the opening name."""
if open_pos is None or close_pos is None:
return True
cname = close_name(s, close_pos)
return cname is not None and cname != open_name(s, open_pos)
def token_region(s, pos, kind):
"""(start, end) of the whole delimiter token whose @ is at POS.
Opening: @ plus its name. Named close: the name plus @. Bare @: just @."""
if kind == 'open':
return (pos, _name_forward(s, pos + 1))
ns = pos
while ns > 0 and name_char_p(s[ns - 1]):
ns -= 1
if ns < pos and (ns == 0 or s[ns - 1] != '@'):
return (ns, pos + 1) # named close NAME@
return (pos, pos + 1) # bare @ (or @name@)
# --- matching dispatch: literal klammers by name, others by depth ----------
def literal_delim_name(s, pos, kind):
"""If the application delimiter at POS (kind 'open'/'close') belongs to a
literal klammer (name in LITERAL_KLAMMERS), return its name; else None.
A literal klammer's @NAME open and NAME@ close are matched by name, not by
depth counting, because its content is verbatim."""
name = open_name(s, pos) if kind == 'open' else close_name(s, pos)
if name and name in LITERAL_KLAMMERS:
return name
return None
def literal_match_forward(s, open_pos, name):
"""Index of the @ of the NAME@ that closes the literal @NAME at OPEN_POS, or
None. The content is opaque, so search for the literal close string."""
start = open_pos + 1 + len(name)
idx = s.find(name + '@', start)
return idx + len(name) if idx != -1 else None
def literal_match_backward(s, close_pos, name):
"""Index of the @ of the @NAME that opens the literal NAME@ whose @ is at
CLOSE_POS, or None. Literal spans do not nest, so the nearest preceding
real @NAME is the opener (not @@NAME, and not escaped)."""
open_str = '@' + name
end = close_pos
while True:
idx = s.rfind(open_str, 0, end)
if idx == -1:
return None
before = s[idx - 1] if idx > 0 else None
if before != '@' and not escaped_p(s, idx):
return idx
end = idx
def app_match(s, pos, kind):
"""Matching application delimiter for the delimiter at POS of KIND
('open'/'close'), or None. A literal klammer matches by name (@NAME <->
NAME@) with content opaque; other klammers match by depth."""
lit = literal_delim_name(s, pos, kind)
if lit is not None:
return (literal_match_forward(s, pos, lit) if kind == 'open'
else literal_match_backward(s, pos, lit))
return match_forward(s, pos) if kind == 'open' else match_backward(s, pos)
# --- the command -----------------------------------------------------------
class KlammertextJumpToMatchCommand(sublime_plugin.TextCommand):
"""Jump between a klammer application's opening and closing delimiter.
Sublime equivalent of the Emacs mode's C-c C-j."""
def run(self, edit):
view = self.view
s = view.substr(sublime.Region(0, view.size()))
new_regions = []
moved = False
message = None
for region in view.sel():
p = region.b
info = app_delim_info(s, p)
if info is None and p > 0:
info = app_delim_info(s, p - 1)
if info is None:
new_regions.append(region)
message = "point is not on a klammer application delimiter (@)"
continue
dpos, kind = info
match = app_match(s, dpos, kind)
if match is None:
new_regions.append(region)
message = ("no matching delimiter for this %s klammer"
% ("opening" if kind == 'open' else "closing"))
continue
new_regions.append(sublime.Region(match, match))
moved = True
view.sel().clear()
for r in new_regions:
view.sel().add(r)
if moved:
view.show(view.sel()[0].b)
elif message:
sublime.status_message("Klammertext: " + message)
def is_enabled(self):
# Only meaningful in Klammertext buffers.
return self.view.match_selector(0, "text.klammertext")
# --- live matched-delimiter highlighting (show-paren equivalent) -----------
class KlammertextMatchHighlighter(sublime_plugin.ViewEventListener):
"""Highlight the matching klammer application delimiter as the caret sits
on one. The Sublime equivalent of the Emacs mode's show-paren support —
driven by cursor movement, reusing the same context-sensitive matcher.
A matched pair is boxed (region.bluish); a mismatch or unbalanced delimiter
is boxed in red (region.redish) with a status-bar message. Both the token
under the caret and its match are boxed; the Emacs mode highlights only the
single @ character, but boxing the whole @name / name@ reads better here.
To highlight only the far delimiter, drop the first region in _update()."""
MATCH_KEY = 'klammertext_paren_match'
MISMATCH_KEY = 'klammertext_paren_mismatch'
@classmethod
def is_applicable(cls, settings):
return str(settings.get('syntax', '')).endswith('Klammertext.sublime-syntax')
def __init__(self, view):
super().__init__(view)
self._change_count = -1
self._text = ''
def _buffer(self):
# Re-read the buffer only when it has actually changed, so plain cursor
# movement over a large file does not re-copy the whole document.
cc = self.view.change_count()
if cc != self._change_count:
self._text = self.view.substr(sublime.Region(0, self.view.size()))
self._change_count = cc
return self._text
def on_selection_modified_async(self):
self._update()
def on_activated_async(self):
self._update()
def _clear(self):
self.view.erase_regions(self.MATCH_KEY)
self.view.erase_regions(self.MISMATCH_KEY)
def _update(self):
view = self.view
sel = view.sel()
if len(sel) == 0:
self._clear()
return
p = sel[0].b
s = self._buffer()
info = app_delim_info(s, p)
if info is None and p > 0:
info = app_delim_info(s, p - 1)
if info is None:
self._clear()
return
dpos, kind = info
match = app_match(s, dpos, kind)
open_pos = dpos if kind == 'open' else match
close_pos = match if kind == 'open' else dpos
mism = paren_mismatch(s, open_pos, close_pos)
regions = [sublime.Region(*token_region(s, dpos, kind))]
if match is not None:
other_kind = 'close' if kind == 'open' else 'open'
regions.append(sublime.Region(*token_region(s, match, other_kind)))
flags = sublime.DRAW_NO_FILL
if mism:
view.erase_regions(self.MATCH_KEY)
view.add_regions(self.MISMATCH_KEY, regions, 'region.redish', '', flags)
if match is None:
if kind == 'open':
msg = "opening @%s has no matching close" % open_name(s, open_pos)
else:
msg = "closing delimiter has no matching open"
else:
msg = ("closing %s@ does not match opening @%s"
% (close_name(s, close_pos) or '?', open_name(s, open_pos)))
sublime.status_message("Klammertext: " + msg)
else:
view.erase_regions(self.MISMATCH_KEY)
view.add_regions(self.MATCH_KEY, regions, 'region.bluish', '', flags)

View File

@@ -0,0 +1,183 @@
%YAML 1.2
---
# Klammertext.sublime-syntax
#
# Sublime Text syntax highlighting for Klammertext (.kt and .k files).
# A port of the Emacs major mode doc/emacs/klammertext-mode.el.
#
# ---------------------------------------------------------------------------
# What it highlights (mirrors the Emacs mode's eight token classes):
#
# Text removal (#):
# # ... remove to end of line (marker + removed text)
# ## ... remove to end of file (marker + removed text)
# #[ ... ]# remove enclosed text, nestable (markers + removed text)
# #- #+ #/ whitespace operators: NOT removals, left unhighlighted
# (matched only so the '#' above does not eat the line)
#
# Klammer applications (@), definitions (@@), system commands (@@@):
# @name @@name @@@name opening (@ and name are one unit)
# name@ name@@ name@@@ named closing
# @ @@ @@@ bare closing
#
# Escapes: ^@ ^# ^| ^^ the caret makes the next character literal, so it
# is consumed and NOT treated as a delimiter. Left unscoped, to
# match the Emacs mode, which shows escaped characters as ordinary
# text. (A run of carets pairs left-to-right: ^^ is a literal
# caret, a leftover single ^ escapes the following character —
# the '\^.' rule reproduces exactly that parity.)
#
# Literal klammers: @code ... code@ interior is verbatim (no # or @
# interpreted). To add another literal klammer 'foo', copy the
# '@code' rule and the 'literal_code' context below, replacing
# code -> foo.
#
# SYNC: the literal-klammer set is duplicated in three places that
# must agree (a .sublime-syntax file is static and cannot read the
# Emacs defcustom). When you add or remove one, mirror it in all:
# * klammertext-literal-klammers in
# doc/emacs/klammertext-mode.el (the source of truth)
# * LITERAL_KLAMMERS in Klammertext.py
# * the @NAME rule + literal_NAME context here
# All three are currently seeded with just 'code'.
#
# ---------------------------------------------------------------------------
# How open vs. close is decided (the same rule the Emacs scanner uses):
# * a delimiter whose NAME follows the @-run (@name) is an OPENING;
# * a bare @-run, or one whose NAME precedes it (name@), is a CLOSING.
# Because this tokenizer runs left-to-right, an opening consumes "@name" as one
# unit, so a trailing bare @ in the compact form @name@ is naturally a close.
# The (?![A-Za-z0-9_@]) look-ahead on every closing keeps "foo@bar" correct:
# @ is followed by a name, so it opens @bar and 'foo' stays plain text.
#
# ---------------------------------------------------------------------------
# Scope -> color. Colors live in the color scheme, not here. The package ships
# additive .sublime-color-scheme overrides for all five of Sublime's built-in
# schemes (Breakers, Celeste, Mariana, Monokai, Sixteen); each merges onto its
# scheme by filename and recolors only .klammertext scopes. They use one hue
# system — application blue, definition green, system orange, each opening bright
# and its close the same hue darker — shown at full intensity on dark grounds and
# scaled down on light grounds. Exact values are in each override's header.
#
# Without a matching override (e.g. a third-party scheme) a stock scheme still
# gives a meaningful default from these scope names: three klammer-family colors
# (function / storage / keyword), muted removed text (comment), plain escapes.
# To get the full palette on another scheme, copy one of the shipped overrides
# to <SchemeName>.sublime-color-scheme.
#
# ---------------------------------------------------------------------------
# Install: put this file — together with its companions Klammertext.py,
# Default.sublime-keymap and Comments.tmPreferences — in a dedicated package
# folder named 'Klammertext' under Packages/ (Preferences -> Browse Packages
# opens Packages/):
# ~/.config/sublime-text/Packages/Klammertext/ (Linux)
# ~/Library/Application Support/Sublime Text/Packages/Klammertext/ (macOS)
# A dedicated folder (not Packages/User/) keeps the bundled keymap from
# merging into your personal one. Sublime picks it all up live and applies
# the syntax to .kt and .k files. (The syntax file alone also works from
# Packages/User/ if you only want highlighting.)
#
# ---------------------------------------------------------------------------
# Known differences from the Emacs mode (deliberate, matching its own limits):
# * @@ and @@@ definition BODIES are highlighted as ordinary Klammertext,
# not treated specially — same as the Emacs mode.
# * Delimiter MATCHING (jump + live highlight) is not in this syntax file —
# Sublime's built-in bracket matching needs fixed character pairs, which @
# (both open and close, decided by context) cannot provide. It lives in
# the companion Klammertext.py instead: klammertext_jump_to_match (C-c C-j
# equivalent) and a ViewEventListener that highlights the matching
# delimiter as the caret moves (show-paren equivalent), both reusing one
# context-sensitive matcher. This is a plugin concern, not a tokenizer one.
# * Comment toggling is provided by the companion Comments.tmPreferences:
# Ctrl-/ inserts '# ' (line removal), Ctrl-Shift-/ wraps in '#[ ... ]#'
# (block removal).
name: Klammertext
file_extensions:
- kt
- k
scope: text.klammertext
version: 2
variables:
# A klammer name: letters, digits, underscore. A hyphen is NOT a name char
# (the abbreviated form @name-arg1-arg2 ends the name at the first hyphen).
name: '[A-Za-z0-9_]+'
# A closing delimiter must not be followed by a name char (that would be an
# opening @name) or another @ (that would be a longer @-run).
not_delim: '(?![A-Za-z0-9_@])'
contexts:
main:
# --- escapes: ^X makes X literal; consumed so # / @ are not delimiters ---
- match: '\^.'
# --- text removal (#) ---
- match: '##'
scope: punctuation.definition.comment.klammertext
push: removal_file
- match: '#\['
scope: punctuation.definition.comment.klammertext
push: removal_block
# whitespace operators #- #+ #/ (with optional count): not removals.
# Matched (and left unscoped) so the '#' line rule below does not consume
# the rest of the line. Add a scope here if you would rather color them.
- match: '#[-+/]\d*'
- match: '#'
scope: punctuation.definition.comment.klammertext
push: removal_line
# --- literal klammer: interior is verbatim (seeded default: @code) ---
- match: '@code(?![A-Za-z0-9_])'
scope: entity.name.function.begin.klammertext
push: literal_code
# --- system / target commands @@@ ---
- match: '@@@{{name}}'
scope: keyword.control.begin.klammertext # @@@name opening
- match: '@@@{{not_delim}}'
scope: keyword.control.end.klammertext # bare @@@ close
- match: '{{name}}@@@{{not_delim}}'
scope: keyword.control.end.klammertext # name@@@ named close
# --- klammer definitions @@ ---
- match: '@@{{name}}'
scope: storage.type.begin.klammertext # @@name opening
- match: '@@{{not_delim}}'
scope: storage.type.end.klammertext # bare @@ close
- match: '{{name}}@@{{not_delim}}'
scope: storage.type.end.klammertext # name@@ named close
# --- klammer applications @ ---
- match: '@{{name}}'
scope: entity.name.function.begin.klammertext # @name opening
- match: '@{{not_delim}}'
scope: entity.name.function.end.klammertext # bare @ close
- match: '{{name}}@{{not_delim}}'
scope: entity.name.function.end.klammertext # name@ named close
# rest of line is removed
removal_line:
- meta_scope: comment.line.klammertext
- match: '\n'
pop: true
# rest of file is removed (## never closes)
removal_file:
- meta_scope: comment.block.klammertext
# #[ ... ]# removed, nestable
removal_block:
- meta_scope: comment.block.klammertext
- match: '#\['
scope: punctuation.definition.comment.klammertext
push: removal_block
- match: '\]#'
scope: punctuation.definition.comment.klammertext
pop: true
# @code ... code@ — interior verbatim (unscoped), only the close ends it
literal_code:
- match: 'code@'
scope: entity.name.function.end.klammertext
pop: true

View File

@@ -0,0 +1,102 @@
# Klammertext for Sublime Text
A Sublime Text port of the Emacs major mode for Klammertext
(`doc/emacs/klammertext-mode.el`). It brings syntax highlighting, delimiter
matching, and comment toggling to `.kt` and `.k` files. Behavior mirrors the
Emacs mode closely; where the two intentionally differ, the file headers say so.
## Files
| File | Purpose |
|------|---------|
| `Klammertext.sublime-syntax` | Syntax highlighting. Colors the text-removal constructs (`#`, `##`, `#[...]#`) and the three `@`-tiers — application `@`, definition `@@`, system `@@@` — each as an opening vs. a close, plus `^`-escapes and verbatim `@code ... code@` spans. |
| `Klammertext.py` | Plugin with two features that share one context-sensitive matcher: jump between an opening and its close, and live highlighting of the matching delimiter as the caret moves (mismatched or unbalanced delimiters flag in red). |
| `Default.sublime-keymap` | Binds jump-to-match to **Ctrl+M**, scoped to Klammertext files. |
| `Comments.tmPreferences` | Comment toggling: **Ctrl+/** inserts `# ` (line removal), **Ctrl+Shift+/** wraps in `#[ ... ]#` (block removal). |
| `Breakers` / `Celeste` / `Mariana` / `Monokai` / `Sixteen` `.sublime-color-scheme` | Color overrides for Sublime's five built-in schemes — one hue system, full intensity on the dark schemes, scaled down on the light ones. Additive: they recolor only the Klammertext delimiters and leave the rest of each scheme unchanged. |
| `Klammertext_in_Sublime_Text.md` | This file. |
## Installation
Put the files into a folder named `Klammertext` under Sublime's `Packages`
directory:
| Platform | Path |
|----------|------|
| Linux | `~/.config/sublime-text/Packages/Klammertext/` |
| macOS | `~/Library/Application Support/Sublime Text/Packages/Klammertext/` |
| Windows | `%AppData%\Sublime Text\Packages\Klammertext\` |
The quickest way to find it: **Preferences → Browse Packages…** opens the
`Packages` directory. Create the `Klammertext` folder there and copy the files
in. Sublime loads them live — no restart — and applies the syntax to `.kt` and
`.k` files automatically.
Use a dedicated folder (not `Packages/User/`) so the bundled keymap does not
merge into your personal one. If you want highlighting only, the
`.sublime-syntax` file alone works from `Packages/User/`.
The plugin targets **Sublime Text 4**: the live-highlight colors use Sublime's
adaptive `region.*` scopes, which were added in ST4.
## Features and keys
| Trigger | Action |
|---------|--------|
| open a `.kt` / `.k` file | Syntax highlighting (automatic) |
| **Ctrl+M** | Jump between a klammer application's opening and closing `@` (equivalent of the Emacs mode's `C-c C-j`) |
| caret on a klammer `@` | The matching delimiter boxes automatically; a name mismatch or unbalanced delimiter boxes in red with a status-bar message (equivalent of `show-paren-mode`) |
| **Ctrl+/** | Toggle line comment (`#`) |
| **Ctrl+Shift+/** | Toggle block comment (`#[ ... ]#`) |
Ctrl+M is Sublime's own "go to matching bracket" key, reused here because the
built-in cannot match Klammertext's context-dependent `@`. macOS users who
prefer `super+m` can change it in `Default.sublime-keymap`.
## Colors
Colors are installed automatically for all five of Sublime's built-in schemes.
Each `*.sublime-color-scheme` file (Breakers, Celeste, Mariana, Monokai,
Sixteen) is an *additive override*: Sublime merges it onto the matching scheme
by filename, recoloring only the Klammertext delimiters and leaving everything
else untouched. There is nothing to set up.
All five share one hue system — application blue, definition green, system
orange, each opening bright and its close the same hue darker — shown at full
intensity on the dark schemes (Monokai, Mariana) and scaled down for contrast on
the light schemes (Breakers, Celeste, Sixteen). Removed text uses each scheme's
own comment grey.
For any other scheme — a legacy `.tmTheme` such as Solarized, or a third-party
scheme — copy one of the included files to `<Scheme Name>.sublime-color-scheme`
in the package folder (its name is shown at **Preferences → Settings** under
`color_scheme`), choosing a light or dark source file to match the ground. The
exact values are in each file's header comment.
## Keeping literal klammers in sync
Klammers whose content is verbatim (`@code ... code@`) are listed in three
places that must agree — a Sublime syntax/plugin cannot read the Emacs
defcustom, so the list is duplicated:
- `klammertext-literal-klammers` in `doc/emacs/klammertext-mode.el` (the source of truth)
- `LITERAL_KLAMMERS` in `Klammertext.py`
- the `@code` rule and `literal_code` context in `Klammertext.sublime-syntax`
All three are seeded with just `code`. When you add or remove a literal
klammer, change all three.
## Not included
Whole-file semantic validation — persistent error underlines when the cursor is
elsewhere, klammer-name completion, go-to-definition — is not part of this
package. That would need a language server (used through the Sublime LSP
package), a separate program, and is unrelated to the highlighting and matching
provided here.
## Troubleshooting
If the plugin does not seem to load, open **View → Show Console** for any error
message. Check that the files sit directly inside `Packages/Klammertext/` (not
a nested subfolder) and that the current file's syntax reads "Klammertext" in
the status bar at the bottom-right of the window.

View File

@@ -0,0 +1,60 @@
// Klammertext colors for the "Mariana" scheme (dark ground).
// One hue system across all schemes: application = blue, definition =
// green, system = orange; each opens bright and its close is 80%% of the
// open (a klammer "begins bright and gets dark"). Shown at full intensity
// on dark grounds, at 60%% on light grounds for contrast. Delimiters are
// forced to normal style. Merged onto Mariana by filename; recolors only
// .klammertext scopes. (The highlighting was first developed as an Emacs
// major mode; see Klammertext_in_Sublime_Text.md.)
//
// #a6acb9 removed text (Mariana's comment grey)
// #ff6b6b removal markers
// #89ddff @name open blue
// #6eb1cc name@ close darker blue
// #c3e88d @@name open green
// #9cba71 name@@ close darker green
// #ffab70 @@@name open orange
// #cc895a name@@@ close darker orange
{
"name": "Mariana",
"rules": [
{
"scope": "comment.line.klammertext, comment.block.klammertext",
"foreground": "#a6acb9"
},
{
"scope": "punctuation.definition.comment.klammertext",
"foreground": "#ff6b6b"
},
{
"scope": "entity.name.function.begin.klammertext",
"foreground": "#89ddff",
"font_style": ""
},
{
"scope": "entity.name.function.end.klammertext",
"foreground": "#6eb1cc",
"font_style": ""
},
{
"scope": "storage.type.begin.klammertext",
"foreground": "#c3e88d",
"font_style": ""
},
{
"scope": "storage.type.end.klammertext",
"foreground": "#9cba71",
"font_style": ""
},
{
"scope": "keyword.control.begin.klammertext",
"foreground": "#ffab70",
"font_style": ""
},
{
"scope": "keyword.control.end.klammertext",
"foreground": "#cc895a",
"font_style": ""
}
]
}

View File

@@ -0,0 +1,60 @@
// Klammertext colors for the "Monokai" scheme (dark ground).
// One hue system across all schemes: application = blue, definition =
// green, system = orange; each opens bright and its close is 80%% of the
// open (a klammer "begins bright and gets dark"). Shown at full intensity
// on dark grounds, at 60%% on light grounds for contrast. Delimiters are
// forced to normal style. Merged onto Monokai by filename; recolors only
// .klammertext scopes. (The highlighting was first developed as an Emacs
// major mode; see Klammertext_in_Sublime_Text.md.)
//
// #8a8272 removed text (Monokai's comment grey)
// #ff6b6b removal markers
// #89ddff @name open blue
// #6eb1cc name@ close darker blue
// #c3e88d @@name open green
// #9cba71 name@@ close darker green
// #ffab70 @@@name open orange
// #cc895a name@@@ close darker orange
{
"name": "Monokai",
"rules": [
{
"scope": "comment.line.klammertext, comment.block.klammertext",
"foreground": "#8a8272"
},
{
"scope": "punctuation.definition.comment.klammertext",
"foreground": "#ff6b6b"
},
{
"scope": "entity.name.function.begin.klammertext",
"foreground": "#89ddff",
"font_style": ""
},
{
"scope": "entity.name.function.end.klammertext",
"foreground": "#6eb1cc",
"font_style": ""
},
{
"scope": "storage.type.begin.klammertext",
"foreground": "#c3e88d",
"font_style": ""
},
{
"scope": "storage.type.end.klammertext",
"foreground": "#9cba71",
"font_style": ""
},
{
"scope": "keyword.control.begin.klammertext",
"foreground": "#ffab70",
"font_style": ""
},
{
"scope": "keyword.control.end.klammertext",
"foreground": "#cc895a",
"font_style": ""
}
]
}

View File

@@ -0,0 +1,60 @@
// Klammertext colors for the "Sixteen" scheme (light ground).
// One hue system across all schemes: application = blue, definition =
// green, system = orange; each opens bright and its close is 80%% of the
// open (a klammer "begins bright and gets dark"). Shown at full intensity
// on dark grounds, at 60%% on light grounds for contrast. Delimiters are
// forced to normal style. Merged onto Sixteen by filename; recolors only
// .klammertext scopes. (The highlighting was first developed as an Emacs
// major mode; see Klammertext_in_Sublime_Text.md.)
//
// #b8b8b8 removed text (Sixteen's comment grey)
// #994040 removal markers
// #528599 @name open blue
// #426a7a name@ close darker blue
// #758b55 @@name open green
// #5e7044 name@@ close darker green
// #996743 @@@name open orange
// #7a5236 name@@@ close darker orange
{
"name": "Sixteen",
"rules": [
{
"scope": "comment.line.klammertext, comment.block.klammertext",
"foreground": "#b8b8b8"
},
{
"scope": "punctuation.definition.comment.klammertext",
"foreground": "#994040"
},
{
"scope": "entity.name.function.begin.klammertext",
"foreground": "#528599",
"font_style": ""
},
{
"scope": "entity.name.function.end.klammertext",
"foreground": "#426a7a",
"font_style": ""
},
{
"scope": "storage.type.begin.klammertext",
"foreground": "#758b55",
"font_style": ""
},
{
"scope": "storage.type.end.klammertext",
"foreground": "#5e7044",
"font_style": ""
},
{
"scope": "keyword.control.begin.klammertext",
"foreground": "#996743",
"font_style": ""
},
{
"scope": "keyword.control.end.klammertext",
"foreground": "#7a5236",
"font_style": ""
}
]
}

View File

@@ -0,0 +1,45 @@
# This is a line comment removed to end of line, in the "ignored" color.
# The # marker is a different color from the text it removes.
#[ This is a block comment. It can span lines,
and #[ nest ]# like this. ]#
# --- Klammer applications (@) : opening @name vs. closing name@ / bare @ ---
@i italic @ @b bold @ @tt monospace @
@sup 2 | 3 @ # positional arguments separated by |
@sup-2-3 # the abbreviated form colors only the name
@link https://example.com :text a labelled link @
A named close is handy for long arguments: @section a long body here section@
# --- Klammer definitions (@@) and system commands (@@@) ---
@@mdlh : @i Material Definition Language Handbook @ @@
@@heading.html : *arg* @@
@@@target html | HTML output | options @@@
# --- Escapes: a caret makes the next character literal (shown as plain text) ---
^@ and ^# and ^^ and ^| are literal, not delimiters.
# --- Literal klammer: @code ... code@ interior is verbatim ---
# The stray @ and # below are NOT delimiters inside a literal span:
@code
if (a @ b) { return "# not a comment"; }
code@
# --- Whitespace operators (#- #+ #/) are not removals; shown as plain text ---
tight#-spacing gap#+3here break#/2line
# --- A deliberate MISMATCH: put the cursor on @open or close@ to see it turn ---
# --- red with a message (the names disagree); a matched pair boxes normally. ---
@open some content close@
## Everything from this line to the end of the file is removed (## = to EOF).
this trailing line is greyed out as removed text

View File

@@ -0,0 +1,38 @@
# Klammertext via Apple's `container` — macOS (Apple Silicon) shell wrapper
# -----------------------------------------------------------------------------
# Lets you run Klammertext without typing the full `container run ...` command.
# Install: save this file (e.g. ~/klammertext.zsh) and add to your ~/.zshrc:
#
# source ~/klammertext.zsh
#
# Then open a new terminal and use `ktext`, `kdesc`, `kdiag` like normal
# commands. Requires Apple Silicon + macOS 26 or later, with Apple's
# `container` runtime installed and its service started (`container system
# start`). Install `container` from the signed .pkg at
# https://github.com/apple/container/releases (NOT Homebrew). See
# doc/install/macos_container_install.md for the full guide.
# -----------------------------------------------------------------------------
# The published image is multi-arch; on Apple Silicon `container` pulls the
# native arm64 build, so no --platform / --rosetta is needed.
KLAMMERTEXT_IMAGE="${KLAMMERTEXT_IMAGE:-akopra/klammertext:latest}"
_klammertext_run() {
local cmd="$1"; shift
container run --rm \
-v "$PWD:/work" -w /work \
"$KLAMMERTEXT_IMAGE" "$cmd" "$@"
}
# The three Klammertext commands. Files are read from and written to the
# current directory (mounted into the container as /work).
ktext() { _klammertext_run ktext "$@"; }
kdesc() { _klammertext_run kdesc "$@"; }
kdiag() { _klammertext_run kdiag "$@"; }
# Download or update to the latest published image (delete first so the moving
# `latest` tag is definitely refreshed).
klammertext-update() {
container image delete "$KLAMMERTEXT_IMAGE" 2>/dev/null
container image pull "$KLAMMERTEXT_IMAGE"
}

View File

@@ -0,0 +1,133 @@
# Running Klammertext on Linux with Docker
This guide runs Klammertext on a Linux system (Ubuntu or Pop!_OS — the steps are
identical) using the prebuilt Docker container. You do **not** need to install
TeX Live, Python, or any programming tools — everything, including the TeX Live
system that makes PDFs, is packaged inside a single downloadable image. You
install Docker once, then Klammertext works like a normal command.
The published image is multi-arch, so Docker pulls the build matching your CPU
(`amd64` on Intel/AMD, `arm64` on ARM machines) automatically.
For a source build instead (full `@eval` access, no Docker), see
`linux_source_install.md`.
## Step 1 — Install Docker
```bash
sudo apt-get update
sudo apt-get install docker.io
sudo usermod -aG docker $USER
```
Log out and back in for the group change to take effect (so you can run `docker`
without `sudo`). You only do this once.
## Step 2 — Download Klammertext
```bash
docker pull akopra/klammertext:latest
```
This downloads Klammertext and its built-in TeX Live (a few hundred megabytes).
You won't need to do it again unless you're updating.
## Step 3 — Add the Klammertext commands
Add these aliases to `~/.bashrc` (or `~/.zshrc`) so `ktext`, `kdesc`, and
`kdiag` work as ordinary commands that read and write files in whatever folder
you run them from:
```bash
alias ktext='docker run --rm -u $(id -u):$(id -g) -v "$PWD:/work" -w /work akopra/klammertext ktext'
alias kdesc='docker run --rm -v "$PWD:/work" -w /work akopra/klammertext kdesc'
alias kdiag='docker run --rm -v "$PWD:/work" -w /work akopra/klammertext kdiag'
```
The `-u $(id -u):$(id -g)` on `ktext` makes output files owned by you rather than
root. `kdesc` and `kdiag` only read files, so they don't need it. The
`-v "$PWD:/work"` mounts your current directory into the container as `/work`,
which is required for the commands to see your files.
Reload your shell (open a new terminal, or `source ~/.bashrc`).
## Step 4 — Make your first document
In a folder you want to work in, create a test file:
```bash
cat > hello.kt <<'EOF'
@document
:structure article
:title Hello
:text
@s1 Hello, Klammertext @
This document was produced with no TeX Live installed — just Docker and the
Klammertext image.
@
EOF
```
Produce a web page and a PDF:
```bash
ktext hello.kt -t html # makes hello/index.html
ktext hello.kt -t pdf # makes hello.pdf
```
That's it — you're running Klammertext.
## Updating
To update to the latest published image:
```bash
docker pull akopra/klammertext:latest
```
## Haskell support (`@eval :haskell`)
The standard image does not include Haskell. For `@eval :haskell`, pull the
Haskell image and use it in place of the standard one:
```bash
docker pull akopra/klammertext:haskell
alias ktext='docker run --rm -u $(id -u):$(id -g) -v "$PWD:/work" -w /work akopra/klammertext:haskell ktext'
```
Test:
```bash
ktext -s '@eval :haskell main = putStrLn "hello" @' -d
```
Alternatively, a source install gives all `@eval` modes without a separate image
(see `linux_source_install.md`).
## Klammer set loading
The Standard Klammer Set is loaded by default. To load a different klammer set,
pass `-k PATH` (the klammer set's `.k` file). To run with only the three
primitive klammers (`@read`, `@eval`, `@cond`), use `-k none`.
## If something goes wrong
- **`Cannot connect to the Docker daemon`** — the Docker service isn't running:
`sudo systemctl start docker`, then retry.
- **`permission denied` running `docker`** — your user isn't in the `docker`
group yet: `sudo usermod -aG docker $USER`, then log out and back in.
- **`No such file or directory` for your input** — the file must be in the
directory you run the command from (that's what gets mounted). `cd` into the
folder with your `.kt` files first.
- **Output files owned by root** — add `-u $(id -u):$(id -g)` to the `ktext`
command/alias (as shown in Step 3).
## Freeing disk space
To remove the image (you can re-pull it later):
```bash
docker rmi akopra/klammertext:latest
docker system prune # optional: remove all unused Docker data
```

View File

@@ -0,0 +1,277 @@
# Klammertext source installation on Linux (Ubuntu / Pop!_OS)
This document describes how to build and install Klammertext from source on a
Linux system — Ubuntu or Pop!_OS; the steps are identical — without using the
container. A source installation gives full access to all `@eval` modes,
including `:haskell` and `:shell` commands that depend on locally installed
software.
For the container installation on Linux, see `linux_container_install.md`.
## Prerequisites
The following packages are required to build Klammertext:
```bash
sudo apt-get update
sudo apt-get install g++ make python3-dev
```
The C++ compiler must support C++20. GCC 11 or later is required (Ubuntu 22.04
and later include GCC 12+).
The SKS `@image` klammer requires OpenImageIO Python bindings. These must match
the Python version that ktext is built against (check with
`python3.XX -c "import OpenImageIO"`). For example, if ktext links against
Python 3.12:
```bash
pip3.12 install OpenImageIO
```
Verify:
```bash
g++ --version
```
## Clone the repository
```bash
git clone https://git.andykopra.com/ack/klammertext.git
cd klammertext
```
## Environment variables
Klammertext's runtime environment is provided by a single self-configuring
file. Source it from your shell profile (e.g., `~/.bashrc` or `~/.zshrc`):
```bash
source /path/to/klammertext/mac/env/runtime.env
```
It self-locates `KLAMMERTEXT_HOME` from its own path, adds `bin/` and
`tst/` to `PATH` (plus the newest `~/external/texlive/<year>/bin/<arch>` if a
TeX Live is installed there), sets `LD_LIBRARY_PATH` so `libklammertext.so` is
found, and sets the LSan suppressions. There is no per-host or per-OS variable
to set. For a TeX Live or library in a non-standard location, add it to an
optional, gitignored `mac/env/runtime.env.local` (sourced at the end).
After editing your shell profile, reload it:
```bash
source ~/.bashrc
```
## Configure the build
No build configuration is needed. The single `mac/env/makefile.env` is
cross-platform: it reads `KLAMMERTEXT_HOME` from the environment (set by
`runtime.env` above), auto-detects the platform with `uname`, and auto-detects
Python with `python3-config` — no hardcoded version and no per-host file to
edit. Verify the Python development headers are present:
```bash
python3-config --includes # prints -I.../python3.XX for your Python
```
If `python3-config` is missing, install your distribution's `python3-dev`
(Debian/Ubuntu) or `python3-devel` (Fedora/RHEL) package.
## Build
Build the shared library, the SKS components, and the three commands with a
single command: `make -C com` builds its prerequisites in `mac/` and `sks/`
first, then the commands. `OPTIMIZE=1` selects an optimized `-O3` build (what
you want to install and run); without it you get a slower `-O0` debug build
with AddressSanitizer, intended for development:
```bash
make -C com -j OPTIMIZE=1 # lib/libklammertext.so + sks/*.so + bin/{ktext,kdesc,kdiag}
```
Verify the build:
```bash
ktext -s '@eval 1 + 1 @' -d
```
This should print `2`. For a quick document smoke test, create a small file and
render it to HTML:
```bash
cat > hello.kt <<'EOF'
@document
:structure article
:title Hello
:text
@s1 Hello, Klammertext @
This document was built from source.
@
EOF
ktext hello.kt -t html # writes hello/index.html
```
## TeX Live (for PDF output)
The Standard Klammer Set uses **XeLaTeX** for the `pdf` target. Build a complete
Klammertext TeX Live tree with the bundled script, giving it a destination
directory under `~/external/texlive/<year>` — the location `runtime.env`
auto-detects. The script needs `perl`, `xz-utils`, `fontconfig`, and either
`wget` or `curl`:
```bash
sudo apt-get install perl wget xz-utils fontconfig
bash $KLAMMERTEXT_HOME/doc/install/texlive_additional_packages.sh ~/external/texlive/2026
```
This installs `scheme-small` plus the additional packages the SKS needs and
rebuilds all formats, fetching the binaries for your architecture. It writes a
`KLAMMERTEXT_BUILD_INFO.txt` provenance file (mirror, release, package list,
date) into the tree.
Because the tree lives under `~/external/texlive/2026`, `runtime.env` finds it
automatically — open a new shell (or re-source `runtime.env`) and `xelatex`
will be on `PATH`. No manual `KLAMMERTEXT_TEXLIVE_BIN` is needed.
If you would rather reuse a TeX Live you already have, point `runtime.env` at it
from the gitignored escape hatch instead, and install the SKS's extra packages
into it yourself (the package list is in `doc/install/texlive_additional_packages.sh`):
```bash
cat >> "$KLAMMERTEXT_HOME/mac/env/runtime.env.local" <<'EOF'
export KLAMMERTEXT_TEXLIVE_BIN=/path/to/texlive/bin/x86_64-linux
export PATH="$KLAMMERTEXT_TEXLIVE_BIN:$PATH"
EOF
```
Verify and test (reusing the `hello.kt` from the Build section):
```bash
xelatex --version
ktext hello.kt -t pdf # writes hello.pdf
```
## Optional: Haskell (for @eval :haskell)
The `@eval :haskell` mode requires `runghc`, which is part of the Haskell
toolchain. Alternatively, the `akopra/klammertext:haskell` container image
includes GHC (see `linux_container_install.md`).
The recommended way to install Haskell on Ubuntu is via ghcup:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
```
Follow the prompts to install GHC, cabal, and related tools. After
installation, ensure the ghcup bin directory is in your `PATH`:
```bash
export PATH=$HOME/.ghcup/bin:$PATH
```
Verify:
```bash
runghc --version
```
Test in Klammertext:
```bash
ktext -s '@eval :haskell main = putStr "Hello from Haskell" @' -d
```
## Directory layout after build
```
klammertext/
├── bin/ ktext, kdesc, kdiag executables (after build)
├── lib/ libklammertext.so shared library (after build)
├── mac/ Klammermachine C++ source
├── sks/ Standard Klammer Set (.k files and .so modules)
│ ├── document/ document.so
│ ├── kutil/ kutil.o
│ └── target/ html_util.o, latex_util.o
├── com/ command source (ktext, kdesc, kdiag) and Makefile
├── doc/ installation guides (doc/install) and editor support (doc/edit)
└── tst/ test suites
```
## Verifying the installation
Run the following commands to verify that everything works:
```bash
# Basic evaluation (Python)
ktext -s '@eval 1 + 1 @' -d
# Shell evaluation
ktext -s '@eval :shell date @' -d
# Show machine state (SKS is loaded by default)
ktext -s '' -m
# HTML and PDF output (uses the hello.kt from the Build section)
ktext hello.kt -t html
ktext hello.kt -t pdf # requires TeX Live
# Haskell evaluation (requires ghcup)
ktext -s '@eval :haskell main = putStr "42" @' -d
# Run unit tests
make -C $KLAMMERTEXT_HOME/tst test
```
## Updating
To update an existing source installation to the latest version:
```bash
cd $KLAMMERTEXT_HOME
git pull
make -C com -j OPTIMIZE=1 # rebuild library, SKS components, and commands
```
The TeX Live tree only needs rebuilding if the SKS's package requirements
changed (rare); when they do, re-run the script from the TeX Live section
above.
## Troubleshooting
**"libklammertext.so: cannot open shared object file"**
Ensure `LD_LIBRARY_PATH` includes `$KLAMMERTEXT_HOME/lib`:
```bash
export LD_LIBRARY_PATH=$KLAMMERTEXT_HOME/lib:$LD_LIBRARY_PATH
```
**"KLAMMERTEXT_HOME is not set"**
Set the environment variable as described in the Environment variables
section above.
**"python3.XX/Python.h: No such file or directory"**
Install the Python development headers:
```bash
sudo apt-get install python3-dev
```
**"xelatex: command not found" (when using -t pdf)**
Install TeX Live and ensure its bin directory is in `PATH`.
**"@eval :haskell requires runghc"**
Install Haskell via ghcup as described in the Optional: Haskell section.

View File

@@ -0,0 +1,161 @@
# Running Klammertext on a Mac (Apple Silicon)
This guide gets Klammertext running on your Mac in a few minutes. You do
**not** need to install TeX Live, Python, or any programming tools —
everything, including the TeX Live system that makes PDFs, is packaged inside
a single downloadable image. You install Apple's `container` runtime once, and
then Klammertext works like a normal command.
This guide is for **Apple Silicon Macs (M1/M2/M3/M4/M5) running macOS 26 or
later**, which is what Apple's `container` runtime requires. (Support for older
Intel Macs can be provided separately if needed.)
## Step 1 — Install Apple's `container` runtime
`container` is Apple's own tool for running Linux container images natively on
Apple Silicon. It's free.
1. Go to <https://github.com/apple/container/releases> and download the latest
installer package (the `.pkg` file). **Do not** use Homebrew — the Homebrew
`container` formula is a different, unrelated tool.
2. Double-click the downloaded `.pkg` and follow the installer.
3. Open the **Terminal** app (Applications → Utilities → Terminal) and start the
`container` background service (accept the recommended default if prompted):
```sh
container system start
```
You only do this once. You can check the service any time with
`container system status`.
## Step 2 — Download Klammertext
In Terminal, paste this and press Return:
```sh
container image pull akopra/klammertext:latest
```
This downloads Klammertext and its built-in TeX Live. It's a few hundred
megabytes, so it takes a minute the first time. Because the image is multi-arch,
`container` fetches the native Apple Silicon (arm64) build. You won't need to do
this again unless you're updating.
## Step 3 — Add the Klammertext commands
This step makes `ktext` (and its helpers) available as ordinary commands.
1. In Terminal, create the wrapper file by pasting this whole block and
pressing Return:
```sh
cat > ~/klammertext.zsh <<'EOF'
# Klammertext via Apple's `container` runtime (native arm64 image; no Rosetta).
KLAMMERTEXT_IMAGE="${KLAMMERTEXT_IMAGE:-akopra/klammertext:latest}"
_klammertext_run() {
local cmd="$1"; shift
container run --rm \
-v "$PWD:/work" -w /work \
"$KLAMMERTEXT_IMAGE" "$cmd" "$@"
}
ktext() { _klammertext_run ktext "$@"; }
kdesc() { _klammertext_run kdesc "$@"; }
kdiag() { _klammertext_run kdiag "$@"; }
klammertext-update() {
container image delete "$KLAMMERTEXT_IMAGE" 2>/dev/null
container image pull "$KLAMMERTEXT_IMAGE"
}
EOF
```
2. Tell your shell to load it, by pasting this and pressing Return:
```sh
echo 'source ~/klammertext.zsh' >> ~/.zshrc
```
3. **Close Terminal and open a new window** so the change takes effect.
You now have three commands — `ktext`, `kdesc`, `kdiag` — that run Klammertext
inside a container while reading and writing files in whatever folder you're
working in.
## Step 4 — Make your first document
In Terminal, go to a folder you want to work in (for example your Desktop) and
create a test file:
```sh
cd ~/Desktop
cat > hello.kt <<'EOF'
@document
:structure article
:title Hello
:text
@s1 Hello, Klammertext @
This document was produced on macOS with no TeX Live installed —
just Apple's `container` runtime and the Klammertext image.
@
EOF
```
Now produce a web page and a PDF from it:
```sh
ktext hello.kt -t html # makes hello/index.html
ktext hello.kt -t pdf # makes hello.pdf
```
Open the results:
```sh
open hello.pdf
open hello/index.html
```
That's it — you're running Klammertext.
## Good to know
- **Work inside one folder.** Klammertext can only see files in (or below) the
folder you run the command from. Keep a document and the files it uses
together, and run `ktext` from that folder.
- **Runs natively.** On Apple Silicon, `container` runs the native arm64 image
with no Rosetta translation.
- **Fonts.** The default fonts (Crimson Pro, Open Sans, Inconsolata) are built
in, so PDFs work with no internet connection. If you ask for a different font
by name, Klammertext downloads it from Google Fonts the first time, which
needs an internet connection.
- **Updating later.** When a new version is announced, run `klammertext-update`
in Terminal.
- **If you also build Klammertext from source on this Mac.** Most people don't —
the whole point of the container is that you don't need a source build. But if
this machine *also* has a native source build on its `PATH` (so `which ktext`
shows a path like `.../K/com/ktext`), the wrapper's `ktext` function would
shadow that native command. To keep both, give the container wrappers their
own names by using `ktextc` / `kdescc` / `kdiagc` (trailing `c` = container)
in place of `ktext` / `kdesc` / `kdiag` in the Step 3 file. Then plain `ktext`
still runs your source build and `ktextc` runs the container.
- **Quitting.** Klammertext only runs while you're using it; there's nothing
left running afterward. If you want to stop the `container` service entirely,
run `container system stop`; start it again with `container system start` next
time.
## If something goes wrong
- **`command not found: ktext`** — you didn't open a new Terminal window after
Step 3, or the `source` line didn't get added. Re-run the Step 3 commands and
open a fresh Terminal.
- **`container: command not found`** — the `container` runtime isn't installed
(Step 1), or the Terminal window predates the install (open a new one).
- **A command hangs or won't connect** — the `container` service isn't running.
Run `container system start` (check with `container system status`), then try
again.
- **A run aborted and now seems stuck** — `container run --rm` can leave the
container behind after an error. Clear leftovers with:
```sh
for id in $(container list -a -q); do container kill "$id"; container delete "$id"; done
```

View File

@@ -0,0 +1,171 @@
# Klammertext source installation on macOS (Apple Silicon)
Companion to `linux_source_install.md`. Verified on an Apple-Silicon Mac
(arm64, macOS 26 "Tahoe"). Klammertext's core (engine, SKS, HTML/LaTeX,
`@image`) builds and runs natively with Apple Clang; the cross-platform build
environment (`mac/env/makefile.env`) auto-detects the OS via `uname`.
## 1. Toolchain prerequisites
```sh
xcode-select --install # Command Line Tools (clang, headers) — if not already present
```
Then install Python with a linkable `libpython` and `python3-config`, using
whichever package manager you have — **Homebrew** or **MacPorts**. Both work;
only the install prefix differs, and the build auto-detects it.
```sh
# Homebrew (https://brew.sh):
brew install python
brew install gcc # OPTIONAL: a second compiler for a standards check
# MacPorts (https://www.macports.org) — provisional, pending testing on a
# MacPorts system:
sudo port install python312
sudo port select --set python3 python312 # so python3 / python3-config resolve
sudo port install gcc14 # OPTIONAL: a second compiler for a standards check
```
Notes:
- The build embeds Python, which needs `python3-config` and a linkable
`libpython`. Apple's `/usr/bin/python3` does **not** ship a usable
`python3-config` and Apple discourages linking it — so a package-manager
Python (Homebrew or MacPorts) is required. It coexists with Apple's;
`python3-config` resolves to it when the manager's `bin` is early on `PATH`
(`/opt/homebrew/bin` for Homebrew, `/opt/local/bin` for MacPorts — both set up
by their installers). For MacPorts, `port select --set python3 python312`
makes `python3` and `python3-config` resolve.
- `makefile.env` gets all Python include/link flags from `python3-config` and
auto-detects the package-manager prefix (`/opt/homebrew` or `/opt/local`, via
`MACOS_PREFIX`), so either manager works without edits. Override with
`make MACOS_PREFIX=...` if yours is installed elsewhere.
## 2. Image support (the `@image` klammer): OpenImageIO
```sh
# Homebrew:
/opt/homebrew/bin/pip3.<N> install --break-system-packages OpenImageIO
# e.g. pip3.14 — match your Python's version
# MacPorts (matches the python312 installed above):
sudo port install py312-openimageio
```
This provides the OpenImageIO Python bindings the embedded interpreter uses.
With pip, the self-contained PyPI wheel goes into that Python's site-packages;
`--break-system-packages` is needed because the Python is PEP-668 "externally
managed". (Homebrew alternative: `brew install openimageio`, heavier — it pulls
ffmpeg/openexr/etc.)
## 3. Clone and configure the environment
```sh
git clone https://git.andykopra.com/ack/klammertext.git ~/projects/klammertext
# Set up the runtime environment (KLAMMERTEXT_HOME, PATH); add to ~/.zprofile:
echo 'source "$HOME/projects/klammertext/mac/env/runtime.env"' >> ~/.zprofile
```
The single self-configuring `runtime.env` self-locates `KLAMMERTEXT_HOME` from
its own path. On macOS it sets no `LD_LIBRARY_PATH`/`DYLD_LIBRARY_PATH`
(`libklammertext.so` is found via the binaries' `@loader_path` rpath, and
`document.so` is dlopen'd by absolute path under `$KLAMMERTEXT_HOME`) and no
`LSAN_OPTIONS` (LeakSanitizer is unsupported on macOS).
## 4. Build (Apple Clang)
Clang is the compiler you run on macOS, and it is the **default** here
(`makefile.env` selects clang on Darwin), so no `COMPILER=` flag is needed.
A single `make -C com` builds its prerequisites in `mac/` and `sks/` first,
then the commands:
```sh
cd "$KLAMMERTEXT_HOME"
make -C com -j OPTIMIZE=1 # libklammertext.so + sks/*.so + bin/{ktext,kdesc,kdiag}
ktext -s '@eval 1 + 1 @' -d # smoke test — prints 2
```
For a document smoke test, create a small file and render it to HTML:
```sh
cat > hello.kt <<'EOF'
@document
:structure article
:title Hello
:text
@s1 Hello, Klammertext @
This document was built from source.
@
EOF
ktext hello.kt -t html # writes hello/index.html
```
(A PDF render needs TeX Live — see section 5.)
## 5. PDF target: TeX Live
Build a complete Klammertext TeX Live tree with the bundled script, into
`~/external/texlive/<year>` — the location `runtime.env` auto-detects
(`bin/universal-darwin`). macOS already has `curl`, `perl`, and `tar`, and
`install-tl` self-provides `xz`, so nothing extra is needed:
```sh
bash "$KLAMMERTEXT_HOME/doc/install/texlive_additional_packages.sh" ~/external/texlive/2026
```
This installs `scheme-small` plus the SKS's additional packages and rebuilds all
formats, fetching the `universal-darwin` binaries, and writes a
`KLAMMERTEXT_BUILD_INFO.txt` provenance file into the tree. **This is the same
command used on Linux**, so the TeX Live layout is identical across your
machines. `runtime.env` then finds the tree automatically — open a new shell (or
re-source it) and `xelatex` is on `PATH`; no manual `KLAMMERTEXT_TEXLIVE_BIN` is
needed.
If you already run BasicTeX/MacTeX and prefer to reuse it, point `runtime.env`
at its bin directory from the gitignored escape hatch instead (and install the
SKS's extra packages into it yourself — the list is in the script). Set the
variable **and** prepend it to `PATH`, since `runtime.env.local` is sourced
after the main `PATH` is built:
```sh
cat >> "$KLAMMERTEXT_HOME/mac/env/runtime.env.local" <<'EOF'
export KLAMMERTEXT_TEXLIVE_BIN=/usr/local/texlive/2025basic/bin/universal-darwin
export PATH="$KLAMMERTEXT_TEXLIVE_BIN:$PATH"
EOF
```
Verify and test (reusing `hello.kt` from section 4):
```sh
xelatex --version
ktext hello.kt -t pdf # writes hello.pdf
```
## 6. Updating
To update an existing source installation to the latest version:
```sh
cd "$KLAMMERTEXT_HOME"
git pull
make -C com -j OPTIMIZE=1 # rebuild library, SKS components, and commands
```
Rebuild the TeX Live tree only if the SKS's package requirements changed (rare);
re-run the script from section 5.
## Compiler notes (macOS)
- **Clang is the compiler you run, and the default here.** Apple Clang builds
run correctly; `makefile.env` selects clang on Darwin automatically.
- **Do not run gcc-built binaries on macOS.** GCC (Homebrew `g++-NN` or MacPorts
`g++-mp-NN`) is useful only as an optional compile-time standards check
(`make -C com COMPILER=gcc`); the resulting binaries **crash at runtime** on
macOS because of a gcc/macOS codegen issue (for example `std::source_location`
returning a bad pointer, so `Machine::Machine()` walks into `strlen` and
SIGSEGVs). Always run the clang-built binary. (gcc-built binaries run fine on
Linux.)
- **Switching compilers requires a full clean** — `g++` and `clang++` objects
must not be mixed (ABI). `make -C com redo` does a full clean rebuild across
`mac`, `sks`, and `com`; a partial `make -C mac clean` does not.

View File

@@ -0,0 +1,117 @@
#!/bin/bash
# Build a self-contained TeX Live tree for the Klammertext SKS "tex"/"pdf"
# targets: scheme-small plus the additional packages the SKS requires, with
# all formats rebuilt. This is the single source of truth for constructing a
# Klammertext TeX Live directory — used both by the Docker build (Dockerfile,
# per-arch) and for native installs.
#
# Usage:
# texlive_additional_packages.sh <texdir> [mirror]
#
# <texdir> Destination directory for the TeX Live tree (created by
# install-tl), e.g. /opt/texlive or ~/external/texlive/2026.
# Should not already exist.
# [mirror] A CONCRETE tlnet mirror URL. Must NOT be the mirror.ctan.org
# redirect: it resolves to a different mirror (possibly a different
# TeX Live revision) on each call, which makes tlmgr abort partway
# with "tlmgr itself needs to be updated". Defaults to a pinned
# CTAN mirror. Once the current TeX Live year is frozen (the next
# release ships), point this at the historic tlnet-final snapshot
# for exact reproducibility, e.g.
# https://ftp.math.utah.edu/pub/texlive/historic/2026/tlnet-final
#
# install-tl fetches the binaries for the ARCHITECTURE it runs on, so running
# this under arm64 produces an arm64 tree and under x86_64 an x86_64 tree.
#
# Prerequisites on the host: perl, tar, gzip, xz (xz-utils), and either wget or
# curl (macOS ships curl, not wget). fontconfig is recommended so fmtutil can
# build all formats cleanly.
set -eux
TEXDIR="${1:?usage: $0 <texdir> [mirror]}"
MIRROR="${2:-https://ctan.math.illinois.edu/systems/texlive/tlnet}"
# --- Fetch the installer into a scratch dir --------------------------------
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
cd "$WORK"
# Fetch the installer with whichever downloader is present (macOS has curl, not
# wget; Debian build images have wget).
if command -v wget >/dev/null 2>&1; then
wget -q "$MIRROR/install-tl-unx.tar.gz"
else
curl -fsSL -O "$MIRROR/install-tl-unx.tar.gz"
fi
tar --strip-components=1 -xzf install-tl-unx.tar.gz
# --- Base install: scheme-small into $TEXDIR -------------------------------
cat > texlive.profile <<PROFILE
selected_scheme scheme-small
TEXDIR $TEXDIR
TEXMFLOCAL $TEXDIR/texmf-local
TEXMFSYSCONFIG $TEXDIR/texmf-config
TEXMFSYSVAR $TEXDIR/texmf-var
tlpdbopt_install_docfiles 0
tlpdbopt_install_srcfiles 0
tlpdbopt_autobackup 0
PROFILE
# install-tl's in-line fmtutil can exit 3 before all packages are present;
# that is non-fatal (install-tl continues) and the formats are rebuilt below.
./install-tl --profile=texlive.profile --repository="$MIRROR"
# --- Use the tlmgr from the tree we just built (absolute path), not whatever
# tlmgr may be on PATH. Pin its repo to the same concrete mirror and sync
# it to that repo's revision before installing more packages. -----------
ARCH="$(ls "$TEXDIR/bin")"
TLMGR="$TEXDIR/bin/$ARCH/tlmgr"
"$TLMGR" option repository "$MIRROR"
"$TLMGR" update --self
# --- Additional packages required by the SKS beyond scheme-small -----------
# Single list, used both for the install and for the provenance README below,
# so the two cannot drift apart.
PACKAGES="adjustbox collectbox collection-fontsrecommended enumitem fontaxes \
footmisc inconsolata layouts mdframed multirow needspace opensans pict2e \
textpos titlesec upquote zref"
# shellcheck disable=SC2086 # intentional word splitting into separate args
"$TLMGR" install $PACKAGES
# --- Rebuild every format now that the full package set is installed -------
"$TEXDIR/bin/$ARCH/fmtutil-sys" --all
# --- Provenance: record how this tree was constructed ----------------------
# Written into the tree itself so it is self-documenting when found later.
RELEASE="$(head -n1 "$TEXDIR/release-texlive.txt" 2>/dev/null || echo unknown)"
BUILT="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
cat > "$TEXDIR/KLAMMERTEXT_BUILD_INFO.txt" <<INFO
Klammertext TeX Live tree
=========================
Built for the Klammertext Standard Klammer Set (SKS) "tex"/"pdf" targets by
doc/install/texlive_additional_packages.sh.
Built: $BUILT
Mirror: $MIRROR
TeX Live: $RELEASE
Architecture: bin/$ARCH
Base scheme: scheme-small
Docfiles/srcfiles omitted; all formats rebuilt with fmtutil-sys --all.
Additional packages installed beyond scheme-small:
$(printf ' %s\n' $PACKAGES)
Reconstruct an equivalent tree with:
texlive_additional_packages.sh <texdir> $MIRROR
Note: the mirror above serves the CURRENT TeX Live release, which receives
package updates within its year, so a rebuild is not guaranteed byte-identical.
For exact reproducibility, rebuild from the frozen historic tlnet-final
snapshot once the release year is no longer current, e.g.
https://ftp.math.utah.edu/pub/texlive/historic/<year>/tlnet-final
INFO
echo "Klammertext TeX Live tree built in $TEXDIR (binaries in bin/$ARCH)"
echo "Provenance written to $TEXDIR/KLAMMERTEXT_BUILD_INFO.txt"

3
lib/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!.gitignore
!README.md

5
lib/README.md Normal file
View File

@@ -0,0 +1,5 @@
# lib
This directory holds the compiled Klammermachine shared library
(`libklammertext.so`) after you build Klammertext with `make -C com`.
It is empty in the repository.

1
mac/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.d

67
mac/Makefile Normal file
View File

@@ -0,0 +1,67 @@
# Klammertext mac/ Makefile
# Improved version with automatic header dependency tracking
K := $(KLAMMERTEXT_HOME)
KS := $(K)/sks
include $(K)/mac/env/makefile.env
# Source files
BASENAMES := util error locator file argv character ktype katom katom_list \
log show command argument argument_set argtype argtype_set \
state eval eval_python eval_cpp klammer klammer_set deftype \
target target_set machine
SOURCES := $(addsuffix .cpp,$(BASENAMES))
OBJECTS := $(addsuffix .o,$(BASENAMES))
HEADERS := $(addsuffix .h,$(BASENAMES))
DEPFILES := $(addsuffix .d,$(BASENAMES))
# Shared library
LIBDIR := ../lib
LIBRARY := $(LIBDIR)/libklammertext.so
# Compiler flags for dependency generation
DEPFLAGS = -MMD -MP -MF $(@:.o=.d)
# Pattern rule for object files with automatic dependency generation
%.o : %.cpp
$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $< -o $@
# Default target
.PHONY: all sks clean redo clang
all : $(LIBRARY)
$(MAKE) sks
# Create lib directory
$(LIBDIR):
mkdir -p $(LIBDIR)
# Build shared library
$(LIBRARY): $(OBJECTS) | $(LIBDIR)
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(SHARED) $(SONAME) -o $@ $(OBJECTS) $(LDLIBS)
# Build sks components
sks :
$(MAKE) -C $(KS)/kutil
$(MAKE) -C $(KS)/target
$(MAKE) -C $(KS)/document
clean :
rm -f $(OBJECTS) $(DEPFILES) $(LIBRARY) *~
redo :
ifneq ($(filter clang,$(MAKECMDGOALS)),)
@:
else
$(MAKE) clean
$(MAKE) -j all
endif
# "make clang" = incremental build; "make clang redo" = full clean rebuild
clang :
$(MAKE) $(or $(filter-out clang,$(MAKECMDGOALS)),all) COMPILER=clang
# Include generated dependency files (if they exist)
-include $(DEPFILES)

84
mac/argtype.cpp Normal file
View File

@@ -0,0 +1,84 @@
#include <set>
#include <regex>
#include <sstream>
#include "argtype.h"
#include "error.h"
#include "util.h"
Argtype::Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
std::string python_cast, modify_string_f python_format,
const Locator& loc)
: m_name(name)
, m_desc(desc)
, m_symbolic_pattern(symbolic_pattern)
, m_pattern(pattern)
, m_python_cast(python_cast)
, m_python_format(python_format)
, m_regex(std::regex(pattern))
, m_loc(loc)
{
}
std::string pyformat_string(const strings_t& v)
{
std::string p = v[0];
std::string delim = "\"";
if (contains(p, "\"\"\"")) {
delim = "'''";
} else if (contains(p, "'''")) {
delim = "\"\"\"";
} else if (contains(p, "\"")) {
delim = "'";
}
return delim + p + delim;
}
std::string pyformat_bool(const strings_t& v)
{
std::string value = v[0];
std::set<std::string> true_values { "1", "true", "True", "yes" };
std::set<std::string> false_values { "0", "false", "False", "no" };
if (true_values.contains(value)) {
return "True";
} else if (false_values.contains(value)) {
return "False";
} else {
throw Argument_error("The argument\"" + value + "\" is not a Boolean value");
}
}
std::string pyformat_number(const strings_t& value)
{
return value[0];
}
std::string pylist(strings_t words)
{
std::transform(words.begin(), words.end(), words.begin(),
[](const std::string& s) { return pyformat_string({s}); });
return "[" + join(words, ", ") + "]";
}
std::string pyformat_list(const strings_t& value)
{
return pylist(value);
}
std::string pyformat_dlist(const strings_t& value)
{
return pylist(value);
}
std::string Argtype::python_value(const std::string& var_name, std::vector<std::string> value, size_t name_size)
{
std::stringstream ss {};
ss << " " << std::left << std::setw(name_size) << var_name << " = ";
if (m_python_format) {
ss << m_python_format(value);
} else {
ss << m_python_cast << "(" << pyformat_string(value) << ")";
}
return ss.str();
}

51
mac/argtype.h Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
#include <string>
#include <variant>
#include <functional>
#include <regex>
#include "locator.h"
using argtype_t = std::variant<bool,double,std::string,std::vector<std::string>>;
using modify_string_f = std::function<std::string(std::vector<std::string>)>;
class Argtype
{
public:
Argtype()
: m_name("default")
, m_desc("default argument type")
, m_symbolic_pattern(".+")
, m_pattern(".+")
, m_python_cast("str")
, m_python_format()
, m_loc()
{};
Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern,
std::string python_cast, modify_string_f python_format,
const Locator& loc);
std::string python_value(const std::string& var_name, std::vector<std::string> value, size_t name_size);
std::string m_name {};
std::string m_desc {};
std::string m_symbolic_pattern {};
std::string m_pattern {};
std::string m_python_cast {};
modify_string_f m_python_format {};
std::regex m_regex {};
int m_count {1};
int m_mincount {1};
int m_maxcount {1};
Locator m_loc;
};
std::string pyformat_string(const std::vector<std::string>& value);
std::string pyformat_bool(const std::vector<std::string>& value);
std::string pyformat_number(const std::vector<std::string>& value);
std::string pyformat_list(const std::vector<std::string>& value);
std::string pyformat_dlist(const std::vector<std::string>& value);

155
mac/argtype_set.cpp Normal file
View File

@@ -0,0 +1,155 @@
#include <regex>
#include <sstream>
#include <algorithm>
#include <numeric>
#include "argtype_set.h"
#include "error.h"
#include "show.h"
#include "log.h"
#include "util.h"
#include "character.h"
#include "katom.h"
Parameter_set& Argtype_set::parameters()
{
static Parameter_set instance("name | desc :pattern .* :python_cast str");
return instance;
}
Argtype_set::Argtype_set()
{
(void)(void)K::log(2);
Locator loc = current_locator();
for (auto [name, desc, pattern, python_cast, python_format] : base_argtypes) {
add(name, desc, pattern, python_cast, python_format, loc);
}
}
std::string Argtype_set::replace_symbols(const std::string& pattern, const Locator& loc)
{
std::smatch match {};
std::regex symbol_pat(R"('(\w+)')");
std::string expanded { pattern };
for (const std::string& symbol : find_all(pattern, symbol_pat, 0)) {
std::string name { symbol.begin()+1, symbol.end()-1 };
if (m_types.find(name) != m_types.end()) {
expanded = string_replace(
expanded, symbol, R"((?:)" + m_types[name].m_pattern + R"())");
} else {
std::stringstream ss;
ss << "Argtype symbol " << symbol << " not defined.\n\n"
<< "Defined argtypes:\n";
ss << describe();
throw Definition_error(ss.str(), loc, false);
}
}
return expanded;
}
void Argtype_set::check_for_existing_definition(
const std::string& name, const Locator& loc)
{
if (count(m_names.begin(), m_names.end(), name) > 0) {
std::stringstream ss {};
ss << "Argument type '" << name << "' is already defined at "
<< m_types[name].m_loc;
throw Definition_error(ss.str(), loc);
}
}
void Argtype_set::add(const std::string& name, const std::string& desc,
const std::string& pattern,
const std::string& python_cast, modify_string_f python_format,
const Locator& loc)
{
// (void)K::log(3, name, ":", abbrev(string_replace(desc, "\n", "/"), 50), pattern);
check_for_existing_definition(name, loc);
std::string expanded_pattern = replace_symbols(pattern, loc);
m_name_size = std::max(m_name_size, name.size()); // For display
m_pattern_size = std::max(m_pattern_size, expanded_pattern.size());
m_types[name] = Argtype(name, desc, pattern, expanded_pattern, python_cast, python_format, loc);
m_names.push_back(name);
}
void Argtype_set::add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms)
{
(void)K::log(3);
//Argument_set parameters("name | desc :pattern .* :python_cast str");
auto [positional, optional, rest] =
argument_split(begin + 1, end); //, Argtype_set::parameters.m_positional.size());
check_for_existing_definition(positional[0][0].m_text, begin->m_loc);
auto values = Argtype_set::parameters().value_map(positional, optional, rest, begin->m_loc);
// std::for_each(begin, end+1, [](Katom& k) { k.m_type = katom_t::replaced; });
modify_string_f pyformat {};
std::string pycast {};
add(values["name"], values["desc"], values["pattern"],
pycast, pyformat,
begin->m_loc);
modify_type(katom_t::replaced, begin, end);
auto next_iter = end;
ignore_whitespace(next_iter, katoms);
}
Argtype Argtype_set::get(const std::string& name, const Locator& loc) const
{
if (is_not_in(name, m_names)) {
std::stringstream msg {};
msg << "Type '" << name << "' is not an argument type";
throw Argument_error(msg.str(), loc);
}
return m_types.at(name);
}
std::string Argtype_set::eval(
const std::string& value, const std::string& type_name, const Locator& loc)
{
std::regex re = m_types[type_name].m_regex;
std::smatch match {};
if (std::regex_match(value, match, re)) {
return value;
} else {
//return "NO MATCH";
std::stringstream ss {};
ss << "Argument \"" << value << "\" does not match the pattern for \""
<< type_name << "\"\n";
throw Definition_error(ss.str(), loc);
}
}
std::string Argtype_set::describe(bool long_form, int indent_width) const
{
std::string indent(' ', indent_width);
std::size_t name_width = std::accumulate(
m_names.begin(), m_names.end(), 0,
[&] (size_t w, const std::string& name) { return std::max(w, name.size()); });
std::stringstream result {};
for (const auto& name : m_names) {
std::string label { "Regex:" };
int pat_width = name_width + 2 + indent_width + label.size();
result << indent << std::setw(name_width) << name << sp_arrow;
if (long_form) {
result << m_types.at(name).m_desc << "\n";
result << std::setw(pat_width) << "regex: " << m_types.at(name).m_symbolic_pattern;
if (m_types.at(name).m_symbolic_pattern != m_types.at(name).m_pattern)
result << sp_arrow << m_types.at(name).m_pattern;
result << "\n";
} else {
// result << abbrev(m_types.at(name).m_desc) << "\n";
result << regex_split(m_types.at(name).m_desc, std::regex("\\n"), true)[0] << "\n";
}
}
if (long_form) {
result << "\nA previously defined type can be included in the definition of a new type\n"
<< "by surrounding the name of the existing type in single quotation marks.\n";
}
return result.str();
}

88
mac/argtype_set.h Normal file
View File

@@ -0,0 +1,88 @@
#pragma once
#include <string>
#include <vector>
#include <map>
#include "argtype.h"
#include "katom.h"
#include "argument_set.h"
class Argtype_set
{
public:
static Parameter_set& parameters();
Argtype_set();
std::string replace_symbols(const std::string& pattern, const Locator& loc);
void check_for_existing_definition(const std::string& name, const Locator& loc);
void add(const std::string& name, const std::string& desc,
const std::string& pattern, const std::string& python_cast, modify_string_f python_format,
const Locator& loc);
void add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
Argtype get(const std::string& name, const Locator& loc) const;
std::string eval(const std::string& value, const std::string& type_name, const Locator& loc);
std::string describe(bool long_form=false, int indent_width=4) const;
inline bool is_defined(std::string name) {
return count(m_names.begin(), m_names.end(), name) > 0;
}
std::vector<std::string> m_names {};
std::map<std::string, Argtype> m_types {};
size_t m_name_size = 0;
size_t m_pattern_size = 0;
};
const
std::string default_argtype = "string";
const
std::vector<std::tuple<std::string, std::string, std::string, std::string, modify_string_f>> base_argtypes = {
{ "string", "arbitrary text",
R"((?:.|\n)*)", "str",
pyformat_string },
{ "word", "a series of characters without a space",
R"([^\s]+)", "str",
pyformat_string },
{ "bool", "a Boolean value of 'false', 'False', '0', 'true', 'True', or '1'",
R"(0|1|true|false|True|False)",
"(lambda b : True if b not in {'0','false','False'} else False)",
pyformat_bool },
{ "uint", "an integer greater than or equal to zero",
R"(\d\d*)", "int",
pyformat_number },
{ "int","an integer",
R"([-+]?'uint')", "int",
pyformat_number },
{ "float", "a floating-point number",
R"([-+]?\d+(\.\d*)?)", "float",
pyformat_number },
{ "fraction", "a number in the form 'n/d'",
R"('int'/'uint')", "(lambda f : float(f.split('/')[0]) / float(f.split('/')[1]))",
pyformat_number },
{ "list", "list of elements separated by whitespace",
R"((?:.|\n)*)", "(lambda s : s.split())",
pyformat_list },
{ "dlist", "list of elements separated by the first word",
R"((?:.|\n)*)", "",
pyformat_dlist },
{ "rest", "a list of strings delimited by the bar character",
R"(.*)", "str",
pyformat_string },
{ "literal", "literal text passed without interpretation",
R"((?:.|\n)*)", "str",
pyformat_string },
};

12
mac/argument.cpp Normal file
View File

@@ -0,0 +1,12 @@
#include "argument.h"
#include "argtype.h"
Parameter::Parameter(const std::string& name, Argtype argtype, const Locator& loc,
bool optional, std::string default_value)
: m_name(name)
, m_argtype(argtype)
, m_optional(optional)
, m_default(default_value)
, m_loc(loc)
{
}

37
mac/argument.h Normal file
View File

@@ -0,0 +1,37 @@
#pragma once
#include "argtype.h"
// The Parameter class describes both parameters (in definitions) and
// arguments (in applications). Parameters are the primary concept:
// a designer declares parameters, and a writer supplies arguments to
// fill them. The class is named for the definition side because
// definitions come first; the Argument alias is used at application sites.
class Parameter
{
public:
Parameter()
: m_name("_default")
, m_argtype(Argtype())
, m_optional(false)
, m_default("")
, m_loc()
, m_target()
{};
Parameter(const std::string& name, Argtype argtype, const Locator& loc,
bool optional=false, std::string default_value = "");
~Parameter() = default;
std::string python_value(std::string value);
bool undefined() const { return m_name == "_default"; };
std::string m_name {};
Argtype m_argtype; // {};
bool m_optional {};
std::string m_default {};
Locator m_loc;
std::string m_target {};
};
using Argument = Parameter;

420
mac/argument_set.cpp Normal file
View File

@@ -0,0 +1,420 @@
#include <ranges>
#include <algorithm>
#include <utility>
#include "show.h"
#include "log.h"
#include "katom.h"
#include "argtype_set.h"
#include "argument_set.h"
#include "util.h"
bool operator==(Parameter_set lhs, Parameter_set rhs)
{
return as_string(lhs.m_katoms.begin(), lhs.m_katoms.end(), true) ==
as_string(rhs.m_katoms.begin(), rhs.m_katoms.end(), true);
}
std::regex parameter_regex(bool optional=false)
{
//std::string pattern = R"(([A-Za-z]\w*)(?:(\.\w*)(?:(\.\w*)))?)";
std::string pattern = R"((?:([A-Za-z]\w*))|(?:([A-Za-z]\w*)\.(\w+))|(?:([A-Za-z]\w*)\.(\w+)\.(\w+)))";
if (optional) {
pattern = R"((?::([A-Za-z]\w*))|(?::([A-Za-z]\w*)\.(\w+))|(?::([A-Za-z]\w*)\.(\w*)\.(\w+)))";
// x x
}
// (void)K::log(3, pattern);
return std::regex(pattern);
}
Parameter_set::Parameter_set(const std::string parameter_string)
{
(void)K::log(3);
Argtype_set argtypes {};
parse_parameters(
katomize(line_split(parameter_string), Locator().str()),
argtypes);
}
Parameter_set::Parameter_set(const std::vector<Katom>& katoms)
: m_katoms(katoms)
{
(void)K::log(3);
Argtype_set argtypes {};
parse_parameters(m_katoms, argtypes);
}
Parameter_set::Parameter_set(const std::vector<Katom>& katoms, const Argtype_set& argtypes)
: m_katoms(katoms)
{
(void)K::log(3);
parse_parameters(m_katoms, argtypes);
}
// Parameter parsing
Parameter parse_positional_parameter(const katom_list& katoms, const Argtype_set& argtypes)
{
// (void)K::log(3, katoms);
if (katoms.size() > 1) {
throw Argument_error("Multiple katoms for positional argument: " +
as_string(katoms.begin(), katoms.end(), true) +
"\nPositional arguments are separated by the bar (|) character.",
katoms[0].m_loc, false);
}
Katom k = katoms[0];
std::string name = k.m_text;
std::smatch match {};
if (!std::regex_match(name, match, parameter_regex())) {
throw Argument_error(
"The structure of the word \"" + name + "\" is not correct for a positional parameter",
k.m_loc);
} else {
std::string match_name = std::string(match[1]) + std::string(match[2]) + std::string(match[4]);
std::string match_type = std::string(match[3]) + std::string(match[5]);
std::string match_target = match[6];
if (match_type.empty()) {
match_type = "string";
}
return Parameter(match_name, argtypes.get(match_type, k.m_loc), k.m_loc);
}
}
Parameter parse_optional_parameter(const katom_list& katoms, const Argtype_set& argtypes)
{
//(void)K::log(3);
Katom k = katoms[0];
std::string default_value {};
if (katoms.size() > 1) {
default_value = to_string(katoms.cbegin() + 1, katoms.cend(), true);
}
std::string name = k.m_text;
std::smatch match {};
if (!std::regex_match(name, match, parameter_regex(true))) {
throw Argument_error(
"The structure of the word \"" + name +
"\" is not correct for an optional parameter",
k.m_loc);
} else {
std::string match_name = std::string(match[1]) + std::string(match[2]) + std::string(match[4]);
std::string match_type = std::string(match[3]) + std::string(match[5]);
std::string match_target = match[6];
if (match_type.empty()) {
match_type = "string";
}
return Parameter(match_name, argtypes.get(match_type, k.m_loc),
k.m_loc, true, default_value);
}
}
void check_for_missing_parameter(const katom_list& katoms)
{
(void)K::log(3, katoms.size());
// Yeah, yeah, "algorithms."
auto ki = katoms.begin();
while (ki < katoms.end() - 1) {
ki = std::find_if(ki, katoms.end(), [](const Katom& k) {
return k.m_type == katom_t::bar; });
if (ki == katoms.end()) {
break;
}
auto kstart = ki;
ki = std::find_if(ki + 1, katoms.end(), [](const Katom& k) {
return !k.is_whitespace(); });
if (ki == katoms.end()) {
throw Argument_error(
"A parameter list ends with a bar character", kstart->m_loc);
}
auto type_after_bar = ki->m_type;
if (type_after_bar == katom_t::bar) {
throw Argument_error(
"A parameter name was missing between two bar characters", kstart->m_loc);
} else if (type_after_bar == katom_t::option_name) {
throw Argument_error(
"A parameter name was missing between a bar character and an option name",
kstart->m_loc);
}
++ki;
}
}
bool is_boundary(katom_list::const_iterator ki)
{
return ki->m_type == katom_t::option_name || ki->m_type == katom_t::bar;
}
std::vector<std::vector<Katom>>
function_symbol_parts(katom_list::const_iterator kbegin, katom_list::const_iterator kend)
{
std::vector<std::vector<Katom>> parts;
katom_list part {};
auto ki = kbegin;
while (ki != kend && ki->is_whitespace()) {
ki++;
}
if (ki == kend) {
return {};
}
if (ki->m_type != katom_t::option_name) {
part.push_back(Katom("|", katom_t::bar, kbegin->m_loc));
}
while (ki < kend) {
if (!part.empty() && is_boundary(ki)) {
parts.push_back(trim(part));
part = {};
}
part.push_back(*ki);
ki++;
}
if (!part.empty()) {
parts.push_back(trim(part));
}
// std::cout << "PARTS:\n";
// for (size_t i = 0; i < parts.size(); i++) {
// std::cout << i << sp_arrow << parts[i] << "\n";
// }
return parts;
}
katom_list trim_part(katom_list part)
{
return trim(part, {katom_t::space, katom_t::newline, katom_t::bar});
}
std::tuple<katom_lists,katom_lists>
parameter_split(katom_list::const_iterator kbegin, katom_list::const_iterator kend)
{
(void)K::log(3); //, "begin:", *kbegin, "end:", *(kend - 1));
// "distance:", std::distance(kbegin, kend));
auto parts = function_symbol_parts(kbegin, kend);
// std::cout << "parts: " << parts << "\n";
katom_lists positional {};
katom_lists optional {};
for (auto p : parts) {
if (p[0].m_type == katom_t::option_name) {
optional.push_back(p);
} else {
positional.push_back(trim_part(p));
}
}
return {positional, optional};
}
void Parameter_set::parse_parameters(const katom_list& katoms, const Argtype_set& argtypes)
{
(void)K::log(3, trim(katoms));
if (katoms.empty()) {
return;
}
check_for_missing_parameter(katoms);
auto [positional, optional] = parameter_split(katoms.cbegin(), katoms.cend());
for (auto req : positional) {
auto pos = parse_positional_parameter(req, argtypes);
if (pos.m_argtype.m_name == "rest") {
m_rest.push_back(pos);
} else {
m_positional.push_back(pos);
}
}
for (auto opt : optional) {
auto param = parse_optional_parameter(opt, argtypes);
if (std::ranges::count(m_optional_names, param.m_name) > 0) {
throw Argument_error(
"Optional parameter \":" + param.m_name + "\" already defined",
katoms[0].m_loc);
}
m_optional.push_back(param);
m_optional_names.push_back(param.m_name);
}
if (!m_rest.empty()) {
m_positional_count = m_positional.size();
}
}
void describe_arguments(
std::string label,
std::vector<std::vector<Katom>> positional,
std::vector<std::vector<Katom>> optional,
std::vector<Katom> rest)
{
std::cout << label << ":\n"
<< " positional: " << positional << "\n"
<< " optional: " << optional << "\n"
<< " rest: " << rest << "\n";
}
void Parameter_set::describe_parameters()
{
(void)K::log(3);
std::cout << " positional: ";
if (!m_positional.empty()) {
for (auto p : m_positional) {
std::cout << p << " ";
}
} else {
std::cout << "[none]";
}
std::cout << "\n optional: ";
if (!m_optional.empty()) {
for (auto p : m_optional) {
std::cout << p << " ";
}
} else {
std::cout << "[none]";
}
std::cout << "\n rest: ";
if (!m_rest.empty()) {
std::cout << kall << m_rest << kreset << "\n";
} else {
std::cout << "[none]";
}
std::cout << "\n";
}
std::tuple<katom_lists,katom_lists,katom_list>
argument_split(katom_list::const_iterator kbegin, katom_list::const_iterator kend,
long unsigned int positional_limit)
{
// (void)K::log(3, "begin:", *(kbegin+1), "end:", *(kend - 1),
// "distance:", std::distance(kbegin, kend), "limit:", positional_limit);
(void)K::log(3);
// msg() << std::pair(kbegin, kend) << "\n";
auto parts = function_symbol_parts(kbegin, kend);
katom_lists positional {};
katom_lists optional {};
katom_list rest {};
for (auto p : parts) {
if (p[0].m_type == katom_t::option_name) {
optional.push_back(p);
} else if (positional.size() < positional_limit) {
positional.push_back(trim_part(p));
} else {
rest.insert(rest.end(), p.begin(), p.end());
}
}
return {positional, optional, trim_part(rest)};
}
// Parameter/argument mapping
void Parameter_set::check_positional(const katom_lists& positional_arguments, const Locator& loc)
{
(void)K::log(3, "required:", m_positional.size(), positional_arguments.size()); //, positional_arguments);
auto positional_count = m_positional.size();
auto given_count = positional_arguments.size();
if (positional_count > given_count) {
// std::cout << "Given less than required\n";
std::vector<Parameter> missing(m_positional.begin() + given_count, m_positional.end());
//std::cout << "missing: " << missing << "\n";
auto missing_count = missing.size();
std::stringstream ss {};
ss << "Positional " << plural("argument", missing_count) << " " << to_be(missing_count)
<< " missing:\n";
std::cout << ss.str();
for (auto arg : missing) {
ss << " " << arg.m_name << "\n";
}
//std::cout << ss.str();
throw Argument_error(ss.str(), loc, false);
} else if (positional_count < given_count) {
//std::cout << "DESCRIBE\n";
//describe_parameters();
std::stringstream ss {};
ss << "Too many positional arguments were given; "
<< positional_count << " needed but " << given_count << " given";
throw Argument_error(ss.str(), loc);
}
}
std::map<std::string, std::string>
Parameter_set::check_optional(const katom_lists& optional_arguments, const Locator& loc)
{
(void)K::log(3, optional_arguments.size());
std::vector<std::string> optional_names_used {};
std::map<std::string, std::string> values {};
for (const auto& opt : optional_arguments) {
std::string name(opt[0].m_text, 1);
if (std::ranges::count(m_optional_names, name) == 0) {
throw Argument_error("Optional argument \":" + name + "\" not defined", loc);
}
if (std::ranges::count(optional_names_used, name) > 0) {
throw Argument_error("Optional argument \":" + name + "\" already provided "
+ "with a value of:\n" + values[name], loc, false);
}
katom_list value_katoms(opt.begin()+1, opt.end());
std::string value = trim(to_string(value_katoms));
values[name] = value;
optional_names_used.push_back(name);
}
return values;
}
const std::map<std::string, std::string>
Parameter_set::value_map(
const katom_lists& positional, const katom_lists& optional, const katom_list& rest,
const Locator& loc)
{
(void)K::log(3, "positional:", positional.size(), "optional:", optional.size(), "rest:", rest.size());
std::map<std::string, std::string> values {};
check_positional(positional, loc);
for (size_t i = 0; i < m_positional.size(); ++i) {
auto param = m_positional[i];
std::string arg = as_string(positional[i].begin(), positional[i].end(), true);
values[param.m_name] = arg;
}
auto optional_values = check_optional(optional, loc);
for (auto [key, value] : optional_values) {
values[key] = value;
}
for (auto opt : m_optional) {
values.try_emplace(opt.m_name, opt.m_default);
}
if (active(rest)) {
if (!m_rest.empty()) {
values[m_rest[0].m_name] = as_string(rest.begin(), rest.end(), true);
} else {
std::stringstream ss {};
ss << "More positional arguments were given (" << positional.size() + rest.size()
<< ") than defined (" << m_positional.size() << ")";
throw Argument_error(ss.str(), loc);
}
}
return values;
}
// Parameter/argument substitution
std::string replace_arguments(
const std::map<std::string, std::string>& values,
const std::string& parameterized_text, const Locator& loc)
{
(void)K::log(3);
std::string result = parameterized_text;
for (auto [name, value] : values) {
result = string_replace(result, '*' + name + '*', value);
}
auto matches = find_all(result, std::regex(R"((\*.*?\*))"));
std::vector<std::string> unmatched;
unmatched.reserve(matches.size());
std::copy(matches.begin(), matches.end(), std::back_inserter(unmatched));
auto unmatched_count = unmatched.size();
if (unmatched_count > 0) {
std::stringstream ss {};
ss << "Undefined " << plural("argument", unmatched_count) << " in klammer:\n";
for (const auto& arg : unmatched) {
ss << " " << arg << "\n";
}
ss << "To prevent the \"*\" character from specifying an argument, "
<< "precede it with the \"^\" character.";
throw Argument_error(ss.str(), loc, false);
}
return result;
}

63
mac/argument_set.h Normal file
View File

@@ -0,0 +1,63 @@
#pragma once
#include <limits>
#include <tuple>
#include <vector>
#include "katom.h"
#include "argument.h"
#include "locator.h"
class Argtype_set;
std::tuple<std::vector<std::vector<Katom>>,std::vector<std::vector<Katom>>,std::vector<Katom>>
argument_split(std::vector<Katom>::const_iterator kbegin, std::vector<Katom>::const_iterator kend,
long unsigned int positional_limit = std::numeric_limits<int>::max());
class Parameter_set
{
public:
Parameter_set() {};
~Parameter_set() = default;
Parameter_set(const std::string parameter_string);
Parameter_set(const std::vector<Katom>& katoms);
Parameter_set(const std::vector<Katom>& katoms, const Argtype_set& argtypes);
void parse_parameters(const std::vector<Katom>& katoms, const Argtype_set& argtypes);
void describe_parameters();
void check_positional(
const std::vector<std::vector<Katom>>& positional_arguments, const Locator& loc);
std::map<std::string, std::string> check_optional(
const std::vector<std::vector<Katom>>& optional_arguments, const Locator& loc);
const std::map<std::string, std::string> value_map(
const std::vector<std::vector<Katom>>& positional,
const std::vector<std::vector<Katom>>& optional,
const std::vector<Katom>& rest,
const Locator& loc);
bool empty() const { return m_katoms.size() == 0; };
std::vector<Katom> m_katoms {};
//Argtype_set m_argtypes {};
std::vector<Parameter> m_positional {};
std::vector<Parameter> m_optional {};
std::vector<std::string> m_optional_names {};
std::vector<Parameter> m_rest {};
size_t m_positional_count = std::numeric_limits<int>::max();
};
bool operator==(Parameter_set lhs, Parameter_set rhs);
// The Argument_set alias is used at application sites, where the set
// describes arguments rather than parameters. See argument.h.
using Argument_set = Parameter_set;
void describe_arguments(
std::string label,
std::vector<std::vector<Katom>> positional,
std::vector<std::vector<Katom>> optional,
std::vector<Katom> rest);
std::string replace_arguments(
const std::map<std::string, std::string>& values,
const std::string& parameterized_text, const Locator& loc);

569
mac/argv.cpp Normal file
View File

@@ -0,0 +1,569 @@
#include <numeric>
#include "file.h"
#include "argv.h"
#include "error.h"
#include "log.h"
#include "util.h"
#include "show.h"
std::string Argv::delimiter = "--";
std::ostream& operator<<(std::ostream& os, const Arg& arg)
{
os << "<" << arg.m_type << " " << arg.m_name
<< " " << q_(arg.m_value) << ">";
return os;
}
std::string wrap_around(const std::string& text, std::size_t indent, std::size_t width=96)
{
std::string result {};
std::size_t current = indent;
std::string margin(indent, ' ');
for (const std::string& word : word_split(text)) {
if (current + 1 + word.size() > width) {
result += "\n" + margin;
current = indent;
}
result += word + " ";
current += word.size() + 1;
}
return result;
}
std::string flag_name(const std::string& name)
{
std::string result {};
if (name.size() == 1) {
result = "-" + name;
} else {
result = "--" + name;
}
return result;
}
std::string Arg::symbol()
{
std::string result = m_name;
if (m_type != "req") {
result = flag_name(m_name);
/*
if (m_name.size() == 1) {
result = "-" + m_name;
} else {
result = "--" + m_name;
}
*/
} else {
result = "<" + result + ">";
}
return result;
}
void Arg::make_regex(const std::string& key)
{
std::string pat {};
if (regex_symbols.find(key) != regex_symbols.end()) {
pat = regex_symbols[key];
m_rgx_symbol = key;
// std::cout << "rgx_symbol: " << m_rgx_symbol << "\n";
} else if (key.find("(") != std::string::npos) {
pat = key;
}
if (pat.size() == 0) {
throw Definition_error("No regex pattern defined for \"" + key + "\".");
}
m_pattern = pat;
m_rgx = std::regex(m_pattern);
}
void Argv::update_width(Arg arg)
{
m_syntax_size = std::max(m_syntax_size, arg.m_syntax.size());
}
std::string get_regex_desc(const std::string& desc)
{
if (regex_desc.find(desc) != regex_desc.end()) {
return regex_desc[desc];
} else {
return desc;
}
}
void Argv::flag(const std::string& name, const std::string& desc)
{
(void)K::log(2, name, desc);
Arg arg {};
arg.m_type = "flag";
arg.m_name = name;
arg.m_desc = get_regex_desc(desc);
arg.m_rgx = std::regex(R"((\w+))");
arg.m_syntax = arg.symbol();
arg.m_value = "false";
m_args[name] = arg;
m_names.push_back(name);
m_flag_names.push_back(name);
m_hyphen_markers.push_back(flag_name(name));
update_width(arg);
}
void Argv::req(const std::string& name, const std::string& desc, const std::string& regex_pattern)
{
(void)K::log(2, name, desc, regex_pattern);
Arg arg {};
arg.m_type = "req";
arg.m_name = name;
arg.m_desc = get_regex_desc(desc);
arg.make_regex(regex_pattern);
arg.m_syntax = "<" + name + ">";
m_args[name] = arg;
m_names.push_back(name);
m_req_names.push_back(name);
update_width(arg);
}
void Argv::opt(const std::string& name, const std::string& desc, const std::string& parameter, const std::string& default_value, const std::string& regex_pattern)
{
(void)K::log(2, name, desc, parameter, default_value, regex_pattern);
Arg arg {};
arg.m_type = "opt";
arg.m_name = name;
arg.m_parameter = parameter;
arg.m_default_value = default_value;
arg.m_value = default_value;
arg.m_desc = get_regex_desc(desc);
arg.make_regex(regex_pattern);
arg.m_syntax = arg.symbol() + " <" + arg.m_parameter + ">";
m_args[name] = arg;
m_names.push_back(name);
m_opt_names.push_back(name);
m_hyphen_markers.push_back(flag_name(name));
update_width(arg);
}
void Argv::usage_line(Arg arg)
{
std::cout.fill(' ');
std::cout << " " << std::left << std::setw(m_syntax_size) << arg.m_syntax << " "
<< wrap_around(arg.m_desc, m_syntax_size + 6) << "\n";
}
void Argv::usage(const std::string& command)
{
std::cout << "\nUsage: " << command << " ";
for (const std::string& name : m_req_names) {
std::cout << "<" + name + "> ";
}
if (m_opt_names.size() + m_flag_names.size() > 5) {
std::cout << "[<optional-arguments>]\n";
} else {
for (const std::string& name : m_names) {
if (m_args[name].m_type == "req") {
continue;
}
std::cout << "[" + m_args[name].m_syntax + "] ";
}
std::cout << "\n";
}
if (!m_req_names.empty()) {
//std::cout << "\n" << plural("Argument", m_req_names.size()) << ":\n";
std::cout << "\n" << "Arguments:\n";
for (const std::string& req_name : m_req_names) {
usage_line(m_args[req_name]);
}
}
int flag_count = m_flag_names.size();
int opt_count = m_opt_names.size();
if (flag_count > 0 || opt_count > 0) {
std::cout << "\n" << plural("Option", flag_count + opt_count) << ":\n";
}
for (const auto& name : m_names) {
if (m_args[name].m_type == "req") {
continue;
}
usage_line(m_args[name]);
}
std::cout << "\n";
}
void Argv::check_flags_and_options(std::string command, strings_t& words)
{
std::vector<std::string> not_defined {};
for (auto word : words) {
if (word[0] == '-' && word != Argv::delimiter && !is_in(word, m_hyphen_markers)) {
not_defined.push_back(word);
}
}
if (!not_defined.empty()) {
std::stringstream ss {};
ss << "The following flags were not defined for command " << q_(command) << ":\n";
for (auto w : not_defined) {
ss << " " << w << "\n";
}
throw Argument_error(ss.str(), Locator(), false);
}
}
void Argv::parse_flags(strings_t& words, string_map& named_args)
{
std::vector<std::string> flag_args {};
for (std::string flag : m_flag_names) {
// std::cout << "Flag: " << flag << "\n";
if (is_in(flag_name(flag), words)) {
flag_args.push_back(flag);
remove_element(words, flag_name(flag));
named_args[flag] = "true";
} else {
named_args[flag] = "false";
}
}
/*
std::vector<std::string> not_defined {};
for (auto word : words) {
if (word[0] == '-' && word != Argv::delimiter) {
not_defined.push_back(word);
}
}
if (!not_defined.empty()) {
std::stringstream ss {};
ss << "The following flags were not defined for command " << q_(command) << ":\n";
for (auto w : not_defined) {
ss << " " << w << "\n";
}
throw Argument_error(ss.str(), Locator(), false);
}
*/
// std::cout << "Flags found: " << flag_args << "\n";
}
void Argv::parse_optional(strings_t& words, string_map& named_args)
{
// msg() << "parse_optional: " << words << "\n";
std::map<std::string, std::string> opt_args {};
for (std::string opt : m_opt_names) {
// std::cout << "Opt: " << opt << sp_arrow << m_args[opt].m_pattern << "\n";
auto it = std::ranges::find(words, flag_name(opt));
if (it != words.end()) {
// msg() << "words: " << words.size() << " " << words << "\n";
size_t index = std::distance(words.begin(), it);
// std::cout << " Found: " << words[index] << "\n";
//std::vector<std::string> opt_args = {};
index++;
/*
if (index >= words.size()) {
break;
}
*/
std::string opt_arg = words[index] + " ";
index++;
/*
if (index >= words.size()) {
break;
}
*/
std::regex opt_regex = m_args[opt].m_rgx;
// std::cout << "Regex match? " << std::regex_match(trim(opt_arg), opt_regex) << "\n";
while (index < words.size() && words[index][0] != '-'
// && std::regex_match(trim(opt_arg), opt_regex)
&& std::regex_match(trim(opt_arg + words[index]), opt_regex)
) {
// opt_args.push_back(words[index++]);
opt_arg += words[index++] + " " ;
}
opt_arg = trim(opt_arg);
// std::cout << " Value: " << opt_arg << "[" << index << "]\n";
opt_args[opt] = opt_arg;
words.erase(it, words.begin() + index);
// std::cout << " Remaining: " << words << "\n";
named_args[opt] = opt_arg;
} else { // Not found
}
}
// std::cout << "opt_args:\n";
// if (opt_args.empty()) {
// std::cout << "[none]";
// } else {
// std::cout << opt_args;
// }
// std::cout << "\n";
}
void Argv::parse_positional(std::string command, //strings_t words,
std::string pos_args, string_map& named_args)
{
for (std::string req : m_req_names) {
auto arg = m_args[req];
auto [substring, rest, found] = regex_split_prefix(arg.m_rgx, pos_args);
if (!found) {
std::stringstream ss {};
ss << "The argument " << q_(req) << " was not found in:\n " << command;
throw Argument_error(ss.str(), Locator(), false);
}
named_args[req] = substring;
pos_args = rest;
}
// std::cout << "Remaining words: " << words << "\n" << pos_args << "\n";
}
std::map<std::string, std::string>
Argv::classify_arguments(int argc, char* argv[], bool full_parse)
{
(void)K::log(2, argc);
if (argc == 1) {
return {};
}
std::map<std::string, std::string> named_args {};
std::vector<std::string> words(argv + 1, argv + argc);
if (full_parse) {
check_flags_and_options(argv[0], words);
}
parse_flags(words, named_args);
parse_optional(words, named_args);
parse_positional(argv_to_string(argc, argv), join(words, " "), named_args);
words.erase(std::remove(words.begin(), words.end(), Argv::delimiter), words.end());
// std::cout << "Named args:\n" << named_args << "\n";
return named_args;
}
void Argv::check_required(
const std::vector<std::string>& req_args, const std::string& command)
{
(void)K::log(2);
auto required = m_req_names.size();
auto given = req_args.size();
if (required > given) {
std::string missing = m_req_names[required - given - 1];
if (given == 0 && m_args[missing].m_rgx_symbol == "'list'") {
return;
}
std::stringstream ss {};
ss << "The required argument \"" << missing
<< "\" was not provided in command \"" << command << "\"";
throw Argument_error(ss.str());
} else if (required < given) {
std::stringstream ss {};
ss << "Too many required arguments were given for command \""
<< command << "\" (" << required << " needed but "
<< given << " given" << ")";
throw Argument_error(ss.str());
}
}
void Argv::check_flags(const string_map& arg_map, const std::string& command)
{
(void)K::log(3);
// std::cout << "arg_map:\n" << arg_map << "\n";
strings_t undefined {};
for (const auto& pair : arg_map) {
auto [key, value] = pair;
// std::cout << "m_type: " << m_args[key].m_type << "\n";
if (key[0] != '_') {
if (m_args[key].m_type != "req"
&& !contains(m_flag_names, key)
&& !contains(m_opt_names, key)) {
undefined.push_back(key);
}
}
}
if (!undefined.empty()) {
std::stringstream ss {};
ss << "Undefined arguments given for command \"" << command << "\":";
for (const std::string& undef : undefined) {
ss << " " << flag_name(undef);
}
throw Argument_error(ss.str(), Locator());
}
}
void Argv::parse(int argc, char* argv[], bool full_parse)
{
(void)K::log(2);
command_name = argv[0];
describe();
auto input_args = classify_arguments(argc, argv, full_parse);
// std::cout << "parse() classify:\n" << input_args << "\n";
for (auto [key, value] : input_args) {
m_args[key].m_value = value;
}
// std::cout << "m_flag_names: " << m_flag_names << "\n";
/*
for (const std::string& name : m_flag_names) {
if (input_args.find(name) != input_args.end()) {
m_args[name].m_value = "true";
}
}
*/
/*
for (const std::string& name : m_opt_names) {
if (input_args.find(name) != input_args.end()) {
std::smatch match {};
if (std::regex_match(input_args[name], match, m_args[name].m_rgx)) {
m_args[name].m_value = input_args[name];
} else {
usage(file_basename(argv[0]));
throw Argument_error(
"Incorrect value for option \"" + name + "\":\n" + m_args[name].m_desc + "\n",
Locator(), false);
}
}
}
std::cout << "input_args:\n" << input_args << "\n";
if (full_parse) {
for (auto [key, value] : input_args) {
auto arg = m_args[key];
if (arg.m_type == "req") {
m_args[key].m_value = value;
}
}
*/
/*
std::vector<std::string> required_args {};
for (auto [key, value] : input_args) {
// std::cout << " full_parse: key: " << key << " value: " << value << "\n";
if (key[0] == '_' && !trim(value).empty()) {
required_args.push_back(value);
}
}
check_required(required_args, command_name);
for (unsigned int i = 0; i < required_args.size(); ++i) {
m_args[m_req_names[i]].m_value = required_args[i];
}
*/
//check_flags(input_args, command_name);
// }
// std::cout << "FINAL:\n"
// << m_args;
}
std::string Argv::get(const std::string& name, bool missing_is_error)
{
if (m_args.count(name) > 0) {
return m_args.at(name).m_value;
} else if (missing_is_error) {
throw Argument_error("Command-line argument \"" + name + "\" is not defined");
} else {
return "";
}
}
bool Argv::as_bool(const std::string& name)
{
(void)K::log(2, name);
return get(name) == "true";
}
int Argv::as_int(const std::string& name)
{
(void)K::log(2, name);
auto value = get(name);
if (!std::regex_match(value, std::regex(R"([-+]?\d+)"))) {
throw Argument_error("Argument \"" + value + "\" is not an integer");
}
return std::stoi(value);
}
int Argv::as_integer_range(const std::string& name, int low, int high)
{
(void)K::log(2, name);
auto value = get(name);
std::stringstream ss {};
ss << "Argument \"" << value << "\" is not an integer in the range of "
<< low << " to " << high;
if (!std::regex_match(value, std::regex(R"([-+]?\d+)"))) {
throw Argument_error(ss.str());
}
int result = std::stoi(value);
if (result < low || result > high) {
throw Argument_error(ss.str());
}
return result;
}
int Argv::as_verbosity(const std::string& name)
{
(void)K::log(2, name);
auto value = get(name);
if (!std::regex_match(value, std::regex(regex_symbols["'verbosity'"]))) {
throw Argument_error("Argument \"" + value + "\" is not a verbosity level");
}
return std::stoi(value);
}
std::string Argv::as_string(const std::string& name)
{
(void)K::log(2, name);
return get(name);
}
strings_t Argv::as_vector(const std::string& name)
{
(void)K::log(2, name);
return regex_split(get(name), std::regex(R"(\s+)"));
}
std::pair<std::string, strings_t> Argv::as_input(const std::string& name, bool allow_empty)
{
(void)K::log(2, name);
const std::string& input_arg = get(name);
std::regex filename_rgx(R"(([./\w]+\.kt?))");
strings_t input_filenames = find_all(input_arg, filename_rgx, 1);
const std::string& input_text = trim(std::regex_replace(input_arg, filename_rgx, ""));
if (input_text.empty() && input_filenames.empty()) {
if (allow_empty) {
return {{},{}};
} else {
throw Argument_error("No input text or filenames specified");
}
}
(void)K::log(2, "Input text", input_text, 1);
for (const auto& f : input_filenames) {
(void)K::log(2, "Input filename", f);
}
return { input_text, input_filenames };
}
void Argv::describe()
{
std::size_t width = std::accumulate(
m_names.begin(), m_names.end(), 0,
[&] (size_t w, const std::string& name) {
return std::max(w, m_args[name].symbol().size()); });
for (const std::string& name : m_names) {
std::string value = m_args[name].m_value;
if (value.empty()) {
value = "<none>";
}
std::stringstream ss {};
ss << " "<< std::setfill(' ') << std::setw(width)
<< m_args[name].symbol() << " : " << value;
}
/*
if (verbose_level == 1) {
std::cout << "\n";
}
*/
}

103
mac/argv.h Normal file
View File

@@ -0,0 +1,103 @@
#pragma once
// Delusions of generality, but it's really just for Klammertext commands.
#include <map>
#include <ranges>
#include <algorithm>
#include <regex>
inline std::map<std::string, std::string> regex_symbols {
{"'text'", R"((^[^-](?:\s|.)*))" },
{"'word'", R"((([^\s]+)))" },
{"'list'", R"((^[^-]?(?:\s|.)*))" },
// {"int", R"((\d))" },
// {"ints", R"((\d[ \d]*))" },
{"'verbosity'", R"(([01234]))" },
// {"targets", R"((html|tex|pdf|txt))" },
//{"'katom_display'", R"((none ?|all ?|type ?|index ?|ignored ?|replaced ?)*)" },
{"'katom_display'", R"(( *|none|all|type|index|ignored|replaced)*)" },
};
inline std::map<std::string, std::string> regex_desc {
{"'verbosity'", "Verbosity during processing (0,1,2,3,4); default is 0." }
};
std::regex make_regex(const std::string& key);
class Arg
{
public:
std::string symbol();
void make_regex(const std::string& key);
std::string m_type {};
std::string m_name {};
std::string m_pattern {};
std::string m_rgx_symbol {};
std::regex m_rgx {};
std::string m_default_value {};
std::string m_parameter {};
std::string m_syntax {};
std::string m_desc {};
std::string m_value {};
};
std::ostream& operator<<(std::ostream& os, const Arg& arg);
class Argv
{
public:
static std::string delimiter;
void flag(const std::string& name, const std::string& desc);
void req(const std::string& name, const std::string& desc, const std::string& regex_pattern="'text'");
void opt(const std::string& name, const std::string& desc="", const std::string& parameter="", const std::string& default_value="", const std::string& regex_pattern="text");
void update_width(Arg arg);
void check_flags_and_options(std::string command, std::vector<std::string>& words);
void parse_flags(std::vector<std::string>& words, std::map<std::string, std::string>& named_args);
void parse_optional(std::vector<std::string>& words, std::map<std::string, std::string>& named_args);
void parse_positional(
std::string command, // std::vector<std::string> words,
std::string pos_args, std::map<std::string, std::string>& named_args);
std::map<std::string, std::string> classify_arguments(int argc, char* argv[], bool full_parse=true);
void check_required(
const std::vector<std::string>& req_args, const std::string& command_name);
void check_flags(const std::map<std::string, std::string>& arg_map, const std::string& command_name);
void parse(int argc, char* argv[], bool full_parse=true);
std::string get(const std::string& name, bool missing_is_error=true);
bool as_bool(const std::string& name);
int as_int(const std::string& name);
int as_integer_range(const std::string& name, int low, int high);
int as_verbosity(const std::string& name);
std::string as_string(const std::string& name);
std::vector<std::string> as_vector(const std::string& name);
std::pair<std::string, std::vector<std::string>> as_input(const std::string& name, bool allow_empty=false);
void usage_line(Arg arg);
void usage(const std::string& command_name);
void describe();
// void describe(Argv original);
bool is_flag(std::string name) {
return std::ranges::count(m_flag_names, name) > 0;
}
bool is_opt(std::string name) {
return std::ranges::count(m_opt_names, name) > 0;
}
std::string m_command {};
std::map<std::string, Arg> m_args {};
std::vector<std::string> m_names {};
std::vector<std::string> m_req_names {};
std::vector<std::string> m_flag_names {};
std::vector<std::string> m_opt_names {};
std::vector<std::string> m_hyphen_markers {};
long unsigned int m_syntax_size = 0;
};

1
mac/basenames.mk Normal file
View File

@@ -0,0 +1 @@
BASENAMES := util error locator file argv character ktype katom katom_list log show command argument argument_set argtype argtype_set state eval eval_python eval_cpp klammer klammer_set target target_set machine

304
mac/character.cpp Normal file
View File

@@ -0,0 +1,304 @@
#include <fstream>
#include "util.h"
#include "character.h"
#include "log.h"
#include "show.h"
inline
std::string klammertext_special_characters { "@|*^#" };
inline
std::string encoding_marker { "UU" };
inline
std::string diacritic_symbols = "-'`h~\"cbrdwa";
inline
std::string diacritic_symbols_order = "'`h~\"c-brdwa";
std::string utf8char(int cp)
{
char c[5]={ 0x00,0x00,0x00,0x00,0x00 };
if (cp<=0x7F) {
c[0] = cp;
} else if(cp<=0x7FF) {
c[0] = (cp>>6)+192;
c[1] = (cp&63)+128;
} else if(0xd800<=cp && cp<=0xdfff) {
return "Invalid Unicode: " + std::to_string(cp);
} else if(cp<=0xFFFF) {
c[0] = (cp>>12)+224;
c[1]= ((cp>>6)&63)+128;
c[2]=(cp&63)+128;
} else if (cp<=0x10FFFF) {
c[0] = (cp>>18)+240;
c[1] = ((cp>>12)&63)+128;
c[2] = ((cp>>6)&63)+128;
c[3]=(cp&63)+128;
}
return std::string(c);
}
std::string unicode_hex_to_char(std::string s, int width=4) //, std::string marker)
{
(void)K::log(4, s);
std::string result {s};
std::sregex_iterator end {};
std::regex re;
switch (width) {
case 2: re = hex2_re; break;
case 4: re = hex4_re; break;
case 5: re = hex5_re; break;
}
for (std::sregex_iterator p { s.begin(), s.end(), re }; p!= end; ++p) {
int codepoint = stoi((*p)[1].str(), nullptr, 16);
auto c = utf8char(codepoint);
std::regex hit_re { regex_escape((*p)[0]) };
result = std::regex_replace(result, hit_re, c);
}
return result;
}
std::string process_diacritics(std::string s)
{
(void)K::log(4);
std::regex diacritic_re("\\^([^\\s`'~@|^:*#])([" + diacritic_symbols + "])");
std::string result {s};
std::sregex_iterator end {};
for (std::sregex_iterator p { s.begin(), s.end(), diacritic_re }; p!= end; ++p) {
std::regex hit { regex_escape((*p)[0].str()) };
std::string ch = (*p)[1].str();
std::string d = (*p)[2].str();
result = std::regex_replace(result, hit, ch + unicode_hex_to_char(diacritics[d].first));
}
return result;
}
std::string extended_latin_symbol_pattern()
{
std::string result {};
std::string sep = "";
for (const auto& nr : extended_latin_symbols) {
result += sep;
result += nr;
sep = "|";
}
return result;
}
std::string process_extended_latin(std::string s)
{
(void)K::log(4);
std::regex diacritic_re("\\^(" + extended_latin_symbol_pattern() + ")\\^");
std::string result {s};
std::sregex_iterator end {};
for (std::sregex_iterator p { s.begin(), s.end(), diacritic_re }; p!= end; ++p) {
std::regex hit { regex_escape((*p)[0].str()) };
std::string ch = (*p)[1].str();
result = std::regex_replace(result, hit, unicode_hex_to_char(extended_latin[ch].first));
}
return result;
}
std::string process_pinyin(std::string s)
{
(void)K::log(4);
std::regex pinyin_re(R"(\^([aeiou])([1-4]))");
std::string result {s};
std::sregex_iterator end {};
for (std::sregex_iterator p { s.begin(), s.end(), pinyin_re }; p!= end; ++p) {
std::regex hit { regex_escape((*p)[0].str()) };
std::string vowel = (*p)[1].str();
std::string tone = (*p)[2].str();
result = std::regex_replace(result, hit,
vowel + unicode_hex_to_char(pinyin_tones[tone].first));
}
return result;
}
std::string process_unicode_codepoint(std::string s)
{
(void)K::log(4);
//return std::regex_replace(s, unicode_re, hidehat + "$1" + hidehat);
std::string result {s};
std::sregex_iterator end {};
for (std::sregex_iterator p { s.begin(), s.end(), unicode_re }; p!= end; ++p) {
std::regex hit_re { regex_escape((*p)[0].str()) };
result = std::regex_replace(result, hit_re, unicode_hex_to_char((*p)[1].str()));
}
return result;
}
// Old xhide/xrestore/hide/restore functions removed. The ^X mechanism
// is handled by the katomizer (katom_t::special) and the general KTESC
// escape mechanism in Target::escape_text/resolve_escapes.
std::string encode(const std::string& s)
{
if (s.find("^") == std::string::npos)
return s;
(void)(void)K::log(3);
std::string result = s;
bool dbg = verbose_level > 3;
std::string lit_start = "__LITSTART__";
std::string lit_end = "__LITEND__";
result = string_replace(result, "^'", lit_start);
result = string_replace(result, "'^", lit_end);
if (result.find("^") == std::string::npos)
return s;
if (dbg) std::cout << "start: " << result << "\n";
result = process_extended_latin(result);
if (dbg) std::cout << "extended_latin: " << result << "\n";
result = process_unicode_codepoint(result);
if (dbg) std::cout << "unicode: " << result << "\n";
result = process_diacritics(result);
if (dbg) std::cout << "diacrit: " << result << "\n";
result = process_pinyin(result);
if (dbg) std::cout << "pinyin: " << result << "\n";
result = string_replace(result, lit_start, "^'");
result = string_replace(result, lit_end, "'^");
return result;
}
// Decode
// Display
void write_kt_example_file(std::stringstream& kt, std::string kt_filename)
{
kt << "|| @line@\n";
kt << "@\n";
std::cout << "Writing " << kt_filename << "...";
std::ofstream out(kt_filename);
out << kt.str();
out.close();
std::cout << "done\n";
}
void diacritics_examples(const std::string& kt_filename)
{
if (diacritics.size() != diacritic_symbols_order.size()) {
//throw Internal_error("Mismatch between diacritics order list and their definitions");
std::cout << "Mismatch error\n";
}
std::stringstream kt {};
bool write_kt_file = kt_filename.size() != 0;
if (write_kt_file)
kt << "@table :caption Diacritics (with typical base characters) |\n"
<< " @i-Displayed | @i-Written | @i-Name\n";
else
std::cout << boldblack
<< "\nDiacritics (with typical base characters)\n" << black;
std::string line_sep = "|| @line@ ";
for (char symbol : diacritic_symbols_order) {
std::string sym { symbol };
auto [code, name] = diacritics[sym];
std::string letter = diacritic_example_letter[sym];
std::string written = "^" + letter + sym;
if (sym == "~") {
written = "^" + letter + "=7e=";
}
if (write_kt_file) {
std::string display = "^" + letter + sym;
kt << line_sep << display << " | @t ^" << written << " @ | " << name << "\n";
line_sep = "|| ";
}
else {
std::cout << " " << letter << unicode_hex_to_char(code)
<< " " << "^" << letter << symbol << " " << name << "\n";
}
}
if (write_kt_file)
write_kt_example_file(kt, kt_filename);
}
void extended_latin_examples(const std::string& kt_filename)
{
std::stringstream kt {};
bool write_kt_file = kt_filename.size() != 0;
std::string title = "Extended Latin characters and ligatures";
if (write_kt_file)
kt << "@table :caption " << title << " |\n"
<< " @i-Displayed | @i-Written | @i-Name\n";
else
std::cout << "\n" << boldblack << title << black << "\n";
std::string line_sep = "|| @line@ ";
for (const std::string& symbol : extended_latin_symbols) {
auto [code, name] = extended_latin[symbol];
if (write_kt_file) {
std::string coded = "^" + symbol + "^";
std::string file_literal = "^^ #- " + symbol + " #- ^^";
kt << line_sep << coded << " | @t " << file_literal << " @ | " << name << "\n";
line_sep = "|| ";
}
else {
std::string screen_literal = "^" + symbol + "^";
std::cout << " " << unicode_hex_to_char(code)
<< " " << std::setw(4) << std::left << screen_literal << " " << name << "\n";
}
}
if (write_kt_file)
write_kt_example_file(kt, kt_filename);
}
void pinyin_examples(const std::string& kt_filename)
{
std::stringstream kt {};
bool write_kt_file = kt_filename.size() != 0;
if (write_kt_file)
kt << "@table :caption Mandarin pinyin tones (using vowel ``a'') |\n"
<< " @i-Displayed | @i-Written | @i-Name\n";
else
std::cout << boldblack
<< "\nMandarin pinyin tones (using vowel \"a\")\n" << black;
std::string line_sep = "|| @line@ ";
for (auto [symbol, codename] : pinyin_tones) {
auto [code, name] = codename;
// std::string hat_code = "^a" + code;
std::string literal = "^a" + symbol;
if (write_kt_file) {
kt << line_sep << literal << " | @t ^" << literal << " @ | " << name << "\n";
line_sep = "|| ";
} else
std::cout << " a" << unicode_hex_to_char(code)
<< " "<< std::setw(4) << std::left << literal << " " << name << "\n";
}
if (write_kt_file)
write_kt_example_file(kt, kt_filename);
}
void show_special_characters()
{
std::cout << std::setfill(' ');
diacritics_examples();
extended_latin_examples();
pinyin_examples();
}

105
mac/character.h Normal file
View File

@@ -0,0 +1,105 @@
#pragma once
// https://jakubmarian.com/special-characters-diacritics-used-in-european-languages/
#include <string>
#include <map>
#include <iomanip>
#include <tuple>
#include <vector>
#include <utility>
#include <regex>
#include <unistd.h>
const std::regex unicode_re(R"(\^(([0-9A-Fa-f]{5})|([0-9A-Fa-f]{4})|([0-9A-Fa-f]))\^)");
const std::regex unicode_hide_re(R"(=([0-9A-Fa-f]{2})=)");
const std::regex hex2_re(R"(([0-9A-Fa-f]{2}))");
const std::regex hex4_re(R"(([0-9A-Fa-f]{4}))");
const std::regex hex5_re(R"(([0-9A-Fa-f]{5}))");
// The hide_special_characters vector was removed. The ^X mechanism for
// Klammertext special characters is handled by the katomizer (type
// katom_t::special) and the general KTESC escape mechanism for
// target-specific characters.
inline
std::map<std::string, std::string> diacritic_example_letter {
{"'", "e"},
{"`", "a"},
{"h", "o"},
{"~", "n"},
{"\"", "u"},
{"c", "c"},
{"-", "o"},
{"b", "g"},
{"r", "a"},
{"d", "e"},
{"w", "s"},
{"a", "o"}};
inline
std::map<std::string, std::pair<std::string, std::string>> diacritics {
{"`", {"0300", "grave accent"}},
{"'", {"0301", "acute accent"}},
{"h", {"0302", "circumflex"}},
{"~", {"0303", "tilde"}},
{"-", {"0304", "macron"}},
{"\"", {"0308", "diaresis"}},
// {"v", {"0305", "vinculum"}},
{"b", {"0306", "breve"}},
{"d", {"0307", "dot above"}},
{"r", {"030A", "ring above"}},
{"w", {"030C", "wedge"}},
{"c", {"0327", "cedilla"}},
{"a", {"030B", "double acute accent"}}};
inline
std::vector<std::string> extended_latin_symbols = {
"s",
"i", "I", "t", "T", "e", "E", "o", "O", "d", "D",
"ae", "AE", "oe", "OE"
};
inline
std::map<std::string, std::pair<std::string, std::string>> extended_latin {
{"ae", {"00E6", "ae ligature"}},
{"AE", {"00C6", "ae ligature capital"}},
{"oe", {"0153", "oe ligature"}},
{"OE", {"0152", "oe ligature capital"}},
{"i", {"0131", "dotless i"}},
{"I", {"0130", "capital dotted i"}},
{"t", {"00FE", "thorn"}},
{"T", {"00DE", "thorn capital"}},
{"e", {"00F0", "eth"}},
{"E", {"00D0", "eth capital"}},
{"o", {"00F8", "o stroke"}},
{"O", {"00D8", "o stroke capital"}},
{"s", {"00DF", "Eszett"}},
{"d", {"0111", "d stroke"}},
{"D", {"0110", "d stroke capital"}}};
inline
std::map<std::string, std::pair<std::string, std::string>> pinyin_tones {
{"1", {"0304", "high"}},
{"2", {"0301", "rising"}},
{"3", {"030C", "falling-rising"}},
{"4", {"0300", "falling"}}};
inline
bool is_tty() { return isatty(fileno(stdout)); }
//const char* italic_on() { return tty() ? "\033[3m" : ""; }
//const char* italic_off() { return tty() ? "\033[0m" : ""; }
inline
const std::string italic_on() { return is_tty() ? "\033[3m" : ""; }
inline
const std::string italic_off() { return is_tty() ? "\033[0m" : ""; }
std::string encode(const std::string& s);
void diacritics_examples(const std::string& kt_filename = "");
void extended_latin_examples(const std::string& kt_filename = "");
void pinyin_examples(const std::string& kt_filename = "");
void show_special_characters();

109
mac/command.cpp Normal file
View File

@@ -0,0 +1,109 @@
#include <tuple>
#include "argv.h"
#include "file.h"
#include "log.h"
using namespace std::string_literals;
fs::path construct_command_pathname(char* command)
{
return fs::path(fs::current_path().string() + "/" + std::string(command));
}
void set_verbose_level(int argc, char* argv[])
{
//command_name = absolute_pathname(argv[0]);
command_name = std::string(argv[0]);
command_pathname = construct_command_pathname(argv[0]);
//std::cout << "set_verbose_level: " << command_pathname << "\n";
Argv args {};
args.opt("v", "'verbosity'", "level", "0", "'verbosity'");
args.parse(argc, argv, false);
verbose_level = args.as_verbosity("v");
}
bool show_usage(int argc, char* argv[])
{
return argc == 1 || (argc == 3 && std::string(argv[1]) == "-v"s);
}
std::string construct_output_filename(
const std::string& output_dir, const std::string& output_basename, const std::string& target)
{
std::string result {};
if (target == "html") {
result = output_dir + "/" + output_basename + "/index.html";
} else {
result = output_dir + "/" + output_basename + "." + target;
}
return result;
}
bool only_definitions(std::vector<std::string> filenames)
{
if (filenames.empty()) return false;
bool result = true;
for (auto f : filenames) {
if (fs::path(f).extension() != ".k") {
result = false;
break;
}
}
return result;
}
std::tuple<std::string, std::string, std::string, std::string, bool, bool>
parse_args(
const std::vector<std::string>& input_filenames, std::string target, std::string output_basename, bool display_only)
{
// output_dir
std::string output_target = target;
std::string output_dir = "";
std::string ext = extension(output_basename);
// std::string output_filename = "";
bool write_files = true;
if (target.empty() && output_basename.empty()) {
output_target = "any";
}
if (output_basename == "-") {
write_files = false;
} else if (!output_basename.empty()) {
output_dir = file_directory(output_basename);
output_basename = file_basename(output_basename);
} else if (!input_filenames.empty()) {
output_dir = ""; // defaults to cwd via absolute_pathname below
output_basename = file_basename(input_filenames[0]);
}
output_dir = absolute_pathname(output_dir);
if (output_target.empty()) {
target = ext;
}
std::string output_filename =
construct_output_filename(output_dir, output_basename, target);
if (output_basename.empty() && input_filenames.empty()) {
display_only = true;
} else if (only_definitions(input_filenames)) {
display_only = true;
}
std::vector<std::pair<std::string,std::string>> vars = {
{"Output target", output_target},
{"Output directory", output_dir},
{"Output basename", output_basename},
{"Output filename", output_filename},
{"Write file", write_files ? "true" : "false"}};
int verbose = 1;
for (auto [label, value] : vars) {
if (label == "Output filename") {
//verbose = 2;
}
(void)K::log(int(verbose), label + ":", value);
}
return {output_target, output_dir, output_basename, output_filename, write_files, display_only};
}

17
mac/command.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include <string>
//#include <filesystem>
#include <vector>
#include "file.h"
void set_verbose_level(int argc, char* argv[]);
bool show_usage(int argc, char* argv[]);
fs::path construct_command_pathname(char* command);
std::tuple<std::string, std::string, std::string, std::string, bool, bool>
parse_args(
const std::vector<std::string>& input_filenames, std::string target,
std::string output_basename, bool display_only);

56
mac/deftype.cpp Normal file
View File

@@ -0,0 +1,56 @@
#include "deftype.h"
#include "error.h"
defmode_t defmode_from_katom(katom_t type)
{
switch (type) {
case katom_t::klammer_definition:
case katom_t::klammer_instance:
return defmode_t::def_create;
case katom_t::klammer_override:
return defmode_t::def_override;
case katom_t::klammer_default:
return defmode_t::def_default;
default:
throw Internal_error("defmode_from_katom: not a definition type");
}
}
static const std::map<std::pair<defmode_t,defmode_t>, defmode_result> transition_table {
// create + create = error
{ {defmode_t::def_create, defmode_t::def_create},
{false, false, "Klammer NAME already defined at AT"} },
// create + override = replace with warning
{ {defmode_t::def_create, defmode_t::def_override},
{true, true, "Klammer NAME at AT overridden"} },
// create + default = ignore silently
{ {defmode_t::def_create, defmode_t::def_default},
{false, false, ""} },
// override + create = error
{ {defmode_t::def_override, defmode_t::def_create},
{false, false, "Klammer NAME already overridden at AT"} },
// override + override = replace with warning
{ {defmode_t::def_override, defmode_t::def_override},
{true, true, "Klammer NAME at AT overridden again"} },
// override + default = ignore silently
{ {defmode_t::def_override, defmode_t::def_default},
{false, false, ""} },
// default + create = replace silently
{ {defmode_t::def_default, defmode_t::def_create},
{true, false, ""} },
// default + override = replace with warning
{ {defmode_t::def_default, defmode_t::def_override},
{true, true, "Default klammer NAME at AT overridden"} },
// default + default = error
{ {defmode_t::def_default, defmode_t::def_default},
{false, false, "Default klammer NAME already defined at AT"} }
};
const defmode_result& defmode_transition(defmode_t existing, defmode_t incoming)
{
auto it = transition_table.find({existing, incoming});
if (it == transition_table.end()) {
throw Internal_error("defmode_transition: unknown combination");
}
return it->second;
}

17
mac/deftype.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include <string>
#include <map>
#include "ktype.h"
enum class defmode_t { def_create, def_override, def_default };
struct defmode_result {
bool replace;
bool warn;
std::string message;
};
defmode_t defmode_from_katom(katom_t type);
const defmode_result& defmode_transition(defmode_t existing, defmode_t incoming);

12
mac/env/lsan.supp vendored Normal file
View File

@@ -0,0 +1,12 @@
# LeakSanitizer suppressions for Klammertext debug builds.
#
# Suppress unactionable leaks from the embedded Python interpreter and
# OpenImageIO module initialization — one-time-init allocations that
# libraries conventionally leave for the OS to reclaim at exit. Real
# leaks in Klammertext's own C++ code are still reported.
#
# Each "leak:<pattern>" line suppresses any leak whose stack trace has
# a frame matching the substring (in a function name or library path).
leak:libpython
leak:OpenImageIO

68
mac/env/makefile.env vendored Normal file
View File

@@ -0,0 +1,68 @@
# mac/env/makefile.env — single cross-platform build environment for Klammertext.
#
# Included identically by every component Makefile:
# include $(KLAMMERTEXT_HOME)/mac/env/makefile.env
#
# The platform is auto-detected with uname; the compiler is chosen with
# make COMPILER=gcc (default)
# make COMPILER=clang
# There is no per-host/per-OS file and no DESKTOP_SESSION/HOST/SITE selector.
ifndef KLAMMERTEXT_HOME
$(error KLAMMERTEXT_HOME is not set -- source mac/env/runtime.env first)
endif
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
UNAME_S := $(shell uname -s)
CPP_VERSION := c++20
# Default compiler: clang on macOS, gcc elsewhere. gcc-built Klammertext
# binaries crash at runtime on macOS (a documented gcc/macOS codegen issue), so
# gcc there is only a compile-time conformance check (via `./dbg/rebuild.sh
# gcc`, which compiles under g++ then rebuilds with clang). Override with
# `make COMPILER=...`.
ifeq ($(UNAME_S),Darwin)
COMPILER ?= clang # gcc | clang
else
COMPILER ?= gcc # gcc | clang
endif
# Python: version- and platform-agnostic, no hardcoded paths.
PYTHON_INC := $(shell python3-config --includes)
PYTHON_LIB := $(shell python3-config --ldflags --embed)
# Flags common to all platforms.
CPPFLAGS = -I$(KLAMMERTEXT_HOME)/mac $(PYTHON_INC)
CXXFLAGS = -Wall -Wextra -Weffc++ -Wshadow -std=$(CPP_VERSION) -fPIC $(OPTIMIZE) $(SANITIZE)
ifeq ($(UNAME_S),Darwin)
# ---------- macOS (Apple Silicon) ----------
# Package-manager prefix, auto-detected: Homebrew (/opt/homebrew) or MacPorts
# (/opt/local). Override with `make MACOS_PREFIX=...`. Only the prefix differs
# between the two: the compiler is Apple Clang either way, and all Python flags
# come from python3-config (prefix-agnostic), so nothing else is PM-specific.
MACOS_PREFIX ?= $(shell test -d /opt/homebrew && echo /opt/homebrew || (test -d /opt/local && echo /opt/local || echo /opt/homebrew))
# Newest optional gcc (conformance check only): Homebrew g++-NN or MacPorts g++-mp-NN.
GCC := $(shell g=$$(ls $(MACOS_PREFIX)/bin/g++-[0-9]* $(MACOS_PREFIX)/bin/g++-mp-[0-9]* 2>/dev/null | sort -V | tail -1); echo $${g:-g++})
CLANG := clang++ # Apple clang (or brew/port llvm)
CPPFLAGS += -I$(MACOS_PREFIX)/include
LDFLAGS = -L$(MACOS_PREFIX)/lib
LDLIBS = $(if $(NOPYTHON),,$(PYTHON_LIB))
SHARED = -dynamiclib
SONAME = -install_name @rpath/$(notdir $@)
ORIGIN := @loader_path
EXPORT_DYNAMIC = -Wl,-export_dynamic
else
# ---------- Linux ----------
GCC := /usr/bin/g++
CLANG := $(shell command -v clang++-18 2>/dev/null || command -v clang++ 2>/dev/null)
LDFLAGS = -L/usr/lib/x86_64-linux-gnu
LDLIBS = -ldl $(if $(NOPYTHON),,$(PYTHON_LIB))
SHARED = -shared
SONAME = -Wl,-soname,$(notdir $@)
ORIGIN := $$ORIGIN
EXPORT_DYNAMIC = -rdynamic
endif
# Resolve the compiler choice (gcc by default; clang with COMPILER=clang).
CXX := $(if $(filter clang,$(COMPILER)),$(CLANG),$(GCC))

46
mac/env/makefile.env.hollis.DISABLED vendored Normal file
View File

@@ -0,0 +1,46 @@
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
CPP_VERSION = c++20
#GCC_LIB = /usr/lib/gcc/x86_64-linux-gnu/7.4.0
CXX = /usr/bin/g++
#IMPORT = -fmodules -fsearch-include-path bits/std.cc
IMPORT =
# Hack for now; to be generalized:
ifneq ("$(wildcard /usr/include/python3.14)","")
PYTHON = python3.14
else ifneq ("$(wildcard /usr/include/python3.13)","")
PYTHON = python3.13
else
PYTHON = python3.12
endif
# -no-pie?
CXXFLAGS = -Wall -Wextra -Weffc++ -fPIC $(PROFILE) -std=$(CPP_VERSION) $(IMPORT) $(OPTIMIZE) \
-I$(KLAMMERTEXT_HOME)/mac \
-I/usr/include \
-I/usr/include/$(PYTHON)
# -L$(GCC_LIB) \
LDFLAGS = \
-L/usr/lib/x86_64-linux-gnu
LDLIBS = \
-ldl
ifndef NOPYTHON
LDLIBS += -l$(PYTHON)
endif
#GCC_ROOT = /h/dev/pkg/gcc-$(GCC_VERSION)
#GCC_LIB = $(GCC_ROOT)/$(GCC_DIR)/lib/gcc/$(GCC_DIR)/$(GCC_VERSION)
#$(GCC_LIB)
LD_LIBRARY_PATH=\
/usr/lib64\
:/usr/lib/x86_64-linux-gnu

33
mac/env/makefile.env.jatke.DISABLED vendored Normal file
View File

@@ -0,0 +1,33 @@
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
CPP_VERSION = c++20
CXX = /usr/bin/g++
# Hack for now; to be generalized:
ifneq ("$(wildcard /usr/include/python3.14)","")
PYTHON = python3.14
else ifneq ("$(wildcard /usr/include/python3.13)","")
PYTHON = python3.13
else
PYTHON = python3.12
endif
CPPFLAGS = \
-I$(KLAMMERTEXT_HOME)/mac \
-I/usr/include \
-I/usr/include/$(PYTHON)
CXXFLAGS = -Wall -Wextra -Weffc++ -Wshadow -std=$(CPP_VERSION) -fPIC $(OPTIMIZE) $(SANITIZE)
LDFLAGS = \
-L/usr/lib/x86_64-linux-gnu
LDLIBS = \
-ldl
ifndef NOPYTHON
LDLIBS += -l$(PYTHON)
endif

33
mac/env/makefile.env.pop.DISABLED vendored Normal file
View File

@@ -0,0 +1,33 @@
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
CPP_VERSION = c++20
CXX = /usr/bin/g++
# Hack for now; to be generalized:
ifneq ("$(wildcard /usr/include/python3.14)","")
PYTHON = python3.14
else ifneq ("$(wildcard /usr/include/python3.13)","")
PYTHON = python3.13
else
PYTHON = python3.12
endif
CPPFLAGS = \
-I$(KLAMMERTEXT_HOME)/mac \
-I/usr/include \
-I/usr/include/$(PYTHON)
CXXFLAGS = -Wall -Wextra -Weffc++ -Wshadow -std=$(CPP_VERSION) -fPIC $(OPTIMIZE) $(SANITIZE)
LDFLAGS = \
-L/usr/lib/x86_64-linux-gnu
LDLIBS = \
-ldl
ifndef NOPYTHON
LDLIBS += -l$(PYTHON)
endif

33
mac/env/makefile.env.ubuntu.DISABLED vendored Normal file
View File

@@ -0,0 +1,33 @@
KLAMMERTEXT_HOME = /home/ack/projects/klammertext/K
include $(KLAMMERTEXT_HOME)/mac/env/optimize.env
CPP_VERSION = c++20
CXX = /usr/bin/g++
# Hack for now; to be generalized:
ifneq ("$(wildcard /usr/include/python3.14)","")
PYTHON = python3.14
else ifneq ("$(wildcard /usr/include/python3.13)","")
PYTHON = python3.13
else
PYTHON = python3.12
endif
CPPFLAGS = \
-I$(KLAMMERTEXT_HOME)/mac \
-I/usr/include \
-I/usr/include/$(PYTHON)
CXXFLAGS = -Wall -Wextra -Weffc++ -Wshadow -std=$(CPP_VERSION) -fPIC $(OPTIMIZE) $(SANITIZE)
LDFLAGS = \
-L/usr/lib/x86_64-linux-gnu
LDLIBS = \
-ldl
ifndef NOPYTHON
LDLIBS += -l$(PYTHON)
endif

16
mac/env/optimize.env vendored Normal file
View File

@@ -0,0 +1,16 @@
ifdef OPTIMIZE
# Performance build: make OPTIMIZE=1
# `override` so a command-line `make OPTIMIZE=1` is remapped to -O3 too;
# without it, a command-line assignment wins over this `:=` and the literal
# `1` leaks into CXXFLAGS (g++ then treats `1` as an input file).
override OPTIMIZE := -O3
SANITIZE :=
else ifdef NOPYTHON
OPTIMIZE := -O0 -g -DNOPYTHON
SANITIZE :=
else
# Debug build (default): includes AddressSanitizer
OPTIMIZE := -O0 -g
SANITIZE := -fsanitize=address -fno-omit-frame-pointer
endif

72
mac/env/runtime.env vendored Normal file
View File

@@ -0,0 +1,72 @@
# mac/env/runtime.env — single self-configuring runtime environment for Klammertext.
#
# Source this from your shell profile (~/.bashrc etc.):
# source /path/to/klammertext/K/mac/env/runtime.env
#
# This is the runtime counterpart of the shared mac/env/makefile.env: one file
# for every machine, with all machine-specific values AUTO-DETECTED. There is no
# per-host runtime.env.<host> file and no HOST/OS/SITE selector.
# - KLAMMERTEXT_HOME : derived from this file's own location (self-locating)
# - KLAMMERTEXT_TEXLIVE_BIN : newest ~/external/texlive/<year>/bin/<arch>
# Machine-unique, non-committable additions (extra library paths such as NVIDIA
# iray, local tools, etc.) go in an optional, gitignored mac/env/runtime.env.local
# sourced at the end -- NOT in this shared file, so one machine's bundled
# libraries can't shadow another's system libraries.
# --- KLAMMERTEXT_HOME: self-locate (bash sets BASH_SOURCE; zsh sets $0) --------
# This file lives at $KLAMMERTEXT_HOME/mac/env/runtime.env, so go up two levels.
_kt_self="${BASH_SOURCE[0]:-$0}"
export KLAMMERTEXT_HOME="$(cd "$(dirname "$_kt_self")/../.." && pwd)"
unset _kt_self
_kt_uname="$(uname -s)"
# --- KLAMMERTEXT_TEXLIVE_BIN: newest installed TeX Live under ~/external -------
# Klammertext's TeX Live installs are named by 4-digit year (2024, 2025, 2026).
# Match only those so unrelated dirs (e.g. a "texlive-2022-min" system install)
# are never selected; sort -V then picks the newest year.
if [ "$_kt_uname" = "Darwin" ]; then
_kt_arch="universal-darwin"
else
_kt_arch="$(uname -m)-linux" # e.g. x86_64-linux
fi
# Use `find` with a -path pattern, NOT a shell glob: on macOS/zsh an unmatched
# glob (no texlive installed) is a hard "no matches found" error, whereas find
# just returns nothing. The -path pattern keeps the 4-digit-year restriction.
_kt_tl="$(find "$HOME/external/texlive" -maxdepth 3 -type d \
-path "*/[0-9][0-9][0-9][0-9]/bin/$_kt_arch" 2>/dev/null \
| sort -V | tail -1)"
[ -n "$_kt_tl" ] && export KLAMMERTEXT_TEXLIVE_BIN="$_kt_tl"
unset _kt_tl _kt_arch
# --- PATH (TeX Live appended only if one was found) ---------------------------
export PATH="$KLAMMERTEXT_HOME/bin:$KLAMMERTEXT_HOME/tst${KLAMMERTEXT_TEXLIVE_BIN:+:$KLAMMERTEXT_TEXLIVE_BIN}:$PATH"
# --- Per-platform: LSan suppressions + shared library search path -------------
# The shared library path deliberately includes ONLY Klammertext's own .so dirs,
# NOT third-party bundled-library dirs. In particular NVIDIA iray's platforms/
# ships its own libfreetype.so and Qt6; if added here they shadow the system
# libraries, and iray's freetype drags in unversioned libharfbuzz.so/libbz2.so
# (absent without -dev packages), which breaks `import OpenImageIO` (SKS @image).
# Machine-specific library paths (iray included) go in runtime.env.local.
if [ "$_kt_uname" = "Darwin" ]; then
# macOS: no LD/DYLD_LIBRARY_PATH needed — libklammertext.so is found via the
# binaries' @loader_path rpath and document.so is dlopen'd by absolute path.
# LeakSanitizer is unsupported on macOS, so LSAN_OPTIONS does not apply.
:
else
# LSan suppressions for unactionable libpython/OpenImageIO leaks (see
# lsan.supp). Real leaks in Klammertext code are still reported; no effect
# in performance builds (OPTIMIZE=1, which disables ASan).
export LSAN_OPTIONS="suppressions=$KLAMMERTEXT_HOME/mac/env/lsan.supp:print_suppressions=0"
export LD_LIBRARY_PATH="$KLAMMERTEXT_HOME/mac:$KLAMMERTEXT_HOME/sks/kutil:$KLAMMERTEXT_HOME/sks/document:$KLAMMERTEXT_HOME/doc/handbook${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
fi
unset _kt_uname
# --- Optional per-machine escape hatch (gitignored, absent by default) ---------
# Use an if-block (not `[ -f ] && .`) so that when the local file is absent this
# file's final exit status is 0 -- otherwise `source runtime.env` returns
# non-zero, breaking `source runtime.env && ...` and `set -e` callers.
if [ -f "$KLAMMERTEXT_HOME/mac/env/runtime.env.local" ]; then
. "$KLAMMERTEXT_HOME/mac/env/runtime.env.local"
fi

35
mac/error.cpp Normal file
View File

@@ -0,0 +1,35 @@
#include "error.h"
#include "show.h"
#include "util.h"
#include "file.h"
bool display_source(const std::string& filename_arg)
{
if (filename_arg.empty()) {
return false;
} else {
fs::path filename = fs::path(filename_arg).filename();
return sks_commands.count(filename) == 0;
}
}
void Error::print_message(const std::string& epilog)
{
if (epilog != "")
//desc += " " + epilog + "\n";
m_desc += epilog + "\n";
if (m_just)
m_desc = justify(m_desc, 80, 0);
std::cerr << "\n" << red << command_name << " (" << m_type << " error)";
if (display_source(m_loc.m_filename)) {
std::cerr << ":\n " << m_loc.m_filename;
}
if (m_loc.m_line > -1) {
std::cerr << ", line " << m_loc.m_line;
}
if (m_loc.m_chr > -1) {
std::cerr << ", character " << m_loc.m_chr + 1;
}
std::cerr << ":\n\n" << m_desc << reset << "\n";
std::cout << reset;
}

77
mac/error.h Normal file
View File

@@ -0,0 +1,77 @@
#pragma once
#include <string>
#include "locator.h"
inline std::string command_name { "Command executed on the command line" };
inline std::string command_pathname { "Pathname of command executed on the command line" };
class Error : std::exception {
public:
Error(std::string error_type, std::string description,
Locator locator = Locator(), bool do_justify = true)
: m_type(error_type)
, m_desc(description)
, m_loc(locator)
, m_just(do_justify)
{}
void print_message(const std::string& epilog="");
std::string m_type {};
std::string m_desc {};
Locator m_loc; // {};
bool m_just { true };
};
class Parsing_error : public Error {
public:
explicit Parsing_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("parsing", description, locator, do_justify) {};
};
class File_error : public Error {
public:
explicit File_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("file", description, locator, do_justify) {};
};
class Target_error : public Error {
public:
explicit Target_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("target", description, locator, do_justify) {};
};
class Definition_error : public Error {
public:
explicit Definition_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("definition", description, locator, do_justify) {};
};
class Argument_error : public Error {
public:
explicit Argument_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("argument", description, locator, do_justify) {};
};
class Environment_error : public Error {
public:
explicit Environment_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("environment", description, locator, do_justify) {};
};
class Internal_error : public Error {
public:
explicit Internal_error(
const std::string& description, const Locator& locator=Locator(), bool do_justify=true)
: Error("internal", description, locator, do_justify) {};
};

159
mac/eval.cpp Normal file
View File

@@ -0,0 +1,159 @@
#include "util.h"
#include "eval.h"
#include "eval_python.h"
#include "eval_cpp.h"
#include "log.h"
#include "show.h"
#include "katom.h"
#include "file.h"
#include <unistd.h>
std::string shell(State state, std::string command, Locator loc)
{
command = state.subst(command);
FILE* pipe = popen(command.c_str(), "r");
if (!pipe) {
throw Parsing_error(
"Could not run command:\n" + command, loc, false);
}
char buffer[128];
std::string result = "";
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
result += buffer;
}
pclose(pipe);
// std::cout << "Command output:\n" << result << "\n";
return result;
}
bool is_haskell_file(const std::string& text)
{
std::string trimmed = trim(text);
if (trimmed.size() < 4) return false;
if (trimmed.find(' ') != std::string::npos) return false;
if (trimmed.find('\n') != std::string::npos) return false;
return trimmed.substr(trimmed.size() - 3) == ".hs";
}
std::string run_haskell(const std::string& hsfile, Locator loc)
{
std::string command = "runghc " + hsfile + " 2>&1";
FILE* pipe = popen(command.c_str(), "r");
if (!pipe) {
throw Parsing_error(
"Could not run runghc.", loc, false);
}
char buffer[128];
std::string result = "";
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
result += buffer;
}
int status = pclose(pipe);
if (status != 0) {
throw Parsing_error(
"Haskell evaluation failed:\n" + result, loc, false);
}
return result;
}
std::string haskell(State state, std::string code, Locator loc)
{
if (system("which runghc > /dev/null 2>&1") != 0) {
throw Parsing_error(
"@eval with the :haskell argument requires runghc, which was not found in PATH.\n"
"Install it using GHCup; see https://www.haskell.org/ghcup/install/.",
loc, false);
}
code = state.subst(code);
if (is_haskell_file(code)) {
return run_haskell(trim(code), loc);
}
const char* tmpdir = std::getenv("TMPDIR");
if (!tmpdir) tmpdir = "/tmp";
std::string tmpl = std::string(tmpdir) + "/klammertext_haskell_XXXXXX";
std::vector<char> tmppath(tmpl.begin(), tmpl.end());
tmppath.push_back('\0');
int fd = mkstemp(tmppath.data());
if (fd < 0) {
throw Parsing_error(
"Could not create temporary file for Haskell evaluation.", loc, false);
}
std::string hsfile = std::string(tmppath.data()) + ".hs";
close(fd);
rename(tmppath.data(), hsfile.c_str());
FILE* f = fopen(hsfile.c_str(), "w");
if (!f) {
unlink(hsfile.c_str());
throw Parsing_error(
"Could not write temporary Haskell file.", loc, false);
}
fprintf(f, "%s\n", code.c_str());
fclose(f);
std::string result = run_haskell(hsfile, loc);
unlink(hsfile.c_str());
return result;
}
void check_cpp_arguments(katom_list args, Locator loc)
{
if (args.size() != 4 && args.size() != 5) {
std::stringstream ss {};
ss << "Incorrect @eval format for a C++ function. Either:\n"
<< " @eval :cpp <library-basename> @\n"
<< "or\n"
<< " @eval :cpp <library-basename> <function-name> @\n"
<< "In the first case, the library basename is used for the function name.";
throw Argument_error(ss.str(), loc, false);
}
}
katom_list Eval::eval(katom_iter begin, katom_iter end)
{
(void)K::log(3, *begin, *(end - 1));
//msg() << "in Eval::eval:\n" << ktype << kall << kindex << std::pair(begin, end) << "\n";
katom_iter first = after_whitespace(begin + 1);
std::string eval_result = "[unevaluated]";
std::string first_word = first->m_text;
int offset = first_word[0] == ':' ? 1 : 0;
std::string command = as_string(first + offset, end - 1, true);
if (offset == 0 || first_word == ":python") { // Default is Python
Eval_python E_python(m_machine, begin->m_loc);
eval_result = E_python.eval(command);
} else if (first_word == ":shell") {
eval_result = shell(m_machine.m_state, command, begin->m_loc);
} else if (first_word == ":haskell") {
eval_result = haskell(m_machine.m_state, command, begin->m_loc);
} else if (first_word == ":cpp") {
//msg() << ":cpp: " << first_word << *(begin + 4) << "\n";
for (auto ki = begin; ki < end; ki++) {
//msg() << " " << kall << kindex << *ki << "\n";
}
katom_list args = text_katoms(begin, end);
check_cpp_arguments(args, begin->m_loc);
//msg() << "args: |" << args << "|\n";
std::string lib_text = args[2].m_text;
std::string khome = m_machine.m_state.value("KLAMMERTEXT_HOME", false);
if (!khome.empty()) {
lib_text = string_replace(lib_text, "*KLAMMERTEXT_HOME*", khome);
}
fs::path libpath(lib_text + ".so");
libpath = fs::absolute(libpath);
std::string funcname = args.size() == 4 ? libpath.stem().string() : args[3].m_text;
Eval_cpp E_cpp(m_machine, begin->m_loc);
eval_result = E_cpp.eval(libpath, funcname);
}
katom_list result {};
Machine M = m_machine;
M.read(eval_result);
M.apply(M.m_state.value("K_target"), false, false);
result = trim(M.m_katoms);
return result;
}

30
mac/eval.h Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include <string>
#include "machine.h"
#include "katom.h"
enum class eval_t {
shell,
python,
cpp,
};
class Eval
{
public:
explicit Eval(Machine& machine, Locator loc)
: m_machine(machine),
m_loc(loc)
{};
Eval(const Eval&) = delete;
Eval& operator=(const Eval&) = delete;
//~Eval_cpp();
std::vector<Katom> eval(
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
Machine m_machine;
Locator m_loc;
};

43
mac/eval_cpp.cpp Normal file
View File

@@ -0,0 +1,43 @@
#include <memory>
#include <dlfcn.h>
#include "eval_cpp.h"
#include "util.h"
#include "log.h"
#include "file.h"
#include "show.h"
Eval_cpp::Eval_cpp(Machine& machine, Locator loc)
: m_machine(machine)
, m_loc(loc)
{
(void)K::log(3);
}
std::string Eval_cpp::eval(fs::path library_path, std::string function_name)
{
(void)K::log(3, library_path, function_name);
// msg() << "library path: " << library_path.string().c_str() << "\n";
void* handle = dlopen(library_path.string().c_str(), RTLD_LAZY);
if (!handle) {
const char* error = dlerror();
std::string error_desc = error ? error : "unknown error";
throw File_error("Cannot open library: " + library_path.string() + "\n " + error_desc, m_loc);
}
dlerror();
typedef std::string (*func_t)(Machine);
func_t func = (func_t) dlsym(handle, function_name.c_str());
const char* dlsym_error = dlerror();
if (dlsym_error) {
std::string error_desc(dlsym_error);
dlclose(handle);
std::stringstream ss {};
ss << "Cannot load symbol " << function_name
<< " from library " << library_path << ":\n " << error_desc;
throw File_error(ss.str(), m_loc, false);
}
std::string result = func(m_machine);
dlclose(handle);
return result;
}

19
mac/eval_cpp.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include <string>
#include "machine.h"
class Eval_cpp
{
public:
explicit Eval_cpp(Machine& machine, Locator loc);
Eval_cpp(const Eval_cpp&) = delete;
Eval_cpp& operator=(const Eval_cpp&) = delete;
//~Eval_cpp();
std::string eval(fs::path library_path, std::string function_name);
Machine m_machine {};
Locator m_loc;
};

229
mac/eval_python.cpp Normal file
View File

@@ -0,0 +1,229 @@
#include "eval_python.h"
#include "show.h"
#include "util.h"
#include "log.h"
std::regex Eval_python::statement_delimiter("\\s*;\\s*");
Eval_python::Eval_python(Machine& machine, Locator loc)
: m_machine(machine)
, m_loc(loc)
, m_globals(nullptr)
, m_locals(nullptr)
{
(void)K::log(3);
// Only initialize if Python is not already initialized
if (!Py_IsInitialized()) {
#if PY_VERSION_HEX >= 0x030B0000
// Python 3.11+ uses PyConfig API
PyConfig config;
PyConfig_InitPythonConfig(&config);
Py_InitializeFromConfig(&config);
PyConfig_Clear(&config);
#else
Py_Initialize();
#endif
}
add_module_path("..");
add_module_path(".");
m_globals = PyDict_New();
m_locals = PyDict_New();
PyDict_SetItemString(m_globals, "__builtins__", PyEval_GetBuiltins());
import_module("inspect", false);
if (!m_machine.m_state.m_frames.empty()) {
PyRun_String(m_machine.m_state.python_code().c_str(), Py_file_input, m_globals, m_locals);
}
}
Eval_python::~Eval_python()
{
(void)K::log(3, "destructor");
// Clean up our objects BEFORE finalizing Python
if (m_globals) {
Py_DECREF(m_globals);
m_globals = nullptr;
}
if (m_locals) {
Py_DECREF(m_locals);
m_locals = nullptr;
}
// Don't call Py_Finalize() here - it can cause double-free if other
// Eval_python objects exist or if Python is used elsewhere.
// Python will clean up automatically at program exit.
}
std::string remove_string_values(std::string s)
{
return std::regex_replace(s, std::regex(R"(\".*?\")"), "\"\"");
}
void Eval_python::add_module_path(const std::string& path)
{
PyObject* sys_path = PySys_GetObject("path"); // Borrowed reference
if (sys_path) {
PyObject* py_path = PyUnicode_FromString(path.c_str());
if (py_path) {
PyList_Insert(sys_path, 0, py_path); // Insert at front for priority
Py_DECREF(py_path);
}
}
}
strings_t Eval_python::parse_modules(std::string code)
{
(void)K::log(3, code);
code = remove_string_values(code); // Hack! Don't look for module patterns in strings.
std::regex module_re(R"(([A-Za-z]\w*)\.[A-Za-z_]\w*)");
auto code_begin = std::sregex_iterator(code.begin(), code.end(), module_re);
auto code_end = std::sregex_iterator();
std::vector<std::string> modules;
for (std::sregex_iterator it = code_begin; it != code_end; ++it) {
modules.push_back((*it).str(1));
}
return modules;
}
void Eval_python::import_module(std::string module_name, bool verify)
{
(void)K::log(3, module_name);
std::string module_check =
"\"" + module_name + "\" in locals() and inspect.isclass(" + module_name + ")";
if (verify && eval_expression(module_check, false) == "True") {
return;
}
PyObject* module = PyImport_ImportModule(module_name.c_str());
if (module == nullptr) {
// Extract the Python traceback before clearing the error.
// This reveals the actual source of the failure (e.g., a syntax
// error in a transitively imported module), not just the top-level
// module name that failed to load.
std::string detail;
PyObject* ptype;
PyObject* pvalue;
PyObject* ptraceback;
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
if (pvalue) {
PyErr_NormalizeException(&ptype, &pvalue, &ptraceback);
PyObject* str = PyObject_Str(pvalue);
if (str) {
detail = PyUnicode_AsUTF8(str);
Py_DECREF(str);
}
// Format the traceback if available
if (ptraceback) {
PyObject* tb_module = PyImport_ImportModule("traceback");
if (tb_module) {
PyObject* format_tb = PyObject_GetAttrString(tb_module, "format_exception");
if (format_tb) {
PyObject* args = PyTuple_Pack(3, ptype, pvalue, ptraceback);
PyObject* tb_list = PyObject_CallObject(format_tb, args);
if (tb_list) {
PyObject* separator = PyUnicode_FromString("");
PyObject* joined = PyUnicode_Join(separator, tb_list);
if (joined) {
detail = PyUnicode_AsUTF8(joined);
Py_DECREF(joined);
}
Py_DECREF(separator);
Py_DECREF(tb_list);
}
Py_XDECREF(args);
Py_DECREF(format_tb);
}
Py_DECREF(tb_module);
}
}
}
Py_XDECREF(ptype);
Py_XDECREF(pvalue);
Py_XDECREF(ptraceback);
PyErr_Clear();
std::string message = "Cannot import module \"" + module_name + "\"";
if (!detail.empty()) {
message += ":\n\n" + detail;
}
throw Argument_error(message, m_loc, false);
}
// PyDict_SetItemString steals a reference, so we don't need to DECREF module
// The dictionary will own the reference
PyDict_SetItemString(m_globals, module_name.c_str(), module);
}
std::string Eval_python::get_result(PyObject* result_object)
{
std::string result {};
if (result_object) {
const char* value = PyUnicode_AsUTF8(result_object);
result = std::string(value);
Py_DECREF(result_object);
} else {
std::cout << red;
PyErr_Print();
throw Parsing_error("Python code error in @eval", m_loc);
}
return result;
}
std::string Eval_python::eval_expression(std::string expression, bool import_modules)
{
(void)K::log(3, expression);
// msg() << "expression: " << expression << "\n";
if (import_modules && expression.find('.') != std::string::npos) {
for (auto m : parse_modules(expression)) {
import_module(m);
}
}
return get_result(
PyRun_String(
std::string("str(" + expression +")").c_str(),
Py_eval_input, m_globals, m_locals));
}
std::string Eval_python::eval_statements(std::string script)
{
(void)K::log(3);
strings_t statements = regex_split(script, statement_delimiter);
for (auto iter = statements.begin(); iter < statements.end() - 1; iter++) {
(void)K::log(3, " Run: " + (*iter));
PyRun_String(iter->c_str(), Py_file_input, m_globals, m_locals);
}
(void)K::log(3, " Result from: " + statements.back());
return get_result(
PyRun_String(std::string("str("+statements.back()+")").c_str(),
Py_eval_input, m_globals, m_locals));
}
std::string Eval_python::eval(std::string code)
{
(void)K::log(3, code);
code = m_machine.m_state.subst(code, true);
/*
msg() << "\n"
<< std::string(80, '-') << "\n"
<< code << "\n"
<< std::string(80, '-') << "\n";
*/
if (std::regex_search(code, statement_delimiter)) {
return eval_statements(code);
} else {
return eval_expression(code);
}
}
std::string Eval_python::eval_katom_list(
katom_list& katoms, const katom_iter& begin, const katom_iter& end)
{
(void)K::log(3, katoms);
katom_iter code_begin = begin + 1;
katom_iter code_end = end - 1;
std::string code_result = eval(as_string(code_begin, code_end, true));
msg() << "code_result: " << code_result << "\n";
katom_list code_katoms = m_machine.process(code_result, command_name);
for (auto kiter = code_begin; kiter < code_end; kiter++) {
kiter->m_type = katom_t::replaced;
}
katoms.insert(end, code_katoms.begin(), code_katoms.end());
return code_result;
}

32
mac/eval_python.h Normal file
View File

@@ -0,0 +1,32 @@
#pragma once
#include <string>
#include <Python.h>
#include "machine.h"
class Eval_python
{
public:
static std::regex statement_delimiter;
explicit Eval_python(Machine& machine, Locator loc);
Eval_python(const Eval_python&) = delete;
Eval_python& operator=(const Eval_python&) = delete;
~Eval_python();
void add_module_path(const std::string& path);
std::vector<std::string> parse_modules(std::string code);
void import_module(std::string module_name, bool verify = true);
std::string get_result(PyObject* result_object);
std::string eval_expression(std::string expression, bool import_modules = true);
std::string eval_statements(std::string script);
std::string eval(std::string code);
//std::string eval_katom_list(const katom_iter& begin, const katom_iter& end);
std::string eval_katom_list(
std::vector<Katom>& katoms,
const std::vector<Katom>::iterator& begin, const std::vector<Katom>::iterator& end);
Machine m_machine;
Locator m_loc;
PyObject* m_globals;
PyObject* m_locals;
};

571
mac/file.cpp Normal file
View File

@@ -0,0 +1,571 @@
#include <fstream>
#include <algorithm>
#include <cstring>
#include "file.h"
#include "error.h"
#include "log.h"
#include "util.h"
#include "show.h"
std::string file_basename(const std::string& filename)
{
fs::path p(filename);
return p.stem().string();
}
std::string extension(const std::string& filename)
{
auto pos = filename.find_last_of(".");
if (pos != std::string::npos)
return filename.substr(pos + 1);
return "";
}
std::string file_directory(const std::string& filename)
{
fs::path p(filename);
return p.parent_path().string();
}
std::string absolute_pathname(const std::string& filename, const std::string& base)
{
fs::path p(filename);
if (filename.empty()) {
return fs::current_path().string();
}
if (!base.empty()) {
// Resolve filename relative to base's directory
fs::path b(base);
fs::path base_dir = fs::is_directory(b) ? b : b.parent_path();
p = base_dir / p;
}
return fs::absolute(p).string();
}
std::string relative_pathname(const std::string& filename)
{
/*
fs::path relative_to_current(const fs::path& input,
const fs::path& currentFile) {
const auto base = fs::absolute(currentFile).parent_path();
return fs::relative(fs::absolute(input), base);
*/
fs::path p(absolute_pathname(filename)); // + "/" + filename);
auto relpath = fs::relative(p, fs::current_path());
std::cout << "Absolute: " << fs::current_path() << " " << p << "->" << relpath << "\n";
return relpath.string();
}
size_t count_substrings(const std::string& text, const std::string& substring) {
size_t count = 0;
size_t pos = 0;
while ((pos = text.find(substring, pos)) != std::string::npos) {
count++;
pos += substring.length();
}
return count;
}
fs::path relative_to_cwd(const fs::path& input)
{
const auto base = fs::current_path();
std::error_code ec;
auto abs_input = fs::weakly_canonical(input, ec);
if (ec) abs_input = fs::absolute(input);
auto rel = fs::relative(abs_input, base, ec);
if (ec) rel = abs_input.lexically_relative(base);
auto result = rel.empty() ? abs_input : rel;
if (count_substrings(result.string(), "../") > 3) {
result = input;
}
return result;
}
std::string relative_pathname(const std::string& filename, std::string base)
{
fs::path p(absolute_pathname(filename));
auto relpath = relative(p, base);
relpath = relpath.lexically_normal();
return relpath.string();
}
bool file_exists(const std::string& pathname, bool error_if_not, bool is_directory)
{
fs::path p(pathname);
bool exists = fs::exists(p);
bool regular = fs::is_regular_file(p);
bool directory = fs::is_directory(p);
bool valid = exists and (regular or directory);
std::string filetype = is_directory ? "Directory " : "File";
Locator no_source("", -1, -1);
if (error_if_not and not valid) {
if (not exists)
throw File_error(filetype + " '" + pathname + "' does not exist", no_source);
else if (not is_directory and not regular)
throw File_error(filetype + " '" + pathname + "' is not a regular text file", no_source);
else if (is_directory and regular)
throw File_error(filetype + " '" + pathname + "' is not a directory", no_source);
}
return valid;
}
std::string string_from_file(const std::string& pathname, bool strip_surrounding_whitespace)
{
std::regex klammertext_filename_re { R"(.*\.kt?)" };
fs::path p(pathname);
std::string result {};
if (fs::exists(p)) {
if (fs::is_regular_file(p)) {
std::ifstream stream { pathname };
if (!stream.is_open()) {
throw File_error("Error opening file \"" + pathname + "\"", Locator("", -1, -1));
} else {
std::ostringstream buffer {};
stream >> std::noskipws >> buffer.rdbuf();
if (stream.fail() && !stream.eof()) {
throw File_error("Error reading file \"" + pathname + "\"", Locator("", -1, -1));
} else {
result = buffer.str();
result = string_replace(result, "\r\n", ""); // Urgh.
if (std::regex_match(pathname, klammertext_filename_re)) {
//std::cout << "Read Klammertext source file: " << pathname << "\n";
// result = encode(result);
} else {
//std::cout << "Read file: " << pathname << "\n";
}
if (strip_surrounding_whitespace)
result = trim(result);
return result;
}
}
} else {
throw File_error("File \"" + pathname + "\" is not a regular text file", Locator("", -1, -1));
}
} else {
throw File_error("File '" + pathname + "' does not exist", Locator("", -1, -1));
}
return result;
}
void string_to_file(const std::string& pathname, std::string contents)
{
fs::create_directories(file_directory(fs::absolute(pathname)));
std::ofstream out(pathname);
if (!out) {
throw File_error("Could not write file " + pathname);
}
out << contents;
out.close();
}
strings_t get_subdirectories(const std::string& s, std::regex name_match_re)
{
strings_t result {};
for (auto& p : fs::recursive_directory_iterator(s)) {
std::smatch match {};
//std::cout << "Check dir: " << p.path().string() << "\n";
std::string base = file_basename(p.path().string());
if (fs::is_directory(p) and std::regex_match(base, match, name_match_re))
result.push_back(p.path().string());
}
return result;
}
strings_t get_files_in_directory(const std::string& dir)
{
strings_t result {};
try {
for (const auto& entry : fs::directory_iterator(dir)) {
if (entry.is_regular_file()) {
// std::cout << entry.path().filename() << std::endl;
result.push_back(entry.path().filename());
}
}
} catch (const fs::filesystem_error& ex) {
std::cerr << "Error: " << ex.what() << std::endl;
}
return result;
}
std::string find_file(const std::string& basename, strings_t search_path, bool error_if_not_found)
{
bool found = false;
std::string pathname { "" };
for (const std::string& s : search_path) {
pathname = s + "/" + basename;
if (file_exists(pathname)) {
found = true;
break;
}
}
if (error_if_not_found and not found) {
std::stringstream ss {};
std::sort(search_path.begin(), search_path.end());
ss << "File with basename \"" << basename << "\" not found in search path:\n "
<< join(search_path, "\n ");
throw File_error(ss.str(), Locator(), false);
}
return pathname;
}
std::vector<fs::path>
find_file_recursive(const fs::path& root, const std::string& filename, bool only_one) //, Locator loc)
{
std::vector<fs::path> result {};
if (!fs::exists(root) || !fs::is_directory(root)) {
return result;
}
for (const auto& entry : fs::recursive_directory_iterator(root)) {
// msg() << "Entry: " << entry.path().string() << "\n";
if (entry.is_regular_file() && entry.path().filename() == filename) {
result.push_back(entry.path());
}
}
if (only_one && result.size() > 1) {
std::stringstream ss {};
ss << "More than one file named " + q_(filename) + " found:\n";
for (auto f : result) {
ss << " " << f << "\n";
}
std::cerr << ss.str();
throw File_error("More than one file named " + q_(filename) + " found");
}
return result;
}
std::vector<fs::path>
find_file_from_roots(const std::vector<std::string>& roots, const std::string& filename, bool only_one)
{
(void)K::log(3, filename);
std::vector<fs::path> result {};
for (std::string root : roots) {
std::vector<fs::path> filenames = find_file_recursive(root, filename, only_one);
for (auto f : filenames) {
if (std::ranges::count(result, f) == 0) {
result.push_back(f);
}
}
// result.insert(result.end(), filenames.begin(), filenames.end());
}
if (result.empty()) {
throw File_error("File \"" + filename + "\" not found");
}
if (only_one && result.size() > 1) {
std::stringstream ss {};
ss << "More than one file named " + q_(filename) + " found:\n";
for (auto f : result) {
ss << " " << f << "\n";
}
std::cerr << ss.str();
throw File_error("More than one file named " + q_(filename) + " found");
}
for (auto f : result) {
if (!fs::exists(f)) {
throw File_error("File \"" + filename + "\" does not exist");
}
}
return result;
}
fs::path klammertext_filename(const std::string& basename, bool error_if_missing, bool make_directory_if_missing)
{
std::string home { get_env_var("KLAMMERTEXT_HOME") };
std::string result = home + "/" + basename;
if (make_directory_if_missing)
fs::create_directory(file_directory(result));
if (error_if_missing and !file_exists(result, false, true)) {
throw File_error(
"Klammertext file does not exist: " + result);
}
return fs::path(result);
}
strings_t sks_dirs()
{
std::string home { get_env_var("KLAMMERTEXT_HOME") };
strings_t result = get_subdirectories(home + "/sks", std::regex(R"([a-zA-Z]\w*)"));
result.emplace(result.begin(), home + "/sks/kutil");
// result.push_back(home + "/doc/handbook"); // Not included in container yet
return result;
}
strings_t get_sks_directories(const std::string& s, bool include_argument)
{
strings_t result;
if (include_argument)
result.push_back(s);
for (auto& p : fs::recursive_directory_iterator(s)) {
auto basename = file_basename(p.path().string());
auto parent = p.path().parent_path();
if (fs::is_directory(p)
and basename[0] != '_'
and basename != "css"
and basename != "sty"
and basename != "js"
//and basename != "font"
and parent != "font"
and parent != "fonts")
result.push_back(p.path().string());
}
return result;
}
std::string cache_directory(std::string subdirectory, std::string parent_directory)
{
if (parent_directory.empty()) {
// /dev/shm is a fast RAM-backed tmpfs on Linux; it does not exist on
// macOS, so fall back to the platform temp directory there.
if (fs::is_directory("/dev/shm")) {
parent_directory = "/dev/shm";
} else {
parent_directory = fs::temp_directory_path().string();
}
}
std::string result = parent_directory + "/_klammertext_cache/" + subdirectory;
// msg() << "Cache directory: " << result << "\n";
return result;
}
std::time_t to_time_t(const fs::file_time_type& ftime)
{
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
return std::chrono::system_clock::to_time_t(sctp);
}
bool in_modification_order(std::string filename1, std::string filename2)
{
if ((!file_exists(filename1)) || (!file_exists(filename2))) {
return false;
} else {
auto time1 = fs::last_write_time(fs::path(filename1));
auto time2 = fs::last_write_time(fs::path(filename2));
return time1 < time2;
}
}
void write_to_cache(std::string cache_dir, std::string basename, std::string text)
{
if (!file_exists(cache_dir)) {
//std::cout << "Creating cache directory: " << cache_dir << "\n";
fs::create_directories(cache_dir);
}
// msg() << "Writing file to cache: " << basename << "\n";
std::string output_filename = cache_dir + "/" + basename;
string_to_file(output_filename, text);
}
std::string read_from_cache(std::string cache_dir, std::string basename)
{
std::string input_filename = cache_dir + "/" + basename;
// msg() << "Reading file from cache: " << input_filename << "\n";
return string_from_file(input_filename);
}
bool cache_requires_update(std::string cache_dir, std::string file_to_cache, std::string basename)
{
std::string cache_filename = cache_dir + "/" + basename;
return !in_modification_order(file_to_cache, cache_filename);
}
std::vector<fs::path> pathnames_with_extension(
const fs::path& dir, const std::string extension)
{
std::vector<fs::path> files;
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
if (!entry.is_regular_file()) continue;
auto ext = entry.path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (ext == "." + extension) files.push_back(entry.path());
}
return files;
}
/*
std::string find_file(const fs::path& root, const std::string& name)
{
//std::vector<fs::path> matches;
strings_t matches {};
auto normalize = [&](std::string s) {
if (!case_insensitive) return s;
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
return s;
};
const std::string norm_ext = normalize("." + ext);
for (const auto& entry : fs::recursive_directory_iterator(root)) {
if (!entry.is_regular_file())
continue;
//std::string entry_ext = normalize(entry.path().extension().string());
if (entry == name) {
matches.push_back(entry.path().string());
}
}
return matches[0];
}
*/
// std::vector<fs::path> find_files_with_extension(
strings_t find_files_with_extension(
const fs::path& root, const std::string& ext, bool case_insensitive)
{
//std::vector<fs::path> matches;
strings_t matches {};
auto normalize = [&](std::string s) {
if (!case_insensitive) return s;
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
return s;
};
const std::string norm_ext = normalize("." + ext);
for (const auto& entry : fs::recursive_directory_iterator(root)) {
if (!entry.is_regular_file())
continue;
std::string entry_ext = normalize(entry.path().extension().string());
if (entry_ext == norm_ext)
matches.push_back(entry.path().string());
}
return matches;
}
std::string combine_files(
std::vector<std::string> filenames,
std::string prolog, std::string epilog,
std::function <std::string(std::string)> processor)
{
std::string result = prolog;
for (std::string f : filenames) {
result += "\n/* " + file_basename(f) + " */\n";
result += string_from_file(f);
}
result += "\n" + epilog + "\n";
if (processor) {
result = processor(result);
}
return result;
}
bool files_differ(const fs::path& p1,
const fs::path& p2,
std::size_t buffer_size) // 64 KiB
{
// 1. Check existence and type
if (!fs::exists(p1) || !fs::exists(p2)) return true;
if (!fs::is_regular_file(p1) || !fs::is_regular_file(p2)) return true;
// 2. Compare sizes
auto s1 = fs::file_size(p1);
auto s2 = fs::file_size(p2);
if (s1 != s2) return true;
// 3. Open both files in binary mode
std::ifstream f1(p1, std::ios::binary);
std::ifstream f2(p2, std::ios::binary);
if (!f1 || !f2) return true; // treat I/O error as "different"
// 4. Compare in chunks
std::vector<char> buf1(buffer_size);
std::vector<char> buf2(buffer_size);
while (f1 && f2) {
f1.read(buf1.data(), buffer_size);
f2.read(buf2.data(), buffer_size);
std::streamsize r1 = f1.gcount();
std::streamsize r2 = f2.gcount();
if (r1 != r2) return true; // should not happen if sizes equal
if (r1 == 0) break; // EOF both
if (std::memcmp(buf1.data(), buf2.data(), static_cast<std::size_t>(r1)) != 0)
return true;
}
return false; // no difference found
}
// Copy a single file with an explicit binary read/write stream, forcing the
// destination to be world-readable. Deliberately NOT std::filesystem::copy_file
// or fs::copy: under Apple's `container` runtime the HTML output directory is a
// virtiofs bind mount, and libstdc++'s copy_file/copy create the destination
// with openat(O_WRONLY|O_CREAT|O_TRUNC, 0200) — a mode lacking the owner-read
// bit, which virtiofs rejects with EACCES (apple/container #1344, an OS-level
// Virtualization.framework bug), leaving a 0-byte --w------- file and aborting
// output. A stream copy creates the destination owner-readable and works
// identically on virtiofs, on in-VM filesystems, and under Docker. Used for
// every file Klammertext writes into the output tree (fonts, CSS, JS, ...).
void copy_file_stream(const fs::path& src, const fs::path& dst)
{
{
std::ifstream in(src, std::ios::binary);
if (!in)
throw File_error("Cannot read file for copy:\n " + src.string());
std::ofstream out(dst, std::ios::binary | std::ios::trunc);
if (!out)
throw File_error("Cannot create output file:\n " + dst.string());
// Guard against the empty-source failbit quirk of rdbuf insertion.
if (in.peek() != std::ifstream::traits_type::eof())
out << in.rdbuf();
out.flush();
if (!out || in.bad())
throw File_error("Failed to copy file:\n " + src.string()
+ "\n -> " + dst.string());
}
fs::permissions(dst,
fs::perms::owner_read | fs::perms::owner_write |
fs::perms::group_read | fs::perms::others_read,
fs::perm_options::replace);
}
void copy_preserving_basename(
strings_t filenames, std::string output_directory, std::string link_directory)
{
fs::path outdir(output_directory + "/" + link_directory);
fs::create_directories(outdir);
for (std::string filename : filenames) {
fs::path pname(filename);
auto out_path = outdir / pname.filename();
// Preserve the previous copy_options::update_existing behavior: skip
// when the destination already exists and is no older than the source.
if (fs::exists(out_path) &&
fs::last_write_time(out_path) >= fs::last_write_time(pname))
continue;
copy_file_stream(pname, out_path);
}
}
fs::path resolve_relative_to(const fs::path& relative, const fs::path& base)
{
fs::path base_dir = is_directory(base) ? base : base.parent_path();
return fs::weakly_canonical(base_dir / relative);
}

66
mac/file.h Normal file
View File

@@ -0,0 +1,66 @@
#pragma once
#include <string>
#include <vector>
#include <filesystem>
#include <functional>
namespace fs = std::filesystem;
std::string file_basename(const std::string& filename);
std::string extension(const std::string& filename);
std::string file_directory(const std::string& filename);
std::string absolute_pathname(const std::string& filename, const std::string& base="");
std::string relative_pathname(const std::string& filename);
std::string relative_pathname(const std::string& filename, const std::string& base);
fs::path relative_to_cwd(const fs::path& input);
bool file_exists(const std::string& pathname, bool error_if_not=false, bool is_directory=false);
std::string string_from_file(const std::string& pathname, bool strip_surrounding_whitespace=false);
void string_to_file(const std::string& pathname, std::string contents);
std::vector<std::string> get_files_in_directory(const std::string& dir);
std::string find_file(const std::string& basename, std::vector<std::string> search_path,
bool error_if_not_found=true);
std::vector<fs::path>
find_file_recursive(const fs::path& root, const std::string& filename,
bool only_one=true); //, Locator loc=Locator());
std::vector<fs::path>
find_file_from_roots(const std::vector<std::string>& roots, const std::string& filename, bool only_one);
fs::path klammertext_filename(
const std::string& basename, bool error_if_missing=true, bool make_directory_if_missing=false);
std::vector<std::string> sks_dirs();
std::vector<std::string> get_sks_directories(const std::string& s, bool include_argument=true);
std::string cache_directory(std::string subdirectory, std::string parent_directory="");
std::time_t to_time_t(const fs::file_time_type& ftime);
bool in_modification_order(std::string filename1, std::string filename2);
void write_to_cache(std::string cache_dir, std::string basename, std::string text);
std::string read_from_cache(std::string cache_dir, std::string basename);
bool cache_requires_update(std::string cache_dir, std::string file_to_cache, std::string basename);
std::vector<fs::path> pathnames_with_extension(
const fs::path& dir, const std::string extension
);
//std::string find_file(const fs::path& root, const std::string& name);
std::vector<std::string> find_files_with_extension(
const fs::path& root, const std::string& ext, bool case_insensitive=false);
std::string combine_files(
std::vector<std::string> filenames,
std::string prolog="", std::string epilog="",
std::function <std::string(std::string)> processor = nullptr);
bool files_differ(const fs::path& p1,
const fs::path& p2,
std::size_t buffer_size = 1 << 16); // 64 KiB
// Copy a single file with an explicit binary stream (NOT std::filesystem
// copy_file/copy), forcing the destination world-readable. Required for output
// onto Apple `container` virtiofs mounts — see the definition in file.cpp.
void copy_file_stream(const fs::path& src, const fs::path& dst);
void copy_preserving_basename(
std::vector<std::string> filenames, std::string output_directory, std::string link_directory);
fs::path resolve_relative_to(const fs::path& relative, const fs::path& base=std::filesystem::current_path());

1
mac/headers_only.mk Normal file
View File

@@ -0,0 +1 @@
HEADERS_ONLY := alias env

397
mac/katom.cpp Normal file
View File

@@ -0,0 +1,397 @@
#include <iostream>
#include <fstream>
#include <ranges>
#include <algorithm>
#include "katom.h"
#include "ktype.h"
#include "error.h"
#include "log.h"
#include "show.h"
#include "util.h"
#include "file.h"
bool dbg = false;
size_t Katom::index = 0;
Katom::Katom(const std::string& src, katom_t type, Locator loc)
: m_index(Katom::index++)
, m_text(src)
, m_src(src)
, m_loc(loc)
, m_type(type)
, m_initial_type(type)
{
if (m_type == katom_t::special && m_text[0] == '^') {
m_text.erase(0, 1);
}
}
int active_count(const katom_list& katoms)
{
return std::ranges::count_if(
katoms,
[] (const Katom& k) {
return k.m_type != katom_t::replaced
&& k.m_type != katom_t::ignored
&& k.m_type != katom_t::space
&& k.m_type != katom_t::newline;
});
}
bool active(const katom_list& katoms)
{
return active_count(katoms) > 0;
}
std::string expand_compound_katom(const std::string& s, std::regex rgx, const std::string& expanded)
{
if (s.find('-') != std::string::npos) { // Hyphen shortcut for arguments that allow it
const std::regex shortcut_rgx(R"((@\w+)(-\w+)+)");
std::smatch match {};
if (std::regex_match(s, match, shortcut_rgx)) {
std::string modified = string_replace(s, "-", " | ") + " @";
return std::regex_replace(modified, std::regex(R"((@\w+) \|)"), "$1");
}
}
return std::regex_replace(s, rgx, expanded);
}
katom_list make_katoms_from_word(std::string s, const std::string& source_desc, int line, int chr)
{
if (dbg) msg() << " make word: " << broken_bar << s << broken_bar << " [" << chr << "]\n";
auto ktyp = std::find_if(katom_types.begin(), katom_types.end(), [&](const auto& ktype) { return ktype.match(s); });
if (ktyp != katom_types.end()) {
return { Katom(s, ktyp->m_type, Locator(source_desc, line, chr)) };
} else {
if (dbg) {
std::cout << "\nBREAK: |" << s << "|\n";
}
for (const auto& [desc, rgx, expanded] : katom_rewrite_rules) {
std::string modified = expand_compound_katom(s, rgx.m_regex, expanded);
if (dbg) {
msg() << "Rewrite: " << desc << " " << rgx.m_pattern << " " << expanded << "\n";
}
if (modified != s) {
auto parts = word_split(modified);
if (dbg) std::cout << " Parts: " << parts << "\n";
if (show_rewrite_rules) {
Locator loc(source_desc, line, chr);
std::cout << loc << " Rewrite (" << desc << "): " << rgx.m_pattern
<< " " << right_arrow << " " << expanded << "\n";
}
katom_list klist {};
for (const std::string& p : parts) {
if (!p.empty()) {
auto ks = make_katoms_from_word(p, source_desc, line, chr);
std::copy(ks.begin(), ks.end(), std::back_inserter(klist));
}
}
return klist;
}
}
if (verbose_level > 0) {
std::cerr << command_name
<< " [warning]: Word not parsed in "
<< source_desc << ", line " << line+1 << ":\n"
<< " " << s << "\n"
<< "To include a special character (@, |, #, and ^), put \"^\" before it.\n";
//return std::vector{ std::make_shared<Katom>(s, Locator(), katom_t::word) };
//return std::vector{ std::make_shared<Katom>(s, katom_t::word, Locator(source_desc, line, chr)) };
}
return std::vector{ Katom(s, katom_t::word, Locator(source_desc, line, chr)) };
}
}
katom_list split_into_katoms(std::string s, const std::string& source, int source_line)
{
if (dbg) {
msg() << "Make katoms: " << s << "<\n";
}
// const std::string middle_dot { "\u00B7" };
s = string_replace(s, "\r", "");
s = string_replace(s, "\t", " ");
//std::regex words_regex("^\||[ ]|[\\n]|[^\\s]+|.+");
//std::regex words_regex(R"((?:[^][|])|[ ]|[\n]|[^\s]+|.+)");
std::regex words_regex(R"([ ]|[\n]|[^\s]+|.+)");
auto words_begin = std::sregex_iterator(s.begin(), s.end(), words_regex);
auto words_end = std::sregex_iterator();
if (dbg) {
std::cout << "Found " << std::distance(words_begin, words_end) << " words:\n";
}
strings_t atoms {};
for (std::sregex_iterator iter = words_begin; iter != words_end; ++iter) {
if (dbg) {
std::cout << middle_dot << iter->str();
}
atoms.push_back(iter->str());
}
if (dbg) std::cout << middle_dot << "\n";
//std::cout << kall << ktype;
katom_list result {};
int cpos = 0;
for (const auto& a : atoms) {
auto k = make_katoms_from_word(a, source, source_line, cpos);
for (auto& kk : k) {
Locator loc(source, source_line, cpos);
//kk->m_loc = loc;
kk.m_loc = loc;
//m_katoms.push_back(kk);
result.push_back(kk);
if (dbg) {
//std::cout << "LOOP: " << kk.m_text << " - " << cpos << "\n";
}
cpos += kk.m_src.size();
}
}
return result;
}
void restore_initial_type(katom_iter begin, katom_iter end)
{
std::for_each(
begin, end, [](Katom& k) { k.m_type = k.m_initial_type; });
}
void modify_type(katom_t new_type, katom_iter begin, katom_iter end)
{
std::for_each(
begin, end, [&new_type](Katom& k) { k.m_type = new_type; });
}
void modify_type(katom_t old_type, katom_t new_type, katom_iter begin, katom_iter end)
{
std::for_each(
begin, end, [&](Katom& k) { if (k.m_type == old_type) k.m_type = new_type; });
}
void ignore_whitespace(katom_iter& begin, katom_list& katoms)
{
constexpr bool _dbg = false;
if (begin < katoms.end()) {
katom_iter ki = begin;
if (_dbg) std::cout << "IGNORE_WHITESPACE: ";
auto end = katoms.end();
while (ki < end && ki->is_whitespace()) {
if (_dbg) std::cout << kall << ktype << *ki << sp_arrow;
ki->m_type = katom_t::ignored;
if (_dbg) std::cout << kignored << kall << ktype << *ki << " " << "\n";
++ki;
}
if (_dbg) std::cout << black << kreset;
}
}
katom_iter after_whitespace(katom_iter begin)
{
katom_iter result = begin;
while (result->is_whitespace()) {
result++;
}
return result;
}
std::vector<Katom> text_katoms(katom_iter& begin, katom_iter& end)
{
katom_list result {};
for (auto ki = begin; ki < end; ki++) {
if (!ki->is_whitespace()) {
auto k = *ki;
//msg() << " push: " << kindex << ktype << kall << kws << k << "\n";
result.push_back(k);
}
}
return result;
}
std::string as_string(std::vector<Katom>::const_iterator begin, std::vector<Katom>::const_iterator end, bool strip_whitespace)
{
auto include =
[&](katom_t t)
{ return t != katom_t::replaced and t != katom_t::ignored; };
std::string result {};
for (auto ki = begin; ki < end; ki++) {
katom_t t = ki->m_type;
if (include(t)) {
std::string text = !ki->m_display.empty() ? ki->m_display : ki->m_text;
result += text;
}
}
if (strip_whitespace)
result = trim(result);
return result;
}
std::string as_string(const katom_list& ks, bool strip_whitespace)
{
return as_string(ks.begin(), ks.end(), strip_whitespace);
}
katom_list trim(katom_list& katoms, std::set<katom_t> trim_types)
{
katom_iter begin =
std::find_if(katoms.begin(), katoms.end(),
[&trim_types](const Katom& k) { return !trim_types.contains(k.m_type); });
if (begin == katoms.end()) {
return katom_list{};
}
katom_iter end = katoms.end() - 1;
while (trim_types.contains(end->m_type)) {
--end;
}
return katom_list(begin, end+1);
}
katom_list trim(const katom_list& katoms, bool trim_inactive)
{
katom_list result = katoms;
std::set<katom_t> trim_types {katom_t::space, katom_t::newline};
if (trim_inactive) {
trim_types.insert(katom_t::replaced);
trim_types.insert(katom_t::ignored);
}
return trim(result, trim_types);
}
strings_t line_split(std::string s)
{
std::istringstream is(s);
strings_t result {};
std::string line;
while (std::getline(is, line)) {
result.push_back(line);
}
result.push_back("\n");
return result;
}
std::pair<std::string, strings_t> line_split(fs::path pathname)
{
std::string source {};
strings_t lines {};
std::ifstream infile(pathname);
std::string line;
while (std::getline(infile, line)) {
source += line + "\n";
lines.push_back(line);
}
if (!source.empty()) {
source.pop_back();
}
return {source, lines};
}
// I'm, like, this is
// some weird shit;
// what the fuck?
katom_list katomize(const strings_t& lines, const std::string& source_desc)
{
(void)K::log(3);
katom_list katoms {};
int i = 0;
for (std::string line : lines) {
(void)K::log(4, line);
katom_list ks = split_into_katoms(line + "\n", source_desc, ++i);
katoms.insert(katoms.end(), ks.begin(), ks.end());
}
katoms.pop_back();
return katoms;
}
// Whitespace
std::pair<katom_iter,katom_iter> whitespace_span(const katom_list& katoms, const katom_iter& start, katom_t start_type)
{
katom_iter ki = start;
if (ki != katoms.begin()) {
--ki;
while (ki != katoms.begin() && ki->is_whitespace()) {
--ki;
}
if (!ki->is_whitespace()) {
++ki;
}
}
katom_iter begin = ki;
while (ki != katoms.end() && (ki->is_whitespace() || ki->m_type == start_type
|| ki->m_type == katom_t::ignored || ki->m_type == katom_t::replaced)) {
++ki;
}
//katom_iter end = ki;
return {begin, ki}; //end};
}
std::vector<Katom> find_katoms_of_type(const katom_list& katoms, katom_t type)
{
auto result = katoms | std::views::filter([type](const Katom& k) { return k.m_type == type; });
return std::vector(result.begin(), result.end());
}
katom_iter find_katom_of_type(katom_iter begin, katom_iter end, katom_t type)
{
return std::find_if(begin, end, [&](const Katom& k) { return k.m_type == type; });
}
// #-
void remove_whitespace(katom_list& katoms) // Lint error
{
(void)K::log(3);
auto begin = katoms.begin();
while (begin < katoms.end()) {
auto ki = find_katom_of_type(begin, katoms.end(), katom_t::ws_remove);
if (ki == katoms.end()) break;
auto [b, e] = whitespace_span(katoms, ki, katom_t::ws_remove);
std::for_each(b, e, [](Katom& k) { k.m_type = katom_t::ignored; });
begin = e;
}
}
// #+n and #/n
int whitespace_arg(Katom& k)
{
int result = 1;
std::smatch match{};
if (std::regex_match(k.m_text, match, std::regex(R"(#[+/](\d*))"))) {
if (!match[1].str().empty()) {
result = stoi(match[1]);
}
}
return result;
}
void insert_whitespace(katom_list& katoms, katom_t type, const std::string& c)
{
(void)K::log(3);
for (Katom k : find_katoms_of_type(katoms, type)) {
katom_iter ki = find_katom(katoms.begin(), katoms.end(), k.m_index);
auto count = whitespace_arg(k);
auto [b, e] = whitespace_span(katoms, ki, type);
std::for_each(b, e, [](Katom& ka) { ka.m_type = katom_t::ignored; });
katom_list inserted = std::vector<Katom>{};
for (int i = 0; i < count; ++i) {
inserted.push_back(Katom(c, katom_t::ws_added, k.m_loc));
}
katoms.insert(e, inserted.begin(), inserted.end());
}
}
void process_whitespace_modifiers(katom_list& katoms)
{
(void)K::log(4);
remove_whitespace(katoms);
insert_whitespace(katoms, katom_t::ws_space, " ");
insert_whitespace(katoms, katom_t::ws_newline, "\n");
}
katom_list trim_whitespace(katom_list katoms)
{
return trim(katoms, {katom_t::space, katom_t::newline});
}

205
mac/katom.h Normal file
View File

@@ -0,0 +1,205 @@
#pragma once
#include <limits>
#include <memory>
#include "ktype.h"
#include "locator.h"
#include "util.h"
inline bool show_rewrite_rules = false;
class Katom
{
public:
static size_t index;
Katom(const std::string& src, katom_t type, Locator loc);
// Copy constructor
Katom(const Katom& other)
: m_index(other.m_index)
, m_text(other.m_text)
, m_src(other.m_src)
, m_loc(other.m_loc)
, m_type(other.m_type)
, m_initial_type(other.m_initial_type)
, m_display(other.m_display)
{}
// Copy assignment operator
Katom& operator=(const Katom& other) {
if (this != &other) {
m_index = other.m_index;
m_text = other.m_text;
m_src = other.m_src;
m_loc = other.m_loc;
m_type = other.m_type;
m_initial_type = other.m_initial_type;
m_display = other.m_display;
}
return *this;
}
static inline bool show_index;
static inline bool show_type;
static inline bool show_id;
static inline bool show_whitespace;
static inline bool show_all;
static inline bool show_replaced;
static inline bool show_ignored;
bool is_whitespace() const {
return m_initial_type == katom_t::space
|| m_initial_type == katom_t::newline
|| m_type == katom_t::ws_added; }
bool is_word() const { return m_initial_type == katom_t::word; }
bool is_text() const { return m_initial_type == katom_t::text; }
//bool is_text() { return m_initial_type == katom_t::text; }
bool is_literal() const { return m_type == katom_t::literal; }
bool is_active() const {
return m_type != katom_t::ignored
&& m_type != katom_t::replaced; };
bool is_nonascii() const {
return m_type == katom_t::nonascii; };
size_t m_index;
std::string m_text{};
std::string m_src{};
Locator m_loc;
katom_t m_type;
katom_t m_initial_type;
std::string m_display {};
};
bool active(const std::vector<Katom>& katoms);
int active_count(const std::vector<Katom>& katoms);
std::vector<Katom> split_into_katoms(std::string s, const std::string& source, int source_line);
void restore_initial_type(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
void modify_type(katom_t new_type, std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
void modify_type(katom_t old_type, katom_t new_type,
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
void ignore_whitespace(std::vector<Katom>::iterator& begin, std::vector<Katom>& katoms);
std::vector<Katom>::iterator after_whitespace(std::vector<Katom>::iterator begin);
std::vector<Katom> text_katoms(
std::vector<Katom>::iterator& begin, std::vector<Katom>::iterator& end);
std::string as_string(std::vector<Katom>::const_iterator begin, std::vector<Katom>::const_iterator end, bool strip_whitespace);
std::string as_string(const std::vector<Katom>& katoms, bool strip_whitespace);
std::vector<Katom> trim(std::vector<Katom>& katoms, std::set<katom_t> trim_types = {katom_t::space, katom_t::newline});
std::vector<Katom> trim(const std::vector<Katom>& katoms, bool trim_inactive = false);
std::vector<std::vector<Katom>> bar_split(std::vector<Katom>::iterator kbegin, std::vector<Katom>::iterator kend);
std::vector<std::string> line_split(std::string s);
std::pair<std::string, std::vector<std::string>> line_split(fs::path pathname);
std::vector<Katom> katomize(const std::vector<std::string>& lines, const std::string& source_desc);
void process_whitespace_modifiers(std::vector<Katom>& katoms);
std::vector<Katom> trim_whitespace(std::vector<Katom> katoms);
// escape_backslash() and unescape_backslash() (the ___BS___ hack) were
// removed. Backslash is now handled by the general target escape mechanism
// via the :escape parameter on @@@target.
inline bool is_bar(const Katom& k) {
return k.m_type == katom_t::bar;
}
inline bool is_nonascii(const Katom& k) {
return k.m_type == katom_t::nonascii;
}
inline bool is_newline(const Katom& k) {
return k.m_type == katom_t::newline;
}
inline bool is_ignore_rest(const Katom& k) {
return k.m_type == katom_t::ignore_rest;
}
inline bool is_ignore_line(const Katom& k) {
return k.m_type == katom_t::ignore_line;
}
inline void mark_as_replaced(Katom& k) {
k.m_type = katom_t::replaced;
}
inline void mark_as_literal(Katom& k) {
k.m_type = katom_t::literal;
}
inline void mark_as_ignored(Katom& k) {
k.m_type = katom_t::ignored;
}
inline bool begin_read(Katom& k) {
return k.m_type == katom_t::read_begin;
}
inline bool begin_ignore(const Katom& k) {
return k.m_type == katom_t::ignore_begin;
}
inline bool end_ignore(const Katom& k) {
return k.m_type == katom_t::ignore_end;
}
inline bool begin_literal(const Katom& k) {
return k.m_type == katom_t::literal_begin;
}
inline bool end_literal(const Katom& k) {
return k.m_type == katom_t::literal_end;
}
inline bool begin_eval(const Katom& k) {
return k.m_type == katom_t::eval_begin;
}
inline bool begin_cond(const Katom& k) {
return k.m_type == katom_t::cond_begin;
}
inline bool begin_klammer_def(const Katom& k) {
return k.m_type == katom_t::define_begin;
}
inline bool end_klammer_def(const Katom& k) {
return k.m_type == katom_t::define_end;
}
inline bool begin_klammer_apply(const Katom& k) {
return k.m_type == katom_t::apply_begin;
}
inline bool end_klammer_apply(const Katom& k) {
return k.m_type == katom_t::apply_end;
}
inline bool begin_machine_def(const Katom& k) {
return k.m_type == katom_t::machine_begin;
}
inline bool end_machine_def(const Katom& k) {
return k.m_type == katom_t::machine_end;
}
inline bool begin_apply(const Katom& k)
{
const std::set<katom_t> opens {
katom_t::read_begin,
katom_t::eval_begin,
katom_t::cond_begin,
katom_t::apply_begin};
return opens.contains(k.m_type);
}
inline bool end_apply(const Katom& k)
{
return k.m_type == katom_t::apply_end;
}

428
mac/katom_list.cpp Normal file
View File

@@ -0,0 +1,428 @@
// #include <algorithm>
#include <numeric>
#include "katom_list.h"
#include "util.h"
#include "ktype.h"
#include "file.h"
#include "log.h"
#include "show.h"
#include "character.h"
std::string to_string(std::vector<Katom>::const_iterator begin, std::vector<Katom>::const_iterator end, bool trim_result)
{
(void)K::log(3);
std::string result =
std::accumulate(
begin, end,
std::string(""),
[](const std::string& s, const Katom& k)
{ return k.is_active() ? s + k.m_text : s; });
if (trim_result) {
result = trim(result);
}
return result;
}
std::string to_string(const katom_list& katoms, bool trim_result)
{
return to_string(katoms.cbegin(), katoms.cend(), trim_result);
}
bool level_increase(const Katom& k)
{
return k.m_type == katom_t::define_begin
|| k.m_type == katom_t::literal_begin
|| k.m_type == katom_t::ignore_begin
|| k.m_type == katom_t::read_begin
|| k.m_type == katom_t::eval_begin
|| k.m_type == katom_t::cond_begin
|| k.m_type == katom_t::machine_begin
|| k.m_type == katom_t::apply_begin;
}
bool level_decrease(const Katom& k)
{
return k.m_type == katom_t::apply_end
|| k.m_type == katom_t::define_end
|| k.m_type == katom_t::literal_end
|| k.m_type == katom_t::ignore_end
|| k.m_type == katom_t::machine_end;
}
const katom_list::iterator
find_katom_named(const katom_list::iterator begin, const katom_list::iterator end, std::string name)
{
auto result = find_if(
begin, end, [&](const Katom& ki) {
return ki.m_text == ("@" + name); });
return result;
}
// Spans
//katom_iter get_katom_iterator(katom_list& katoms, size_t index)
const katom_list::iterator
find_katom(const katom_list::iterator begin, const katom_list::iterator end, size_t index)
{
// msg() << "find_katom: " << std::pair(begin, end) << "\n";
auto result = find_if(
begin, end, [&](const Katom& ki) {
return ki.m_index == index && ki.m_type != katom_t::ignored && ki.m_type != katom_t::replaced; });
if (result == end) {
std::stringstream ss {};
ss << "Katom with index " << index << " not found in katom list";
throw Internal_error(ss.str(), begin->m_loc);
} else {
return result;
}
}
std::pair<katom_iter, katom_iter>
find_span_katoms(katom_list& katoms, const Katom& begin, const Katom& end) // Lint error
{
//(void)K::log(3, begin, end);
//static std::mutex katoms_mutex;
//std::lock_guard<std::mutex> lock(katoms_mutex);
katom_iter kbegin = find_katom(katoms.begin(), katoms.end(), begin.m_index);
katom_iter kend = find_katom(kbegin, katoms.end(), end.m_index) + 1;
//(void)K::log(3, kbegin->m_text, (kbegin+1)->m_text, (kbegin+2)->m_text, "...", kend->m_text);
//(void)K::log(3, "resolved:", kbegin, (kbegin+2), "..."); //, kend);
(void)K::log(3, "resolved:", kbegin, kend - 1);
return { kbegin, kend };
}
std::pair<katom_iter, katom_iter>
find_span_katoms(katom_iter katoms_begin, katom_iter katoms_end, const Katom& begin, const Katom& end) // Lint error
{
katom_iter kbegin = find_katom(katoms_begin, katoms_end, begin.m_index);
katom_iter kend = find_katom(katoms_begin, katoms_end, end.m_index) + 1;
//(void)K::log(3, kbegin->m_text, (kbegin+1)->m_text, (kbegin+2)->m_text, "...", kend->m_text);
//(void)K::log(3, "resolved:", kbegin, (kbegin+2), "..."); //, kend);
(void)K::log(3, "resolved:", kbegin, kend - 1);
return { kbegin, kend };
}
void missing_open(const Katom& k, bool error_exit)
{
(void)K::log(2);
std::stringstream ss {};
ss << "A klammer ends without a beginning: " << k;
if (error_exit) {
throw Parsing_error(ss.str(), k.m_loc, false);
} else {
std::cout << " " << ss.str() << "\n";
}
}
//void missing_close(std::vector<katom_ptr>& bounds, bool error_exit)
void missing_close(const katom_list& bounds, bool error_exit)
{
(void)K::log(2);
std::stringstream ss {};
if (bounds.size() == 1) {
ss << " Beginning of a span that does not end:\n";
} else {
ss << " A span ends that does not have a beginning:\n";
}
for (const auto& k : bounds) {
ss << " " << k.m_loc << " " << k.m_src << "\n";
}
if (error_exit) {
throw Parsing_error(ss.str(), bounds[0].m_loc, false);
} else {
std::cout << ss.str() << "\n";
}
}
void bad_close(const Katom& open, const Katom& close, bool error_exit)
{
(void)K::log(2);
std::stringstream ss {};
ss << " Klammer begins with " << open << " but ends with " << close << ".\n"
<< " " << open.m_loc << " " << open << "\n"
<< " " << close.m_loc << " " << close;
if (error_exit) {
throw Parsing_error(ss.str(), open.m_loc, false);
} else {
std::cout << ss.str() << "\n";
}
}
std::string trim_span_markers(const Katom& katom)
{
// Trim @ as well as ^ (for literal span)
return trim_char(trim_char(katom.m_text, '@'), '^');
}
void check_named_katom_span(const Katom& begin, const Katom& end)
{
(void)K::log(4);
std::string end_text = trim_span_markers(end);
if (!end_text.empty() &&
//!(begin.m_type == katom_t::ignore_begin && end.m_type == katom_t::ignore_end)) {
!(begin_ignore(begin) && end_ignore(end))) {
std::string begin_text = trim_span_markers(begin);
if (begin_text != end_text) {
std::stringstream ss;
ss << " A named end katom does not match:\n"
<< " " << begin.m_loc << " " << begin << "\n"
<< " " << end.m_loc << " " << end;
throw Parsing_error(ss.str(), end.m_loc, false);
}
}
}
spans_t find_spans(
katom_iter begin, katom_iter end,
std::function<bool(const Katom&)> level_inc,
std::function<bool(const Katom&)> level_dec,
bool error_exit,
std::string name)
{
(void)K::log(4, name);
bool _dbg = false;
spans_t spans {};
katom_list bounds {};
for (auto k = begin; k < end; k++) {
if (_dbg) msg() << ktype << kall << kignored << kreplaced << " " << k << " type: " << type_to_name(k->m_type) << "\n";
if (level_inc(*k)) {
if (_dbg) msg() << boldblack << " level_inc: " << k << black << "\n";
bounds.push_back(*k);
} else if (level_dec(*k)) {
if (_dbg) msg() << boldblack << " level_dec: " << k << " bounds: " << bounds << black << "\n";
if (!bounds.empty()) {
auto span_start = bounds.back();
if (_dbg) msg() << " span_start of this level: " << span_start << "\n";
if (katom_spans[span_start.m_type] == k->m_type) {
check_named_katom_span(span_start, *k);
spans.push_back({std::move(span_start), std::move(*k)});
bounds.pop_back();
} else {
bad_close(span_start, *k, error_exit);
}
} else {
missing_open(*k, error_exit);
}
}
}
if (!bounds.empty()) {
missing_close(bounds, error_exit);
}
if (_dbg) msg() << black;
// Post-order invariant: if span A is nested inside span B, then A
// appears before B in the list. This is a necessary consequence of
// stack-based bracket matching and is required for correct inside-out
// evaluation of nested structures. See doc/taxonomy.md, "Spans".
if (verbose_level >= 2) {
for (size_t i = 0; i < spans.size(); i++) {
for (size_t j = i + 1; j < spans.size(); j++) {
bool j_inside_i =
spans[j].first.m_index > spans[i].first.m_index &&
spans[j].second.m_index < spans[i].second.m_index;
if (j_inside_i) {
std::stringstream ss;
ss << "Post-order span invariant violated: span "
<< j << " (" << spans[j].first << ")"
<< " is nested inside span "
<< i << " (" << spans[i].first << ")"
<< " but appears after it";
throw Internal_error(ss.str(), spans[j].first.m_loc);
}
}
}
}
return spans;
}
spans_t find_spans(
katom_list& katoms,
std::function<bool(const Katom&)> level_inc,
std::function<bool(const Katom&)> level_dec,
bool error_exit,
std::string name)
{
return find_spans(katoms.begin(), katoms.end(), level_inc, level_dec, error_exit, name);
}
void describe_spans(const katom_list& katoms)
{
katom_list non_const_katoms = katoms;
auto spans = find_spans(non_const_katoms, level_increase, level_decrease);
std::vector<std::tuple<std::string, Katom, Katom>> span_specs {};
size_t width = 0;
std::string margin = " ";
string_map relpath {};
for (auto [op,cl] : spans) {
std::string loc = locator_range(op.m_loc, cl.m_loc, relpath);
width = std::max(width, loc.size());
span_specs.push_back(std::make_tuple(margin + loc, op, cl));
}
width += margin.size();
for (auto [span,op,cl] : span_specs) {
std::cout << std::setw(width) << std::left << span << " "
<< ktype << kreplaced
<< op << right_arrow << cl << "\n";
}
}
// Nonascii
void encode_nonascii_characters(katom_list& katoms)
{
for (Katom& k : katoms) {
if (is_nonascii(k)) {
k.m_text = encode(k.m_text);
}
}
}
// Literal
void mark_literal_katoms(katom_list& katoms)
{
(void)K::log(4);
for (auto [op, cl] : find_spans(katoms, begin_literal, end_literal, true, "literal")) {
auto [begin, end] = find_span_katoms(katoms, op, cl);
//if (begin->m_type == katom_t::literal_begin) {
if (begin_literal(*begin)) {
//begin->m_type = katom_t::replaced;
mark_as_replaced(*begin);
//end->m_type = katom_t::replaced;
mark_as_replaced(*(end - 1));
//std::for_each(begin + 1, end, [](Katom& k) { k.m_type = katom_t::literal; });
std::for_each(begin + 1, end - 1, mark_as_literal);
}
}
}
// Ignore
void mark_ignored_katoms(katom_list& katoms)
{
(void)K::log(4);
//auto is_ignore_begin = [](const Katom& k) { return k.m_type == katom_t::ignore_begin; };
//auto is_ignore_end = [](const Katom& k) { return k.m_type == katom_t::ignore_end; };
for (auto [op,cl] : find_spans(katoms, begin_ignore, end_ignore, true, "ignored")) {
auto [begin, end] = find_span_katoms(katoms, op, cl);
// std::for_each(begin, end + 1, [](Katom& k) { k.m_type = katom_t::ignored; });
std::for_each(begin, end + 1, mark_as_ignored);
//if (end + 1 != katoms.end() && (end+1)->m_type == katom_t::newline) {
if (end + 1 != katoms.end() && is_newline(*(end + 1))) {
//(end+1)->m_type = katom_t::ignored;
mark_as_ignored(*(end + 1));
}
}
bool ignore_line = false;
bool ignore_rest = false;
std::vector<katom_iter> after_line {};
for (Katom& k : katoms) {
//if (k.m_type == katom_t::ignore_rest) {
if (is_ignore_rest(k)) {
ignore_rest = true;
// } else if (k.m_type == katom_t::ignore_line) {
} else if (is_ignore_line(k)) {
ignore_line = true;
}
//if (!ignore_rest && ignore_line && k.m_type == katom_t::newline) {
if (!ignore_rest && ignore_line && is_newline(k)) {
//k.m_type = katom_t::ignored;
mark_as_ignored(k);
ignore_line = false;
}
if (ignore_line or ignore_rest) {
//k.m_type = katom_t::ignored;
mark_as_ignored(k);
}
}
size_t i = 0;
while (i < katoms.size()) {
if (katoms[i].m_initial_type == katom_t::ignore_end) {
// std::cout << "IGNORED: " << kall << ktype << kignored << katoms[i] << " " << katoms[i+1] << black << "\n";
i++;
while (i < katoms.size() && katoms[i].is_whitespace()) {
katoms[i].m_type = katom_t::ignored;
i++;
}
} else {
i++;
}
}
}
// Klammers:
void process_klammer_katoms(katom_list& katoms)
{
(void)K::log(4);
for (auto [op, cl] : find_spans(katoms, begin_klammer_def, end_klammer_def , true, "define")) {
auto [begin, end] = find_span_katoms(katoms, op, cl);
for (auto k = begin + 1; k < end - 1; k++) {
//k->m_type = katom_t::literal;
mark_as_literal(*k);
}
}
}
// Application: read, cond, and defined klammers
// Cond
/*
void check_bar_count(katom_iter begin, katom_iter end)
{
//int count = std::count_if(begin, end, [](const Katom& k) { return k.m_type == katom_t::bar; });
int count = std::count_if(begin, end, is_bar); //[](const Katom& k) { return k.m_type == katom_t::bar; });
if (count != 1 && count != 2) {
std::stringstream ss {};
ss << "Incorrectly formatted @cond klammer. There should only be one or two bar characters:\n"
<< " @cond <predicate> | <result-if-true @\nor:\n"
<< " @cond <predicate> | <result-if-true> | <result-if-false> @";
throw Argument_error(ss.str(), begin->m_loc, false);
}
}
bool is_true(const std::string& s)
{
return s == "True" || s == "true" || s == "1";
}
void process_cond_katoms(katom_list& katoms)
{
if (std::find_if(katoms.begin(), katoms.end(), begin_cond) != katoms.end()) {
(void)K::log(3);
//for (auto [op, cl] : find_spans(katoms, begin_cond, end_apply, true, "cond")) {
for (auto [op, cl] : find_spans(katoms, level_increase, level_decrease, true, "cond")) {
auto [begin, end] = find_span_katoms(katoms, op, cl);
// msg() << "find_spans: " << std::pair(begin, end) << "\n";
if (begin_cond(*begin)) {
check_bar_count(begin, end);
auto bar_1 = std::find_if(begin, end, is_bar);
std::string predicate = to_string(begin + 1, bar_1, true);
auto bar_2 = std::find_if(bar_1 + 1, end - 1, is_bar);
katom_list true_clause {};
katom_list false_clause {};
if (bar_2->m_type == katom_t::bar) {
true_clause = katom_list(bar_1 + 1, bar_2);
false_clause = katom_list(bar_2 + 1, end - 1);
} else {
true_clause = katom_list(bar_1 + 1, end - 1);
}
katom_list result = is_true(predicate)
? trim_whitespace(true_clause) : trim_whitespace(false_clause);
std::for_each(begin, end, mark_as_replaced);
katoms.insert(end, result.begin(), result.end());
}
}
}
}
*/

52
mac/katom_list.h Normal file
View File

@@ -0,0 +1,52 @@
#pragma once
#include "katom.h"
#include "state.h"
std::string to_string(std::vector<Katom>::const_iterator begin, std::vector<Katom>::const_iterator end, bool trim_result=false);
std::string to_string(const std::vector<Katom>& katoms, bool trim_result=false);
// std::vector<Katom> process_katoms(
// const std::string& s, State& state, const std::string& source,
// bool nonascii=true, bool literal=true, bool ignore=true, bool whitespace=true,
// bool klammers=true, bool eval=true, bool cond=true, bool read=true);
bool level_increase(const Katom& k);
bool level_decrease(const Katom& k);
const katom_list::iterator
find_katom_named(const katom_list::iterator begin, const katom_list::iterator end, std::string name);
const std::vector<Katom>::iterator
find_katom(const std::vector<Katom>::iterator begin, const std::vector<Katom>::iterator end, size_t index);
std::vector<std::pair<Katom, Katom>>
find_spans(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end,
std::function<bool(const Katom&)> level_inc,
std::function<bool(const Katom&)> level_dec,
bool error_exit,
std::string name);
std::vector<std::pair<Katom, Katom>>
find_spans(std::vector<Katom>& katoms,
std::function<bool(const Katom&)> level_inc,
std::function<bool(const Katom&)> level_dec,
bool error_exit=true,
std::string name="all");
void describe_spans(const std::vector<Katom>& katoms);
std::pair<std::vector<Katom>::iterator, std::vector<Katom>::iterator>
find_span_katoms(std::vector<Katom>& katoms, const Katom& begin, const Katom& end);
std::pair<std::vector<Katom>::iterator, std::vector<Katom>::iterator>
find_span_katoms(
std::vector<Katom>::iterator kbegin, std::vector<Katom>::iterator kend, const Katom& begin, const Katom& end);
void encode_nonascii_characters(std::vector<Katom>& katoms);
void mark_literal_katoms(std::vector<Katom>& katoms);
void mark_ignored_katoms(std::vector<Katom>& katoms);
void process_klammer_katoms(std::vector<Katom>& katoms);

493
mac/klammer.cpp Normal file
View File

@@ -0,0 +1,493 @@
#include <algorithm>
#include "klammer.h"
#include "show.h"
#include "log.h"
#include "argument_set.h"
#include "eval.h"
#include "util.h"
#include "character.h"
using namespace std::literals;
std::regex Klammer::name_re = std::regex(R"((\w+)(?:\.(\w+))?)");
std::tuple<std::string, std::string>
parse_name(Target_set targets, Katom name_katom)
{
std::string name_with_target = trim_char(name_katom.m_text, '@');
std::smatch match {};
if (!std::regex_match(name_with_target, match, Klammer::name_re)) {
throw Parsing_error(
"The klammer name \"" + name_with_target + "\" is not correctly defined. "
"The form is \"<klammer-name>\" for general klammers or \"<klammer-name>.<target-name>\" "
"for a specialized target. The klammer defined as \"<klammer-name>.k\" specifies the "
"arguments and contains a description of the klammer in the definition body.",
name_katom.m_loc);
}
std::string klammer_name = match[1];
std::string target_name = match[2];
if (target_name.empty()) {
target_name = Target_set::general_name;
}
if (!targets.has(target_name)) {
throw Target_error(
"The target \"" + target_name + "\" in klammer definition \"" + name_with_target + "\" "
"is not defined. Enter \"kdesc -t\" to see the targets defined by the Standard Klammer Set.",
name_katom.m_loc);
}
return { klammer_name, target_name };
}
std::tuple<Katom, Parameter_set, katom_list, Locator>
parse_definition_katoms(std::string klammer_name, Argtype_set argtypes, katom_iter& begin, katom_iter& end)
{
(void)K::log(3, *begin, *(end - 1));
katom_iter deftype = std::find_if(
begin, end, [](const Katom& k) { return is_deftype(k.m_type); });
if (deftype == end) {
throw Argument_error(
"The klammer definition does not contain a definition separator that specifies\n"
"how the klammer should be defined. The klammer syntax is:\n\n"
" @@<name>[.<target>] <parameters> : <body> @@ definition\n"
" @@<name>[.<target>] :: <body> @@ instance (uses .k parameters)\n"
" @@<name>[.<target>] ::: <body> @@ override existing definition\n"
" @@<name>[.<target>] <parameters> :::: <body> @@ default (can be overridden)",
begin->m_loc, false);
}
katom_list parameter_katoms(begin, deftype);
// Resolve text-removal markers (#, #[...]#, ##) in the parameter list.
// The interior of a @@...@@ definition is held verbatim until now (so a
// literal klammer body receives its content untouched), which means the
// parameter declarations still contain any removal markers the writer
// used. Apply removal here, to the parameters only -- the body is left
// verbatim by add_target_definition.
mark_ignored_katoms(parameter_katoms);
std::erase_if(parameter_katoms,
[](const Katom& k) { return k.m_type == katom_t::ignored; });
parameter_katoms = trim_whitespace(parameter_katoms);
if ((deftype->m_type == katom_t::klammer_instance ||
deftype->m_type == katom_t::klammer_override) &&
!parameter_katoms.empty()) {
std::string sym = deftype->m_type == katom_t::klammer_instance ? "::" : ":::";
throw Definition_error(
"The \"" + klammer_name + "\" klammer uses the \"" + sym + "\" symbol but defines parameters.",
begin->m_loc);
}
Parameter_set parameters(parameter_katoms, argtypes);
katom_list body_katoms(deftype + 1, end - 1);
body_katoms = trim_whitespace(body_katoms);
return { *deftype, parameters, body_katoms, begin->m_loc };
}
void Klammer::add_target_definition(
std::string target_name, Argtype_set argtypes, katom_iter begin, katom_iter end)
{
(void)K::log(3, *begin, *(end-1));
auto [deftype, parameters, body, loc] =
parse_definition_katoms(m_name, argtypes, begin, end); // targets, begin, end);
// msg() << "Klammer " << m_name << " add: " << target_name << "\n";
// parameters.describe_parameters();
std::regex variable_re(R"(\*(\w+)\*)");
int i = 0;
variable_map_t varmap {};
for (auto k : body) {
std::smatch match {};
std::string txt = k.m_text;
while (std::regex_search(txt, match, variable_re) &&
(k.m_type == katom_t::karg || k.m_type == katom_t::text)) {
//std::cout << " txt: " << txt << "\n";
std::string varname = match[1];
//m_varmap[target_name][varname].push_back(i);
varmap[varname].push_back(i);
txt = std::regex_replace(txt, std::regex(R"(\*)" + varname + R"(\*)"), "");
}
i++;
}
// m_body[target_name] = body;
m_defs.push_back({target_name, deftype.m_initial_type, parameters, body, varmap, begin->m_loc});
m_defloc[target_name] = begin->m_loc;
m_defmode[target_name] = defmode_from_katom(deftype.m_initial_type);
// std::cout << "Klammer " << m_name << "." << target_name << ": " << m_defloc[target_name] << "\n";
}
void Klammer::remove_target_definition(const std::string& target_name)
{
std::erase_if(m_defs,
[&target_name](const auto& def) { return def.target == target_name; });
m_defloc.erase(target_name);
m_defmode.erase(target_name);
m_body.erase(target_name);
m_varmap.erase(target_name);
}
// Rationalize multiple definitions
std::string error_list(std::string label, auto components, std::string after="")
{
std::stringstream ss {};
ss << label << ":\n";
for (auto c : components) {
ss << " " << c.loc.desc() << "\n";
}
ss << after;
return ss.str();
}
auto Klammer::target_defs(std::vector<std::string> target_names)
{
std::vector<Klammer::components> defs {};
for (auto target : target_names) {
auto target_defs = collect_if(
m_defs, [target](const auto& def) { return def.target == target; });
defs.insert(defs.end(), target_defs.begin(), target_defs.end());
}
return defs;
}
auto Klammer::instance_defs()
{
return collect_if(
m_defs,
[] (const auto& def) {
return def.deftype == katom_t::klammer_instance
|| def.deftype == katom_t::klammer_override; });
}
void Klammer::disallow_instances() //Klammer::components declaration)
{
auto instances = instance_defs();
if (!instances.empty()) {
int icount = instances.size();
std::stringstream ss {};
ss << "There " << to_be(icount) << " " << icount << " "
<< plural("instance", icount) << " (defined by \"::\"), but "
<< "no declarations (defined by a \".k\" target)";
throw Definition_error(
error_list(ss.str(), instances), instances[0].loc, false);
}
}
bool Klammer::copy_to_instances(Target_set targets)
{
auto instances = instance_defs();
if (!instances.empty()) {
auto definitions = collect_if(
m_defs,
[] (const auto& def) {
return def.deftype != katom_t::klammer_instance
&& def.deftype != katom_t::klammer_override; });
int dcount = definitions.size();
if (dcount != 1) {
int icount = instances.size();
std::stringstream ss {};
ss << "There " << to_be(icount) << " " << icount << " "
<< plural("instance", icount) << " (defined by \"::\"), but "
<< dcount << " "<< plural("definition", dcount) << " (defined by \":\")";
throw Definition_error(
error_list(ss.str(), definitions), definitions[0].loc, false);
} else {
copy_components(definitions[0].parameters, m_defs, targets);
return true;
}
} else {
return false;
}
}
void Klammer::copy_components(
Parameter_set parameters, std::vector<Klammer::components> cs, Target_set targets)
{
(void)K::log(4);
for (auto target_name : targets.m_names) {
if (target_name == Target_set::declare_name ||
target_name == Target_set::general_name) {
continue;
}
m_parameters = parameters;
}
for (auto c : cs) {
m_body[c.target] = c.body;
m_varmap[c.target] = c.varmap;
}
}
// Verify that there is no more than one general klammer definition
void Klammer::check_for_multiple_general_klammers()
{
auto general_klammers = target_defs({Target_set::general_name});
if (general_klammers.size() > 1) {
throw Definition_error(
error_list(
"There is more than one general klammer (a klammer in which no target is defined)",
general_klammers),
general_klammers[0].loc, false);
}
}
void Klammer::check_for_declaration_and_definitions()
{
auto declares = target_defs({Target_set::declare_name});
if (!declares.empty()) {
std::vector<Klammer::components> definitions {};
for (auto def : m_defs) {
if (def.target != Target_set::declare_name) {
if (def.deftype == katom_t::klammer_definition ||
def.deftype == katom_t::klammer_default) {
msg() << def << "\n";
definitions.push_back(def);
}
}
}
if (!definitions.empty()) {
auto defsize = definitions.size();
std::string desc = defsize == 1 ? "a definition" :
std::to_string(defsize) + " definitions";
throw Definition_error(
error_list("A klammer has both a declaration (.k) as well as " + desc + "\n(instances are defined by \"::\")",
definitions),
declares[0].loc, false);
}
}
}
// If a general definition exists, use it for targets not defined, but check signatures
void Klammer::copy_general_klammer_to_undefined(Target_set targets)
{
(void)K::log(4);
auto general_klammers = target_defs({Target_set::general_name});
auto declares = target_defs({Target_set::declare_name});
// Check matching signatures (though this case already handled)
if (general_klammers.size() == 1) {
if (declares.empty() && !m_parameters.m_katoms.empty()) {
auto general_parameters = general_klammers[0].parameters;
if (general_parameters != m_parameters) {
// m_parameters.describe_parameters();
throw Definition_error(
error_list("The general parameters are different than the defined parameters",
general_klammers),
m_parameters.m_katoms[0].m_loc, false);
}
}
auto [target, deftype, parameters, body, varmap, loc] = general_klammers[0];
if (m_parameters.m_katoms.empty()) {
m_parameters = parameters;
}
for (auto target_name : targets.m_names) {
// std::cout << "General copy, considering " << target_name << "\n";
if (m_body.count(target_name) == 0 && target_name != Target_set::declare_name) {
// std::cout << " Copying to " << target_name << "\n";
m_body[target_name] = body;
m_defloc[target_name] = loc;
m_varmap[target_name] = varmap;
}
}
}
}
// Three declaration cases: none, one, many
void Klammer::no_declarations(Target_set targets)
{
(void)K::log(4);
// std::cout << boldblack << "No declarations\n" << black;
check_for_multiple_general_klammers();
std::vector<Klammer::components> defs {};
std::vector<std::string> target_names = targets.applicable();
std::vector<Parameter_set> all_parameter_sets {};
// Are all parameters the same?
for (auto def : m_defs) {
if (std::ranges::find(target_names, def.target) != target_names.end()) {
// std::cout << " Found: " << def.target << "\n";
all_parameter_sets.push_back(def.parameters);
} else {
// std::cout << " Not found: " << def.target << "\n";
}
}
if (!all_equal<Parameter_set>(all_parameter_sets)) {
// std::cout << " Not all equal\n";
throw Definition_error(
error_list("There is no declaration (.k) target for klammer \"" + m_name + "\"\n"
"but the parameters of all targets are not the same",
m_defs,
"Use a .k klammer to define the parameters and describe the klammer,\n"
"with \"::\" and no parameters for all targets."),
m_defs[0].loc, false);
} else {
// std::cout << " All equal\n";
copy_components(m_defs[0].parameters, m_defs, targets);
}
copy_general_klammer_to_undefined(targets);
}
void Klammer::one_declaration(Target_set targets, Klammer::components declare)
{
(void)K::log(4);
// std::cout << boldblack << "One declaration\n" << black;
check_for_multiple_general_klammers();
check_for_declaration_and_definitions();
copy_components(declare.parameters, m_defs, targets);
copy_general_klammer_to_undefined(targets);
}
void Klammer::many_declarations(std::vector<Klammer::components> declares)
{
(void)K::log(4);
// std::cout << boldblack << "Many declarations\n" << black;
throw Definition_error(
error_list("More than one declaration (.k) klammer", declares),
declares[0].loc, false);
}
void Klammer::rationalize(Target_set targets)
{
(void)K::log(3, m_name);
auto declares = target_defs({Target_set::declare_name});
auto declare_count = declares.size();
if (declare_count == 0) {
disallow_instances();
if (!copy_to_instances(targets)) {
no_declarations(targets);
}
} else if (declare_count == 1) {
one_declaration(targets, declares[0]);
} else {
many_declarations(declares);
}
}
std::string klammer_name_from_katom(std::string s, Locator loc)
{
std::regex rgx(R"(@(\w+).*)");
std::smatch match {};
if (std::regex_match(s, match, rgx)) {
return match[1];
} else {
throw Definition_error("The form of the klammer name " + q_(s) + " is not correct", loc);
}
}
void label(const std::string& s)
{
int w = 13;
std::cout << std::right << std::setw(w) << std::setfill(' ') << s << ": ";
}
void show_args(const std::string& label_text, std::vector<Argument> arguments)
{
if (!arguments.empty()) {
label(label_text);
for (auto a : arguments) {
std::cout << a << " ";
}
std::cout << '\n';
}
}
strings_t Klammer::get_target_names() const
{
strings_t names {};
for (auto [target, body] : m_body) {
std::stringstream ss {};
// ss << name << target.m_loc.m_line;
ss << target;
names.push_back(ss.str());
}
return names; // return join(names, ","s);
}
strings_t Klammer::get_locations()
{
strings_t locs {};
for (auto [target, locator] : m_defloc) {
std::cout << target << right_arrow << locator << "\n";
}
return {};
}
std::string Klammer::signature_text()
{
std::string result {};
bool has_pos = false;
bool has_opt = false;
for (auto pos : m_parameters.m_positional) {
result += pos.m_name;
std::string type = pos.m_argtype.m_name;
if (type != default_argtype) {
result += "." + type;
}
result += " | ";
has_pos = true;
}
if (result.size() >= 2)
result.resize(result.size() - 2);
auto opt_count = m_parameters.m_optional.size();
if (opt_count <= 3 && !has_pos) {
result += " ";
}
for (auto opt : m_parameters.m_optional) {
if (opt_count > 3) {
result += "\n :" + opt.m_name;
} else {
result += ":" + opt.m_name;
}
std::string type = opt.m_argtype.m_name;
if (type != default_argtype) {
result += "." + type;
}
// result += ":" + opt.m_name + "." + opt.m_argtype.m_name;
if (!opt.m_default.empty()) {
result += " " + italic_on() + opt.m_default + italic_off();
}
result += " ";
has_opt = true;
}
result = trim_right(result);
if (has_pos) {
result = " " + result;
}
if (opt_count > 3) {
result += "\n";
} else if (has_opt or has_pos) {
result += " ";
}
result += "@\n";
return result;
}
std::string Klammer::description_text()
{
std::string result = " [" + m_name + ": no description]";
if (m_body.contains("k")) {
result = to_string(m_body["k"], true);
result = justify(result, 80, 1);
}
return result;
}
std::string Klammer::describe(int margin)
{
std::string result {};
result += "@" + m_name + signature_text() + description_text();
result = add_margin(result, margin) + "\n";
return result;
}

106
mac/klammer.h Normal file
View File

@@ -0,0 +1,106 @@
#pragma once
#include <regex>
#include "deftype.h"
#include "argument_set.h"
#include "target_set.h"
#include "locator.h"
class Klammer
{
public:
Klammer() = default;
Klammer(const std::string& name)
: m_name(name)
{};
using variable_map_t = std::map<std::string, std::vector<int>>;
using target_variable_map_t = std::map<std::string, variable_map_t>;
static std::regex name_re; // = std::regex(R"((\w+)(?:\.(\w+))?)");
struct components {
std::string target;
katom_t deftype;
Parameter_set parameters;
std::vector<Katom> body;
variable_map_t varmap;
Locator loc;
};
// target-name -> [variable -> index]
void add_target_definition(
std::string target_name, Argtype_set argtypes,
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
void remove_target_definition(const std::string& target_name);
auto target_defs(std::vector<std::string> target_names);
auto instance_defs();
void disallow_instances(); //Klammer::components declaration);
bool copy_to_instances(Target_set targets);
void copy_components(
Parameter_set parameters, std::vector<Klammer::components> cs, Target_set targets);
void check_for_multiple_general_klammers();
void check_for_declaration_and_definitions();
void copy_general_klammer_to_undefined(Target_set targets);
void no_declarations(Target_set targets);
void one_declaration(Target_set targets, Klammer::components declare);
void many_declarations(std::vector<Klammer::components> declares);
void rationalize(Target_set target);
/*
void add_description(const std::string& desc, Katom definition_type);
auto user_defs();
auto klammer_defines_parameters();
auto klammer_uses_parameters();
void check_for_target_errors(Target_set targets);
bool explicit_parameters_match();
void copy_components(Klammer::components cs, Target_set targets);
*/
std::string signature_text();
std::string description_text();
std::string describe(int margin=0);
bool has_literal_param() const {
for (const auto& p : m_parameters.m_positional)
if (p.m_argtype.m_name == "literal") return true;
for (const auto& p : m_parameters.m_rest)
if (p.m_argtype.m_name == "literal") return true;
return false;
}
strings_t get_target_names() const;
strings_t get_locations();
// Initial instantiation:
std::string m_name {};
// Collection: target, deftype, parameters, body, locator
std::vector<Klammer::components> m_defs {};
// After rationalization:
Parameter_set m_parameters {};
std::map<std::string, std::vector<Katom>> m_body {};
std::string m_desc {};
std::map<std::string, Locator> m_defloc {}; // target -> Locator
std::map<std::string, defmode_t> m_defmode {}; // target -> defmode
target_variable_map_t m_varmap {}; // target -> map: variable -> index
};
std::string klammer_name_from_katom(std::string s, Locator loc);
std::tuple<std::string,std::string>
parse_name(Target_set targets, Katom name_katom);
std::tuple<Katom, Parameter_set, std::vector<Katom>, Locator>
parse_definition_katoms(Argtype_set argtypes, //Target_set targets,
std::vector<Katom>::iterator& begin, std::vector<Katom>::iterator& end);
/*
klammer_definition_args parse_klammer_definition_katoms(
katom_list& katoms, Argtype_set& argtypes);
void check_for_undefined_arguments(
std::string name, Parameters parameters, katom_list body_katoms, Locator loc);
*/

175
mac/klammer_set.cpp Normal file
View File

@@ -0,0 +1,175 @@
#include "klammer.h"
#include "klammer_set.h"
#include "show.h"
#include "util.h"
#include "log.h"
#include "error.h"
/*
bool Klammer_set::has(std::string name, std::string target)
{
return m_klammers.count(name) > 0;
}
*/
void Klammer_set::add(Argtype_set argtypes, Target_set& targets, katom_iter begin, katom_iter end, katom_list& katoms)
{
(void)K::log(3, *begin, *(end - 1));
restore_initial_type(begin, end);
auto [klammer_name, target_name] = parse_name(targets, *begin);
if (!targets.has(target_name)) {
throw Argument_error("The target \"" + target_name + "\" is not defined", begin->m_loc);
}
// Find the incoming definition mode
katom_t incoming_deftype = katom_t::klammer_definition;
for (auto it = begin + 1; it != end - 1; ++it) {
if (is_deftype(it->m_type)) {
incoming_deftype = it->m_type;
break;
}
}
defmode_t incoming_mode = defmode_from_katom(incoming_deftype);
if (m_klammers.count(klammer_name) == 0) {
m_klammers[klammer_name] = Klammer(klammer_name);
} else if (m_klammers[klammer_name].m_defloc.count(target_name) > 0) {
defmode_t existing_mode = m_klammers[klammer_name].m_defmode[target_name];
const auto& result = defmode_transition(existing_mode, incoming_mode);
std::string name_target = klammer_name + "." + target_name;
std::string at_desc = m_klammers[klammer_name].m_defloc[target_name].desc();
if (!result.replace) {
if (result.message.empty()) {
// Silent ignore (e.g., create + default)
modify_type(katom_t::replaced, begin, end);
ignore_whitespace(end, katoms);
return;
}
std::string msg = result.message;
msg = string_replace(msg, "NAME", q_(name_target));
msg = string_replace(msg, "AT", at_desc);
throw Definition_error(msg, begin->m_loc);
}
if (result.warn) {
std::string msg = result.message;
msg = string_replace(msg, "NAME", q_(name_target));
msg = string_replace(msg, "AT", at_desc);
warning(msg, begin->m_loc);
}
m_klammers[klammer_name].remove_target_definition(target_name);
}
m_klammers[klammer_name].add_target_definition(target_name, argtypes, begin + 1, end - 1);
// This add's target:
Target target = targets.get(target_name, begin->m_loc);
if (!target.m_provides.empty()) {
for (auto provide_name : target.m_provides) {
if (m_klammers[klammer_name].m_defloc.count(provide_name) > 0) {
m_klammers[klammer_name].remove_target_definition(provide_name);
}
m_klammers[klammer_name].add_target_definition(provide_name, argtypes, begin + 1, end - 1);
}
}
modify_type(katom_t::replaced, begin, end);
auto next_iter = end;
if (next_iter < katoms.end()) {
// std::cout << "next_iter: " << kindex << *next_iter << "\n";
} else {
// std::cout << "next_iter past end. katoms length: " << katoms.size() << "\n";
}
ignore_whitespace(next_iter, katoms);
}
void Klammer_set::rationalize(Target_set targets)
{
(void)K::log(3);
for (auto k : m_klammers) {
m_klammers[k.first].rationalize(targets);
}
}
/*
bool Klammer_set::has(std::string name, std::string target)
{
return m_klammers.count(name) > 0;
}
*/
void Klammer_set::check_klammer(std::string name, std::string target, Locator loc)
{
if (m_klammers.count(name) == 0) {
throw Definition_error("The klammer " + q_(name) + " is not defined for an unspecified target", loc);
}
Klammer k = m_klammers[name];
if (k.m_defloc.count(target) == 0) {
std::string desc;
if (target == Target_set::general_name) {
desc = "an unspecified target";
} else {
desc = "target " + q_(target);
}
throw Definition_error("The " + q_(name) + " klammer is not defined for " + desc, loc);
}
}
const std::vector<Katom>* Klammer_set::constant_body(const std::string& name) const
{
auto it = m_klammers.find(name);
if (it == m_klammers.end()) return nullptr;
const Klammer& k = it->second;
// A constant klammer has no parameters at all — neither in the general
// definition nor inherited from a .k declaration. A "::" instance has
// empty d.parameters (it inherits), so we must also check that no other
// definition (especially .k) declares parameters for this klammer.
for (const auto& d : k.m_defs) {
if (!d.parameters.empty()) return nullptr;
}
for (const auto& d : k.m_defs) {
if (d.target == Target_set::general_name && d.parameters.empty()) {
return &d.body;
}
}
return nullptr;
}
int max_length(std::map<std::string, Klammer> ss)
{
size_t result = 0;
for_each(ss.begin(), ss.end(),
[&result](const auto& s) { result = std::max(result, s.first.size()); });
return result;
}
std::string Klammer_set::instance_list(int margin) const
{
std::stringstream ss {};
std::string tab(margin, ' ');
auto name_width = max_length(m_klammers);
for (auto [name, k] : m_klammers) {
ss << tab << std::setfill(' ') << std::setw(name_width) << name
<< sp_arrow << k << "\n";
}
return ss.str();
}
std::string Klammer_set::describe(int margin) const
{
/*
strings_t names {};
std::vector<strings_t> targets {};
strings_t locations {};
*/
std::string result;
for (auto [name, k] : m_klammers) {
result += k.describe(margin) + "\n";
/*
names.push_back(name);
targets.push_back(k.get_target_names());
std::cout << name << " " << k.get_target_names() << "\n";
locator_summary(k.get_locations());
//locations.push_back(
*/
}
return result;
}

19
mac/klammer_set.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "klammer.h"
#include "target_set.h"
class Klammer_set
{
public:
Klammer_set() = default;
void add(Argtype_set argtypes, Target_set& targets,
std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
void rationalize(Target_set targets);
void check_klammer(std::string name, std::string target, Locator loc);
const std::vector<Katom>* constant_body(const std::string& name) const;
std::string instance_list(int margin) const;
std::string describe(int margin=0) const;
std::map<std::string, Klammer> m_klammers {};
};

207
mac/ktype.cpp Normal file
View File

@@ -0,0 +1,207 @@
#include <iostream>
#include <iomanip>
#include <algorithm>
#include <utility>
#include "locator.h"
#include "ktype.h"
#include "log.h"
#include "file.h"
#include "show.h"
#include "util.h"
/*
bool is_error(katom_t t)
{
return
t == katom_t::undefined ||
t == katom_t::no_matching_end ||
t == katom_t::no_matching_begin ||
t == katom_t::karg_error ||
t == katom_t::klammer_definition_type_error ||
t == katom_t::eval_error;
}
*/
bool is_active(katom_t type)
{
return type != katom_t::ignored
&& type != katom_t::replaced;
}
bool is_printable(katom_t type)
{
return printable_katom_types.find(type) != printable_katom_types.end();
}
bool is_deftype(katom_t type)
{
return
type == katom_t::klammer_definition ||
type == katom_t::klammer_instance ||
type == katom_t::klammer_override ||
type == katom_t::klammer_default;
}
std::string pattern_display(Ktype t)
{
std::string pat{};
if (t.m_type == katom_t::space) {
pat = "<space>";
} else if (t.m_type == katom_t::newline) {
pat = "\\n";
} else if (t.m_type == katom_t::ws_space) {
pat = "#+ or #+<number>";
} else if (t.m_type == katom_t::ws_newline) {
pat = "#/ or #/<number>";
} else if (t.m_type == katom_t::special) {
pat = "^@, ^|, ^#, ^:, or ^^";
} else if (t.m_type == katom_t::apply_end) {
pat = "@ or <name>@";
} else if (t.m_type == katom_t::define_end) {
pat = "@@ or <name>@@";
} else if (t.m_type == katom_t::machine_end) {
pat = "@@@ or <name>@@@";
} else if (t.m_type == katom_t::ws_added) {
pat = "<space> or \\n";
} else if (t.m_type == katom_t::word) {
pat = "<only-letters>";
} else if (t.m_type == katom_t::text) {
pat = "<no-special-chars>";
} else if (t.m_type == katom_t::nonascii) {
pat = "^<code> or ^<code>^";
} else {
pat = t.m_pattern;
pat = string_replace(pat, definition_name, "<name>");
pat = string_replace(pat, "\\", "");
}
return pat;
}
std::string regex_display(Ktype t)
{
std::string rgx{};
if (t.m_type == katom_t::space) {
rgx = "<space>";
} else if (t.m_type == katom_t::newline) {
rgx = "\\n";
} else {
rgx = t.m_pattern;
}
return rgx;
}
Ktype find_katom_type(katom_t type)
{
for (auto t : katom_types) {
if (t.m_type == type) {
return t;
}
}
throw Internal_error("Unknown katom_t: " + std::to_string((int)type));
}
void describe_katoms(bool show_regex)
{
size_t name_w = 0;
size_t pattern_w = 0;
size_t regex_w = 0;
size_t description_w = 0;
std::stringstream ss {};
ss << R"(
A "katom" is an individual element in the Klammertext input text.
Each katom has a type, listed below. You can see how Klammertext
divides up text into katoms with the "kdiag" command.)" << "\n\n";
if (show_regex) {
ss << R"(The "Regex" column is the argument to the std::regex C++ function.)";
} else {
ss << R"(In the katom patterns, "<name>" is a word that begins with a letter
and only contains letters, numbers, or the underscore (_) or period (.) characters. )";
}
ss << "A <number> is an integer greater than or equal to 1. "
"A <code> is one of the non-ASCII codes that are displayed by the command "
"\"kdesc -c\".";
for (auto t : katom_types) {
name_w = std::max(name_w, t.m_name.size());
pattern_w = std::max(pattern_w, pattern_display(t).size());
regex_w = std::max(regex_w, regex_display(t).size());
description_w = std::max(description_w, t.m_description.size());
}
pattern_w++;
regex_w++;
std::cout << justify(ss.str()) << "\n\n"
<< std::setfill(' ') << boldblack << std::right << std::setw(6) << "Index"
<< std::left
<< std::setw(name_w) << " Name" << " ";
if (show_regex) {
std::cout << std::setw(regex_w) << " Regex" << " ";
} else {
std::cout << std::setw(pattern_w) << " Pattern" << " ";
}
std::cout << std::setw(description_w) << "Description"
<< "\n" << black;
for (auto typ : katom_type_display_order) {
auto t = find_katom_type(typ);
std::cout << " " << std::right << std::setw(3)
<< static_cast<int>(t.m_type) << " "
<< std::left
<< std::setw(name_w) << t.m_name << " ";
if (show_regex) {
std::cout << std::setw(regex_w) << regex_display(t) << " ";
} else {
std::cout << std::setw(pattern_w) << pattern_display(t) << " ";
}
std::cout << std::setw(description_w) << t.m_description << "\n";
}
std::cout << "\n";
}
void describe_rewrite_patterns()
{
if (verbose_level > 0) {
std::string desc_text = "doc/katom_patterns.desc";
std::cout << "\n"
<< justify(string_from_file(klammertext_filename(desc_text)))
<< "\n\n";
}
size_t width1 = 0;
size_t width2 = 0;
for (auto [desc, pattern, replace] : katom_rewrite_rules) {
width1 = std::max(width1, desc.size());
width2 = std::max(width2, pattern.m_pattern.size());
}
width1 += 2;
width2 += 2;
std::cout << boldblack << std::setfill(' ')
<< std::setw(width1) << std::left << " Description"
<< std::setw(width2) << std::left << " Pattern"
<< " Replacement" << black << "\n";
for (auto [desc, pattern, replace] : katom_rewrite_rules) {
desc[0] = toupper(desc[0]);
std::cout << " "
<< std::left
<< std::setw(width1) << desc
<< std::setw(width2) << pattern.m_pattern
<< replace
<< "\n";
}
if (verbose_level > 0) {
std::string notes_text = "doc/katom_patterns.notes";
std::cout << "\n\n"
<< justify(string_from_file(klammertext_filename(notes_text)))
<< "\n\n";
}
}
std::string type_to_name(katom_t type)
{
auto var = std::find_if(katom_types.begin(), katom_types.end(),
[&] (Ktype t) { return t.m_type == type; });
return var->m_name;
}

305
mac/ktype.h Normal file
View File

@@ -0,0 +1,305 @@
#pragma once
#include <vector>
#include <map>
#include <string>
#include <regex>
#include <tuple>
#include <set>
inline const std::string at_s {"@"};
inline const std::string at2_s { "@@" };
inline const std::string at3_s { "@@@" };
inline const std::string bar_s { "|" };
inline const std::string open_s { R"(\()" };
inline const std::string close_s { R"(\))" };
inline const std::string karg_s { "*" };
inline const std::string read_s { "read" };
inline const std::string eval_s { "eval" };
inline const std::string cond_s { "cond" };
inline const std::string lit_s { "lit" };
inline const std::string hat_s { "^" };
inline const std::string ignore_line_s { "#" };
inline const std::string ignore_begin_s { R"(#\[)" };
inline const std::string ignore_end_s { R"(\]#)" };
inline const std::string ignore_rest_s { "##" };
inline const std::string ws_remove_s { "#-" };
inline const std::string ws_space_s { R"(#\+\d*)" };
inline const std::string ws_newline_s { R"(#/\d*)" };
// const std::string kname = "[a-zA-Z]+[a-zA-Z0-9_.]*";
// const std::string dname = "[a-zA-Z]+[a-zA-Z0-9_]*";
// const std::string k_name = R"([^^@#|\:]+)";
inline const std::string definition_name = R"([a-zA-Z][a-zA-Z0-9_.]*)";
enum class katom_t {
space,
word,
newline,
text,
apply_begin,
apply_end,
bar,
double_bar,
option_name,
read_begin,
eval_begin,
cond_begin,
define_begin,
define_end,
klammer_definition,
klammer_instance,
klammer_override,
klammer_default,
karg,
machine_begin,
machine_end,
ignore_begin,
ignore_end,
ignore_rest,
ignore_line,
ws_remove,
ws_space,
ws_newline,
special,
nonascii,
literal_begin,
literal_end,
ws_added,
literal,
eval_result,
replaced,
ignored,
};
inline const
std::vector<katom_t> katom_type_display_order {
katom_t::space,
katom_t::word,
katom_t::newline,
katom_t::text,
katom_t::apply_begin,
katom_t::apply_end,
katom_t::bar,
katom_t::double_bar,
katom_t::option_name,
katom_t::read_begin,
katom_t::eval_begin,
katom_t::cond_begin,
katom_t::define_begin,
katom_t::define_end,
katom_t::klammer_definition,
katom_t::klammer_instance,
katom_t::klammer_override,
katom_t::klammer_default,
katom_t::karg,
katom_t::machine_begin,
katom_t::machine_end,
katom_t::ignore_begin,
katom_t::ignore_end,
katom_t::ignore_rest,
katom_t::ignore_line,
katom_t::ws_remove,
katom_t::ws_space,
katom_t::ws_newline,
katom_t::special,
katom_t::nonascii,
katom_t::literal_begin,
katom_t::literal_end,
katom_t::ws_added,
katom_t::literal,
katom_t::eval_result,
katom_t::replaced,
katom_t::ignored
};
class Ktype;
inline std::map<katom_t, std::string> katom_type_names {};
inline std::map<katom_t, std::string> katom_type_descs {};
inline std::vector<katom_t> katom_type_list {};
class Ktype {
public:
// Ktype() = default;
Ktype(katom_t type, std::string name, std::string pattern, std::string description, bool use_equal = false)
: m_type(type),
m_name(name),
m_pattern(pattern),
m_description(description),
m_use_equal(use_equal),
m_rgx(std::regex(pattern)) {
katom_type_list.push_back(type);
katom_type_names.insert({m_type, m_name});
katom_type_descs.insert({m_type, m_description});
}
bool match(const std::string& s) const {
return m_use_equal ? s == m_pattern : std::regex_match(s, m_rgx);
}
katom_t m_type;
std::string m_name;
std::string m_pattern;
std::string m_description;
bool m_use_equal;
std::regex m_rgx;
};
inline const
std::vector<Ktype> katom_types {
Ktype(katom_t::space, "space", " ", "One space character", false),
Ktype(katom_t::word, "word", "[a-zA-Z]+", "Text only containing letters", false),
Ktype(katom_t::newline, "newline", "\n", "One newline character", false),
Ktype(katom_t::bar, "bar", "\\|", "Bar character used as positional argument separator", false),
Ktype(katom_t::double_bar, "double-bar", "\\|\\|", "Separator for compound positional arguments", false),
Ktype(katom_t::read_begin, "read-begin", at_s+read_s, "Beginning of the file input klammer", false),
Ktype(katom_t::eval_begin, "eval-begin", at_s+eval_s, "Beginning of the evaluation klammer", false),
Ktype(katom_t::cond_begin, "cond-begin", at_s+cond_s, "Beginning of the conditional (if/then/else) klammer", false),
Ktype(katom_t::apply_begin, "apply-begin", at_s + definition_name, "Beginning of a klammer call"),
Ktype(katom_t::apply_end, "apply-end", "(" + definition_name + ")?" + at_s, "End of a klammer call"),
Ktype(katom_t::option_name, "option-name", ":" + definition_name, "Optional argument name"),
Ktype(katom_t::define_begin, "define-begin", at2_s + definition_name, "Beginning of a klammer definition"),
Ktype(katom_t::define_end, "define-end", "(" + definition_name + ")?" + at2_s, "End of a klammer definition"),
Ktype(katom_t::klammer_default, "klammer-default", "::::", "Define klammer default value for possible override"),
Ktype(katom_t::klammer_override, "klammer-override", ":::", "Override existing klammer definition"),
Ktype(katom_t::klammer_definition, "klammer-definition", ":", "Klammer definition, including parameters"),
Ktype(katom_t::klammer_instance, "klammer-instance", "::", "Klammer definition using previously defined parameters"),
Ktype(katom_t::karg, "klammer-arg", "\\*" + definition_name +"\\*", "Klammer argument in klammer body definition"),
Ktype(katom_t::machine_begin, "machine-begin", at3_s + definition_name, "Beginning of a processor modification definition"),
Ktype(katom_t::machine_end, "machine-end", "(" + definition_name + ")?" + at3_s, "End of a processor modification definition"),
Ktype(katom_t::ignore_begin, "ignore-begin", ignore_begin_s, "Beginning of text to remove"),
Ktype(katom_t::ignore_end, "ignore-end", ignore_end_s, "End of text to remove"),
Ktype(katom_t::ignore_rest, "ignore-rest", ignore_rest_s, "Remove all text to the end of file or string"),
Ktype(katom_t::ignore_line, "ignore-line", ignore_line_s, "Remove all text to the first newline, inclusive"),
Ktype(katom_t::ws_remove, "ws-remove", ws_remove_s, "Remove all whitespace at this point"),
Ktype(katom_t::ws_space, "ws-space", ws_space_s, "Remove all whitespace, leaving <number> spaces (default: 1)"),
Ktype(katom_t::ws_newline, "ws-newline", ws_newline_s, "Remove all whitespace, leaving <number> newlines (default: 1)"),
Ktype(katom_t::special, "special-char", R"(\^[@|#^:*])", "Klammertext special character treated as regular text"),
//Ktype(katom_t::nonascii, "non-ascii-char", R"(\^\w(?:.|\^))", "Non-ASCII character"),
Ktype(katom_t::nonascii, "non-ascii-char", R"((\^(\w(?:.|\^)))|(\^[A-Fa-f0-9]{1,5}\^))", "Non-ASCII character"),
// Ktype(katom_t::word, "word", R"([a-z][a-z0-9_]*)", "Lower-case letters, numbers, or underscore"),
// Ktype(katom_t::text, "text",
Ktype(katom_t::literal_begin, "literal-begin", R"(\^')", "Begin unprocessed text"),
Ktype(katom_t::literal_end, "literal-end", R"('\^)", "End unprocessed text"),
Ktype(katom_t::text, "text", R"([^^@#|]+)", "No special characters, whitespace, or \":\" at the beginning"), // spaces, @, #, |, or ^,
Ktype(katom_t::ws_added, "ws-added", "<space> or \\n", "Added whitespace characters from #+ and #/"),
Ktype(katom_t::literal, "literal", "", "Literal katom (changed from its original type by ^'...'^)"),
Ktype(katom_t::eval_result, "eval-result", "", "Katom produced by @eval klammer"),
Ktype(katom_t::replaced, "replaced", "", "A katom replaced by definitions, applications, or file input"),
Ktype(katom_t::ignored, "ignored", "", "A katom ignored by the action of a \"#\" katom"),
// Ktype(katom_t::undefined, "undefined", "", "Undefined pattern")
};
class Rgx {
public:
explicit Rgx(std::string pattern)
: m_pattern(pattern)
, m_regex(std::regex(pattern)) {}
std::string m_pattern;
std::regex m_regex;
};
inline const
std::vector<std::tuple<std::string, Rgx, std::string>> katom_rewrite_rules {
{ "bar precedence", Rgx(R"((.*?)(\^\|)(.*))"), "$1 $2 $3" },
{ "literal without space", Rgx(R"((\^')(.*?)('\^))"), "$1 $2 $3" },
{ "literal start to the left", Rgx(R"((.+)(\^'))"), "$1 $2" },
{ "literal start to the right", Rgx(R"((\^')(.+))"), "$1 $2" },
{ "literal end to the left", Rgx(R"(('\^)(.+))"), "$1 $2" },
{ "literal end to the right", Rgx(R"((.+)('\^))"), "$1 $2" },
//{ "successive non-ascii characters", Rgx(R"((.?)(\^\w[-\"^`'~hcbrdwa])(\^\w)(.?))"), "$1 $2 $3 $4" },
// { "special character", Rgx(R"((.*?)(\^[@\|\^#\*])(.*))"), "$1 $2 $3" },
{ "special character", Rgx(R"((.*?)(\^[@|^#*:])(.*))"), "$1 $2 $3" },
{ "non-diacritic non-ascii", Rgx(R"((.*?)(\^[a-zA-Z0-9]{5}\^)(.*?))"), "$1 $2 $3" },
{ "non-diacritic non-ascii 2", Rgx(R"((.*?)(\^[a-zA-Z0-9]{1,4}\^)(.*?))"), "$1 $2 $3" },
{ "diacritic non-ascii", Rgx(R"((.*?)(\^[a-zA-z][^^])(.*?))"), "$1 $2 $3" },
{ "double bar separator", Rgx(R"(([^\s\^]+?)(\|\|)(.*))"), "$1 $2 $3" },
{ "bar separator", Rgx(R"(([^\s\^]+?)(\|)(.*))"), "$1 $2 $3" },
{ "embedded ignore begin", Rgx(R"((.*?)#\[(.*))"), "$1 #[ $2" },
{ "embedded ignore end", Rgx(R"((.*?)\]#(.*))"), "$1 ]# $2" },
{ "embedded remove whitespace", Rgx(R"((.*?)#-(.*))"), "$1 #- $2" },
{ "single argument for special character", Rgx(R"((@\w+)-(\^[@\|:\*\^])@?)"), "$1 $2 @" },
// { "special character", Rgx(R"((.*?)(\^[@\|:\*\^])(.*))"), "$1 $2 $3" },
{ "klammer body arguments", Rgx(R"((.*?)(\*\w+\*)(.*))"), "$1 $2 $3" },
{ "trailing punctuation", Rgx(R"((\w?)@([().,:;?'!]))"), "$1@ $2" },
{ "trailing punctuation", Rgx(R"(@([().,:;?'!].*))"), "@ $1" },
{ "trailing punctuation, shortcut", Rgx(R"((@\w+)-(\w+)([().,:;?'!][^\s]*))"), "$1 $2 @ $3" },
// { "single argument shortcut", Rgx(R"((@\w+)-([^\s@]+)@?)"), "$1 $2 @" },
{ "no argument klammer", Rgx(R"((@\w+)@)"), "$1 @" },
{ "left parenthesis", Rgx(R"(([(])@(\w+))"), "$1 @$2" },
{ "embedded klammer", Rgx(R"(([^@]+)(@[^@]+@)([^@]+))"), "$1 $2 $3" },
{ "embedded klammer to the right", Rgx(R"(([^@]+)(@[^@]+@))"), "$1 $2" },
{ "embedded klammer to the left", Rgx(R"((@[^@]+@)([^@]+))"), "$1 $2" },
{ "embedded add whitespace", Rgx(R"(([^\s])(#\+\d*)([^\s]))"), "$1 $2 $3" },
{ "embedded add whitespace to the left", Rgx(R"(([^\s])(#\+\d*))"), "$1 $2" },
{ "embedded add whitespace to the right", Rgx(R"((#\+\d*)([^\s]))"), "$1 $2" },
{ "embedded ignore on the left", Rgx(R"(#([^-+/\s]))"), "# $1" },
{ "embedded ignore on the right", Rgx(R"(([^\s])#)"), "$1 #" },
};
inline const
std::set<katom_t> printable_katom_types {
katom_t::word,
katom_t::space,
katom_t::newline,
katom_t::literal,
katom_t::ws_added
};
inline const
std::set<katom_t> open_level {
katom_t::literal_begin,
katom_t::read_begin,
katom_t::eval_begin,
katom_t::cond_begin,
katom_t::apply_begin,
katom_t::define_begin,
katom_t::machine_begin,
katom_t::ignore_begin
};
inline const
std::set<katom_t> close_level {
katom_t::literal_end,
katom_t::apply_end,
katom_t::apply_end,
katom_t::apply_end,
katom_t::apply_end,
katom_t::define_end,
katom_t::machine_end,
katom_t::ignore_end
};
inline
std::map<katom_t, katom_t> katom_spans {
{ katom_t::literal_begin, katom_t::literal_end },
{ katom_t::read_begin, katom_t::apply_end },
{ katom_t::eval_begin, katom_t::apply_end },
{ katom_t::cond_begin, katom_t::apply_end },
{ katom_t::apply_begin, katom_t::apply_end },
{ katom_t::define_begin, katom_t::define_end },
{ katom_t::machine_begin, katom_t::machine_end },
{ katom_t::ignore_begin, katom_t::ignore_end }
};
bool is_active(katom_t t);
bool is_printable(katom_t type);
void describe_katoms(bool show_regex = false);
void describe_rewrite_patterns();
bool is_deftype(katom_t type);
std::string type_to_name(katom_t type);

137
mac/locator.cpp Normal file
View File

@@ -0,0 +1,137 @@
#include <iostream>
#include "character.h"
#include "locator.h"
#include "show.h"
#include "file.h"
#include "util.h"
std::string abbreviate_location(
const std::string& location, bool make_map,
std::map<std::string, std::string>& relpath)
{
std::string result = location;
fs::path basename = fs::path(location).filename();
if (sks_commands.contains(basename)) {
result = basename;
} else {
if (make_map) {
if (relpath.count(location)) {
result = relpath[location];
} else {
result = fs::relative(location, fs::current_path());
relpath[location] = result;
}
}
}
return result;
}
Locator::Locator(fs::path filename, int line, int chr)
: m_filename(filename)
, m_line(line)
, m_chr(chr)
{
string_map relpath {};
//m_filename = abbreviate_location(m_filename, false, relpath);
}
std::ostream &nformat(std::ostream &os)
{
os << std::setfill('0') << std::setw(3);
return os;
}
std::string Locator::str(bool relative) const
{
// relative_pathname(m_filename, fs::current_path().string()) << ":"
std::string filename = m_filename;
if (relative) {
filename = relative_to_cwd(m_filename);
}
std::stringstream ss {};
ss << "[" << filename << ":"
<< nformat << m_line << "."
<< nformat << m_chr+1 << "]";
return ss.str();
}
std::string Locator::desc(bool relative) const
{
// relative_pathname(m_filename, fs::current_path().string()) << ":"
std::string filename = m_filename;
if (relative) {
filename = relative_to_cwd(m_filename);
}
std::stringstream ss {};
ss << filename << ", line " << m_line << ", character " << m_chr + 1;
return ss.str();
}
std::string Locator::abbrev(bool include_chr) const
{
fs::path fname(m_filename);
std::stringstream ss {};
ss << "[" << fname.filename().string() << ":"
<< nformat << m_line;
if (include_chr) {
ss << "." << nformat << m_chr+1;
}
ss << "]";
return ss.str();
}
std::string locator_range(const Locator& start_loc, const Locator& end_loc, string_map& relpath)
{
std::string start_filename = abbreviate_location(start_loc.m_filename, true, relpath);
std::string end_filename = abbreviate_location(end_loc.m_filename, true, relpath);
std::stringstream ss {};
std::cout << std::setfill('0') << std::setw(3);
if (start_filename != end_filename) {
ss << start_loc << right_arrow << end_loc;
} else {
ss << "[" << start_filename << ":";
if (start_loc.m_line == end_loc.m_line) {
ss << nformat << start_loc.m_line << "."
<< nformat << start_loc.m_chr+1 << right_arrow
<< nformat << end_loc.m_chr+1;
} else {
ss << nformat << start_loc.m_line << "."
<< nformat << start_loc.m_chr+1
<< right_arrow
<< nformat << end_loc.m_line << "."
<< nformat << end_loc.m_chr+1;
}
ss << "]";
}
std::cout << std::setfill(' ');
return ss.str();
}
Locator current_locator(const std::source_location location)
{
// file_name() may be null (Homebrew GCC on macOS) -> avoid path(nullptr).
return Locator(location.file_name() ? location.file_name() : "",
location.line(), location.column());
}
std::string locator_summary(std::vector<Locator> locators)
{
std::map<std::string, std::vector<int>> file_locs {};
for (auto loc : locators) {
if (!file_locs.contains(loc.m_filename)) {
file_locs[loc.m_filename] = {};
}
file_locs[loc.m_filename].push_back(loc.m_line);
}
for (auto [filename, lines] : file_locs) {
std::cout << " " << filename << ": " << lines << "\n";
}
return "";
}

52
mac/locator.h Normal file
View File

@@ -0,0 +1,52 @@
#pragma once
#include <iostream>
#include <string>
// #include <filesystem>
#include <source_location>
#include <map>
#include "file.h"
inline std::string klammertext_home_var = "KLAMMERTEXT_HOME";
class Locator
{
public:
explicit Locator(const std::source_location location =
std::source_location::current())
// std::source_location::file_name() can return nullptr on some
// toolchains (e.g. Homebrew GCC on macOS); guard against
// fs::path(nullptr) -> strlen(NULL).
: m_filename(location.file_name()
? fs::absolute(location.file_name()) : fs::path{})
, m_line(int(location.line()))
, m_chr(int(location.column()))
{};
Locator(fs::path filename, int line, int chr);
std::string str(bool relative = false) const;
std::string desc(bool relative = false) const;
std::string abbrev(bool include_chr=true) const;
std::string m_filename {};
int m_line;
int m_chr;
//std::string m_desc {};
};
std::ostream &nformat(std::ostream &os);
std::string locator_range(
const Locator& start_loc, const Locator& end_loc,
std::map<std::string, std::string>& relpath);
Locator current_locator(
const std::source_location location = std::source_location::current());
std::string locator_summary(std::vector<Locator> locators);
inline
std::string showloc(Locator loc=Locator()) {
return loc.abbrev(false) + " ";
}

117
mac/log.cpp Normal file
View File

@@ -0,0 +1,117 @@
#include <source_location>
#include "log.h"
#include "show.h"
#include "util.h"
int verbose_level = 0;
using log_arg = std::variant<bool,int,float,std::string,const char*,Locator>;
std::ostream& operator<<(std::ostream& os, const log_arg& arg)
{
bool quoted_string = verbose_level > 2;
if (std::holds_alternative<bool>(arg)) {
os << (std::get<bool>(arg) ? "true" : "false");
} else if (std::holds_alternative<int>(arg)) {
os << std::get<int>(arg);
} else if (std::holds_alternative<float>(arg)) {
os << std::get<float>(arg);
}
else if (std::holds_alternative<std::string>(arg)) {
std::string s = std::get<std::string>(arg);
if (quoted_string)
os << "\"";
if (!quoted_string && s.empty()) {
os << "<none>";
} else {
os << s;
}
if (quoted_string)
os << "\"";
} else if (std::holds_alternative<const char*>(arg)) {
os << std::get<const char*>(arg);
// } else if (std::holds_alternative<strings_t>(arg)) {
// os << std::get<strings_t>(arg);
} else if (std::holds_alternative<Locator>(arg)) {
os << std::get<Locator>(arg);
// } else if (std::holds_alternative<Source>(arg)) {
// os << std::get<Source>(arg);
// } else if (std::holds_alternative<katom_t>(arg)) {
// os << std::get<katom_t>(arg);
// } else if (std::holds_alternative<Katom>(arg)) {
// os << std::get<Katom>(arg);
}
/*
} else if (std::holds_alternative<katom_list>(arg)) {
os << std::get<katom_list>(arg);
} else if (std::holds_alternative<katom_iter>(arg)) {
os << std::get<katom_iter>(arg);
}
*/
return os;
}
void log_indent(const std::string& filename, const std::string& color)
{
std::cout
<< color
<< std::setfill(' ')
<< std::setw(22-static_cast<int>(std::size(filename)))
<< std::right;
}
void log_filepos(const std::string& filename, int line, const std::string& color)
{
// if (show_verbose_location) {
log_indent(filename, color);
std::cout
<< "[" << filename << ":"
<< std::setw(3) << std::setfill('0')
<< line << "] " << reset;
// }
}
std::string prettify_name(const std::string& s)
{
std::string result = s;
result = string_replace(result, "std::__cxx11::basic_string<char>", "std::string");
result = string_replace(result, " >", ">");
std::smatch match;
std::regex re(R"(.*?(\w+)\(.*)");
if (verbose_level < 4 && std::regex_search(result, match, re)) {
std::stringstream ss;
ss << match[1] << "()";
result = ss.str();
}
return result;
}
void display_location(int log_level, std::source_location location)
{
if (verbose_level >= log_level) {
std::string color = blue; // cyan;
if (log_level == 1) {
color = magenta;
}
if (verbose_level > 1) {
log_filepos(location.file_name() ? location.file_name() : "",
location.line(), color);
}
if (verbose_level > 3) {
std::cout << location.function_name();
} else if (verbose_level > 1) {
std::cout << prettify_name(location.function_name());
}
}
}
void warning(const std::string& message, const Locator& loc)
{
std::cerr << " " << red << command_name << " (warning): " << message << "\n";
if (loc.m_filename != "") {
std::cerr << " " << loc.desc();
}
std::cerr << "\n" << reset;
}

63
mac/log.h Normal file
View File

@@ -0,0 +1,63 @@
#pragma once
#include <iostream>
#include <source_location>
#include <sstream>
#include <string>
#include <variant>
#include <vector>
#include "error.h"
#include "locator.h"
// #include "source.h"
#include "katom_list.h"
extern int verbose_level;
extern bool show_verbose_location;
std::ostream& operator<<(std::ostream& os, const std::variant<bool,int,float,std::string,const char*,Locator>& arg);
void display_location(int log_level, std::source_location location);
namespace K {
template <typename... Ts>
struct log
{
log(int log_level, Ts&&... ts, const std::source_location& location = std::source_location::current()) {
if (verbose_level >= log_level) {
display_location(log_level, location);
if (verbose_level > 1 && sizeof...(ts) > 0) {
std::cout << ": ";
} else if (verbose_level == 1) {
std::cout << command_name << ": ";
}
if (log_level > 0) {
((std::cout << std::forward<Ts>(ts) << " "), ...);
std::cout << '\n';
}
}
}
};
template <typename... Ts>
log(int log_level, Ts&&...) -> log<Ts...>;
}
/*
void log(int level = 3, const std::source_location location = std::source_location::current());
template<typename T, typename... Args>
void log(Args... args, int level = 3, const std::source_location location = std::source_location::current());
*/
/*
void log(std::vector<std::variant<bool,int,float,std::string,const char*,Locator,Source>> args={}, int level=3,
const std::source_location location
= std::source_location::current());
void xlog(std::vector<std::variant<bool,int,float,std::string,const char*,Locator,Source>> args={}, int verbose_override=1,
const std::source_location location
= std::source_location::current());
*/
void warning(const std::string& message, const Locator& loc);

528
mac/machine.cpp Normal file
View File

@@ -0,0 +1,528 @@
#include <set>
#include <utility>
#include "machine.h"
#include "error.h"
#include "show.h"
#include "util.h"
#include "file.h"
#include "log.h"
#include "eval.h"
Machine::Machine()
: m_argtypes(Argtype_set())
, m_state(State())
, m_targets(Target_set())
, m_klammers(Klammer_set())
{
(void)K::log(3);
m_state.add_environment_frame();
/*
if (sks) {
fs::path sks_filename(klammertext_filename("sks/sks.k"));
// msg() << "SKS filename: " << sks_filename << "\n";
read(sks_filename);
}
*/
}
void Machine::process_eval_katoms(katom_list& katoms)
{
(void)K::log(3);
if (std::find_if(katoms.begin(), katoms.end(), begin_eval) != katoms.end()) {
for (const auto& [op, cl] : find_spans(katoms, level_increase, level_decrease, true, "eval")) {
auto [begin, end] = find_span_katoms(katoms, op, cl);
if (begin_eval(*begin)) {
Eval E(*this, begin->m_loc);
katom_list eval_katoms = E.eval(begin, end);
std::for_each(begin, end, mark_as_replaced);
katoms.insert(end, eval_katoms.begin(), eval_katoms.end());
}
}
}
}
// Collect the bars that are direct argument separators of a @cond span:
// the bar katoms at nesting depth 0 within the span. A bar that lies
// inside a nested span — for example the "|" in an inner @frac a | b @, or
// in a nested @eval/@read/@cond — has positive depth and is excluded.
//
// This makes @cond's argument delimitation a property of the span tree
// (the operad's arity: each operator owns the bars at its own level) rather
// than of the flat katom range. Counting every bar in the range, as the
// original check did, conflated the arities of nested operators and rejected
// well-formed input such as
// @cond *bool* | @frac 1 | 2 @ | @frac 2 | 1 @ @
// because the inner @frac bars were miscounted as @cond separators.
//
// begin is the cond_begin katom; end is one past the closing apply_end, so
// *(end - 1) is the apply_end. Bars are returned in source order.
std::vector<katom_iter> cond_separator_bars(katom_iter begin, katom_iter end)
{
std::vector<katom_iter> bars {};
int depth = 0;
for (auto it = begin + 1; it != end - 1; ++it) {
if (is_bar(*it) && depth == 0) {
bars.push_back(it);
} else if (level_increase(*it)) {
++depth;
} else if (level_decrease(*it)) {
--depth;
}
}
return bars;
}
void check_bar_count(katom_iter begin, std::size_t count)
{
if (count != 1 && count != 2) {
std::stringstream ss {};
ss << "Incorrectly formatted @cond klammer. There should only be one or two bar characters:\n"
<< " @cond <predicate> | <result-if-true @\nor:\n"
<< " @cond <predicate> | <result-if-true> | <result-if-false> @";
throw Argument_error(ss.str(), begin->m_loc, false);
}
}
bool is_true(const std::string& s)
{
return s == "True" || s == "true" || s == "1";
}
void Machine::process_cond_katoms(katom_list& katoms)
{
if (std::find_if(katoms.begin(), katoms.end(), begin_cond) != katoms.end()) {
(void)K::log(3);
//for (auto [op, cl] : find_spans(katoms, begin_cond, end_apply, true, "cond")) {
for (const auto& [op, cl] : find_spans(katoms, level_increase, level_decrease, true, "cond")) {
auto [begin, end] = find_span_katoms(katoms, op, cl);
// msg() << "find_spans: " << std::pair(begin, end) << "\n";
if (begin_cond(*begin)) {
// Delimit @cond's arguments by the bars at depth 0 within the
// span, so that bars belonging to nested klammers are not
// mistaken for @cond's own separators (see cond_separator_bars).
std::vector<katom_iter> bars = cond_separator_bars(begin, end);
check_bar_count(begin, bars.size());
auto bar_1 = bars[0];
std::string predicate = to_string(begin + 1, bar_1, true);
katom_list true_clause {};
katom_list false_clause {};
if (bars.size() == 2) {
auto bar_2 = bars[1];
true_clause = katom_list(bar_1 + 1, bar_2);
false_clause = katom_list(bar_2 + 1, end - 1);
} else {
true_clause = katom_list(bar_1 + 1, end - 1);
}
// Splice only the selected branch. Its nested klammers remain
// unreduced here and are reduced by the outer fixed-point apply
// loop; the unselected branch is discarded without evaluation
// (@cond is a non-strict special form).
katom_list result = is_true(predicate)
? trim_whitespace(true_clause) : trim_whitespace(false_clause);
std::for_each(begin, end, mark_as_replaced);
katoms.insert(end, result.begin(), result.end());
}
}
}
}
void Machine::expand_constant_klammers(katom_list& katoms, const Katom& op, const Katom& cl)
{
auto [begin, end] = find_span_katoms(katoms, op, cl);
restore_initial_type(begin + 1, end - 1);
if (std::find_if(begin + 1, end - 1, begin_klammer_apply) == end - 1) return;
for (const auto& [app_op, app_cl] : find_spans(begin + 1, end - 1, begin_apply, end_apply, false, "def-time")) {
auto [app_begin, app_end] = find_span_katoms(katoms, app_op, app_cl);
if (app_begin->m_type == katom_t::apply_begin) {
std::string name = trim_char(app_begin->m_text, '@');
const auto* body = m_klammers.constant_body(name);
if (body) {
// Set both m_type and m_initial_type so that
// restore_initial_type() in add() won't resurrect them
for (auto it = app_begin; it != app_end; ++it) {
it->m_type = katom_t::replaced;
it->m_initial_type = katom_t::replaced;
}
katoms.insert(app_end, body->begin(), body->end());
}
}
}
}
//katom_list
void Machine::mark_literal_klammer_content(katom_list& katoms)
{
(void)K::log(4);
// Collect names of klammers that have a literal parameter
std::set<std::string> literal_names {};
for (const auto& [name, klammer] : m_klammers.m_klammers) {
if (klammer.has_literal_param())
literal_names.insert(name);
}
if (literal_names.empty()) return;
// Scan for matching @name ... name@ spans.
// Stop at ## (ignore-rest) since everything after it will be removed.
for (auto k = katoms.begin(); k != katoms.end(); ++k) {
if (k->m_type == katom_t::ignore_rest) break;
if (k->m_type != katom_t::apply_begin) continue;
std::string name = trim_char(k->m_text, '@');
if (literal_names.count(name) == 0) continue;
(void)K::log(2, "Literal klammer: " + name);
// Find the matching named closing delimiter
std::string close_text = name + "@";
auto close = k + 1;
int depth = 1;
while (close != katoms.end()) {
if (close->m_type == katom_t::apply_begin &&
trim_char(close->m_text, '@') == name)
depth++;
else if (close->m_type == katom_t::apply_end &&
trim_char(close->m_text, '@') == name)
depth--;
if (depth == 0) break;
++close;
}
if (close == katoms.end()) {
throw Parsing_error(
"Klammer " + q_(name) + " has a literal parameter and must be closed with "
+ q_(close_text),
k->m_loc);
}
// Count positional parameters before the literal one.
// The literal parameter is always last. Bars separate
// the preceding positional arguments and the literal content.
const auto& klammer = m_klammers.m_klammers[name];
int bars_before_literal = 0;
for (const auto& p : klammer.m_parameters.m_positional) {
if (p.m_argtype.m_name == "literal") break;
bars_before_literal++;
}
// Find where literal content starts.
// Skip bars_before_literal bars (separating preceding positional args).
// If options are present, skip past the bar after them.
// Options are identified by :name katoms before any bar.
auto literal_start = k + 1;
bool has_options = false;
for (auto j = k + 1; j < close; ++j) {
if (j->m_type == katom_t::option_name) {
has_options = true;
}
if (j->m_type == katom_t::bar) {
if (bars_before_literal > 0) {
bars_before_literal--;
literal_start = j + 1;
} else if (has_options) {
// This bar separates options from literal content
literal_start = j + 1;
break;
} else {
// No preceding args, no options: bar is part of literal
break;
}
}
}
std::for_each(literal_start, close, mark_as_literal);
k = close; // Skip past this span
}
}
void Machine::process_katoms(
katom_list& katoms, const std::string& source,
bool nonascii, bool literal, bool ignore, bool whitespace, bool klammers,
bool eval, bool cond, bool read)
{
mark_literal_klammer_content(katoms);
if (literal) mark_literal_katoms(katoms);
if (nonascii) encode_nonascii_characters(katoms);
if (ignore) mark_ignored_katoms(katoms);
if (whitespace) process_whitespace_modifiers(katoms);
if (klammers) process_klammer_katoms(katoms);
if (eval) process_eval_katoms(katoms);
if (cond) process_cond_katoms(katoms);
if (read) expand_read_katoms(
katoms, source,
nonascii, literal, ignore, whitespace, klammers, eval, cond, read);
// return katoms;
}
katom_list Machine::process(
std::string text, const std::string& source,
bool nonascii, bool literal, bool ignore, bool whitespace, bool klammers,
bool eval, bool cond, bool read)
{
katom_list katoms = katomize(line_split(text), source);
//katoms =
process_katoms(
katoms, source,
nonascii, literal, ignore, whitespace, klammers, eval, cond, read);
return katoms;
}
void Machine::read(const fs::path& pathname)
{
(void)K::log(3, pathname.string());
m_state.open_frame("Machine state: " + pathname.string());
std::string text = m_state.subst(trim_right(string_from_file(pathname)));
katom_list katoms = process(text, pathname);
m_katoms.insert(m_katoms.end(), katoms.begin(), katoms.end());
extract_machine_definitions();
extract_klammer_definitions();
m_sources.push_back(pathname);
}
void Machine::read(const std::string& s)
{
(void)K::log(3, s);
m_state.open_frame("Machine state: " + s);
std::string text = m_state.subst(trim_right(s));
katom_list katoms = process(text, command_pathname);
m_katoms.insert(m_katoms.end(), katoms.begin(), katoms.end());
extract_machine_definitions();
extract_klammer_definitions();
m_sources.push_back(s);
}
// Read
void Machine::expand_read_katoms(
katom_list& katoms, std::string current_filename,
bool nonascii, bool literal, bool ignore, bool whitespace,
bool klammers, bool eval, bool cond, bool read)
{
(void)K::log(3);
current_filename = resolve_relative_to(current_filename);
// msg() << "current_filename: " << current_filename << "\n";
if (std::find_if(katoms.begin(), katoms.end(), begin_read) != katoms.end()) {
(void)K::log(3);
for (const auto& [op, cl] : find_spans(katoms, begin_apply, end_apply, true, "read")) {
auto [begin, end] = find_span_katoms(katoms, op, cl);
if (begin_read(*begin)) {
std::string read_filename = to_string(begin + 1, end - 1, true);
// msg() << "read: " << resolve_relative_to(read_filename, current_filename) << "\n";
/*
std::string current_directory =
fs::path(current_filename).parent_path().string();
fs::path input_filename =
fs::path(current_directory + "/" + read_filename);
*/
fs::path input_filename = resolve_relative_to(read_filename, current_filename);
// msg() << "read: " << input_filename << "\n";
(void)K::log(2, input_filename.string());
if (!fs::exists(input_filename)) {
std::stringstream ss{};
ss <<"File " << input_filename << " does not exist";
throw File_error(ss.str(), begin->m_loc);
}
std::for_each(begin, end, mark_as_replaced);
input_filename = fs::canonical(input_filename);
std::string text = trim_right(string_from_file(input_filename.string()));
katom_list ks = katomize(line_split(text), input_filename);
// ks =
process_katoms(
// ks, command_pathname,
ks, input_filename,
nonascii, literal, ignore, whitespace, klammers, eval, cond, read);
katoms.insert(end, ks.begin(), ks.end());
}
}
}
}
void Machine::extract_machine_definitions()
{
(void)K::log(3);
if (m_katoms.empty()) {
return;
}
for (const auto& [op, cl] : find_spans(m_katoms, begin_machine_def, end_machine_def, true, command_name)) {
auto [begin, end] = find_span_katoms(m_katoms, op, cl);
//std::string name = trim_char(begin->m_text, '@');
std::string name = begin->m_text;
if (name == "@@@target") {
m_targets.add(begin, end, m_katoms);
} else if (name == "@@@argtype") {
m_argtypes.add(begin, end, m_katoms);
} else if (name == "@@@state") {
m_state.parse_state_katoms(begin, end, m_katoms);
}
}
}
void Machine::extract_klammer_definitions(katom_list katoms)
{
fmsg() << katoms << "\n";
(void)K::log(3);
for (const auto& [op, cl] : find_spans(katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
expand_constant_klammers(katoms, op, cl);
auto [begin, end] = find_span_katoms(katoms, op, cl);
m_klammers.add(m_argtypes, m_targets, begin, end, katoms);
}
m_klammers.rationalize(m_targets);
}
void Machine::extract_klammer_definitions()
{
(void)K::log(3);
for (const auto& [op, cl] : find_spans(m_katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
expand_constant_klammers(m_katoms, op, cl);
auto [begin, end] = find_span_katoms(m_katoms, op, cl);
m_klammers.add(m_argtypes, m_targets, begin, end, m_katoms);
}
m_klammers.rationalize(m_targets);
}
void Machine::update_state(const std::map<std::string, std::string>& arg_map)
{
for (const auto& [k, v] : arg_map) {
m_state.set(k, v);
}
}
katom_list Machine::apply_klammer(
Klammer& klammer, const std::string& target, katom_iter arguments_begin, katom_iter arguments_end)
{
(void)K::log(3, "argument substitution", *arguments_begin, *(arguments_end - 1));
m_state.replace("K_loc", arguments_begin->m_loc.str(), false);
auto [positional, optional, rest] =
argument_split(arguments_begin + 1, arguments_end - 1, klammer.m_parameters.m_positional.size());
auto values = klammer.m_parameters.value_map(positional, optional, rest, arguments_begin->m_loc);
// Resolve KTESC markers in argument values so that @eval code receives
// the original characters (e.g., filenames with underscores). The markers
// remain in the klammer body substitution for final target-specific output.
katom_list result(klammer.m_body[target].begin(), klammer.m_body[target].end());
auto varmap = klammer.m_varmap[target];
m_state.open_frame("Arguments for klammer " + q_(klammer.m_name));
m_state.set(values);
for (const auto& [name, indices] : varmap) {
std::regex arg("\\*" + name + "\\*");
for (auto i : indices) {
result[i].m_text = std::regex_replace(result[i].m_text, arg, m_state.value(name));
result[i].m_type = katom_t::text;
}
}
process_katoms(result, klammer.m_name);
apply(m_klammers, result, target);
m_state.close_frame();
// msg() << boldblack << "APPLY: " << std::pair(arguments_begin, arguments_end) << "\n"
// << boldblack << "RESULT: " << ktype << result << black << "\n";
modify_type(katom_t::replaced, arguments_begin, arguments_end);
return result;
}
void Machine::apply_klammer_set(
Klammer_set& klammer_set, katom_list& katoms, const std::string& target, katom_iter begin, katom_iter end)
{
(void)K::log(3, "Klammer");
std::string name = trim_char(begin->m_text, '@');
katom_list applied_katoms = apply_klammer(klammer_set.m_klammers[name], target, begin, end);
for (auto& k : applied_katoms) {
if (k.m_type == katom_t::bar || k.m_type == katom_t::double_bar || k.m_type == katom_t::option_name) {
k.m_type = katom_t::text;
}
}
katoms.insert(end, applied_katoms.begin(), applied_katoms.end());
}
void Machine::apply(
Klammer_set& klammer_set, katom_list& katoms, const std::string& target)
{
(void)K::log(3, "Klammer_set");
for (const auto& [op, cl] : find_spans(
katoms, begin_klammer_apply, end_klammer_apply, true, command_name)) {
auto [begin, end] = find_span_katoms(katoms, op, cl);
klammer_set.check_klammer(
klammer_name_from_katom(begin->m_text, begin->m_loc),
target, begin->m_loc);
apply_klammer_set(klammer_set, katoms, target, begin, end);
}
}
std::string Machine::run_phase_functions()
{
Target target = m_targets.get(m_state.value("K_target"), Locator());
if (!target.m_after_apply.empty()) {
(void)K::log(2, target);
Eval E(*this, Locator());
for (auto f : target.m_after_apply) {
f = "@eval " + f + " @";
auto katoms = katomize(line_split(f), "phase");
katom_list eval_katoms = E.eval(katoms.begin(), katoms.end() - 2);
// msg() << "eval_katoms: " << eval_katoms << "\n";
m_result = to_string(eval_katoms.begin(), eval_katoms.end());
}
}
return m_result;
}
void Machine::escape_target_characters(const Target& target, katom_list& katoms)
{
if (target.m_escapes.empty()) return;
for (auto& k : katoms) {
// Only escape writer content katoms — text, words, and newlines.
// Skip structural katoms (option names, bars, klammer delimiters)
// whose text is Klammertext syntax, not writer content.
if (k.m_type == katom_t::text ||
k.m_type == katom_t::word ||
k.m_type == katom_t::newline) {
k.m_text = target.escape_text(k.m_text);
}
}
}
std::string Machine::apply(const std::string& target_name, bool final_processing, bool escape_characters)
{
(void)K::log(3, "top level");
int recursive_limit = 5;
m_state.set("K_target", target_name);
m_state.subst(m_katoms.begin(), m_katoms.end());
// Escape target-specific characters in writer text before klammer application.
// Characters produced later by klammer bodies will not be escaped.
// Skipped for sub-Machine apply() calls (e.g., from @eval), where the
// text is already in target-specific form.
auto target = m_targets.get(target_name, Locator());
if (escape_characters)
escape_target_characters(target, m_katoms);
int apply_count = 0;
auto katom_size = m_katoms.size();
while (true) {
apply(m_klammers, m_katoms, target_name);
if (m_katoms.size() == katom_size) {
break;
}
if (++apply_count > recursive_limit) {
msg() << red << "Error: Recursive limit ("
<< recursive_limit << ") reached\n" << black;
break;
}
katom_size = m_katoms.size();
}
m_result = to_string(m_katoms.begin(), m_katoms.end());
if (final_processing) {
for (const auto& [old_str, new_str] : target.m_transforms) {
m_result = string_replace(m_result, old_str, new_str);
}
m_result = target.resolve_escapes(m_result);
m_result = run_phase_functions();
}
m_result = trim_char(m_result, '\n');
return m_result;
}

128
mac/machine.h Normal file
View File

@@ -0,0 +1,128 @@
#pragma once
#include <string>
// #include "source.h"
#include "katom_list.h"
#include "klammer_set.h"
#include "target_set.h"
#include "argtype_set.h"
#include "state.h"
#include "error.h"
class Machine
{
public:
using input_sources_t =
std::vector<std::variant<fs::path, std::string>>;
Machine();
// Copy constructor
Machine(const Machine& other)
: m_argtypes(other.m_argtypes)
, m_state(other.m_state)
, m_targets(other.m_targets)
, m_klammers(other.m_klammers)
, m_result(other.m_result)
{}
// Copy assignment operator
Machine& operator=(const Machine& other) {
if (this != &other) {
m_argtypes = other.m_argtypes;
m_state = other.m_state;
m_targets = other.m_targets;
m_klammers = other.m_klammers;
m_result = other.m_result;
}
return *this;
}
void process_cond_katoms(std::vector<Katom>& katoms);
void process_eval_katoms(std::vector<Katom>& katoms);
void mark_literal_klammer_content(std::vector<Katom>& katoms);
void escape_target_characters(const Target& target, std::vector<Katom>& katoms);
// std::vector<Katom>
void process_katoms(
std::vector<Katom>& katoms, const std::string& source,
bool nonascii=true, bool literal=true, bool ignore=true, bool whitespace=true,
bool klammers=true, bool eval=true, bool cond=true, bool read=true);
std::vector<Katom> process(
std::string text, const std::string& source,
bool nonascii=true, bool literal=true, bool ignore=true, bool whitespace=true,
bool klammers=true, bool eval=true, bool cond=true, bool read=true);
void read(const fs::path& pathname);
void read(const std::string& s);
void expand_read_katoms(
std::vector<Katom>& katoms, std::string source_filename,
bool nonascii=true, bool literal=true, bool ignore=true, bool ws=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 extract_machine_definitions();
void extract_klammer_definitions();
void extract_klammer_definitions(katom_list katoms);
void update_state(const std::map<std::string, std::string>& arg_map);
katom_list apply_klammer(Klammer& klammer, const std::string& target, katom_iter arguments_begin, katom_iter arguments_end);
void apply_klammer_set(Klammer_set& klammer_set,
katom_list& katoms, const std::string& target, katom_iter begin, katom_iter end);
void apply(Klammer_set& klammer_set, katom_list& katoms, const std::string& target);
std::string run_phase_functions();
std::string apply(const std::string& target_name, bool final_processing=true, bool escape_characters=true);
Argtype_set m_argtypes {};
State m_state {};
Target_set m_targets {};
Klammer_set m_klammers {};
input_sources_t m_sources {};
std::string m_result {};
std::vector<Katom> m_katoms {};
};
/*
class Machine {
private:
std::string m_name;
int m_id;
std::vector<std::string> m_sources;
bool m_active;
double m_value;
public:
// Default constructor
Machine() : m_name(""), m_id(0), m_sources(), m_active(false), m_value(0.0) {}
// Copy constructor
Machine(const Machine& other)
: m_name(other.m_name) // Copy name
, m_id(other.m_id) // Copy id
, m_sources() // Initialize m_sources as empty
, m_active(other.m_active) // Copy active status
, m_value(other.m_value) // Copy value
{
// m_sources is now empty, ready for new initialization
}
// Assignment operator (if needed)
Machine& operator=(const Machine& other) {
if (this != &other) {
m_name = other.m_name;
m_id = other.m_id;
m_sources.clear(); // Clear and leave empty
m_active = other.m_active;
m_value = other.m_value;
}
return *this;
}
};
*/

559
mac/show.cpp Normal file
View File

@@ -0,0 +1,559 @@
#include <algorithm>
#include <iterator>
#include "ktype.h"
#include "show.h"
#include "util.h"
#include "log.h"
#include "file.h"
std::ostream& msg(Locator loc)
{
std::cout << blue << loc.abbrev(false) << black << " ";
return std::cout;
}
std::ostream& fmsg(Locator loc)
{
std::cout << blue << loc.abbrev(false) << black
<< kall << ktype << kindex << kreplaced << " ";
return std::cout;
}
// std::vector<int>
std::ostream& operator<<(std::ostream& os, const std::vector<int>& ns)
{
if (!ns.empty()) {
auto rest = std::vector<int>(ns.begin() + 1, ns.end());
os << ns[0];
for (auto n : rest) {
os << ", " << n;
}
}
return os;
}
// strings_t
std::ostream& operator<<(std::ostream& os, const strings_t& ss)
{
if (!ss.empty()) {
for (const std::string& s : ss) {
os << "<" << s << ">";
}
}
return os;
}
// std::vector<fs::path>
std::ostream& operator<<(std::ostream& os, const std::vector<fs::path>& pp)
{
if (!pp.empty()) {
for (const fs::path& p : pp) {
os << "<" << p.string() << ">";
}
}
return os;
}
// std::map<std::string,std::string>
std::ostream& operator<<(std::ostream& os, const std::map<std::string,std::string>& sm)
{
size_t name_length = 0;
for (auto [k,v] : sm) {
name_length = std::max(k.size(), name_length);
}
for (auto [k,v] : sm) {
os << std::setfill(' ') << " " << std::setw(name_length)
<< k << sp_arrow << " " << display_string(v) << "\n";
}
return os;
}
// Katom iterator pair
std::ostream& operator<<(std::ostream& os, const std::pair<katom_iter,katom_iter>& iters)
{
for (auto e = iters.first; e < iters.second; e++) {
std::cout << e;
}
std::cout << "\n";
return os;
}
// Locator
std::ostream& operator<<(std::ostream& os, const Locator& loc)
{
os << loc.str();
return os;
}
// Katom
std::string subscript(int n)
{
strings_t chars {
"\u2080", "\u2081", "\u2082", "\u2083", "\u2084",
"\u2085", "\u2086", "\u2087", "\u2088", "\u2089" };
std::string prefix {};
if (n < 0)
prefix = "-";
std::string sn {std::to_string(std::abs(n))};
std::string result { prefix };
for (unsigned int i = 0; i < sn.size(); i++) {
std::string d {sn[i]};
result += chars[std::stoi(d)];
}
return result;
}
void os_char(std::ostream& os, katom_t type, unsigned char c)
{
if (type == katom_t::eval_result) {
os << c;
} else if ((Katom::show_all || Katom::show_whitespace) and c == ' ') {
if (is_active(type)) os << boldgreen;
const std::string space_s { "\u00B7" };
os << space_s;
if (is_active(type)) os << black;
} else if ((Katom::show_all || Katom::show_whitespace) and c == '\n') {
if (is_active(type)) os << boldgreen;
os << "/";
if (is_active(type)) os << black;
} else if (c != '\n')
os << c;
}
void os_body(std::ostream& os, const Katom& k)
{
bool bracketed = true;
if (k.is_whitespace() || k.is_word() || k.is_text() || k.is_literal() || k.is_nonascii()) {
bracketed = Katom::show_all;
}
if (bracketed)
os << left_bracket;
for (auto c : k.m_text) {
os_char(os, k.m_type, c);
}
if (bracketed) {
if (Katom::show_index) {
os << subscript(k.m_index);
}
if (Katom::show_type){
if (Katom::show_index) {
os << ".";
}
if (k.m_initial_type != k.m_type) {
os << subscript(static_cast<int>(k.m_initial_type)) << right_arrow;
}
os << subscript(static_cast<int>(k.m_type));
}
os << right_bracket;
}
}
std::ostream& operator<<(std::ostream& os, const Katom& k)
{
//const std::string space_s { "\u2423" };
/// const std::string space_s { "\u00B7" };
// const std::string tab { "\u2023" };
// const std::string left_index { "\u27EA" };
// const std::string right_index { "\u27EB" };
// const std::string diamond { "\u2B29" };
//bool in_red = mark_as_error(k.m_type);
//if (k.m_type == katom_t::ignored)
// show_type = false;
//if (in_red)
//if (is_error(k.m_type))
// os << red;
if (k.m_type == katom_t::replaced) {
os << blue; // cyan;
} else if (k.m_type == katom_t::ignored) {
os << yellow;
}
//else if (k.m_type == katom_t::literal)
// os << green;
else {
os << black;
}
if (is_active(k.m_type) or
(Katom::show_replaced and (k.m_type == katom_t::replaced)) or
(Katom::show_ignored and (k.m_type == katom_t::ignored))) {
os_body(os, k);
if (k.m_initial_type == katom_t::newline ||
(k.m_type == katom_t::ws_added && k.m_text == "\n")) {
os << "\n";
}
}
//os << black;
return os;
}
katom_list abbrev(const katom_list& ks, unsigned int max_length)
{
if (ks.size() > max_length) {
katom_list result(ks.begin(), (ks.begin())+(max_length));
Katom ellipsis = Katom("[...]", katom_t::word, Locator());
result.push_back(ellipsis);
result.push_back(ks.back());
std::for_each(result.begin(), result.end(), [](Katom& k) {
if (k.m_type == katom_t::newline) {
//std::cout << "NEWLINE\n";
k.m_type = katom_t::word;
k.m_text = "/";
k.m_src = "/";
k.m_initial_type = katom_t::word;
}
});
return result;
} else {
katom_list result(ks.begin(), ks.end());
std::for_each(result.begin(), result.end(), [](Katom& k) {
if (k.m_type == katom_t::newline) {
//std::cout << "NEWLINE\n";
k.m_type = katom_t::word;
k.m_text = "/";
k.m_initial_type = katom_t::word;
}
});
return result;
}
}
std::ostream& operator<<(std::ostream& os, const katom_lists& ks)
{
//os << double_bar;
//os << red < "|" << black;
for (const auto& k : ks) {
os << left_double_bracket << abbrev(k) << right_double_bracket << " ";
//os << k << red << "|" << black;
}
// os << "\n";
return os;
}
std::ostream& operator<<(std::ostream& os, const katom_ptr& k)
{
os << *k;
return os;
}
std::ostream& operator<<(std::ostream& os, const katom_iter& k)
{
os << *k;
return os;
}
std::ostream& operator<<(std::ostream& os, const std::vector<Katom>& ks)
{
for (const auto& k : ks) {
os << k;
}
return os;
}
std::ostream& operator<<(std::ostream& os, const std::vector<katom_iter>& ks)
{
for (const auto& k : ks) {
os << *k;
}
return os;
}
std::ostream &kindex(std::ostream &os)
{
Katom::show_index = true;
return os;
}
std::ostream &ktype(std::ostream &os)
{
Katom::show_type = true;
return os;
}
std::ostream &kws(std::ostream &os)
{
Katom::show_whitespace = true;
return os;
}
std::ostream &kall(std::ostream &os)
{
Katom::show_all = true;
return os;
}
std::ostream &kreplaced(std::ostream &os)
{
Katom::show_replaced = true;
return os;
}
std::ostream &kignored(std::ostream &os)
{
Katom::show_ignored = true;
return os;
}
std::ostream &kreset(std::ostream &os)
{
Katom::show_index = false;
Katom::show_type = false;
Katom::show_all = false;
Katom::show_replaced = false;
Katom::show_ignored = false;
Katom::show_whitespace = false;
os << black;
return os;
}
/*
std::ostream& operator<<(std::ostream& os, const std::vector<Katom> ks)
{
std::ranges::copy(ks, std::ostream_iterator<Katom>(os, ""));
return os;
}
*/
// Argtype
std::ostream& operator<<(std::ostream& os, const Argtype& a)
{
return os
<< left_bracket << "\U0001D504" << broken_bar
<< a.m_name << broken_bar
<< a.m_symbolic_pattern << broken_bar
<< a.m_count << broken_bar
<< a.m_mincount << broken_bar
<< a.m_maxcount << right_bracket;
}
// Parameter
std::ostream& operator<<(std::ostream& os, const Parameter& arg)
{
os << left_bracket << "\U0001D513" << broken_bar;
if (arg.m_optional)
os << ":";
os << arg.m_name << "." << arg.m_argtype.m_name;
//if (arg.m_default.size() != 0)
if (!arg.m_default.empty()) {
os << broken_bar << arg.m_default;
}
os << right_bracket;
return os;
}
// std::vector<Parameter>
std::ostream& operator<<(std::ostream& os, const std::vector<Parameter>& as)
{
for (const auto& a : as) {
os << a;
}
return os;
}
// Parameter_set
std::ostream& operator<<(std::ostream& os, const Parameter_set& as)
{
os << left_bracket << "\U0001D513\U0001D530" << broken_bar
<< as.m_positional.size() << broken_bar
<< as.m_optional.size() << broken_bar
// << (as.m_rest.undefined() ? "-" : "+")
<< (as.m_rest.empty() ? "-" : "+")
<< broken_bar << "[" << as.m_katoms.size() << "]"
<< right_bracket;
return os;
}
// Var
std::ostream& operator<<(std::ostream& os, const Var& v)
{
os << "<" << v.m_name << ">";
return os;
}
// Frame
std::ostream& operator<<(std::ostream& os, const Frame& f)
{
os << "{";
// for (auto [k,v] : f.m_vars) {
// os << v;
// }
os << join(f.names(), "|");
os << "}";
return os;
}
// State
std::ostream& operator<<(std::ostream& os, const State& s)
{
//for (int i = s.m_frames.size() - 1; i != 0; --i) {
for (auto f : s.m_frames) {
os << f << "\n";
}
return os;
}
// Klammer
std::ostream& operator<<(std::ostream& os, const Klammer& k)
{
std::string targets = join(k.get_target_names(), ","s);
os << left_bracket << "\U0001D50E" << broken_bar
<< k.m_name << broken_bar
<< "p" << k.m_parameters.m_positional.size() << broken_bar
<< "o" << k.m_parameters.m_optional.size() << broken_bar
<< (targets.empty() ? "?" : targets)
<< right_bracket;
return os;
}
// Klammer::variable_map_t
std::ostream& operator<<(std::ostream& os, const Klammer::variable_map_t& vm)
{
for (auto [k, v] : vm) {
os << " " << k << sp_arrow << v << "\n";
}
return os;
}
// Klammer::components
std::string katom_type_name(katom_t type)
{
// Could cache, but why bother - only for kdesc.
for (auto t : katom_types) {
if (t.m_type == type) {
return t.m_name;
}
}
throw Internal_error("Unknown katom_t: " + std::to_string((int)type));
}
std::ostream& operator<<(std::ostream& os, const Klammer::components& kc)
{
std::string del = " ";
auto [target, deftype, parameters, body, varmap, locator] = kc;
os << std::setw(4) << target << del
// << katom_types[(int)deftype].m_name << del
<< katom_type_name(deftype) << del
<< parameters << del << kall << ktype << red << body << del << locator;
return os;
}
// Klammer_set
std::ostream& operator<<(std::ostream& os, const Klammer_set& ks)
{
for (auto klam : ks.m_klammers) {
for (auto [k,v] : klam.second.m_defloc) {
os << " " << k << sp_arrow << v << "\n";
}
//klam.second.m_defloc.str() << "\n";
std::cout << " " << klam.first << sp_arrow << klam.second << " " << "\n";
}
return os;
}
// Target
void show_arrow_pair(std::ostream& os, std::pair<std::string,std::string> transform)
{
os << transform.first << sp_arrow << transform.second;
}
std::ostream& operator<<(std::ostream& os, const Target& t)
{
os << "<\U0001D517" << broken_bar << t.m_name << broken_bar << t.m_desc << broken_bar;
for (auto i : t.m_includes) {
os << i << right_arrow << t.m_name << broken_bar;
}
for (auto p : t.m_provides) {
os << t.m_name << right_arrow << p << broken_bar;
}
os << ">";
return os;
}
// Target_set
std::ostream& operator<<(std::ostream& os, const Target_set& ts)
{
size_t width = 0;
for (auto t : ts.m_targets) {
width = std::max(width, t.first.size());
}
for (auto [name, target] : ts.m_targets) {
os << std::setw(width) << name << sp_arrow << target << "\n";
}
return os;
}
// Machine
std::string describe_sources(Machine m)
{
std::stringstream ss {};
for (auto s : m.m_sources) {
if (std::holds_alternative<std::string>(s)) {
ss << " String: " << trim(std::get<std::string>(s)) << "\n";
} else {
ss << " Filename: " << std::get<fs::path>(s).string() << "\n";
}
}
return ss.str();
}
std::string label(std::string name, int count)
{
std::stringstream ss {};
ss << " " << name << " (" << count << "):\n";
return ss.str();
}
std::ostream& operator<<(std::ostream& os, const Machine& m)
{
std::string argtypes_desc = m.m_argtypes.describe(false, 8);
std::string state_desc = m.m_state.describe(false, 3);
std::string targets_desc = m.m_targets.describe(4);
std::string klammer_desc = m.m_klammers.describe(2);
std::string source_desc = describe_sources(m);
os << "\nMachine " << "\U000133DE \U00013000\n"
<< label("Sources", m.m_sources.size()) << source_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("Klammers", m.m_klammers.m_klammers.size()) << klammer_desc << "\n"
<< label("State", m.m_state.m_frames.size()) << state_desc;
return os;
}
void modify_stream(std::string name)
{
if (name == "all") std::cout << kall;
if (name == "type") std::cout << ktype;
if (name == "index") std::cout << kindex;
if (name == "ignored") std::cout << kignored;
if (name == "replaced") std::cout << kreplaced;
};

158
mac/show.h Normal file
View File

@@ -0,0 +1,158 @@
#pragma once
#include <iostream>
#include <vector>
#include "locator.h"
#include "katom.h"
#include "argtype.h"
#include "argument.h"
#include "klammer.h"
#include "klammer_set.h"
#include "state.h"
#include "target_set.h"
#include "machine.h"
#include "file.h"
using namespace std::string_literals;
const std::string middle_dot { "\uFF65" };
const std::string broken_bar { "\u00A6" };
const std::string right_arrow { "\uFFEB" };
const std::string sp_arrow { " \u2192 " };
const std::string right_bracket { "\u27E9" };
const std::string left_bracket { "\u27E8" };
const std::string bbar { "¦" };
const std::string left_double_bracket { "\u27E6" };
const std::string right_double_bracket { "\u27E7" };
const std::string left_square_bracket { "\u2045" };
const std::string right_square_bracket { "\u2046" };
const std::string check { "\u2713" };
const std::string black("\033[0;30m");
const std::string boldblack("\033[1;30m");
const std::string green("\033[0;32m");
const std::string boldgreen("\033[1;32m");
const std::string cyan("\033[0;36m");
const std::string blue("\033[0;34m");
const std::string boldblue("\033[1;34m");
const std::string magenta("\033[0;35m");
const std::string red("\033[31m");
const std::string yellow("\033[0;33m");
const std::string reset("\033[0m");
const auto seqout = [](auto x) { std::cout << "seq: " << x << "\n"; };
//const auto mapout = [](auto m) { auto const& key std::cout << m.first << sp_arrow << m.second << "\n"; };
const auto mapout = [](auto const& kv){
auto const& [k, v] = kv;
std::cout << k << sp_arrow << v << "\n";
};
inline const
std::set sks_commands = {
"ktext"s,
"kdesc"s,
"kdiag"s,
"argument_test"s,
"argtype_test"s
};
std::ostream& msg(Locator loc=Locator());
std::ostream& fmsg(Locator loc=Locator());
std::vector<Katom> abbrev(const std::vector<Katom>& ks, unsigned int max_length=16);
// std::vector<int>
std::ostream& operator<<(std::ostream& os, const std::vector<int>& ns);
// std::vector<std::string>
std::ostream& operator<<(std::ostream& os, const std::vector<std::string>& ss);
// std::vector<fs::path>
std::ostream& operator<<(std::ostream& os, const std::vector<fs::path>& pp);
// std::map<std::string,std::string>
std::ostream& operator<<(std::ostream& os, const std::map<std::string,std::string>& sm);
// Katom iterator pair
std::ostream& operator<<(
std::ostream& os,
const std::pair<std::vector<Katom>::iterator, std::vector<Katom>::iterator>& iters);
// Locator
std::ostream& operator<<(std::ostream& os, const Locator& loc);
// Katom
std::ostream& operator<<(std::ostream& os, const Katom& k);
std::ostream& operator<<(std::ostream& os, const std::vector<std::vector<Katom>>& ks);
std::ostream& operator<<(std::ostream& os, const std::shared_ptr<Katom>& k);
std::ostream& operator<<(std::ostream& os, const std::vector<Katom>::iterator& k);
std::ostream& operator<<(std::ostream& os, const std::vector<Katom>& ks);
std::ostream& operator<<(std::ostream& os, const std::vector<std::vector<Katom>::iterator>& ks);
std::ostream& kindex(std::ostream& os);
std::ostream& ktype(std::ostream& os);
std::ostream& klevel(std::ostream& os);
// std::ostream& kspans(std::ostream& os);
std::ostream& kws(std::ostream& os);
std::ostream& kall(std::ostream& os);
std::ostream& kreplaced(std::ostream& os);
std::ostream& kignored(std::ostream& os);
std::ostream& kreset(std::ostream& os);
inline static std::string newline_symbol = "/";
std::ostream& operator<<(std::ostream& os, const std::vector<Katom>& ks);
std::ostream& operator<<(std::ostream& os, const std::vector<Katom>& ks);
// Argtype
std::ostream& operator<<(std::ostream& os, const Argtype& a);
// Parameter (aliased as Argument at application sites)
std::ostream& operator<<(std::ostream& os, const Parameter& p);
// Parameter_set (aliased as Argument_set at application sites)
std::ostream& operator<<(std::ostream& os, const Parameter_set& as);
// std::vector<Parameter>
std::ostream& operator<<(std::ostream& os, const std::vector<Parameter>& as);
// Var
std::ostream& operator<<(std::ostream& os, const Var& v);
// Frame
std::ostream& operator<<(std::ostream& os, const Frame& f);
// State
std::ostream& operator<<(std::ostream& os, const State& s);
// Klammer
std::ostream& operator<<(std::ostream& os, const Klammer& k);
// Klammer::variable_map_t
std::ostream& operator<<(std::ostream& os, const Klammer::variable_map_t& vm);
// Klammer::components
std::ostream& operator<<(std::ostream& os, const Klammer::components& kc);
// Klammer_set
std::ostream& operator<<(std::ostream& os, const Klammer_set& ks);
// Target
std::ostream& operator<<(std::ostream& os, const Target& t);
// Target_set
std::ostream& operator<<(std::ostream& os, const Target_set& ts);
// Machine
std::ostream& operator<<(std::ostream& os, const Machine& m);
void modify_stream(std::string name);

308
mac/state.cpp Normal file
View File

@@ -0,0 +1,308 @@
#include <regex>
#include <iostream>
#include "show.h"
#include "file.h"
#include "error.h"
#include "state.h"
#include "log.h"
#include "util.h"
int State::class_id = 0;
bool Var::defined()
{
return !m_name.empty();
}
std::vector<std::string> Frame::names() const
{
std::vector<std::string> result;
result.reserve(m_vars.size());
std::transform(m_vars.begin(), m_vars.end(), std::back_inserter(result),
[](const auto& pair) { return pair.first; });
return result;
}
void Frame::set(std::string name, std::string value,
std::string delim, std::string desc, Locator loc)
{
Var v(name, value, delim, desc, loc);
m_vars[name] = v;
}
std::pair<Var, bool> Frame::get(std::string name)
{
std::pair<Var, bool> result {Var(), false};
if (m_vars.contains(name)) {
result = {m_vars[name], true};
}
return result;
}
// State
void State::open_frame(std::string name)
{
Frame f(name);
// m_frames.push_back(f);
m_frames.emplace(m_frames.begin(), f);
}
void State::close_frame()
{
if (m_frames.empty()) {
throw Internal_error("No frame to close", Locator());
}
// Preserve altered machine state:
string_map machine_state {};
for (auto [name, var] : m_frames[0].m_vars) {
if (contains(name, "K_")) {
machine_state[name] = var.m_value;
}
}
m_frames.erase(m_frames.begin());
for (auto [name, value] : machine_state) {
set(name, value, true);
}
}
void State::set(std::string name, std::string value, bool update,
std::string delim, std::string desc, Locator loc)
{
if (m_frames.empty()) {
std::stringstream ss {};
ss << "No open frame to set " << q_(name) << " to " << q_(value);
throw Internal_error(ss.str(), Locator());
}
auto [current, exists] = m_frames[0].get(name);
if (exists && current.m_value != klammerstate::no_value && !update) {
std::stringstream ss {};
ss << "Variable " << q_(name) << " is already defined at " << current.m_loc.desc()
<< ". Use ':replace <new-value>' to replace the current value of "
<< q_(current.m_value) << ".";
throw Argument_error(ss.str(), current.m_loc);
}
m_frames[0].set(name, value, delim, desc, loc);
}
void State::set(std::map<std::string, std::string> varmap)
{
for (auto [k, v] : varmap) {
set(k, v);
}
}
void State::replace(std::string name, std::string value, bool error_if_not_defined)
{
if (error_if_not_defined && !get(name).defined()) {
std::stringstream ss {};
ss << "Cannot replace value of nonexistent variable " << q_(name) << " with " << q_(value);
throw Argument_error(ss.str(), Locator());
}
set(name, value, true);
}
void State::add_environment_frame()
{
open_frame(klammerstate::shell_environment_name);
for (auto [name, value] : environment_variables()) {
set(name, value);
}
}
Var State::get(std::string name, bool error_if_not_defined, Locator loc)
{
for (auto f : m_frames) {
auto [result, found] = f.get(name);
if (found) {
return result;
}
}
if (error_if_not_defined) {
msg() << describe();
throw Argument_error("Variable " + q_(name) + " not defined", loc);
} else {
return Var();
}
}
std::string State::value(std::string name, bool error_if_not_defined, Locator loc)
{
return get(name, error_if_not_defined, loc).m_value;
}
std::string State::subst(std::string text, bool quote_values)
{
(void)K::log(3);
std::string result = text;
std::regex varpat(R"(\*(\w+)\*)");
for (std::sregex_iterator iter(text.begin(), text.end(), varpat), end; iter != end; ++iter) {
std::string match = iter->str();
std::string var = (*iter)[1].str();
//std::cout << "Found: " << iter->str() << sp_arrow << (*iter)[1].str() << "\n";
// std::cout << "Found: " << match << sp_arrow << var << "\n";
//std::string value = get(var).m_value;
auto var_value = value(var, false);
auto printable = q_(var_value);
if (var_value != klammerstate::no_value) {
if (quote_values) {
var_value = q_(var_value);
}
result = string_replace(result, match, var_value);
} else {
// throw Argument_error("Variable " + printable + " is not defined");
}
}
return result;
}
void State::subst(katom_iter begin, katom_iter end)
{
(void)K::log(3);
std::regex varpat(R"((.*?)\*(\w+)\*(.*))");
for (auto ki = begin; ki < end ; ki++) {
// msg() << kall << ktype << *ki << "\n";
std::smatch match;
if (std::regex_match(ki->m_text, match, varpat)
&& ki->m_type == katom_t::karg) {
//auto [var, found] = get(match[1]);
auto var_value = value(match[2], true, begin->m_loc);
if (var_value != klammerstate::no_value) {
msg() << "Found subst: " << match[1] << sp_arrow << var_value << "\n";
std::stringstream ss {};
ss << match[1] << var_value << match[3];
ki->m_text = ss.str(); // match[1] + var_value + match[3];
} else {
throw Argument_error("Variable " + q_(match[1]) + " is not defined", begin->m_loc);
}
}
}
}
void prohibit_change_of_description(
std::string name, bool defined, std::string old_desc, std::string new_desc, Locator old_loc, Locator loc)
{
if (defined && !old_desc.empty() && !new_desc.empty()) {
std::stringstream ss {};
ss << "The " << q_(name) << " variable's description is already defined";
if (new_desc != old_desc) {
ss << "; the description cannot be changed to " << q_(new_desc)
<< " from " << q_(old_desc);
}
ss << " at " << old_loc.desc() << ".";
throw Argument_error(ss.str(), loc);
}
}
// Parameter_set m_parameters = Parameter_set("name :value :append :replace :delim :desc");
void State::parse_state_katoms(katom_iter begin, katom_iter end, katom_list katoms)
{
(void)K::log(3, *begin, *(end-1));
// std::cout << "parse_katoms: " << std::pair(begin + 1, end - 1) << "\n";
auto [positional, optional, rest] = argument_split(begin + 1, end - 1);
auto args = m_parameters.value_map(positional, optional, rest, begin->m_loc);
// std::cout << std::setfill(' ') << "\nArgument values:\n" << args;
Var current = get(args["name"]);
bool defined = current.defined();
std::string delim = args["delim"];
delim = delim.empty() ? " " : delim;
prohibit_change_of_description(
args["name"], defined, current.m_desc, args["desc"], current.m_loc, begin->m_loc);
if (defined && !args["replace"].empty()) {
replace(args["name"], args["replace"]);
} else if (defined && !args["append"].empty()) {
replace(args["name"], current.m_value + delim + args["append"]);
} else if (!args["value"].empty()) {
set(args["name"], args["value"], false, delim, args["desc"], begin->m_loc);
}
modify_type(katom_t::replaced, begin, end);
auto next_iter = end;
ignore_whitespace(next_iter, katoms);
}
std::vector<std::string> State::all_names()
{
std::vector<std::string> result {};
for (Frame f : m_frames) {
for (auto [name, var] : f.m_vars) {
// std::cout << "Name: " << name << "\n";
result.push_back(name);
}
}
return result;
}
std::string State::python_code()
{
(void)K::log(3);
std::vector<std::string> names = all_names();
std::string margin = " ";
std::stringstream ss {};
ss << "import sys\n";
for (auto d : sks_dirs()) {
auto python_files = pathnames_with_extension(d, "py");
if (!python_files.empty()) {
ss << "sys.path.append('" << d << "')\n";
}
}
if (!m_frames.empty()) {
int name_length = max_length(names);
ss << "class K:\n"
<< margin << std::left << std::setw(name_length) << "K_eval_id" << " = "
<< State::class_id++ << "\n";
for (const auto& name : names) {
// auto [var_value, argtype] = value_type(name);
// ss << argtype.python_value(name, var_value, name_length) << "\n";
std::string v = value(name);
v = string_replace(v, "\\", "\\\\");
v = string_replace(v, "\"", "\\\"");
std::string var_value = qq_(v);
ss << margin << std::left << std::setw(name_length) << name << " = " << var_value << "\n";
}
}
// msg() << ss.str() << "\n";
return ss.str();
}
std::string State::describe(bool show_environment, int margin_size) const
{
std::string margin(margin_size, ' ');
int i = m_frames.size() - 1;
std::stringstream ss {};
for (auto f : m_frames) {
int width = max_key_length(f.m_vars);
ss << margin << "Frame " << i-- << ": " << f.m_name << "\n";
if ((f.m_name != klammerstate::shell_environment_name) ||
(show_environment && f.m_name == klammerstate::shell_environment_name)) {
for (auto [key, value] : f.m_vars) {
std::string print_value = value.m_value;
if (print_value == klammerstate::no_value) {
print_value = "<no-value>";
}
ss << margin << " " << std::setw(width) << std::left << key << " "
<< abbrev(print_value) << "\n";
}
}
}
ss << "\n";
return ss.str();
}

80
mac/state.h Normal file
View File

@@ -0,0 +1,80 @@
#pragma once
#include "util.h"
#include "locator.h"
//#include "argtype.h"
#include "argument_set.h"
namespace klammerstate {
inline std::string no_value = "\0";
}
class Var
{
public:
Var() = default;
Var(std::string name, std::string value=klammerstate::no_value,
std::string delim=":", std::string desc="", Locator loc=Locator())
: m_name(name)
, m_value(value)
, m_delim(delim)
, m_desc(desc)
, m_loc(loc)
{};
bool defined();
std::string m_name {};
std::string m_value {};
std::string m_delim {};
std::string m_desc {};
Locator m_loc {};
};
namespace klammerstate {
inline std::string shell_environment_name = "Shell environment";
}
class Frame
{
public:
Frame(std::string name)
: m_name(name)
{};
std::vector<std::string> names() const;
void set(std::string name, std::string value,
std::string delim=":", std::string desc="", Locator loc=Locator());
std::pair<Var, bool> get(std::string name);
std::string m_name {};
std::map<std::string, Var> m_vars {};
};
class State
{
public:
static int class_id;
void open_frame(std::string name);
void close_frame();
void set(std::string name, std::string value, bool update=false,
std::string delim=" ", std::string desc="", Locator loc=Locator());
void set(std::map<std::string, std::string> varmap);
void replace(std::string name, std::string value, bool error_if_not_defined=true);
void add_environment_frame();
Var get(std::string name, bool error_if_not_defined=false, Locator loc=Locator());
std::string value(std::string name, bool error_if_not_defined=true, Locator loc=Locator());
std::string subst(std::string text, bool quote_values=false);
void subst(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end);
void parse_state_katoms(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, katom_list katoms);
std::vector<std::string> all_names();
std::string python_code();
std::string describe(bool show_environment=false, int margin_size=2) const;
std::vector<Frame> m_frames {};
// @@@state Image_search_path :set :append :replace :argtype :desc
// Parameter_set m_parameters = Parameter_set("name :set :append :replace :argtype :delim :desc");
Parameter_set m_parameters = Parameter_set("name :value :append :replace :delim :desc");
};

108
mac/target.cpp Normal file
View File

@@ -0,0 +1,108 @@
#include "target.h"
#include "log.h"
#include "show.h"
//#include "text.h"
#include "util.h"
void Target::add_transform(std::string original, std::string transformed)
{
m_transforms.push_back({original, transformed});
}
void Target::add_transforms(std::string transforms)
{
add_transforms(parse_transforms(transforms));
}
void Target::add_transforms(string_pairs transforms)
{
for (auto [old_str, new_str] : transforms) {
add_transform(old_str, new_str);
}
}
void Target::transform(katom_list& katoms)
{
(void)K::log(3);
std::for_each(
katoms.begin(), katoms.end(),
[this] (Katom& k) {
// std::cout << "transform: " << k << "\n";
if (k.m_type != katom_t::literal) {
for (auto [a, b] : this->m_transforms) {
// std::cout << " " << a << right_arrow << b << "\n";
k.m_text = string_replace(k.m_text, a, b);
}
}
});
}
std::vector<std::pair<std::string, std::string>>
parse_transforms(std::string transform_string)
{
(void)K::log(3);
if (trim(transform_string).empty()) return {};
auto transforms = regex_split(transform_string, std::regex(R"(\s*\|\s*)"), true);
std::vector<std::pair<std::string, std::string>> result;
std::transform(transforms.begin(), transforms.end(), std::back_inserter(result),
[] (std::string s) {
strings_t v = word_split(s);
if (v.size() < 2) return std::pair(std::string{}, std::string{});
return std::pair(v[0], v[1]);
});
// Remove empty pairs
result.erase(std::remove_if(result.begin(), result.end(),
[](const auto& p) { return p.first.empty(); }), result.end());
return result;
}
void Target::add_escapes(std::string escape_spec)
{
auto words = word_split(escape_spec);
for (size_t i = 0; i + 1 < words.size(); i += 2) {
m_escapes.push_back({words[i], words[i+1]});
}
}
std::string Target::escape_marker(const std::string& ch)
{
std::stringstream ss {};
ss << "KTESC";
for (unsigned char c : ch)
ss << std::hex << std::setfill('0') << std::setw(4) << (int)c;
ss << "KTESC";
return ss.str();
}
std::string Target::escape_text(std::string text) const
{
for (const auto& [ch, repl] : m_escapes) {
text = string_replace(text, ch, escape_marker(ch));
}
return text;
}
std::string Target::unescape_text(std::string text) const
{
// Restore KTESC markers to original characters (for programmatic use)
for (const auto& [ch, repl] : m_escapes) {
text = string_replace(text, escape_marker(ch), ch);
}
return text;
}
std::string Target::resolve_escapes(std::string text) const
{
for (const auto& [ch, repl] : m_escapes) {
text = string_replace(text, escape_marker(ch), repl);
}
return text;
}
void Target::add_after_apply(std::string function_specs)
{
for (auto f : regex_split(function_specs, std::regex(R"(\s+;\s+)"), true)) {
// msg() << "Add " << m_name << " after-apply: " << f << "\n";
m_after_apply.push_back(f);
}
}

49
mac/target.h Normal file
View File

@@ -0,0 +1,49 @@
#pragma once
#include <map>
#include <iomanip>
#include <sstream>
#include "katom.h"
#include "locator.h"
class Target
{
public:
Target() = default;
// Target(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, Argtype_set argtypes);
Target(std::string name, std::string desc, Locator loc)
: m_name(name)
, m_desc(desc)
, m_loc(loc)
{};
void add_transform(std::string original, std::string transformed);
void add_transforms(std::string transforms);
void add_transforms(std::vector<std::pair<std::string, std::string>> transforms);
void transform(std::vector<Katom>& katoms);
void add_escapes(std::string escape_spec);
std::string escape_text(std::string text) const;
std::string unescape_text(std::string text) const;
std::string resolve_escapes(std::string text) const;
static std::string escape_marker(const std::string& ch);
void add_after_apply(std::string function_specs);
std::string m_name {};
std::string m_desc {};
std::vector<std::string> m_includes {};
std::vector<std::string> m_provides {};
std::vector<std::string> m_after_apply {};
Locator m_loc {};
std::vector<std::pair<std::string, std::string>> m_transforms {};
std::vector<std::pair<std::string, std::string>> m_escapes {};
// Argtype_set m_argtypes {};
};
std::vector<std::pair<std::string, std::string>>
parse_transforms(std::string transform_string);

160
mac/target_set.cpp Normal file
View File

@@ -0,0 +1,160 @@
#include <sstream>
#include <iterator>
#include <algorithm>
#include "target_set.h"
#include "error.h"
#include "log.h"
#include "util.h"
#include "show.h"
#include "log.h"
#include "katom.h"
std::string Target_set::declare_name = "k";
std::string Target_set::general_name = "*";
Target_set::Target_set()
: m_parameters(Parameter_set("name | desc :after_apply :after_write :includes :escape | transforms.rest"))
{
Target k(declare_name, "Description of parameters and klammer result", Locator());
Target general(general_name, "General target, used when a target is not specified", Locator());
add(k);
add(general);
}
void Target_set::add(Target target)
{
(void)K::log(3, target);
check_for_previous_definition(target.m_name, target.m_loc);
m_targets[target.m_name] = target;
m_names.push_back(target.m_name);
m_descs.push_back(target.m_desc);
}
void Target_set::add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms)
{
(void)K::log(3, *begin, *(end - 1));
auto [positional, optional, rest] =
argument_split(begin + 1, end - 1, m_parameters.m_positional.size());
auto values = m_parameters.value_map(positional, optional, rest, begin->m_loc);
//std::cout << ktype << "Transformed: " << kreplaced << std::pair(begin, end) << "\n";
//std::cout << values << "\n";
check_for_previous_definition(values["name"], begin->m_loc);
Target target(values["name"], values["desc"], begin->m_loc);
target.add_transforms(values["transforms"]);
target.add_escapes(values["escape"]);
target.add_after_apply(values["after_apply"]);
// for (auto included_target : word_split(values["includes"])) {
// msg() << "Include: " << included_target << "\n";
// }
target.m_includes = word_split(values["includes"]);
// Inherit escapes from included targets
for (const auto& included : target.m_includes) {
if (m_targets.count(included)) {
for (const auto& esc : m_targets[included].m_escapes) {
target.m_escapes.push_back(esc);
}
}
}
m_targets[target.m_name] = target;
m_names.push_back(target.m_name);
for (auto [name, defined_target] : m_targets) {
// msg() << name << sp_arrow << defined_target << "\n";
if (is_in(name, target.m_includes)) {
defined_target.m_provides.push_back(target.m_name);
// msg() << " " << name << " provides " << target.m_name << "\n " << defined_target.m_provides << "\n";
m_targets[name] = defined_target;
}
}
// std::for_each(begin, end, [](Katom& k) { k.m_type = katom_t::replaced; });
modify_type(katom_t::replaced, begin, end);
auto next_iter = end;
ignore_whitespace(next_iter, katoms);
}
void Target_set::check_for_previous_definition(std::string name, Locator loc)
{
if (has(name)) {
Target current = m_targets[name];
throw Target_error("Target \"" + name + "\" is already defined:\n " + current.m_loc.desc(),
loc, false);
}
}
bool Target_set::has(std::string target_name)
{
return std::ranges::find(m_names, target_name) != m_names.end();
}
Target Target_set::get(std::string target_name, Locator loc)
{
if (has(target_name) || target_name == Target_set::general_name) {
return m_targets.at(target_name);
} else {
throw Target_error("Target " + target_name + " does not exist", loc);
}
}
void Target_set::transform(std::string target_name, katom_list& katoms)
{
(void)K::log(3);
m_targets[target_name].transform(katoms);
}
std::vector<std::string> Target_set::user_defined()
{
return collect_if(
m_names, [](auto name) {
return name != Target_set::declare_name && name != Target_set::general_name; });
}
std::vector<std::string> Target_set::applicable()
{
return collect_if(
m_names, [](auto name) {
return name != Target_set::declare_name; });
}
std::string Target_set::describe(int margin, bool long_format) const
{
std::string tab(margin, ' ');
std::stringstream ss {};
auto name_width = max_length(m_names);
auto desc_width = max_length(m_descs);
for (const std::string& name : m_names) {
const Target& t = m_targets.at(name);
if (long_format) {
ss << tab << std::setfill(' ') << std::setw(name_width) << std::right << name << " "
<< std::setw(desc_width) << std::left << t.m_desc << " "
<< t.m_loc.str() << "\n";
} else {
ss << tab << std::setfill(' ') << std::setw(name_width) << name << sp_arrow << t << "\n";
}
}
return ss.str();
}
/*
void Targets::describe()
{
std::string intro =
"A \"target\" specifies the output format of Klammertext processing. "
"Targets are identified by the typical filename extension of the format. "
"A klammer defines how it converts its arguments to the appropriate structure for one or more targets. "
"The special target \"k\" is used for a klammer definition that describes that klammer's "
"arguments and purpose in the various targets for which it is defined. "
"If the klammer definition does not specify a target, the klammer can be used for any target.";
std::cout << "Klammertext targets\n\n" << justify(intro) << "\n\n";
for (std::string name : names) {
if (name == Target::any_target_name)
continue;
targets[name]->describe();
std::cout << "\n";
}
}
*/

74
mac/target_set.h Normal file
View File

@@ -0,0 +1,74 @@
#pragma once
#include <map>
#include <string>
#include "target.h"
#include "argument_set.h"
#include "katom.h"
class Target_set
{
public:
static std::string declare_name;
static std::string general_name;
Target_set();
void add(Target target);
void add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
void check_for_previous_definition(std::string name, Locator loc);
/*
void add_transforms(std::string target_name, std::string transforms);
void add_transforms(
std::string target_name,
std::vector<std::pair<std::string, std::string>> transforms);
*/
bool has(std::string target_name);
Target get(std::string target_name, Locator loc);
void transform(std::string target_name, std::vector<Katom>& katoms);
std::vector<std::string> user_defined();
std::vector<std::string> applicable();
std::string describe(int margin=2, bool long_format=false) const;
//Argtype_set m_argtypes {};
std::map<std::string, Target> m_targets {};
std::vector<std::string> m_names {};
std::vector<std::string> m_descs {};
Parameter_set m_parameters {};
/*
std::string m_parameters_spec {"name | desc :after_apply :after_write :includes | transforms.rest"};
Parameter_set m_parameters =
katomize(line_split(m_parameters_spec), Locator().str());
*/
//Parameter_set m_parameters =
// Parameter_set(katomize({"name :desc | transforms.rest"}, "target_set"));
/*
// Targets(Statevar_set& statevars);
target_ptr check_target_existence(
std::string name, Locator loc, bool should_exist);
void add(std::string name, std::string desc, std::string include,
// Statevar_set& statevars,
std::string transform,
std::string before_apply, std::string after_apply, std::string after_write,
Locator loc);
target_ptr check_target_name(
std::string target, bool allow_all, Locator loc);
std::map<std::string, target_ptr> targets {};
std::vector<std::string> names {};
void describe_suffixes();
void describe();
*/
};

516
mac/util.cpp Normal file
View File

@@ -0,0 +1,516 @@
#include <stdlib.h>
#include <cstdio>
#include <algorithm>
#include <cctype>
#include <iterator>
#include <set>
#include <sstream>
#include <utility>
#include <regex>
#include "util.h"
#include "show.h"
std::string trim_left(const std::string& s)
{
std::string result = s;
result.erase(result.begin(), std::find_if(result.begin(), result.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
return result;
}
std::string trim_right(const std::string& s)
{
std::string result = s;
result.erase(std::find_if(result.rbegin(), result.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), result.end());
return result;
}
std::string trim(std::string s)
{
return trim_left(trim_right(std::move(s)));
}
std::string trim_char_left(std::string s, char remove)
{
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [&](char c) { return c != remove; }));
return s;
}
std::string trim_char_right(std::string s, char remove)
{
s.erase(std::find_if(s.rbegin(), s.rend(), [&](char c) { return c != remove; }).base(), s.end());
return s;
}
std::string trim_char(std::string s, char remove)
{
return trim_char_left(trim_char_right(std::move(s), remove), remove);
}
std::string escape_regex(const std::string& input)
{
std::string result;
result.reserve(input.length() * 2); // Reserve space for potential escapes
for (char c : input) {
// Escape special regex characters
if (std::string("\\^$.|?*+()[{}]").find(c) != std::string::npos) {
result += '\\';
}
result += c;
}
return result;
}
std::string string_replace(const std::string& source, const std::string& old_str, const std::string& new_str)
{
return std::regex_replace(source, std::regex(escape_regex(old_str)), new_str);
/*
std::string result { source };
auto pos = result.find(old_str);
std::string old_str_e = old_str; // escape_regex(old_str);
while (pos != std::string::npos) {
// result = result.replace(pos, old_str.size(), new_str);
result = result.replace(pos, old_str_e.size(), new_str);
// pos = result.find(old_str);
pos = result.find(old_str_e);
// std::cout << " " << result << "\n";
}
return result;
*/
}
bool contains(const std::string& str, const std::string& substr)
{
return str.find(substr) != std::string::npos;
}
bool contains(const std::vector<std::string>& strings, const std::string& element)
{
return std::find(strings.begin(), strings.end(), element) != strings.end();
}
std::string regex_escape(const std::string& s)
{
/*
regex special { R"([\$.|?*+(){})" }; // ^ is reserved
return regex_replace(s, special, "\\[&$]");
*/
std::set chars { '\\', '|', '(', ')', '{', '}', '[', ']', '$', '^' };
std::string result {};
for (char c : s) {
if (chars.find(c) != chars.end())
result += "\\";
result += c;
}
return result;
}
strings_t regex_split(std::string s, std::regex re, bool trim_parts)
{
strings_t result = {};
if (s.size() == 0) {
return result;
}
auto it = std::sregex_token_iterator(s.begin(), s.end(), re, -1);
while (it != std::sregex_token_iterator()) {
std::string part { *it };
if (trim_parts)
part = trim(part);
result.push_back(part);
it++;
}
return result;
}
strings_t word_split(const std::string& s)
{
return regex_split(s, std::regex("\\s+"));
}
bool is_in(std::string s, strings_t v)
{
return find(v.begin(), v.end(), s) != v.end();
}
bool is_not_in(std::string s, strings_t v)
{
return find(v.begin(), v.end(), s) == v.end();
}
strings_t find_all(std::string str, std::regex pattern, int match_group)
{
std::sregex_iterator end {};
strings_t result;
for (std::sregex_iterator p {str.begin(), str.end(), pattern}; p!= end; ++p)
result.push_back((*p)[match_group]);
return result;
}
strings_t find_all(std::string str, std::string pattern, int match_group)
{
return find_all(str, std::regex(pattern), match_group);
}
strings_t split_into_paragraphs(const std::string& s)
{
std::string t {trim(s)};
std::string marker { "_PAR_" };
t = trim(std::regex_replace(t, std::regex(R"(\n *(\n *)+)"), marker)) + marker;
//return find_all(t, regex(R"(((\s|.)*?)" + marker + ")"), 1);
return find_all(t, std::regex(R"(((\s|.)*?)_PAR_)"), 1);
}
std::string add_margin(std::string s, unsigned int margin_size)
{
auto margin = std::string(margin_size, ' ');
return trim_right(
margin + std::regex_replace(s, std::regex(R"(\n)"), '\n' + margin));
}
std::string justify_string(const std::string& s, unsigned int width=80, bool french_spacing=false)
{
strings_t words = find_all(s, R"([^\s]+)");
std::stringstream ss {};
std::stringstream line {};
for (std::string w : words) {
if (line.str().size() + w.size() + 1 > width) {
ss << line.str() << '\n';
line.str("");
}
if (not french_spacing and w[w.size()-1] == '.')
w += " ";
line << w << " ";
}
if (!line.str().empty())
ss << line.str();
std::string result = trim(ss.str());
result = std::regex_replace(result, std::regex("~"), " ");
return result;
}
std::string justify(
const std::string& input_text, unsigned int text_width, unsigned int margin_width)
{
std::string result {};
text_width -= margin_width;
for (const std::string& par : split_into_paragraphs(trim(input_text))) {
result += justify_string(par, text_width) + "\n\n";
}
if (margin_width > 0) {
result = add_margin(result, margin_width);
}
return result;
}
std::string join(const strings_t& ss, const std::string& separator)
{
if (ss.empty()) {
return std::string();
} else if (ss.size() == 1) {
return ss[0];
} else {
std::stringstream strm {};
std::copy(ss.begin(), ss.end() - 1,
std::ostream_iterator<std::string>(strm, separator.c_str()));
strm << ss.back();
return strm.str();
}
}
std::string join(int argc, char* argv[], const std::string& separator)
{
std::string result {};
for (int i = 0; i < argc; ++i) {
result += std::string(argv[i]) + separator;
}
return result;
}
std::string argv_to_string(int argc, char* argv[])
{
if (argc == 0) {
return "";
}
std::string result = argv[0];
for (int i = 1; i < argc; ++i) {
result += " " + std::string(argv[i]);
}
return result;
}
std::string plural(const std::string& word, int count)
{
std::string result {word};
if (count != 1) {
if (*(word.end()-1) == 'y')
result = word.substr(0, word.size()-2) + "ies";
else
result = word + "s";
}
return result;
}
std::string plural(const std::string& word, const strings_t& things)
{
std::string result { word };
if (things.size() != 1) {
if (*(word.end()-1) == 'y')
result = word.substr(0, word.size()-2) + "ies";
else
result = word + "s";
}
return result;
}
std::string to_be(int count, bool present)
{
std::string result {};
if (count > 1) {
if (present) {
result = "are";
} else {
result = "were";
}
} else {
if (present) {
result = "is";
} else {
result = "was";
}
}
return result;
}
int max_length(strings_t ss)
{
size_t result = 0;
for_each(ss.begin(), ss.end(),
[&result](const std::string& s) { result = std::max(result, s.size()); });
return result;
}
/*
std::vector<std::string> map_key_lengths(std::map<std::string, auto> map)
{
int result = 0;
for (auto const& item: map) {
result = std::max(result, item.first.size());
}
}
int
std::map<int, int> m;
std::vector<int> key, value;
for(std::map<int,int>::iterator it = m.begin(); it != m.end(); ++it) {
key.push_back(it->first);
value.push_back(it->second);
std::cout << "Key: " << it->first << std::endl;
std::cout << "Value: " << it->second << std::endl;
}
*/
std::vector<std::pair<std::string, std::string>> environment_variables(bool allow_empty_definitions)
{
// std::cout << "read_environment:\n";
std::vector<std::pair<std::string, std::string>> result {};
extern char **environ;
for (int i = 0; environ[i]; i++) {
auto parts = regex_split(environ[i], std::regex("="), true);
if (!allow_empty_definitions && parts.size() < 2) {
throw Internal_error(
"Incorrect environment variable format:\n" + std::string(environ[i]),
Locator(), false);
}
std::string name = parts[0];
parts.erase(parts.begin());
std::string value = join(parts, "=");
// std::cout << name << sp_arrow << value << "\n";
result.push_back({name, value});
}
return result;
}
std::string replace_environment_variables(std::string str)
{
if (str.find('{') == std::string::npos || str.find('}') == std::string::npos) {
return str;
}
if (str.find('{') == std::string::npos || str.find('\n') != std::string::npos) {
return str;
}
if (str.size() < 3) {
return str;
}
std::regex variable_re(R"((.*?)\{([A-Z_]+)\})");
std::sregex_iterator end {};
std::string result {};
//sregex_iterator q {};
size_t endpos = 0;
for (std::sregex_iterator p {str.begin(), str.end(), variable_re}; p!= end; ++p) {
std::smatch m = *p;
std::string prefix = m[1];
std::string var = m[2];
endpos = m.position() + m.length();
std::string envvar = get_env_var(var);
result += prefix + envvar;
}
if (endpos < str.size() - 1) {
result += str.substr(endpos);
}
return result;
}
std::string abbrev(const std::string& s, unsigned int max_length, bool remove_newlines)
{
std::string result {s};
if (s.size() > max_length) {
if (remove_newlines) {
result = trim(result);
result = std::regex_replace(result, std::regex("\n"), broken_bar);
}
int suffix_size = 8;
std::string ellipsis { red + "[...]" + black };
int prefix_end = max_length - suffix_size - ellipsis.size();
result = result.replace(result.begin() + prefix_end,
result.end() - suffix_size,
ellipsis);
}
return result;
}
void remove_element(std::vector<std::string>& ss, std::string removed)
{
ss.erase(std::remove_if(ss.begin(), ss.end(),
[&removed](std::string s) { return s == removed; }),
ss.end());
}
void remove_duplicates(strings_t& ss)
{
// https://en.cppreference.com/w/cpp/algorithm/unique
std::sort(ss.begin(), ss.end());
auto last = std::unique(ss.begin(), ss.end());
ss.erase(last, ss.end());
}
std::string display_string(const std::string& s, unsigned int width, bool replace_newlines)
{
std::string suffix { "..." };
std::string result { s };
if (replace_newlines)
result = std::regex_replace(result, std::regex("\n"), "/");
auto rlen = result.length();
auto slen = suffix.length();
if ((rlen > slen) and (rlen - slen) > width) {
result = result.replace(result.begin()+width, result.end(), suffix); //substr(0, width) + suffix;
}
//result = '"' + result + '"';
return result;
}
std::pair<std::string,std::string> extract_parameter_type(std::string parameter_name)
{
std::regex name_pat { R"((\w+)\.(\w+))" };
std::smatch match {};
std::string type_name = "string";
std::string name = parameter_name;
if (std::regex_match(parameter_name, match, name_pat)) {
name = match[1];
type_name = match[2];
}
return { name, type_name };
}
std::tuple<std::string, std::string, bool> regex_split_prefix(const std::regex& pattern, const std::string& text)
{
std::smatch match;
if (std::regex_search(text, match, pattern) && match.position() == 0) {
// Match found at the beginning of the string
std::string matched = match.str();
std::string remainder = text.substr(matched.length());
return { matched, remainder, true };
} else {
// No match at the beginning
return { "", text, false };
}
}
std::vector<std::string> dlist_split(const std::string& s)
{
// If a [^\w] character surrounded by spaces exists in s, it is the delimiter.
// If not, the first space-delimited word is the delimiter.
if (s.find(" ") == std::string::npos) { // Only one element.
//std::vector<std::string> result {s};
//return result;
return {s};
} else {
std::smatch match{};
std::string elements_str{s};
std::string delimiter {};
if (std::regex_search(s, match, std::regex(R"(\s+([^\w])\s+)"))) {
delimiter = match[1];
} else {
std::string::const_iterator iter =
std::find_if(s.cbegin(), s.cend(), [](char c) { return c == ' '; });
delimiter = std::string(s.cbegin(), iter);
elements_str = std::string(iter, s.cend());
}
std::vector<std::string> elements {
regex_split(elements_str, std::regex(delimiter), true) };
return elements;
}
}
std::string get_env_var(const std::string& var) {
std::lock_guard<std::mutex> lock(env_mutex);
const char* val = getenv(var.c_str());
return val ? std::string(val) : "";
}
std::string freplace(const std::string src, std::regex pattern, std::function<std::string(std::smatch)> func)
{
std::string result {};
std::smatch match;
bool found = std::regex_search(src.begin(), src.end(), match, pattern);
auto pos = src.begin();
// int n = 0;
while (found) {
std::string part(pos, pos + match.position());
result += part;
result += func(match);
pos += match.position(0) + match.length(0);
found = std::regex_search(pos, src.end(), match, pattern);
// n++;
}
std::string part(pos, pos + match.position());
result += part;
// std::cout << "Found " << n << " matches\n";
return result;
}
std::string exec(const char* cmd)
{
std::array<char, 128> buffer;
std::string result;
FILE* pipe = popen(cmd, "r");
if (!pipe) throw std::runtime_error("popen() failed");
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
result += buffer.data();
}
pclose(pipe);
return result;
}

107
mac/util.h Normal file
View File

@@ -0,0 +1,107 @@
#pragma once
#include <vector>
#include <string>
#include <regex>
#include <mutex>
#include <map>
#include <memory>
#include <functional>
#include <tuple>
#include <utility>
#include <algorithm>
inline std::mutex env_mutex;
class Katom;
using strings_t = std::vector<std::string>;
using string_pairs = std::vector<std::pair<std::string, std::string>>;
using string_map = std::map<std::string, std::string>;
using katom_ptr = std::shared_ptr<Katom>;
using katom_list = std::vector<Katom>;
using katom_lists = std::vector<katom_list>;
using katom_list_map = std::map<std::string, katom_list>;
using katom_iter = katom_list::iterator;
using spans_t = std::vector<std::pair<Katom, Katom>>;
using argument_value_map = std::map<std::string, std::string>;
std::string trim_left(const std::string& s);
std::string trim_right(const std::string& s);
std::string trim(std::string s);
std::string trim_char_left(std::string s, char remove);
std::string trim_char_right(std::string s, char remove);
std::string trim_char(std::string s, char remove);
std::string string_replace(const std::string& source, const std::string& old_str, const std::string& new_str);
std::string regex_escape(const std::string& s);
bool contains(const std::string& str, const std::string& substr);
bool contains(const std::vector<std::string>& strings, const std::string& element);
std::vector<std::string> regex_split(std::string s, std::regex re, bool trim_parts=true);
std::vector<std::string> word_split(const std::string& s);
bool is_in(std::string s, std::vector<std::string> v);
bool is_not_in(std::string s, std::vector<std::string> v);
std::vector<std::string> find_all(std::string str, std::regex pattern, int match_group=0);
std::vector<std::string> find_all(std::string str, std::string pattern, int match_group=0);
std::string add_margin(std::string s, unsigned int margin_size);
std::string justify(const std::string& input_text, unsigned int text_width=80, unsigned int margin_width=0);
std::string join(const std::vector<std::string>& ss, const std::string& separator = " ");
std::string join(int argc, char* array[], const std::string& separator = " ");
std::string argv_to_string(int argc, char* argv[]);
std::string plural(const std::string& word, int count);
std::string plural(const std::string& word, const std::vector<std::string>& things);
std::string to_be(int count, bool present = true);
int max_length(std::vector<std::string> ss);
std::vector<std::pair<std::string, std::string>> environment_variables(bool allow_empty_definitions=true);
std::string replace_environment_variables(std::string str);
std::string abbrev(const std::string& s, unsigned int max_length=65, bool remove_newlines=true);
void remove_element(std::vector<std::string>& ss, std::string removed);
void remove_duplicates(std::vector<std::string>& ss);
std::string display_string(const std::string& s, unsigned int width=40, bool replace_newlines=true);
std::pair<std::string,std::string> extract_parameter_type(std::string parameter_name);
std::tuple<std::string, std::string, bool> regex_split_prefix(const std::regex& pattern, const std::string& text);
std::vector<std::string> dlist_split(const std::string& s);
std::string get_env_var(const std::string& var);
std::string freplace(const std::string src, std::regex pattern, std::function<std::string(std::smatch)> func);
std::string exec(const char* cmd);
inline std::string q_(std::string s)
{
return "\"" + s + "\"";
}
inline std::string qq_(std::string s)
{
if (s.find('\n') == std::string::npos) {
return "\"" + s + "\"";
} else {
return "\"\"\"" + s + "\"\"\"";
}
}
template <typename T, typename Pred>
std::vector<T> collect_if(const std::vector<T>& xs, Pred pred) {
std::vector<T> out;
out.reserve(xs.size());
for (const auto& v : xs) {
if (pred(v)) out.push_back(v);
}
out.shrink_to_fit();
return out;
}
template <typename T>
bool all_equal(const std::vector<T>& v) {
if (v.size() < 2) return true;
return std::adjacent_find(v.begin(), v.end(), std::not_equal_to<T>{}) == v.end();
}
template <typename T>
int max_key_length(std::map<std::string, T> map)
{
size_t result = 0;
for_each(map.begin(), map.end(),
[&result](const auto& item) { result = std::max(result, item.first.size()); });
return result;
}

53
sks/Makefile Normal file
View File

@@ -0,0 +1,53 @@
# Klammertext sks/ Makefile
# Builds all .so files in sks/ subdirectories
K := $(KLAMMERTEXT_HOME)
KS := $(K)/sks
KM := $(K)/mac
# Shared library that all .so files depend on
LIBRARY := $(K)/lib/libklammertext.so
# Support directories (build .o files used by other sks/ components)
SUPPORT_DIRS := kutil target
# Excluded directories:
# kutil, target - support directories that build .o files, not .so
# book, phase - obsolete, not updated for libklammertext.so
# Directories that build .so files (excluding support and obsolete directories)
# Find all subdirectories with Makefiles that have .so build targets
# (look for lines like "xyz.so :" or "all : xyz.so")
SO_DIRS := $(shell for dir in $(KS)/*/; do \
if [ -f "$${dir}Makefile" ] && grep -qE '^[a-z]+\.so\s*:|all\s*:.*\.so' "$${dir}Makefile" 2>/dev/null; then \
basename "$$dir"; \
fi; done | grep -v -E '^(kutil|target|book|phase)$$')
.PHONY: all clean support $(SUPPORT_DIRS) $(SO_DIRS)
# Default target: build support first, then all .so files
all : support $(SO_DIRS)
# Build support directories
support : $(SUPPORT_DIRS)
kutil :
$(MAKE) -j -C $(KS)/kutil
target : kutil
$(MAKE) -j -C $(KS)/target
# Pattern rule for .so directories
# Each depends on the library and support directories
$(SO_DIRS) : support $(LIBRARY)
$(MAKE) -j -C $(KS)/$@
# Clean all subdirectories
clean :
@for dir in $(SUPPORT_DIRS) $(SO_DIRS); do \
echo "Cleaning $$dir..."; \
$(MAKE) -C $(KS)/$$dir clean; \
done
# Rebuild everything
redo : clean all

113
sks/block/block.k Normal file
View File

@@ -0,0 +1,113 @@
@@sp.k : Non-breaking space character @@
@@sp.html :: &^#160; @@
@@sp.tex :: ~ @@
@@footnote.k s : Footnote (TBD) @@
@@footnote :: [*s*] @@
@@indent.k s :w.int 3 :linebreak.bool false : Indented block @@
@@indent :: @eval block.Indent(K) eval@ @@
@@quote.k s :w.int 1 :source : Quotation block @@
@@quote.html ::
<div class="quote">
*s*
</div>
@@
@@quote.tex ::
ANDY: QUOTE: *s*
#[
\hspace*{@{justify.length_mul("|margin|", 1, 'latex')}@}
\begin{minipage}{\textwidth- @{justify.length_mul("|margin|", 2, 'latex')}@ }
\raggedright
|text|
@? """|source|""" |?
\vspace*{6pt}
{\begin{spacing}{1.1}\footnotesize\raggedleft |source| \end{spacing}}
?@
\end{minipage}
]#
@@
@@quote.txt ::
@eval block.block_indent(K) eval@
@@
@@note.k s :label Note :color 1.0,1.0,0.9 :bordercolor 0.2,0.2,0.2 :level.int 0 :width
: Rectangular block for a special note @@
@@note :: @eval block.Note(K) eval@ @@
@@center.k s : Center text @@
@@center.tex ::
\begin{center}
*s*
\end{center}
@@
@@center.html ::
<div class="center">
*s*
</div>
@@
@@right.k s : Right-justified text @@
@@right.html ::
<b>TBD</b> *s*
@@
@@right.tex ::
\begin{flushright}
*s*
\end{flushright}
@@
@@nl.k : Newline character @@
@@nl.html :: <br> @@
@@nl.tex :: \newline @@
@@nl.txt :: \n @@
@@tnl.k : Table newline (deprecated; check) @@
@@tnl.html :: <br> @@
@@tnl.tex :: \\\\ @@
@@tnl.txt :: \n @@
@@newpage.k : Start new page @@
@@newpage.html :: @@
@@newpage.tex :: \newpage @@
@@newpage.txt :: @@
@@extendpage.k linecount : Extenad current page @@
@@extendpage.html :: @@
@@extendpage.tex :: \enlargethispage{*linecount*\baselineskip} @@
@@extendpage.txt :: @@
@@vspace.k length : Vertical space @@
@@vspace.tex :: \vspace*{*length*} @@
@@qa.k question | answer : Question and answer formatting @@
@@qa ::
@b Q: @ *question*
@b A: @ *answer*
@@
@@@argtype coords | x and y coordinates :pattern 'float'\s+'float' @@@
@@block.k : to.coords | content :width.float .5 :point.coords 0.0 0.0
: Absolute positioning of text block @@
@@block :: @eval block.Block(K) eval@ @@
@@lines.k s : Maintain line breaks @@
@@lines :: @eval block.Lines(K) eval@ @@
@@twocolumns.tex s :
\begin{multicols}{2}
*s*
\end{multicols}
@@

114
sks/block/block.py Normal file
View File

@@ -0,0 +1,114 @@
import re
import textwrap
import pprint
import klammer_base
import kutil
import latex_util
class Indent(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
self.s = kutil.escape(self.s)
def html(self):
return "FIX: INDENT " + self.s
def tex(self):
tab = f"\\hspace*{{{self.w}ex}}"
result = ""
if self.linebreak:
for e in self.s.split("\n"):
result += f"{tab}{e}\\\\\n"
result = result[:-3]
result = re.sub(r"\t", r"\\t", result)
else:
result = tab + latex_util.minipage(
"\\raggedright " + self.s, f"\\textwidth - {self.w}ex", center=False, vmargin="4pt")
#print(result)
return result
def txt(self):
indent = " " * self.w
result = self.s
result = re.sub("KK0022", '"', result)
result = indent + f"\n{indent}".join(textwrap.wrap(result, width=70, break_on_hyphens=False))
return f"@lit\n{result}\nlit@"
class Note(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
def html(self):
color = ",".join([f"{float(e)*100}%" for e in self.color.split(",")])
result = f'''<div class="box" style="background-color: rgb({color}); border-color: rgb({self.bordercolor}) ;">
<b>{self.label}:</b> {self.s}
</div>'''
return result
def tex(self):
if self.width:
width = r'{}\\textwidth'.format(self.width)
else:
#width = r'\\textwidth - 16pt - {}\\leftmargin'.format(self.level)
width = r'\\linewidth - \\leftmargin + 2pt'
result = '''
\\par\\begingroup
COLOR\\setlength{\\fboxsep}{8pt}
\\fcolorbox{bordercolor}{localcolor}{
\\parbox{WIDTH}{\\raggedright\\setlength{\\parskip}{8pt}
\\textbf{LABEL:} TEXT
}}\\endgroup\\par
'''
result = re.sub('LABEL', self.label, result)
result = re.sub('TEXT', re.sub(r'\\', r'\\\\', self.s), result)
result = re.sub('COLOR', r'\\definecolor{{localcolor}}{{rgb}}{{{}}}\nCOLOR'.format(self.color), result)
result = re.sub('COLOR', r'\\definecolor{{bordercolor}}{{rgb}}{{{}}}\n'.format(self.bordercolor), result)
result = re.sub('WIDTH', width, result)
print(result)
return result
class Block(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
#self.show()
def html(self):
return ""
def tex(self):
to_x, to_y = [float(e) for e in self.to.split()]
pt_x, pt_y = [float(e) for e in self.point.split()]
result = f"""
\\begin{{textblock}}{{{self.width}}}[{pt_x},{pt_y}]({to_x},{to_y})
\\vspace*{{-1\\parskip}}
{self.content.strip()}
\\end{{textblock}}
"""
return result
class Lines(klammer_base.Klammer_base):
def __init__(self, K):
super().__init__(K)
self.s = kutil.escape(self.s)
self.lines = self.s.split("\n")
def html(self):
result = ""
for line in self.lines:
result += line + "<br>\n"
return result
def tex(self):
return "\\\\\n".join(self.lines) + "\n"
result = ""
for line in self.lines:
if line:
line += r" \\"
result += line + "\n"
result = re.sub(r"\\\\\n\n", "\n\n", result)
return result

26
sks/block/css/block.css Normal file
View File

@@ -0,0 +1,26 @@
p {
margin: .5rem 0 .5rem 0;
}
.quote {
margin-left: 2em;
}
.box {
border: solid black 1px;
padding: 0.5em 1em;
clear: both;
margin: 1.0em 0;
overflow: auto;
}
.centered {
margin-left: auto;
margin-right: auto;
width: fit-content;
}
.indent {
margin-left: 2rem;
}

1
sks/block/css/list.txt Normal file
View File

@@ -0,0 +1 @@
block.css

Some files were not shown because too many files have changed in this diff Show More